PPFrame_Hash.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @file
  19. * @ingroup Parser
  20. */
  21. /**
  22. * An expansion frame, used as a context to expand the result of preprocessToObj()
  23. * @ingroup Parser
  24. */
  25. // phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
  26. class PPFrame_Hash implements PPFrame {
  27. /**
  28. * @var Parser
  29. */
  30. public $parser;
  31. /**
  32. * @var Preprocessor
  33. */
  34. public $preprocessor;
  35. /**
  36. * @var Title
  37. */
  38. public $title;
  39. public $titleCache;
  40. /**
  41. * Hashtable listing templates which are disallowed for expansion in this frame,
  42. * having been encountered previously in parent frames.
  43. */
  44. public $loopCheckHash;
  45. /**
  46. * Recursion depth of this frame, top = 0
  47. * Note that this is NOT the same as expansion depth in expand()
  48. */
  49. public $depth;
  50. private $volatile = false;
  51. private $ttl = null;
  52. /**
  53. * @var array
  54. */
  55. protected $childExpansionCache;
  56. /**
  57. * Construct a new preprocessor frame.
  58. * @param Preprocessor $preprocessor The parent preprocessor
  59. */
  60. public function __construct( $preprocessor ) {
  61. $this->preprocessor = $preprocessor;
  62. $this->parser = $preprocessor->parser;
  63. $this->title = $this->parser->getTitle();
  64. $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
  65. $this->loopCheckHash = [];
  66. $this->depth = 0;
  67. $this->childExpansionCache = [];
  68. }
  69. /**
  70. * Create a new child frame
  71. * $args is optionally a multi-root PPNode or array containing the template arguments
  72. *
  73. * @param array|bool|PPNode_Hash_Array $args
  74. * @param Title|bool $title
  75. * @param int $indexOffset
  76. * @throws MWException
  77. * @return PPTemplateFrame_Hash
  78. */
  79. public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
  80. $namedArgs = [];
  81. $numberedArgs = [];
  82. if ( $title === false ) {
  83. $title = $this->title;
  84. }
  85. if ( $args !== false ) {
  86. if ( $args instanceof PPNode_Hash_Array ) {
  87. $args = $args->value;
  88. } elseif ( !is_array( $args ) ) {
  89. throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
  90. }
  91. foreach ( $args as $arg ) {
  92. $bits = $arg->splitArg();
  93. if ( $bits['index'] !== '' ) {
  94. // Numbered parameter
  95. $index = $bits['index'] - $indexOffset;
  96. if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
  97. $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
  98. wfEscapeWikiText( $this->title ),
  99. wfEscapeWikiText( $title ),
  100. wfEscapeWikiText( $index ) )->text() );
  101. $this->parser->addTrackingCategory( 'duplicate-args-category' );
  102. }
  103. $numberedArgs[$index] = $bits['value'];
  104. unset( $namedArgs[$index] );
  105. } else {
  106. // Named parameter
  107. $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
  108. if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
  109. $this->parser->getOutput()->addWarning( wfMessage( 'duplicate-args-warning',
  110. wfEscapeWikiText( $this->title ),
  111. wfEscapeWikiText( $title ),
  112. wfEscapeWikiText( $name ) )->text() );
  113. $this->parser->addTrackingCategory( 'duplicate-args-category' );
  114. }
  115. $namedArgs[$name] = $bits['value'];
  116. unset( $numberedArgs[$name] );
  117. }
  118. }
  119. }
  120. return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
  121. }
  122. /**
  123. * @throws MWException
  124. * @param string|int $key
  125. * @param string|PPNode $root
  126. * @param int $flags
  127. * @return string
  128. */
  129. public function cachedExpand( $key, $root, $flags = 0 ) {
  130. // we don't have a parent, so we don't have a cache
  131. return $this->expand( $root, $flags );
  132. }
  133. /**
  134. * @throws MWException
  135. * @param string|PPNode $root
  136. * @param int $flags
  137. * @return string
  138. */
  139. public function expand( $root, $flags = 0 ) {
  140. static $expansionDepth = 0;
  141. if ( is_string( $root ) ) {
  142. return $root;
  143. }
  144. if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->getMaxPPNodeCount() ) {
  145. $this->parser->limitationWarn( 'node-count-exceeded',
  146. $this->parser->mPPNodeCount,
  147. $this->parser->mOptions->getMaxPPNodeCount()
  148. );
  149. return '<span class="error">Node-count limit exceeded</span>';
  150. }
  151. if ( $expansionDepth > $this->parser->mOptions->getMaxPPExpandDepth() ) {
  152. $this->parser->limitationWarn( 'expansion-depth-exceeded',
  153. $expansionDepth,
  154. $this->parser->mOptions->getMaxPPExpandDepth()
  155. );
  156. return '<span class="error">Expansion depth limit exceeded</span>';
  157. }
  158. ++$expansionDepth;
  159. if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
  160. $this->parser->mHighestExpansionDepth = $expansionDepth;
  161. }
  162. $outStack = [ '', '' ];
  163. $iteratorStack = [ false, $root ];
  164. $indexStack = [ 0, 0 ];
  165. while ( count( $iteratorStack ) > 1 ) {
  166. $level = count( $outStack ) - 1;
  167. $iteratorNode =& $iteratorStack[$level];
  168. $out =& $outStack[$level];
  169. $index =& $indexStack[$level];
  170. if ( is_array( $iteratorNode ) ) {
  171. if ( $index >= count( $iteratorNode ) ) {
  172. // All done with this iterator
  173. $iteratorStack[$level] = false;
  174. $contextNode = false;
  175. } else {
  176. $contextNode = $iteratorNode[$index];
  177. $index++;
  178. }
  179. } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
  180. if ( $index >= $iteratorNode->getLength() ) {
  181. // All done with this iterator
  182. $iteratorStack[$level] = false;
  183. $contextNode = false;
  184. } else {
  185. $contextNode = $iteratorNode->item( $index );
  186. $index++;
  187. }
  188. } else {
  189. // Copy to $contextNode and then delete from iterator stack,
  190. // because this is not an iterator but we do have to execute it once
  191. $contextNode = $iteratorStack[$level];
  192. $iteratorStack[$level] = false;
  193. }
  194. $newIterator = false;
  195. $contextName = false;
  196. $contextChildren = false;
  197. if ( $contextNode === false ) {
  198. // nothing to do
  199. } elseif ( is_string( $contextNode ) ) {
  200. $out .= $contextNode;
  201. } elseif ( $contextNode instanceof PPNode_Hash_Array ) {
  202. $newIterator = $contextNode;
  203. } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
  204. // No output
  205. } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
  206. $out .= $contextNode->value;
  207. } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
  208. $contextName = $contextNode->name;
  209. $contextChildren = $contextNode->getRawChildren();
  210. } elseif ( is_array( $contextNode ) ) {
  211. // Node descriptor array
  212. if ( count( $contextNode ) !== 2 ) {
  213. throw new MWException( __METHOD__ .
  214. ': found an array where a node descriptor should be' );
  215. }
  216. list( $contextName, $contextChildren ) = $contextNode;
  217. } else {
  218. throw new MWException( __METHOD__ . ': Invalid parameter type' );
  219. }
  220. // Handle node descriptor array or tree object
  221. if ( $contextName === false ) {
  222. // Not a node, already handled above
  223. } elseif ( $contextName[0] === '@' ) {
  224. // Attribute: no output
  225. } elseif ( $contextName === 'template' ) {
  226. # Double-brace expansion
  227. $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
  228. if ( $flags & PPFrame::NO_TEMPLATES ) {
  229. $newIterator = $this->virtualBracketedImplode(
  230. '{{', '|', '}}',
  231. $bits['title'],
  232. $bits['parts']
  233. );
  234. } else {
  235. $ret = $this->parser->braceSubstitution( $bits, $this );
  236. if ( isset( $ret['object'] ) ) {
  237. $newIterator = $ret['object'];
  238. } else {
  239. $out .= $ret['text'];
  240. }
  241. }
  242. } elseif ( $contextName === 'tplarg' ) {
  243. # Triple-brace expansion
  244. $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
  245. if ( $flags & PPFrame::NO_ARGS ) {
  246. $newIterator = $this->virtualBracketedImplode(
  247. '{{{', '|', '}}}',
  248. $bits['title'],
  249. $bits['parts']
  250. );
  251. } else {
  252. $ret = $this->parser->argSubstitution( $bits, $this );
  253. if ( isset( $ret['object'] ) ) {
  254. $newIterator = $ret['object'];
  255. } else {
  256. $out .= $ret['text'];
  257. }
  258. }
  259. } elseif ( $contextName === 'comment' ) {
  260. # HTML-style comment
  261. # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
  262. # Not in RECOVER_COMMENTS mode (msgnw) though.
  263. if ( ( $this->parser->ot['html']
  264. || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
  265. || ( $flags & PPFrame::STRIP_COMMENTS )
  266. ) && !( $flags & PPFrame::RECOVER_COMMENTS )
  267. ) {
  268. $out .= '';
  269. } elseif ( $this->parser->ot['wiki'] && !( $flags & PPFrame::RECOVER_COMMENTS ) ) {
  270. # Add a strip marker in PST mode so that pstPass2() can
  271. # run some old-fashioned regexes on the result.
  272. # Not in RECOVER_COMMENTS mode (extractSections) though.
  273. $out .= $this->parser->insertStripItem( $contextChildren[0] );
  274. } else {
  275. # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
  276. $out .= $contextChildren[0];
  277. }
  278. } elseif ( $contextName === 'ignore' ) {
  279. # Output suppression used by <includeonly> etc.
  280. # OT_WIKI will only respect <ignore> in substed templates.
  281. # The other output types respect it unless NO_IGNORE is set.
  282. # extractSections() sets NO_IGNORE and so never respects it.
  283. if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] )
  284. || ( $flags & PPFrame::NO_IGNORE )
  285. ) {
  286. $out .= $contextChildren[0];
  287. } else {
  288. // $out .= '';
  289. }
  290. } elseif ( $contextName === 'ext' ) {
  291. # Extension tag
  292. $bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
  293. [ 'attr' => null, 'inner' => null, 'close' => null ];
  294. if ( $flags & PPFrame::NO_TAGS ) {
  295. $s = '<' . $bits['name']->getFirstChild()->value;
  296. if ( $bits['attr'] ) {
  297. $s .= $bits['attr']->getFirstChild()->value;
  298. }
  299. if ( $bits['inner'] ) {
  300. $s .= '>' . $bits['inner']->getFirstChild()->value;
  301. if ( $bits['close'] ) {
  302. $s .= $bits['close']->getFirstChild()->value;
  303. }
  304. } else {
  305. $s .= '/>';
  306. }
  307. $out .= $s;
  308. } else {
  309. $out .= $this->parser->extensionSubstitution( $bits, $this );
  310. }
  311. } elseif ( $contextName === 'h' ) {
  312. # Heading
  313. if ( $this->parser->ot['html'] ) {
  314. # Expand immediately and insert heading index marker
  315. $s = $this->expand( $contextChildren, $flags );
  316. $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
  317. $titleText = $this->title->getPrefixedDBkey();
  318. $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
  319. $serial = count( $this->parser->mHeadings ) - 1;
  320. $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
  321. $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
  322. $this->parser->mStripState->addGeneral( $marker, '' );
  323. $out .= $s;
  324. } else {
  325. # Expand in virtual stack
  326. $newIterator = $contextChildren;
  327. }
  328. } else {
  329. # Generic recursive expansion
  330. $newIterator = $contextChildren;
  331. }
  332. if ( $newIterator !== false ) {
  333. $outStack[] = '';
  334. $iteratorStack[] = $newIterator;
  335. $indexStack[] = 0;
  336. } elseif ( $iteratorStack[$level] === false ) {
  337. // Return accumulated value to parent
  338. // With tail recursion
  339. while ( $iteratorStack[$level] === false && $level > 0 ) {
  340. $outStack[$level - 1] .= $out;
  341. array_pop( $outStack );
  342. array_pop( $iteratorStack );
  343. array_pop( $indexStack );
  344. $level--;
  345. }
  346. }
  347. }
  348. --$expansionDepth;
  349. return $outStack[0];
  350. }
  351. /**
  352. * @param string $sep
  353. * @param int $flags
  354. * @param string|PPNode ...$args
  355. * @return string
  356. */
  357. public function implodeWithFlags( $sep, $flags, ...$args ) {
  358. $first = true;
  359. $s = '';
  360. foreach ( $args as $root ) {
  361. if ( $root instanceof PPNode_Hash_Array ) {
  362. $root = $root->value;
  363. }
  364. if ( !is_array( $root ) ) {
  365. $root = [ $root ];
  366. }
  367. foreach ( $root as $node ) {
  368. if ( $first ) {
  369. $first = false;
  370. } else {
  371. $s .= $sep;
  372. }
  373. $s .= $this->expand( $node, $flags );
  374. }
  375. }
  376. return $s;
  377. }
  378. /**
  379. * Implode with no flags specified
  380. * This previously called implodeWithFlags but has now been inlined to reduce stack depth
  381. * @param string $sep
  382. * @param string|PPNode ...$args
  383. * @return string
  384. */
  385. public function implode( $sep, ...$args ) {
  386. $first = true;
  387. $s = '';
  388. foreach ( $args as $root ) {
  389. if ( $root instanceof PPNode_Hash_Array ) {
  390. $root = $root->value;
  391. }
  392. if ( !is_array( $root ) ) {
  393. $root = [ $root ];
  394. }
  395. foreach ( $root as $node ) {
  396. if ( $first ) {
  397. $first = false;
  398. } else {
  399. $s .= $sep;
  400. }
  401. $s .= $this->expand( $node );
  402. }
  403. }
  404. return $s;
  405. }
  406. /**
  407. * Makes an object that, when expand()ed, will be the same as one obtained
  408. * with implode()
  409. *
  410. * @param string $sep
  411. * @param string|PPNode ...$args
  412. * @return PPNode_Hash_Array
  413. */
  414. public function virtualImplode( $sep, ...$args ) {
  415. $out = [];
  416. $first = true;
  417. foreach ( $args as $root ) {
  418. if ( $root instanceof PPNode_Hash_Array ) {
  419. $root = $root->value;
  420. }
  421. if ( !is_array( $root ) ) {
  422. $root = [ $root ];
  423. }
  424. foreach ( $root as $node ) {
  425. if ( $first ) {
  426. $first = false;
  427. } else {
  428. $out[] = $sep;
  429. }
  430. $out[] = $node;
  431. }
  432. }
  433. return new PPNode_Hash_Array( $out );
  434. }
  435. /**
  436. * Virtual implode with brackets
  437. *
  438. * @param string $start
  439. * @param string $sep
  440. * @param string $end
  441. * @param string|PPNode ...$args
  442. * @return PPNode_Hash_Array
  443. */
  444. public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
  445. $out = [ $start ];
  446. $first = true;
  447. foreach ( $args as $root ) {
  448. if ( $root instanceof PPNode_Hash_Array ) {
  449. $root = $root->value;
  450. }
  451. if ( !is_array( $root ) ) {
  452. $root = [ $root ];
  453. }
  454. foreach ( $root as $node ) {
  455. if ( $first ) {
  456. $first = false;
  457. } else {
  458. $out[] = $sep;
  459. }
  460. $out[] = $node;
  461. }
  462. }
  463. $out[] = $end;
  464. return new PPNode_Hash_Array( $out );
  465. }
  466. public function __toString() {
  467. return 'frame{}';
  468. }
  469. /**
  470. * @param bool $level
  471. * @return array|bool|string
  472. */
  473. public function getPDBK( $level = false ) {
  474. if ( $level === false ) {
  475. return $this->title->getPrefixedDBkey();
  476. } else {
  477. return $this->titleCache[$level] ?? false;
  478. }
  479. }
  480. /**
  481. * @return array
  482. */
  483. public function getArguments() {
  484. return [];
  485. }
  486. /**
  487. * @return array
  488. */
  489. public function getNumberedArguments() {
  490. return [];
  491. }
  492. /**
  493. * @return array
  494. */
  495. public function getNamedArguments() {
  496. return [];
  497. }
  498. /**
  499. * Returns true if there are no arguments in this frame
  500. *
  501. * @return bool
  502. */
  503. public function isEmpty() {
  504. return true;
  505. }
  506. /**
  507. * @param int|string $name
  508. * @return bool Always false in this implementation.
  509. */
  510. public function getArgument( $name ) {
  511. return false;
  512. }
  513. /**
  514. * Returns true if the infinite loop check is OK, false if a loop is detected
  515. *
  516. * @param Title $title
  517. *
  518. * @return bool
  519. */
  520. public function loopCheck( $title ) {
  521. return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
  522. }
  523. /**
  524. * Return true if the frame is a template frame
  525. *
  526. * @return bool
  527. */
  528. public function isTemplate() {
  529. return false;
  530. }
  531. /**
  532. * Get a title of frame
  533. *
  534. * @return Title
  535. */
  536. public function getTitle() {
  537. return $this->title;
  538. }
  539. /**
  540. * Set the volatile flag
  541. *
  542. * @param bool $flag
  543. */
  544. public function setVolatile( $flag = true ) {
  545. $this->volatile = $flag;
  546. }
  547. /**
  548. * Get the volatile flag
  549. *
  550. * @return bool
  551. */
  552. public function isVolatile() {
  553. return $this->volatile;
  554. }
  555. /**
  556. * Set the TTL
  557. *
  558. * @param int $ttl
  559. */
  560. public function setTTL( $ttl ) {
  561. if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
  562. $this->ttl = $ttl;
  563. }
  564. }
  565. /**
  566. * Get the TTL
  567. *
  568. * @return int|null
  569. */
  570. public function getTTL() {
  571. return $this->ttl;
  572. }
  573. }