Spike PHPCoverage Details: Parser.php

Line #FrequencySource Line
1 <?php
2 /**
3  * File for Parser and related classes
4  *
5  * @package MediaWiki
6  * @subpackage Parser
7  */
8 
9 /**
10  * Update this version number when the ParserOutput format
11  * changes in an incompatible way, so the parser cache
12  * can automatically discard old data.
13  */
141define( 'MW_PARSER_VERSION', '1.6.1' );
15 
16 /**
17  * Variable substitution O(N^2) attack
18  *
19  * Without countermeasures, it would be possible to attack the parser by saving
20  * a page filled with a large number of inclusions of large pages. The size of
21  * the generated page would be proportional to the square of the input size.
22  * Hence, we limit the number of inclusions of any given page, thus bringing any
23  * attack back to O(N).
24  */
25 
261define( 'MAX_INCLUDE_REPEAT', 100 );
271define( 'MAX_INCLUDE_SIZE', 1000000 ); // 1 Million
28 
291define( 'RLH_FOR_UPDATE', 1 );
30 
31 # Allowed values for $mOutputType
321define( 'OT_HTML', 1 );
331define( 'OT_WIKI', 2 );
341define( 'OT_MSG' , 3 );
35 
36 # Flags for setFunctionHook
371define( 'SFH_NO_HASH', 1 );
38 
39 # string parameter for extractTags which will cause it
40 # to strip HTML comments in addition to regular
41 # <XML>-style tags. This should not be anything we
42 # may want to use in wikisyntax
431define( 'STRIP_COMMENTS', 'HTMLCommentStrip' );
44 
45 # Constants needed for external link processing
461define( 'HTTP_PROTOCOLS', 'http:\/\/|https:\/\/' );
47 # Everything except bracket, space, or control characters
481define( 'EXT_LINK_URL_CLASS', '[^][<>"\\x00-\\x20\\x7F]' );
49 # Including space, but excluding newlines
501define( 'EXT_LINK_TEXT_CLASS', '[^\]\\x0a\\x0d]' );
511define( 'EXT_IMAGE_FNAME_CLASS', '[A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]' );
521define( 'EXT_IMAGE_EXTENSIONS', 'gif|png|jpg|jpeg' );
531define( 'EXT_LINK_BRACKETED',  '/\[(\b(' . wfUrlProtocols() . ')'.
541    EXT_LINK_URL_CLASS.'+) *('.EXT_LINK_TEXT_CLASS.'*?)\]/S' );
551define( 'EXT_IMAGE_REGEX',
56     '/^('.HTTP_PROTOCOLS.')'.  # Protocol
57     '('.EXT_LINK_URL_CLASS.'+)\\/'.  # Hostname and path
58     '('.EXT_IMAGE_FNAME_CLASS.'+)\\.((?i)'.EXT_IMAGE_EXTENSIONS.')$/S' # Filename
591);
60 
61 // State constants for the definition list colon extraction
621define( 'MW_COLON_STATE_TEXT', 0 );
631define( 'MW_COLON_STATE_TAG', 1 );
641define( 'MW_COLON_STATE_TAGSTART', 2 );
651define( 'MW_COLON_STATE_CLOSETAG', 3 );
661define( 'MW_COLON_STATE_TAGSLASH', 4 );
671define( 'MW_COLON_STATE_COMMENT', 5 );
681define( 'MW_COLON_STATE_COMMENTDASH', 6 );
691define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 );
70 
71 /**
72  * PHP Parser
73  *
74  * Processes wiki markup
75  *
76  * <pre>
77  * There are three main entry points into the Parser class:
78  * parse()
79  *   produces HTML output
80  * preSaveTransform().
81  *   produces altered wiki markup.
82  * transformMsg()
83  *   performs brace substitution on MediaWiki messages
84  *
85  * Globals used:
86  *    objects:   $wgLang, $wgContLang
87  *
88  * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
89  *
90  * settings:
91  *  $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
92  *  $wgNamespacesWithSubpages, $wgAllowExternalImages*,
93  *  $wgLocaltimezone, $wgAllowSpecialInclusion*
94  *
95  *  * only within ParserOptions
96  * </pre>
97  *
98  * @package MediaWiki
99  */
100 class Parser
101 {
102     /**#@+
103      * @private
104      */
105     # Persistent:
106     var $mTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables;
107 
108     # Cleared with clearState():
109     var $mOutput, $mAutonumber, $mDTopen, $mStripState = array();
110     var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
111     var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix;
112     var $mTemplates,    // cache of already loaded templates, avoids
113                         // multiple SQL queries for the same string
114         $mTemplatePath;    // stores an unsorted hash of all the templates already loaded
115                         // in this path. Used for loop detection.
116 
117     # Temporary
118     # These are variables reset at least once per parse regardless of $clearState
119     var $mOptions,      // ParserOptions object
120         $mTitle,        // Title context, used for self-link rendering and similar things
121         $mOutputType,   // Output type, one of the OT_xxx constants
122         $mRevisionId;   // ID to display in {{REVISIONID}} tags
123 
124     /**#@-*/
125 
126     /**
127      * Constructor
128      *
129      * @public
130      */
131     function Parser() {
1321        $this->mTagHooks = array();
1331        $this->mFunctionHooks = array();
1341        $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
1351        $this->mFirstCall = true;
136     }
137 
138     /**
139      * Do various kinds of initialisation on the first call of the parser
140      */
141     function firstCallInit() {
1421        if ( !$this->mFirstCall ) {
1431            return;
144         }
145 
1461        wfProfileIn( __METHOD__ );
1471        global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
148 
1491        $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
150 
1511        $this->setFunctionHook( MAG_NS, array( 'CoreParserFunctions', 'ns' ), SFH_NO_HASH );
1521        $this->setFunctionHook( MAG_URLENCODE, array( 'CoreParserFunctions', 'urlencode' ), SFH_NO_HASH );
1531        $this->setFunctionHook( MAG_LCFIRST, array( 'CoreParserFunctions', 'lcfirst' ), SFH_NO_HASH );
1541        $this->setFunctionHook( MAG_UCFIRST, array( 'CoreParserFunctions', 'ucfirst' ), SFH_NO_HASH );
1551        $this->setFunctionHook( MAG_LC, array( 'CoreParserFunctions', 'lc' ), SFH_NO_HASH );
1561        $this->setFunctionHook( MAG_UC, array( 'CoreParserFunctions', 'uc' ), SFH_NO_HASH );
1571        $this->setFunctionHook( MAG_LOCALURL, array( 'CoreParserFunctions', 'localurl' ), SFH_NO_HASH );
1581        $this->setFunctionHook( MAG_LOCALURLE, array( 'CoreParserFunctions', 'localurle' ), SFH_NO_HASH );
1591        $this->setFunctionHook( MAG_FULLURL, array( 'CoreParserFunctions', 'fullurl' ), SFH_NO_HASH );
1601        $this->setFunctionHook( MAG_FULLURLE, array( 'CoreParserFunctions', 'fullurle' ), SFH_NO_HASH );
1611        $this->setFunctionHook( MAG_FORMATNUM, array( 'CoreParserFunctions', 'formatnum' ), SFH_NO_HASH );
1621        $this->setFunctionHook( MAG_GRAMMAR, array( 'CoreParserFunctions', 'grammar' ), SFH_NO_HASH );
1631        $this->setFunctionHook( MAG_PLURAL, array( 'CoreParserFunctions', 'plural' ), SFH_NO_HASH );
1641        $this->setFunctionHook( MAG_NUMBEROFPAGES, array( 'CoreParserFunctions', 'numberofpages' ), SFH_NO_HASH );
1651        $this->setFunctionHook( MAG_NUMBEROFUSERS, array( 'CoreParserFunctions', 'numberofusers' ), SFH_NO_HASH );
1661        $this->setFunctionHook( MAG_NUMBEROFARTICLES, array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH );
1671        $this->setFunctionHook( MAG_NUMBEROFFILES, array( 'CoreParserFunctions', 'numberoffiles' ), SFH_NO_HASH );
1681        $this->setFunctionHook( MAG_NUMBEROFADMINS, array( 'CoreParserFunctions', 'numberofadmins' ), SFH_NO_HASH );
1691        $this->setFunctionHook( MAG_LANGUAGE, array( 'CoreParserFunctions', 'language' ), SFH_NO_HASH );
170 
1711        if ( $wgAllowDisplayTitle ) {
172             $this->setFunctionHook( MAG_DISPLAYTITLE, array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH );
173         }
1741        if ( $wgAllowSlowParserFunctions ) {
175             $this->setFunctionHook( MAG_PAGESINNAMESPACE, array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH );
176         }
177         
1781        $this->initialiseVariables();
179 
1801        $this->mFirstCall = false;
1811        wfProfileOut( __METHOD__ );
182     }        
183 
184     /**
185      * Clear Parser state
186      *
187      * @private
188      */
189     function clearState() {
1901        if ( $this->mFirstCall ) {
1911            $this->firstCallInit();
192         }
1931        $this->mOutput = new ParserOutput;
1941        $this->mAutonumber = 0;
1951        $this->mLastSection = '';
1961        $this->mDTopen = false;
1971        $this->mIncludeCount = array();
1981        $this->mStripState = array();
1991        $this->mArgStack = array();
2001        $this->mInPre = false;
201         $this->mInterwikiLinkHolders = array(
2021            'texts' => array(),
203             'titles' => array()
204         );
205         $this->mLinkHolders = array(
2061            'namespaces' => array(),
207             'dbkeys' => array(),
208             'queries' => array(),
209             'texts' => array(),
210             'titles' => array()
211         );
2121        $this->mRevisionId = null;
213         
214         /**
215          * Prefix for temporary replacement strings for the multipass parser.
216          * \x07 should never appear in input as it's disallowed in XML.
217          * Using it at the front also gives us a little extra robustness
218          * since it shouldn't match when butted up against identifier-like
219          * string constructs.
220          */
2211        $this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
222 
223         # Clear these on every parse, bug 4549
2241         $this->mTemplates = array();
2251         $this->mTemplatePath = array();
226 
2271        $this->mShowToc = true;
2281        $this->mForceTocPosition = false;
229 
2301        wfRunHooks( 'ParserClearState', array( &$this ) );
231     }
232 
233     /**
234      * Accessor for mUniqPrefix.
235      *
236      * @public
237      */
238     function UniqPrefix() {
2391        return $this->mUniqPrefix;
240     }
241 
242     /**
243      * Convert wikitext to HTML
244      * Do not call this function recursively.
245      *
246      * @private
247      * @param string $text Text we want to parse
248      * @param Title &$title A title object
249      * @param array $options
250      * @param boolean $linestart
251      * @param boolean $clearState
252      * @param int $revid number to pass in {{REVISIONID}}
253      * @return ParserOutput a ParserOutput
254      */
255     function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
256         /**
257          * First pass--just handle <nowiki> sections, pass the rest off
258          * to internalParse() which does all the real work.
259          */
260 
2611        global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
2621        $fname = 'Parser::parse';
2631        wfProfileIn( $fname );
264 
2651        if ( $clearState ) {
2661            $this->clearState();
267         }
268 
2691        $this->mOptions = $options;
2701        $this->mTitle =& $title;
2711        $this->mRevisionId = $revid;
2721        $this->mOutputType = OT_HTML;
273 
274         //$text = $this->strip( $text, $this->mStripState );
275         // VOODOO MAGIC FIX! Sometimes the above segfaults in PHP5.
2761        $x =& $this->mStripState;
277 
2781        wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$x ) );
2791        $text = $this->strip( $text, $x );
2801        wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$x ) );
281 
282         # Hook to suspend the parser in this state
2831        if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$x ) ) ) {
284             wfProfileOut( $fname );
285             return $text ;
286         }
287 
2881        $text = $this->internalParse( $text );
289 
2901        $text = $this->unstrip( $text, $this->mStripState );
291 
292         # Clean up special characters, only run once, next-to-last before doBlockLevels
293         $fixtags = array(
294             # french spaces, last one Guillemet-left
295             # only if there is something before the space
2961            '/(.) (?=\\?|:|;|!|\\302\\273)/' => '\\1&nbsp;\\2',
297             # french spaces, Guillemet-right
298             '/(\\302\\253) /' => '\\1&nbsp;',
299         );
3001        $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
301 
302         # only once and last
3031        $text = $this->doBlockLevels( $text, $linestart );
304 
3051        $this->replaceLinkHolders( $text );
306 
307         # the position of the parserConvert() call should not be changed. it
308         # assumes that the links are all replaced and the only thing left
309         # is the <nowiki> mark.
310         # Side-effects: this calls $this->mOutput->setTitleText()
3111        $text = $wgContLang->parserConvert( $text, $this );
312 
3131        $text = $this->unstripNoWiki( $text, $this->mStripState );
314 
3151        wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
316 
3171        $text = Sanitizer::normalizeCharReferences( $text );
318 
3191        if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
320             $text = Parser::tidy($text);
321         } else {
322             # attempt to sanitize at least some nesting problems
323             # (bug #2702 and quite a few others)
324             $tidyregs = array(    
325                 # ''Something [http://www.cool.com cool''] --> 
326                 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
327                 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
3281                '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
329                 # fix up an anchor inside another anchor, only
330                 # at least for a single single nested link (bug 3695)
331                 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
332                 '\\1\\2</a>\\3</a>\\1\\4</a>',
333                 # fix div inside inline elements- doBlockLevels won't wrap a line which
334                 # contains a div, so fix it up here; replace
335                 # div with escaped text
336                 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
337                 '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
338                 # remove empty italic or bold tag pairs, some
339                 # introduced by rules above
340                 '/<([bi])><\/\\1>/' => '' 
341             );
342 
3431            $text = preg_replace( 
3441                array_keys( $tidyregs ),
3451                array_values( $tidyregs ),
3461                $text );
347         }
348 
3491        wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
350 
3511        $this->mOutput->setText( $text );
3521        wfProfileOut( $fname );
353 
3541        return $this->mOutput;
355     }
356 
357     /**
358      * Get a random string
359      *
360      * @private
361      * @static
362      */
363     function getRandomString() {
3641        return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
365     }
366 
367     function &getTitle() { return $this->mTitle; }
368     function getOptions() { return $this->mOptions; }
369 
370     function getFunctionLang() {
371         global $wgLang, $wgContLang;
372         return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
373     }
374 
375     /**
376      * Replaces all occurrences of HTML-style comments and the given tags
377      * in the text with a random marker and returns teh next text. The output
378      * parameter $matches will be an associative array filled with data in
379      * the form:
380      *   'UNIQ-xxxxx' => array(
381      *     'element',
382      *     'tag content',
383      *     array( 'param' => 'x' ),
384      *     '<element param="x">tag content</element>' ) )
385      *
386      * @param $elements list of element names. Comments are always extracted.
387      * @param $text Source text string.
388      * @param $uniq_prefix
389      *
390      * @private
391      * @static
392      */
393     function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
3941        $rand = Parser::getRandomString();
3951        $n = 1;
3961        $stripped = '';
397         $matches = array();
398 
3991        $taglist = implode( '|', $elements );
4001        $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
401 
4021        while ( '' != $text ) {
4031            $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
4041            $stripped .= $p[0];
4051            if( count( $p ) < 5 ) {
4061                break;
407             }
4081            if( count( $p ) > 5 ) {
409                 // comment
4101                $element    = $p[4];
4111                $attributes = '';
4121                $close      = '';
4131                $inside     = $p[5];
414             } else {
415                 // tag
4161                $element    = $p[1];
4171                $attributes = $p[2];
4181                $close      = $p[3];
4191                $inside     = $p[4];
420             }
421 
4221            $marker = "$uniq_prefix-$element-$rand" . sprintf('%08X', $n++) . '-QINU';
4231            $stripped .= $marker;
424 
4251            if ( $close === '/>' ) {
426                 // Empty element tag, <tag />
4271                $content = null;
4281                $text = $inside;
4291                $tail = null;
430             } else {
4311                if( $element == '!--' ) {
4321                    $end = '/(-->)/';
433                 } else {
4341                    $end = "/(<\\/$element\\s*>)/i";
435                 }
4361                $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
4371                $content = $q[0];
4381                if( count( $q ) < 3 ) {
439                     # No end tag -- let it run out to the end of the text.
4401                    $tail = '';
4411                    $text = '';
442                 } else {
4431                    $tail = $q[1];
4441                    $text = $q[2];
445                 }
446             }
447             
448             $matches[$marker] = array( $element,
449                 $content,
4501                Sanitizer::decodeTagAttributes( $attributes ),
451                 "<$element$attributes$close$content$tail" );
452         }
4531        return $stripped;
454     }
455 
456     /**
457      * Strips and renders nowiki, pre, math, hiero
458      * If $render is set, performs necessary rendering operations on plugins
459      * Returns the text, and fills an array with data needed in unstrip()
460      * If the $state is already a valid strip state, it adds to the state
461      *
462      * @param bool $stripcomments when set, HTML comments <!-- like this -->
463      *  will be stripped in addition to other tags. This is important
464      *  for section editing, where these comments cause confusion when