PPFrame: Use explicit varargs in method parameters
[lhc/web/wiklou.git] / includes / parser / ParserOptions.php
1 <?php
2 /**
3 * Options for the PHP parser
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 use MediaWiki\MediaWikiServices;
25 use Wikimedia\ScopedCallback;
26
27 /**
28 * @brief Set options of the Parser
29 *
30 * How to add an option in core:
31 * 1. Add it to one of the arrays in ParserOptions::setDefaults()
32 * 2. If necessary, add an entry to ParserOptions::$inCacheKey
33 * 3. Add a getter and setter in the section for that.
34 *
35 * How to add an option in an extension:
36 * 1. Use the 'ParserOptionsRegister' hook to register it.
37 * 2. Where necessary, use $popt->getOption() and $popt->setOption()
38 * to access it.
39 *
40 * @ingroup Parser
41 */
42 class ParserOptions {
43
44 /**
45 * Flag indicating that newCanonical() accepts an IContextSource or the string 'canonical', for
46 * back-compat checks from extensions.
47 * @since 1.32
48 */
49 const HAS_NEWCANONICAL_FROM_CONTEXT = 1;
50
51 /**
52 * Default values for all options that are relevant for caching.
53 * @see self::getDefaults()
54 * @var array|null
55 */
56 private static $defaults = null;
57
58 /**
59 * Lazy-loaded options
60 * @var callable[]
61 */
62 private static $lazyOptions = [
63 'dateformat' => [ __CLASS__, 'initDateFormat' ],
64 'speculativeRevId' => [ __CLASS__, 'initSpeculativeRevId' ],
65 'speculativePageId' => [ __CLASS__, 'initSpeculativePageId' ],
66 ];
67
68 /**
69 * Specify options that are included in the cache key
70 * @var array
71 */
72 private static $inCacheKey = [
73 'dateformat' => true,
74 'numberheadings' => true,
75 'thumbsize' => true,
76 'stubthreshold' => true,
77 'printable' => true,
78 'userlang' => true,
79 ];
80
81 /**
82 * Current values for all options that are relevant for caching.
83 * @var array
84 */
85 private $options;
86
87 /**
88 * Timestamp used for {{CURRENTDAY}} etc.
89 * @var string|null
90 * @note Caching based on parse time is handled externally
91 */
92 private $mTimestamp;
93
94 /**
95 * Stored user object
96 * @var User
97 * @todo Track this for caching somehow without fragmenting the cache insanely
98 */
99 private $mUser;
100
101 /**
102 * Function to be called when an option is accessed.
103 * @var callable|null
104 * @note Used for collecting used options, does not affect caching
105 */
106 private $onAccessCallback = null;
107
108 /**
109 * If the page being parsed is a redirect, this should hold the redirect
110 * target.
111 * @var Title|null
112 * @todo Track this for caching somehow
113 */
114 private $redirectTarget = null;
115
116 /**
117 * Appended to the options hash
118 */
119 private $mExtraKey = '';
120
121 /**
122 * Fetch an option and track that is was accessed
123 * @since 1.30
124 * @param string $name Option name
125 * @return mixed
126 */
127 public function getOption( $name ) {
128 if ( !array_key_exists( $name, $this->options ) ) {
129 throw new InvalidArgumentException( "Unknown parser option $name" );
130 }
131
132 $this->lazyLoadOption( $name );
133 if ( !empty( self::$inCacheKey[$name] ) ) {
134 $this->optionUsed( $name );
135 }
136 return $this->options[$name];
137 }
138
139 /**
140 * @param string $name Lazy load option without tracking usage
141 */
142 private function lazyLoadOption( $name ) {
143 if ( isset( self::$lazyOptions[$name] ) && $this->options[$name] === null ) {
144 $this->options[$name] = call_user_func( self::$lazyOptions[$name], $this, $name );
145 }
146 }
147
148 /**
149 * Set an option, generically
150 * @since 1.30
151 * @param string $name Option name
152 * @param mixed $value New value. Passing null will set null, unlike many
153 * of the existing accessors which ignore null for historical reasons.
154 * @return mixed Old value
155 */
156 public function setOption( $name, $value ) {
157 if ( !array_key_exists( $name, $this->options ) ) {
158 throw new InvalidArgumentException( "Unknown parser option $name" );
159 }
160 $old = $this->options[$name];
161 $this->options[$name] = $value;
162 return $old;
163 }
164
165 /**
166 * Legacy implementation
167 * @since 1.30 For implementing legacy setters only. Don't use this in new code.
168 * @deprecated since 1.30
169 * @param string $name Option name
170 * @param mixed $value New value. Passing null does not set the value.
171 * @return mixed Old value
172 */
173 protected function setOptionLegacy( $name, $value ) {
174 if ( !array_key_exists( $name, $this->options ) ) {
175 throw new InvalidArgumentException( "Unknown parser option $name" );
176 }
177 return wfSetVar( $this->options[$name], $value );
178 }
179
180 /**
181 * Whether to extract interlanguage links
182 *
183 * When true, interlanguage links will be returned by
184 * ParserOutput::getLanguageLinks() instead of generating link HTML.
185 *
186 * @return bool
187 */
188 public function getInterwikiMagic() {
189 return $this->getOption( 'interwikiMagic' );
190 }
191
192 /**
193 * Specify whether to extract interlanguage links
194 * @param bool|null $x New value (null is no change)
195 * @return bool Old value
196 */
197 public function setInterwikiMagic( $x ) {
198 return $this->setOptionLegacy( 'interwikiMagic', $x );
199 }
200
201 /**
202 * Allow all external images inline?
203 * @return bool
204 */
205 public function getAllowExternalImages() {
206 return $this->getOption( 'allowExternalImages' );
207 }
208
209 /**
210 * Allow all external images inline?
211 * @param bool|null $x New value (null is no change)
212 * @return bool Old value
213 */
214 public function setAllowExternalImages( $x ) {
215 return $this->setOptionLegacy( 'allowExternalImages', $x );
216 }
217
218 /**
219 * External images to allow
220 *
221 * When self::getAllowExternalImages() is false
222 *
223 * @return string|string[] URLs to allow
224 */
225 public function getAllowExternalImagesFrom() {
226 return $this->getOption( 'allowExternalImagesFrom' );
227 }
228
229 /**
230 * External images to allow
231 *
232 * When self::getAllowExternalImages() is false
233 *
234 * @param string|string[]|null $x New value (null is no change)
235 * @return string|string[] Old value
236 */
237 public function setAllowExternalImagesFrom( $x ) {
238 return $this->setOptionLegacy( 'allowExternalImagesFrom', $x );
239 }
240
241 /**
242 * Use the on-wiki external image whitelist?
243 * @return bool
244 */
245 public function getEnableImageWhitelist() {
246 return $this->getOption( 'enableImageWhitelist' );
247 }
248
249 /**
250 * Use the on-wiki external image whitelist?
251 * @param bool|null $x New value (null is no change)
252 * @return bool Old value
253 */
254 public function setEnableImageWhitelist( $x ) {
255 return $this->setOptionLegacy( 'enableImageWhitelist', $x );
256 }
257
258 /**
259 * Automatically number headings?
260 * @return bool
261 */
262 public function getNumberHeadings() {
263 return $this->getOption( 'numberheadings' );
264 }
265
266 /**
267 * Automatically number headings?
268 * @param bool|null $x New value (null is no change)
269 * @return bool Old value
270 */
271 public function setNumberHeadings( $x ) {
272 return $this->setOptionLegacy( 'numberheadings', $x );
273 }
274
275 /**
276 * Allow inclusion of special pages?
277 * @return bool
278 */
279 public function getAllowSpecialInclusion() {
280 return $this->getOption( 'allowSpecialInclusion' );
281 }
282
283 /**
284 * Allow inclusion of special pages?
285 * @param bool|null $x New value (null is no change)
286 * @return bool Old value
287 */
288 public function setAllowSpecialInclusion( $x ) {
289 return $this->setOptionLegacy( 'allowSpecialInclusion', $x );
290 }
291
292 /**
293 * Use tidy to cleanup output HTML?
294 * @return bool
295 */
296 public function getTidy() {
297 return $this->getOption( 'tidy' );
298 }
299
300 /**
301 * Use tidy to cleanup output HTML?
302 * @param bool|null $x New value (null is no change)
303 * @return bool Old value
304 */
305 public function setTidy( $x ) {
306 return $this->setOptionLegacy( 'tidy', $x );
307 }
308
309 /**
310 * Parsing an interface message?
311 * @return bool
312 */
313 public function getInterfaceMessage() {
314 return $this->getOption( 'interfaceMessage' );
315 }
316
317 /**
318 * Parsing an interface message?
319 * @param bool|null $x New value (null is no change)
320 * @return bool Old value
321 */
322 public function setInterfaceMessage( $x ) {
323 return $this->setOptionLegacy( 'interfaceMessage', $x );
324 }
325
326 /**
327 * Target language for the parse
328 * @return Language|null
329 */
330 public function getTargetLanguage() {
331 return $this->getOption( 'targetLanguage' );
332 }
333
334 /**
335 * Target language for the parse
336 * @param Language|null $x New value
337 * @return Language|null Old value
338 */
339 public function setTargetLanguage( $x ) {
340 return $this->setOption( 'targetLanguage', $x );
341 }
342
343 /**
344 * Maximum size of template expansions, in bytes
345 * @return int
346 */
347 public function getMaxIncludeSize() {
348 return $this->getOption( 'maxIncludeSize' );
349 }
350
351 /**
352 * Maximum size of template expansions, in bytes
353 * @param int|null $x New value (null is no change)
354 * @return int Old value
355 */
356 public function setMaxIncludeSize( $x ) {
357 return $this->setOptionLegacy( 'maxIncludeSize', $x );
358 }
359
360 /**
361 * Maximum number of nodes touched by PPFrame::expand()
362 * @return int
363 */
364 public function getMaxPPNodeCount() {
365 return $this->getOption( 'maxPPNodeCount' );
366 }
367
368 /**
369 * Maximum number of nodes touched by PPFrame::expand()
370 * @param int|null $x New value (null is no change)
371 * @return int Old value
372 */
373 public function setMaxPPNodeCount( $x ) {
374 return $this->setOptionLegacy( 'maxPPNodeCount', $x );
375 }
376
377 /**
378 * Maximum number of nodes generated by Preprocessor::preprocessToObj()
379 * @return int
380 */
381 public function getMaxGeneratedPPNodeCount() {
382 return $this->getOption( 'maxGeneratedPPNodeCount' );
383 }
384
385 /**
386 * Maximum number of nodes generated by Preprocessor::preprocessToObj()
387 * @param int|null $x New value (null is no change)
388 * @return int
389 */
390 public function setMaxGeneratedPPNodeCount( $x ) {
391 return $this->setOptionLegacy( 'maxGeneratedPPNodeCount', $x );
392 }
393
394 /**
395 * Maximum recursion depth in PPFrame::expand()
396 * @return int
397 */
398 public function getMaxPPExpandDepth() {
399 return $this->getOption( 'maxPPExpandDepth' );
400 }
401
402 /**
403 * Maximum recursion depth for templates within templates
404 * @return int
405 */
406 public function getMaxTemplateDepth() {
407 return $this->getOption( 'maxTemplateDepth' );
408 }
409
410 /**
411 * Maximum recursion depth for templates within templates
412 * @param int|null $x New value (null is no change)
413 * @return int Old value
414 */
415 public function setMaxTemplateDepth( $x ) {
416 return $this->setOptionLegacy( 'maxTemplateDepth', $x );
417 }
418
419 /**
420 * Maximum number of calls per parse to expensive parser functions
421 * @since 1.20
422 * @return int
423 */
424 public function getExpensiveParserFunctionLimit() {
425 return $this->getOption( 'expensiveParserFunctionLimit' );
426 }
427
428 /**
429 * Maximum number of calls per parse to expensive parser functions
430 * @since 1.20
431 * @param int|null $x New value (null is no change)
432 * @return int Old value
433 */
434 public function setExpensiveParserFunctionLimit( $x ) {
435 return $this->setOptionLegacy( 'expensiveParserFunctionLimit', $x );
436 }
437
438 /**
439 * Remove HTML comments
440 * @warning Only applies to preprocess operations
441 * @return bool
442 */
443 public function getRemoveComments() {
444 return $this->getOption( 'removeComments' );
445 }
446
447 /**
448 * Remove HTML comments
449 * @warning Only applies to preprocess operations
450 * @param bool|null $x New value (null is no change)
451 * @return bool Old value
452 */
453 public function setRemoveComments( $x ) {
454 return $this->setOptionLegacy( 'removeComments', $x );
455 }
456
457 /**
458 * Enable limit report in an HTML comment on output
459 * @return bool
460 */
461 public function getEnableLimitReport() {
462 return $this->getOption( 'enableLimitReport' );
463 }
464
465 /**
466 * Enable limit report in an HTML comment on output
467 * @param bool|null $x New value (null is no change)
468 * @return bool Old value
469 */
470 public function enableLimitReport( $x = true ) {
471 return $this->setOptionLegacy( 'enableLimitReport', $x );
472 }
473
474 /**
475 * Clean up signature texts?
476 * @see Parser::cleanSig
477 * @return bool
478 */
479 public function getCleanSignatures() {
480 return $this->getOption( 'cleanSignatures' );
481 }
482
483 /**
484 * Clean up signature texts?
485 * @see Parser::cleanSig
486 * @param bool|null $x New value (null is no change)
487 * @return bool Old value
488 */
489 public function setCleanSignatures( $x ) {
490 return $this->setOptionLegacy( 'cleanSignatures', $x );
491 }
492
493 /**
494 * Target attribute for external links
495 * @return string
496 */
497 public function getExternalLinkTarget() {
498 return $this->getOption( 'externalLinkTarget' );
499 }
500
501 /**
502 * Target attribute for external links
503 * @param string|null $x New value (null is no change)
504 * @return string Old value
505 */
506 public function setExternalLinkTarget( $x ) {
507 return $this->setOptionLegacy( 'externalLinkTarget', $x );
508 }
509
510 /**
511 * Whether content conversion should be disabled
512 * @return bool
513 */
514 public function getDisableContentConversion() {
515 return $this->getOption( 'disableContentConversion' );
516 }
517
518 /**
519 * Whether content conversion should be disabled
520 * @param bool|null $x New value (null is no change)
521 * @return bool Old value
522 */
523 public function disableContentConversion( $x = true ) {
524 return $this->setOptionLegacy( 'disableContentConversion', $x );
525 }
526
527 /**
528 * Whether title conversion should be disabled
529 * @return bool
530 */
531 public function getDisableTitleConversion() {
532 return $this->getOption( 'disableTitleConversion' );
533 }
534
535 /**
536 * Whether title conversion should be disabled
537 * @param bool|null $x New value (null is no change)
538 * @return bool Old value
539 */
540 public function disableTitleConversion( $x = true ) {
541 return $this->setOptionLegacy( 'disableTitleConversion', $x );
542 }
543
544 /**
545 * Thumb size preferred by the user.
546 * @return int
547 */
548 public function getThumbSize() {
549 return $this->getOption( 'thumbsize' );
550 }
551
552 /**
553 * Thumb size preferred by the user.
554 * @param int|null $x New value (null is no change)
555 * @return int Old value
556 */
557 public function setThumbSize( $x ) {
558 return $this->setOptionLegacy( 'thumbsize', $x );
559 }
560
561 /**
562 * Thumb size preferred by the user.
563 * @return int
564 */
565 public function getStubThreshold() {
566 return $this->getOption( 'stubthreshold' );
567 }
568
569 /**
570 * Thumb size preferred by the user.
571 * @param int|null $x New value (null is no change)
572 * @return int Old value
573 */
574 public function setStubThreshold( $x ) {
575 return $this->setOptionLegacy( 'stubthreshold', $x );
576 }
577
578 /**
579 * Parsing the page for a "preview" operation?
580 * @return bool
581 */
582 public function getIsPreview() {
583 return $this->getOption( 'isPreview' );
584 }
585
586 /**
587 * Parsing the page for a "preview" operation?
588 * @param bool|null $x New value (null is no change)
589 * @return bool Old value
590 */
591 public function setIsPreview( $x ) {
592 return $this->setOptionLegacy( 'isPreview', $x );
593 }
594
595 /**
596 * Parsing the page for a "preview" operation on a single section?
597 * @return bool
598 */
599 public function getIsSectionPreview() {
600 return $this->getOption( 'isSectionPreview' );
601 }
602
603 /**
604 * Parsing the page for a "preview" operation on a single section?
605 * @param bool|null $x New value (null is no change)
606 * @return bool Old value
607 */
608 public function setIsSectionPreview( $x ) {
609 return $this->setOptionLegacy( 'isSectionPreview', $x );
610 }
611
612 /**
613 * Parsing the printable version of the page?
614 * @return bool
615 */
616 public function getIsPrintable() {
617 return $this->getOption( 'printable' );
618 }
619
620 /**
621 * Parsing the printable version of the page?
622 * @param bool|null $x New value (null is no change)
623 * @return bool Old value
624 */
625 public function setIsPrintable( $x ) {
626 return $this->setOptionLegacy( 'printable', $x );
627 }
628
629 /**
630 * Transform wiki markup when saving the page?
631 * @return bool
632 */
633 public function getPreSaveTransform() {
634 return $this->getOption( 'preSaveTransform' );
635 }
636
637 /**
638 * Transform wiki markup when saving the page?
639 * @param bool|null $x New value (null is no change)
640 * @return bool Old value
641 */
642 public function setPreSaveTransform( $x ) {
643 return $this->setOptionLegacy( 'preSaveTransform', $x );
644 }
645
646 /**
647 * Date format index
648 * @return string
649 */
650 public function getDateFormat() {
651 return $this->getOption( 'dateformat' );
652 }
653
654 /**
655 * Lazy initializer for dateFormat
656 * @param ParserOptions $popt
657 * @return string
658 */
659 private static function initDateFormat( ParserOptions $popt ) {
660 return $popt->mUser->getDatePreference();
661 }
662
663 /**
664 * Date format index
665 * @param string|null $x New value (null is no change)
666 * @return string Old value
667 */
668 public function setDateFormat( $x ) {
669 return $this->setOptionLegacy( 'dateformat', $x );
670 }
671
672 /**
673 * Get the user language used by the parser for this page and split the parser cache.
674 *
675 * @warning Calling this causes the parser cache to be fragmented by user language!
676 * To avoid cache fragmentation, output should not depend on the user language.
677 * Use Parser::getFunctionLang() or Parser::getTargetLanguage() instead!
678 *
679 * @note This function will trigger a cache fragmentation by recording the
680 * 'userlang' option, see optionUsed(). This is done to avoid cache pollution
681 * when the page is rendered based on the language of the user.
682 *
683 * @note When saving, this will return the default language instead of the user's.
684 * {{int: }} uses this which used to produce inconsistent link tables (T16404).
685 *
686 * @return Language
687 * @since 1.19
688 */
689 public function getUserLangObj() {
690 return $this->getOption( 'userlang' );
691 }
692
693 /**
694 * Same as getUserLangObj() but returns a string instead.
695 *
696 * @warning Calling this causes the parser cache to be fragmented by user language!
697 * To avoid cache fragmentation, output should not depend on the user language.
698 * Use Parser::getFunctionLang() or Parser::getTargetLanguage() instead!
699 *
700 * @see getUserLangObj()
701 *
702 * @return string Language code
703 * @since 1.17
704 */
705 public function getUserLang() {
706 return $this->getUserLangObj()->getCode();
707 }
708
709 /**
710 * Set the user language used by the parser for this page and split the parser cache.
711 * @param string|Language $x New value
712 * @return Language Old value
713 */
714 public function setUserLang( $x ) {
715 if ( is_string( $x ) ) {
716 $x = Language::factory( $x );
717 }
718
719 return $this->setOptionLegacy( 'userlang', $x );
720 }
721
722 /**
723 * Are magic ISBN links enabled?
724 * @since 1.28
725 * @return bool
726 */
727 public function getMagicISBNLinks() {
728 return $this->getOption( 'magicISBNLinks' );
729 }
730
731 /**
732 * Are magic PMID links enabled?
733 * @since 1.28
734 * @return bool
735 */
736 public function getMagicPMIDLinks() {
737 return $this->getOption( 'magicPMIDLinks' );
738 }
739
740 /**
741 * Are magic RFC links enabled?
742 * @since 1.28
743 * @return bool
744 */
745 public function getMagicRFCLinks() {
746 return $this->getOption( 'magicRFCLinks' );
747 }
748
749 /**
750 * If the wiki is configured to allow raw html ($wgRawHtml = true)
751 * is it allowed in the specific case of parsing this page.
752 *
753 * This is meant to disable unsafe parser tags in cases where
754 * a malicious user may control the input to the parser.
755 *
756 * @note This is expected to be true for normal pages even if the
757 * wiki has $wgRawHtml disabled in general. The setting only
758 * signifies that raw html would be unsafe in the current context
759 * provided that raw html is allowed at all.
760 * @since 1.29
761 * @return bool
762 */
763 public function getAllowUnsafeRawHtml() {
764 return $this->getOption( 'allowUnsafeRawHtml' );
765 }
766
767 /**
768 * If the wiki is configured to allow raw html ($wgRawHtml = true)
769 * is it allowed in the specific case of parsing this page.
770 * @see self::getAllowUnsafeRawHtml()
771 * @since 1.29
772 * @param bool|null $x Value to set or null to get current value
773 * @return bool Current value for allowUnsafeRawHtml
774 */
775 public function setAllowUnsafeRawHtml( $x ) {
776 return $this->setOptionLegacy( 'allowUnsafeRawHtml', $x );
777 }
778
779 /**
780 * Class to use to wrap output from Parser::parse()
781 * @since 1.30
782 * @return string|bool
783 */
784 public function getWrapOutputClass() {
785 return $this->getOption( 'wrapclass' );
786 }
787
788 /**
789 * CSS class to use to wrap output from Parser::parse()
790 * @since 1.30
791 * @param string $className Class name to use for wrapping.
792 * Passing false to indicate "no wrapping" was deprecated in MediaWiki 1.31.
793 * @return string|bool Current value
794 */
795 public function setWrapOutputClass( $className ) {
796 if ( $className === true ) { // DWIM, they probably want the default class name
797 $className = 'mw-parser-output';
798 }
799 if ( $className === false ) {
800 wfDeprecated( __METHOD__ . '( false )', '1.31' );
801 }
802 return $this->setOption( 'wrapclass', $className );
803 }
804
805 /**
806 * Callback for current revision fetching; first argument to call_user_func().
807 * @since 1.24
808 * @return callable
809 */
810 public function getCurrentRevisionCallback() {
811 return $this->getOption( 'currentRevisionCallback' );
812 }
813
814 /**
815 * Callback for current revision fetching; first argument to call_user_func().
816 * @since 1.24
817 * @param callable|null $x New value (null is no change)
818 * @return callable Old value
819 */
820 public function setCurrentRevisionCallback( $x ) {
821 return $this->setOptionLegacy( 'currentRevisionCallback', $x );
822 }
823
824 /**
825 * Callback for template fetching; first argument to call_user_func().
826 * @return callable
827 */
828 public function getTemplateCallback() {
829 return $this->getOption( 'templateCallback' );
830 }
831
832 /**
833 * Callback for template fetching; first argument to call_user_func().
834 * @param callable|null $x New value (null is no change)
835 * @return callable Old value
836 */
837 public function setTemplateCallback( $x ) {
838 return $this->setOptionLegacy( 'templateCallback', $x );
839 }
840
841 /**
842 * A guess for {{REVISIONID}}, calculated using the callback provided via
843 * setSpeculativeRevIdCallback(). For consistency, the value will be calculated upon the
844 * first call of this method, and re-used for subsequent calls.
845 *
846 * If no callback was defined via setSpeculativeRevIdCallback(), this method will return false.
847 *
848 * @since 1.32
849 * @return int|false
850 */
851 public function getSpeculativeRevId() {
852 return $this->getOption( 'speculativeRevId' );
853 }
854
855 /**
856 * A guess for {{PAGEID}}, calculated using the callback provided via
857 * setSpeculativeRevPageCallback(). For consistency, the value will be calculated upon the
858 * first call of this method, and re-used for subsequent calls.
859 *
860 * If no callback was defined via setSpeculativePageIdCallback(), this method will return false.
861 *
862 * @since 1.34
863 * @return int|false
864 */
865 public function getSpeculativePageId() {
866 return $this->getOption( 'speculativePageId' );
867 }
868
869 /**
870 * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativeRevId().
871 *
872 * @param ParserOptions $popt
873 * @return int|false
874 */
875 private static function initSpeculativeRevId( ParserOptions $popt ) {
876 $cb = $popt->getOption( 'speculativeRevIdCallback' );
877 $id = $cb ? $cb() : null;
878
879 // returning null would result in this being re-called every access
880 return $id ?? false;
881 }
882
883 /**
884 * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativePageId().
885 *
886 * @param ParserOptions $popt
887 * @return int|false
888 */
889 private static function initSpeculativePageId( ParserOptions $popt ) {
890 $cb = $popt->getOption( 'speculativePageIdCallback' );
891 $id = $cb ? $cb() : null;
892
893 // returning null would result in this being re-called every access
894 return $id ?? false;
895 }
896
897 /**
898 * Callback to generate a guess for {{REVISIONID}}
899 * @param callable|null $x New value
900 * @return callable|null Old value
901 * @since 1.28
902 */
903 public function setSpeculativeRevIdCallback( $x ) {
904 $this->setOption( 'speculativeRevId', null ); // reset
905 return $this->setOption( 'speculativeRevIdCallback', $x );
906 }
907
908 /**
909 * Callback to generate a guess for {{PAGEID}}
910 * @param callable|null $x New value
911 * @return callable|null Old value
912 * @since 1.34
913 */
914 public function setSpeculativePageIdCallback( $x ) {
915 $this->setOption( 'speculativePageId', null ); // reset
916 return $this->setOption( 'speculativePageIdCallback', $x );
917 }
918
919 /**
920 * Timestamp used for {{CURRENTDAY}} etc.
921 * @return string TS_MW timestamp
922 */
923 public function getTimestamp() {
924 if ( !isset( $this->mTimestamp ) ) {
925 $this->mTimestamp = wfTimestampNow();
926 }
927 return $this->mTimestamp;
928 }
929
930 /**
931 * Timestamp used for {{CURRENTDAY}} etc.
932 * @param string|null $x New value (null is no change)
933 * @return string Old value
934 */
935 public function setTimestamp( $x ) {
936 return wfSetVar( $this->mTimestamp, $x );
937 }
938
939 /**
940 * Set the redirect target.
941 *
942 * Note that setting or changing this does not *make* the page a redirect
943 * or change its target, it merely records the information for reference
944 * during the parse.
945 *
946 * @since 1.24
947 * @param Title|null $title
948 */
949 function setRedirectTarget( $title ) {
950 $this->redirectTarget = $title;
951 }
952
953 /**
954 * Get the previously-set redirect target.
955 *
956 * @since 1.24
957 * @return Title|null
958 */
959 function getRedirectTarget() {
960 return $this->redirectTarget;
961 }
962
963 /**
964 * Extra key that should be present in the parser cache key.
965 * @warning Consider registering your additional options with the
966 * ParserOptionsRegister hook instead of using this method.
967 * @param string $key
968 */
969 public function addExtraKey( $key ) {
970 $this->mExtraKey .= '!' . $key;
971 }
972
973 /**
974 * Current user
975 * @return User
976 */
977 public function getUser() {
978 return $this->mUser;
979 }
980
981 /**
982 * @warning For interaction with the parser cache, use
983 * WikiPage::makeParserOptions() or ParserOptions::newCanonical() instead.
984 * @param User|null $user
985 * @param Language|null $lang
986 */
987 public function __construct( $user = null, $lang = null ) {
988 if ( $user === null ) {
989 global $wgUser;
990 if ( $wgUser === null ) {
991 $user = new User;
992 } else {
993 $user = $wgUser;
994 }
995 }
996 if ( $lang === null ) {
997 global $wgLang;
998 if ( !StubObject::isRealObject( $wgLang ) ) {
999 $wgLang->_unstub();
1000 }
1001 $lang = $wgLang;
1002 }
1003 $this->initialiseFromUser( $user, $lang );
1004 }
1005
1006 /**
1007 * Get a ParserOptions object for an anonymous user
1008 * @warning For interaction with the parser cache, use
1009 * WikiPage::makeParserOptions() or ParserOptions::newCanonical() instead.
1010 * @since 1.27
1011 * @return ParserOptions
1012 */
1013 public static function newFromAnon() {
1014 return new ParserOptions( new User,
1015 MediaWikiServices::getInstance()->getContentLanguage() );
1016 }
1017
1018 /**
1019 * Get a ParserOptions object from a given user.
1020 * Language will be taken from $wgLang.
1021 *
1022 * @warning For interaction with the parser cache, use
1023 * WikiPage::makeParserOptions() or ParserOptions::newCanonical() instead.
1024 * @param User $user
1025 * @return ParserOptions
1026 */
1027 public static function newFromUser( $user ) {
1028 return new ParserOptions( $user );
1029 }
1030
1031 /**
1032 * Get a ParserOptions object from a given user and language
1033 *
1034 * @warning For interaction with the parser cache, use
1035 * WikiPage::makeParserOptions() or ParserOptions::newCanonical() instead.
1036 * @param User $user
1037 * @param Language $lang
1038 * @return ParserOptions
1039 */
1040 public static function newFromUserAndLang( User $user, Language $lang ) {
1041 return new ParserOptions( $user, $lang );
1042 }
1043
1044 /**
1045 * Get a ParserOptions object from a IContextSource object
1046 *
1047 * @warning For interaction with the parser cache, use
1048 * WikiPage::makeParserOptions() or ParserOptions::newCanonical() instead.
1049 * @param IContextSource $context
1050 * @return ParserOptions
1051 */
1052 public static function newFromContext( IContextSource $context ) {
1053 return new ParserOptions( $context->getUser(), $context->getLanguage() );
1054 }
1055
1056 /**
1057 * Creates a "canonical" ParserOptions object
1058 *
1059 * For historical reasons, certain options have default values that are
1060 * different from the canonical values used for caching.
1061 *
1062 * @since 1.30
1063 * @since 1.32 Added string and IContextSource as options for the first parameter
1064 * @param IContextSource|string|User|null $context
1065 * - If an IContextSource, the options are initialized based on the source's User and Language.
1066 * - If the string 'canonical', the options are initialized with an anonymous user and
1067 * the content language.
1068 * - If a User or null, the options are initialized for that User (or $wgUser if null).
1069 * 'userlang' is taken from the $userLang parameter, defaulting to $wgLang if that is null.
1070 * @param Language|StubObject|null $userLang (see above)
1071 * @return ParserOptions
1072 */
1073 public static function newCanonical( $context = null, $userLang = null ) {
1074 if ( $context instanceof IContextSource ) {
1075 $ret = self::newFromContext( $context );
1076 } elseif ( $context === 'canonical' ) {
1077 $ret = self::newFromAnon();
1078 } elseif ( $context instanceof User || $context === null ) {
1079 $ret = new self( $context, $userLang );
1080 } else {
1081 throw new InvalidArgumentException(
1082 '$context must be an IContextSource, the string "canonical", a User, or null'
1083 );
1084 }
1085
1086 foreach ( self::getCanonicalOverrides() as $k => $v ) {
1087 $ret->setOption( $k, $v );
1088 }
1089 return $ret;
1090 }
1091
1092 /**
1093 * Get default option values
1094 * @warning If you change the default for an existing option (unless it's
1095 * being overridden by self::getCanonicalOverrides()), all existing parser
1096 * cache entries will be invalid. To avoid bugs, you'll need to handle
1097 * that somehow (e.g. with the RejectParserCacheValue hook) because
1098 * MediaWiki won't do it for you.
1099 * @return array
1100 */
1101 private static function getDefaults() {
1102 global $wgInterwikiMagic, $wgAllowExternalImages,
1103 $wgAllowExternalImagesFrom, $wgEnableImageWhitelist, $wgAllowSpecialInclusion,
1104 $wgMaxArticleSize, $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth,
1105 $wgCleanSignatures, $wgExternalLinkTarget, $wgExpensiveParserFunctionLimit,
1106 $wgMaxGeneratedPPNodeCount, $wgDisableLangConversion, $wgDisableTitleConversion,
1107 $wgEnableMagicLinks;
1108
1109 if ( self::$defaults === null ) {
1110 // *UPDATE* ParserOptions::matches() if any of this changes as needed
1111 self::$defaults = [
1112 'dateformat' => null,
1113 'tidy' => true,
1114 'interfaceMessage' => false,
1115 'targetLanguage' => null,
1116 'removeComments' => true,
1117 'enableLimitReport' => false,
1118 'preSaveTransform' => true,
1119 'isPreview' => false,
1120 'isSectionPreview' => false,
1121 'printable' => false,
1122 'allowUnsafeRawHtml' => true,
1123 'wrapclass' => 'mw-parser-output',
1124 'currentRevisionCallback' => [ Parser::class, 'statelessFetchRevision' ],
1125 'templateCallback' => [ Parser::class, 'statelessFetchTemplate' ],
1126 'speculativeRevIdCallback' => null,
1127 'speculativeRevId' => null,
1128 'speculativePageIdCallback' => null,
1129 'speculativePageId' => null,
1130 ];
1131
1132 Hooks::run( 'ParserOptionsRegister', [
1133 &self::$defaults,
1134 &self::$inCacheKey,
1135 &self::$lazyOptions,
1136 ] );
1137
1138 ksort( self::$inCacheKey );
1139 }
1140
1141 // Unit tests depend on being able to modify the globals at will
1142 return self::$defaults + [
1143 'interwikiMagic' => $wgInterwikiMagic,
1144 'allowExternalImages' => $wgAllowExternalImages,
1145 'allowExternalImagesFrom' => $wgAllowExternalImagesFrom,
1146 'enableImageWhitelist' => $wgEnableImageWhitelist,
1147 'allowSpecialInclusion' => $wgAllowSpecialInclusion,
1148 'maxIncludeSize' => $wgMaxArticleSize * 1024,
1149 'maxPPNodeCount' => $wgMaxPPNodeCount,
1150 'maxGeneratedPPNodeCount' => $wgMaxGeneratedPPNodeCount,
1151 'maxPPExpandDepth' => $wgMaxPPExpandDepth,
1152 'maxTemplateDepth' => $wgMaxTemplateDepth,
1153 'expensiveParserFunctionLimit' => $wgExpensiveParserFunctionLimit,
1154 'externalLinkTarget' => $wgExternalLinkTarget,
1155 'cleanSignatures' => $wgCleanSignatures,
1156 'disableContentConversion' => $wgDisableLangConversion,
1157 'disableTitleConversion' => $wgDisableLangConversion || $wgDisableTitleConversion,
1158 'magicISBNLinks' => $wgEnableMagicLinks['ISBN'],
1159 'magicPMIDLinks' => $wgEnableMagicLinks['PMID'],
1160 'magicRFCLinks' => $wgEnableMagicLinks['RFC'],
1161 'numberheadings' => User::getDefaultOption( 'numberheadings' ),
1162 'thumbsize' => User::getDefaultOption( 'thumbsize' ),
1163 'stubthreshold' => 0,
1164 'userlang' => MediaWikiServices::getInstance()->getContentLanguage(),
1165 ];
1166 }
1167
1168 /**
1169 * Get "canonical" non-default option values
1170 * @see self::newCanonical
1171 * @warning If you change the override for an existing option, all existing
1172 * parser cache entries will be invalid. To avoid bugs, you'll need to
1173 * handle that somehow (e.g. with the RejectParserCacheValue hook) because
1174 * MediaWiki won't do it for you.
1175 * @return array
1176 */
1177 private static function getCanonicalOverrides() {
1178 global $wgEnableParserLimitReporting;
1179
1180 return [
1181 'enableLimitReport' => $wgEnableParserLimitReporting,
1182 ];
1183 }
1184
1185 /**
1186 * Get user options
1187 *
1188 * @param User $user
1189 * @param Language $lang
1190 */
1191 private function initialiseFromUser( $user, $lang ) {
1192 $this->options = self::getDefaults();
1193
1194 $this->mUser = $user;
1195 $this->options['numberheadings'] = $user->getOption( 'numberheadings' );
1196 $this->options['thumbsize'] = $user->getOption( 'thumbsize' );
1197 $this->options['stubthreshold'] = $user->getStubThreshold();
1198 $this->options['userlang'] = $lang;
1199 }
1200
1201 /**
1202 * Check if these options match that of another options set
1203 *
1204 * This ignores report limit settings that only affect HTML comments
1205 *
1206 * @param ParserOptions $other
1207 * @return bool
1208 * @since 1.25
1209 */
1210 public function matches( ParserOptions $other ) {
1211 // Compare most options
1212 $options = array_keys( $this->options );
1213 $options = array_diff( $options, [
1214 'enableLimitReport', // only affects HTML comments
1215 ] );
1216 foreach ( $options as $option ) {
1217 // Resolve any lazy options
1218 $this->lazyLoadOption( $option );
1219 $other->lazyLoadOption( $option );
1220
1221 $o1 = $this->optionToString( $this->options[$option] );
1222 $o2 = $this->optionToString( $other->options[$option] );
1223 if ( $o1 !== $o2 ) {
1224 return false;
1225 }
1226 }
1227
1228 // Compare most other fields
1229 $fields = array_keys( get_class_vars( __CLASS__ ) );
1230 $fields = array_diff( $fields, [
1231 'defaults', // static
1232 'lazyOptions', // static
1233 'inCacheKey', // static
1234 'options', // Already checked above
1235 'onAccessCallback', // only used for ParserOutput option tracking
1236 ] );
1237 foreach ( $fields as $field ) {
1238 if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) {
1239 return false;
1240 }
1241 }
1242
1243 return true;
1244 }
1245
1246 /**
1247 * @param ParserOptions $other
1248 * @return bool Whether the cache key relevant options match those of $other
1249 * @since 1.33
1250 */
1251 public function matchesForCacheKey( ParserOptions $other ) {
1252 foreach ( self::allCacheVaryingOptions() as $option ) {
1253 // Populate any lazy options
1254 $this->lazyLoadOption( $option );
1255 $other->lazyLoadOption( $option );
1256
1257 $o1 = $this->optionToString( $this->options[$option] );
1258 $o2 = $this->optionToString( $other->options[$option] );
1259 if ( $o1 !== $o2 ) {
1260 return false;
1261 }
1262 }
1263
1264 return true;
1265 }
1266
1267 /**
1268 * Registers a callback for tracking which ParserOptions which are used.
1269 * This is a private API with the parser.
1270 * @param callable $callback
1271 */
1272 public function registerWatcher( $callback ) {
1273 $this->onAccessCallback = $callback;
1274 }
1275
1276 /**
1277 * Called when an option is accessed.
1278 * Calls the watcher that was set using registerWatcher().
1279 * Typically, the watcher callback is ParserOutput::registerOption().
1280 * The information registered that way will be used by ParserCache::save().
1281 *
1282 * @param string $optionName Name of the option
1283 */
1284 public function optionUsed( $optionName ) {
1285 if ( $this->onAccessCallback ) {
1286 call_user_func( $this->onAccessCallback, $optionName );
1287 }
1288 }
1289
1290 /**
1291 * Return all option keys that vary the options hash
1292 * @since 1.30
1293 * @return string[]
1294 */
1295 public static function allCacheVaryingOptions() {
1296 // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
1297 // already been called.
1298 if ( self::$defaults === null ) {
1299 self::getDefaults();
1300 }
1301 return array_keys( array_filter( self::$inCacheKey ) );
1302 }
1303
1304 /**
1305 * Convert an option to a string value
1306 * @param mixed $value
1307 * @return string
1308 */
1309 private function optionToString( $value ) {
1310 if ( $value === true ) {
1311 return '1';
1312 } elseif ( $value === false ) {
1313 return '0';
1314 } elseif ( $value === null ) {
1315 return '';
1316 } elseif ( $value instanceof Language ) {
1317 return $value->getCode();
1318 } elseif ( is_array( $value ) ) {
1319 return '[' . implode( ',', array_map( [ $this, 'optionToString' ], $value ) ) . ']';
1320 } else {
1321 return (string)$value;
1322 }
1323 }
1324
1325 /**
1326 * Generate a hash string with the values set on these ParserOptions
1327 * for the keys given in the array.
1328 * This will be used as part of the hash key for the parser cache,
1329 * so users sharing the options with vary for the same page share
1330 * the same cached data safely.
1331 *
1332 * @since 1.17
1333 * @param string[] $forOptions
1334 * @param Title|null $title Used to get the content language of the page (since r97636)
1335 * @return string Page rendering hash
1336 */
1337 public function optionsHash( $forOptions, $title = null ) {
1338 global $wgRenderHashAppend;
1339
1340 $inCacheKey = self::allCacheVaryingOptions();
1341
1342 // Resolve any lazy options
1343 $lazyOpts = array_intersect( $forOptions, $inCacheKey, array_keys( self::$lazyOptions ) );
1344 foreach ( $lazyOpts as $k ) {
1345 $this->lazyLoadOption( $k );
1346 }
1347
1348 $options = $this->options;
1349 $defaults = self::getCanonicalOverrides() + self::getDefaults();
1350
1351 // We only include used options with non-canonical values in the key
1352 // so adding a new option doesn't invalidate the entire parser cache.
1353 // The drawback to this is that changing the default value of an option
1354 // requires manual invalidation of existing cache entries, as mentioned
1355 // in the docs on the relevant methods and hooks.
1356 $values = [];
1357 foreach ( array_intersect( $inCacheKey, $forOptions ) as $option ) {
1358 $v = $this->optionToString( $options[$option] );
1359 $d = $this->optionToString( $defaults[$option] );
1360 if ( $v !== $d ) {
1361 $values[] = "$option=$v";
1362 }
1363 }
1364
1365 $confstr = $values ? implode( '!', $values ) : 'canonical';
1366
1367 // add in language specific options, if any
1368 // @todo FIXME: This is just a way of retrieving the url/user preferred variant
1369 if ( !is_null( $title ) ) {
1370 $confstr .= $title->getPageLanguage()->getExtraHashOptions();
1371 } else {
1372 $confstr .=
1373 MediaWikiServices::getInstance()->getContentLanguage()->getExtraHashOptions();
1374 }
1375
1376 $confstr .= $wgRenderHashAppend;
1377
1378 if ( $this->mExtraKey != '' ) {
1379 $confstr .= $this->mExtraKey;
1380 }
1381
1382 // Give a chance for extensions to modify the hash, if they have
1383 // extra options or other effects on the parser cache.
1384 Hooks::run( 'PageRenderingHash', [ &$confstr, $this->getUser(), &$forOptions ] );
1385
1386 // Make it a valid memcached key fragment
1387 $confstr = str_replace( ' ', '_', $confstr );
1388
1389 return $confstr;
1390 }
1391
1392 /**
1393 * Test whether these options are safe to cache
1394 * @since 1.30
1395 * @return bool
1396 */
1397 public function isSafeToCache() {
1398 $defaults = self::getCanonicalOverrides() + self::getDefaults();
1399 foreach ( $this->options as $option => $value ) {
1400 if ( empty( self::$inCacheKey[$option] ) ) {
1401 $v = $this->optionToString( $value );
1402 $d = $this->optionToString( $defaults[$option] );
1403 if ( $v !== $d ) {
1404 return false;
1405 }
1406 }
1407 }
1408 return true;
1409 }
1410
1411 /**
1412 * Sets a hook to force that a page exists, and sets a current revision callback to return
1413 * a revision with custom content when the current revision of the page is requested.
1414 *
1415 * @since 1.25
1416 * @param Title $title
1417 * @param Content $content
1418 * @param User $user The user that the fake revision is attributed to
1419 * @return ScopedCallback to unset the hook
1420 */
1421 public function setupFakeRevision( $title, $content, $user ) {
1422 $oldCallback = $this->setCurrentRevisionCallback(
1423 function (
1424 $titleToCheck, $parser = false ) use ( $title, $content, $user, &$oldCallback
1425 ) {
1426 if ( $titleToCheck->equals( $title ) ) {
1427 return new Revision( [
1428 'page' => $title->getArticleID(),
1429 'user_text' => $user->getName(),
1430 'user' => $user->getId(),
1431 'parent_id' => $title->getLatestRevID(),
1432 'title' => $title,
1433 'content' => $content
1434 ] );
1435 } else {
1436 return call_user_func( $oldCallback, $titleToCheck, $parser );
1437 }
1438 }
1439 );
1440
1441 global $wgHooks;
1442 $wgHooks['TitleExists'][] =
1443 function ( $titleToCheck, &$exists ) use ( $title ) {
1444 if ( $titleToCheck->equals( $title ) ) {
1445 $exists = true;
1446 }
1447 };
1448 end( $wgHooks['TitleExists'] );
1449 $key = key( $wgHooks['TitleExists'] );
1450 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1451 $linkCache->clearBadLink( $title->getPrefixedDBkey() );
1452 return new ScopedCallback( function () use ( $title, $key, $linkCache ) {
1453 global $wgHooks;
1454 unset( $wgHooks['TitleExists'][$key] );
1455 $linkCache->clearLink( $title );
1456 } );
1457 }
1458 }
1459
1460 /**
1461 * For really cool vim folding this needs to be at the end:
1462 * vim: foldmarker=@{,@} foldmethod=marker
1463 */