Completed
Branch master (6ee3f9)
by
unknown
29:15
created

OutputPage   F

Complexity

Total Complexity 452

Size/Duplication

Total Lines 3810
Duplicated Lines 0.87 %

Coupling/Cohesion

Components 1
Dependencies 48

Importance

Changes 0
Metric Value
dl 33
loc 3810
rs 0.5217
c 0
b 0
f 0
wmc 452
lcom 1
cbo 48

177 Methods

Rating   Name   Duplication   Size   Complexity  
A addParserOutputContent() 0 9 1
A addParserOutputText() 0 5 1
A addParserOutput() 0 11 2
A addTemplate() 0 3 1
B parse() 0 29 6
A parseInline() 0 4 1
A setSquidMaxage() 0 3 1
A setCdnMaxage() 0 3 1
A lowerCdnMaxage() 0 4 1
A enableClientCache() 0 3 1
A getCacheVaryCookies() 0 15 2
A haveCacheVaryCookies() 0 11 3
A addVaryHeader() 0 9 3
A getVaryHeader() 0 11 3
B getKeyHeader() 0 25 6
C addAcceptLanguage() 0 28 7
A preventClickjacking() 0 3 1
A allowClickjacking() 0 3 1
A getPreventClickjacking() 0 3 1
A getFrameOptions() 0 9 4
A __construct() 0 8 2
A redirect() 0 5 1
A getRedirect() 0 3 1
A setCopyrightUrl() 0 3 1
A setStatusCode() 0 3 1
A addMeta() 0 3 1
A getMetaTags() 0 3 1
A addLink() 0 3 1
A getLinkTags() 0 3 1
A addMetadataLink() 0 4 1
A setCanonicalUrl() 0 3 1
A getCanonicalUrl() 0 3 1
A getMetadataAttribute() 0 10 2
A addScript() 0 3 1
A addExtensionStyle() 0 4 1
A getExtStyle() 0 4 1
A addScriptFile() 0 12 4
A addInlineScript() 0 3 1
B filterModules() 0 17 8
A getModules() 0 8 2
A addModules() 0 3 1
A getModuleScripts() 0 5 1
A addModuleScripts() 0 3 1
A getModuleStyles() 0 5 1
A addModuleStyles() 0 3 1
A getTarget() 0 3 1
A setTarget() 0 3 1
A getHeadItemsArray() 0 3 1
A addHeadItem() 0 3 1
A addHeadItems() 0 3 1
A hasHeadItem() 0 3 1
A setETag() 0 2 1
A setArticleBodyOnly() 0 3 1
A getArticleBodyOnly() 0 3 1
A setProperty() 0 3 1
A getProperty() 0 7 2
C checkLastModified() 0 80 10
A setLastModified() 0 3 1
A setRobotPolicy() 0 10 3
A setIndexPolicy() 0 6 2
A setFollowPolicy() 0 6 2
A setPageTitleActionText() 0 3 1
A getPageTitleActionText() 0 3 1
A setHTMLTitle() 0 7 2
A getHTMLTitle() 0 3 1
A setRedirectedFrom() 0 3 1
A setPageTitle() 0 16 2
A getPageTitle() 0 3 1
A setTitle() 0 3 1
A setSubtitle() 0 4 1
A addSubtitle() 0 7 2
A buildBacklinkSubtitle() 0 7 2
A addBacklinkSubtitle() 0 3 1
A clearSubtitle() 0 3 1
A getSubtitle() 0 3 1
A setPrintable() 0 3 1
A isPrintable() 0 3 1
A disable() 0 3 1
A isDisabled() 0 3 1
A showNewSectionLink() 0 3 1
A forceHideNewSectionLink() 0 3 1
A setSyndicated() 0 7 2
A setFeedAppendQuery() 0 11 3
A addFeedLink() 0 5 2
A isSyndicated() 0 3 1
A getSyndicationLinks() 0 3 1
A getFeedAppendQuery() 0 3 1
A setArticleFlag() 0 6 2
A isArticle() 0 3 1
A setArticleRelated() 0 6 2
A isArticleRelated() 0 3 1
A addLanguageLinks() 0 3 1
A setLanguageLinks() 0 3 1
A getLanguageLinks() 0 3 1
C addCategoryLinks() 0 66 10
A setCategoryLinks() 0 4 1
A getCategoryLinks() 0 3 1
A getCategories() 0 3 1
A setIndicators() 0 5 1
A getIndicators() 0 3 1
A addHelpLink() 0 23 2
A disallowUserJs() 0 18 2
A getAllowedModules() 0 9 3
A reduceAllowedModules() 0 3 1
A prependHTML() 0 3 1
A addHTML() 0 3 1
A addElement() 0 3 1
A clearHTML() 0 3 1
A getHTML() 0 3 1
D parserOptions() 0 38 10
A setRevisionId() 0 4 2
A getRevisionId() 0 3 1
A setRevisionTimestamp() 0 3 1
A getRevisionTimestamp() 0 3 1
A setFileVersion() 0 7 3
A getFileVersion() 0 3 1
A getTemplateIds() 0 3 1
A getFileSearchOptions() 0 3 1
A addWikiText() 0 7 2
A addWikiTextWithTitle() 0 3 1
A addWikiTextTitleTidy() 0 3 1
A addWikiTextTidy() 0 4 1
A addWikiTextTitle() 0 19 1
A addParserOutputNoText() 0 4 1
C addParserOutputMetadata() 0 57 10
C sendCacheControl() 16 65 11
D output() 0 116 19
A prepareErrorPage() 0 12 2
A showErrorPage() 0 20 4
C showPermissionsErrorPage() 7 65 17
A versionRequired() 0 6 1
B formatPermissionsErrorMessage() 0 29 4
A readOnlyPage() 0 7 2
A rateLimited() 0 4 1
A showLagWarning() 0 11 3
A showFatalError() 0 5 1
A showUnexpectedValueError() 0 3 1
A showFileCopyError() 0 3 1
A showFileRenameError() 0 3 1
A showFileDeleteError() 0 3 1
A showFileNotFoundError() 0 3 1
A addReturnTo() 0 5 1
B returnToMain() 0 24 6
A getRlClientContext() 0 20 3
C getRlClient() 0 78 11
B headElement() 0 71 4
A getResourceLoader() 0 9 2
A makeResourceLoaderLink() 0 11 1
A combineWrappedStrings() 0 5 1
A isUserJsPreview() 0 6 4
A isUserCssPreview() 0 6 4
B getBottomScripts() 0 52 4
A getJsConfigVars() 0 3 1
A addJsConfigVars() 10 10 3
F getJSVars() 0 117 16
D userCanPreview() 0 36 10
F getHeadLinksArray() 0 261 35
A getHeadLinks() 0 4 1
A feedLink() 0 8 1
A addStyle() 0 13 4
A addInlineStyle() 0 7 3
B buildExemptModules() 0 49 4
A buildCssLinksArray() 0 17 4
D styleLink() 0 34 9
A transformResourcePath() 0 17 3
A transformFilePath() 0 8 2
C transformCssMedia() 0 38 7
A addWikiMsg() 0 5 1
A addWikiMsgArray() 0 3 1
B wrapWikiMsg() 0 24 4
A enableTOC() 0 3 1
A isTOCEnabled() 0 3 1
A enableSectionEditLinks() 0 3 1
A sectionEditLinksEnabled() 0 3 1
A setupOOUI() 0 10 2
A enableOOUI() 0 13 1
A setLimitReportData() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like OutputPage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OutputPage, and based on these observations, apply Extract Interface, too.

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\Session\SessionManager;
25
use WrappedString\WrappedString;
26
use WrappedString\WrappedStringList;
27
28
/**
29
 * This class should be covered by a general architecture document which does
30
 * not exist as of January 2011.  This is one of the Core classes and should
31
 * be read at least once by any new developers.
32
 *
33
 * This class is used to prepare the final rendering. A skin is then
34
 * applied to the output parameters (links, javascript, html, categories ...).
35
 *
36
 * @todo FIXME: Another class handles sending the whole page to the client.
37
 *
38
 * Some comments comes from a pairing session between Zak Greant and Antoine Musso
39
 * in November 2010.
40
 *
41
 * @todo document
42
 */
43
class OutputPage extends ContextSource {
44
	/** @var array Should be private. Used with addMeta() which adds "<meta>" */
45
	protected $mMetatags = [];
46
47
	/** @var array */
48
	protected $mLinktags = [];
49
50
	/** @var bool */
51
	protected $mCanonicalUrl = false;
52
53
	/**
54
	 * @var array Additional stylesheets. Looks like this is for extensions.
55
	 *   Might be replaced by ResourceLoader.
56
	 */
57
	protected $mExtStyles = [];
58
59
	/**
60
	 * @var string Should be private - has getter and setter. Contains
61
	 *   the HTML title */
62
	public $mPagetitle = '';
63
64
	/**
65
	 * @var string Contains all of the "<body>" content. Should be private we
66
	 *   got set/get accessors and the append() method.
67
	 */
68
	public $mBodytext = '';
69
70
	/** @var string Stores contents of "<title>" tag */
71
	private $mHTMLtitle = '';
72
73
	/**
74
	 * @var bool Is the displayed content related to the source of the
75
	 *   corresponding wiki article.
76
	 */
77
	private $mIsarticle = false;
78
79
	/** @var bool Stores "article flag" toggle. */
80
	private $mIsArticleRelated = true;
81
82
	/**
83
	 * @var bool We have to set isPrintable(). Some pages should
84
	 * never be printed (ex: redirections).
85
	 */
86
	private $mPrintable = false;
87
88
	/**
89
	 * @var array Contains the page subtitle. Special pages usually have some
90
	 *   links here. Don't confuse with site subtitle added by skins.
91
	 */
92
	private $mSubtitle = [];
93
94
	/** @var string */
95
	public $mRedirect = '';
96
97
	/** @var int */
98
	protected $mStatusCode;
99
100
	/**
101
	 * @var string Used for sending cache control.
102
	 *   The whole caching system should probably be moved into its own class.
103
	 */
104
	protected $mLastModified = '';
105
106
	/** @var array */
107
	protected $mCategoryLinks = [];
108
109
	/** @var array */
110
	protected $mCategories = [];
111
112
	/** @var array */
113
	protected $mIndicators = [];
114
115
	/** @var array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page') */
116
	private $mLanguageLinks = [];
117
118
	/**
119
	 * Used for JavaScript (predates ResourceLoader)
120
	 * @todo We should split JS / CSS.
121
	 * mScripts content is inserted as is in "<head>" by Skin. This might
122
	 * contain either a link to a stylesheet or inline CSS.
123
	 */
124
	private $mScripts = '';
125
126
	/** @var string Inline CSS styles. Use addInlineStyle() sparingly */
127
	protected $mInlineStyles = '';
128
129
	/**
130
	 * @var string Used by skin template.
131
	 * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle );
132
	 */
133
	public $mPageLinkTitle = '';
134
135
	/** @var array Array of elements in "<head>". Parser might add its own headers! */
136
	protected $mHeadItems = [];
137
138
	/** @var array */
139
	protected $mModules = [];
140
141
	/** @var array */
142
	protected $mModuleScripts = [];
143
144
	/** @var array */
145
	protected $mModuleStyles = [];
146
147
	/** @var ResourceLoader */
148
	protected $mResourceLoader;
149
150
	/** @var ResourceLoaderClientHtml */
151
	private $rlClient;
152
153
	/** @var ResourceLoaderContext */
154
	private $rlClientContext;
155
156
	/** @var string */
157
	private $rlUserModuleState;
158
159
	/** @var array */
160
	private $rlExemptStyleModules;
161
162
	/** @var array */
163
	protected $mJsConfigVars = [];
164
165
	/** @var array */
166
	protected $mTemplateIds = [];
167
168
	/** @var array */
169
	protected $mImageTimeKeys = [];
170
171
	/** @var string */
172
	public $mRedirectCode = '';
173
174
	protected $mFeedLinksAppendQuery = null;
175
176
	/** @var array
177
	 * What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page?
178
	 * @see ResourceLoaderModule::$origin
179
	 * ResourceLoaderModule::ORIGIN_ALL is assumed unless overridden;
180
	 */
181
	protected $mAllowedModules = [
182
		ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
183
	];
184
185
	/** @var bool Whether output is disabled.  If this is true, the 'output' method will do nothing. */
186
	protected $mDoNothing = false;
187
188
	// Parser related.
189
190
	/** @var int */
191
	protected $mContainsNewMagic = 0;
192
193
	/**
194
	 * lazy initialised, use parserOptions()
195
	 * @var ParserOptions
196
	 */
197
	protected $mParserOptions = null;
198
199
	/**
200
	 * Handles the Atom / RSS links.
201
	 * We probably only support Atom in 2011.
202
	 * @see $wgAdvertisedFeedTypes
203
	 */
204
	private $mFeedLinks = [];
205
206
	// Gwicke work on squid caching? Roughly from 2003.
207
	protected $mEnableClientCache = true;
208
209
	/** @var bool Flag if output should only contain the body of the article. */
210
	private $mArticleBodyOnly = false;
211
212
	/** @var bool */
213
	protected $mNewSectionLink = false;
214
215
	/** @var bool */
216
	protected $mHideNewSectionLink = false;
217
218
	/**
219
	 * @var bool Comes from the parser. This was probably made to load CSS/JS
220
	 * only if we had "<gallery>". Used directly in CategoryPage.php.
221
	 * Looks like ResourceLoader can replace this.
222
	 */
223
	public $mNoGallery = false;
224
225
	/** @var string */
226
	private $mPageTitleActionText = '';
227
228
	/** @var int Cache stuff. Looks like mEnableClientCache */
229
	protected $mCdnMaxage = 0;
230
	/** @var int Upper limit on mCdnMaxage */
231
	protected $mCdnMaxageLimit = INF;
232
233
	/**
234
	 * @var bool Controls if anti-clickjacking / frame-breaking headers will
235
	 * be sent. This should be done for pages where edit actions are possible.
236
	 * Setters: $this->preventClickjacking() and $this->allowClickjacking().
237
	 */
238
	protected $mPreventClickjacking = true;
239
240
	/** @var int To include the variable {{REVISIONID}} */
241
	private $mRevisionId = null;
242
243
	/** @var string */
244
	private $mRevisionTimestamp = null;
245
246
	/** @var array */
247
	protected $mFileVersion = null;
248
249
	/**
250
	 * @var array An array of stylesheet filenames (relative from skins path),
251
	 * with options for CSS media, IE conditions, and RTL/LTR direction.
252
	 * For internal use; add settings in the skin via $this->addStyle()
253
	 *
254
	 * Style again! This seems like a code duplication since we already have
255
	 * mStyles. This is what makes Open Source amazing.
256
	 */
257
	protected $styles = [];
258
259
	private $mIndexPolicy = 'index';
260
	private $mFollowPolicy = 'follow';
261
	private $mVaryHeader = [
262
		'Accept-Encoding' => [ 'match=gzip' ],
263
	];
264
265
	/**
266
	 * If the current page was reached through a redirect, $mRedirectedFrom contains the Title
267
	 * of the redirect.
268
	 *
269
	 * @var Title
270
	 */
271
	private $mRedirectedFrom = null;
272
273
	/**
274
	 * Additional key => value data
275
	 */
276
	private $mProperties = [];
277
278
	/**
279
	 * @var string|null ResourceLoader target for load.php links. If null, will be omitted
280
	 */
281
	private $mTarget = null;
282
283
	/**
284
	 * @var bool Whether parser output should contain table of contents
285
	 */
286
	private $mEnableTOC = true;
287
288
	/**
289
	 * @var bool Whether parser output should contain section edit links
290
	 */
291
	private $mEnableSectionEditLinks = true;
292
293
	/**
294
	 * @var string|null The URL to send in a <link> element with rel=copyright
295
	 */
296
	private $copyrightUrl;
297
298
	/** @var array Profiling data */
299
	private $limitReportData = [];
300
301
	/**
302
	 * Constructor for OutputPage. This should not be called directly.
303
	 * Instead a new RequestContext should be created and it will implicitly create
304
	 * a OutputPage tied to that context.
305
	 * @param IContextSource|null $context
306
	 */
307
	function __construct( IContextSource $context = null ) {
308
		if ( $context === null ) {
309
			# Extensions should use `new RequestContext` instead of `new OutputPage` now.
310
			wfDeprecated( __METHOD__, '1.18' );
311
		} else {
312
			$this->setContext( $context );
313
		}
314
	}
315
316
	/**
317
	 * Redirect to $url rather than displaying the normal page
318
	 *
319
	 * @param string $url URL
320
	 * @param string $responsecode HTTP status code
321
	 */
322
	public function redirect( $url, $responsecode = '302' ) {
323
		# Strip newlines as a paranoia check for header injection in PHP<5.1.2
324
		$this->mRedirect = str_replace( "\n", '', $url );
325
		$this->mRedirectCode = $responsecode;
326
	}
327
328
	/**
329
	 * Get the URL to redirect to, or an empty string if not redirect URL set
330
	 *
331
	 * @return string
332
	 */
333
	public function getRedirect() {
334
		return $this->mRedirect;
335
	}
336
337
	/**
338
	 * Set the copyright URL to send with the output.
339
	 * Empty string to omit, null to reset.
340
	 *
341
	 * @since 1.26
342
	 *
343
	 * @param string|null $url
344
	 */
345
	public function setCopyrightUrl( $url ) {
346
		$this->copyrightUrl = $url;
347
	}
348
349
	/**
350
	 * Set the HTTP status code to send with the output.
351
	 *
352
	 * @param int $statusCode
353
	 */
354
	public function setStatusCode( $statusCode ) {
355
		$this->mStatusCode = $statusCode;
356
	}
357
358
	/**
359
	 * Add a new "<meta>" tag
360
	 * To add an http-equiv meta tag, precede the name with "http:"
361
	 *
362
	 * @param string $name Tag name
363
	 * @param string $val Tag value
364
	 */
365
	function addMeta( $name, $val ) {
366
		array_push( $this->mMetatags, [ $name, $val ] );
367
	}
368
369
	/**
370
	 * Returns the current <meta> tags
371
	 *
372
	 * @since 1.25
373
	 * @return array
374
	 */
375
	public function getMetaTags() {
376
		return $this->mMetatags;
377
	}
378
379
	/**
380
	 * Add a new \<link\> tag to the page header.
381
	 *
382
	 * Note: use setCanonicalUrl() for rel=canonical.
383
	 *
384
	 * @param array $linkarr Associative array of attributes.
385
	 */
386
	function addLink( array $linkarr ) {
387
		array_push( $this->mLinktags, $linkarr );
388
	}
389
390
	/**
391
	 * Returns the current <link> tags
392
	 *
393
	 * @since 1.25
394
	 * @return array
395
	 */
396
	public function getLinkTags() {
397
		return $this->mLinktags;
398
	}
399
400
	/**
401
	 * Add a new \<link\> with "rel" attribute set to "meta"
402
	 *
403
	 * @param array $linkarr Associative array mapping attribute names to their
404
	 *                 values, both keys and values will be escaped, and the
405
	 *                 "rel" attribute will be automatically added
406
	 */
407
	function addMetadataLink( array $linkarr ) {
408
		$linkarr['rel'] = $this->getMetadataAttribute();
409
		$this->addLink( $linkarr );
410
	}
411
412
	/**
413
	 * Set the URL to be used for the <link rel=canonical>. This should be used
414
	 * in preference to addLink(), to avoid duplicate link tags.
415
	 * @param string $url
416
	 */
417
	function setCanonicalUrl( $url ) {
418
		$this->mCanonicalUrl = $url;
0 ignored issues
show
Documentation Bug introduced by
The property $mCanonicalUrl was declared of type boolean, but $url is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
419
	}
420
421
	/**
422
	 * Returns the URL to be used for the <link rel=canonical> if
423
	 * one is set.
424
	 *
425
	 * @since 1.25
426
	 * @return bool|string
427
	 */
428
	public function getCanonicalUrl() {
429
		return $this->mCanonicalUrl;
430
	}
431
432
	/**
433
	 * Get the value of the "rel" attribute for metadata links
434
	 *
435
	 * @return string
436
	 */
437
	public function getMetadataAttribute() {
438
		# note: buggy CC software only reads first "meta" link
439
		static $haveMeta = false;
440
		if ( $haveMeta ) {
441
			return 'alternate meta';
442
		} else {
443
			$haveMeta = true;
444
			return 'meta';
445
		}
446
	}
447
448
	/**
449
	 * Add raw HTML to the list of scripts (including \<script\> tag, etc.)
450
	 * Internal use only. Use OutputPage::addModules() or OutputPage::addJsConfigVars()
451
	 * if possible.
452
	 *
453
	 * @param string $script Raw HTML
454
	 */
455
	function addScript( $script ) {
456
		$this->mScripts .= $script;
457
	}
458
459
	/**
460
	 * Register and add a stylesheet from an extension directory.
461
	 *
462
	 * @deprecated since 1.27 use addModuleStyles() or addStyle() instead
463
	 * @param string $url Path to sheet.  Provide either a full url (beginning
464
	 *             with 'http', etc) or a relative path from the document root
465
	 *             (beginning with '/').  Otherwise it behaves identically to
466
	 *             addStyle() and draws from the /skins folder.
467
	 */
468
	public function addExtensionStyle( $url ) {
469
		wfDeprecated( __METHOD__, '1.27' );
470
		array_push( $this->mExtStyles, $url );
471
	}
472
473
	/**
474
	 * Get all styles added by extensions
475
	 *
476
	 * @deprecated since 1.27
477
	 * @return array
478
	 */
479
	function getExtStyle() {
480
		wfDeprecated( __METHOD__, '1.27' );
481
		return $this->mExtStyles;
482
	}
483
484
	/**
485
	 * Add a JavaScript file out of skins/common, or a given relative path.
486
	 * Internal use only. Use OutputPage::addModules() if possible.
487
	 *
488
	 * @param string $file Filename in skins/common or complete on-server path
489
	 *              (/foo/bar.js)
490
	 * @param string $version Style version of the file. Defaults to $wgStyleVersion
491
	 */
492
	public function addScriptFile( $file, $version = null ) {
493
		// See if $file parameter is an absolute URL or begins with a slash
494
		if ( substr( $file, 0, 1 ) == '/' || preg_match( '#^[a-z]*://#i', $file ) ) {
495
			$path = $file;
496
		} else {
497
			$path = $this->getConfig()->get( 'StylePath' ) . "/common/{$file}";
498
		}
499
		if ( is_null( $version ) ) {
500
			$version = $this->getConfig()->get( 'StyleVersion' );
501
		}
502
		$this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ) ) );
503
	}
504
505
	/**
506
	 * Add a self-contained script tag with the given contents
507
	 * Internal use only. Use OutputPage::addModules() if possible.
508
	 *
509
	 * @param string $script JavaScript text, no script tags
510
	 */
511
	public function addInlineScript( $script ) {
512
		$this->mScripts .= Html::inlineScript( $script );
513
	}
514
515
	/**
516
	 * Filter an array of modules to remove insufficiently trustworthy members, and modules
517
	 * which are no longer registered (eg a page is cached before an extension is disabled)
518
	 * @param array $modules
519
	 * @param string|null $position If not null, only return modules with this position
520
	 * @param string $type
521
	 * @return array
522
	 */
523
	protected function filterModules( array $modules, $position = null,
524
		$type = ResourceLoaderModule::TYPE_COMBINED
525
	) {
526
		$resourceLoader = $this->getResourceLoader();
527
		$filteredModules = [];
528
		foreach ( $modules as $val ) {
529
			$module = $resourceLoader->getModule( $val );
530
			if ( $module instanceof ResourceLoaderModule
531
				&& $module->getOrigin() <= $this->getAllowedModules( $type )
532
				&& ( is_null( $position ) || $module->getPosition() == $position )
533
				&& ( !$this->mTarget || in_array( $this->mTarget, $module->getTargets() ) )
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->mTarget of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
534
			) {
535
				$filteredModules[] = $val;
536
			}
537
		}
538
		return $filteredModules;
539
	}
540
541
	/**
542
	 * Get the list of modules to include on this page
543
	 *
544
	 * @param bool $filter Whether to filter out insufficiently trustworthy modules
545
	 * @param string|null $position If not null, only return modules with this position
546
	 * @param string $param
547
	 * @return array Array of module names
548
	 */
549
	public function getModules( $filter = false, $position = null, $param = 'mModules',
550
		$type = ResourceLoaderModule::TYPE_COMBINED
551
	) {
552
		$modules = array_values( array_unique( $this->$param ) );
553
		return $filter
554
			? $this->filterModules( $modules, $position, $type )
555
			: $modules;
556
	}
557
558
	/**
559
	 * Add one or more modules recognized by ResourceLoader. Modules added
560
	 * through this function will be loaded by ResourceLoader when the
561
	 * page loads.
562
	 *
563
	 * @param string|array $modules Module name (string) or array of module names
564
	 */
565
	public function addModules( $modules ) {
566
		$this->mModules = array_merge( $this->mModules, (array)$modules );
567
	}
568
569
	/**
570
	 * Get the list of module JS to include on this page
571
	 *
572
	 * @param bool $filter
573
	 * @param string|null $position
574
	 * @return array Array of module names
575
	 */
576
	public function getModuleScripts( $filter = false, $position = null ) {
577
		return $this->getModules( $filter, $position, 'mModuleScripts',
578
			ResourceLoaderModule::TYPE_SCRIPTS
579
		);
580
	}
581
582
	/**
583
	 * Add only JS of one or more modules recognized by ResourceLoader. Module
584
	 * scripts added through this function will be loaded by ResourceLoader when
585
	 * the page loads.
586
	 *
587
	 * @param string|array $modules Module name (string) or array of module names
588
	 */
589
	public function addModuleScripts( $modules ) {
590
		$this->mModuleScripts = array_merge( $this->mModuleScripts, (array)$modules );
591
	}
592
593
	/**
594
	 * Get the list of module CSS to include on this page
595
	 *
596
	 * @param bool $filter
597
	 * @param string|null $position
598
	 * @return array Array of module names
599
	 */
600
	public function getModuleStyles( $filter = false, $position = null ) {
601
		return $this->getModules( $filter, $position, 'mModuleStyles',
602
			ResourceLoaderModule::TYPE_STYLES
603
		);
604
	}
605
606
	/**
607
	 * Add only CSS of one or more modules recognized by ResourceLoader.
608
	 *
609
	 * Module styles added through this function will be added using standard link CSS
610
	 * tags, rather than as a combined Javascript and CSS package. Thus, they will
611
	 * load when JavaScript is disabled (unless CSS also happens to be disabled).
612
	 *
613
	 * @param string|array $modules Module name (string) or array of module names
614
	 */
615
	public function addModuleStyles( $modules ) {
616
		$this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
617
	}
618
619
	/**
620
	 * @return null|string ResourceLoader target
621
	 */
622
	public function getTarget() {
623
		return $this->mTarget;
624
	}
625
626
	/**
627
	 * Sets ResourceLoader target for load.php links. If null, will be omitted
628
	 *
629
	 * @param string|null $target
630
	 */
631
	public function setTarget( $target ) {
632
		$this->mTarget = $target;
633
	}
634
635
	/**
636
	 * Get an array of head items
637
	 *
638
	 * @return array
639
	 */
640
	function getHeadItemsArray() {
641
		return $this->mHeadItems;
642
	}
643
644
	/**
645
	 * Add or replace a head item to the output
646
	 *
647
	 * Whenever possible, use more specific options like ResourceLoader modules,
648
	 * OutputPage::addLink(), OutputPage::addMetaLink() and OutputPage::addFeedLink()
649
	 * Fallback options for those are: OutputPage::addStyle, OutputPage::addScript(),
650
	 * OutputPage::addInlineScript() and OutputPage::addInlineStyle()
651
	 * This would be your very LAST fallback.
652
	 *
653
	 * @param string $name Item name
654
	 * @param string $value Raw HTML
655
	 */
656
	public function addHeadItem( $name, $value ) {
657
		$this->mHeadItems[$name] = $value;
658
	}
659
660
	/**
661
	 * Add one or more head items to the output
662
	 *
663
	 * @since 1.28
664
	 * @param string|string[] $value Raw HTML
0 ignored issues
show
Documentation introduced by
There is no parameter named $value. Did you maybe mean $values?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
665
	 */
666
	public function addHeadItems( $values ) {
667
		$this->mHeadItems = array_merge( $this->mHeadItems, (array)$values );
668
	}
669
670
	/**
671
	 * Check if the header item $name is already set
672
	 *
673
	 * @param string $name Item name
674
	 * @return bool
675
	 */
676
	public function hasHeadItem( $name ) {
677
		return isset( $this->mHeadItems[$name] );
678
	}
679
680
	/**
681
	 * @deprecated since 1.28 Obsolete - wgUseETag experiment was removed.
682
	 * @param string $tag
683
	 */
684
	public function setETag( $tag ) {
685
	}
686
687
	/**
688
	 * Set whether the output should only contain the body of the article,
689
	 * without any skin, sidebar, etc.
690
	 * Used e.g. when calling with "action=render".
691
	 *
692
	 * @param bool $only Whether to output only the body of the article
693
	 */
694
	public function setArticleBodyOnly( $only ) {
695
		$this->mArticleBodyOnly = $only;
696
	}
697
698
	/**
699
	 * Return whether the output will contain only the body of the article
700
	 *
701
	 * @return bool
702
	 */
703
	public function getArticleBodyOnly() {
704
		return $this->mArticleBodyOnly;
705
	}
706
707
	/**
708
	 * Set an additional output property
709
	 * @since 1.21
710
	 *
711
	 * @param string $name
712
	 * @param mixed $value
713
	 */
714
	public function setProperty( $name, $value ) {
715
		$this->mProperties[$name] = $value;
716
	}
717
718
	/**
719
	 * Get an additional output property
720
	 * @since 1.21
721
	 *
722
	 * @param string $name
723
	 * @return mixed Property value or null if not found
724
	 */
725
	public function getProperty( $name ) {
726
		if ( isset( $this->mProperties[$name] ) ) {
727
			return $this->mProperties[$name];
728
		} else {
729
			return null;
730
		}
731
	}
732
733
	/**
734
	 * checkLastModified tells the client to use the client-cached page if
735
	 * possible. If successful, the OutputPage is disabled so that
736
	 * any future call to OutputPage->output() have no effect.
737
	 *
738
	 * Side effect: sets mLastModified for Last-Modified header
739
	 *
740
	 * @param string $timestamp
741
	 *
742
	 * @return bool True if cache-ok headers was sent.
743
	 */
744
	public function checkLastModified( $timestamp ) {
745
		if ( !$timestamp || $timestamp == '19700101000000' ) {
746
			wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
747
			return false;
748
		}
749
		$config = $this->getConfig();
750
		if ( !$config->get( 'CachePages' ) ) {
751
			wfDebug( __METHOD__ . ": CACHE DISABLED\n" );
752
			return false;
753
		}
754
755
		$timestamp = wfTimestamp( TS_MW, $timestamp );
756
		$modifiedTimes = [
757
			'page' => $timestamp,
758
			'user' => $this->getUser()->getTouched(),
759
			'epoch' => $config->get( 'CacheEpoch' )
760
		];
761
		if ( $config->get( 'UseSquid' ) ) {
762
			// bug 44570: the core page itself may not change, but resources might
763
			$modifiedTimes['sepoch'] = wfTimestamp( TS_MW, time() - $config->get( 'SquidMaxage' ) );
764
		}
765
		Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] );
766
767
		$maxModified = max( $modifiedTimes );
768
		$this->mLastModified = wfTimestamp( TS_RFC2822, $maxModified );
0 ignored issues
show
Documentation Bug introduced by
It seems like wfTimestamp(TS_RFC2822, $maxModified) can also be of type false. However, the property $mLastModified is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
769
770
		$clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' );
771
		if ( $clientHeader === false ) {
772
			wfDebug( __METHOD__ . ": client did not send If-Modified-Since header", 'private' );
773
			return false;
774
		}
775
776
		# IE sends sizes after the date like this:
777
		# Wed, 20 Aug 2003 06:51:19 GMT; length=5202
778
		# this breaks strtotime().
779
		$clientHeader = preg_replace( '/;.*$/', '', $clientHeader );
780
781
		MediaWiki\suppressWarnings(); // E_STRICT system time bitching
782
		$clientHeaderTime = strtotime( $clientHeader );
783
		MediaWiki\restoreWarnings();
784
		if ( !$clientHeaderTime ) {
785
			wfDebug( __METHOD__
786
				. ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
787
			return false;
788
		}
789
		$clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
790
791
		# Make debug info
792
		$info = '';
793
		foreach ( $modifiedTimes as $name => $value ) {
794
			if ( $info !== '' ) {
795
				$info .= ', ';
796
			}
797
			$info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
798
		}
799
800
		wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
801
			wfTimestamp( TS_ISO_8601, $clientHeaderTime ), 'private' );
802
		wfDebug( __METHOD__ . ": effective Last-Modified: " .
803
			wfTimestamp( TS_ISO_8601, $maxModified ), 'private' );
804
		if ( $clientHeaderTime < $maxModified ) {
805
			wfDebug( __METHOD__ . ": STALE, $info", 'private' );
806
			return false;
807
		}
808
809
		# Not modified
810
		# Give a 304 Not Modified response code and disable body output
811
		wfDebug( __METHOD__ . ": NOT MODIFIED, $info", 'private' );
812
		ini_set( 'zlib.output_compression', 0 );
813
		$this->getRequest()->response()->statusHeader( 304 );
814
		$this->sendCacheControl();
815
		$this->disable();
816
817
		// Don't output a compressed blob when using ob_gzhandler;
818
		// it's technically against HTTP spec and seems to confuse
819
		// Firefox when the response gets split over two packets.
820
		wfClearOutputBuffers();
821
822
		return true;
823
	}
824
825
	/**
826
	 * Override the last modified timestamp
827
	 *
828
	 * @param string $timestamp New timestamp, in a format readable by
829
	 *        wfTimestamp()
830
	 */
831
	public function setLastModified( $timestamp ) {
832
		$this->mLastModified = wfTimestamp( TS_RFC2822, $timestamp );
0 ignored issues
show
Documentation Bug introduced by
It seems like wfTimestamp(TS_RFC2822, $timestamp) can also be of type false. However, the property $mLastModified is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
833
	}
834
835
	/**
836
	 * Set the robot policy for the page: <http://www.robotstxt.org/meta.html>
837
	 *
838
	 * @param string $policy The literal string to output as the contents of
839
	 *   the meta tag.  Will be parsed according to the spec and output in
840
	 *   standardized form.
841
	 * @return null
842
	 */
843
	public function setRobotPolicy( $policy ) {
844
		$policy = Article::formatRobotPolicy( $policy );
845
846
		if ( isset( $policy['index'] ) ) {
847
			$this->setIndexPolicy( $policy['index'] );
848
		}
849
		if ( isset( $policy['follow'] ) ) {
850
			$this->setFollowPolicy( $policy['follow'] );
851
		}
852
	}
853
854
	/**
855
	 * Set the index policy for the page, but leave the follow policy un-
856
	 * touched.
857
	 *
858
	 * @param string $policy Either 'index' or 'noindex'.
859
	 * @return null
860
	 */
861
	public function setIndexPolicy( $policy ) {
862
		$policy = trim( $policy );
863
		if ( in_array( $policy, [ 'index', 'noindex' ] ) ) {
864
			$this->mIndexPolicy = $policy;
865
		}
866
	}
867
868
	/**
869
	 * Set the follow policy for the page, but leave the index policy un-
870
	 * touched.
871
	 *
872
	 * @param string $policy Either 'follow' or 'nofollow'.
873
	 * @return null
874
	 */
875
	public function setFollowPolicy( $policy ) {
876
		$policy = trim( $policy );
877
		if ( in_array( $policy, [ 'follow', 'nofollow' ] ) ) {
878
			$this->mFollowPolicy = $policy;
879
		}
880
	}
881
882
	/**
883
	 * Set the new value of the "action text", this will be added to the
884
	 * "HTML title", separated from it with " - ".
885
	 *
886
	 * @param string $text New value of the "action text"
887
	 */
888
	public function setPageTitleActionText( $text ) {
889
		$this->mPageTitleActionText = $text;
890
	}
891
892
	/**
893
	 * Get the value of the "action text"
894
	 *
895
	 * @return string
896
	 */
897
	public function getPageTitleActionText() {
898
		return $this->mPageTitleActionText;
899
	}
900
901
	/**
902
	 * "HTML title" means the contents of "<title>".
903
	 * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file.
904
	 *
905
	 * @param string|Message $name
906
	 */
907
	public function setHTMLTitle( $name ) {
908
		if ( $name instanceof Message ) {
909
			$this->mHTMLtitle = $name->setContext( $this->getContext() )->text();
910
		} else {
911
			$this->mHTMLtitle = $name;
912
		}
913
	}
914
915
	/**
916
	 * Return the "HTML title", i.e. the content of the "<title>" tag.
917
	 *
918
	 * @return string
919
	 */
920
	public function getHTMLTitle() {
921
		return $this->mHTMLtitle;
922
	}
923
924
	/**
925
	 * Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
926
	 *
927
	 * @param Title $t
928
	 */
929
	public function setRedirectedFrom( $t ) {
930
		$this->mRedirectedFrom = $t;
931
	}
932
933
	/**
934
	 * "Page title" means the contents of \<h1\>. It is stored as a valid HTML
935
	 * fragment. This function allows good tags like \<sup\> in the \<h1\> tag,
936
	 * but not bad tags like \<script\>. This function automatically sets
937
	 * \<title\> to the same content as \<h1\> but with all tags removed. Bad
938
	 * tags that were escaped in \<h1\> will still be escaped in \<title\>, and
939
	 * good tags like \<i\> will be dropped entirely.
940
	 *
941
	 * @param string|Message $name
942
	 */
943
	public function setPageTitle( $name ) {
944
		if ( $name instanceof Message ) {
945
			$name = $name->setContext( $this->getContext() )->text();
946
		}
947
948
		# change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
949
		# but leave "<i>foobar</i>" alone
950
		$nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
951
		$this->mPagetitle = $nameWithTags;
952
953
		# change "<i>foo&amp;bar</i>" to "foo&bar"
954
		$this->setHTMLTitle(
955
			$this->msg( 'pagetitle' )->rawParams( Sanitizer::stripAllTags( $nameWithTags ) )
956
				->inContentLanguage()
957
		);
958
	}
959
960
	/**
961
	 * Return the "page title", i.e. the content of the \<h1\> tag.
962
	 *
963
	 * @return string
964
	 */
965
	public function getPageTitle() {
966
		return $this->mPagetitle;
967
	}
968
969
	/**
970
	 * Set the Title object to use
971
	 *
972
	 * @param Title $t
973
	 */
974
	public function setTitle( Title $t ) {
975
		$this->getContext()->setTitle( $t );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setTitle() does only exist in the following implementations of said interface: DerivativeContext, EditWatchlistNormalHTMLForm, HTMLForm, OOUIHTMLForm, OutputPage, PreferencesForm, RequestContext, UploadForm, VFormHTMLForm.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
976
	}
977
978
	/**
979
	 * Replace the subtitle with $str
980
	 *
981
	 * @param string|Message $str New value of the subtitle. String should be safe HTML.
982
	 */
983
	public function setSubtitle( $str ) {
984
		$this->clearSubtitle();
985
		$this->addSubtitle( $str );
986
	}
987
988
	/**
989
	 * Add $str to the subtitle
990
	 *
991
	 * @param string|Message $str String or Message to add to the subtitle. String should be safe HTML.
992
	 */
993
	public function addSubtitle( $str ) {
994
		if ( $str instanceof Message ) {
995
			$this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
996
		} else {
997
			$this->mSubtitle[] = $str;
998
		}
999
	}
1000
1001
	/**
1002
	 * Build message object for a subtitle containing a backlink to a page
1003
	 *
1004
	 * @param Title $title Title to link to
1005
	 * @param array $query Array of additional parameters to include in the link
1006
	 * @return Message
1007
	 * @since 1.25
1008
	 */
1009
	public static function buildBacklinkSubtitle( Title $title, $query = [] ) {
1010
		if ( $title->isRedirect() ) {
1011
			$query['redirect'] = 'no';
1012
		}
1013
		return wfMessage( 'backlinksubtitle' )
1014
			->rawParams( Linker::link( $title, null, [], $query ) );
1015
	}
1016
1017
	/**
1018
	 * Add a subtitle containing a backlink to a page
1019
	 *
1020
	 * @param Title $title Title to link to
1021
	 * @param array $query Array of additional parameters to include in the link
1022
	 */
1023
	public function addBacklinkSubtitle( Title $title, $query = [] ) {
1024
		$this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) );
1025
	}
1026
1027
	/**
1028
	 * Clear the subtitles
1029
	 */
1030
	public function clearSubtitle() {
1031
		$this->mSubtitle = [];
1032
	}
1033
1034
	/**
1035
	 * Get the subtitle
1036
	 *
1037
	 * @return string
1038
	 */
1039
	public function getSubtitle() {
1040
		return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
1041
	}
1042
1043
	/**
1044
	 * Set the page as printable, i.e. it'll be displayed with all
1045
	 * print styles included
1046
	 */
1047
	public function setPrintable() {
1048
		$this->mPrintable = true;
1049
	}
1050
1051
	/**
1052
	 * Return whether the page is "printable"
1053
	 *
1054
	 * @return bool
1055
	 */
1056
	public function isPrintable() {
1057
		return $this->mPrintable;
1058
	}
1059
1060
	/**
1061
	 * Disable output completely, i.e. calling output() will have no effect
1062
	 */
1063
	public function disable() {
1064
		$this->mDoNothing = true;
1065
	}
1066
1067
	/**
1068
	 * Return whether the output will be completely disabled
1069
	 *
1070
	 * @return bool
1071
	 */
1072
	public function isDisabled() {
1073
		return $this->mDoNothing;
1074
	}
1075
1076
	/**
1077
	 * Show an "add new section" link?
1078
	 *
1079
	 * @return bool
1080
	 */
1081
	public function showNewSectionLink() {
1082
		return $this->mNewSectionLink;
1083
	}
1084
1085
	/**
1086
	 * Forcibly hide the new section link?
1087
	 *
1088
	 * @return bool
1089
	 */
1090
	public function forceHideNewSectionLink() {
1091
		return $this->mHideNewSectionLink;
1092
	}
1093
1094
	/**
1095
	 * Add or remove feed links in the page header
1096
	 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
1097
	 * for the new version
1098
	 * @see addFeedLink()
1099
	 *
1100
	 * @param bool $show True: add default feeds, false: remove all feeds
1101
	 */
1102
	public function setSyndicated( $show = true ) {
1103
		if ( $show ) {
1104
			$this->setFeedAppendQuery( false );
1105
		} else {
1106
			$this->mFeedLinks = [];
1107
		}
1108
	}
1109
1110
	/**
1111
	 * Add default feeds to the page header
1112
	 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
1113
	 * for the new version
1114
	 * @see addFeedLink()
1115
	 *
1116
	 * @param string $val Query to append to feed links or false to output
1117
	 *        default links
1118
	 */
1119
	public function setFeedAppendQuery( $val ) {
1120
		$this->mFeedLinks = [];
1121
1122
		foreach ( $this->getConfig()->get( 'AdvertisedFeedTypes' ) as $type ) {
1123
			$query = "feed=$type";
1124
			if ( is_string( $val ) ) {
1125
				$query .= '&' . $val;
1126
			}
1127
			$this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
1128
		}
1129
	}
1130
1131
	/**
1132
	 * Add a feed link to the page header
1133
	 *
1134
	 * @param string $format Feed type, should be a key of $wgFeedClasses
1135
	 * @param string $href URL
1136
	 */
1137
	public function addFeedLink( $format, $href ) {
1138
		if ( in_array( $format, $this->getConfig()->get( 'AdvertisedFeedTypes' ) ) ) {
1139
			$this->mFeedLinks[$format] = $href;
1140
		}
1141
	}
1142
1143
	/**
1144
	 * Should we output feed links for this page?
1145
	 * @return bool
1146
	 */
1147
	public function isSyndicated() {
1148
		return count( $this->mFeedLinks ) > 0;
1149
	}
1150
1151
	/**
1152
	 * Return URLs for each supported syndication format for this page.
1153
	 * @return array Associating format keys with URLs
1154
	 */
1155
	public function getSyndicationLinks() {
1156
		return $this->mFeedLinks;
1157
	}
1158
1159
	/**
1160
	 * Will currently always return null
1161
	 *
1162
	 * @return null
1163
	 */
1164
	public function getFeedAppendQuery() {
1165
		return $this->mFeedLinksAppendQuery;
1166
	}
1167
1168
	/**
1169
	 * Set whether the displayed content is related to the source of the
1170
	 * corresponding article on the wiki
1171
	 * Setting true will cause the change "article related" toggle to true
1172
	 *
1173
	 * @param bool $v
1174
	 */
1175
	public function setArticleFlag( $v ) {
1176
		$this->mIsarticle = $v;
1177
		if ( $v ) {
1178
			$this->mIsArticleRelated = $v;
1179
		}
1180
	}
1181
1182
	/**
1183
	 * Return whether the content displayed page is related to the source of
1184
	 * the corresponding article on the wiki
1185
	 *
1186
	 * @return bool
1187
	 */
1188
	public function isArticle() {
1189
		return $this->mIsarticle;
1190
	}
1191
1192
	/**
1193
	 * Set whether this page is related an article on the wiki
1194
	 * Setting false will cause the change of "article flag" toggle to false
1195
	 *
1196
	 * @param bool $v
1197
	 */
1198
	public function setArticleRelated( $v ) {
1199
		$this->mIsArticleRelated = $v;
1200
		if ( !$v ) {
1201
			$this->mIsarticle = false;
1202
		}
1203
	}
1204
1205
	/**
1206
	 * Return whether this page is related an article on the wiki
1207
	 *
1208
	 * @return bool
1209
	 */
1210
	public function isArticleRelated() {
1211
		return $this->mIsArticleRelated;
1212
	}
1213
1214
	/**
1215
	 * Add new language links
1216
	 *
1217
	 * @param array $newLinkArray Associative array mapping language code to the page
1218
	 *                      name
1219
	 */
1220
	public function addLanguageLinks( array $newLinkArray ) {
1221
		$this->mLanguageLinks += $newLinkArray;
1222
	}
1223
1224
	/**
1225
	 * Reset the language links and add new language links
1226
	 *
1227
	 * @param array $newLinkArray Associative array mapping language code to the page
1228
	 *                      name
1229
	 */
1230
	public function setLanguageLinks( array $newLinkArray ) {
1231
		$this->mLanguageLinks = $newLinkArray;
1232
	}
1233
1234
	/**
1235
	 * Get the list of language links
1236
	 *
1237
	 * @return array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page')
1238
	 */
1239
	public function getLanguageLinks() {
1240
		return $this->mLanguageLinks;
1241
	}
1242
1243
	/**
1244
	 * Add an array of categories, with names in the keys
1245
	 *
1246
	 * @param array $categories Mapping category name => sort key
1247
	 */
1248
	public function addCategoryLinks( array $categories ) {
1249
		global $wgContLang;
1250
1251
		if ( !is_array( $categories ) || count( $categories ) == 0 ) {
1252
			return;
1253
		}
1254
1255
		# Add the links to a LinkBatch
1256
		$arr = [ NS_CATEGORY => $categories ];
1257
		$lb = new LinkBatch;
1258
		$lb->setArray( $arr );
1259
1260
		# Fetch existence plus the hiddencat property
1261
		$dbr = wfGetDB( DB_REPLICA );
1262
		$fields = array_merge(
1263
			LinkCache::getSelectFields(),
1264
			[ 'page_namespace', 'page_title', 'pp_value' ]
1265
		);
1266
1267
		$res = $dbr->select( [ 'page', 'page_props' ],
1268
			$fields,
1269
			$lb->constructSet( 'page', $dbr ),
0 ignored issues
show
Bug introduced by
It seems like $lb->constructSet('page', $dbr) targeting LinkBatch::constructSet() can also be of type boolean; however, Database::select() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $dbr defined by wfGetDB(DB_REPLICA) on line 1261 can be null; however, LinkBatch::constructSet() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1270
			__METHOD__,
1271
			[],
1272
			[ 'page_props' => [ 'LEFT JOIN', [
1273
				'pp_propname' => 'hiddencat',
1274
				'pp_page = page_id'
1275
			] ] ]
1276
		);
1277
1278
		# Add the results to the link cache
1279
		$lb->addResultToCache( LinkCache::singleton(), $res );
1280
1281
		# Set all the values to 'normal'.
1282
		$categories = array_fill_keys( array_keys( $categories ), 'normal' );
1283
1284
		# Mark hidden categories
1285
		foreach ( $res as $row ) {
1286
			if ( isset( $row->pp_value ) ) {
1287
				$categories[$row->page_title] = 'hidden';
1288
			}
1289
		}
1290
1291
		# Add the remaining categories to the skin
1292
		if ( Hooks::run(
1293
			'OutputPageMakeCategoryLinks',
1294
			[ &$this, $categories, &$this->mCategoryLinks ] )
1295
		) {
1296
			foreach ( $categories as $category => $type ) {
1297
				// array keys will cast numeric category names to ints, so cast back to string
1298
				$category = (string)$category;
1299
				$origcategory = $category;
1300
				$title = Title::makeTitleSafe( NS_CATEGORY, $category );
1301
				if ( !$title ) {
1302
					continue;
1303
				}
1304
				$wgContLang->findVariantLink( $category, $title, true );
1305
				if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1306
					continue;
1307
				}
1308
				$text = $wgContLang->convertHtml( $title->getText() );
1309
				$this->mCategories[] = $title->getText();
1310
				$this->mCategoryLinks[$type][] = Linker::link( $title, $text );
1311
			}
1312
		}
1313
	}
1314
1315
	/**
1316
	 * Reset the category links (but not the category list) and add $categories
1317
	 *
1318
	 * @param array $categories Mapping category name => sort key
1319
	 */
1320
	public function setCategoryLinks( array $categories ) {
1321
		$this->mCategoryLinks = [];
1322
		$this->addCategoryLinks( $categories );
1323
	}
1324
1325
	/**
1326
	 * Get the list of category links, in a 2-D array with the following format:
1327
	 * $arr[$type][] = $link, where $type is either "normal" or "hidden" (for
1328
	 * hidden categories) and $link a HTML fragment with a link to the category
1329
	 * page
1330
	 *
1331
	 * @return array
1332
	 */
1333
	public function getCategoryLinks() {
1334
		return $this->mCategoryLinks;
1335
	}
1336
1337
	/**
1338
	 * Get the list of category names this page belongs to
1339
	 *
1340
	 * @return array Array of strings
1341
	 */
1342
	public function getCategories() {
1343
		return $this->mCategories;
1344
	}
1345
1346
	/**
1347
	 * Add an array of indicators, with their identifiers as array
1348
	 * keys and HTML contents as values.
1349
	 *
1350
	 * In case of duplicate keys, existing values are overwritten.
1351
	 *
1352
	 * @param array $indicators
1353
	 * @since 1.25
1354
	 */
1355
	public function setIndicators( array $indicators ) {
1356
		$this->mIndicators = $indicators + $this->mIndicators;
1357
		// Keep ordered by key
1358
		ksort( $this->mIndicators );
1359
	}
1360
1361
	/**
1362
	 * Get the indicators associated with this page.
1363
	 *
1364
	 * The array will be internally ordered by item keys.
1365
	 *
1366
	 * @return array Keys: identifiers, values: HTML contents
1367
	 * @since 1.25
1368
	 */
1369
	public function getIndicators() {
1370
		return $this->mIndicators;
1371
	}
1372
1373
	/**
1374
	 * Adds help link with an icon via page indicators.
1375
	 * Link target can be overridden by a local message containing a wikilink:
1376
	 * the message key is: lowercase action or special page name + '-helppage'.
1377
	 * @param string $to Target MediaWiki.org page title or encoded URL.
1378
	 * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
1379
	 * @since 1.25
1380
	 */
1381
	public function addHelpLink( $to, $overrideBaseUrl = false ) {
1382
		$this->addModuleStyles( 'mediawiki.helplink' );
1383
		$text = $this->msg( 'helppage-top-gethelp' )->escaped();
1384
1385
		if ( $overrideBaseUrl ) {
1386
			$helpUrl = $to;
1387
		} else {
1388
			$toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) );
1389
			$helpUrl = "//www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1390
		}
1391
1392
		$link = Html::rawElement(
1393
			'a',
1394
			[
1395
				'href' => $helpUrl,
1396
				'target' => '_blank',
1397
				'class' => 'mw-helplink',
1398
			],
1399
			$text
1400
		);
1401
1402
		$this->setIndicators( [ 'mw-helplink' => $link ] );
1403
	}
1404
1405
	/**
1406
	 * Do not allow scripts which can be modified by wiki users to load on this page;
1407
	 * only allow scripts bundled with, or generated by, the software.
1408
	 * Site-wide styles are controlled by a config setting, since they can be
1409
	 * used to create a custom skin/theme, but not user-specific ones.
1410
	 *
1411
	 * @todo this should be given a more accurate name
1412
	 */
1413
	public function disallowUserJs() {
1414
		$this->reduceAllowedModules(
1415
			ResourceLoaderModule::TYPE_SCRIPTS,
1416
			ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
1417
		);
1418
1419
		// Site-wide styles are controlled by a config setting, see bug 71621
1420
		// for background on why. User styles are never allowed.
1421
		if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
1422
			$styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
1423
		} else {
1424
			$styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL;
1425
		}
1426
		$this->reduceAllowedModules(
1427
			ResourceLoaderModule::TYPE_STYLES,
1428
			$styleOrigin
1429
		);
1430
	}
1431
1432
	/**
1433
	 * Show what level of JavaScript / CSS untrustworthiness is allowed on this page
1434
	 * @see ResourceLoaderModule::$origin
1435
	 * @param string $type ResourceLoaderModule TYPE_ constant
1436
	 * @return int ResourceLoaderModule ORIGIN_ class constant
1437
	 */
1438
	public function getAllowedModules( $type ) {
1439
		if ( $type == ResourceLoaderModule::TYPE_COMBINED ) {
1440
			return min( array_values( $this->mAllowedModules ) );
1441
		} else {
1442
			return isset( $this->mAllowedModules[$type] )
1443
				? $this->mAllowedModules[$type]
1444
				: ResourceLoaderModule::ORIGIN_ALL;
1445
		}
1446
	}
1447
1448
	/**
1449
	 * Limit the highest level of CSS/JS untrustworthiness allowed.
1450
	 *
1451
	 * If passed the same or a higher level than the current level of untrustworthiness set, the
1452
	 * level will remain unchanged.
1453
	 *
1454
	 * @param string $type
1455
	 * @param int $level ResourceLoaderModule class constant
1456
	 */
1457
	public function reduceAllowedModules( $type, $level ) {
1458
		$this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
1459
	}
1460
1461
	/**
1462
	 * Prepend $text to the body HTML
1463
	 *
1464
	 * @param string $text HTML
1465
	 */
1466
	public function prependHTML( $text ) {
1467
		$this->mBodytext = $text . $this->mBodytext;
1468
	}
1469
1470
	/**
1471
	 * Append $text to the body HTML
1472
	 *
1473
	 * @param string $text HTML
1474
	 */
1475
	public function addHTML( $text ) {
1476
		$this->mBodytext .= $text;
1477
	}
1478
1479
	/**
1480
	 * Shortcut for adding an Html::element via addHTML.
1481
	 *
1482
	 * @since 1.19
1483
	 *
1484
	 * @param string $element
1485
	 * @param array $attribs
1486
	 * @param string $contents
1487
	 */
1488
	public function addElement( $element, array $attribs = [], $contents = '' ) {
1489
		$this->addHTML( Html::element( $element, $attribs, $contents ) );
1490
	}
1491
1492
	/**
1493
	 * Clear the body HTML
1494
	 */
