begin cleanup on magnus' url upload thingy
[lhc/web/wiklou.git] / includes / Parser.php
index e4b9d23..3307191 100644 (file)
@@ -6,11 +6,6 @@
  * @subpackage Parser
  */
 
-/** */
-require_once( 'Sanitizer.php' );
-require_once( 'HttpFunctions.php' );
-require_once( 'ImageGallery.php' );
-
 /**
  * Update this version number when the ParserOutput format
  * changes in an incompatible way, so the parser cache
@@ -18,25 +13,16 @@ require_once( 'ImageGallery.php' );
  */
 define( 'MW_PARSER_VERSION', '1.6.1' );
 
-/**
- * Variable substitution O(N^2) attack
- *
- * Without countermeasures, it would be possible to attack the parser by saving
- * a page filled with a large number of inclusions of large pages. The size of
- * the generated page would be proportional to the square of the input size.
- * Hence, we limit the number of inclusions of any given page, thus bringing any
- * attack back to O(N).
- */
-
-define( 'MAX_INCLUDE_REPEAT', 100 );
-define( 'MAX_INCLUDE_SIZE', 1000000 ); // 1 Million
-
 define( 'RLH_FOR_UPDATE', 1 );
 
 # Allowed values for $mOutputType
 define( 'OT_HTML', 1 );
 define( 'OT_WIKI', 2 );
 define( 'OT_MSG' , 3 );
+define( 'OT_PREPROCESS', 4 );
+
+# Flags for setFunctionHook
+define( 'SFH_NO_HASH', 1 );
 
 # string parameter for extractTags which will cause it
 # to strip HTML comments in addition to regular
@@ -48,11 +34,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
@@ -91,7 +78,8 @@ define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 );
  * settings:
  *  $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
  *  $wgNamespacesWithSubpages, $wgAllowExternalImages*,
- *  $wgLocaltimezone, $wgAllowSpecialInclusion*
+ *  $wgLocaltimezone, $wgAllowSpecialInclusion*, 
+ *  $wgMaxArticleSize*
  *
  *  * only within ParserOptions
  * </pre>
@@ -104,12 +92,13 @@ class Parser
         * @private
         */
        # Persistent:
-       var $mTagHooks, $mFunctionHooks;
+       var $mTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables;
 
        # Cleared with clearState():
        var $mOutput, $mAutonumber, $mDTopen, $mStripState = array();
-       var $mVariables, $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
+       var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
        var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix;
+       var $mIncludeSizes;
        var $mTemplates,        // cache of already loaded templates, avoids
                                // multiple SQL queries for the same string
            $mTemplatePath;     // stores an unsorted hash of all the templates already loaded
@@ -120,6 +109,7 @@ class Parser
        var $mOptions,      // ParserOptions object
                $mTitle,        // Title context, used for self-link rendering and similar things
                $mOutputType,   // Output type, one of the OT_xxx constants
+               $ot,            // Shortcut alias, see setOutputType()
                $mRevisionId;   // ID to display in {{REVISIONID}} tags
 
        /**#@-*/
@@ -132,20 +122,72 @@ class Parser
        function Parser() {
                $this->mTagHooks = array();
                $this->mFunctionHooks = array();
-               $this->clearState();
+               $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
+               $this->mFirstCall = true;
        }
 
+       /**
+        * Do various kinds of initialisation on the first call of the parser
+        */
+       function firstCallInit() {
+               if ( !$this->mFirstCall ) {
+                       return;
+               }
+
+               wfProfileIn( __METHOD__ );
+               global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
+
+               $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
+
+               $this->setFunctionHook( 'ns', array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'urlencode', array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'lcfirst', array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'ucfirst', array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'lc', array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'uc', array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'localurl', array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'localurle', array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'fullurl', array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'fullurle', array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'formatnum', array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'grammar', array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'plural', array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberofpages', array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberofusers', array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberofarticles', array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberoffiles', array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberofadmins', array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'language', array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'padleft', array( 'CoreParserFunctions', 'padleft' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'padright', array( 'CoreParserFunctions', 'padright' ), SFH_NO_HASH );
+
+               if ( $wgAllowDisplayTitle ) {
+                       $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH );
+               }
+               if ( $wgAllowSlowParserFunctions ) {
+                       $this->setFunctionHook( 'pagesinnamespace', array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH );
+               }
+               
+               $this->initialiseVariables();
+
+               $this->mFirstCall = false;
+               wfProfileOut( __METHOD__ );
+       }               
+
        /**
         * Clear Parser state
         *
         * @private
         */
        function clearState() {
+               wfProfileIn( __METHOD__ );
+               if ( $this->mFirstCall ) {
+                       $this->firstCallInit();
+               }
                $this->mOutput = new ParserOutput;
                $this->mAutonumber = 0;
                $this->mLastSection = '';
                $this->mDTopen = false;
-               $this->mVariables = false;
                $this->mIncludeCount = array();
                $this->mStripState = array();
                $this->mArgStack = array();
@@ -178,8 +220,25 @@ class Parser
 
                $this->mShowToc = true;
                $this->mForceTocPosition = false;
-               
+               $this->mIncludeSizes = array(
+                       'pre-expand' => 0,
+                       'post-expand' => 0,
+                       'arg' => 0
+               );
+
                wfRunHooks( 'ParserClearState', array( &$this ) );
+               wfProfileOut( __METHOD__ );
+       }
+
+       function setOutputType( $ot ) {
+               $this->mOutputType = $ot;
+               // Shortcut alias
+               $this->ot = array(
+                       'html' => $ot == OT_HTML,
+                       'wiki' => $ot == OT_WIKI,
+                       'msg' => $ot == OT_MSG,
+                       'pre' => $ot == OT_PREPROCESS,
+               );
        }
 
        /**
@@ -187,7 +246,7 @@ class Parser
         *
         * @public
         */
-       function UniqPrefix() {
+       function uniqPrefix() {
                return $this->mUniqPrefix;
        }
 
@@ -220,8 +279,11 @@ class Parser
 
                $this->mOptions = $options;
                $this->mTitle =& $title;
-               $this->mRevisionId = $revid;
-               $this->mOutputType = OT_HTML;
+               $oldRevisionId = $this->mRevisionId;
+               if( $revid !== null ) {
+                       $this->mRevisionId = $revid;
+               }
+               $this->setOutputType( OT_HTML );
 
                //$text = $this->strip( $text, $this->mStripState );
                // VOODOO MAGIC FIX! Sometimes the above segfaults in PHP5.
@@ -231,12 +293,6 @@ class Parser
                $text = $this->strip( $text, $x );
                wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) );
 
-               # Hook to suspend the parser in this state
-               if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$x ) ) ) {
-                       wfProfileOut( $fname );
-                       return $text ;
-               }
-
                $text = $this->internalParse( $text );
 
                $text = $this->unstrip( $text, $this->mStripState );
@@ -248,7 +304,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 );
 
@@ -274,8 +329,8 @@ class Parser
                } else {
                        # attempt to sanitize at least some nesting problems
                        # (bug #2702 and quite a few others)
-                       $tidyregs = array(      
-                               # ''Something [http://www.cool.com cool''] --> 
+                       $tidyregs = array(
+                               # ''Something [http://www.cool.com cool''] -->
                                # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
                                '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
                                '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
@@ -290,10 +345,10 @@ class Parser
                                '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
                                # remove empty italic or bold tag pairs, some
                                # introduced by rules above
-                               '/<([bi])><\/\\1>/' => '' 
+                               '/<([bi])><\/\\1>/' => '',
                        );
 
-                       $text = preg_replace( 
+                       $text = preg_replace(
                                array_keys( $tidyregs ),
                                array_values( $tidyregs ),
                                $text );
@@ -301,12 +356,62 @@ class Parser
 
                wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
 
+               # Information on include size limits, for the benefit of users who try to skirt them
+               if ( max( $this->mIncludeSizes ) > 1000 ) {
+                       $max = $this->mOptions->getMaxIncludeSize();
+                       $text .= "<!-- \n" .
+                               "Pre-expand include size: {$this->mIncludeSizes['pre-expand']} bytes\n" .
+                               "Post-expand include size: {$this->mIncludeSizes['post-expand']} bytes\n" .
+                               "Template argument size: {$this->mIncludeSizes['arg']} bytes\n" .
+                               "Maximum: $max bytes\n" .
+                               "-->\n";
+               }
                $this->mOutput->setText( $text );
+               $this->mRevisionId = $oldRevisionId;
                wfProfileOut( $fname );
 
                return $this->mOutput;
        }
 
+       /**
+        * Recursive parser entry point that can be called from an extension tag
+        * hook.
+        */
+       function recursiveTagParse( $text ) {
+               wfProfileIn( __METHOD__ );
+               $x =& $this->mStripState;
+               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) );
+               $text = $this->strip( $text, $x );
+               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) );
+               $text = $this->internalParse( $text );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       /**
+        * Expand templates and variables in the text, producing valid, static wikitext.
+        * Also removes comments.
+        */
+       function preprocess( $text, $title, $options ) {
+               wfProfileIn( __METHOD__ );
+               $this->clearState();
+               $this->setOutputType( OT_PREPROCESS );
+               $this->mOptions = $options;
+               $this->mTitle = $title;
+               $x =& $this->mStripState;
+               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) );
+               $text = $this->strip( $text, $x );
+               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) );
+               if ( $this->mOptions->getRemoveComments() ) {
+                       $text = Sanitizer::removeHTMLcomments( $text );
+               }
+               $text = $this->replaceVariables( $text );
+               $text = $this->unstrip( $text, $x );
+               $text = $this->unstripNowiki( $text, $x );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
        /**
         * Get a random string
         *
@@ -320,6 +425,11 @@ class Parser
        function &getTitle() { return $this->mTitle; }
        function getOptions() { return $this->mOptions; }
 
+       function getFunctionLang() {
+               global $wgLang, $wgContLang;
+               return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
+       }
+
        /**
         * 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
@@ -339,8 +449,7 @@ class Parser
         * @static
         */
        function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
-               $rand = Parser::getRandomString();
-               $n = 1;
+               static $n = 1;
                $stripped = '';
                $matches = array();
 
@@ -367,7 +476,7 @@ class Parser
                                $inside     = $p[4];
                        }
 
-                       $marker = "$uniq_prefix-$element-$rand" . sprintf('%08X', $n++);
+                       $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . '-QINU';
                        $stripped .= $marker;
 
                        if ( $close === '/>' ) {
@@ -411,19 +520,21 @@ 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 () ) {
+               wfProfileIn( __METHOD__ );
                $render = ($this->mOutputType == OT_HTML);
 
-               # Replace any instances of the placeholders
                $uniq_prefix = $this->mUniqPrefix;
-               #$text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text );
                $commentState = array();
                
                $elements = array_merge(
-                       array( 'nowiki', 'pre', 'gallery' ),
+                       array( 'nowiki', 'gallery' ),
                        array_keys( $this->mTagHooks ) );
                global $wgRawHtml;
                if( $wgRawHtml ) {
@@ -433,14 +544,20 @@ 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 ) {
                                $tagName = strtolower( $element );
+                               wfProfileIn( __METHOD__."-render-$tagName" );
                                switch( $tagName ) {
                                case '!--':
                                        // Comment
@@ -462,32 +579,35 @@ 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" );
                                        }
                                }
+                               wfProfileOut( __METHOD__."-render-$tagName" );
                        } else {
                                // Just stripping tags; keep the source
                                $output = $tag;
                        }
