MediaWiki
REL1_24
|
00001 <?php 00028 class Preprocessor_DOM implements Preprocessor { 00029 // @codingStandardsIgnoreEnd 00030 00034 public $parser; 00035 00036 public $memoryLimit; 00037 00038 const CACHE_VERSION = 1; 00039 00040 public function __construct( $parser ) { 00041 $this->parser = $parser; 00042 $mem = ini_get( 'memory_limit' ); 00043 $this->memoryLimit = false; 00044 if ( strval( $mem ) !== '' && $mem != -1 ) { 00045 if ( preg_match( '/^\d+$/', $mem ) ) { 00046 $this->memoryLimit = $mem; 00047 } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) { 00048 $this->memoryLimit = $m[1] * 1048576; 00049 } 00050 } 00051 } 00052 00056 public function newFrame() { 00057 return new PPFrame_DOM( $this ); 00058 } 00059 00064 public function newCustomFrame( $args ) { 00065 return new PPCustomFrame_DOM( $this, $args ); 00066 } 00067 00072 public function newPartNodeArray( $values ) { 00073 //NOTE: DOM manipulation is slower than building & parsing XML! (or so Tim sais) 00074 $xml = "<list>"; 00075 00076 foreach ( $values as $k => $val ) { 00077 if ( is_int( $k ) ) { 00078 $xml .= "<part><name index=\"$k\"/><value>" 00079 . htmlspecialchars( $val ) . "</value></part>"; 00080 } else { 00081 $xml .= "<part><name>" . htmlspecialchars( $k ) 00082 . "</name>=<value>" . htmlspecialchars( $val ) . "</value></part>"; 00083 } 00084 } 00085 00086 $xml .= "</list>"; 00087 00088 wfProfileIn( __METHOD__ . '-loadXML' ); 00089 $dom = new DOMDocument(); 00090 wfSuppressWarnings(); 00091 $result = $dom->loadXML( $xml ); 00092 wfRestoreWarnings(); 00093 if ( !$result ) { 00094 // Try running the XML through UtfNormal to get rid of invalid characters 00095 $xml = UtfNormal::cleanUp( $xml ); 00096 // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 00097 // don't barf when the XML is >256 levels deep 00098 $result = $dom->loadXML( $xml, 1 << 19 ); 00099 } 00100 wfProfileOut( __METHOD__ . '-loadXML' ); 00101 00102 if ( !$result ) { 00103 throw new MWException( 'Parameters passed to ' . __METHOD__ . ' result in invalid XML' ); 00104 } 00105 00106 $root = $dom->documentElement; 00107 $node = new PPNode_DOM( $root->childNodes ); 00108 return $node; 00109 } 00110 00115 public function memCheck() { 00116 if ( $this->memoryLimit === false ) { 00117 return true; 00118 } 00119 $usage = memory_get_usage(); 00120 if ( $usage > $this->memoryLimit * 0.9 ) { 00121 $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 ); 00122 throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" ); 00123 } 00124 return $usage <= $this->memoryLimit * 0.8; 00125 } 00126 00151 public function preprocessToObj( $text, $flags = 0 ) { 00152 wfProfileIn( __METHOD__ ); 00153 global $wgMemc, $wgPreprocessorCacheThreshold; 00154 00155 $xml = false; 00156 $cacheable = ( $wgPreprocessorCacheThreshold !== false 00157 && strlen( $text ) > $wgPreprocessorCacheThreshold ); 00158 if ( $cacheable ) { 00159 wfProfileIn( __METHOD__ . '-cacheable' ); 00160 00161 $cacheKey = wfMemcKey( 'preprocess-xml', md5( $text ), $flags ); 00162 $cacheValue = $wgMemc->get( $cacheKey ); 00163 if ( $cacheValue ) { 00164 $version = substr( $cacheValue, 0, 8 ); 00165 if ( intval( $version ) == self::CACHE_VERSION ) { 00166 $xml = substr( $cacheValue, 8 ); 00167 // From the cache 00168 wfDebugLog( "Preprocessor", "Loaded preprocessor XML from memcached (key $cacheKey)" ); 00169 } 00170 } 00171 if ( $xml === false ) { 00172 wfProfileIn( __METHOD__ . '-cache-miss' ); 00173 $xml = $this->preprocessToXml( $text, $flags ); 00174 $cacheValue = sprintf( "%08d", self::CACHE_VERSION ) . $xml; 00175 $wgMemc->set( $cacheKey, $cacheValue, 86400 ); 00176 wfProfileOut( __METHOD__ . '-cache-miss' ); 00177 wfDebugLog( "Preprocessor", "Saved preprocessor XML to memcached (key $cacheKey)" ); 00178 } 00179 } else { 00180 $xml = $this->preprocessToXml( $text, $flags ); 00181 } 00182 00183 // Fail if the number of elements exceeds acceptable limits 00184 // Do not attempt to generate the DOM 00185 $this->parser->mGeneratedPPNodeCount += substr_count( $xml, '<' ); 00186 $max = $this->parser->mOptions->getMaxGeneratedPPNodeCount(); 00187 if ( $this->parser->mGeneratedPPNodeCount > $max ) { 00188 if ( $cacheable ) { 00189 wfProfileOut( __METHOD__ . '-cacheable' ); 00190 } 00191 wfProfileOut( __METHOD__ ); 00192 throw new MWException( __METHOD__ . ': generated node count limit exceeded' ); 00193 } 00194 00195 wfProfileIn( __METHOD__ . '-loadXML' ); 00196 $dom = new DOMDocument; 00197 wfSuppressWarnings(); 00198 $result = $dom->loadXML( $xml ); 00199 wfRestoreWarnings(); 00200 if ( !$result ) { 00201 // Try running the XML through UtfNormal to get rid of invalid characters 00202 $xml = UtfNormal::cleanUp( $xml ); 00203 // 1 << 19 == XML_PARSE_HUGE, needed so newer versions of libxml2 00204 // don't barf when the XML is >256 levels deep. 00205 $result = $dom->loadXML( $xml, 1 << 19 ); 00206 } 00207 if ( $result ) { 00208 $obj = new PPNode_DOM( $dom->documentElement ); 00209 } 00210 wfProfileOut( __METHOD__ . '-loadXML' ); 00211 00212 if ( $cacheable ) { 00213 wfProfileOut( __METHOD__ . '-cacheable' ); 00214 } 00215 00216 wfProfileOut( __METHOD__ ); 00217 00218 if ( !$result ) { 00219 throw new MWException( __METHOD__ . ' generated invalid XML' ); 00220 } 00221 return $obj; 00222 } 00223 00229 public function preprocessToXml( $text, $flags = 0 ) { 00230 wfProfileIn( __METHOD__ ); 00231 $rules = array( 00232 '{' => array( 00233 'end' => '}', 00234 'names' => array( 00235 2 => 'template', 00236 3 => 'tplarg', 00237 ), 00238 'min' => 2, 00239 'max' => 3, 00240 ), 00241 '[' => array( 00242 'end' => ']', 00243 'names' => array( 2 => null ), 00244 'min' => 2, 00245 'max' => 2, 00246 ) 00247 ); 00248 00249 $forInclusion = $flags & Parser::PTD_FOR_INCLUSION; 00250 00251 $xmlishElements = $this->parser->getStripList(); 00252 $enableOnlyinclude = false; 00253 if ( $forInclusion ) { 00254 $ignoredTags = array( 'includeonly', '/includeonly' ); 00255 $ignoredElements = array( 'noinclude' ); 00256 $xmlishElements[] = 'noinclude'; 00257 if ( strpos( $text, '<onlyinclude>' ) !== false 00258 && strpos( $text, '</onlyinclude>' ) !== false 00259 ) { 00260 $enableOnlyinclude = true; 00261 } 00262 } else { 00263 $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' ); 00264 $ignoredElements = array( 'includeonly' ); 00265 $xmlishElements[] = 'includeonly'; 00266 } 00267 $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) ); 00268 00269 // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset 00270 $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA"; 00271 00272 $stack = new PPDStack; 00273 00274 $searchBase = "[{<\n"; #} 00275 // For fast reverse searches 00276 $revText = strrev( $text ); 00277 $lengthText = strlen( $text ); 00278 00279 // Input pointer, starts out pointing to a pseudo-newline before the start 00280 $i = 0; 00281 // Current accumulator 00282 $accum =& $stack->getAccum(); 00283 $accum = '<root>'; 00284 // True to find equals signs in arguments 00285 $findEquals = false; 00286 // True to take notice of pipe characters 00287 $findPipe = false; 00288 $headingIndex = 1; 00289 // True if $i is inside a possible heading 00290 $inHeading = false; 00291 // True if there are no more greater-than (>) signs right of $i 00292 $noMoreGT = false; 00293 // True to ignore all input up to the next <onlyinclude> 00294 $findOnlyinclude = $enableOnlyinclude; 00295 // Do a line-start run without outputting an LF character 00296 $fakeLineStart = true; 00297 00298 while ( true ) { 00299 //$this->memCheck(); 00300 00301 if ( $findOnlyinclude ) { 00302 // Ignore all input up to the next <onlyinclude> 00303 $startPos = strpos( $text, '<onlyinclude>', $i ); 00304 if ( $startPos === false ) { 00305 // Ignored section runs to the end 00306 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>'; 00307 break; 00308 } 00309 $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end 00310 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>'; 00311 $i = $tagEndPos; 00312 $findOnlyinclude = false; 00313 } 00314 00315 if ( $fakeLineStart ) { 00316 $found = 'line-start'; 00317 $curChar = ''; 00318 } else { 00319 # Find next opening brace, closing brace or pipe 00320 $search = $searchBase; 00321 if ( $stack->top === false ) { 00322 $currentClosing = ''; 00323 } else { 00324 $currentClosing = $stack->top->close; 00325 $search .= $currentClosing; 00326 } 00327 if ( $findPipe ) { 00328 $search .= '|'; 00329 } 00330 if ( $findEquals ) { 00331 // First equals will be for the template 00332 $search .= '='; 00333 } 00334 $rule = null; 00335 # Output literal section, advance input counter 00336 $literalLength = strcspn( $text, $search, $i ); 00337 if ( $literalLength > 0 ) { 00338 $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) ); 00339 $i += $literalLength; 00340 } 00341 if ( $i >= $lengthText ) { 00342 if ( $currentClosing == "\n" ) { 00343 // Do a past-the-end run to finish off the heading 00344 $curChar = ''; 00345 $found = 'line-end'; 00346 } else { 00347 # All done 00348 break; 00349 } 00350 } else { 00351 $curChar = $text[$i]; 00352 if ( $curChar == '|' ) { 00353 $found = 'pipe'; 00354 } elseif ( $curChar == '=' ) { 00355 $found = 'equals'; 00356 } elseif ( $curChar == '<' ) { 00357 $found = 'angle'; 00358 } elseif ( $curChar == "\n" ) { 00359 if ( $inHeading ) { 00360 $found = 'line-end'; 00361 } else { 00362 $found = 'line-start'; 00363 } 00364 } elseif ( $curChar == $currentClosing ) { 00365 $found = 'close'; 00366 } elseif ( isset( $rules[$curChar] ) ) { 00367 $found = 'open'; 00368 $rule = $rules[$curChar]; 00369 } else { 00370 # Some versions of PHP have a strcspn which stops on null characters 00371 # Ignore and continue 00372 ++$i; 00373 continue; 00374 } 00375 } 00376 } 00377 00378 if ( $found == 'angle' ) { 00379 $matches = false; 00380 // Handle </onlyinclude> 00381 if ( $enableOnlyinclude 00382 && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' 00383 ) { 00384 $findOnlyinclude = true; 00385 continue; 00386 } 00387 00388 // Determine element name 00389 if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) { 00390 // Element name missing or not listed 00391 $accum .= '<'; 00392 ++$i; 00393 continue; 00394 } 00395 // Handle comments 00396 if ( isset( $matches[2] ) && $matches[2] == '!--' ) { 00397 00398 // To avoid leaving blank lines, when a sequence of 00399 // space-separated comments is both preceded and followed by 00400 // a newline (ignoring spaces), then 00401 // trim leading and trailing spaces and the trailing newline. 00402 00403 // Find the end 00404 $endPos = strpos( $text, '-->', $i + 4 ); 00405 if ( $endPos === false ) { 00406 // Unclosed comment in input, runs to end 00407 $inner = substr( $text, $i ); 00408 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; 00409 $i = $lengthText; 00410 } else { 00411 // Search backwards for leading whitespace 00412 $wsStart = $i ? ( $i - strspn( $revText, " \t", $lengthText - $i ) ) : 0; 00413 00414 // Search forwards for trailing whitespace 00415 // $wsEnd will be the position of the last space (or the '>' if there's none) 00416 $wsEnd = $endPos + 2 + strspn( $text, " \t", $endPos + 3 ); 00417 00418 // Keep looking forward as long as we're finding more 00419 // comments. 00420 $comments = array( array( $wsStart, $wsEnd ) ); 00421 while ( substr( $text, $wsEnd + 1, 4 ) == '<!--' ) { 00422 $c = strpos( $text, '-->', $wsEnd + 4 ); 00423 if ( $c === false ) { 00424 break; 00425 } 00426 $c = $c + 2 + strspn( $text, " \t", $c + 3 ); 00427 $comments[] = array( $wsEnd + 1, $c ); 00428 $wsEnd = $c; 00429 } 00430 00431 // Eat the line if possible 00432 // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at 00433 // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but 00434 // it's a possible beneficial b/c break. 00435 if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n" 00436 && substr( $text, $wsEnd + 1, 1 ) == "\n" 00437 ) { 00438 // Remove leading whitespace from the end of the accumulator 00439 // Sanity check first though 00440 $wsLength = $i - $wsStart; 00441 if ( $wsLength > 0 00442 && strspn( $accum, " \t", -$wsLength ) === $wsLength 00443 ) { 00444 $accum = substr( $accum, 0, -$wsLength ); 00445 } 00446 00447 // Dump all but the last comment to the accumulator 00448 foreach ( $comments as $j => $com ) { 00449 $startPos = $com[0]; 00450 $endPos = $com[1] + 1; 00451 if ( $j == ( count( $comments ) - 1 ) ) { 00452 break; 00453 } 00454 $inner = substr( $text, $startPos, $endPos - $startPos ); 00455 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; 00456 } 00457 00458 // Do a line-start run next time to look for headings after the comment 00459 $fakeLineStart = true; 00460 } else { 00461 // No line to eat, just take the comment itself 00462 $startPos = $i; 00463 $endPos += 2; 00464 } 00465 00466 if ( $stack->top ) { 00467 $part = $stack->top->getCurrentPart(); 00468 if ( !( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) ) { 00469 $part->visualEnd = $wsStart; 00470 } 00471 // Else comments abutting, no change in visual end 00472 $part->commentEnd = $endPos; 00473 } 00474 $i = $endPos + 1; 00475 $inner = substr( $text, $startPos, $endPos - $startPos + 1 ); 00476 $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>'; 00477 } 00478 continue; 00479 } 00480 $name = $matches[1]; 00481 $lowerName = strtolower( $name ); 00482 $attrStart = $i + strlen( $name ) + 1; 00483 00484 // Find end of tag 00485 $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart ); 00486 if ( $tagEndPos === false ) { 00487 // Infinite backtrack 00488 // Disable tag search to prevent worst-case O(N^2) performance 00489 $noMoreGT = true; 00490 $accum .= '<'; 00491 ++$i; 00492 continue; 00493 } 00494 00495 // Handle ignored tags 00496 if ( in_array( $lowerName, $ignoredTags ) ) { 00497 $accum .= '<ignore>' 00498 . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) 00499 . '</ignore>'; 00500 $i = $tagEndPos + 1; 00501 continue; 00502 } 00503 00504 $tagStartPos = $i; 00505 if ( $text[$tagEndPos - 1] == '/' ) { 00506 $attrEnd = $tagEndPos - 1; 00507 $inner = null; 00508 $i = $tagEndPos + 1; 00509 $close = ''; 00510 } else { 00511 $attrEnd = $tagEndPos; 00512 // Find closing tag 00513 if ( preg_match( "/<\/" . preg_quote( $name, '/' ) . "\s*>/i", 00514 $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) 00515 ) { 00516 $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 ); 00517 $i = $matches[0][1] + strlen( $matches[0][0] ); 00518 $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>'; 00519 } else { 00520 // No end tag -- let it run out to the end of the text. 00521 $inner = substr( $text, $tagEndPos + 1 ); 00522 $i = $lengthText; 00523 $close = ''; 00524 } 00525 } 00526 // <includeonly> and <noinclude> just become <ignore> tags 00527 if ( in_array( $lowerName, $ignoredElements ) ) { 00528 $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) ) 00529 . '</ignore>'; 00530 continue; 00531 } 00532 00533 $accum .= '<ext>'; 00534 if ( $attrEnd <= $attrStart ) { 00535 $attr = ''; 00536 } else { 00537 $attr = substr( $text, $attrStart, $attrEnd - $attrStart ); 00538 } 00539 $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' . 00540 // Note that the attr element contains the whitespace between name and attribute, 00541 // this is necessary for precise reconstruction during pre-save transform. 00542 '<attr>' . htmlspecialchars( $attr ) . '</attr>'; 00543 if ( $inner !== null ) { 00544 $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>'; 00545 } 00546 $accum .= $close . '</ext>'; 00547 } elseif ( $found == 'line-start' ) { 00548 // Is this the start of a heading? 00549 // Line break belongs before the heading element in any case 00550 if ( $fakeLineStart ) { 00551 $fakeLineStart = false; 00552 } else { 00553 $accum .= $curChar; 00554 $i++; 00555 } 00556 00557 $count = strspn( $text, '=', $i, 6 ); 00558 if ( $count == 1 && $findEquals ) { 00559 // DWIM: This looks kind of like a name/value separator. 00560 // Let's let the equals handler have it and break the 00561 // potential heading. This is heuristic, but AFAICT the 00562 // methods for completely correct disambiguation are very 00563 // complex. 00564 } elseif ( $count > 0 ) { 00565 $piece = array( 00566 'open' => "\n", 00567 'close' => "\n", 00568 'parts' => array( new PPDPart( str_repeat( '=', $count ) ) ), 00569 'startPos' => $i, 00570 'count' => $count ); 00571 $stack->push( $piece ); 00572 $accum =& $stack->getAccum(); 00573 $flags = $stack->getFlags(); 00574 extract( $flags ); 00575 $i += $count; 00576 } 00577 } elseif ( $found == 'line-end' ) { 00578 $piece = $stack->top; 00579 // A heading must be open, otherwise \n wouldn't have been in the search list 00580 assert( '$piece->open == "\n"' ); 00581 $part = $piece->getCurrentPart(); 00582 // Search back through the input to see if it has a proper close. 00583 // Do this using the reversed string since the other solutions 00584 // (end anchor, etc.) are inefficient. 00585 $wsLength = strspn( $revText, " \t", $lengthText - $i ); 00586 $searchStart = $i - $wsLength; 00587 if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) { 00588 // Comment found at line end 00589 // Search for equals signs before the comment 00590 $searchStart = $part->visualEnd; 00591 $searchStart -= strspn( $revText, " \t", $lengthText - $searchStart ); 00592 } 00593 $count = $piece->count; 00594 $equalsLength = strspn( $revText, '=', $lengthText - $searchStart ); 00595 if ( $equalsLength > 0 ) { 00596 if ( $searchStart - $equalsLength == $piece->startPos ) { 00597 // This is just a single string of equals signs on its own line 00598 // Replicate the doHeadings behavior /={count}(.+)={count}/ 00599 // First find out how many equals signs there really are (don't stop at 6) 00600 $count = $equalsLength; 00601 if ( $count < 3 ) { 00602 $count = 0; 00603 } else { 00604 $count = min( 6, intval( ( $count - 1 ) / 2 ) ); 00605 } 00606 } else { 00607 $count = min( $equalsLength, $count ); 00608 } 00609 if ( $count > 0 ) { 00610 // Normal match, output <h> 00611 $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>"; 00612 $headingIndex++; 00613 } else { 00614 // Single equals sign on its own line, count=0 00615 $element = $accum; 00616 } 00617 } else { 00618 // No match, no <h>, just pass down the inner text 00619 $element = $accum; 00620 } 00621 // Unwind the stack 00622 $stack->pop(); 00623 $accum =& $stack->getAccum(); 00624 $flags = $stack->getFlags(); 00625 extract( $flags ); 00626 00627 // Append the result to the enclosing accumulator 00628 $accum .= $element; 00629 // Note that we do NOT increment the input pointer. 00630 // This is because the closing linebreak could be the opening linebreak of 00631 // another heading. Infinite loops are avoided because the next iteration MUST 00632 // hit the heading open case above, which unconditionally increments the 00633 // input pointer. 00634 } elseif ( $found == 'open' ) { 00635 # count opening brace characters 00636 $count = strspn( $text, $curChar, $i ); 00637 00638 # we need to add to stack only if opening brace count is enough for one of the rules 00639 if ( $count >= $rule['min'] ) { 00640 # Add it to the stack 00641 $piece = array( 00642 'open' => $curChar, 00643 'close' => $rule['end'], 00644 'count' => $count, 00645 'lineStart' => ( $i > 0 && $text[$i - 1] == "\n" ), 00646 ); 00647 00648 $stack->push( $piece ); 00649 $accum =& $stack->getAccum(); 00650 $flags = $stack->getFlags(); 00651 extract( $flags ); 00652 } else { 00653 # Add literal brace(s) 00654 $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); 00655 } 00656 $i += $count; 00657 } elseif ( $found == 'close' ) { 00658 $piece = $stack->top; 00659 # lets check if there are enough characters for closing brace 00660 $maxCount = $piece->count; 00661 $count = strspn( $text, $curChar, $i, $maxCount ); 00662 00663 # check for maximum matching characters (if there are 5 closing 00664 # characters, we will probably need only 3 - depending on the rules) 00665 $rule = $rules[$piece->open]; 00666 if ( $count > $rule['max'] ) { 00667 # The specified maximum exists in the callback array, unless the caller 00668 # has made an error 00669 $matchingCount = $rule['max']; 00670 } else { 00671 # Count is less than the maximum 00672 # Skip any gaps in the callback array to find the true largest match 00673 # Need to use array_key_exists not isset because the callback can be null 00674 $matchingCount = $count; 00675 while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) { 00676 --$matchingCount; 00677 } 00678 } 00679 00680 if ( $matchingCount <= 0 ) { 00681 # No matching element found in callback array 00682 # Output a literal closing brace and continue 00683 $accum .= htmlspecialchars( str_repeat( $curChar, $count ) ); 00684 $i += $count; 00685 continue; 00686 } 00687 $name = $rule['names'][$matchingCount]; 00688 if ( $name === null ) { 00689 // No element, just literal text 00690 $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount ); 00691 } else { 00692 # Create XML element 00693 # Note: $parts is already XML, does not need to be encoded further 00694 $parts = $piece->parts; 00695 $title = $parts[0]->out; 00696 unset( $parts[0] ); 00697 00698 # The invocation is at the start of the line if lineStart is set in 00699 # the stack, and all opening brackets are used up. 00700 if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) { 00701 $attr = ' lineStart="1"'; 00702 } else { 00703 $attr = ''; 00704 } 00705 00706 $element = "<$name$attr>"; 00707 $element .= "<title>$title</title>"; 00708 $argIndex = 1; 00709 foreach ( $parts as $part ) { 00710 if ( isset( $part->eqpos ) ) { 00711 $argName = substr( $part->out, 0, $part->eqpos ); 00712 $argValue = substr( $part->out, $part->eqpos + 1 ); 00713 $element .= "<part><name>$argName</name>=<value>$argValue</value></part>"; 00714 } else { 00715 $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>"; 00716 $argIndex++; 00717 } 00718 } 00719 $element .= "</$name>"; 00720 } 00721 00722 # Advance input pointer 00723 $i += $matchingCount; 00724 00725 # Unwind the stack 00726 $stack->pop(); 00727 $accum =& $stack->getAccum(); 00728 00729 # Re-add the old stack element if it still has unmatched opening characters remaining 00730 if ( $matchingCount < $piece->count ) { 00731 $piece->parts = array( new PPDPart ); 00732 $piece->count -= $matchingCount; 00733 # do we still qualify for any callback with remaining count? 00734 $min = $rules[$piece->open]['min']; 00735 if ( $piece->count >= $min ) { 00736 $stack->push( $piece ); 00737 $accum =& $stack->getAccum(); 00738 } else { 00739 $accum .= str_repeat( $piece->open, $piece->count ); 00740 } 00741 } 00742 $flags = $stack->getFlags(); 00743 extract( $flags ); 00744 00745 # Add XML element to the enclosing accumulator 00746 $accum .= $element; 00747 } elseif ( $found == 'pipe' ) { 00748 $findEquals = true; // shortcut for getFlags() 00749 $stack->addPart(); 00750 $accum =& $stack->getAccum(); 00751 ++$i; 00752 } elseif ( $found == 'equals' ) { 00753 $findEquals = false; // shortcut for getFlags() 00754 $stack->getCurrentPart()->eqpos = strlen( $accum ); 00755 $accum .= '='; 00756 ++$i; 00757 } 00758 } 00759 00760 # Output any remaining unclosed brackets 00761 foreach ( $stack->stack as $piece ) { 00762 $stack->rootAccum .= $piece->breakSyntax(); 00763 } 00764 $stack->rootAccum .= '</root>'; 00765 $xml = $stack->rootAccum; 00766 00767 wfProfileOut( __METHOD__ ); 00768 00769 return $xml; 00770 } 00771 } 00772 00777 class PPDStack { 00778 public $stack, $rootAccum; 00779 00783 public $top; 00784 public $out; 00785 public $elementClass = 'PPDStackElement'; 00786 00787 public static $false = false; 00788 00789 public function __construct() { 00790 $this->stack = array(); 00791 $this->top = false; 00792 $this->rootAccum = ''; 00793 $this->accum =& $this->rootAccum; 00794 } 00795 00799 public function count() { 00800 return count( $this->stack ); 00801 } 00802 00803 public function &getAccum() { 00804 return $this->accum; 00805 } 00806 00807 public function getCurrentPart() { 00808 if ( $this->top === false ) { 00809 return false; 00810 } else { 00811 return $this->top->getCurrentPart(); 00812 } 00813 } 00814 00815 public function push( $data ) { 00816 if ( $data instanceof $this->elementClass ) { 00817 $this->stack[] = $data; 00818 } else { 00819 $class = $this->elementClass; 00820 $this->stack[] = new $class( $data ); 00821 } 00822 $this->top = $this->stack[count( $this->stack ) - 1]; 00823 $this->accum =& $this->top->getAccum(); 00824 } 00825 00826 public function pop() { 00827 if ( !count( $this->stack ) ) { 00828 throw new MWException( __METHOD__ . ': no elements remaining' ); 00829 } 00830 $temp = array_pop( $this->stack ); 00831 00832 if ( count( $this->stack ) ) { 00833 $this->top = $this->stack[count( $this->stack ) - 1]; 00834 $this->accum =& $this->top->getAccum(); 00835 } else { 00836 $this->top = self::$false; 00837 $this->accum =& $this->rootAccum; 00838 } 00839 return $temp; 00840 } 00841 00842 public function addPart( $s = '' ) { 00843 $this->top->addPart( $s ); 00844 $this->accum =& $this->top->getAccum(); 00845 } 00846 00850 public function getFlags() { 00851 if ( !count( $this->stack ) ) { 00852 return array( 00853 'findEquals' => false, 00854 'findPipe' => false, 00855 'inHeading' => false, 00856 ); 00857 } else { 00858 return $this->top->getFlags(); 00859 } 00860 } 00861 } 00862 00866 class PPDStackElement { 00867 public $open, // Opening character (\n for heading) 00868 $close, // Matching closing character 00869 $count, // Number of opening characters found (number of "=" for heading) 00870 $parts, // Array of PPDPart objects describing pipe-separated parts. 00871 $lineStart; // True if the open char appeared at the start of the input line. Not set for headings. 00872 00873 public $partClass = 'PPDPart'; 00874 00875 public function __construct( $data = array() ) { 00876 $class = $this->partClass; 00877 $this->parts = array( new $class ); 00878 00879 foreach ( $data as $name => $value ) { 00880 $this->$name = $value; 00881 } 00882 } 00883 00884 public function &getAccum() { 00885 return $this->parts[count( $this->parts ) - 1]->out; 00886 } 00887 00888 public function addPart( $s = '' ) { 00889 $class = $this->partClass; 00890 $this->parts[] = new $class( $s ); 00891 } 00892 00893 public function getCurrentPart() { 00894 return $this->parts[count( $this->parts ) - 1]; 00895 } 00896 00900 public function getFlags() { 00901 $partCount = count( $this->parts ); 00902 $findPipe = $this->open != "\n" && $this->open != '['; 00903 return array( 00904 'findPipe' => $findPipe, 00905 'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ), 00906 'inHeading' => $this->open == "\n", 00907 ); 00908 } 00909 00916 public function breakSyntax( $openingCount = false ) { 00917 if ( $this->open == "\n" ) { 00918 $s = $this->parts[0]->out; 00919 } else { 00920 if ( $openingCount === false ) { 00921 $openingCount = $this->count; 00922 } 00923 $s = str_repeat( $this->open, $openingCount ); 00924 $first = true; 00925 foreach ( $this->parts as $part ) { 00926 if ( $first ) { 00927 $first = false; 00928 } else { 00929 $s .= '|'; 00930 } 00931 $s .= $part->out; 00932 } 00933 } 00934 return $s; 00935 } 00936 } 00937 00941 class PPDPart { 00942 public $out; // Output accumulator string 00943 00944 // Optional member variables: 00945 // eqpos Position of equals sign in output accumulator 00946 // commentEnd Past-the-end input pointer for the last comment encountered 00947 // visualEnd Past-the-end input pointer for the end of the accumulator minus comments 00948 00949 public function __construct( $out = '' ) { 00950 $this->out = $out; 00951 } 00952 } 00953 00959 class PPFrame_DOM implements PPFrame { 00960 // @codingStandardsIgnoreEnd 00961 00965 public $preprocessor; 00966 00970 public $parser; 00971 00975 public $title; 00976 public $titleCache; 00977 00982 public $loopCheckHash; 00983 00988 public $depth; 00989 00990 private $volatile = false; 00991 private $ttl = null; 00992 00996 protected $childExpansionCache; 00997 01002 public function __construct( $preprocessor ) { 01003 $this->preprocessor = $preprocessor; 01004 $this->parser = $preprocessor->parser; 01005 $this->title = $this->parser->mTitle; 01006 $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false ); 01007 $this->loopCheckHash = array(); 01008 $this->depth = 0; 01009 $this->childExpansionCache = array(); 01010 } 01011 01021 public function newChild( $args = false, $title = false, $indexOffset = 0 ) { 01022 $namedArgs = array(); 01023 $numberedArgs = array(); 01024 if ( $title === false ) { 01025 $title = $this->title; 01026 } 01027 if ( $args !== false ) { 01028 $xpath = false; 01029 if ( $args instanceof PPNode ) { 01030 $args = $args->node; 01031 } 01032 foreach ( $args as $arg ) { 01033 if ( $arg instanceof PPNode ) { 01034 $arg = $arg->node; 01035 } 01036 if ( !$xpath || $xpath->document !== $arg->ownerDocument ) { 01037 $xpath = new DOMXPath( $arg->ownerDocument ); 01038 } 01039 01040 $nameNodes = $xpath->query( 'name', $arg ); 01041 $value = $xpath->query( 'value', $arg ); 01042 if ( $nameNodes->item( 0 )->hasAttributes() ) { 01043 // Numbered parameter 01044 $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent; 01045 $index = $index - $indexOffset; 01046 $numberedArgs[$index] = $value->item( 0 ); 01047 unset( $namedArgs[$index] ); 01048 } else { 01049 // Named parameter 01050 $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) ); 01051 $namedArgs[$name] = $value->item( 0 ); 01052 unset( $numberedArgs[$name] ); 01053 } 01054 } 01055 } 01056 return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title ); 01057 } 01058 01066 public function cachedExpand( $key, $root, $flags = 0 ) { 01067 // we don't have a parent, so we don't have a cache 01068 return $this->expand( $root, $flags ); 01069 } 01070 01077 public function expand( $root, $flags = 0 ) { 01078 static $expansionDepth = 0; 01079 if ( is_string( $root ) ) { 01080 return $root; 01081 } 01082 01083 if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) { 01084 $this->parser->limitationWarn( 'node-count-exceeded', 01085 $this->parser->mPPNodeCount, 01086 $this->parser->mOptions->getMaxPPNodeCount() 01087 ); 01088 return '<span class="error">Node-count limit exceeded</span>'; 01089 } 01090 01091 if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) { 01092 $this->parser->limitationWarn( 'expansion-depth-exceeded', 01093 $expansionDepth, 01094 $this->parser->mOptions->getMaxPPExpandDepth() 01095 ); 01096 return '<span class="error">Expansion depth limit exceeded</span>'; 01097 } 01098 wfProfileIn( __METHOD__ ); 01099 ++$expansionDepth; 01100 if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) { 01101 $this->parser->mHighestExpansionDepth = $expansionDepth; 01102 } 01103 01104 if ( $root instanceof PPNode_DOM ) { 01105 $root = $root->node; 01106 } 01107 if ( $root instanceof DOMDocument ) { 01108 $root = $root->documentElement; 01109 } 01110 01111 $outStack = array( '', '' ); 01112 $iteratorStack = array( false, $root ); 01113 $indexStack = array( 0, 0 ); 01114 01115 while ( count( $iteratorStack ) > 1 ) { 01116 $level = count( $outStack ) - 1; 01117 $iteratorNode =& $iteratorStack[$level]; 01118 $out =& $outStack[$level]; 01119 $index =& $indexStack[$level]; 01120 01121 if ( $iteratorNode instanceof PPNode_DOM ) { 01122 $iteratorNode = $iteratorNode->node; 01123 } 01124 01125 if ( is_array( $iteratorNode ) ) { 01126 if ( $index >= count( $iteratorNode ) ) { 01127 // All done with this iterator 01128 $iteratorStack[$level] = false; 01129 $contextNode = false; 01130 } else { 01131 $contextNode = $iteratorNode[$index]; 01132 $index++; 01133 } 01134 } elseif ( $iteratorNode instanceof DOMNodeList ) { 01135 if ( $index >= $iteratorNode->length ) { 01136 // All done with this iterator 01137 $iteratorStack[$level] = false; 01138 $contextNode = false; 01139 } else { 01140 $contextNode = $iteratorNode->item( $index ); 01141 $index++; 01142 } 01143 } else { 01144 // Copy to $contextNode and then delete from iterator stack, 01145 // because this is not an iterator but we do have to execute it once 01146 $contextNode = $iteratorStack[$level]; 01147 $iteratorStack[$level] = false; 01148 } 01149 01150 if ( $contextNode instanceof PPNode_DOM ) { 01151 $contextNode = $contextNode->node; 01152 } 01153 01154 $newIterator = false; 01155 01156 if ( $contextNode === false ) { 01157 // nothing to do 01158 } elseif ( is_string( $contextNode ) ) { 01159 $out .= $contextNode; 01160 } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) { 01161 $newIterator = $contextNode; 01162 } elseif ( $contextNode instanceof DOMNode ) { 01163 if ( $contextNode->nodeType == XML_TEXT_NODE ) { 01164 $out .= $contextNode->nodeValue; 01165 } elseif ( $contextNode->nodeName == 'template' ) { 01166 # Double-brace expansion 01167 $xpath = new DOMXPath( $contextNode->ownerDocument ); 01168 $titles = $xpath->query( 'title', $contextNode ); 01169 $title = $titles->item( 0 ); 01170 $parts = $xpath->query( 'part', $contextNode ); 01171 if ( $flags & PPFrame::NO_TEMPLATES ) { 01172 $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts ); 01173 } else { 01174 $lineStart = $contextNode->getAttribute( 'lineStart' ); 01175 $params = array( 01176 'title' => new PPNode_DOM( $title ), 01177 'parts' => new PPNode_DOM( $parts ), 01178 'lineStart' => $lineStart ); 01179 $ret = $this->parser->braceSubstitution( $params, $this ); 01180 if ( isset( $ret['object'] ) ) { 01181 $newIterator = $ret['object']; 01182 } else { 01183 $out .= $ret['text']; 01184 } 01185 } 01186 } elseif ( $contextNode->nodeName == 'tplarg' ) { 01187 # Triple-brace expansion 01188 $xpath = new DOMXPath( $contextNode->ownerDocument ); 01189 $titles = $xpath->query( 'title', $contextNode ); 01190 $title = $titles->item( 0 ); 01191 $parts = $xpath->query( 'part', $contextNode ); 01192 if ( $flags & PPFrame::NO_ARGS ) { 01193 $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts ); 01194 } else { 01195 $params = array( 01196 'title' => new PPNode_DOM( $title ), 01197 'parts' => new PPNode_DOM( $parts ) ); 01198 $ret = $this->parser->argSubstitution( $params, $this ); 01199 if ( isset( $ret['object'] ) ) { 01200 $newIterator = $ret['object']; 01201 } else { 01202 $out .= $ret['text']; 01203 } 01204 } 01205 } elseif ( $contextNode->nodeName == 'comment' ) { 01206 # HTML-style comment 01207 # Remove it in HTML, pre+remove and STRIP_COMMENTS modes 01208 if ( $this->parser->ot['html'] 01209 || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() ) 01210 || ( $flags & PPFrame::STRIP_COMMENTS ) 01211 ) { 01212 $out .= ''; 01213 } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) { 01214 # Add a strip marker in PST mode so that pstPass2() can 01215 # run some old-fashioned regexes on the result. 01216 # Not in RECOVER_COMMENTS mode (extractSections) though. 01217 $out .= $this->parser->insertStripItem( $contextNode->textContent ); 01218 } else { 01219 # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove 01220 $out .= $contextNode->textContent; 01221 } 01222 } elseif ( $contextNode->nodeName == 'ignore' ) { 01223 # Output suppression used by <includeonly> etc. 01224 # OT_WIKI will only respect <ignore> in substed templates. 01225 # The other output types respect it unless NO_IGNORE is set. 01226 # extractSections() sets NO_IGNORE and so never respects it. 01227 if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) 01228 || ( $flags & PPFrame::NO_IGNORE ) 01229 ) { 01230 $out .= $contextNode->textContent; 01231 } else { 01232 $out .= ''; 01233 } 01234 } elseif ( $contextNode->nodeName == 'ext' ) { 01235 # Extension tag 01236 $xpath = new DOMXPath( $contextNode->ownerDocument ); 01237 $names = $xpath->query( 'name', $contextNode ); 01238 $attrs = $xpath->query( 'attr', $contextNode ); 01239 $inners = $xpath->query( 'inner', $contextNode ); 01240 $closes = $xpath->query( 'close', $contextNode ); 01241 if ( $flags & PPFrame::NO_TAGS ) { 01242 $s = '<' . $this->expand( $names->item( 0 ), $flags ); 01243 if ( $attrs->length > 0 ) { 01244 $s .= $this->expand( $attrs->item( 0 ), $flags ); 01245 } 01246 if ( $inners->length > 0 ) { 01247 $s .= '>' . $this->expand( $inners->item( 0 ), $flags ); 01248 if ( $closes->length > 0 ) { 01249 $s .= $this->expand( $closes->item( 0 ), $flags ); 01250 } 01251 } else { 01252 $s .= '/>'; 01253 } 01254 $out .= $s; 01255 } else { 01256 $params = array( 01257 'name' => new PPNode_DOM( $names->item( 0 ) ), 01258 'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null, 01259 'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null, 01260 'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null, 01261 ); 01262 $out .= $this->parser->extensionSubstitution( $params, $this ); 01263 } 01264 } elseif ( $contextNode->nodeName == 'h' ) { 01265 # Heading 01266 $s = $this->expand( $contextNode->childNodes, $flags ); 01267 01268 # Insert a heading marker only for <h> children of <root> 01269 # This is to stop extractSections from going over multiple tree levels 01270 if ( $contextNode->parentNode->nodeName == 'root' && $this->parser->ot['html'] ) { 01271 # Insert heading index marker 01272 $headingIndex = $contextNode->getAttribute( 'i' ); 01273 $titleText = $this->title->getPrefixedDBkey(); 01274 $this->parser->mHeadings[] = array( $titleText, $headingIndex ); 01275 $serial = count( $this->parser->mHeadings ) - 1; 01276 $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX; 01277 $count = $contextNode->getAttribute( 'level' ); 01278 $s = substr( $s, 0, $count ) . $marker . substr( $s, $count ); 01279 $this->parser->mStripState->addGeneral( $marker, '' ); 01280 } 01281 $out .= $s; 01282 } else { 01283 # Generic recursive expansion 01284 $newIterator = $contextNode->childNodes; 01285 } 01286 } else { 01287 wfProfileOut( __METHOD__ ); 01288 throw new MWException( __METHOD__ . ': Invalid parameter type' ); 01289 } 01290 01291 if ( $newIterator !== false ) { 01292 if ( $newIterator instanceof PPNode_DOM ) { 01293 $newIterator = $newIterator->node; 01294 } 01295 $outStack[] = ''; 01296 $iteratorStack[] = $newIterator; 01297 $indexStack[] = 0; 01298 } elseif ( $iteratorStack[$level] === false ) { 01299 // Return accumulated value to parent 01300 // With tail recursion 01301 while ( $iteratorStack[$level] === false && $level > 0 ) { 01302 $outStack[$level - 1] .= $out; 01303 array_pop( $outStack ); 01304 array_pop( $iteratorStack ); 01305 array_pop( $indexStack ); 01306 $level--; 01307 } 01308 } 01309 } 01310 --$expansionDepth; 01311 wfProfileOut( __METHOD__ ); 01312 return $outStack[0]; 01313 } 01314 01321 public function implodeWithFlags( $sep, $flags /*, ... */ ) { 01322 $args = array_slice( func_get_args(), 2 ); 01323 01324 $first = true; 01325 $s = ''; 01326 foreach ( $args as $root ) { 01327 if ( $root instanceof PPNode_DOM ) { 01328 $root = $root->node; 01329 } 01330 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 01331 $root = array( $root ); 01332 } 01333 foreach ( $root as $node ) { 01334 if ( $first ) { 01335 $first = false; 01336 } else { 01337 $s .= $sep; 01338 } 01339 $s .= $this->expand( $node, $flags ); 01340 } 01341 } 01342 return $s; 01343 } 01344 01353 public function implode( $sep /*, ... */ ) { 01354 $args = array_slice( func_get_args(), 1 ); 01355 01356 $first = true; 01357 $s = ''; 01358 foreach ( $args as $root ) { 01359 if ( $root instanceof PPNode_DOM ) { 01360 $root = $root->node; 01361 } 01362 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 01363 $root = array( $root ); 01364 } 01365 foreach ( $root as $node ) { 01366 if ( $first ) { 01367 $first = false; 01368 } else { 01369 $s .= $sep; 01370 } 01371 $s .= $this->expand( $node ); 01372 } 01373 } 01374 return $s; 01375 } 01376 01385 public function virtualImplode( $sep /*, ... */ ) { 01386 $args = array_slice( func_get_args(), 1 ); 01387 $out = array(); 01388 $first = true; 01389 01390 foreach ( $args as $root ) { 01391 if ( $root instanceof PPNode_DOM ) { 01392 $root = $root->node; 01393 } 01394 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 01395 $root = array( $root ); 01396 } 01397 foreach ( $root as $node ) { 01398 if ( $first ) { 01399 $first = false; 01400 } else { 01401 $out[] = $sep; 01402 } 01403 $out[] = $node; 01404 } 01405 } 01406 return $out; 01407 } 01408 01417 public function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) { 01418 $args = array_slice( func_get_args(), 3 ); 01419 $out = array( $start ); 01420 $first = true; 01421 01422 foreach ( $args as $root ) { 01423 if ( $root instanceof PPNode_DOM ) { 01424 $root = $root->node; 01425 } 01426 if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) { 01427 $root = array( $root ); 01428 } 01429 foreach ( $root as $node ) { 01430 if ( $first ) { 01431 $first = false; 01432 } else { 01433 $out[] = $sep; 01434 } 01435 $out[] = $node; 01436 } 01437 } 01438 $out[] = $end; 01439 return $out; 01440 } 01441 01442 public function __toString() { 01443 return 'frame{}'; 01444 } 01445 01446 public function getPDBK( $level = false ) { 01447 if ( $level === false ) { 01448 return $this->title->getPrefixedDBkey(); 01449 } else { 01450 return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false; 01451 } 01452 } 01453 01457 public function getArguments() { 01458 return array(); 01459 } 01460 01464 public function getNumberedArguments() { 01465 return array(); 01466 } 01467 01471 public function getNamedArguments() { 01472 return array(); 01473 } 01474 01480 public function isEmpty() { 01481 return true; 01482 } 01483 01484 public function getArgument( $name ) { 01485 return false; 01486 } 01487 01494 public function loopCheck( $title ) { 01495 return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] ); 01496 } 01497 01503 public function isTemplate() { 01504 return false; 01505 } 01506 01512 public function getTitle() { 01513 return $this->title; 01514 } 01515 01521 public function setVolatile( $flag = true ) { 01522 $this->volatile = $flag; 01523 } 01524 01530 public function isVolatile() { 01531 return $this->volatile; 01532 } 01533 01539 public function setTTL( $ttl ) { 01540 if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) { 01541 $this->ttl = $ttl; 01542 } 01543 } 01544 01550 public function getTTL() { 01551 return $this->ttl; 01552 } 01553 } 01554 01560 class PPTemplateFrame_DOM extends PPFrame_DOM { 01561 // @codingStandardsIgnoreEnd 01562 01563 public $numberedArgs, $namedArgs; 01564 01568 public $parent; 01569 public $numberedExpansionCache, $namedExpansionCache; 01570 01578 public function __construct( $preprocessor, $parent = false, $numberedArgs = array(), 01579 $namedArgs = array(), $title = false 01580 ) { 01581 parent::__construct( $preprocessor ); 01582 01583 $this->parent = $parent; 01584 $this->numberedArgs = $numberedArgs; 01585 $this->namedArgs = $namedArgs; 01586 $this->title = $title; 01587 $pdbk = $title ? $title->getPrefixedDBkey() : false; 01588 $this->titleCache = $parent->titleCache; 01589 $this->titleCache[] = $pdbk; 01590 $this->loopCheckHash = /*clone*/ $parent->loopCheckHash; 01591 if ( $pdbk !== false ) { 01592 $this->loopCheckHash[$pdbk] = true; 01593 } 01594 $this->depth = $parent->depth + 1; 01595 $this->numberedExpansionCache = $this->namedExpansionCache = array(); 01596 } 01597 01598 public function __toString() { 01599 $s = 'tplframe{'; 01600 $first = true; 01601 $args = $this->numberedArgs + $this->namedArgs; 01602 foreach ( $args as $name => $value ) { 01603 if ( $first ) { 01604 $first = false; 01605 } else { 01606 $s .= ', '; 01607 } 01608 $s .= "\"$name\":\"" . 01609 str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"'; 01610 } 01611 $s .= '}'; 01612 return $s; 01613 } 01614 01622 public function cachedExpand( $key, $root, $flags = 0 ) { 01623 if ( isset( $this->parent->childExpansionCache[$key] ) ) { 01624 return $this->parent->childExpansionCache[$key]; 01625 } 01626 $retval = $this->expand( $root, $flags ); 01627 if ( !$this->isVolatile() ) { 01628 $this->parent->childExpansionCache[$key] = $retval; 01629 } 01630 return $retval; 01631 } 01632 01638 public function isEmpty() { 01639 return !count( $this->numberedArgs ) && !count( $this->namedArgs ); 01640 } 01641 01642 public function getArguments() { 01643 $arguments = array(); 01644 foreach ( array_merge( 01645 array_keys( $this->numberedArgs ), 01646 array_keys( $this->namedArgs ) ) as $key ) { 01647 $arguments[$key] = $this->getArgument( $key ); 01648 } 01649 return $arguments; 01650 } 01651 01652 public function getNumberedArguments() { 01653 $arguments = array(); 01654 foreach ( array_keys( $this->numberedArgs ) as $key ) { 01655 $arguments[$key] = $this->getArgument( $key ); 01656 } 01657 return $arguments; 01658 } 01659 01660 public function getNamedArguments() { 01661 $arguments = array(); 01662 foreach ( array_keys( $this->namedArgs ) as $key ) { 01663 $arguments[$key] = $this->getArgument( $key ); 01664 } 01665 return $arguments; 01666 } 01667 01668 public function getNumberedArgument( $index ) { 01669 if ( !isset( $this->numberedArgs[$index] ) ) { 01670 return false; 01671 } 01672 if ( !isset( $this->numberedExpansionCache[$index] ) ) { 01673 # No trimming for unnamed arguments 01674 $this->numberedExpansionCache[$index] = $this->parent->expand( 01675 $this->numberedArgs[$index], 01676 PPFrame::STRIP_COMMENTS 01677 ); 01678 } 01679 return $this->numberedExpansionCache[$index]; 01680 } 01681 01682 public function getNamedArgument( $name ) { 01683 if ( !isset( $this->namedArgs[$name] ) ) { 01684 return false; 01685 } 01686 if ( !isset( $this->namedExpansionCache[$name] ) ) { 01687 # Trim named arguments post-expand, for backwards compatibility 01688 $this->namedExpansionCache[$name] = trim( 01689 $this->parent->expand( $this->namedArgs[$name], PPFrame::STRIP_COMMENTS ) ); 01690 } 01691 return $this->namedExpansionCache[$name]; 01692 } 01693 01694 public function getArgument( $name ) { 01695 $text = $this->getNumberedArgument( $name ); 01696 if ( $text === false ) { 01697 $text = $this->getNamedArgument( $name ); 01698 } 01699 return $text; 01700 } 01701 01707 public function isTemplate() { 01708 return true; 01709 } 01710 01711 public function setVolatile( $flag = true ) { 01712 parent::setVolatile( $flag ); 01713 $this->parent->setVolatile( $flag ); 01714 } 01715 01716 public function setTTL( $ttl ) { 01717 parent::setTTL( $ttl ); 01718 $this->parent->setTTL( $ttl ); 01719 } 01720 } 01721 01727 class PPCustomFrame_DOM extends PPFrame_DOM { 01728 // @codingStandardsIgnoreEnd 01729 01730 public $args; 01731 01732 public function __construct( $preprocessor, $args ) { 01733 parent::__construct( $preprocessor ); 01734 $this->args = $args; 01735 } 01736 01737 public function __toString() { 01738 $s = 'cstmframe{'; 01739 $first = true; 01740 foreach ( $this->args as $name => $value ) { 01741 if ( $first ) { 01742 $first = false; 01743 } else { 01744 $s .= ', '; 01745 } 01746 $s .= "\"$name\":\"" . 01747 str_replace( '"', '\\"', $value->__toString() ) . '"'; 01748 } 01749 $s .= '}'; 01750 return $s; 01751 } 01752 01756 public function isEmpty() { 01757 return !count( $this->args ); 01758 } 01759 01760 public function getArgument( $index ) { 01761 if ( !isset( $this->args[$index] ) ) { 01762 return false; 01763 } 01764 return $this->args[$index]; 01765 } 01766 01767 public function getArguments() { 01768 return $this->args; 01769 } 01770 } 01771 01776 class PPNode_DOM implements PPNode { 01777 // @codingStandardsIgnoreEnd 01778 01782 public $node; 01783 public $xpath; 01784 01785 public function __construct( $node, $xpath = false ) { 01786 $this->node = $node; 01787 } 01788 01792 public function getXPath() { 01793 if ( $this->xpath === null ) { 01794 $this->xpath = new DOMXPath( $this->node->ownerDocument ); 01795 } 01796 return $this->xpath; 01797 } 01798 01799 public function __toString() { 01800 if ( $this->node instanceof DOMNodeList ) { 01801 $s = ''; 01802 foreach ( $this->node as $node ) { 01803 $s .= $node->ownerDocument->saveXML( $node ); 01804 } 01805 } else { 01806 $s = $this->node->ownerDocument->saveXML( $this->node ); 01807 } 01808 return $s; 01809 } 01810 01814 public function getChildren() { 01815 return $this->node->childNodes ? new self( $this->node->childNodes ) : false; 01816 } 01817 01821 public function getFirstChild() { 01822 return $this->node->firstChild ? new self( $this->node->firstChild ) : false; 01823 } 01824 01828 public function getNextSibling() { 01829 return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false; 01830 } 01831 01837 public function getChildrenOfType( $type ) { 01838 return new self( $this->getXPath()->query( $type, $this->node ) ); 01839 } 01840 01844 public function getLength() { 01845 if ( $this->node instanceof DOMNodeList ) { 01846 return $this->node->length; 01847 } else { 01848 return false; 01849 } 01850 } 01851 01856 public function item( $i ) { 01857 $item = $this->node->item( $i ); 01858 return $item ? new self( $item ) : false; 01859 } 01860 01864 public function getName() { 01865 if ( $this->node instanceof DOMNodeList ) { 01866 return '#nodelist'; 01867 } else { 01868 return $this->node->nodeName; 01869 } 01870 } 01871 01881 public function splitArg() { 01882 $xpath = $this->getXPath(); 01883 $names = $xpath->query( 'name', $this->node ); 01884 $values = $xpath->query( 'value', $this->node ); 01885 if ( !$names->length || !$values->length ) { 01886 throw new MWException( 'Invalid brace node passed to ' . __METHOD__ ); 01887 } 01888 $name = $names->item( 0 ); 01889 $index = $name->getAttribute( 'index' ); 01890 return array( 01891 'name' => new self( $name ), 01892 'index' => $index, 01893 'value' => new self( $values->item( 0 ) ) ); 01894 } 01895 01903 public function splitExt() { 01904 $xpath = $this->getXPath(); 01905 $names = $xpath->query( 'name', $this->node ); 01906 $attrs = $xpath->query( 'attr', $this->node ); 01907 $inners = $xpath->query( 'inner', $this->node ); 01908 $closes = $xpath->query( 'close', $this->node ); 01909 if ( !$names->length || !$attrs->length ) { 01910 throw new MWException( 'Invalid ext node passed to ' . __METHOD__ ); 01911 } 01912 $parts = array( 01913 'name' => new self( $names->item( 0 ) ), 01914 'attr' => new self( $attrs->item( 0 ) ) ); 01915 if ( $inners->length ) { 01916 $parts['inner'] = new self( $inners->item( 0 ) ); 01917 } 01918 if ( $closes->length ) { 01919 $parts['close'] = new self( $closes->item( 0 ) ); 01920 } 01921 return $parts; 01922 } 01923 01929 public function splitHeading() { 01930 if ( $this->getName() !== 'h' ) { 01931 throw new MWException( 'Invalid h node passed to ' . __METHOD__ ); 01932 } 01933 return array( 01934 'i' => $this->node->getAttribute( 'i' ), 01935 'level' => $this->node->getAttribute( 'level' ), 01936 'contents' => $this->getChildren() 01937 ); 01938 } 01939 }