1495
	public function clearHTML() {
1496
		$this->mBodytext = '';
1497
	}
1498
1499
	/**
1500
	 * Get the body HTML
1501
	 *
1502
	 * @return string HTML
1503
	 */
1504
	public function getHTML() {
1505
		return $this->mBodytext;
1506
	}
1507
1508
	/**
1509
	 * Get/set the ParserOptions object to use for wikitext parsing
1510
	 *
1511
	 * @param ParserOptions|null $options Either the ParserOption to use or null to only get the
1512
	 *   current ParserOption object
1513
	 * @return ParserOptions
1514
	 */
1515
	public function parserOptions( $options = null ) {
1516
		if ( $options !== null && !empty( $options->isBogus ) ) {
1517
			// Someone is trying to set a bogus pre-$wgUser PO. Check if it has
1518
			// been changed somehow, and keep it if so.
1519
			$anonPO = ParserOptions::newFromAnon();
1520
			$anonPO->setEditSection( false );
1521
			if ( !$options->matches( $anonPO ) ) {
1522
				wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) );
1523
				$options->isBogus = false;
0 ignored issues
show
Bug introduced by
The property isBogus does not seem to exist in ParserOptions.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1524
			}
1525
		}
1526
1527
		if ( !$this->mParserOptions ) {
1528
			if ( !$this->getContext()->getUser()->isSafeToLoad() ) {
1529
				// $wgUser isn't unstubbable yet, so don't try to get a
1530
				// ParserOptions for it. And don't cache this ParserOptions
1531
				// either.
1532
				$po = ParserOptions::newFromAnon();
1533
				$po->setEditSection( false );
1534
				$po->isBogus = true;
1535
				if ( $options !== null ) {
1536
					$this->mParserOptions = empty( $options->isBogus ) ? $options : null;
1537
				}
1538
				return $po;
1539
			}
1540
1541
			$this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
1542
			$this->mParserOptions->setEditSection( false );
1543
		}
1544
1545
		if ( $options !== null && !empty( $options->isBogus ) ) {
1546
			// They're trying to restore the bogus pre-$wgUser PO. Do the right
1547
			// thing.
1548
			return wfSetVar( $this->mParserOptions, null, true );
1549
		} else {
1550
			return wfSetVar( $this->mParserOptions, $options );
1551
		}
1552
	}
1553
1554
	/**
1555
	 * Set the revision ID which will be seen by the wiki text parser
1556
	 * for things such as embedded {{REVISIONID}} variable use.
1557
	 *
1558
	 * @param int|null $revid An positive integer, or null
1559
	 * @return mixed Previous value
1560
	 */
1561
	public function setRevisionId( $revid ) {
1562
		$val = is_null( $revid ) ? null : intval( $revid );
1563
		return wfSetVar( $this->mRevisionId, $val );
1564
	}
1565
1566
	/**
1567
	 * Get the displayed revision ID
1568
	 *
1569
	 * @return int
1570
	 */
1571
	public function getRevisionId() {
1572
		return $this->mRevisionId;
1573
	}
1574
1575
	/**
1576
	 * Set the timestamp of the revision which will be displayed. This is used
1577
	 * to avoid a extra DB call in Skin::lastModified().
1578
	 *
1579
	 * @param string|null $timestamp
1580
	 * @return mixed Previous value
1581
	 */
1582
	public function setRevisionTimestamp( $timestamp ) {
1583
		return wfSetVar( $this->mRevisionTimestamp, $timestamp );
1584
	}
1585
1586
	/**
1587
	 * Get the timestamp of displayed revision.
1588
	 * This will be null if not filled by setRevisionTimestamp().
1589
	 *
1590
	 * @return string|null
1591
	 */
1592
	public function getRevisionTimestamp() {
1593
		return $this->mRevisionTimestamp;
1594
	}
1595
1596
	/**
1597
	 * Set the displayed file version
1598
	 *
1599
	 * @param File|bool $file
1600
	 * @return mixed Previous value
1601
	 */
1602
	public function setFileVersion( $file ) {
1603
		$val = null;
1604
		if ( $file instanceof File && $file->exists() ) {
1605
			$val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ];
1606
		}
1607
		return wfSetVar( $this->mFileVersion, $val, true );
1608
	}
1609
1610
	/**
1611
	 * Get the displayed file version
1612
	 *
1613
	 * @return array|null ('time' => MW timestamp, 'sha1' => sha1)
1614
	 */
1615
	public function getFileVersion() {
1616
		return $this->mFileVersion;
1617
	}
1618
1619
	/**
1620
	 * Get the templates used on this page
1621
	 *
1622
	 * @return array (namespace => dbKey => revId)
1623
	 * @since 1.18
1624
	 */
1625
	public function getTemplateIds() {
1626
		return $this->mTemplateIds;
1627
	}
1628
1629
	/**
1630
	 * Get the files used on this page
1631
	 *
1632
	 * @return array (dbKey => array('time' => MW timestamp or null, 'sha1' => sha1 or ''))
1633
	 * @since 1.18
1634
	 */
1635
	public function getFileSearchOptions() {
1636
		return $this->mImageTimeKeys;
1637
	}
1638
1639
	/**
1640
	 * Convert wikitext to HTML and add it to the buffer
1641
	 * Default assumes that the current page title will be used.
1642
	 *
1643
	 * @param string $text
1644
	 * @param bool $linestart Is this the start of a line?
1645
	 * @param bool $interface Is this text in the user interface language?
1646
	 * @throws MWException
1647
	 */
1648
	public function addWikiText( $text, $linestart = true, $interface = true ) {
1649
		$title = $this->getTitle(); // Work around E_STRICT
1650
		if ( !$title ) {
1651
			throw new MWException( 'Title is null' );
1652
		}
1653
		$this->addWikiTextTitle( $text, $title, $linestart, /*tidy*/false, $interface );
1654
	}
1655
1656
	/**
1657
	 * Add wikitext with a custom Title object
1658
	 *
1659
	 * @param string $text Wikitext
1660
	 * @param Title $title
1661
	 * @param bool $linestart Is this the start of a line?
1662
	 */
1663
	public function addWikiTextWithTitle( $text, &$title, $linestart = true ) {
1664
		$this->addWikiTextTitle( $text, $title, $linestart );
1665
	}
1666
1667
	/**
1668
	 * Add wikitext with a custom Title object and tidy enabled.
1669
	 *
1670
	 * @param string $text Wikitext
1671
	 * @param Title $title
1672
	 * @param bool $linestart Is this the start of a line?
1673
	 */
1674
	function addWikiTextTitleTidy( $text, &$title, $linestart = true ) {
1675
		$this->addWikiTextTitle( $text, $title, $linestart, true );
1676
	}
1677
1678
	/**
1679
	 * Add wikitext with tidy enabled
1680
	 *
1681
	 * @param string $text Wikitext
1682
	 * @param bool $linestart Is this the start of a line?
1683
	 */
1684
	public function addWikiTextTidy( $text, $linestart = true ) {
1685
		$title = $this->getTitle();
1686
		$this->addWikiTextTitleTidy( $text, $title, $linestart );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->getTitle() on line 1685 can be null; however, OutputPage::addWikiTextTitleTidy() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1687
	}
1688
1689
	/**
1690
	 * Add wikitext with a custom Title object
1691
	 *
1692
	 * @param string $text Wikitext
1693
	 * @param Title $title
1694
	 * @param bool $linestart Is this the start of a line?
1695
	 * @param bool $tidy Whether to use tidy
1696
	 * @param bool $interface Whether it is an interface message
1697
	 *   (for example disables conversion)
1698
	 */
1699
	public function addWikiTextTitle( $text, Title $title, $linestart,
1700
		$tidy = false, $interface = false
1701
	) {
1702
		global $wgParser;
1703
1704
		$popts = $this->parserOptions();
1705
		$oldTidy = $popts->setTidy( $tidy );
1706
		$popts->setInterfaceMessage( (bool)$interface );
1707
1708
		$parserOutput = $wgParser->getFreshParser()->parse(
1709
			$text, $title, $popts,
1710
			$linestart, true, $this->mRevisionId
1711
		);
1712
1713
		$popts->setTidy( $oldTidy );
1714
1715
		$this->addParserOutput( $parserOutput );
1716
1717
	}
1718
1719
	/**
1720
	 * Add a ParserOutput object, but without Html.
1721
	 *
1722
	 * @deprecated since 1.24, use addParserOutputMetadata() instead.
1723
	 * @param ParserOutput $parserOutput
1724
	 */
1725
	public function addParserOutputNoText( $parserOutput ) {
1726
		wfDeprecated( __METHOD__, '1.24' );
1727
		$this->addParserOutputMetadata( $parserOutput );
1728
	}
1729
1730
	/**
1731
	 * Add all metadata associated with a ParserOutput object, but without the actual HTML. This
1732
	 * includes categories, language links, ResourceLoader modules, effects of certain magic words,
1733
	 * and so on.
1734
	 *
1735
	 * @since 1.24
1736
	 * @param ParserOutput $parserOutput
1737
	 */
1738
	public function addParserOutputMetadata( $parserOutput ) {
1739
		$this->mLanguageLinks += $parserOutput->getLanguageLinks();
1740
		$this->addCategoryLinks( $parserOutput->getCategories() );
1741
		$this->setIndicators( $parserOutput->getIndicators() );
1742
		$this->mNewSectionLink = $parserOutput->getNewSection();
1743
		$this->mHideNewSectionLink = $parserOutput->getHideNewSection();
1744
1745
		if ( !$parserOutput->isCacheable() ) {
1746
			$this->enableClientCache( false );
1747
		}
1748
		$this->mNoGallery = $parserOutput->getNoGallery();
1749
		$this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
1750
		$this->addModules( $parserOutput->getModules() );
1751
		$this->addModuleScripts( $parserOutput->getModuleScripts() );
1752
		$this->addModuleStyles( $parserOutput->getModuleStyles() );
1753
		$this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1754
		$this->mPreventClickjacking = $this->mPreventClickjacking
1755
			|| $parserOutput->preventClickjacking();
1756
1757
		// Template versioning...
1758
		foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
1759
			if ( isset( $this->mTemplateIds[$ns] ) ) {
1760
				$this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1761
			} else {
1762
				$this->mTemplateIds[$ns] = $dbks;
1763
			}
1764
		}
1765
		// File versioning...
1766
		foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
1767
			$this->mImageTimeKeys[$dbk] = $data;
1768
		}
1769
1770
		// Hooks registered in the object
1771
		$parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' );
1772
		foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
1773
			list( $hookName, $data ) = $hookInfo;
1774
			if ( isset( $parserOutputHooks[$hookName] ) ) {
1775
				call_user_func( $parserOutputHooks[$hookName], $this, $parserOutput, $data );
1776
			}
1777
		}
1778
1779
		// Enable OOUI if requested via ParserOutput
1780
		if ( $parserOutput->getEnableOOUI() ) {
1781
			$this->enableOOUI();
1782
		}
1783
1784
		// Include profiling data
1785
		if ( !$this->limitReportData ) {
1786
			$this->setLimitReportData( $parserOutput->getLimitReportData() );
1787
		}
1788
1789
		// Link flags are ignored for now, but may in the future be
1790
		// used to mark individual language links.
1791
		$linkFlags = [];
1792
		Hooks::run( 'LanguageLinks', [ $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ] );
1793
		Hooks::run( 'OutputPageParserOutput', [ &$this, $parserOutput ] );
1794
	}
1795
1796
	/**
1797
	 * Add the HTML and enhancements for it (like ResourceLoader modules) associated with a
1798
	 * ParserOutput object, without any other metadata.
1799
	 *
1800
	 * @since 1.24
1801
	 * @param ParserOutput $parserOutput
1802
	 */
1803
	public function addParserOutputContent( $parserOutput ) {
1804
		$this->addParserOutputText( $parserOutput );
1805
1806
		$this->addModules( $parserOutput->getModules() );
1807
		$this->addModuleScripts( $parserOutput->getModuleScripts() );
1808
		$this->addModuleStyles( $parserOutput->getModuleStyles() );
1809
1810
		$this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1811
	}
1812
1813
	/**
1814
	 * Add the HTML associated with a ParserOutput object, without any metadata.
1815
	 *
1816
	 * @since 1.24
1817
	 * @param ParserOutput $parserOutput
1818
	 */
1819
	public function addParserOutputText( $parserOutput ) {
1820
		$text = $parserOutput->getText();
1821
		Hooks::run( 'OutputPageBeforeHTML', [ &$this, &$text ] );
1822
		$this->addHTML( $text );
1823
	}
1824
1825
	/**
1826
	 * Add everything from a ParserOutput object.
1827
	 *
1828
	 * @param ParserOutput $parserOutput
1829
	 */
1830
	function addParserOutput( $parserOutput ) {
1831
		$this->addParserOutputMetadata( $parserOutput );
1832
		$parserOutput->setTOCEnabled( $this->mEnableTOC );
1833
1834
		// Touch section edit links only if not previously disabled
1835
		if ( $parserOutput->getEditSectionTokens() ) {
1836
			$parserOutput->setEditSectionTokens( $this->mEnableSectionEditLinks );
1837
		}
1838
1839
		$this->addParserOutputText( $parserOutput );
1840
	}
1841
1842
	/**
1843
	 * Add the output of a QuickTemplate to the output buffer
1844
	 *
1845
	 * @param QuickTemplate $template
1846
	 */
1847
	public function addTemplate( &$template ) {
1848
		$this->addHTML( $template->getHTML() );
1849
	}
1850
1851
	/**
1852
	 * Parse wikitext and return the HTML.
1853
	 *
1854
	 * @param string $text
1855
	 * @param bool $linestart Is this the start of a line?
1856
	 * @param bool $interface Use interface language ($wgLang instead of
1857
	 *   $wgContLang) while parsing language sensitive magic words like GRAMMAR and PLURAL.
1858
	 *   This also disables LanguageConverter.
1859
	 * @param Language $language Target language object, will override $interface
1860
	 * @throws MWException
1861
	 * @return string HTML
1862
	 */
1863
	public function parse( $text, $linestart = true, $interface = false, $language = null ) {
1864
		global $wgParser;
1865
1866
		if ( is_null( $this->getTitle() ) ) {
1867
			throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
1868
		}
1869
1870
		$popts = $this->parserOptions();
1871
		if ( $interface ) {
1872
			$popts->setInterfaceMessage( true );
1873
		}
1874
		if ( $language !== null ) {
1875
			$oldLang = $popts->setTargetLanguage( $language );
1876
		}
1877
1878
		$parserOutput = $wgParser->getFreshParser()->parse(
1879
			$text, $this->getTitle(), $popts,
1880
			$linestart, true, $this->mRevisionId
1881
		);
1882
1883
		if ( $interface ) {
1884
			$popts->setInterfaceMessage( false );
1885
		}
1886
		if ( $language !== null ) {
1887
			$popts->setTargetLanguage( $oldLang );
0 ignored issues
show
Bug introduced by
The variable $oldLang does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1888
		}
1889
1890
		return $parserOutput->getText();
1891
	}
1892
1893
	/**
1894
	 * Parse wikitext, strip paragraphs, and return the HTML.
1895
	 *
1896
	 * @param string $text
1897
	 * @param bool $linestart Is this the start of a line?
1898
	 * @param bool $interface Use interface language ($wgLang instead of
1899
	 *   $wgContLang) while parsing language sensitive magic
1900
	 *   words like GRAMMAR and PLURAL
1901
	 * @return string HTML
1902
	 */
1903
	public function parseInline( $text, $linestart = true, $interface = false ) {
1904
		$parsed = $this->parse( $text, $linestart, $interface );
1905
		return Parser::stripOuterParagraph( $parsed );
1906
	}
1907
1908
	/**
1909
	 * @param $maxage
1910
	 * @deprecated since 1.27 Use setCdnMaxage() instead
1911
	 */
1912
	public function setSquidMaxage( $maxage ) {
1913
		$this->setCdnMaxage( $maxage );
1914
	}
1915
1916
	/**
1917
	 * Set the value of the "s-maxage" part of the "Cache-control" HTTP header
1918
	 *
1919
	 * @param int $maxage Maximum cache time on the CDN, in seconds.
1920
	 */
1921
	public function setCdnMaxage( $maxage ) {
1922
		$this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
1923
	}
1924
1925
	/**
1926
	 * Lower the value of the "s-maxage" part of the "Cache-control" HTTP header
1927
	 *
1928
	 * @param int $maxage Maximum cache time on the CDN, in seconds
1929
	 * @since 1.27
1930
	 */
1931
	public function lowerCdnMaxage( $maxage ) {
1932
		$this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
1933
		$this->setCdnMaxage( $this->mCdnMaxage );
1934
	}
1935
1936
	/**
1937
	 * Use enableClientCache(false) to force it to send nocache headers
1938
	 *
1939
	 * @param bool $state
1940
	 *
1941
	 * @return bool
1942
	 */
1943
	public function enableClientCache( $state ) {
1944
		return wfSetVar( $this->mEnableClientCache, $state );
1945
	}
1946
1947
	/**
1948
	 * Get the list of cookies that will influence on the cache
1949
	 *
1950
	 * @return array
1951
	 */
1952
	function getCacheVaryCookies() {
1953
		static $cookies;
1954
		if ( $cookies === null ) {
1955
			$config = $this->getConfig();
1956
			$cookies = array_merge(
1957
				SessionManager::singleton()->getVaryCookies(),
1958
				[
1959
					'forceHTTPS',
1960
				],
1961
				$config->get( 'CacheVaryCookies' )
1962
			);
1963
			Hooks::run( 'GetCacheVaryCookies', [ $this, &$cookies ] );
1964
		}
1965
		return $cookies;
1966
	}
1967
1968
	/**
1969
	 * Check if the request has a cache-varying cookie header
1970
	 * If it does, it's very important that we don't allow public caching
1971
	 *
1972
	 * @return bool
1973
	 */
1974
	function haveCacheVaryCookies() {
1975
		$request = $this->getRequest();
1976
		foreach ( $this->getCacheVaryCookies() as $cookieName ) {
1977
			if ( $request->getCookie( $cookieName, '', '' ) !== '' ) {
1978
				wfDebug( __METHOD__ . ": found $cookieName\n" );
1979
				return true;
1980
			}
1981
		}
1982
		wfDebug( __METHOD__ . ": no cache-varying cookies found\n" );
1983
		return false;
1984
	}
1985
1986
	/**
1987
	 * Add an HTTP header that will influence on the cache
1988
	 *
1989
	 * @param string $header Header name
1990
	 * @param string[]|null $option Options for the Key header. See
1991
	 * https://datatracker.ietf.org/doc/draft-fielding-http-key/
1992
	 * for the list of valid options.
1993
	 */
1994
	public function addVaryHeader( $header, array $option = null ) {
1995
		if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
1996
			$this->mVaryHeader[$header] = [];
1997
		}
