4ccb0d427477b17dbd6c4027224360ea43f1189c
[lhc/web/wiklou.git] / includes / OutputPage.php
1 <?php
2 /**
3 * Preparation for the final page rendering.
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 */
22
23 use MediaWiki\Linker\LinkTarget;
24 use MediaWiki\MediaWikiServices;
25 use MediaWiki\Session\SessionManager;
26 use Wikimedia\Rdbms\IResultWrapper;
27 use Wikimedia\RelPath;
28 use Wikimedia\WrappedString;
29 use Wikimedia\WrappedStringList;
30
31 /**
32 * This class should be covered by a general architecture document which does
33 * not exist as of January 2011. This is one of the Core classes and should
34 * be read at least once by any new developers.
35 *
36 * This class is used to prepare the final rendering. A skin is then
37 * applied to the output parameters (links, javascript, html, categories ...).
38 *
39 * @todo FIXME: Another class handles sending the whole page to the client.
40 *
41 * Some comments comes from a pairing session between Zak Greant and Antoine Musso
42 * in November 2010.
43 *
44 * @todo document
45 */
46 class OutputPage extends ContextSource {
47 /** @var array Should be private. Used with addMeta() which adds "<meta>" */
48 protected $mMetatags = [];
49
50 /** @var array */
51 protected $mLinktags = [];
52
53 /** @var bool */
54 protected $mCanonicalUrl = false;
55
56 /**
57 * @var string The contents of <h1> */
58 private $mPageTitle = '';
59
60 /**
61 * @var string The displayed title of the page. Different from page title
62 * if overridden by display title magic word or hooks. Can contain safe
63 * HTML. Different from page title which may contain messages such as
64 * "Editing X" which is displayed in h1. This can be used for other places
65 * where the page name is referred on the page.
66 */
67 private $displayTitle;
68
69 /**
70 * @var string Contains all of the "<body>" content. Should be private we
71 * got set/get accessors and the append() method.
72 */
73 public $mBodytext = '';
74
75 /** @var string Stores contents of "<title>" tag */
76 private $mHTMLtitle = '';
77
78 /**
79 * @var bool Is the displayed content related to the source of the
80 * corresponding wiki article.
81 */
82 private $mIsArticle = false;
83
84 /** @var bool Stores "article flag" toggle. */
85 private $mIsArticleRelated = true;
86
87 /** @var bool Is the content subject to copyright */
88 private $mHasCopyright = false;
89
90 /**
91 * @var bool We have to set isPrintable(). Some pages should
92 * never be printed (ex: redirections).
93 */
94 private $mPrintable = false;
95
96 /**
97 * @var array Contains the page subtitle. Special pages usually have some
98 * links here. Don't confuse with site subtitle added by skins.
99 */
100 private $mSubtitle = [];
101
102 /** @var string */
103 public $mRedirect = '';
104
105 /** @var int */
106 protected $mStatusCode;
107
108 /**
109 * @var string Used for sending cache control.
110 * The whole caching system should probably be moved into its own class.
111 */
112 protected $mLastModified = '';
113
114 /** @var array */
115 protected $mCategoryLinks = [];
116
117 /** @var array */
118 protected $mCategories = [
119 'hidden' => [],
120 'normal' => [],
121 ];
122
123 /** @var array */
124 protected $mIndicators = [];
125
126 /** @var array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page') */
127 private $mLanguageLinks = [];
128
129 /**
130 * Used for JavaScript (predates ResourceLoader)
131 * @todo We should split JS / CSS.
132 * mScripts content is inserted as is in "<head>" by Skin. This might
133 * contain either a link to a stylesheet or inline CSS.
134 */
135 private $mScripts = '';
136
137 /** @var string Inline CSS styles. Use addInlineStyle() sparingly */
138 protected $mInlineStyles = '';
139
140 /**
141 * @var string Used by skin template.
142 * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle );
143 */
144 public $mPageLinkTitle = '';
145
146 /** @var array Array of elements in "<head>". Parser might add its own headers! */
147 protected $mHeadItems = [];
148
149 /** @var array Additional <body> classes; there are also <body> classes from other sources */
150 protected $mAdditionalBodyClasses = [];
151
152 /** @var array */
153 protected $mModules = [];
154
155 /** @var array */
156 protected $mModuleStyles = [];
157
158 /** @var ResourceLoader */
159 protected $mResourceLoader;
160
161 /** @var ResourceLoaderClientHtml */
162 private $rlClient;
163
164 /** @var ResourceLoaderContext */
165 private $rlClientContext;
166
167 /** @var array */
168 private $rlExemptStyleModules;
169
170 /** @var array */
171 protected $mJsConfigVars = [];
172
173 /** @var array */
174 protected $mTemplateIds = [];
175
176 /** @var array */
177 protected $mImageTimeKeys = [];
178
179 /** @var string */
180 public $mRedirectCode = '';
181
182 protected $mFeedLinksAppendQuery = null;
183
184 /** @var array
185 * What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page?
186 * @see ResourceLoaderModule::$origin
187 * ResourceLoaderModule::ORIGIN_ALL is assumed unless overridden;
188 */
189 protected $mAllowedModules = [
190 ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
191 ];
192
193 /** @var bool Whether output is disabled. If this is true, the 'output' method will do nothing. */
194 protected $mDoNothing = false;
195
196 // Parser related.
197
198 /** @var int */
199 protected $mContainsNewMagic = 0;
200
201 /**
202 * lazy initialised, use parserOptions()
203 * @var ParserOptions
204 */
205 protected $mParserOptions = null;
206
207 /**
208 * Handles the Atom / RSS links.
209 * We probably only support Atom in 2011.
210 * @see $wgAdvertisedFeedTypes
211 */
212 private $mFeedLinks = [];
213
214 // Gwicke work on squid caching? Roughly from 2003.
215 protected $mEnableClientCache = true;
216
217 /** @var bool Flag if output should only contain the body of the article. */
218 private $mArticleBodyOnly = false;
219
220 /** @var bool */
221 protected $mNewSectionLink = false;
222
223 /** @var bool */
224 protected $mHideNewSectionLink = false;
225
226 /**
227 * @var bool Comes from the parser. This was probably made to load CSS/JS
228 * only if we had "<gallery>". Used directly in CategoryPage.php.
229 * Looks like ResourceLoader can replace this.
230 */
231 public $mNoGallery = false;
232
233 /** @var int Cache stuff. Looks like mEnableClientCache */
234 protected $mCdnMaxage = 0;
235 /** @var int Upper limit on mCdnMaxage */
236 protected $mCdnMaxageLimit = INF;
237
238 /**
239 * @var bool Controls if anti-clickjacking / frame-breaking headers will
240 * be sent. This should be done for pages where edit actions are possible.
241 * Setters: $this->preventClickjacking() and $this->allowClickjacking().
242 */
243 protected $mPreventClickjacking = true;
244
245 /** @var int To include the variable {{REVISIONID}} */
246 private $mRevisionId = null;
247
248 /** @var string */
249 private $mRevisionTimestamp = null;
250
251 /** @var array */
252 protected $mFileVersion = null;
253
254 /**
255 * @var array An array of stylesheet filenames (relative from skins path),
256 * with options for CSS media, IE conditions, and RTL/LTR direction.
257 * For internal use; add settings in the skin via $this->addStyle()
258 *
259 * Style again! This seems like a code duplication since we already have
260 * mStyles. This is what makes Open Source amazing.
261 */
262 protected $styles = [];
263
264 private $mIndexPolicy = 'index';
265 private $mFollowPolicy = 'follow';
266
267 /**
268 * @var array Headers that cause the cache to vary. Key is header name,
269 * value should always be null. (Value was an array of options for
270 * the `Key` header, which was deprecated in 1.32 and removed in 1.34.)
271 */
272 private $mVaryHeader = [
273 'Accept-Encoding' => null,
274 ];
275
276 /**
277 * If the current page was reached through a redirect, $mRedirectedFrom contains the Title
278 * of the redirect.
279 *
280 * @var Title
281 */
282 private $mRedirectedFrom = null;
283
284 /**
285 * Additional key => value data
286 */
287 private $mProperties = [];
288
289 /**
290 * @var string|null ResourceLoader target for load.php links. If null, will be omitted
291 */
292 private $mTarget = null;
293
294 /**
295 * @var bool Whether parser output contains a table of contents
296 */
297 private $mEnableTOC = false;
298
299 /**
300 * @var string|null The URL to send in a <link> element with rel=license
301 */
302 private $copyrightUrl;
303
304 /** @var array Profiling data */
305 private $limitReportJSData = [];
306
307 /** @var array Map Title to Content */
308 private $contentOverrides = [];
309
310 /** @var callable[] */
311 private $contentOverrideCallbacks = [];
312
313 /**
314 * Link: header contents
315 */
316 private $mLinkHeader = [];
317
318 /**
319 * @var string The nonce for Content-Security-Policy
320 */
321 private $CSPNonce;
322
323 /**
324 * @var array A cache of the names of the cookies that will influence the cache
325 */
326 private static $cacheVaryCookies = null;
327
328 /**
329 * Constructor for OutputPage. This should not be called directly.
330 * Instead a new RequestContext should be created and it will implicitly create
331 * a OutputPage tied to that context.
332 * @param IContextSource $context
333 */
334 function __construct( IContextSource $context ) {
335 $this->setContext( $context );
336 }
337
338 /**
339 * Redirect to $url rather than displaying the normal page
340 *
341 * @param string $url
342 * @param string $responsecode HTTP status code
343 */
344 public function redirect( $url, $responsecode = '302' ) {
345 # Strip newlines as a paranoia check for header injection in PHP<5.1.2
346 $this->mRedirect = str_replace( "\n", '', $url );
347 $this->mRedirectCode = $responsecode;
348 }
349
350 /**
351 * Get the URL to redirect to, or an empty string if not redirect URL set
352 *
353 * @return string
354 */
355 public function getRedirect() {
356 return $this->mRedirect;
357 }
358
359 /**
360 * Set the copyright URL to send with the output.
361 * Empty string to omit, null to reset.
362 *
363 * @since 1.26
364 *
365 * @param string|null $url
366 */
367 public function setCopyrightUrl( $url ) {
368 $this->copyrightUrl = $url;
369 }
370
371 /**
372 * Set the HTTP status code to send with the output.
373 *
374 * @param int $statusCode
375 */
376 public function setStatusCode( $statusCode ) {
377 $this->mStatusCode = $statusCode;
378 }
379
380 /**
381 * Add a new "<meta>" tag
382 * To add an http-equiv meta tag, precede the name with "http:"
383 *
384 * @param string $name Name of the meta tag
385 * @param string $val Value of the meta tag
386 */
387 function addMeta( $name, $val ) {
388 array_push( $this->mMetatags, [ $name, $val ] );
389 }
390
391 /**
392 * Returns the current <meta> tags
393 *
394 * @since 1.25
395 * @return array
396 */
397 public function getMetaTags() {
398 return $this->mMetatags;
399 }
400
401 /**
402 * Add a new \<link\> tag to the page header.
403 *
404 * Note: use setCanonicalUrl() for rel=canonical.
405 *
406 * @param array $linkarr Associative array of attributes.
407 */
408 function addLink( array $linkarr ) {
409 array_push( $this->mLinktags, $linkarr );
410 }
411
412 /**
413 * Returns the current <link> tags
414 *
415 * @since 1.25
416 * @return array
417 */
418 public function getLinkTags() {
419 return $this->mLinktags;
420 }
421
422 /**
423 * Set the URL to be used for the <link rel=canonical>. This should be used
424 * in preference to addLink(), to avoid duplicate link tags.
425 * @param string $url
426 */
427 function setCanonicalUrl( $url ) {
428 $this->mCanonicalUrl = $url;
429 }
430
431 /**
432 * Returns the URL to be used for the <link rel=canonical> if
433 * one is set.
434 *
435 * @since 1.25
436 * @return bool|string
437 */
438 public function getCanonicalUrl() {
439 return $this->mCanonicalUrl;
440 }
441
442 /**
443 * Add raw HTML to the list of scripts (including \<script\> tag, etc.)
444 * Internal use only. Use OutputPage::addModules() or OutputPage::addJsConfigVars()
445 * if possible.
446 *
447 * @param string $script Raw HTML
448 */
449 function addScript( $script ) {
450 $this->mScripts .= $script;
451 }
452
453 /**
454 * Add a JavaScript file to be loaded as `<script>` on this page.
455 *
456 * Internal use only. Use OutputPage::addModules() if possible.
457 *
458 * @param string $file URL to file (absolute path, protocol-relative, or full url)
459 * @param string|null $unused Previously used to change the cache-busting query parameter
460 */
461 public function addScriptFile( $file, $unused = null ) {
462 if ( substr( $file, 0, 1 ) !== '/' && !preg_match( '#^[a-z]*://#i', $file ) ) {
463 // This is not an absolute path, protocol-relative url, or full scheme url,
464 // presumed to be an old call intended to include a file from /w/skins/common,
465 // which doesn't exist anymore as of MediaWiki 1.24 per T71277. Ignore.
466 wfDeprecated( __METHOD__, '1.24' );
467 return;
468 }
469 $this->addScript( Html::linkedScript( $file, $this->getCSPNonce() ) );
470 }
471
472 /**
473 * Add a self-contained script tag with the given contents
474 * Internal use only. Use OutputPage::addModules() if possible.
475 *
476 * @param string $script JavaScript text, no script tags
477 */
478 public function addInlineScript( $script ) {
479 $this->mScripts .= Html::inlineScript( "\n$script\n", $this->getCSPNonce() ) . "\n";
480 }
481
482 /**
483 * Filter an array of modules to remove insufficiently trustworthy members, and modules
484 * which are no longer registered (eg a page is cached before an extension is disabled)
485 * @param array $modules
486 * @param string|null $position Unused
487 * @param string $type
488 * @return array
489 */
490 protected function filterModules( array $modules, $position = null,
491 $type = ResourceLoaderModule::TYPE_COMBINED
492 ) {
493 $resourceLoader = $this->getResourceLoader();
494 $filteredModules = [];
495 foreach ( $modules as $val ) {
496 $module = $resourceLoader->getModule( $val );
497 if ( $module instanceof ResourceLoaderModule
498 && $module->getOrigin() <= $this->getAllowedModules( $type )
499 ) {
500 if ( $this->mTarget && !in_array( $this->mTarget, $module->getTargets() ) ) {
501 $this->warnModuleTargetFilter( $module->getName() );
502 continue;
503 }
504 $filteredModules[] = $val;
505 }
506 }
507 return $filteredModules;
508 }
509
510 private function warnModuleTargetFilter( $moduleName ) {
511 static $warnings = [];
512 if ( isset( $warnings[$this->mTarget][$moduleName] ) ) {
513 return;
514 }
515 $warnings[$this->mTarget][$moduleName] = true;
516 $this->getResourceLoader()->getLogger()->debug(
517 'Module "{module}" not loadable on target "{target}".',
518 [
519 'module' => $moduleName,
520 'target' => $this->mTarget,
521 ]
522 );
523 }
524
525 /**
526 * Get the list of modules to include on this page
527 *
528 * @param bool $filter Whether to filter out insufficiently trustworthy modules
529 * @param string|null $position Unused
530 * @param string $param
531 * @param string $type
532 * @return array Array of module names
533 */
534 public function getModules( $filter = false, $position = null, $param = 'mModules',
535 $type = ResourceLoaderModule::TYPE_COMBINED
536 ) {
537 $modules = array_values( array_unique( $this->$param ) );
538 return $filter
539 ? $this->filterModules( $modules, null, $type )
540 : $modules;
541 }
542
543 /**
544 * Load one or more ResourceLoader modules on this page.
545 *
546 * @param string|array $modules Module name (string) or array of module names
547 */
548 public function addModules( $modules ) {
549 $this->mModules = array_merge( $this->mModules, (array)$modules );
550 }
551
552 /**
553 * Get the list of style-only modules to load on this page.
554 *
555 * @param bool $filter
556 * @param string|null $position Unused
557 * @return array Array of module names
558 */
559 public function getModuleStyles( $filter = false, $position = null ) {
560 return $this->getModules( $filter, null, 'mModuleStyles',
561 ResourceLoaderModule::TYPE_STYLES
562 );
563 }
564
565 /**
566 * Load the styles of one or more ResourceLoader modules on this page.
567 *
568 * Module styles added through this function will be loaded as a stylesheet,
569 * using a standard `<link rel=stylesheet>` HTML tag, rather than as a combined
570 * Javascript and CSS package. Thus, they will even load when JavaScript is disabled.
571 *
572 * @param string|array $modules Module name (string) or array of module names
573 */
574 public function addModuleStyles( $modules ) {
575 $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
576 }
577
578 /**
579 * @return null|string ResourceLoader target
580 */
581 public function getTarget() {
582 return $this->mTarget;
583 }
584
585 /**
586 * Sets ResourceLoader target for load.php links. If null, will be omitted
587 *
588 * @param string|null $target
589 */
590 public function setTarget( $target ) {
591 $this->mTarget = $target;
592 }
593
594 /**
595 * Add a mapping from a LinkTarget to a Content, for things like page preview.
596 * @see self::addContentOverrideCallback()
597 * @since 1.32
598 * @param LinkTarget $target
599 * @param Content $content
600 */
601 public function addContentOverride( LinkTarget $target, Content $content ) {
602 if ( !$this->contentOverrides ) {
603 // Register a callback for $this->contentOverrides on the first call
604 $this->addContentOverrideCallback( function ( LinkTarget $target ) {
605 $key = $target->getNamespace() . ':' . $target->getDBkey();
606 return $this->contentOverrides[$key] ?? null;
607 } );
608 }
609
610 $key = $target->getNamespace() . ':' . $target->getDBkey();
611 $this->contentOverrides[$key] = $content;
612 }
613
614 /**
615 * Add a callback for mapping from a Title to a Content object, for things
616 * like page preview.
617 * @see ResourceLoaderContext::getContentOverrideCallback()
618 * @since 1.32
619 * @param callable $callback
620 */
621 public function addContentOverrideCallback( callable $callback ) {
622 $this->contentOverrideCallbacks[] = $callback;
623 }
624
625 /**
626 * Get an array of head items
627 *
628 * @return array
629 */
630 function getHeadItemsArray() {
631 return $this->mHeadItems;
632 }
633
634 /**
635 * Add or replace a head item to the output
636 *
637 * Whenever possible, use more specific options like ResourceLoader modules,
638 * OutputPage::addLink(), OutputPage::addMetaLink() and OutputPage::addFeedLink()
639 * Fallback options for those are: OutputPage::addStyle, OutputPage::addScript(),
640 * OutputPage::addInlineScript() and OutputPage::addInlineStyle()
641 * This would be your very LAST fallback.
642 *
643 * @param string $name Item name
644 * @param string $value Raw HTML
645 */
646 public function addHeadItem( $name, $value ) {
647 $this->mHeadItems[$name] = $value;
648 }
649
650 /**
651 * Add one or more head items to the output
652 *
653 * @since 1.28
654 * @param string|string[] $values Raw HTML
655 */
656 public function addHeadItems( $values ) {
657 $this->mHeadItems = array_merge( $this->mHeadItems, (array)$values );
658 }
659
660 /**
661 * Check if the header item $name is already set
662 *
663 * @param string $name Item name
664 * @return bool
665 */
666 public function hasHeadItem( $name ) {
667 return isset( $this->mHeadItems[$name] );
668 }
669
670 /**
671 * Add a class to the <body> element
672 *
673 * @since 1.30
674 * @param string|string[] $classes One or more classes to add
675 */
676 public function addBodyClasses( $classes ) {
677 $this->mAdditionalBodyClasses = array_merge( $this->mAdditionalBodyClasses, (array)$classes );
678 }
679
680 /**
681 * Set whether the output should only contain the body of the article,
682 * without any skin, sidebar, etc.
683 * Used e.g. when calling with "action=render".
684 *
685 * @param bool $only Whether to output only the body of the article
686 */
687 public function setArticleBodyOnly( $only ) {
688 $this->mArticleBodyOnly = $only;
689 }
690
691 /**
692 * Return whether the output will contain only the body of the article
693 *
694 * @return bool
695 */
696 public function getArticleBodyOnly() {
697 return $this->mArticleBodyOnly;
698 }
699
700 /**
701 * Set an additional output property
702 * @since 1.21
703 *
704 * @param string $name
705 * @param mixed $value
706 */
707 public function setProperty( $name, $value ) {
708 $this->mProperties[$name] = $value;
709 }
710
711 /**
712 * Get an additional output property
713 * @since 1.21
714 *
715 * @param string $name
716 * @return mixed Property value or null if not found
717 */
718 public function getProperty( $name ) {
719 return $this->mProperties[$name] ?? null;
720 }
721
722 /**
723 * checkLastModified tells the client to use the client-cached page if
724 * possible. If successful, the OutputPage is disabled so that
725 * any future call to OutputPage->output() have no effect.
726 *
727 * Side effect: sets mLastModified for Last-Modified header
728 *
729 * @param string $timestamp
730 *
731 * @return bool True if cache-ok headers was sent.
732 */
733 public function checkLastModified( $timestamp ) {
734 if ( !$timestamp || $timestamp == '19700101000000' ) {
735 wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
736 return false;
737 }
738 $config = $this->getConfig();
739 if ( !$config->get( 'CachePages' ) ) {
740 wfDebug( __METHOD__ . ": CACHE DISABLED\n" );
741 return false;
742 }
743
744 $timestamp = wfTimestamp( TS_MW, $timestamp );
745 $modifiedTimes = [
746 'page' => $timestamp,
747 'user' => $this->getUser()->getTouched(),
748 'epoch' => $config->get( 'CacheEpoch' )
749 ];
750 if ( $config->get( 'UseCdn' ) ) {
751 $modifiedTimes['sepoch'] = wfTimestamp( TS_MW, $this->getCdnCacheEpoch(
752 time(),
753 $config->get( 'CdnMaxAge' )
754 ) );
755 }
756 Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] );
757
758 $maxModified = max( $modifiedTimes );
759 $this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified );
760
761 $clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' );
762 if ( $clientHeader === false ) {
763 wfDebug( __METHOD__ . ": client did not send If-Modified-Since header", 'private' );
764 return false;
765 }
766
767 # IE sends sizes after the date like this:
768 # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
769 # this breaks strtotime().
770 $clientHeader = preg_replace( '/;.*$/', '', $clientHeader );
771
772 Wikimedia\suppressWarnings(); // E_STRICT system time warnings
773 $clientHeaderTime = strtotime( $clientHeader );
774 Wikimedia\restoreWarnings();
775 if ( !$clientHeaderTime ) {
776 wfDebug( __METHOD__
777 . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
778 return false;
779 }
780 $clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
781
782 # Make debug info
783 $info = '';
784 foreach ( $modifiedTimes as $name => $value ) {
785 if ( $info !== '' ) {
786 $info .= ', ';
787 }
788 $info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
789 }
790
791 wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
792 wfTimestamp( TS_ISO_8601, $clientHeaderTime ), 'private' );
793 wfDebug( __METHOD__ . ": effective Last-Modified: " .
794 wfTimestamp( TS_ISO_8601, $maxModified ), 'private' );
795 if ( $clientHeaderTime < $maxModified ) {
796 wfDebug( __METHOD__ . ": STALE, $info", 'private' );
797 return false;
798 }
799
800 # Not modified
801 # Give a 304 Not Modified response code and disable body output
802 wfDebug( __METHOD__ . ": NOT MODIFIED, $info", 'private' );
803 ini_set( 'zlib.output_compression', 0 );
804 $this->getRequest()->response()->statusHeader( 304 );
805 $this->sendCacheControl();
806 $this->disable();
807
808 // Don't output a compressed blob when using ob_gzhandler;
809 // it's technically against HTTP spec and seems to confuse
810 // Firefox when the response gets split over two packets.
811 wfClearOutputBuffers();
812
813 return true;
814 }
815
816 /**
817 * @param int $reqTime Time of request (eg. now)
818 * @param int $maxAge Cache TTL in seconds
819 * @return int Timestamp
820 */
821 private function getCdnCacheEpoch( $reqTime, $maxAge ) {
822 // Ensure Last-Modified is never more than $wgCdnMaxAge in the past,
823 // because even if the wiki page content hasn't changed since, static
824 // resources may have changed (skin HTML, interface messages, urls, etc.)
825 // and must roll-over in a timely manner (T46570)
826 return $reqTime - $maxAge;
827 }
828
829 /**
830 * Override the last modified timestamp
831 *
832 * @param string $timestamp New timestamp, in a format readable by
833 * wfTimestamp()
834 */
835 public function setLastModified( $timestamp ) {
836 $this->mLastModified = wfTimestamp( TS_RFC2822, $timestamp );
837 }
838
839 /**
840 * Set the robot policy for the page: <http://www.robotstxt.org/meta.html>
841 *
842 * @param string $policy The literal string to output as the contents of
843 * the meta tag. Will be parsed according to the spec and output in
844 * standardized form.
845 * @return null
846 */
847 public function setRobotPolicy( $policy ) {
848 $policy = Article::formatRobotPolicy( $policy );
849
850 if ( isset( $policy['index'] ) ) {
851 $this->setIndexPolicy( $policy['index'] );
852 }
853 if ( isset( $policy['follow'] ) ) {
854 $this->setFollowPolicy( $policy['follow'] );
855 }
856 }
857
858 /**
859 * Set the index policy for the page, but leave the follow policy un-
860 * touched.
861 *
862 * @param string $policy Either 'index' or 'noindex'.
863 * @return null
864 */
865 public function setIndexPolicy( $policy ) {
866 $policy = trim( $policy );
867 if ( in_array( $policy, [ 'index', 'noindex' ] ) ) {
868 $this->mIndexPolicy = $policy;
869 }
870 }
871
872 /**
873 * Set the follow policy for the page, but leave the index policy un-
874 * touched.
875 *
876 * @param string $policy Either 'follow' or 'nofollow'.
877 * @return null
878 */
879 public function setFollowPolicy( $policy ) {
880 $policy = trim( $policy );
881 if ( in_array( $policy, [ 'follow', 'nofollow' ] ) ) {
882 $this->mFollowPolicy = $policy;
883 }
884 }
885
886 /**
887 * "HTML title" means the contents of "<title>".
888 * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file.
889 *
890 * @param string|Message $name
891 */
892 public function setHTMLTitle( $name ) {
893 if ( $name instanceof Message ) {
894 $this->mHTMLtitle = $name->setContext( $this->getContext() )->text();
895 } else {
896 $this->mHTMLtitle = $name;
897 }
898 }
899
900 /**
901 * Return the "HTML title", i.e. the content of the "<title>" tag.
902 *
903 * @return string
904 */
905 public function getHTMLTitle() {
906 return $this->mHTMLtitle;
907 }
908
909 /**
910 * Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
911 *
912 * @param Title $t
913 */
914 public function setRedirectedFrom( $t ) {
915 $this->mRedirectedFrom = $t;
916 }
917
918 /**
919 * "Page title" means the contents of \<h1\>. It is stored as a valid HTML
920 * fragment. This function allows good tags like \<sup\> in the \<h1\> tag,
921 * but not bad tags like \<script\>. This function automatically sets
922 * \<title\> to the same content as \<h1\> but with all tags removed. Bad
923 * tags that were escaped in \<h1\> will still be escaped in \<title\>, and
924 * good tags like \<i\> will be dropped entirely.
925 *
926 * @param string|Message $name
927 * @param-taint $name tainted
928 * Phan-taint-check gets very confused by $name being either a string or a Message
929 */
930 public function setPageTitle( $name ) {
931 if ( $name instanceof Message ) {
932 $name = $name->setContext( $this->getContext() )->text();
933 }
934
935 # change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
936 # but leave "<i>foobar</i>" alone
937 $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
938 $this->mPageTitle = $nameWithTags;
939
940 # change "<i>foo&amp;bar</i>" to "foo&bar"
941 $this->setHTMLTitle(
942 $this->msg( 'pagetitle' )->plaintextParams( Sanitizer::stripAllTags( $nameWithTags ) )
943 ->inContentLanguage()
944 );
945 }
946
947 /**
948 * Return the "page title", i.e. the content of the \<h1\> tag.
949 *
950 * @return string
951 */
952 public function getPageTitle() {
953 return $this->mPageTitle;
954 }
955
956 /**
957 * Same as page title but only contains name of the page, not any other text.
958 *
959 * @since 1.32
960 * @param string $html Page title text.
961 * @see OutputPage::setPageTitle
962 */
963 public function setDisplayTitle( $html ) {
964 $this->displayTitle = $html;
965 }
966
967 /**
968 * Returns page display title.
969 *
970 * Performs some normalization, but this not as strict the magic word.
971 *
972 * @since 1.32
973 * @return string HTML
974 */
975 public function getDisplayTitle() {
976 $html = $this->displayTitle;
977 if ( $html === null ) {
978 $html = $this->getTitle()->getPrefixedText();
979 }
980
981 return Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $html ) );
982 }
983
984 /**
985 * Returns page display title without namespace prefix if possible.
986 *
987 * @since 1.32
988 * @return string HTML
989 */
990 public function getUnprefixedDisplayTitle() {
991 $text = $this->getDisplayTitle();
992 $nsPrefix = $this->getTitle()->getNsText() . ':';
993 $prefix = preg_quote( $nsPrefix, '/' );
994
995 return preg_replace( "/^$prefix/i", '', $text );
996 }
997
998 /**
999 * Set the Title object to use
1000 *
1001 * @param Title $t
1002 */
1003 public function setTitle( Title $t ) {
1004 $this->getContext()->setTitle( $t );
1005 }
1006
1007 /**
1008 * Replace the subtitle with $str
1009 *
1010 * @param string|Message $str New value of the subtitle. String should be safe HTML.
1011 */
1012 public function setSubtitle( $str ) {
1013 $this->clearSubtitle();
1014 $this->addSubtitle( $str );
1015 }
1016
1017 /**
1018 * Add $str to the subtitle
1019 *
1020 * @param string|Message $str String or Message to add to the subtitle. String should be safe HTML.
1021 */
1022 public function addSubtitle( $str ) {
1023 if ( $str instanceof Message ) {
1024 $this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
1025 } else {
1026 $this->mSubtitle[] = $str;
1027 }
1028 }
1029
1030 /**
1031 * Build message object for a subtitle containing a backlink to a page
1032 *
1033 * @param Title $title Title to link to
1034 * @param array $query Array of additional parameters to include in the link
1035 * @return Message
1036 * @since 1.25
1037 */
1038 public static function buildBacklinkSubtitle( Title $title, $query = [] ) {
1039 if ( $title->isRedirect() ) {
1040 $query['redirect'] = 'no';
1041 }
1042 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1043 return wfMessage( 'backlinksubtitle' )
1044 ->rawParams( $linkRenderer->makeLink( $title, null, [], $query ) );
1045 }
1046
1047 /**
1048 * Add a subtitle containing a backlink to a page
1049 *
1050 * @param Title $title Title to link to
1051 * @param array $query Array of additional parameters to include in the link
1052 */
1053 public function addBacklinkSubtitle( Title $title, $query = [] ) {
1054 $this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) );
1055 }
1056
1057 /**
1058 * Clear the subtitles
1059 */
1060 public function clearSubtitle() {
1061 $this->mSubtitle = [];
1062 }
1063
1064 /**
1065 * Get the subtitle
1066 *
1067 * @return string
1068 */
1069 public function getSubtitle() {
1070 return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
1071 }
1072
1073 /**
1074 * Set the page as printable, i.e. it'll be displayed with all
1075 * print styles included
1076 */
1077 public function setPrintable() {
1078 $this->mPrintable = true;
1079 }
1080
1081 /**
1082 * Return whether the page is "printable"
1083 *
1084 * @return bool
1085 */
1086 public function isPrintable() {
1087 return $this->mPrintable;
1088 }
1089
1090 /**
1091 * Disable output completely, i.e. calling output() will have no effect
1092 */
1093 public function disable() {
1094 $this->mDoNothing = true;
1095 }
1096
1097 /**
1098 * Return whether the output will be completely disabled
1099 *
1100 * @return bool
1101 */
1102 public function isDisabled() {
1103 return $this->mDoNothing;
1104 }
1105
1106 /**
1107 * Show an "add new section" link?
1108 *
1109 * @return bool
1110 */
1111 public function showNewSectionLink() {
1112 return $this->mNewSectionLink;
1113 }
1114
1115 /**
1116 * Forcibly hide the new section link?
1117 *
1118 * @return bool
1119 */
1120 public function forceHideNewSectionLink() {
1121 return $this->mHideNewSectionLink;
1122 }
1123
1124 /**
1125 * Add or remove feed links in the page header
1126 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
1127 * for the new version
1128 * @see addFeedLink()
1129 *
1130 * @param bool $show True: add default feeds, false: remove all feeds
1131 */
1132 public function setSyndicated( $show = true ) {
1133 if ( $show ) {
1134 $this->setFeedAppendQuery( false );
1135 } else {
1136 $this->mFeedLinks = [];
1137 }
1138 }
1139
1140 /**
1141 * Return effective list of advertised feed types
1142 * @see addFeedLink()
1143 *
1144 * @return array Array of feed type names ( 'rss', 'atom' )
1145 */
1146 protected function getAdvertisedFeedTypes() {
1147 if ( $this->getConfig()->get( 'Feed' ) ) {
1148 return $this->getConfig()->get( 'AdvertisedFeedTypes' );
1149 } else {
1150 return [];
1151 }
1152 }
1153
1154 /**
1155 * Add default feeds to the page header
1156 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
1157 * for the new version
1158 * @see addFeedLink()
1159 *
1160 * @param string $val Query to append to feed links or false to output
1161 * default links
1162 */
1163 public function setFeedAppendQuery( $val ) {
1164 $this->mFeedLinks = [];
1165
1166 foreach ( $this->getAdvertisedFeedTypes() as $type ) {
1167 $query = "feed=$type";
1168 if ( is_string( $val ) ) {
1169 $query .= '&' . $val;
1170 }
1171 $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
1172 }
1173 }
1174
1175 /**
1176 * Add a feed link to the page header
1177 *
1178 * @param string $format Feed type, should be a key of $wgFeedClasses
1179 * @param string $href URL
1180 */
1181 public function addFeedLink( $format, $href ) {
1182 if ( in_array( $format, $this->getAdvertisedFeedTypes() ) ) {
1183 $this->mFeedLinks[$format] = $href;
1184 }
1185 }
1186
1187 /**
1188 * Should we output feed links for this page?
1189 * @return bool
1190 */
1191 public function isSyndicated() {
1192 return count( $this->mFeedLinks ) > 0;
1193 }
1194
1195 /**
1196 * Return URLs for each supported syndication format for this page.
1197 * @return array Associating format keys with URLs
1198 */
1199 public function getSyndicationLinks() {
1200 return $this->mFeedLinks;
1201 }
1202
1203 /**
1204 * Will currently always return null
1205 *
1206 * @return null
1207 */
1208 public function getFeedAppendQuery() {
1209 return $this->mFeedLinksAppendQuery;
1210 }
1211
1212 /**
1213 * Set whether the displayed content is related to the source of the
1214 * corresponding article on the wiki
1215 * Setting true will cause the change "article related" toggle to true
1216 *
1217 * @param bool $newVal
1218 */
1219 public function setArticleFlag( $newVal ) {
1220 $this->mIsArticle = $newVal;
1221 if ( $newVal ) {
1222 $this->mIsArticleRelated = $newVal;
1223 }
1224 }
1225
1226 /**
1227 * Return whether the content displayed page is related to the source of
1228 * the corresponding article on the wiki
1229 *
1230 * @return bool
1231 */
1232 public function isArticle() {
1233 return $this->mIsArticle;
1234 }
1235
1236 /**
1237 * Set whether this page is related an article on the wiki
1238 * Setting false will cause the change of "article flag" toggle to false
1239 *
1240 * @param bool $newVal
1241 */
1242 public function setArticleRelated( $newVal ) {
1243 $this->mIsArticleRelated = $newVal;
1244 if ( !$newVal ) {
1245 $this->mIsArticle = false;
1246 }
1247 }
1248
1249 /**
1250 * Return whether this page is related an article on the wiki
1251 *
1252 * @return bool
1253 */
1254 public function isArticleRelated() {
1255 return $this->mIsArticleRelated;
1256 }
1257
1258 /**
1259 * Set whether the standard copyright should be shown for the current page.
1260 *
1261 * @param bool $hasCopyright
1262 */
1263 public function setCopyright( $hasCopyright ) {
1264 $this->mHasCopyright = $hasCopyright;
1265 }
1266
1267 /**
1268 * Return whether the standard copyright should be shown for the current page.
1269 * By default, it is true for all articles but other pages
1270 * can signal it by using setCopyright( true ).
1271 *
1272 * Used by SkinTemplate to decided whether to show the copyright.
1273 *
1274 * @return bool
1275 */
1276 public function showsCopyright() {
1277 return $this->isArticle() || $this->mHasCopyright;
1278 }
1279
1280 /**
1281 * Add new language links
1282 *
1283 * @param string[] $newLinkArray Array of interwiki-prefixed (non DB key) titles
1284 * (e.g. 'fr:Test page')
1285 */
1286 public function addLanguageLinks( array $newLinkArray ) {
1287 $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $newLinkArray );
1288 }
1289
1290 /**
1291 * Reset the language links and add new language links
1292 *
1293 * @param string[] $newLinkArray Array of interwiki-prefixed (non DB key) titles
1294 * (e.g. 'fr:Test page')
1295 */
1296 public function setLanguageLinks( array $newLinkArray ) {
1297 $this->mLanguageLinks = $newLinkArray;
1298 }
1299
1300 /**
1301 * Get the list of language links
1302 *
1303 * @return string[] Array of interwiki-prefixed (non DB key) titles (e.g. 'fr:Test page')
1304 */
1305 public function getLanguageLinks() {
1306 return $this->mLanguageLinks;
1307 }
1308
1309 /**
1310 * Add an array of categories, with names in the keys
1311 *
1312 * @param array $categories Mapping category name => sort key
1313 */
1314 public function addCategoryLinks( array $categories ) {
1315 if ( !$categories ) {
1316 return;
1317 }
1318
1319 $res = $this->addCategoryLinksToLBAndGetResult( $categories );
1320
1321 # Set all the values to 'normal'.
1322 $categories = array_fill_keys( array_keys( $categories ), 'normal' );
1323
1324 # Mark hidden categories
1325 foreach ( $res as $row ) {
1326 if ( isset( $row->pp_value ) ) {
1327 $categories[$row->page_title] = 'hidden';
1328 }
1329 }
1330
1331 // Avoid PHP 7.1 warning of passing $this by reference
1332 $outputPage = $this;
1333 # Add the remaining categories to the skin
1334 if ( Hooks::run(
1335 'OutputPageMakeCategoryLinks',
1336 [ &$outputPage, $categories, &$this->mCategoryLinks ] )
1337 ) {
1338 $services = MediaWikiServices::getInstance();
1339 $linkRenderer = $services->getLinkRenderer();
1340 foreach ( $categories as $category => $type ) {
1341 // array keys will cast numeric category names to ints, so cast back to string
1342 $category = (string)$category;
1343 $origcategory = $category;
1344 $title = Title::makeTitleSafe( NS_CATEGORY, $category );
1345 if ( !$title ) {
1346 continue;
1347 }
1348 $services->getContentLanguage()->findVariantLink( $category, $title, true );
1349 if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1350 continue;
1351 }
1352 $text = $services->getContentLanguage()->convertHtml( $title->getText() );
1353 $this->mCategories[$type][] = $title->getText();
1354 $this->mCategoryLinks[$type][] = $linkRenderer->makeLink( $title, new HtmlArmor( $text ) );
1355 }
1356 }
1357 }
1358
1359 /**
1360 * @param array $categories
1361 * @return bool|IResultWrapper
1362 */
1363 protected function addCategoryLinksToLBAndGetResult( array $categories ) {
1364 # Add the links to a LinkBatch
1365 $arr = [ NS_CATEGORY => $categories ];
1366 $lb = new LinkBatch;
1367 $lb->setArray( $arr );
1368
1369 # Fetch existence plus the hiddencat property
1370 $dbr = wfGetDB( DB_REPLICA );
1371 $fields = array_merge(
1372 LinkCache::getSelectFields(),
1373 [ 'page_namespace', 'page_title', 'pp_value' ]
1374 );
1375
1376 $res = $dbr->select( [ 'page', 'page_props' ],
1377 $fields,
1378 $lb->constructSet( 'page', $dbr ),
1379 __METHOD__,
1380 [],
1381 [ 'page_props' => [ 'LEFT JOIN', [
1382 'pp_propname' => 'hiddencat',
1383 'pp_page = page_id'
1384 ] ] ]
1385 );
1386
1387 # Add the results to the link cache
1388 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1389 $lb->addResultToCache( $linkCache, $res );
1390
1391 return $res;
1392 }
1393
1394 /**
1395 * Reset the category links (but not the category list) and add $categories
1396 *
1397 * @param array $categories Mapping category name => sort key
1398 */
1399 public function setCategoryLinks( array $categories ) {
1400 $this->mCategoryLinks = [];
1401 $this->addCategoryLinks( $categories );
1402 }
1403
1404 /**
1405 * Get the list of category links, in a 2-D array with the following format:
1406 * $arr[$type][] = $link, where $type is either "normal" or "hidden" (for
1407 * hidden categories) and $link a HTML fragment with a link to the category
1408 * page
1409 *
1410 * @return array
1411 */
1412 public function getCategoryLinks() {
1413 return $this->mCategoryLinks;
1414 }
1415
1416 /**
1417 * Get the list of category names this page belongs to.
1418 *
1419 * @param string $type The type of categories which should be returned. Possible values:
1420 * * all: all categories of all types
1421 * * hidden: only the hidden categories
1422 * * normal: all categories, except hidden categories
1423 * @return array Array of strings
1424 */
1425 public function getCategories( $type = 'all' ) {
1426 if ( $type === 'all' ) {
1427 $allCategories = [];
1428 foreach ( $this->mCategories as $categories ) {
1429 $allCategories = array_merge( $allCategories, $categories );
1430 }
1431 return $allCategories;
1432 }
1433 if ( !isset( $this->mCategories[$type] ) ) {
1434 throw new InvalidArgumentException( 'Invalid category type given: ' . $type );
1435 }
1436 return $this->mCategories[$type];
1437 }
1438
1439 /**
1440 * Add an array of indicators, with their identifiers as array
1441 * keys and HTML contents as values.
1442 *
1443 * In case of duplicate keys, existing values are overwritten.
1444 *
1445 * @param array $indicators
1446 * @since 1.25
1447 */
1448 public function setIndicators( array $indicators ) {
1449 $this->mIndicators = $indicators + $this->mIndicators;
1450 // Keep ordered by key
1451 ksort( $this->mIndicators );
1452 }
1453
1454 /**
1455 * Get the indicators associated with this page.
1456 *
1457 * The array will be internally ordered by item keys.
1458 *
1459 * @return array Keys: identifiers, values: HTML contents
1460 * @since 1.25
1461 */
1462 public function getIndicators() {
1463 return $this->mIndicators;
1464 }
1465
1466 /**
1467 * Adds help link with an icon via page indicators.
1468 * Link target can be overridden by a local message containing a wikilink:
1469 * the message key is: lowercase action or special page name + '-helppage'.
1470 * @param string $to Target MediaWiki.org page title or encoded URL.
1471 * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
1472 * @since 1.25
1473 */
1474 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1475 $this->addModuleStyles( 'mediawiki.helplink' );
1476 $text = $this->msg( 'helppage-top-gethelp' )->escaped();
1477
1478 if ( $overrideBaseUrl ) {
1479 $helpUrl = $to;
1480 } else {
1481 $toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) );
1482 $helpUrl = "https://www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1483 }
1484
1485 $link = Html::rawElement(
1486 'a',
1487 [
1488 'href' => $helpUrl,
1489 'target' => '_blank',
1490 'class' => 'mw-helplink',
1491 ],
1492 $text
1493 );
1494
1495 $this->setIndicators( [ 'mw-helplink' => $link ] );
1496 }
1497
1498 /**
1499 * Do not allow scripts which can be modified by wiki users to load on this page;
1500 * only allow scripts bundled with, or generated by, the software.
1501 * Site-wide styles are controlled by a config setting, since they can be
1502 * used to create a custom skin/theme, but not user-specific ones.
1503 *
1504 * @todo this should be given a more accurate name
1505 */
1506 public function disallowUserJs() {
1507 $this->reduceAllowedModules(
1508 ResourceLoaderModule::TYPE_SCRIPTS,
1509 ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
1510 );
1511
1512 // Site-wide styles are controlled by a config setting, see T73621
1513 // for background on why. User styles are never allowed.
1514 if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
1515 $styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
1516 } else {
1517 $styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL;
1518 }
1519 $this->reduceAllowedModules(
1520 ResourceLoaderModule::TYPE_STYLES,
1521 $styleOrigin
1522 );
1523 }
1524
1525 /**
1526 * Show what level of JavaScript / CSS untrustworthiness is allowed on this page
1527 * @see ResourceLoaderModule::$origin
1528 * @param string $type ResourceLoaderModule TYPE_ constant
1529 * @return int ResourceLoaderModule ORIGIN_ class constant
1530 */
1531 public function getAllowedModules( $type ) {
1532 if ( $type == ResourceLoaderModule::TYPE_COMBINED ) {
1533 return min( array_values( $this->mAllowedModules ) );
1534 } else {
1535 return $this->mAllowedModules[$type] ?? ResourceLoaderModule::ORIGIN_ALL;
1536 }
1537 }
1538
1539 /**
1540 * Limit the highest level of CSS/JS untrustworthiness allowed.
1541 *
1542 * If passed the same or a higher level than the current level of untrustworthiness set, the
1543 * level will remain unchanged.
1544 *
1545 * @param string $type
1546 * @param int $level ResourceLoaderModule class constant
1547 */
1548 public function reduceAllowedModules( $type, $level ) {
1549 $this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
1550 }
1551
1552 /**
1553 * Prepend $text to the body HTML
1554 *
1555 * @param string $text HTML
1556 */
1557 public function prependHTML( $text ) {
1558 $this->mBodytext = $text . $this->mBodytext;
1559 }
1560
1561 /**
1562 * Append $text to the body HTML
1563 *
1564 * @param string $text HTML
1565 */
1566 public function addHTML( $text ) {
1567 $this->mBodytext .= $text;
1568 }
1569
1570 /**
1571 * Shortcut for adding an Html::element via addHTML.
1572 *
1573 * @since 1.19
1574 *
1575 * @param string $element
1576 * @param array $attribs
1577 * @param string $contents
1578 */
1579 public function addElement( $element, array $attribs = [], $contents = '' ) {
1580 $this->addHTML( Html::element( $element, $attribs, $contents ) );
1581 }
1582
1583 /**
1584 * Clear the body HTML
1585 */
1586 public function clearHTML() {
1587 $this->mBodytext = '';
1588 }
1589
1590 /**
1591 * Get the body HTML
1592 *
1593 * @return string HTML
1594 */
1595 public function getHTML() {
1596 return $this->mBodytext;
1597 }
1598
1599 /**
1600 * Get/set the ParserOptions object to use for wikitext parsing
1601 *
1602 * @param ParserOptions|null $options Either the ParserOption to use or null to only get the
1603 * current ParserOption object. This parameter is deprecated since 1.31.
1604 * @return ParserOptions
1605 */
1606 public function parserOptions( $options = null ) {
1607 if ( $options !== null ) {
1608 wfDeprecated( __METHOD__ . ' with non-null $options', '1.31' );
1609 }
1610
1611 if ( $options !== null && !empty( $options->isBogus ) ) {
1612 // Someone is trying to set a bogus pre-$wgUser PO. Check if it has
1613 // been changed somehow, and keep it if so.
1614 $anonPO = ParserOptions::newFromAnon();
1615 $anonPO->setAllowUnsafeRawHtml( false );
1616 if ( !$options->matches( $anonPO ) ) {
1617 wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) );
1618 $options->isBogus = false;
1619 }
1620 }
1621
1622 if ( !$this->mParserOptions ) {
1623 if ( !$this->getUser()->isSafeToLoad() ) {
1624 // $wgUser isn't unstubbable yet, so don't try to get a
1625 // ParserOptions for it. And don't cache this ParserOptions
1626 // either.
1627 $po = ParserOptions::newFromAnon();
1628 $po->setAllowUnsafeRawHtml( false );
1629 $po->isBogus = true;
1630 if ( $options !== null ) {
1631 $this->mParserOptions = empty( $options->isBogus ) ? $options : null;
1632 }
1633 return $po;
1634 }
1635
1636 $this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
1637 $this->mParserOptions->setAllowUnsafeRawHtml( false );
1638 }
1639
1640 if ( $options !== null && !empty( $options->isBogus ) ) {
1641 // They're trying to restore the bogus pre-$wgUser PO. Do the right
1642 // thing.
1643 return wfSetVar( $this->mParserOptions, null, true );
1644 } else {
1645 return wfSetVar( $this->mParserOptions, $options );
1646 }
1647 }
1648
1649 /**
1650 * Set the revision ID which will be seen by the wiki text parser
1651 * for things such as embedded {{REVISIONID}} variable use.
1652 *
1653 * @param int|null $revid A positive integer, or null
1654 * @return mixed Previous value
1655 */
1656 public function setRevisionId( $revid ) {
1657 $val = is_null( $revid ) ? null : intval( $revid );
1658 return wfSetVar( $this->mRevisionId, $val, true );
1659 }
1660
1661 /**
1662 * Get the displayed revision ID
1663 *
1664 * @return int
1665 */
1666 public function getRevisionId() {
1667 return $this->mRevisionId;
1668 }
1669
1670 /**
1671 * Set the timestamp of the revision which will be displayed. This is used
1672 * to avoid a extra DB call in Skin::lastModified().
1673 *
1674 * @param string|null $timestamp
1675 * @return mixed Previous value
1676 */
1677 public function setRevisionTimestamp( $timestamp ) {
1678 return wfSetVar( $this->mRevisionTimestamp, $timestamp, true );
1679 }
1680
1681 /**
1682 * Get the timestamp of displayed revision.
1683 * This will be null if not filled by setRevisionTimestamp().
1684 *
1685 * @return string|null
1686 */
1687 public function getRevisionTimestamp() {
1688 return $this->mRevisionTimestamp;
1689 }
1690
1691 /**
1692 * Set the displayed file version
1693 *
1694 * @param File|null $file
1695 * @return mixed Previous value
1696 */
1697 public function setFileVersion( $file ) {
1698 $val = null;
1699 if ( $file instanceof File && $file->exists() ) {
1700 $val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ];
1701 }
1702 return wfSetVar( $this->mFileVersion, $val, true );
1703 }
1704
1705 /**
1706 * Get the displayed file version
1707 *
1708 * @return array|null ('time' => MW timestamp, 'sha1' => sha1)
1709 */
1710 public function getFileVersion() {
1711 return $this->mFileVersion;
1712 }
1713
1714 /**
1715 * Get the templates used on this page
1716 *
1717 * @return array (namespace => dbKey => revId)
1718 * @since 1.18
1719 */
1720 public function getTemplateIds() {
1721 return $this->mTemplateIds;
1722 }
1723
1724 /**
1725 * Get the files used on this page
1726 *
1727 * @return array [ dbKey => [ 'time' => MW timestamp or null, 'sha1' => sha1 or '' ] ]
1728 * @since 1.18
1729 */
1730 public function getFileSearchOptions() {
1731 return $this->mImageTimeKeys;
1732 }
1733
1734 /**
1735 * Convert wikitext *in the user interface language* to HTML and
1736 * add it to the buffer. The result will not be
1737 * language-converted, as user interface messages are already
1738 * localized into a specific variant. Assumes that the current
1739 * page title will be used if optional $title is not
1740 * provided. Output will be tidy.
1741 *
1742 * @param string $text Wikitext in the user interface language
1743 * @param bool $linestart Is this the start of a line? (Defaults to true)
1744 * @param Title|null $title Optional title to use; default of `null`
1745 * means use current page title.
1746 * @throws MWException if $title is not provided and OutputPage::getTitle()
1747 * is null
1748 * @since 1.32
1749 */
1750 public function addWikiTextAsInterface(
1751 $text, $linestart = true, Title $title = null
1752 ) {
1753 if ( $title === null ) {
1754 $title = $this->getTitle();
1755 }
1756 if ( !$title ) {
1757 throw new MWException( 'Title is null' );
1758 }
1759 $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/true );
1760 }
1761
1762 /**
1763 * Convert wikitext *in the user interface language* to HTML and
1764 * add it to the buffer with a `<div class="$wrapperClass">`
1765 * wrapper. The result will not be language-converted, as user
1766 * interface messages as already localized into a specific
1767 * variant. The $text will be parsed in start-of-line context.
1768 * Output will be tidy.
1769 *
1770 * @param string $wrapperClass The class attribute value for the <div>
1771 * wrapper in the output HTML
1772 * @param string $text Wikitext in the user interface language
1773 * @since 1.32
1774 */
1775 public function wrapWikiTextAsInterface(
1776 $wrapperClass, $text
1777 ) {
1778 $this->addWikiTextTitleInternal(
1779 $text, $this->getTitle(),
1780 /*linestart*/true, /*interface*/true,
1781 $wrapperClass
1782 );
1783 }
1784
1785 /**
1786 * Convert wikitext *in the page content language* to HTML and add
1787 * it to the buffer. The result with be language-converted to the
1788 * user's preferred variant. Assumes that the current page title
1789 * will be used if optional $title is not provided. Output will be
1790 * tidy.
1791 *
1792 * @param string $text Wikitext in the page content language
1793 * @param bool $linestart Is this the start of a line? (Defaults to true)
1794 * @param Title|null $title Optional title to use; default of `null`
1795 * means use current page title.
1796 * @throws MWException if $title is not provided and OutputPage::getTitle()
1797 * is null
1798 * @since 1.32
1799 */
1800 public function addWikiTextAsContent(
1801 $text, $linestart = true, Title $title = null
1802 ) {
1803 if ( $title === null ) {
1804 $title = $this->getTitle();
1805 }
1806 if ( !$title ) {
1807 throw new MWException( 'Title is null' );
1808 }
1809 $this->addWikiTextTitleInternal( $text, $title, $linestart, /*interface*/false );
1810 }
1811
1812 /**
1813 * Add wikitext with a custom Title object.
1814 * Output is unwrapped.
1815 *
1816 * @param string $text Wikitext
1817 * @param Title $title
1818 * @param bool $linestart Is this the start of a line?
1819 * @param bool $tidy Whether to use tidy.
1820 * Setting this to false (or omitting it) is deprecated
1821 * since 1.32; all wikitext should be tidied.
1822 * @param bool $interface Whether it is an interface message
1823 * (for example disables conversion)
1824 * @param string $wrapperClass if not empty, wraps the output in
1825 * a `<div class="$wrapperClass">`
1826 * @private
1827 */
1828 private function addWikiTextTitleInternal(
1829 $text, Title $title, $linestart, $interface, $wrapperClass = null
1830 ) {
1831 $parserOutput = $this->parseInternal(
1832 $text, $title, $linestart, true, $interface, /*language*/null
1833 );
1834
1835 $this->addParserOutput( $parserOutput, [
1836 'enableSectionEditLinks' => false,
1837 'wrapperDivClass' => $wrapperClass ?? '',
1838 ] );
1839 }
1840
1841 /**
1842 * Add all metadata associated with a ParserOutput object, but without the actual HTML. This
1843 * includes categories, language links, ResourceLoader modules, effects of certain magic words,
1844 * and so on.
1845 *
1846 * @since 1.24
1847 * @param ParserOutput $parserOutput
1848 */
1849 public function addParserOutputMetadata( ParserOutput $parserOutput ) {
1850 $this->mLanguageLinks =
1851 array_merge( $this->mLanguageLinks, $parserOutput->getLanguageLinks() );
1852 $this->addCategoryLinks( $parserOutput->getCategories() );
1853 $this->setIndicators( $parserOutput->getIndicators() );
1854 $this->mNewSectionLink = $parserOutput->getNewSection();
1855 $this->mHideNewSectionLink = $parserOutput->getHideNewSection();
1856
1857 if ( !$parserOutput->isCacheable() ) {
1858 $this->enableClientCache( false );
1859 }
1860 $this->mNoGallery = $parserOutput->getNoGallery();
1861 $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
1862 $this->addModules( $parserOutput->getModules() );
1863 $this->addModuleStyles( $parserOutput->getModuleStyles() );
1864 $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1865 $this->mPreventClickjacking = $this->mPreventClickjacking
1866 || $parserOutput->preventClickjacking();
1867
1868 // Template versioning...
1869 foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
1870 if ( isset( $this->mTemplateIds[$ns] ) ) {
1871 $this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1872 } else {
1873 $this->mTemplateIds[$ns] = $dbks;
1874 }
1875 }
1876 // File versioning...
1877 foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
1878 $this->mImageTimeKeys[$dbk] = $data;
1879 }
1880
1881 // Hooks registered in the object
1882 $parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' );
1883 foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
1884 list( $hookName, $data ) = $hookInfo;
1885 if ( isset( $parserOutputHooks[$hookName] ) ) {
1886 $parserOutputHooks[$hookName]( $this, $parserOutput, $data );
1887 }
1888 }
1889
1890 // Enable OOUI if requested via ParserOutput
1891 if ( $parserOutput->getEnableOOUI() ) {
1892 $this->enableOOUI();
1893 }
1894
1895 // Include parser limit report
1896 if ( !$this->limitReportJSData ) {
1897 $this->limitReportJSData = $parserOutput->getLimitReportJSData();
1898 }
1899
1900 // Link flags are ignored for now, but may in the future be
1901 // used to mark individual language links.
1902 $linkFlags = [];
1903 // Avoid PHP 7.1 warning of passing $this by reference
1904 $outputPage = $this;
1905 Hooks::run( 'LanguageLinks', [ $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ] );
1906 Hooks::runWithoutAbort( 'OutputPageParserOutput', [ &$outputPage, $parserOutput ] );
1907
1908 // This check must be after 'OutputPageParserOutput' runs in addParserOutputMetadata
1909 // so that extensions may modify ParserOutput to toggle TOC.
1910 // This cannot be moved to addParserOutputText because that is not
1911 // called by EditPage for Preview.
1912 if ( $parserOutput->getTOCHTML() ) {
1913 $this->mEnableTOC = true;
1914 }
1915 }
1916
1917 /**
1918 * Add the HTML and enhancements for it (like ResourceLoader modules) associated with a
1919 * ParserOutput object, without any other metadata.
1920 *
1921 * @since 1.24
1922 * @param ParserOutput $parserOutput
1923 * @param array $poOptions Options to ParserOutput::getText()
1924 */
1925 public function addParserOutputContent( ParserOutput $parserOutput, $poOptions = [] ) {
1926 $this->addParserOutputText( $parserOutput, $poOptions );
1927
1928 $this->addModules( $parserOutput->getModules() );
1929 $this->addModuleStyles( $parserOutput->getModuleStyles() );
1930
1931 $this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1932 }
1933
1934 /**
1935 * Add the HTML associated with a ParserOutput object, without any metadata.
1936 *
1937 * @since 1.24
1938 * @param ParserOutput $parserOutput
1939 * @param array $poOptions Options to ParserOutput::getText()
1940 */
1941 public function addParserOutputText( ParserOutput $parserOutput, $poOptions = [] ) {
1942 $text = $parserOutput->getText( $poOptions );
1943 // Avoid PHP 7.1 warning of passing $this by reference
1944 $outputPage = $this;
1945 Hooks::runWithoutAbort( 'OutputPageBeforeHTML', [ &$outputPage, &$text ] );
1946 $this->addHTML( $text );
1947 }
1948
1949 /**
1950 * Add everything from a ParserOutput object.
1951 *
1952 * @param ParserOutput $parserOutput
1953 * @param array $poOptions Options to ParserOutput::getText()
1954 */
1955 function addParserOutput( ParserOutput $parserOutput, $poOptions = [] ) {
1956 $this->addParserOutputMetadata( $parserOutput );
1957 $this->addParserOutputText( $parserOutput, $poOptions );
1958 }
1959
1960 /**
1961 * Add the output of a QuickTemplate to the output buffer
1962 *
1963 * @param QuickTemplate &$template
1964 */
1965 public function addTemplate( &$template ) {
1966 $this->addHTML( $template->getHTML() );
1967 }
1968
1969 /**
1970 * Parse wikitext and return the HTML.
1971 *
1972 * @todo The output is wrapped in a <div> iff $interface is false; it's
1973 * probably best to always strip the wrapper.
1974 *
1975 * @param string $text
1976 * @param bool $linestart Is this the start of a line?
1977 * @param bool $interface Use interface language (instead of content language) while parsing
1978 * language sensitive magic words like GRAMMAR and PLURAL. This also disables
1979 * LanguageConverter.
1980 * @param Language|null $language Target language object, will override $interface
1981 * @throws MWException
1982 * @return string HTML
1983 * @deprecated since 1.32, due to untidy output and inconsistent wrapper;
1984 * use parseAsContent() if $interface is default value or false, or else
1985 * parseAsInterface() if $interface is true.
1986 */
1987 public function parse( $text, $linestart = true, $interface = false, $language = null ) {
1988 wfDeprecated( __METHOD__, '1.33' );
1989 return $this->parseInternal(
1990 $text, $this->getTitle(), $linestart, /*tidy*/false, $interface, $language
1991 )->getText( [
1992 'enableSectionEditLinks' => false,
1993 ] );
1994 }
1995
1996 /**
1997 * Parse wikitext *in the page content language* and return the HTML.
1998 * The result will be language-converted to the user's preferred variant.
1999 * Output will be tidy.
2000 *
2001 * @param string $text Wikitext in the page content language
2002 * @param bool $linestart Is this the start of a line? (Defaults to true)
2003 * @throws MWException
2004 * @return string HTML
2005 * @since 1.32
2006 */
2007 public function parseAsContent( $text, $linestart = true ) {
2008 return $this->parseInternal(
2009 $text, $this->getTitle(), $linestart, /*tidy*/true, /*interface*/false, /*language*/null
2010 )->getText( [
2011 'enableSectionEditLinks' => false,
2012 'wrapperDivClass' => ''
2013 ] );
2014 }
2015
2016 /**
2017 * Parse wikitext *in the user interface language* and return the HTML.
2018 * The result will not be language-converted, as user interface messages
2019 * are already localized into a specific variant.
2020 * Output will be tidy.
2021 *
2022 * @param string $text Wikitext in the user interface language
2023 * @param bool $linestart Is this the start of a line? (Defaults to true)
2024 * @throws MWException
2025 * @return string HTML
2026 * @since 1.32
2027 */
2028 public function parseAsInterface( $text, $linestart = true ) {
2029 return $this->parseInternal(
2030 $text, $this->getTitle(), $linestart, /*tidy*/true, /*interface*/true, /*language*/null
2031 )->getText( [
2032 'enableSectionEditLinks' => false,
2033 'wrapperDivClass' => ''
2034 ] );
2035 }
2036
2037 /**
2038 * Parse wikitext *in the user interface language*, strip
2039 * paragraph wrapper, and return the HTML.
2040 * The result will not be language-converted, as user interface messages
2041 * are already localized into a specific variant.
2042 * Output will be tidy. Outer paragraph wrapper will only be stripped
2043 * if the result is a single paragraph.
2044 *
2045 * @param string $text Wikitext in the user interface language
2046 * @param bool $linestart Is this the start of a line? (Defaults to true)
2047 * @throws MWException
2048 * @return string HTML
2049 * @since 1.32
2050 */
2051 public function parseInlineAsInterface( $text, $linestart = true ) {
2052 return Parser::stripOuterParagraph(
2053 $this->parseAsInterface( $text, $linestart )
2054 );
2055 }
2056
2057 /**
2058 * Parse wikitext, strip paragraph wrapper, and return the HTML.
2059 *
2060 * @param string $text
2061 * @param bool $linestart Is this the start of a line?
2062 * @param bool $interface Use interface language (instead of content language) while parsing
2063 * language sensitive magic words like GRAMMAR and PLURAL
2064 * @return string HTML
2065 * @deprecated since 1.32, due to untidy output and confusing default
2066 * for $interface. Use parseInlineAsInterface() if $interface is
2067 * the default value or false, or else use
2068 * Parser::stripOuterParagraph($outputPage->parseAsContent(...)).
2069 */
2070 public function parseInline( $text, $linestart = true, $interface = false ) {
2071 wfDeprecated( __METHOD__, '1.33' );
2072 $parsed = $this->parseInternal(
2073 $text, $this->getTitle(), $linestart, /*tidy*/false, $interface, /*language*/null
2074 )->getText( [
2075 'enableSectionEditLinks' => false,
2076 'wrapperDivClass' => '', /* no wrapper div */
2077 ] );
2078 return Parser::stripOuterParagraph( $parsed );
2079 }
2080
2081 /**
2082 * Parse wikitext and return the HTML (internal implementation helper)
2083 *
2084 * @param string $text
2085 * @param Title $title The title to use
2086 * @param bool $linestart Is this the start of a line?
2087 * @param bool $tidy Whether the output should be tidied
2088 * @param bool $interface Use interface language (instead of content language) while parsing
2089 * language sensitive magic words like GRAMMAR and PLURAL. This also disables
2090 * LanguageConverter.
2091 * @param Language|null $language Target language object, will override $interface
2092 * @throws MWException
2093 * @return ParserOutput
2094 */
2095 private function parseInternal( $text, $title, $linestart, $tidy, $interface, $language ) {
2096 if ( is_null( $title ) ) {
2097 throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
2098 }
2099
2100 $popts = $this->parserOptions();
2101 $oldTidy = $popts->setTidy( $tidy );
2102 $oldInterface = $popts->setInterfaceMessage( (bool)$interface );
2103
2104 if ( $language !== null ) {
2105 $oldLang = $popts->setTargetLanguage( $language );
2106 }
2107
2108 $parserOutput = MediaWikiServices::getInstance()->getParser()->getFreshParser()->parse(
2109 $text, $title, $popts,
2110 $linestart, true, $this->mRevisionId
2111 );
2112
2113 $popts->setTidy( $oldTidy );
2114 $popts->setInterfaceMessage( $oldInterface );
2115
2116 if ( $language !== null ) {
2117 $popts->setTargetLanguage( $oldLang );
2118 }
2119
2120 return $parserOutput;
2121 }
2122
2123 /**
2124 * Set the value of the "s-maxage" part of the "Cache-control" HTTP header
2125 *
2126 * @param int $maxage Maximum cache time on the CDN, in seconds.
2127 */
2128 public function setCdnMaxage( $maxage ) {
2129 $this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
2130 }
2131
2132 /**
2133 * Set the value of the "s-maxage" part of the "Cache-control" HTTP header to $maxage if that is
2134 * lower than the current s-maxage. Either way, $maxage is now an upper limit on s-maxage, so
2135 * that future calls to setCdnMaxage() will no longer be able to raise the s-maxage above
2136 * $maxage.
2137 *
2138 * @param int $maxage Maximum cache time on the CDN, in seconds
2139 * @since 1.27
2140 */
2141 public function lowerCdnMaxage( $maxage ) {
2142 $this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
2143 $this->setCdnMaxage( $this->mCdnMaxage );
2144 }
2145
2146 /**
2147 * Get TTL in [$minTTL,$maxTTL] in pass it to lowerCdnMaxage()
2148 *
2149 * This sets and returns $minTTL if $mtime is false or null. Otherwise,
2150 * the TTL is higher the older the $mtime timestamp is. Essentially, the
2151 * TTL is 90% of the age of the object, subject to the min and max.
2152 *
2153 * @param string|int|float|bool|null $mtime Last-Modified timestamp
2154 * @param int $minTTL Minimum TTL in seconds [default: 1 minute]
2155 * @param int $maxTTL Maximum TTL in seconds [default: $wgCdnMaxAge]
2156 * @since 1.28
2157 */
2158 public function adaptCdnTTL( $mtime, $minTTL = 0, $maxTTL = 0 ) {
2159 $minTTL = $minTTL ?: IExpiringStore::TTL_MINUTE;
2160 $maxTTL = $maxTTL ?: $this->getConfig()->get( 'CdnMaxAge' );
2161
2162 if ( $mtime === null || $mtime === false ) {
2163 return $minTTL; // entity does not exist
2164 }
2165
2166 $age = MWTimestamp::time() - wfTimestamp( TS_UNIX, $mtime );
2167 $adaptiveTTL = max( 0.9 * $age, $minTTL );
2168 $adaptiveTTL = min( $adaptiveTTL, $maxTTL );
2169
2170 $this->lowerCdnMaxage( (int)$adaptiveTTL );
2171 }
2172
2173 /**
2174 * Use enableClientCache(false) to force it to send nocache headers
2175 *
2176 * @param bool|null $state New value, or null to not set the value
2177 *
2178 * @return bool Old value
2179 */
2180 public function enableClientCache( $state ) {
2181 return wfSetVar( $this->mEnableClientCache, $state );
2182 }
2183
2184 /**
2185 * Get the list of cookie names that will influence the cache
2186 *
2187 * @return array
2188 */
2189 function getCacheVaryCookies() {
2190 if ( self::$cacheVaryCookies === null ) {
2191 $config = $this->getConfig();
2192 self::$cacheVaryCookies = array_values( array_unique( array_merge(
2193 SessionManager::singleton()->getVaryCookies(),
2194 [
2195 'forceHTTPS',
2196 ],
2197 $config->get( 'CacheVaryCookies' )
2198 ) ) );
2199 Hooks::run( 'GetCacheVaryCookies', [ $this, &self::$cacheVaryCookies ] );
2200 }
2201 return self::$cacheVaryCookies;
2202 }
2203
2204 /**
2205 * Check if the request has a cache-varying cookie header
2206 * If it does, it's very important that we don't allow public caching
2207 *
2208 * @return bool
2209 */
2210 function haveCacheVaryCookies() {
2211 $request = $this->getRequest();
2212 foreach ( $this->getCacheVaryCookies() as $cookieName ) {
2213 if ( $request->getCookie( $cookieName, '', '' ) !== '' ) {
2214 wfDebug( __METHOD__ . ": found $cookieName\n" );
2215 return true;
2216 }
2217 }
2218 wfDebug( __METHOD__ . ": no cache-varying cookies found\n" );
2219 return false;
2220 }
2221
2222 /**
2223 * Add an HTTP header that will influence on the cache
2224 *
2225 * @param string $header Header name
2226 * @param string[]|null $option Deprecated; formerly options for the
2227 * Key header, deprecated in 1.32 and removed in 1.34. See
2228 * https://datatracker.ietf.org/doc/draft-fielding-http-key/
2229 * for the list of formerly-valid options.
2230 */
2231 public function addVaryHeader( $header, array $option = null ) {
2232 if ( $option !== null && count( $option ) > 0 ) {
2233 wfDeprecated( 'addVaryHeader $option is ignored', '1.34' );
2234 }
2235 if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
2236 $this->mVaryHeader[$header] = null;
2237 }
2238 }
2239
2240 /**
2241 * Return a Vary: header on which to vary caches. Based on the keys of $mVaryHeader,
2242 * such as Accept-Encoding or Cookie
2243 *
2244 * @return string
2245 */
2246 public function getVaryHeader() {
2247 // If we vary on cookies, let's make sure it's always included here too.
2248 if ( $this->getCacheVaryCookies() ) {
2249 $this->addVaryHeader( 'Cookie' );
2250 }
2251
2252 foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2253 $this->addVaryHeader( $header, $options );
2254 }
2255 return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
2256 }
2257
2258 /**
2259 * Add an HTTP Link: header
2260 *
2261 * @param string $header Header value
2262 */
2263 public function addLinkHeader( $header ) {
2264 $this->mLinkHeader[] = $header;
2265 }
2266
2267 /**
2268 * Return a Link: header. Based on the values of $mLinkHeader.
2269 *
2270 * @return string
2271 */
2272 public function getLinkHeader() {
2273 if ( !$this->mLinkHeader ) {
2274 return false;
2275 }
2276
2277 return 'Link: ' . implode( ',', $this->mLinkHeader );
2278 }
2279
2280 /**
2281 * T23672: Add Accept-Language to Vary header if there's no 'variant' parameter in GET.
2282 *
2283 * For example:
2284 * /w/index.php?title=Main_page will vary based on Accept-Language; but
2285 * /w/index.php?title=Main_page&variant=zh-cn will not.
2286 */
2287 private function addAcceptLanguage() {
2288 $title = $this->getTitle();
2289 if ( !$title instanceof Title ) {
2290 return;
2291 }
2292
2293 $lang = $title->getPageLanguage();
2294 if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
2295 $this->addVaryHeader( 'Accept-Language' );
2296 }
2297 }
2298
2299 /**
2300 * Set a flag which will cause an X-Frame-Options header appropriate for
2301 * edit pages to be sent. The header value is controlled by
2302 * $wgEditPageFrameOptions.
2303 *
2304 * This is the default for special pages. If you display a CSRF-protected
2305 * form on an ordinary view page, then you need to call this function.
2306 *
2307 * @param bool $enable
2308 */
2309 public function preventClickjacking( $enable = true ) {
2310 $this->mPreventClickjacking = $enable;
2311 }
2312
2313 /**
2314 * Turn off frame-breaking. Alias for $this->preventClickjacking(false).
2315 * This can be called from pages which do not contain any CSRF-protected
2316 * HTML form.
2317 */
2318 public function allowClickjacking() {
2319 $this->mPreventClickjacking = false;
2320 }
2321
2322 /**
2323 * Get the prevent-clickjacking flag
2324 *
2325 * @since 1.24
2326 * @return bool
2327 */
2328 public function getPreventClickjacking() {
2329 return $this->mPreventClickjacking;
2330 }
2331
2332 /**
2333 * Get the X-Frame-Options header value (without the name part), or false
2334 * if there isn't one. This is used by Skin to determine whether to enable
2335 * JavaScript frame-breaking, for clients that don't support X-Frame-Options.
2336 *
2337 * @return string|false
2338 */
2339 public function getFrameOptions() {
2340 $config = $this->getConfig();
2341 if ( $config->get( 'BreakFrames' ) ) {
2342 return 'DENY';
2343 } elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) {
2344 return $config->get( 'EditPageFrameOptions' );
2345 }
2346 return false;
2347 }
2348
2349 /**
2350 * Get the Origin-Trial header values. This is used to enable Chrome Origin
2351 * Trials: https://github.com/GoogleChrome/OriginTrials
2352 *
2353 * @return array
2354 */
2355 private function getOriginTrials() {
2356 $config = $this->getConfig();
2357
2358 return $config->get( 'OriginTrials' );
2359 }
2360
2361 private function getReportTo() {
2362 $config = $this->getConfig();
2363
2364 $expiry = $config->get( 'ReportToExpiry' );
2365
2366 if ( !$expiry ) {
2367 return false;
2368 }
2369
2370 $endpoints = $config->get( 'ReportToEndpoints' );
2371
2372 if ( !$endpoints ) {
2373 return false;
2374 }
2375
2376 $output = [ 'max_age' => $expiry, 'endpoints' => [] ];
2377
2378 foreach ( $endpoints as $endpoint ) {
2379 $output['endpoints'][] = [ 'url' => $endpoint ];
2380 }
2381
2382 return json_encode( $output, JSON_UNESCAPED_SLASHES );
2383 }
2384
2385 private function getFeaturePolicyReportOnly() {
2386 $config = $this->getConfig();
2387
2388 $features = $config->get( 'FeaturePolicyReportOnly' );
2389 return implode( ';', $features );
2390 }
2391
2392 /**
2393 * Send cache control HTTP headers
2394 */
2395 public function sendCacheControl() {
2396 $response = $this->getRequest()->response();
2397 $config = $this->getConfig();
2398
2399 $this->addVaryHeader( 'Cookie' );
2400 $this->addAcceptLanguage();
2401
2402 # don't serve compressed data to clients who can't handle it
2403 # maintain different caches for logged-in users and non-logged in ones
2404 $response->header( $this->getVaryHeader() );
2405
2406 if ( $this->mEnableClientCache ) {
2407 if (
2408 $config->get( 'UseCdn' ) &&
2409 !$response->hasCookies() &&
2410 !SessionManager::getGlobalSession()->isPersistent() &&
2411 !$this->isPrintable() &&
2412 $this->mCdnMaxage != 0 &&
2413 !$this->haveCacheVaryCookies()
2414 ) {
2415 if ( $config->get( 'UseESI' ) ) {
2416 wfDeprecated( '$wgUseESI = true', '1.33' );
2417 # We'll purge the proxy cache explicitly, but require end user agents
2418 # to revalidate against the proxy on each visit.
2419 # Surrogate-Control controls our CDN, Cache-Control downstream caches
2420 wfDebug( __METHOD__ .
2421 ": proxy caching with ESI; {$this->mLastModified} **", 'private' );
2422 # start with a shorter timeout for initial testing
2423 # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
2424 $response->header(
2425 "Surrogate-Control: max-age={$config->get( 'CdnMaxAge' )}" .
2426 "+{$this->mCdnMaxage}, content=\"ESI/1.0\""
2427 );
2428 $response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
2429 } else {
2430 # We'll purge the proxy cache for anons explicitly, but require end user agents
2431 # to revalidate against the proxy on each visit.
2432 # IMPORTANT! The CDN needs to replace the Cache-Control header with
2433 # Cache-Control: s-maxage=0, must-revalidate, max-age=0
2434 wfDebug( __METHOD__ .
2435 ": local proxy caching; {$this->mLastModified} **", 'private' );
2436 # start with a shorter timeout for initial testing
2437 # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
2438 $response->header( "Cache-Control: " .
2439 "s-maxage={$this->mCdnMaxage}, must-revalidate, max-age=0" );
2440 }
2441 } else {
2442 # We do want clients to cache if they can, but they *must* check for updates
2443 # on revisiting the page.
2444 wfDebug( __METHOD__ . ": private caching; {$this->mLastModified} **", 'private' );
2445 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2446 $response->header( "Cache-Control: private, must-revalidate, max-age=0" );
2447 }
2448 if ( $this->mLastModified ) {
2449 $response->header( "Last-Modified: {$this->mLastModified}" );
2450 }
2451 } else {
2452 wfDebug( __METHOD__ . ": no caching **", 'private' );
2453
2454 # In general, the absence of a last modified header should be enough to prevent
2455 # the client from using its cache. We send a few other things just to make sure.
2456 $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2457 $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
2458 $response->header( 'Pragma: no-cache' );
2459 }
2460 }
2461
2462 /**
2463 * Transfer styles and JavaScript modules from skin.
2464 *
2465 * @param Skin $sk to load modules for
2466 */
2467 public function loadSkinModules( $sk ) {
2468 foreach ( $sk->getDefaultModules() as $group => $modules ) {
2469 if ( $group === 'styles' ) {
2470 foreach ( $modules as $key => $moduleMembers ) {
2471 $this->addModuleStyles( $moduleMembers );
2472 }
2473 } else {
2474 $this->addModules( $modules );
2475 }
2476 }
2477 }
2478
2479 /**
2480 * Finally, all the text has been munged and accumulated into
2481 * the object, let's actually output it:
2482 *
2483 * @param bool $return Set to true to get the result as a string rather than sending it
2484 * @return string|null
2485 * @throws Exception
2486 * @throws FatalError
2487 * @throws MWException
2488 */
2489 public function output( $return = false ) {
2490 if ( $this->mDoNothing ) {
2491 return $return ? '' : null;
2492 }
2493
2494 $response = $this->getRequest()->response();
2495 $config = $this->getConfig();
2496
2497 if ( $this->mRedirect != '' ) {
2498 # Standards require redirect URLs to be absolute
2499 $this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT );
2500
2501 $redirect = $this->mRedirect;
2502 $code = $this->mRedirectCode;
2503
2504 if ( Hooks::run( "BeforePageRedirect", [ $this, &$redirect, &$code ] ) ) {
2505 if ( $code == '301' || $code == '303' ) {
2506 if ( !$config->get( 'DebugRedirects' ) ) {
2507 $response->statusHeader( $code );
2508 }
2509 $this->mLastModified = wfTimestamp( TS_RFC2822 );
2510 }
2511 if ( $config->get( 'VaryOnXFP' ) ) {
2512 $this->addVaryHeader( 'X-Forwarded-Proto' );
2513 }
2514 $this->sendCacheControl();
2515
2516 $response->header( "Content-Type: text/html; charset=utf-8" );
2517 if ( $config->get( 'DebugRedirects' ) ) {
2518 $url = htmlspecialchars( $redirect );
2519 print "<!DOCTYPE html>\n<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
2520 print "<p>Location: <a href=\"$url\">$url</a></p>\n";
2521 print "</body>\n</html>\n";
2522 } else {
2523 $response->header( 'Location: ' . $redirect );
2524 }
2525 }
2526
2527 return $return ? '' : null;
2528 } elseif ( $this->mStatusCode ) {
2529 $response->statusHeader( $this->mStatusCode );
2530 }
2531
2532 # Buffer output; final headers may depend on later processing
2533 ob_start();
2534
2535 $response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
2536 $response->header( 'Content-language: ' .
2537 MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() );
2538
2539 $linkHeader = $this->getLinkHeader();
2540 if ( $linkHeader ) {
2541 $response->header( $linkHeader );
2542 }
2543
2544 // Prevent framing, if requested
2545 $frameOptions = $this->getFrameOptions();
2546 if ( $frameOptions ) {
2547 $response->header( "X-Frame-Options: $frameOptions" );
2548 }
2549
2550 $originTrials = $this->getOriginTrials();
2551 foreach ( $originTrials as $originTrial ) {
2552 $response->header( "Origin-Trial: $originTrial", false );
2553 }
2554
2555 $reportTo = $this->getReportTo();
2556 if ( $reportTo ) {
2557 $response->header( "Report-To: $reportTo" );
2558 }
2559
2560 $featurePolicyReportOnly = $this->getFeaturePolicyReportOnly();
2561 if ( $featurePolicyReportOnly ) {
2562 $response->header( "Feature-Policy-Report-Only: $featurePolicyReportOnly" );
2563 }
2564
2565 ContentSecurityPolicy::sendHeaders( $this );
2566
2567 if ( $this->mArticleBodyOnly ) {
2568 echo $this->mBodytext;
2569 } else {
2570 // Enable safe mode if requested (T152169)
2571 if ( $this->getRequest()->getBool( 'safemode' ) ) {
2572 $this->disallowUserJs();
2573 }
2574
2575 $sk = $this->getSkin();
2576 $this->loadSkinModules( $sk );
2577
2578 MWDebug::addModules( $this );
2579
2580 // Avoid PHP 7.1 warning of passing $this by reference
2581 $outputPage = $this;
2582 // Hook that allows last minute changes to the output page, e.g.
2583 // adding of CSS or Javascript by extensions.
2584 Hooks::runWithoutAbort( 'BeforePageDisplay', [ &$outputPage, &$sk ] );
2585
2586 try {
2587 $sk->outputPage();
2588 } catch ( Exception $e ) {
2589 ob_end_clean(); // bug T129657
2590 throw $e;
2591 }
2592 }
2593
2594 try {
2595 // This hook allows last minute changes to final overall output by modifying output buffer
2596 Hooks::runWithoutAbort( 'AfterFinalPageOutput', [ $this ] );
2597 } catch ( Exception $e ) {
2598 ob_end_clean(); // bug T129657
2599 throw $e;
2600 }
2601
2602 $this->sendCacheControl();
2603
2604 if ( $return ) {
2605 return ob_get_clean();
2606 } else {
2607 ob_end_flush();
2608 return null;
2609 }
2610 }
2611
2612 /**
2613 * Prepare this object to display an error page; disable caching and
2614 * indexing, clear the current text and redirect, set the page's title
2615 * and optionally an custom HTML title (content of the "<title>" tag).
2616 *
2617 * @param string|Message $pageTitle Will be passed directly to setPageTitle()
2618 * @param string|Message $htmlTitle Will be passed directly to setHTMLTitle();
2619 * optional, if not passed the "<title>" attribute will be
2620 * based on $pageTitle
2621 */
2622 public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
2623 $this->setPageTitle( $pageTitle );
2624 if ( $htmlTitle !== false ) {
2625 $this->setHTMLTitle( $htmlTitle );
2626 }
2627 $this->setRobotPolicy( 'noindex,nofollow' );
2628 $this->setArticleRelated( false );
2629 $this->enableClientCache( false );
2630 $this->mRedirect = '';
2631 $this->clearSubtitle();
2632 $this->clearHTML();
2633 }
2634
2635 /**
2636 * Output a standard error page
2637 *
2638 * showErrorPage( 'titlemsg', 'pagetextmsg' );
2639 * showErrorPage( 'titlemsg', 'pagetextmsg', [ 'param1', 'param2' ] );
2640 * showErrorPage( 'titlemsg', $messageObject );
2641 * showErrorPage( $titleMessageObject, $messageObject );
2642 *
2643 * @param string|Message $title Message key (string) for page title, or a Message object
2644 * @param string|Message $msg Message key (string) for page text, or a Message object
2645 * @param array $params Message parameters; ignored if $msg is a Message object
2646 */
2647 public function showErrorPage( $title, $msg, $params = [] ) {
2648 if ( !$title instanceof Message ) {
2649 $title = $this->msg( $title );
2650 }
2651
2652 $this->prepareErrorPage( $title );
2653
2654 if ( $msg instanceof Message ) {
2655 if ( $params !== [] ) {
2656 trigger_error( 'Argument ignored: $params. The message parameters argument '
2657 . 'is discarded when the $msg argument is a Message object instead of '
2658 . 'a string.', E_USER_NOTICE );
2659 }
2660 $this->addHTML( $msg->parseAsBlock() );
2661 } else {
2662 $this->addWikiMsgArray( $msg, $params );
2663 }
2664
2665 $this->returnToMain();
2666 }
2667
2668 /**
2669 * Output a standard permission error page
2670 *
2671 * @param array $errors Error message keys or [key, param...] arrays
2672 * @param string|null $action Action that was denied or null if unknown
2673 */
2674 public function showPermissionsErrorPage( array $errors, $action = null ) {
2675 foreach ( $errors as $key => $error ) {
2676 $errors[$key] = (array)$error;
2677 }
2678
2679 // For some action (read, edit, create and upload), display a "login to do this action"
2680 // error if all of the following conditions are met:
2681 // 1. the user is not logged in
2682 // 2. the only error is insufficient permissions (i.e. no block or something else)
2683 // 3. the error can be avoided simply by logging in
2684 if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] )
2685 && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
2686 && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
2687 && ( User::groupHasPermission( 'user', $action )
2688 || User::groupHasPermission( 'autoconfirmed', $action ) )
2689 ) {
2690 $displayReturnto = null;
2691
2692 # Due to T34276, if a user does not have read permissions,
2693 # $this->getTitle() will just give Special:Badtitle, which is
2694 # not especially useful as a returnto parameter. Use the title
2695 # from the request instead, if there was one.
2696 $request = $this->getRequest();
2697 $returnto = Title::newFromText( $request->getVal( 'title', '' ) );
2698 if ( $action == 'edit' ) {
2699 $msg = 'whitelistedittext';
2700 $displayReturnto = $returnto;
2701 } elseif ( $action == 'createpage' || $action == 'createtalk' ) {
2702 $msg = 'nocreatetext';
2703 } elseif ( $action == 'upload' ) {
2704 $msg = 'uploadnologintext';
2705 } else { # Read
2706 $msg = 'loginreqpagetext';
2707 $displayReturnto = Title::newMainPage();
2708 }
2709
2710 $query = [];
2711
2712 if ( $returnto ) {
2713 $query['returnto'] = $returnto->getPrefixedText();
2714
2715 if ( !$request->wasPosted() ) {
2716 $returntoquery = $request->getValues();
2717 unset( $returntoquery['title'] );
2718 unset( $returntoquery['returnto'] );
2719 unset( $returntoquery['returntoquery'] );
2720 $query['returntoquery'] = wfArrayToCgi( $returntoquery );
2721 }
2722 }
2723
2724 $services = MediaWikiServices::getInstance();
2725
2726 $title = SpecialPage::getTitleFor( 'Userlogin' );
2727 $linkRenderer = $services->getLinkRenderer();
2728 $loginUrl = $title->getLinkURL( $query, false, PROTO_RELATIVE );
2729 $loginLink = $linkRenderer->makeKnownLink(
2730 $title,
2731 $this->msg( 'loginreqlink' )->text(),
2732 [],
2733 $query
2734 );
2735
2736 $this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
2737 $this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->params( $loginUrl )->parse() );
2738
2739 $permissionManager = $services->getPermissionManager();
2740
2741 # Don't return to a page the user can't read otherwise
2742 # we'll end up in a pointless loop
2743 if ( $displayReturnto && $permissionManager->userCan(
2744 'read', $this->getUser(), $displayReturnto
2745 ) ) {
2746 $this->returnToMain( null, $displayReturnto );
2747 }
2748 } else {
2749 $this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
2750 $this->addWikiTextAsInterface( $this->formatPermissionsErrorMessage( $errors, $action ) );
2751 }
2752 }
2753
2754 /**
2755 * Display an error page indicating that a given version of MediaWiki is
2756 * required to use it
2757 *
2758 * @param mixed $version The version of MediaWiki needed to use the page
2759 */
2760 public function versionRequired( $version ) {
2761 $this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
2762
2763 $this->addWikiMsg( 'versionrequiredtext', $version );
2764 $this->returnToMain();
2765 }
2766
2767 /**
2768 * Format a list of error messages
2769 *
2770 * @param array $errors Array of arrays returned by Title::getUserPermissionsErrors
2771 * @param string|null $action Action that was denied or null if unknown
2772 * @return string The wikitext error-messages, formatted into a list.
2773 */
2774 public function formatPermissionsErrorMessage( array $errors, $action = null ) {
2775 if ( $action == null ) {
2776 $text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
2777 } else {
2778 $action_desc = $this->msg( "action-$action" )->plain();
2779 $text = $this->msg(
2780 'permissionserrorstext-withaction',
2781 count( $errors ),
2782 $action_desc
2783 )->plain() . "\n\n";
2784 }
2785
2786 if ( count( $errors ) > 1 ) {
2787 $text .= '<ul class="permissions-errors">' . "\n";
2788
2789 foreach ( $errors as $error ) {
2790 $text .= '<li>';
2791 $text .= $this->msg( ...$error )->plain();
2792 $text .= "</li>\n";
2793 }
2794 $text .= '</ul>';
2795 } else {
2796 $text .= "<div class=\"permissions-errors\">\n" .
2797 $this->msg( ...reset( $errors ) )->plain() .
2798 "\n</div>";
2799 }
2800
2801 return $text;
2802 }
2803
2804 /**
2805 * Show a warning about replica DB lag
2806 *
2807 * If the lag is higher than $wgSlaveLagCritical seconds,
2808 * then the warning is a bit more obvious. If the lag is
2809 * lower than $wgSlaveLagWarning, then no warning is shown.
2810 *
2811 * @param int $lag Replica lag
2812 */
2813 public function showLagWarning( $lag ) {
2814 $config = $this->getConfig();
2815 if ( $lag >= $config->get( 'SlaveLagWarning' ) ) {
2816 $lag = floor( $lag ); // floor to avoid nano seconds to display
2817 $message = $lag < $config->get( 'SlaveLagCritical' )
2818 ? 'lag-warn-normal'
2819 : 'lag-warn-high';
2820 $wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" );
2821 $this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] );
2822 }
2823 }
2824
2825 /**
2826 * Output an error page
2827 *
2828 * @note FatalError exception class provides an alternative.
2829 * @param string $message Error to output. Must be escaped for HTML.
2830 */
2831 public function showFatalError( $message ) {
2832 $this->prepareErrorPage( $this->msg( 'internalerror' ) );
2833
2834 $this->addHTML( $message );
2835 }
2836
2837 /**
2838 * Add a "return to" link pointing to a specified title
2839 *
2840 * @param Title $title Title to link
2841 * @param array $query Query string parameters
2842 * @param string|null $text Text of the link (input is not escaped)
2843 * @param array $options Options array to pass to Linker
2844 */
2845 public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) {
2846 $linkRenderer = MediaWikiServices::getInstance()
2847 ->getLinkRendererFactory()->createFromLegacyOptions( $options );
2848 $link = $this->msg( 'returnto' )->rawParams(
2849 $linkRenderer->makeLink( $title, $text, [], $query ) )->escaped();
2850 $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
2851 }
2852
2853 /**
2854 * Add a "return to" link pointing to a specified title,
2855 * or the title indicated in the request, or else the main page
2856 *
2857 * @param mixed|null $unused
2858 * @param Title|string|null $returnto Title or String to return to
2859 * @param string|null $returntoquery Query string for the return to link
2860 */
2861 public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
2862 if ( $returnto == null ) {
2863 $returnto = $this->getRequest()->getText( 'returnto' );
2864 }
2865
2866 if ( $returntoquery == null ) {
2867 $returntoquery = $this->getRequest()->getText( 'returntoquery' );
2868 }
2869
2870 if ( $returnto === '' ) {
2871 $returnto = Title::newMainPage();
2872 }
2873
2874 if ( is_object( $returnto ) ) {
2875 $titleObj = $returnto;
2876 } else {
2877 $titleObj = Title::newFromText( $returnto );
2878 }
2879 // We don't want people to return to external interwiki. That
2880 // might potentially be used as part of a phishing scheme
2881 if ( !is_object( $titleObj ) || $titleObj->isExternal() ) {
2882 $titleObj = Title::newMainPage();
2883 }
2884
2885 $this->addReturnTo( $titleObj, wfCgiToArray( $returntoquery ) );
2886 }
2887
2888 private function getRlClientContext() {
2889 if ( !$this->rlClientContext ) {
2890 $query = ResourceLoader::makeLoaderQuery(
2891 [], // modules; not relevant
2892 $this->getLanguage()->getCode(),
2893 $this->getSkin()->getSkinName(),
2894 $this->getUser()->isLoggedIn() ? $this->getUser()->getName() : null,
2895 null, // version; not relevant
2896 ResourceLoader::inDebugMode(),
2897 null, // only; not relevant
2898 $this->isPrintable(),
2899 $this->getRequest()->getBool( 'handheld' )
2900 );
2901 $this->rlClientContext = new ResourceLoaderContext(
2902 $this->getResourceLoader(),
2903 new FauxRequest( $query )
2904 );
2905 if ( $this->contentOverrideCallbacks ) {
2906 $this->rlClientContext = new DerivativeResourceLoaderContext( $this->rlClientContext );
2907 $this->rlClientContext->setContentOverrideCallback( function ( Title $title ) {
2908 foreach ( $this->contentOverrideCallbacks as $callback ) {
2909 $content = $callback( $title );
2910 if ( $content !== null ) {
2911 $text = ContentHandler::getContentText( $content );
2912 if ( strpos( $text, '</script>' ) !== false ) {
2913 // Proactively replace this so that we can display a message
2914 // to the user, instead of letting it go to Html::inlineScript(),
2915 // where it would be considered a server-side issue.
2916 $titleFormatted = $title->getPrefixedText();
2917 $content = new JavaScriptContent(
2918 Xml::encodeJsCall( 'mw.log.error', [
2919 "Cannot preview $titleFormatted due to script-closing tag."
2920 ] )
2921 );
2922 }
2923 return $content;
2924 }
2925 }
2926 return null;
2927 } );
2928 }
2929 }
2930 return $this->rlClientContext;
2931 }
2932
2933 /**
2934 * Call this to freeze the module queue and JS config and create a formatter.
2935 *
2936 * Depending on the Skin, this may get lazy-initialised in either headElement() or
2937 * getBottomScripts(). See SkinTemplate::prepareQuickTemplate(). Calling this too early may
2938 * cause unexpected side-effects since disallowUserJs() may be called at any time to change
2939 * the module filters retroactively. Skins and extension hooks may also add modules until very
2940 * late in the request lifecycle.
2941 *
2942 * @return ResourceLoaderClientHtml
2943 */
2944 public function getRlClient() {
2945 if ( !$this->rlClient ) {
2946 $context = $this->getRlClientContext();
2947 $rl = $this->getResourceLoader();
2948 $this->addModules( [
2949 'user',
2950 'user.options',
2951 'user.tokens',
2952 ] );
2953 $this->addModuleStyles( [
2954 'site.styles',
2955 'noscript',
2956 'user.styles',
2957 ] );
2958 $this->getSkin()->setupSkinUserCss( $this );
2959
2960 // Prepare exempt modules for buildExemptModules()
2961 $exemptGroups = [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ];
2962 $exemptStates = [];
2963 $moduleStyles = $this->getModuleStyles( /*filter*/ true );
2964
2965 // Preload getTitleInfo for isKnownEmpty calls below and in ResourceLoaderClientHtml
2966 // Separate user-specific batch for improved cache-hit ratio.
2967 $userBatch = [ 'user.styles', 'user' ];
2968 $siteBatch = array_diff( $moduleStyles, $userBatch );
2969 $dbr = wfGetDB( DB_REPLICA );
2970 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $siteBatch );
2971 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $userBatch );
2972
2973 // Filter out modules handled by buildExemptModules()
2974 $moduleStyles = array_filter( $moduleStyles,
2975 function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
2976 $module = $rl->getModule( $name );
2977 if ( $module ) {
2978 $group = $module->getGroup();
2979 if ( isset( $exemptGroups[$group] ) ) {
2980 $exemptStates[$name] = 'ready';
2981 if ( !$module->isKnownEmpty( $context ) ) {
2982 // E.g. Don't output empty <styles>
2983 $exemptGroups[$group][] = $name;
2984 }
2985 return false;
2986 }
2987 }
2988 return true;
2989 }
2990 );
2991 $this->rlExemptStyleModules = $exemptGroups;
2992
2993 $rlClient = new ResourceLoaderClientHtml( $context, [
2994 'target' => $this->getTarget(),
2995 'nonce' => $this->getCSPNonce(),
2996 // When 'safemode', disallowUserJs(), or reduceAllowedModules() is used
2997 // to only restrict modules to ORIGIN_CORE (ie. disallow ORIGIN_USER), the list of
2998 // modules enqueud for loading on this page is filtered to just those.
2999 // However, to make sure we also apply the restriction to dynamic dependencies and
3000 // lazy-loaded modules at run-time on the client-side, pass 'safemode' down to the
3001 // StartupModule so that the client-side registry will not contain any restricted
3002 // modules either. (T152169, T185303)
3003 'safemode' => ( $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
3004 <= ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
3005 ) ? '1' : null,
3006 ] );
3007 $rlClient->setConfig( $this->getJSVars() );
3008 $rlClient->setModules( $this->getModules( /*filter*/ true ) );
3009 $rlClient->setModuleStyles( $moduleStyles );
3010 $rlClient->setExemptStates( $exemptStates );
3011 $this->rlClient = $rlClient;
3012 }
3013 return $this->rlClient;
3014 }
3015
3016 /**
3017 * @param Skin $sk The given Skin
3018 * @param bool $includeStyle Unused
3019 * @return string The doctype, opening "<html>", and head element.
3020 */
3021 public function headElement( Skin $sk, $includeStyle = true ) {
3022 $userdir = $this->getLanguage()->getDir();
3023 $sitedir = MediaWikiServices::getInstance()->getContentLanguage()->getDir();
3024
3025 $pieces = [];
3026 $pieces[] = Html::htmlHeader( Sanitizer::mergeAttributes(
3027 $this->getRlClient()->getDocumentAttributes(),
3028 $sk->getHtmlElementAttributes()
3029 ) );
3030 $pieces[] = Html::openElement( 'head' );
3031
3032 if ( $this->getHTMLTitle() == '' ) {
3033 $this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() );
3034 }
3035
3036 if ( !Html::isXmlMimeType( $this->getConfig()->get( 'MimeType' ) ) ) {
3037 // Add <meta charset="UTF-8">
3038 // This should be before <title> since it defines the charset used by
3039 // text including the text inside <title>.
3040 // The spec recommends defining XHTML5's charset using the XML declaration
3041 // instead of meta.
3042 // Our XML declaration is output by Html::htmlHeader.
3043 // https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-content-type
3044 // https://html.spec.whatwg.org/multipage/semantics.html#charset
3045 $pieces[] = Html::element( 'meta', [ 'charset' => 'UTF-8' ] );
3046 }
3047
3048 $pieces[] = Html::element( 'title', null, $this->getHTMLTitle() );
3049 $pieces[] = $this->getRlClient()->getHeadHtml();
3050 $pieces[] = $this->buildExemptModules();
3051 $pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
3052 $pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
3053
3054 // Use an IE conditional comment to serve the script only to old IE
3055 $pieces[] = '<!--[if lt IE 9]>' .
3056 ResourceLoaderClientHtml::makeLoad(
3057 new ResourceLoaderContext(
3058 $this->getResourceLoader(),
3059 new FauxRequest( [] )
3060 ),
3061 [ 'html5shiv' ],
3062 ResourceLoaderModule::TYPE_SCRIPTS,
3063 [ 'raw' => '1', 'sync' => '1' ],
3064 $this->getCSPNonce()
3065 ) .
3066 '<![endif]-->';
3067
3068 $pieces[] = Html::closeElement( 'head' );
3069
3070 $bodyClasses = $this->mAdditionalBodyClasses;
3071 $bodyClasses[] = 'mediawiki';
3072
3073 # Classes for LTR/RTL directionality support
3074 $bodyClasses[] = $userdir;
3075 $bodyClasses[] = "sitedir-$sitedir";
3076
3077 $underline = $this->getUser()->getOption( 'underline' );
3078 if ( $underline < 2 ) {
3079 // The following classes can be used here:
3080 // * mw-underline-always
3081 // * mw-underline-never
3082 $bodyClasses[] = 'mw-underline-' . ( $underline ? 'always' : 'never' );
3083 }
3084
3085 if ( $this->getLanguage()->capitalizeAllNouns() ) {
3086 # A <body> class is probably not the best way to do this . . .
3087 $bodyClasses[] = 'capitalize-all-nouns';
3088 }
3089
3090 // Parser feature migration class
3091 // The idea is that this will eventually be removed, after the wikitext
3092 // which requires it is cleaned up.
3093 $bodyClasses[] = 'mw-hide-empty-elt';
3094
3095 $bodyClasses[] = $sk->getPageClasses( $this->getTitle() );
3096 $bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
3097 $bodyClasses[] =
3098 'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
3099
3100 $bodyAttrs = [];
3101 // While the implode() is not strictly needed, it's used for backwards compatibility
3102 // (this used to be built as a string and hooks likely still expect that).
3103 $bodyAttrs['class'] = implode( ' ', $bodyClasses );
3104
3105 // Allow skins and extensions to add body attributes they need
3106 $sk->addToBodyAttributes( $this, $bodyAttrs );
3107 Hooks::run( 'OutputPageBodyAttributes', [ $this, $sk, &$bodyAttrs ] );
3108
3109 $pieces[] = Html::openElement( 'body', $bodyAttrs );
3110
3111 return self::combineWrappedStrings( $pieces );
3112 }
3113
3114 /**
3115 * Get a ResourceLoader object associated with this OutputPage
3116 *
3117 * @return ResourceLoader
3118 */
3119 public function getResourceLoader() {
3120 if ( is_null( $this->mResourceLoader ) ) {
3121 // Lazy-initialise as needed
3122 $this->mResourceLoader = MediaWikiServices::getInstance()->getResourceLoader();
3123 }
3124 return $this->mResourceLoader;
3125 }
3126
3127 /**
3128 * Explicily load or embed modules on a page.
3129 *
3130 * @param array|string $modules One or more module names
3131 * @param string $only ResourceLoaderModule TYPE_ class constant
3132 * @param array $extraQuery [optional] Array with extra query parameters for the request
3133 * @return string|WrappedStringList HTML
3134 */
3135 public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
3136 // Apply 'target' and 'origin' filters
3137 $modules = $this->filterModules( (array)$modules, null, $only );
3138
3139 return ResourceLoaderClientHtml::makeLoad(
3140 $this->getRlClientContext(),
3141 $modules,
3142 $only,
3143 $extraQuery,
3144 $this->getCSPNonce()
3145 );
3146 }
3147
3148 /**
3149 * Combine WrappedString chunks and filter out empty ones
3150 *
3151 * @param array $chunks
3152 * @return string|WrappedStringList HTML
3153 */
3154 protected static function combineWrappedStrings( array $chunks ) {
3155 // Filter out empty values
3156 $chunks = array_filter( $chunks, 'strlen' );
3157 return WrappedString::join( "\n", $chunks );
3158 }
3159
3160 /**
3161 * JS stuff to put at the bottom of the `<body>`.
3162 * These are legacy scripts ($this->mScripts), and user JS.
3163 *
3164 * @return string|WrappedStringList HTML
3165 */
3166 public function getBottomScripts() {
3167 $chunks = [];
3168 $chunks[] = $this->getRlClient()->getBodyHtml();
3169
3170 // Legacy non-ResourceLoader scripts
3171 $chunks[] = $this->mScripts;
3172
3173 if ( $this->limitReportJSData ) {
3174 $chunks[] = ResourceLoader::makeInlineScript(
3175 ResourceLoader::makeConfigSetScript(
3176 [ 'wgPageParseReport' => $this->limitReportJSData ]
3177 ),
3178 $this->getCSPNonce()
3179 );
3180 }
3181
3182 return self::combineWrappedStrings( $chunks );
3183 }
3184
3185 /**
3186 * Get the javascript config vars to include on this page
3187 *
3188 * @return array Array of javascript config vars
3189 * @since 1.23
3190 */
3191 public function getJsConfigVars() {
3192 return $this->mJsConfigVars;
3193 }
3194
3195 /**
3196 * Add one or more variables to be set in mw.config in JavaScript
3197 *
3198 * @param string|array $keys Key or array of key/value pairs
3199 * @param mixed|null $value [optional] Value of the configuration variable
3200 */
3201 public function addJsConfigVars( $keys, $value = null ) {
3202 if ( is_array( $keys ) ) {
3203 foreach ( $keys as $key => $value ) {
3204 $this->mJsConfigVars[$key] = $value;
3205 }
3206 return;
3207 }
3208
3209 $this->mJsConfigVars[$keys] = $value;
3210 }
3211
3212 /**
3213 * Get an array containing the variables to be set in mw.config in JavaScript.
3214 *
3215 * Do not add things here which can be evaluated in ResourceLoaderStartUpModule
3216 * - in other words, page-independent/site-wide variables (without state).
3217 * You will only be adding bloat to the html page and causing page caches to
3218 * have to be purged on configuration changes.
3219 * @return array
3220 */
3221 public function getJSVars() {
3222 $curRevisionId = 0;
3223 $articleId = 0;
3224 $canonicalSpecialPageName = false; # T23115
3225 $services = MediaWikiServices::getInstance();
3226
3227 $title = $this->getTitle();
3228 $ns = $title->getNamespace();
3229 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
3230 $canonicalNamespace = $nsInfo->exists( $ns )
3231 ? $nsInfo->getCanonicalName( $ns )
3232 : $title->getNsText();
3233
3234 $sk = $this->getSkin();
3235 // Get the relevant title so that AJAX features can use the correct page name
3236 // when making API requests from certain special pages (T36972).
3237 $relevantTitle = $sk->getRelevantTitle();
3238 $relevantUser = $sk->getRelevantUser();
3239
3240 if ( $ns == NS_SPECIAL ) {
3241 list( $canonicalSpecialPageName, /*...*/ ) =
3242 $services->getSpecialPageFactory()->
3243 resolveAlias( $title->getDBkey() );
3244 } elseif ( $this->canUseWikiPage() ) {
3245 $wikiPage = $this->getWikiPage();
3246 $curRevisionId = $wikiPage->getLatest();
3247 $articleId = $wikiPage->getId();
3248 }
3249
3250 $lang = $title->getPageViewLanguage();
3251
3252 // Pre-process information
3253 $separatorTransTable = $lang->separatorTransformTable();
3254 $separatorTransTable = $separatorTransTable ?: [];
3255 $compactSeparatorTransTable = [
3256 implode( "\t", array_keys( $separatorTransTable ) ),
3257 implode( "\t", $separatorTransTable ),
3258 ];
3259 $digitTransTable = $lang->digitTransformTable();
3260 $digitTransTable = $digitTransTable ?: [];
3261 $compactDigitTransTable = [
3262 implode( "\t", array_keys( $digitTransTable ) ),
3263 implode( "\t", $digitTransTable ),
3264 ];
3265
3266 $user = $this->getUser();
3267
3268 $vars = [
3269 'wgCanonicalNamespace' => $canonicalNamespace,
3270 'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
3271 'wgNamespaceNumber' => $title->getNamespace(),
3272 'wgPageName' => $title->getPrefixedDBkey(),
3273 'wgTitle' => $title->getText(),
3274 'wgCurRevisionId' => $curRevisionId,
3275 'wgRevisionId' => (int)$this->getRevisionId(),
3276 'wgArticleId' => $articleId,
3277 'wgIsArticle' => $this->isArticle(),
3278 'wgIsRedirect' => $title->isRedirect(),
3279 'wgAction' => Action::getActionName( $this->getContext() ),
3280 'wgUserName' => $user->isAnon() ? null : $user->getName(),
3281 'wgUserGroups' => $user->getEffectiveGroups(),
3282 'wgCategories' => $this->getCategories(),
3283 'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
3284 'wgPageContentLanguage' => $lang->getCode(),
3285 'wgPageContentModel' => $title->getContentModel(),
3286 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
3287 'wgDigitTransformTable' => $compactDigitTransTable,
3288 'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
3289 'wgMonthNames' => $lang->getMonthNamesArray(),
3290 'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(),
3291 'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
3292 'wgRelevantArticleId' => $relevantTitle->getArticleID(),
3293 'wgRequestId' => WebRequest::getRequestId(),
3294 'wgCSPNonce' => $this->getCSPNonce(),
3295 ];
3296
3297 if ( $user->isLoggedIn() ) {
3298 $vars['wgUserId'] = $user->getId();
3299 $vars['wgUserEditCount'] = $user->getEditCount();
3300 $userReg = $user->getRegistration();
3301 $vars['wgUserRegistration'] = $userReg ? wfTimestamp( TS_UNIX, $userReg ) * 1000 : null;
3302 // Get the revision ID of the oldest new message on the user's talk
3303 // page. This can be used for constructing new message alerts on
3304 // the client side.
3305 $vars['wgUserNewMsgRevisionId'] = $user->getNewMessageRevisionId();
3306 }
3307
3308 $contLang = $services->getContentLanguage();
3309 if ( $contLang->hasVariants() ) {
3310 $vars['wgUserVariant'] = $contLang->getPreferredVariant();
3311 }
3312 // Same test as SkinTemplate
3313 $vars['wgIsProbablyEditable'] = $title->quickUserCan( 'edit', $user )
3314 && ( $title->exists() || $title->quickUserCan( 'create', $user ) );
3315
3316 $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle
3317 && $relevantTitle->quickUserCan( 'edit', $user )
3318 && ( $relevantTitle->exists() || $relevantTitle->quickUserCan( 'create', $user ) );
3319
3320 foreach ( $title->getRestrictionTypes() as $type ) {
3321 // Following keys are set in $vars:
3322 // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload
3323 $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
3324 }
3325
3326 if ( $title->isMainPage() ) {
3327 $vars['wgIsMainPage'] = true;
3328 }
3329
3330 if ( $this->mRedirectedFrom ) {
3331 $vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
3332 }
3333
3334 if ( $relevantUser ) {
3335 $vars['wgRelevantUserName'] = $relevantUser->getName();
3336 }
3337
3338 // Allow extensions to add their custom variables to the mw.config map.
3339 // Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
3340 // page-dependant but site-wide (without state).
3341 // Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
3342 Hooks::run( 'MakeGlobalVariablesScript', [ &$vars, $this ] );
3343
3344 // Merge in variables from addJsConfigVars last
3345 return array_merge( $vars, $this->getJsConfigVars() );
3346 }
3347
3348 /**
3349 * To make it harder for someone to slip a user a fake
3350 * JavaScript or CSS preview, a random token
3351 * is associated with the login session. If it's not
3352 * passed back with the preview request, we won't render
3353 * the code.
3354 *
3355 * @return bool
3356 */
3357 public function userCanPreview() {
3358 $request = $this->getRequest();
3359 if (
3360 $request->getVal( 'action' ) !== 'submit' ||
3361 !$request->wasPosted()
3362 ) {
3363 return false;
3364 }
3365
3366 $user = $this->getUser();
3367
3368 if ( !$user->isLoggedIn() ) {
3369 // Anons have predictable edit tokens
3370 return false;
3371 }
3372 if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
3373 return false;
3374 }
3375
3376 $title = $this->getTitle();
3377 $errors = $title->getUserPermissionsErrors( 'edit', $user );
3378 if ( count( $errors ) !== 0 ) {
3379 return false;
3380 }
3381
3382 return true;
3383 }
3384
3385 /**
3386 * @return array Array in format "link name or number => 'link html'".
3387 */
3388 public function getHeadLinksArray() {
3389 global $wgVersion;
3390
3391 $tags = [];
3392 $config = $this->getConfig();
3393
3394 $canonicalUrl = $this->mCanonicalUrl;
3395
3396 $tags['meta-generator'] = Html::element( 'meta', [
3397 'name' => 'generator',
3398 'content' => "MediaWiki $wgVersion",
3399 ] );
3400
3401 if ( $config->get( 'ReferrerPolicy' ) !== false ) {
3402 // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
3403 // fallbacks should come before the primary value so we need to reverse the array.
3404 foreach ( array_reverse( (array)$config->get( 'ReferrerPolicy' ) ) as $i => $policy ) {
3405 $tags["meta-referrer-$i"] = Html::element( 'meta', [
3406 'name' => 'referrer',
3407 'content' => $policy,
3408 ] );
3409 }
3410 }
3411
3412 $p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
3413 if ( $p !== 'index,follow' ) {
3414 // http://www.robotstxt.org/wc/meta-user.html
3415 // Only show if it's different from the default robots policy
3416 $tags['meta-robots'] = Html::element( 'meta', [
3417 'name' => 'robots',
3418 'content' => $p,
3419 ] );
3420 }
3421
3422 foreach ( $this->mMetatags as $tag ) {
3423 if ( strncasecmp( $tag[0], 'http:', 5 ) === 0 ) {
3424 $a = 'http-equiv';
3425 $tag[0] = substr( $tag[0], 5 );
3426 } elseif ( strncasecmp( $tag[0], 'og:', 3 ) === 0 ) {
3427 $a = 'property';
3428 } else {
3429 $a = 'name';
3430 }
3431 $tagName = "meta-{$tag[0]}";
3432 if ( isset( $tags[$tagName] ) ) {
3433 $tagName .= $tag[1];
3434 }
3435 $tags[$tagName] = Html::element( 'meta',
3436 [
3437 $a => $tag[0],
3438 'content' => $tag[1]
3439 ]
3440 );
3441 }
3442
3443 foreach ( $this->mLinktags as $tag ) {
3444 $tags[] = Html::element( 'link', $tag );
3445 }
3446
3447 # Universal edit button
3448 if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
3449 $user = $this->getUser();
3450 if ( $this->getTitle()->quickUserCan( 'edit', $user )
3451 && ( $this->getTitle()->exists() ||
3452 $this->getTitle()->quickUserCan( 'create', $user ) )
3453 ) {
3454 // Original UniversalEditButton
3455 $msg = $this->msg( 'edit' )->text();
3456 $tags['universal-edit-button'] = Html::element( 'link', [
3457 'rel' => 'alternate',
3458 'type' => 'application/x-wiki',
3459 'title' => $msg,
3460 'href' => $this->getTitle()->getEditURL(),
3461 ] );
3462 // Alternate edit link
3463 $tags['alternative-edit'] = Html::element( 'link', [
3464 'rel' => 'edit',
3465 'title' => $msg,
3466 'href' => $this->getTitle()->getEditURL(),
3467 ] );
3468 }
3469 }
3470
3471 # Generally the order of the favicon and apple-touch-icon links
3472 # should not matter, but Konqueror (3.5.9 at least) incorrectly
3473 # uses whichever one appears later in the HTML source. Make sure
3474 # apple-touch-icon is specified first to avoid this.
3475 if ( $config->get( 'AppleTouchIcon' ) !== false ) {
3476 $tags['apple-touch-icon'] = Html::element( 'link', [
3477 'rel' => 'apple-touch-icon',
3478 'href' => $config->get( 'AppleTouchIcon' )
3479 ] );
3480 }
3481
3482 if ( $config->get( 'Favicon' ) !== false ) {
3483 $tags['favicon'] = Html::element( 'link', [
3484 'rel' => 'shortcut icon',
3485 'href' => $config->get( 'Favicon' )
3486 ] );
3487 }
3488
3489 # OpenSearch description link
3490 $tags['opensearch'] = Html::element( 'link', [
3491 'rel' => 'search',
3492 'type' => 'application/opensearchdescription+xml',
3493 'href' => wfScript( 'opensearch_desc' ),
3494 'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
3495 ] );
3496
3497 # Real Simple Discovery link, provides auto-discovery information
3498 # for the MediaWiki API (and potentially additional custom API
3499 # support such as WordPress or Twitter-compatible APIs for a
3500 # blogging extension, etc)
3501 $tags['rsd'] = Html::element( 'link', [
3502 'rel' => 'EditURI',
3503 'type' => 'application/rsd+xml',
3504 // Output a protocol-relative URL here if $wgServer is protocol-relative.
3505 // Whether RSD accepts relative or protocol-relative URLs is completely
3506 // undocumented, though.
3507 'href' => wfExpandUrl( wfAppendQuery(
3508 wfScript( 'api' ),
3509 [ 'action' => 'rsd' ] ),
3510 PROTO_RELATIVE
3511 ),
3512 ] );
3513
3514 # Language variants
3515 if ( !$config->get( 'DisableLangConversion' ) ) {
3516 $lang = $this->getTitle()->getPageLanguage();
3517 if ( $lang->hasVariants() ) {
3518 $variants = $lang->getVariants();
3519 foreach ( $variants as $variant ) {
3520 $tags["variant-$variant"] = Html::element( 'link', [
3521 'rel' => 'alternate',
3522 'hreflang' => LanguageCode::bcp47( $variant ),
3523 'href' => $this->getTitle()->getLocalURL(
3524 [ 'variant' => $variant ] )
3525 ]
3526 );
3527 }
3528 # x-default link per https://support.google.com/webmasters/answer/189077?hl=en
3529 $tags["variant-x-default"] = Html::element( 'link', [
3530 'rel' => 'alternate',
3531 'hreflang' => 'x-default',
3532 'href' => $this->getTitle()->getLocalURL() ] );
3533 }
3534 }
3535
3536 # Copyright
3537 if ( $this->copyrightUrl !== null ) {
3538 $copyright = $this->copyrightUrl;
3539 } else {
3540 $copyright = '';
3541 if ( $config->get( 'RightsPage' ) ) {
3542 $copy = Title::newFromText( $config->get( 'RightsPage' ) );
3543
3544 if (