Fixed issues with careless use of Sanitizer::decodeCharReferences(), added a parser...
[lhc/web/wiklou.git] / includes / Parser.php
index 4564871..45ef8a1 100644 (file)
@@ -47,17 +47,28 @@ define( 'STRIP_COMMENTS', 'HTMLCommentStrip' );
 define( 'HTTP_PROTOCOLS', 'http:\/\/|https:\/\/' );
 # Everything except bracket, space, or control characters
 define( 'EXT_LINK_URL_CLASS', '[^][<>"\\x00-\\x20\\x7F]' );
-# Including space
-define( 'EXT_LINK_TEXT_CLASS', '[^\]\\x00-\\x1F\\x7F]' );
+# Including space, but excluding newlines
+define( 'EXT_LINK_TEXT_CLASS', '[^\]\\x0a\\x0d]' );
 define( 'EXT_IMAGE_FNAME_CLASS', '[A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]' );
 define( 'EXT_IMAGE_EXTENSIONS', 'gif|png|jpg|jpeg' );
-define( 'EXT_LINK_BRACKETED',  '/\[(\b(' . wfUrlProtocols() . ')'.EXT_LINK_URL_CLASS.'+) *('.EXT_LINK_TEXT_CLASS.'*?)\]/S' );
+define( 'EXT_LINK_BRACKETED',  '/\[(\b(' . wfUrlProtocols() . ')'.
+       EXT_LINK_URL_CLASS.'+) *('.EXT_LINK_TEXT_CLASS.'*?)\]/S' );
 define( 'EXT_IMAGE_REGEX',
        '/^('.HTTP_PROTOCOLS.')'.  # Protocol
        '('.EXT_LINK_URL_CLASS.'+)\\/'.  # Hostname and path
        '('.EXT_IMAGE_FNAME_CLASS.'+)\\.((?i)'.EXT_IMAGE_EXTENSIONS.')$/S' # Filename
 );
 
+// State constants for the definition list colon extraction
+define( 'MW_COLON_STATE_TEXT', 0 );
+define( 'MW_COLON_STATE_TAG', 1 );
+define( 'MW_COLON_STATE_TAGSTART', 2 );
+define( 'MW_COLON_STATE_CLOSETAG', 3 );
+define( 'MW_COLON_STATE_TAGSLASH', 4 );
+define( 'MW_COLON_STATE_COMMENT', 5 );
+define( 'MW_COLON_STATE_COMMENTDASH', 6 );
+define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 );
+
 /**
  * PHP Parser
  *
@@ -122,6 +133,7 @@ class Parser
                $this->mTagHooks = array();
                $this->mFunctionHooks = array();
                $this->clearState();
+               $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
        }
 
        /**
@@ -151,12 +163,23 @@ class Parser
                        'titles' => array()
                );
                $this->mRevisionId = null;
-               $this->mUniqPrefix = 'UNIQ' . Parser::getRandomString();
+               
+               /**
+                * Prefix for temporary replacement strings for the multipass parser.
+                * \x07 should never appear in input as it's disallowed in XML.
+                * Using it at the front also gives us a little extra robustness
+                * since it shouldn't match when butted up against identifier-like
+                * string constructs.
+                */
+               $this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
 
                # Clear these on every parse, bug 4549
                $this->mTemplates = array();
                $this->mTemplatePath = array();
 
+               $this->mShowToc = true;
+               $this->mForceTocPosition = false;
+               
                wfRunHooks( 'ParserClearState', array( &$this ) );
        }
 
@@ -226,7 +249,6 @@ class Parser
                        '/(.) (?=\\?|:|;|!|\\302\\273)/' => '\\1&nbsp;\\2',
                        # french spaces, Guillemet-right
                        '/(\\302\\253) /' => '\\1&nbsp;',
-                       '/<center *>(.*)<\\/center *>/i' => '<div class="center">\\1</div>',
                );
                $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
 
@@ -299,99 +321,86 @@ class Parser
        function getOptions() { return $this->mOptions; }
 
        /**
-        * Replaces all occurrences of <$tag>content</$tag> in the text
-        * with a random marker and returns the new text. the output parameter
-        * $content will be an associative array filled with data on the form
-        * $unique_marker => content.
+        * Replaces all occurrences of HTML-style comments and the given tags
+        * in the text with a random marker and returns teh next text. The output
+        * parameter $matches will be an associative array filled with data in
+        * the form:
+        *   'UNIQ-xxxxx' => array(
+        *     'element',
+        *     'tag content',
+        *     array( 'param' => 'x' ),
+        *     '<element param="x">tag content</element>' ) )
         *
-        * If $content is already set, the additional entries will be appended
-        * If $tag is set to STRIP_COMMENTS, the function will extract
-        * <!-- HTML comments -->
+        * @param $elements list of element names. Comments are always extracted.
+        * @param $text Source text string.
+        * @param $uniq_prefix
         *
         * @private
         * @static
         */
