Make RECOVER_ORIG preserve extension tags
[lhc/web/wiklou.git] / includes / parser / Parser.php
index 7423006..66cfd55 100644 (file)
@@ -167,7 +167,6 @@ class Parser {
        var $mLinkID;
        var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
        var $mDefaultSort;
-       var $mTplExpandCache; # empty-frame expansion cache
        var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
        var $mExpensiveFunctionCount; # number of expensive parser function calls
        var $mShowToc, $mForceTocPosition;
@@ -212,8 +211,12 @@ class Parser {
        var $mLangLinkLanguages;
 
        /**
-        * Constructor
-        *
+        * @var boolean Recursive call protection.
+        * This variable should be treated as if it were private.
+        */
+       public $mInParse = false;
+
+       /**
         * @param array $conf
         */
        public function __construct( $conf = array() ) {
@@ -254,6 +257,7 @@ class Parser {
         * Allow extensions to clean up when the parser is cloned
         */
        function __clone() {
+               $this->mInParse = false;
                wfRunHooks( 'ParserCloned', array( $this ) );
        }
 
@@ -316,7 +320,7 @@ class Parser {
                $this->mStripState = new StripState( $this->mUniqPrefix );
 
                # Clear these on every parse, bug 4549
-               $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
+               $this->mTplRedirCache = $this->mTplDomCache = array();
 
                $this->mShowToc = true;
                $this->mForceTocPosition = false;
@@ -353,7 +357,9 @@ class Parser {
         * @param int $revid Number to pass in {{REVISIONID}}
         * @return ParserOutput A ParserOutput
         */
-       public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) {
+       public function parse( $text, Title $title, ParserOptions $options,
+               $linestart = true, $clearState = true, $revid = null
+       ) {
                /**
                 * First pass--just handle <nowiki> sections, pass the rest off
                 * to internalParse() which does all the real work.
@@ -364,6 +370,10 @@ class Parser {
                wfProfileIn( __METHOD__ );
                wfProfileIn( $fname );
 
+               if ( $clearState ) {
+                       $magicScopeVariable = $this->lock();
+               }
+
                $this->startParse( $title, $options, self::OT_HTML, $clearState );
 
                $this->mInputSize = strlen( $text );
@@ -591,7 +601,7 @@ class Parser {
         * If $frame is not provided, then template variables (e.g., {{{1}}}) within $text are not expanded
         *
         * @param string $text Text extension wants to have parsed
-        * @param PPFrame $frame The frame to use for expanding any template variables
+        * @param bool|PPFrame $frame The frame to use for expanding any template variables
         *
         * @return string
         */
@@ -607,21 +617,24 @@ class Parser {
        /**
         * Expand templates and variables in the text, producing valid, static wikitext.
         * Also removes comments.
+        * Do not call this function recursively.
         * @param string $text
         * @param Title $title
         * @param ParserOptions $options
         * @param int|null $revid
+        * @param bool|PPFrame $frame
         * @return mixed|string
         */
-       function preprocess( $text, Title $title = null, ParserOptions $options, $revid = null ) {
+       function preprocess( $text, Title $title = null, ParserOptions $options, $revid = null, $frame = false ) {
                wfProfileIn( __METHOD__ );
+               $magicScopeVariable = $this->lock();
                $this->startParse( $title, $options, self::OT_PREPROCESS, true );
                if ( $revid !== null ) {
                        $this->mRevisionId = $revid;
                }
                wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
                wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->replaceVariables( $text );
+               $text = $this->replaceVariables( $text, $frame );
                $text = $this->mStripState->unstripBoth( $text );
                wfProfileOut( __METHOD__ );
                return $text;
@@ -632,7 +645,7 @@ class Parser {
         * hook.
         *
         * @param string $text Text to be expanded
-        * @param PPFrame $frame The frame to use for expanding any template variables
+        * @param bool|PPFrame $frame The frame to use for expanding any template variables
         * @return string
         * @since 1.19
         */
@@ -662,6 +675,7 @@ class Parser {
                $text = $msg->params( $params )->plain();
 
                # Parser (re)initialisation
+               $magicScopeVariable = $this->lock();
                $this->startParse( $title, $options, self::OT_PLAIN, true );
 
                $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
@@ -1078,7 +1092,10 @@ class Parser {
                                array_push( $tr_history, false );
                                array_push( $td_history, false );
                                array_push( $last_tag_history, '' );
-                       } elseif ( $first_character === '|' || $first_character === '!' || substr( $line, 0, 2 ) === '|+' ) {
+                       } elseif ( $first_character === '|'
+                               || $first_character === '!'
+                               || substr( $line, 0, 2 ) === '|+'
+                       ) {
                                # This might be cell elements, td, th or captions
                                if ( substr( $line, 0, 2 ) === '|+' ) {
                                        $first_character = '+';
@@ -1223,7 +1240,12 @@ class Parser {
                }
 
                wfRunHooks( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) );
-               $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
+               $text = Sanitizer::removeHTMLtags(
+                       $text,
+                       array( &$this, 'attributeStripCallback' ),
+                       false,
+                       array_keys( $this->mTransparentTagHooks )
+               );
                wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
 
                # Tables need to come after variable replacement for things to work
@@ -1629,7 +1651,8 @@ class Parser {
                $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
                if ( $bits === false ) {
                        wfProfileOut( __METHOD__ );
-                       throw new MWException( "PCRE needs to be compiled with --enable-unicode-properties in order for MediaWiki to function" );
+                       throw new MWException( "PCRE needs to be compiled with "
+                               . "--enable-unicode-properties in order for MediaWiki to function" );
                }
                $s = array_shift( $bits );
 
@@ -1775,13 +1798,12 @@ class Parser {
        /**
         * make an image if it's allowed, either through the global
         * option, through the exception, or through the on-wiki whitelist
-        * @private
         *
-        * $param string $url
+        * @param string $url
         *
         * @return string
         */
-       function maybeMakeExternalImage( $url ) {
+       private function maybeMakeExternalImage( $url ) {
                $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
                $imagesexception = !empty( $imagesfrom );
                $text = false;
@@ -1799,16 +1821,23 @@ class Parser {
                } else {
                        $imagematch = false;
                }
+
                if ( $this->mOptions->getAllowExternalImages()
-                       || ( $imagesexception && $imagematch ) ) {
+                       || ( $imagesexception && $imagematch )
+               ) {
                        if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
                                # Image found
                                $text = Linker::makeExternalImage( $url );
                        }
                }
                if ( !$text && $this->mOptions->getEnableImageWhitelist()
-                       && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
-                       $whitelist = explode( "\n", wfMessage( 'external_image_whitelist' )->inContentLanguage()->text() );
+                       && preg_match( self::EXT_IMAGE_REGEX, $url )
+               ) {
+                       $whitelist = explode(
+                               "\n",
+                               wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
+                       );
+
                        foreach ( $whitelist as $entry ) {
                                # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
                                if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
@@ -1901,8 +1930,11 @@ class Parser {
                $useSubpages = $this->areSubpagesAllowed();
                wfProfileOut( __METHOD__ . '-setup' );
 
+               // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
                # Loop for each link
                for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
+                       // @codingStandardsIgnoreStart
+
                        # Check for excessive memory usage
                        if ( $holders->isBig() ) {
                                # Too big
@@ -1953,7 +1985,8 @@ class Parser {
                                        $m[1] = str_replace( array( '<', '>' ), array( '&lt;', '&gt;' ), rawurldecode( $m[1] ) );
                                }
                                $trail = $m[3];
-                       } elseif ( preg_match( $e1_img, $line, $m ) ) { # Invalid, but might be an image with a link in its caption
+                       } 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 ( strpos( $m[1], '%' ) !== false ) {
@@ -2063,7 +2096,9 @@ class Parser {
                        if ( $noforce ) {
                                # Interwikis
                                wfProfileIn( __METHOD__ . "-interwiki" );
-                               if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
+                               if ( $iw && $this->mOptions->getInterwikiMagic()
+                                       && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' )
+                               ) {
                                        // XXX: the above check prevents links to sites with identifiers that are not language codes
 
                                        # Bug 24502: filter duplicates
@@ -2293,13 +2328,13 @@ class Parser {
                $result = $this->closeParagraph();
 
                if ( '*' === $char ) {
-                       $result .= "<ul>\n<li>";
+                       $result .= "<ul><li>";
                } elseif ( '#' === $char ) {
-                       $result .= "<ol>\n<li>";
+                       $result .= "<ol><li>";
                } elseif ( ':' === $char ) {
-                       $result .= "<dl>\n<dd>";
+                       $result .= "<dl><dd>";
                } elseif ( ';' === $char ) {
-                       $result .= "<dl>\n<dt>";
+                       $result .= "<dl><dt>";
                        $this->mDTopen = true;
                } else {
                        $result = '<!-- ERR 1 -->';
@@ -2335,7 +2370,7 @@ class Parser {
        }
 
        /**
-        * TODO: document
+        * @todo Document
         * @param string $char
         * @private
         *
@@ -2343,20 +2378,20 @@ class Parser {
         */
        function closeList( $char ) {
                if ( '*' === $char ) {
-                       $text = "</li>\n</ul>";
+                       $text = "</li></ul>";
                } elseif ( '#' === $char ) {
-                       $text = "</li>\n</ol>";
+                       $text = "</li></ol>";
                } elseif ( ':' === $char ) {
                        if ( $this->mDTopen ) {
                                $this->mDTopen = false;
-                               $text = "</dt>\n</dl>";
+                               $text = "</dt></dl>";
                        } else {
-                               $text = "</dd>\n</dl>";
+                               $text = "</dd></dl>";
                        }
                } else {
                        return '<!-- ERR 3 -->';
                }
-               return $text . "\n";
+               return $text;
        }
        /**#@-*/
 
@@ -2454,6 +2489,9 @@ class Parser {
                                }
 
                                # Open prefixes where appropriate.
+                               if (  $lastPrefix && $prefixLength > $commonPrefixLength ) {
+                                       $output .= "\n";
+                               }
                                while ( $prefixLength > $commonPrefixLength ) {
                                        $char = substr( $prefix, $commonPrefixLength, 1 );
                                        $output .= $this->openList( $char );
@@ -2467,6 +2505,9 @@ class Parser {
                                        }
                                        ++$commonPrefixLength;
                                }
+                               if ( !$prefixLength && $lastPrefix ) {
+                                       $output .= "\n";
+                               }
                                $lastPrefix = $prefix2;
                        }
 
@@ -2475,13 +2516,22 @@ class Parser {
                                wfProfileIn( __METHOD__ . "-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|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
+                               $openmatch = preg_match(
+                                       '/(?:<table|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|'
+                                               . '<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS',
+                                       $t
+                               );
                                $closematch = preg_match(
-                                       '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|' .
-                                       '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|' . $this->mUniqPrefix . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS', $t );
+                                       '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'
+                                               . '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|'
+                                               . $this->mUniqPrefix
+                                               . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS',
+                                       $t
+                               );
+
                                if ( $openmatch or $closematch ) {
                                        $paragraphStack = false;
-                                       # TODO bug 5718: paragraph closed
+                                       # @todo bug 5718: paragraph closed
                                        $output .= $this->closeParagraph();
                                        if ( $preOpenMatch and !$preCloseMatch ) {
                                                $this->mInPre = true;
@@ -2493,7 +2543,10 @@ class Parser {
                                        }
                                        $inBlockElem = !$closematch;
                                } elseif ( !$inBlockElem && !$this->mInPre ) {
-                                       if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' || trim( $t ) != '' ) and !$inBlockquote ) {
+                                       if ( ' ' == substr( $t, 0, 1 )
+                                               && ( $this->mLastSection === 'pre' || trim( $t ) != '' )
+                                               && !$inBlockquote
+                                       ) {
                                                # pre
                                                if ( $this->mLastSection !== 'pre' ) {
                                                        $paragraphStack = false;
@@ -2536,12 +2589,18 @@ class Parser {
                                $this->mInPre = false;
                        }
                        if ( $paragraphStack === false ) {
-                               $output .= $t . "\n";
+                               $output .= $t;
+                               if ( $prefixLength === 0 ) {
+                                       $output .= "\n";
+                               }
                        }
                }
                while ( $prefixLength ) {
                        $output .= $this->closeList( $prefix2[$prefixLength - 1] );
                        --$prefixLength;
+                       if ( !$prefixLength ) {
+                               $output .= "\n";
+                       }
                }
                if ( $this->mLastSection != '' ) {
                        $output .= '</' . $this->mLastSection . '>';
@@ -2731,7 +2790,7 @@ class Parser {
         * @return string
         */
        function getVariableValue( $index, $frame = false ) {
-               global $wgContLang, $wgSitename, $wgServer;
+               global $wgContLang, $wgSitename, $wgServer, $wgServerName;
                global $wgArticlePath, $wgScriptPath, $wgStylePath;
 
                if ( is_null( $this->mTitle ) ) {
@@ -2823,13 +2882,21 @@ class Parser {
                                $value = wfEscapeWikiText( $this->mTitle->getRootText() );
                                break;
                        case 'rootpagenamee':
-                               $value = wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getRootText() ) ) );
+                               $value = wfEscapeWikiText( wfUrlEncode( str_replace(
+                                       ' ',
+                                       '_',
+                                       $this->mTitle->getRootText()
+                               ) ) );
                                break;
                        case 'basepagename':
                                $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
                                break;
                        case 'basepagenamee':
-                               $value = wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ) );
+                               $value = wfEscapeWikiText( wfUrlEncode( str_replace(
+                                       ' ',
+                                       '_',
+                                       $this->mTitle->getBaseText()
+                               ) ) );
                                break;
                        case 'talkpagename':
                                if ( $this->mTitle->canTalk() ) {
@@ -2940,7 +3007,9 @@ class Parser {
                                $value = $this->mTitle->getNamespace();
                                break;
                        case 'talkspace':
-                               $value = $this->mTitle->canTalk() ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() ) : '';
+                               $value = $this->mTitle->canTalk()
+                                       ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
+                                       : '';
                                break;
                        case 'talkspacee':
                                $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
@@ -2972,13 +3041,19 @@ class Parser {
                                $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
                                break;
                        case 'localdayname':
-                               $value = $pageLang->getWeekdayName( (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1 );
+                               $value = $pageLang->getWeekdayName(
+                                       (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
+                               );
                                break;
                        case 'localyear':
                                $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
                                break;
                        case 'localtime':
-                               $value = $pageLang->time( MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ), false, false );
+                               $value = $pageLang->time(
+                                       MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
+                                       false,
+                                       false
+                               );
                                break;
                        case 'localhour':
                                $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
@@ -3032,8 +3107,7 @@ class Parser {
                        case 'server':
                                return $wgServer;
                        case 'servername':
-                               $serverParts = wfParseUrl( $wgServer );
-                               return $serverParts && isset( $serverParts['host'] ) ? $serverParts['host'] : $wgServer;
+                               return $wgServerName;
                        case 'scriptpath':
                                return $wgScriptPath;
                        case 'stylepath':
@@ -3048,7 +3122,11 @@ class Parser {
                                break;
                        default:
                                $ret = null;
-                               wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret, &$frame ) );
+                               wfRunHooks(
+                                       'ParserGetVariableValueSwitch',
+                                       array( &$this, &$this->mVarCache, &$index, &$ret, &$frame )
+                               );
+
                                return $ret;
                }
 
@@ -3132,15 +3210,16 @@ class Parser {
         *  self::OT_HTML: all templates and extension tags
         *
         * @param string $text The text to transform
-        * @param PPFrame $frame Object describing the arguments passed to the template.
-        *        Arguments may also be provided as an associative array, as was the usual case before MW1.12.
-        *        Providing arguments this way may be useful for extensions wishing to perform variable replacement explicitly.
-        * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion
-        * @private
-        *
+        * @param bool|PPFrame $frame Object describing the arguments passed to the
+        *   template. Arguments may also be provided as an associative array, as
+        *   was the usual case before MW1.12. Providing arguments this way may be
+        *   useful for extensions wishing to perform variable replacement
+        *   explicitly.
+        * @param bool $argsOnly Only do argument (triple-brace) expansion, not
+        *   double-brace expansion.
         * @return string
         */
-       function replaceVariables( $text, $frame = false, $argsOnly = false ) {
+       public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
                # Is there any text? Also, Prevent too big inclusions!
                if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
                        return $text;
@@ -3150,7 +3229,8 @@ class Parser {
                if ( $frame === false ) {
                        $frame = $this->getPreprocessor()->newFrame();
                } elseif ( !( $frame instanceof PPFrame ) ) {
-                       wfDebug( __METHOD__ . " called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
+                       wfDebug( __METHOD__ . " called using plain parameters instead of "
+                               . "a PPFrame instance. Creating custom frame.\n" );
                        $frame = $this->getPreprocessor()->newCustomFrame( $frame );
                }
 
@@ -3211,8 +3291,8 @@ class Parser {
         *   'expansion-depth-exceeded' (corresponding messages:
         *       'expansion-depth-exceeded-warning',
         *       'expansion-depth-exceeded-category')
-        * @param int|null $current Current value
-        * @param int|null $max Maximum allowed, when an explicit limit has been
+        * @param string|int|null $current Current value
+        * @param string|int|null $max Maximum allowed, when an explicit limit has been
         *       exceeded, provide the values (optional)
         */
        function limitationWarn( $limitationType, $current = '', $max = '' ) {
@@ -3228,25 +3308,31 @@ class Parser {
         * replacing any variables or templates within the template.
         *
         * @param array $piece The parts of the template
-        *  $piece['title']: the title, i.e. the part before the |
-        *  $piece['parts']: the parameter array
-        *  $piece['lineStart']: whether the brace was at the start of a line
+        *   $piece['title']: the title, i.e. the part before the |
+        *   $piece['parts']: the parameter array
+        *   $piece['lineStart']: whether the brace was at the start of a line
         * @param PPFrame $frame The current frame, contains template arguments
-        * @throws MWException
+        * @throws Exception
         * @return string The text of the template
-        * @private
         */
-       function braceSubstitution( $piece, $frame ) {
+       public function braceSubstitution( $piece, $frame ) {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-setup' );
 
-               # Flags
-               $found = false;             # $text has been filled
-               $nowiki = false;            # wiki markup in $text should be escaped
-               $isHTML = false;            # $text is HTML, armour it against wikitext transformation
-               $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
-               $isChildObj = false;        # $text is a DOM node needing expansion in a child frame
-               $isLocalObj = false;        # $text is a DOM node needing expansion in the current frame
+               // Flags
+
+               // $text has been filled
+               $found = false;
+               // wiki markup in $text should be escaped
+               $nowiki = false;
+               // $text is HTML, armour it against wikitext transformation
+               $isHTML = false;
+               // Force interwiki transclusion to be done in raw mode not rendered
+               $forceRawInterwiki = false;
+               // $text is a DOM node needing expansion in a child frame
+               $isChildObj = false;
+               // $text is a DOM node needing expansion in the current frame
+               $isLocalObj = false;
 
                # Title object, where $text came from
                $title = false;
@@ -3261,7 +3347,8 @@ class Parser {
                $originalTitle = $part1;
 
                # $args is a list of argument nodes, starting from index 0, not including $part1
-               # @todo FIXME: If piece['parts'] is null then the call to getLength() below won't work b/c this $args isn't an object
+               # @todo FIXME: If piece['parts'] is null then the call to getLength()
+               # below won't work b/c this $args isn't an object
                $args = ( null == $piece['parts'] ) ? array() : $piece['parts'];
                wfProfileOut( __METHOD__ . '-setup' );
 
@@ -3401,7 +3488,8 @@ class Parser {
                                        // "uselang" will have no effect since the Language object
                                        // is forced to the one defined in ParserOptions.
                                        $pageArgs = array();
-                                       for ( $i = 0; $i < $args->getLength(); $i++ ) {
+                                       $argsLength = $args->getLength();
+                                       for ( $i = 0; $i < $argsLength; $i++ ) {
                                                $bits = $args->item( $i )->splitArg();
                                                if ( strval( $bits['index'] ) === '' ) {
                                                        $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
@@ -3487,12 +3575,7 @@ class Parser {
                                $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
                        } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
                                # Expansion is eligible for the empty-frame cache
-                               if ( isset( $this->mTplExpandCache[$titleText] ) ) {
-                                       $text = $this->mTplExpandCache[$titleText];
-                               } else {
-                                       $text = $newFrame->expand( $text );
-                                       $this->mTplExpandCache[$titleText] = $text;
-                               }
+                               $text = $newFrame->cachedExpand( $titleText, $text );
                        } else {
                                # Uncached expansion
                                $text = $newFrame->expand( $text );
@@ -3534,7 +3617,8 @@ class Parser {
                                preg_replace( '/^:/', '', $originalTitle );
                                $text = "[[:$originalTitle]]";
                        }
-                       $text .= $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' );
+                       $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
+                               . 'post-expand include size too large -->' );
                        $this->limitationWarn( 'post-expand-template-inclusion' );
                }
 
@@ -3564,6 +3648,7 @@ class Parser {
         * @param PPFrame $frame The current frame, contains template arguments
         * @param string $function Function name
         * @param array $args Arguments to the function
+        * @throws MWException
         * @return array
         */
        public function callParserFunction( $frame, $function, array $args = array() ) {
@@ -3708,7 +3793,8 @@ class Parser {
         * @return array ( string or false, Title )
         */
        function fetchTemplateAndTitle( $title ) {
-               $templateCb = $this->mOptions->getTemplateCallback(); # Defaults to Parser::statelessFetchTemplate()
+               // Defaults to Parser::statelessFetchTemplate()
+               $templateCb = $this->mOptions->getTemplateCallback();
                $stuff = call_user_func( $templateCb, $title, $this );
                $text = $stuff['text'];
                $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
@@ -3740,7 +3826,7 @@ class Parser {
         * Can be overridden via ParserOptions::setTemplateCallback().
         *
         * @param Title $title
-        * @param Parser $parser
+        * @param bool|Parser $parser
         *
         * @return array
         */
@@ -3917,8 +4003,10 @@ class Parser {
                $status = $req->execute(); // Status object
                if ( $status->isOK() ) {
                        $text = $req->getContent();
-               } elseif ( $req->getStatus() != 200 ) { // Though we failed to fetch the content, this status is useless.
-                       return wfMessage( 'scarytranscludefailed-httpstatus', $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
+               } elseif ( $req->getStatus() != 200 ) {
+                       // Though we failed to fetch the content, this status is useless.
+                       return wfMessage( 'scarytranscludefailed-httpstatus' )
+                               ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
                } else {
                        return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
                }
@@ -4000,7 +4088,8 @@ class Parser {
                $name = $frame->expand( $params['name'] );
                $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
                $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
-               $marker = "{$this->mUniqPrefix}-$name-" . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
+               $marker = "{$this->mUniqPrefix}-$name-"
+                       . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
 
                $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
                        ( $this->ot['html'] || $this->ot['pre'] );
@@ -4132,7 +4221,9 @@ class Parser {
                if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
                        $this->mShowToc = false;
                }
-               if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) {
+               if ( isset( $this->mDoubleUnderscores['hiddencat'] )
+                       && $this->mTitle->getNamespace() == NS_CATEGORY
+               ) {
                        $this->addTrackingCategory( 'hidden-category-category' );
                }
                # (bug 8068) Allow control over whether robots index a page.
@@ -4227,7 +4318,11 @@ class Parser {
                # Get all headlines for numbering them and adding funky stuff like [edit]
                # links - this is for later, but we need the number of headlines right now
                $matches = array();
-               $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?' . '>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i', $text, $matches );
+               $numMatches = preg_match_all(
+                       '/<H(?P<level>[1-6])(?P<attrib>.*?' . '>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
+                       $text,
+                       $matches
+               );
 
                # if there are fewer than 4 headlines in the article, do not show TOC
                # unless it's been explicitly enabled.
@@ -4376,7 +4471,10 @@ class Parser {
                        # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
                        # to allow setting directionality in toc items.
                        $tocline = preg_replace(
-                               array( '#<(?!/?(span|sup|sub|i|b)(?: [^>]*)?>).*?' . '>#', '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|i|b))(?: .*?)?' . '>#' ),
+                               array(
+                                       '#<(?!/?(span|sup|sub|i|b)(?: [^>]*)?>).*?' . '>#',
+                                       '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|i|b))(?: .*?)?' . '>#'
+                               ),
                                array( '', '<$1>' ),
                                $safeHeadline
                        );
@@ -4440,7 +4538,11 @@ class Parser {
                        # Don't number the heading if it is the only one (looks silly)
                        if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
                                # the two are different if the line contains a link
-                               $headline = Html::element( 'span', array( 'class' => 'mw-headline-number' ), $numbering ) . ' ' . $headline;
+                               $headline = Html::element(
+                                       'span',
+                                       array( 'class' => 'mw-headline-number' ),
+                                       $numbering
+                               ) . ' ' . $headline;
                        }
 
                        # Create the anchor for linking from the TOC to the section
@@ -4490,14 +4592,22 @@ class Parser {
                                        # that sections inside <includeonly> should be counted.
                                        $editlinkArgs = array( $titleText, "T-$sectionIndex"/*, null */ );
                                } else {
-                                       $editlinkArgs = array( $this->mTitle->getPrefixedText(), $sectionIndex, $headlineHint );
+                                       $editlinkArgs = array(
+                                               $this->mTitle->getPrefixedText(),
+                                               $sectionIndex,
+                                               $headlineHint
+                                       );
                                }
-                               // We use a bit of pesudo-xml for editsection markers. The language converter is run later on
-                               // Using a UNIQ style marker leads to the converter screwing up the tokens when it converts stuff
-                               // And trying to insert strip tags fails too. At this point all real inputted tags have already been escaped
-                               // so we don't have to worry about a user trying to input one of these markers directly.
-                               // We use a page and section attribute to stop the language converter from converting these important bits
-                               // of data, but put the headline hint inside a content block because the language converter is supposed to
+                               // We use a bit of pesudo-xml for editsection markers. The
+                               // language converter is run later on. Using a UNIQ style marker
+                               // leads to the converter screwing up the tokens when it
+                               // converts stuff. And trying to insert strip tags fails too. At
+                               // this point all real inputted tags have already been escaped,
+                               // so we don't have to worry about a user trying to input one of
+                               // these markers directly. We use a page and section attribute
+                               // to stop the language converter from converting these
+                               // important bits of data, but put the headline hint inside a
+                               // content block because the language converter is supposed to
                                // be able to convert that piece of data.
                                $editlink = '<mw:editsection page="' . htmlspecialchars( $editlinkArgs[0] );
                                $editlink .= '" section="' . htmlspecialchars( $editlinkArgs[1] ) . '"';
@@ -4592,7 +4702,12 @@ class Parser {
         * @param bool $clearState Whether to clear the parser state first
         * @return string The altered wiki markup
         */
-       public function preSaveTransform( $text, Title $title, User $user, ParserOptions $options, $clearState = true ) {
+       public function preSaveTransform( $text, Title $title, User $user,
+               ParserOptions $options, $clearState = true
+       ) {
+               if ( $clearState ) {
+                       $magicScopeVariable = $this->lock();
+               }
                $this->startParse( $title, $options, self::OT_WIKI, $clearState );
                $this->setUser( $user );
 
@@ -4661,10 +4776,14 @@ class Parser {
                $tc = '[' . Title::legalChars() . ']';
                $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
 
-               $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";                   # [[ns:page (context)|]]
-               $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";                           # [[ns:page(context)|]] (double-width brackets, added in r40257)
-               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";         # [[ns:page (context), context|]] (using either single or double-width comma)
-               $p2 = "/\[\[\\|($tc+)]]/";                                              # [[|page]] (reverse pipe trick: add context from page title)
+               // [[ns:page (context)|]]
+               $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
+               // [[ns:page(context)|]] (double-width brackets, added in r40257)
+               $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
+               // [[ns:page (context), context|]] (using either single or double-width comma)
+               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
+               // [[|page]] (reverse pipe trick: add context from page title)
+               $p2 = "/\[\[\\|($tc+)]]/";
 
                # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
                $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
@@ -4741,7 +4860,8 @@ class Parser {
                $nickText = wfEscapeWikiText( $nickname );
                $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
 
-               return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()->title( $this->getTitle() )->text();
+               return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
+                       ->title( $this->getTitle() )->text();
        }
 
        /**
@@ -4767,6 +4887,7 @@ class Parser {
        public function cleanSig( $text, $parsing = false ) {
                if ( !$parsing ) {
                        global $wgTitle;
+                       $magicScopeVariable = $this->lock();
                        $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
                }
 
@@ -4814,7 +4935,9 @@ class Parser {
         * @param int $outputType
         * @param bool $clearState
         */
-       public function startExternalParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) {
+       public function startExternalParse( Title $title = null, ParserOptions $options,
+               $outputType, $clearState = true
+       ) {
                $this->startParse( $title, $options, $outputType, $clearState );
        }
 
@@ -4824,7 +4947,9 @@ class Parser {
         * @param int $outputType
         * @param bool $clearState
         */
-       private function startParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) {
+       private function startParse( Title $title = null, ParserOptions $options,
+               $outputType, $clearState = true
+       ) {
                $this->setTitle( $title );
                $this->mOptions = $options;
                $this->setOutputType( $outputType );
@@ -5508,7 +5633,7 @@ class Parser {
 
        /**
         * @param string $caption
-        * @param LinkHolderArray $holders
+        * @param LinkHolderArray|bool $holders
         * @return mixed|string
         */
        protected function stripAltText( $caption, $holders ) {
@@ -5549,7 +5674,7 @@ class Parser {
         * values, so they can be safely tested and escaped.
         *
         * @param string $text
-        * @param PPFrame $frame
+        * @param bool|PPFrame $frame
         * @return string
         */
        function attributeStripCallback( &$text, $frame = false ) {
@@ -5564,7 +5689,11 @@ class Parser {
         * @return array
         */
        function getTags() {
-               return array_merge( array_keys( $this->mTransparentTagHooks ), array_keys( $this->mTagHooks ), array_keys( $this->mFunctionTagHooks ) );
+               return array_merge(
+                       array_keys( $this->mTransparentTagHooks ),
+                       array_keys( $this->mTagHooks ),
+                       array_keys( $this->mFunctionTagHooks )
+               );
        }
 
        /**
@@ -5587,7 +5716,10 @@ class Parser {
                        list( $element, $content, $params, $tag ) = $data;
                        $tagName = strtolower( $element );
                        if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
-                               $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], array( $content, $params, $this ) );
+                               $output = call_user_func_array(
+                                       $this->mTransparentTagHooks[$tagName],
+                                       array( $content, $params, $this )
+                               );
                        } else {
                                $output = $tag;
                        }
@@ -5627,6 +5759,8 @@ class Parser {
         */
        private function extractSections( $text, $section, $mode, $newText = '' ) {
                global $wgTitle; # not generally used but removes an ugly failure mode
+
+               $magicScopeVariable = $this->lock();
                $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
                $outText = '';
                $frame = $this->getPreprocessor()->newFrame();
@@ -5972,6 +6106,7 @@ class Parser {
         * @return string
         */
        function testSrvus( $text, Title $title, ParserOptions $options, $outputType = self::OT_HTML ) {
+               $magicScopeVariable = $this->lock();
                $this->startParse( $title, $options, $outputType, true );
 
                $text = $this->replaceVariables( $text );
@@ -6149,4 +6284,49 @@ class Parser {
                }
                return $parsedWidthParam;
        }
+
+       /**
+        * Lock the current instance of the parser.
+        *
+        * This is meant to stop someone from calling the parser
+        * recursively and messing up all the strip state.
+        *
+        * @throws MWException If parser is in a parse
+        * @return ScopedCallback The lock will be released once the return value goes out of scope.
+        */
+       protected function lock() {
+               if ( $this->mInParse ) {
+                       throw new MWException( "Parser state cleared while parsing. "
+                               . "Did you call Parser::parse recursively?" );
+               }
+               $this->mInParse = true;
+
+               $that = $this;
+               $recursiveCheck = new ScopedCallback( function() use ( $that ) {
+                       $that->mInParse = false;
+               } );
+
+               return $recursiveCheck;
+       }
+
+       /**
+        * Strip outer <p></p> tag from the HTML source of a single paragraph.
+        *
+        * Returns original HTML if the <p/> tag has any attributes, if there's no wrapping <p/> tag,
+        * or if there is more than one <p/> tag in the input HTML.
+        *
+        * @param string $html
+        * @return string
+        * @since 1.24
+        */
+       public static function stripOuterParagraph( $html ) {
+               $m = array();
+               if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
+                       if ( strpos( $m[1], '</p>' ) === false ) {
+                               $html = $m[1];
+                       }
+               }
+
+               return $html;
+       }
 }