(bug 20274) Strict standards issue in 5.3.
[lhc/web/wiklou.git] / includes / parser / Parser.php
index bfff371..d5e329e 100644 (file)
@@ -92,7 +92,8 @@ class Parser
        # Persistent:
        var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
                $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
-               $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList, $mVarCache, $mConf;
+               $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList, $mVarCache, $mConf,
+               $mFunctionTagHooks;
 
 
        # Cleared with clearState():
@@ -103,7 +104,6 @@ class Parser
        var $mTplExpandCache; // empty-frame expansion cache
        var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
        var $mExpensiveFunctionCount; // number of expensive parser function calls
-       var $mFileCache;
 
        # Temporary
        # These are variables reset at least once per parse regardless of $clearState
@@ -127,6 +127,7 @@ class Parser
                $this->mTagHooks = array();
                $this->mTransparentTagHooks = array();
                $this->mFunctionHooks = array();
+               $this->mFunctionTagHooks = array();
                $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
                $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
                $this->mUrlProtocols = wfUrlProtocols();
@@ -230,7 +231,6 @@ class Parser
                $this->mHeadings = array();
                $this->mDoubleUnderscores = array();
                $this->mExpensiveFunctionCount = 0;
-               $this->mFileCache = array();
 
                # Fix cloning
                if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
@@ -441,7 +441,7 @@ class Parser
                wfProfileIn( __METHOD__ );
                wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
                wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->internalParse( $text );
+               $text = $this->internalParse( $text, false );
                wfProfileOut( __METHOD__ );
                return $text;
        }
@@ -862,9 +862,10 @@ class Parser
         *
         * @private
         */
-       function internalParse( $text ) {
-               $isMain = true;
+       function internalParse( $text, $isMain = true ) {
                wfProfileIn( __METHOD__ );
+               
+               $origText = $text;
 
                # Hook to suspend the parser in this state
                if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
@@ -899,7 +900,7 @@ class Parser
                $text = str_replace($this->mUniqPrefix.'NOPARSE', '', $text);
 
                $text = $this->doMagicLinks( $text );
-               $text = $this->formatHeadings( $text, $isMain );
+               $text = $this->formatHeadings( $text, $origText, $isMain );
 
                wfProfileOut( __METHOD__ );
                return $text;
@@ -958,7 +959,7 @@ class Parser
                        }
                        $url = wfMsg( $urlmsg, $id);
                        $sk = $this->mOptions->getSkin();
-                       $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
+                       $la = $sk->getExternalLinkAttributes();
                        return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
                } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
                        # ISBN
@@ -1581,29 +1582,29 @@ 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( '/^\b(?:' . wfUrlProtocols() . ')/', $m[1] ) ) {
                                $s .= $prefix . '[[' . $line ;
                                wfProfileOut( __METHOD__."-misc" );
                                continue;
                        }
 
                        # Make subpage if necessary
-                       if( $useSubpages ) {
+                       if ( $useSubpages ) {
                                $link = $this->maybeDoSubpageLink( $m[1], $text );
                        } else {
                                $link = $m[1];
                        }
 
-                       $noforce = (substr($m[1], 0, 1) !== ':');
+                       $noforce = (substr( $m[1], 0, 1 ) !== ':');
                        if (!$noforce) {
                                # Strip off leading ':'
-                               $link = substr($link, 1);
+                               $link = substr( $link, 1 );
                        }
 
                        wfProfileOut( __METHOD__."-misc" );
                        wfProfileIn( __METHOD__."-title" );
-                       $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
-                       if( $nt === NULL ) {
+                       $nt = Title::newFromText( $this->mStripState->unstripNoWiki( $link ) );
+                       if ( $nt === NULL ) {
                                $s .= $prefix . '[[' . $line;
                                wfProfileOut( __METHOD__."-title" );
                                continue;
@@ -1613,9 +1614,9 @@ class Parser
                        $iw = $nt->getInterWiki();
                        wfProfileOut( __METHOD__."-title" );
 
-                       if ($might_be_img) { # if this is actually an invalid link
+                       if ( $might_be_img ) { # if this is actually an invalid link
                                wfProfileIn( __METHOD__."-might_be_img" );
-                               if ($ns == NS_FILE && $noforce) { #but might be an image
+                               if ( $ns == NS_FILE && $noforce ) { #but might be an image
                                        $found = false;
                                        while ( true ) {
                                                #look at the next 'line' to see if we can close it there
@@ -1659,14 +1660,14 @@ class Parser
                        }
 
                        $wasblank = ( '' == $text );
-                       if( $wasblank ) $text = $link;
+                       if ( $wasblank ) $text = $link;
 
                        # Link not escaped by : , create the various objects
-                       if( $noforce ) {
+                       if ( $noforce ) {
 
                                # Interwikis
                                wfProfileIn( __METHOD__."-interwiki" );
-                               if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
+                               if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
                                        $this->mOutput->addLanguageLink( $nt->getFullText() );
                                        $s = rtrim($s . $prefix);
                                        $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
@@ -1678,12 +1679,19 @@ class Parser
                                if ( $ns == NS_FILE ) {
                                        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);
-                                               $holders->merge( $this->replaceInternalLinks2( $text ) );
-
+                                               if ( $wasblank ) {
+                                                       # if no parameters were passed, $text
+                                                       # becomes something like "File:Foo.png",
+                                                       # which we don't want to pass on to the
+                                                       # image generator
+                                                       $text = '';
+                                               } else {
+                                                       # 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);
+                                                       $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, $holders ) ) . $trail;
                                        }
@@ -1695,7 +1703,7 @@ class Parser
 
                                if ( $ns == NS_CATEGORY ) {
                                        wfProfileIn( __METHOD__."-category" );
-                                       $s = rtrim($s . "\n"); # bug 87
+                                       $s = preg_replace( "/(\s*\n)+\s*$/D", '', $s ); # bug 87
 
                                        if ( $wasblank ) {
                                                $sortkey = $this->getDefaultSort();
@@ -1711,7 +1719,7 @@ class Parser
                                         * Strip the whitespace Category links produce, see bug 87
                                         * @todo We might want to use trim($tmp, "\n") here.
                                         */
-                                       $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
+                                       $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
 
                                        wfProfileOut( __METHOD__."-category" );
                                        continue;
@@ -1793,6 +1801,7 @@ class Parser
        function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
                list( $inside, $trail ) = Linker::splitTrail( $trail );
                $sk = $this->mOptions->getSkin();
+               // FIXME: use link() instead of deprecated makeKnownLinkObj()
                $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
                return $this->armorLinks( $link ) . $trail;
        }
@@ -1829,75 +1838,7 @@ class Parser
         * @private
         */
        function maybeDoSubpageLink($target, &$text) {
-               # Valid link forms:
-               # Foobar -- normal
-               # :Foobar -- override special treatment of prefix (images, language links)
-               # /Foobar -- convert to CurrentPage/Foobar
-               # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
-               # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
-               # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
-
-               wfProfileIn( __METHOD__ );
-               $ret = $target; # default return value is no change
-
-               # Some namespaces don't allow subpages,
-               # so only perform processing if subpages are allowed
-               if( $this->areSubpagesAllowed() ) {
-                       $hash = strpos( $target, '#' );
-                       if( $hash !== false ) {
-                               $suffix = substr( $target, $hash );
-                               $target = substr( $target, 0, $hash );
-                       } else {
-                               $suffix = '';
-                       }
-                       # bug 7425
-                       $target = trim( $target );
-                       # Look at the first character
-                       if( $target != '' && $target{0} === '/' ) {
-                               # / at end means we don't want the slash to be shown
-                               $m = array();
-                               $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
-                               if( $trailingSlashes ) {
-                                       $noslash = $target = substr( $target, 1, -strlen($m[0][0]) );
-                               } else {
-                                       $noslash = substr( $target, 1 );
-                               }
-
-                               $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix;
-                               if( '' === $text ) {
-                                       $text = $target . $suffix;
-                               } # this might be changed for ugliness reasons
-                       } else {
-                               # check for .. subpage backlinks
-                               $dotdotcount = 0;
-                               $nodotdot = $target;
-                               while( strncmp( $nodotdot, "../", 3 ) == 0 ) {
-                                       ++$dotdotcount;
-                                       $nodotdot = substr( $nodotdot, 3 );
-                               }
-                               if($dotdotcount > 0) {
-                                       $exploded = explode( '/', $this->mTitle->GetPrefixedText() );
-                                       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 ) === '/' ) {
-                                                       $nodotdot = substr( $nodotdot, 0, -1 );
-                                                       if( '' === $text ) {
-                                                               $text = $nodotdot . $suffix;
-                                                       }
-                                               }
-                                               $nodotdot = trim( $nodotdot );
-                                               if( $nodotdot != '' ) {
-                                                       $ret .= '/' . $nodotdot;
-                                               }
-                                               $ret .= $suffix;
-                                       }
-                               }
-                       }
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $ret;
+               return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
        }
 
        /**#@+
@@ -1933,12 +1874,12 @@ class Parser
                $result = $this->closeParagraph();
 
                if ( '*' === $char ) { $result .= '<ul><li>'; }
-               else if ( '#' === $char ) { $result .= '<ol><li>'; }
-               else if ( ':' === $char ) { $result .= '<dl><dd>'; }
-               else if ( ';' === $char ) {
+               elseif ( '#' === $char ) { $result .= '<ol><li>'; }
+               elseif ( ':' === $char ) { $result .= '<dl><dd>'; }
+               elseif ( ';' === $char ) {
                        $result .= '<dl><dt>';
                        $this->mDTopen = true;
-               }
+               } elseif ( '>' === $char ) { $result .= "<blockquote><p>"; }
                else { $result = '<!-- ERR 1 -->'; }
 
                return $result;
@@ -1946,7 +1887,8 @@ class Parser
 
        /* private */ function nextItem( $char ) {
                if ( '*' === $char || '#' === $char ) { return '</li><li>'; }
-               else if ( ':' === $char || ';' === $char ) {
+               elseif ( '>' === $char ) { return "</p><p>"; }
+               elseif ( ':' === $char || ';' === $char ) {
                        $close = '</dd>';
                        if ( $this->mDTopen ) { $close = '</dt>'; }
                        if ( ';' === $char ) {
@@ -1962,8 +1904,9 @@ class Parser
 
        /* private */ function closeList( $char ) {
                if ( '*' === $char ) { $text = '</li></ul>'; }
-               else if ( '#' === $char ) { $text = '</li></ol>'; }
-               else if ( ':' === $char ) {
+               elseif ( '#' === $char ) { $text = '</li></ol>'; }
+               elseif ( '>' === $char ) { $text = "</p></blockquote>"; }
+               elseif ( ':' === $char ) {
                        if ( $this->mDTopen ) {
                                $this->mDTopen = false;
                                $text = '</dt></dl>';
@@ -1979,6 +1922,7 @@ class Parser
        /**
         * Make lists from lines starting with ':', '*', '#', etc. (DBL)
         *
+        * @param $linestart bool whether or not this is at the start of a line.
         * @private
         * @return string the lists rendered as HTML
         */
@@ -2003,16 +1947,33 @@ class Parser
                                $linestart = true;
                                continue;
                        }
+                       // * = ul
+                       // # = ol
+                       // ; = dt
+                       // : = dd
+                       // > = blockquote
 
                        $lastPrefixLength = strlen( $lastPrefix );
                        $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
                        $preOpenMatch = preg_match('/<pre/i', $oLine );
+                       
+                       // Need to decode &gt; --> > for blockquote syntax. Re-encode later.
+                       // To avoid collision with real >s, we temporarily convert them to &gt;
+                       // This is a weird choice of armouring, but it's totally resistant to any
+                       //  collision.
+                       $orig = $oLine;
+                       $oLine = strtr( $oLine, array( '&gt;' => '>', '>' => '&gt;' ) );
+                       
+                       // If not in a <pre> element, scan for and figure out what prefixes are there.
                        if ( !$this->mInPre ) {
                                # Multiple prefixes may abut each other for nested lists.
-                               $prefixLength = strspn( $oLine, '*#:;' );
+                               $prefixLength = strspn( $oLine, '*#:;>' );
                                $prefix = substr( $oLine, 0, $prefixLength );
 
                                # eh?
+                               // ; and : are both from definition-lists, so they're equivalent
+                               //  for the purposes of determining whether or not we need to open/close
+                               //  elements.
                                $prefix2 = str_replace( ';', ':', $prefix );
                                $t = substr( $oLine, $prefixLength );
                                $this->mInPre = (bool)$preOpenMatch;
@@ -2022,6 +1983,9 @@ class Parser
                                $prefix = $prefix2 = '';
                                $t = $oLine;
                        }
+                       
+                       // Re-encode >s now
+                       $t = strtr( $t, array( '&gt;' => '>', '>' => '&gt;' ) );
 
                        # List generation
                        if( $prefixLength && $lastPrefix === $prefix2 ) {
@@ -2041,17 +2005,24 @@ class Parser
                                        }
                                }
                        } elseif( $prefixLength || $lastPrefixLength ) {
+                               // We need to open or close prefixes, or both.
+                               
                                # Either open or close a level...
                                $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
                                $paragraphStack = false;
 
+                               // Close all the prefixes which aren't shared.
                                while( $commonPrefixLength < $lastPrefixLength ) {
                                        $output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
                                        --$lastPrefixLength;
                                }
+                               
+                               // Continue the current prefix if appropriate.
                                if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
                                        $output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
                                }
+                               
+                               // Open prefixes where appropriate.
                                while ( $prefixLength > $commonPrefixLength ) {
                                        $char = substr( $prefix, $commonPrefixLength, 1 );
                                        $output .= $this->openList( $char );
@@ -2067,6 +2038,8 @@ class Parser
                                }
                                $lastPrefix = $prefix2;
                        }
+                       
+                       // If we have no prefixes, go to paragraph mode.
                        if( 0 == $prefixLength ) {
                                wfProfileIn( __METHOD__."-paragraph" );
                                # No prefix (not in list)--go to paragraph mode
@@ -2341,6 +2314,7 @@ class Parser
                wfSuppressWarnings(); // E_STRICT system time bitching
                $localTimestamp = date( 'YmdHis', $ts );
                $localMonth = date( 'm', $ts );
+               $localMonth1 = date( 'n', $ts );
                $localMonthName = date( 'n', $ts );
                $localDay = date( 'j', $ts );
                $localDay2 = date( 'd', $ts );
@@ -2356,6 +2330,8 @@ class Parser
                switch ( $index ) {
                        case 'currentmonth':
                                return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+                       case 'currentmonth1':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'n', $ts ) );
                        case 'currentmonthname':
                                return $this->mVarCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
                        case 'currentmonthnamegen':
@@ -2368,6 +2344,8 @@ class Parser
                                return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
                        case 'localmonth':
                                return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth );
+                       case 'localmonth1':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth1 );
                        case 'localmonthname':
                                return $this->mVarCache[$index] = $wgContLang->getMonthName( $localMonthName );
                        case 'localmonthnamegen':
@@ -2808,6 +2786,8 @@ class Parser
 
                                        # Workaround for PHP bug 35229 and similar
                                        if ( !is_callable( $callback ) ) {
+                                               wfProfileOut( __METHOD__ . '-pfunc' );
+                                               wfProfileOut( __METHOD__ );
                                                throw new MWException( "Tag hook for $function is not callable\n" );
                                        }
                                        $result = call_user_func_array( $callback, $allArgs );
@@ -2853,12 +2833,6 @@ class Parser
                                if($wgContLang->hasVariants() && $title->getArticleID() == 0){
                                        $wgContLang->findVariantLink( $part1, $title, true );
                                }
-                               # Do infinite loop check
-                               if ( !$frame->loopCheck( $title ) ) {
-                                       $found = true;
-                                       $text = '<span class="error">' . wfMsgForContent( 'parser-template-loop-warning', $titleText ) . '</span>';
-                                       wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
-                               }
                                # Do recursion depth check
                                $limit = $this->mOptions->getMaxTemplateDepth();
                                if ( $frame->depth >= $limit ) {
@@ -2908,6 +2882,14 @@ class Parser
                                }
                                $found = true;
                        }
+
+                       # Do infinite loop check
+                       # This has to be done after redirect resolution to avoid infinite loops via redirects
+                       if ( !$frame->loopCheck( $title ) ) {
+                               $found = true;
+                               $text = '<span class="error">' . wfMsgForContent( 'parser-template-loop-warning', $titleText ) . '</span>';
+                               wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
+                       }
                        wfProfileOut( __METHOD__ . '-loadtpl' );
                }
 
@@ -3205,9 +3187,10 @@ class Parser
 
                $marker = "{$this->mUniqPrefix}-$name-" . sprintf('%08X', $this->mMarkerIndex++) . self::MARKER_SUFFIX;
 
-               if ( $this->ot['html'] ) {
+               $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower($name)] ) &&
+                       ( $this->ot['html'] || $this->ot['pre'] );
+               if ( $this->ot['html'] || $isFunctionTag ) {
                        $name = strtolower( $name );
-
                        $attributes = Sanitizer::decodeTagAttributes( $attrText );
                        if ( isset( $params['attributes'] ) ) {
                                $attributes = $attributes + $params['attributes'];
@@ -3239,6 +3222,13 @@ class Parser
                                                }
                                                $output = call_user_func_array( $this->mTagHooks[$name],
                                                        array( $content, $attributes, $this ) );
+                                       } elseif( isset( $this->mFunctionTagHooks[$name] ) ) {
+                                               list( $callback, $flags ) = $this->mFunctionTagHooks[$name];
+                                               if( !is_callable( $callback ) )
+                                                       throw new MWException( "Tag hook for $name is not callable\n" );
+
+                                               $output = call_user_func_array( $callback,
+                                                       array( &$this, $frame, $content, $attributes ) );
                                        } else {
                                                $output = '<span class="error">Invalid tag extension name: ' .
                                                        htmlspecialchars( $name ) . '</span>';
@@ -3262,7 +3252,9 @@ class Parser
                        }
                }
 
-               if ( $name === 'html' || $name === 'nowiki' ) {
+               if( $isFunctionTag ) {
+                       return $output;
+               } elseif ( $name === 'html' || $name === 'nowiki' ) {
                        $this->mStripState->nowiki->setPair( $marker, $output );
                } else {
                        $this->mStripState->general->setPair( $marker, $output );
@@ -3363,10 +3355,11 @@ class Parser
         * string and re-inserts the newly formatted headlines.
         *
         * @param string $text
+        * @param string $origText Original, untouched wikitext
         * @param boolean $isMain
         * @private
         */
-       function formatHeadings( $text, $isMain=true ) {
+       function formatHeadings( $text, $origText, $isMain=true ) {
                global $wgMaxTocLevel, $wgContLang, $wgEnforceHtmlIds;
 
                $doNumberHeadings = $this->mOptions->getNumberHeadings();
@@ -3432,6 +3425,12 @@ class Parser
                $prevtoclevel = 0;
                $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
                $baseTitleText = $this->mTitle->getPrefixedDBkey();
+               $oldType = $this->mOutputType;
+               $this->setOutputType( self::OT_WIKI );
+               $frame = $this->getPreprocessor()->newFrame();
+               $root = $this->preprocessToDom( $origText );
+               $node = $root->getFirstChild();
+               $byteOffset = 0;
                $tocraw = array();
 
                foreach( $matches[3] as $headline ) {
@@ -3453,68 +3452,61 @@ class Parser
                        }
                        $level = $matches[1][$headlineCount];
 
-                       if( $doNumberHeadings || $enoughToc ) {
-
-                               if ( $level > $prevlevel ) {
-                                       # Increase TOC level
-                                       $toclevel++;
-                                       $sublevelCount[$toclevel] = 0;
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               $prevtoclevel = $toclevel;
-                                               $toc .= $sk->tocIndent();
-                                               $numVisible++;
-                                       }
+                       if ( $level > $prevlevel ) {
+                               # Increase TOC level
+                               $toclevel++;
+                               $sublevelCount[$toclevel] = 0;
+                               if( $toclevel<$wgMaxTocLevel ) {
+                                       $prevtoclevel = $toclevel;
+                                       $toc .= $sk->tocIndent();
+                                       $numVisible++;
                                }
-                               elseif ( $level < $prevlevel && $toclevel > 1 ) {
-                                       # Decrease TOC level, find level to jump to
+                       }
+                       elseif ( $level < $prevlevel && $toclevel > 1 ) {
+                               # Decrease TOC level, find level to jump to
 
-                                       if ( $toclevel == 2 && $level <= $levelCount[1] ) {
-                                               # Can only go down to level 1
-                                               $toclevel = 1;
-                                       } else {
-                                               for ($i = $toclevel; $i > 0; $i--) {
-                                                       if ( $levelCount[$i] == $level ) {
-                                                               # Found last matching level
-                                                               $toclevel = $i;
-                                                               break;
-                                                       }
-                                                       elseif ( $levelCount[$i] < $level ) {
-                                                               # Found first matching level below current level
-                                                               $toclevel = $i + 1;
-                                                               break;
-                                                       }
-                                               }
+                               for ($i = $toclevel; $i > 0; $i--) {
+                                       if ( $levelCount[$i] == $level ) {
+                                               # Found last matching level
+                                               $toclevel = $i;
+                                               break;
                                        }
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               if($prevtoclevel < $wgMaxTocLevel) {
-                                                       # Unindent only if the previous toc level was shown :p
-                                                       $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
-                                                       $prevtoclevel = $toclevel;
-                                               } else {
-                                                       $toc .= $sk->tocLineEnd();
-                                               }
+                                       elseif ( $levelCount[$i] < $level ) {
+                                               # Found first matching level below current level
+                                               $toclevel = $i + 1;
+                                               break;
                                        }
                                }
-                               else {
-                                       # No change in level, end TOC line
-                                       if( $toclevel<$wgMaxTocLevel ) {
+                               if( $i == 0 ) $toclevel = 1;
+                               if( $toclevel<$wgMaxTocLevel ) {
+                                       if($prevtoclevel < $wgMaxTocLevel) {
+                                               # Unindent only if the previous toc level was shown :p
+                                               $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
+                                               $prevtoclevel = $toclevel;
+                                       } else {
                                                $toc .= $sk->tocLineEnd();
                                        }
                                }
+                       }
+                       else {
+                               # No change in level, end TOC line
+                               if( $toclevel<$wgMaxTocLevel ) {
+                                       $toc .= $sk->tocLineEnd();
+                               }
+                       }
 
-                               $levelCount[$toclevel] = $level;
+                       $levelCount[$toclevel] = $level;
 
-                               # count number of headlines for each level
-                               @$sublevelCount[$toclevel]++;
-                               $dot = 0;
-                               for( $i = 1; $i <= $toclevel; $i++ ) {
-                                       if( !empty( $sublevelCount[$i] ) ) {
-                                               if( $dot ) {
-                                                       $numbering .= '.';
-                                               }
-                                               $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
-                                               $dot = 1;
+                       # count number of headlines for each level
+                       @$sublevelCount[$toclevel]++;
+                       $dot = 0;
+                       for( $i = 1; $i <= $toclevel; $i++ ) {
+                               if( !empty( $sublevelCount[$i] ) ) {
+                                       if( $dot ) {
+                                               $numbering .= '.';
                                        }
+                                       $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
+                                       $dot = 1;
                                }
                        }
 
@@ -3538,6 +3530,7 @@ class Parser
 
                        # For the anchor, strip out HTML-y stuff period
                        $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
+                       $safeHeadline = preg_replace( '/[ _]+/', ' ', $safeHeadline );
                        $safeHeadline = trim( $safeHeadline );
 
                        # Save headline for section edit hint before it's escaped
@@ -3613,9 +3606,33 @@ class Parser
                                $legacyAnchor .= '_' . $refers[$legacyArrayKey];
                        }
                        if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
-                               $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
-                               $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering );
+                               $toc .= $sk->tocLine($anchor, $tocline,
+                                       $numbering, $toclevel, ($isTemplate ? false : $sectionIndex));
+                       }
+                       
+                       # Add the section to the section tree
+                       # Find the DOM node for this header
+                       while ( $node && !$isTemplate ) {
+                               if ( $node->getName() === 'h' ) {
+                                       $bits = $node->splitHeading();
+                                       if ( $bits['i'] == $sectionIndex )
+                                               break;
+                               }
+                               $byteOffset += mb_strlen( $this->mStripState->unstripBoth( 
+                                       $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
+                               $node = $node->getNextSibling();
                        }
+                       $tocraw[] = array( 
+                               'toclevel' => $toclevel,
+                               'level' => $level,
+                               'line' => $tocline,
+                               'number' => $numbering,
+                               'index' => ($isTemplate ? 'T-' : '' ) . $sectionIndex,
+                               'fromtitle' => $titleText,
+                               'byteoffset' => ( $isTemplate ? null : $byteOffset ),
+                               'anchor' => $anchor,
+                       );
+                       
                        # give headline the correct <h#> tag
                        if( $showEditLink && $sectionIndex !== false ) {
                                if( $isTemplate ) {
@@ -3635,7 +3652,7 @@ class Parser
                        $headlineCount++;
                }
 
-               $this->mOutput->setSections( $tocraw );
+               $this->setOutputType( $oldType );
 
                # Never ever show TOC if no headers
                if( $numVisible < 1 ) {
@@ -3647,13 +3664,18 @@ class Parser
                                $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
                        }
                        $toc = $sk->tocList( $toc );
+                       $this->mOutput->setTOCHTML( $toc );
+               }
+               
+               if ( $isMain ) {
+                       $this->mOutput->setSections( $tocraw );
                }
 
                # split up and insert constructed headlines
 
                $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
                $i = 0;
-
+               
                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
@@ -3680,20 +3702,110 @@ class Parser
                        return $full;
                }
        }
-
+       
+       /**
+        * Merge $tree2 into $tree1 by replacing the section with index
+        * $section in $tree1 and its descendants with the sections in $tree2.
+        * Note that in the returned section tree, only the 'index' and
+        * 'byteoffset' fields are guaranteed to be correct.
+        * @param $tree1 array Section tree from ParserOutput::getSectons()
+       *  @param $tree2 array Section tree
+        * @param $section int Section index
+        * @param $title Title Title both section trees come from
+        * @param $len2 int Length of the original wikitext for $tree2
+        * @return array Merged section tree
+        */
+       public static function mergeSectionTrees( $tree1, $tree2, $section, $title, $len2 ) {
+               global $wgContLang;
+               $newTree = array();
+               $targetLevel = false;
+               $merged = false;
+               $lastLevel = 1;
+               $nextIndex = 1;
+               $numbering = array( 0 );
+               $titletext = $title->getPrefixedDBkey();
+               foreach ( $tree1 as $s ) {
+                       if ( $targetLevel !== false ) { 
+                               if ( $s['level'] <= $targetLevel )
+                                       // We've skipped enough
+                                       $targetLevel = false;
+                               else
+                                       continue;
+                       }
+                       if ( $s['index'] != $section ||
+                                       $s['fromtitle'] != $titletext ) {
+                               self::incrementNumbering( $numbering,
+                                       $s['toclevel'], $lastLevel );
+                               
+                               // Rewrite index, byteoffset and number
+                               if ( $s['fromtitle'] == $titletext ) {
+                                       $s['index'] = $nextIndex++;
+                                       if ( $merged )
+                                               $s['byteoffset'] += $len2;
+                               }
+                               $s['number']  = implode( '.', array_map(
+                                       array( $wgContLang, 'formatnum' ),
+                                       $numbering ) );
+                               $lastLevel = $s['toclevel'];
+                               $newTree[] = $s;
+                       } else {
+                               // We're at $section
+                               // Insert sections from $tree2 here
+                               foreach ( $tree2 as $s2 ) {
+                                       // Rewrite the fields in $s2
+                                       // before inserting it
+                                       $s2['toclevel'] += $s['toclevel'] - 1;
+                                       $s2['level'] += $s['level'] - 1;
+                                       $s2['index'] = $nextIndex++;
+                                       $s2['byteoffset'] += $s['byteoffset'];
+                                       
+                                       self::incrementNumbering( $numbering,
+                                               $s2['toclevel'], $lastLevel );
+                                       $s2['number']  = implode( '.', array_map(
+                                               array( $wgContLang, 'formatnum' ),
+                                               $numbering ) );
+                                       $lastLevel = $s2['toclevel'];
+                                       $newTree[] = $s2;
+                               }
+                               // Skip all descendants of $section in $tree1
+                               $targetLevel = $s['level'];
+                               $merged = true;
+                       }
+               }
+               return $newTree;
+       }
+       
+       /**
+        * Increment a section number. Helper function for mergeSectionTrees()
+        * @param $number array Array representing a section number
+        * @param $level int Current TOC level (depth)
+        * @param $lastLevel int Level of previous TOC entry
+        */
+       private static function incrementNumbering( &$number, $level, $lastLevel ) {
+               if ( $level > $lastLevel )
+                       $number[$level - 1] = 1;
+               else if ( $level < $lastLevel ) {
+                       foreach ( $number as $key => $unused )
+                               if ( $key >= $level )
+                                       unset( $number[$key] );
+                       $number[$level - 1]++;
+               } else
+                       $number[$level - 1]++;
+       }
+       
        /**
         * Transform wiki markup when saving a page by doing \r\n -> \n
         * conversion, substitting signatures, {{subst:}} templates, etc.
         *
         * @param string $text the text to transform
         * @param Title &$title the Title object for the current article
-        * @param User &$user the User object describing the current user
+        * @param User $user the User object describing the current user
         * @param ParserOptions $options parsing options
         * @param bool $clearState whether to clear the parser state first
         * @return string the altered wiki markup
         * @public
         */
-       function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) {
+       function preSaveTransform( $text, Title $title, $user, $options, $clearState = true ) {
                $this->mOptions = $options;
                $this->setTitle( $title );
                $this->setOutputType( self::OT_WIKI );
@@ -3805,7 +3917,7 @@ class Parser
 
                $username = $user->getName();
                $nickname = $user->getOption( 'nickname' );
-               $nickname = $nickname === '' ? $username : $nickname;
+               $nickname = $nickname === null ? $username : $nickname;
 
                if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
                        $nickname = $username;
@@ -4064,6 +4176,25 @@ class Parser
        }
 
        /**
+        * Create a tag function, e.g. <test>some stuff</test>.
+        * Unlike tag hooks, tag functions are parsed at preprocessor level.
+        * Unlike parser functions, their content is not preprocessed.
+        */
+       function setFunctionTagHook( $tag, $callback, $flags ) {
+               $tag = strtolower( $tag );
+               $old = isset( $this->mFunctionTagHooks[$tag] ) ?
+                       $this->mFunctionTagHooks[$tag] : null;
+               $this->mFunctionTagHooks[$tag] = array( $callback, $flags );
+
+               if( !in_array( $tag, $this->mStripList ) ) {
+                       $this->mStripList[] = $tag;
+               }
+
+               return $old;
+       }
+
+       /**
+        * 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.
@@ -4225,11 +4356,13 @@ class Parser
                #  * none       same, but not aligned
                #  * ___px      scale to ___ pixels width, no aligning. e.g. use in taxobox
                #  * center     center the image
-               #  * framed     Keep original image size, no magnify-button.
+               #  * frame      Keep original image size, no magnify-button.
+               #  * framed     Same as "frame"
                #  * frameless  like 'thumb' but without a frame. Keeps user preferences for width
                #  * upright    reduce width for upright images, rounded to full __0 px
                #  * border     draw a 1px border around the image
                #  * alt        Text for HTML alt attribute (defaults to empty)
+               #  * link       Set the target of the image link. Can be external, interwiki, or local 
                # vertical-align values (no % or length right now):
                #  * baseline
                #  * sub
@@ -4253,15 +4386,7 @@ class Parser
 
                # Get the file
                $imagename = $title->getDBkey();
-               if ( isset( $this->mFileCache[$imagename][$time] ) ) {
-                       $file = $this->mFileCache[$imagename][$time];
-               } else {
-                       $file = wfFindFile( $title, $time );
-                       if ( count( $this->mFileCache ) > 1000 ) {
-                               $this->mFileCache = array();
-                       }
-                       $this->mFileCache[$imagename][$time] = $file;
-               }
+               $file = wfFindFile( $title, array( 'time' => $time ) );
                # Get parameter map
                $handler = $file ? $file->getHandler() : false;
 
@@ -4365,7 +4490,11 @@ class Parser
 
                $params['frame']['caption'] = $caption;
 
-               $params['frame']['title'] = $this->stripAltText( $caption, $holders );
+               # Will the image be presented in a frame, with the caption below?
+               $imageIsFramed = isset( $params['frame']['frame'] ) ||
+                                isset( $params['frame']['framed'] ) ||
+                                isset( $params['frame']['thumbnail'] ) ||
+                                isset( $params['frame']['manualthumb'] );
 
                # In the old days, [[Image:Foo|text...]] would set alt text.  Later it
                # came to also set the caption, ordinary text after the image -- which
@@ -4383,11 +4512,27 @@ class Parser
                # named parameter entirely for images without a caption; adding an ex-
                # plicit caption= parameter and preserving the old magic unnamed para-
                # meter for BC; ...
-               if( $caption !== '' && !isset( $params['frame']['alt'] )
-               && !isset( $params['frame']['framed'] )
-               && !isset( $params['frame']['thumbnail'] )
-               && !isset( $params['frame']['manualthumb'] ) ) {
-                       $params['frame']['alt'] = $params['frame']['title'];
+               if ( $imageIsFramed ) { # Framed image
+                       if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
+                               # No caption or alt text, add the filename as the alt text so
+                               # that screen readers at least get some description of the image
+                               $params['frame']['alt'] = $title->getText();
+                       }
+                       # Do not set $params['frame']['title'] because tooltips don't make sense
+                       # for framed images
+               } else { # Inline image
+                       if ( !isset( $params['frame']['alt'] ) ) {
+                               # No alt text, use the "caption" for the alt text
+                               if ( $caption !== '') {
+                                       $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
+                               } else {
+                                       # No caption, fall back to using the filename for the
+                                       # alt text
+                                       $params['frame']['alt'] = $title->getText();
+                               }
+                       }
+                       # Use the "caption" for the tooltip text
+                       $params['frame']['title'] = $this->stripAltText( $caption, $holders );
                }
 
                wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );