Added an extension, called EditSectionHiliteLink, that highlights the appropriate...
[lhc/web/wiklou.git] / includes / parser / Parser.php
index 032ecaa..fa0b57d 100644 (file)
@@ -103,6 +103,7 @@ 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
@@ -134,6 +135,10 @@ class Parser
                $this->mVarCache = array();
                if ( isset( $conf['preprocessorClass'] ) ) {
                        $this->mPreprocessorClass = $conf['preprocessorClass'];
+               } elseif ( extension_loaded( 'domxml' ) ) {
+                       // PECL extension that conflicts with the core DOM extension (bug 13770)
+                       wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
+                       $this->mPreprocessorClass = 'Preprocessor_Hash';
                } elseif ( extension_loaded( 'dom' ) ) {
                        $this->mPreprocessorClass = 'Preprocessor_DOM';
                } else {
@@ -225,6 +230,7 @@ class Parser
                $this->mHeadings = array();
                $this->mDoubleUnderscores = array();
                $this->mExpensiveFunctionCount = 0;
+               $this->mFileCache = array();
 
                # Fix cloning
                if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
@@ -368,8 +374,8 @@ class Parser
 
                $text = Sanitizer::normalizeCharReferences( $text );
 
-               if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
-                       $text = self::tidy($text);
+               if ( ( $wgUseTidy && $this->mOptions->mTidy ) || $wgAlwaysUseTidy ) {
+                       $text = MWTidy::tidy( $text );
                } else {
                        # attempt to sanitize at least some nesting problems
                        # (bug #2702 and quite a few others)
@@ -642,117 +648,14 @@ class Parser
                $this->mStripState->general->setPair( $rnd, $text );
                return $rnd;
        }
-
+       
        /**
-        * Interface with html tidy, used if $wgUseTidy = true.
-        * If tidy isn't able to correct the markup, the original will be
-        * returned in all its glory with a warning comment appended.
-        *
-        * Either the external tidy program or the in-process tidy extension
-        * will be used depending on availability. Override the default
-        * $wgTidyInternal setting to disable the internal if it's not working.
-        *
-        * @param string $text Hideous HTML input
-        * @return string Corrected HTML output
-        * @public
-        * @static
+        * Interface with html tidy
+        * @deprecated Use MWTidy::tidy()
         */
-       function tidy( $text ) {
-               global $wgTidyInternal;
-               $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
-' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
-'<head><title>test</title></head><body>'.$text.'</body></html>';
-               if( $wgTidyInternal ) {
-                       $correctedtext = self::internalTidy( $wrappedtext );
-               } else {
-                       $correctedtext = self::externalTidy( $wrappedtext );
-               }
-               if( is_null( $correctedtext ) ) {
-                       wfDebug( "Tidy error detected!\n" );
-                       return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
-               }
-               return $correctedtext;
-       }
-
-       /**
-        * Spawn an external HTML tidy process and get corrected markup back from it.
-        *
-        * @private
-        * @static
-        */
-       function externalTidy( $text ) {
-               global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
-               wfProfileIn( __METHOD__ );
-
-               $cleansource = '';
-               $opts = ' -utf8';
-
-               $descriptorspec = array(
-                       0 => array('pipe', 'r'),
-                       1 => array('pipe', 'w'),
-                       2 => array('file', wfGetNull(), 'a')
-               );
-               $pipes = array();
-               if( function_exists('proc_open') ) {
-                       $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
-                       if (is_resource($process)) {
-                               // Theoretically, this style of communication could cause a deadlock
-                               // here. If the stdout buffer fills up, then writes to stdin could
-                               // block. This doesn't appear to happen with tidy, because tidy only
-                               // writes to stdout after it's finished reading from stdin. Search
-                               // for tidyParseStdin and tidySaveStdout in console/tidy.c
-                               fwrite($pipes[0], $text);
-                               fclose($pipes[0]);
-                               while (!feof($pipes[1])) {
-                                       $cleansource .= fgets($pipes[1], 1024);
-                               }
-                               fclose($pipes[1]);
-                               proc_close($process);
-                       }
-               }
-
-               wfProfileOut( __METHOD__ );
-
-               if( $cleansource == '' && $text != '') {
-                       // Some kind of error happened, so we couldn't get the corrected text.
-                       // Just give up; we'll use the source text and append a warning.
-                       return null;
-               } else {
-                       return $cleansource;
-               }
-       }
-
-       /**
-        * Use the HTML tidy PECL extension to use the tidy library in-process,
-        * saving the overhead of spawning a new process.
-        *
-        * 'pear install tidy' should be able to compile the extension module.
-        *
-        * @private
-        * @static
-        */
-       function internalTidy( $text ) {
-               global $wgTidyConf, $IP, $wgDebugTidy;
-               wfProfileIn( __METHOD__ );
-
-               $tidy = new tidy;
-               $tidy->parseString( $text, $wgTidyConf, 'utf8' );
-               $tidy->cleanRepair();
-               if( $tidy->getStatus() == 2 ) {
-                       // 2 is magic number for fatal error
-                       // http://www.php.net/manual/en/function.tidy-get-status.php
-                       $cleansource = null;
-               } else {
-                       $cleansource = tidy_get_output( $tidy );
-               }
-               if ( $wgDebugTidy && $tidy->getStatus() > 0 ) {
-                       $cleansource .= "<!--\nTidy reports:\n" .
-                               str_replace( '-->', '--&gt;', $tidy->errorBuffer ) .
-                               "\n-->";
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $cleansource;
+       public static function tidy( $text ) {
+               wfDeprecated( __METHOD__ );
+               return MWTidy::tidy( $text );   
        }
 
        /**
@@ -983,7 +886,7 @@ class Parser
 
                $text = $this->doDoubleUnderscore( $text );
                $text = $this->doHeadings( $text );
-               if($this->mOptions->getUseDynamicDates()) {
+               if( $this->mOptions->getUseDynamicDates() ) {
                        $df = DateFormatter::getInstance();
                        $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
                }
@@ -993,7 +896,7 @@ class Parser
 
                # replaceInternalLinks may sometimes leave behind
                # absolute URLs, which have to be masked to hide them from replaceExternalLinks
-               $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text);
+               $text = str_replace($this->mUniqPrefix.'NOPARSE', '', $text);
 
                $text = $this->doMagicLinks( $text );
                $text = $this->formatHeadings( $text, $isMain );
@@ -1030,16 +933,16 @@ class Parser
        }
 
        function magicLinkCallback( $m ) {
-               if ( isset( $m[1] ) && strval( $m[1] ) !== '' ) {
+               if ( isset( $m[1] ) && $m[1] !== '' ) {
                        # Skip anchor
                        return $m[0];
-               } elseif ( isset( $m[2] ) && strval( $m[2] ) !== '' ) {
+               } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
                        # Skip HTML element
                        return $m[0];
-               } elseif ( isset( $m[3] ) && strval( $m[3] ) !== '' ) {
+               } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
                        # Free external link
                        return $this->makeFreeExternalLink( $m[0] );
-               } elseif ( isset( $m[4] ) && strval( $m[4] ) !== '' ) {
+               } elseif ( isset( $m[4] ) && $m[4] !== '' ) {
                        # RFC or PMID
                        if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
                                $keyword = 'RFC';
@@ -1057,7 +960,7 @@ class Parser
                        $sk = $this->mOptions->getSkin();
                        $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
                        return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
-               } elseif ( isset( $m[5] ) && strval( $m[5] ) !== '' ) {
+               } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
                        # ISBN
                        $isbn = $m[5];
                        $num = strtr( $isbn, array(
@@ -1114,7 +1017,8 @@ class Parser
                $text = $this->maybeMakeExternalImage( $url );
                if ( $text === false ) {
                        # Not an image, make a link
-                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
+                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', 
+                               $this->getExternalLinkAttribs( $url ) );
                        # Register it in the output object...
                        # Replace unnecessary URL escape codes with their equivalent characters
                        $pasteurized = self::replaceUnusualEscapes( $url );
@@ -1372,7 +1276,8 @@ class Parser
                        if ( $text == '' ) {
                                # Autonumber if allowed. See bug #5918
                                if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
-                                       $text = '[' . ++$this->mAutonumber . ']';
+                                       $langObj = $this->getFunctionLang();
+                                       $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
                                        $linktype = 'autonumber';
                                } else {
                                        # Otherwise just use the URL
@@ -1393,7 +1298,8 @@ class Parser
                        # This means that users can paste URLs directly into the text
                        # Funny characters like &ouml; aren't valid in URLs anyway
                        # This was changed in August 2004
-                       $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail;
+                       $s .= $sk->makeExternalLink( $url, $text, false, $linktype,
+                               $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
 
                        # Register link in the output object.
                        # Replace unnecessary URL escape codes with the referenced character
@@ -1406,6 +1312,44 @@ class Parser
                return $s;
        }
 
+       /**
+        * Get an associative array of additional HTML attributes appropriate for a
+        * particular external link.  This currently may include rel => nofollow
+        * (depending on configuration, namespace, and the URL's domain) and/or a
+        * target attribute (depending on configuration).
+        *
+        * @param string $url Optional URL, to extract the domain from for rel =>
+        *   nofollow if appropriate
+        * @return array Associative array of HTML attributes
+        */
+       function getExternalLinkAttribs( $url = false ) {
+               $attribs = array();
+               global $wgNoFollowLinks, $wgNoFollowNsExceptions;
+               $ns = $this->mTitle->getNamespace();
+               if( $wgNoFollowLinks && !in_array($ns, $wgNoFollowNsExceptions) ) {
+                       $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();
+               }
+               return $attribs;
+       }
+
+
        /**
         * Replace unusual URL escape codes with their equivalent characters
         * @param string
@@ -1442,7 +1386,7 @@ class Parser
 
        /**
         * make an image if it's allowed, either through the global
-        * option or through the exception
+        * option, through the exception, or through the on-wiki whitelist
         * @private
         */
        function maybeMakeExternalImage( $url ) {
@@ -1450,13 +1394,41 @@ class Parser
                $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
                $imagesexception = !empty($imagesfrom);
                $text = false;
+               # $imagesfrom could be either a single string or an array of strings, parse out the latter
+               if( $imagesexception && is_array( $imagesfrom ) ) {
+                       $imagematch = false;
+                       foreach( $imagesfrom as $match ) {
+                               if( strpos( $url, $match ) === 0 ) {
+                                       $imagematch = true;
+                                       break;
+                               }
+                       }
+               } elseif( $imagesexception ) {
+                       $imagematch = (strpos( $url, $imagesfrom ) === 0);
+               } else {
+                       $imagematch = false;
+               }
                if ( $this->mOptions->getAllowExternalImages()
-                    || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
+                    || ( $imagesexception && $imagematch ) ) {
                        if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
                                # Image found
                                $text = $sk->makeExternalImage( $url );
                        }
                }
+               if( !$text && $this->mOptions->getEnableImageWhitelist()
+                        && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
+                       $whitelist = explode( "\n", wfMsgForContent( 'external_image_whitelist' ) );
+                       foreach( $whitelist as $entry ) {
+                               # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
+                               if( strpos( $entry, '#' ) === 0 || $entry === '' )
+                                       continue;
+                               if( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
+                                       # Image matches a whitelist entry
+                                       $text = $sk->makeExternalImage( $url );
+                                       break;
+                               }
+                       }
+               }
                return $text;
        }
 
@@ -1631,7 +1603,7 @@ class Parser
                        wfProfileOut( __METHOD__."-misc" );
                        wfProfileIn( __METHOD__."-title" );
                        $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
-                       if( !$nt ) {
+                       if( $nt === NULL ) {
                                $s .= $prefix . '[[' . $line;
                                wfProfileOut( __METHOD__."-title" );
                                continue;
@@ -1643,7 +1615,7 @@ class Parser
 
                        if ($might_be_img) { # if this is actually an invalid link
                                wfProfileIn( __METHOD__."-might_be_img" );
-                               if ($ns == NS_IMAGE && $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
@@ -1703,7 +1675,7 @@ class Parser
                                }
                                wfProfileOut( __METHOD__."-interwiki" );
 
-                               if ( $ns == NS_IMAGE ) {
+                               if ( $ns == NS_FILE ) {
                                        wfProfileIn( __METHOD__."-image" );
                                        if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
                                                # recursively parse links inside the image caption
@@ -1747,15 +1719,17 @@ class Parser
                        }
 
                        # Self-link checking
-                       if( $nt->getFragment() === '' && $nt->getNamespace() != NS_SPECIAL ) {
+                       if( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
                                if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
                                        $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
                                        continue;
                                }
                        }
 
-                       # Special and Media are pseudo-namespaces; no pages actually exist in them
+                       # NS_MEDIA is a pseudo-namespace for linking directly to a file
+                       # FIXME: Should do batch file existence checks, see comment below
                        if( $ns == NS_MEDIA ) {
+                               wfProfileIn( __METHOD__."-media" );
                                # Give extensions a chance to select the file revision for us
                                $skip = $time = false;
                                wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
@@ -1767,26 +1741,24 @@ class Parser
                                # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
                                $s .= $prefix . $this->armorLinks( $link ) . $trail;
                                $this->mOutput->addImage( $nt->getDBkey() );
+                               wfProfileOut( __METHOD__."-media" );
                                continue;
-                       } elseif( $ns == NS_SPECIAL ) {
-                               if( SpecialPage::exists( $nt->getDBkey() ) ) {
-                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
-                               } else {
-                                       $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
-                               }
-                               continue;
-                       } elseif( $ns == NS_IMAGE ) {
-                               $img = wfFindFile( $nt );
-                               if( $img ) {
-                                       // Force a blue link if the file exists; may be a remote
-                                       // upload on the shared repository, and we want to see its
-                                       // auto-generated page.
-                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
-                                       $this->mOutput->addLink( $nt );
-                                       continue;
-                               }
                        }
-                       $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
+
+                       wfProfileIn( __METHOD__."-always_known" );
+                       # Some titles, such as valid special pages or files in foreign repos, should
+                       # be shown as bluelinks even though they're not included in the page table
+                       #
+                       # FIXME: isAlwaysKnown() can be expensive for file links; we should really do
+                       # batch file existence checks for NS_FILE and NS_MEDIA
+                       if( $iw == '' && $nt->isAlwaysKnown() ) {
+                               $this->mOutput->addLink( $nt );
+                               $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+                       } else {
+                               # Links will be added to the output link list after checking
+                               $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
+                       }
+                       wfProfileOut( __METHOD__."-always_known" );
                }
                wfProfileOut( __METHOD__ );
                return $holders;
@@ -2116,7 +2088,7 @@ class Parser
                                                $inBlockElem = true;
                                        }
                                } else if ( !$inBlockElem && !$this->mInPre ) {
-                                       if ( ' ' == $t{0} and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
+                                       if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
                                                // pre
                                                if ($this->mLastSection !== 'pre') {
                                                        $paragraphStack = false;
@@ -2478,6 +2450,12 @@ class Parser
                                $this->mOutput->setFlag( 'vary-revision' );
                                wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
                                return $this->getRevisionTimestamp();
+                       case 'revisionuser':
+                                // Let the edit saving system know we should parse the page
+                                // *after* a revision ID has been assigned. This is for null edits.
+                               $this->mOutput->setFlag( 'vary-revision' );
+                               wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
+                               return $this->getRevisionUser();
                        case 'namespace':
                                return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
                        case 'namespacee':
@@ -2524,12 +2502,16 @@ class Parser
                                return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() );
                        case 'numberofusers':
                                return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() );
+                       case 'numberofactiveusers':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::activeUsers() );
                        case 'numberofpages':
                                return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
                        case 'numberofadmins':
                                return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::numberingroup('sysop') );
                        case 'numberofedits':
                                return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
+                       case 'numberofviews':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::views() );
                        case 'currenttimestamp':
                                return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
                        case 'localtimestamp':
@@ -2632,11 +2614,10 @@ class Parser
         * @private
         */
        function replaceVariables( $text, $frame = false, $argsOnly = false ) {
-               # Prevent too big inclusions
-               if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
+               # Is there any text? Also, Prevent too big inclusions!
+               if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
                        return $text;
                }
-
                wfProfileIn( __METHOD__ );
 
                if ( $frame === false ) {
@@ -2712,7 +2693,7 @@ class Parser
         * @private
         */
        function braceSubstitution( $piece, $frame ) {
-               global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
+               global $wgContLang, $wgNonincludableNamespaces;
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__.'-setup' );
 
@@ -2827,7 +2808,9 @@ class Parser
 
                                        # Workaround for PHP bug 35229 and similar
                                        if ( !is_callable( $callback ) ) {
-                                               throw new MWException( "Tag hook for $name is not callable\n" );
+                                               wfProfileOut( __METHOD__ . '-pfunc' );
+                                               wfProfileOut( __METHOD__ );
+                                               throw new MWException( "Tag hook for $function is not callable\n" );
                                        }
                                        $result = call_user_func_array( $callback, $allArgs );
                                        $found = true;
@@ -2872,17 +2855,11 @@ 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\">Template loop detected: [[$titleText]]</span>";
-                                       wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
-                               }
                                # Do recursion depth check
                                $limit = $this->mOptions->getMaxTemplateDepth();
                                if ( $frame->depth >= $limit ) {
                                        $found = true;
-                                       $text = "<span class=\"error\">Template recursion depth limit exceeded ($limit)</span>";
+                                       $text = '<span class="error">' . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit ) . '</span>';
                                }
                        }
                }