1998
		if ( !is_array( $option ) ) {
1999
			$option = [];
2000
		}
2001
		$this->mVaryHeader[$header] = array_unique( array_merge( $this->mVaryHeader[$header], $option ) );
2002
	}
2003
2004
	/**
2005
	 * Return a Vary: header on which to vary caches. Based on the keys of $mVaryHeader,
2006
	 * such as Accept-Encoding or Cookie
2007
	 *
2008
	 * @return string
2009
	 */
2010
	public function getVaryHeader() {
2011
		// If we vary on cookies, let's make sure it's always included here too.
2012
		if ( $this->getCacheVaryCookies() ) {
2013
			$this->addVaryHeader( 'Cookie' );
2014
		}
2015
2016
		foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2017
			$this->addVaryHeader( $header, $options );
2018
		}
2019
		return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
2020
	}
2021
2022
	/**
2023
	 * Get a complete Key header
2024
	 *
2025
	 * @return string
2026
	 */
2027
	public function getKeyHeader() {
2028
		$cvCookies = $this->getCacheVaryCookies();
2029
2030
		$cookiesOption = [];
2031
		foreach ( $cvCookies as $cookieName ) {
2032
			$cookiesOption[] = 'param=' . $cookieName;
2033
		}
2034
		$this->addVaryHeader( 'Cookie', $cookiesOption );
2035
2036
		foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2037
			$this->addVaryHeader( $header, $options );
2038
		}
2039
2040
		$headers = [];
2041
		foreach ( $this->mVaryHeader as $header => $option ) {
2042
			$newheader = $header;
2043
			if ( is_array( $option ) && count( $option ) > 0 ) {
2044
				$newheader .= ';' . implode( ';', $option );
2045
			}
2046
			$headers[] = $newheader;
2047
		}
2048
		$key = 'Key: ' . implode( ',', $headers );
2049
2050
		return $key;
2051
	}
2052
2053
	/**
2054
	 * T23672: Add Accept-Language to Vary and Key headers
2055
	 * if there's no 'variant' parameter existed in GET.
2056
	 *
2057
	 * For example:
2058
	 *   /w/index.php?title=Main_page should always be served; but
2059
	 *   /w/index.php?title=Main_page&variant=zh-cn should never be served.
2060
	 */
2061
	function addAcceptLanguage() {
2062
		$title = $this->getTitle();
2063
		if ( !$title instanceof Title ) {
2064
			return;
2065
		}
2066
2067
		$lang = $title->getPageLanguage();
2068
		if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
2069
			$variants = $lang->getVariants();
2070
			$aloption = [];
2071
			foreach ( $variants as $variant ) {
2072
				if ( $variant === $lang->getCode() ) {
2073
					continue;
2074
				} else {
2075
					$aloption[] = 'substr=' . $variant;
2076
2077
					// IE and some other browsers use BCP 47 standards in
2078
					// their Accept-Language header, like "zh-CN" or "zh-Hant".
2079
					// We should handle these too.
2080
					$variantBCP47 = wfBCP47( $variant );
2081
					if ( $variantBCP47 !== $variant ) {
2082
						$aloption[] = 'substr=' . $variantBCP47;
2083
					}
2084
				}
2085
			}
2086
			$this->addVaryHeader( 'Accept-Language', $aloption );
2087
		}
2088
	}
2089
2090
	/**
2091
	 * Set a flag which will cause an X-Frame-Options header appropriate for
2092
	 * edit pages to be sent. The header value is controlled by
2093
	 * $wgEditPageFrameOptions.
2094
	 *
2095
	 * This is the default for special pages. If you display a CSRF-protected
2096
	 * form on an ordinary view page, then you need to call this function.
2097
	 *
2098
	 * @param bool $enable
2099
	 */
2100
	public function preventClickjacking( $enable = true ) {
2101
		$this->mPreventClickjacking = $enable;
2102
	}
2103
2104
	/**
2105
	 * Turn off frame-breaking. Alias for $this->preventClickjacking(false).
2106
	 * This can be called from pages which do not contain any CSRF-protected
2107
	 * HTML form.
2108
	 */
2109
	public function allowClickjacking() {
2110
		$this->mPreventClickjacking = false;
2111
	}
2112
2113
	/**
2114
	 * Get the prevent-clickjacking flag
2115
	 *
2116
	 * @since 1.24
2117
	 * @return bool
2118
	 */
2119
	public function getPreventClickjacking() {
2120
		return $this->mPreventClickjacking;
2121
	}
2122
2123
	/**
2124
	 * Get the X-Frame-Options header value (without the name part), or false
2125
	 * if there isn't one. This is used by Skin to determine whether to enable
2126
	 * JavaScript frame-breaking, for clients that don't support X-Frame-Options.
2127
	 *
2128
	 * @return string
2129
	 */
2130
	public function getFrameOptions() {
2131
		$config = $this->getConfig();
2132
		if ( $config->get( 'BreakFrames' ) ) {
2133
			return 'DENY';
2134
		} elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) {
2135
			return $config->get( 'EditPageFrameOptions' );
2136
		}
2137
		return false;
2138
	}
2139
2140
	/**
2141
	 * Send cache control HTTP headers
2142
	 */
2143
	public function sendCacheControl() {
2144
		$response = $this->getRequest()->response();
2145
		$config = $this->getConfig();
2146
2147
		$this->addVaryHeader( 'Cookie' );
2148
		$this->addAcceptLanguage();
2149
2150
		# don't serve compressed data to clients who can't handle it
2151
		# maintain different caches for logged-in users and non-logged in ones
2152
		$response->header( $this->getVaryHeader() );
2153
2154
		if ( $config->get( 'UseKeyHeader' ) ) {
2155
			$response->header( $this->getKeyHeader() );
2156
		}
2157
2158
		if ( $this->mEnableClientCache ) {
2159
			if (
2160
				$config->get( 'UseSquid' ) &&
2161
				!$response->hasCookies() &&
2162
				!SessionManager::getGlobalSession()->isPersistent() &&
2163
				!$this->isPrintable() &&
2164
				$this->mCdnMaxage != 0 &&
2165
				!$this->haveCacheVaryCookies()
2166
			) {
2167
				if ( $config->get( 'UseESI' ) ) {
2168
					# We'll purge the proxy cache explicitly, but require end user agents
2169
					# to revalidate against the proxy on each visit.
2170
					# Surrogate-Control controls our CDN, Cache-Control downstream caches
2171
					wfDebug( __METHOD__ . ": proxy caching with ESI; {$this->mLastModified} **", 'private' );
2172
					# start with a shorter timeout for initial testing
2173
					# header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
2174
					$response->header( 'Surrogate-Control: max-age=' . $config->get( 'SquidMaxage' )
2175
						. '+' . $this->mCdnMaxage . ', content="ESI/1.0"' );
2176
					$response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
2177
				} else {
2178
					# We'll purge the proxy cache for anons explicitly, but require end user agents
2179
					# to revalidate against the proxy on each visit.
2180
					# IMPORTANT! The CDN needs to replace the Cache-Control header with
2181
					# Cache-Control: s-maxage=0, must-revalidate, max-age=0
2182
					wfDebug( __METHOD__ . ": local proxy caching; {$this->mLastModified} **", 'private' );
2183
					# start with a shorter timeout for initial testing
2184
					# header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
2185
					$response->header( 'Cache-Control: s-maxage=' . $this->mCdnMaxage
2186
						. ', must-revalidate, max-age=0' );
2187
				}
2188 View Code Duplication
			} else {
2189
				# We do want clients to cache if they can, but they *must* check for updates
2190
				# on revisiting the page.
2191
				wfDebug( __METHOD__ . ": private caching; {$this->mLastModified} **", 'private' );
2192
				$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2193
				$response->header( "Cache-Control: private, must-revalidate, max-age=0" );
2194
			}
2195
			if ( $this->mLastModified ) {
2196
				$response->header( "Last-Modified: {$this->mLastModified}" );
2197
			}
2198 View Code Duplication
		} else {
2199
			wfDebug( __METHOD__ . ": no caching **", 'private' );
2200
2201
			# In general, the absence of a last modified header should be enough to prevent
2202
			# the client from using its cache. We send a few other things just to make sure.
2203
			$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2204
			$response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
2205
			$response->header( 'Pragma: no-cache' );
2206
		}
2207
	}
2208
2209
	/**
2210
	 * Finally, all the text has been munged and accumulated into
2211
	 * the object, let's actually output it:
2212
	 *
2213
	 * @param bool $return Set to true to get the result as a string rather than sending it
2214
	 * @return string|null
2215
	 * @throws Exception
2216
	 * @throws FatalError
2217
	 * @throws MWException
2218
	 */
2219
	public function output( $return = false ) {
2220
		global $wgContLang;
2221
2222
		if ( $this->mDoNothing ) {
2223
			return $return ? '' : null;
2224
		}
2225
2226
		$response = $this->getRequest()->response();
2227
		$config = $this->getConfig();
2228
2229
		if ( $this->mRedirect != '' ) {
2230
			# Standards require redirect URLs to be absolute
2231
			$this->mRedirect = wfExpandUrl( $this->mRedirect, PROTO_CURRENT );
0 ignored issues
show
Documentation Bug introduced by
It seems like wfExpandUrl($this->mRedirect, PROTO_CURRENT) can also be of type false. However, the property $mRedirect is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2232
2233
			$redirect = $this->mRedirect;
2234
			$code = $this->mRedirectCode;
2235
2236
			if ( Hooks::run( "BeforePageRedirect", [ $this, &$redirect, &$code ] ) ) {
2237
				if ( $code == '301' || $code == '303' ) {
2238
					if ( !$config->get( 'DebugRedirects' ) ) {
2239
						$response->statusHeader( $code );
2240
					}
2241
					$this->mLastModified = wfTimestamp( TS_RFC2822 );
0 ignored issues
show
Documentation Bug introduced by
It seems like wfTimestamp(TS_RFC2822) can also be of type false. However, the property $mLastModified is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2242
				}
2243
				if ( $config->get( 'VaryOnXFP' ) ) {
2244
					$this->addVaryHeader( 'X-Forwarded-Proto' );
2245
				}
2246
				$this->sendCacheControl();
2247
2248
				$response->header( "Content-Type: text/html; charset=utf-8" );
2249
				if ( $config->get( 'DebugRedirects' ) ) {
2250
					$url = htmlspecialchars( $redirect );
2251
					print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
2252
					print "<p>Location: <a href=\"$url\">$url</a></p>\n";
2253
					print "</body>\n</html>\n";
2254
				} else {
2255
					$response->header( 'Location: ' . $redirect );
2256
				}
2257
			}
2258
2259
			return $return ? '' : null;
2260
		} elseif ( $this->mStatusCode ) {
2261
			$response->statusHeader( $this->mStatusCode );
2262
		}
2263
2264
		# Buffer output; final headers may depend on later processing
2265
		ob_start();
2266
2267
		$response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
2268
		$response->header( 'Content-language: ' . $wgContLang->getHtmlCode() );
2269
2270
		// Avoid Internet Explorer "compatibility view" in IE 8-10, so that
2271
		// jQuery etc. can work correctly.
2272
		$response->header( 'X-UA-Compatible: IE=Edge' );
2273
2274
		// Prevent framing, if requested
2275
		$frameOptions = $this->getFrameOptions();
2276
		if ( $frameOptions ) {
2277
			$response->header( "X-Frame-Options: $frameOptions" );
2278
		}
2279
2280
		if ( $this->mArticleBodyOnly ) {
2281
			echo $this->mBodytext;
2282
		} else {
2283
			$sk = $this->getSkin();
2284
			// add skin specific modules
2285
			$modules = $sk->getDefaultModules();
2286
2287
			// Enforce various default modules for all pages and all skins
2288
			$coreModules = [
2289
				// Keep this list as small as possible
2290
				'site',
2291
				'mediawiki.page.startup',
2292
				'mediawiki.user',
2293
			];
2294
2295
			// Support for high-density display images if enabled
2296
			if ( $config->get( 'ResponsiveImages' ) ) {
2297
				$coreModules[] = 'mediawiki.hidpi';
2298
			}
2299
2300
			$this->addModules( $coreModules );
2301
			foreach ( $modules as $group ) {
2302
				$this->addModules( $group );
2303
			}
2304
			MWDebug::addModules( $this );
2305
2306
			// Hook that allows last minute changes to the output page, e.g.
2307
			// adding of CSS or Javascript by extensions.
2308
			Hooks::run( 'BeforePageDisplay', [ &$this, &$sk ] );
2309
2310
			try {
2311
				$sk->outputPage();
2312
			} catch ( Exception $e ) {
2313
				ob_end_clean(); // bug T129657
2314
				throw $e;
2315
			}
2316
		}
2317
2318
		try {
2319
			// This hook allows last minute changes to final overall output by modifying output buffer
2320
			Hooks::run( 'AfterFinalPageOutput', [ $this ] );
2321
		} catch ( Exception $e ) {
2322
			ob_end_clean(); // bug T129657
2323
			throw $e;
2324
		}
2325
2326
		$this->sendCacheControl();
2327
2328
		if ( $return ) {
2329
			return ob_get_clean();
2330
		} else {
2331
			ob_end_flush();
2332
			return null;
2333
		}
2334
	}
2335
2336
	/**
2337
	 * Prepare this object to display an error page; disable caching and
2338
	 * indexing, clear the current text and redirect, set the page's title
2339
	 * and optionally an custom HTML title (content of the "<title>" tag).
2340
	 *
2341
	 * @param string|Message $pageTitle Will be passed directly to setPageTitle()
2342
	 * @param string|Message $htmlTitle Will be passed directly to setHTMLTitle();
2343
	 *                   optional, if not passed the "<title>" attribute will be
2344
	 *                   based on $pageTitle
2345
	 */
2346
	public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
2347
		$this->setPageTitle( $pageTitle );
2348
		if ( $htmlTitle !== false ) {
2349
			$this->setHTMLTitle( $htmlTitle );
2350
		}
2351
		$this->setRobotPolicy( 'noindex,nofollow' );
2352
		$this->setArticleRelated( false );
2353
		$this->enableClientCache( false );
2354
		$this->mRedirect = '';
2355
		$this->clearSubtitle();
2356
		$this->clearHTML();
2357
	}
2358
2359
	/**
2360
	 * Output a standard error page
2361
	 *
2362
	 * showErrorPage( 'titlemsg', 'pagetextmsg' );
2363
	 * showErrorPage( 'titlemsg', 'pagetextmsg', [ 'param1', 'param2' ] );
2364
	 * showErrorPage( 'titlemsg', $messageObject );
2365
	 * showErrorPage( $titleMessageObject, $messageObject );
2366
	 *
2367
	 * @param string|Message $title Message key (string) for page title, or a Message object
2368
	 * @param string|Message $msg Message key (string) for page text, or a Message object
2369
	 * @param array $params Message parameters; ignored if $msg is a Message object
2370
	 */
2371
	public function showErrorPage( $title, $msg, $params = [] ) {
2372
		if ( !$title instanceof Message ) {
2373
			$title = $this->msg( $title );
2374
		}
2375
2376
		$this->prepareErrorPage( $title );
2377
2378
		if ( $msg instanceof Message ) {
2379
			if ( $params !== [] ) {
2380
				trigger_error( 'Argument ignored: $params. The message parameters argument '
2381
					. 'is discarded when the $msg argument is a Message object instead of '
2382
					. 'a string.', E_USER_NOTICE );
2383
			}
2384
			$this->addHTML( $msg->parseAsBlock() );
2385
		} else {
2386
			$this->addWikiMsgArray( $msg, $params );
2387
		}
2388
2389
		$this->returnToMain();
2390
	}
2391
2392
	/**
2393
	 * Output a standard permission error page
2394
	 *
2395
	 * @param array $errors Error message keys
2396
	 * @param string $action Action that was denied or null if unknown
2397
	 */
2398
	public function showPermissionsErrorPage( array $errors, $action = null ) {
2399
		// For some action (read, edit, create and upload), display a "login to do this action"
2400
		// error if all of the following conditions are met:
2401
		// 1. the user is not logged in
2402
		// 2. the only error is insufficient permissions (i.e. no block or something else)
2403
		// 3. the error can be avoided simply by logging in
2404
		if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] )
2405
			&& $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
2406
			&& ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
2407
			&& ( User::groupHasPermission( 'user', $action )
2408
			|| User::groupHasPermission( 'autoconfirmed', $action ) )
2409
		) {
2410
			$displayReturnto = null;
2411
2412
			# Due to bug 32276, if a user does not have read permissions,
2413
			# $this->getTitle() will just give Special:Badtitle, which is
2414
			# not especially useful as a returnto parameter. Use the title
2415
			# from the request instead, if there was one.
2416
			$request = $this->getRequest();
2417
			$returnto = Title::newFromText( $request->getVal( 'title', '' ) );
2418
			if ( $action == 'edit' ) {
2419
				$msg = 'whitelistedittext';
2420
				$displayReturnto = $returnto;
2421
			} elseif ( $action == 'createpage' || $action == 'createtalk' ) {
2422
				$msg = 'nocreatetext';
2423
			} elseif ( $action == 'upload' ) {
2424
				$msg = 'uploadnologintext';
2425
			} else { # Read
2426
				$msg = 'loginreqpagetext';
2427
				$displayReturnto = Title::newMainPage();
2428
			}
2429
2430
			$query = [];
2431
2432
			if ( $returnto ) {
2433
				$query['returnto'] = $returnto->getPrefixedText();
2434
2435 View Code Duplication
				if ( !$request->wasPosted() ) {
2436
					$returntoquery = $request->getValues();
2437
					unset( $returntoquery['title'] );
2438
					unset( $returntoquery['returnto'] );
2439
					unset( $returntoquery['returntoquery'] );
2440
					$query['returntoquery'] = wfArrayToCgi( $returntoquery );
2441
				}
2442
			}
2443
			$loginLink = Linker::linkKnown(
2444
				SpecialPage::getTitleFor( 'Userlogin' ),
2445
				$this->msg( 'loginreqlink' )->escaped(),
2446
				[],
2447
				$query
2448
			);
2449
2450
			$this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
2451
			$this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->parse() );
2452
2453
			# Don't return to a page the user can't read otherwise
2454
			# we'll end up in a pointless loop
2455
			if ( $displayReturnto && $displayReturnto->userCan( 'read', $this->getUser() ) ) {
2456
				$this->returnToMain( null, $displayReturnto );
2457
			}
2458
		} else {
2459
			$this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
2460
			$this->addWikiText( $this->formatPermissionsErrorMessage( $errors, $action ) );
2461
		}
2462
	}
2463
2464
	/**
2465
	 * Display an error page indicating that a given version of MediaWiki is
2466
	 * required to use it
2467
	 *
2468
	 * @param mixed $version The version of MediaWiki needed to use the page
2469
	 */