-       function extractTagsAndParams($tag, $text, &$content, &$tags, &$params, $uniq_prefix = ''){
-               $rnd = $uniq_prefix . '-' . $tag . Parser::getRandomString();
-               if ( !$content ) {
-                       $content = array( );
-               }
+       function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
+               $rand = Parser::getRandomString();
                $n = 1;
                $stripped = '';
+               $matches = array();
 
-               if ( !$tags ) {
-                       $tags = array( );
-               }
-
-               if ( !$params ) {
-                       $params = array( );
-               }
-
-               if( $tag == STRIP_COMMENTS ) {
-                       $start = '/<!--()/';
-                       $end   = '/-->/';
-               } else {
-                       $start = "/<$tag(\\s+[^>]*|\\s*\/?)>/i";
-                       $end   = "/<\\/$tag\\s*>/i";
-               }
+               $taglist = implode( '|', $elements );
+               $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
 
                while ( '' != $text ) {
                        $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
                        $stripped .= $p[0];
-                       if( count( $p ) < 3 ) {
+                       if( count( $p ) < 5 ) {
                                break;
                        }
-                       $attributes = $p[1];
-                       $inside     = $p[2];
-
-                       // If $attributes ends with '/', we have an empty element tag, <tag />
-                       if( $tag != STRIP_COMMENTS && substr( $attributes, -1 ) == '/' ) {
-                               $attributes = substr( $attributes, 0, -1);
-                               $empty = '/';
+                       if( count( $p ) > 5 ) {
+                               // comment
+                               $element    = $p[4];
+                               $attributes = '';
+                               $close      = '';
+                               $inside     = $p[5];
                        } else {
-                               $empty = '';
+                               // tag
+                               $element    = $p[1];
+                               $attributes = $p[2];
+                               $close      = $p[3];
+                               $inside     = $p[4];
                        }
 
-                       $marker = $rnd . sprintf('%08X', $n++);
+                       $marker = "$uniq_prefix-$element-$rand" . sprintf('%08X', $n++) . '-QINU';
                        $stripped .= $marker;
 
-                       $tags[$marker] = "<$tag$attributes$empty>";
-                       $params[$marker] = Sanitizer::decodeTagAttributes( $attributes );
-
-                       if ( $empty === '/' ) {
+                       if ( $close === '/>' ) {
                                // Empty element tag, <tag />
-                               $content[$marker] = null;
+                               $content = null;
                                $text = $inside;
+                               $tail = null;
                        } else {
-                               $q = preg_split( $end, $inside, 2 );
-                               $content[$marker] = $q[0];
-                               if( count( $q ) < 2 ) {
+                               if( $element == '!--' ) {
+                                       $end = '/(-->)/';
+                               } else {
+                                       $end = "/(<\\/$element\\s*>)/i";
+                               }
+                               $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
+                               $content = $q[0];
+                               if( count( $q ) < 3 ) {
                                        # No end tag -- let it run out to the end of the text.
-                                       break;
+                                       $tail = '';
+                                       $text = '';
                                } else {
-                                       $text = $q[1];
+                                       $tail = $q[1];
+                                       $text = $q[2];
                                }
                        }
+                       
+                       $matches[$marker] = array( $element,
+                               $content,
+                               Sanitizer::decodeTagAttributes( $attributes ),
+                               "<$element$attributes$close$content$tail" );
                }
                return $stripped;
        }
 
-       /**
-        * Wrapper function for extractTagsAndParams
-        * for cases where $tags and $params isn't needed
-        * i.e. where tags will never have params, like <nowiki>
-        *
-        * @private
-        * @static
-        */
-       function extractTags( $tag, $text, &$content, $uniq_prefix = '' ) {
-               $dummy_tags = array();
-               $dummy_params = array();
-
-               return Parser::extractTagsAndParams( $tag, $text, $content,
-                       $dummy_tags, $dummy_params, $uniq_prefix );
-       }
-
        /**
         * Strips and renders nowiki, pre, math, hiero
         * If $render is set, performs necessary rendering operations on plugins
@@ -402,148 +411,103 @@ class Parser
         *  will be stripped in addition to other tags. This is important
         *  for section editing, where these comments cause confusion when
         *  counting the sections in the wikisource
+        * 
+        * @param array dontstrip contains tags which should not be stripped;
+        *  used to prevent stipping of <gallery> when saving (fixes bug 2700)
         *
         * @private
         */
-       function strip( $text, &$state, $stripcomments = false ) {
+       function strip( $text, &$state, $stripcomments = false , $dontstrip = array () ) {
                $render = ($this->mOutputType == OT_HTML);
-               $html_content = array();
-               $nowiki_content = array();
-               $math_content = array();
-               $pre_content = array();
-               $comment_content = array();
-               $ext_content = array();
-               $ext_tags = array();
-               $ext_params = array();
-               $gallery_content = array();
 
                # Replace any instances of the placeholders
                $uniq_prefix = $this->mUniqPrefix;
                #$text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text );
-
-               # html
+               $commentState = array();
+               
+               $elements = array_merge(
+                       array( 'nowiki', 'gallery' ),
+                       array_keys( $this->mTagHooks ) );
                global $wgRawHtml;
                if( $wgRawHtml ) {
-                       $text = Parser::extractTags('html', $text, $html_content, $uniq_prefix);
-                       foreach( $html_content as $marker => $content ) {
-                               if ($render ) {
-                                       # Raw and unchecked for validity.
-                                       $html_content[$marker] = $content;
-                               } else {
-                                       $html_content[$marker] = '<html>'.$content.'</html>';
-                               }
-                       }
-               }
-
-               # nowiki
-               $text = Parser::extractTags('nowiki', $text, $nowiki_content, $uniq_prefix);
-               foreach( $nowiki_content as $marker => $content ) {
-                       if( $render ){
-                               $nowiki_content[$marker] = wfEscapeHTMLTagsOnly( $content );
-                       } else {
-                               $nowiki_content[$marker] = '<nowiki>'.$content.'</nowiki>';
-                       }
+                       $elements[] = 'html';
                }
-
-               # math
                if( $this->mOptions->getUseTeX() ) {
-                       $text = Parser::extractTags('math', $text, $math_content, $uniq_prefix);
-                       foreach( $math_content as $marker => $content ){
-                               if( $render ) {
-                                       $math_content[$marker] = renderMath( $content );
-                               } else {
-                                       $math_content[$marker] = '<math>'.$content.'</math>';
-                               }
-                       }
+                       $elements[] = 'math';
                }
-
-               # pre
-               $text = Parser::extractTags('pre', $text, $pre_content, $uniq_prefix);
-               foreach( $pre_content as $marker => $content ){
-                       if( $render ){
-                               $pre_content[$marker] = '<pre>' . wfEscapeHTMLTagsOnly( $content ) . '</pre>';
-                       } else {
-                               $pre_content[$marker] = '<pre>'.$content.'</pre>';
-                       }
-               }
-
-               # gallery
-               $text = Parser::extractTags('gallery', $text, $gallery_content, $uniq_prefix);
-               foreach( $gallery_content as $marker => $content ) {
-                       require_once( 'ImageGallery.php' );
-                       if ( $render ) {
-                               $gallery_content[$marker] = $this->renderImageGallery( $content );
-                       } else {
-                               $gallery_content[$marker] = '<gallery>'.$content.'</gallery>';
-                       }
-               }
-
-               # Comments
-               $text = Parser::extractTags(STRIP_COMMENTS, $text, $comment_content, $uniq_prefix);
-               foreach( $comment_content as $marker => $content ){
-                       $comment_content[$marker] = '<!--'.$content.'-->';
+               
+               # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700)
+               foreach ( $elements AS $k => $v ) {
+                       if ( !in_array ( $v , $dontstrip ) ) continue;
+                       unset ( $elements[$k] );
                }
-
-               # Extensions
-               foreach ( $this->mTagHooks as $tag => $callback ) {
-                       $ext_content[$tag] = array();
-                       $text = Parser::extractTagsAndParams( $tag, $text, $ext_content[$tag],
-                               $ext_tags[$tag], $ext_params[$tag], $uniq_prefix );
-                       foreach( $ext_content[$tag] as $marker => $content ) {
-                               $full_tag = $ext_tags[$tag][$marker];
-                               $params = $ext_params[$tag][$marker];
-                               if ( $render )
-                                       $ext_content[$tag][$marker] = call_user_func_array( $callback, array( $content, $params, &$this ) );
-                               else {
-                                       if ( is_null( $content ) ) {
-                                               // Empty element tag
-                                               $ext_content[$tag][$marker] = $full_tag;
+               
+               $matches = array();
+               $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+               foreach( $matches as $marker => $data ) {
+                       list( $element, $content, $params, $tag ) = $data;
+                       if( $render ) {
+                               $tagName = strtolower( $element );
+                               switch( $tagName ) {
+                               case '!--':
+                                       // Comment
+                                       if( substr( $tag, -3 ) == '-->' ) {
+                                               $output = $tag;
                                        } else {
-                                               $ext_content[$tag][$marker] = "$full_tag$content</$tag>";
+                                               // Unclosed comment in input.
+                                               // Close it so later stripping can remove it
+                                               $output = "$tag-->";
+                                       }
+                                       break;
+                               case 'html':
+                                       if( $wgRawHtml ) {
+                                               $output = $content;
+                                               break;
+                                       }
+                                       // Shouldn't happen otherwise. :)
+                               case 'nowiki':
+                                       $output = wfEscapeHTMLTagsOnly( $content );
+                                       break;
+                               case 'math':
+                                       $output = MathRenderer::renderMath( $content );
+                                       break;
+                               case 'gallery':
+                                       $output = $this->renderImageGallery( $content );
+                                       break;
+                               default:
+                                       if( isset( $this->mTagHooks[$tagName] ) ) {
+                                               $output = call_user_func_array( $this->mTagHooks[$tagName],
+                                                       array( $content, $params, $this ) );
+                                       } else {
+                                               throw new MWException( "Invalid call hook $element" );
                                        }
                                }
+                       } else {
+                               // Just stripping tags; keep the source
+                               $output = $tag;
+                       }
+                       if( !$stripcomments && $element == '!--' ) {
+                               $commentState[$marker] = $output;
+                       } else {
+                               $state[$element][$marker] = $output;
                        }
                }
-
+               
                # Unstrip comments unless explicitly told otherwise.
                # (The comments are always stripped prior to this point, so as to
                # not invoke any extension tags / parser hooks contained within
                # a comment.)
                if ( !$stripcomments ) {
-                       $tempstate = array( 'comment' => $comment_content );
-                       $text = $this->unstrip( $text, $tempstate );
-                       $comment_content = array();
+                       // Put them all back and forget them
+                       $text = strtr( $text, $commentState );
                }
 
-               # Merge state with the pre-existing state, if there is one
-               if ( $state ) {
-                       $state['html'] = $state['html'] + $html_content;
-                       $state['nowiki'] = $state['nowiki'] + $nowiki_content;
-                       $state['math'] = $state['math'] + $math_content;
-                       $state['pre'] = $state['pre'] + $pre_content;
-                       $state['gallery'] = $state['gallery'] + $gallery_content;
-                       $state['comment'] = $state['comment'] + $comment_content;
-
-                       foreach( $ext_content as $tag => $array ) {
-                               if ( array_key_exists( $tag, $state ) ) {
-                                       $state[$tag] = $state[$tag] + $array;
-                               }
-                       }
-               } else {
-                       $state = array(
-                         'html' => $html_content,
-                         'nowiki' => $nowiki_content,
-                         'math' => $math_content,
-                         'pre' => $pre_content,
-                         'gallery' => $gallery_content,
-                         'comment' => $comment_content,
-                       ) + $ext_content;
-               }
                return $text;
        }
 
        /**
-        * restores pre, math, and hiero removed by strip()
+        * Restores pre, math, and other extensions removed by strip()
         *
         * always call unstripNoWiki() after this one
         * @private
@@ -553,20 +517,21 @@ class Parser
                        return $text;
                }
 
-               # Must expand in reverse order, otherwise nested tags will be corrupted
-               foreach( array_reverse( $state, true ) as $tag => $contentDict ) {
+               $replacements = array();
+               foreach( $state as $tag => $contentDict ) {
                        if( $tag != 'nowiki' && $tag != 'html' ) {
-                               foreach( array_reverse( $contentDict, true ) as $uniq => $content ) {
-                                       $text = str_replace( $uniq, $content, $text );
+                               foreach( $contentDict as $uniq => $content ) {
+                                       $replacements[$uniq] = $content;
                                }
                        }
                }
+               $text = strtr( $text, $replacements );
 
                return $text;
        }
 
        /**
-        * always call this after unstrip() to preserve the order
+        * Always call this after unstrip() to preserve the order
         *
         * @private
         */
@@ -575,17 +540,15 @@ class Parser
                        return $text;
                }
 
-               # Must expand in reverse order, otherwise nested tags will be corrupted
-               for ( $content = end($state['nowiki']); $content !== false; $content = prev( $state['nowiki'] ) ) {
-                       $text = str_replace( key( $state['nowiki'] ), $content, $text );
-               }
-
-               global $wgRawHtml;
-               if ($wgRawHtml) {
-                       for ( $content = end($state['html']); $content !== false; $content = prev( $state['html'] ) ) {
-                               $text = str_replace( key( $state['html'] ), $content, $text );
+               $replacements = array();
+               foreach( $state as $tag => $contentDict ) {
+                       if( $tag == 'nowiki' || $tag == 'html' ) {
+                               foreach( $contentDict as $uniq => $content ) {
+                                       $replacements[$uniq] = $content;
+                               }
                        }
                }
+               $text = strtr( $text, $replacements );
 
                return $text;
        }
@@ -600,14 +563,7 @@ class Parser
        function insertStripItem( $text, &$state ) {
                $rnd = $this->mUniqPrefix . '-item' . Parser::getRandomString();
                if ( !$state ) {
-                       $state = array(
-                         'html' => array(),
-                         'nowiki' => array(),
-                         'math' => array(),
-                         'pre' => array(),
-                         'comment' => array(),
-                         'gallery' => array(),
-                       );
+                       $state = array();
                }
                $state['item'][$rnd] = $text;
                return $rnd;
@@ -873,12 +829,21 @@ class Parser
                $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) );
                $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') );
                $text = preg_replace( '/<includeonly>.*?<\/includeonly>/s', '', $text );
-
+               
                $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ) );
+
                $text = $this->replaceVariables( $text, $args );
 
+               // Tables need to come after variable replacement for things to work
+               // properly; putting them before other transformations should keep
+               // exciting things like link expansions from showing up in surprising
+               // places.
+               $text = $this->doTableStuff( $text );
+
                $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
 
+               $text = $this->stripToc( $text );
+               $this->stripNoGallery( $text );
                $text = $this->doHeadings( $text );
                if($this->mOptions->getUseDynamicDates()) {
                        $df =& DateFormatter::getInstance();
@@ -893,7 +858,6 @@ class Parser
                $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text);
 
                $text = $this->doMagicLinks( $text );
-               $text = $this->doTableStuff( $text );
                $text = $this->formatHeadings( $text, $isMain );
 
                wfProfileOut( $fname );
@@ -923,7 +887,7 @@ class Parser
                wfProfileIn( $fname );
                for ( $i = 6; $i >= 1; --$i ) {
                        $h = str_repeat( '=', $i );
-                       $text = preg_replace( "/^{$h}(.+){$h}(\\s|$)/m",
+                       $text = preg_replace( "/^{$h}(.+){$h}\\s*$/m",
                          "<h{$i}>\\1</h{$i}>\\2", $text );
                }
                wfProfileOut( $fname );
@@ -1161,8 +1125,8 @@ class Parser
 
                        # No link text, e.g. [http://domain.tld/some.link]
                        if ( $text == '' ) {
-                               # Autonumber if allowed
-                               if ( strpos( HTTP_PROTOCOLS, str_replace('/','\/', $protocol) ) !== false ) {
+                               # Autonumber if allowed. See bug #5918
+                               if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
                                        $text = '[' . ++$this->mAutonumber . ']';
                                        $linktype = 'autonumber';
                                } else {
@@ -1178,9 +1142,12 @@ class Parser
 
                        $text = $wgContLang->markNoConversion($text);
 
-                       # Replace &amp; from obsolete syntax with &.
-                       # All HTML entities will be escaped by makeExternalLink()
-                       $url = str_replace( '&amp;', '&', $url );
+                       # Normalize any HTML entities in input. They will be
+                       # re-escaped by makeExternalLink().
+                       $url = Sanitizer::decodeCharReferences( $url );
+                       
+                       # Escape any control characters introduced by the above step
+                       $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url );
 
                        # Process the trail (i.e. everything after this link up until start of the next link),
                        # replacing any non-bracketed links
@@ -1190,7 +1157,7 @@ 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 ) . $dtrail . $trail;
+                       $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail;
 
                        # Register link in the output object.
                        # Replace unnecessary URL escape codes with the referenced character
@@ -1261,16 +1228,18 @@ class Parser
                                        $url = substr( $url, 0, -$numSepChars );
                                }
 
-                               # Replace &amp; from obsolete syntax with &.
-                               # All HTML entities will be escaped by makeExternalLink()
-                               # or maybeMakeExternalImage()
-                               $url = str_replace( '&amp;', '&', $url );
+                               # Normalize any HTML entities in input. They will be
+                               # re-escaped by makeExternalLink() or maybeMakeExternalImage()
+                               $url = Sanitizer::decodeCharReferences( $url );
+                               
+                               # Escape any control characters introduced by the above step
+                               $url = preg_replace( '/[\][<>"\\x00-\\x20\\x7F]/e', "urlencode('\\0')", $url );
 
                                # Is this an external image?
                                $text = $this->maybeMakeExternalImage( $url );
                                if ( $text === false ) {
                                        # Not an image, make a link
-                                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free' );
+                                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
                                        # Register it in the output object...
                                        # Replace unnecessary URL escape codes with their equivalent characters
                                        $pasteurized = Parser::replaceUnusualEscapes( $url );
@@ -1376,7 +1345,7 @@ class Parser
                $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
 
                if( is_null( $this->mTitle ) ) {
-                       wfDebugDieBacktrace( 'nooo' );
+                       throw new MWException( 'nooo' );
                }
                $nottalk = !$this->mTitle->isTalkPage();
 
@@ -1573,6 +1542,7 @@ class Parser
                                                $sortkey = $text;
                                        }
                                        $sortkey = Sanitizer::decodeCharReferences( $sortkey );
+                                       $sortkey = str_replace( "\n", '', $sortkey );
                                        $sortkey = $wgContLang->convertCategoryKey( $sortkey );
                                        $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
 
@@ -1937,10 +1907,10 @@ class Parser
                                wfProfileIn( "$fname-paragraph" );
                                # No prefix (not in list)--go to paragraph mode
                                // XXX: use a stack for nestable elements like span, table and div
-                               $openmatch = preg_match('/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
+                               $openmatch = preg_match('/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/center|<\\/tr|<\\/td|<\\/th)/iS', $t );
                                $closematch = preg_match(
                                        '/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
-                                       '<td|<th|<div|<\\/div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul)/iS', $t );
+                                       '<td|<th|<div|<\\/div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<center)/iS', $t );
                                if ( $openmatch or $closematch ) {
                                        $paragraphStack = false;
                                        # TODO bug 5718: paragraph closed
@@ -2014,43 +1984,167 @@ class Parser
        }
 
        /**
-        * Split up a string on ':', ignoring any occurences inside
-        * <a>..</a> or <span>...</span>
+        * Split up a string on ':', ignoring any occurences inside tags
+        * to prevent illegal overlapping.
         * @param string $str the string to split
         * @param string &$before set to everything before the ':'
         * @param string &$after set to everything after the ':'
         * return string the position of the ':', or false if none found
         */
        function findColonNoLinks($str, &$before, &$after) {
-               # I wonder if we should make this count all tags, not just <a>
-               # and <span>. That would prevent us from matching a ':' that
-               # comes in the middle of italics other such formatting....
-               # -- Wil
                $fname = 'Parser::findColonNoLinks';
                wfProfileIn( $fname );
-               $pos = 0;
-               do {
-                       $colon = strpos($str, ':', $pos);
-
-                       if ($colon !== false) {
-                               $before = substr($str, 0, $colon);
-                               $after = substr($str, $colon + 1);
-
-                               # Skip any ':' within <a> or <span> pairs
-                               $a = substr_count($before, '<a');
-                               $s = substr_count($before, '<span');
-                               $ca = substr_count($before, '</a>');
-                               $cs = substr_count($before, '</span>');
-
-                               if ($a <= $ca and $s <= $cs) {
-                                       # Tags are balanced before ':'; ok
+               
+               $pos = strpos( $str, ':' );
+               if( $pos === false ) {
+                       // Nothing to find!
+                       wfProfileOut( $fname );
+                       return false;
+               }
+               
+               $lt = strpos( $str, '<' );
+               if( $lt === false || $lt > $pos ) {
+                       // Easy; no tag nesting to worry about
+                       $before = substr( $str, 0, $pos );
+                       $after = substr( $str, $pos+1 );
+                       wfProfileOut( $fname );
+                       return $pos;
+               }
+               
+               // Ugly state machine to walk through avoiding tags.
+               $state = MW_COLON_STATE_TEXT;
+               $stack = 0;
+               $len = strlen( $str );
+               for( $i = 0; $i < $len; $i++ ) {
+                       $c = $str{$i};
+                       
+                       switch( $state ) {
+                       // (Using the number is a performance hack for common cases)
+                       case 0: // MW_COLON_STATE_TEXT:
+                               switch( $c ) {
+                               case "<":
+                                       // Could be either a <start> tag or an </end> tag
+                                       $state = MW_COLON_STATE_TAGSTART;
+                                       break;
+                               case ":":
+                                       if( $stack == 0 ) {
+                                               // We found it!
+                                               $before = substr( $str, 0, $i );
+                                               $after = substr( $str, $i + 1 );
+                                               wfProfileOut( $fname );
+                                               return $i;
+                                       }
+                                       // Embedded in a tag; don't break it.
+                                       break;
+                               default:
+                                       // Skip ahead looking for something interesting
+                                       $colon = strpos( $str, ':', $i );
+                                       if( $colon === false ) {
+                                               // Nothing else interesting
+                                               wfProfileOut( $fname );
+                                               return false;
+                                       }
+                                       $lt = strpos( $str, '<', $i );
+                                       if( $stack === 0 ) {
+                                               if( $lt === false || $colon < $lt ) {
+                                                       // We found it!
+                                                       $before = substr( $str, 0, $colon );
+                                                       $after = substr( $str, $colon + 1 );
+                                                       wfProfileOut( $fname );
+                                                       return $i;
+                                               }
+                                       }
+                                       if( $lt === false ) {
+                                               // Nothing else interesting to find; abort!
+                                               // We're nested, but there's no close tags left. Abort!
+                                               break 2;
+                                       }
+                                       // Skip ahead to next tag start
+                                       $i = $lt;
+                                       $state = MW_COLON_STATE_TAGSTART;
+                               }
+                               break;
+                       case 1: // MW_COLON_STATE_TAG:
+                               // In a <tag>
+                               switch( $c ) {
+                               case ">":
+                                       $stack++;
+                                       $state = MW_COLON_STATE_TEXT;
+                                       break;
+                               case "/":
+                                       // Slash may be followed by >?
+                                       $state = MW_COLON_STATE_TAGSLASH;
                                        break;
+                               default:
+                                       // ignore
+                               }
+                               break;
+                       case 2: // MW_COLON_STATE_TAGSTART:
+                               switch( $c ) {
+                               case "/":
+                                       $state = MW_COLON_STATE_CLOSETAG;
+                                       break;
+                               case "!":
+                                       $state = MW_COLON_STATE_COMMENT;
+                                       break;
+                               case ">":
+                                       // Illegal early close? This shouldn't happen D:
+                                       $state = MW_COLON_STATE_TEXT;
+                                       break;
+                               default:
+                                       $state = MW_COLON_STATE_TAG;
+                               }
+                               break;
+                       case 3: // MW_COLON_STATE_CLOSETAG:
+                               // In a </tag>
+                               if( $c == ">" ) {
+                                       $stack--;
+                                       if( $stack < 0 ) {
+                                               wfDebug( "Invalid input in $fname; too many close tags\n" );
+                                               wfProfileOut( $fname );
+                                               return false;
+                                       }
+                                       $state = MW_COLON_STATE_TEXT;
+                               }
+                               break;
+                       case MW_COLON_STATE_TAGSLASH:
+                               if( $c == ">" ) {
+                                       // Yes, a self-closed tag <blah/>
+                                       $state = MW_COLON_STATE_TEXT;
+                               } else {
+                                       // Probably we're jumping the gun, and this is an attribute
+                                       $state = MW_COLON_STATE_TAG;
+                               }
+                               break;
+                       case 5: // MW_COLON_STATE_COMMENT:
+                               if( $c == "-" ) {
+                                       $state = MW_COLON_STATE_COMMENTDASH;
+                               }
+                               break;
+                       case MW_COLON_STATE_COMMENTDASH:
+                               if( $c == "-" ) {
+                                       $state = MW_COLON_STATE_COMMENTDASHDASH;
+                               } else {
+                                       $state = MW_COLON_STATE_COMMENT;
+                               }
+                               break;
+                       case MW_COLON_STATE_COMMENTDASHDASH:
+                               if( $c == ">" ) {
+                                       $state = MW_COLON_STATE_TEXT;
+                               } else {
+                                       $state = MW_COLON_STATE_COMMENT;
                                }
-                               $pos = $colon + 1;
+                               break;
+                       default:
+                               throw new MWException( "State machine error in $fname" );
                        }
-               } while ($colon !== false);
+               }
+               if( $stack > 0 ) {
+                       wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
+                       return false;
+               }
                wfProfileOut( $fname );
-               return $colon;
+               return false;
        }
 
        /**
@@ -2098,6 +2192,10 @@ class Parser
                                return $this->mTitle->getSubpageText();
                        case MAG_SUBPAGENAMEE:
                                return $this->mTitle->getSubpageUrlForm();
+                       case MAG_BASEPAGENAME:
+                               return $this->mTitle->getBaseText();
+                       case MAG_BASEPAGENAMEE:
+                               return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
                        case MAG_TALKPAGENAME:
                                if( $this->mTitle->canTalk() ) {
                                        $talkPage = $this->mTitle->getTalkPage();
@@ -2121,11 +2219,11 @@ class Parser
                        case MAG_REVISIONID:
                                return $this->mRevisionId;
                        case MAG_NAMESPACE:
-                               return $wgContLang->getNsText( $this->mTitle->getNamespace() );
+                               return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
                        case MAG_NAMESPACEE:
                                return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
                        case MAG_TALKSPACE:
-                               return $this->mTitle->canTalk() ? $this->mTitle->getTalkNsText() : '';
+                               return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
                        case MAG_TALKSPACEE:
                                return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
                        case MAG_SUBJECTSPACE:
@@ -2152,6 +2250,10 @@ class Parser
                                return $varCache[$index] = $wgContLang->formatNum( wfNumberOfUsers() );
                        case MAG_NUMBEROFPAGES:
                                return $varCache[$index] = $wgContLang->formatNum( wfNumberOfPages() );
+                       case MAG_NUMBEROFADMINS:
+                               return $varCache[$index]  = $wgContLang->formatNum( wfNumberOfAdmins() );
+                       case MAG_CURRENTTIMESTAMP:
+                               return $varCache[$index] = wfTimestampNow();
                        case MAG_CURRENTVERSION:
                                global $wgVersion;
                                return $wgVersion;
@@ -2163,6 +2265,11 @@ class Parser
                                return $wgServerName;
                        case MAG_SCRIPTPATH:
                                return $wgScriptPath;
+                       case MAG_DIRECTIONMARK:
+                               return $wgContLang->getDirMark();
+                       case MAG_CONTENTLANGUAGE:
+                               global $wgContLanguageCode;
+                               return $wgContLanguageCode;
                        default:
                                $ret = null;
                                if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) )
@@ -2407,7 +2514,7 @@ class Parser
                wfProfileOut( $fname );
                return $text;
        }
-
+       
        /**
         * Replace magic variables
         * @private
@@ -2575,6 +2682,15 @@ class Parser
                        }
                }
 
+               # URLENCODE
+               if( !$found ) {
+                       $urlencode =& MagicWord::get( MAG_URLENCODE );
+                       if( $urlencode->matchStartAndRemove( $part1 ) ) {
+                               $text = $linestart . urlencode( $part1 );
+                               $found = true;
+                       }
+               }
+
                # LCFIRST, UCFIRST, LC and UC
                if ( !$found ) {
                        $lcfirst =& MagicWord::get( MAG_LCFIRST );
@@ -2618,6 +2734,12 @@ class Parser
 
                        if ( $func !== false ) {
                                $title = Title::newFromText( $part1 );
+                               # Due to order of execution of a lot of bits, the values might be encoded
+                               # before arriving here; if that's true, then the title can't be created
+                               # and the variable will fail. If we can't get a decent title from the first
+                               # attempt, url-decode and try for a second.
+                               if( is_null( $title ) )
+                                       $title = Title::newFromUrl( urldecode( $part1 ) );
                                if ( !is_null( $title ) ) {
                                        if ( $argc > 0 ) {
                                                $text = $linestart . $title->$func( $args[0] );
@@ -2643,8 +2765,9 @@ class Parser
                if ( !$found && $argc >= 2 ) {
                        $mwPluralForm =& MagicWord::get( MAG_PLURAL );
                        if ( $mwPluralForm->matchStartAndRemove( $part1 ) ) {
-                               if ($argc==2) {$args[2]=$args[1];}
-                               $text = $linestart . $lang->convertPlural( $part1, $args[0], $args[1], $args[2]);
+                               while ( count($args) < 5 ) { $args[] = $args[count($args)-1]; }
+                               $text = $linestart . $lang->convertPlural( $part1, $args[0], $args[1],
+                                       $args[2], $args[3], $args[4]);
                                $found = true;
                        }
                }
@@ -2674,12 +2797,13 @@ class Parser
                        $mwWordsToCheck = array( MAG_NUMBEROFPAGES => 'wfNumberOfPages',
                                                                         MAG_NUMBEROFUSERS => 'wfNumberOfUsers',
                                                                         MAG_NUMBEROFARTICLES => 'wfNumberOfArticles',
-                                                                        MAG_NUMBEROFFILES => 'wfNumberOfFiles' );
+                                                                        MAG_NUMBEROFFILES => 'wfNumberOfFiles',
+                                                                        MAG_NUMBEROFADMINS => 'wfNumberOfAdmins' );
                        foreach( $mwWordsToCheck as $word => $func ) {
                                $mwCurrentWord =& MagicWord::get( $word );
                                if( $mwCurrentWord->matchStartAndRemove( $part1 ) ) {
                                        $mwRawSuffix =& MagicWord::get( MAG_RAWSUFFIX );
-                                       if( $mwRawSuffix->match( $args[0] ) ) {
+                                       if( isset( $args[0] ) && $mwRawSuffix->match( $args[0] ) ) {
                                                # Raw and unformatted
                                                $text = $linestart . call_user_func( $func );
                                        } else {
@@ -2690,6 +2814,31 @@ class Parser
                                }
                        }
                }
+               
+               # PAGESINNAMESPACE
+               if( !$found ) {
+                       $mwPagesInNs =& MagicWord::get( MAG_PAGESINNAMESPACE );
+                       if( $mwPagesInNs->matchStartAndRemove( $part1 ) ) {
+                               $found = true;
+                               $count = wfPagesInNs( intval( $part1 ) );
+                               $mwRawSuffix =& MagicWord::get( MAG_RAWSUFFIX );
+                               if( isset( $args[0] ) && $mwRawSuffix->match( $args[0] ) ) {
+                                       $text = $linestart . $count;
+                               } else {
+                                       $text = $linestart . $wgContLang->formatNum( $count );
+                               }
+                       }
+               }
+
+               # #LANGUAGE:
+               if( !$found ) {
+                       $mwLanguage =& MagicWord::get( MAG_LANGUAGE );
+                       if( $mwLanguage->matchStartAndRemove( $part1 ) ) {
+                               $lang = $wgContLang->getLanguageName( strtolower( $part1 ) );
+                               $text = $linestart . ( $lang != '' ? $lang : $part1 );
+                               $found = true;
+                       }               
+               }
 
                # Extensions
                if ( !$found && substr( $part1, 0, 1 ) == '#' ) {
@@ -2756,7 +2905,14 @@ class Parser
                        }
                        $title = Title::newFromText( $part1, $ns );
 
+
                        if ( !is_null( $title ) ) {
+                               $checkVariantLink = sizeof($wgContLang->getVariants())>1;
+                               # Check for language variants if the template is not found
+                               if($checkVariantLink && $title->getArticleID() == 0){
+                                       $wgContLang->findVariantLink($part1, $title);
+                               }
+
                                if ( !$title->isExternal() ) {
                                        # Check for excessive inclusion
                                        $dbk = $title->getPrefixedDBkey();
@@ -2802,7 +2958,11 @@ class Parser
                                # Use the original $piece['title'] not the mangled $part1, so that
                                # modifiers such as RAW: produce separate cache entries
                                if( $found ) {
-                                       $this->mTemplates[$piece['title']] = $text;
+                                       if( $isHTML ) {
+                                               // A special page; don't store it in the template cache.
+                                       } else {
+                                               $this->mTemplates[$piece['title']] = $text;
+                                       }
                                        $text = $linestart . $text;
                                }
                        }
@@ -3030,6 +3190,41 @@ class Parser
                }
        }
 
+       /**
+        * Detect __NOGALLERY__ magic word and set a placeholder
+        */
+       function stripNoGallery( &$text ) {
+               # if the string __NOGALLERY__ (not case-sensitive) occurs in the HTML,
+               # do not add TOC
+               $mw = MagicWord::get( MAG_NOGALLERY );
+               $this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ;
+       }
+
+       /**
+        * Detect __TOC__ magic word and set a placeholder
+        */
+       function stripToc( $text ) {
+               # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
+               # do not add TOC
+               $mw = MagicWord::get( MAG_NOTOC );
+               if( $mw->matchAndRemove( $text ) ) {
+                       $this->mShowToc = false;
+               }
+               
+               $mw = MagicWord::get( MAG_TOC );
+               if( $mw->match( $text ) ) {
+                       $this->mShowToc = true;
+                       $this->mForceTocPosition = true;
+                       
+                       // Set a placeholder. At the end we'll fill it in with the TOC.
+                       $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
+                       
+                       // Only keep the first one.
+                       $text = $mw->replace( '', $text );
+               }
+               return $text;
+       }
+
        /**
         * This function accomplishes several tasks:
         * 1) Auto-number headings if that option is enabled
@@ -3048,8 +3243,6 @@ class Parser
                global $wgMaxTocLevel, $wgContLang;
 
                $doNumberHeadings = $this->mOptions->getNumberHeadings();
-               $doShowToc = true;
-               $forceTocHere = false;
                if( !$this->mTitle->userCanEdit() ) {
                        $showEditLink = 0;
                } else {
@@ -3061,21 +3254,15 @@ class Parser
                if( $esw->matchAndRemove( $text ) ) {
                        $showEditLink = 0;
                }
-               # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
-               # do not add TOC
-               $mw =& MagicWord::get( MAG_NOTOC );
-               if( $mw->matchAndRemove( $text ) ) {
-                       $doShowToc = false;
-               }
 
                # Get all headlines for numbering them and adding funky stuff like [edit]
                # links - this is for later, but we need the number of headlines right now
                $numMatches = preg_match_all( '/<H([1-6])(.*?'.'>)(.*?)<\/H[1-6] *>/i', $text, $matches );
 
                # if there are fewer than 4 headlines in the article, do not show TOC
-               if( $numMatches < 4 ) {
-                       $doShowToc = false;
-               }
+               # unless it's been explicitly enabled.
+               $enoughToc = $this->mShowToc &&
+                       (($numMatches >= 4) || $this->mForceTocPosition);
 
                # Allow user to stipulate that a page should have a "new section"
                # link added via __NEWSECTIONLINK__
@@ -3083,25 +3270,17 @@ class Parser
                if( $mw->matchAndRemove( $text ) )
                        $this->mOutput->setNewSection( true );
 
-               # if the string __TOC__ (not case-sensitive) occurs in the HTML,
-               # override above conditions and always show TOC at that place
-
-               $mw =& MagicWord::get( MAG_TOC );
-               if($mw->match( $text ) ) {
-                       $doShowToc = true;
-                       $forceTocHere = true;
-               } else {
-                       # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
-                       # override above conditions and always show TOC above first header
-                       $mw =& MagicWord::get( MAG_FORCETOC );
-                       if ($mw->matchAndRemove( $text ) ) {
-                               $doShowToc = true;
-                       }
+               # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
+               # override above conditions and always show TOC above first header
+               $mw =& MagicWord::get( MAG_FORCETOC );
+               if ($mw->matchAndRemove( $text ) ) {
+                       $this->mShowToc = true;
+                       $enoughToc = true;
                }
 
                # Never ever show TOC if no headers
                if( $numMatches < 1 ) {
-                       $doShowToc = false;
+                       $enoughToc = false;
                }
 
                # We need this to perform operations on the HTML
@@ -3143,7 +3322,7 @@ class Parser
                        }
                        $level = $matches[1][$headlineCount];
 
-                       if( $doNumberHeadings || $doShowToc ) {
+                       if( $doNumberHeadings || $enoughToc ) {
 
                                if ( $level > $prevlevel ) {
                                        # Increase TOC level
@@ -3239,7 +3418,7 @@ class Parser
                        if($refcount[$headlineCount] > 1 ) {
                                $anchor .= '_' . $refcount[$headlineCount];
                        }
-                       if( $doShowToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
+                       if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
                                $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
                        }
                        if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) {
@@ -3260,7 +3439,7 @@ class Parser
                                $sectionCount++;
                }
 
-               if( $doShowToc ) {
+               if( $enoughToc ) {
                        if( $toclevel<$wgMaxTocLevel ) {
                                $toc .= $sk->tocUnindent( $toclevel - 1 );
                        }
@@ -3282,8 +3461,8 @@ class Parser
                                # $full .= $sk->editSectionLink(0);
                        }
                        $full .= $block;
-                       if( $doShowToc && !$i && $isMain && !$forceTocHere) {
-                       # Top anchor now in skin
+                       if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
+                               # Top anchor now in skin
                                $full = $full.$toc;
                        }
 
@@ -3292,9 +3471,8 @@ class Parser
                        }
                        $i++;
                }
-               if($forceTocHere) {
-                       $mw =& MagicWord::get( MAG_TOC );
-                       return $mw->replace( $toc, $full );
+               if( $this->mForceTocPosition ) {
+                       return str_replace( '<!--MWTOC-->', $toc, $full );
                } else {
                        return $full;
                }
@@ -3422,7 +3600,7 @@ class Parser
                                $url = wfMsg( $urlmsg, $id);
                                $sk =& $this->mOptions->getSkin();
                                $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
-                               $text .= "<a href='{$url}'{$la}>{$keyword}{$id}</a>{$x}";
+                               $text .= "<a href=\"{$url}\"{$la}>{$keyword}{$id}</a>{$x}";
                        }
 
                        /* Check if the next RFC keyword is preceed by [[ */
@@ -3457,7 +3635,7 @@ class Parser
                        "\r\n" => "\n",
                );
                $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
-               $text = $this->strip( $text, $stripState, true );
+               $text = $this->strip( $text, $stripState, true, array( 'gallery' ) );
                $text = $this->pstPass2( $text, $stripState, $user );
                $text = $this->unstrip( $text, $stripState );
                $text = $this->unstripNoWiki( $text, $stripState );
@@ -3491,7 +3669,7 @@ class Parser
                $text = $this->replaceVariables( $text );
                
                # Strip out <nowiki> etc. added via replaceVariables
-               $text = $this->strip( $text, $stripState );
+               $text = $this->strip( $text, $stripState, false, array( 'gallery' ) );
        
                # Signatures
                $sigText = $this->getUserSig( $user );
@@ -3668,6 +3846,7 @@ class Parser
         * @return The old value of the mTagHooks array associated with the hook
         */
        function setHook( $tag, $callback ) {
+               $tag = strtolower( $tag );
                $oldVal = @$this->mTagHooks[$tag];
                $this->mTagHooks[$tag] = $callback;
 
@@ -3903,6 +4082,19 @@ class Parser
                return $matches[0];
        }
 
+       /**
+        * Tag hook handler for 'pre'.
+        */
+       function renderPreTag( $text, $attribs, $parser ) {
+               // Backwards-compatibility hack
+               $content = preg_replace( '!<nowiki>(.*?)</nowiki>!is', '\\1', $text );
+               
+               $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
+               return wfOpenElement( 'pre', $attribs ) .
+                       wfEscapeHTMLTagsOnly( $content ) .
+                       '</pre>';
+       }
+       
        /**
         * Renders an image gallery from a text with one line per image.
         * text labels may be given by using |-style alternative text. E.g.
@@ -3913,13 +4105,10 @@ class Parser
         * 'A tree'.
         */
        function renderImageGallery( $text ) {
-               # Setup the parser
-               $parserOptions = new ParserOptions;
-               $localParser = new Parser();
-
                $ig = new ImageGallery();
                $ig->setShowBytes( false );
                $ig->setShowFilename( false );
+               $ig->setParsing();
                $lines = explode( "\n", $text );
 
                foreach ( $lines as $line ) {
@@ -3941,7 +4130,12 @@ class Parser
                                $label = '';
                        }
 
-                       $pout = $localParser->parse( $label , $this->mTitle, $parserOptions );
+                       $pout = $this->parse( $label,
+                               $this->mTitle,
+                               $this->mOptions,
+                               false, // Strip whitespace...?
+                               false  // Don't clear state!
+                       );
                        $html = $pout->getText();
 
                        $ig->add( new Image( $nt ), $html );
@@ -4040,6 +4234,7 @@ class Parser
         * shouldn't be cached.
         */
        function disableCache() {
+               wfDebug( "Parser output marked as uncacheable.\n" );
                $this->mOutput->mCacheTime = -1;
        }
 
@@ -4077,6 +4272,165 @@ class Parser
         */
        function getTags() { return array_keys( $this->mTagHooks ); }
        /**#@-*/
+
+
+       /**
+        * Break wikitext input into sections, and either pull or replace
+        * some particular section's text.
+        *
+        * External callers should use the getSection and replaceSection methods.
+        *
+        * @param $text Page wikitext
+        * @param $section Numbered section. 0 pulls the text before the first
+        *                 heading; other numbers will pull the given section
+        *                 along with its lower-level subsections.
+        * @param $mode One of "get" or "replace"
+        * @param $newtext Replacement text for section data.
+        * @return string for "get", the extracted section text.
+        *                for "replace", the whole page with the section replaced.
+        */
+       private function extractSections( $text, $section, $mode, $newtext='' ) {
+               # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
+               # comments to be stripped as well)
+               $striparray = array();
+               
+               $oldOutputType = $this->mOutputType;
+               $oldOptions = $this->mOptions;
+               $this->mOptions = new ParserOptions();
+               $this->mOutputType = OT_WIKI;
+               
+               $striptext = $this->strip( $text, $striparray, true );
+               
+               $this->mOutputType = $oldOutputType;
+               $this->mOptions = $oldOptions;
+
+               # now that we can be sure that no pseudo-sections are in the source,
+               # split it up by section
+               $uniq = preg_quote( $this->uniqPrefix(), '/' );
+               $comment = "(?:$uniq-!--.*?QINU)";
+               $secs = preg_split(
+               /*
+                       "/
+                       ^(
+                       (?:$comment|<\/?noinclude>)* # Initial comments will be stripped
+                       (?:
+                               (=+) # Should this be limited to 6?
+                               .+?  # Section title...
+                               \\2  # Ending = count must match start
+                       |
+                               ^
+                               <h([1-6])\b.*?>
+                               .*?
+                               <\/h\\3\s*>
+                       )
+                       (?:$comment|<\/?noinclude>|\s+)* # Trailing whitespace ok
+                       )$
+                       /mix",
+               */
+                       "/
+                       (
+                               ^
+                               (?:$comment|<\/?noinclude>)* # Initial comments will be stripped
+                               (=+) # Should this be limited to 6?
+                               .+?  # Section title...
+                               \\2  # Ending = count must match start
+                               (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok
+                               $
+                       |
+                               <h([1-6])\b.*?>
+                               .*?
+                               <\/h\\3\s*>
+                       )
+                       /mix",
+                       $striptext, -1,
+                       PREG_SPLIT_DELIM_CAPTURE);
+               
+               if( $mode == "get" ) {
+                       if( $section == 0 ) {
+                               // "Section 0" returns the content before any other section.
+                               $rv = $secs[0];
+                       } else {
+                               $rv = "";
+                       }
+               } elseif( $mode == "replace" ) {
+                       if( $section == 0 ) {
+                               $rv = $newtext . "\n\n";
+                               $remainder = true;
+                       } else {
+                               $rv = $secs[0];
+                               $remainder = false;
+                       }
+               }
+               $count = 0;
+               $sectionLevel = 0;
+               for( $index = 1; $index < count( $secs ); ) {
+                       $headerLine = $secs[$index++];
+                       if( $secs[$index] ) {
+                               // A wiki header
+                               $headerLevel = strlen( $secs[$index++] );
+                       } else {
+                               // An HTML header
+                               $index++;
+                               $headerLevel = intval( $secs[$index++] );
+                       }
+                       $content = $secs[$index++];
+
+                       $count++;
+                       if( $mode == "get" ) {
+                               if( $count == $section ) {
+                                       $rv = $headerLine . $content;
+                                       $sectionLevel = $headerLevel;
+                               } elseif( $count > $section ) {
+                                       if( $sectionLevel && $headerLevel > $sectionLevel ) {
+                                               $rv .= $headerLine . $content;
+                                       } else {
+                                               // Broke out to a higher-level section
+                                               break;
+                                       }
+                               }
+                       } elseif( $mode == "replace" ) {
+                               if( $count < $section ) {
+                                       $rv .= $headerLine . $content;
+                               } elseif( $count == $section ) {
+                                       $rv .= $newtext . "\n\n";
+                                       $sectionLevel = $headerLevel;
+                               } elseif( $count > $section ) {
+                                       if( $headerLevel <= $sectionLevel ) {
+                                               // Passed the section's sub-parts.
+                                               $remainder = true;
+                                       }
+                                       if( $remainder ) {
+                                               $rv .= $headerLine . $content;
+                                       }
+                               }
+                       }
+               }
+               # reinsert stripped tags
+               $rv = $this->unstrip( $rv, $striparray );
+               $rv = $this->unstripNoWiki( $rv, $striparray );
+               $rv = trim( $rv );
+               return $rv;
+       }
+       
+       /**
+        * This function returns the text of a section, specified by a number ($section).
+        * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
+        * the first section before any such heading (section 0).
+        *
+        * If a section contains subsections, these are also returned.
+        *
+        * @param $text String: text to look in
+        * @param $section Integer: section number
+        * @return string text of the requested section
+        */
+       function getSection( $text, $section ) {
+               return $this->extractSections( $text, $section, "get" );
+       }
+       
+       function replaceSection( $oldtext, $section, $text ) {
+               return $this->extractSections( $oldtext, $section, "replace", $text );
+       }
+
 }
 
 /**
@@ -4098,7 +4452,8 @@ class ParserOutput
                $mExternalLinks,    # External link URLs, in the key only
                $mHTMLtitle,            # Display HTML title
                $mSubtitle,                     # Additional subtitle
-               $mNewSection;           # Show a new section link?
+               $mNewSection,           # Show a new section link?
+               $mNoGallery;            # No gallery on category page? (__NOGALLERY__)
 
        function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(),
                $containsOldMagic = false, $titletext = '' )
@@ -4117,6 +4472,7 @@ class ParserOutput
                $this->mHTMLtitle = "" ;
                $this->mSubtitle = "" ;
                $this->mNewSection = false;
+               $this->mNoGallery = false;
        }
 
        function getText()                   { return $this->mText; }
@@ -4129,6 +4485,7 @@ class ParserOutput
        function &getTemplates()             { return $this->mTemplates; }
        function &getImages()                { return $this->mImages; }
        function &getExternalLinks()         { return $this->mExternalLinks; }
+       function getNoGallery()              { return $this->mNoGallery; }
 
        function containsOldMagic()          { return $this->mContainsOldMagic; }
        function setText( $text )            { return wfSetVar( $this->mText, $text ); }
@@ -4337,6 +4694,39 @@ function wfNumberOfPages() {
        return (int)$count;
 }
 
+/**
+ * Return the total number of admins
+ *
+ * @return integer
+ */
+function wfNumberOfAdmins() {
+       static $admins = -1;
+       wfProfileIn( 'wfNumberOfAdmins' );
+       if( $admins == -1 ) {
+               $dbr =& wfGetDB( DB_SLAVE );
+               $admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), 'wfNumberOfAdmins' );
+       }
+       wfProfileOut( 'wfNumberOfAdmins' );
+       return (int)$admins;
+}
+
+/**
+ * Count the number of pages in a particular namespace
+ *
+ * @param $ns Namespace
+ * @return integer
+ */
+function wfPagesInNs( $ns ) {
+       static $pageCount = array();
+       wfProfileIn( 'wfPagesInNs' );
+       if( !isset( $pageCount[$ns] ) ) {
+               $dbr =& wfGetDB( DB_SLAVE );
+               $pageCount[$ns] = $dbr->selectField( 'page', 'COUNT(*)', array( 'page_namespace' => $ns ), 'wfPagesInNs' );
+       }
+       wfProfileOut( 'wfPagesInNs' );
+       return (int)$pageCount[$ns];
+}
+
 /**
  * Get various statistics from the database
  * @private