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