2470
	public function versionRequired( $version ) {
2471
		$this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
2472
2473
		$this->addWikiMsg( 'versionrequiredtext', $version );
2474
		$this->returnToMain();
2475
	}
2476
2477
	/**
2478
	 * Format a list of error messages
2479
	 *
2480
	 * @param array $errors Array of arrays returned by Title::getUserPermissionsErrors
2481
	 * @param string $action Action that was denied or null if unknown
2482
	 * @return string The wikitext error-messages, formatted into a list.
2483
	 */
2484
	public function formatPermissionsErrorMessage( array $errors, $action = null ) {
2485
		if ( $action == null ) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $action of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2486
			$text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
2487
		} else {
2488
			$action_desc = $this->msg( "action-$action" )->plain();
2489
			$text = $this->msg(
2490
				'permissionserrorstext-withaction',
2491
				count( $errors ),
2492
				$action_desc
2493
			)->plain() . "\n\n";
2494
		}
2495
2496
		if ( count( $errors ) > 1 ) {
2497
			$text .= '<ul class="permissions-errors">' . "\n";
2498
2499
			foreach ( $errors as $error ) {
2500
				$text .= '<li>';
2501
				$text .= call_user_func_array( [ $this, 'msg' ], $error )->plain();
2502
				$text .= "</li>\n";
2503
			}
2504
			$text .= '</ul>';
2505
		} else {
2506
			$text .= "<div class=\"permissions-errors\">\n" .
2507
					call_user_func_array( [ $this, 'msg' ], reset( $errors ) )->plain() .
2508
					"\n</div>";
2509
		}
2510
2511
		return $text;
2512
	}
2513
2514
	/**
2515
	 * Display a page stating that the Wiki is in read-only mode.
2516
	 * Should only be called after wfReadOnly() has returned true.
2517
	 *
2518
	 * Historically, this function was used to show the source of the page that the user
2519
	 * was trying to edit and _also_ permissions error messages. The relevant code was
2520
	 * moved into EditPage in 1.19 (r102024 / d83c2a431c2a) and removed here in 1.25.
2521
	 *
2522
	 * @deprecated since 1.25; throw the exception directly
2523
	 * @throws ReadOnlyError
2524
	 */
2525
	public function readOnlyPage() {
2526
		if ( func_num_args() > 0 ) {
2527
			throw new MWException( __METHOD__ . ' no longer accepts arguments since 1.25.' );
2528
		}
2529
2530
		throw new ReadOnlyError;
2531
	}
2532
2533
	/**
2534
	 * Turn off regular page output and return an error response
2535
	 * for when rate limiting has triggered.
2536
	 *
2537
	 * @deprecated since 1.25; throw the exception directly
2538
	 */
2539
	public function rateLimited() {
2540
		wfDeprecated( __METHOD__, '1.25' );
2541
		throw new ThrottledError;
2542
	}
2543
2544
	/**
2545
	 * Show a warning about replica DB lag
2546
	 *
2547
	 * If the lag is higher than $wgSlaveLagCritical seconds,
2548
	 * then the warning is a bit more obvious. If the lag is
2549
	 * lower than $wgSlaveLagWarning, then no warning is shown.
2550
	 *
2551
	 * @param int $lag Slave lag
2552
	 */
2553
	public function showLagWarning( $lag ) {
2554
		$config = $this->getConfig();
2555
		if ( $lag >= $config->get( 'SlaveLagWarning' ) ) {
2556
			$lag = floor( $lag ); // floor to avoid nano seconds to display
2557
			$message = $lag < $config->get( 'SlaveLagCritical' )
2558
				? 'lag-warn-normal'
2559
				: 'lag-warn-high';
2560
			$wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" );
2561
			$this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] );
2562
		}
2563
	}
2564
2565
	public function showFatalError( $message ) {
2566
		$this->prepareErrorPage( $this->msg( 'internalerror' ) );
2567
2568
		$this->addHTML( $message );
2569
	}
2570
2571
	public function showUnexpectedValueError( $name, $val ) {
2572
		$this->showFatalError( $this->msg( 'unexpected', $name, $val )->text() );
2573
	}
2574
2575
	public function showFileCopyError( $old, $new ) {
2576
		$this->showFatalError( $this->msg( 'filecopyerror', $old, $new )->text() );
2577
	}
2578
2579
	public function showFileRenameError( $old, $new ) {
2580
		$this->showFatalError( $this->msg( 'filerenameerror', $old, $new )->text() );
2581
	}
2582
2583
	public function showFileDeleteError( $name ) {
2584
		$this->showFatalError( $this->msg( 'filedeleteerror', $name )->text() );
2585
	}
2586
2587
	public function showFileNotFoundError( $name ) {
2588
		$this->showFatalError( $this->msg( 'filenotfound', $name )->text() );
2589
	}
2590
2591
	/**
2592
	 * Add a "return to" link pointing to a specified title
2593
	 *
2594
	 * @param Title $title Title to link
2595
	 * @param array $query Query string parameters
2596
	 * @param string $text Text of the link (input is not escaped)
2597
	 * @param array $options Options array to pass to Linker
2598
	 */
2599
	public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) {
2600
		$link = $this->msg( 'returnto' )->rawParams(
2601
			Linker::link( $title, $text, [], $query, $options ) )->escaped();
2602
		$this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
2603
	}
2604
2605
	/**
2606
	 * Add a "return to" link pointing to a specified title,
2607
	 * or the title indicated in the request, or else the main page
2608
	 *
2609
	 * @param mixed $unused
2610
	 * @param Title|string $returnto Title or String to return to
2611
	 * @param string $returntoquery Query string for the return to link
2612
	 */
2613
	public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
2614
		if ( $returnto == null ) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $returnto of type Title|string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2615
			$returnto = $this->getRequest()->getText( 'returnto' );
2616
		}
2617
2618
		if ( $returntoquery == null ) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $returntoquery of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2619
			$returntoquery = $this->getRequest()->getText( 'returntoquery' );
2620
		}
2621
2622
		if ( $returnto === '' ) {
2623
			$returnto = Title::newMainPage();
2624
		}
2625
2626
		if ( is_object( $returnto ) ) {
2627
			$titleObj = $returnto;
2628
		} else {
2629
			$titleObj = Title::newFromText( $returnto );
2630
		}
2631
		if ( !is_object( $titleObj ) ) {
2632
			$titleObj = Title::newMainPage();
2633
		}
2634
2635
		$this->addReturnTo( $titleObj, wfCgiToArray( $returntoquery ) );
0 ignored issues
show
Bug introduced by
It seems like $titleObj can be null; however, addReturnTo() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2636
	}
2637
2638
	private function getRlClientContext() {
2639
		if ( !$this->rlClientContext ) {
2640
			$query = ResourceLoader::makeLoaderQuery(
2641
				[], // modules; not relevant
2642
				$this->getLanguage()->getCode(),
2643
				$this->getSkin()->getSkinName(),
2644
				$this->getUser()->isLoggedIn() ? $this->getUser()->getName() : null,
2645
				null, // version; not relevant
2646
				ResourceLoader::inDebugMode(),
2647
				null, // only; not relevant
2648
				$this->isPrintable(),
2649
				$this->getRequest()->getBool( 'handheld' )
2650
			);
2651
			$this->rlClientContext = new ResourceLoaderContext(
2652
				$this->getResourceLoader(),
2653
				new FauxRequest( $query )
2654
			);
2655
		}
2656
		return $this->rlClientContext;
2657
	}
2658
2659
	/**
2660
	 * Call this to freeze the module queue and JS config and create a formatter.
2661
	 *
2662
	 * Depending on the Skin, this may get lazy-initialised in either headElement() or
2663
	 * getBottomScripts(). See SkinTemplate::prepareQuickTemplate(). Calling this too early may
2664
	 * cause unexpected side-effects since disallowUserJs() may be called at any time to change
2665
	 * the module filters retroactively. Skins and extension hooks may also add modules until very
2666
	 * late in the request lifecycle.
2667
	 *
2668
	 * @return ResourceLoaderClientHtml
2669
	 */
2670
	public function getRlClient() {
2671
		if ( !$this->rlClient ) {
2672
			$context = $this->getRlClientContext();
2673
			$rl = $this->getResourceLoader();
2674
			$this->addModules( [
2675
				'user.options',
2676
				'user.tokens',
2677
			] );
2678
			$this->addModuleStyles( [
2679
				'site.styles',
2680
				'noscript',
2681
				'user.styles',
2682
				'user.cssprefs',
2683
			] );
2684
			$this->getSkin()->setupSkinUserCss( $this );
2685
2686
			// Prepare exempt modules for buildExemptModules()
2687
			$exemptGroups = [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ];
2688
			$exemptStates = [];
2689
			$moduleStyles = $this->getModuleStyles( /*filter*/ true );
2690
2691
			// Batch preload getTitleInfo for isKnownEmpty() calls below
2692
			$exemptModules = array_filter( $moduleStyles,
2693
				function ( $name ) use ( $rl, &$exemptGroups ) {
2694
					$module = $rl->getModule( $name );
2695
					return $module && isset( $exemptGroups[ $module->getGroup() ] );
2696
				}
2697
			);
2698
			ResourceLoaderWikiModule::preloadTitleInfo(
2699
				$context, wfGetDB( DB_REPLICA ), $exemptModules );
0 ignored issues
show
Bug introduced by
It seems like wfGetDB(DB_REPLICA) can be null; however, preloadTitleInfo() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2700
2701
			// Filter out modules handled by buildExemptModules()
2702
			$moduleStyles = array_filter( $moduleStyles,
2703
				function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
2704
					$module = $rl->getModule( $name );
2705
					if ( $module ) {
2706
						if ( $name === 'user.styles' && $this->isUserCssPreview() ) {
2707
							$exemptStates[$name] = 'ready';
2708
							// Special case in buildExemptModules()
2709
							return false;
2710
						}
2711
						$group = $module->getGroup();
2712
						if ( isset( $exemptGroups[$group] ) ) {
2713
							$exemptStates[$name] = 'ready';
2714
							if ( !$module->isKnownEmpty( $context ) ) {
2715
								// E.g. Don't output empty <styles>
2716
								$exemptGroups[$group][] = $name;
2717
							}
2718
							return false;
2719
						}
2720
					}
2721
					return true;
2722
				}
2723
			);
2724
			$this->rlExemptStyleModules = $exemptGroups;
2725
2726
			$isUserModuleFiltered = !$this->filterModules( [ 'user' ] );
2727
			// If this page filters out 'user', makeResourceLoaderLink will drop it.
2728
			// Avoid indefinite "loading" state or untrue "ready" state (T145368).
2729
			if ( !$isUserModuleFiltered ) {
2730
				// Manually handled by getBottomScripts()
2731
				$userModule = $rl->getModule( 'user' );
2732
				$userState = $userModule->isKnownEmpty( $context ) && !$this->isUserJsPreview()
2733
					? 'ready'
2734
					: 'loading';
2735
				$this->rlUserModuleState = $exemptStates['user'] = $userState;
2736
			}
2737
2738
			$rlClient = new ResourceLoaderClientHtml( $context, $this->getTarget() );
0 ignored issues
show
Bug introduced by
It seems like $this->getTarget() targeting OutputPage::getTarget() can also be of type string; however, ResourceLoaderClientHtml::__construct() does only seem to accept object<aray>|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2739
			$rlClient->setConfig( $this->getJSVars() );
2740
			$rlClient->setModules( $this->getModules( /*filter*/ true ) );
2741
			$rlClient->setModuleStyles( $moduleStyles );
2742
			$rlClient->setModuleScripts( $this->getModuleScripts( /*filter*/ true ) );
2743
			$rlClient->setExemptStates( $exemptStates );
2744
			$this->rlClient = $rlClient;
2745
		}
2746
		return $this->rlClient;
2747
	}
2748
2749
	/**
2750
	 * @param Skin $sk The given Skin
2751
	 * @param bool $includeStyle Unused
2752
	 * @return string The doctype, opening "<html>", and head element.
2753
	 */
2754
	public function headElement( Skin $sk, $includeStyle = true ) {
2755
		global $wgContLang;
2756
2757
		$userdir = $this->getLanguage()->getDir();
2758
		$sitedir = $wgContLang->getDir();
2759
2760
		$pieces = [];
2761
		$pieces[] = Html::htmlHeader( Sanitizer::mergeAttributes(
2762
			$this->getRlClient()->getDocumentAttributes(),
2763
			$sk->getHtmlElementAttributes()
2764
		) );
2765
		$pieces[] = Html::openElement( 'head' );
2766
2767
		if ( $this->getHTMLTitle() == '' ) {
2768
			$this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() );
2769
		}
2770
2771
		if ( !Html::isXmlMimeType( $this->getConfig()->get( 'MimeType' ) ) ) {
2772
			// Add <meta charset="UTF-8">
2773
			// This should be before <title> since it defines the charset used by
2774
			// text including the text inside <title>.
2775
			// The spec recommends defining XHTML5's charset using the XML declaration
2776
			// instead of meta.
2777
			// Our XML declaration is output by Html::htmlHeader.
2778
			// http://www.whatwg.org/html/semantics.html#attr-meta-http-equiv-content-type
2779
			// http://www.whatwg.org/html/semantics.html#charset
2780
			$pieces[] = Html::element( 'meta', [ 'charset' => 'UTF-8' ] );
2781
		}
2782
2783
		$pieces[] = Html::element( 'title', null, $this->getHTMLTitle() );
2784
		$pieces[] = $this->getRlClient()->getHeadHtml();
2785
		$pieces[] = $this->buildExemptModules();
2786
		$pieces = array_merge( $pieces, array_values( $this->getHeadLinksArray() ) );
2787
		$pieces = array_merge( $pieces, array_values( $this->mHeadItems ) );
2788
		$pieces[] = Html::closeElement( 'head' );
2789
2790
		$bodyClasses = [];
2791
		$bodyClasses[] = 'mediawiki';
2792
2793
		# Classes for LTR/RTL directionality support
2794
		$bodyClasses[] = $userdir;
2795
		$bodyClasses[] = "sitedir-$sitedir";
2796
2797
		if ( $this->getLanguage()->capitalizeAllNouns() ) {
2798
			# A <body> class is probably not the best way to do this . . .
2799
			$bodyClasses[] = 'capitalize-all-nouns';
2800
		}
2801
2802
		// Parser feature migration class
2803
		// The idea is that this will eventually be removed, after the wikitext
2804
		// which requires it is cleaned up.
2805
		$bodyClasses[] = 'mw-hide-empty-elt';
2806
2807
		$bodyClasses[] = $sk->getPageClasses( $this->getTitle() );
0 ignored issues
show
Bug introduced by
It seems like $this->getTitle() can be null; however, getPageClasses() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2808
		$bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
2809
		$bodyClasses[] =
2810
			'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
2811
2812
		$bodyAttrs = [];
2813
		// While the implode() is not strictly needed, it's used for backwards compatibility
2814
		// (this used to be built as a string and hooks likely still expect that).
2815
		$bodyAttrs['class'] = implode( ' ', $bodyClasses );
2816
2817
		// Allow skins and extensions to add body attributes they need
2818
		$sk->addToBodyAttributes( $this, $bodyAttrs );
2819
		Hooks::run( 'OutputPageBodyAttributes', [ $this, $sk, &$bodyAttrs ] );
2820
2821
		$pieces[] = Html::openElement( 'body', $bodyAttrs );
2822
2823
		return self::combineWrappedStrings( $pieces );
2824
	}
2825
2826
	/**
2827
	 * Get a ResourceLoader object associated with this OutputPage
2828
	 *
2829
	 * @return ResourceLoader
2830
	 */
2831
	public function getResourceLoader() {
2832
		if ( is_null( $this->mResourceLoader ) ) {
2833
			$this->mResourceLoader = new ResourceLoader(
2834
				$this->getConfig(),
2835
				LoggerFactory::getInstance( 'resourceloader' )
2836
			);
2837
		}
2838
		return $this->mResourceLoader;
2839
	}
2840
2841
	/**
2842
	 * Explicily load or embed modules on a page.
2843
	 *
2844
	 * @param array|string $modules One or more module names
2845
	 * @param string $only ResourceLoaderModule TYPE_ class constant
2846
	 * @param array $extraQuery [optional] Array with extra query parameters for the request
2847
	 * @return string|WrappedStringList HTML
2848
	 */
2849
	public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
2850
		// Apply 'target' and 'origin' filters
2851
		$modules = $this->filterModules( (array)$modules, null, $only );
2852
2853
		return ResourceLoaderClientHtml::makeLoad(
2854
			$this->getRlClientContext(),
2855
			$modules,
2856
			$only,
2857
			$extraQuery
2858
		);
2859
	}
2860
2861
	/**
2862
	 * Combine WrappedString chunks and filter out empty ones
2863
	 *
2864
	 * @param array $chunks
2865
	 * @return string|WrappedStringList HTML
2866
	 */
2867
	protected static function combineWrappedStrings( array $chunks ) {
2868
		// Filter out empty values
2869
		$chunks = array_filter( $chunks, 'strlen' );
2870
		return WrappedString::join( "\n", $chunks );
2871
	}
2872
2873
	private function isUserJsPreview() {
2874
		return $this->getConfig()->get( 'AllowUserJs' )
2875
			&& $this->getTitle()
2876
			&& $this->getTitle()->isJsSubpage()
2877
			&& $this->userCanPreview();
2878
	}
2879
2880
	private function isUserCssPreview() {
2881
		return $this->getConfig()->get( 'AllowUserCss' )
2882
			&& $this->getTitle()
2883
			&& $this->getTitle()->isCssSubpage()
2884
			&& $this->userCanPreview();
2885
	}
2886
2887
	/**
2888
	 * JS stuff to put at the bottom of the `<body>`. These are modules with position 'bottom',
2889
	 * legacy scripts ($this->mScripts), and user JS.
2890
	 *
2891
	 * @return string|WrappedStringList HTML
2892
	 */
2893
	public function getBottomScripts() {
2894
		$chunks = [];
2895
		$chunks[] = $this->getRlClient()->getBodyHtml();
2896
2897
		// Legacy non-ResourceLoader scripts
2898
		$chunks[] = $this->mScripts;
2899
2900
		// Exempt 'user' module
2901
		// - May need excludepages for live preview. (T28283)
2902
		// - Must use TYPE_COMBINED so its response is handled by mw.loader.implement() which
2903
		//   ensures execution is scheduled after the "site" module.
2904
		// - Don't load if module state is already resolved as "ready".
2905
		if ( $this->rlUserModuleState === 'loading' ) {
2906
			if ( $this->isUserJsPreview() ) {
2907
				$chunks[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED,
2908
					[ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
2909
				);
2910
				$chunks[] = ResourceLoader::makeInlineScript(
2911
					Xml::encodeJsCall( 'mw.loader.using', [
0 ignored issues
show
Security Bug introduced by
It seems like \Xml::encodeJsCall('mw.l...wpTextbox1'))) . '}'))) targeting Xml::encodeJsCall() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
2912
						[ 'user', 'site' ],
2913
						new XmlJsCode(
2914
							'function () {'
2915
								. Xml::encodeJsCall( '$.globalEval', [
2916
									$this->getRequest()->getText( 'wpTextbox1' )
2917
								] )
2918
								. '}'
2919
						)
2920
					] )