+
+                       // Unstrip the output, because unstrip() is no longer recursive so 
+                       // it won't do it itself
+                       $output = $this->unstrip( $output, $state );
+
                        if( !$stripcomments && $element == '!--' ) {
                                $commentState[$marker] = $output;
+                       } elseif ( $element == 'html' || $element == 'nowiki' ) {
+                               $state['nowiki'][$marker] = $output;
                        } else {
-                               $state[$element][$marker] = $output;
+                               $state['general'][$marker] = $output;
                        }
                }
 
@@ -500,6 +620,7 @@ class Parser
                        $text = strtr( $text, $commentState );
                }
 
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
@@ -510,20 +631,14 @@ class Parser
         * @private
         */
        function unstrip( $text, &$state ) {
-               if ( !is_array( $state ) ) {
+               if ( !isset( $state['general'] ) ) {
                        return $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 );
-
+               wfProfileIn( __METHOD__ );
+               # TODO: good candidate for FSS
+               $text = strtr( $text, $state['general'] );
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
@@ -533,20 +648,15 @@ class Parser
         * @private
         */
        function unstripNoWiki( $text, &$state ) {
-               if ( !is_array( $state ) ) {
+               if ( !isset( $state['nowiki'] ) ) {
                        return $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 );
-
+               wfProfileIn( __METHOD__ );
+               # TODO: good candidate for FSS
+               $text = strtr( $text, $state['nowiki'] );
+               wfProfileOut( __METHOD__ );
+               
                return $text;
        }
 
@@ -562,7 +672,7 @@ class Parser
                if ( !$state ) {
                        $state = array();
                }
-               $state['item'][$rnd] = $text;
+               $state['general'][$rnd] = $text;
                return $rnd;
        }
 
@@ -742,13 +852,13 @@ class Parser
                                }
                                $after = substr ( $x , 1 ) ;
                                if ( $fc == '!' ) $after = str_replace ( '!!' , '||' , $after ) ;
-                               
+
                                // Split up multiple cells on the same line.
                                // FIXME: This can result in improper nesting of tags processed
                                // by earlier parser steps, but should avoid splitting up eg
                                // attribute values containing literal "||".
                                $after = wfExplodeMarkup( '||', $after );
-                               
+
                                $t[$k] = '' ;
 
                                # Loop through each table cell
@@ -822,11 +932,17 @@ class Parser
                $fname = 'Parser::internalParse';
                wfProfileIn( $fname );
 
+               # Hook to suspend the parser in this state
+               if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$x ) ) ) {
+                       wfProfileOut( $fname );
+                       return $text ;
+               }
+
                # Remove <noinclude> tags and <includeonly> sections
                $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 );
@@ -840,6 +956,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();
@@ -867,9 +984,52 @@ class Parser
         * @private
         */
        function &doMagicLinks( &$text ) {
-               $text = $this->magicISBN( $text );
-               $text = $this->magicRFC( $text, 'RFC ', 'rfcurl' );
-               $text = $this->magicRFC( $text, 'PMID ', 'pubmedurl' );
+               wfProfileIn( __METHOD__ );
+               $text = preg_replace_callback( 
+                       '!(?:                           # Start cases
+                           <a.*?</a> |                 # Skip link text
+                           <.*?> |                     # Skip stuff inside HTML elements
+                           (?:RFC|PMID)\s+([0-9]+) |   # RFC or PMID, capture number as m[1]
+                           ISBN\s+([0-9Xx-]+)          # ISBN, capture number as m[2]
+                       )!x', array( &$this, 'magicLinkCallback' ), $text );
+               wfProfileOut( __METHOD__ );
+               return $text; 
+       }
+
+       function magicLinkCallback( $m ) {
+               if ( substr( $m[0], 0, 1 ) == '<' ) {
+                       # Skip HTML element
+                       return $m[0];
+               } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) {
+                       $isbn = $m[2];
+                       $num = strtr( $isbn, array( 
+                               '-' => '',
+                               ' ' => '',
+                               'x' => 'X',
+                       ));
+                       $titleObj = Title::makeTitle( NS_SPECIAL, 'Booksources' );
+                       $text = '<a href="' .
+                               $titleObj->escapeLocalUrl( "isbn=$num" ) .
+                               "\" class=\"internal\">ISBN $isbn</a>";
+               } else {
+                       if ( substr( $m[0], 0, 3 ) == 'RFC' ) {
+                               $keyword = 'RFC';
+                               $urlmsg = 'rfcurl';
+                               $id = $m[1];
+                       } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) {
+                               $keyword = 'PMID';
+                               $urlmsg = 'pubmedurl';
+                               $id = $m[1];
+                       } else {
+                               throw new MWException( __METHOD__.': unrecognised match type "' . 
+                                       substr($m[0], 0, 20 ) . '"' );
+                       }
+               
+                       $url = wfMsg( $urlmsg, $id);
+                       $sk =& $this->mOptions->getSkin();
+                       $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
+                       $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+               }
                return $text;
        }
 
@@ -883,7 +1043,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 );
@@ -1137,10 +1297,8 @@ class Parser
                        }
 
                        $text = $wgContLang->markNoConversion($text);
-
-                       # Normalize any HTML entities in input. They will be
-                       # re-escaped by makeExternalLink().
-                       $url = Sanitizer::decodeCharReferences( $url );
+                       
+                       $url = Sanitizer::cleanUrl( $url );
 
                        # Process the trail (i.e. everything after this link up until start of the next link),
                        # replacing any non-bracketed links
@@ -1221,9 +1379,7 @@ class Parser
                                        $url = substr( $url, 0, -$numSepChars );
                                }
 
-                               # Normalize any HTML entities in input. They will be
-                               # re-escaped by makeExternalLink() or maybeMakeExternalImage()
-                               $url = Sanitizer::decodeCharReferences( $url );
+                               $url = Sanitizer::cleanUrl( $url );
 
                                # Is this an external image?
                                $text = $this->maybeMakeExternalImage( $url );
@@ -1254,7 +1410,7 @@ class Parser
         *        the URL differently; as a workaround, just use the output for
         *        statistical records, not for actual linking/output.
         */
-       function replaceUnusualEscapes( $url ) {
+       static function replaceUnusualEscapes( $url ) {
                return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
                        array( 'Parser', 'replaceUnusualEscapesCallback' ), $url );
        }
@@ -1265,7 +1421,7 @@ class Parser
         * @static
         * @private
         */
-       function replaceUnusualEscapesCallback( $matches ) {
+       private static function replaceUnusualEscapesCallback( $matches ) {
                $char = urldecode( $matches[0] );
                $ord = ord( $char );
                // Is it an unsafe or HTTP reserved character according to RFC 1738?
@@ -1335,7 +1491,7 @@ class Parser
                $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
 
                if( is_null( $this->mTitle ) ) {
-                       wfDebugDieBacktrace( 'nooo' );
+                       throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
                }
                $nottalk = !$this->mTitle->isTalkPage();
 
@@ -1350,10 +1506,9 @@ class Parser
                }
 
                $selflink = $this->mTitle->getPrefixedText();
-               wfProfileOut( $fname.'-setup' );
-
                $checkVariantLink = sizeof($wgContLang->getVariants())>1;
                $useSubpages = $this->areSubpagesAllowed();
+               wfProfileOut( $fname.'-setup' );
 
                # Loop for each link
                for ($k = 0; isset( $a[$k] ); $k++) {
@@ -1376,6 +1531,7 @@ class Parser
 
                        $might_be_img = false;
 
+                       wfProfileIn( "$fname-e1" );
                        if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
                                $text = $m[2];
                                # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
@@ -1387,27 +1543,33 @@ class Parser
                                # and no image is in sight. See bug 2095.
                                #
                                if( $text !== '' && 
-                                       preg_match( "/^\](.*)/s", $m[3], $n ) && 
+                                       substr( $m[3], 0, 1 ) === ']' && 
                                        strpos($text, '[') !== false 
                                ) 
                                {
                                        $text .= ']'; # so that replaceExternalLinks($text) works later
-                                       $m[3] = $n[1];
+                                       $m[3] = substr( $m[3], 1 );
                                }
                                # fix up urlencoded title texts
-                               if(preg_match('/%/', $m[1] )) 
+                               if( strpos( $m[1], '%' ) !== false ) {
                                        # Should anchors '#' also be rejected?
                                        $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode($m[1]) );
+                               }
                                $trail = $m[3];
                        } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
                                $might_be_img = true;
                                $text = $m[2];
-                               if(preg_match('/%/', $m[1] )) $m[1] = urldecode($m[1]);
+                               if ( strpos( $m[1], '%' ) !== false ) {
+                                      $m[1] = urldecode($m[1]);
+                               }
                                $trail = "";
                        } else { # Invalid form; output directly
                                $s .= $prefix . '[[' . $line ;
+                               wfProfileOut( "$fname-e1" );
                                continue;
                        }
+                       wfProfileOut( "$fname-e1" );
+                       wfProfileIn( "$fname-misc" );
 
                        # Don't allow internal links to pages containing
                        # PROTO: where PROTO is a valid URL protocol; these
@@ -1430,9 +1592,12 @@ class Parser
                                $link = substr($link, 1);
                        }
 
+                       wfProfileOut( "$fname-misc" );
+                       wfProfileIn( "$fname-title" );
                        $nt = Title::newFromText( $this->unstripNoWiki($link, $this->mStripState) );
                        if( !$nt ) {
                                $s .= $prefix . '[[' . $line;
+                               wfProfileOut( "$fname-title" );
                                continue;
                        }
 
@@ -1445,23 +1610,26 @@ class Parser
 
                        $ns = $nt->getNamespace();
                        $iw = $nt->getInterWiki();
-
+                       wfProfileOut( "$fname-title" );
+                       
                        if ($might_be_img) { # if this is actually an invalid link
+                               wfProfileIn( "$fname-might_be_img" );
                                if ($ns == NS_IMAGE && $noforce) { #but might be an image
                                        $found = false;
                                        while (isset ($a[$k+1]) ) {
                                                #look at the next 'line' to see if we can close it there
                                                $spliced = array_splice( $a, $k + 1, 1 );
                                                $next_line = array_shift( $spliced );
-                                               if( preg_match("/^(.*?]].*?)]](.*)$/sD", $next_line, $m) ) {
-                                               # the first ]] closes the inner link, the second the image
+                                               $m = explode( ']]', $next_line, 3 );
+                                               if ( count( $m ) == 3 ) {
+                                                       # the first ]] closes the inner link, the second the image
                                                        $found = true;
-                                                       $text .= '[[' . $m[1];
+                                                       $text .= "[[{$m[0]}]]{$m[1]}";
                                                        $trail = $m[2];
                                                        break;
-                                               } elseif( preg_match("/^.*?]].*$/sD", $next_line, $m) ) {
+                                               } elseif ( count( $m ) == 2 ) {
                                                        #if there's exactly one ]] that's fine, we'll keep looking
-                                                       $text .= '[[' . $m[0];
+                                                       $text .= "[[{$m[0]}]]{$m[1]}";
                                                } else {
                                                        #if $next_line is invalid too, we need look no further
                                                        $text .= '[[' . $next_line;
@@ -1472,31 +1640,36 @@ class Parser
                                                # we couldn't find the end of this imageLink, so output it raw
                                                #but don't ignore what might be perfectly normal links in the text we've examined
                                                $text = $this->replaceInternalLinks($text);
-                                               $s .= $prefix . '[[' . $link . '|' . $text;
+                                               $s .= "{$prefix}[[$link|$text";
                                                # note: no $trail, because without an end, there *is* no trail
+                                               wfProfileOut( "$fname-might_be_img" );
                                                continue;
                                        }
                                } else { #it's not an image, so output it raw
-                                       $s .= $prefix . '[[' . $link . '|' . $text;
+                                       $s .= "{$prefix}[[$link|$text";
                                        # note: no $trail, because without an end, there *is* no trail
+                                       wfProfileOut( "$fname-might_be_img" );
                                        continue;
                                }
+                               wfProfileOut( "$fname-might_be_img" );
                        }
 
                        $wasblank = ( '' == $text );
                        if( $wasblank ) $text = $link;
 
-
                        # Link not escaped by : , create the various objects
                        if( $noforce ) {
 
                                # Interwikis
+                               wfProfileIn( "$fname-interwiki" );
                                if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
                                        $this->mOutput->addLanguageLink( $nt->getFullText() );
                                        $s = rtrim($s . "\n");
                                        $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
+                                       wfProfileOut( "$fname-interwiki" );
                                        continue;
                                }
+                               wfProfileOut( "$fname-interwiki" );
 
                                if ( $ns == NS_IMAGE ) {
                                        wfProfileIn( "$fname-image" );
@@ -1513,6 +1686,9 @@ class Parser
 
                                                wfProfileOut( "$fname-image" );
                                                continue;
+                                       } else {
+                                               # We still need to record the image's presence on the page
+                                               $this->mOutput->addImage( $nt->getDBkey() );
                                        }
                                        wfProfileOut( "$fname-image" );
 
@@ -1532,6 +1708,7 @@ class Parser
                                                $sortkey = $text;
                                        }
                                        $sortkey = Sanitizer::decodeCharReferences( $sortkey );
+                                       $sortkey = str_replace( "\n", '', $sortkey );
                                        $sortkey = $wgContLang->convertCategoryKey( $sortkey );
                                        $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
 
@@ -1564,12 +1741,13 @@ class Parser
                                $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
                                continue;
                        } elseif( $ns == NS_IMAGE ) {
-                               $img = Image::newFromTitle( $nt );
+                               $img = new Image( $nt );
                                if( $img->exists() ) {
                                        // Force a blue link if the file exists; may be a remote
                                        // upload on the shared repository, and we want to see its
                                        // auto-generated page.
                                        $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+                                       $this->mOutput->addLink( $nt );
                                        continue;
                                }
                        }
@@ -1582,11 +1760,12 @@ class Parser
        /**
         * Make a link placeholder. The text returned can be later resolved to a real link with
         * replaceLinkHolders(). This is done for two reasons: firstly to avoid further
-        * parsing of interwiki links, and secondly to allow all extistence checks and
+        * parsing of interwiki links, and secondly to allow all existence checks and
         * article length checks (for stub links) to be bundled into a single query.
         *
         */
        function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+               wfProfileIn( __METHOD__ );
                if ( ! is_object($nt) ) {
                        # Fail gracefully
                        $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
@@ -1608,6 +1787,7 @@ class Parser
                                $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
                        }
                }
+               wfProfileOut( __METHOD__ );
                return $retVal;
        }
 
@@ -1896,10 +2076,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
@@ -1983,14 +2163,14 @@ class Parser
        function findColonNoLinks($str, &$before, &$after) {
                $fname = 'Parser::findColonNoLinks';
                wfProfileIn( $fname );
-               
+
                $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
@@ -1999,14 +2179,14 @@ class Parser
                        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:
@@ -2125,7 +2305,7 @@ class Parser
                                }
                                break;
                        default:
-                               wfDebugDieBacktrace( "State machine error in $fname" );
+                               throw new MWException( "State machine error in $fname" );
                        }
                }
                if( $stack > 0 ) {
@@ -2157,103 +2337,110 @@ class Parser
                wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
 
                switch ( $index ) {
-                       case MAG_CURRENTMONTH:
+                       case 'currentmonth':
                                return $varCache[$index] = $wgContLang->formatNum( date( 'm', $ts ) );
-                       case MAG_CURRENTMONTHNAME:
+                       case 'currentmonthname':
                                return $varCache[$index] = $wgContLang->getMonthName( date( 'n', $ts ) );
-                       case MAG_CURRENTMONTHNAMEGEN:
+                       case 'currentmonthnamegen':
                                return $varCache[$index] = $wgContLang->getMonthNameGen( date( 'n', $ts ) );
-                       case MAG_CURRENTMONTHABBREV:
+                       case 'currentmonthabbrev':
                                return $varCache[$index] = $wgContLang->getMonthAbbreviation( date( 'n', $ts ) );
-                       case MAG_CURRENTDAY:
+                       case 'currentday':
                                return $varCache[$index] = $wgContLang->formatNum( date( 'j', $ts ) );
-                       case MAG_CURRENTDAY2:
+                       case 'currentday2':
                                return $varCache[$index] = $wgContLang->formatNum( date( 'd', $ts ) );
-                       case MAG_PAGENAME:
+                       case 'pagename':
                                return $this->mTitle->getText();
-                       case MAG_PAGENAMEE:
+                       case 'pagenamee':
                                return $this->mTitle->getPartialURL();
-                       case MAG_FULLPAGENAME:
+                       case 'fullpagename':
                                return $this->mTitle->getPrefixedText();
-                       case MAG_FULLPAGENAMEE:
+                       case 'fullpagenamee':
                                return $this->mTitle->getPrefixedURL();
-                       case MAG_SUBPAGENAME:
+                       case 'subpagename':
                                return $this->mTitle->getSubpageText();
-                       case MAG_SUBPAGENAMEE:
+                       case 'subpagenamee':
                                return $this->mTitle->getSubpageUrlForm();
-                       case MAG_BASEPAGENAME:
+                       case 'basepagename':
                                return $this->mTitle->getBaseText();
-                       case MAG_BASEPAGENAMEE:
+                       case 'basepagenamee':
                                return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
-                       case MAG_TALKPAGENAME:
+                       case 'talkpagename':
                                if( $this->mTitle->canTalk() ) {
                                        $talkPage = $this->mTitle->getTalkPage();
                                        return $talkPage->getPrefixedText();
                                } else {
                                        return '';
                                }
-                       case MAG_TALKPAGENAMEE:
+                       case 'talkpagenamee':
                                if( $this->mTitle->canTalk() ) {
                                        $talkPage = $this->mTitle->getTalkPage();
                                        return $talkPage->getPrefixedUrl();
                                } else {
                                        return '';
                                }
-                       case MAG_SUBJECTPAGENAME:
+                       case 'subjectpagename':
                                $subjPage = $this->mTitle->getSubjectPage();
                                return $subjPage->getPrefixedText();
-                       case MAG_SUBJECTPAGENAMEE:
+                       case 'subjectpagenamee':
                                $subjPage = $this->mTitle->getSubjectPage();
                                return $subjPage->getPrefixedUrl();
-                       case MAG_REVISIONID:
+                       case 'revisionid':
                                return $this->mRevisionId;
-                       case MAG_NAMESPACE:
+                       case 'namespace':
                                return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
-                       case MAG_NAMESPACEE:
+                       case 'namespacee':
                                return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
-                       case MAG_TALKSPACE:
+                       case 'talkspace':
                                return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
-                       case MAG_TALKSPACEE:
+                       case 'talkspacee':
                                return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
-                       case MAG_SUBJECTSPACE:
+                       case 'subjectspace':
                                return $this->mTitle->getSubjectNsText();
-                       case MAG_SUBJECTSPACEE:
+                       case 'subjectspacee':
                                return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
-                       case MAG_CURRENTDAYNAME:
+                       case 'currentdayname':
                                return $varCache[$index] = $wgContLang->getWeekdayName( date( 'w', $ts ) + 1 );
-                       case MAG_CURRENTYEAR:
+                       case 'currentyear':
                                return $varCache[$index] = $wgContLang->formatNum( date( 'Y', $ts ), true );
-                       case MAG_CURRENTTIME:
+                       case 'currenttime':
                                return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
-                       case MAG_CURRENTWEEK:
+                       case 'currenthour':
+                               return $varCache[$index] = $wgContLang->formatNum( date( 'H', $ts ), true );
+                       case 'currentweek':
                                // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
                                // int to remove the padding
                                return $varCache[$index] = $wgContLang->formatNum( (int)date( 'W', $ts ) );
-                       case MAG_CURRENTDOW:
+                       case 'currentdow':
                                return $varCache[$index] = $wgContLang->formatNum( date( 'w', $ts ) );
-                       case MAG_NUMBEROFARTICLES:
+                       case 'numberofarticles':
                                return $varCache[$index] = $wgContLang->formatNum( wfNumberOfArticles() );
-                       case MAG_NUMBEROFFILES:
+                       case 'numberoffiles':
                                return $varCache[$index] = $wgContLang->formatNum( wfNumberOfFiles() );
-                       case MAG_NUMBEROFUSERS:
+                       case 'numberofusers':
                                return $varCache[$index] = $wgContLang->formatNum( wfNumberOfUsers() );
-                       case MAG_NUMBEROFPAGES:
+                       case 'numberofpages':
                                return $varCache[$index] = $wgContLang->formatNum( wfNumberOfPages() );
-                       case MAG_CURRENTTIMESTAMP:
+                       case 'numberofadmins':
+                               return $varCache[$index]  = $wgContLang->formatNum( wfNumberOfAdmins() );
+                       case 'currenttimestamp':
                                return $varCache[$index] = wfTimestampNow();
-                       case MAG_CURRENTVERSION:
+                       case 'currentversion':
                                global $wgVersion;
                                return $wgVersion;
-                       case MAG_SITENAME:
+                       case 'sitename':
                                return $wgSitename;
-                       case MAG_SERVER:
+                       case 'server':
                                return $wgServer;
-                       case MAG_SERVERNAME:
+                       case 'servername':
                                return $wgServerName;
-                       case MAG_SCRIPTPATH:
+                       case 'scriptpath':
                                return $wgScriptPath;
-                       case MAG_DIRECTIONMARK:
+                       case 'directionmark':
                                return $wgContLang->getDirMark();
+                       case 'contentlanguage':
+                               global $wgContLanguageCode;
+                               return $wgContLanguageCode;
                        default:
                                $ret = null;
                                if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) )
@@ -2271,9 +2458,10 @@ class Parser
        function initialiseVariables() {
                $fname = 'Parser::initialiseVariables';
                wfProfileIn( $fname );
-               global $wgVariableIDs;
+               $variableIDs = MagicWord::getVariableIDs();
+
                $this->mVariables = array();
-               foreach ( $wgVariableIDs as $id ) {
+               foreach ( $variableIDs as $id ) {
                        $mw =& MagicWord::get( $id );
                        $mw->addToArray( $this->mVariables, $id );
                }
@@ -2289,168 +2477,164 @@ class Parser
         *     '{' => array(                            # opening parentheses
         *                                      'end' => '}',   # closing parentheses
         *                                      'cb' => array(2 => callback,    # replacement callback to call if {{..}} is found
-        *                                                                4 => callback         # replacement callback to call if {{{{..}}}} is found
+        *                                                                3 => callback         # replacement callback to call if {{{..}}} is found
         *                                                                )
         *                                      )
+        *                                      'min' => 2,     # Minimum parenthesis count in cb
+        *                                      'max' => 3,     # Maximum parenthesis count in cb
         * @private
         */
        function replace_callback ($text, $callbacks) {
+               wfProfileIn( __METHOD__ );
                $openingBraceStack = array();   # this array will hold a stack of parentheses which are not closed yet
                $lastOpeningBrace = -1;                 # last not closed parentheses
 
-               for ($i = 0; $i < strlen($text); $i++) {
-                       # check for any opening brace
-                       $rule = null;
-                       $nextPos = -1;
-                       foreach ($callbacks as $key => $value) {
-                               $pos = strpos ($text, $key, $i);
-                               if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)) {
-                                       $rule = $value;
-                                       $nextPos = $pos;
-                               }
+               $validOpeningBraces = implode( '', array_keys( $callbacks ) );
+               
+               $i = 0;
+               while ( $i < strlen( $text ) ) {
+                       # Find next opening brace, closing brace or pipe
+                       if ( $lastOpeningBrace == -1 ) {
+                               $currentClosing = '';
+                               $search = $validOpeningBraces;
+                       } else {
+                               $currentClosing = $openingBraceStack[$lastOpeningBrace]['braceEnd'];
+                               $search = $validOpeningBraces . '|' . $currentClosing;
                        }
-
-                       if ($lastOpeningBrace >= 0) {
-                               $pos = strpos ($text, $openingBraceStack[$lastOpeningBrace]['braceEnd'], $i);
-
-                               if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){
-                                       $rule = null;
-                                       $nextPos = $pos;
-                               }
-
-                               $pos = strpos ($text, '|', $i);
-
-                               if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){
-                                       $rule = null;
-                                       $nextPos = $pos;
+                       $rule = null;
+                       $i += strcspn( $text, $search, $i );
+                       if ( $i < strlen( $text ) ) {
+                               if ( $text[$i] == '|' ) {
+                                       $found = 'pipe';
+                               } elseif ( $text[$i] == $currentClosing ) {
+                                       $found = 'close';
+                               } else {
+                                       $found = 'open';
+                                       $rule = $callbacks[$text[$i]];
                                }
-                       }
-
-                       if ($nextPos == -1)
+                       } else {
+                               # All done
                                break;
+                       }
 
-                       $i = $nextPos;
-
-                       # found openning brace, lets add it to parentheses stack
-                       if (null != $rule) {
+                       if ( $found == 'open' ) {
+                               # found opening brace, let's add it to parentheses stack
                                $piece = array('brace' => $text[$i],
                                                           'braceEnd' => $rule['end'],
-                                                          'count' => 1,
                                                           'title' => '',
                                                           'parts' => null);
 
-                               # count openning brace characters
-                               while ($i+1 < strlen($text) && $text[$i+1] == $piece['brace']) {
-                                       $piece['count']++;
-                                       $i++;
-                               }
-
-                               $piece['startAt'] = $i+1;
-                               $piece['partStart'] = $i+1;
+                               # count opening brace characters
+                               $piece['count'] = strspn( $text, $piece['brace'], $i );
+                               $piece['startAt'] = $piece['partStart'] = $i + $piece['count'];
+                               $i += $piece['count'];
 
-                               # we need to add to stack only if openning brace count is enough for any given rule
-                               foreach ($rule['cb'] as $cnt => $fn) {
-                                       if ($piece['count'] >= $cnt) {
-                                               $lastOpeningBrace ++;
-                                               $openingBraceStack[$lastOpeningBrace] = $piece;
-                                               break;
+                               # we need to add to stack only if opening brace count is enough for one of the rules
+                               if ( $piece['count'] >= $rule['min'] ) {
+                                       $lastOpeningBrace ++;
+                                       $openingBraceStack[$lastOpeningBrace] = $piece;
+                               }
+                       } elseif ( $found == 'close' ) {
+                               # lets check if it is enough characters for closing brace
+                               $maxCount = $openingBraceStack[$lastOpeningBrace]['count'];
+                               $count = strspn( $text, $text[$i], $i, $maxCount );
+
+                               # check for maximum matching characters (if there are 5 closing 
+                               # characters, we will probably need only 3 - depending on the rules)
+                               $matchingCount = 0;
+                               $matchingCallback = null;
+                               $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']];
+                               if ( $count > $cbType['max'] ) {
+                                       # The specified maximum exists in the callback array, unless the caller 
+                                       # has made an error
+                                       $matchingCount = $cbType['max'];
+                               } else {
+                                       # Count is less than the maximum
+                                       # Skip any gaps in the callback array to find the true largest match
+                                       # Need to use array_key_exists not isset because the callback can be null
+                                       $matchingCount = $count;
+                                       while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $cbType['cb'] ) ) {
+                                               --$matchingCount;
                                        }
                                }
 
-                               continue;
-                       }
-                       else if ($lastOpeningBrace >= 0) {
-                               # first check if it is a closing brace
-                               if ($openingBraceStack[$lastOpeningBrace]['braceEnd'] == $text[$i]) {
-                                       # lets check if it is enough characters for closing brace
-                                       $count = 1;
-                                       while ($i+$count < strlen($text) && $text[$i+$count] == $text[$i])
-                                               $count++;
-
-                                       # if there are more closing parentheses than opening ones, we parse less
-                                       if ($openingBraceStack[$lastOpeningBrace]['count'] < $count)
-                                               $count = $openingBraceStack[$lastOpeningBrace]['count'];
-
-                                       # check for maximum matching characters (if there are 5 closing characters, we will probably need only 3 - depending on the rules)
-                                       $matchingCount = 0;
-                                       $matchingCallback = null;
-                                       foreach ($callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]['cb'] as $cnt => $fn) {
-                                               if ($count >= $cnt && $matchingCount < $cnt) {
-                                                       $matchingCount = $cnt;
-                                                       $matchingCallback = $fn;
-                                               }
-                                       }
+                               if ($matchingCount <= 0) {
+                                       $i += $count;
+                                       continue;
+                               }
+                               $matchingCallback = $cbType['cb'][$matchingCount];
 
-                                       if ($matchingCount == 0) {
-                                               $i += $count - 1;
-                                               continue;
-                                       }
+                               # let's set a title or last part (if '|' was found)
+                               if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
+                                       $openingBraceStack[$lastOpeningBrace]['title'] = 
+                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], 
+                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+                               } else {
+                                       $openingBraceStack[$lastOpeningBrace]['parts'][] = 
+                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], 
+                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+                               }
 
-                                       # lets set a title or last part (if '|' was found)
-                                       if (null === $openingBraceStack[$lastOpeningBrace]['parts'])
-                                               $openingBraceStack[$lastOpeningBrace]['title'] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
-                                       else
-                                               $openingBraceStack[$lastOpeningBrace]['parts'][] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
-
-                                       $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount;
-                                       $pieceEnd = $i + $matchingCount;
-
-                                       if( is_callable( $matchingCallback ) ) {
-                                               $cbArgs = array (
-                                                                                'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart),
-                                                                                'title' => trim($openingBraceStack[$lastOpeningBrace]['title']),
-                                                                                'parts' => $openingBraceStack[$lastOpeningBrace]['parts'],
-                                                                                '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 );
-                                               $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd);
-                                               $i = $pieceStart + strlen($replaceWith) - 1;
-                                       }
-                                       else {
-                                               # null value for callback means that parentheses should be parsed, but not replaced
-                                               $i += $matchingCount - 1;
-                                       }
+                               $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount;
+                               $pieceEnd = $i + $matchingCount;
+
+                               if( is_callable( $matchingCallback ) ) {
+                                       $cbArgs = array (
+                                                                        'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart),
+                                                                        'title' => trim($openingBraceStack[$lastOpeningBrace]['title']),
+                                                                        'parts' => $openingBraceStack[$lastOpeningBrace]['parts'],
+                                                                        '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 );
+                                       $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd);
+                                       $i = $pieceStart + strlen($replaceWith);
+                               } else {
+                                       # null value for callback means that parentheses should be parsed, but not replaced
+                                       $i += $matchingCount;
+                               }
 
-                                       # reset last openning parentheses, but keep it in case there are unused characters
-                                       $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'],
-                                                                  'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'],
-                                                                  'count' => $openingBraceStack[$lastOpeningBrace]['count'],
-                                                                  'title' => '',
-                                                                  'parts' => null,
-                                                                  'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']);
-                                       $openingBraceStack[$lastOpeningBrace--] = null;
-
-                                       if ($matchingCount < $piece['count']) {
-                                               $piece['count'] -= $matchingCount;
-                                               $piece['startAt'] -= $matchingCount;
-                                               $piece['partStart'] = $piece['startAt'];
-                                               # do we still qualify for any callback with remaining count?
-                                               foreach ($callbacks[$piece['brace']]['cb'] as $cnt => $fn) {
-                                                       if ($piece['count'] >= $cnt) {
-                                                               $lastOpeningBrace ++;
-                                                               $openingBraceStack[$lastOpeningBrace] = $piece;
-                                                               break;
-                                                       }
+                               # reset last opening parentheses, but keep it in case there are unused characters
+                               $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'],
+                                                          'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'],
+                                                          'count' => $openingBraceStack[$lastOpeningBrace]['count'],
+                                                          'title' => '',
+                                                          'parts' => null,
+                                                          'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']);
+                               $openingBraceStack[$lastOpeningBrace--] = null;
+
+                               if ($matchingCount < $piece['count']) {
+                                       $piece['count'] -= $matchingCount;
+                                       $piece['startAt'] -= $matchingCount;
+                                       $piece['partStart'] = $piece['startAt'];
+                                       # do we still qualify for any callback with remaining count?
+                                       $currentCbList = $callbacks[$piece['brace']]['cb'];
+                                       while ( $piece['count'] ) {
+                                               if ( array_key_exists( $piece['count'], $currentCbList ) ) {
+                                                       $lastOpeningBrace++;
+                                                       $openingBraceStack[$lastOpeningBrace] = $piece;
+                                                       break;
                                                }
+                                               --$piece['count'];
                                        }
-                                       continue;
                                }
-
+                       } elseif ( $found == 'pipe' ) {
                                # lets set a title if it is a first separator, or next part otherwise
-                               if ($text[$i] == '|') {
-                                       if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
-                                               $openingBraceStack[$lastOpeningBrace]['title'] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
-                                               $openingBraceStack[$lastOpeningBrace]['parts'] = array();
-                                       }
-                                       else
-                                               $openingBraceStack[$lastOpeningBrace]['parts'][] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
-
-                                       $openingBraceStack[$lastOpeningBrace]['partStart'] = $i + 1;
+                               if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
+                                       $openingBraceStack[$lastOpeningBrace]['title'] = 
+                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], 
+                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+                                       $openingBraceStack[$lastOpeningBrace]['parts'] = array();
+                               } else {
+                                       $openingBraceStack[$lastOpeningBrace]['parts'][] = 
+                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], 
+                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
                                }
+                               $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i;
                        }
                }
 
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
@@ -2471,11 +2655,11 @@ class Parser
         */
        function replaceVariables( $text, $args = array(), $argsOnly = false ) {
                # Prevent too big inclusions
-               if( strlen( $text ) > MAX_INCLUDE_SIZE ) {
+               if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
                        return $text;
                }
 
-               $fname = 'Parser::replaceVariables';
+               $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
                wfProfileIn( $fname );
 
                # This function is called recursively. To keep track of arguments we need a stack:
@@ -2485,20 +2669,32 @@ class Parser
                if ( !$argsOnly ) {
                        $braceCallbacks[2] = array( &$this, 'braceSubstitution' );
                }
-               if ( $this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI ) {
+               if ( !$this->mOutputType != OT_MSG ) {
                        $braceCallbacks[3] = array( &$this, 'argSubstitution' );
                }
-               $callbacks = array();
-               $callbacks['{'] = array('end' => '}', 'cb' => $braceCallbacks);
-               $callbacks['['] = array('end' => ']', 'cb' => array(2=>null));
-               $text = $this->replace_callback ($text, $callbacks);
-
-               array_pop( $this->mArgStack );
+               if ( $braceCallbacks ) {
+                       $callbacks = array( 
+                               '{' => array(
+                                       'end' => '}',
+                                       'cb' => $braceCallbacks,
+                                       'min' => $argsOnly ? 3 : 2,
+                                       'max' => isset( $braceCallbacks[3] ) ? 3 : 2,
+                               ),
+                               '[' => array( 
+                                       'end' => ']', 
+                                       'cb' => array(2=>null),
+                                       'min' => 2,
+                                       'max' => 2,
+                               )
+                       );
+                       $text = $this->replace_callback ($text, $callbacks);
 
+                       array_pop( $this->mArgStack );
+               }
                wfProfileOut( $fname );
                return $text;
        }
-       
+
        /**
         * Replace magic variables
         * @private
@@ -2507,13 +2703,10 @@ class Parser
                $fname = 'Parser::variableSubstitution';
                $varname = $matches[1];
                wfProfileIn( $fname );
-               if ( !$this->mVariables ) {
-                       $this->initialiseVariables();
-               }
                $skip = false;
                if ( $this->mOutputType == OT_WIKI ) {
                        # Do only magic variables prefixed by SUBST
-                       $mwSubst =& MagicWord::get( MAG_SUBST );
+                       $mwSubst =& MagicWord::get( 'subst' );
                        if (!$mwSubst->matchStartAndRemove( $varname ))
                                $skip = true;
                        # Note that if we don't substitute the variable below,
@@ -2569,8 +2762,9 @@ class Parser
         */
        function braceSubstitution( $piece ) {
                global $wgContLang, $wgLang, $wgAllowDisplayTitle, $action;
-               $fname = 'Parser::braceSubstitution';
+               $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
                wfProfileIn( $fname );
+               wfProfileIn( __METHOD__.'-setup' );
 
                # Flags
                $found = false;             # $text has been filled
@@ -2586,10 +2780,11 @@ class Parser
 
                $linestart = '';
 
+                       
                # $part1 is the bit before the first |, and must contain only title characters
                # $args is a list of arguments, starting from index 0, not including $part1
 
-               $part1 = $piece['title'];
+               $titleText = $part1 = $piece['title'];
                # If the third subpattern matched anything, it will start with |
 
                if (null == $piece['parts']) {
@@ -2604,11 +2799,13 @@ class Parser
 
                $args = (null == $piece['parts']) ? array() : $piece['parts'];
                $argc = count( $args );
+               wfProfileOut( __METHOD__.'-setup' );
 
                # SUBST
+               wfProfileIn( __METHOD__.'-modifiers' );
                if ( !$found ) {
-                       $mwSubst =& MagicWord::get( MAG_SUBST );
-                       if ( $mwSubst->matchStartAndRemove( $part1 ) xor ($this->mOutputType == OT_WIKI) ) {
+                       $mwSubst =& MagicWord::get( 'subst' );
+                       if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) {
                                # One of two possibilities is true:
                                # 1) Found SUBST but not in the PST phase
                                # 2) Didn't find SUBST and in the PST phase
@@ -2623,196 +2820,50 @@ class Parser
                # MSG, MSGNW, INT and RAW
                if ( !$found ) {
                        # Check for MSGNW:
-                       $mwMsgnw =& MagicWord::get( MAG_MSGNW );
+                       $mwMsgnw =& MagicWord::get( 'msgnw' );
                        if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
                                $nowiki = true;
                        } else {
                                # Remove obsolete MSG:
-                               $mwMsg =& MagicWord::get( MAG_MSG );
+                               $mwMsg =& MagicWord::get( 'msg' );
                                $mwMsg->matchStartAndRemove( $part1 );
                        }
                        
                        # Check for RAW:
-                       $mwRaw =& MagicWord::get( MAG_RAW );
+                       $mwRaw =& MagicWord::get( 'raw' );
                        if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
                                $forceRawInterwiki = true;
                        }
                        
                        # Check if it is an internal message
-                       $mwInt =& MagicWord::get( MAG_INT );
+                       $mwInt =& MagicWord::get( 'int' );
                        if ( $mwInt->matchStartAndRemove( $part1 ) ) {
-                               if ( $this->incrementIncludeCount( 'int:'.$part1 ) ) {
-                                       $text = $linestart . wfMsgReal( $part1, $args, true );
-                                       $found = true;
-                               }
-                       }
-               }
-
-               # NS
-               if ( !$found ) {
-                       # Check for NS: (namespace expansion)
-                       $mwNs = MagicWord::get( MAG_NS );
-                       if ( $mwNs->matchStartAndRemove( $part1 ) ) {
-                               if ( intval( $part1 ) || $part1 == "0" ) {
-                                       $text = $linestart . $wgContLang->getNsText( intval( $part1 ) );
-                                       $found = true;
-                               } else {
-                                       $index = Namespace::getCanonicalIndex( strtolower( $part1 ) );
-                                       if ( !is_null( $index ) ) {
-                                               $text = $linestart . $wgContLang->getNsText( $index );
-                                               $found = true;
-                                       }
-                               }
-                       }
-               }
-
-               # URLENCODE
-               if( !$found ) {
-                       $urlencode =& MagicWord::get( MAG_URLENCODE );
-                       if( $urlencode->matchStartAndRemove( $part1 ) ) {
-                               $text = $linestart . urlencode( $part1 );
+                               $text = $linestart . wfMsgReal( $part1, $args, true );
                                $found = true;
                        }
                }
+               wfProfileOut( __METHOD__.'-modifiers' );
 
-               # LCFIRST, UCFIRST, LC and UC
+               # Parser functions
                if ( !$found ) {
-                       $lcfirst =& MagicWord::get( MAG_LCFIRST );
-                       $ucfirst =& MagicWord::get( MAG_UCFIRST );
-                       $lc =& MagicWord::get( MAG_LC );
-                       $uc =& MagicWord::get( MAG_UC );
-                       if ( $lcfirst->matchStartAndRemove( $part1 ) ) {
-                               $text = $linestart . $wgContLang->lcfirst( $part1 );
-                               $found = true;
-                       } else if ( $ucfirst->matchStartAndRemove( $part1 ) ) {
-                               $text = $linestart . $wgContLang->ucfirst( $part1 );
-                               $found = true;
-                       } else if ( $lc->matchStartAndRemove( $part1 ) ) {
-                               $text = $linestart . $wgContLang->lc( $part1 );
-                               $found = true;
-                       } else if ( $uc->matchStartAndRemove( $part1 ) ) {
-                                $text = $linestart . $wgContLang->uc( $part1 );
-                                $found = true;
-                       }
-               }
-
-               # LOCALURL and FULLURL
-               if ( !$found ) {
-                       $mwLocal =& MagicWord::get( MAG_LOCALURL );
-                       $mwLocalE =& MagicWord::get( MAG_LOCALURLE );
-                       $mwFull =& MagicWord::get( MAG_FULLURL );
-                       $mwFullE =& MagicWord::get( MAG_FULLURLE );
-
-
-                       if ( $mwLocal->matchStartAndRemove( $part1 ) ) {
-                               $func = 'getLocalURL';
-                       } elseif ( $mwLocalE->matchStartAndRemove( $part1 ) ) {
-                               $func = 'escapeLocalURL';
-                       } elseif ( $mwFull->matchStartAndRemove( $part1 ) ) {
-                               $func = 'getFullURL';
-                       } elseif ( $mwFullE->matchStartAndRemove( $part1 ) ) {
-                               $func = 'escapeFullURL';
-                       } else {
-                               $func = false;
-                       }
-
-                       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] );
-                                       } else {
-                                               $text = $linestart . $title->$func();
-                                       }
-                                       $found = true;
-                               }
-                       }
-               }
-
-               $lang = $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
-               # GRAMMAR
-               if ( !$found && $argc == 1 ) {
-                       $mwGrammar =& MagicWord::get( MAG_GRAMMAR );
-                       if ( $mwGrammar->matchStartAndRemove( $part1 ) ) {
-                               $text = $linestart . $lang->convertGrammar( $args[0], $part1 );
-                               $found = true;
-                       }
-               }
-
-               # PLURAL
-               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]);
-                               $found = true;
-                       }
-               }
-               
-               # DISPLAYTITLE
-               if ( !$found && $argc == 1 && $wgAllowDisplayTitle ) {
-                       $mwDT =& MagicWord::get( MAG_DISPLAYTITLE );
-                       if ( $mwDT->matchStartAndRemove( $part1 ) ) {
-                               
-                               # Set title in parser output object
-                               $param = $args[0];
-                               $parserOptions = new ParserOptions;
-                               $local_parser = new Parser ();
-                               $t2 = $local_parser->parse ( $param, $this->mTitle, $parserOptions, false );
-                               $this->mOutput->mHTMLtitle = $t2->GetText();
-
-                               # Add subtitle
-                               $t = $this->mTitle->getPrefixedText();
-                               $this->mOutput->mSubtitle .= wfMsg('displaytitle', $t);
-                               $text = "" ;
-                               $found = true ;
-                       }
-               }               
-
-               # NUMBEROFPAGES, NUMBEROFUSERS, NUMBEROFARTICLES, and NUMBEROFFILES
-               if( !$found ) {
-                       $mwWordsToCheck = array( MAG_NUMBEROFPAGES => 'wfNumberOfPages',
-                                                                        MAG_NUMBEROFUSERS => 'wfNumberOfUsers',
-                                                                        MAG_NUMBEROFARTICLES => 'wfNumberOfArticles',
-                                                                        MAG_NUMBEROFFILES => 'wfNumberOfFiles' );
-                       foreach( $mwWordsToCheck as $word => $func ) {
-                               $mwCurrentWord =& MagicWord::get( $word );
-                               if( $mwCurrentWord->matchStartAndRemove( $part1 ) ) {
-                                       $mwRawSuffix =& MagicWord::get( MAG_RAWSUFFIX );
-                                       if( $mwRawSuffix->match( $args[0] ) ) {
-                                               # Raw and unformatted
-                                               $text = $linestart . call_user_func( $func );
+                       wfProfileIn( __METHOD__ . '-pfunc' );
+                       
+                       $colonPos = strpos( $part1, ':' );
+                       if ( $colonPos !== false ) {
+                               # Case sensitive functions
+                               $function = substr( $part1, 0, $colonPos );
+                               if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
+                                       $function = $this->mFunctionSynonyms[1][$function];
+                               } else {
+                                       # Case insensitive functions
+                                       $function = strtolower( $function );
+                                       if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
+                                               $function = $this->mFunctionSynonyms[0][$function];
                                        } else {
-                                               # Formatted according to the content default
-                                               $text = $linestart . $wgContLang->formatNum( call_user_func( $func ) );
+                                               $function = false;
                                        }
-                                       $found = true;
                                }
-                       }
-               }
-
-               # #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 ) == '#' ) {
-                       $colonPos = strpos( $part1, ':' );
-                       if ( $colonPos !== false ) {
-                               $function = strtolower( substr( $part1, 1, $colonPos - 1 ) );
-                               if ( isset( $this->mFunctionHooks[$function] ) ) {
+                               if ( $function ) {
                                        $funcArgs = array_map( 'trim', $args );
                                        $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs );
                                        $result = call_user_func_array( $this->mFunctionHooks[$function], $funcArgs );
@@ -2821,10 +2872,12 @@ class Parser
                                        // The text is usually already parsed, doesn't need triple-brace tags expanded, etc.
                                        //$noargs = true;
                                        //$noparse = true;
-                                       
+
                                        if ( is_array( $result ) ) {
-                                               $text = $linestart . $result[0];
-                                               unset( $result[0] );
+                                               if ( isset( $result[0] ) ) {
+                                                       $text = $linestart . $result[0];
+                                                       unset( $result[0] );
+                                               }
 
                                                // Extract flags into the local scope
                                                // This allows callers to set flags such as nowiki, noparse, found, etc.
@@ -2834,6 +2887,7 @@ class Parser
                                        }
                                }
                        }
+                       wfProfileOut( __METHOD__ . '-pfunc' );
                }
 
                # Template table test