@@ -2927,6 +2904,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' );
                }
 
@@ -3093,8 +3078,8 @@ class Parser
                        if( $rev ) {
                                $text = $rev->getText();
                        } elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
-                               global $wgLang;
-                               $message = $wgLang->lcfirst( $title->getText() );
+                               global $wgContLang;
+                               $message = $wgContLang->lcfirst( $title->getText() );
                                $text = wfMsgForContentNoTrans( $message );
                                if( wfEmptyMsg( $message, $text ) ) {
                                        $text = false;
@@ -3240,6 +3225,7 @@ class Parser
                                                throw new MWException( '<html> extension tag encountered unexpectedly' );
                                        }
                                case 'nowiki':
+                                       $content = strtr($content, array('-{' => '-&#123;', '}-' => '&#125;-'));
                                        $output = Xml::escapeTagsOnly( $content );
                                        break;
                                case 'math':
@@ -3323,6 +3309,7 @@ class Parser
         * Fills $this->mDoubleUnderscores, returns the modified text
         */
        function doDoubleUnderscore( $text ) {
+               wfProfileIn( __METHOD__ );
                // The position of __TOC__ needs to be recorded
                $mw = MagicWord::get( 'toc' );
                if( $mw->match( $text ) ) {
@@ -3365,7 +3352,7 @@ class Parser
                } elseif( isset( $this->mDoubleUnderscores['index'] ) ) {
                        $this->mOutput->setIndexPolicy( 'index' );
                }
-
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
@@ -3384,17 +3371,18 @@ class Parser
         * @private
         */
        function formatHeadings( $text, $isMain=true ) {
-               global $wgMaxTocLevel, $wgContLang;
+               global $wgMaxTocLevel, $wgContLang, $wgEnforceHtmlIds, $wgSectionContainers;
 
                $doNumberHeadings = $this->mOptions->getNumberHeadings();
-               if( !$this->mTitle->quickUserCan( 'edit' ) ) {
+               $showEditLink = $this->mOptions->getEditSection();
+
+               // Do not call quickUserCan unless necessary
+               if( $showEditLink && !$this->mTitle->quickUserCan( 'edit' ) ) {
                        $showEditLink = 0;
-               } else {
-                       $showEditLink = $this->mOptions->getEditSection();
                }
 
                # Inhibit editsection links if requested in the page
-               if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
+               if ( isset( $this->mDoubleUnderscores['noeditsection'] )  || $this->mOptions->getIsPrintable() ) {
                        $showEditLink = 0;
                }
 
@@ -3414,6 +3402,12 @@ class Parser
                        $this->mOutput->setNewSection( true );
                }
 
+               # Allow user to remove the "new section"
+               # link via __NONEWSECTIONLINK__
+               if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
+                       $this->mOutput->hideNewSection( true );
+               }
+
                # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
                # override above conditions and always show TOC above first header
                if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
@@ -3552,13 +3546,60 @@ class Parser
 
                        # Save headline for section edit hint before it's escaped
                        $headlineHint = $safeHeadline;
-                       $safeHeadline = Sanitizer::escapeId( $safeHeadline );
-                       # HTML names must be case-insensitively unique (bug 10721)
+
+                       if ( $wgEnforceHtmlIds ) {
+                               $legacyHeadline = false;
+                               $safeHeadline = Sanitizer::escapeId( $safeHeadline,
+                                       'noninitial' );
+                       } else {
+                               # For reverse compatibility, provide an id that's
+                               # HTML4-compatible, like we used to.
+                               #
+                               # It may be worth noting, academically, that it's possible for
+                               # the legacy anchor to conflict with a non-legacy headline
+                               # anchor on the page.  In this case likely the "correct" thing
+                               # would be to either drop the legacy anchors or make sure
+                               # they're numbered first.  However, this would require people
+                               # to type in section names like "abc_.D7.93.D7.90.D7.A4"
+                               # manually, so let's not bother worrying about it.
+                               $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
+                                       'noninitial' );
+                               $safeHeadline = Sanitizer::escapeId( $safeHeadline, 'xml' );
+
+                               if ( $legacyHeadline == $safeHeadline ) {
+                                       # No reason to have both (in fact, we can't)
+                                       $legacyHeadline = false;
+                               } elseif ( $legacyHeadline != Sanitizer::escapeId(
+                               $legacyHeadline, 'xml' ) ) {
+                                       # The legacy id is invalid XML.  We used to allow this, but
+                                       # there's no reason to do so anymore.  Backward
+                                       # compatibility will fail slightly in this case, but it's
+                                       # no big deal.
+                                       $legacyHeadline = false;
+                               }
+                       }
+
+                       # HTML names must be case-insensitively unique (bug 10721).  FIXME:
+                       # Does this apply to Unicode characters?  Because we aren't
+                       # handling those here.
                        $arrayKey = strtolower( $safeHeadline );
+                       if ( $legacyHeadline === false ) {
+                               $legacyArrayKey = false;
+                       } else {
+                               $legacyArrayKey = strtolower( $legacyHeadline );
+                       }
 
                        # count how many in assoc. array so we can track dupes in anchors
-                       isset( $refers[$arrayKey] ) ? $refers[$arrayKey]++ : $refers[$arrayKey] = 1;
-                       $refcount[$headlineCount] = $refers[$arrayKey];
+                       if ( isset( $refers[$arrayKey] ) ) {
+                               $refers[$arrayKey]++;
+                       } else {
+                               $refers[$arrayKey] = 1;
+                       }
+                       if ( isset( $refers[$legacyArrayKey] ) ) {
+                               $refers[$legacyArrayKey]++;
+                       } else {
+                               $refers[$legacyArrayKey] = 1;
+                       }
 
                        # Don't number the heading if it is the only one (looks silly)
                        if( $doNumberHeadings && count( $matches[3] ) > 1) {
@@ -3568,8 +3609,12 @@ class Parser
 
                        # Create the anchor for linking from the TOC to the section
                        $anchor = $safeHeadline;
-                       if($refcount[$headlineCount] > 1 ) {
-                               $anchor .= '_' . $refcount[$headlineCount];
+                       $legacyAnchor = $legacyHeadline;
+                       if ( $refers[$arrayKey] > 1 ) {
+                               $anchor .= '_' . $refers[$arrayKey];
+                       }
+                       if ( $legacyHeadline !== false && $refers[$legacyArrayKey] > 1 ) {
+                               $legacyAnchor .= '_' . $refers[$legacyArrayKey];
                        }
                        if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
                                $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
@@ -3587,7 +3632,9 @@ class Parser
                        } else {
                                $editlink = '';
                        }
-                       $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink );
+                       $head[$headlineCount] = $sk->makeHeadline( $level,
+                               $matches['attrib'][$headlineCount], $anchor, $headline,
+                               $editlink, $legacyAnchor );
 
                        $headlineCount++;
                }
@@ -3611,6 +3658,10 @@ class Parser
                $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
                $i = 0;
 
+               if ( $wgSectionContainers ) {
+                       $openDivs = 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
@@ -3625,6 +3676,34 @@ class Parser
                                # Top anchor now in skin
                                $full = $full.$toc;
                        }
+                       
+                       # wrap each section in a div if $wgSectionContainers is set to true
+                       if ( $wgSectionContainers ) {
+                       if( !empty( $head[$i] ) ) { # if there's no next header, then don't try to close out any existing sections here 
+                               # get the level of the next header section
+                                       preg_match('/<H([0-6])/i', $head[$i], $hLevelMatches);
+                               
+                                       if ( count($hLevelMatches) > 0 ) {
+                                               $hLevel = $hLevelMatches[1];
+                                               if ( $i != 0 ) { # we don't have an open div for section 0, so don't try to close it
+                                                       # close any open divs for sections with headers that are <= to the next header level
+                                                       $this->closeSectionContainers( $hLevel, &$currentHLevel, &$full, &$openDivs);
+                                               }
+                                               $currentHLevel = $hLevel;
+                                       }
+                               }
+
+                               # open the div for the next header, if there is one
+                               if ( isset($currentHLevel) && !empty( $head[$i] ) ) {
+                                       $full .= '<div id="section_' . $i . '_container">';
+                                        array_push($openDivs, array($currentHLevel, $i));
+                               }
+
+                               # if we've outputed the last section of the article, close any open divs that are remaining
+                               if ( $i == ( count($blocks) - 1)  && isset($currentHLevel) ) {
+                                       $this->closeSectionContainers( $hLevel, &$currentHLevel, &$full, &$openDivs);
+                               }
+                       }
 
                        if( !empty( $head[$i] ) ) {
                                $full .= $head[$i];
@@ -3638,19 +3717,42 @@ class Parser
                }
        }
 
+       /**
+        * Analyze the header level of the current and next section being parsed to 
+        * determine if any already parsed sections need to be closed
+        *
+        * @param string $hLevel the level of the next header to be parsed
+        * @param string $currentHLevel the level of the last parsed header
+        * @param string $full a reference to the string that stores the output of the parser
+        * @param array $openDivs a reference to the array that stores a list of open section containers
+        * @return true
+        */
+       function closeSectionContainers( $hLevel, &$currentHLevel, &$full, &$openDivs) {
+               while ( $hLevel <= $currentHLevel ) {
+                       $full .= '</div>';
+                       $popped = array_pop($openDivs);
+                       if ( count($openDivs) ) {
+                               $currentHLevel = $openDivs[count($openDivs) - 1][0];
+                       } else {
+                               break;
+                       }
+               }                       
+               return true;
+       }
+       
        /**
         * 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 );
@@ -3690,6 +3792,15 @@ class Parser
                        putenv( 'TZ='.$wgLocaltimezone );
                        $ts = date( 'YmdHis', $unixts );
                        $tz = date( 'T', $unixts );  # might vary on DST changeover!
+
+                       /* Allow translation of timezones trough wiki. date() can return
+                        * whatever crap the system uses, localised or not, so we cannot
+                        * ship premade translations.
+                        */
+                       $key = 'timezone-' . strtolower( trim( $tz ) );
+                       $value = wfMsgForContent( $key );
+                       if ( !wfEmptyMsg( $key, $value ) ) $tz = $value;
+
                        putenv( 'TZ='.$oldtz );
                }
 
@@ -3790,7 +3901,7 @@ class Parser
         * @return mixed An expanded string, or false if invalid.
         */
        function validateSig( $text ) {
-               return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
+               return( Xml::isWellFormedXmlFragment( $text ) ? $text : false );
        }
 
        /**
@@ -4038,7 +4149,7 @@ class Parser
                $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
 
                $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
-               return wfOpenElement( 'pre', $attribs ) .
+               return Xml::openElement( 'pre', $attribs ) .
                        Xml::escapeTagsOnly( $content ) .
                        '</pre>';
        }
@@ -4094,7 +4205,7 @@ class Parser
                        
                        if ( strpos( $matches[0], '%' ) !== false )
                                $matches[1] = urldecode( $matches[1] );
-                       $tp = Title::newFromText( $matches[1]/*, NS_IMAGE*/ );
+                       $tp = Title::newFromText( $matches[1]/*, NS_FILE*/ );
                        $nt =& $tp;
                        if( is_null( $nt ) ) {
                                # Bogus title. Ignore these so we don't bomb out later.
@@ -4111,7 +4222,7 @@ class Parser
                        $ig->add( $nt, $html );
 
                        # Only add real images (bug #5586)
-                       if ( $nt->getNamespace() == NS_IMAGE ) {
+                       if ( $nt->getNamespace() == NS_FILE ) {
                                $this->mOutput->addImage( $nt->getDBkey() );
                        }
                }
@@ -4131,7 +4242,7 @@ class Parser
                                'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
                                        'bottom', 'text-bottom' ),
                                'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
-                                       'upright', 'border' ),
+                                       'upright', 'border', 'link', 'alt' ),
                        );
                        static $internalParamMap;
                        if ( !$internalParamMap ) {
@@ -4167,16 +4278,17 @@ class Parser
        function makeImage( $title, $options, $holders = false ) {
                # Check if the options text is of the form "options|alt text"
                # Options are:
-               #  * thumbnail          make a thumbnail with enlarge-icon and caption, alignment depends on lang
-               #  * left               no resizing, just left align. label is used for alt= only
-               #  * right              same, but right aligned
-               #  * 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.
-               #  * 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
+               #  * thumbnail  make a thumbnail with enlarge-icon and caption, alignment depends on lang
+               #  * left       no resizing, just left align. label is used for alt= only
+               #  * right      same, but right aligned
+               #  * 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.
+               #  * 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)
                # vertical-align values (no % or length right now):
                #  * baseline
                #  * sub
@@ -4198,8 +4310,18 @@ class Parser
                        return $sk->link( $title );
                }
 
+               # 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;
+               }
                # Get parameter map
