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