@@ -2849,9 +2903,8 @@ class Parser
                                $noargs = true;
                                $found = true;
                                $text = $linestart .
-                                       '{{' . $part1 . '}}' .
-                                       '<!-- WARNING: template loop detected -->';
-                               wfDebug( "$fname: template loop broken at '$part1'\n" );
+                                       "[[$part1]]<!-- WARNING: template loop detected -->";
+                               wfDebug( __METHOD__.": template loop broken at '$part1'\n" );
                        } else {
                                # set $text to cached message.
                                $text = $linestart . $this->mTemplates[$piece['title']];
@@ -2861,6 +2914,7 @@ class Parser
                # Load from database
                $lastPathLevel = $this->mTemplatePath;
                if ( !$found ) {
+                       wfProfileIn( __METHOD__ . '-loadtpl' );
                        $ns = NS_TEMPLATE;
                        # declaring $subpage directly in the function call
                        # does not work correctly with references and breaks
@@ -2872,38 +2926,42 @@ class Parser
                        }
                        $title = Title::newFromText( $part1, $ns );
 
+
                        if ( !is_null( $title ) ) {
+                               $titleText = $title->getPrefixedText();
+                               $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();
-                                       if ( $this->incrementIncludeCount( $dbk ) ) {
-                                               if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->mOutputType != OT_WIKI ) {
-                                                       $text = SpecialPage::capturePath( $title );
-                                                       if ( is_string( $text ) ) {
-                                                               $found = true;
-                                                               $noparse = true;
-                                                               $noargs = true;
-                                                               $isHTML = true;
-                                                               $this->disableCache();
-                                                       }
-                                               } else {
-                                                       $articleContent = $this->fetchTemplate( $title );
-                                                       if ( $articleContent !== false ) {
-                                                               $found = true;
-                                                               $text = $articleContent;
-                                                               $replaceHeadings = true;
-                                                       }
+                                       if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
+                                               $text = SpecialPage::capturePath( $title );
+                                               if ( is_string( $text ) ) {
+                                                       $found = true;
+                                                       $noparse = true;
+                                                       $noargs = true;
+                                                       $isHTML = true;
+                                                       $this->disableCache();
+                                               }
+                                       } else {
+                                               $articleContent = $this->fetchTemplate( $title );
+                                               if ( $articleContent !== false ) {
+                                                       $found = true;
+                                                       $text = $articleContent;
+                                                       $replaceHeadings = true;
                                                }
                                        }
 
                                        # If the title is valid but undisplayable, make a link to it
-                                       if ( $this->mOutputType == OT_HTML && !$found ) {
-                                               $text = '[['.$title->getPrefixedText().']]';
+                                       if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
+                                               $text = "[[$titleText]]";
                                                $found = true;
                                        }
                                } elseif ( $title->isTrans() ) {
                                        // Interwiki transclusion
-                                       if ( $this->mOutputType == OT_HTML && !$forceRawInterwiki ) {
+                                       if ( $this->ot['html'] && !$forceRawInterwiki ) {
                                                $text = $this->interwikiTransclude( $title, 'render' );
                                                $isHTML = true;
                                                $noparse = true;
@@ -2913,22 +2971,35 @@ class Parser
                                        }
                                        $found = true;
                                }
-                               
+
                                # Template cache array insertion
                                # 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;
                                }
                        }
+                       wfProfileOut( __METHOD__ . '-loadtpl' );
+               }
+
+               if ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) {
+                       # Error, oversize inclusion
+                       $text = $linestart .
+                               "[[$titleText]]<!-- WARNING: template omitted, pre-expand include size too large -->";
+                       $noparse = true;
+                       $noargs = true;
                }
 
                # Recursive parsing, escaping and link table handling
                # Only for HTML output
-               if ( $nowiki && $found && $this->mOutputType == OT_HTML ) {
+               if ( $nowiki && $found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
                        $text = wfEscapeWikiText( $text );
-               } elseif ( ($this->mOutputType == OT_HTML || $this->mOutputType == OT_WIKI) && $found ) {
+               } elseif ( !$this->ot['msg'] && $found ) {
                        if ( $noargs ) {
                                $assocArgs = array();
                        } else {
@@ -2967,16 +3038,20 @@ class Parser
                                $text = preg_replace( '/<noinclude>.*?<\/noinclude>/s', '', $text );
                                $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) );
 
-                               if( $this->mOutputType == OT_HTML ) {
+                               if( $this->ot['html'] || $this->ot['pre'] ) {
                                        # Strip <nowiki>, <pre>, etc.
                                        $text = $this->strip( $text, $this->mStripState );
-                                       $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs );
+                                       if ( $this->ot['html'] ) {
+                                               $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs );
+                                       } elseif ( $this->ot['pre'] && $this->mOptions->getRemoveComments() ) {
+                                               $text = Sanitizer::removeHTMLcomments( $text );
+                                       }
                                }
                                $text = $this->replaceVariables( $text, $assocArgs );
 
                                # If the template begins with a table or block-level
                                # element, it should be treated as beginning a new line.
-                               if (!$piece['lineStart'] && preg_match('/^({\\||:|;|#|\*)/', $text)) {
+                               if (!$piece['lineStart'] && preg_match('/^({\\||:|;|#|\*)/', $text)) /*}*/{ 
                                        $text = "\n" . $text;
                                }
                        } elseif ( !$noargs ) {
@@ -2989,10 +3064,19 @@ class Parser
                # Prune lower levels off the recursion check path
                $this->mTemplatePath = $lastPathLevel;
 
+               if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
+                       # Error, oversize inclusion
+                       $text = $linestart .
+                               "[[$titleText]]<!-- WARNING: template omitted, post-expand include size too large -->";
+                       $noparse = true;
+                       $noargs = true;
+               }
+
                if ( !$found ) {
                        wfProfileOut( $fname );
                        return $piece['text'];
                } else {
+                       wfProfileIn( __METHOD__ . '-placeholders' );
                        if ( $isHTML ) {
                                # Replace raw HTML by a placeholder
                                # Add a blank line preceding, to prevent it from mucking up
@@ -3001,7 +3085,7 @@ class Parser
                        } else {
                                # replace ==section headers==
                                # XXX this needs to go away once we have a better parser.
-                               if ( $this->mOutputType != OT_WIKI && $replaceHeadings ) {
+                               if ( !$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings ) {
                                        if( !is_null( $title ) )
                                                $encodedname = base64_encode($title->getPrefixedDBkey());
                                        else
@@ -3026,6 +3110,7 @@ class Parser
                                        }
                                }
                        }
+                       wfProfileOut( __METHOD__ . '-placeholders' );
                }
 
                # Prune lower levels off the recursion check path
@@ -3100,7 +3185,7 @@ class Parser
                        }
                }
 
-               $text = wfGetHTTP($url);
+               $text = Http::get($url);
                if (!$text)
                        return wfMsg('scarytranscludefailed', $url);
 
@@ -3124,47 +3209,63 @@ class Parser
 
                if ( array_key_exists( $arg, $inputArgs ) ) {
                        $text = $inputArgs[$arg];
-               } else if ($this->mOutputType == OT_HTML && null != $matches['parts'] && count($matches['parts']) > 0) {
+               } else if (($this->mOutputType == OT_HTML || $this->mOutputType == OT_PREPROCESS ) && 
+               null != $matches['parts'] && count($matches['parts']) > 0) {
                        $text = $matches['parts'][0];
                }
