* (bug 6201) Treat spaces as underscores in parameters to {{ns:}}
[lhc/web/wiklou.git] / includes / Parser.php
index e4b9d23..c6098d5 100644 (file)
@@ -9,7 +9,6 @@
 /** */
 require_once( 'Sanitizer.php' );
 require_once( 'HttpFunctions.php' );
-require_once( 'ImageGallery.php' );
 
 /**
  * Update this version number when the ParserOutput format
@@ -48,11 +47,12 @@ 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
@@ -133,6 +133,7 @@ class Parser
                $this->mTagHooks = array();
                $this->mFunctionHooks = array();
                $this->clearState();
+               $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
        }
 
        /**
@@ -248,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 );
 
@@ -367,7 +367,7 @@ class Parser
                                $inside     = $p[4];
                        }
 
-                       $marker = "$uniq_prefix-$element-$rand" . sprintf('%08X', $n++);
+                       $marker = "$uniq_prefix-$element-$rand" . sprintf('%08X', $n++) . '-QINU';
                        $stripped .= $marker;
 
                        if ( $close === '/>' ) {
@@ -411,10 +411,13 @@ 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);
 
                # Replace any instances of the placeholders
@@ -423,7 +426,7 @@ class Parser
                $commentState = array();
                
                $elements = array_merge(
-                       array( 'nowiki', 'pre', 'gallery' ),
+                       array( 'nowiki', 'gallery' ),
                        array_keys( $this->mTagHooks ) );
                global $wgRawHtml;
                if( $wgRawHtml ) {
@@ -433,10 +436,15 @@ class Parser
                        $elements[] = 'math';
                }
                
-
+               # 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] );
+               }
+               
                $matches = array();
                $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
-               
+
                foreach( $matches as $marker => $data ) {
                        list( $element, $content, $params, $tag ) = $data;
                        if( $render ) {
@@ -462,22 +470,17 @@ class Parser
                                        $output = wfEscapeHTMLTagsOnly( $content );
                                        break;
                                case 'math':
-                                       $output = renderMath( $content );
-                                       break;
-                               case 'pre':
-                                       // Backwards-compatibility hack
-                                       $content = preg_replace( '!<nowiki>(.*?)</nowiki>!is', '\\1', $content );
-                                       $output = '<pre>' . wfEscapeHTMLTagsOnly( $content ) . '</pre>';
+                                       $output = MathRenderer::renderMath( $content );
                                        break;
                                case 'gallery':
-                                       $output = $this->renderImageGallery( $content );
+                                       $output = $this->renderImageGallery( $content, $params );
                                        break;
                                default:
                                        if( isset( $this->mTagHooks[$tagName] ) ) {
                                                $output = call_user_func_array( $this->mTagHooks[$tagName],
                                                        array( $content, $params, $this ) );
                                        } else {
-                                               wfDebugDieBacktrace( "Invalid call hook $element" );
+                                               throw new MWException( "Invalid call hook $element" );
                                        }
                                }
                        } else {
@@ -490,7 +493,7 @@ class Parser
                                $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
@@ -840,6 +843,7 @@ class Parser
                $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();
@@ -883,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 );
@@ -1141,6 +1145,9 @@ class Parser
                        # 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
@@ -1224,6 +1231,9 @@ class Parser
                                # 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 );
@@ -1335,7 +1345,7 @@ class Parser
                $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
 
                if( is_null( $this->mTitle ) ) {
-                       wfDebugDieBacktrace( 'nooo' );
+                       throw new MWException( 'nooo' );
                }
                $nottalk = !$this->mTitle->isTalkPage();
 
@@ -1532,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 );
 
@@ -1896,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|<ol|<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|<\\/ol)/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
@@ -2125,7 +2136,7 @@ class Parser
                                }
                                break;
                        default:
-                               wfDebugDieBacktrace( "State machine error in $fname" );
+                               throw new MWException( "State machine error in $fname" );
                        }
                }
                if( $stack > 0 ) {
@@ -2239,6 +2250,8 @@ 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:
@@ -2254,6 +2267,9 @@ class Parser
                                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 ) ) )
@@ -2400,7 +2416,7 @@ class Parser
                                                                                 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart),
                                                                                 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']),
                                                                                 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'],
-                                                                                'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == '\n')),
+                                                                                'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")),
                                                                                 );
                                                # finally we can call a user callback and replace piece of text
                                                $replaceWith = call_user_func( $matchingCallback, $cbArgs );
@@ -2657,7 +2673,8 @@ class Parser
                                        $text = $linestart . $wgContLang->getNsText( intval( $part1 ) );
                                        $found = true;
                                } else {
-                                       $index = Namespace::getCanonicalIndex( strtolower( $part1 ) );
+                                       $param = str_replace( ' ', '_', strtolower( $part1 ) );
+                                       $index = Namespace::getCanonicalIndex( strtolower( $param ) );
                                        if ( !is_null( $index ) ) {
                                                $text = $linestart . $wgContLang->getNsText( $index );
                                                $found = true;
@@ -2749,8 +2766,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;
                        }
                }
@@ -2780,12 +2798,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 {
@@ -2796,6 +2815,21 @@ 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 ) {
@@ -2812,6 +2846,17 @@ class Parser
                        $colonPos = strpos( $part1, ':' );
                        if ( $colonPos !== false ) {
                                $function = strtolower( substr( $part1, 1, $colonPos - 1 ) );
+                               if ( !isset( $this->mFunctionHooks[$function] ) ) {
+                                       foreach ($this->mFunctionHooks as $key => $value) {
+                                               if( is_int( $key ) ) {
+                                                       $mwExtension =& MagicWord::get( $key );
+                                                       if( $mwExtension->matchVariableStartToEnd( $function ) ) {
+                                                               $function = $key;
+                                                               break;
+                                                       }
+                                               }
+                                       }
+                               }
                                if ( isset( $this->mFunctionHooks[$function] ) ) {
                                        $funcArgs = array_map( 'trim', $args );
                                        $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs );
@@ -2872,7 +2917,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();
@@ -2918,7 +2970,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;
                                }
                        }
@@ -3146,6 +3202,16 @@ 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
         */
