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