+               if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
+                       $text = $matches['text'] .
+                               '<!-- WARNING: argument omitted, expansion size too large -->';
+               }
 
                return $text;
        }
 
        /**
-        * Returns true if the function is allowed to include this entity
-        * @private
+        * Increment an include size counter
+        *
+        * @param string $type The type of expansion
+        * @param integer $size The size of the text
+        * @return boolean False if this inclusion would take it over the maximum, true otherwise
         */
-       function incrementIncludeCount( $dbk ) {
-               if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) {
-                       $this->mIncludeCount[$dbk] = 0;
-               }
-               if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) {
-                       return true;
-               } else {
+       function incrementIncludeSize( $type, $size ) {
+               if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
                        return false;
+               } else {
+                       $this->mIncludeSizes[$type] += $size;
+                       return true;
                }
        }
 
+       /**
+        * 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( '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 );
+               $mw = MagicWord::get( 'notoc' );
                if( $mw->matchAndRemove( $text ) ) {
                        $this->mShowToc = false;
                }
-               
-               $mw = MagicWord::get( MAG_TOC );
+
+               $mw = MagicWord::get( '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 );
                }
@@ -3196,7 +3297,7 @@ class Parser
                }
 
                # Inhibit editsection links if requested in the page
-               $esw =& MagicWord::get( MAG_NOEDITSECTION );
+               $esw =& MagicWord::get( 'noeditsection' );
                if( $esw->matchAndRemove( $text ) ) {
                        $showEditLink = 0;
                }
@@ -3212,13 +3313,13 @@ class Parser
 
                # Allow user to stipulate that a page should have a "new section"
                # link added via __NEWSECTIONLINK__
-               $mw =& MagicWord::get( MAG_NEWSECTIONLINK );
+               $mw =& MagicWord::get( 'newsectionlink' );
                if( $mw->matchAndRemove( $text ) )
                        $this->mOutput->setNewSection( 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 );
+               $mw =& MagicWord::get( 'forcetoc' );
                if ($mw->matchAndRemove( $text ) ) {
                        $this->mShowToc = true;
                        $enoughToc = true;
@@ -3424,137 +3525,6 @@ class Parser
                }
        }
 
-       /**
-        * Return an HTML link for the "ISBN 123456" text
-        * @private
-        */
-       function magicISBN( $text ) {
-               $fname = 'Parser::magicISBN';
-               wfProfileIn( $fname );
-
-               $a = split( 'ISBN ', ' '.$text );
-               if ( count ( $a ) < 2 ) {
-                       wfProfileOut( $fname );
-                       return $text;
-               }
-               $text = substr( array_shift( $a ), 1);
-               $valid = '0123456789-Xx';
-
-               foreach ( $a as $x ) {
-                       # hack: don't replace inside thumbnail title/alt
-                       # attributes
-                       if(preg_match('/<[^>]+(alt|title)="[^">]*$/', $text)) {
-                               $text .= "ISBN $x";
-                               continue;
-                       }
-
-                       $isbn = $blank = '' ;
-                       while ( ' ' == $x{0} ) {
-                               $blank .= ' ';
-                               $x = substr( $x, 1 );
-                       }
-                       if ( $x == '' ) { # blank isbn
-                               $text .= "ISBN $blank";
-                               continue;
-                       }
-                       while ( strstr( $valid, $x{0} ) != false ) {
-                               $isbn .= $x{0};
-                               $x = substr( $x, 1 );
-                       }
-                       $num = str_replace( '-', '', $isbn );
-                       $num = str_replace( ' ', '', $num );
-                       $num = str_replace( 'x', 'X', $num );
-
-                       if ( '' == $num ) {
-                               $text .= "ISBN $blank$x";
-                       } else {
-                               $titleObj = Title::makeTitle( NS_SPECIAL, 'Booksources' );
-                               $text .= '<a href="' .
-                                       $titleObj->escapeLocalUrl( 'isbn='.$num ) .
-                                       "\" class=\"internal\">ISBN $isbn</a>";
-                               $text .= $x;
-                       }
-               }
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /**
-        * Return an HTML link for the "RFC 1234" text
-        *
-        * @private
-        * @param string $text     Text to be processed
-        * @param string $keyword  Magic keyword to use (default RFC)
-        * @param string $urlmsg   Interface message to use (default rfcurl)
-        * @return string
-        */
-       function magicRFC( $text, $keyword='RFC ', $urlmsg='rfcurl'  ) {
-
-               $valid = '0123456789';
-               $internal = false;
-
-               $a = split( $keyword, ' '.$text );
-               if ( count ( $a ) < 2 ) {
-                       return $text;
-               }
-               $text = substr( array_shift( $a ), 1);
-
-               /* Check if keyword is preceed by [[.
-                * This test is made here cause of the array_shift above
-                * that prevent the test to be done in the foreach.
-                */
-               if ( substr( $text, -2 ) == '[[' ) {
-                       $internal = true;
-               }
-
-               foreach ( $a as $x ) {
-                       /* token might be empty if we have RFC RFC 1234 */
-                       if ( $x=='' ) {
-                               $text.=$keyword;
-                               continue;
-                               }
-
-                       # hack: don't replace inside thumbnail title/alt
-                       # attributes
-                       if(preg_match('/<[^>]+(alt|title)="[^">]*$/', $text)) {
-                               $text .= $keyword . $x;
-                               continue;
-                       }
-                       
-                       $id = $blank = '' ;
-
-                       /** remove and save whitespaces in $blank */
-                       while ( $x{0} == ' ' ) {
-                               $blank .= ' ';
-                               $x = substr( $x, 1 );
-                       }
-
-                       /** remove and save the rfc number in $id */
-                       while ( strstr( $valid, $x{0} ) != false ) {
-                               $id .= $x{0};
-                               $x = substr( $x, 1 );
-                       }
-
-                       if ( $id == '' ) {
-                               /* call back stripped spaces*/
-                               $text .= $keyword.$blank.$x;
-                       } elseif( $internal ) {
-                               /* normal link */
-                               $text .= $keyword.$id.$x;
-                       } else {
-                               /* build the external link*/
-                               $url = wfMsg( $urlmsg, $id);
-                               $sk =& $this->mOptions->getSkin();
-                               $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
-                               $text .= "<a href='{$url}'{$la}>{$keyword}{$id}</a>{$x}";
-                       }
-
-                       /* Check if the next RFC keyword is preceed by [[ */
-                       $internal = ( substr($x,-2) == '[[' );
-               }
-               return $text;
-       }
-
        /**
         * Transform wiki markup when saving a page by doing \r\n -> \n
         * conversion, substitting signatures, {{subst:}} templates, etc.
@@ -3570,7 +3540,7 @@ class Parser
        function preSaveTransform( $text, &$title, &$user, $options, $clearState = true ) {
                $this->mOptions = $options;
                $this->mTitle =& $title;
-               $this->mOutputType = OT_WIKI;
+               $this->setOutputType( OT_WIKI );
 
                if ( $clearState ) {
                        $this->clearState();
@@ -3581,7 +3551,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 );
@@ -3613,10 +3583,10 @@ class Parser
                # Variable replacement
                # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
                $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 );
                $text = strtr( $text, array(
@@ -3629,35 +3599,28 @@ class Parser
                #
                global $wgLegalTitleChars;
                $tc = "[$wgLegalTitleChars]";
-               $np = str_replace( array( '(', ')' ), array( '', '' ), $tc ); # No parens
 
                $namespacechar = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
-               $conpat = "/^({$np}+) \\(({$tc}+)\\)$/";
+               $conpat = "/^{$tc}+?( \\({$tc}+\\)|)$/";
 
-               $p1 = "/\[\[({$np}+) \\(({$np}+)\\)\\|]]/";             # [[page (context)|]]
-               $p2 = "/\[\[\\|({$tc}+)]]/";                                    # [[|page]]
-               $p3 = "/\[\[(:*$namespacechar+):({$np}+)\\|]]/";                # [[namespace:page|]] and [[:namespace:page|]]
-               $p4 = "/\[\[(:*$namespacechar+):({$np}+) \\(({$np}+)\\)\\|]]/"; # [[ns:page (cont)|]] and [[:ns:page (cont)|]]
-               $context = '';
-               $t = $this->mTitle->getText();
-               if ( preg_match( $conpat, $t, $m ) ) {
-                       $context = $m[2];
-               }
-               $text = preg_replace( $p4, '[[\\1:\\2 (\\3)|\\2]]', $text );
-               $text = preg_replace( $p1, '[[\\1 (\\2)|\\1]]', $text );
-               $text = preg_replace( $p3, '[[\\1:\\2|\\2]]', $text );
+               $p1 = "/\[\[(:?$namespacechar+:|:|)({$tc}+?)( \\({$tc}+\\)|)\\|]]/";    # [[ns:page (context)|]]
+               $p2 = "/\[\[\\|({$tc}+)]]/";                                            # [[|page]]
 
-               if ( '' == $context ) {
-                       $text = preg_replace( $p2, '[[\\1]]', $text );
+               $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
+
+               $t = $this->mTitle->getText();
+               if ( preg_match( $conpat, $t, $m ) && '' != $m[1] ) {
+                       $text = preg_replace( $p2, "[[\\1{$m[1]}|\\1]]", $text );
                } else {
-                       $text = preg_replace( $p2, "[[\\1 ({$context})|\\1]]", $text );
+                       # if $m[1] is empty, don't bother duplicating the title
+                       $text = preg_replace( $p2, '[[\\1]]', $text );
                }
 
                # Trim trailing whitespace
-               # MAG_END (__END__) tag allows for trailing
+               # __END__ tag allows for trailing
                # whitespace to be deliberately included
                $text = rtrim( $text );
-               $mw =& MagicWord::get( MAG_END );
+               $mw =& MagicWord::get( 'end' );
                $mw->matchAndRemove( $text );
 
                return $text;
@@ -3675,7 +3638,7 @@ class Parser
                $username = $user->getName();
                $nickname = $user->getOption( 'nickname' );
                $nickname = $nickname === '' ? $username : $nickname;
-       
+
                if( $user->getBoolOption( 'fancysig' ) !== false ) {
                        # Sig. might contain markup; validate this
                        if( $this->validateSig( $nickname ) !== false ) {
@@ -3688,6 +3651,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 ) . ']]' );
@@ -3702,11 +3668,11 @@ class Parser
        function validateSig( $text ) {
                return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
        }
-       
+
        /**
         * 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
@@ -3716,19 +3682,29 @@ class Parser
        function cleanSig( $text, $parsing = false ) {
                global $wgTitle;
                $this->startExternalParse( $wgTitle, new ParserOptions(), $parsing ? OT_WIKI : OT_MSG );
-       
-               $substWord = MagicWord::get( MAG_SUBST );
+
+               $substWord = MagicWord::get( 'subst' );
                $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
                $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();    
+
+               $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()
         * so that an external function can call some class members with confidence
@@ -3737,7 +3713,7 @@ class Parser
        function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
                $this->mTitle =& $title;
                $this->mOptions = $options;
-               $this->mOutputType = $outputType;
+               $this->setOutputType( $outputType );
                if ( $clearState ) {
                        $this->clearState();
                }
@@ -3767,7 +3743,7 @@ class Parser
 
                $this->mTitle = $wgTitle;
                $this->mOptions = $options;
-               $this->mOutputType = OT_MSG;
+               $this->setOutputType( OT_MSG );
                $this->clearState();
                $text = $this->replaceVariables( $text );
 
@@ -3816,15 +3792,42 @@ class Parser
         *
         * @public
         *
-        * @param string $name The function name. Function names are case-insensitive.
+        * @param string $id The magic word ID
         * @param mixed $callback The callback function (and object) to use
+        * @param integer $flags a combination of the following flags: 
+        *                SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
         *
         * @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, $flags = 0 ) {
+               $oldVal = @$this->mFunctionHooks[$id];
+               $this->mFunctionHooks[$id] = $callback;
+
+               # Add to function cache
+               $mw = MagicWord::get( $id );
+               if ( !$mw ) {
+                       throw new MWException( 'The calling convention to Parser::setFunctionHook() has changed, ' .
+                               'it is now required to pass a MagicWord ID as the first parameter.' );
+               }
+
+               $synonyms = $mw->getSynonyms();
+               $sensitive = intval( $mw->isCaseSensitive() );
+
+               foreach ( $synonyms as $syn ) {
+                       # Case
+                       if ( !$sensitive ) {
+                               $syn = strtolower( $syn );
+                       }
+                       # Add leading hash
+                       if ( !( $flags & SFH_NO_HASH ) ) {
+                               $syn = '#' . $syn;
+                       }
+                       # Remove trailing colon
+                       if ( substr( $syn, -1, 1 ) == ':' ) {
+                               $syn = substr( $syn, 0, -1 );
+                       }
+                       $this->mFunctionSynonyms[$sensitive][$syn] = $id;
+               }
                return $oldVal;
        }
 
@@ -4028,6 +4031,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 +4053,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
@@ -4052,7 +4072,8 @@ class Parser
                        if ( count( $matches ) == 0 ) {
                                continue;
                        }
-                       $nt =& Title::newFromText( $matches[1] );
+                       $tp = Title::newFromText( $matches[1] );
+                       $nt =& $tp;
                        if( is_null( $nt ) ) {
                                # Bogus title. Ignore these so we don't bomb out later.
                                continue;
@@ -4101,14 +4122,14 @@ class Parser
 
                $part = explode( '|', $options);
 
-               $mwThumb  =& MagicWord::get( MAG_IMG_THUMBNAIL );
-               $mwManualThumb =& MagicWord::get( MAG_IMG_MANUALTHUMB );
-               $mwLeft   =& MagicWord::get( MAG_IMG_LEFT );
-               $mwRight  =& MagicWord::get( MAG_IMG_RIGHT );
-               $mwNone   =& MagicWord::get( MAG_IMG_NONE );
-               $mwWidth  =& MagicWord::get( MAG_IMG_WIDTH );
-               $mwCenter =& MagicWord::get( MAG_IMG_CENTER );
-               $mwFramed =& MagicWord::get( MAG_IMG_FRAMED );
+               $mwThumb  =& MagicWord::get( 'img_thumbnail' );
+               $mwManualThumb =& MagicWord::get( 'img_manualthumb' );
+               $mwLeft   =& MagicWord::get( 'img_left' );
+               $mwRight  =& MagicWord::get( 'img_right' );
+               $mwNone   =& MagicWord::get( 'img_none' );
+               $mwWidth  =& MagicWord::get( 'img_width' );
+               $mwCenter =& MagicWord::get( 'img_center' );
+               $mwFramed =& MagicWord::get( 'img_framed' );
                $caption = '';
 
                $width = $height = $framed = $thumb = false;
@@ -4134,7 +4155,7 @@ class Parser
                                # remember to set an alignment, don't render immediately
                                $align = 'none';
                        } elseif ( $wgUseImageResize && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) {
-                               wfDebug( "MAG_IMG_WIDTH match: $match\n" );
+                               wfDebug( "img_width match: $match\n" );
                                # $match is the image width in pixels
                                if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $match, $m ) ) {
                                        $width = intval( $m[1] );
@@ -4205,6 +4226,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->setOutputType( OT_WIKI );
+
+               $striptext = $this->strip( $text, $striparray, true );
+
+               $this->setOutputType( $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 +4406,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 +4426,7 @@ class ParserOutput
                $this->mHTMLtitle = "" ;
                $this->mSubtitle = "" ;
                $this->mNewSection = false;
+               $this->mNoGallery = false;
        }
 
        function getText()                   { return $this->mText; }
@@ -4257,6 +4439,8 @@ class ParserOutput
        function &getTemplates()             { return $this->mTemplates; }
        function &getImages()                { return $this->mImages; }
        function &getExternalLinks()         { return $this->mExternalLinks; }
+       function getNoGallery()              { return $this->mNoGallery; }
+       function getSubtitle()               { return $this->mSubtitle; }
 
        function containsOldMagic()          { return $this->mContainsOldMagic; }
        function setText( $text )            { return wfSetVar( $this->mText, $text ); }
@@ -4264,7 +4448,8 @@ class ParserOutput
        function setCategoryLinks( $cl )     { return wfSetVar( $this->mCategories, $cl ); }
        function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
        function setCacheTime( $t )          { return wfSetVar( $this->mCacheTime, $t ); }
-       function setTitleText( $t )          { return wfSetVar ($this->mTitleText, $t); }
+       function setTitleText( $t )          { return wfSetVar($this->mTitleText, $t); }
+       function setSubtitle( $st )          { return wfSetVar( $this->mSubtitle, $st ); }
 
        function addCategory( $c, $sort )    { $this->mCategories[$c] = $sort; }
        function addImage( $name )           { $this->mImages[$name] = 1; }
@@ -4278,12 +4463,15 @@ class ParserOutput
                return (bool)$this->mNewSection;
        }
 
-       function addLink( $title, $id ) {
+       function addLink( $title, $id = null ) {
                $ns = $title->getNamespace();
                $dbk = $title->getDBkey();
                if ( !isset( $this->mLinks[$ns] ) ) {
                        $this->mLinks[$ns] = array();
                }
+               if ( is_null( $id ) ) {
+                       $id = $title->getArticleID();
+               }
                $this->mLinks[$ns][$dbk] = $id;
        }
 
@@ -4335,19 +4523,37 @@ class ParserOptions
        var $mAllowSpecialInclusion;     # Allow inclusion of special pages
        var $mTidy;                      # Ask for tidy cleanup
        var $mInterfaceMessage;          # Which lang to call for PLURAL and GRAMMAR
+       var $mMaxIncludeSize;            # Maximum size of template expansions, in bytes
+       var $mRemoveComments;            # Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS
+
+       var $mUser;                      # Stored user object, just used to initialise the skin
 
        function getUseTeX()                        { return $this->mUseTeX; }
        function getUseDynamicDates()               { return $this->mUseDynamicDates; }
        function getInterwikiMagic()                { return $this->mInterwikiMagic; }
        function getAllowExternalImages()           { return $this->mAllowExternalImages; }
        function getAllowExternalImagesFrom()       { return $this->mAllowExternalImagesFrom; }
-       function &getSkin()                         { return $this->mSkin; }
-       function getDateFormat()                    { return $this->mDateFormat; }
        function getEditSection()                   { return $this->mEditSection; }
        function getNumberHeadings()                { return $this->mNumberHeadings; }
        function getAllowSpecialInclusion()         { return $this->mAllowSpecialInclusion; }
        function getTidy()                          { return $this->mTidy; }
        function getInterfaceMessage()              { return $this->mInterfaceMessage; }
+       function getMaxIncludeSize()                { return $this->mMaxIncludeSize; }
+       function getRemoveComments()                { return $this->mRemoveComments; }
+
+       function &getSkin() {
+               if ( !isset( $this->mSkin ) ) {
+                       $this->mSkin = $this->mUser->getSkin();
+               }
+               return $this->mSkin;
+       }
+
+       function getDateFormat() { 
+               if ( !isset( $this->mDateFormat ) ) {
+                       $this->mDateFormat = $this->mUser->getDatePreference();
+               }
+               return $this->mDateFormat;
+       }
 
        function setUseTeX( $x )                    { return wfSetVar( $this->mUseTeX, $x ); }
        function setUseDynamicDates( $x )           { return wfSetVar( $this->mUseDynamicDates, $x ); }
@@ -4361,49 +4567,55 @@ class ParserOptions
        function setTidy( $x )                      { return wfSetVar( $this->mTidy, $x); }
        function setSkin( &$x ) { $this->mSkin =& $x; }
        function setInterfaceMessage( $x )          { return wfSetVar( $this->mInterfaceMessage, $x); }
+       function setMaxIncludeSize( $x )            { return wfSetVar( $this->mMaxIncludeSize, $x ); }
+       function setRemoveComments( $x )            { return wfSetVar( $this->mRemoveComments, $x ); }
 
-       function ParserOptions() {
-               global $wgUser;
-               $this->initialiseFromUser( $wgUser );
+       function ParserOptions( $user = null ) {
+               $this->initialiseFromUser( $user );
        }
 
        /**
         * Get parser options
         * @static
         */
-       function newFromUser( &$user ) {
-               $popts = new ParserOptions;
-               $popts->initialiseFromUser( $user );
-               return $popts;
+       static function newFromUser( $user ) {
+               return new ParserOptions( $user );
        }
 
        /** Get user options */
-       function initialiseFromUser( &$userInput ) {
+       function initialiseFromUser( $userInput ) {
                global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
-               global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion;
+               global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize;
                $fname = 'ParserOptions::initialiseFromUser';
                wfProfileIn( $fname );
                if ( !$userInput ) {
-                       $user = new User;
-                       $user->setLoaded( true );
+                       global $wgUser;
+                       if ( isset( $wgUser ) ) {
+                               $user = $wgUser;
+                       } else {
+                               $user = new User;
+                               $user->setLoaded( true );
+                       }
                } else {
                        $user =& $userInput;
                }
 
+               $this->mUser = $user;
+
                $this->mUseTeX = $wgUseTeX;
                $this->mUseDynamicDates = $wgUseDynamicDates;
                $this->mInterwikiMagic = $wgInterwikiMagic;
                $this->mAllowExternalImages = $wgAllowExternalImages;
                $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
-               wfProfileIn( $fname.'-skin' );
-               $this->mSkin =& $user->getSkin();
-               wfProfileOut( $fname.'-skin' );
-               $this->mDateFormat = $user->getOption( 'date' );
+               $this->mSkin = null; # Deferred
+               $this->mDateFormat = null; # Deferred
                $this->mEditSection = true;
                $this->mNumberHeadings = $user->getOption( 'numberheadings' );
                $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion;
                $this->mTidy = false;
                $this->mInterfaceMessage = false;
+               $this->mMaxIncludeSize = $wgMaxArticleSize * 1024;
+               $this->mRemoveComments = true;
                wfProfileOut( $fname );
        }
 }
@@ -4465,6 +4677,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