-               $file = wfFindFile( $title, $time );
                $handler = $file ? $file->getHandler() : false;
 
                list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
@@ -4245,10 +4367,36 @@ class Parser
                                        } else {
                                                # Validate internal parameters
                                                switch( $paramName ) {
-                                               case "manualthumb":
-                                                       /// @fixme - possibly check validity here?
-                                                       /// downstream behavior seems odd with missing manual thumbs.
+                                               case 'manualthumb':
+                                               case 'alt':
+                                                       // @fixme - possibly check validity here for
+                                                       // manualthumb? downstream behavior seems odd with
+                                                       // missing manual thumbs.
                                                        $validated = true;
+                                                       $value = $this->stripAltText( $value, $holders );
+                                                       break;
+                                               case 'link':
+                                                       $chars = self::EXT_LINK_URL_CLASS;
+                                                       $prots = $this->mUrlProtocols;
+                                                       if ( $value === '' ) {
+                                                               $paramName = 'no-link';
+                                                               $value = true;
+                                                               $validated = true;
+                                                       } elseif ( preg_match( "/^$prots/", $value ) ) {
+                                                               if ( preg_match( "/^($prots)$chars+$/", $value, $m ) ) {
+                                                                       $paramName = 'link-url';
+                                                                       $this->mOutput->addExternalLink( $value );
+                                                                       $validated = true;
+                                                               }
+                                                       } else {
+                                                               $linkTitle = Title::newFromText( $value );
+                                                               if ( $linkTitle ) {
+                                                                       $paramName = 'link-title';
+                                                                       $value = $linkTitle;
+                                                                       $this->mOutput->addLink( $linkTitle );
+                                                                       $validated = true;
+                                                               }
+                                                       }
                                                        break;
                                                default:
                                                        // Most other things appear to be empty or numeric...
@@ -4274,23 +4422,32 @@ class Parser
                        $params['frame']['valign'] = key( $params['vertAlign'] );
                }
 
-               # Strip bad stuff out of the alt text
-               # We can't just use replaceLinkHoldersText() here, because if this function
-               # is called from replaceInternalLinks2(), mLinkHolders won't be up to date.
-               if ( $holders ) {
-                       $alt = $holders->replaceText( $caption );
-               } else {
-                       $alt = $this->replaceLinkHoldersText( $caption );
-               }
+               $params['frame']['caption'] = $caption;
 
-               # make sure there are no placeholders in thumbnail attributes
-               # that are later expanded to html- so expand them now and
-               # remove the tags
-               $alt = $this->mStripState->unstripBoth( $alt );
-               $alt = Sanitizer::stripAllTags( $alt );
+               $params['frame']['title'] = $this->stripAltText( $caption, $holders );
 
-               $params['frame']['alt'] = $alt;
-               $params['frame']['caption'] = $caption;
+               # 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
+               # makes no sense, because that just repeats the text multiple times in
+               # screen readers.  It *also* came to set the title attribute.
+               #
+               # Now that we have an alt attribute, we should not set the alt text to
+               # equal the caption: that's worse than useless, it just repeats the
+               # text.  This is the framed/thumbnail case.  If there's no caption, we
+               # use the unnamed parameter for alt text as well, just for the time be-
+               # ing, if the unnamed param is set and the alt param is not.
+               #
+               # For the future, we need to figure out if we want to tweak this more,
+               # e.g., introducing a title= parameter for the title; ignoring the un-
+               # 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'];
+               }
 
                wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
 
@@ -4304,6 +4461,25 @@ class Parser
 
                return $ret;
        }
+       
+       protected function stripAltText( $caption, $holders ) {
+               # Strip bad stuff out of the title (tooltip).  We can't just use
+               # replaceLinkHoldersText() here, because if this function is called
+               # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
+               if ( $holders ) {
+                       $tooltip = $holders->replaceText( $caption );
+               } else {
+                       $tooltip = $this->replaceLinkHoldersText( $caption );
+               }
+
+               # make sure there are no placeholders in thumbnail attributes
+               # that are later expanded to html- so expand them now and
+               # remove the tags
+               $tooltip = $this->mStripState->unstripBoth( $tooltip );
+               $tooltip = Sanitizer::stripAllTags( $tooltip );
+               
+               return $tooltip;
+       }
 
        /**
         * Set a flag in the output object indicating that the content is dynamic and
@@ -4444,7 +4620,11 @@ class Parser
                        // Output the replacement text
                        // Add two newlines on -- trailing whitespace in $newText is conventionally
                        // stripped by the editor, so we need both newlines to restore the paragraph gap
-                       $outText .= $newText . "\n\n";
+                       // Only add trailing whitespace if there is newText
+                       if($newText != "") {
+                               $outText .= $newText . "\n\n";
+                       }
+
                        while ( $node ) {
                                $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
                                $node = $node->getNextSibling();
@@ -4510,6 +4690,22 @@ class Parser
                return $this->mRevisionTimestamp;
        }
 
+       /**
+        * Get the name of the user that edited the last revision
+        */
+       function getRevisionUser() {
+               // if this template is subst: the revision id will be blank,
+               // so just use the current user's name
+               if( $this->mRevisionId ) {
+                       $revision = Revision::newFromId( $this->mRevisionId );
+                       $revuser = $revision->getUserText();
+               } else {
+                       global $wgUser;
+                       $revuser = $wgUser->getName();
+               }
+               return $revuser;
+       }
+
        /**
         * Mutator for $mDefaultSort
         *
@@ -4537,6 +4733,16 @@ class Parser
                }
        }
 
+       /**
+        * Accessor for $mDefaultSort
+        * Unlike getDefaultSort(), will return false if none is set
+        *
+        * @return string or false
+        */
+       public function getCustomDefaultSort() {
+               return $this->mDefaultSort;
+       }
+
        /**
         * Try to guess the section anchor name based on a wikitext fragment
         * presumably extracted from a heading, for example "Header" from
@@ -4651,6 +4857,102 @@ class Parser
                }
                return $out;
        }
+
+       function serialiseHalfParsedText( $text ) {
+               $data = array();
+               $data['text'] = $text;
+
+               // First, find all strip markers, and store their
+               //  data in an array.
+               $stripState = new StripState;
+               $pos = 0;
+               while( ( $start_pos = strpos( $text, $this->mUniqPrefix, $pos ) ) && ( $end_pos = strpos( $text, self::MARKER_SUFFIX, $pos ) ) ) {
+                       $end_pos += strlen( self::MARKER_SUFFIX );
+                       $marker = substr( $text, $start_pos, $end_pos-$start_pos );
+
+                       if ( !empty( $this->mStripState->general->data[$marker] ) ) {
+                               $replaceArray = $stripState->general;
+                               $stripText = $this->mStripState->general->data[$marker];
+                       } elseif ( !empty( $this->mStripState->nowiki->data[$marker] ) ) {
+                               $replaceArray = $stripState->nowiki;
+                               $stripText = $this->mStripState->nowiki->data[$marker];
+                       } else {
+                               throw new MWException( "Hanging strip marker: '$marker'." );
+                       }
+
+                       $replaceArray->setPair( $marker, $stripText );
+                       $pos = $end_pos;
+               }
+               $data['stripstate'] = $stripState;
+
+               // Now, find all of our links, and store THEIR
+               //  data in an array! :)
+               $links = array( 'internal' => array(), 'interwiki' => array() );
+               $pos = 0;
+
+               // Internal links
+               while( ( $start_pos = strpos( $text, '<!--LINK ', $pos ) ) ) {
+                       list( $ns, $trail ) = explode( ':', substr( $text, $start_pos + strlen( '<!--LINK ' ) ), 2 );
+
+                       $ns = trim($ns);
+                       if (empty( $links['internal'][$ns] )) {
+                               $links['internal'][$ns] = array();
+                       }
+
+                       $key = trim( substr( $trail, 0, strpos( $trail, '-->' ) ) );
+                       $links['internal'][$ns][] = $this->mLinkHolders->internals[$ns][$key];
+                       $pos = $start_pos + strlen( "<!--LINK $ns:$key-->" );
+               }
+
+               $pos = 0;
+
+               // Interwiki links
+               while( ( $start_pos = strpos( $text, '<!--IWLINK ', $pos ) ) ) {
+                       $data = substr( $text, $start_pos );
+                       $key = trim( substr( $data, 0, strpos( $data, '-->' ) ) );
+                       $links['interwiki'][] = $this->mLinkHolders->interwiki[$key];
+                       $pos = $start_pos + strlen( "<!--IWLINK $key-->" );
+               }
+               
+               $data['linkholder'] = $links;
+
+               return $data;
+       }
+
+       function unserialiseHalfParsedText( $data, $intPrefix = null /* Unique identifying prefix */ ) {
+               if (!$intPrefix)
+                       $intPrefix = $this->getRandomString();
+               
+               // First, extract the strip state.
+               $stripState = $data['stripstate'];
+               $this->mStripState->general->merge( $stripState->general );
+               $this->mStripState->nowiki->merge( $stripState->nowiki );
+
+               // Now, extract the text, and renumber links
+               $text = $data['text'];
+               $links = $data['linkholder'];
+
+               // Internal...
+               foreach( $links['internal'] as $ns => $nsLinks ) {
+                       foreach( $nsLinks as $key => $entry ) {
+                               $newKey = $intPrefix . '-' . $key;
+                               $this->mLinkHolders->internals[$ns][$newKey] = $entry;
+
+                               $text = str_replace( "<!--LINK $ns:$key-->", "<!--LINK $ns:$newKey-->", $text );
+                       }
+               }
+
+               // Interwiki...
+               foreach( $links['interwiki'] as $key => $entry ) {
+                       $newKey = "$intPrefix-$key";
+                       $this->mLinkHolders->interwikis[$newKey] = $entry;
+
+                       $text = str_replace( "<!--IWLINK $key-->", "<!--IWLINK $newKey-->", $text );
+               }
+
+               // Should be good to go.
+               return $text;
+       }
 }
 
 /**