@@ -3546,7 +3612,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 [[ */
@@ -3581,7 +3647,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 );
@@ -3615,7 +3681,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 );
@@ -3688,6 +3754,9 @@ class Parser
                        }
                }
 
+               // Make sure nickname doesnt get a sig in a sig
+               $nickname = $this->cleanSigInSig( $nickname );
+
                # If we're still here, make it a link to the user page
                $userpage = $user->getUserPage();
                return( '[[' . $userpage->getPrefixedText() . '|' . wfEscapeWikiText( $nickname ) . ']]' );
@@ -3706,7 +3775,7 @@ class Parser
        /**
         * Clean up signature text
         *
-        * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures
+        * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
         * 2) Substitute all transclusions
         *
         * @param string $text
@@ -3722,12 +3791,22 @@ class Parser
                $substText = '{{' . $substWord->getSynonym( 0 );
 
                $text = preg_replace( $substRegex, $substText, $text );
-               $text = preg_replace( '/~{3,5}/', '', $text );
+               $text = $this->cleanSigInSig( $text );
                $text = $this->replaceVariables( $text );
                
                $this->clearState();    
                return $text;
        }
+
+       /**
+        * Strip ~~~, ~~~~ and ~~~~~ out of signatures
+        * @param string $text
+        * @return string Signature text with /~{3,5}/ removed
+        */
+       function cleanSigInSig( $text ) {
+               $text = preg_replace( '/~{3,5}/', '', $text );
+               return $text;
+       }
        
        /**
         * Set up some variables which are usually set up in parse()
@@ -3816,15 +3895,17 @@ class Parser
         *
         * @public
         *
-        * @param string $name The function name. Function names are case-insensitive.
+        * @param mixed $id The magic word ID, or (deprecated) the function name. Function names are case-insensitive.
         * @param mixed $callback The callback function (and object) to use
         *
         * @return The old callback function for this name, if any
         */
-       function setFunctionHook( $name, $callback ) {
-               $name = strtolower( $name );
-               $oldVal = @$this->mFunctionHooks[$name];
-               $this->mFunctionHooks[$name] = $callback;
+       function setFunctionHook( $id, $callback ) {
+               if( is_string( $id ) ) {
+                       $id = strtolower( $id );
+               }
+               $oldVal = @$this->mFunctionHooks[$id];
+               $this->mFunctionHooks[$id] = $callback;
                return $oldVal;
        }
 
@@ -4028,6 +4109,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.
@@ -4037,13 +4131,17 @@ class Parser
         * labeled 'The number "1"' and
         * 'A tree'.
         */
-       function renderImageGallery( $text ) {
+       function renderImageGallery( $text, $params ) {
                $ig = new ImageGallery();
                $ig->setShowBytes( false );
                $ig->setShowFilename( false );
                $ig->setParsing();
-               $lines = explode( "\n", $text );
+               $ig->useSkin( $this->mOptions->getSkin() );
 
+               if( isset( $params['caption'] ) )
+                       $ig->setCaption( $params['caption'] );
+               
+               $lines = explode( "\n", $text );
                foreach ( $lines as $line ) {
                        # match lines like these:
                        # Image:someimage.jpg|This is some image
@@ -4205,6 +4303,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 );
+       }
+
 }
 
 /**
@@ -4226,7 +4483,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 = '' )
@@ -4245,6 +4503,7 @@ class ParserOutput
                $this->mHTMLtitle = "" ;
                $this->mSubtitle = "" ;
                $this->mNewSection = false;
+               $this->mNoGallery = false;
        }
 
        function getText()                   { return $this->mText; }
@@ -4257,6 +4516,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 ); }
@@ -4465,6 +4725,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