Merge "Hygiene: Use [i] instead of charAt(i) for string index access"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 23 Jun 2015 10:55:51 +0000 (10:55 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 23 Jun 2015 10:55:51 +0000 (10:55 +0000)
19 files changed:
.gitattributes
docs/linkcache.txt
docs/sitelist.txt
includes/GlobalFunctions.php
includes/MediaWiki.php
includes/content/ContentHandler.php
includes/htmlform/HTMLFormFieldWithButton.php
includes/htmlform/HTMLSelectNamespaceWithButton.php
includes/htmlform/HTMLTextFieldWithButton.php
includes/jobqueue/jobs/RefreshLinksJob.php
includes/objectcache/ObjectCacheSessionHandler.php
includes/page/Article.php
includes/page/WikiPage.php
includes/profiler/Profiler.php
includes/profiler/SectionProfiler.php
maintenance/copyFileBackend.php
tests/phpunit/includes/GlobalFunctions/GlobalTest.php
tests/phpunit/includes/MediaWikiTest.php [new file with mode: 0644]
tests/qunit/suites/resources/jquery/jquery.mwExtension.test.js

index 50ca329..69d7b1b 100644 (file)
@@ -1 +1,2 @@
 *.sh eol=lf
+*.icc binary
index 266f200..13b6961 100644 (file)
@@ -21,4 +21,4 @@ foreach( $pages as $page ){
 }
 
 $batch = new LinkBatch( $titles );
-$batch->execute();
\ No newline at end of file
+$batch->execute();
index 48c7ce5..24e1b9a 100644 (file)
@@ -44,4 +44,4 @@ The XML elements are used as follows:
 ** link: Generic URL template, often the document root.
 ** page_path: (for mediawiki sites) URL template for wiki pages (corresponds to the target wiki's $wgArticlePath setting)
 ** file_path: (for mediawiki sites) URL pattern for application entry points and resources (corresponds to the target wiki's $wgScriptPath setting).
-* forward: Whether using a prefix defined by a localid tag in the URL will cause the request to be redirected to the corresponding page on the target wiki (currently unused). E.g. whether http://wiki.acme.com/wiki/foo:Buzz should be forwarded to http://wiki.foo.com/read/Buzz. (CAVEAT: not yet implement, can be specified but has no effect)
\ No newline at end of file
+* forward: Whether using a prefix defined by a localid tag in the URL will cause the request to be redirected to the corresponding page on the target wiki (currently unused). E.g. whether http://wiki.acme.com/wiki/foo:Buzz should be forwarded to http://wiki.foo.com/read/Buzz. (CAVEAT: not yet implement, can be specified but has no effect)
index e2e0666..c618426 100644 (file)
@@ -3495,7 +3495,7 @@ function wfGetPrecompiledData( $name ) {
 }
 
 /**
- * Get a cache key
+ * Make a cache key for the local wiki.
  *
  * @param string $args,...
  * @return string
@@ -3510,7 +3510,9 @@ function wfMemcKey( /*...*/ ) {
 }
 
 /**
- * Get a cache key for a foreign DB
+ * Make a cache key for a foreign DB.
+ *
+ * Must match what wfMemcKey() would produce in context of the foreign wiki.
  *
  * @param string $db
  * @param string $prefix
@@ -3520,6 +3522,7 @@ function wfMemcKey( /*...*/ ) {
 function wfForeignMemcKey( $db, $prefix /*...*/ ) {
        $args = array_slice( func_get_args(), 2 );
        if ( $prefix ) {
+               // Match wfWikiID() logic
                $key = "$db-$prefix:" . implode( ':', $args );
        } else {
                $key = $db . ':' . implode( ':', $args );
@@ -3527,6 +3530,24 @@ function wfForeignMemcKey( $db, $prefix /*...*/ ) {
        return str_replace( ' ', '_', $key );
 }
 
+/**
+ * Make a cache key with database-agnostic prefix.
+ *
+ * Doesn't have a wiki-specific namespace. Uses a generic 'global' prefix
+ * instead. Must have a prefix as otherwise keys that use a database name
+ * in the first segment will clash with wfMemcKey/wfForeignMemcKey.
+ *
+ * @since 1.26
+ * @param string $args,...
+ * @return string
+ */
+function wfGlobalCacheKey( /*...*/ ) {
+       $args = func_get_args();
+       $key = 'global:' . implode( ':', $args );
+       $key = str_replace( ' ', '_', $key );
+       return $key;
+}
+
 /**
  * Get an ASCII string identifying this wiki
  * This is used as a prefix in memcached keys
index 7a0d7b7..932dea2 100644 (file)
@@ -239,63 +239,97 @@ class MediaWiki {
                                }
                                throw new BadTitleError();
                        }
-               // Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant
-               } elseif ( $request->getVal( 'action', 'view' ) == 'view' && !$request->wasPosted()
-                       && ( $request->getVal( 'title' ) === null
-                               || $title->getPrefixedDBkey() != $request->getVal( 'title' ) )
-                       && !count( $request->getValueNames( array( 'action', 'title' ) ) )
-                       && Hooks::run( 'TestCanonicalRedirect', array( $request, $title, $output ) )
-               ) {
-                       if ( $title->isSpecialPage() ) {
-                               list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
-                               if ( $name ) {
-                                       $title = SpecialPage::getTitleFor( $name, $subpage );
-                               }
-                       }
-                       $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
-                       // Redirect to canonical url, make it a 301 to allow caching
-                       if ( $targetUrl == $request->getFullRequestURL() ) {
-                               $message = "Redirect loop detected!\n\n" .
-                                       "This means the wiki got confused about what page was " .
-                                       "requested; this sometimes happens when moving a wiki " .
-                                       "to a new server or changing the server configuration.\n\n";
-
-                               if ( $this->config->get( 'UsePathInfo' ) ) {
-                                       $message .= "The wiki is trying to interpret the page " .
-                                               "title from the URL path portion (PATH_INFO), which " .
-                                               "sometimes fails depending on the web server. Try " .
-                                               "setting \"\$wgUsePathInfo = false;\" in your " .
-                                               "LocalSettings.php, or check that \$wgArticlePath " .
-                                               "is correct.";
+               // Handle any other redirects.
+               // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant
+               } elseif ( !$this->tryNormaliseRedirect( $title ) ) {
+
+                       // Special pages
+                       if ( NS_SPECIAL == $title->getNamespace() ) {
+                               // Actions that need to be made when we have a special pages
+                               SpecialPageFactory::executePath( $title, $this->context );
+                       } else {
+                               // ...otherwise treat it as an article view. The article
+                               // may still be a wikipage redirect to another article or URL.
+                               $article = $this->initializeArticle();
+                               if ( is_object( $article ) ) {
+                                       $this->performAction( $article, $requestTitle );
+                               } elseif ( is_string( $article ) ) {
+                                       $output->redirect( $article );
                                } else {
-                                       $message .= "Your web server was detected as possibly not " .
-                                               "supporting URL path components (PATH_INFO) correctly; " .
-                                               "check your LocalSettings.php for a customized " .
-                                               "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
-                                               "to true.";
+                                       throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
+                                               . " returned neither an object nor a URL" );
                                }
-                               throw new HttpError( 500, $message );
-                       } else {
-                               $output->setSquidMaxage( 1200 );
-                               $output->redirect( $targetUrl, '301' );
                        }
-               // Special pages
-               } elseif ( NS_SPECIAL == $title->getNamespace() ) {
-                       // Actions that need to be made when we have a special pages
-                       SpecialPageFactory::executePath( $title, $this->context );
-               } else {
-                       // ...otherwise treat it as an article view. The article
-                       // may be a redirect to another article or URL.
-                       $article = $this->initializeArticle();
-                       if ( is_object( $article ) ) {
-                               $this->performAction( $article, $requestTitle );
-                       } elseif ( is_string( $article ) ) {
-                               $output->redirect( $article );
+               }
+       }
+
+       /**
+        * Handle redirects for uncanonical title requests.
+        *
+        * Handles:
+        * - Redirect loops.
+        * - No title in URL.
+        * - $wgUsePathInfo URLs.
+        * - URLs with a variant.
+        * - Other non-standard URLs (as long as they have no extra query parameters).
+        *
+        * Behaviour:
+        * - Normalise title values:
+        *   /wiki/Foo%20Bar -> /wiki/Foo_Bar
+        * - Normalise empty title:
+        *   /wiki/ -> /wiki/Main
+        *   /w/index.php?title= -> /wiki/Main
+        * - Don't redirect anything with query parameters other than 'title' or 'action=view'.
+        *
+        * @return bool True if a redirect was set.
+        */
+       private function tryNormaliseRedirect( $title ) {
+               $request = $this->context->getRequest();
+               $output = $this->context->getOutput();
+
+               if ( $request->getVal( 'action', 'view' ) != 'view'
+                       || $request->wasPosted()
+                       || ( $request->getVal( 'title' ) !== null
+                               && $title->getPrefixedDBkey() == $request->getVal( 'title' ) )
+                       || count( $request->getValueNames( array( 'action', 'title' ) ) )
+                       || !Hooks::run( 'TestCanonicalRedirect', array( $request, $title, $output ) )
+               ) {
+                       return false;
+               }
+
+               if ( $title->isSpecialPage() ) {
+                       list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
+                       if ( $name ) {
+                               $title = SpecialPage::getTitleFor( $name, $subpage );
+                       }
+               }
+               // Redirect to canonical url, make it a 301 to allow caching
+               $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
+               if ( $targetUrl == $request->getFullRequestURL() ) {
+                       $message = "Redirect loop detected!\n\n" .
+                               "This means the wiki got confused about what page was " .
+                               "requested; this sometimes happens when moving a wiki " .
+                               "to a new server or changing the server configuration.\n\n";
+
+                       if ( $this->config->get( 'UsePathInfo' ) ) {
+                               $message .= "The wiki is trying to interpret the page " .
+                                       "title from the URL path portion (PATH_INFO), which " .
+                                       "sometimes fails depending on the web server. Try " .
+                                       "setting \"\$wgUsePathInfo = false;\" in your " .
+                                       "LocalSettings.php, or check that \$wgArticlePath " .
+                                       "is correct.";
                        } else {
-                               throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()"
-                                       . " returned neither an object nor a URL" );
+                               $message .= "Your web server was detected as possibly not " .
+                                       "supporting URL path components (PATH_INFO) correctly; " .
+                                       "check your LocalSettings.php for a customized " .
+                                       "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " .
+                                       "to true.";
                        }
+                       throw new HttpError( 500, $message );
                }
+               $output->setSquidMaxage( 1200 );
+               $output->redirect( $targetUrl, '301' );
+               return true;
        }
 
        /**
index b25bf14..468c7e9 100644 (file)
@@ -1023,7 +1023,7 @@ abstract class ContentHandler {
 
        /**
         * Returns true for content models that support caching using the
-        * ParserCache mechanism. See WikiPage::isParserCacheUsed().
+        * ParserCache mechanism. See WikiPage::shouldCheckParserCache().
         *
         * @since 1.21
         *
index 3c8910f..113bb4b 100644 (file)
@@ -54,4 +54,4 @@ class HTMLFormFieldWithButton extends HTMLFormField {
        public function getElement( $element ) {
                return $element . '&#160;' . $this->getInputHTML( '' );
        }
-}
\ No newline at end of file
+}
index 5ab57a3..24b15bd 100644 (file)
@@ -14,4 +14,4 @@ class HTMLSelectNamespaceWithButton extends HTMLSelectNamespace {
        public function getInputHTML( $value ) {
                return $this->mClassWithButton->getElement( parent::getInputHTML( $value ) );
        }
-}
\ No newline at end of file
+}
index 00c0517..c6dac32 100644 (file)
@@ -14,4 +14,4 @@ class HTMLTextFieldWithButton extends HTMLTextField {
        public function getInputHTML( $value ) {
                return $this->mClassWithButton->getElement( parent::getInputHTML( $value ) );
        }
-}
\ No newline at end of file
+}
index f706455..dec944a 100644 (file)
@@ -180,12 +180,12 @@ class RefreshLinksJob extends Job {
                        // Revision ID must be passed to the parser output to get revision variables correct
                        $parserOutput = $content->getParserOutput(
                                $title, $revision->getId(), $parserOptions, false );
-                       $ellapsed = microtime( true ) - $start;
+                       $elapsed = microtime( true ) - $start;
                        // If it took a long time to render, then save this back to the cache to avoid
                        // wasted CPU by other apaches or job runners. We don't want to always save to
                        // cache as this can cause high cache I/O and LRU churn when a template changes.
-                       if ( $ellapsed >= self::PARSE_THRESHOLD_SEC
-                               && $page->isParserCacheUsed( $parserOptions, $revision->getId() )
+                       if ( $elapsed >= self::PARSE_THRESHOLD_SEC
+                               && $page->shouldCheckParserCache( $parserOptions, $revision->getId() )
                                && $parserOutput->isCacheable()
                        ) {
                                $ctime = wfTimestamp( TS_MW, (int)$start ); // cache time
index 789f1e3..ae3850f 100644 (file)
@@ -31,6 +31,8 @@ class ObjectCacheSessionHandler {
        /** @var array Map of (session ID => SHA-1 of the data) */
        protected static $hashCache = array();
 
+       const TTL_REFRESH_WINDOW = 600; // refresh if expiring in 10 minutes
+
        /**
         * Install a session handler for the current web request
         */
@@ -157,10 +159,24 @@ class ObjectCacheSessionHandler {
        }
 
        /**
-        * Shutdown function. See the comment inside ObjectCacheSessionHandler::install
-        * for rationale.
+        * Shutdown function.
+        * See the comment inside ObjectCacheSessionHandler::install for rationale.
         */
        static function handleShutdown() {
+               global $wgObjectCacheSessionExpiry;
+
+               $now = microtime( true );
+               // Session are only written in object stores when $_SESSION changes,
+               // which also renews the TTL ($wgObjectCacheSessionExpiry). If a user
+               // is active but not causing session data changes, it may suddenly
+               // as they view a form, blocking the first submission.
+               // Make a dummy change every so often to avoid this.
+               if ( !isset( $_SESSION['wsExpiresUnix'] )
+                       || ( $now + self::TTL_REFRESH_WINDOW ) > isset( $_SESSION['wsExpiresUnix'] )
+               ) {
+                       $_SESSION['wsExpiresUnix'] = $now + $wgObjectCacheSessionExpiry;
+               }
+
                session_write_close();
        }
 }
index f373eaa..450251a 100644 (file)
@@ -574,7 +574,7 @@ class Article implements Page {
                }
 
                # Should the parser cache be used?
-               $useParserCache = $this->mPage->isParserCacheUsed( $parserOptions, $oldid );
+               $useParserCache = $this->mPage->shouldCheckParserCache( $parserOptions, $oldid );
                wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
                if ( $user->getStubThreshold() ) {
                        $this->getContext()->getStats()->increment( 'pcache_miss_stub' );
index cb3def3..f7f2528 100644 (file)
@@ -1082,13 +1082,13 @@ class WikiPage implements Page, IDBAccessObject {
         * Should the parser cache be used?
         *
         * @param ParserOptions $parserOptions ParserOptions to check
-        * @param int $oldid
+        * @param int $oldId
         * @return bool
         */
-       public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) {
+       public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) {
                return $parserOptions->getStubThreshold() == 0
                        && $this->exists()
-                       && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
+                       && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() )
                        && $this->getContentHandler()->isParserCacheSupported();
        }
 
@@ -1105,7 +1105,7 @@ class WikiPage implements Page, IDBAccessObject {
         */
        public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
 
-               $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid );
+               $useParserCache = $this->shouldCheckParserCache( $parserOptions, $oldid );
                wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
                if ( $parserOptions->getStubThreshold() ) {
                        wfIncrStats( 'pcache.miss.stub' );
index c65a3f9..9fd5a36 100644 (file)
@@ -297,9 +297,9 @@ abstract class Profiler {
         * @return array List of method entries arrays, each having:
         *   - name     : method name
         *   - calls    : the number of invoking calls
-        *   - real     : real time ellapsed (ms)
+        *   - real     : real time elapsed (ms)
         *   - %real    : percent real time
-        *   - cpu      : CPU time ellapsed (ms)
+        *   - cpu      : CPU time elapsed (ms)
         *   - %cpu     : percent CPU time
         *   - memory   : memory used (bytes)
         *   - %memory  : percent memory used
index bab8eba..63940bc 100644 (file)
@@ -89,9 +89,9 @@ class SectionProfiler {
         * @return array List of method entries arrays, each having:
         *   - name    : method name
         *   - calls   : the number of invoking calls
-        *   - real    : real time ellapsed (ms)
+        *   - real    : real time elapsed (ms)
         *   - %real   : percent real time
-        *   - cpu     : real time ellapsed (ms)
+        *   - cpu     : real time elapsed (ms)
         *   - %cpu    : percent real time
         *   - memory  : memory used (bytes)
         *   - %memory : percent memory used
index 9ed63c3..b39ff55 100644 (file)
@@ -226,8 +226,8 @@ class CopyFileBackend extends Maintenance {
                        }
                        $t_start = microtime( true );
                        $fsFiles = $src->getLocalReferenceMulti( array( 'srcs' => $srcPaths, 'latest' => 1 ) );
-                       $ellapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
-                       $this->output( "\n\tDownloaded these file(s) [{$ellapsed_ms}ms]:\n\t" .
+                       $elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
+                       $this->output( "\n\tDownloaded these file(s) [{$elapsed_ms}ms]:\n\t" .
                                implode( "\n\t", $srcPaths ) . "\n\n" );
                }
 
@@ -281,12 +281,12 @@ class CopyFileBackend extends Maintenance {
                        sleep( 10 ); // wait and retry copy again
                        $status = $dst->doQuickOperations( $ops, array( 'bypassReadOnly' => 1 ) );
                }
-               $ellapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
+               $elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
                if ( !$status->isOK() ) {
                        $this->error( print_r( $status->getErrorsArray(), true ) );
                        $this->error( "$wikiId: Could not copy file batch.", 1 ); // die
                } elseif ( count( $copiedRel ) ) {
-                       $this->output( "\n\tCopied these file(s) [{$ellapsed_ms}ms]:\n\t" .
+                       $this->output( "\n\tCopied these file(s) [{$elapsed_ms}ms]:\n\t" .
                                implode( "\n\t", $copiedRel ) . "\n\n" );
                }
        }
@@ -318,12 +318,12 @@ class CopyFileBackend extends Maintenance {
                        sleep( 10 ); // wait and retry copy again
                        $status = $dst->doQuickOperations( $ops, array( 'bypassReadOnly' => 1 ) );
                }
-               $ellapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
+               $elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
                if ( !$status->isOK() ) {
                        $this->error( print_r( $status->getErrorsArray(), true ) );
                        $this->error( "$wikiId: Could not delete file batch.", 1 ); // die
                } elseif ( count( $deletedRel ) ) {
-                       $this->output( "\n\tDeleted these file(s) [{$ellapsed_ms}ms]:\n\t" .
+                       $this->output( "\n\tDeleted these file(s) [{$elapsed_ms}ms]:\n\t" .
                                implode( "\n\t", $deletedRel ) . "\n\n" );
                }
        }
index 5c28ece..e89e36f 100644 (file)
@@ -687,6 +687,105 @@ class GlobalTest extends MediaWikiTestCase {
                $this->assertEquals( $expected, $actual, $description );
        }
 
+       public function wfWikiID() {
+               $this->setMwGlobals( array(
+                       'wgDBname' => 'example',
+                       'wgDBprefix' => '',
+               ) );
+               $this->assertEquals(
+                       wfWikiID(),
+                       'example'
+               );
+
+               $this->setMwGlobals( array(
+                       'wgDBname' => 'example',
+                       'wgDBprefix' => 'mw_',
+               ) );
+               $this->assertEquals(
+                       wfWikiID(),
+                       'example-mw_'
+               );
+       }
+
+       public function testWfMemcKey() {
+               // Just assert the exact output so we can catch unintentional changes to key
+               // construction, which would effectively invalidate all existing cache.
+
+               $this->setMwGlobals( array(
+                       'wgCachePrefix' => false,
+                       'wgDBname' => 'example',
+                       'wgDBprefix' => '',
+               ) );
+               $this->assertEquals(
+                       wfMemcKey( 'foo', '123', 'bar' ),
+                       'example:foo:123:bar'
+               );
+
+               $this->setMwGlobals( array(
+                       'wgCachePrefix' => false,
+                       'wgDBname' => 'example',
+                       'wgDBprefix' => 'mw_',
+               ) );
+               $this->assertEquals(
+                       wfMemcKey( 'foo', '123', 'bar' ),
+                       'example-mw_:foo:123:bar'
+               );
+
+               $this->setMwGlobals( array(
+                       'wgCachePrefix' => 'custom',
+                       'wgDBname' => 'example',
+                       'wgDBprefix' => 'mw_',
+               ) );
+               $this->assertEquals(
+                       wfMemcKey( 'foo', '123', 'bar' ),
+                       'custom:foo:123:bar'
+               );
+       }
+
+       public function testWfForeignMemcKey() {
+               $this->setMwGlobals( array(
+                       'wgCachePrefix' => false,
+                       'wgDBname' => 'example',
+                       'wgDBprefix' => '',
+               ) );
+               $local = wfMemcKey( 'foo', 'bar' );
+
+               $this->setMwGlobals( array(
+                       'wgDBname' => 'other',
+                       'wgDBprefix' => 'mw_',
+               ) );
+               $this->assertEquals(
+                       wfForeignMemcKey( 'example', '', 'foo', 'bar' ),
+                       $local,
+                       'Match output of wfMemcKey from local wiki'
+               );
+       }
+
+       public function testWfGlobalCacheKey() {
+               $this->setMwGlobals( array(
+                       'wgCachePrefix' => 'ignored',
+                       'wgDBname' => 'example',
+                       'wgDBprefix' => ''
+               ) );
+               $one = wfGlobalCacheKey( 'some', 'thing' );
+               $this->assertEquals(
+                       $one,
+                       'global:some:thing'
+               );
+
+               $this->setMwGlobals( array(
+                       'wgDBname' => 'other',
+                       'wgDBprefix' => 'mw_'
+               ) );
+               $two = wfGlobalCacheKey( 'some', 'thing' );
+
+               $this->assertEquals(
+                       $one,
+                       $two,
+                       'Not fragmented by wiki id'
+               );
+       }
+
        public static function provideWfShellWikiCmdList() {
                global $wgPhpCli;
 
diff --git a/tests/phpunit/includes/MediaWikiTest.php b/tests/phpunit/includes/MediaWikiTest.php
new file mode 100644 (file)
index 0000000..df94d3e
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+
+class MediaWikiTest extends MediaWikiTestCase {
+       protected function setUp() {
+               parent::setUp();
+
+               $this->setMwGlobals( array(
+                       'wgServer' => 'http://example.org',
+                       'wgScriptPath' => '/w',
+                       'wgScript' => '/w/index.php',
+                       'wgArticlePath' => '/wiki/$1',
+                       'wgActionPaths' => array(),
+               ) );
+       }
+
+       public static function provideTryNormaliseRedirect() {
+               return array(
+                       array(
+                               // View: Canonical
+                               'url' => 'http://example.org/wiki/Foo_Bar',
+                               'query' => array(),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+                       array(
+                               // View: Escaped title
+                               'url' => 'http://example.org/wiki/Foo%20Bar',
+                               'query' => array(),
+                               'title' => 'Foo_Bar',
+                               'redirect' => 'http://example.org/wiki/Foo_Bar',
+                       ),
+                       array(
+                               // View: Script path
+                               'url' => 'http://example.org/w/index.php?title=Foo_Bar',
+                               'query' => array( 'title' => 'Foo_Bar' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+                       array(
+                               // View: Script path with implicit title from page id
+                               'url' => 'http://example.org/w/index.php?curid=123',
+                               'query' => array( 'curid' => '123' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+                       array(
+                               // View: Script path with implicit title from revision id
+                               'url' => 'http://example.org/w/index.php?oldid=123',
+                               'query' => array( 'oldid' => '123' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+                       array(
+                               // View: Script path without title
+                               'url' => 'http://example.org/w/index.php',
+                               'query' => array(),
+                               'title' => 'Main_Page',
+                               'redirect' => 'http://example.org/wiki/Main_Page',
+                       ),
+                       array(
+                               // View: Script path with empty title
+                               'url' => 'http://example.org/w/index.php?title=',
+                               'query' => array( 'title' => '' ),
+                               'title' => 'Main_Page',
+                               'redirect' => 'http://example.org/wiki/Main_Page',
+                       ),
+                       array(
+                               // View: Index with escaped title
+                               'url' => 'http://example.org/w/index.php?title=Foo%20Bar',
+                               'query' => array( 'title' => 'Foo Bar' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => 'http://example.org/wiki/Foo_Bar',
+                       ),
+                       array(
+                               // View: Script path with escaped title
+                               'url' => 'http://example.org/w/?title=Foo_Bar',
+                               'query' => array( 'title' => 'Foo_Bar' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+                       array(
+                               // View: Root path with escaped title
+                               'url' => 'http://example.org/?title=Foo_Bar',
+                               'query' => array( 'title' => 'Foo_Bar' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+                       array(
+                               // View: Canonical with redundant query
+                               'url' => 'http://example.org/wiki/Foo_Bar?action=view',
+                               'query' => array( 'action' => 'view' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+                       array(
+                               // Edit: Canonical view url with action query
+                               'url' => 'http://example.org/wiki/Foo_Bar?action=edit',
+                               'query' => array( 'action' => 'edit' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+                       array(
+                               // View: Index with action query
+                               'url' => 'http://example.org/w/index.php?title=Foo_Bar&action=view',
+                               'query' => array( 'title' => 'Foo_Bar', 'action' => 'view' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+                       array(
+                               // Edit: Index with action query
+                               'url' => 'http://example.org/w/index.php?title=Foo_Bar&action=edit',
+                               'query' => array( 'title' => 'Foo_Bar', 'action' => 'edit' ),
+                               'title' => 'Foo_Bar',
+                               'redirect' => false,
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider provideTryNormaliseRedirect
+        * @covers MediaWiki::tryNormaliseRedirect
+        */
+       public function testTryNormaliseRedirect( $url, $query, $title, $expectedRedirect = false ) {
+               // Set SERVER because interpolateTitle() doesn't use getRequestURL(),
+               // whereas tryNormaliseRedirect does().
+               $_SERVER['REQUEST_URI'] = $url;
+
+               $req = new FauxRequest( $query );
+               $req->setRequestURL( $url );
+               // This adds a virtual 'title' query parameter. Normally called from Setup.php
+               $req->interpolateTitle();
+
+               $titleObj = Title::newFromText( $title );
+
+               // Set global context since some involved code paths don't yet have context
+               $context = RequestContext::getMain();
+               $context->setRequest( $req );
+               $context->setTitle( $titleObj );
+
+               $mw = new MediaWiki( $context );
+
+               $method = new ReflectionMethod( $mw, 'tryNormaliseRedirect' );
+               $method->setAccessible( true );
+               $ret = $method->invoke( $mw, $titleObj );
+
+               $this->assertEquals(
+                       $expectedRedirect !== false,
+                       $ret,
+                       'Return true only when redirecting'
+               );
+
+               $this->assertEquals(
+                       $expectedRedirect ?: '',
+                       $context->getOutput()->getRedirect()
+               );
+       }
+}
index 795c2bb..911e9d0 100644 (file)
@@ -1,5 +1,14 @@
 ( function ( $ ) {
-       QUnit.module( 'jquery.mwExtension', QUnit.newMwEnvironment() );
+       QUnit.module( 'jquery.mwExtension', QUnit.newMwEnvironment( {
+               // This entire module is deprecated.
+               // Surpress deprecation warnings in test output.
+               setup: function () {
+                       this.suppressWarnings();
+               },
+               teardown: function () {
+                       this.restoreWarnings();
+               }
+       }) );
 
        QUnit.test( 'String functions', 7, function ( assert ) {
                assert.equal( $.trimLeft( '  foo bar  ' ), 'foo bar  ', 'trimLeft' );