* Revert back to my parser pseudo-branch again. Note: if you feel like reverting...
authorTim Starling <tstarling@users.mediawiki.org>
Tue, 26 Aug 2008 14:37:15 +0000 (14:37 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Tue, 26 Aug 2008 14:37:15 +0000 (14:37 +0000)
* Merged replaceFreeExternalLinks() with doMagicLinks(). Makes a lot of sense, very similar operations, doesn't break any parser tests. Stops free links from interacting with other parser stages, the same way ISBN links don't.
* The pass order change fixes Brion's complaint in r39980. Early link expansion, triggered by having more than 1000 links in the page, was outputting URLs which were destroyed by RFEL. Added parser test.
* Fixed an unrelated bug in LinkHolderArray::replace(): if a link to a redirect appears in two separate RLH calls, the second and subsequent calls do not add the mw-redirect class. Caused by an unmigrated LinkCache fetch.
* Added a parser test for a pass interaction bug that the pass order change fixes.
* The fuzzer told me to tell you that free external links in non-caption image parameters, which are and have always been invisible, are now not registered either.
* Miscellaneous supporting updates to the test infrastructure.

14 files changed:
RELEASE-NOTES
includes/DefaultSettings.php
includes/Exception.php
includes/MessageCache.php
includes/Title.php
includes/parser/LinkHolderArray.php
includes/parser/Parser.php
includes/parser/Parser_DiffTest.php
languages/Language.php
languages/LanguageConverter.php
maintenance/parserTests.inc
maintenance/parserTests.php
maintenance/parserTests.txt
maintenance/parserTestsStaticParserHook.php

index 95c698c..d9508ef 100644 (file)
@@ -94,6 +94,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
 * HTML entities like &nbsp; now work (are not escaped) in edit summaries.
 * (bug 13815) In the comment for page moves, use the colon-separator message
   instead of a hardcoded colon.
+* Allow <gallery> to accept image names without an Image: prefix
 
 === Bug fixes in 1.14 ===
 
index 9fc302d..47590ed 100644 (file)
@@ -3335,6 +3335,12 @@ $wgParserConf = array(
        #'preprocessorClass' => 'Preprocessor_Hash',
 );
 
+/**
+ * LinkHolderArray batch size
+ * For debugging
+ */
+$wgLinkHolderBatchSize = 1000;
+
 /**
  * Hooks that are used for outputting exceptions.  Format is:
  *   $wgExceptionHooks[] = $funcname
index 34671ff..5aa9ba5 100644 (file)
@@ -83,7 +83,7 @@ class MWException extends Exception {
        function getHTML() {
                global $wgShowExceptionDetails;
                if( $wgShowExceptionDetails ) {
-                       return '<p>' . htmlspecialchars( $this->getMessage() ) .
+                       return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) .
                                '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) .
                                "</p>\n";
                } else {
index c46b932..0f2fc22 100644 (file)
@@ -649,12 +649,18 @@ class MessageCache {
                        return $message;
                }
 
-               global $wgParser;
+               global $wgParser, $wgParserConf;
                if ( !$this->mParser && isset( $wgParser ) ) {
                        # Do some initialisation so that we don't have to do it twice
                        $wgParser->firstCallInit();
                        # Clone it and store it
-                       $this->mParser = clone $wgParser;
+                       $class = $wgParserConf['class'];
+                       if ( $class == 'Parser_DiffTest' ) {
+                               # Uncloneable
+                               $this->mParser = new $class( $wgParserConf );
+                       } else {
+                               $this->mParser = clone $wgParser;
+                       }
                        #wfDebug( __METHOD__ . ": following contents triggered transform: $message\n" );
                }
                if ( $this->mParser ) {
index 2234eb1..0713344 100644 (file)
@@ -410,6 +410,12 @@ class Title {
                global $wgInterwikiCache, $wgContLang;
                $fname = 'Title::getInterwikiLink';
 
+               if ( count( Title::$interwikiCache ) >= self::CACHE_MAX ) {
+                       // Don't use infinite memory
+                       reset( Title::$interwikiCache );
+                       unset( Title::$interwikiCache[ key( Title::$interwikiCache ) ] );
+               }
+
                $key = $wgContLang->lc( $key );
 
                $k = wfMemcKey( 'interwiki', $key );
index c9b33a2..5dece6d 100644 (file)
@@ -1,8 +1,6 @@
 <?php
 
 class LinkHolderArray {
-       var $batchSize = 1000;
-
        var $internals = array(), $interwikis = array();
        var $size = 0;
        var $parent;
@@ -11,6 +9,15 @@ class LinkHolderArray {
                $this->parent = $parent;
        }
 
+       /**
+        * Reduce memory usage to reduce the impact of circular references
+        */
+       function __destruct() {
+               foreach ( $this as $name => $value ) {
+                       unset( $this->$name );
+               }
+       }
+
        /**
         * Merge another LinkHolderArray into this one
         */
@@ -30,7 +37,8 @@ class LinkHolderArray {
         * Returns true if the memory requirements of this object are getting large
         */
        function isBig() {
-               return $this->size > $this->batchSize;
+               global $wgLinkHolderBatchSize;
+               return $this->size > $wgLinkHolderBatchSize;
        }
 
        /**
@@ -145,7 +153,7 @@ class LinkHolderArray {
                                if ( $title->isAlwaysKnown() ) {
                                        $colours[$pdbk] = '';
                                } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
-                                       $colours[$pdbk] = '';
+                                       $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
                                        $output->addLink( $title, $id );
                                } elseif ( $linkCache->isBadLink( $pdbk ) ) {
                                        $colours[$pdbk] = 'new';
@@ -180,6 +188,9 @@ class LinkHolderArray {
                                $pdbk = $title->getPrefixedDBkey();
                                $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
                                $output->addLink( $title, $s->page_id );
+                               # FIXME: convoluted data flow
+                               # The redirect status and length is passed to getLinkColour via the LinkCache
+                               # Use formal parameters instead
                                $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
                                //add id to the extension todolist
                                $linkcolour_ids[$s->page_id] = $pdbk;
@@ -274,6 +285,9 @@ class LinkHolderArray {
                                                        $entry['pdbk'] = $varPdbk;
 
                                                        // set pdbk and colour
+                                                       # FIXME: convoluted data flow
+                                                       # The redirect status and length is passed to getLinkColour via the LinkCache
+                                                       # Use formal parameters instead
                                                        $colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold );
                                                        $linkcolour_ids[$s->page_id] = $pdbk;
                                                }
index 8bd9a03..d17949a 100644 (file)
@@ -92,13 +92,13 @@ class Parser
        # Persistent:
        var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
                $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
-               $mExtLinkBracketedRegex, $mDefaultStripList, $mVarCache, $mConf;
+               $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList, $mVarCache, $mConf;
 
 
        # Cleared with clearState():
        var $mOutput, $mAutonumber, $mDTopen, $mStripState;
        var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
-       var $mInterwikiLinkHolders, $mLinkHolders;
+       var $mLinkHolders, $mLinkID;
        var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
        var $mTplExpandCache; // empty-frame expansion cache
        var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
@@ -128,6 +128,7 @@ class Parser
                $this->mFunctionHooks = array();
                $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
                $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
+               $this->mUrlProtocols = wfUrlProtocols();
                $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
                        '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
                $this->mVarCache = array();
@@ -146,6 +147,9 @@ class Parser
         * Reduce memory usage to reduce the impact of circular references
         */
        function __destruct() {
+               if ( isset( $this->mLinkHolders ) ) {
+                       $this->mLinkHolders->__destruct();
+               }
                foreach ( $this as $name => $value ) {
                        unset( $this->$name );
                }
@@ -188,17 +192,8 @@ class Parser
                $this->mStripState = new StripState;
                $this->mArgStack = false;
                $this->mInPre = false;
-               $this->mInterwikiLinkHolders = array(
-                       'texts' => array(),
-                       'titles' => array()
-               );
-               $this->mLinkHolders = array(
-                       'namespaces' => array(),
-                       'dbkeys' => array(),
-                       'queries' => array(),
-                       'texts' => array(),
-                       'titles' => array()
-               );
+               $this->mLinkHolders = new LinkHolderArray( $this );
+               $this->mLinkID = 0;
                $this->mRevisionTimestamp = $this->mRevisionId = null;
 
                /**
@@ -213,7 +208,7 @@ class Parser
                 */
                #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
                # Changed to \x7f to allow XML double-parsing -- TS
-               $this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
+               $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
 
 
                # Clear these on every parse, bug 4549
@@ -303,7 +298,7 @@ class Parser
                 */
 
                global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
-               $fname = 'Parser::parse-' . wfGetCaller();
+               $fname = __METHOD__.'-' . wfGetCaller();
                wfProfileIn( __METHOD__ );
                wfProfileIn( $fname );
 
@@ -337,7 +332,6 @@ class Parser
                );
                $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
 
-               # only once and last
                $text = $this->doBlockLevels( $text, $linestart );
 
                $this->replaceLinkHolders( $text );
@@ -357,7 +351,7 @@ class Parser
                $uniq_prefix = $this->mUniqPrefix;
                $matches = array();
                $elements = array_keys( $this->mTransparentTagHooks );
-               $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+               $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
 
                foreach( $matches as $marker => $data ) {
                        list( $element, $content, $params, $tag ) = $data;
@@ -375,7 +369,7 @@ class Parser
                $text = Sanitizer::normalizeCharReferences( $text );
 
                if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
-                       $text = Parser::tidy($text);
+                       $text = self::tidy($text);
                } else {
                        # attempt to sanitize at least some nesting problems
                        # (bug #2702 and quite a few others)
@@ -480,6 +474,8 @@ class Parser
        function &getTitle() { return $this->mTitle; }
        function getOptions() { return $this->mOptions; }
        function getRevisionId() { return $this->mRevisionId; }
+       function getOutput() { return $this->mOutput; }
+       function nextLinkID() { return $this->mLinkID++; }
 
        function getFunctionLang() {
                global $wgLang, $wgContLang;
@@ -558,7 +554,7 @@ class Parser
                                $text = $inside;
                                $tail = null;
                        } else {
-                               if( $element == '!--' ) {
+                               if( $element === '!--' ) {
                                        $end = '/(-->)/';
                                } else {
                                        $end = "/(<\\/$element\\s*>)/i";
@@ -667,9 +663,9 @@ class Parser
 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
 '<head><title>test</title></head><body>'.$text.'</body></html>';
                if( $wgTidyInternal ) {
-                       $correctedtext = Parser::internalTidy( $wrappedtext );
+                       $correctedtext = self::internalTidy( $wrappedtext );
                } else {
-                       $correctedtext = Parser::externalTidy( $wrappedtext );
+                       $correctedtext = self::externalTidy( $wrappedtext );
                }
                if( is_null( $correctedtext ) ) {
                        wfDebug( "Tidy error detected!\n" );
@@ -686,8 +682,7 @@ class Parser
         */
        function externalTidy( $text ) {
                global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
-               $fname = 'Parser::externalTidy';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                $cleansource = '';
                $opts = ' -utf8';
@@ -716,7 +711,7 @@ class Parser
                        }
                }
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
 
                if( $cleansource == '' && $text != '') {
                        // Some kind of error happened, so we couldn't get the corrected text.
@@ -738,8 +733,7 @@ class Parser
         */
        function internalTidy( $text ) {
                global $wgTidyConf, $IP, $wgDebugTidy;
-               $fname = 'Parser::internalTidy';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                $tidy = new tidy;
                $tidy->parseString( $text, $wgTidyConf, 'utf8' );
@@ -757,7 +751,7 @@ class Parser
                                "\n-->";
                }
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $cleansource;
        }
 
@@ -767,34 +761,35 @@ class Parser
         * @private
         */
        function doTableStuff ( $text ) {
-               $fname = 'Parser::doTableStuff';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
-               $lines = explode ( "\n" , $text );
+               $lines = StringUtils::explode( "\n", $text );
+               $out = '';
                $td_history = array (); // Is currently a td tag open?
                $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
                $tr_history = array (); // Is currently a tr tag open?
                $tr_attributes = array (); // history of tr attributes
                $has_opened_tr = array(); // Did this table open a <tr> element?
                $indent_level = 0; // indent level of the table
-               foreach ( $lines as $key => $line )
-               {
-                       $line = trim ( $line );
+
+               foreach ( $lines as $outLine ) {
+                       $line = trim( $outLine );
 
                        if( $line == '' ) { // empty line, go to next line
+                               $out .= $outLine."\n";
                                continue;
                        }
-                       $first_character = $line{0};
+                       $first_character = $line[0];
                        $matches = array();
 
-                       if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
+                       if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) {
                                // First check if we are starting a new table
                                $indent_level = strlen( $matches[1] );
 
                                $attributes = $this->mStripState->unstripBoth( $matches[2] );
                                $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
 
-                               $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
+                               $outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
                                array_push ( $td_history , false );
                                array_push ( $last_tag_history , '' );
                                array_push ( $tr_history , false );
@@ -802,8 +797,9 @@ class Parser
                                array_push ( $has_opened_tr , false );
                        } else if ( count ( $td_history ) == 0 ) {
                                // Don't do any of the following
+                               $out .= $outLine."\n";
                                continue;
-                       } else if ( substr ( $line , 0 , 2 ) == '|}' ) {
+                       } else if ( substr ( $line , 0 , 2 ) === '|}' ) {
                                // We are ending a table
                                $line = '</table>' . substr ( $line , 2 );
                                $last_tag = array_pop ( $last_tag_history );
@@ -820,8 +816,8 @@ class Parser
                                        $line = "</{$last_tag}>{$line}";
                                }
                                array_pop ( $tr_attributes );
-                               $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level );
-                       } else if ( substr ( $line , 0 , 2 ) == '|-' ) {
+                               $outLine = $line . str_repeat( '</dd></dl>' , $indent_level );
+                       } else if ( substr ( $line , 0 , 2 ) === '|-' ) {
                                // Now we have a table row
                                $line = preg_replace( '#^\|-+#', '', $line );
 
@@ -844,21 +840,21 @@ class Parser
                                        $line = "</{$last_tag}>{$line}";
                                }
 
-                               $lines[$key] = $line;
+                               $outLine = $line;
                                array_push ( $tr_history , false );
                                array_push ( $td_history , false );
                                array_push ( $last_tag_history , '' );
                        }
-                       else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 )  == '|+' ) {
+                       else if ( $first_character === '|' || $first_character === '!' || substr ( $line , 0 , 2 )  === '|+' ) {
                                // This might be cell elements, td, th or captions
-                               if ( substr ( $line , 0 , 2 ) == '|+' ) {
+                               if ( substr ( $line , 0 , 2 ) === '|+' ) {
                                        $first_character = '+';
                                        $line = substr ( $line , 1 );
                                }
 
                                $line = substr ( $line , 1 );
 
-                               if ( $first_character == '!' ) {
+                               if ( $first_character === '!' ) {
                                        $line = str_replace ( '!!' , '||' , $line );
                                }
 
@@ -868,13 +864,13 @@ class Parser
                                // attribute values containing literal "||".
                                $cells = StringUtils::explodeMarkup( '||' , $line );
 
-                               $lines[$key] = '';
+                               $outLine = '';
 
                                // Loop through each table cell
                                foreach ( $cells as $cell )
                                {
                                        $previous = '';
-                                       if ( $first_character != '+' )
+                                       if ( $first_character !== '+' )
                                        {
                                                $tr_after = array_pop ( $tr_attributes );
                                                if ( !array_pop ( $tr_history ) ) {
@@ -892,11 +888,11 @@ class Parser
                                                $previous = "</{$last_tag}>{$previous}";
                                        }
 
-                                       if ( $first_character == '|' ) {
+                                       if ( $first_character === '|' ) {
                                                $last_tag = 'td';
-                                       } else if ( $first_character == '!' ) {
+                                       } else if ( $first_character === '!' ) {
                                                $last_tag = 'th';
-                                       } else if ( $first_character == '+' ) {
+                                       } else if ( $first_character === '+' ) {
                                                $last_tag = 'caption';
                                        } else {
                                                $last_tag = '';
@@ -919,38 +915,42 @@ class Parser
                                                $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
                                        }
 
-                                       $lines[$key] .= $cell;
+                                       $outLine .= $cell;
                                        array_push ( $td_history , true );
                                }
                        }
+                       $out .= $outLine . "\n";
                }
 
                // Closing open td, tr && table
                while ( count ( $td_history ) > 0 )
                {
                        if ( array_pop ( $td_history ) ) {
-                               $lines[] = '</td>' ;
+                               $out .= "</td>\n";
                        }
                        if ( array_pop ( $tr_history ) ) {
-                               $lines[] = '</tr>' ;
+                               $out .= "</tr>\n";
                        }
                        if ( !array_pop ( $has_opened_tr ) ) {
-                               $lines[] = "<tr><td></td></tr>" ;
+                               $out .= "<tr><td></td></tr>\n" ;
                        }
 
-                       $lines[] = '</table>' ;
+                       $out .= "</table>\n";
                }
 
-               $output = implode ( "\n" , $lines ) ;
+               // Remove trailing line-ending (b/c)
+               if ( substr( $out, -1 ) === "\n" ) {
+                       $out = substr( $out, 0, -1 );
+               }
 
                // special case: don't return empty table
-               if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) {
-                       $output = '';
+               if( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
+                       $out = '';
                }
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
 
-               return $output;
+               return $out;
        }
 
        /**
@@ -961,12 +961,11 @@ class Parser
         */
        function internalParse( $text ) {
                $isMain = true;
-               $fname = 'Parser::internalParse';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                # Hook to suspend the parser in this state
                if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
-                       wfProfileOut( $fname );
+                       wfProfileOut( __METHOD__ );
                        return $text ;
                }
 
@@ -999,84 +998,146 @@ class Parser
                $text = $this->doMagicLinks( $text );
                $text = $this->formatHeadings( $text, $isMain );
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
        /**
         * Replace special strings like "ISBN xxx" and "RFC xxx" with
         * magic external links.
-        *
+        * 
+        * DML
         * @private
         */
        function doMagicLinks( $text ) {
                wfProfileIn( __METHOD__ );
+               $prots = $this->mUrlProtocols;
+               $urlChar = self::EXT_LINK_URL_CLASS;
                $text = preg_replace_callback(
                        '!(?:                           # Start cases
-                           <a.*?</a> |                 # Skip link text
-                           <.*?> |                     # Skip stuff inside HTML elements
-                           (?:RFC|PMID)\s+([0-9]+) |   # RFC or PMID, capture number as m[1]
-                           ISBN\s+(\b                  # ISBN, capture number as m[2]
-                                     (?: 97[89] [\ \-]? )?   # optional 13-digit ISBN prefix
-                                     (?: [0-9]  [\ \-]? ){9} # 9 digits with opt. delimiters
-                                     [0-9Xx]                 # check digit
-                                   \b)
+                               (<a.*?</a>) |               # m[1]: Skip link text 
+                               (<.*?>) |                   # m[2]: Skip stuff inside HTML elements' . "
+                               (\\b(?:$prots)$urlChar+) |  # m[3]: Free external links" . '
+                               (?:RFC|PMID)\s+([0-9]+) |   # m[4]: RFC or PMID, capture number
+                               ISBN\s+(\b                  # m[5]: ISBN, capture number
+                                   (?: 97[89] [\ \-]? )?   # optional 13-digit ISBN prefix
+                                   (?: [0-9]  [\ \-]? ){9} # 9 digits with opt. delimiters
+                                   [0-9Xx]                 # check digit
+                                   \b)
                        )!x', array( &$this, 'magicLinkCallback' ), $text );
                wfProfileOut( __METHOD__ );
                return $text;
        }
 
        function magicLinkCallback( $m ) {
-               if ( substr( $m[0], 0, 1 ) == '<' ) {
+               if ( isset( $m[1] ) && strval( $m[1] ) !== '' ) {
+                       # Skip anchor
+                       return $m[0];
+               } elseif ( isset( $m[2] ) && strval( $m[2] ) !== '' ) {
                        # Skip HTML element
                        return $m[0];
-               } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) {
-                       $isbn = $m[2];
-                       $num = strtr( $isbn, array(
-                               '-' => '',
-                               ' ' => '',
-                               'x' => 'X',
-                       ));
-                       $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
-                       $text = '<a href="' .
-                               $titleObj->escapeLocalUrl() .
-                               "\" class=\"internal\">ISBN $isbn</a>";
-               } else {
-                       if ( substr( $m[0], 0, 3 ) == 'RFC' ) {
+               } elseif ( isset( $m[3] ) && strval( $m[3] ) !== '' ) {
+                       # Free external link
+                       return $this->makeFreeExternalLink( $m[0] );
+               } elseif ( isset( $m[4] ) && strval( $m[4] ) !== '' ) {
+                       # RFC or PMID
+                       if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
                                $keyword = 'RFC';
                                $urlmsg = 'rfcurl';
-                               $id = $m[1];
-                       } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) {
+                               $id = $m[4];
+                       } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
                                $keyword = 'PMID';
                                $urlmsg = 'pubmedurl';
-                               $id = $m[1];
+                               $id = $m[4];
                        } else {
                                throw new MWException( __METHOD__.': unrecognised match type "' .
                                        substr($m[0], 0, 20 ) . '"' );
                        }
-
                        $url = wfMsg( $urlmsg, $id);
                        $sk = $this->mOptions->getSkin();
                        $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
-                       $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+                       return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+               } elseif ( isset( $m[5] ) && strval( $m[5] ) !== '' ) {
+                       # ISBN
+                       $isbn = $m[5];
+                       $num = strtr( $isbn, array(
+                               '-' => '',
+                               ' ' => '',
+                               'x' => 'X',
+                       ));
+                       $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
+                       return'<a href="' .
+                               $titleObj->escapeLocalUrl() .
+                               "\" class=\"internal\">ISBN $isbn</a>";
+               } else {
+                       return $m[0];
                }
-               return $text;
        }
 
+       /**
+        * Make a free external link, given a user-supplied URL
+        * @return HTML
+        * @private
+        */
+       function makeFreeExternalLink( $url ) {
+               global $wgContLang;
+               wfProfileIn( __METHOD__ );
+
+               $sk = $this->mOptions->getSkin();
+               $trail = '';
+
+               # The characters '<' and '>' (which were escaped by
+               # removeHTMLtags()) should not be included in
+               # URLs, per RFC 2396.
+               $m2 = array();
+               if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
+                       $trail = substr($url, $m2[0][1]) . $trail;
+                       $url = substr($url, 0, $m2[0][1]);
+               }
+
+               # Move trailing punctuation to $trail
+               $sep = ',;\.:!?';
+               # If there is no left bracket, then consider right brackets fair game too
+               if ( strpos( $url, '(' ) === false ) {
+                       $sep .= ')';
+               }
+
+               $numSepChars = strspn( strrev( $url ), $sep );
+               if ( $numSepChars ) {
+                       $trail = substr( $url, -$numSepChars ) . $trail;
+                       $url = substr( $url, 0, -$numSepChars );
+               }
+
+               $url = Sanitizer::cleanUrl( $url );
+
+               # Is this an external image?
+               $text = $this->maybeMakeExternalImage( $url );
+               if ( $text === false ) {
+                       # Not an image, make a link
+                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
+                       # Register it in the output object...
+                       # Replace unnecessary URL escape codes with their equivalent characters
+                       $pasteurized = self::replaceUnusualEscapes( $url );
+                       $this->mOutput->addExternalLink( $pasteurized );
+               }
+               wfProfileOut( __METHOD__ );
+               return $text . $trail;
+       }
+
+
        /**
         * Parse headers and return html
         *
         * @private
         */
        function doHeadings( $text ) {
-               $fname = 'Parser::doHeadings';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
                for ( $i = 6; $i >= 1; --$i ) {
                        $h = str_repeat( '=', $i );
                        $text = preg_replace( "/^$h(.+)$h\\s*$/m",
                          "<h$i>\\1</h$i>", $text );
                }
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
@@ -1086,15 +1147,14 @@ class Parser
         * @return string the altered text
         */
        function doAllQuotes( $text ) {
-               $fname = 'Parser::doAllQuotes';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
                $outtext = '';
-               $lines = explode( "\n", $text );
+               $lines = StringUtils::explode( "\n", $text );
                foreach ( $lines as $line ) {
-                       $outtext .= $this->doQuotes ( $line ) . "\n";
+                       $outtext .= $this->doQuotes( $line ) . "\n";
                }
                $outtext = substr($outtext, 0,-1);
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $outtext;
        }
 
@@ -1156,9 +1216,9 @@ class Parser
                                        {
                                                $x1 = substr ($arr[$i-1], -1);
                                                $x2 = substr ($arr[$i-1], -2, 1);
-                                               if ($x1 == ' ') {
+                                               if ($x1 === ' ') {
                                                        if ($firstspace == -1) $firstspace = $i;
-                                               } else if ($x2 == ' ') {
+                                               } else if ($x2 === ' ') {
                                                        if ($firstsingleletterword == -1) $firstsingleletterword = $i;
                                                } else {
                                                        if ($firstmultiletterword == -1) $firstmultiletterword = $i;
@@ -1198,7 +1258,7 @@ class Parser
                        {
                                if (($i % 2) == 0)
                                {
-                                       if ($state == 'both')
+                                       if ($state === 'both')
                                                $buffer .= $r;
                                        else
                                                $output .= $r;
@@ -1207,41 +1267,41 @@ class Parser
                                {
                                        if (strlen ($r) == 2)
                                        {
-                                               if ($state == 'i')
+                                               if ($state === 'i')
                                                { $output .= '</i>'; $state = ''; }
-                                               else if ($state == 'bi')
+                                               else if ($state === 'bi')
                                                { $output .= '</i>'; $state = 'b'; }
-                                               else if ($state == 'ib')
+                                               else if ($state === 'ib')
                                                { $output .= '</b></i><b>'; $state = 'b'; }
-                                               else if ($state == 'both')
+                                               else if ($state === 'both')
                                                { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
                                                else # $state can be 'b' or ''
                                                { $output .= '<i>'; $state .= 'i'; }
                                        }
                                        else if (strlen ($r) == 3)
                                        {
-                                               if ($state == 'b')
+                                               if ($state === 'b')
                                                { $output .= '</b>'; $state = ''; }
-                                               else if ($state == 'bi')
+                                               else if ($state === 'bi')
                                                { $output .= '</i></b><i>'; $state = 'i'; }
-                                               else if ($state == 'ib')
+                                               else if ($state === 'ib')
                                                { $output .= '</b>'; $state = 'i'; }
-                                               else if ($state == 'both')
+                                               else if ($state === 'both')
                                                { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
                                                else # $state can be 'i' or ''
                                                { $output .= '<b>'; $state .= 'b'; }
                                        }
                                        else if (strlen ($r) == 5)
                                        {
-                                               if ($state == 'b')
+                                               if ($state === 'b')
                                                { $output .= '</b><i>'; $state = 'i'; }
-                                               else if ($state == 'i')
+                                               else if ($state === 'i')
                                                { $output .= '</i><b>'; $state = 'b'; }
-                                               else if ($state == 'bi')
+                                               else if ($state === 'bi')
                                                { $output .= '</i></b>'; $state = ''; }
-                                               else if ($state == 'ib')
+                                               else if ($state === 'ib')
                                                { $output .= '</b></i>'; $state = ''; }
-                                               else if ($state == 'both')
+                                               else if ($state === 'both')
                                                { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
                                                else # ($state == '')
                                                { $buffer = ''; $state = 'both'; }
@@ -1250,21 +1310,21 @@ class Parser
                                $i++;
                        }
                        # Now close all remaining tags.  Notice that the order is important.
-                       if ($state == 'b' || $state == 'ib')
+                       if ($state === 'b' || $state === 'ib')
                                $output .= '</b>';
-                       if ($state == 'i' || $state == 'bi' || $state == 'ib')
+                       if ($state === 'i' || $state === 'bi' || $state === 'ib')
                                $output .= '</i>';
-                       if ($state == 'bi')
+                       if ($state === 'bi')
                                $output .= '</b>';
                        # There might be lonely ''''', so make sure we have a buffer
-                       if ($state == 'both' && $buffer)
+                       if ($state === 'both' && $buffer)
                                $output .= '<b><i>'.$buffer.'</i></b>';
                        return $output;
                }
        }
 
        /**
-        * Replace external links
+        * Replace external links (REL)
         *
         * Note: this is all very hackish and the order of execution matters a lot.
         * Make sure to run maintenance/parserTests.php if you change this code.
@@ -1273,14 +1333,12 @@ class Parser
         */
        function replaceExternalLinks( $text ) {
                global $wgContLang;
-               $fname = 'Parser::replaceExternalLinks';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                $sk = $this->mOptions->getSkin();
 
                $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-
-               $s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
+               $s = array_shift( $bits );
 
                $i = 0;
                while ( $i<count( $bits ) ) {
@@ -1308,7 +1366,7 @@ class Parser
                        $dtrail = '';
 
                        # Set linktype for CSS - if URL==text, link is essentially free
-                       $linktype = ($text == $url) ? 'free' : 'text';
+                       $linktype = ($text === $url) ? 'free' : 'text';
 
                        # No link text, e.g. [http://domain.tld/some.link]
                        if ( $text == '' ) {
@@ -1331,10 +1389,6 @@ class Parser
 
                        $url = Sanitizer::cleanUrl( $url );
 
-                       # Process the trail (i.e. everything after this link up until start of the next link),
-                       # replacing any non-bracketed links
-                       $trail = $this->replaceFreeExternalLinks( $trail );
-
                        # Use the encoded URL
                        # This means that users can paste URLs directly into the text
                        # Funny characters like &ouml; aren't valid in URLs anyway
@@ -1344,92 +1398,11 @@ class Parser
                        # Register link in the output object.
                        # Replace unnecessary URL escape codes with the referenced character
                        # This prevents spammers from hiding links from the filters
-                       $pasteurized = Parser::replaceUnusualEscapes( $url );
+                       $pasteurized = self::replaceUnusualEscapes( $url );
                        $this->mOutput->addExternalLink( $pasteurized );
                }
 
-               wfProfileOut( $fname );
-               return $s;
-       }
-
-       /**
-        * Replace anything that looks like a URL with a link
-        * @private
-        */
-       function replaceFreeExternalLinks( $text ) {
-               global $wgContLang;
-               $fname = 'Parser::replaceFreeExternalLinks';
-               wfProfileIn( $fname );
-
-               $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-               $s = array_shift( $bits );
-               $i = 0;
-
-               $sk = $this->mOptions->getSkin();
-
-               while ( $i < count( $bits ) ){
-                       $protocol = $bits[$i++];
-                       $remainder = $bits[$i++];
-
-                       $m = array();
-                       if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
-                               # Found some characters after the protocol that look promising
-                               $url = $protocol . $m[1];
-                               $trail = $m[2];
-
-                               # special case: handle urls as url args:
-                               # http://www.example.com/foo?=http://www.example.com/bar
-                               if(strlen($trail) == 0 &&
-                                       isset($bits[$i]) &&
-                                       preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
-                                       preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
-                               {
-                                       # add protocol, arg
-                                       $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link
-                                       $i += 2;
-                                       $trail = $m[2];
-                               }
-
-                               # The characters '<' and '>' (which were escaped by
-                               # removeHTMLtags()) should not be included in
-                               # URLs, per RFC 2396.
-                               $m2 = array();
-                               if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
-                                       $trail = substr($url, $m2[0][1]) . $trail;
-                                       $url = substr($url, 0, $m2[0][1]);
-                               }
-
-                               # Move trailing punctuation to $trail
-                               $sep = ',;\.:!?';
-                               # If there is no left bracket, then consider right brackets fair game too
-                               if ( strpos( $url, '(' ) === false ) {
-                                       $sep .= ')';
-                               }
-
-                               $numSepChars = strspn( strrev( $url ), $sep );
-                               if ( $numSepChars ) {
-                                       $trail = substr( $url, -$numSepChars ) . $trail;
-                                       $url = substr( $url, 0, -$numSepChars );
-                               }
-
-                               $url = Sanitizer::cleanUrl( $url );
-
-                               # Is this an external image?
-                               $text = $this->maybeMakeExternalImage( $url );
-                               if ( $text === false ) {
-                                       # Not an image, make a link
-                                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
-                                       # Register it in the output object...
-                                       # Replace unnecessary URL escape codes with their equivalent characters
-                                       $pasteurized = Parser::replaceUnusualEscapes( $url );
-                                       $this->mOutput->addExternalLink( $pasteurized );
-                               }
-                               $s .= $text . $trail;
-                       } else {
-                               $s .= $protocol . $remainder;
-                       }
-               }
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $s;
        }
 
@@ -1445,7 +1418,7 @@ class Parser
         */
        static function replaceUnusualEscapes( $url ) {
                return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
-                       array( 'Parser', 'replaceUnusualEscapesCallback' ), $url );
+                       array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
        }
 
        /**
@@ -1489,35 +1462,48 @@ class Parser
 
        /**
         * Process [[ ]] wikilinks
+        * @return processed text
         *
         * @private
         */
        function replaceInternalLinks( $s ) {
+               $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
+               return $s;
+       }
+
+       /**
+        * Process [[ ]] wikilinks (RIL)
+        * @return LinkHolderArray
+        *
+        * @private
+        */
+       function replaceInternalLinks2( &$s ) {
                global $wgContLang;
-               static $fname = 'Parser::replaceInternalLinks' ;
 
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
-               wfProfileIn( $fname.'-setup' );
-               static $tc = FALSE;
+               wfProfileIn( __METHOD__.'-setup' );
+               static $tc = FALSE, $e1, $e1_img;
                # the % is needed to support urlencoded titles as well
-               if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
+               if ( !$tc ) { 
+                       $tc = Title::legalChars() . '#%';
+                       # Match a link having the form [[namespace:link|alternate]]trail
+                       $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
+                       # Match cases where there is no "]]", which might still be images
+                       $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
+               }
 
                $sk = $this->mOptions->getSkin();
+               $holders = new LinkHolderArray( $this );
 
                #split the entire text string on occurences of [[
-               $a = explode( '[[', ' ' . $s );
+               $a = StringUtils::explode( '[[', ' ' . $s );
                #get the first element (all text up to first [[), and remove the space we added
-               $s = array_shift( $a );
+               $s = $a->current();
+               $a->next();
+               $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
                $s = substr( $s, 1 );
 
-               # Match a link having the form [[namespace:link|alternate]]trail
-               static $e1 = FALSE;
-               if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; }
-               # Match cases where there is no "]]", which might still be images
-               static $e1_img = FALSE;
-               if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
-
                $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
                $e2 = null;
                if ( $useLinkPrefixExtension ) {
@@ -1527,8 +1513,8 @@ class Parser
                }
 
                if( is_null( $this->mTitle ) ) {
-                       wfProfileOut( $fname );
-                       wfProfileOut( $fname.'-setup' );
+                       wfProfileOut( __METHOD__.'-setup' );
+                       wfProfileOut( __METHOD__ );
                        throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
                }
                $nottalk = !$this->mTitle->isTalkPage();
@@ -1550,13 +1536,20 @@ class Parser
                        $selflink = array($this->mTitle->getPrefixedText());
                }
                $useSubpages = $this->areSubpagesAllowed();
-               wfProfileOut( $fname.'-setup' );
+               wfProfileOut( __METHOD__.'-setup' );
 
                # Loop for each link
-               for ($k = 0; isset( $a[$k] ); $k++) {
-                       $line = $a[$k];
+               for ( ; $line !== false && $line !== null ; $a->next(), $line = $a->current() ) {
+                       # Check for excessive memory usage
+                       if ( $holders->isBig() ) {
+                               # Too big
+                               # Do the existence check, replace the link holders and clear the array
+                               $holders->replace( $s );
+                               $holders->clear();
+                       }
+
                        if ( $useLinkPrefixExtension ) {
-                               wfProfileIn( $fname.'-prefixhandling' );
+                               wfProfileIn( __METHOD__.'-prefixhandling' );
                                if ( preg_match( $e2, $s, $m ) ) {
                                        $prefix = $m[2];
                                        $s = $m[1];
@@ -1568,12 +1561,12 @@ class Parser
                                        $prefix = $first_prefix;
                                        $first_prefix = false;
                                }
-                               wfProfileOut( $fname.'-prefixhandling' );
+                               wfProfileOut( __METHOD__.'-prefixhandling' );
                        }
 
                        $might_be_img = false;
 
-                       wfProfileIn( "$fname-e1" );
+                       wfProfileIn( __METHOD__."-e1" );
                        if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
                                $text = $m[2];
                                # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
@@ -1607,18 +1600,18 @@ class Parser
                                $trail = "";
                        } else { # Invalid form; output directly
                                $s .= $prefix . '[[' . $line ;
-                               wfProfileOut( "$fname-e1" );
+                               wfProfileOut( __METHOD__."-e1" );
                                continue;
                        }
-                       wfProfileOut( "$fname-e1" );
-                       wfProfileIn( "$fname-misc" );
+                       wfProfileOut( __METHOD__."-e1" );
+                       wfProfileIn( __METHOD__."-misc" );
 
                        # Don't allow internal links to pages containing
                        # PROTO: where PROTO is a valid URL protocol; these
                        # should be external links.
                        if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) {
                                $s .= $prefix . '[[' . $line ;
-                               wfProfileOut( "$fname-misc" );
+                               wfProfileOut( __METHOD__."-misc" );
                                continue;
                        }
 
@@ -1629,33 +1622,36 @@ class Parser
                                $link = $m[1];
                        }
 
-                       $noforce = (substr($m[1], 0, 1) != ':');
+                       $noforce = (substr($m[1], 0, 1) !== ':');
                        if (!$noforce) {
                                # Strip off leading ':'
                                $link = substr($link, 1);
                        }
 
-                       wfProfileOut( "$fname-misc" );
-                       wfProfileIn( "$fname-title" );
+                       wfProfileOut( __METHOD__."-misc" );
+                       wfProfileIn( __METHOD__."-title" );
                        $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
                        if( !$nt ) {
                                $s .= $prefix . '[[' . $line;
-                               wfProfileOut( "$fname-title" );
+                               wfProfileOut( __METHOD__."-title" );
                                continue;
                        }
 
                        $ns = $nt->getNamespace();
                        $iw = $nt->getInterWiki();
-                       wfProfileOut( "$fname-title" );
+                       wfProfileOut( __METHOD__."-title" );
 
                        if ($might_be_img) { # if this is actually an invalid link
-                               wfProfileIn( "$fname-might_be_img" );
+                               wfProfileIn( __METHOD__."-might_be_img" );
                                if ($ns == NS_IMAGE && $noforce) { #but might be an image
                                        $found = false;
-                                       while (isset ($a[$k+1]) ) {
+                                       while ( true ) {
                                                #look at the next 'line' to see if we can close it there
-                                               $spliced = array_splice( $a, $k + 1, 1 );
-                                               $next_line = array_shift( $spliced );
+                                               $a->next();
+                                               $next_line = $a->current();
+                                               if ( $next_line === false || $next_line === null ) {
+                                                       break;
+                                               }
                                                $m = explode( ']]', $next_line, 3 );
                                                if ( count( $m ) == 3 ) {
                                                        # the first ]] closes the inner link, the second the image
@@ -1675,19 +1671,19 @@ class Parser
                                        if ( !$found ) {
                                                # we couldn't find the end of this imageLink, so output it raw
                                                #but don't ignore what might be perfectly normal links in the text we've examined
-                                               $text = $this->replaceInternalLinks($text);
+                                               $holders->merge( $this->replaceInternalLinks2( $text ) );
                                                $s .= "{$prefix}[[$link|$text";
                                                # note: no $trail, because without an end, there *is* no trail
-                                               wfProfileOut( "$fname-might_be_img" );
+                                               wfProfileOut( __METHOD__."-might_be_img" );
                                                continue;
                                        }
                                } else { #it's not an image, so output it raw
                                        $s .= "{$prefix}[[$link|$text";
                                        # note: no $trail, because without an end, there *is* no trail
-                                       wfProfileOut( "$fname-might_be_img" );
+                                       wfProfileOut( __METHOD__."-might_be_img" );
                                        continue;
                                }
-                               wfProfileOut( "$fname-might_be_img" );
+                               wfProfileOut( __METHOD__."-might_be_img" );
                        }
 
                        $wasblank = ( '' == $text );
@@ -1697,41 +1693,36 @@ class Parser
                        if( $noforce ) {
 
                                # Interwikis
-                               wfProfileIn( "$fname-interwiki" );
+                               wfProfileIn( __METHOD__."-interwiki" );
                                if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
                                        $this->mOutput->addLanguageLink( $nt->getFullText() );
                                        $s = rtrim($s . $prefix);
                                        $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
-                                       wfProfileOut( "$fname-interwiki" );
+                                       wfProfileOut( __METHOD__."-interwiki" );
                                        continue;
                                }
-                               wfProfileOut( "$fname-interwiki" );
+                               wfProfileOut( __METHOD__."-interwiki" );
 
                                if ( $ns == NS_IMAGE ) {
-                                       wfProfileIn( "$fname-image" );
+                                       wfProfileIn( __METHOD__."-image" );
                                        if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
                                                # recursively parse links inside the image caption
                                                # actually, this will parse them in any other parameters, too,
                                                # but it might be hard to fix that, and it doesn't matter ATM
                                                $text = $this->replaceExternalLinks($text);
-                                               $text = $this->replaceInternalLinks($text);
+                                               $holders->merge( $this->replaceInternalLinks2( $text ) );
 
                                                # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
-                                               $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail;
-                                               $this->mOutput->addImage( $nt->getDBkey() );
-
-                                               wfProfileOut( "$fname-image" );
-                                               continue;
-                                       } else {
-                                               # We still need to record the image's presence on the page
-                                               $this->mOutput->addImage( $nt->getDBkey() );
+                                               $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text, $holders ) ) . $trail;
                                        }
-                                       wfProfileOut( "$fname-image" );
+                                       $this->mOutput->addImage( $nt->getDBkey() );
+                                       wfProfileOut( __METHOD__."-image" );
+                                       continue;
 
                                }
 
                                if ( $ns == NS_CATEGORY ) {
-                                       wfProfileIn( "$fname-category" );
+                                       wfProfileIn( __METHOD__."-category" );
                                        $s = rtrim($s . "\n"); # bug 87
 
                                        if ( $wasblank ) {
@@ -1750,7 +1741,7 @@ class Parser
                                         */
                                        $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
 
-                                       wfProfileOut( "$fname-category" );
+                                       wfProfileOut( __METHOD__."-category" );
                                        continue;
                                }
                        }
@@ -1781,7 +1772,7 @@ class Parser
                                if( SpecialPage::exists( $nt->getDBkey() ) ) {
                                        $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
                                } else {
-                                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+                                       $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
                                }
                                continue;
                        } elseif( $ns == NS_IMAGE ) {
@@ -1795,10 +1786,10 @@ class Parser
                                        continue;
                                }
                        }
-                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+                       $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
                }
-               wfProfileOut( $fname );
-               return $s;
+               wfProfileOut( __METHOD__ );
+               return $holders;
        }
 
        /**
@@ -1807,32 +1798,10 @@ class Parser
         * parsing of interwiki links, and secondly to allow all existence checks and
         * article length checks (for stub links) to be bundled into a single query.
         *
+        * @deprecated
         */
        function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
-               wfProfileIn( __METHOD__ );
-               if ( ! is_object($nt) ) {
-                       # Fail gracefully
-                       $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
-               } else {
-                       # Separate the link trail from the rest of the link
-                       list( $inside, $trail ) = Linker::splitTrail( $trail );
-
-                       if ( $nt->isExternal() ) {
-                               $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside );
-                               $this->mInterwikiLinkHolders['titles'][] = $nt;
-                               $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}";
-                       } else {
-                               $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() );
-                               $this->mLinkHolders['dbkeys'][] = $nt->getDBkey();
-                               $this->mLinkHolders['queries'][] = $query;
-                               $this->mLinkHolders['texts'][] = $prefix.$text.$inside;
-                               $this->mLinkHolders['titles'][] = $nt;
-
-                               $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-               return $retVal;
+               return $this->mLinkHolders->makeHolder( $nt, $text, $query, $trail, $prefix );
        }
 
        /**
@@ -1860,10 +1829,8 @@ class Parser
         * Insert a NOPARSE hacky thing into any inline links in a chunk that's
         * going to go through further parsing steps before inline URL expansion.
         *
-        * In particular this is important when using action=render, which causes
-        * full URLs to be included.
-        *
-        * Oh man I hate our multi-layer parser!
+        * Not needed quite as much as it used to be since free links are a bit
+        * more sensible these days. But bracketed links are still an issue.
         *
         * @param string more-or-less HTML
         * @return string less-or-more HTML with NOPARSE bits
@@ -1898,8 +1865,7 @@ class Parser
                # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
                # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
 
-               $fname = 'Parser::maybeDoSubpageLink';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
                $ret = $target; # default return value is no change
 
                # Some namespaces don't allow subpages,
@@ -1915,7 +1881,7 @@ class Parser
                        # bug 7425
                        $target = trim( $target );
                        # Look at the first character
-                       if( $target != '' && $target{0} == '/' ) {
+                       if( $target != '' && $target{0} === '/' ) {
                                # / at end means we don't want the slash to be shown
                                $m = array();
                                $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
@@ -1942,7 +1908,7 @@ class Parser
                                        if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
                                                $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
                                                # / at the end means don't show full path
-                                               if( substr( $nodotdot, -1, 1 ) == '/' ) {
+                                               if( substr( $nodotdot, -1, 1 ) === '/' ) {
                                                        $nodotdot = substr( $nodotdot, 0, -1 );
                                                        if( '' === $text ) {
                                                                $text = $nodotdot . $suffix;
@@ -1958,7 +1924,7 @@ class Parser
                        }
                }
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $ret;
        }
 
@@ -1994,10 +1960,10 @@ class Parser
        /* private */ function openList( $char ) {
                $result = $this->closeParagraph();
 
-               if ( '*' == $char ) { $result .= '<ul><li>'; }
-               else if ( '#' == $char ) { $result .= '<ol><li>'; }
-               else if ( ':' == $char ) { $result .= '<dl><dd>'; }
-               else if ( ';' == $char ) {
+               if ( '*' === $char ) { $result .= '<ul><li>'; }
+               else if ( '#' === $char ) { $result .= '<ol><li>'; }
+               else if ( ':' === $char ) { $result .= '<dl><dd>'; }
+               else if ( ';' === $char ) {
                        $result .= '<dl><dt>';
                        $this->mDTopen = true;
                }
@@ -2007,11 +1973,11 @@ class Parser
        }
 
        /* private */ function nextItem( $char ) {
-               if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
-               else if ( ':' == $char || ';' == $char ) {
+               if ( '*' === $char || '#' === $char ) { return '</li><li>'; }
+               else if ( ':' === $char || ';' === $char ) {
                        $close = '</dd>';
                        if ( $this->mDTopen ) { $close = '</dt>'; }
-                       if ( ';' == $char ) {
+                       if ( ';' === $char ) {
                                $this->mDTopen = true;
                                return $close . '<dt>';
                        } else {
@@ -2023,9 +1989,9 @@ class Parser
        }
 
        /* private */ function closeList( $char ) {
-               if ( '*' == $char ) { $text = '</li></ul>'; }
-               else if ( '#' == $char ) { $text = '</li></ol>'; }
-               else if ( ':' == $char ) {
+               if ( '*' === $char ) { $text = '</li></ul>'; }
+               else if ( '#' === $char ) { $text = '</li></ol>'; }
+               else if ( ':' === $char ) {
                        if ( $this->mDTopen ) {
                                $this->mDTopen = false;
                                $text = '</dt></dl>';
@@ -2039,56 +2005,59 @@ class Parser
        /**#@-*/
 
        /**
-        * Make lists from lines starting with ':', '*', '#', etc.
+        * Make lists from lines starting with ':', '*', '#', etc. (DBL)
         *
         * @private
         * @return string the lists rendered as HTML
         */
        function doBlockLevels( $text, $linestart ) {
-               $fname = 'Parser::doBlockLevels';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                # Parsing through the text line by line.  The main thing
                # happening here is handling of block-level elements p, pre,
                # and making lists from lines starting with * # : etc.
                #
-               $textLines = explode( "\n", $text );
+               $textLines = StringUtils::explode( "\n", $text );
 
                $lastPrefix = $output = '';
                $this->mDTopen = $inBlockElem = false;
                $prefixLength = 0;
                $paragraphStack = false;
 
-               if ( !$linestart ) {
-                       $output .= array_shift( $textLines );
-               }
                foreach ( $textLines as $oLine ) {
+                       # Fix up $linestart
+                       if ( !$linestart ) {
+                               $output .= $oLine;
+                               $linestart = true;
+                               continue;
+                       }
+
                        $lastPrefixLength = strlen( $lastPrefix );
                        $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
                        $preOpenMatch = preg_match('/<pre/i', $oLine );
                        if ( !$this->mInPre ) {
                                # Multiple prefixes may abut each other for nested lists.
                                $prefixLength = strspn( $oLine, '*#:;' );
-                               $pref = substr( $oLine, 0, $prefixLength );
+                               $prefix = substr( $oLine, 0, $prefixLength );
 
                                # eh?
-                               $pref2 = str_replace( ';', ':', $pref );
+                               $prefix2 = str_replace( ';', ':', $prefix );
                                $t = substr( $oLine, $prefixLength );
-                               $this->mInPre = !empty($preOpenMatch);
+                               $this->mInPre = (bool)$preOpenMatch;
                        } else {
                                # Don't interpret any other prefixes in preformatted text
                                $prefixLength = 0;
-                               $pref = $pref2 = '';
+                               $prefix = $prefix2 = '';
                                $t = $oLine;
                        }
 
                        # List generation
-                       if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
+                       if( $prefixLength && $lastPrefix === $prefix2 ) {
                                # Same as the last item, so no need to deal with nesting or opening stuff
-                               $output .= $this->nextItem( substr( $pref, -1 ) );
+                               $output .= $this->nextItem( substr( $prefix, -1 ) );
                                $paragraphStack = false;
 
-                               if ( substr( $pref, -1 ) == ';') {
+                               if ( substr( $prefix, -1 ) === ';') {
                                        # The one nasty exception: definition lists work like this:
                                        # ; title : definition text
                                        # So we check for : in the remainder text to split up the
@@ -2101,21 +2070,21 @@ class Parser
                                }
                        } elseif( $prefixLength || $lastPrefixLength ) {
                                # Either open or close a level...
-                               $commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
+                               $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
                                $paragraphStack = false;
 
                                while( $commonPrefixLength < $lastPrefixLength ) {
-                                       $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
+                                       $output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
                                        --$lastPrefixLength;
                                }
                                if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
-                                       $output .= $this->nextItem( $pref{$commonPrefixLength-1} );
+                                       $output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
                                }
                                while ( $prefixLength > $commonPrefixLength ) {
-                                       $char = substr( $pref, $commonPrefixLength, 1 );
+                                       $char = substr( $prefix, $commonPrefixLength, 1 );
                                        $output .= $this->openList( $char );
 
-                                       if ( ';' == $char ) {
+                                       if ( ';' === $char ) {
                                                # FIXME: This is dupe of code above
                                                if ($this->findColonNoLinks($t, $term, $t2) !== false) {
                                                        $t = $t2;
@@ -2124,10 +2093,10 @@ class Parser
                                        }
                                        ++$commonPrefixLength;
                                }
-                               $lastPrefix = $pref2;
+                               $lastPrefix = $prefix2;
                        }
                        if( 0 == $prefixLength ) {
-                               wfProfileIn( "$fname-paragraph" );
+                               wfProfileIn( __METHOD__."-paragraph" );
                                # No prefix (not in list)--go to paragraph mode
                                // XXX: use a stack for nestable elements like span, table and div
                                $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
@@ -2147,9 +2116,9 @@ class Parser
                                                $inBlockElem = true;
                                        }
                                } else if ( !$inBlockElem && !$this->mInPre ) {
-                                       if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
+                                       if ( ' ' == $t{0} and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
                                                // pre
-                                               if ($this->mLastSection != 'pre') {
+                                               if ($this->mLastSection !== 'pre') {
                                                        $paragraphStack = false;
                                                        $output .= $this->closeParagraph().'<pre>';
                                                        $this->mLastSection = 'pre';
@@ -2163,7 +2132,7 @@ class Parser
                                                                $paragraphStack = false;
                                                                $this->mLastSection = 'p';
                                                        } else {
-                                                               if ($this->mLastSection != 'p' ) {
+                                                               if ($this->mLastSection !== 'p' ) {
                                                                        $output .= $this->closeParagraph();
                                                                        $this->mLastSection = '';
                                                                        $paragraphStack = '<p>';
@@ -2176,14 +2145,14 @@ class Parser
                                                                $output .= $paragraphStack;
                                                                $paragraphStack = false;
                                                                $this->mLastSection = 'p';
-                                                       } else if ($this->mLastSection != 'p') {
+                                                       } else if ($this->mLastSection !== 'p') {
                                                                $output .= $this->closeParagraph().'<p>';
                                                                $this->mLastSection = 'p';
                                                        }
                                                }
                                        }
                                }
-                               wfProfileOut( "$fname-paragraph" );
+                               wfProfileOut( __METHOD__."-paragraph" );
                        }
                        // somewhere above we forget to get out of pre block (bug 785)
                        if($preCloseMatch && $this->mInPre) {
@@ -2194,7 +2163,7 @@ class Parser
                        }
                }
                while ( $prefixLength ) {
-                       $output .= $this->closeList( $pref2{$prefixLength-1} );
+                       $output .= $this->closeList( $prefix2[$prefixLength-1] );
                        --$prefixLength;
                }
                if ( '' != $this->mLastSection ) {
@@ -2202,7 +2171,7 @@ class Parser
                        $this->mLastSection = '';
                }
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $output;
        }
 
@@ -2215,13 +2184,12 @@ class Parser
         * return string the position of the ':', or false if none found
         */
        function findColonNoLinks($str, &$before, &$after) {
-               $fname = 'Parser::findColonNoLinks';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                $pos = strpos( $str, ':' );
                if( $pos === false ) {
                        // Nothing to find!
-                       wfProfileOut( $fname );
+                       wfProfileOut( __METHOD__ );
                        return false;
                }
 
@@ -2230,7 +2198,7 @@ class Parser
                        // Easy; no tag nesting to worry about
                        $before = substr( $str, 0, $pos );
                        $after = substr( $str, $pos+1 );
-                       wfProfileOut( $fname );
+                       wfProfileOut( __METHOD__ );
                        return $pos;
                }
 
@@ -2254,7 +2222,7 @@ class Parser
                                                // We found it!
                                                $before = substr( $str, 0, $i );
                                                $after = substr( $str, $i + 1 );
-                                               wfProfileOut( $fname );
+                                               wfProfileOut( __METHOD__ );
                                                return $i;
                                        }
                                        // Embedded in a tag; don't break it.
@@ -2264,7 +2232,7 @@ class Parser
                                        $colon = strpos( $str, ':', $i );
                                        if( $colon === false ) {
                                                // Nothing else interesting
-                                               wfProfileOut( $fname );
+                                               wfProfileOut( __METHOD__ );
                                                return false;
                                        }
                                        $lt = strpos( $str, '<', $i );
@@ -2273,7 +2241,7 @@ class Parser
                                                        // We found it!
                                                        $before = substr( $str, 0, $colon );
                                                        $after = substr( $str, $colon + 1 );
-                                                       wfProfileOut( $fname );
+                                                       wfProfileOut( __METHOD__ );
                                                        return $i;
                                                }
                                        }
@@ -2320,18 +2288,18 @@ class Parser
                                break;
                        case 3: // self::COLON_STATE_CLOSETAG:
                                // In a </tag>
-                               if( $c == ">" ) {
+                               if( $c === ">" ) {
                                        $stack--;
                                        if( $stack < 0 ) {
-                                               wfDebug( "Invalid input in $fname; too many close tags\n" );
-                                               wfProfileOut( $fname );
+                                               wfDebug( __METHOD__.": Invalid input; too many close tags\n" );
+                                               wfProfileOut( __METHOD__ );
                                                return false;
                                        }
                                        $state = self::COLON_STATE_TEXT;
                                }
                                break;
                        case self::COLON_STATE_TAGSLASH:
-                               if( $c == ">" ) {
+                               if( $c === ">" ) {
                                        // Yes, a self-closed tag <blah/>
                                        $state = self::COLON_STATE_TEXT;
                                } else {
@@ -2340,33 +2308,33 @@ class Parser
                                }
                                break;
                        case 5: // self::COLON_STATE_COMMENT:
-                               if( $c == "-" ) {
+                               if( $c === "-" ) {
                                        $state = self::COLON_STATE_COMMENTDASH;
                                }
                                break;
                        case self::COLON_STATE_COMMENTDASH:
-                               if( $c == "-" ) {
+                               if( $c === "-" ) {
                                        $state = self::COLON_STATE_COMMENTDASHDASH;
                                } else {
                                        $state = self::COLON_STATE_COMMENT;
                                }
                                break;
                        case self::COLON_STATE_COMMENTDASHDASH:
-                               if( $c == ">" ) {
+                               if( $c === ">" ) {
                                        $state = self::COLON_STATE_TEXT;
                                } else {
                                        $state = self::COLON_STATE_COMMENT;
                                }
                                break;
                        default:
-                               throw new MWException( "State machine error in $fname" );
+                               throw new MWException( "State machine error in " . __METHOD__ );
                        }
                }
                if( $stack > 0 ) {
-                       wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
+                       wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" );
                        return false;
                }
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return false;
        }
 
@@ -2596,12 +2564,11 @@ class Parser
         * @private
         */
        function initialiseVariables() {
-               $fname = 'Parser::initialiseVariables';
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
                $variableIDs = MagicWord::getVariableIDs();
 
                $this->mVariables = new MagicWordArray( $variableIDs );
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
        }
 
        /**
@@ -2670,8 +2637,7 @@ class Parser
                        return $text;
                }
 
-               $fname = __METHOD__;
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
 
                if ( $frame === false ) {
                        $frame = $this->getPreprocessor()->newFrame();
@@ -2684,7 +2650,7 @@ class Parser
                $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
                $text = $frame->expand( $dom, $flags );
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
@@ -2747,8 +2713,7 @@ class Parser
         */
        function braceSubstitution( $piece, $frame ) {
                global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
-               $fname = __METHOD__;
-               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__.'-setup' );
 
                # Flags
@@ -2935,7 +2900,7 @@ class Parser
                                        }
                                } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
                                        $found = false; //access denied
-                                       wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() );
+                                       wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() );
                                } else {
                                        list( $text, $title ) = $this->getTemplateDom( $title );
                                        if ( $text !== false ) {
@@ -2969,7 +2934,7 @@ class Parser
                # Recover the source wikitext and return it
                if ( !$found ) {
                        $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
-                       wfProfileOut( $fname );
+                       wfProfileOut( __METHOD__ );
                        return array( 'object' => $text );
                }
 
@@ -3028,7 +2993,7 @@ class Parser
                        $ret = array( 'text' => $text );
                }
 
-               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
                return $ret;
        }
 
@@ -3315,7 +3280,7 @@ class Parser
                        }
                }
 
-               if ( $name == 'html' || $name == 'nowiki' ) {
+               if ( $name === 'html' || $name === 'nowiki' ) {
                        $this->mStripState->nowiki->setPair( $marker, $output );
                } else {
                        $this->mStripState->general->setPair( $marker, $output );
@@ -3571,12 +3536,7 @@ class Parser
                        #     <!--LINK number-->
                        # turns into
                        #     link text with suffix
-                       $safeHeadline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
-                                                           "\$this->mLinkHolders['texts'][\$1]",
-                                                           $safeHeadline );
-                       $safeHeadline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e',
-                                                           "\$this->mInterwikiLinkHolders['texts'][\$1]",
-                                                           $safeHeadline );
+                       $safeHeadline = $this->replaceLinkHoldersText( $safeHeadline );
 
                        # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
                        $tocline = preg_replace(
@@ -3652,7 +3612,7 @@ class Parser
                $i = 0;
 
                foreach( $blocks as $block ) {
-                       if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
+                       if( $showEditLink && $headlineCount > 0 && $i == 0 && $block !== "\n" ) {
                                # This is the [edit] link that appears for the top block of text when
                                # section editing is enabled
 
@@ -3804,7 +3764,7 @@ class Parser
                        } else {
                                # Failed to validate; fall back to the default
                                $nickname = $username;
-                               wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
+                               wfDebug( __METHOD__.": $username has bad XML tags in signature.\n" );
                        }
                }
 
@@ -3910,19 +3870,17 @@ class Parser
                global $wgTitle;
                static $executing = false;
 
-               $fname = "Parser::transformMsg";
-
                # Guard against infinite recursion
                if ( $executing ) {
                        return $text;
                }
                $executing = true;
 
-               wfProfileIn($fname);
+               wfProfileIn(__METHOD__);
                $text = $this->preprocess( $text, $wgTitle, $options );
 
                $executing = false;
-               wfProfileOut($fname);
+               wfProfileOut(__METHOD__);
                return $text;
        }
 
@@ -4019,7 +3977,7 @@ class Parser
                # Add to function cache
                $mw = MagicWord::get( $id );
                if( !$mw )
-                       throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' );
+                       throw new MWException( __METHOD__.'() expecting a magic word identifier.' );
 
                $synonyms = $mw->getSynonyms();
                $sensitive = intval( $mw->isCaseSensitive() );
@@ -4034,7 +3992,7 @@ class Parser
                                $syn = '#' . $syn;
                        }
                        # Remove trailing colon
-                       if ( substr( $syn, -1, 1 ) == ':' ) {
+                       if ( substr( $syn, -1, 1 ) === ':' ) {
                                $syn = substr( $syn, 0, -1 );
                        }
                        $this->mFunctionSynonyms[$sensitive][$syn] = $id;
@@ -4055,266 +4013,9 @@ class Parser
         * Replace <!--LINK--> link placeholders with actual links, in the buffer
         * Placeholders created in Skin::makeLinkObj()
         * Returns an array of link CSS classes, indexed by PDBK.
-        * $options is a bit field, RLH_FOR_UPDATE to select for update
         */
        function replaceLinkHolders( &$text, $options = 0 ) {
-               global $wgUser;
-               global $wgContLang;
-
-               $fname = 'Parser::replaceLinkHolders';
-               wfProfileIn( $fname );
-
-               $pdbks = array();
-               $colours = array();
-               $linkcolour_ids = array();
-               $sk = $this->mOptions->getSkin();
-               $linkCache = LinkCache::singleton();
-
-               if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
-                       wfProfileIn( $fname.'-check' );
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $page = $dbr->tableName( 'page' );
-                       $threshold = $wgUser->getOption('stubthreshold');
-
-                       # Sort by namespace
-                       asort( $this->mLinkHolders['namespaces'] );
-
-                       # Generate query
-                       $query = false;
-                       $current = null;
-                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-                               # Make title object
-                               $title = $this->mLinkHolders['titles'][$key];
-
-                               # Skip invalid entries.
-                               # Result will be ugly, but prevents crash.
-                               if ( is_null( $title ) ) {
-                                       continue;
-                               }
-                               $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
-
-                               # Check if it's a static known link, e.g. interwiki
-                               if ( $title->isAlwaysKnown() ) {
-                                       $colours[$pdbk] = '';
-                               } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
-                                       $colours[$pdbk] = '';
-                                       $this->mOutput->addLink( $title, $id );
-                               } elseif ( $linkCache->isBadLink( $pdbk ) ) {
-                                       $colours[$pdbk] = 'new';
-                               } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
-                                       $colours[$pdbk] = 'new';
-                               } else {
-                                       # Not in the link cache, add it to the query
-                                       if ( !isset( $current ) ) {
-                                               $current = $ns;
-                                               $query =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
-                                               $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
-                                       } elseif ( $current != $ns ) {
-                                               $current = $ns;
-                                               $query .= ")) OR (page_namespace=$ns AND page_title IN(";
-                                       } else {
-                                               $query .= ', ';
-                                       }
-
-                                       $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
-                               }
-                       }
-                       if ( $query ) {
-                               $query .= '))';
-                               if ( $options & RLH_FOR_UPDATE ) {
-                                       $query .= ' FOR UPDATE';
-                               }
-
-                               $res = $dbr->query( $query, $fname );
-
-                               # Fetch data and form into an associative array
-                               # non-existent = broken
-                               while ( $s = $dbr->fetchObject($res) ) {
-                                       $title = Title::makeTitle( $s->page_namespace, $s->page_title );
-                                       $pdbk = $title->getPrefixedDBkey();
-                                       $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
-                                       $this->mOutput->addLink( $title, $s->page_id );
-                                       $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
-                                       //add id to the extension todolist
-                                       $linkcolour_ids[$s->page_id] = $pdbk;
-                               }
-                               //pass an array of page_ids to an extension
-                               wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
-                       }
-                       wfProfileOut( $fname.'-check' );
-
-                       # Do a second query for different language variants of links and categories
-                       if($wgContLang->hasVariants()){
-                               $linkBatch = new LinkBatch();
-                               $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
-                               $categoryMap = array(); // maps $category_variant => $category (dbkeys)
-                               $varCategories = array(); // category replacements oldDBkey => newDBkey
-
-                               $categories = $this->mOutput->getCategoryLinks();
-
-                               // Add variants of links to link batch
-                               foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-                                       $title = $this->mLinkHolders['titles'][$key];
-                                       if ( is_null( $title ) )
-                                               continue;
-
-                                       $pdbk = $title->getPrefixedDBkey();
-                                       $titleText = $title->getText();
-
-                                       // generate all variants of the link title text
-                                       $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
-
-                                       // if link was not found (in first query), add all variants to query
-                                       if ( !isset($colours[$pdbk]) ){
-                                               foreach($allTextVariants as $textVariant){
-                                                       if($textVariant != $titleText){
-                                                               $variantTitle = Title::makeTitle( $ns, $textVariant );
-                                                               if(is_null($variantTitle)) continue;
-                                                               $linkBatch->addObj( $variantTitle );
-                                                               $variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
-                                                       }
-                                               }
-                                       }
-                               }
-
-                               // process categories, check if a category exists in some variant
-                               foreach( $categories as $category ){
-                                       $variants = $wgContLang->convertLinkToAllVariants($category);
-                                       foreach($variants as $variant){
-                                               if($variant != $category){
-                                                       $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
-                                                       if(is_null($variantTitle)) continue;
-                                                       $linkBatch->addObj( $variantTitle );
-                                                       $categoryMap[$variant] = $category;
-                                               }
-                                       }
-                               }
-
-
-                               if(!$linkBatch->isEmpty()){
-                                       // construct query
-                                       $titleClause = $linkBatch->constructSet('page', $dbr);
-
-                                       $variantQuery =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
-
-                                       $variantQuery .= " FROM $page WHERE $titleClause";
-                                       if ( $options & RLH_FOR_UPDATE ) {
-                                               $variantQuery .= ' FOR UPDATE';
-                                       }
-
-                                       $varRes = $dbr->query( $variantQuery, $fname );
-
-                                       // for each found variants, figure out link holders and replace
-                                       while ( $s = $dbr->fetchObject($varRes) ) {
-
-                                               $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
-                                               $varPdbk = $variantTitle->getPrefixedDBkey();
-                                               $vardbk = $variantTitle->getDBkey();
-
-                                               $holderKeys = array();
-                                               if(isset($variantMap[$varPdbk])){
-                                                       $holderKeys = $variantMap[$varPdbk];
-                                                       $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
-                                                       $this->mOutput->addLink( $variantTitle, $s->page_id );
-                                               }
-
-                                               // loop over link holders
-                                               foreach($holderKeys as $key){
-                                                       $title = $this->mLinkHolders['titles'][$key];
-                                                       if ( is_null( $title ) ) continue;
-
-                                                       $pdbk = $title->getPrefixedDBkey();
-
-                                                       if(!isset($colours[$pdbk])){
-                                                               // found link in some of the variants, replace the link holder data
-                                                               $this->mLinkHolders['titles'][$key] = $variantTitle;
-                                                               $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey();
-
-                                                               // set pdbk and colour
-                                                               $pdbks[$key] = $varPdbk;
-                                                               $colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold );
-                                                               $linkcolour_ids[$s->page_id] = $pdbk;
-                                                       }
-                                                       wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
-                                               }
-
-                                               // check if the object is a variant of a category
-                                               if(isset($categoryMap[$vardbk])){
-                                                       $oldkey = $categoryMap[$vardbk];
-                                                       if($oldkey != $vardbk)
-                                                               $varCategories[$oldkey]=$vardbk;
-                                               }
-                                       }
-
-                                       // rebuild the categories in original order (if there are replacements)
-                                       if(count($varCategories)>0){
-                                               $newCats = array();
-                                               $originalCats = $this->mOutput->getCategories();
-                                               foreach($originalCats as $cat => $sortkey){
-                                                       // make the replacement
-                                                       if( array_key_exists($cat,$varCategories) )
-                                                               $newCats[$varCategories[$cat]] = $sortkey;
-                                                       else $newCats[$cat] = $sortkey;
-                                               }
-                                               $this->mOutput->setCategoryLinks($newCats);
-                                       }
-                               }
-                       }
-
-                       # Construct search and replace arrays
-                       wfProfileIn( $fname.'-construct' );
-                       $replacePairs = array();
-                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-                               $pdbk = $pdbks[$key];
-                               $searchkey = "<!--LINK $key-->";
-                               $title = $this->mLinkHolders['titles'][$key];
-                               if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) {
-                                       $linkCache->addBadLinkObj( $title );
-                                       $colours[$pdbk] = 'new';
-                                       $this->mOutput->addLink( $title, 0 );
-                                       $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
-                                                                       $this->mLinkHolders['texts'][$key],
-                                                                       $this->mLinkHolders['queries'][$key] );
-                               } else {
-                                       $replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk],
-                                                                       $this->mLinkHolders['texts'][$key],
-                                                                       $this->mLinkHolders['queries'][$key] );
-                               }
-                       }
-                       $replacer = new HashtableReplacer( $replacePairs, 1 );
-                       wfProfileOut( $fname.'-construct' );
-
-                       # Do the thing
-                       wfProfileIn( $fname.'-replace' );
-                       $text = preg_replace_callback(
-                               '/(<!--LINK .*?-->)/',
-                               $replacer->cb(),
-                               $text);
-
-                       wfProfileOut( $fname.'-replace' );
-               }
-
-               # Now process interwiki link holders
-               # This is quite a bit simpler than internal links
-               if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
-                       wfProfileIn( $fname.'-interwiki' );
-                       # Make interwiki link HTML
-                       $replacePairs = array();
-                       foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
-                               $title = $this->mInterwikiLinkHolders['titles'][$key];
-                               $replacePairs[$key] = $sk->link( $title, $link );
-                       }
-                       $replacer = new HashtableReplacer( $replacePairs, 1 );
-
-                       $text = preg_replace_callback(
-                               '/<!--IWLINK (.*?)-->/',
-                               $replacer->cb(),
-                               $text );
-                       wfProfileOut( $fname.'-interwiki' );
-               }
-
-               wfProfileOut( $fname );
-               return $colours;
+               return $this->mLinkHolders->replace( $text );
        }
 
        /**
@@ -4324,36 +4025,7 @@ class Parser
         * @return string
         */
        function replaceLinkHoldersText( $text ) {
-               $fname = 'Parser::replaceLinkHoldersText';
-               wfProfileIn( $fname );
-
-               $text = preg_replace_callback(
-                       '/<!--(LINK|IWLINK) (.*?)-->/',
-                       array( &$this, 'replaceLinkHoldersTextCallback' ),
-                       $text );
-
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /**
-        * @param array $matches
-        * @return string
-        * @private
-        */
-       function replaceLinkHoldersTextCallback( $matches ) {
-               $type = $matches[1];
-               $key  = $matches[2];
-               if( $type == 'LINK' ) {
-                       if( isset( $this->mLinkHolders['texts'][$key] ) ) {
-                               return $this->mLinkHolders['texts'][$key];
-                       }
-               } elseif( $type == 'IWLINK' ) {
-                       if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
-                               return $this->mInterwikiLinkHolders['texts'][$key];
-                       }
-               }
-               return $matches[0];
+               return $this->mLinkHolders->replaceText( $text );
        }
 
        /**
@@ -4407,7 +4079,7 @@ class Parser
 
                wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
 
-               $lines = explode( "\n", $text );
+               $lines = StringUtils::explode( "\n", $text );
                foreach ( $lines as $line ) {
                        # match lines like these:
                        # Image:someimage.jpg|This is some image
@@ -4420,7 +4092,7 @@ class Parser
                        
                        if ( strpos( $matches[0], '%' ) !== false )
                                $matches[1] = urldecode( $matches[1] );
-                       $tp = Title::newFromText( $matches[1] );
+                       $tp = Title::newFromText( $matches[1]/*, NS_IMAGE*/ );
                        $nt =& $tp;
                        if( is_null( $nt ) ) {
                                # Bogus title. Ignore these so we don't bomb out later.
@@ -4486,8 +4158,11 @@ class Parser
 
        /**
         * Parse image options text and use it to make an image
+        * @param Title $title
+        * @param string $options
+        * @param LinkHolderArray $holders
         */
-       function makeImage( $title, $options ) {
+       function makeImage( $title, $options, $holders = false ) {
                # Check if the options text is of the form "options|alt text"
                # Options are:
                #  * thumbnail          make a thumbnail with enlarge-icon and caption, alignment depends on lang
@@ -4510,7 +4185,7 @@ class Parser
                #  * bottom
                #  * text-bottom
 
-               $parts = array_map( 'trim', explode( '|', $options) );
+               $parts = StringUtils::explode( "|", $options );
                $sk = $this->mOptions->getSkin();
 
                # Give extensions a chance to select the file revision for us
@@ -4532,13 +4207,14 @@ class Parser
                $params = array( 'frame' => array(), 'handler' => array(),
                        'horizAlign' => array(), 'vertAlign' => array() );
                foreach( $parts as $part ) {
+                       $part = trim( $part );
                        list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
                        $validated = false;
                        if( isset( $paramMap[$magicName] ) ) {
                                list( $type, $paramName ) = $paramMap[$magicName];
 
                                // Special case; width and height come in one variable together
-                               if( $type == 'handler' && $paramName == 'width' ) {
+                               if( $type === 'handler' && $paramName === 'width' ) {
                                        $m = array();
                                        # (bug 13500) In both cases (width/height and width only),
                                        # permit trailing "px" for backward compatibility.
@@ -4561,7 +4237,7 @@ class Parser
                                                }
                                        } // else no validation -- bug 13436
                                } else {
-                                       if ( $type == 'handler' ) {
+                                       if ( $type === 'handler' ) {
                                                # Validate handler parameter
                                                $validated = $handler->validateParam( $paramName, $value );
                                        } else {
@@ -4597,7 +4273,13 @@ class Parser
                }
 
                # Strip bad stuff out of the alt text
-               $alt = $this->replaceLinkHoldersText( $caption );
+               # We can't just use replaceLinkHoldersText() here, because if this function
+               # is called from replaceInternalLinks2(), mLinkHolders won't be up to date.
+               if ( $holders ) {
+                       $alt = $holders->replaceText( $caption );
+               } else {
+                       $alt = $this->replaceLinkHoldersText( $caption );
+               }
 
                # make sure there are no placeholders in thumbnail attributes
                # that are later expanded to html- so expand them now and
@@ -4700,7 +4382,7 @@ class Parser
                $sectionParts = explode( '-', $section );
                $sectionIndex = array_pop( $sectionParts );
                foreach ( $sectionParts as $part ) {
-                       if ( $part == 'T' ) {
+                       if ( $part === 'T' ) {
                                $flags |= self::PTD_FOR_INCLUSION;
                        }
                }
@@ -4717,14 +4399,14 @@ class Parser
                        $targetLevel = 1000;
                } else {
             while ( $node ) {
-                if ( $node->getName() == 'h' ) {
+                if ( $node->getName() === 'h' ) {
                     $bits = $node->splitHeading();
                                        if ( $bits['i'] == $sectionIndex ) {
                                        $targetLevel = $bits['level'];
                                                break;
                                        }
                                }
-                               if ( $mode == 'replace' ) {
+                               if ( $mode === 'replace' ) {
                                        $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
                                }
                                $node = $node->getNextSibling();
@@ -4733,7 +4415,7 @@ class Parser
 
                if ( !$node ) {
                        // Not found
-                       if ( $mode == 'get' ) {
+                       if ( $mode === 'get' ) {
                                return $newText;
                        } else {
                                return $text;
@@ -4742,21 +4424,21 @@ class Parser
 
                // Find the end of the section, including nested sections
                do {
-                       if ( $node->getName() == 'h' ) {
+                       if ( $node->getName() === 'h' ) {
                                $bits = $node->splitHeading();
                                $curLevel = $bits['level'];
                                if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
                                        break;
                                }
                        }
-                       if ( $mode == 'get' ) {
+                       if ( $mode === 'get' ) {
                                $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
                        }
                        $node = $node->getNextSibling();
                } while ( $node );
 
                // Write out the remainder (in replace mode only)
-               if ( $mode == 'replace' ) {
+               if ( $mode === 'replace' ) {
                        // Output the replacement text
                        // Add two newlines on -- trailing whitespace in $newText is conventionally
                        // stripped by the editor, so we need both newlines to restore the paragraph gap
@@ -4986,7 +4668,7 @@ class StripState {
                do {
                        $oldText = $text;
                        $text = $this->general->replace( $text );
-               } while ( $text != $oldText );
+               } while ( $text !== $oldText );
                wfProfileOut( __METHOD__ );
                return $text;
        }
@@ -4996,7 +4678,7 @@ class StripState {
                do {
                        $oldText = $text;
                        $text = $this->nowiki->replace( $text );
-               } while ( $text != $oldText );
+               } while ( $text !== $oldText );
                wfProfileOut( __METHOD__ );
                return $text;
        }
@@ -5007,7 +4689,7 @@ class StripState {
                        $oldText = $text;
                        $text = $this->general->replace( $text );
                        $text = $this->nowiki->replace( $text );
-               } while ( $text != $oldText );
+               } while ( $text !== $oldText );
                wfProfileOut( __METHOD__ );
                return $text;
        }
@@ -5021,7 +4703,7 @@ class OnlyIncludeReplacer {
        var $output = '';
 
        function replace( $matches ) {
-               if ( substr( $matches[1], -1 ) == "\n" ) {
+               if ( substr( $matches[1], -1 ) === "\n" ) {
                        $this->output .= substr( $matches[1], 0, -1 );
                } else {
                        $this->output .= $matches[1];
index 2a677ea..608c883 100644 (file)
@@ -69,9 +69,22 @@ class Parser_DiffTest
                        $lastResult = $currentResult;
                }
                if ( $mismatch ) {
-                       throw new MWException( "Parser_DiffTest: results mismatch on call to $name\n" .
-                               'Arguments: ' . $this->formatArray( $args ) . "\n" .
-                               'Results: ' . $this->formatArray( $results ) . "\n" );
+                       if ( count( $results ) == 2 ) {
+                               $resultsList = array();
+                               foreach ( $this->parsers as $i => $parser ) {
+                                       $resultsList[] = var_export( $results[$i], true );
+                               }
+                               $diff = wfDiff( $resultsList[0], $resultsList[1] );
+                       } else {
+                               $diff = '[too many parsers]';
+                       }
+                       $msg = "Parser_DiffTest: results mismatch on call to $name\n";
+                       if ( !$this->shortOutput ) {
+                               $msg .= 'Arguments: ' . $this->formatArray( $args ) . "\n";
+                       }
+                       $msg .= 'Results: ' . $this->formatArray( $results ) . "\n" .
+                               "Diff: $diff\n";
+                       throw new MWException( $msg );
                }
                return $lastResult;
        }
index 18f523a..44ac062 100644 (file)
@@ -176,6 +176,15 @@ class Language {
                }
        }
 
+       /**
+        * Reduce memory usage
+        */
+       function __destruct() {
+               foreach ( $this as $name => $value ) {
+                       unset( $this->$name );
+               }
+       }
+
        /**
         * Hook which will be called if this is the content language.
         * Descendants can use this to register hook functions or modify globals
index c38d586..1c3926c 100644 (file)
@@ -435,8 +435,9 @@ class LanguageConverter {
                if ($isTitle) return $this->convertTitle($text);
 
                $plang = $this->getPreferredVariant();
-               $tarray = explode($this->mMarkup['end'], $text);
+               $tarray = StringUtils::explode($this->mMarkup['end'], $text);
                $text = '';
+               $lastDelim = false;
                foreach($tarray as $txt) {
                        $marked = explode($this->mMarkup['begin'], $txt, 2);
 
@@ -452,8 +453,17 @@ class LanguageConverter {
 
                                $text .= $crule->getDisplay();
                                $this->applyManualConv($crule);
+                               $lastDelim = false;
+                       } else {
+                               // Reinsert the }- which wasn't part of anything
+                               $text .= $this->mMarkup['end'];
+                               $lastDelim = true;
                        }
                }
+               if ( $lastDelim ) {
+                       // Remove the last delimiter (wasn't real)
+                       $text = substr( $text, 0, -strlen( $this->mMarkup['end'] ) );
+               }
 
                return $text;
        }
index 01409c1..4cccd3b 100644 (file)
@@ -26,7 +26,7 @@
 
 /** */
 $options = array( 'quick', 'color', 'quiet', 'help', 'show-output', 'record' );
-$optionsWithArgs = array( 'regex' );
+$optionsWithArgs = array( 'regex', 'seed' );
 
 require_once( 'commandLine.inc' );
 require_once( "$IP/maintenance/parserTestsParserHook.php" );
@@ -62,6 +62,10 @@ class ParserTest {
         */
        private $oldTablePrefix;
 
+       private $maxFuzzTestLength = 300;
+       private $fuzzSeed = 0;
+       private $memoryLimit = 50;
+
        /**
         * Sets terminal colorization and diff/quick modes depending on OS and
         * command-line options (--color and --quick).
@@ -117,6 +121,10 @@ class ParserTest {
                }
                $this->keepUploads = isset( $options['keep-uploads'] );
 
+               if ( isset( $options['seed'] ) ) {
+                       $this->fuzzSeed = intval( $options['seed'] ) - 1;
+               }
+
                $this->hooks = array();
                $this->functionHooks = array();
        }
@@ -133,6 +141,119 @@ class ParserTest {
                }
        }
 
+       /**
+        * Run a fuzz test series
+        * Draw input from a set of test files
+        */
+       function fuzzTest( $filenames ) {
+               $dict = $this->getFuzzInput( $filenames );
+               $dictSize = strlen( $dict );
+               $logMaxLength = log( $this->maxFuzzTestLength );
+               $this->setupDatabase();
+               ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
+
+               $numTotal = 0;
+               $numSuccess = 0;
+               $user = new User;
+               $opts = ParserOptions::newFromUser( $user );
+               $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
+
+               while ( true ) {
+                       // Generate test input
+                       mt_srand( ++$this->fuzzSeed );
+                       $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
+                       $input = '';
+                       while ( strlen( $input ) < $totalLength ) {
+                               $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
+                               $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
+                               $offset = mt_rand( 0, $dictSize - $hairLength );
+                               $input .= substr( $dict, $offset, $hairLength );
+                       }
+
+                       $this->setupGlobals();
+                       $parser = $this->getParser();
+                       // Run the test
+                       try {
+                               $parser->parse( $input, $title, $opts );
+                               $fail = false;
+                       } catch ( Exception $exception ) {
+                               $fail = true;
+                       }
+
+                       if ( $fail ) {
+                               echo "Test failed with seed {$this->fuzzSeed}\n";
+                               echo "Input:\n";
+                               var_dump( $input );
+                               echo "\n\n";
+                               echo "$exception\n";
+                       } else {
+                               $numSuccess++;
+                       }
+                       $numTotal++;
+                       $this->teardownGlobals();
+                       $parser->__destruct();
+
+                       if ( $numTotal % 100 == 0 ) {
+                               $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
+                               echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
+                               if ( $usage > 90 ) {
+                                       echo "Out of memory:\n";
+                                       $memStats = $this->getMemoryBreakdown();
+                                       foreach ( $memStats as $name => $usage ) {
+                                               echo "$name: $usage\n";
+                                       }
+                                       $this->abort();
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Get an input dictionary from a set of parser test files
+        */
+       function getFuzzInput( $filenames ) {
+               $dict = '';
+               foreach( $filenames as $filename ) {
+                       $contents = file_get_contents( $filename );
+                       preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
+                       foreach ( $matches[1] as $match ) {
+                               $dict .= $match . "\n";
+                       }
+               }
+               return $dict;
+       }
+
+       /**
+        * Get a memory usage breakdown
+        */
+       function getMemoryBreakdown() {
+               $memStats = array();
+               foreach ( $GLOBALS as $name => $value ) {
+                       $memStats['$'.$name] = strlen( serialize( $value ) );
+               }
+               $classes = get_declared_classes();
+               foreach ( $classes as $class ) {
+                       $rc = new ReflectionClass( $class );
+                       $props = $rc->getStaticProperties();
+                       $memStats[$class] = strlen( serialize( $props ) );
+                       $methods = $rc->getMethods();
+                       foreach ( $methods as $method ) {
+                               $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
+                       }
+               }
+               $functions = get_defined_functions();
+               foreach ( $functions['user'] as $function ) {
+                       $rf = new ReflectionFunction( $function );
+                       $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
+               }
+               asort( $memStats );
+               return $memStats;
+       }
+
+       function abort() {
+               $this->abort();
+       }
+
        /**
         * Run a series of tests listed in the given text files.
         * Each test consists of a brief description, wikitext input,
@@ -266,6 +387,24 @@ class ParserTest {
                return $ok;
        }
 
+       /**
+        * Get a Parser object
+        */
+       function getParser() {
+               global $wgParserConf;
+               $class = $wgParserConf['class'];
+               $parser = new $class( $wgParserConf );
+               foreach( $this->hooks as $tag => $callback ) {
+                       $parser->setHook( $tag, $callback );
+               }
+               foreach( $this->functionHooks as $tag => $bits ) {
+                       list( $callback, $flags ) = $bits;
+                       $parser->setFunctionHook( $tag, $callback, $flags );
+               }
+               wfRunHooks( 'ParserTestParser', array( &$parser ) );
+               return $parser;
+       }
+
        /**
         * Run a given wikitext input through a freshly-constructed wiki parser,
         * and compare the output against the expected results.
@@ -276,7 +415,6 @@ class ParserTest {
         * @return bool
         */
        private function runTest( $desc, $input, $result, $opts ) {
-               global $wgParserConf;
                if( $this->showProgress ) {
                        $this->showTesting( $desc );
                }
@@ -300,18 +438,7 @@ class ParserTest {
                }
 
                $noxml = (bool)preg_match( '~\\b noxml \\b~x', $opts );
-
-               $class = $wgParserConf['class'];
-               $parser = new $class( $wgParserConf );
-               foreach( $this->hooks as $tag => $callback ) {
-                       $parser->setHook( $tag, $callback );
-               }
-               foreach( $this->functionHooks as $tag => $bits ) {
-                       list( $callback, $flags ) = $bits;
-                       $parser->setFunctionHook( $tag, $callback, $flags );
-               }
-               wfRunHooks( 'ParserTestParser', array( &$parser ) );
-
+               $parser = $this->getParser();
                $title =& Title::makeTitle( NS_MAIN, $titleText );
 
                $matches = array();
@@ -387,6 +514,8 @@ class ParserTest {
                        self::getOptionValue( '/variant=([a-z]+(?:-[a-z]+)?)/', $opts, false );
                $maxtoclevel =
                        self::getOptionValue( '/wgMaxTocLevel=(\d+)/', $opts, 999 );
+               $linkHolderBatchSize = 
+                       self::getOptionValue( '/wgLinkHolderBatchSize=(\d+)/', $opts, 1000 );
 
                $settings = array(
                        'wgServer' => 'http://localhost',
@@ -432,6 +561,7 @@ class ParserTest {
                        ) ),
                        'wgDefaultExternalStore' => array(),
                        'wgForeignFileRepos' => array(),
+                       'wgLinkHolderBatchSize' => $linkHolderBatchSize,
                        );
                $this->savedGlobals = array();
                foreach( $settings as $var => $val ) {
@@ -441,6 +571,7 @@ class ParserTest {
                $langObj = Language::factory( $lang );
                $GLOBALS['wgLang'] = $langObj;
                $GLOBALS['wgContLang'] = $langObj;
+               $GLOBALS['wgMemc'] = new FakeMemCachedClient;
 
                //$GLOBALS['wgMessageCache'] = new MessageCache( new BagOStuff(), false, 0, $GLOBALS['wgDBname'] );
 
@@ -551,10 +682,10 @@ class ParserTest {
                # Hack: insert a few Wikipedia in-project interwiki prefixes,
                # for testing inter-language links
                $db->insert( 'interwiki', array(
-                       array( 'iw_prefix' => 'Wikipedia',
+                       array( 'iw_prefix' => 'wikipedia',
                                   'iw_url'    => 'http://en.wikipedia.org/wiki/$1',
                                   'iw_local'  => 0 ),
-                       array( 'iw_prefix' => 'MeatBall',
+                       array( 'iw_prefix' => 'meatball',
                                   'iw_url'    => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
                                   'iw_local'  => 0 ),
                        array( 'iw_prefix' => 'zh',
@@ -621,11 +752,12 @@ class ParserTest {
                        return;
                }
 
+               /*
                $tables = $this->listTables();
                $db = wfGetDB( DB_MASTER );
                foreach ( $tables as $table ) {
                        $db->query( "DROP TABLE `parsertest_$table`" );
-               }
+               }*/
        }
        
        /**
@@ -645,6 +777,10 @@ class ParserTest {
                }
 
                wfDebug( "Creating upload directory $dir\n" );
+               if ( file_exists( $dir ) ) {
+                       wfDebug( "Already exists!\n" );
+                       return $dir;
+               }
                mkdir( $dir );
                mkdir( $dir . '/3' );
                mkdir( $dir . '/3/3a' );
@@ -658,6 +794,8 @@ class ParserTest {
         */
        private function teardownGlobals() {
                RepoGroup::destroySingleton();
+               LinkCache::singleton()->clear();
+               $GLOBALS['wgLang']->__destruct();
                foreach( $this->savedGlobals as $var => $val ) {
                        $GLOBALS[$var] = $val;
                }
index 192eeaa..0d50feb 100644 (file)
@@ -28,22 +28,21 @@ require('parserTests.inc');
 if( isset( $options['help'] ) ) {
     echo <<<ENDS
 MediaWiki $wgVersion parser test suite
-Usage: php parserTests.php [--quick] [--quiet] [--show-output]
-                           [--color[=(yes|no)]]
-                           [--regex=<expression>] [--file=<testfile>]
-                           [--record] [--compare]
-                           [--help]
+Usage: php parserTests.php [options...]
+
 Options:
   --quick          Suppress diff output of failed tests
   --quiet          Suppress notification of passed tests (shows only failed tests)
   --show-output    Show expected and actual output
-  --color          Override terminal detection and force color output on or off
+  --color[=yes|no] Override terminal detection and force color output on or off
                    use wgCommandLineDarkBg = true; if your term is dark 
   --regex          Only run tests whose descriptions which match given regex
-  --file           Run test cases from a custom file instead of parserTests.txt
+  --file=<testfile> Run test cases from a custom file instead of parserTests.txt
   --record         Record tests in database
   --compare        Compare with recorded results, without updating the database.
   --keep-uploads   Re-use the same upload directory for each test, don't delete it
+  --fuzz           Do a fuzz test instead of a normal test
+  --seed <n>       Start the fuzz test from the specified seed
   --help           Show this help message
 
 
@@ -67,7 +66,10 @@ if( isset( $options['file'] ) ) {
 # Print out software version to assist with locating regressions
 $version = SpecialVersion::getVersion();
 echo( "This is MediaWiki version {$version}.\n\n" );
-$ok = $tester->runTestsFromFiles( $files );
-
-exit ($ok ? 0 : -1);
 
+if ( isset( $options['fuzz'] ) ) {
+       $tester->fuzzTest( $files );
+} else {
+       $ok = $tester->runTestsFromFiles( $files );
+       exit ($ok ? 0 : -1);
+}
index edd3ea9..43463c0 100644 (file)
@@ -7066,6 +7066,29 @@ Line two
 
 !! end
 
+!! test
+Interwiki links trounced by replaceExternalLinks after early LinkHolderArray expansion
+!! options
+wgLinkHolderBatchSize=0
+!! input
+[[meatball:1]]
+[[meatball:2]]
+[[meatball:3]]
+!! result
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?1" class="extiw" title="meatball:1">meatball:1</a>
+<a href="http://www.usemod.com/cgi-bin/mb.pl?2" class="extiw" title="meatball:2">meatball:2</a>
+<a href="http://www.usemod.com/cgi-bin/mb.pl?3" class="extiw" title="meatball:3">meatball:3</a>
+</p>
+!! end
+
+!! test
+Free external link invading image caption
+!! input
+[[Image:Foobar.jpg|thumb|http://x|hello]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="hello"><img alt="hello" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>hello</div></div></div>
+
+!! end
 
 #
 #
index 5a98a89..98c4bba 100644 (file)
@@ -21,24 +21,27 @@ function wfParserTestStaticParserHookSetup( &$parser ) {
        return true;
 }
 
-function wfParserTestStaticParserHookHook( $in, $argv ) {
-       static $buf = null;
-       
+function wfParserTestStaticParserHookHook( $in, $argv, $parser ) {
        if ( ! count( $argv ) ) {
-               $buf = $in;
+               $parser->static_tag_buf = $in;
                return '';
-       } else if ( count( $argv ) === 1 && $argv['action'] === 'flush' && $in === null ) {
+       } else if ( count( $argv ) === 1 && isset( $argv['action'] ) 
+               && $argv['action'] === 'flush' && $in === null ) 
+       {
                // Clear the buffer, we probably don't need to
-               $tmp = $buf;
-               $buf = null;
+               if ( isset( $parser->static_tag_buf ) ) {
+                       $tmp = $parser->static_tag_buf;
+               } else {
+                       $tmp = '';
+               }
+               $parser->static_tag_buf = null;
                return $tmp;
        } else
                // wtf?
-               die(
+               return
                        "\nCall this extension as <statictag>string</statictag> or as" .
                        " <statictag action=flush/>, not in any other way.\n" .
                        "text: " . var_export( $in, true ) . "\n" .
-                       "argv: " . var_export( $argv, true ) . "\n"
-               );
+                       "argv: " . var_export( $argv, true ) . "\n";
 }