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