2921
				);
2922
				// FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded
2923
				// asynchronously and may arrive *after* the inline script here. So the previewed code
2924
				// may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js.
2925
				// Similarly, when previewing ./common.js and the user module does arrive first,
2926
				// it will arrive without common.js and the inline script runs after.
2927
				// Thus running common after the excluded subpage.
2928
			} else {
2929
				// Load normally
2930
				$chunks[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED );
2931
			}
2932
		}
2933
2934
		if ( $this->limitReportData ) {
2935
			$chunks[] = ResourceLoader::makeInlineScript(
2936
				ResourceLoader::makeConfigSetScript(
0 ignored issues
show
Security Bug introduced by
It seems like \ResourceLoader::makeCon...limitReportData), true) targeting ResourceLoader::makeConfigSetScript() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
2937
					[ 'wgPageParseReport' => $this->limitReportData ],
2938
					true
2939
				)
2940
			);
2941
		}
2942
2943
		return self::combineWrappedStrings( $chunks );
2944
	}
2945
2946
	/**
2947
	 * Get the javascript config vars to include on this page
2948
	 *
2949
	 * @return array Array of javascript config vars
2950
	 * @since 1.23
2951
	 */
2952
	public function getJsConfigVars() {
2953
		return $this->mJsConfigVars;
2954
	}
2955
2956
	/**
2957
	 * Add one or more variables to be set in mw.config in JavaScript
2958
	 *
2959
	 * @param string|array $keys Key or array of key/value pairs
2960
	 * @param mixed $value [optional] Value of the configuration variable
2961
	 */
2962 View Code Duplication
	public function addJsConfigVars( $keys, $value = null ) {
2963
		if ( is_array( $keys ) ) {
2964
			foreach ( $keys as $key => $value ) {
2965
				$this->mJsConfigVars[$key] = $value;
2966
			}
2967
			return;
2968
		}
2969
2970
		$this->mJsConfigVars[$keys] = $value;
2971
	}
2972
2973
	/**
2974
	 * Get an array containing the variables to be set in mw.config in JavaScript.
2975
	 *
2976
	 * Do not add things here which can be evaluated in ResourceLoaderStartUpModule
2977
	 * - in other words, page-independent/site-wide variables (without state).
2978
	 * You will only be adding bloat to the html page and causing page caches to
2979
	 * have to be purged on configuration changes.
2980
	 * @return array
2981
	 */
2982
	public function getJSVars() {
2983
		global $wgContLang;
2984
2985
		$curRevisionId = 0;
2986
		$articleId = 0;
2987
		$canonicalSpecialPageName = false; # bug 21115
2988
2989
		$title = $this->getTitle();
2990
		$ns = $title->getNamespace();
2991
		$canonicalNamespace = MWNamespace::exists( $ns )
2992
			? MWNamespace::getCanonicalName( $ns )
2993
			: $title->getNsText();
2994
2995
		$sk = $this->getSkin();
2996
		// Get the relevant title so that AJAX features can use the correct page name
2997
		// when making API requests from certain special pages (bug 34972).
2998
		$relevantTitle = $sk->getRelevantTitle();
2999
		$relevantUser = $sk->getRelevantUser();
3000
3001
		if ( $ns == NS_SPECIAL ) {
3002
			list( $canonicalSpecialPageName, /*...*/ ) =
3003
				SpecialPageFactory::resolveAlias( $title->getDBkey() );
3004
		} elseif ( $this->canUseWikiPage() ) {
3005
			$wikiPage = $this->getWikiPage();
3006
			$curRevisionId = $wikiPage->getLatest();
3007
			$articleId = $wikiPage->getId();
3008
		}
3009
3010
		$lang = $title->getPageViewLanguage();
3011
3012
		// Pre-process information
3013
		$separatorTransTable = $lang->separatorTransformTable();
3014
		$separatorTransTable = $separatorTransTable ? $separatorTransTable : [];
3015
		$compactSeparatorTransTable = [
3016
			implode( "\t", array_keys( $separatorTransTable ) ),
3017
			implode( "\t", $separatorTransTable ),
3018
		];
3019
		$digitTransTable = $lang->digitTransformTable();
3020
		$digitTransTable = $digitTransTable ? $digitTransTable : [];
3021
		$compactDigitTransTable = [
3022
			implode( "\t", array_keys( $digitTransTable ) ),
3023
			implode( "\t", $digitTransTable ),
3024
		];
3025
3026
		$user = $this->getUser();
3027
3028
		$vars = [
3029
			'wgCanonicalNamespace' => $canonicalNamespace,
3030
			'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
3031
			'wgNamespaceNumber' => $title->getNamespace(),
3032
			'wgPageName' => $title->getPrefixedDBkey(),
3033
			'wgTitle' => $title->getText(),
3034
			'wgCurRevisionId' => $curRevisionId,
3035
			'wgRevisionId' => (int)$this->getRevisionId(),
3036
			'wgArticleId' => $articleId,
3037
			'wgIsArticle' => $this->isArticle(),
3038
			'wgIsRedirect' => $title->isRedirect(),
3039
			'wgAction' => Action::getActionName( $this->getContext() ),
3040
			'wgUserName' => $user->isAnon() ? null : $user->getName(),
3041
			'wgUserGroups' => $user->getEffectiveGroups(),
3042
			'wgCategories' => $this->getCategories(),
3043
			'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
3044
			'wgPageContentLanguage' => $lang->getCode(),
3045
			'wgPageContentModel' => $title->getContentModel(),
3046
			'wgSeparatorTransformTable' => $compactSeparatorTransTable,
3047
			'wgDigitTransformTable' => $compactDigitTransTable,
3048
			'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
3049
			'wgMonthNames' => $lang->getMonthNamesArray(),
3050
			'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(),
3051
			'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
3052
			'wgRelevantArticleId' => $relevantTitle->getArticleID(),
3053
			'wgRequestId' => WebRequest::getRequestId(),
3054
		];
3055
3056
		if ( $user->isLoggedIn() ) {
3057
			$vars['wgUserId'] = $user->getId();
3058
			$vars['wgUserEditCount'] = $user->getEditCount();
3059
			$userReg = $user->getRegistration();
3060
			$vars['wgUserRegistration'] = $userReg ? wfTimestamp( TS_UNIX, $userReg ) * 1000 : null;
3061
			// Get the revision ID of the oldest new message on the user's talk
3062
			// page. This can be used for constructing new message alerts on
3063
			// the client side.
3064
			$vars['wgUserNewMsgRevisionId'] = $user->getNewMessageRevisionId();
3065
		}
3066
3067
		if ( $wgContLang->hasVariants() ) {
3068
			$vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
3069
		}
3070
		// Same test as SkinTemplate
3071
		$vars['wgIsProbablyEditable'] = $title->quickUserCan( 'edit', $user )
3072
			&& ( $title->exists() || $title->quickUserCan( 'create', $user ) );
3073
3074
		foreach ( $title->getRestrictionTypes() as $type ) {
3075
			$vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
3076
		}
3077
3078
		if ( $title->isMainPage() ) {
3079
			$vars['wgIsMainPage'] = true;
3080
		}
3081
3082
		if ( $this->mRedirectedFrom ) {
3083
			$vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
3084
		}
3085
3086
		if ( $relevantUser ) {
3087
			$vars['wgRelevantUserName'] = $relevantUser->getName();
3088
		}
3089
3090
		// Allow extensions to add their custom variables to the mw.config map.
3091
		// Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
3092
		// page-dependant but site-wide (without state).
3093
		// Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
3094
		Hooks::run( 'MakeGlobalVariablesScript', [ &$vars, $this ] );
3095
3096
		// Merge in variables from addJsConfigVars last
3097
		return array_merge( $vars, $this->getJsConfigVars() );
3098
	}
3099
3100
	/**
3101
	 * To make it harder for someone to slip a user a fake
3102
	 * user-JavaScript or user-CSS preview, a random token
3103
	 * is associated with the login session. If it's not
3104
	 * passed back with the preview request, we won't render
3105
	 * the code.
3106
	 *
3107
	 * @return bool
3108
	 */
3109
	public function userCanPreview() {
3110
		$request = $this->getRequest();
3111
		if (
3112
			$request->getVal( 'action' ) !== 'submit' ||
3113
			!$request->getCheck( 'wpPreview' ) ||
3114
			!$request->wasPosted()
3115
		) {
3116
			return false;
3117
		}
3118
3119
		$user = $this->getUser();
3120
3121
		if ( !$user->isLoggedIn() ) {
3122
			// Anons have predictable edit tokens
3123
			return false;
3124
		}
3125
		if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
3126
			return false;
3127
		}
3128
3129
		$title = $this->getTitle();
3130
		if ( !$title->isJsSubpage() && !$title->isCssSubpage() ) {
3131
			return false;
3132
		}
3133
		if ( !$title->isSubpageOf( $user->getUserPage() ) ) {
3134
			// Don't execute another user's CSS or JS on preview (T85855)
3135
			return false;
3136
		}
3137
3138
		$errors = $title->getUserPermissionsErrors( 'edit', $user );
3139
		if ( count( $errors ) !== 0 ) {
3140
			return false;
3141
		}
3142
3143
		return true;
3144
	}
3145
3146
	/**
3147
	 * @return array Array in format "link name or number => 'link html'".
3148
	 */
3149
	public function getHeadLinksArray() {
3150
		global $wgVersion;
3151
3152
		$tags = [];
3153
		$config = $this->getConfig();
3154
3155
		$canonicalUrl = $this->mCanonicalUrl;
3156
3157
		$tags['meta-generator'] = Html::element( 'meta', [
3158
			'name' => 'generator',
3159
			'content' => "MediaWiki $wgVersion",
3160
		] );
3161
3162
		if ( $config->get( 'ReferrerPolicy' ) !== false ) {
3163
			$tags['meta-referrer'] = Html::element( 'meta', [
3164
				'name' => 'referrer',
3165
				'content' => $config->get( 'ReferrerPolicy' )
3166
			] );
3167
		}
3168
3169
		$p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
3170
		if ( $p !== 'index,follow' ) {
3171
			// http://www.robotstxt.org/wc/meta-user.html
3172
			// Only show if it's different from the default robots policy
3173
			$tags['meta-robots'] = Html::element( 'meta', [
3174
				'name' => 'robots',
3175
				'content' => $p,
3176
			] );
3177
		}
3178
3179
		foreach ( $this->mMetatags as $tag ) {
3180
			if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) {
3181
				$a = 'http-equiv';
3182
				$tag[0] = substr( $tag[0], 5 );
3183
			} else {
3184
				$a = 'name';
3185
			}
3186
			$tagName = "meta-{$tag[0]}";
3187
			if ( isset( $tags[$tagName] ) ) {
3188
				$tagName .= $tag[1];
3189
			}
3190
			$tags[$tagName] = Html::element( 'meta',
3191
				[
3192
					$a => $tag[0],
3193
					'content' => $tag[1]
3194
				]
3195
			);
3196
		}
3197
3198
		foreach ( $this->mLinktags as $tag ) {
3199
			$tags[] = Html::element( 'link', $tag );
3200
		}
3201
3202
		# Universal edit button
3203
		if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
3204
			$user = $this->getUser();
3205
			if ( $this->getTitle()->quickUserCan( 'edit', $user )
3206
				&& ( $this->getTitle()->exists() ||
3207
					$this->getTitle()->quickUserCan( 'create', $user ) )
3208
			) {
3209
				// Original UniversalEditButton
3210
				$msg = $this->msg( 'edit' )->text();
3211
				$tags['universal-edit-button'] = Html::element( 'link', [
3212
					'rel' => 'alternate',
3213
					'type' => 'application/x-wiki',
3214
					'title' => $msg,
3215
					'href' => $this->getTitle()->getEditURL(),
3216
				] );
3217
				// Alternate edit link
3218
				$tags['alternative-edit'] = Html::element( 'link', [
3219
					'rel' => 'edit',
3220
					'title' => $msg,
3221
					'href' => $this->getTitle()->getEditURL(),
3222
				] );
3223
			}
3224
		}
3225
3226
		# Generally the order of the favicon and apple-touch-icon links
3227
		# should not matter, but Konqueror (3.5.9 at least) incorrectly
3228
		# uses whichever one appears later in the HTML source. Make sure
3229
		# apple-touch-icon is specified first to avoid this.
3230
		if ( $config->get( 'AppleTouchIcon' ) !== false ) {
3231
			$tags['apple-touch-icon'] = Html::element( 'link', [
3232
				'rel' => 'apple-touch-icon',
3233
				'href' => $config->get( 'AppleTouchIcon' )
3234
			] );
3235
		}
3236
3237
		if ( $config->get( 'Favicon' ) !== false ) {
3238
			$tags['favicon'] = Html::element( 'link', [
3239
				'rel' => 'shortcut icon',
3240
				'href' => $config->get( 'Favicon' )
3241
			] );
3242
		}
3243
3244
		# OpenSearch description link
3245
		$tags['opensearch'] = Html::element( 'link', [
3246
			'rel' => 'search',
3247
			'type' => 'application/opensearchdescription+xml',
3248
			'href' => wfScript( 'opensearch_desc' ),
3249
			'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
3250
		] );
3251
3252
		if ( $config->get( 'EnableAPI' ) ) {
3253
			# Real Simple Discovery link, provides auto-discovery information
3254
			# for the MediaWiki API (and potentially additional custom API
3255
			# support such as WordPress or Twitter-compatible APIs for a
3256
			# blogging extension, etc)
3257
			$tags['rsd'] = Html::element( 'link', [
3258
				'rel' => 'EditURI',
3259
				'type' => 'application/rsd+xml',
3260
				// Output a protocol-relative URL here if $wgServer is protocol-relative.
3261
				// Whether RSD accepts relative or protocol-relative URLs is completely
3262
				// undocumented, though.
3263
				'href' => wfExpandUrl( wfAppendQuery(
3264
					wfScript( 'api' ),
3265
					[ 'action' => 'rsd' ] ),
3266
					PROTO_RELATIVE
3267
				),
3268
			] );
3269
		}
3270
3271
		# Language variants
3272
		if ( !$config->get( 'DisableLangConversion' ) ) {
3273
			$lang = $this->getTitle()->getPageLanguage();
3274
			if ( $lang->hasVariants() ) {
3275
				$variants = $lang->getVariants();
3276
				foreach ( $variants as $variant ) {
3277
					$tags["variant-$variant"] = Html::element( 'link', [
3278
						'rel' => 'alternate',
3279
						'hreflang' => wfBCP47( $variant ),
3280
						'href' => $this->getTitle()->getLocalURL(
3281
							[ 'variant' => $variant ] )
3282
						]
3283
					);
3284
				}
3285
				# x-default link per https://support.google.com/webmasters/answer/189077?hl=en
3286
				$tags["variant-x-default"] = Html::element( 'link', [
3287
					'rel' => 'alternate',
3288
					'hreflang' => 'x-default',
3289
					'href' => $this->getTitle()->getLocalURL() ] );
3290
			}
3291
		}
3292
3293
		# Copyright
3294
		if ( $this->copyrightUrl !== null ) {
3295
			$copyright = $this->copyrightUrl;
3296
		} else {
3297
			$copyright = '';
3298
			if ( $config->get( 'RightsPage' ) ) {
3299
				$copy = Title::newFromText( $config->get( 'RightsPage' ) );
3300
3301
				if ( $copy ) {
3302
					$copyright = $copy->getLocalURL();
3303
				}
3304
			}
3305
3306
			if ( !$copyright && $config->get( 'RightsUrl' ) ) {
3307
				$copyright = $config->get( 'RightsUrl' );
3308
			}
3309
		}
3310
3311
		if ( $copyright ) {
3312
			$tags['copyright'] = Html::element( 'link', [
3313
				'rel' => 'copyright',
3314
				'href' => $copyright ]
3315
			);
3316
		}
3317
3318
		# Feeds
3319
		if ( $config->get( 'Feed' ) ) {
3320
			$feedLinks = [];
3321
3322
			foreach ( $this->getSyndicationLinks() as $format => $link ) {
3323
				# Use the page name for the title.  In principle, this could
3324
				# lead to issues with having the same name for different feeds
3325
				# corresponding to the same page, but we can't avoid that at
3326
				# this low a level.
3327
3328
				$feedLinks[] = $this->feedLink(
3329
					$format,
3330
					$link,
3331
					# Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
3332
					$this->msg(
3333
						"page-{$format}-feed", $this->getTitle()->getPrefixedText()
3334
					)->text()
3335
				);
3336
			}
3337
3338
			# Recent changes feed should appear on every page (except recentchanges,
3339
			# that would be redundant). Put it after the per-page feed to avoid
3340
			# changing existing behavior. It's still available, probably via a
3341
			# menu in your browser. Some sites might have a different feed they'd
3342
			# like to promote instead of the RC feed (maybe like a "Recent New Articles"
3343
			# or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
3344
			# If so, use it instead.
3345
			$sitename = $config->get( 'Sitename' );
3346
			if ( $config->get( 'OverrideSiteFeed' ) ) {
3347
				foreach ( $config->get( 'OverrideSiteFeed' ) as $type => $feedUrl ) {
3348
					// Note, this->feedLink escapes the url.
3349
					$feedLinks[] = $this->feedLink(
3350
						$type,
3351
						$feedUrl,
3352
						$this->msg( "site-{$type}-feed", $sitename )->text()
3353
					);
3354
				}
3355
			} elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
3356
				$rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
3357
				foreach ( $config->get( 'AdvertisedFeedTypes' ) as $format ) {
3358
					$feedLinks[] = $this->feedLink(
3359
						$format,
3360
						$rctitle->getLocalURL( [ 'feed' => $format ] ),
3361
						# For grep: 'site-rss-feed', 'site-atom-feed'
3362
						$this->msg( "site-{$format}-feed", $sitename )->text()
3363
					);
3364
				}
3365
			}
3366
3367
			# Allow extensions to change the list pf feeds. This hook is primarily for changing,
3368
			# manipulating or removing existing feed tags. If you want to add new feeds, you should
3369
			# use OutputPage::addFeedLink() instead.
3370
			Hooks::run( 'AfterBuildFeedLinks', [ &$feedLinks ] );
3371
3372
			$tags += $feedLinks;
3373
		}
3374
3375
		# Canonical URL
3376
		if ( $config->get( 'EnableCanonicalServerLink' ) ) {
3377
			if ( $canonicalUrl !== false ) {
3378
				$canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL );
3379
			} else {
3380
				if ( $this->isArticleRelated() ) {
3381
					// This affects all requests where "setArticleRelated" is true. This is
3382
					// typically all requests that show content (query title, curid, oldid, diff),
3383
					// and all wikipage actions (edit, delete, purge, info, history etc.).
3384
					// It does not apply to File pages and Special pages.
3385
					// 'history' and 'info' actions address page metadata rather than the page
3386
					// content itself, so they may not be canonicalized to the view page url.
3387
					// TODO: this ought to be better encapsulated in the Action class.
3388
					$action = Action::getActionName( $this->getContext() );
3389
					if ( in_array( $action, [ 'history', 'info' ] ) ) {
3390
						$query = "action={$action}";
3391
					} else {
3392
						$query = '';
3393
					}
3394
					$canonicalUrl = $this->getTitle()->getCanonicalURL( $query );
3395
				} else {
3396
					$reqUrl = $this->getRequest()->getRequestURL();
3397
					$canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL );
3398
				}
3399
			}
3400
		}
3401
		if ( $canonicalUrl !== false ) {
3402
			$tags[] = Html::element( 'link', [
3403
				'rel' => 'canonical',
3404
				'href' => $canonicalUrl
3405
			] );
3406
		}
3407
3408
		return $tags;
3409
	}
3410
3411
	/**
3412
	 * @return string HTML tag links to be put in the header.
3413
	 * @deprecated since 1.24 Use OutputPage::headElement or if you have to,
3414
	 *   OutputPage::getHeadLinksArray directly.
3415
	 */
3416
	public function getHeadLinks() {
3417
		wfDeprecated( __METHOD__, '1.24' );
3418
		return implode( "\n", $this->getHeadLinksArray() );
3419
	}
3420
3421
	/**
3422
	 * Generate a "<link rel/>" for a feed.
3423
	 *
3424
	 * @param string $type Feed type
3425
	 * @param string $url URL to the feed
3426
	 * @param string $text Value of the "title" attribute
3427
	 * @return string HTML fragment
3428
	 */
3429
	private function feedLink( $type, $url, $text ) {
3430
		return Html::element( 'link', [
3431
			'rel' => 'alternate',
3432
			'type' => "application/$type+xml",
3433
			'title' => $text,
3434
			'href' => $url ]
3435
		);
3436
	}
3437
3438
	/**
3439
	 * Add a local or specified stylesheet, with the given media options.
3440
	 * Internal use only. Use OutputPage::addModuleStyles() if possible.
3441
	 *
3442
	 * @param string $style URL to the file
3443
	 * @param string $media To specify a media type, 'screen', 'printable', 'handheld' or any.
3444
	 * @param string $condition For IE conditional comments, specifying an IE version
3445
	 * @param string $dir Set to 'rtl' or 'ltr' for direction-specific sheets
3446
	 */
3447
	public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
3448
		$options = [];
3449
		if ( $media ) {
3450
			$options['media'] = $media;
3451
		}
3452
		if ( $condition ) {
3453
			$options['condition'] = $condition;
3454
		}
3455
		if ( $dir ) {
3456
			$options['dir'] = $dir;
3457
		}
3458
		$this->styles[$style] = $options;
3459
	}
3460
3461
	/**
3462
	 * Adds inline CSS styles
3463
	 * Internal use only. Use OutputPage::addModuleStyles() if possible.
3464
	 *
3465
	 * @param mixed $style_css Inline CSS
3466
	 * @param string $flip Set to 'flip' to flip the CSS if needed
3467
	 */
3468
	public function addInlineStyle( $style_css, $flip = 'noflip' ) {
3469
		if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
3470
			# If wanted, and the interface is right-to-left, flip the CSS
3471
			$style_css = CSSJanus::transform( $style_css, true, false );
3472
		}
3473
		$this->mInlineStyles .= Html::inlineStyle( $style_css );
3474
	}
3475
3476
	/**
3477
	 * Build exempt modules and legacy non-ResourceLoader styles.
3478
	 *
3479
	 * @return string|WrappedStringList HTML
3480
	 */
3481
	protected function buildExemptModules() {
3482
		global $wgContLang;
3483
3484
		$resourceLoader = $this->getResourceLoader();
0 ignored issues
show
Unused Code introduced by
$resourceLoader is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
3485
		$chunks = [];
3486
		// Things that go after the ResourceLoaderDynamicStyles marker
3487
		$append = [];
3488
3489
		// Exempt 'user' styles module (may need 'excludepages' for live preview)
3490
		if ( $this->isUserCssPreview() ) {
3491
			$append[] = $this->makeResourceLoaderLink(
3492
				'user.styles',
3493
				ResourceLoaderModule::TYPE_STYLES,
3494
				[ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
3495
			);
3496
3497
			// Load the previewed CSS. Janus it if needed.
3498
			// User-supplied CSS is assumed to in the wiki's content language.
3499
			$previewedCSS = $this->getRequest()->getText( 'wpTextbox1' );
3500
			if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) {
3501
				$previewedCSS = CSSJanus::transform( $previewedCSS, true, false );
3502
			}
3503
			$append[] = Html::inlineStyle( $previewedCSS );
3504
		}
3505
3506
		// We want site, private and user styles to override dynamically added styles from
3507
		// general modules, but we want dynamically added styles to override statically added
3508
		// style modules. So the order has to be:
3509
		// - page style modules (formatted by ResourceLoaderClientHtml::getHeadHtml())
3510
		// - dynamically loaded styles (added by mw.loader before ResourceLoaderDynamicStyles)
3511
		// - ResourceLoaderDynamicStyles marker
3512
		// - site/private/user styles
3513
3514
		// Add legacy styles added through addStyle()/addInlineStyle() here
3515
		$chunks[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3516
3517
		$chunks[] = Html::element(
3518
			'meta',
3519
			[ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
3520
		);
3521
3522
		foreach ( $this->rlExemptStyleModules as $group => $moduleNames ) {
3523
			$chunks[] = $this->makeResourceLoaderLink( $moduleNames,
3524
				ResourceLoaderModule::TYPE_STYLES
3525
			);
3526
		}
3527
3528
		return self::combineWrappedStrings( array_merge( $chunks, $append ) );
3529
	}
3530
3531
	/**
3532
	 * @return array
3533
	 */
3534
	public function buildCssLinksArray() {
3535
		$links = [];
3536
3537
		// Add any extension CSS
3538
		foreach ( $this->mExtStyles as $url ) {
3539
			$this->addStyle( $url );
3540
		}
3541
		$this->mExtStyles = [];
3542
3543
		foreach ( $this->styles as $file => $options ) {
3544
			$link = $this->styleLink( $file, $options );
3545
			if ( $link ) {
3546
				$links[$file] = $link;
3547
			}
3548
		}
3549
		return $links;
3550
	}
3551
3552
	/**
3553
	 * Generate \<link\> tags for stylesheets
3554
	 *
3555
	 * @param string $style URL to the file
3556
	 * @param array $options Option, can contain 'condition', 'dir', 'media' keys
3557
	 * @return string HTML fragment
3558
	 */
3559
	protected function styleLink( $style, array $options ) {
3560
		if ( isset( $options['dir'] ) ) {
3561
			if ( $this->getLanguage()->getDir() != $options['dir'] ) {
3562
				return '';
3563
			}
3564
		}
3565
3566
		if ( isset( $options['media'] ) ) {
3567
			$media = self::transformCssMedia( $options['media'] );
3568
			if ( is_null( $media ) ) {
3569
				return '';
3570
			}
3571
		} else {
3572
			$media = 'all';
3573
		}
3574
3575
		if ( substr( $style, 0, 1 ) == '/' ||
3576
			substr( $style, 0, 5 ) == 'http:' ||
3577
			substr( $style, 0, 6 ) == 'https:' ) {
3578
			$url = $style;
3579
		} else {
3580
			$config = $this->getConfig();
3581
			$url = $config->get( 'StylePath' ) . '/' . $style . '?' .
3582
				$config->get( 'StyleVersion' );
3583
		}
3584
3585
		$link = Html::linkedStyle( $url, $media );
3586
3587
		if ( isset( $options['condition'] ) ) {
3588
			$condition = htmlspecialchars( $options['condition'] );
3589
			$link = "<!--[if $condition]>$link<![endif]-->";
3590
		}
3591
		return $link;
3592
	}
3593
3594
	/**
3595
	 * Transform path to web-accessible static resource.
3596
	 *
3597
	 * This is used to add a validation hash as query string.
3598
	 * This aids various behaviors:
3599
	 *
3600
	 * - Put long Cache-Control max-age headers on responses for improved
3601
	 *   cache performance.
3602
	 * - Get the correct version of a file as expected by the current page.
3603
	 * - Instantly get the updated version of a file after deployment.
3604
	 *
3605
	 * Avoid using this for urls included in HTML as otherwise clients may get different
3606
	 * versions of a resource when navigating the site depending on when the page was cached.
3607
	 * If changes to the url propagate, this is not a problem (e.g. if the url is in
3608
	 * an external stylesheet).
3609
	 *
3610
	 * @since 1.27
3611
	 * @param Config $config
3612
	 * @param string $path Path-absolute URL to file (from document root, must start with "/")
3613
	 * @return string URL
3614
	 */
3615
	public static function transformResourcePath( Config $config, $path ) {
3616
		global $IP;
3617
		$remotePathPrefix = $config->get( 'ResourceBasePath' );
3618
		if ( $remotePathPrefix === '' ) {
3619
			// The configured base path is required to be empty string for
3620
			// wikis in the domain root
3621
			$remotePath = '/';
3622
		} else {
3623
			$remotePath = $remotePathPrefix;
3624
		}
3625
		if ( strpos( $path, $remotePath ) !== 0 ) {
3626
			// Path is outside wgResourceBasePath, ignore.
3627
			return $path;
3628
		}
3629
		$path = RelPath\getRelativePath( $path, $remotePath );
3630
		return self::transformFilePath( $remotePathPrefix, $IP, $path );
0 ignored issues
show
Security Bug introduced by
It seems like $path defined by \RelPath\getRelativePath($path, $remotePath) on line 3629 can also be of type false; however, OutputPage::transformFilePath() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
3631
	}
3632
3633
	/**
3634
	 * Utility method for transformResourceFilePath().
3635
	 *
3636
	 * Caller is responsible for ensuring the file exists. Emits a PHP warning otherwise.
3637
	 *
3638
	 * @since 1.27
3639
	 * @param string $remotePath URL path prefix that points to $localPath
0 ignored issues
show
Documentation introduced by
There is no parameter named $remotePath. Did you maybe mean $remotePathPrefix?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
3640
	 * @param string $localPath File directory exposed at $remotePath
3641
	 * @param string $file Path to target file relative to $localPath
3642
	 * @return string URL
3643
	 */
3644
	public static function transformFilePath( $remotePathPrefix, $localPath, $file ) {
3645
		$hash = md5_file( "$localPath/$file" );
3646
		if ( $hash === false ) {
3647
			wfLogWarning( __METHOD__ . ": Failed to hash $localPath/$file" );
3648
			$hash = '';
3649
		}
3650
		return "$remotePathPrefix/$file?" . substr( $hash, 0, 5 );
3651
	}
3652
3653
	/**
3654
	 * Transform "media" attribute based on request parameters
3655
	 *
3656
	 * @param string $media Current value of the "media" attribute
3657
	 * @return string Modified value of the "media" attribute, or null to skip
3658
	 * this stylesheet
3659
	 */
3660
	public static function transformCssMedia( $media ) {
3661
		global $wgRequest;
3662
3663
		// http://www.w3.org/TR/css3-mediaqueries/#syntax
3664
		$screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
3665
3666
		// Switch in on-screen display for media testing
3667
		$switches = [
3668
			'printable' => 'print',
3669
			'handheld' => 'handheld',
3670
		];
3671
		foreach ( $switches as $switch => $targetMedia ) {
3672
			if ( $wgRequest->getBool( $switch ) ) {
3673
				if ( $media == $targetMedia ) {
3674
					$media = '';
3675
				} elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
3676
					/* This regex will not attempt to understand a comma-separated media_query_list
3677
					 *
3678
					 * Example supported values for $media:
3679
					 * 'screen', 'only screen', 'screen and (min-width: 982px)' ),
3680
					 * Example NOT supported value for $media:
3681
					 * '3d-glasses, screen, print and resolution > 90dpi'
3682
					 *
3683
					 * If it's a print request, we never want any kind of screen stylesheets
3684
					 * If it's a handheld request (currently the only other choice with a switch),
3685
					 * we don't want simple 'screen' but we might want screen queries that
3686
					 * have a max-width or something, so we'll pass all others on and let the
3687
					 * client do the query.
3688
					 */
3689
					if ( $targetMedia == 'print' || $media == 'screen' ) {
3690
						return null;
3691
					}
3692
				}
3693
			}
3694
		}
3695
3696
		return $media;
3697
	}
3698
3699
	/**
3700
	 * Add a wikitext-formatted message to the output.
3701
	 * This is equivalent to:
3702
	 *
3703
	 *    $wgOut->addWikiText( wfMessage( ... )->plain() )
3704
	 */
3705
	public function addWikiMsg( /*...*/ ) {
3706
		$args = func_get_args();
3707
		$name = array_shift( $args );
3708
		$this->addWikiMsgArray( $name, $args );
3709
	}
3710
3711
	/**
3712
	 * Add a wikitext-formatted message to the output.
3713
	 * Like addWikiMsg() except the parameters are taken as an array
3714
	 * instead of a variable argument list.
3715
	 *
3716
	 * @param string $name
3717
	 * @param array $args
3718
	 */
3719
	public function addWikiMsgArray( $name, $args ) {
3720
		$this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
3721
	}
3722
3723
	/**
3724
	 * This function takes a number of message/argument specifications, wraps them in
3725
	 * some overall structure, and then parses the result and adds it to the output.
3726
	 *
3727
	 * In the $wrap, $1 is replaced with the first message, $2 with the second,
3728
	 * and so on. The subsequent arguments may be either
3729
	 * 1) strings, in which case they are message names, or
3730
	 * 2) arrays, in which case, within each array, the first element is the message
3731
	 *    name, and subsequent elements are the parameters to that message.
3732
	 *
3733
	 * Don't use this for messages that are not in the user's interface language.
3734
	 *
3735
	 * For example:
3736
	 *
3737
	 *    $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>", 'some-error' );
3738
	 *
3739
	 * Is equivalent to:
3740
	 *
3741
	 *    $wgOut->addWikiText( "<div class='error'>\n"
3742
	 *        . wfMessage( 'some-error' )->plain() . "\n</div>" );
3743
	 *
3744
	 * The newline after the opening div is needed in some wikitext. See bug 19226.
3745
	 *
3746
	 * @param string $wrap
3747
	 */
3748
	public function wrapWikiMsg( $wrap /*, ...*/ ) {
3749
		$msgSpecs = func_get_args();
3750
		array_shift( $msgSpecs );
3751
		$msgSpecs = array_values( $msgSpecs );
3752
		$s = $wrap;
3753
		foreach ( $msgSpecs as $n => $spec ) {
3754
			if ( is_array( $spec ) ) {
3755
				$args = $spec;
3756
				$name = array_shift( $args );
3757
				if ( isset( $args['options'] ) ) {
3758
					unset( $args['options'] );
3759
					wfDeprecated(
3760
						'Adding "options" to ' . __METHOD__ . ' is no longer supported',
3761
						'1.20'
3762
					);
3763
				}
3764
			} else {
3765
				$args = [];
3766
				$name = $spec;
3767
			}
3768
			$s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
3769
		}
3770
		$this->addWikiText( $s );
3771
	}
3772
3773
	/**
3774
	 * Enables/disables TOC, doesn't override __NOTOC__
3775
	 * @param bool $flag
3776
	 * @since 1.22
3777
	 */
3778
	public function enableTOC( $flag = true ) {
3779
		$this->mEnableTOC = $flag;
3780
	}
3781
3782
	/**
3783
	 * @return bool
3784
	 * @since 1.22
3785
	 */
3786
	public function isTOCEnabled() {
3787
		return $this->mEnableTOC;
3788
	}
3789
3790
	/**
3791
	 * Enables/disables section edit links, doesn't override __NOEDITSECTION__
3792
	 * @param bool $flag
3793
	 * @since 1.23
3794
	 */
3795
	public function enableSectionEditLinks( $flag = true ) {
3796
		$this->mEnableSectionEditLinks = $flag;
3797
	}
3798
3799
	/**
3800
	 * @return bool
3801
	 * @since 1.23
3802
	 */
3803
	public function sectionEditLinksEnabled() {
3804
		return $this->mEnableSectionEditLinks;
3805
	}
3806
3807
	/**
3808
	 * Helper function to setup the PHP implementation of OOUI to use in this request.
3809
	 *
3810
	 * @since 1.26
3811
	 * @param String $skinName The Skin name to determine the correct OOUI theme
3812
	 * @param String $dir Language direction
3813
	 */
3814
	public static function setupOOUI( $skinName = '', $dir = 'ltr' ) {
3815
		$themes = ExtensionRegistry::getInstance()->getAttribute( 'SkinOOUIThemes' );
3816
		// Make keys (skin names) lowercase for case-insensitive matching.
3817
		$themes = array_change_key_case( $themes, CASE_LOWER );
3818
		$theme = isset( $themes[$skinName] ) ? $themes[$skinName] : 'MediaWiki';
3819
		// For example, 'OOUI\MediaWikiTheme'.
3820
		$themeClass = "OOUI\\{$theme}Theme";
3821
		OOUI\Theme::setSingleton( new $themeClass() );
3822
		OOUI\Element::setDefaultDir( $dir );
3823
	}
3824
3825
	/**
3826
	 * Add ResourceLoader module styles for OOUI and set up the PHP implementation of it for use with
3827
	 * MediaWiki and this OutputPage instance.
3828
	 *
3829
	 * @since 1.25
3830
	 */
3831
	public function enableOOUI() {
3832
		self::setupOOUI(
3833
			strtolower( $this->getSkin()->getSkinName() ),
3834
			$this->getLanguage()->getDir()
3835
		);
3836
		$this->addModuleStyles( [
3837
			'oojs-ui-core.styles',
3838
			'oojs-ui.styles.icons',
3839
			'oojs-ui.styles.indicators',
3840
			'oojs-ui.styles.textures',
3841
			'mediawiki.widgets.styles',
3842
		] );
3843
	}
3844
3845
	/**
3846
	 * @param array $data Data from ParserOutput::getLimitReportData()
3847
	 * @since 1.28
3848
	 */
3849
	public function setLimitReportData( array $data ) {
3850
		$this->limitReportData = $data;
3851
	}
3852
}
3853