[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Preprocessor using PHP's dom extension 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup Parser 22 */ 23 24 /** 25 * @ingroup Parser 26 * @codingStandardsIgnoreStart 27 */ 28 class Preprocessor_DOM implements Preprocessor { 29 // @codingStandardsIgnoreEnd 30 31 /** 32 * @var Parser 33 */ 34 public $parser; 35 36 public $memoryLimit; 37 38 const CACHE_VERSION = 1; 39 40 public function __construct( $parser ) { 41 $this->parser = $parser; 42 $mem = ini_get( 'memory_limit' ); 43 $this->memoryLimit = false; 44 if ( strval( $mem ) !== '' && $mem != -1 ) { 45 if ( preg_match( '/^\d+$/', $mem ) ) { 46 $this->memoryLimit = $mem; 47 } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) { 48 $this->memoryLimit = $m[1] * 1048576; 49 } 50 } 51 } 52 53 /** 54 * @return PPFrame_DOM 55 */ 56 public function newFrame() { 57 return new PPFrame_DOM( $this ); 58 } 59 60 /** 61 * @param array $args 62 * @return PPCustomFrame_DOM 63 */ 64 public function newCustomFrame( $args ) { 65 return new PPCustomFrame_DOM( $this, $args ); 66 } 67 68 /** 69 * @param array $values 70 * @return PPNode_DOM 71 */ 72 public function newPartNodeArray( $values ) { 73 //NOTE: DOM manipulation is slower than building & parsing XML! (or so Tim sais) 74 $xml = "<list>"; 75 76 foreach ( $values as $k => $val ) { 77 if ( is_int( $k ) ) { 78 $xml .= "<part><name index=\"$k\"/><value>" 79 . htmlspecialchars( $val ) . "</value></part>"; 80 } else { 81 $xml .= "<part><name>" . htmlspecialchars( $k ) 82 . "</name>=<value>" . htmlspecialchars( $val ) . "</value></part>"; 83 } 84 } 85 86 $xml .= "</list>"; 87 88 wfProfileIn( __METHOD__ . '-loadXML' ); 89 $dom = new DOMDocument(); 90 wfSuppressWarnings(); 91 $result = $dom->loadXML( $xml ); 92 wfRestoreWarnings(); 93 if ( !$result ) { 94 // Try running the XML through UtfNormal to get rid of invalid characters 95 $xml = UtfNormal::cleanUp( $xml ); 96 // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 97 // don't barf when the XML is >256 levels deep 98 $result = $dom->loadXML( $xml, 1 << 19 ); 99 } 100 wfProfileOut( __METHOD__ . '-loadXML' ); 101 102 if ( !$result ) { 103 throw new MWException( 'Parameters passed to ' . __METHOD__ . ' result in invalid XML' ); 104 } 105 106 $root = $dom->documentElement; 107 $node = new PPNode_DOM( $root->childNodes ); 108 return $node; 109 } 110 111 /** 112 * @throws MWException 113 * @return bool 114 */ 115 public function memCheck() { 116 if ( $this->memoryLimit === false ) { 117 return true; 118 } 119 $usage = memory_get_usage(); 120 if ( $usage > $this->memoryLimit * 0.9 ) { 121 $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 ); 122 throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" ); 123 } 124 return $usage <= $this->memoryLimit * 0.8; 125 } 126 127 /** 128 * Preprocess some wikitext and return the document tree. 129 * This is the ghost of Parser::replace_variables(). 130 * 131 * @param string $text The text to parse 132 * @param int $flags Bitwise combination of: 133 * Parser::PTD_FOR_INCLUSION Handle "<noinclude>" and "<includeonly>" 134 * as if the text is being included. Default 135 * is to assume a direct page view. 136 * 137 * The generated DOM tree must depend only on the input text and the flags. 138 * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. 139 * 140 * Any flag added to the $flags parameter here, or any other parameter liable to cause a 141 * change in the DOM tree for a given text, must be passed through the section identifier 142 * in the section edit link and thus back to extractSections(). 143 * 144 * The output of this function is currently only cached in process memory, but a persistent 145 * cache may be implemented at a later date which takes further advantage of these strict 146 * dependency requirements. 147 * 148 * @throws MWException 149 * @return PPNode_DOM 150 */ 151 public function preprocessToObj( $text, $flags = 0 ) { 152 wfProfileIn( __METHOD__ ); 153 global $wgMemc, $wgPreprocessorCacheThreshold; 154 155 $xml = false; 156 $cacheable = ( $wgPreprocessorCacheThreshold !== false 157 && strlen( $text ) > $wgPreprocessorCacheThreshold ); 158 if ( $cacheable ) { 159 wfProfileIn( __METHOD__ . '-cacheable' ); 160 161 $cacheKey = wfMemcKey( 'preprocess-xml', md5( $text ), $flags ); 162 $cacheValue = $wgMemc->get( $cacheKey ); 163 if ( $cacheValue ) { 164 $version = substr( $cacheValue, 0, 8 ); 165 if ( intval( $version ) == self::CACHE_VERSION ) { 166 $xml = substr( $cacheValue, 8 ); 167 // From the cache 168 wfDebugLog( "Preprocessor", "Loaded preprocessor XML from memcached (key $cacheKey)" ); 169 } 170 } 171 if ( $xml === false ) { 172 wfProfileIn( __METHOD__ . '-cache-miss' ); 173 $xml = $this->preprocessToXml( $text, $flags ); 174 $cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . $xml; 175 $wgMemc->set( $cacheKey, $cacheValue, 86400 ); 176 wfProfileOut( __METHOD__ . '-cache-miss' ); 177 wfDebugLog( "Preprocessor", "Saved preprocessor XML to memcached (key $cacheKey)" ); 178 } 179 } else { 180 $xml = $this->preprocessToXml( $text, $flags ); 181 } 182 183 // Fail if the number of elements exceeds acceptable limits 184 // Do not attempt to generate the DOM 185 $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' ); 186 $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount(); 187 if ( $this->parser->mGeneratedPPNodeCount > $max ) { 188 if ( $cacheable ) { 189 wfProfileOut( __METHOD__ . '-cacheable' ); 190 } 191 wfProfileOut( __METHOD__ ); 192 throw new MWException( __METHOD__ . ': generated node count limit exceeded' ); 193 } 194 195 wfProfileIn( __METHOD__ . '-loadXML' ); 196 $dom = new DOMDocument; 197 wfSuppressWarnings(); 198 $result = $dom->loadXML( $xml ); 199 wfRestoreWarnings(); 200 if ( !$result ) { 201 // Try running the XML through UtfNormal to get rid of invalid characters 202 $xml = UtfNormal::cleanUp( $xml ); 203 // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 204 // don't barf when the XML is >256 levels deep. 205 $result = $dom->loadXML( $xml, 1 << 19 ); 206 } 207 if ( $result ) { 208 $obj = new PPNode_DOM( $dom->documentElement ); 209 } 210 wfProfileOut( __METHOD__ . '-loadXML' ); 211 212 if ( $cacheable ) { 213 wfProfileOut( __METHOD__ . '-cacheable' ); 214 } 215 216 wfProfileOut( __METHOD__ ); 217 218 if ( !$result ) { 219 throw new MWException( __METHOD__ . ' generated invalid XML' ); 220 } 221 return $obj; 222 } 223 224 /** 225 * @param string $text 226 * @param int $flags 227 * @return string 228 */ 229 public function preprocessToXml( $text, $flags = 0 ) { 230 wfProfileIn( __METHOD__ ); 231 $rules = array( 232 '{' => array( 233 'end' => '}', 234 'names' => array( 235 2 => 'template', 236 3 => 'tplarg', 237 ), 238 'min' => 2, 239 'max' => 3, 240 ), 241 '[' => array( 242 'end' => ']', 243 'names' => array( 2 => null ), 244 'min' => 2, 245 'max' => 2, 246 ) 247 ); 248 249 $forInclusion = $flags & Parser::PTD_FOR_INCLUSION; 250 251 $xmlishElements = $this->parser->getStripList(); 252 $enableOnlyinclude = false; 253 if ( $forInclusion ) { 254 $ignoredTags = array( 'includeonly', '/includeonly' ); 255 $ignoredElements = array( 'noinclude' ); 256 $xmlishElements[] = 'noinclude'; 257 if ( strpos( $text, '<onlyinclude>' ) !== false 258 && strpos( $text, '</onlyinclude>' ) !== false 259 ) { 260 $enableOnlyinclude = true; 261 } 262 } else { 263 $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ); 264 $ignoredElements = array( 'includeonly' ); 265 $xmlishElements[] = 'includeonly'; 266 } 267 $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) ); 268 269 // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset 270 $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA"; 271 272 $stack = new PPDStack; 273 274 $searchBase = "[{<\n"; #} 275 // For fast reverse searches 276 $revText = strrev( $text ); 277 $lengthText = strlen( $text ); 278 279 // Input pointer, starts out pointing to a pseudo-newline before the start 280 $i = 0; 281 // Current accumulator 282 $accum =& $stack->getAccum(); 283 $accum = '<root>'; 284 // True to find equals signs in arguments 285 $findEquals = false; 286 // True to take notice of pipe characters 287 $findPipe = false; 288 $headingIndex = 1; 289 // True if $i is inside a possible heading 290 $inHeading = false; 291 // True if there are no more greater-than (>) signs right of $i 292 $noMoreGT = false; 293 // True to ignore all input up to the next <onlyinclude> 294 $findOnlyinclude = $enableOnlyinclude; 295 // Do a line-start run without outputting an LF character 296 $fakeLineStart = true; 297 298 while ( true ) { 299 //$this->memCheck(); 300 301 if ( $findOnlyinclude ) { 302 // Ignore all input up to the next <onlyinclude> 303 $startPos = strpos( $text, '<onlyinclude>', $i ); 304 if ( $startPos === false ) { 305 // Ignored section runs to the end 306 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>'; 307 break; 308 } 309 $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end 310 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>'; 311 $i = $tagEndPos; 312 $findOnlyinclude = false; 313 } 314 315 if ( $fakeLineStart ) { 316 $found = 'line-start'; 317 $curChar = ''; 318 } else { 319 # Find next opening brace, closing brace or pipe 320 $search = $searchBase; 321 if ( $stack->top === false ) { 322 $currentClosing = ''; 323 } else { 324 $currentClosing = $stack->top->close; 325 $search .= $currentClosing; 326 } 327 if ( $findPipe ) { 328 $search .= '|'; 329 } 330 if ( $findEquals ) { 331 // First equals will be for the template 332 $search .= '='; 333 } 334 $rule = null; 335 # Output literal section, advance input counter 336 $literalLength = strcspn( $text, $search, $i ); 337 if ( $literalLength > 0 ) { 338 $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) ); 339 $i += $literalLength; 340 } 341 if ( $i >= $lengthText ) { 342 if ( $currentClosing == "\n" ) { 343 // Do a past-the-end run to finish off the heading 344 $curChar = ''; 345 $found = 'line-end'; 346 } else { 347 # All done 348 break; 349 } 350 } else { 351 $curChar = $text[$i]; 352 if ( $curChar == '|' ) { 353 $found = 'pipe'; 354 } elseif ( $curChar == '=' ) { 355 $found = 'equals'; 356 } elseif ( $curChar == '<' ) { 357 $found = 'angle'; 358 } elseif ( $curChar == "\n" ) { 359 if ( $inHeading ) { 360 $found = 'line-end'; 361 } else { 362 $found = 'line-start'; 363 } 364 } elseif ( $curChar == $currentClosing ) { 365 $found = 'close'; 366 } elseif ( isset( $rules[$curChar] ) ) { 367 $found = 'open'; 368 $rule = $rules[$curChar]; 369 } else { 370 # Some versions of PHP have a strcspn which stops on null characters 371 # Ignore and continue 372 ++$i; 373 continue; 374 } 375 } 376 } 377 378 if ( $found == 'angle' ) { 379 $matches = false; 380 // Handle </onlyinclude> 381 if ( $enableOnlyinclude 382 && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' 383 ) { 384 $findOnlyinclude = true; 385 continue; 386 } 387 388 // Determine element name 389 if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) { 390 // Element name missing or not listed 391 $accum .= '<'; 392 ++$i; 393 continue; 394 } 395 // Handle comments 396 if ( isset( $matches[2] ) && $matches[2] == '!--' ) { 397 398 // To avoid leaving blank lines, when a sequence of 399 // space-separated comments is both preceded and followed by 400 // a newline (ignoring spaces), then 401 // trim leading and trailing spaces and the trailing newline. 402 403 // Find the end 404 $endPos = strpos( $text, '-->', $i + 4 ); 405 if ( $endPos === false ) { 406 // Unclosed comment in input, runs to end 407 $inner = substr( $text, $i ); 408 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; 409 $i = $lengthText; 410 } else { 411 // Search backwards for leading whitespace 412 $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0; 413 414 // Search forwards for trailing whitespace 415 // $wsEnd will be the position of the last space (or the '>' if there's none) 416 $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 ); 417 418 // Keep looking forward as long as we're finding more 419 // comments. 420 $comments = array( array( $wsStart, $wsEnd ) ); 421 while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) { 422 $c = strpos( $text, '-->', $wsEnd + 4 ); 423 if ( $c === false ) { 424 break; 425 } 426 $c = $c + 2 + strspn( $text, " \t", $c + 3 ); 427 $comments[] = array( $wsEnd + 1, $c ); 428 $wsEnd = $c; 429 } 430 431 // Eat the line if possible 432 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at 433 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but 434 // it's a possible beneficial b/c break. 435 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" 436 && substr( $text, $wsEnd + 1, 1 ) == "\n" 437 ) { 438 // Remove leading whitespace from the end of the accumulator 439 // Sanity check first though 440 $wsLength = $i - $wsStart; 441 if ( $wsLength > 0 442 && strspn( $accum, " \t", -$wsLength ) === $wsLength 443 ) { 444 $accum = substr( $accum, 0, -$wsLength ); 445 } 446 447 // Dump all but the last comment to the accumulator 448 foreach ( $comments as $j => $com ) { 449 $startPos = $com[0]; 450 $endPos = $com[1] + 1; 451 if ( $j == ( count( $comments ) - 1 ) ) { 452 break; 453 } 454 $inner = substr( $text, $startPos, $endPos - $startPos ); 455 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; 456 } 457 458 // Do a line-start run next time to look for headings after the comment 459 $fakeLineStart = true; 460 } else { 461 // No line to eat, just take the comment itself 462 $startPos = $i; 463 $endPos += 2; 464 } 465 466 if ( $stack->top ) { 467 $part = $stack->top->getCurrentPart(); 468 if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) { 469 $part->visualEnd = $wsStart; 470 } 471 // Else comments abutting, no change in visual end 472 $part->commentEnd = $endPos; 473 } 474 $i = $endPos + 1; 475 $inner = substr( $text, $startPos, $endPos - $startPos + 1 ); 476 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; 477 } 478 continue; 479 } 480 $name = $matches[1]; 481 $lowerName = strtolower( $name ); 482 $attrStart = $i + strlen( $name ) + 1; 483 484 // Find end of tag 485 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart ); 486 if ( $tagEndPos === false ) { 487 // Infinite backtrack 488 // Disable tag search to prevent worst-case O(N^2) performance 489 $noMoreGT = true; 490 $accum .= '<'; 491 ++$i; 492 continue; 493 } 494 495 // Handle ignored tags 496 if ( in_array( $lowerName, $ignoredTags ) ) { 497 $accum .= '<ignore>' 498 . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) 499 . '</ignore>'; 500 $i = $tagEndPos + 1; 501 continue; 502 } 503 504 $tagStartPos = $i; 505 if ( $text[$tagEndPos - 1] == '/' ) { 506 $attrEnd = $tagEndPos - 1; 507 $inner = null; 508 $i = $tagEndPos + 1; 509 $close = ''; 510 } else { 511 $attrEnd = $tagEndPos; 512 // Find closing tag 513 if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i", 514 $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) 515 ) { 516 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); 517 $i = $matches[0][1] + strlen( $matches[0][0] ); 518 $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>'; 519 } else { 520 // No end tag -- let it run out to the end of the text. 521 $inner = substr( $text, $tagEndPos + 1 ); 522 $i = $lengthText; 523 $close = ''; 524 } 525 } 526 // <includeonly> and <noinclude> just become <ignore> tags 527 if ( in_array( $lowerName, $ignoredElements ) ) { 528 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) ) 529 . '</ignore>'; 530 continue; 531 } 532 533 $accum .= '<ext>'; 534 if ( $attrEnd <= $attrStart ) { 535 $attr = ''; 536 } else { 537 $attr = substr( $text, $attrStart, $attrEnd - $attrStart ); 538 } 539 $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' . 540 // Note that the attr element contains the whitespace between name and attribute, 541 // this is necessary for precise reconstruction during pre-save transform. 542 '<attr>' . htmlspecialchars( $attr ) . '</attr>'; 543 if ( $inner !== null ) { 544 $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>'; 545 } 546 $accum .= $close . '</ext>'; 547 } elseif ( $found == 'line-start' ) { 548 // Is this the start of a heading? 549 // Line break belongs before the heading element in any case 550 if ( $fakeLineStart ) { 551 $fakeLineStart = false; 552 } else { 553 $accum .= $curChar; 554 $i++; 555 } 556 557 $count = strspn( $text, '=', $i, 6 ); 558 if ( $count == 1 && $findEquals ) { 559 // DWIM: This looks kind of like a name/value separator. 560 // Let's let the equals handler have it and break the 561 // potential heading. This is heuristic, but AFAICT the 562 // methods for completely correct disambiguation are very 563 // complex. 564 } elseif ( $count > 0 ) { 565 $piece = array( 566 'open' => "\n", 567 'close' => "\n", 568 'parts' => array( new PPDPart( str_repeat( '=', $count ) ) ), 569 'startPos' => $i, 570 'count' => $count ); 571 $stack->push( $piece ); 572 $accum =& $stack->getAccum(); 573 $flags = $stack->getFlags(); 574 extract( $flags ); 575 $i += $count; 576 } 577 } elseif ( $found == 'line-end' ) { 578 $piece = $stack->top; 579 // A heading must be open, otherwise \n wouldn't have been in the search list 580 assert( '$piece->open == "\n"' ); 581 $part = $piece->getCurrentPart(); 582 // Search back through the input to see if it has a proper close. 583 // Do this using the reversed string since the other solutions 584 // (end anchor, etc.) are inefficient. 585 $wsLength = strspn( $revText, " \t", $lengthText - $i ); 586 $searchStart = $i - $wsLength; 587 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { 588 // Comment found at line end 589 // Search for equals signs before the comment 590 $searchStart = $part->visualEnd; 591 $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart ); 592 } 593 $count = $piece->count; 594 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart ); 595 if ( $equalsLength > 0 ) { 596 if ( $searchStart - $equalsLength == $piece->startPos ) { 597 // This is just a single string of equals signs on its own line 598 // Replicate the doHeadings behavior /={count}(.+)={count}/ 599 // First find out how many equals signs there really are (don't stop at 6) 600 $count = $equalsLength; 601 if ( $count < 3 ) { 602 $count = 0; 603 } else { 604 $count = min( 6, intval( ( $count - 1 ) / 2 ) ); 605 } 606 } else { 607 $count = min( $equalsLength, $count ); 608 } 609 if ( $count > 0 ) { 610 // Normal match, output <h> 611 $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>"; 612 $headingIndex++; 613 } else { 614 // Single equals sign on its own line, count=0 615 $element = $accum; 616 } 617 } else { 618 // No match, no <h>, just pass down the inner text 619 $element = $accum; 620 } 621 // Unwind the stack 622 $stack->pop(); 623 $accum =& $stack->getAccum(); 624 $flags = $stack->getFlags(); 625 extract( $flags ); 626 627 // Append the result to the enclosing accumulator 628 $accum .= $element; 629 // Note that we do NOT increment the input pointer. 630 // This is because the closing linebreak could be the opening linebreak of 631 // another heading. Infinite loops are avoided because the next iteration MUST 632 // hit the heading open case above, which unconditionally increments the 633 // input pointer. 634 } elseif ( $found == 'open' ) { 635 # count opening brace characters 636 $count = strspn( $text, $curChar, $i ); 637 638 # we need to add to stack only if opening brace count is enough for one of the rules 639 if ( $count >= $rule['min'] ) { 640 # Add it to the stack 641 $piece = array( 642 'open' => $curChar, 643 'close' => $rule['end'], 644 'count' => $count, 645 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ), 646 ); 647 648 $stack->push( $piece ); 649 $accum =& $stack->getAccum(); 650 $flags = $stack->getFlags(); 651 extract( $flags ); 652 } else { 653 # Add literal brace(s) 654 $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); 655 } 656 $i += $count; 657 } elseif ( $found == 'close' ) { 658 $piece = $stack->top; 659 # lets check if there are enough characters for closing brace 660 $maxCount = $piece->count; 661 $count = strspn( $text, $curChar, $i, $maxCount ); 662 663 # check for maximum matching characters (if there are 5 closing 664 # characters, we will probably need only 3 - depending on the rules) 665 $rule = $rules[$piece->open]; 666 if ( $count > $rule['max'] ) { 667 # The specified maximum exists in the callback array, unless the caller 668 # has made an error 669 $matchingCount = $rule['max']; 670 } else { 671 # Count is less than the maximum 672 # Skip any gaps in the callback array to find the true largest match 673 # Need to use array_key_exists not isset because the callback can be null 674 $matchingCount = $count; 675 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) { 676 --$matchingCount; 677 } 678 } 679 680 if ( $matchingCount <= 0 ) { 681 # No matching element found in callback array 682 # Output a literal closing brace and continue 683 $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); 684 $i += $count; 685 continue; 686 } 687 $name = $rule['names'][$matchingCount]; 688 if ( $name === null ) { 689 // No element, just literal text 690 $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount ); 691 } else { 692 # Create XML element 693 # Note: $parts is already XML, does not need to be encoded further 694 $parts = $piece->parts; 695 $title = $parts[0]->out; 696 unset( $parts[0] ); 697 698 # The invocation is at the start of the line if lineStart is set in 699 # the stack, and all opening brackets are used up. 700 if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) { 701 $attr = ' lineStart="1"'; 702 } else { 703 $attr = ''; 704 } 705 706 $element = "<$name$attr>"; 707 $element .= "<title>$title</title>"; 708 $argIndex = 1; 709 foreach ( $parts as $part ) { 710 if ( isset( $part->eqpos ) ) { 711 $argName = substr( $part->out, 0, $part->eqpos ); 712 $argValue = substr( $part->out, $part->eqpos + 1 ); 713 $element .= "<part><name>$argName</name>=<value>$argValue</value></part>"; 714 } else { 715 $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>"; 716 $argIndex++; 717 } 718 } 719 $element .= "</$name>"; 720 } 721 722 # Advance input pointer 723 $i += $matchingCount; 724 725 # Unwind the stack 726 $stack->pop(); 727 $accum =& $stack->getAccum(); 728 729 # Re-add the old stack element if it still has unmatched opening characters remaining 730 if ( $matchingCount < $piece->count ) { 731 $piece->parts = array( new PPDPart ); 732 $piece->count -= $matchingCount; 733 # do we still qualify for any callback with remaining count? 734 $min = $rules[$piece->open]['min']; 735 if ( $piece->count >= $min ) { 736 $stack->push( $piece ); 737 $accum =& $stack->getAccum(); 738 } else { 739 $accum .= str_repeat( $piece->open, $piece->count ); 740 } 741 } 742 $flags = $stack->getFlags(); 743 extract( $flags ); 744 745 # Add XML element to the enclosing accumulator 746 $accum .= $element; 747 } elseif ( $found == 'pipe' ) { 748 $findEquals = true; // shortcut for getFlags() 749 $stack->addPart(); 750 $accum =& $stack->getAccum(); 751 ++$i; 752 } elseif ( $found == 'equals' ) { 753 $findEquals = false; // shortcut for getFlags() 754 $stack->getCurrentPart()->eqpos = strlen( $accum ); 755 $accum .= '='; 756 ++$i; 757 } 758 } 759 760 # Output any remaining unclosed brackets 761 foreach ( $stack->stack as $piece ) { 762 $stack->rootAccum .= $piece->breakSyntax(); 763 } 764 $stack->rootAccum .= '</root>'; 765 $xml = $stack->rootAccum; 766 767 wfProfileOut( __METHOD__ ); 768 769 return $xml; 770 } 771 } 772 773 /** 774 * Stack class to help Preprocessor::preprocessToObj() 775 * @ingroup Parser 776 */ 777 class PPDStack { 778 public $stack, $rootAccum; 779 780 /** 781 * @var PPDStack 782 */ 783 public $top; 784 public $out; 785 public $elementClass = 'PPDStackElement'; 786 787 public static $false = false; 788 789 public function __construct() { 790 $this->stack = array(); 791 $this->top = false; 792 $this->rootAccum = ''; 793 $this->accum =& $this->rootAccum; 794 } 795 796 /** 797 * @return int 798 */ 799 public function count() { 800 return count( $this->stack ); 801 } 802 803 public function &getAccum() { 804 return $this->accum; 805 } 806 807 public function getCurrentPart() { 808 if ( $this->top === false ) { 809 return false; 810 } else { 811 return $this->top->getCurrentPart(); 812 } 813 } 814 815 public function push( $data ) { 816 if ( $data instanceof $this->elementClass ) { 817 $this->stack[] = $data; 818 } else { 819 $class = $this->elementClass; 820 $this->stack[] = new $class( $data ); 821 } 822 $this->top = $this->stack[count( $this->stack ) - 1]; 823 $this->accum =& $this->top->getAccum(); 824 } 825 826 public function pop() { 827 if ( !count( $this->stack ) ) { 828 throw new MWException( __METHOD__ . ': no elements remaining' ); 829 } 830 $temp = array_pop( $this->stack ); 831 832 if ( count( $this->stack ) ) { 833 $this->top = $this->stack[count( $this->stack ) - 1]; 834 $this->accum =& $this->top->getAccum(); 835 } else { 836 $this->top = self::$false; 837 $this->accum =& $this->rootAccum; 838 } 839 return $temp; 840 } 841 842 public function addPart( $s = '' ) { 843 $this->top->addPart( $s ); 844 $this->accum =& $this->top->getAccum(); 845 } 846 847 /** 848 * @return array 849 */ 850 public function getFlags() { 851 if ( !count( $this->stack ) ) { 852 return array( 853 'findEquals' => false, 854 'findPipe' => false, 855 'inHeading' => false, 856 ); 857 } else { 858 return $this->top->getFlags(); 859 } 860 } 861 } 862 863 /** 864 * @ingroup Parser 865 */ 866 class PPDStackElement { 867 public $open, // Opening character (\n for heading) 868 $close, // Matching closing character 869 $count, // Number of opening characters found (number of "=" for heading) 870 $parts, // Array of PPDPart objects describing pipe-separated parts. 871 $lineStart; // True if the open char appeared at the start of the input line. Not set for headings. 872 873 public $partClass = 'PPDPart'; 874 875 public function __construct( $data = array() ) { 876 $class = $this->partClass; 877 $this->parts = array( new $class ); 878 879 foreach ( $data as $name => $value ) { 880 $this->$name = $value; 881 } 882 } 883 884 public function &getAccum() { 885 return $this->parts[count( $this->parts ) - 1]->out; 886 } 887 888 public function addPart( $s = '' ) { 889 $class = $this->partClass; 890 $this->parts[] = new $class( $s ); 891 } 892 893 public function getCurrentPart() { 894 return $this->parts[count( $this->parts ) - 1]; 895 } 896 897 /** 898 * @return array 899 */ 900 public function getFlags() { 901 $partCount = count( $this->parts ); 902 $findPipe = $this->open != "\n" && $this->open != '['; 903 return array( 904 'findPipe' => $findPipe, 905 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ), 906 'inHeading' => $this->open == "\n", 907 ); 908 } 909 910 /** 911 * Get the output string that would result if the close is not found. 912 * 913 * @param bool|int $openingCount 914 * @return string 915 */ 916 public function breakSyntax( $openingCount = false ) { 917 if ( $this->open == "\n" ) { 918 $s = $this->parts[0]->out; 919 } else { 920 if ( $openingCount === false ) { 921 $openingCount = $this->count; 922 } 923 $s = str_repeat( $this->open, $openingCount ); 924 $first = true; 925 foreach ( $this->parts as $part ) { 926 if ( $first ) { 927 $first = false; 928 } else { 929 $s .= '|'; 930 } 931 $s .= $part->out; 932 } 933 } 934 return $s; 935 } 936 } 937 938 /** 939 * @ingroup Parser 940 */ 941 class PPDPart { 942 public $out; // Output accumulator string 943 944 // Optional member variables: 945 // eqpos Position of equals sign in output accumulator 946 // commentEnd Past-the-end input pointer for the last comment encountered 947 // visualEnd Past-the-end input pointer for the end of the accumulator minus comments 948 949 public function __construct( $out = '' ) { 950 $this->out = $out; 951 } 952 } 953 954 /** 955 * An expansion frame, used as a context to expand the result of preprocessToObj() 956 * @ingroup Parser 957 * @codingStandardsIgnoreStart 958 */ 959 class PPFrame_DOM implements PPFrame { 960 // @codingStandardsIgnoreEnd 961 962 /** 963 * @var Preprocessor 964 */ 965 public $preprocessor; 966 967 /** 968 * @var Parser 969 */ 970 public $parser; 971 972 /** 973 * @var Title 974 */ 975 public $title; 976 public $titleCache; 977 978 /** 979 * Hashtable listing templates which are disallowed for expansion in this frame, 980 * having been encountered previously in parent frames. 981 */ 982 public $loopCheckHash; 983 984 /** 985 * Recursion depth of this frame, top = 0 986 * Note that this is NOT the same as expansion depth in expand() 987 */ 988 public $depth; 989 990 private $volatile = false; 991 private $ttl = null; 992 993 /** 994 * @var array 995 */ 996 protected $childExpansionCache; 997 998 /** 999 * Construct a new preprocessor frame. 1000 * @param Preprocessor $preprocessor The parent preprocessor 1001 */ 1002 public function __construct( $preprocessor ) { 1003 $this->preprocessor = $preprocessor; 1004 $this->parser = $preprocessor->parser; 1005 $this->title = $this->parser->mTitle; 1006 $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false ); 1007 $this->loopCheckHash = array(); 1008 $this->depth = 0; 1009 $this->childExpansionCache = array(); 1010 } 1011 1012 /** 1013 * Create a new child frame 1014 * $args is optionally a multi-root PPNode or array containing the template arguments 1015 * 1016 * @param bool|array $args 1017 * @param Title|bool $title 1018 * @param int $indexOffset 1019 * @return PPTemplateFrame_DOM 1020 */ 1021 public function newChild( $args = false, $title = false, $indexOffset = 0 ) { 1022 $namedArgs = array(); 1023 $numberedArgs = array(); 1024 if ( $title === false ) { 1025 $title = $this->title; 1026 } 1027 if ( $args !== false ) { 1028 $xpath = false; 1029 if ( $args instanceof PPNode ) { 1030 $args = $args->node; 1031 } 1032 foreach ( $args as $arg ) { 1033 if ( $arg instanceof PPNode ) { 1034 $arg = $arg->node; 1035 } 1036 if ( !$xpath || $xpath->document !== $arg->ownerDocument ) { 1037 $xpath = new DOMXPath( $arg->ownerDocument ); 1038 } 1039 1040 $nameNodes = $xpath->query( 'name', $arg ); 1041 $value = $xpath->query( 'value', $arg ); 1042 if ( $nameNodes->item( 0 )->hasAttributes() ) { 1043 // Numbered parameter 1044 $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent; 1045 $index = $index - $indexOffset; 1046 $numberedArgs[$index] = $value->item( 0 ); 1047 unset( $namedArgs[$index] ); 1048 } else { 1049 // Named parameter 1050 $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) ); 1051 $namedArgs[$name] = $value->item( 0 ); 1052 unset( $numberedArgs[$name] ); 1053 } 1054 } 1055 } 1056 return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title ); 1057 } 1058 1059 /** 1060 * @throws MWException 1061 * @param string|int $key 1062 * @param string|PPNode_DOM|DOMDocument $root 1063 * @param int $flags 1064 * @return string 1065 */ 1066 public function cachedExpand( $key, $root, $flags = 0 ) { 1067 // we don't have a parent, so we don't have a cache 1068 return $this->expand( $root, $flags ); 1069 } 1070 1071 /** 1072 * @throws MWException 1073 * @param string|PPNode_DOM|DOMDocument $root 1074 * @param int $flags 1075 * @return string 1076 */ 1077 public function expand( $root, $flags = 0 ) { 1078 static $expansionDepth = 0; 1079 if ( is_string( $root ) ) { 1080 return $root; 1081 } 1082 1083 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) { 1084 $this->parser->limitationWarn( 'node-count-exceeded', 1085 $this->parser->mPPNodeCount, 1086 $this->parser->mOptions->getMaxPPNodeCount() 1087 ); 1088 return '<span class="error">Node-count limit exceeded</span>'; 1089 } 1090 1091 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) { 1092 $this->parser->limitationWarn( 'expansion-depth-exceeded', 1093 $expansionDepth, 1094 $this->parser->mOptions->getMaxPPExpandDepth() 1095 ); 1096 return '<span class="error">Expansion depth limit exceeded</span>'; 1097 } 1098 wfProfileIn( __METHOD__ ); 1099 ++$expansionDepth; 1100 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) { 1101 $this->parser->mHighestExpansionDepth = $expansionDepth; 1102 } 1103 1104 if ( $root instanceof PPNode_DOM ) { 1105 $root = $root->node; 1106 } 1107 if ( $root instanceof DOMDocument ) { 1108 $root = $root->documentElement; 1109 } 1110 1111 $outStack = array( '', '' ); 1112 $iteratorStack = array( false, $root ); 1113 $indexStack = array( 0, 0 ); 1114 1115 while ( count( $iteratorStack ) > 1 ) { 1116 $level = count( $outStack ) - 1; 1117 $iteratorNode =& $iteratorStack[$level]; 1118 $out =& $outStack[$level]; 1119 $index =& $indexStack[$level]; 1120 1121 if ( $iteratorNode instanceof PPNode_DOM ) { 1122 $iteratorNode = $iteratorNode->node; 1123 } 1124 1125 if ( is_array( $iteratorNode ) ) { 1126 if ( $index >= count( $iteratorNode ) ) { 1127 // All done with this iterator 1128 $iteratorStack[$level] = false; 1129 $contextNode = false; 1130 } else { 1131 $contextNode = $iteratorNode[$index]; 1132 $index++; 1133 } 1134 } elseif ( $iteratorNode instanceof DOMNodeList ) { 1135 if ( $index >= $iteratorNode->length ) { 1136 // All done with this iterator 1137 $iteratorStack[$level] = false; 1138 $contextNode = false; 1139 } else { 1140 $contextNode = $iteratorNode->item( $index ); 1141 $index++; 1142 } 1143 } else { 1144 // Copy to $contextNode and then delete from iterator stack, 1145 // because this is not an iterator but we do have to execute it once 1146 $contextNode = $iteratorStack[$level]; 1147 $iteratorStack[$level] = false; 1148 } 1149 1150 if ( $contextNode instanceof PPNode_DOM ) { 1151 $contextNode = $contextNode->node; 1152 } 1153 1154 $newIterator = false; 1155 1156 if ( $contextNode === false ) { 1157 // nothing to do 1158 } elseif ( is_string( $contextNode ) ) { 1159 $out .= $contextNode; 1160 } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) { 1161 $newIterator = $contextNode; 1162 } elseif ( $contextNode instanceof DOMNode ) { 1163 if ( $contextNode->nodeType == XML_TEXT_NODE ) { 1164 $out .= $contextNode->nodeValue; 1165 } elseif ( $contextNode->nodeName == 'template' ) { 1166 # Double-brace expansion 1167 $xpath = new DOMXPath( $contextNode->ownerDocument ); 1168 $titles = $xpath->query( 'title', $contextNode ); 1169 $title = $titles->item( 0 ); 1170 $parts = $xpath->query( 'part', $contextNode ); 1171 if ( $flags & PPFrame::NO_TEMPLATES ) { 1172 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts ); 1173 } else { 1174 $lineStart = $contextNode->getAttribute( 'lineStart' ); 1175 $params = array( 1176 'title' => new PPNode_DOM( $title ), 1177 'parts' => new PPNode_DOM( $parts ), 1178 'lineStart' => $lineStart ); 1179 $ret = $this->parser->braceSubstitution( $params, $this ); 1180 if ( isset( $ret['object'] ) ) { 1181 $newIterator = $ret['object']; 1182 } else { 1183 $out .= $ret['text']; 1184 } 1185 } 1186 } elseif ( $contextNode->nodeName == 'tplarg' ) { 1187 # Triple-brace expansion 1188 $xpath = new DOMXPath( $contextNode->ownerDocument ); 1189 $titles = $xpath->query( 'title', $contextNode ); 1190 $title = $titles->item( 0 ); 1191 $parts = $xpath->query( 'part', $contextNode ); 1192 if ( $flags & PPFrame::NO_ARGS ) { 1193 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts ); 1194 } else { 1195 $params = array( 1196 'title' => new PPNode_DOM( $title ), 1197 'parts' => new PPNode_DOM( $parts ) ); 1198 $ret = $this->parser->argSubstitution( $params, $this ); 1199 if ( isset( $ret['object'] ) ) { 1200 $newIterator = $ret['object']; 1201 } else { 1202 $out .= $ret['text']; 1203 } 1204 } 1205 } elseif ( $contextNode->nodeName == 'comment' ) { 1206 # HTML-style comment 1207 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes 1208 if ( $this->parser->ot['html'] 1209 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) 1210 || ( $flags & PPFrame::STRIP_COMMENTS ) 1211 ) { 1212 $out .= ''; 1213 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) { 1214 # Add a strip marker in PST mode so that pstPass2() can 1215 # run some old-fashioned regexes on the result. 1216 # Not in RECOVER_COMMENTS mode (extractSections) though. 1217 $out .= $this->parser->insertStripItem( $contextNode->textContent ); 1218 } else { 1219 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove 1220 $out .= $contextNode->textContent; 1221 } 1222 } elseif ( $contextNode->nodeName == 'ignore' ) { 1223 # Output suppression used by <includeonly> etc. 1224 # OT_WIKI will only respect <ignore> in substed templates. 1225 # The other output types respect it unless NO_IGNORE is set. 1226 # extractSections() sets NO_IGNORE and so never respects it. 1227 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) 1228 || ( $flags & PPFrame::NO_IGNORE ) 1229 ) { 1230 $out .= $contextNode->textContent; 1231 } else { 1232 $out .= ''; 1233 } 1234 } elseif ( $contextNode->nodeName == 'ext' ) { 1235 # Extension tag 1236 $xpath = new DOMXPath( $contextNode->ownerDocument ); 1237 $names = $xpath->query( 'name', $contextNode ); 1238 $attrs = $xpath->query( 'attr', $contextNode ); 1239 $inners = $xpath->query( 'inner', $contextNode ); 1240 $closes = $xpath->query( 'close', $contextNode ); 1241 if ( $flags & PPFrame::NO_TAGS ) { 1242 $s = '<' . $this->expand( $names->item( 0 ), $flags ); 1243 if ( $attrs->length > 0 ) { 1244 $s .= $this->expand( $attrs->item( 0 ), $flags ); 1245 } 1246 if ( $inners->length > 0 ) { 1247 $s .= '>' . $this->expand( $inners->item( 0 ), $flags ); 1248 if ( $closes->length > 0 ) { 1249 $s .= $this->expand( $closes->item( 0 ), $flags ); 1250 } 1251 } else { 1252 $s .= '/>'; 1253 } 1254 $out .= $s; 1255 } else { 1256 $params = array( 1257 'name' => new PPNode_DOM( $names->item( 0 ) ), 1258 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null, 1259 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null, 1260 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null, 1261 ); 1262 $out .= $this->parser->extensionSubstitution( $params, $this ); 1263 } 1264 } elseif ( $contextNode->nodeName == 'h' ) { 1265 # Heading 1266 $s = $this->expand( $contextNode->childNodes, $flags ); 1267 1268 # Insert a heading marker only for <h> children of <root> 1269 # This is to stop extractSections from going over multiple tree levels 1270 if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) { 1271 # Insert heading index marker 1272 $headingIndex = $contextNode->getAttribute( 'i' ); 1273 $titleText = $this->title->getPrefixedDBkey(); 1274 $this->parser->mHeadings[] = array( $titleText, $headingIndex ); 1275 $serial = count( $this->parser->mHeadings ) - 1; 1276 $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX; 1277 $count = $contextNode->getAttribute( 'level' ); 1278 $s = substr( $s, 0, $count ) . $marker . substr( $s, $count ); 1279 $this->parser->mStripState->addGeneral( $marker, '' ); 1280 } 1281 $out .= $s; 1282 } else { 1283 # Generic recursive expansion 1284 $newIterator = $contextNode->childNodes; 1285 } 1286 } else { 1287 wfProfileOut( __METHOD__ ); 1288 throw new MWException( __METHOD__ . ': Invalid parameter type' ); 1289 } 1290 1291 if ( $newIterator !== false ) { 1292 if ( $newIterator instanceof PPNode_DOM ) { 1293 $newIterator = $newIterator->node; 1294 } 1295 $outStack[] = ''; 1296 $iteratorStack[] = $newIterator; 1297 $indexStack[] = 0; 1298 } elseif ( $iteratorStack[$level] === false ) { 1299 // Return accumulated value to parent 1300 // With tail recursion 1301 while ( $iteratorStack[$level] === false && $level > 0 ) { 1302 $outStack[$level - 1] .= $out; 1303 array_pop( $outStack ); 1304 array_pop( $iteratorStack ); 1305 array_pop( $indexStack ); 1306 $level--; 1307 } 1308 } 1309 } 1310 --$expansionDepth; 1311 wfProfileOut( __METHOD__ ); 1312 return $outStack[0]; 1313 } 1314 1315 /** 1316 * @param string $sep 1317 * @param int $flags 1318 * @param string|PPNode_DOM|DOMDocument $args,... 1319 * @return string 1320 */ 1321 public function implodeWithFlags( $sep, $flags /*, ... */ ) { 1322 $args = array_slice( func_get_args(), 2 ); 1323 1324 $first = true; 1325 $s = ''; 1326 foreach ( $args as $root ) { 1327 if ( $root instanceof PPNode_DOM ) { 1328 $root = $root->node; 1329 } 1330 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 1331 $root = array( $root ); 1332 } 1333 foreach ( $root as $node ) { 1334 if ( $first ) { 1335 $first = false; 1336 } else { 1337 $s .= $sep; 1338 } 1339 $s .= $this->expand( $node, $flags ); 1340 } 1341 } 1342 return $s; 1343 } 1344 1345 /** 1346 * Implode with no flags specified 1347 * This previously called implodeWithFlags but has now been inlined to reduce stack depth 1348 * 1349 * @param string $sep 1350 * @param string|PPNode_DOM|DOMDocument $args,... 1351 * @return string 1352 */ 1353 public function implode( $sep /*, ... */ ) { 1354 $args = array_slice( func_get_args(), 1 ); 1355 1356 $first = true; 1357 $s = ''; 1358 foreach ( $args as $root ) { 1359 if ( $root instanceof PPNode_DOM ) { 1360 $root = $root->node; 1361 } 1362 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 1363 $root = array( $root ); 1364 } 1365 foreach ( $root as $node ) { 1366 if ( $first ) { 1367 $first = false; 1368 } else { 1369 $s .= $sep; 1370 } 1371 $s .= $this->expand( $node ); 1372 } 1373 } 1374 return $s; 1375 } 1376 1377 /** 1378 * Makes an object that, when expand()ed, will be the same as one obtained 1379 * with implode() 1380 * 1381 * @param string $sep 1382 * @param string|PPNode_DOM|DOMDocument $args,... 1383 * @return array 1384 */ 1385 public function virtualImplode( $sep /*, ... */ ) { 1386 $args = array_slice( func_get_args(), 1 ); 1387 $out = array(); 1388 $first = true; 1389 1390 foreach ( $args as $root ) { 1391 if ( $root instanceof PPNode_DOM ) { 1392 $root = $root->node; 1393 } 1394 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 1395 $root = array( $root ); 1396 } 1397 foreach ( $root as $node ) { 1398 if ( $first ) { 1399 $first = false; 1400 } else { 1401 $out[] = $sep; 1402 } 1403 $out[] = $node; 1404 } 1405 } 1406 return $out; 1407 } 1408 1409 /** 1410 * Virtual implode with brackets 1411 * @param string $start 1412 * @param string $sep 1413 * @param string $end 1414 * @param string|PPNode_DOM|DOMDocument $args,... 1415 * @return array 1416 */ 1417 public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { 1418 $args = array_slice( func_get_args(), 3 ); 1419 $out = array( $start ); 1420 $first = true; 1421 1422 foreach ( $args as $root ) { 1423 if ( $root instanceof PPNode_DOM ) { 1424 $root = $root->node; 1425 } 1426 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 1427 $root = array( $root ); 1428 } 1429 foreach ( $root as $node ) { 1430 if ( $first ) { 1431 $first = false; 1432 } else { 1433 $out[] = $sep; 1434 } 1435 $out[] = $node; 1436 } 1437 } 1438 $out[] = $end; 1439 return $out; 1440 } 1441 1442 public function __toString() { 1443 return 'frame{}'; 1444 } 1445 1446 public function getPDBK( $level = false ) { 1447 if ( $level === false ) { 1448 return $this->title->getPrefixedDBkey(); 1449 } else { 1450 return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false; 1451 } 1452 } 1453 1454 /** 1455 * @return array 1456 */ 1457 public function getArguments() { 1458 return array(); 1459 } 1460 1461 /** 1462 * @return array 1463 */ 1464 public function getNumberedArguments() { 1465 return array(); 1466 } 1467 1468 /** 1469 * @return array 1470 */ 1471 public function getNamedArguments() { 1472 return array(); 1473 } 1474 1475 /** 1476 * Returns true if there are no arguments in this frame 1477 * 1478 * @return bool 1479 */ 1480 public function isEmpty() { 1481 return true; 1482 } 1483 1484 public function getArgument( $name ) { 1485 return false; 1486 } 1487 1488 /** 1489 * Returns true if the infinite loop check is OK, false if a loop is detected 1490 * 1491 * @param Title $title 1492 * @return bool 1493 */ 1494 public function loopCheck( $title ) { 1495 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); 1496 } 1497 1498 /** 1499 * Return true if the frame is a template frame 1500 * 1501 * @return bool 1502 */ 1503 public function isTemplate() { 1504 return false; 1505 } 1506 1507 /** 1508 * Get a title of frame 1509 * 1510 * @return Title 1511 */ 1512 public function getTitle() { 1513 return $this->title; 1514 } 1515 1516 /** 1517 * Set the volatile flag 1518 * 1519 * @param bool $flag 1520 */ 1521 public function setVolatile( $flag = true ) { 1522 $this->volatile = $flag; 1523 } 1524 1525 /** 1526 * Get the volatile flag 1527 * 1528 * @return bool 1529 */ 1530 public function isVolatile() { 1531 return $this->volatile; 1532 } 1533 1534 /** 1535 * Set the TTL 1536 * 1537 * @param int $ttl 1538 */ 1539 public function setTTL( $ttl ) { 1540 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) { 1541 $this->ttl = $ttl; 1542 } 1543 } 1544 1545 /** 1546 * Get the TTL 1547 * 1548 * @return int|null 1549 */ 1550 public function getTTL() { 1551 return $this->ttl; 1552 } 1553 } 1554 1555 /** 1556 * Expansion frame with template arguments 1557 * @ingroup Parser 1558 * @codingStandardsIgnoreStart 1559 */ 1560 class PPTemplateFrame_DOM extends PPFrame_DOM { 1561 // @codingStandardsIgnoreEnd 1562 1563 public $numberedArgs, $namedArgs; 1564 1565 /** 1566 * @var PPFrame_DOM 1567 */ 1568 public $parent; 1569 public $numberedExpansionCache, $namedExpansionCache; 1570 1571 /** 1572 * @param Preprocessor $preprocessor 1573 * @param bool|PPFrame_DOM $parent 1574 * @param array $numberedArgs 1575 * @param array $namedArgs 1576 * @param bool|Title $title 1577 */ 1578 public function __construct( $preprocessor, $parent = false, $numberedArgs = array(), 1579 $namedArgs = array(), $title = false 1580 ) { 1581 parent::__construct( $preprocessor ); 1582 1583 $this->parent = $parent; 1584 $this->numberedArgs = $numberedArgs; 1585 $this->namedArgs = $namedArgs; 1586 $this->title = $title; 1587 $pdbk = $title ? $title->getPrefixedDBkey() : false; 1588 $this->titleCache = $parent->titleCache; 1589 $this->titleCache[] = $pdbk; 1590 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash; 1591 if ( $pdbk !== false ) { 1592 $this->loopCheckHash[$pdbk] = true; 1593 } 1594 $this->depth = $parent->depth + 1; 1595 $this->numberedExpansionCache = $this->namedExpansionCache = array(); 1596 } 1597 1598 public function __toString() { 1599 $s = 'tplframe{'; 1600 $first = true; 1601 $args = $this->numberedArgs + $this->namedArgs; 1602 foreach ( $args as $name => $value ) { 1603 if ( $first ) { 1604 $first = false; 1605 } else { 1606 $s .= ', '; 1607 } 1608 $s .= "\"$name\":\"" . 1609 str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"'; 1610 } 1611 $s .= '}'; 1612 return $s; 1613 } 1614 1615 /** 1616 * @throws MWException 1617 * @param string|int $key 1618 * @param string|PPNode_DOM|DOMDocument $root 1619 * @param int $flags 1620 * @return string 1621 */ 1622 public function cachedExpand( $key, $root, $flags = 0 ) { 1623 if ( isset( $this->parent->childExpansionCache[$key] ) ) { 1624 return $this->parent->childExpansionCache[$key]; 1625 } 1626 $retval = $this->expand( $root, $flags ); 1627 if ( !$this->isVolatile() ) { 1628 $this->parent->childExpansionCache[$key] = $retval; 1629 } 1630 return $retval; 1631 } 1632 1633 /** 1634 * Returns true if there are no arguments in this frame 1635 * 1636 * @return bool 1637 */ 1638 public function isEmpty() { 1639 return !count( $this->numberedArgs ) && !count( $this->namedArgs ); 1640 } 1641 1642 public function getArguments() { 1643 $arguments = array(); 1644 foreach ( array_merge( 1645 array_keys( $this->numberedArgs ), 1646 array_keys( $this->namedArgs ) ) as $key ) { 1647 $arguments[$key] = $this->getArgument( $key ); 1648 } 1649 return $arguments; 1650 } 1651 1652 public function getNumberedArguments() { 1653 $arguments = array(); 1654 foreach ( array_keys( $this->numberedArgs ) as $key ) { 1655 $arguments[$key] = $this->getArgument( $key ); 1656 } 1657 return $arguments; 1658 } 1659 1660 public function getNamedArguments() { 1661 $arguments = array(); 1662 foreach ( array_keys( $this->namedArgs ) as $key ) { 1663 $arguments[$key] = $this->getArgument( $key ); 1664 } 1665 return $arguments; 1666 } 1667 1668 public function getNumberedArgument( $index ) { 1669 if ( !isset( $this->numberedArgs[$index] ) ) { 1670 return false; 1671 } 1672 if ( !isset( $this->numberedExpansionCache[$index] ) ) { 1673 # No trimming for unnamed arguments 1674 $this->numberedExpansionCache[$index] = $this->parent->expand( 1675 $this->numberedArgs[$index], 1676 PPFrame::STRIP_COMMENTS 1677 ); 1678 } 1679 return $this->numberedExpansionCache[$index]; 1680 } 1681 1682 public function getNamedArgument( $name ) { 1683 if ( !isset( $this->namedArgs[$name] ) ) { 1684 return false; 1685 } 1686 if ( !isset( $this->namedExpansionCache[$name] ) ) { 1687 # Trim named arguments post-expand, for backwards compatibility 1688 $this->namedExpansionCache[$name] = trim( 1689 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) ); 1690 } 1691 return $this->namedExpansionCache[$name]; 1692 } 1693 1694 public function getArgument( $name ) { 1695 $text = $this->getNumberedArgument( $name ); 1696 if ( $text === false ) { 1697 $text = $this->getNamedArgument( $name ); 1698 } 1699 return $text; 1700 } 1701 1702 /** 1703 * Return true if the frame is a template frame 1704 * 1705 * @return bool 1706 */ 1707 public function isTemplate() { 1708 return true; 1709 } 1710 1711 public function setVolatile( $flag = true ) { 1712 parent::setVolatile( $flag ); 1713 $this->parent->setVolatile( $flag ); 1714 } 1715 1716 public function setTTL( $ttl ) { 1717 parent::setTTL( $ttl ); 1718 $this->parent->setTTL( $ttl ); 1719 } 1720 } 1721 1722 /** 1723 * Expansion frame with custom arguments 1724 * @ingroup Parser 1725 * @codingStandardsIgnoreStart 1726 */ 1727 class PPCustomFrame_DOM extends PPFrame_DOM { 1728 // @codingStandardsIgnoreEnd 1729 1730 public $args; 1731 1732 public function __construct( $preprocessor, $args ) { 1733 parent::__construct( $preprocessor ); 1734 $this->args = $args; 1735 } 1736 1737 public function __toString() { 1738 $s = 'cstmframe{'; 1739 $first = true; 1740 foreach ( $this->args as $name => $value ) { 1741 if ( $first ) { 1742 $first = false; 1743 } else { 1744 $s .= ', '; 1745 } 1746 $s .= "\"$name\":\"" . 1747 str_replace( '"', '\\"', $value->__toString() ) . '"'; 1748 } 1749 $s .= '}'; 1750 return $s; 1751 } 1752 1753 /** 1754 * @return bool 1755 */ 1756 public function isEmpty() { 1757 return !count( $this->args ); 1758 } 1759 1760 public function getArgument( $index ) { 1761 if ( !isset( $this->args[$index] ) ) { 1762 return false; 1763 } 1764 return $this->args[$index]; 1765 } 1766 1767 public function getArguments() { 1768 return $this->args; 1769 } 1770 } 1771 1772 /** 1773 * @ingroup Parser 1774 * @codingStandardsIgnoreStart 1775 */ 1776 class PPNode_DOM implements PPNode { 1777 // @codingStandardsIgnoreEnd 1778 1779 /** 1780 * @var DOMElement 1781 */ 1782 public $node; 1783 public $xpath; 1784 1785 public function __construct( $node, $xpath = false ) { 1786 $this->node = $node; 1787 } 1788 1789 /** 1790 * @return DOMXPath 1791 */ 1792 public function getXPath() { 1793 if ( $this->xpath === null ) { 1794 $this->xpath = new DOMXPath( $this->node->ownerDocument ); 1795 } 1796 return $this->xpath; 1797 } 1798 1799 public function __toString() { 1800 if ( $this->node instanceof DOMNodeList ) { 1801 $s = ''; 1802 foreach ( $this->node as $node ) { 1803 $s .= $node->ownerDocument->saveXML( $node ); 1804 } 1805 } else { 1806 $s = $this->node->ownerDocument->saveXML( $this->node ); 1807 } 1808 return $s; 1809 } 1810 1811 /** 1812 * @return bool|PPNode_DOM 1813 */ 1814 public function getChildren() { 1815 return $this->node->childNodes ? new self( $this->node->childNodes ) : false; 1816 } 1817 1818 /** 1819 * @return bool|PPNode_DOM 1820 */ 1821 public function getFirstChild() { 1822 return $this->node->firstChild ? new self( $this->node->firstChild ) : false; 1823 } 1824 1825 /** 1826 * @return bool|PPNode_DOM 1827 */ 1828 public function getNextSibling() { 1829 return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false; 1830 } 1831 1832 /** 1833 * @param string $type 1834 * 1835 * @return bool|PPNode_DOM 1836 */ 1837 public function getChildrenOfType( $type ) { 1838 return new self( $this->getXPath()->query( $type, $this->node ) ); 1839 } 1840 1841 /** 1842 * @return int 1843 */ 1844 public function getLength() { 1845 if ( $this->node instanceof DOMNodeList ) { 1846 return $this->node->length; 1847 } else { 1848 return false; 1849 } 1850 } 1851 1852 /** 1853 * @param int $i 1854 * @return bool|PPNode_DOM 1855 */ 1856 public function item( $i ) { 1857 $item = $this->node->item( $i ); 1858 return $item ? new self( $item ) : false; 1859 } 1860 1861 /** 1862 * @return string 1863 */ 1864 public function getName() { 1865 if ( $this->node instanceof DOMNodeList ) { 1866 return '#nodelist'; 1867 } else { 1868 return $this->node->nodeName; 1869 } 1870 } 1871 1872 /** 1873 * Split a "<part>" node into an associative array containing: 1874 * - name PPNode name 1875 * - index String index 1876 * - value PPNode value 1877 * 1878 * @throws MWException 1879 * @return array 1880 */ 1881 public function splitArg() { 1882 $xpath = $this->getXPath(); 1883 $names = $xpath->query( 'name', $this->node ); 1884 $values = $xpath->query( 'value', $this->node ); 1885 if ( !$names->length || !$values->length ) { 1886 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ ); 1887 } 1888 $name = $names->item( 0 ); 1889 $index = $name->getAttribute( 'index' ); 1890 return array( 1891 'name' => new self( $name ), 1892 'index' => $index, 1893 'value' => new self( $values->item( 0 ) ) ); 1894 } 1895 1896 /** 1897 * Split an "<ext>" node into an associative array containing name, attr, inner and close 1898 * All values in the resulting array are PPNodes. Inner and close are optional. 1899 * 1900 * @throws MWException 1901 * @return array 1902 */ 1903 public function splitExt() { 1904 $xpath = $this->getXPath(); 1905 $names = $xpath->query( 'name', $this->node ); 1906 $attrs = $xpath->query( 'attr', $this->node ); 1907 $inners = $xpath->query( 'inner', $this->node ); 1908 $closes = $xpath->query( 'close', $this->node ); 1909 if ( !$names->length || !$attrs->length ) { 1910 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ ); 1911 } 1912 $parts = array( 1913 'name' => new self( $names->item( 0 ) ), 1914 'attr' => new self( $attrs->item( 0 ) ) ); 1915 if ( $inners->length ) { 1916 $parts['inner'] = new self( $inners->item( 0 ) ); 1917 } 1918 if ( $closes->length ) { 1919 $parts['close'] = new self( $closes->item( 0 ) ); 1920 } 1921 return $parts; 1922 } 1923 1924 /** 1925 * Split a "<h>" node 1926 * @throws MWException 1927 * @return array 1928 */ 1929 public function splitHeading() { 1930 if ( $this->getName() !== 'h' ) { 1931 throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); 1932 } 1933 return array( 1934 'i' => $this->node->getAttribute( 'i' ), 1935 'level' => $this->node->getAttribute( 'level' ), 1936 'contents' => $this->getChildren() 1937 ); 1938 } 1939 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |