Merge "Add framework for file warnings"
[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 /**
25 * @brief Set options of the Parser
26 *
27 * All member variables are supposed to be private in theory, although in
28 * practice this is not the case.
29 *
30 * @ingroup Parser
31 */
32 class ParserOptions {
33
34 /**
35 * Interlanguage links are removed and returned in an array
36 */
37 public $mInterwikiMagic;
38
39 /**
40 * Allow external images inline?
41 */
42 public $mAllowExternalImages;
43
44 /**
45 * If not, any exception?
46 */
47 public $mAllowExternalImagesFrom;
48
49 /**
50 * If not or it doesn't match, should we check an on-wiki whitelist?
51 */
52 public $mEnableImageWhitelist;
53
54 /**
55 * Date format index
56 */
57 public $mDateFormat = null;
58
59 /**
60 * Create "edit section" links?
61 */
62 public $mEditSection = true;
63
64 /**
65 * Allow inclusion of special pages?
66 */
67 public $mAllowSpecialInclusion;
68
69 /**
70 * Use tidy to cleanup output HTML?
71 */
72 public $mTidy = false;
73
74 /**
75 * Which lang to call for PLURAL and GRAMMAR
76 */
77 public $mInterfaceMessage = false;
78
79 /**
80 * Overrides $mInterfaceMessage with arbitrary language
81 */
82 public $mTargetLanguage = null;
83
84 /**
85 * Maximum size of template expansions, in bytes
86 */
87 public $mMaxIncludeSize;
88
89 /**
90 * Maximum number of nodes touched by PPFrame::expand()
91 */
92 public $mMaxPPNodeCount;
93
94 /**
95 * Maximum number of nodes generated by Preprocessor::preprocessToObj()
96 */
97 public $mMaxGeneratedPPNodeCount;
98
99 /**
100 * Maximum recursion depth in PPFrame::expand()
101 */
102 public $mMaxPPExpandDepth;
103
104 /**
105 * Maximum recursion depth for templates within templates
106 */
107 public $mMaxTemplateDepth;
108
109 /**
110 * Maximum number of calls per parse to expensive parser functions
111 */
112 public $mExpensiveParserFunctionLimit;
113
114 /**
115 * Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS
116 */
117 public $mRemoveComments = true;
118
119 /**
120 * Callback for current revision fetching. Used as first argument to call_user_func().
121 */
122 public $mCurrentRevisionCallback =
123 array( 'Parser', 'statelessFetchRevision' );
124
125 /**
126 * Callback for template fetching. Used as first argument to call_user_func().
127 */
128 public $mTemplateCallback =
129 array( 'Parser', 'statelessFetchTemplate' );
130
131 /**
132 * Enable limit report in an HTML comment on output
133 */
134 public $mEnableLimitReport = false;
135
136 /**
137 * Timestamp used for {{CURRENTDAY}} etc.
138 */
139 public $mTimestamp;
140
141 /**
142 * Target attribute for external links
143 */
144 public $mExternalLinkTarget;
145
146 /**
147 * Clean up signature texts?
148 *
149 * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures
150 * 2) Substitute all transclusions
151 */
152 public $mCleanSignatures;
153
154 /**
155 * Transform wiki markup when saving the page?
156 */
157 public $mPreSaveTransform = true;
158
159 /**
160 * Whether content conversion should be disabled
161 */
162 public $mDisableContentConversion;
163
164 /**
165 * Whether title conversion should be disabled
166 */
167 public $mDisableTitleConversion;
168
169 /**
170 * Automatically number headings?
171 */
172 public $mNumberHeadings;
173
174 /**
175 * Thumb size preferred by the user.
176 */
177 public $mThumbSize;
178
179 /**
180 * Maximum article size of an article to be marked as "stub"
181 */
182 private $mStubThreshold;
183
184 /**
185 * Language object of the User language.
186 */
187 public $mUserLang;
188
189 /**
190 * @var User
191 * Stored user object
192 */
193 public $mUser;
194
195 /**
196 * Parsing the page for a "preview" operation?
197 */
198 public $mIsPreview = false;
199
200 /**
201 * Parsing the page for a "preview" operation on a single section?
202 */
203 public $mIsSectionPreview = false;
204
205 /**
206 * Parsing the printable version of the page?
207 */
208 public $mIsPrintable = false;
209
210 /**
211 * Extra key that should be present in the caching key.
212 */
213 public $mExtraKey = '';
214
215 /**
216 * Function to be called when an option is accessed.
217 */
218 protected $onAccessCallback = null;
219
220 /**
221 * If the page being parsed is a redirect, this should hold the redirect
222 * target.
223 * @var Title|null
224 */
225 private $redirectTarget = null;
226
227 public function getInterwikiMagic() {
228 return $this->mInterwikiMagic;
229 }
230
231 public function getAllowExternalImages() {
232 return $this->mAllowExternalImages;
233 }
234
235 public function getAllowExternalImagesFrom() {
236 return $this->mAllowExternalImagesFrom;
237 }
238
239 public function getEnableImageWhitelist() {
240 return $this->mEnableImageWhitelist;
241 }
242
243 public function getEditSection() {
244 return $this->mEditSection;
245 }
246
247 public function getNumberHeadings() {
248 $this->optionUsed( 'numberheadings' );
249
250 return $this->mNumberHeadings;
251 }
252
253 public function getAllowSpecialInclusion() {
254 return $this->mAllowSpecialInclusion;
255 }
256
257 public function getTidy() {
258 return $this->mTidy;
259 }
260
261 public function getInterfaceMessage() {
262 return $this->mInterfaceMessage;
263 }
264
265 public function getTargetLanguage() {
266 return $this->mTargetLanguage;
267 }
268
269 public function getMaxIncludeSize() {
270 return $this->mMaxIncludeSize;
271 }
272
273 public function getMaxPPNodeCount() {
274 return $this->mMaxPPNodeCount;
275 }
276
277 public function getMaxGeneratedPPNodeCount() {
278 return $this->mMaxGeneratedPPNodeCount;
279 }
280
281 public function getMaxPPExpandDepth() {
282 return $this->mMaxPPExpandDepth;
283 }
284
285 public function getMaxTemplateDepth() {
286 return $this->mMaxTemplateDepth;
287 }
288
289 /* @since 1.20 */
290 public function getExpensiveParserFunctionLimit() {
291 return $this->mExpensiveParserFunctionLimit;
292 }
293
294 public function getRemoveComments() {
295 return $this->mRemoveComments;
296 }
297
298 /* @since 1.24 */
299 public function getCurrentRevisionCallback() {
300 return $this->mCurrentRevisionCallback;
301 }
302
303 public function getTemplateCallback() {
304 return $this->mTemplateCallback;
305 }
306
307 public function getEnableLimitReport() {
308 return $this->mEnableLimitReport;
309 }
310
311 public function getCleanSignatures() {
312 return $this->mCleanSignatures;
313 }
314
315 public function getExternalLinkTarget() {
316 return $this->mExternalLinkTarget;
317 }
318
319 public function getDisableContentConversion() {
320 return $this->mDisableContentConversion;
321 }
322
323 public function getDisableTitleConversion() {
324 return $this->mDisableTitleConversion;
325 }
326
327 public function getThumbSize() {
328 $this->optionUsed( 'thumbsize' );
329
330 return $this->mThumbSize;
331 }
332
333 public function getStubThreshold() {
334 $this->optionUsed( 'stubthreshold' );
335
336 return $this->mStubThreshold;
337 }
338
339 public function getIsPreview() {
340 return $this->mIsPreview;
341 }
342
343 public function getIsSectionPreview() {
344 return $this->mIsSectionPreview;
345 }
346
347 public function getIsPrintable() {
348 $this->optionUsed( 'printable' );
349
350 return $this->mIsPrintable;
351 }
352
353 public function getUser() {
354 return $this->mUser;
355 }
356
357 public function getPreSaveTransform() {
358 return $this->mPreSaveTransform;
359 }
360
361 public function getDateFormat() {
362 $this->optionUsed( 'dateformat' );
363 if ( !isset( $this->mDateFormat ) ) {
364 $this->mDateFormat = $this->mUser->getDatePreference();
365 }
366 return $this->mDateFormat;
367 }
368
369 public function getTimestamp() {
370 if ( !isset( $this->mTimestamp ) ) {
371 $this->mTimestamp = wfTimestampNow();
372 }
373 return $this->mTimestamp;
374 }
375
376 /**
377 * Get the user language used by the parser for this page.
378 *
379 * You shouldn't use this. Really. $parser->getFunctionLang() is all you need.
380 *
381 * To avoid side-effects where the page will be rendered based on the language
382 * of the user who last saved, this function will triger a cache fragmentation.
383 * Usage of this method is discouraged for that reason.
384 *
385 * When saving, this will return the default language instead of the user's.
386 *
387 * {{int: }} uses this which used to produce inconsistent link tables (bug 14404).
388 *
389 * @return Language
390 * @since 1.19
391 */
392 public function getUserLangObj() {
393 $this->optionUsed( 'userlang' );
394 return $this->mUserLang;
395 }
396
397 /**
398 * Same as getUserLangObj() but returns a string instead.
399 *
400 * @return string Language code
401 * @since 1.17
402 */
403 public function getUserLang() {
404 return $this->getUserLangObj()->getCode();
405 }
406
407 public function setInterwikiMagic( $x ) {
408 return wfSetVar( $this->mInterwikiMagic, $x );
409 }
410
411 public function setAllowExternalImages( $x ) {
412 return wfSetVar( $this->mAllowExternalImages, $x );
413 }
414
415 public function setAllowExternalImagesFrom( $x ) {
416 return wfSetVar( $this->mAllowExternalImagesFrom, $x );
417 }
418
419 public function setEnableImageWhitelist( $x ) {
420 return wfSetVar( $this->mEnableImageWhitelist, $x );
421 }
422
423 public function setDateFormat( $x ) {
424 return wfSetVar( $this->mDateFormat, $x );
425 }
426
427 public function setEditSection( $x ) {
428 return wfSetVar( $this->mEditSection, $x );
429 }
430
431 public function setNumberHeadings( $x ) {
432 return wfSetVar( $this->mNumberHeadings, $x );
433 }
434
435 public function setAllowSpecialInclusion( $x ) {
436 return wfSetVar( $this->mAllowSpecialInclusion, $x );
437 }
438
439 public function setTidy( $x ) {
440 return wfSetVar( $this->mTidy, $x );
441 }
442
443 public function setInterfaceMessage( $x ) {
444 return wfSetVar( $this->mInterfaceMessage, $x );
445 }
446
447 public function setTargetLanguage( $x ) {
448 return wfSetVar( $this->mTargetLanguage, $x, true );
449 }
450
451 public function setMaxIncludeSize( $x ) {
452 return wfSetVar( $this->mMaxIncludeSize, $x );
453 }
454
455 public function setMaxPPNodeCount( $x ) {
456 return wfSetVar( $this->mMaxPPNodeCount, $x );
457 }
458
459 public function setMaxGeneratedPPNodeCount( $x ) {
460 return wfSetVar( $this->mMaxGeneratedPPNodeCount, $x );
461 }
462
463 public function setMaxTemplateDepth( $x ) {
464 return wfSetVar( $this->mMaxTemplateDepth, $x );
465 }
466
467 /* @since 1.20 */
468 public function setExpensiveParserFunctionLimit( $x ) {
469 return wfSetVar( $this->mExpensiveParserFunctionLimit, $x );
470 }
471
472 public function setRemoveComments( $x ) {
473 return wfSetVar( $this->mRemoveComments, $x );
474 }
475
476 /* @since 1.24 */
477 public function setCurrentRevisionCallback( $x ) {
478 return wfSetVar( $this->mCurrentRevisionCallback, $x );
479 }
480
481 public function setTemplateCallback( $x ) {
482 return wfSetVar( $this->mTemplateCallback, $x );
483 }
484
485 public function enableLimitReport( $x = true ) {
486 return wfSetVar( $this->mEnableLimitReport, $x );
487 }
488
489 public function setTimestamp( $x ) {
490 return wfSetVar( $this->mTimestamp, $x );
491 }
492
493 public function setCleanSignatures( $x ) {
494 return wfSetVar( $this->mCleanSignatures, $x );
495 }
496
497 public function setExternalLinkTarget( $x ) {
498 return wfSetVar( $this->mExternalLinkTarget, $x );
499 }
500
501 public function disableContentConversion( $x = true ) {
502 return wfSetVar( $this->mDisableContentConversion, $x );
503 }
504
505 public function disableTitleConversion( $x = true ) {
506 return wfSetVar( $this->mDisableTitleConversion, $x );
507 }
508
509 public function setUserLang( $x ) {
510 if ( is_string( $x ) ) {
511 $x = Language::factory( $x );
512 }
513
514 return wfSetVar( $this->mUserLang, $x );
515 }
516
517 public function setThumbSize( $x ) {
518 return wfSetVar( $this->mThumbSize, $x );
519 }
520
521 public function setStubThreshold( $x ) {
522 return wfSetVar( $this->mStubThreshold, $x );
523 }
524
525 public function setPreSaveTransform( $x ) {
526 return wfSetVar( $this->mPreSaveTransform, $x );
527 }
528
529 public function setIsPreview( $x ) {
530 return wfSetVar( $this->mIsPreview, $x );
531 }
532
533 public function setIsSectionPreview( $x ) {
534 return wfSetVar( $this->mIsSectionPreview, $x );
535 }
536
537 public function setIsPrintable( $x ) {
538 return wfSetVar( $this->mIsPrintable, $x );
539 }
540
541 /**
542 * Set the redirect target.
543 *
544 * Note that setting or changing this does not *make* the page a redirect
545 * or change its target, it merely records the information for reference
546 * during the parse.
547 *
548 * @since 1.24
549 * @param Title|null $title
550 */
551 function setRedirectTarget( $title ) {
552 $this->redirectTarget = $title;
553 }
554
555 /**
556 * Get the previously-set redirect target.
557 *
558 * @since 1.24
559 * @return Title|null
560 */
561 function getRedirectTarget() {
562 return $this->redirectTarget;
563 }
564
565 /**
566 * Extra key that should be present in the parser cache key.
567 * @param string $key
568 */
569 public function addExtraKey( $key ) {
570 $this->mExtraKey .= '!' . $key;
571 }
572
573 /**
574 * Constructor
575 * @param User $user
576 * @param Language $lang
577 */
578 public function __construct( $user = null, $lang = null ) {
579 if ( $user === null ) {
580 global $wgUser;
581 if ( $wgUser === null ) {
582 $user = new User;
583 } else {
584 $user = $wgUser;
585 }
586 }
587 if ( $lang === null ) {
588 global $wgLang;
589 if ( !StubObject::isRealObject( $wgLang ) ) {
590 $wgLang->_unstub();
591 }
592 $lang = $wgLang;
593 }
594 $this->initialiseFromUser( $user, $lang );
595 }
596
597 /**
598 * Get a ParserOptions object from a given user.
599 * Language will be taken from $wgLang.
600 *
601 * @param User $user
602 * @return ParserOptions
603 */
604 public static function newFromUser( $user ) {
605 return new ParserOptions( $user );
606 }
607
608 /**
609 * Get a ParserOptions object from a given user and language
610 *
611 * @param User $user
612 * @param Language $lang
613 * @return ParserOptions
614 */
615 public static function newFromUserAndLang( User $user, Language $lang ) {
616 return new ParserOptions( $user, $lang );
617 }
618
619 /**
620 * Get a ParserOptions object from a IContextSource object
621 *
622 * @param IContextSource $context
623 * @return ParserOptions
624 */
625 public static function newFromContext( IContextSource $context ) {
626 return new ParserOptions( $context->getUser(), $context->getLanguage() );
627 }
628
629 /**
630 * Get user options
631 *
632 * @param User $user
633 * @param Language $lang
634 */
635 private function initialiseFromUser( $user, $lang ) {
636 global $wgInterwikiMagic, $wgAllowExternalImages,
637 $wgAllowExternalImagesFrom, $wgEnableImageWhitelist, $wgAllowSpecialInclusion,
638 $wgMaxArticleSize, $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth,
639 $wgCleanSignatures, $wgExternalLinkTarget, $wgExpensiveParserFunctionLimit,
640 $wgMaxGeneratedPPNodeCount, $wgDisableLangConversion, $wgDisableTitleConversion;
641
642 // *UPDATE* ParserOptions::matches() if any of this changes as needed
643 $this->mInterwikiMagic = $wgInterwikiMagic;
644 $this->mAllowExternalImages = $wgAllowExternalImages;
645 $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
646 $this->mEnableImageWhitelist = $wgEnableImageWhitelist;
647 $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion;
648 $this->mMaxIncludeSize = $wgMaxArticleSize * 1024;
649 $this->mMaxPPNodeCount = $wgMaxPPNodeCount;
650 $this->mMaxGeneratedPPNodeCount = $wgMaxGeneratedPPNodeCount;
651 $this->mMaxPPExpandDepth = $wgMaxPPExpandDepth;
652 $this->mMaxTemplateDepth = $wgMaxTemplateDepth;
653 $this->mExpensiveParserFunctionLimit = $wgExpensiveParserFunctionLimit;
654 $this->mCleanSignatures = $wgCleanSignatures;
655 $this->mExternalLinkTarget = $wgExternalLinkTarget;
656 $this->mDisableContentConversion = $wgDisableLangConversion;
657 $this->mDisableTitleConversion = $wgDisableLangConversion || $wgDisableTitleConversion;
658
659 $this->mUser = $user;
660 $this->mNumberHeadings = $user->getOption( 'numberheadings' );
661 $this->mThumbSize = $user->getOption( 'thumbsize' );
662 $this->mStubThreshold = $user->getStubThreshold();
663 $this->mUserLang = $lang;
664
665 }
666
667 /**
668 * Check if these options match that of another options set
669 *
670 * This ignores report limit settings that only affect HTML comments
671 *
672 * @return bool
673 * @since 1.25
674 */
675 public function matches( ParserOptions $other ) {
676 $fields = array_keys( get_class_vars( __CLASS__ ) );
677 $fields = array_diff( $fields, array(
678 'mEnableLimitReport', // only effects HTML comments
679 'onAccessCallback', // only used for ParserOutput option tracking
680 ) );
681 foreach ( $fields as $field ) {
682 if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) {
683 return false;
684 }
685 }
686 // Check the object and lazy-loaded options
687 return (
688 $this->mUserLang->getCode() === $other->mUserLang->getCode() &&
689 $this->getDateFormat() === $other->getDateFormat()
690 );
691 }
692
693 /**
694 * Registers a callback for tracking which ParserOptions which are used.
695 * This is a private API with the parser.
696 * @param callable $callback
697 */
698 public function registerWatcher( $callback ) {
699 $this->onAccessCallback = $callback;
700 }
701
702 /**
703 * Called when an option is accessed.
704 * @param string $optionName Name of the option
705 */
706 public function optionUsed( $optionName ) {
707 if ( $this->onAccessCallback ) {
708 call_user_func( $this->onAccessCallback, $optionName );
709 }
710 }
711
712 /**
713 * Returns the full array of options that would have been used by
714 * in 1.16.
715 * Used to get the old parser cache entries when available.
716 * @return array
717 */
718 public static function legacyOptions() {
719 return array(
720 'stubthreshold',
721 'numberheadings',
722 'userlang',
723 'thumbsize',
724 'editsection',
725 'printable'
726 );
727 }
728
729 /**
730 * Generate a hash string with the values set on these ParserOptions
731 * for the keys given in the array.
732 * This will be used as part of the hash key for the parser cache,
733 * so users sharing the options with vary for the same page share
734 * the same cached data safely.
735 *
736 * Extensions which require it should install 'PageRenderingHash' hook,
737 * which will give them a chance to modify this key based on their own
738 * settings.
739 *
740 * @since 1.17
741 * @param array $forOptions
742 * @param Title $title Used to get the content language of the page (since r97636)
743 * @return string Page rendering hash
744 */
745 public function optionsHash( $forOptions, $title = null ) {
746 global $wgRenderHashAppend;
747
748 // FIXME: Once the cache key is reorganized this argument
749 // can be dropped. It was used when the math extension was
750 // part of core.
751 $confstr = '*';
752
753 // Space assigned for the stubthreshold but unused
754 // since it disables the parser cache, its value will always
755 // be 0 when this function is called by parsercache.
756 if ( in_array( 'stubthreshold', $forOptions ) ) {
757 $confstr .= '!' . $this->mStubThreshold;
758 } else {
759 $confstr .= '!*';
760 }
761
762 if ( in_array( 'dateformat', $forOptions ) ) {
763 $confstr .= '!' . $this->getDateFormat();
764 }
765
766 if ( in_array( 'numberheadings', $forOptions ) ) {
767 $confstr .= '!' . ( $this->mNumberHeadings ? '1' : '' );
768 } else {
769 $confstr .= '!*';
770 }
771
772 if ( in_array( 'userlang', $forOptions ) ) {
773 $confstr .= '!' . $this->mUserLang->getCode();
774 } else {
775 $confstr .= '!*';
776 }
777
778 if ( in_array( 'thumbsize', $forOptions ) ) {
779 $confstr .= '!' . $this->mThumbSize;
780 } else {
781 $confstr .= '!*';
782 }
783
784 // add in language specific options, if any
785 // @todo FIXME: This is just a way of retrieving the url/user preferred variant
786 if ( !is_null( $title ) ) {
787 $confstr .= $title->getPageLanguage()->getExtraHashOptions();
788 } else {
789 global $wgContLang;
790 $confstr .= $wgContLang->getExtraHashOptions();
791 }
792
793 $confstr .= $wgRenderHashAppend;
794
795 if ( !in_array( 'editsection', $forOptions ) ) {
796 $confstr .= '!*';
797 } elseif ( !$this->mEditSection ) {
798 $confstr .= '!edit=0';
799 }
800
801 if ( $this->mIsPrintable && in_array( 'printable', $forOptions ) ) {
802 $confstr .= '!printable=1';
803 }
804
805 if ( $this->mExtraKey != '' ) {
806 $confstr .= $this->mExtraKey;
807 }
808
809 // Give a chance for extensions to modify the hash, if they have
810 // extra options or other effects on the parser cache.
811 Hooks::run( 'PageRenderingHash', array( &$confstr, $this->getUser(), &$forOptions ) );
812
813 // Make it a valid memcached key fragment
814 $confstr = str_replace( ' ', '_', $confstr );
815
816 return $confstr;
817 }
818
819 /**
820 * Sets a hook to force that a page exists, and sets a current revision callback to return a
821 * revision with custom content when the current revision of the page is requested.
822 *
823 * @since 1.25
824 * @param Title $title
825 * @param Content $content
826 * @param User $user The user that the fake revision is attributed to
827 * @return ScopedCallback to unset the hook
828 */
829 public function setupFakeRevision( $title, $content, $user ) {
830 $oldCallback = $this->setCurrentRevisionCallback( function ( $titleToCheck, $parser = false ) use ( $title, $content, $user, &$oldCallback ) {
831 if ( $titleToCheck->equals( $title ) ) {
832 return new Revision( array(
833 'page' => $title->getArticleID(),
834 'user_text' => $user->getName(),
835 'user' => $user->getId(),
836 'parent_id' => $title->getLatestRevId(),
837 'title' => $title,
838 'content' => $content
839 ) );
840 } else {
841 return call_user_func( $oldCallback, $titleToCheck, $parser );
842 }
843 } );
844 global $wgHooks;
845 $wgHooks['TitleExists'][] =
846 function ( $titleToCheck, &$exists ) use ( $title ) {
847 if ( $titleToCheck->equals( $title ) ) {
848 $exists = true;
849 }
850 };
851 end( $wgHooks['TitleExists'] );
852 $key = key( $wgHooks['TitleExists'] );
853 LinkCache::singleton()->clearBadLink( $title->getPrefixedDBkey() );
854 return new ScopedCallback( function () use ( $title, $key ) {
855 global $wgHooks;
856 unset( $wgHooks['TitleExists'][$key] );
857 LinkCache::singleton()->clearLink( $title );
858 } );
859 }
860 }