Reverted r84357 and r90461 and fixed the bug in a better way: in an empty document...
[lhc/web/wiklou.git] / includes / parser / Parser.php
index 5676b41..6ff1e44 100644 (file)
@@ -53,7 +53,7 @@ class Parser {
         * changes in an incompatible way, so the parser cache
         * can automatically discard old data.
         */
-       const VERSION = '1.6.5';
+       const VERSION = '1.6.4';
 
        /**
         * Update this version number when the output of serialiseHalfParsedText()
@@ -68,9 +68,11 @@ class Parser {
 
        # Constants needed for external link processing
        # Everything except bracket, space, or control characters
-       const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
-       const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
-               \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
+       # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
+       # as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
+       const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
+       const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
+               \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
 
        # State constants for the definition list colon extraction
        const COLON_STATE_TEXT = 0;
@@ -146,6 +148,7 @@ class Parser {
        var $mTplExpandCache; # empty-frame expansion cache
        var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
        var $mExpensiveFunctionCount; # number of expensive parser function calls
+       var $mShowToc, $mForceTocPosition;
 
        /**
         * @var User
@@ -169,7 +172,7 @@ class Parser {
        var $mRevisionObject; # The revision object of the specified revision ID
        var $mRevisionId;   # ID to display in {{REVISIONID}} tags
        var $mRevisionTimestamp; # The timestamp of the specified revision ID
-       var $mRevisionUser; # Userto display in {{REVISIONUSER}} tag
+       var $mRevisionUser; # User to display in {{REVISIONUSER}} tag
        var $mRevIdForTs;   # The revision ID which was used to fetch the timestamp
 
        /**
@@ -179,12 +182,14 @@ class Parser {
 
        /**
         * Constructor
+        *
+        * @param $conf array
         */
        public function __construct( $conf = array() ) {
                $this->mConf = $conf;
                $this->mUrlProtocols = wfUrlProtocols();
-               $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
-                       '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/S';
+               $this->mExtLinkBracketedRegex = '/\[((' . wfUrlProtocols() . ')'.
+                       self::EXT_LINK_URL_CLASS.'+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
                if ( isset( $conf['preprocessorClass'] ) ) {
                        $this->mPreprocessorClass = $conf['preprocessorClass'];
                } elseif ( defined( 'MW_COMPILED' ) ) {
@@ -345,7 +350,7 @@ class Parser {
                $fixtags = array(
                        # french spaces, last one Guillemet-left
                        # only if there is something before the space
-                       '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;\\2',
+                       '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
                        # french spaces, Guillemet-right
                        '/(\\302\\253) /' => '\\1&#160;',
                        '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
@@ -357,14 +362,16 @@ class Parser {
                $this->replaceLinkHolders( $text );
 
                /**
-                * The page doesn't get language converted if
+                * The input doesn't get language converted if
                 * a) It's disabled
                 * b) Content isn't converted
                 * c) It's a conversion table
+                * d) it is an interface message (which is in the user language)
                 */
                if ( !( $wgDisableLangConversion
                                || isset( $this->mDoubleUnderscores['nocontentconvert'] )
-                               || $this->mTitle->isConversionTable() ) ) {
+                               || $this->mTitle->isConversionTable()
+                               || $this->mOptions->getInterfaceMessage() ) ) {
 
                        # The position of the convert() call should not be changed. it
                        # assumes that the links are all replaced and the only thing left
@@ -473,6 +480,8 @@ class Parser {
         *
         * @param $text String: text extension wants to have parsed
         * @param $frame PPFrame: The frame to use for expanding any template variables
+        *
+        * @return string
         */
        function recursiveTagParse( $text, $frame=false ) {
                wfProfileIn( __METHOD__ );
@@ -501,6 +510,22 @@ class Parser {
                return $text;
        }
 
+       /**
+        * Recursive parser entry point that can be called from an extension tag
+        * hook.
+        *
+        * @param $text String: text to be expanded
+        * @param $frame PPFrame: The frame to use for expanding any template variables
+        * @return String
+        */
+       public function recursivePreprocess( $text, $frame = false ) {
+               wfProfileIn( __METHOD__ );
+               $text = $this->replaceVariables( $text, $frame );
+               $text = $this->mStripState->unstripBoth( $text );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
        /**
         * Process the wikitext for the ?preload= feature. (bug 5210)
         *
@@ -665,13 +690,13 @@ class Parser {
         * @return Language
         */
        function getFunctionLang() {
-               global $wgLang, $wgContLang;
+               global $wgLang;
 
                $target = $this->mOptions->getTargetLanguage();
                if ( $target !== null ) {
                        return $target;
                } else {
-                       return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
+                       return $this->mOptions->getInterfaceMessage() ? $wgLang : $this->mTitle->getPageLanguage();
                }
        }
 
@@ -793,6 +818,10 @@ class Parser {
         * Add an item to the strip state
         * Returns the unique tag which must be inserted into the stripped text
         * The tag will be replaced with the original text in unstrip()
+        *
+        * @param $text string
+        *
+        * @return string
         */
        function insertStripItem( $text ) {
                $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
@@ -805,6 +834,10 @@ class Parser {
         * parse the wiki syntax used to render tables
         *
         * @private
+        *
+        * @param $text string
+        *
+        * @return string
         */
        function doTableStuff( $text ) {
                wfProfileIn( __METHOD__ );
@@ -841,11 +874,11 @@ class Parser {
                                if ( $attributes !== '' ) {
                                        $table['attributes'] = $attributes;
                                }
-                       } else if ( !isset( $tables[0] ) ) {
+                       } elseif ( !isset( $tables[0] ) ) {
                                // we're outside the table
 
                                $out .= $outLine . "\n";
-                       } else if ( $firstChars === '|}' ) {
+                       } elseif ( $firstChars === '|}' ) {
                                // trim the |} code from the line
                                $line = substr ( $line , 2 );
 
@@ -886,7 +919,7 @@ class Parser {
 
                                $output .= $o;
 
-                       } else if ( $firstChars === '|-' ) {
+                       } elseif ( $firstChars === '|-' ) {
                                // start a new row element
                                // but only when we haven't started one already
                                if ( count( $currentRow ) != 0 ) {
@@ -901,7 +934,7 @@ class Parser {
                                        $currentRow['attributes'] = $attributes;
                                }
 
-                       } else if ( $firstChars  === '|+' ) {
+                       } elseif ( $firstChars  === '|+' ) {
                                // a table caption, but only proceed if there isn't one already
                                if ( !isset ( $table['caption'] ) ) {
                                        $line = substr ( $line , 2 );
@@ -913,7 +946,7 @@ class Parser {
                                        unset( $c );
                                        $output =& $table['caption']['content'];
                                }
-                       } else if ( $firstChars === '|' || $firstChars === '!' || $firstChars === '!+' ) {
+                       } elseif ( $firstChars === '|' || $firstChars === '!' || $firstChars === '!+' ) {
                                // Which kind of cells are we dealing with
                                $currentTag = 'td';
                                $line = substr ( $line , 1 );
@@ -930,7 +963,7 @@ class Parser {
                                // decide whether thead to tbody
                                if ( !array_key_exists( 'type', $currentRow ) ) {
                                        $currentRow['type'] = ( $firstChars === '!' ) ? 'thead' : 'tbody' ;
-                               } else if ( $firstChars === '|' ) {
+                               } elseif ( $firstChars === '|' ) {
                                        $currentRow['type'] = 'tbody';
                                }
 
@@ -1000,7 +1033,7 @@ class Parser {
                if ( strpos( $cellData[0], '[[' ) !== false ) {
                        $content = trim ( $cell );
                }
-               else if ( count ( $cellData ) == 1 ) {
+               elseif ( count ( $cellData ) == 1 ) {
                        $content = trim ( $cellData[0] );
                } else {
                        $attributes = $this->mStripState->unstripBoth( $cellData[0] );
@@ -1016,10 +1049,13 @@ class Parser {
         * Helper function for doTableStuff(). This converts the structured array into html.
         *
         * @private
+        *
+        * @param $table array
+        *
+        * @return string
         */
-       function generateTableHTML ( &$table ) {
-               $return = "";
-               $return .= str_repeat( '<dl><dd>' , $table['indent'] );
+       function generateTableHTML( &$table ) {
+               $return = str_repeat( '<dl><dd>' , $table['indent'] );
                $return .= '<table';
                $return .= isset( $table['attributes'] ) ? $table['attributes'] : '';
                $return .= '>';
@@ -1039,22 +1075,20 @@ class Parser {
                // If we only have tbodies, mark table as simple
                for ( $i = 0; isset( $table[$i] ); $i++ ) {
                        if ( !count( $table[$i] ) ) continue;
-                       if ( !isset( $table[$i]['type'] ) ) $table[$i]['type'] = 'tbody';
+                       if ( !isset( $table[$i]['type'] ) ) {
+                               $table[$i]['type'] = 'tbody';
+                       }
                        if ( !$lastSection ) {
                                $lastSection = $table[$i]['type'];
-                       } else if ( $lastSection != $table[$i]['type'] ) {
+                       } elseif ( $lastSection != $table[$i]['type'] ) {
                                $simple = false;
-                               break;
                        }
                }
                $lastSection = '';
                for ( $i = 0; isset( $table[$i] ); $i++ ) {
-                       // Check for empty tables
-                       if ( count( $table[$i] ) ) {
-                               $empty = false;
-                       } else {
-                               continue;
-                       }
+                       if ( !count( $table[$i] ) ) continue;
+                       $empty = false; // check for empty tables
+
                        if ( $table[$i]['type'] != $lastSection && !$simple ) {
                                $return .= "\n<" . $table[$i]['type'] . '>';
                        }
@@ -1103,6 +1137,8 @@ class Parser {
         * no numric elements and an array itself if not previously defined.
         *
         * @private
+        *
+        * @param $arr array
         */
        function &last ( &$arr ) {
                for ( $i = count( $arr ); ( !isset( $arr[$i] ) && $i > 0 ); $i-- ) {  }
@@ -1114,8 +1150,14 @@ class Parser {
         * HTML. Only called for $mOutputType == self::OT_HTML.
         *
         * @private
+        *
+        * @param $text string
+        * @param $isMain bool
+        * @param $frame bool
+        *
+        * @return string
         */
-       function internalParse( $text, $isMain = true, $frame=false ) {
+       function internalParse( $text, $isMain = true, $frame = false ) {
                wfProfileIn( __METHOD__ );
 
                $origText = $text;
@@ -1181,10 +1223,14 @@ class Parser {
         *
         * DML
         * @private
+        *
+        * @param $text string
+        *
+        * @return string
         */
        function doMagicLinks( $text ) {
                wfProfileIn( __METHOD__ );
-               $prots = $this->mUrlProtocols;
+               $prots = wfUrlProtocolsWithoutProtRel();
                $urlChar = self::EXT_LINK_URL_CLASS;
                $text = preg_replace_callback(
                        '!(?:                           # Start cases
@@ -1197,7 +1243,7 @@ class Parser {
                                        (?: [0-9]  [\ \-]? ){9} # 9 digits with opt. delimiters
                                        [0-9Xx]                 # check digit
                                        \b)
-                       )!x', array( &$this, 'magicLinkCallback' ), $text );
+                       )!xu', array( &$this, 'magicLinkCallback' ), $text );
                wfProfileOut( __METHOD__ );
                return $text;
        }
@@ -1254,7 +1300,10 @@ class Parser {
 
        /**
         * Make a free external link, given a user-supplied URL
-        * @return HTML
+        *
+        * @param $url string
+        *
+        * @return string HTML
         * @private
         */
        function makeFreeExternalLink( $url ) {
@@ -1307,6 +1356,10 @@ class Parser {
         * Parse headers and return html
         *
         * @private
+        *
+        * @param $text string
+        *
+        * @return string
         */
        function doHeadings( $text ) {
                wfProfileIn( __METHOD__ );
@@ -1322,6 +1375,9 @@ class Parser {
        /**
         * Replace single quotes with HTML markup
         * @private
+        *
+        * @param $text string
+        *
         * @return string the altered text
         */
        function doAllQuotes( $text ) {
@@ -1338,6 +1394,10 @@ class Parser {
 
        /**
         * Helper function for doAllQuotes()
+        *
+        * @param $text string
+        *
+        * @return string
         */
        public function doQuotes( $text ) {
                $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
@@ -1502,6 +1562,10 @@ class Parser {
         * Make sure to run maintenance/parserTests.php if you change this code.
         *
         * @private
+        *
+        * @param $text string
+        *
+        * @return string
         */
        function replaceExternalLinks( $text ) {
                global $wgContLang;
@@ -1540,16 +1604,10 @@ class Parser {
 
                        # No link text, e.g. [http://domain.tld/some.link]
                        if ( $text == '' ) {
-                               # Autonumber if allowed. See bug #5918
-                               if ( strpos( wfUrlProtocols(), substr( $protocol, 0, strpos( $protocol, ':' ) ) ) !== false ) {
-                                       $langObj = $this->getFunctionLang();
-                                       $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
-                                       $linktype = 'autonumber';
-                               } else {
-                                       # Otherwise just use the URL
-                                       $text = htmlspecialchars( $url );
-                                       $linktype = 'free';
-                               }
+                               # Autonumber
+                               $langObj = $this->getFunctionLang();
+                               $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
+                               $linktype = 'autonumber';
                        } else {
                                # Have link text, e.g. [http://domain.tld/some.link text]s
                                # Check for trail
@@ -1584,29 +1642,18 @@ class Parser {
         * (depending on configuration, namespace, and the URL's domain) and/or a
         * target attribute (depending on configuration).
         *
-        * @param $url String: optional URL, to extract the domain from for rel =>
+        * @param $url String|bool optional URL, to extract the domain from for rel =>
         *   nofollow if appropriate
-        * @return Array: associative array of HTML attributes
+        * @return Array associative array of HTML attributes
         */
        function getExternalLinkAttribs( $url = false ) {
                $attribs = array();
-               global $wgNoFollowLinks, $wgNoFollowNsExceptions;
+               global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
                $ns = $this->mTitle->getNamespace();
-               if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) ) {
+               if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) &&
+                               !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions ) )
+               {
                        $attribs['rel'] = 'nofollow';
-
-                       global $wgNoFollowDomainExceptions;
-                       if ( $wgNoFollowDomainExceptions ) {
-                               $bits = wfParseUrl( $url );
-                               if ( is_array( $bits ) && isset( $bits['host'] ) ) {
-                                       foreach ( $wgNoFollowDomainExceptions as $domain ) {
-                                               if ( substr( $bits['host'], -strlen( $domain ) ) == $domain ) {
-                                                       unset( $attribs['rel'] );
-                                                       break;
-                                               }
-                                       }
-                               }
-                       }
                }
                if ( $this->mOptions->getExternalLinkTarget() ) {
                        $attribs['target'] = $this->mOptions->getExternalLinkTarget();
@@ -1633,6 +1680,10 @@ class Parser {
        /**
         * Callback function used in replaceUnusualEscapes().
         * Replaces unusual URL escape codes with their equivalent character
+        *
+        * @param $matches array
+        *
+        * @return string
         */
        private static function replaceUnusualEscapesCallback( $matches ) {
                $char = urldecode( $matches[0] );
@@ -1651,6 +1702,10 @@ class Parser {
         * make an image if it's allowed, either through the global
         * option, through the exception, or through the on-wiki whitelist
         * @private
+        *
+        * $param $url string
+        *
+        * @return string
         */
        function maybeMakeExternalImage( $url ) {
                $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
@@ -1697,6 +1752,9 @@ class Parser {
 
        /**
         * Process [[ ]] wikilinks
+        *
+        * @param $s string
+        *
         * @return String: processed text
         *
         * @private
@@ -1843,7 +1901,7 @@ class Parser {
                        # 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] ) ) {
+                       if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $m[1] ) ) {
                                $s .= $prefix . '[[' . $line ;
                                wfProfileOut( __METHOD__."-misc" );
                                continue;
@@ -2125,6 +2183,11 @@ class Parser {
         * getCommon() returns the length of the longest common substring
         * of both arguments, starting at the beginning of both.
         * @private
+        *
+        * @param $st1 string
+        * @param $st2 string
+        *
+        * @return int
         */
        function getCommon( $st1, $st2 ) {
                $fl = strlen( $st1 );
@@ -2146,6 +2209,8 @@ class Parser {
         * element appropriate to the prefix character passed into them.
         * @private
         *
+        * @param $char char
+        *
         * @return string
         */
        function openList( $char ) {
@@ -2409,10 +2474,10 @@ class Parser {
         * Split up a string on ':', ignoring any occurences inside tags
         * to prevent illegal overlapping.
         *
-        * @param $str String: the string to split
-        * @param &$before String: set to everything before the ':'
-        * @param &$after String: set to everything after the ':'
-        * return String: the position of the ':', or false if none found
+        * @param $str String the string to split
+        * @param &$before String set to everything before the ':'
+        * @param &$after String set to everything after the ':'
+        * @return String the position of the ':', or false if none found
         */
        function findColonNoLinks( $str, &$before, &$after ) {
                wfProfileIn( __METHOD__ );
@@ -2577,11 +2642,22 @@ class Parser {
         *
         * @param $index integer
         * @param $frame PPFrame
+        *
+        * @return string
         */
-       function getVariableValue( $index, $frame=false ) {
+       function getVariableValue( $index, $frame = false ) {
                global $wgContLang, $wgSitename, $wgServer;
                global $wgArticlePath, $wgScriptPath, $wgStylePath;
 
+               if ( is_null( $this->mTitle ) ) {
+                       // If no title set, bad things are going to happen
+                       // later. Title should always be set since this
+                       // should only be called in the middle of a parse
+                       // operation (but the unit-tests do funky stuff)
+                       throw new MWException( __METHOD__ . ' Should only be '
+                               . ' called while parsing (no title set)' );
+               }
+
                /**
                 * Some of these require message or data lookups and can be
                 * expensive to check many times.
@@ -2616,48 +2692,50 @@ class Parser {
                        date_default_timezone_set( $oldtz );
                }
 
+               $pageLang = $this->getFunctionLang();
+
                switch ( $index ) {
                        case 'currentmonth':
-                               $value = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+                               $value = $pageLang->formatNum( gmdate( 'm', $ts ) );
                                break;
                        case 'currentmonth1':
-                               $value = $wgContLang->formatNum( gmdate( 'n', $ts ) );
+                               $value = $pageLang->formatNum( gmdate( 'n', $ts ) );
                                break;
                        case 'currentmonthname':
-                               $value = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
+                               $value = $pageLang->getMonthName( gmdate( 'n', $ts ) );
                                break;
                        case 'currentmonthnamegen':
-                               $value = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
+                               $value = $pageLang->getMonthNameGen( gmdate( 'n', $ts ) );
                                break;
                        case 'currentmonthabbrev':
-                               $value = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+                               $value = $pageLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
                                break;
                        case 'currentday':
-                               $value = $wgContLang->formatNum( gmdate( 'j', $ts ) );
+                               $value = $pageLang->formatNum( gmdate( 'j', $ts ) );
                                break;
                        case 'currentday2':
-                               $value = $wgContLang->formatNum( gmdate( 'd', $ts ) );
+                               $value = $pageLang->formatNum( gmdate( 'd', $ts ) );
                                break;
                        case 'localmonth':
-                               $value = $wgContLang->formatNum( $localMonth );
+                               $value = $pageLang->formatNum( $localMonth );
                                break;
                        case 'localmonth1':
-                               $value = $wgContLang->formatNum( $localMonth1 );
+                               $value = $pageLang->formatNum( $localMonth1 );
                                break;
                        case 'localmonthname':
-                               $value = $wgContLang->getMonthName( $localMonthName );
+                               $value = $pageLang->getMonthName( $localMonthName );
                                break;
                        case 'localmonthnamegen':
-                               $value = $wgContLang->getMonthNameGen( $localMonthName );
+                               $value = $pageLang->getMonthNameGen( $localMonthName );
                                break;
                        case 'localmonthabbrev':
-                               $value = $wgContLang->getMonthAbbreviation( $localMonthName );
+                               $value = $pageLang->getMonthAbbreviation( $localMonthName );
                                break;
                        case 'localday':
-                               $value = $wgContLang->formatNum( $localDay );
+                               $value = $pageLang->formatNum( $localDay );
                                break;
                        case 'localday2':
-                               $value = $wgContLang->formatNum( $localDay2 );
+                               $value = $pageLang->formatNum( $localDay2 );
                                break;
                        case 'pagename':
                                $value = wfEscapeWikiText( $this->mTitle->getText() );
@@ -2782,68 +2860,68 @@ class Parser {
                                $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
                                break;
                        case 'currentdayname':
-                               $value = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+                               $value = $pageLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
                                break;
                        case 'currentyear':
-                               $value = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
+                               $value = $pageLang->formatNum( gmdate( 'Y', $ts ), true );
                                break;
                        case 'currenttime':
-                               $value = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+                               $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
                                break;
                        case 'currenthour':
-                               $value = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
+                               $value = $pageLang->formatNum( gmdate( 'H', $ts ), true );
                                break;
                        case 'currentweek':
                                # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
                                # int to remove the padding
-                               $value = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
+                               $value = $pageLang->formatNum( (int)gmdate( 'W', $ts ) );
                                break;
                        case 'currentdow':
-                               $value = $wgContLang->formatNum( gmdate( 'w', $ts ) );
+                               $value = $pageLang->formatNum( gmdate( 'w', $ts ) );
                                break;
                        case 'localdayname':
-                               $value = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
+                               $value = $pageLang->getWeekdayName( $localDayOfWeek + 1 );
                                break;
                        case 'localyear':
-                               $value = $wgContLang->formatNum( $localYear, true );
+                               $value = $pageLang->formatNum( $localYear, true );
                                break;
                        case 'localtime':
-                               $value = $wgContLang->time( $localTimestamp, false, false );
+                               $value = $pageLang->time( $localTimestamp, false, false );
                                break;
                        case 'localhour':
-                               $value = $wgContLang->formatNum( $localHour, true );
+                               $value = $pageLang->formatNum( $localHour, true );
                                break;
                        case 'localweek':
                                # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
                                # int to remove the padding
-                               $value = $wgContLang->formatNum( (int)$localWeek );
+                               $value = $pageLang->formatNum( (int)$localWeek );
                                break;
                        case 'localdow':
-                               $value = $wgContLang->formatNum( $localDayOfWeek );
+                               $value = $pageLang->formatNum( $localDayOfWeek );
                                break;
                        case 'numberofarticles':
-                               $value = $wgContLang->formatNum( SiteStats::articles() );
+                               $value = $pageLang->formatNum( SiteStats::articles() );
                                break;
                        case 'numberoffiles':
-                               $value = $wgContLang->formatNum( SiteStats::images() );
+                               $value = $pageLang->formatNum( SiteStats::images() );
                                break;
                        case 'numberofusers':
-                               $value = $wgContLang->formatNum( SiteStats::users() );
+                               $value = $pageLang->formatNum( SiteStats::users() );
                                break;
                        case 'numberofactiveusers':
-                               $value = $wgContLang->formatNum( SiteStats::activeUsers() );
+                               $value = $pageLang->formatNum( SiteStats::activeUsers() );
                                break;
                        case 'numberofpages':
-                               $value = $wgContLang->formatNum( SiteStats::pages() );
+                               $value = $pageLang->formatNum( SiteStats::pages() );
                                break;
                        case 'numberofadmins':
-                               $value = $wgContLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
+                               $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
                                break;
                        case 'numberofedits':
-                               $value = $wgContLang->formatNum( SiteStats::edits() );
+                               $value = $pageLang->formatNum( SiteStats::edits() );
                                break;
                        case 'numberofviews':
-                               $value = $wgContLang->formatNum( SiteStats::views() );
+                               $value = $pageLang->formatNum( SiteStats::views() );
                                break;
                        case 'currenttimestamp':
                                $value = wfTimestamp( TS_MW, $ts );
@@ -2870,7 +2948,7 @@ class Parser {
                        case 'stylepath':
                                return $wgStylePath;
                        case 'directionmark':
-                               return $wgContLang->getDirMark();
+                               return $pageLang->getDirMark();
                        case 'contentlanguage':
                                global $wgLanguageCode;
                                return $wgLanguageCode;
@@ -2883,8 +2961,9 @@ class Parser {
                                }
                }
 
-               if ( $index )
+               if ( $index ) {
                        $this->mVarCache[$index] = $value;
+               }
 
                return $value;
        }
@@ -2936,6 +3015,8 @@ class Parser {
        /**
         * Return a three-element array: leading whitespace, string contents, trailing whitespace
         *
+        * @param $s string
+        *
         * @return array
         */
        public static function splitWhitespace( $s ) {
@@ -2961,11 +3042,11 @@ class Parser {
         *  self::OT_PREPROCESS: templates but not extension tags
         *  self::OT_HTML: all templates and extension tags
         *
-        * @param $text String: the text to transform
+        * @param $text String the text to transform
         * @param $frame PPFrame Object describing the arguments passed to the template.
         *        Arguments may also be provided as an associative array, as was the usual case before MW1.12.
         *        Providing arguments this way may be useful for extensions wishing to perform variable replacement explicitly.
-        * @param $argsOnly Boolean: only do argument (triple-brace) expansion, not double-brace expansion
+        * @param $argsOnly Boolean only do argument (triple-brace) expansion, not double-brace expansion
         * @private
         *
         * @return string
@@ -2995,6 +3076,8 @@ class Parser {
        /**
         * Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
         *
+        * @param $args array
+        *
         * @return array
         */
        static function createAssocArgs( $args ) {
@@ -3057,7 +3140,7 @@ class Parser {
         * @private
         */
        function braceSubstitution( $piece, $frame ) {
-               global $wgContLang, $wgNonincludableNamespaces;
+               global $wgContLang, $wgNonincludableNamespaces, $wgEnableInterwikiTranscluding, $wgEnableInterwikiTemplatesTracking;
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__.'-setup' );
 
@@ -3065,7 +3148,6 @@ class Parser {
                $found = false;             # $text has been filled
                $nowiki = false;            # wiki markup in $text should be escaped
                $isHTML = false;            # $text is HTML, armour it against wikitext transformation
-               $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
                $isChildObj = false;        # $text is a DOM node needing expansion in a child frame
                $isLocalObj = false;        # $text is a DOM node needing expansion in the current frame
 
@@ -3086,7 +3168,7 @@ class Parser {
                $args = ( null == $piece['parts'] ) ? array() : $piece['parts'];
                wfProfileOut( __METHOD__.'-setup' );
                wfProfileIn( __METHOD__."-title-$originalTitle" );
-               
+
                # SUBST
                wfProfileIn( __METHOD__.'-modifiers' );
                if ( !$found ) {
@@ -3230,6 +3312,9 @@ class Parser {
                        }
                        $title = Title::newFromText( $part1, $ns );
                        if ( $title ) {
+                               if ( !$title->isExternal() && $piece['interwiki'] !== '' ) {
+                                       $title->setInterwiki( $piece['interwiki'] );
+                               }
                                $titleText = $title->getPrefixedText();
                                # Check for language variants if the template is not found
                                if ( $wgContLang->hasVariants() && $title->getArticleID() == 0 ) {
@@ -3254,8 +3339,24 @@ class Parser {
                                        && $this->mOptions->getAllowSpecialInclusion()
                                        && $this->ot['html'] )
                                {
-                                       $text = SpecialPageFactory::capturePath( $title );
-                                       if ( is_string( $text ) ) {
+                                       $pageArgs = array();
+                                       for ( $i = 0; $i < $args->getLength(); $i++ ) {
+                                               $bits = $args->item( $i )->splitArg();
+                                               if ( strval( $bits['index'] ) === '' ) {
+                                                       $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
+                                                       $value = trim( $frame->expand( $bits['value'] ) );
+                                                       $pageArgs[$name] = $value;
+                                               }
+                                       }
+                                       $context = new RequestContext;
+                                       $context->setTitle( $title );
+                                       $context->setRequest( new FauxRequest( $pageArgs ) );
+                                       $context->setUser( $this->getUser() );
+                                       $context->setLang( Language::factory( $this->mOptions->getUserLang() ) );
+                                       $ret = SpecialPageFactory::capturePath( $title, $context );
+                                       if ( $ret ) {
+                                               $text = $context->getOutput()->getHTML();
+                                               $this->mOutput->addOutputPageMetadata( $context->getOutput() );
                                                $found = true;
                                                $isHTML = true;
                                                $this->disableCache();
@@ -3276,18 +3377,22 @@ class Parser {
                                        $text = "[[:$titleText]]";
                                        $found = true;
                                }
-                       } elseif ( $title->isTrans() ) {
-                               # Interwiki transclusion
-                               if ( $this->ot['html'] && !$forceRawInterwiki ) {
-                                       $text = $this->interwikiTransclude( $title, 'render' );
-                                       $isHTML = true;
-                               } else {
-                                       $text = $this->interwikiTransclude( $title, 'raw' );
+                       } elseif ( $wgEnableInterwikiTranscluding && $title->isTrans() ) {
+
+                               $text = Interwiki::interwikiTransclude( $title );
+                               $this->registerDistantTemplate( $title );
+
+                               if ( $wgEnableInterwikiTemplatesTracking ) {
+                                       $this->registerDistantTemplate( $title );
+                               }
+
+                               if ( $text !== false ) {
                                        # Preprocess it like a template
                                        $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
+                                       $found = true;
                                        $isChildObj = true;
                                }
-                               $found = true;
+
                        }
 
                        # Do infinite loop check
@@ -3382,6 +3487,8 @@ class Parser {
         * Get the semi-parsed DOM representation of a template with a given title,
         * and its redirect destination title. Cached.
         *
+        * @param $title Title
+        *
         * @return array
         */
        function getTemplateDom( $title ) {
@@ -3435,10 +3542,19 @@ class Parser {
        }
 
        /**
-        * Fetch the unparsed text of a template and register a reference to it.
-        * @param Title $title
-        * @return mixed string or false
+        * Register a distant template as used
         */
+       function registerDistantTemplate( $title ) {
+               $stuff = Parser::distantTemplateCallback( $title, $this );
+               $text = $stuff['text'];
+               $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
+               if ( isset( $stuff['deps'] ) ) {
+                       foreach ( $stuff['deps'] as $dep ) {
+                               $this->mOutput->addDistantTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
+                       }
+               }
+       }
+
        function fetchTemplate( $title ) {
                $rv = $this->fetchTemplateAndTitle( $title );
                return $rv[0];
@@ -3448,6 +3564,9 @@ class Parser {
         * Static function to get a template
         * Can be overridden via ParserOptions::setTemplateCallback().
         *
+        * @parma $title Title
+        * @param $parser Parser
+        *
         * @return array
         */
        static function statelessFetchTemplate( $title, $parser = false ) {
@@ -3525,7 +3644,7 @@ class Parser {
         * @param Title $title
         * @param string $time MW timestamp
         * @param string $sha1 base 36 SHA-1
-        * @return mixed File or false
+        * @return File|false
         */
        function fetchFile( $title, $time = false, $sha1 = false ) {
                $res = $this->fetchFileAndTitle( $title, $time, $sha1 );
@@ -3554,61 +3673,32 @@ class Parser {
                if ( $file && !$title->equals( $file->getTitle() ) ) {
                        # Update fetched file title
                        $title = $file->getTitle();
+                       if ( is_null( $file->getRedirectedTitle() ) ) {
+                               # This file was not a redirect, but the title does not match.
+                               # Register under the new name because otherwise the link will
+                               # get lost.
+                               $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
+                       }
                }
                return array( $file, $title );
        }
 
-       /**
-        * Transclude an interwiki link.
-        *
-        * @param $title Title
-        * @param $action
-        *
-        * @return string
-        */
-       function interwikiTransclude( $title, $action ) {
-               global $wgEnableScaryTranscluding;
-
-               if ( !$wgEnableScaryTranscluding ) {
-                       return wfMsgForContent('scarytranscludedisabled');
-               }
+       static function distantTemplateCallback( $title, $parser=false ) {
+               $text = '';
+               $rev_id = null;
+               $deps[] = array(
+                       'title' => $title,
+                       'page_id' => $title->getArticleID(),
+                       'rev_id' => $rev_id );
 
-               $url = $title->getFullUrl( "action=$action" );
-
-               if ( strlen( $url ) > 255 ) {
-                       return wfMsgForContent( 'scarytranscludetoolong' );
-               }
-               return $this->fetchScaryTemplateMaybeFromCache( $url );
-       }
-
-       /**
-        * @param $url string
-        * @return Mixed|String
-        */
-       function fetchScaryTemplateMaybeFromCache( $url ) {
-               global $wgTranscludeCacheExpiry;
-               $dbr = wfGetDB( DB_SLAVE );
-               $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
-               $obj = $dbr->selectRow( 'transcache', array('tc_time', 'tc_contents' ),
-                               array( 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) );
-               if ( $obj ) {
-                       return $obj->tc_contents;
-               }
+               $finalTitle = $title;
 
-               $text = Http::get( $url );
-               if ( !$text ) {
-                       return wfMsgForContent( 'scarytranscludefailed', $url );
+               return array(
+                       'text' => $text,
+                       'finalTitle' => $finalTitle,
+                       'deps' => $deps );
                }
 
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->replace( 'transcache', array('tc_url'), array(
-                       'tc_url' => $url,
-                       'tc_time' => $dbw->timestamp( time() ),
-                       'tc_contents' => $text)
-               );
-               return $text;
-       }
-
        /**
         * Triple brace replacement -- used for template arguments
         * @private
@@ -3782,6 +3872,10 @@ class Parser {
        /**
         * Strip double-underscore items like __NOGALLERY__ and __NOTOC__
         * Fills $this->mDoubleUnderscores, returns the modified text
+        *
+        * @param $text string
+        *
+        * @return string
         */
        function doDoubleUnderscore( $text ) {
                wfProfileIn( __METHOD__ );
@@ -3842,6 +3936,10 @@ class Parser {
         * @return Boolean: whether the addition was successful
         */
        protected function addTrackingCategory( $msg ) {
+               if ( $this->mTitle->getNamespace() === NS_SPECIAL ) {
+                       wfDebug( __METHOD__.": Not adding tracking category $msg to special page!\n" );
+                       return false;
+               }
                $cat = wfMsgForContent( $msg );
 
                # Allow tracking categories to be disabled by setting them to "-"
@@ -3875,7 +3973,7 @@ class Parser {
         * @private
         */
        function formatHeadings( $text, $origText, $isMain=true ) {
-               global $wgMaxTocLevel, $wgContLang, $wgHtml5, $wgExperimentalHtmlIds;
+               global $wgMaxTocLevel, $wgHtml5, $wgExperimentalHtmlIds;
 
                # Inhibit editsection links if requested in the page
                if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
@@ -4012,7 +4110,7 @@ class Parser {
                                        if ( $dot ) {
                                                $numbering .= '.';
                                        }
-                                       $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
+                                       $numbering .= $this->getFunctionLang()->formatNum( $sublevelCount[$i] );
                                        $dot = 1;
                                }
                        }
@@ -4188,30 +4286,42 @@ class Parser {
                }
 
                # split up and insert constructed headlines
-
                $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
                $i = 0;
 
+               // build an array of document sections
+               $sections = array();
                foreach ( $blocks as $block ) {
-                       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
-
-                               # Disabled because it broke block formatting
-                               # For example, a bullet point in the top line
-                               # $full .= $sk->editSectionLink(0);
-                       }
-                       $full .= $block;
-                       if ( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
-                               # Top anchor now in skin
-                               $full = $full.$toc;
+                       // $head is zero-based, sections aren't.
+                       if ( empty( $head[$i - 1] ) ) {
+                               $sections[$i] = $block;
+                       } else {
+                               $sections[$i] = $head[$i - 1] . $block;
                        }
 
-                       if ( !empty( $head[$i] ) ) {
-                               $full .= $head[$i];
-                       }
+                       /**
+                        * Send a hook, one per section.
+                        * The idea here is to be able to make section-level DIVs, but to do so in a
+                        * lower-impact, more correct way than r50769
+                        *
+                        * $this : caller
+                        * $section : the section number
+                        * &$sectionContent : ref to the content of the section
+                        * $showEditLinks : boolean describing whether this section has an edit link
+                        */
+                       wfRunHooks( 'ParserSectionCreate', array( $this, $i, &$sections[$i], $showEditLink ) );
+
                        $i++;
                }
+
+               if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
+                       // append the TOC at the beginning
+                       // Top anchor now in skin
+                       $sections[0] = $sections[0] . $toc . "\n";
+               }
+
+               $full .= join( '', $sections );
+
                if ( $this->mForceTocPosition ) {
                        return str_replace( '<!--MWTOC-->', $toc, $full );
                } else {
@@ -4251,6 +4361,11 @@ class Parser {
        /**
         * Pre-save transform helper function
         * @private
+        *
+        * @param $text string
+        * @param $user User
+        *
+        * @return string
         */
        function pstPass2( $text, $user ) {
                global $wgContLang, $wgLocaltimezone;
@@ -4306,9 +4421,9 @@ class Parser {
                $tc = "[$wgLegalTitleChars]";
                $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
 
-               $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/";            # [[ns:page (context)|]]
-               $p4 = "/\[\[(:?$nc+:|:|)($tc+?)(($tc+))\\|]]/";             # [[ns:page(context)|]]
-               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/";  # [[ns:page (context), context|]]
+               $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";           # [[ns:page (context)|]]
+               $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";           # [[ns:page(context)|]]
+               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]]
                $p2 = "/\[\[\\|($tc+)]]/";                                      # [[|page]]
 
                # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
@@ -4342,8 +4457,8 @@ class Parser {
         * as it may have changed if it's the $wgParser.
         *
         * @param $user User
-        * @param $nickname String: nickname to use or false to use user's default nickname
-        * @param $fancySig Boolean: whether the nicknname is the complete signature
+        * @param $nickname String|bool nickname to use or false to use user's default nickname
+        * @param $fancySig Boolean|null whether the nicknname is the complete signature
         *                  or null to use default value
         * @return string
         */
@@ -4383,11 +4498,9 @@ class Parser {
                # If we're still here, make it a link to the user page
                $userText = wfEscapeWikiText( $username );
                $nickText = wfEscapeWikiText( $nickname );
-               if ( $user->isAnon() )  {
-                       return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText );
-               } else {
-                       return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText );
-               }
+               $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
+
+               return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()->title( $this->getTitle() )->text();
        }
 
        /**
@@ -4407,16 +4520,13 @@ class Parser {
         * 2) Substitute all transclusions
         *
         * @param $text String
-        * @param $parsing Whether we're cleaning (preferences save) or parsing
+        * @param $parsing bool Whether we're cleaning (preferences save) or parsing
         * @return String: signature text
         */
        function cleanSig( $text, $parsing = false ) {
                if ( !$parsing ) {
                        global $wgTitle;
-                       $this->mOptions = new ParserOptions;
-                       $this->clearState();
-                       $this->setTitle( $wgTitle );
-                       $this->setOutputType = self::OT_PREPROCESS;
+                       $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
                }
 
                # Option to disable this feature
@@ -4457,11 +4567,22 @@ class Parser {
        /**
         * Set up some variables which are usually set up in parse()
         * so that an external function can call some class members with confidence
+        *
+        * @param $title Title|null
+        * @param $options ParserOptions
+        * @param $outputType
+        * @param $clearState bool
         */
        public function startExternalParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) {
                $this->startParse( $title, $options, $outputType, $clearState );
        }
 
+       /**
+        * @param $title Title|null
+        * @param $options ParserOptions
+        * @param $outputType
+        * @param $clearState bool
+        */
        private function startParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) {
                $this->setTitle( $title );
                $this->mOptions = $options;
@@ -4692,7 +4813,11 @@ class Parser {
         * @todo FIXME: Update documentation. makeLinkObj() is deprecated.
         * 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.
+        *
+        * @param $text string
+        * @param $options int
+        *
+        * @return array of link CSS classes, indexed by PDBK.
         */
        function replaceLinkHolders( &$text, $options = 0 ) {
                return $this->mLinkHolders->replace( $text );
@@ -4719,7 +4844,7 @@ class Parser {
         * 'A tree'.
         *
         * @param string $text
-        * @param array $param
+        * @param array $params
         * @return string HTML
         */
        function renderImageGallery( $text, $params ) {
@@ -4804,6 +4929,10 @@ class Parser {
                return $ig->toHTML();
        }
 
+       /**
+        * @param $handler
+        * @return array
+        */
        function getImageParams( $handler ) {
                if ( $handler ) {
                        $handlerClass = get_class( $handler );
@@ -4849,7 +4978,7 @@ class Parser {
         *
         * @param $title Title
         * @param $options String
-        * @param $holders LinkHolderArray
+        * @param $holders LinkHolderArray|false
         * @return string HTML
         */
        function makeImage( $title, $options, $holders = false ) {
@@ -4953,7 +5082,7 @@ class Parser {
                                                                $value = true;
                                                                $validated = true;
                                                        } elseif ( preg_match( "/^$prots/", $value ) ) {
-                                                               if ( preg_match( "/^($prots)$chars+$/", $value, $m ) ) {
+                                                               if ( preg_match( "/^($prots)$chars+$/u", $value, $m ) ) {
                                                                        $paramName = 'link-url';
                                                                        $this->mOutput->addExternalLink( $value );
                                                                        if ( $this->mOptions->getExternalLinkTarget() ) {
@@ -5118,11 +5247,16 @@ class Parser {
         *
         * Transparent tag hooks are like regular XML-style tag hooks, except they
         * operate late in the transformation sequence, on HTML instead of wikitext.
+        *
+        * @param $text string
+        *
+        * @return string
         */
        function replaceTransparentTags( $text ) {
                $matches = array();
                $elements = array_keys( $this->mTransparentTagHooks );
                $text = self::extractTagsAndParams( $elements, $text, $matches, $this->mUniqPrefix );
+               $replacements = array();
 
                foreach ( $matches as $marker => $data ) {
                        list( $element, $content, $params, $tag ) = $data;
@@ -5132,9 +5266,9 @@ class Parser {
                        } else {
                                $output = $tag;
                        }
-                       $this->mStripState->addGeneral( $marker, $output );
+                       $replacements[$marker] = $output;
                }
-               return $text;
+               return strtr( $text, $replacements );
        }
 
        /**
@@ -5157,6 +5291,10 @@ class Parser {
         * pull the given section along with its lower-level subsections. If the section is
         * not found, $mode=get will return $newtext, and $mode=replace will return $text.
         *
+        * Section 0 is always considered to exist, even if it only contains the empty
+        * string. If $text is the empty string and section 0 is replaced, $newText is 
+        * returned.
+        *
         * @param $mode String: one of "get" or "replace"
         * @param $newText String: replacement text for section data.
         * @return String: for "get", the extracted section text.
@@ -5177,6 +5315,25 @@ class Parser {
                                $flags |= self::PTD_FOR_INCLUSION;
                        }
                }
+
+               # Check for empty input
+               if ( strval( $text ) === '' ) {
+                       # Only sections 0 and T-0 exist in an empty document
+                       if ( $sectionIndex == 0 ) {
+                               if ( $mode === 'get' ) {
+                                       return '';
+                               } else {
+                                       return $newText;
+                               }
+                       } else {
+                               if ( $mode === 'get' ) {
+                                       return $newText;
+                               } else {
+                                       return $text;
+                               }
+                       }
+               }
+
                # Preprocess the text
                $root = $this->preprocessToDom( $text, $flags );
 
@@ -5188,10 +5345,6 @@ class Parser {
                if ( $sectionIndex == 0 ) {
                        # Section zero doesn't nest, level=big
                        $targetLevel = 1000;
-                       if ( !$node ) {
-                               # The page definitely exists - we checked that earlier - so it must be blank: see bug #14005
-                               return $text;
-                       }
                } else {
                        while ( $node ) {
                                if ( $node->getName() === 'h' ) {
@@ -5274,7 +5427,8 @@ class Parser {
 
        /**
         * This function returns $oldtext after the content of the section
-        * specified by $section has been replaced with $text.
+        * specified by $section has been replaced with $text. If the target 
+        * section does not exist, $oldtext is returned unchanged.
         *
         * @param $oldtext String: former text of the article
         * @param $section Numeric: section identifier
@@ -5401,6 +5555,10 @@ class Parser {
         * Try to guess the section anchor name based on a wikitext fragment
         * presumably extracted from a heading, for example "Header" from
         * "== Header ==".
+        *
+        * @param $text string
+        *
+        * @return string
         */
        public function guessSectionNameFromWikiText( $text ) {
                # Strip out wikitext links(they break the anchor)
@@ -5460,9 +5618,14 @@ class Parser {
        /**
         * strip/replaceVariables/unstrip for preprocessor regression testing
         *
+        * @param $text string
+        * @param $title Title
+        * @param $options ParserOptions
+        * @param $outputType int
+        *
         * @return string
         */
-       function testSrvus( $text, $title, ParserOptions $options, $outputType = self::OT_HTML ) {
+       function testSrvus( $text, Title $title, ParserOptions $options, $outputType = self::OT_HTML ) {
                $this->startParse( $title, $options, $outputType, true );
 
                $text = $this->replaceVariables( $text );
@@ -5471,11 +5634,23 @@ class Parser {
                return $text;
        }
 
-       function testPst( $text, $title, $options ) {
+       /**
+        * @param $text string
+        * @param $title Title
+        * @param $options ParserOptions
+        * @return string
+        */
+       function testPst( $text, Title $title, ParserOptions $options ) {
                return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
        }
 
-       function testPreprocess( $text, $title, $options ) {
+       /**
+        * @param $text
+        * @param $title Title
+        * @param $options ParserOptions
+        * @return string
+        */
+       function testPreprocess( $text, Title $title, ParserOptions $options ) {
                return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
        }
 
@@ -5490,6 +5665,9 @@ class Parser {
         * two strings will be replaced with the value returned by the callback in
         * each case.
         *
+        * @param $s string
+        * @param $callback
+        *
         * @return string
         */
        function markerSkipCallback( $s, $callback ) {
@@ -5528,6 +5706,8 @@ class Parser {
         * unserializeHalfParsedText(). The text can then be safely incorporated into
         * the return value of a parser hook.
         *
+        * @param $text string
+        *
         * @return array
         */
        function serializeHalfParsedText( $text ) {