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