In PHP 8, but not in PHP 7.4, every class with a __toString() function implicitly implements the Stringable interface. Therefore, the behavior of checks like "instanceof Stringable" differs between these PHP versions when such classes are involved. Make every such class implement the interface so that the behavior will be consistent. The PHP 7.4 fallback for the Stringable interface is provided by symfony/polyfill-php80. Change-Id: I3f0330c2555c7d3bf99b654ed3c0b0303e257ea1
638 lines
17 KiB
PHP
638 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
* @ingroup Parser
|
|
*/
|
|
|
|
use MediaWiki\Parser\Parser;
|
|
use MediaWiki\Title\Title;
|
|
|
|
/**
|
|
* An expansion frame, used as a context to expand the result of preprocessToObj()
|
|
* @ingroup Parser
|
|
*/
|
|
// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
|
|
class PPFrame_Hash implements Stringable, PPFrame {
|
|
|
|
/**
|
|
* @var Parser
|
|
*/
|
|
public $parser;
|
|
|
|
/**
|
|
* @var Preprocessor
|
|
*/
|
|
public $preprocessor;
|
|
|
|
/**
|
|
* @var Title
|
|
*/
|
|
public $title;
|
|
|
|
/**
|
|
* @var (string|false)[]
|
|
*/
|
|
public $titleCache;
|
|
|
|
/**
|
|
* Hashtable listing templates which are disallowed for expansion in this frame,
|
|
* having been encountered previously in parent frames.
|
|
* @var true[]
|
|
*/
|
|
public $loopCheckHash;
|
|
|
|
/**
|
|
* Recursion depth of this frame, top = 0
|
|
* Note that this is NOT the same as expansion depth in expand()
|
|
* @var int
|
|
*/
|
|
public $depth;
|
|
|
|
/** @var bool */
|
|
private $volatile = false;
|
|
/** @var int|null */
|
|
private $ttl = null;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $childExpansionCache;
|
|
/**
|
|
* @var int
|
|
*/
|
|
private $maxPPNodeCount;
|
|
/**
|
|
* @var int
|
|
*/
|
|
private $maxPPExpandDepth;
|
|
|
|
/**
|
|
* @param Preprocessor $preprocessor The parent preprocessor
|
|
*/
|
|
public function __construct( $preprocessor ) {
|
|
$this->preprocessor = $preprocessor;
|
|
$this->parser = $preprocessor->parser;
|
|
$this->title = $this->parser->getTitle();
|
|
$this->maxPPNodeCount = $this->parser->getOptions()->getMaxPPNodeCount();
|
|
$this->maxPPExpandDepth = $this->parser->getOptions()->getMaxPPExpandDepth();
|
|
$this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
|
|
$this->loopCheckHash = [];
|
|
$this->depth = 0;
|
|
$this->childExpansionCache = [];
|
|
}
|
|
|
|
/**
|
|
* Create a new child frame
|
|
* $args is optionally a multi-root PPNode or array containing the template arguments
|
|
*
|
|
* @param PPNode[]|false|PPNode_Hash_Array $args
|
|
* @param Title|false $title
|
|
* @param int $indexOffset
|
|
* @return PPTemplateFrame_Hash
|
|
*/
|
|
public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
|
|
$namedArgs = [];
|
|
$numberedArgs = [];
|
|
if ( $title === false ) {
|
|
$title = $this->title;
|
|
}
|
|
if ( $args !== false ) {
|
|
if ( $args instanceof PPNode_Hash_Array ) {
|
|
$args = $args->value;
|
|
} elseif ( !is_array( $args ) ) {
|
|
throw new InvalidArgumentException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
|
|
}
|
|
foreach ( $args as $arg ) {
|
|
$bits = $arg->splitArg();
|
|
if ( $bits['index'] !== '' ) {
|
|
// Numbered parameter
|
|
$index = $bits['index'] - $indexOffset;
|
|
if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
|
|
$this->parser->getOutput()->addWarningMsg(
|
|
'duplicate-args-warning',
|
|
Message::plaintextParam( (string)$this->title ),
|
|
Message::plaintextParam( (string)$title ),
|
|
Message::numParam( $index )
|
|
);
|
|
$this->parser->addTrackingCategory( 'duplicate-args-category' );
|
|
}
|
|
$numberedArgs[$index] = $bits['value'];
|
|
unset( $namedArgs[$index] );
|
|
} else {
|
|
// Named parameter
|
|
$name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
|
|
if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
|
|
$this->parser->getOutput()->addWarningMsg(
|
|
'duplicate-args-warning',
|
|
Message::plaintextParam( (string)$this->title ),
|
|
Message::plaintextParam( (string)$title ),
|
|
Message::plaintextParam( $name )
|
|
);
|
|
$this->parser->addTrackingCategory( 'duplicate-args-category' );
|
|
}
|
|
$namedArgs[$name] = $bits['value'];
|
|
unset( $numberedArgs[$name] );
|
|
}
|
|
}
|
|
}
|
|
return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
|
|
}
|
|
|
|
/**
|
|
* @param string|int $key
|
|
* @param string|PPNode $root
|
|
* @param int $flags
|
|
* @return string
|
|
*/
|
|
public function cachedExpand( $key, $root, $flags = 0 ) {
|
|
// we don't have a parent, so we don't have a cache
|
|
return $this->expand( $root, $flags );
|
|
}
|
|
|
|
/**
|
|
* @param string|PPNode $root
|
|
* @param int $flags
|
|
* @return string
|
|
*/
|
|
public function expand( $root, $flags = 0 ) {
|
|
static $expansionDepth = 0;
|
|
if ( is_string( $root ) ) {
|
|
return $root;
|
|
}
|
|
|
|
if ( ++$this->parser->mPPNodeCount > $this->maxPPNodeCount ) {
|
|
$this->parser->limitationWarn( 'node-count-exceeded',
|
|
$this->parser->mPPNodeCount,
|
|
$this->maxPPNodeCount
|
|
);
|
|
return '<span class="error">Node-count limit exceeded</span>';
|
|
}
|
|
if ( $expansionDepth > $this->maxPPExpandDepth ) {
|
|
$this->parser->limitationWarn( 'expansion-depth-exceeded',
|
|
$expansionDepth,
|
|
$this->maxPPExpandDepth
|
|
);
|
|
return '<span class="error">Expansion depth limit exceeded</span>';
|
|
}
|
|
++$expansionDepth;
|
|
if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
|
|
$this->parser->mHighestExpansionDepth = $expansionDepth;
|
|
}
|
|
|
|
$outStack = [ '', '' ];
|
|
$iteratorStack = [ false, $root ];
|
|
$indexStack = [ 0, 0 ];
|
|
|
|
while ( count( $iteratorStack ) > 1 ) {
|
|
$level = count( $outStack ) - 1;
|
|
$iteratorNode =& $iteratorStack[$level];
|
|
$out =& $outStack[$level];
|
|
$index =& $indexStack[$level];
|
|
|
|
if ( is_array( $iteratorNode ) ) {
|
|
if ( $index >= count( $iteratorNode ) ) {
|
|
// All done with this iterator
|
|
$iteratorStack[$level] = false;
|
|
$contextNode = false;
|
|
} else {
|
|
$contextNode = $iteratorNode[$index];
|
|
$index++;
|
|
}
|
|
} elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
|
|
if ( $index >= $iteratorNode->getLength() ) {
|
|
// All done with this iterator
|
|
$iteratorStack[$level] = false;
|
|
$contextNode = false;
|
|
} else {
|
|
$contextNode = $iteratorNode->item( $index );
|
|
$index++;
|
|
}
|
|
} else {
|
|
// Copy to $contextNode and then delete from iterator stack,
|
|
// because this is not an iterator but we do have to execute it once
|
|
$contextNode = $iteratorStack[$level];
|
|
$iteratorStack[$level] = false;
|
|
}
|
|
|
|
$newIterator = false;
|
|
$contextName = false;
|
|
$contextChildren = false;
|
|
|
|
if ( $contextNode === false ) {
|
|
// nothing to do
|
|
} elseif ( is_string( $contextNode ) ) {
|
|
$out .= $contextNode;
|
|
} elseif ( $contextNode instanceof PPNode_Hash_Array ) {
|
|
$newIterator = $contextNode;
|
|
} elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
|
|
// No output
|
|
} elseif ( $contextNode instanceof PPNode_Hash_Text ) {
|
|
$out .= $contextNode->value;
|
|
} elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
|
|
$contextName = $contextNode->name;
|
|
$contextChildren = $contextNode->getRawChildren();
|
|
} elseif ( is_array( $contextNode ) ) {
|
|
// Node descriptor array
|
|
if ( count( $contextNode ) !== 2 ) {
|
|
throw new RuntimeException( __METHOD__ .
|
|
': found an array where a node descriptor should be' );
|
|
}
|
|
[ $contextName, $contextChildren ] = $contextNode;
|
|
} else {
|
|
throw new RuntimeException( __METHOD__ . ': Invalid parameter type' );
|
|
}
|
|
|
|
// Handle node descriptor array or tree object
|
|
if ( $contextName === false ) {
|
|
// Not a node, already handled above
|
|
} elseif ( $contextName[0] === '@' ) {
|
|
// Attribute: no output
|
|
} elseif ( $contextName === 'template' ) {
|
|
# Double-brace expansion
|
|
$bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
|
|
if ( $flags & PPFrame::NO_TEMPLATES ) {
|
|
$newIterator = $this->virtualBracketedImplode(
|
|
'{{', '|', '}}',
|
|
$bits['title'],
|
|
$bits['parts']
|
|
);
|
|
} else {
|
|
$ret = $this->parser->braceSubstitution( $bits, $this );
|
|
if ( isset( $ret['object'] ) ) {
|
|
$newIterator = $ret['object'];
|
|
} else {
|
|
$out .= $ret['text'];
|
|
}
|
|
}
|
|
} elseif ( $contextName === 'tplarg' ) {
|
|
# Triple-brace expansion
|
|
$bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
|
|
if ( $flags & PPFrame::NO_ARGS ) {
|
|
$newIterator = $this->virtualBracketedImplode(
|
|
'{{{', '|', '}}}',
|
|
$bits['title'],
|
|
$bits['parts']
|
|
);
|
|
} else {
|
|
$ret = $this->parser->argSubstitution( $bits, $this );
|
|
if ( isset( $ret['object'] ) ) {
|
|
$newIterator = $ret['object'];
|
|
} else {
|
|
$out .= $ret['text'];
|
|
}
|
|
}
|
|
} elseif ( $contextName === 'comment' ) {
|
|
# HTML-style comment
|
|
# Remove it in HTML, pre+remove and STRIP_COMMENTS modes
|
|
# Not in RECOVER_COMMENTS mode (msgnw) though.
|
|
if ( ( $this->parser->getOutputType() === Parser::OT_HTML
|
|
|| ( $this->parser->getOutputType() === Parser::OT_PREPROCESS &&
|
|
$this->parser->getOptions()->getRemoveComments() )
|
|
|| ( $flags & PPFrame::STRIP_COMMENTS )
|
|
) && !( $flags & PPFrame::RECOVER_COMMENTS )
|
|
) {
|
|
$out .= '';
|
|
} elseif (
|
|
$this->parser->getOutputType() === Parser::OT_WIKI &&
|
|
!( $flags & PPFrame::RECOVER_COMMENTS )
|
|
) {
|
|
# Add a strip marker in PST mode so that pstPass2() can
|
|
# run some old-fashioned regexes on the result.
|
|
# Not in RECOVER_COMMENTS mode (extractSections) though.
|
|
$out .= $this->parser->insertStripItem( $contextChildren[0] );
|
|
} else {
|
|
# Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
|
|
$out .= $contextChildren[0];
|
|
}
|
|
} elseif ( $contextName === 'ignore' ) {
|
|
# Output suppression used by <includeonly> etc.
|
|
# OT_WIKI will only respect <ignore> in substed templates.
|
|
# The other output types respect it unless NO_IGNORE is set.
|
|
# extractSections() sets NO_IGNORE and so never respects it.
|
|
if ( ( !isset( $this->parent ) && $this->parser->getOutputType() === Parser::OT_WIKI )
|
|
|| ( $flags & PPFrame::NO_IGNORE )
|
|
) {
|
|
$out .= $contextChildren[0];
|
|
} else {
|
|
// $out .= '';
|
|
}
|
|
} elseif ( $contextName === 'ext' ) {
|
|
# Extension tag
|
|
$bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
|
|
[ 'attr' => null, 'inner' => null, 'close' => null ];
|
|
if ( $flags & PPFrame::NO_TAGS ) {
|
|
$s = '<' . $bits['name']->getFirstChild()->value;
|
|
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
|
|
if ( $bits['attr'] ) {
|
|
$s .= $bits['attr']->getFirstChild()->value;
|
|
}
|
|
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
|
|
if ( $bits['inner'] ) {
|
|
$s .= '>' . $bits['inner']->getFirstChild()->value;
|
|
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
|
|
if ( $bits['close'] ) {
|
|
$s .= $bits['close']->getFirstChild()->value;
|
|
}
|
|
} else {
|
|
$s .= '/>';
|
|
}
|
|
$out .= $s;
|
|
} else {
|
|
$out .= $this->parser->extensionSubstitution( $bits, $this,
|
|
(bool)( $flags & PPFrame::PROCESS_NOWIKI ) );
|
|
}
|
|
} elseif ( $contextName === 'h' ) {
|
|
# Heading
|
|
if ( $this->parser->getOutputType() === Parser::OT_HTML ) {
|
|
# Expand immediately and insert heading index marker
|
|
$s = $this->expand( $contextChildren, $flags );
|
|
$bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
|
|
$titleText = $this->title->getPrefixedDBkey();
|
|
$this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
|
|
$serial = count( $this->parser->mHeadings ) - 1;
|
|
$marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
|
|
$s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
|
|
$this->parser->getStripState()->addGeneral( $marker, '' );
|
|
$out .= $s;
|
|
} else {
|
|
# Expand in virtual stack
|
|
$newIterator = $contextChildren;
|
|
}
|
|
} else {
|
|
# Generic recursive expansion
|
|
$newIterator = $contextChildren;
|
|
}
|
|
|
|
if ( $newIterator !== false ) {
|
|
$outStack[] = '';
|
|
$iteratorStack[] = $newIterator;
|
|
$indexStack[] = 0;
|
|
} elseif ( $iteratorStack[$level] === false ) {
|
|
// Return accumulated value to parent
|
|
// With tail recursion
|
|
while ( $iteratorStack[$level] === false && $level > 0 ) {
|
|
$outStack[$level - 1] .= $out;
|
|
array_pop( $outStack );
|
|
array_pop( $iteratorStack );
|
|
array_pop( $indexStack );
|
|
$level--;
|
|
}
|
|
}
|
|
}
|
|
--$expansionDepth;
|
|
return $outStack[0];
|
|
}
|
|
|
|
/**
|
|
* @param string $sep
|
|
* @param int $flags
|
|
* @param string|PPNode ...$args
|
|
* @return string
|
|
*/
|
|
public function implodeWithFlags( $sep, $flags, ...$args ) {
|
|
$first = true;
|
|
$s = '';
|
|
foreach ( $args as $root ) {
|
|
if ( $root instanceof PPNode_Hash_Array ) {
|
|
$root = $root->value;
|
|
}
|
|
if ( !is_array( $root ) ) {
|
|
$root = [ $root ];
|
|
}
|
|
foreach ( $root as $node ) {
|
|
if ( $first ) {
|
|
$first = false;
|
|
} else {
|
|
$s .= $sep;
|
|
}
|
|
$s .= $this->expand( $node, $flags );
|
|
}
|
|
}
|
|
return $s;
|
|
}
|
|
|
|
/**
|
|
* Implode with no flags specified
|
|
* This previously called implodeWithFlags but has now been inlined to reduce stack depth
|
|
* @param string $sep
|
|
* @param string|PPNode ...$args
|
|
* @return string
|
|
*/
|
|
public function implode( $sep, ...$args ) {
|
|
$first = true;
|
|
$s = '';
|
|
foreach ( $args as $root ) {
|
|
if ( $root instanceof PPNode_Hash_Array ) {
|
|
$root = $root->value;
|
|
}
|
|
if ( !is_array( $root ) ) {
|
|
$root = [ $root ];
|
|
}
|
|
foreach ( $root as $node ) {
|
|
if ( $first ) {
|
|
$first = false;
|
|
} else {
|
|
$s .= $sep;
|
|
}
|
|
$s .= $this->expand( $node );
|
|
}
|
|
}
|
|
return $s;
|
|
}
|
|
|
|
/**
|
|
* Makes an object that, when expand()ed, will be the same as one obtained
|
|
* with implode()
|
|
*
|
|
* @param string $sep
|
|
* @param string|PPNode ...$args
|
|
* @return PPNode_Hash_Array
|
|
*/
|
|
public function virtualImplode( $sep, ...$args ) {
|
|
$out = [];
|
|
$first = true;
|
|
|
|
foreach ( $args as $root ) {
|
|
if ( $root instanceof PPNode_Hash_Array ) {
|
|
$root = $root->value;
|
|
}
|
|
if ( !is_array( $root ) ) {
|
|
$root = [ $root ];
|
|
}
|
|
foreach ( $root as $node ) {
|
|
if ( $first ) {
|
|
$first = false;
|
|
} else {
|
|
$out[] = $sep;
|
|
}
|
|
$out[] = $node;
|
|
}
|
|
}
|
|
return new PPNode_Hash_Array( $out );
|
|
}
|
|
|
|
/**
|
|
* Virtual implode with brackets
|
|
*
|
|
* @param string $start
|
|
* @param string $sep
|
|
* @param string $end
|
|
* @param string|PPNode ...$args
|
|
* @return PPNode_Hash_Array
|
|
*/
|
|
public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
|
|
$out = [ $start ];
|
|
$first = true;
|
|
|
|
foreach ( $args as $root ) {
|
|
if ( $root instanceof PPNode_Hash_Array ) {
|
|
$root = $root->value;
|
|
}
|
|
if ( !is_array( $root ) ) {
|
|
$root = [ $root ];
|
|
}
|
|
foreach ( $root as $node ) {
|
|
if ( $first ) {
|
|
$first = false;
|
|
} else {
|
|
$out[] = $sep;
|
|
}
|
|
$out[] = $node;
|
|
}
|
|
}
|
|
$out[] = $end;
|
|
return new PPNode_Hash_Array( $out );
|
|
}
|
|
|
|
public function __toString() {
|
|
return 'frame{}';
|
|
}
|
|
|
|
/**
|
|
* @param string|false $level
|
|
* @return false|string
|
|
*/
|
|
public function getPDBK( $level = false ) {
|
|
if ( $level === false ) {
|
|
return $this->title->getPrefixedDBkey();
|
|
} else {
|
|
return $this->titleCache[$level] ?? false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getArguments() {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getNumberedArguments() {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getNamedArguments() {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Returns true if there are no arguments in this frame
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isEmpty() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param int|string $name
|
|
* @return bool Always false in this implementation.
|
|
*/
|
|
public function getArgument( $name ) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the infinite loop check is OK, false if a loop is detected
|
|
*
|
|
* @param Title $title
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function loopCheck( $title ) {
|
|
return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
|
|
}
|
|
|
|
/**
|
|
* Return true if the frame is a template frame
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isTemplate() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get a title of frame
|
|
*
|
|
* @return Title
|
|
*/
|
|
public function getTitle() {
|
|
return $this->title;
|
|
}
|
|
|
|
/**
|
|
* Set the volatile flag
|
|
*
|
|
* @param bool $flag
|
|
*/
|
|
public function setVolatile( $flag = true ) {
|
|
$this->volatile = $flag;
|
|
}
|
|
|
|
/**
|
|
* Get the volatile flag
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isVolatile() {
|
|
return $this->volatile;
|
|
}
|
|
|
|
/**
|
|
* @param int $ttl
|
|
*/
|
|
public function setTTL( $ttl ) {
|
|
if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
|
|
$this->ttl = $ttl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return int|null
|
|
*/
|
|
public function getTTL() {
|
|
return $this->ttl;
|
|
}
|
|
}
|