Completed
Branch master (9259dd)
by
unknown
27:26
created

OutputPage::getCacheVaryCookies()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nc 2
nop 0
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
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
27
/**
28
 * This class should be covered by a general architecture document which does
29
 * not exist as of January 2011.  This is one of the Core classes and should
30
 * be read at least once by any new developers.
31
 *
32
 * This class is used to prepare the final rendering. A skin is then
33
 * applied to the output parameters (links, javascript, html, categories ...).
34
 *
35
 * @todo FIXME: Another class handles sending the whole page to the client.
36
 *
37
 * Some comments comes from a pairing session between Zak Greant and Antoine Musso
38
 * in November 2010.
39
 *
40
 * @todo document
41
 */
42
class OutputPage extends ContextSource {
43
	/** @var array Should be private. Used with addMeta() which adds "<meta>" */
44
	protected $mMetatags = [];
45
46
	/** @var array */
47
	protected $mLinktags = [];
48
49
	/** @var bool */
50
	protected $mCanonicalUrl = false;
51
52
	/**
53
	 * @var array Additional stylesheets. Looks like this is for extensions.
54
	 *   Might be replaced by ResourceLoader.
55
	 */
56
	protected $mExtStyles = [];
57
58
	/**
59
	 * @var string Should be private - has getter and setter. Contains
60
	 *   the HTML title */
61
	public $mPagetitle = '';
62
63
	/**
64
	 * @var string Contains all of the "<body>" content. Should be private we
65
	 *   got set/get accessors and the append() method.
66
	 */
67
	public $mBodytext = '';
68
69
	/**
70
	 * Holds the debug lines that will be output as comments in page source if
71
	 * $wgDebugComments is enabled. See also $wgShowDebug.
72
	 * @deprecated since 1.20; use MWDebug class instead.
73
	 */
74
	public $mDebugtext = '';
75
76
	/** @var string Stores contents of "<title>" tag */
77
	private $mHTMLtitle = '';
78
79
	/**
80
	 * @var bool Is the displayed content related to the source of the
81
	 *   corresponding wiki article.
82
	 */
83
	private $mIsarticle = false;
84
85
	/** @var bool Stores "article flag" toggle. */
86
	private $mIsArticleRelated = true;
87
88
	/**
89
	 * @var bool We have to set isPrintable(). Some pages should
90
	 * never be printed (ex: redirections).
91
	 */
92
	private $mPrintable = false;
93
94
	/**
95
	 * @var array Contains the page subtitle. Special pages usually have some
96
	 *   links here. Don't confuse with site subtitle added by skins.
97
	 */
98
	private $mSubtitle = [];
99
100
	/** @var string */
101
	public $mRedirect = '';
102
103
	/** @var int */
104
	protected $mStatusCode;
105
106
	/**
107
	 * @var string Variable mLastModified and mEtag are used for sending cache control.
108
	 *   The whole caching system should probably be moved into its own class.
109
	 */
110
	protected $mLastModified = '';
111
112
	/**
113
	 * Contains an HTTP Entity Tags (see RFC 2616 section 3.13) which is used
114
	 * as a unique identifier for the content. It is later used by the client
115
	 * to compare its cached version with the server version. Client sends
116
	 * headers If-Match and If-None-Match containing its locally cached ETAG value.
117
	 *
118
	 * To get more information, you will have to look at HTTP/1.1 protocol which
119
	 * is properly described in RFC 2616 : http://tools.ietf.org/html/rfc2616
120
	 */
121
	private $mETag = false;
122
123
	/** @var array */
124
	protected $mCategoryLinks = [];
125
126
	/** @var array */
127
	protected $mCategories = [];
128
129
	/** @var array */
130
	protected $mIndicators = [];
131
132
	/** @var array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page') */
133
	private $mLanguageLinks = [];
134
135
	/**
136
	 * Used for JavaScript (predates ResourceLoader)
137
	 * @todo We should split JS / CSS.
138
	 * mScripts content is inserted as is in "<head>" by Skin. This might
139
	 * contain either a link to a stylesheet or inline CSS.
140
	 */
141
	private $mScripts = '';
142
143
	/** @var string Inline CSS styles. Use addInlineStyle() sparingly */
144
	protected $mInlineStyles = '';
145
146
	/**
147
	 * @var string Used by skin template.
148
	 * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle );
149
	 */
150
	public $mPageLinkTitle = '';
151
152
	/** @var array Array of elements in "<head>". Parser might add its own headers! */
153
	protected $mHeadItems = [];
154
155
	/** @var array */
156
	protected $mModules = [];
157
158
	/** @var array */
159
	protected $mModuleScripts = [];
160
161
	/** @var array */
162
	protected $mModuleStyles = [];
163
164
	/** @var ResourceLoader */
165
	protected $mResourceLoader;
166
167
	/** @var array */
168
	protected $mJsConfigVars = [];
169
170
	/** @var array */
171
	protected $mTemplateIds = [];
172
173
	/** @var array */
174
	protected $mImageTimeKeys = [];
175
176
	/** @var string */
177
	public $mRedirectCode = '';
178
179
	protected $mFeedLinksAppendQuery = null;
180
181
	/** @var array
182
	 * What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page?
183
	 * @see ResourceLoaderModule::$origin
184
	 * ResourceLoaderModule::ORIGIN_ALL is assumed unless overridden;
185
	 */
186
	protected $mAllowedModules = [
187
		ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
188
	];
189
190
	/** @var bool Whether output is disabled.  If this is true, the 'output' method will do nothing. */
191
	protected $mDoNothing = false;
192
193
	// Parser related.
194
195
	/** @var int */
196
	protected $mContainsNewMagic = 0;
197
198
	/**
199
	 * lazy initialised, use parserOptions()
200
	 * @var ParserOptions
201
	 */
202
	protected $mParserOptions = null;
203
204
	/**
205
	 * Handles the Atom / RSS links.
206
	 * We probably only support Atom in 2011.
207
	 * @see $wgAdvertisedFeedTypes
208
	 */
209
	private $mFeedLinks = [];
210
211
	// Gwicke work on squid caching? Roughly from 2003.
212
	protected $mEnableClientCache = true;
213
214
	/** @var bool Flag if output should only contain the body of the article. */
215
	private $mArticleBodyOnly = false;
216
217
	/** @var bool */
218
	protected $mNewSectionLink = false;
219
220
	/** @var bool */
221
	protected $mHideNewSectionLink = false;
222
223
	/**
224
	 * @var bool Comes from the parser. This was probably made to load CSS/JS
225
	 * only if we had "<gallery>". Used directly in CategoryPage.php.
226
	 * Looks like ResourceLoader can replace this.
227
	 */
228
	public $mNoGallery = false;
229
230
	/** @var string */
231
	private $mPageTitleActionText = '';
232
233
	/** @var int Cache stuff. Looks like mEnableClientCache */
234
	protected $mCdnMaxage = 0;
235
	/** @var int Upper limit on mCdnMaxage */
236
	protected $mCdnMaxageLimit = INF;
237
238
	/**
239
	 * @var bool Controls if anti-clickjacking / frame-breaking headers will
240
	 * be sent. This should be done for pages where edit actions are possible.
241
	 * Setters: $this->preventClickjacking() and $this->allowClickjacking().
242
	 */
243
	protected $mPreventClickjacking = true;
244
245
	/** @var int To include the variable {{REVISIONID}} */
246
	private $mRevisionId = null;
247
248
	/** @var string */
249
	private $mRevisionTimestamp = null;
250
251
	/** @var array */
252
	protected $mFileVersion = null;
253
254
	/**
255
	 * @var array An array of stylesheet filenames (relative from skins path),
256
	 * with options for CSS media, IE conditions, and RTL/LTR direction.
257
	 * For internal use; add settings in the skin via $this->addStyle()
258
	 *
259
	 * Style again! This seems like a code duplication since we already have
260
	 * mStyles. This is what makes Open Source amazing.
261
	 */
262
	protected $styles = [];
263
264
	/**
265
	 * Whether jQuery is already handled.
266
	 */
267
	protected $mJQueryDone = false;
268
269
	private $mIndexPolicy = 'index';
270
	private $mFollowPolicy = 'follow';
271
	private $mVaryHeader = [
272
		'Accept-Encoding' => [ 'match=gzip' ],
273
	];
274
275
	/**
276
	 * If the current page was reached through a redirect, $mRedirectedFrom contains the Title
277
	 * of the redirect.
278
	 *
279
	 * @var Title
280
	 */
281
	private $mRedirectedFrom = null;
282
283
	/**
284
	 * Additional key => value data
285
	 */
286
	private $mProperties = [];
287
288
	/**
289
	 * @var string|null ResourceLoader target for load.php links. If null, will be omitted
290
	 */
291
	private $mTarget = null;
292
293
	/**
294
	 * @var bool Whether parser output should contain table of contents
295
	 */
296
	private $mEnableTOC = true;
297
298
	/**
299
	 * @var bool Whether parser output should contain section edit links
300
	 */
301
	private $mEnableSectionEditLinks = true;
302
303
	/**
304
	 * @var string|null The URL to send in a <link> element with rel=copyright
305
	 */
306
	private $copyrightUrl;
307
308
	/**
309
	 * Constructor for OutputPage. This should not be called directly.
310
	 * Instead a new RequestContext should be created and it will implicitly create
311
	 * a OutputPage tied to that context.
312
	 * @param IContextSource|null $context
313
	 */
314
	function __construct( IContextSource $context = null ) {
315
		if ( $context === null ) {
316
			# Extensions should use `new RequestContext` instead of `new OutputPage` now.
317
			wfDeprecated( __METHOD__, '1.18' );
318
		} else {
319
			$this->setContext( $context );
320
		}
321
	}
322
323
	/**
324
	 * Redirect to $url rather than displaying the normal page
325
	 *
326
	 * @param string $url URL
327
	 * @param string $responsecode HTTP status code
328
	 */
329
	public function redirect( $url, $responsecode = '302' ) {
330
		# Strip newlines as a paranoia check for header injection in PHP<5.1.2
331
		$this->mRedirect = str_replace( "\n", '', $url );
332
		$this->mRedirectCode = $responsecode;
333
	}
334
335
	/**
336
	 * Get the URL to redirect to, or an empty string if not redirect URL set
337
	 *
338
	 * @return string
339
	 */
340
	public function getRedirect() {
341
		return $this->mRedirect;
342
	}
343
344
	/**
345
	 * Set the copyright URL to send with the output.
346
	 * Empty string to omit, null to reset.
347
	 *
348
	 * @since 1.26
349
	 *
350
	 * @param string|null $url
351
	 */
352
	public function setCopyrightUrl( $url ) {
353
		$this->copyrightUrl = $url;
354
	}
355
356
	/**
357
	 * Set the HTTP status code to send with the output.
358
	 *
359
	 * @param int $statusCode
360
	 */
361
	public function setStatusCode( $statusCode ) {
362
		$this->mStatusCode = $statusCode;
363
	}
364
365
	/**
366
	 * Add a new "<meta>" tag
367
	 * To add an http-equiv meta tag, precede the name with "http:"
368
	 *
369
	 * @param string $name Tag name
370
	 * @param string $val Tag value
371
	 */
372
	function addMeta( $name, $val ) {
373
		array_push( $this->mMetatags, [ $name, $val ] );
374
	}
375
376
	/**
377
	 * Returns the current <meta> tags
378
	 *
379
	 * @since 1.25
380
	 * @return array
381
	 */
382
	public function getMetaTags() {
383
		return $this->mMetatags;
384
	}
385
386
	/**
387
	 * Add a new \<link\> tag to the page header.
388
	 *
389
	 * Note: use setCanonicalUrl() for rel=canonical.
390
	 *
391
	 * @param array $linkarr Associative array of attributes.
392
	 */
393
	function addLink( array $linkarr ) {
394
		array_push( $this->mLinktags, $linkarr );
395
	}
396
397
	/**
398
	 * Returns the current <link> tags
399
	 *
400
	 * @since 1.25
401
	 * @return array
402
	 */
403
	public function getLinkTags() {
404
		return $this->mLinktags;
405
	}
406
407
	/**
408
	 * Add a new \<link\> with "rel" attribute set to "meta"
409
	 *
410
	 * @param array $linkarr Associative array mapping attribute names to their
411
	 *                 values, both keys and values will be escaped, and the
412
	 *                 "rel" attribute will be automatically added
413
	 */
414
	function addMetadataLink( array $linkarr ) {
415
		$linkarr['rel'] = $this->getMetadataAttribute();
416
		$this->addLink( $linkarr );
417
	}
418
419
	/**
420
	 * Set the URL to be used for the <link rel=canonical>. This should be used
421
	 * in preference to addLink(), to avoid duplicate link tags.
422
	 * @param string $url
423
	 */
424
	function setCanonicalUrl( $url ) {
425
		$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...
426
	}
427
428
	/**
429
	 * Returns the URL to be used for the <link rel=canonical> if
430
	 * one is set.
431
	 *
432
	 * @since 1.25
433
	 * @return bool|string
434
	 */
435
	public function getCanonicalUrl() {
436
		return $this->mCanonicalUrl;
437
	}
438
439
	/**
440
	 * Get the value of the "rel" attribute for metadata links
441
	 *
442
	 * @return string
443
	 */
444
	public function getMetadataAttribute() {
445
		# note: buggy CC software only reads first "meta" link
446
		static $haveMeta = false;
447
		if ( $haveMeta ) {
448
			return 'alternate meta';
449
		} else {
450
			$haveMeta = true;
451
			return 'meta';
452
		}
453
	}
454
455
	/**
456
	 * Add raw HTML to the list of scripts (including \<script\> tag, etc.)
457
	 * Internal use only. Use OutputPage::addModules() or OutputPage::addJsConfigVars()
458
	 * if possible.
459
	 *
460
	 * @param string $script Raw HTML
461
	 */
462
	function addScript( $script ) {
463
		$this->mScripts .= $script;
464
	}
465
466
	/**
467
	 * Register and add a stylesheet from an extension directory.
468
	 *
469
	 * @deprecated since 1.27 use addModuleStyles() or addStyle() instead
470
	 * @param string $url Path to sheet.  Provide either a full url (beginning
471
	 *             with 'http', etc) or a relative path from the document root
472
	 *             (beginning with '/').  Otherwise it behaves identically to
473
	 *             addStyle() and draws from the /skins folder.
474
	 */
475
	public function addExtensionStyle( $url ) {
476
		wfDeprecated( __METHOD__, '1.27' );
477
		array_push( $this->mExtStyles, $url );
478
	}
479
480
	/**
481
	 * Get all styles added by extensions
482
	 *
483
	 * @deprecated since 1.27
484
	 * @return array
485
	 */
486
	function getExtStyle() {
487
		wfDeprecated( __METHOD__, '1.27' );
488
		return $this->mExtStyles;
489
	}
490
491
	/**
492
	 * Add a JavaScript file out of skins/common, or a given relative path.
493
	 * Internal use only. Use OutputPage::addModules() if possible.
494
	 *
495
	 * @param string $file Filename in skins/common or complete on-server path
496
	 *              (/foo/bar.js)
497
	 * @param string $version Style version of the file. Defaults to $wgStyleVersion
498
	 */
499
	public function addScriptFile( $file, $version = null ) {
500
		// See if $file parameter is an absolute URL or begins with a slash
501
		if ( substr( $file, 0, 1 ) == '/' || preg_match( '#^[a-z]*://#i', $file ) ) {
502
			$path = $file;
503
		} else {
504
			$path = $this->getConfig()->get( 'StylePath' ) . "/common/{$file}";
505
		}
506
		if ( is_null( $version ) ) {
507
			$version = $this->getConfig()->get( 'StyleVersion' );
508
		}
509
		$this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ) ) );
510
	}
511
512
	/**
513
	 * Add a self-contained script tag with the given contents
514
	 * Internal use only. Use OutputPage::addModules() if possible.
515
	 *
516
	 * @param string $script JavaScript text, no "<script>" tags
517
	 */
518
	public function addInlineScript( $script ) {
519
		$this->mScripts .= Html::inlineScript( $script );
520
	}
521
522
	/**
523
	 * Filter an array of modules to remove insufficiently trustworthy members, and modules
524
	 * which are no longer registered (eg a page is cached before an extension is disabled)
525
	 * @param array $modules
526
	 * @param string|null $position If not null, only return modules with this position
527
	 * @param string $type
528
	 * @return array
529
	 */
530
	protected function filterModules( array $modules, $position = null,
531
		$type = ResourceLoaderModule::TYPE_COMBINED
532
	) {
533
		$resourceLoader = $this->getResourceLoader();
534
		$filteredModules = [];
535
		foreach ( $modules as $val ) {
536
			$module = $resourceLoader->getModule( $val );
537
			if ( $module instanceof ResourceLoaderModule
538
				&& $module->getOrigin() <= $this->getAllowedModules( $type )
539
				&& ( is_null( $position ) || $module->getPosition() == $position )
540
				&& ( !$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...
541
			) {
542
				$filteredModules[] = $val;
543
			}
544
		}
545
		return $filteredModules;
546
	}
547
548
	/**
549
	 * Get the list of modules to include on this page
550
	 *
551
	 * @param bool $filter Whether to filter out insufficiently trustworthy modules
552
	 * @param string|null $position If not null, only return modules with this position
553
	 * @param string $param
554
	 * @return array Array of module names
555
	 */
556
	public function getModules( $filter = false, $position = null, $param = 'mModules' ) {
557
		$modules = array_values( array_unique( $this->$param ) );
558
		return $filter
559
			? $this->filterModules( $modules, $position )
560
			: $modules;
561
	}
562
563
	/**
564
	 * Add one or more modules recognized by ResourceLoader. Modules added
565
	 * through this function will be loaded by ResourceLoader when the
566
	 * page loads.
567
	 *
568
	 * @param string|array $modules Module name (string) or array of module names
569
	 */
570
	public function addModules( $modules ) {
571
		$this->mModules = array_merge( $this->mModules, (array)$modules );
572
	}
573
574
	/**
575
	 * Get the list of module JS to include on this page
576
	 *
577
	 * @param bool $filter
578
	 * @param string|null $position
579
	 *
580
	 * @return array Array of module names
581
	 */
582
	public function getModuleScripts( $filter = false, $position = null ) {
583
		return $this->getModules( $filter, $position, 'mModuleScripts' );
584
	}
585
586
	/**
587
	 * Add only JS of one or more modules recognized by ResourceLoader. Module
588
	 * scripts added through this function will be loaded by ResourceLoader when
589
	 * the page loads.
590
	 *
591
	 * @param string|array $modules Module name (string) or array of module names
592
	 */
593
	public function addModuleScripts( $modules ) {
594
		$this->mModuleScripts = array_merge( $this->mModuleScripts, (array)$modules );
595
	}
596
597
	/**
598
	 * Get the list of module CSS to include on this page
599
	 *
600
	 * @param bool $filter
601
	 * @param string|null $position
602
	 *
603
	 * @return array Array of module names
604
	 */
605
	public function getModuleStyles( $filter = false, $position = null ) {
606
		return $this->getModules( $filter, $position, 'mModuleStyles' );
607
	}
608
609
	/**
610
	 * Add only CSS of one or more modules recognized by ResourceLoader.
611
	 *
612
	 * Module styles added through this function will be added using standard link CSS
613
	 * tags, rather than as a combined Javascript and CSS package. Thus, they will
614
	 * load when JavaScript is disabled (unless CSS also happens to be disabled).
615
	 *
616
	 * @param string|array $modules Module name (string) or array of module names
617
	 */
618
	public function addModuleStyles( $modules ) {
619
		$this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
620
	}
621
622
	/**
623
	 * Get the list of module messages to include on this page
624
	 *
625
	 * @deprecated since 1.26 Obsolete
626
	 * @param bool $filter
627
	 * @param string|null $position
628
	 * @return array Array of module names
629
	 */
630
	public function getModuleMessages( $filter = false, $position = null ) {
631
		wfDeprecated( __METHOD__, '1.26' );
632
		return [];
633
	}
634
635
	/**
636
	 * Load messages of one or more ResourceLoader modules.
637
	 *
638
	 * @deprecated since 1.26 Use addModules() instead
639
	 * @param string|array $modules Module name (string) or array of module names
640
	 */
641
	public function addModuleMessages( $modules ) {
642
		wfDeprecated( __METHOD__, '1.26' );
643
	}
644
645
	/**
646
	 * @return null|string ResourceLoader target
647
	 */
648
	public function getTarget() {
649
		return $this->mTarget;
650
	}
651
652
	/**
653
	 * Sets ResourceLoader target for load.php links. If null, will be omitted
654
	 *
655
	 * @param string|null $target
656
	 */
657
	public function setTarget( $target ) {
658
		$this->mTarget = $target;
659
	}
660
661
	/**
662
	 * Get an array of head items
663
	 *
664
	 * @return array
665
	 */
666
	function getHeadItemsArray() {
667
		return $this->mHeadItems;
668
	}
669
670
	/**
671
	 * Add or replace an header item to the output
672
	 *
673
	 * Whenever possible, use more specific options like ResourceLoader modules,
674
	 * OutputPage::addLink(), OutputPage::addMetaLink() and OutputPage::addFeedLink()
675
	 * Fallback options for those are: OutputPage::addStyle, OutputPage::addScript(),
676
	 * OutputPage::addInlineScript() and OutputPage::addInlineStyle()
677
	 * This would be your very LAST fallback.
678
	 *
679
	 * @param string $name Item name
680
	 * @param string $value Raw HTML
681
	 */
682
	public function addHeadItem( $name, $value ) {
683
		$this->mHeadItems[$name] = $value;
684
	}
685
686
	/**
687
	 * Check if the header item $name is already set
688
	 *
689
	 * @param string $name Item name
690
	 * @return bool
691
	 */
692
	public function hasHeadItem( $name ) {
693
		return isset( $this->mHeadItems[$name] );
694
	}
695
696
	/**
697
	 * Set the value of the ETag HTTP header, only used if $wgUseETag is true
698
	 *
699
	 * @param string $tag Value of "ETag" header
700
	 */
701
	function setETag( $tag ) {
702
		$this->mETag = $tag;
0 ignored issues
show
Documentation Bug introduced by
The property $mETag was declared of type boolean, but $tag 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...
703
	}
704
705
	/**
706
	 * Set whether the output should only contain the body of the article,
707
	 * without any skin, sidebar, etc.
708
	 * Used e.g. when calling with "action=render".
709
	 *
710
	 * @param bool $only Whether to output only the body of the article
711
	 */
712
	public function setArticleBodyOnly( $only ) {
713
		$this->mArticleBodyOnly = $only;
714
	}
715
716
	/**
717
	 * Return whether the output will contain only the body of the article
718
	 *
719
	 * @return bool
720
	 */
721
	public function getArticleBodyOnly() {
722
		return $this->mArticleBodyOnly;
723
	}
724
725
	/**
726
	 * Set an additional output property
727
	 * @since 1.21
728
	 *
729
	 * @param string $name
730
	 * @param mixed $value
731
	 */
732
	public function setProperty( $name, $value ) {
733
		$this->mProperties[$name] = $value;
734
	}
735
736
	/**
737
	 * Get an additional output property
738
	 * @since 1.21
739
	 *
740
	 * @param string $name
741
	 * @return mixed Property value or null if not found
742
	 */
743
	public function getProperty( $name ) {
744
		if ( isset( $this->mProperties[$name] ) ) {
745
			return $this->mProperties[$name];
746
		} else {
747
			return null;
748
		}
749
	}
750
751
	/**
752
	 * checkLastModified tells the client to use the client-cached page if
753
	 * possible. If successful, the OutputPage is disabled so that
754
	 * any future call to OutputPage->output() have no effect.
755
	 *
756
	 * Side effect: sets mLastModified for Last-Modified header
757
	 *
758
	 * @param string $timestamp
759
	 *
760
	 * @return bool True if cache-ok headers was sent.
761
	 */
762
	public function checkLastModified( $timestamp ) {
763
		if ( !$timestamp || $timestamp == '19700101000000' ) {
764
			wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
765
			return false;
766
		}
767
		$config = $this->getConfig();
768
		if ( !$config->get( 'CachePages' ) ) {
769
			wfDebug( __METHOD__ . ": CACHE DISABLED\n" );
770
			return false;
771
		}
772
773
		$timestamp = wfTimestamp( TS_MW, $timestamp );
774
		$modifiedTimes = [
775
			'page' => $timestamp,
776
			'user' => $this->getUser()->getTouched(),
777
			'epoch' => $config->get( 'CacheEpoch' )
778
		];
779
		if ( $config->get( 'UseSquid' ) ) {
780
			// bug 44570: the core page itself may not change, but resources might
781
			$modifiedTimes['sepoch'] = wfTimestamp( TS_MW, time() - $config->get( 'SquidMaxage' ) );
782
		}
783
		Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] );
784
785
		$maxModified = max( $modifiedTimes );
786
		$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...
787
788
		$clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' );
789
		if ( $clientHeader === false ) {
790
			wfDebug( __METHOD__ . ": client did not send If-Modified-Since header", 'private' );
791
			return false;
792
		}
793
794
		# IE sends sizes after the date like this:
795
		# Wed, 20 Aug 2003 06:51:19 GMT; length=5202
796
		# this breaks strtotime().
797
		$clientHeader = preg_replace( '/;.*$/', '', $clientHeader );
798
799
		MediaWiki\suppressWarnings(); // E_STRICT system time bitching
800
		$clientHeaderTime = strtotime( $clientHeader );
801
		MediaWiki\restoreWarnings();
802
		if ( !$clientHeaderTime ) {
803
			wfDebug( __METHOD__
804
				. ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
805
			return false;
806
		}
807
		$clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
808
809
		# Make debug info
810
		$info = '';
811
		foreach ( $modifiedTimes as $name => $value ) {
812
			if ( $info !== '' ) {
813
				$info .= ', ';
814
			}
815
			$info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
816
		}
817
818
		wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
819
			wfTimestamp( TS_ISO_8601, $clientHeaderTime ), 'private' );
820
		wfDebug( __METHOD__ . ": effective Last-Modified: " .
821
			wfTimestamp( TS_ISO_8601, $maxModified ), 'private' );
822
		if ( $clientHeaderTime < $maxModified ) {
823
			wfDebug( __METHOD__ . ": STALE, $info", 'private' );
824
			return false;
825
		}
826
827
		# Not modified
828
		# Give a 304 Not Modified response code and disable body output
829
		wfDebug( __METHOD__ . ": NOT MODIFIED, $info", 'private' );
830
		ini_set( 'zlib.output_compression', 0 );
831
		$this->getRequest()->response()->statusHeader( 304 );
832
		$this->sendCacheControl();
833
		$this->disable();
834
835
		// Don't output a compressed blob when using ob_gzhandler;
836
		// it's technically against HTTP spec and seems to confuse
837
		// Firefox when the response gets split over two packets.
838
		wfClearOutputBuffers();
839
840
		return true;
841
	}
842
843
	/**
844
	 * Override the last modified timestamp
845
	 *
846
	 * @param string $timestamp New timestamp, in a format readable by
847
	 *        wfTimestamp()
848
	 */
849
	public function setLastModified( $timestamp ) {
850
		$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...
851
	}
852
853
	/**
854
	 * Set the robot policy for the page: <http://www.robotstxt.org/meta.html>
855
	 *
856
	 * @param string $policy The literal string to output as the contents of
857
	 *   the meta tag.  Will be parsed according to the spec and output in
858
	 *   standardized form.
859
	 * @return null
860
	 */
861
	public function setRobotPolicy( $policy ) {
862
		$policy = Article::formatRobotPolicy( $policy );
863
864
		if ( isset( $policy['index'] ) ) {
865
			$this->setIndexPolicy( $policy['index'] );
866
		}
867
		if ( isset( $policy['follow'] ) ) {
868
			$this->setFollowPolicy( $policy['follow'] );
869
		}
870
	}
871
872
	/**
873
	 * Set the index policy for the page, but leave the follow policy un-
874
	 * touched.
875
	 *
876
	 * @param string $policy Either 'index' or 'noindex'.
877
	 * @return null
878
	 */
879
	public function setIndexPolicy( $policy ) {
880
		$policy = trim( $policy );
881
		if ( in_array( $policy, [ 'index', 'noindex' ] ) ) {
882
			$this->mIndexPolicy = $policy;
883
		}
884
	}
885
886
	/**
887
	 * Set the follow policy for the page, but leave the index policy un-
888
	 * touched.
889
	 *
890
	 * @param string $policy Either 'follow' or 'nofollow'.
891
	 * @return null
892
	 */
893
	public function setFollowPolicy( $policy ) {
894
		$policy = trim( $policy );
895
		if ( in_array( $policy, [ 'follow', 'nofollow' ] ) ) {
896
			$this->mFollowPolicy = $policy;
897
		}
898
	}
899
900
	/**
901
	 * Set the new value of the "action text", this will be added to the
902
	 * "HTML title", separated from it with " - ".
903
	 *
904
	 * @param string $text New value of the "action text"
905
	 */
906
	public function setPageTitleActionText( $text ) {
907
		$this->mPageTitleActionText = $text;
908
	}
909
910
	/**
911
	 * Get the value of the "action text"
912
	 *
913
	 * @return string
914
	 */
915
	public function getPageTitleActionText() {
916
		return $this->mPageTitleActionText;
917
	}
918
919
	/**
920
	 * "HTML title" means the contents of "<title>".
921
	 * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file.
922
	 *
923
	 * @param string|Message $name
924
	 */
925
	public function setHTMLTitle( $name ) {
926
		if ( $name instanceof Message ) {
927
			$this->mHTMLtitle = $name->setContext( $this->getContext() )->text();
928
		} else {
929
			$this->mHTMLtitle = $name;
930
		}
931
	}
932
933
	/**
934
	 * Return the "HTML title", i.e. the content of the "<title>" tag.
935
	 *
936
	 * @return string
937
	 */
938
	public function getHTMLTitle() {
939
		return $this->mHTMLtitle;
940
	}
941
942
	/**
943
	 * Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
944
	 *
945
	 * @param Title $t
946
	 */
947
	public function setRedirectedFrom( $t ) {
948
		$this->mRedirectedFrom = $t;
949
	}
950
951
	/**
952
	 * "Page title" means the contents of \<h1\>. It is stored as a valid HTML
953
	 * fragment. This function allows good tags like \<sup\> in the \<h1\> tag,
954
	 * but not bad tags like \<script\>. This function automatically sets
955
	 * \<title\> to the same content as \<h1\> but with all tags removed. Bad
956
	 * tags that were escaped in \<h1\> will still be escaped in \<title\>, and
957
	 * good tags like \<i\> will be dropped entirely.
958
	 *
959
	 * @param string|Message $name
960
	 */
961
	public function setPageTitle( $name ) {
962
		if ( $name instanceof Message ) {
963
			$name = $name->setContext( $this->getContext() )->text();
964
		}
965
966
		# change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
967
		# but leave "<i>foobar</i>" alone
968
		$nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
969
		$this->mPagetitle = $nameWithTags;
970
971
		# change "<i>foo&amp;bar</i>" to "foo&bar"
972
		$this->setHTMLTitle(
973
			$this->msg( 'pagetitle' )->rawParams( Sanitizer::stripAllTags( $nameWithTags ) )
974
				->inContentLanguage()
975
		);
976
	}
977
978
	/**
979
	 * Return the "page title", i.e. the content of the \<h1\> tag.
980
	 *
981
	 * @return string
982
	 */
983
	public function getPageTitle() {
984
		return $this->mPagetitle;
985
	}
986
987
	/**
988
	 * Set the Title object to use
989
	 *
990
	 * @param Title $t
991
	 */
992
	public function setTitle( Title $t ) {
993
		$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...
994
	}
995
996
	/**
997
	 * Replace the subtitle with $str
998
	 *
999
	 * @param string|Message $str New value of the subtitle. String should be safe HTML.
1000
	 */
1001
	public function setSubtitle( $str ) {
1002
		$this->clearSubtitle();
1003
		$this->addSubtitle( $str );
1004
	}
1005
1006
	/**
1007
	 * Add $str to the subtitle
1008
	 *
1009
	 * @param string|Message $str String or Message to add to the subtitle. String should be safe HTML.
1010
	 */
1011
	public function addSubtitle( $str ) {
1012
		if ( $str instanceof Message ) {
1013
			$this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
1014
		} else {
1015
			$this->mSubtitle[] = $str;
1016
		}
1017
	}
1018
1019
	/**
1020
	 * Build message object for a subtitle containing a backlink to a page
1021
	 *
1022
	 * @param Title $title Title to link to
1023
	 * @param array $query Array of additional parameters to include in the link
1024
	 * @return Message
1025
	 * @since 1.25
1026
	 */
1027
	public static function buildBacklinkSubtitle( Title $title, $query = [] ) {
1028
		if ( $title->isRedirect() ) {
1029
			$query['redirect'] = 'no';
1030
		}
1031
		return wfMessage( 'backlinksubtitle' )
1032
			->rawParams( Linker::link( $title, null, [], $query ) );
1033
	}
1034
1035
	/**
1036
	 * Add a subtitle containing a backlink to a page
1037
	 *
1038
	 * @param Title $title Title to link to
1039
	 * @param array $query Array of additional parameters to include in the link
1040
	 */
1041
	public function addBacklinkSubtitle( Title $title, $query = [] ) {
1042
		$this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) );
1043
	}
1044
1045
	/**
1046
	 * Clear the subtitles
1047
	 */
1048
	public function clearSubtitle() {
1049
		$this->mSubtitle = [];
1050
	}
1051
1052
	/**
1053
	 * Get the subtitle
1054
	 *
1055
	 * @return string
1056
	 */
1057
	public function getSubtitle() {
1058
		return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
1059
	}
1060
1061
	/**
1062
	 * Set the page as printable, i.e. it'll be displayed with all
1063
	 * print styles included
1064
	 */
1065
	public function setPrintable() {
1066
		$this->mPrintable = true;
1067
	}
1068
1069
	/**
1070
	 * Return whether the page is "printable"
1071
	 *
1072
	 * @return bool
1073
	 */
1074
	public function isPrintable() {
1075
		return $this->mPrintable;
1076
	}
1077
1078
	/**
1079
	 * Disable output completely, i.e. calling output() will have no effect
1080
	 */
1081
	public function disable() {
1082
		$this->mDoNothing = true;
1083
	}
1084
1085
	/**
1086
	 * Return whether the output will be completely disabled
1087
	 *
1088
	 * @return bool
1089
	 */
1090
	public function isDisabled() {
1091
		return $this->mDoNothing;
1092
	}
1093
1094
	/**
1095
	 * Show an "add new section" link?
1096
	 *
1097
	 * @return bool
1098
	 */
1099
	public function showNewSectionLink() {
1100
		return $this->mNewSectionLink;
1101
	}
1102
1103
	/**
1104
	 * Forcibly hide the new section link?
1105
	 *
1106
	 * @return bool
1107
	 */
1108
	public function forceHideNewSectionLink() {
1109
		return $this->mHideNewSectionLink;
1110
	}
1111
1112
	/**
1113
	 * Add or remove feed links in the page header
1114
	 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
1115
	 * for the new version
1116
	 * @see addFeedLink()
1117
	 *
1118
	 * @param bool $show True: add default feeds, false: remove all feeds
1119
	 */
1120
	public function setSyndicated( $show = true ) {
1121
		if ( $show ) {
1122
			$this->setFeedAppendQuery( false );
1123
		} else {
1124
			$this->mFeedLinks = [];
1125
		}
1126
	}
1127
1128
	/**
1129
	 * Add default feeds to the page header
1130
	 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
1131
	 * for the new version
1132
	 * @see addFeedLink()
1133
	 *
1134
	 * @param string $val Query to append to feed links or false to output
1135
	 *        default links
1136
	 */
1137
	public function setFeedAppendQuery( $val ) {
1138
		$this->mFeedLinks = [];
1139
1140
		foreach ( $this->getConfig()->get( 'AdvertisedFeedTypes' ) as $type ) {
1141
			$query = "feed=$type";
1142
			if ( is_string( $val ) ) {
1143
				$query .= '&' . $val;
1144
			}
1145
			$this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
1146
		}
1147
	}
1148
1149
	/**
1150
	 * Add a feed link to the page header
1151
	 *
1152
	 * @param string $format Feed type, should be a key of $wgFeedClasses
1153
	 * @param string $href URL
1154
	 */
1155
	public function addFeedLink( $format, $href ) {
1156
		if ( in_array( $format, $this->getConfig()->get( 'AdvertisedFeedTypes' ) ) ) {
1157
			$this->mFeedLinks[$format] = $href;
1158
		}
1159
	}
1160
1161
	/**
1162
	 * Should we output feed links for this page?
1163
	 * @return bool
1164
	 */
1165
	public function isSyndicated() {
1166
		return count( $this->mFeedLinks ) > 0;
1167
	}
1168
1169
	/**
1170
	 * Return URLs for each supported syndication format for this page.
1171
	 * @return array Associating format keys with URLs
1172
	 */
1173
	public function getSyndicationLinks() {
1174
		return $this->mFeedLinks;
1175
	}
1176
1177
	/**
1178
	 * Will currently always return null
1179
	 *
1180
	 * @return null
1181
	 */
1182
	public function getFeedAppendQuery() {
1183
		return $this->mFeedLinksAppendQuery;
1184
	}
1185
1186
	/**
1187
	 * Set whether the displayed content is related to the source of the
1188
	 * corresponding article on the wiki
1189
	 * Setting true will cause the change "article related" toggle to true
1190
	 *
1191
	 * @param bool $v
1192
	 */
1193
	public function setArticleFlag( $v ) {
1194
		$this->mIsarticle = $v;
1195
		if ( $v ) {
1196
			$this->mIsArticleRelated = $v;
1197
		}
1198
	}
1199
1200
	/**
1201
	 * Return whether the content displayed page is related to the source of
1202
	 * the corresponding article on the wiki
1203
	 *
1204
	 * @return bool
1205
	 */
1206
	public function isArticle() {
1207
		return $this->mIsarticle;
1208
	}
1209
1210
	/**
1211
	 * Set whether this page is related an article on the wiki
1212
	 * Setting false will cause the change of "article flag" toggle to false
1213
	 *
1214
	 * @param bool $v
1215
	 */
1216
	public function setArticleRelated( $v ) {
1217
		$this->mIsArticleRelated = $v;
1218
		if ( !$v ) {
1219
			$this->mIsarticle = false;
1220
		}
1221
	}
1222
1223
	/**
1224
	 * Return whether this page is related an article on the wiki
1225
	 *
1226
	 * @return bool
1227
	 */
1228
	public function isArticleRelated() {
1229
		return $this->mIsArticleRelated;
1230
	}
1231
1232
	/**
1233
	 * Add new language links
1234
	 *
1235
	 * @param array $newLinkArray Associative array mapping language code to the page
1236
	 *                      name
1237
	 */
1238
	public function addLanguageLinks( array $newLinkArray ) {
1239
		$this->mLanguageLinks += $newLinkArray;
1240
	}
1241
1242
	/**
1243
	 * Reset the language links and add new language links
1244
	 *
1245
	 * @param array $newLinkArray Associative array mapping language code to the page
1246
	 *                      name
1247
	 */
1248
	public function setLanguageLinks( array $newLinkArray ) {
1249
		$this->mLanguageLinks = $newLinkArray;
1250
	}
1251
1252
	/**
1253
	 * Get the list of language links
1254
	 *
1255
	 * @return array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page')
1256
	 */
1257
	public function getLanguageLinks() {
1258
		return $this->mLanguageLinks;
1259
	}
1260
1261
	/**
1262
	 * Add an array of categories, with names in the keys
1263
	 *
1264
	 * @param array $categories Mapping category name => sort key
1265
	 */
1266
	public function addCategoryLinks( array $categories ) {
1267
		global $wgContLang;
1268
1269
		if ( !is_array( $categories ) || count( $categories ) == 0 ) {
1270
			return;
1271
		}
1272
1273
		# Add the links to a LinkBatch
1274
		$arr = [ NS_CATEGORY => $categories ];
1275
		$lb = new LinkBatch;
1276
		$lb->setArray( $arr );
1277
1278
		# Fetch existence plus the hiddencat property
1279
		$dbr = wfGetDB( DB_SLAVE );
1280
		$fields = array_merge(
1281
			LinkCache::getSelectFields(),
1282
			[ 'page_namespace', 'page_title', 'pp_value' ]
1283
		);
1284
1285
		$res = $dbr->select( [ 'page', 'page_props' ],
1286
			$fields,
1287
			$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, DatabaseBase::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...
1288
			__METHOD__,
1289
			[],
1290
			[ 'page_props' => [ 'LEFT JOIN', [
1291
				'pp_propname' => 'hiddencat',
1292
				'pp_page = page_id'
1293
			] ] ]
1294
		);
1295
1296
		# Add the results to the link cache
1297
		$lb->addResultToCache( LinkCache::singleton(), $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select(array('page...'pp_page = page_id')))) on line 1285 can also be of type boolean; however, LinkBatch::addResultToCache() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Deprecated Code introduced by
The method LinkCache::singleton() has been deprecated with message: since 1.28, use MediaWikiServices instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1298
1299
		# Set all the values to 'normal'.
1300
		$categories = array_fill_keys( array_keys( $categories ), 'normal' );
1301
1302
		# Mark hidden categories
1303
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1304
			if ( isset( $row->pp_value ) ) {
1305
				$categories[$row->page_title] = 'hidden';
1306
			}
1307
		}
1308
1309
		# Add the remaining categories to the skin
1310
		if ( Hooks::run(
1311
			'OutputPageMakeCategoryLinks',
1312
			[ &$this, $categories, &$this->mCategoryLinks ] )
1313
		) {
1314
			foreach ( $categories as $category => $type ) {
1315
				// array keys will cast numeric category names to ints, so cast back to string
1316
				$category = (string)$category;
1317
				$origcategory = $category;
1318
				$title = Title::makeTitleSafe( NS_CATEGORY, $category );
1319
				if ( !$title ) {
1320
					continue;
1321
				}
1322
				$wgContLang->findVariantLink( $category, $title, true );
1323
				if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1324
					continue;
1325
				}
1326
				$text = $wgContLang->convertHtml( $title->getText() );
1327
				$this->mCategories[] = $title->getText();
1328
				$this->mCategoryLinks[$type][] = Linker::link( $title, $text );
1329
			}
1330
		}
1331
	}
1332
1333
	/**
1334
	 * Reset the category links (but not the category list) and add $categories
1335
	 *
1336
	 * @param array $categories Mapping category name => sort key
1337
	 */
1338
	public function setCategoryLinks( array $categories ) {
1339
		$this->mCategoryLinks = [];
1340
		$this->addCategoryLinks( $categories );
1341
	}
1342
1343
	/**
1344
	 * Get the list of category links, in a 2-D array with the following format:
1345
	 * $arr[$type][] = $link, where $type is either "normal" or "hidden" (for
1346
	 * hidden categories) and $link a HTML fragment with a link to the category
1347
	 * page
1348
	 *
1349
	 * @return array
1350
	 */
1351
	public function getCategoryLinks() {
1352
		return $this->mCategoryLinks;
1353
	}
1354
1355
	/**
1356
	 * Get the list of category names this page belongs to
1357
	 *
1358
	 * @return array Array of strings
1359
	 */
1360
	public function getCategories() {
1361
		return $this->mCategories;
1362
	}
1363
1364
	/**
1365
	 * Add an array of indicators, with their identifiers as array
1366
	 * keys and HTML contents as values.
1367
	 *
1368
	 * In case of duplicate keys, existing values are overwritten.
1369
	 *
1370
	 * @param array $indicators
1371
	 * @since 1.25
1372
	 */
1373
	public function setIndicators( array $indicators ) {
1374
		$this->mIndicators = $indicators + $this->mIndicators;
1375
		// Keep ordered by key
1376
		ksort( $this->mIndicators );
1377
	}
1378
1379
	/**
1380
	 * Get the indicators associated with this page.
1381
	 *
1382
	 * The array will be internally ordered by item keys.
1383
	 *
1384
	 * @return array Keys: identifiers, values: HTML contents
1385
	 * @since 1.25
1386
	 */
1387
	public function getIndicators() {
1388
		return $this->mIndicators;
1389
	}
1390
1391
	/**
1392
	 * Adds help link with an icon via page indicators.
1393
	 * Link target can be overridden by a local message containing a wikilink:
1394
	 * the message key is: lowercase action or special page name + '-helppage'.
1395
	 * @param string $to Target MediaWiki.org page title or encoded URL.
1396
	 * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
1397
	 * @since 1.25
1398
	 */
1399
	public function addHelpLink( $to, $overrideBaseUrl = false ) {
1400
		$this->addModuleStyles( 'mediawiki.helplink' );
1401
		$text = $this->msg( 'helppage-top-gethelp' )->escaped();
1402
1403
		if ( $overrideBaseUrl ) {
1404
			$helpUrl = $to;
1405
		} else {
1406
			$toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) );
1407
			$helpUrl = "//www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1408
		}
1409
1410
		$link = Html::rawElement(
1411
			'a',
1412
			[
1413
				'href' => $helpUrl,
1414
				'target' => '_blank',
1415
				'class' => 'mw-helplink',
1416
			],
1417
			$text
1418
		);
1419
1420
		$this->setIndicators( [ 'mw-helplink' => $link ] );
1421
	}
1422
1423
	/**
1424
	 * Do not allow scripts which can be modified by wiki users to load on this page;
1425
	 * only allow scripts bundled with, or generated by, the software.
1426
	 * Site-wide styles are controlled by a config setting, since they can be
1427
	 * used to create a custom skin/theme, but not user-specific ones.
1428
	 *
1429
	 * @todo this should be given a more accurate name
1430
	 */
1431
	public function disallowUserJs() {
1432
		$this->reduceAllowedModules(
1433
			ResourceLoaderModule::TYPE_SCRIPTS,
1434
			ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
1435
		);
1436
1437
		// Site-wide styles are controlled by a config setting, see bug 71621
1438
		// for background on why. User styles are never allowed.
1439
		if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
1440
			$styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
1441
		} else {
1442
			$styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL;
1443
		}
1444
		$this->reduceAllowedModules(
1445
			ResourceLoaderModule::TYPE_STYLES,
1446
			$styleOrigin
1447
		);
1448
	}
1449
1450
	/**
1451
	 * Show what level of JavaScript / CSS untrustworthiness is allowed on this page
1452
	 * @see ResourceLoaderModule::$origin
1453
	 * @param string $type ResourceLoaderModule TYPE_ constant
1454
	 * @return int ResourceLoaderModule ORIGIN_ class constant
1455
	 */
1456
	public function getAllowedModules( $type ) {
1457
		if ( $type == ResourceLoaderModule::TYPE_COMBINED ) {
1458
			return min( array_values( $this->mAllowedModules ) );
1459
		} else {
1460
			return isset( $this->mAllowedModules[$type] )
1461
				? $this->mAllowedModules[$type]
1462
				: ResourceLoaderModule::ORIGIN_ALL;
1463
		}
1464
	}
1465
1466
	/**
1467
	 * Limit the highest level of CSS/JS untrustworthiness allowed.
1468
	 *
1469
	 * If passed the same or a higher level than the current level of untrustworthiness set, the
1470
	 * level will remain unchanged.
1471
	 *
1472
	 * @param string $type
1473
	 * @param int $level ResourceLoaderModule class constant
1474
	 */
1475
	public function reduceAllowedModules( $type, $level ) {
1476
		$this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
1477
	}
1478
1479
	/**
1480
	 * Prepend $text to the body HTML
1481
	 *
1482
	 * @param string $text HTML
1483
	 */
1484
	public function prependHTML( $text ) {
1485
		$this->mBodytext = $text . $this->mBodytext;
1486
	}
1487
1488
	/**
1489
	 * Append $text to the body HTML
1490
	 *
1491
	 * @param string $text HTML
1492
	 */
1493
	public function addHTML( $text ) {
1494
		$this->mBodytext .= $text;
1495
	}
1496
1497
	/**
1498
	 * Shortcut for adding an Html::element via addHTML.
1499
	 *
1500
	 * @since 1.19
1501
	 *
1502
	 * @param string $element
1503
	 * @param array $attribs
1504
	 * @param string $contents
1505
	 */
1506
	public function addElement( $element, array $attribs = [], $contents = '' ) {
1507
		$this->addHTML( Html::element( $element, $attribs, $contents ) );
1508
	}
1509
1510
	/**
1511
	 * Clear the body HTML
1512
	 */
1513
	public function clearHTML() {
1514
		$this->mBodytext = '';
1515
	}
1516
1517
	/**
1518
	 * Get the body HTML
1519
	 *
1520
	 * @return string HTML
1521
	 */
1522
	public function getHTML() {
1523
		return $this->mBodytext;
1524
	}
1525
1526
	/**
1527
	 * Get/set the ParserOptions object to use for wikitext parsing
1528
	 *
1529
	 * @param ParserOptions|null $options Either the ParserOption to use or null to only get the
1530
	 *   current ParserOption object
1531
	 * @return ParserOptions
1532
	 */
1533
	public function parserOptions( $options = null ) {
1534
		if ( $options !== null && !empty( $options->isBogus ) ) {
1535
			// Someone is trying to set a bogus pre-$wgUser PO. Check if it has
1536
			// been changed somehow, and keep it if so.
1537
			$anonPO = ParserOptions::newFromAnon();
1538
			$anonPO->setEditSection( false );
1539
			if ( !$options->matches( $anonPO ) ) {
1540
				wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) );
1541
				$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...
1542
			}
1543
		}
1544
1545
		if ( !$this->mParserOptions ) {
1546
			if ( !$this->getContext()->getUser()->isSafeToLoad() ) {
1547
				// $wgUser isn't unstubbable yet, so don't try to get a
1548
				// ParserOptions for it. And don't cache this ParserOptions
1549
				// either.
1550
				$po = ParserOptions::newFromAnon();
1551
				$po->setEditSection( false );
1552
				$po->isBogus = true;
1553
				if ( $options !== null ) {
1554
					$this->mParserOptions = empty( $options->isBogus ) ? $options : null;
1555
				}
1556
				return $po;
1557
			}
1558
1559
			$this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
1560
			$this->mParserOptions->setEditSection( false );
1561
		}
1562
1563
		if ( $options !== null && !empty( $options->isBogus ) ) {
1564
			// They're trying to restore the bogus pre-$wgUser PO. Do the right
1565
			// thing.
1566
			return wfSetVar( $this->mParserOptions, null, true );
1567
		} else {
1568
			return wfSetVar( $this->mParserOptions, $options );
1569
		}
1570
	}
1571
1572
	/**
1573
	 * Set the revision ID which will be seen by the wiki text parser
1574
	 * for things such as embedded {{REVISIONID}} variable use.
1575
	 *
1576
	 * @param int|null $revid An positive integer, or null
1577
	 * @return mixed Previous value
1578
	 */
1579
	public function setRevisionId( $revid ) {
1580
		$val = is_null( $revid ) ? null : intval( $revid );
1581
		return wfSetVar( $this->mRevisionId, $val );
1582
	}
1583
1584
	/**
1585
	 * Get the displayed revision ID
1586
	 *
1587
	 * @return int
1588
	 */
1589
	public function getRevisionId() {
1590
		return $this->mRevisionId;
1591
	}
1592
1593
	/**
1594
	 * Set the timestamp of the revision which will be displayed. This is used
1595
	 * to avoid a extra DB call in Skin::lastModified().
1596
	 *
1597
	 * @param string|null $timestamp
1598
	 * @return mixed Previous value
1599
	 */
1600
	public function setRevisionTimestamp( $timestamp ) {
1601
		return wfSetVar( $this->mRevisionTimestamp, $timestamp );
1602
	}
1603
1604
	/**
1605
	 * Get the timestamp of displayed revision.
1606
	 * This will be null if not filled by setRevisionTimestamp().
1607
	 *
1608
	 * @return string|null
1609
	 */
1610
	public function getRevisionTimestamp() {
1611
		return $this->mRevisionTimestamp;
1612
	}
1613
1614
	/**
1615
	 * Set the displayed file version
1616
	 *
1617
	 * @param File|bool $file
1618
	 * @return mixed Previous value
1619
	 */
1620
	public function setFileVersion( $file ) {
1621
		$val = null;
1622
		if ( $file instanceof File && $file->exists() ) {
1623
			$val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ];
1624
		}
1625
		return wfSetVar( $this->mFileVersion, $val, true );
1626
	}
1627
1628
	/**
1629
	 * Get the displayed file version
1630
	 *
1631
	 * @return array|null ('time' => MW timestamp, 'sha1' => sha1)
1632
	 */
1633
	public function getFileVersion() {
1634
		return $this->mFileVersion;
1635
	}
1636
1637
	/**
1638
	 * Get the templates used on this page
1639
	 *
1640
	 * @return array (namespace => dbKey => revId)
1641
	 * @since 1.18
1642
	 */
1643
	public function getTemplateIds() {
1644
		return $this->mTemplateIds;
1645
	}
1646
1647
	/**
1648
	 * Get the files used on this page
1649
	 *
1650
	 * @return array (dbKey => array('time' => MW timestamp or null, 'sha1' => sha1 or ''))
1651
	 * @since 1.18
1652
	 */
1653
	public function getFileSearchOptions() {
1654
		return $this->mImageTimeKeys;
1655
	}
1656
1657
	/**
1658
	 * Convert wikitext to HTML and add it to the buffer
1659
	 * Default assumes that the current page title will be used.
1660
	 *
1661
	 * @param string $text
1662
	 * @param bool $linestart Is this the start of a line?
1663
	 * @param bool $interface Is this text in the user interface language?
1664
	 * @throws MWException
1665
	 */
1666
	public function addWikiText( $text, $linestart = true, $interface = true ) {
1667
		$title = $this->getTitle(); // Work around E_STRICT
1668
		if ( !$title ) {
1669
			throw new MWException( 'Title is null' );
1670
		}
1671
		$this->addWikiTextTitle( $text, $title, $linestart, /*tidy*/false, $interface );
1672
	}
1673
1674
	/**
1675
	 * Add wikitext with a custom Title object
1676
	 *
1677
	 * @param string $text Wikitext
1678
	 * @param Title $title
1679
	 * @param bool $linestart Is this the start of a line?
1680
	 */
1681
	public function addWikiTextWithTitle( $text, &$title, $linestart = true ) {
1682
		$this->addWikiTextTitle( $text, $title, $linestart );
1683
	}
1684
1685
	/**
1686
	 * Add wikitext with a custom Title object and tidy enabled.
1687
	 *
1688
	 * @param string $text Wikitext
1689
	 * @param Title $title
1690
	 * @param bool $linestart Is this the start of a line?
1691
	 */
1692
	function addWikiTextTitleTidy( $text, &$title, $linestart = true ) {
1693
		$this->addWikiTextTitle( $text, $title, $linestart, true );
1694
	}
1695
1696
	/**
1697
	 * Add wikitext with tidy enabled
1698
	 *
1699
	 * @param string $text Wikitext
1700
	 * @param bool $linestart Is this the start of a line?
1701
	 */
1702
	public function addWikiTextTidy( $text, $linestart = true ) {
1703
		$title = $this->getTitle();
1704
		$this->addWikiTextTitleTidy( $text, $title, $linestart );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->getTitle() on line 1703 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...
1705
	}
1706
1707
	/**
1708
	 * Add wikitext with a custom Title object
1709
	 *
1710
	 * @param string $text Wikitext
1711
	 * @param Title $title
1712
	 * @param bool $linestart Is this the start of a line?
1713
	 * @param bool $tidy Whether to use tidy
1714
	 * @param bool $interface Whether it is an interface message
1715
	 *   (for example disables conversion)
1716
	 */
1717
	public function addWikiTextTitle( $text, Title $title, $linestart,
1718
		$tidy = false, $interface = false
1719
	) {
1720
		global $wgParser;
1721
1722
		$popts = $this->parserOptions();
1723
		$oldTidy = $popts->setTidy( $tidy );
1724
		$popts->setInterfaceMessage( (bool)$interface );
1725
1726
		$parserOutput = $wgParser->getFreshParser()->parse(
1727
			$text, $title, $popts,
1728
			$linestart, true, $this->mRevisionId
1729
		);
1730
1731
		$popts->setTidy( $oldTidy );
1732
1733
		$this->addParserOutput( $parserOutput );
1734
1735
	}
1736
1737
	/**
1738
	 * Add a ParserOutput object, but without Html.
1739
	 *
1740
	 * @deprecated since 1.24, use addParserOutputMetadata() instead.
1741
	 * @param ParserOutput $parserOutput
1742
	 */
1743
	public function addParserOutputNoText( $parserOutput ) {
1744
		wfDeprecated( __METHOD__, '1.24' );
1745
		$this->addParserOutputMetadata( $parserOutput );
1746
	}
1747
1748
	/**
1749
	 * Add all metadata associated with a ParserOutput object, but without the actual HTML. This
1750
	 * includes categories, language links, ResourceLoader modules, effects of certain magic words,
1751
	 * and so on.
1752
	 *
1753
	 * @since 1.24
1754
	 * @param ParserOutput $parserOutput
1755
	 */
1756
	public function addParserOutputMetadata( $parserOutput ) {
1757
		$this->mLanguageLinks += $parserOutput->getLanguageLinks();
1758
		$this->addCategoryLinks( $parserOutput->getCategories() );
1759
		$this->setIndicators( $parserOutput->getIndicators() );
1760
		$this->mNewSectionLink = $parserOutput->getNewSection();
1761
		$this->mHideNewSectionLink = $parserOutput->getHideNewSection();
1762
1763
		if ( !$parserOutput->isCacheable() ) {
1764
			$this->enableClientCache( false );
1765
		}
1766
		$this->mNoGallery = $parserOutput->getNoGallery();
1767
		$this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
1768
		$this->addModules( $parserOutput->getModules() );
1769
		$this->addModuleScripts( $parserOutput->getModuleScripts() );
1770
		$this->addModuleStyles( $parserOutput->getModuleStyles() );
1771
		$this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1772
		$this->mPreventClickjacking = $this->mPreventClickjacking
1773
			|| $parserOutput->preventClickjacking();
1774
1775
		// Template versioning...
1776
		foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
1777
			if ( isset( $this->mTemplateIds[$ns] ) ) {
1778
				$this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1779
			} else {
1780
				$this->mTemplateIds[$ns] = $dbks;
1781
			}
1782
		}
1783
		// File versioning...
1784
		foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
1785
			$this->mImageTimeKeys[$dbk] = $data;
1786
		}
1787
1788
		// Hooks registered in the object
1789
		$parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' );
1790
		foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
1791
			list( $hookName, $data ) = $hookInfo;
1792
			if ( isset( $parserOutputHooks[$hookName] ) ) {
1793
				call_user_func( $parserOutputHooks[$hookName], $this, $parserOutput, $data );
1794
			}
1795
		}
1796
1797
		// enable OOUI if requested via ParserOutput
1798
		if ( $parserOutput->getEnableOOUI() ) {
1799
			$this->enableOOUI();
1800
		}
1801
1802
		// Link flags are ignored for now, but may in the future be
1803
		// used to mark individual language links.
1804
		$linkFlags = [];
1805
		Hooks::run( 'LanguageLinks', [ $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ] );
1806
		Hooks::run( 'OutputPageParserOutput', [ &$this, $parserOutput ] );
1807
	}
1808
1809
	/**
1810
	 * Add the HTML and enhancements for it (like ResourceLoader modules) associated with a
1811
	 * ParserOutput object, without any other metadata.
1812
	 *
1813
	 * @since 1.24
1814
	 * @param ParserOutput $parserOutput
1815
	 */
1816
	public function addParserOutputContent( $parserOutput ) {
1817
		$this->addParserOutputText( $parserOutput );
1818
1819
		$this->addModules( $parserOutput->getModules() );
1820
		$this->addModuleScripts( $parserOutput->getModuleScripts() );
1821
		$this->addModuleStyles( $parserOutput->getModuleStyles() );
1822
1823
		$this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1824
	}
1825
1826
	/**
1827
	 * Add the HTML associated with a ParserOutput object, without any metadata.
1828
	 *
1829
	 * @since 1.24
1830
	 * @param ParserOutput $parserOutput
1831
	 */
1832
	public function addParserOutputText( $parserOutput ) {
1833
		$text = $parserOutput->getText();
1834
		Hooks::run( 'OutputPageBeforeHTML', [ &$this, &$text ] );
1835
		$this->addHTML( $text );
1836
	}
1837
1838
	/**
1839
	 * Add everything from a ParserOutput object.
1840
	 *
1841
	 * @param ParserOutput $parserOutput
1842
	 */
1843
	function addParserOutput( $parserOutput ) {
1844
		$this->addParserOutputMetadata( $parserOutput );
1845
		$parserOutput->setTOCEnabled( $this->mEnableTOC );
1846
1847
		// Touch section edit links only if not previously disabled
1848
		if ( $parserOutput->getEditSectionTokens() ) {
1849
			$parserOutput->setEditSectionTokens( $this->mEnableSectionEditLinks );
1850
		}
1851
1852
		$this->addParserOutputText( $parserOutput );
1853
	}
1854
1855
	/**
1856
	 * Add the output of a QuickTemplate to the output buffer
1857
	 *
1858
	 * @param QuickTemplate $template
1859
	 */
1860
	public function addTemplate( &$template ) {
1861
		$this->addHTML( $template->getHTML() );
1862
	}
1863
1864
	/**
1865
	 * Parse wikitext and return the HTML.
1866
	 *
1867
	 * @param string $text
1868
	 * @param bool $linestart Is this the start of a line?
1869
	 * @param bool $interface Use interface language ($wgLang instead of
1870
	 *   $wgContLang) while parsing language sensitive magic words like GRAMMAR and PLURAL.
1871
	 *   This also disables LanguageConverter.
1872
	 * @param Language $language Target language object, will override $interface
1873
	 * @throws MWException
1874
	 * @return string HTML
1875
	 */
1876
	public function parse( $text, $linestart = true, $interface = false, $language = null ) {
1877
		global $wgParser;
1878
1879
		if ( is_null( $this->getTitle() ) ) {
1880
			throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
1881
		}
1882
1883
		$popts = $this->parserOptions();
1884
		if ( $interface ) {
1885
			$popts->setInterfaceMessage( true );
1886
		}
1887
		if ( $language !== null ) {
1888
			$oldLang = $popts->setTargetLanguage( $language );
1889
		}
1890
1891
		$parserOutput = $wgParser->getFreshParser()->parse(
1892
			$text, $this->getTitle(), $popts,
1893
			$linestart, true, $this->mRevisionId
1894
		);
1895
1896
		if ( $interface ) {
1897
			$popts->setInterfaceMessage( false );
1898
		}
1899
		if ( $language !== null ) {
1900
			$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...
1901
		}
1902
1903
		return $parserOutput->getText();
1904
	}
1905
1906
	/**
1907
	 * Parse wikitext, strip paragraphs, and return the HTML.
1908
	 *
1909
	 * @param string $text
1910
	 * @param bool $linestart Is this the start of a line?
1911
	 * @param bool $interface Use interface language ($wgLang instead of
1912
	 *   $wgContLang) while parsing language sensitive magic
1913
	 *   words like GRAMMAR and PLURAL
1914
	 * @return string HTML
1915
	 */
1916
	public function parseInline( $text, $linestart = true, $interface = false ) {
1917
		$parsed = $this->parse( $text, $linestart, $interface );
1918
		return Parser::stripOuterParagraph( $parsed );
1919
	}
1920
1921
	/**
1922
	 * @param $maxage
1923
	 * @deprecated since 1.27 Use setCdnMaxage() instead
1924
	 */
1925
	public function setSquidMaxage( $maxage ) {
1926
		$this->setCdnMaxage( $maxage );
1927
	}
1928
1929
	/**
1930
	 * Set the value of the "s-maxage" part of the "Cache-control" HTTP header
1931
	 *
1932
	 * @param int $maxage Maximum cache time on the CDN, in seconds.
1933
	 */
1934
	public function setCdnMaxage( $maxage ) {
1935
		$this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
1936
	}
1937
1938
	/**
1939
	 * Lower the value of the "s-maxage" part of the "Cache-control" HTTP header
1940
	 *
1941
	 * @param int $maxage Maximum cache time on the CDN, in seconds
1942
	 * @since 1.27
1943
	 */
1944
	public function lowerCdnMaxage( $maxage ) {
1945
		$this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
1946
		$this->setCdnMaxage( $this->mCdnMaxage );
1947
	}
1948
1949
	/**
1950
	 * Use enableClientCache(false) to force it to send nocache headers
1951
	 *
1952
	 * @param bool $state
1953
	 *
1954
	 * @return bool
1955
	 */
1956
	public function enableClientCache( $state ) {
1957
		return wfSetVar( $this->mEnableClientCache, $state );
1958
	}
1959
1960
	/**
1961
	 * Get the list of cookies that will influence on the cache
1962
	 *
1963
	 * @return array
1964
	 */
1965
	function getCacheVaryCookies() {
1966
		static $cookies;
1967
		if ( $cookies === null ) {
1968
			$config = $this->getConfig();
1969
			$cookies = array_merge(
1970
				SessionManager::singleton()->getVaryCookies(),
1971
				[
1972
					'forceHTTPS',
1973
				],
1974
				$config->get( 'CacheVaryCookies' )
1975
			);
1976
			Hooks::run( 'GetCacheVaryCookies', [ $this, &$cookies ] );
1977
		}
1978
		return $cookies;
1979
	}
1980
1981
	/**
1982
	 * Check if the request has a cache-varying cookie header
1983
	 * If it does, it's very important that we don't allow public caching
1984
	 *
1985
	 * @return bool
1986
	 */
1987
	function haveCacheVaryCookies() {
1988
		$request = $this->getRequest();
1989
		foreach ( $this->getCacheVaryCookies() as $cookieName ) {
1990
			if ( $request->getCookie( $cookieName, '', '' ) !== '' ) {
1991
				wfDebug( __METHOD__ . ": found $cookieName\n" );
1992
				return true;
1993
			}
1994
		}
1995
		wfDebug( __METHOD__ . ": no cache-varying cookies found\n" );
1996
		return false;
1997
	}
1998
1999
	/**
2000
	 * Add an HTTP header that will influence on the cache
2001
	 *
2002
	 * @param string $header Header name
2003
	 * @param string[]|null $option Options for the Key header. See
2004
	 * https://datatracker.ietf.org/doc/draft-fielding-http-key/
2005
	 * for the list of valid options.
2006
	 */
2007
	public function addVaryHeader( $header, array $option = null ) {
2008
		if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
2009
			$this->mVaryHeader[$header] = [];
2010
		}
2011
		if ( !is_array( $option ) ) {
2012
			$option = [];
2013
		}
2014
		$this->mVaryHeader[$header] = array_unique( array_merge( $this->mVaryHeader[$header], $option ) );
2015
	}
2016
2017
	/**
2018
	 * Return a Vary: header on which to vary caches. Based on the keys of $mVaryHeader,
2019
	 * such as Accept-Encoding or Cookie
2020
	 *
2021
	 * @return string
2022
	 */
2023
	public function getVaryHeader() {
2024
		// If we vary on cookies, let's make sure it's always included here too.
2025
		if ( $this->getCacheVaryCookies() ) {
2026
			$this->addVaryHeader( 'Cookie' );
2027
		}
2028
2029
		foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2030
			$this->addVaryHeader( $header, $options );
2031
		}
2032
		return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
2033
	}
2034
2035
	/**
2036
	 * Get a complete Key header
2037
	 *
2038
	 * @return string
2039
	 */
2040
	public function getKeyHeader() {
2041
		$cvCookies = $this->getCacheVaryCookies();
2042
2043
		$cookiesOption = [];
2044
		foreach ( $cvCookies as $cookieName ) {
2045
			$cookiesOption[] = 'param=' . $cookieName;
2046
		}
2047
		$this->addVaryHeader( 'Cookie', $cookiesOption );
2048
2049
		foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2050
			$this->addVaryHeader( $header, $options );
2051
		}
2052
2053
		$headers = [];
2054
		foreach ( $this->mVaryHeader as $header => $option ) {
2055
			$newheader = $header;
2056
			if ( is_array( $option ) && count( $option ) > 0 ) {
2057
				$newheader .= ';' . implode( ';', $option );
2058
			}
2059
			$headers[] = $newheader;
2060
		}
2061
		$key = 'Key: ' . implode( ',', $headers );
2062
2063
		return $key;
2064
	}
2065
2066
	/**
2067
	 * T23672: Add Accept-Language to Vary and Key headers
2068
	 * if there's no 'variant' parameter existed in GET.
2069
	 *
2070
	 * For example:
2071
	 *   /w/index.php?title=Main_page should always be served; but
2072
	 *   /w/index.php?title=Main_page&variant=zh-cn should never be served.
2073
	 */
2074
	function addAcceptLanguage() {
2075
		$title = $this->getTitle();
2076
		if ( !$title instanceof Title ) {
2077
			return;
2078
		}
2079
2080
		$lang = $title->getPageLanguage();
2081
		if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
2082
			$variants = $lang->getVariants();
2083
			$aloption = [];
2084
			foreach ( $variants as $variant ) {
2085
				if ( $variant === $lang->getCode() ) {
2086
					continue;
2087
				} else {
2088
					$aloption[] = 'substr=' . $variant;
2089
2090
					// IE and some other browsers use BCP 47 standards in
2091
					// their Accept-Language header, like "zh-CN" or "zh-Hant".
2092
					// We should handle these too.
2093
					$variantBCP47 = wfBCP47( $variant );
2094
					if ( $variantBCP47 !== $variant ) {
2095
						$aloption[] = 'substr=' . $variantBCP47;
2096
					}
2097
				}
2098
			}
2099
			$this->addVaryHeader( 'Accept-Language', $aloption );
2100
		}
2101
	}
2102
2103
	/**
2104
	 * Set a flag which will cause an X-Frame-Options header appropriate for
2105
	 * edit pages to be sent. The header value is controlled by
2106
	 * $wgEditPageFrameOptions.
2107
	 *
2108
	 * This is the default for special pages. If you display a CSRF-protected
2109
	 * form on an ordinary view page, then you need to call this function.
2110
	 *
2111
	 * @param bool $enable
2112
	 */
2113
	public function preventClickjacking( $enable = true ) {
2114
		$this->mPreventClickjacking = $enable;
2115
	}
2116
2117
	/**
2118
	 * Turn off frame-breaking. Alias for $this->preventClickjacking(false).
2119
	 * This can be called from pages which do not contain any CSRF-protected
2120
	 * HTML form.
2121
	 */
2122
	public function allowClickjacking() {
2123
		$this->mPreventClickjacking = false;
2124
	}
2125
2126
	/**
2127
	 * Get the prevent-clickjacking flag
2128
	 *
2129
	 * @since 1.24
2130
	 * @return bool
2131
	 */
2132
	public function getPreventClickjacking() {
2133
		return $this->mPreventClickjacking;
2134
	}
2135
2136
	/**
2137
	 * Get the X-Frame-Options header value (without the name part), or false
2138
	 * if there isn't one. This is used by Skin to determine whether to enable
2139
	 * JavaScript frame-breaking, for clients that don't support X-Frame-Options.
2140
	 *
2141
	 * @return string
2142
	 */
2143
	public function getFrameOptions() {
2144
		$config = $this->getConfig();
2145
		if ( $config->get( 'BreakFrames' ) ) {
2146
			return 'DENY';
2147
		} elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) {
2148
			return $config->get( 'EditPageFrameOptions' );
2149
		}
2150
		return false;
2151
	}
2152
2153
	/**
2154
	 * Send cache control HTTP headers
2155
	 */
2156
	public function sendCacheControl() {
2157
		$response = $this->getRequest()->response();
2158
		$config = $this->getConfig();
2159
		if ( $config->get( 'UseETag' ) && $this->mETag ) {
2160
			$response->header( "ETag: $this->mETag" );
2161
		}
2162
2163
		$this->addVaryHeader( 'Cookie' );
2164
		$this->addAcceptLanguage();
2165
2166
		# don't serve compressed data to clients who can't handle it
2167
		# maintain different caches for logged-in users and non-logged in ones
2168
		$response->header( $this->getVaryHeader() );
2169
2170
		if ( $config->get( 'UseKeyHeader' ) ) {
2171
			$response->header( $this->getKeyHeader() );
2172
		}
2173
2174
		if ( $this->mEnableClientCache ) {
2175
			if (
2176
				$config->get( 'UseSquid' ) &&
2177
				!$response->hasCookies() &&
2178
				!SessionManager::getGlobalSession()->isPersistent() &&
2179
				!$this->isPrintable() &&
2180
				$this->mCdnMaxage != 0 &&
2181
				!$this->haveCacheVaryCookies()
2182
			) {
2183
				if ( $config->get( 'UseESI' ) ) {
2184
					# We'll purge the proxy cache explicitly, but require end user agents
2185
					# to revalidate against the proxy on each visit.
2186
					# Surrogate-Control controls our CDN, Cache-Control downstream caches
2187
					wfDebug( __METHOD__ . ": proxy caching with ESI; {$this->mLastModified} **", 'private' );
2188
					# start with a shorter timeout for initial testing
2189
					# header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
2190
					$response->header( 'Surrogate-Control: max-age=' . $config->get( 'SquidMaxage' )
2191
						. '+' . $this->mCdnMaxage . ', content="ESI/1.0"' );
2192
					$response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
2193
				} else {
2194
					# We'll purge the proxy cache for anons explicitly, but require end user agents
2195
					# to revalidate against the proxy on each visit.
2196
					# IMPORTANT! The CDN needs to replace the Cache-Control header with
2197
					# Cache-Control: s-maxage=0, must-revalidate, max-age=0
2198
					wfDebug( __METHOD__ . ": local proxy caching; {$this->mLastModified} **", 'private' );
2199
					# start with a shorter timeout for initial testing
2200
					# header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
2201
					$response->header( 'Cache-Control: s-maxage=' . $this->mCdnMaxage
2202
						. ', must-revalidate, max-age=0' );
2203
				}
2204 View Code Duplication
			} else {
2205
				# We do want clients to cache if they can, but they *must* check for updates
2206
				# on revisiting the page.
2207
				wfDebug( __METHOD__ . ": private caching; {$this->mLastModified} **", 'private' );
2208
				$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2209
				$response->header( "Cache-Control: private, must-revalidate, max-age=0" );
2210
			}
2211
			if ( $this->mLastModified ) {
2212
				$response->header( "Last-Modified: {$this->mLastModified}" );
2213
			}
2214 View Code Duplication
		} else {
2215
			wfDebug( __METHOD__ . ": no caching **", 'private' );
2216
2217
			# In general, the absence of a last modified header should be enough to prevent
2218
			# the client from using its cache. We send a few other things just to make sure.
2219
			$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2220
			$response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
2221
			$response->header( 'Pragma: no-cache' );
2222
		}
2223
	}
2224
2225
	/**
2226
	 * Finally, all the text has been munged and accumulated into
2227
	 * the object, let's actually output it:
2228
	 */
2229
	public function output() {
2230
		if ( $this->mDoNothing ) {
2231
			return;
2232
		}
2233
2234
		$response = $this->getRequest()->response();
2235
		$config = $this->getConfig();
2236
2237
		if ( $this->mRedirect != '' ) {
2238
			# Standards require redirect URLs to be absolute
2239
			$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...
2240
2241
			$redirect = $this->mRedirect;
2242
			$code = $this->mRedirectCode;
2243
2244
			if ( Hooks::run( "BeforePageRedirect", [ $this, &$redirect, &$code ] ) ) {
2245
				if ( $code == '301' || $code == '303' ) {
2246
					if ( !$config->get( 'DebugRedirects' ) ) {
2247
						$response->statusHeader( $code );
2248
					}
2249
					$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...
2250
				}
2251
				if ( $config->get( 'VaryOnXFP' ) ) {
2252
					$this->addVaryHeader( 'X-Forwarded-Proto' );
2253
				}
2254
				$this->sendCacheControl();
2255
2256
				$response->header( "Content-Type: text/html; charset=utf-8" );
2257
				if ( $config->get( 'DebugRedirects' ) ) {
2258
					$url = htmlspecialchars( $redirect );
2259
					print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
2260
					print "<p>Location: <a href=\"$url\">$url</a></p>\n";
2261
					print "</body>\n</html>\n";
2262
				} else {
2263
					$response->header( 'Location: ' . $redirect );
2264
				}
2265
			}
2266
2267
			return;
2268
		} elseif ( $this->mStatusCode ) {
2269
			$response->statusHeader( $this->mStatusCode );
2270
		}
2271
2272
		# Buffer output; final headers may depend on later processing
2273
		ob_start();
2274
2275
		$response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
2276
		$response->header( 'Content-language: ' . $config->get( 'LanguageCode' ) );
2277
2278
		// Avoid Internet Explorer "compatibility view" in IE 8-10, so that
2279
		// jQuery etc. can work correctly.
2280
		$response->header( 'X-UA-Compatible: IE=Edge' );
2281
2282
		// Prevent framing, if requested
2283
		$frameOptions = $this->getFrameOptions();
2284
		if ( $frameOptions ) {
2285
			$response->header( "X-Frame-Options: $frameOptions" );
2286
		}
2287
2288
		if ( $this->mArticleBodyOnly ) {
2289
			echo $this->mBodytext;
2290
		} else {
2291
			$sk = $this->getSkin();
2292
			// add skin specific modules
2293
			$modules = $sk->getDefaultModules();
2294
2295
			// Enforce various default modules for all skins
2296
			$coreModules = [
2297
				// Keep this list as small as possible
2298
				'site',
2299
				'mediawiki.page.startup',
2300
				'mediawiki.user',
2301
			];
2302
2303
			// Support for high-density display images if enabled
2304
			if ( $config->get( 'ResponsiveImages' ) ) {
2305
				$coreModules[] = 'mediawiki.hidpi';
2306
			}
2307
2308
			$this->addModules( $coreModules );
2309
			foreach ( $modules as $group ) {
2310
				$this->addModules( $group );
2311
			}
2312
			MWDebug::addModules( $this );
2313
2314
			// Hook that allows last minute changes to the output page, e.g.
2315
			// adding of CSS or Javascript by extensions.
2316
			Hooks::run( 'BeforePageDisplay', [ &$this, &$sk ] );
2317
2318
			try {
2319
				$sk->outputPage();
2320
			} catch ( Exception $e ) {
2321
				ob_end_clean(); // bug T129657
2322
				throw $e;
2323
			}
2324
		}
2325
2326
		try {
2327
			// This hook allows last minute changes to final overall output by modifying output buffer
2328
			Hooks::run( 'AfterFinalPageOutput', [ $this ] );
2329
		} catch ( Exception $e ) {
2330
			ob_end_clean(); // bug T129657
2331
			throw $e;
2332
		}
2333
2334
		$this->sendCacheControl();
2335
2336
		ob_end_flush();
2337
2338
	}
2339
2340
	/**
2341
	 * Prepare this object to display an error page; disable caching and
2342
	 * indexing, clear the current text and redirect, set the page's title
2343
	 * and optionally an custom HTML title (content of the "<title>" tag).
2344
	 *
2345
	 * @param string|Message $pageTitle Will be passed directly to setPageTitle()
2346
	 * @param string|Message $htmlTitle Will be passed directly to setHTMLTitle();
2347
	 *                   optional, if not passed the "<title>" attribute will be
2348
	 *                   based on $pageTitle
2349
	 */
2350
	public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
2351
		$this->setPageTitle( $pageTitle );
2352
		if ( $htmlTitle !== false ) {
2353
			$this->setHTMLTitle( $htmlTitle );
2354
		}
2355
		$this->setRobotPolicy( 'noindex,nofollow' );
2356
		$this->setArticleRelated( false );
2357
		$this->enableClientCache( false );
2358
		$this->mRedirect = '';
2359
		$this->clearSubtitle();
2360
		$this->clearHTML();
2361
	}
2362
2363
	/**
2364
	 * Output a standard error page
2365
	 *
2366
	 * showErrorPage( 'titlemsg', 'pagetextmsg' );
2367
	 * showErrorPage( 'titlemsg', 'pagetextmsg', array( 'param1', 'param2' ) );
2368
	 * showErrorPage( 'titlemsg', $messageObject );
2369
	 * showErrorPage( $titleMessageObject, $messageObject );
2370
	 *
2371
	 * @param string|Message $title Message key (string) for page title, or a Message object
2372
	 * @param string|Message $msg Message key (string) for page text, or a Message object
2373
	 * @param array $params Message parameters; ignored if $msg is a Message object
2374
	 */
2375
	public function showErrorPage( $title, $msg, $params = [] ) {
2376
		if ( !$title instanceof Message ) {
2377
			$title = $this->msg( $title );
2378
		}
2379
2380
		$this->prepareErrorPage( $title );
2381
2382
		if ( $msg instanceof Message ) {
2383
			if ( $params !== [] ) {
2384
				trigger_error( 'Argument ignored: $params. The message parameters argument '
2385
					. 'is discarded when the $msg argument is a Message object instead of '
2386
					. 'a string.', E_USER_NOTICE );
2387
			}
2388
			$this->addHTML( $msg->parseAsBlock() );
2389
		} else {
2390
			$this->addWikiMsgArray( $msg, $params );
2391
		}
2392
2393
		$this->returnToMain();
2394
	}
2395
2396
	/**
2397
	 * Output a standard permission error page
2398
	 *
2399
	 * @param array $errors Error message keys
2400
	 * @param string $action Action that was denied or null if unknown
2401
	 */
2402
	public function showPermissionsErrorPage( array $errors, $action = null ) {
2403
		// For some action (read, edit, create and upload), display a "login to do this action"
2404
		// error if all of the following conditions are met:
2405
		// 1. the user is not logged in
2406
		// 2. the only error is insufficient permissions (i.e. no block or something else)
2407
		// 3. the error can be avoided simply by logging in
2408
		if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] )
2409
			&& $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
2410
			&& ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
2411
			&& ( User::groupHasPermission( 'user', $action )
2412
			|| User::groupHasPermission( 'autoconfirmed', $action ) )
2413
		) {
2414
			$displayReturnto = null;
2415
2416
			# Due to bug 32276, if a user does not have read permissions,
2417
			# $this->getTitle() will just give Special:Badtitle, which is
2418
			# not especially useful as a returnto parameter. Use the title
2419
			# from the request instead, if there was one.
2420
			$request = $this->getRequest();
2421
			$returnto = Title::newFromText( $request->getVal( 'title', '' ) );
2422
			if ( $action == 'edit' ) {
2423
				$msg = 'whitelistedittext';
2424
				$displayReturnto = $returnto;
2425
			} elseif ( $action == 'createpage' || $action == 'createtalk' ) {
2426
				$msg = 'nocreatetext';
2427
			} elseif ( $action == 'upload' ) {
2428
				$msg = 'uploadnologintext';
2429
			} else { # Read
2430
				$msg = 'loginreqpagetext';
2431
				$displayReturnto = Title::newMainPage();
2432
			}
2433
2434
			$query = [];
2435
2436
			if ( $returnto ) {
2437
				$query['returnto'] = $returnto->getPrefixedText();
2438
2439 View Code Duplication
				if ( !$request->wasPosted() ) {
2440
					$returntoquery = $request->getValues();
2441
					unset( $returntoquery['title'] );
2442
					unset( $returntoquery['returnto'] );
2443
					unset( $returntoquery['returntoquery'] );
2444
					$query['returntoquery'] = wfArrayToCgi( $returntoquery );
2445
				}
2446
			}
2447
			$loginLink = Linker::linkKnown(
2448
				SpecialPage::getTitleFor( 'Userlogin' ),
2449
				$this->msg( 'loginreqlink' )->escaped(),
2450
				[],
2451
				$query
2452
			);
2453
2454
			$this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
2455
			$this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->parse() );
2456
2457
			# Don't return to a page the user can't read otherwise
2458
			# we'll end up in a pointless loop
2459
			if ( $displayReturnto && $displayReturnto->userCan( 'read', $this->getUser() ) ) {
2460
				$this->returnToMain( null, $displayReturnto );
2461
			}
2462
		} else {
2463
			$this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
2464
			$this->addWikiText( $this->formatPermissionsErrorMessage( $errors, $action ) );
2465
		}
2466
	}
2467
2468
	/**
2469
	 * Display an error page indicating that a given version of MediaWiki is
2470
	 * required to use it
2471
	 *
2472
	 * @param mixed $version The version of MediaWiki needed to use the page
2473
	 */
2474
	public function versionRequired( $version ) {
2475
		$this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
2476
2477
		$this->addWikiMsg( 'versionrequiredtext', $version );
2478
		$this->returnToMain();
2479
	}
2480
2481
	/**
2482
	 * Format a list of error messages
2483
	 *
2484
	 * @param array $errors Array of arrays returned by Title::getUserPermissionsErrors
2485
	 * @param string $action Action that was denied or null if unknown
2486
	 * @return string The wikitext error-messages, formatted into a list.
2487
	 */
2488
	public function formatPermissionsErrorMessage( array $errors, $action = null ) {
2489
		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...
2490
			$text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
2491
		} else {
2492
			$action_desc = $this->msg( "action-$action" )->plain();
2493
			$text = $this->msg(
2494
				'permissionserrorstext-withaction',
2495
				count( $errors ),
2496
				$action_desc
2497
			)->plain() . "\n\n";
2498
		}
2499
2500
		if ( count( $errors ) > 1 ) {
2501
			$text .= '<ul class="permissions-errors">' . "\n";
2502
2503
			foreach ( $errors as $error ) {
2504
				$text .= '<li>';
2505
				$text .= call_user_func_array( [ $this, 'msg' ], $error )->plain();
2506
				$text .= "</li>\n";
2507
			}
2508
			$text .= '</ul>';
2509
		} else {
2510
			$text .= "<div class=\"permissions-errors\">\n" .
2511
					call_user_func_array( [ $this, 'msg' ], reset( $errors ) )->plain() .
2512
					"\n</div>";
2513
		}
2514
2515
		return $text;
2516
	}
2517
2518
	/**
2519
	 * Display a page stating that the Wiki is in read-only mode.
2520
	 * Should only be called after wfReadOnly() has returned true.
2521
	 *
2522
	 * Historically, this function was used to show the source of the page that the user
2523
	 * was trying to edit and _also_ permissions error messages. The relevant code was
2524
	 * moved into EditPage in 1.19 (r102024 / d83c2a431c2a) and removed here in 1.25.
2525
	 *
2526
	 * @deprecated since 1.25; throw the exception directly
2527
	 * @throws ReadOnlyError
2528
	 */
2529
	public function readOnlyPage() {
2530
		if ( func_num_args() > 0 ) {
2531
			throw new MWException( __METHOD__ . ' no longer accepts arguments since 1.25.' );
2532
		}
2533
2534
		throw new ReadOnlyError;
2535
	}
2536
2537
	/**
2538
	 * Turn off regular page output and return an error response
2539
	 * for when rate limiting has triggered.
2540
	 *
2541
	 * @deprecated since 1.25; throw the exception directly
2542
	 */
2543
	public function rateLimited() {
2544
		wfDeprecated( __METHOD__, '1.25' );
2545
		throw new ThrottledError;
2546
	}
2547
2548
	/**
2549
	 * Show a warning about slave lag
2550
	 *
2551
	 * If the lag is higher than $wgSlaveLagCritical seconds,
2552
	 * then the warning is a bit more obvious. If the lag is
2553
	 * lower than $wgSlaveLagWarning, then no warning is shown.
2554
	 *
2555
	 * @param int $lag Slave lag
2556
	 */
2557
	public function showLagWarning( $lag ) {
2558
		$config = $this->getConfig();
2559
		if ( $lag >= $config->get( 'SlaveLagWarning' ) ) {
2560
			$message = $lag < $config->get( 'SlaveLagCritical' )
2561
				? 'lag-warn-normal'
2562
				: 'lag-warn-high';
2563
			$wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" );
2564
			$this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] );
2565
		}
2566
	}
2567
2568
	public function showFatalError( $message ) {
2569
		$this->prepareErrorPage( $this->msg( 'internalerror' ) );
2570
2571
		$this->addHTML( $message );
2572
	}
2573
2574
	public function showUnexpectedValueError( $name, $val ) {
2575
		$this->showFatalError( $this->msg( 'unexpected', $name, $val )->text() );
2576
	}
2577
2578
	public function showFileCopyError( $old, $new ) {
2579
		$this->showFatalError( $this->msg( 'filecopyerror', $old, $new )->text() );
2580
	}
2581
2582
	public function showFileRenameError( $old, $new ) {
2583
		$this->showFatalError( $this->msg( 'filerenameerror', $old, $new )->text() );
2584
	}
2585
2586
	public function showFileDeleteError( $name ) {
2587
		$this->showFatalError( $this->msg( 'filedeleteerror', $name )->text() );
2588
	}
2589
2590
	public function showFileNotFoundError( $name ) {
2591
		$this->showFatalError( $this->msg( 'filenotfound', $name )->text() );
2592
	}
2593
2594
	/**
2595
	 * Add a "return to" link pointing to a specified title
2596
	 *
2597
	 * @param Title $title Title to link
2598
	 * @param array $query Query string parameters
2599
	 * @param string $text Text of the link (input is not escaped)
2600
	 * @param array $options Options array to pass to Linker
2601
	 */
2602
	public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) {
2603
		$link = $this->msg( 'returnto' )->rawParams(
2604
			Linker::link( $title, $text, [], $query, $options ) )->escaped();
2605
		$this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
2606
	}
2607
2608
	/**
2609
	 * Add a "return to" link pointing to a specified title,
2610
	 * or the title indicated in the request, or else the main page
2611
	 *
2612
	 * @param mixed $unused
2613
	 * @param Title|string $returnto Title or String to return to
2614
	 * @param string $returntoquery Query string for the return to link
2615
	 */
2616
	public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
2617
		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...
2618
			$returnto = $this->getRequest()->getText( 'returnto' );
2619
		}
2620
2621
		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...
2622
			$returntoquery = $this->getRequest()->getText( 'returntoquery' );
2623
		}
2624
2625
		if ( $returnto === '' ) {
2626
			$returnto = Title::newMainPage();
2627
		}
2628
2629
		if ( is_object( $returnto ) ) {
2630
			$titleObj = $returnto;
2631
		} else {
2632
			$titleObj = Title::newFromText( $returnto );
2633
		}
2634
		if ( !is_object( $titleObj ) ) {
2635
			$titleObj = Title::newMainPage();
2636
		}
2637
2638
		$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...
2639
	}
2640
2641
	/**
2642
	 * @param Skin $sk The given Skin
2643
	 * @param bool $includeStyle Unused
2644
	 * @return string The doctype, opening "<html>", and head element.
2645
	 */
2646
	public function headElement( Skin $sk, $includeStyle = true ) {
2647
		global $wgContLang;
2648
2649
		$userdir = $this->getLanguage()->getDir();
2650
		$sitedir = $wgContLang->getDir();
2651
2652
		$ret = Html::htmlHeader( $sk->getHtmlElementAttributes() );
2653
2654
		if ( $this->getHTMLTitle() == '' ) {
2655
			$this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() );
2656
		}
2657
2658
		$openHead = Html::openElement( 'head' );
2659
		if ( $openHead ) {
2660
			# Don't bother with the newline if $head == ''
2661
			$ret .= "$openHead\n";
2662
		}
2663
2664
		if ( !Html::isXmlMimeType( $this->getConfig()->get( 'MimeType' ) ) ) {
2665
			// Add <meta charset="UTF-8">
2666
			// This should be before <title> since it defines the charset used by
2667
			// text including the text inside <title>.
2668
			// The spec recommends defining XHTML5's charset using the XML declaration
2669
			// instead of meta.
2670
			// Our XML declaration is output by Html::htmlHeader.
2671
			// http://www.whatwg.org/html/semantics.html#attr-meta-http-equiv-content-type
2672
			// http://www.whatwg.org/html/semantics.html#charset
2673
			$ret .= Html::element( 'meta', [ 'charset' => 'UTF-8' ] ) . "\n";
2674
		}
2675
2676
		$ret .= Html::element( 'title', null, $this->getHTMLTitle() ) . "\n";
2677
		$ret .= $this->getInlineHeadScripts() . "\n";
2678
		$ret .= $this->buildCssLinks() . "\n";
2679
		$ret .= $this->getExternalHeadScripts() . "\n";
2680
2681
		foreach ( $this->getHeadLinksArray() as $item ) {
2682
			$ret .= $item . "\n";
2683
		}
2684
2685
		foreach ( $this->mHeadItems as $item ) {
2686
			$ret .= $item . "\n";
2687
		}
2688
2689
		$closeHead = Html::closeElement( 'head' );
2690
		if ( $closeHead ) {
2691
			$ret .= "$closeHead\n";
2692
		}
2693
2694
		$bodyClasses = [];
2695
		$bodyClasses[] = 'mediawiki';
2696
2697
		# Classes for LTR/RTL directionality support
2698
		$bodyClasses[] = $userdir;
2699
		$bodyClasses[] = "sitedir-$sitedir";
2700
2701
		if ( $this->getLanguage()->capitalizeAllNouns() ) {
2702
			# A <body> class is probably not the best way to do this . . .
2703
			$bodyClasses[] = 'capitalize-all-nouns';
2704
		}
2705
2706
		$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...
2707
		$bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
2708
		$bodyClasses[] =
2709
			'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
2710
2711
		$bodyAttrs = [];
2712
		// While the implode() is not strictly needed, it's used for backwards compatibility
2713
		// (this used to be built as a string and hooks likely still expect that).
2714
		$bodyAttrs['class'] = implode( ' ', $bodyClasses );
2715
2716
		// Allow skins and extensions to add body attributes they need
2717
		$sk->addToBodyAttributes( $this, $bodyAttrs );
2718
		Hooks::run( 'OutputPageBodyAttributes', [ $this, $sk, &$bodyAttrs ] );
2719
2720
		$ret .= Html::openElement( 'body', $bodyAttrs ) . "\n";
2721
2722
		return $ret;
2723
	}
2724
2725
	/**
2726
	 * Get a ResourceLoader object associated with this OutputPage
2727
	 *
2728
	 * @return ResourceLoader
2729
	 */
2730
	public function getResourceLoader() {
2731
		if ( is_null( $this->mResourceLoader ) ) {
2732
			$this->mResourceLoader = new ResourceLoader(
2733
				$this->getConfig(),
2734
				LoggerFactory::getInstance( 'resourceloader' )
2735
			);
2736
		}
2737
		return $this->mResourceLoader;
2738
	}
2739
2740
	/**
2741
	 * Construct neccecary html and loader preset states to load modules on a page.
2742
	 *
2743
	 * Use getHtmlFromLoaderLinks() to convert this array to HTML.
2744
	 *
2745
	 * @param array|string $modules One or more module names
2746
	 * @param string $only ResourceLoaderModule TYPE_ class constant
2747
	 * @param array $extraQuery [optional] Array with extra query parameters for the request
2748
	 * @return array A list of HTML strings and array of client loader preset states
2749
	 */
2750
	public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
2751
		$modules = (array)$modules;
2752
2753
		$links = [
2754
			// List of html strings
2755
			'html' => [],
2756
			// Associative array of module names and their states
2757
			'states' => [],
2758
		];
2759
2760
		if ( !count( $modules ) ) {
2761
			return $links;
2762
		}
2763
2764
		if ( count( $modules ) > 1 ) {
2765
			// Remove duplicate module requests
2766
			$modules = array_unique( $modules );
2767
			// Sort module names so requests are more uniform
2768
			sort( $modules );
2769
2770
			if ( ResourceLoader::inDebugMode() ) {
2771
				// Recursively call us for every item
2772
				foreach ( $modules as $name ) {
2773
					$link = $this->makeResourceLoaderLink( $name, $only, $extraQuery );
2774
					$links['html'] = array_merge( $links['html'], $link['html'] );
2775
					$links['states'] += $link['states'];
2776
				}
2777
				return $links;
2778
			}
2779
		}
2780
2781
		if ( !is_null( $this->mTarget ) ) {
2782
			$extraQuery['target'] = $this->mTarget;
2783
		}
2784
2785
		// Create keyed-by-source and then keyed-by-group list of module objects from modules list
2786
		$sortedModules = [];
2787
		$resourceLoader = $this->getResourceLoader();
2788
		foreach ( $modules as $name ) {
2789
			$module = $resourceLoader->getModule( $name );
2790
			# Check that we're allowed to include this module on this page
2791
			if ( !$module
2792
				|| ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS )
2793
					&& $only == ResourceLoaderModule::TYPE_SCRIPTS )
2794
				|| ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_STYLES )
2795
					&& $only == ResourceLoaderModule::TYPE_STYLES )
2796
				|| ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
2797
					&& $only == ResourceLoaderModule::TYPE_COMBINED )
2798
				|| ( $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 true; 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...
2799
			) {
2800
				continue;
2801
			}
2802
2803
			$sortedModules[$module->getSource()][$module->getGroup()][$name] = $module;
2804
		}
2805
2806
		foreach ( $sortedModules as $source => $groups ) {
2807
			foreach ( $groups as $group => $grpModules ) {
2808
				// Special handling for user-specific groups
2809
				$user = null;
2810
				if ( ( $group === 'user' || $group === 'private' ) && $this->getUser()->isLoggedIn() ) {
2811
					$user = $this->getUser()->getName();
2812
				}
2813
2814
				// Create a fake request based on the one we are about to make so modules return
2815
				// correct timestamp and emptiness data
2816
				$query = ResourceLoader::makeLoaderQuery(
2817
					[], // modules; not determined yet
2818
					$this->getLanguage()->getCode(),
2819
					$this->getSkin()->getSkinName(),
2820
					$user,
2821
					null, // version; not determined yet
2822
					ResourceLoader::inDebugMode(),
2823
					$only === ResourceLoaderModule::TYPE_COMBINED ? null : $only,
2824
					$this->isPrintable(),
2825
					$this->getRequest()->getBool( 'handheld' ),
2826
					$extraQuery
2827
				);
2828
				$context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
2829
2830
				// Extract modules that know they're empty and see if we have one or more
2831
				// raw modules
2832
				$isRaw = false;
2833
				foreach ( $grpModules as $key => $module ) {
2834
					// Inline empty modules: since they're empty, just mark them as 'ready' (bug 46857)
2835
					// If we're only getting the styles, we don't need to do anything for empty modules.
2836
					if ( $module->isKnownEmpty( $context ) ) {
2837
						unset( $grpModules[$key] );
2838
						if ( $only !== ResourceLoaderModule::TYPE_STYLES ) {
2839
							$links['states'][$key] = 'ready';
2840
						}
2841
					}
2842
2843
					$isRaw |= $module->isRaw();
2844
				}
2845
2846
				// If there are no non-empty modules, skip this group
2847
				if ( count( $grpModules ) === 0 ) {
2848
					continue;
2849
				}
2850
2851
				// Inline private modules. These can't be loaded through load.php for security
2852
				// reasons, see bug 34907. Note that these modules should be loaded from
2853
				// getExternalHeadScripts() before the first loader call. Otherwise other modules can't
2854
				// properly use them as dependencies (bug 30914)
2855
				if ( $group === 'private' ) {
2856
					if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
2857
						$links['html'][] = Html::inlineStyle(
2858
							$resourceLoader->makeModuleResponse( $context, $grpModules )
2859
						);
2860
					} else {
2861
						$links['html'][] = ResourceLoader::makeInlineScript(
2862
							$resourceLoader->makeModuleResponse( $context, $grpModules )
2863
						);
2864
					}
2865
					continue;
2866
				}
2867
2868
				// Special handling for the user group; because users might change their stuff
2869
				// on-wiki like user pages, or user preferences; we need to find the highest
2870
				// timestamp of these user-changeable modules so we can ensure cache misses on change
2871
				// This should NOT be done for the site group (bug 27564) because anons get that too
2872
				// and we shouldn't be putting timestamps in CDN-cached HTML
2873
				$version = null;
0 ignored issues
show
Unused Code introduced by
$version 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...
2874
				if ( $group === 'user' ) {
2875
					$query['version'] = $resourceLoader->getCombinedVersion( $context, array_keys( $grpModules ) );
2876
				}
2877
2878
				$query['modules'] = ResourceLoader::makePackedModulesString( array_keys( $grpModules ) );
2879
				$moduleContext = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
2880
				$url = $resourceLoader->createLoaderURL( $source, $moduleContext, $extraQuery );
2881
2882
				// Automatically select style/script elements
2883
				if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
2884
					$link = Html::linkedStyle( $url );
2885
				} else {
2886
					if ( $context->getRaw() || $isRaw ) {
2887
						// Startup module can't load itself, needs to use <script> instead of mw.loader.load
2888
						$link = Html::element( 'script', [
2889
							// In SpecialJavaScriptTest, QUnit must load synchronous
2890
							'async' => !isset( $extraQuery['sync'] ),
2891
							'src' => $url
2892
						] );
2893
					} else {
2894
						$link = ResourceLoader::makeInlineScript(
2895
							Xml::encodeJsCall( 'mw.loader.load', [ $url ] )
0 ignored issues
show
Security Bug introduced by
It seems like \Xml::encodeJsCall('mw.loader.load', array($url)) 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...
2896
						);
2897
					}
2898
2899
					// For modules requested directly in the html via <script> or mw.loader.load
2900
					// tell mw.loader they are being loading to prevent duplicate requests.
2901
					foreach ( $grpModules as $key => $module ) {
2902
						// Don't output state=loading for the startup module.
2903
						if ( $key !== 'startup' ) {
2904
							$links['states'][$key] = 'loading';
2905
						}
2906
					}
2907
				}
2908
2909
				if ( $group == 'noscript' ) {
2910
					$links['html'][] = Html::rawElement( 'noscript', [], $link );
0 ignored issues
show
Bug introduced by
It seems like $link defined by \ResourceLoader::makeInl...er.load', array($url))) on line 2894 can also be of type object<WrappedString\WrappedString>; however, Html::rawElement() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2911
				} else {
2912
					$links['html'][] = $link;
2913
				}
2914
			}
2915
		}
2916
2917
		return $links;
2918
	}
2919
2920
	/**
2921
	 * Build html output from an array of links from makeResourceLoaderLink.
2922
	 * @param array $links
2923
	 * @return string HTML
2924
	 */
2925
	protected static function getHtmlFromLoaderLinks( array $links ) {
2926
		$html = [];
2927
		$states = [];
2928
		foreach ( $links as $link ) {
2929
			if ( !is_array( $link ) ) {
2930
				$html[] = $link;
2931
			} else {
2932
				$html = array_merge( $html, $link['html'] );
2933
				$states += $link['states'];
2934
			}
2935
		}
2936
		// Filter out empty values
2937
		$html = array_filter( $html, 'strlen' );
2938
2939
		if ( count( $states ) ) {
2940
			array_unshift( $html, ResourceLoader::makeInlineScript(
2941
				ResourceLoader::makeLoaderStateScript( $states )
0 ignored issues
show
Security Bug introduced by
It seems like \ResourceLoader::makeLoaderStateScript($states) targeting ResourceLoader::makeLoaderStateScript() 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...
2942
			) );
2943
		}
2944
2945
		return WrappedString::join( "\n", $html );
2946
	}
2947
2948
	/**
2949
	 * JS stuff to put in the "<head>". This is the startup module, config
2950
	 * vars and modules marked with position 'top'
2951
	 *
2952
	 * @return string HTML fragment
2953
	 */
2954
	function getHeadScripts() {
2955
		return $this->getInlineHeadScripts() . $this->getExternalHeadScripts();
2956
	}
2957
2958
	/**
2959
	 * <script src="..."> tags for "<head>". This is the startup module
2960
	 * and other modules marked with position 'top'.
2961
	 *
2962
	 * @return string HTML fragment
2963
	 */
2964
	function getExternalHeadScripts() {
2965
		$links = [];
2966
2967
		// Startup - this provides the client with the module
2968
		// manifest and loads jquery and mediawiki base modules
2969
		$links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS );
2970
2971
		return self::getHtmlFromLoaderLinks( $links );
2972
	}
2973
2974
	/**
2975
	 * <script>...</script> tags to put in "<head>".
2976
	 *
2977
	 * @return string HTML fragment
2978
	 */
2979
	function getInlineHeadScripts() {
2980
		$links = [];
2981
2982
		// Client profile classes for <html>. Allows for easy hiding/showing of UI components.
2983
		// Must be done synchronously on every page to avoid flashes of wrong content.
2984
		// Note: This class distinguishes MediaWiki-supported JavaScript from the rest.
2985
		// The "rest" includes browsers that support JavaScript but not supported by our runtime.
2986
		// For the performance benefit of the majority, this is added unconditionally here and is
2987
		// then fixed up by the startup module for unsupported browsers.
2988
		$links[] = Html::inlineScript(
2989
			'document.documentElement.className = document.documentElement.className'
2990
			. '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );'
2991
		);
2992
2993
		// Load config before anything else
2994
		$links[] = ResourceLoader::makeInlineScript(
2995
			ResourceLoader::makeConfigSetScript( $this->getJSVars() )
0 ignored issues
show
Security Bug introduced by
It seems like \ResourceLoader::makeCon...ipt($this->getJSVars()) 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...
2996
		);
2997
2998
		// Load embeddable private modules before any loader links
2999
		// This needs to be TYPE_COMBINED so these modules are properly wrapped
3000
		// in mw.loader.implement() calls and deferred until mw.user is available
3001
		$embedScripts = [ 'user.options' ];
3002
		$links[] = $this->makeResourceLoaderLink(
3003
			$embedScripts,
3004
			ResourceLoaderModule::TYPE_COMBINED
3005
		);
3006
		// Separate user.tokens as otherwise caching will be allowed (T84960)
3007
		$links[] = $this->makeResourceLoaderLink(
3008
			'user.tokens',
3009
			ResourceLoaderModule::TYPE_COMBINED
3010
		);
3011
3012
		// Modules requests - let the client calculate dependencies and batch requests as it likes
3013
		// Only load modules that have marked themselves for loading at the top
3014
		$modules = $this->getModules( true, 'top' );
3015
		if ( $modules ) {
3016
			$links[] = ResourceLoader::makeInlineScript(
3017
				Xml::encodeJsCall( 'mw.loader.load', [ $modules ] )
0 ignored issues
show
Security Bug introduced by
It seems like \Xml::encodeJsCall('mw.l...load', array($modules)) 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...
3018
			);
3019
		}
3020
3021
		// "Scripts only" modules marked for top inclusion
3022
		$links[] = $this->makeResourceLoaderLink(
3023
			$this->getModuleScripts( true, 'top' ),
3024
			ResourceLoaderModule::TYPE_SCRIPTS
3025
		);
3026
3027
		return self::getHtmlFromLoaderLinks( $links );
3028
	}
3029
3030
	/**
3031
	 * JS stuff to put at the 'bottom', which goes at the bottom of the `<body>`.
3032
	 * These are modules marked with position 'bottom', legacy scripts ($this->mScripts),
3033
	 * site JS, and user JS.
3034
	 *
3035
	 * @param bool $unused Previously used to let this method change its output based
3036
	 *  on whether it was called by getExternalHeadScripts() or getBottomScripts().
3037
	 * @return string
3038
	 */
3039
	function getScriptsForBottomQueue( $unused = null ) {
3040
		// Scripts "only" requests marked for bottom inclusion
3041
		// If we're in the <head>, use load() calls rather than <script src="..."> tags
3042
		$links = [];
3043
3044
		$links[] = $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ),
3045
			ResourceLoaderModule::TYPE_SCRIPTS
3046
		);
3047
3048
		// Modules requests - let the client calculate dependencies and batch requests as it likes
3049
		// Only load modules that have marked themselves for loading at the bottom
3050
		$modules = $this->getModules( true, 'bottom' );
3051
		if ( $modules ) {
3052
			$links[] = ResourceLoader::makeInlineScript(
3053
				Xml::encodeJsCall( 'mw.loader.load', [ $modules ] )
0 ignored issues
show
Security Bug introduced by
It seems like \Xml::encodeJsCall('mw.l...load', array($modules)) 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...
3054
			);
3055
		}
3056
3057
		// Legacy Scripts
3058
		$links[] = $this->mScripts;
3059
3060
		// Add user JS if enabled
3061
		// This must use TYPE_COMBINED instead of only=scripts so that its request is handled by
3062
		// mw.loader.implement() which ensures that execution is scheduled after the "site" module.
3063
		if ( $this->getConfig()->get( 'AllowUserJs' )
3064
			&& $this->getUser()->isLoggedIn()
3065
			&& $this->getTitle()
3066
			&& $this->getTitle()->isJsSubpage()
3067
			&& $this->userCanPreview()
3068
		) {
3069
			// We're on a preview of a JS subpage. Exclude this page from the user module (T28283)
3070
			// and include the draft contents as a raw script instead.
3071
			$links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED,
3072
				[ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
3073
			);
3074
			// Load the previewed JS
3075
			$links[] = ResourceLoader::makeInlineScript(
3076
				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...
3077
					[ 'user', 'site' ],
3078
					new XmlJsCode(
3079
						'function () {'
3080
							. Xml::encodeJsCall( '$.globalEval', [
3081
								$this->getRequest()->getText( 'wpTextbox1' )
3082
							] )
3083
							. '}'
3084
					)
3085
				] )
3086
			);
3087
3088
			// FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded
3089
			// asynchronously and may arrive *after* the inline script here. So the previewed code
3090
			// may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js.
3091
			// Similarly, when previewing ./common.js and the user module does arrive first,
3092
			// it will arrive without common.js and the inline script runs after.
3093
			// Thus running common after the excluded subpage.
3094
		} else {
3095
			// Include the user module normally, i.e., raw to avoid it being wrapped in a closure.
3096
			$links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED );
3097
		}
3098
3099
		return self::getHtmlFromLoaderLinks( $links );
3100
	}
3101
3102
	/**
3103
	 * JS stuff to put at the bottom of the "<body>"
3104
	 * @return string
3105
	 */
3106
	function getBottomScripts() {
3107
		return $this->getScriptsForBottomQueue();
3108
	}
3109
3110
	/**
3111
	 * Get the javascript config vars to include on this page
3112
	 *
3113
	 * @return array Array of javascript config vars
3114
	 * @since 1.23
3115
	 */
3116
	public function getJsConfigVars() {
3117
		return $this->mJsConfigVars;
3118
	}
3119
3120
	/**
3121
	 * Add one or more variables to be set in mw.config in JavaScript
3122
	 *
3123
	 * @param string|array $keys Key or array of key/value pairs
3124
	 * @param mixed $value [optional] Value of the configuration variable
3125
	 */
3126 View Code Duplication
	public function addJsConfigVars( $keys, $value = null ) {
3127
		if ( is_array( $keys ) ) {
3128
			foreach ( $keys as $key => $value ) {
3129
				$this->mJsConfigVars[$key] = $value;
3130
			}
3131
			return;
3132
		}
3133
3134
		$this->mJsConfigVars[$keys] = $value;
3135
	}
3136
3137
	/**
3138
	 * Get an array containing the variables to be set in mw.config in JavaScript.
3139
	 *
3140
	 * Do not add things here which can be evaluated in ResourceLoaderStartUpModule
3141
	 * - in other words, page-independent/site-wide variables (without state).
3142
	 * You will only be adding bloat to the html page and causing page caches to
3143
	 * have to be purged on configuration changes.
3144
	 * @return array
3145
	 */
3146
	public function getJSVars() {
3147
		global $wgContLang;
3148
3149
		$curRevisionId = 0;
3150
		$articleId = 0;
3151
		$canonicalSpecialPageName = false; # bug 21115
3152
3153
		$title = $this->getTitle();
3154
		$ns = $title->getNamespace();
3155
		$canonicalNamespace = MWNamespace::exists( $ns )
3156
			? MWNamespace::getCanonicalName( $ns )
3157
			: $title->getNsText();
3158
3159
		$sk = $this->getSkin();
3160
		// Get the relevant title so that AJAX features can use the correct page name
3161
		// when making API requests from certain special pages (bug 34972).
3162
		$relevantTitle = $sk->getRelevantTitle();
3163
		$relevantUser = $sk->getRelevantUser();
3164
3165
		if ( $ns == NS_SPECIAL ) {
3166
			list( $canonicalSpecialPageName, /*...*/ ) =
3167
				SpecialPageFactory::resolveAlias( $title->getDBkey() );
3168
		} elseif ( $this->canUseWikiPage() ) {
3169
			$wikiPage = $this->getWikiPage();
3170
			$curRevisionId = $wikiPage->getLatest();
3171
			$articleId = $wikiPage->getId();
3172
		}
3173
3174
		$lang = $title->getPageViewLanguage();
3175
3176
		// Pre-process information
3177
		$separatorTransTable = $lang->separatorTransformTable();
3178
		$separatorTransTable = $separatorTransTable ? $separatorTransTable : [];
3179
		$compactSeparatorTransTable = [
3180
			implode( "\t", array_keys( $separatorTransTable ) ),
3181
			implode( "\t", $separatorTransTable ),
3182
		];
3183
		$digitTransTable = $lang->digitTransformTable();
3184
		$digitTransTable = $digitTransTable ? $digitTransTable : [];
3185
		$compactDigitTransTable = [
3186
			implode( "\t", array_keys( $digitTransTable ) ),
3187
			implode( "\t", $digitTransTable ),
3188
		];
3189
3190
		$user = $this->getUser();
3191
3192
		$vars = [
3193
			'wgCanonicalNamespace' => $canonicalNamespace,
3194
			'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
3195
			'wgNamespaceNumber' => $title->getNamespace(),
3196
			'wgPageName' => $title->getPrefixedDBkey(),
3197
			'wgTitle' => $title->getText(),
3198
			'wgCurRevisionId' => $curRevisionId,
3199
			'wgRevisionId' => (int)$this->getRevisionId(),
3200
			'wgArticleId' => $articleId,
3201
			'wgIsArticle' => $this->isArticle(),
3202
			'wgIsRedirect' => $title->isRedirect(),
3203
			'wgAction' => Action::getActionName( $this->getContext() ),
3204
			'wgUserName' => $user->isAnon() ? null : $user->getName(),
3205
			'wgUserGroups' => $user->getEffectiveGroups(),
3206
			'wgCategories' => $this->getCategories(),
3207
			'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
3208
			'wgPageContentLanguage' => $lang->getCode(),
3209
			'wgPageContentModel' => $title->getContentModel(),
3210
			'wgSeparatorTransformTable' => $compactSeparatorTransTable,
3211
			'wgDigitTransformTable' => $compactDigitTransTable,
3212
			'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
3213
			'wgMonthNames' => $lang->getMonthNamesArray(),
3214
			'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(),
3215
			'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
3216
			'wgRelevantArticleId' => $relevantTitle->getArticleID(),
3217
			'wgRequestId' => WebRequest::getRequestId(),
3218
		];
3219
3220
		if ( $user->isLoggedIn() ) {
3221
			$vars['wgUserId'] = $user->getId();
3222
			$vars['wgUserEditCount'] = $user->getEditCount();
3223
			$userReg = wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
0 ignored issues
show
Security Bug introduced by
It seems like $user->getRegistration() targeting User::getRegistration() can also be of type false; however, wfTimestampOrNull() does only seem to accept string|null, did you maybe forget to handle an error condition?
Loading history...
3224
			$vars['wgUserRegistration'] = $userReg !== null ? ( $userReg * 1000 ) : null;
3225
			// Get the revision ID of the oldest new message on the user's talk
3226
			// page. This can be used for constructing new message alerts on
3227
			// the client side.
3228
			$vars['wgUserNewMsgRevisionId'] = $user->getNewMessageRevisionId();
3229
		}
3230
3231
		if ( $wgContLang->hasVariants() ) {
3232
			$vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
3233
		}
3234
		// Same test as SkinTemplate
3235
		$vars['wgIsProbablyEditable'] = $title->quickUserCan( 'edit', $user )
3236
			&& ( $title->exists() || $title->quickUserCan( 'create', $user ) );
3237
3238
		foreach ( $title->getRestrictionTypes() as $type ) {
3239
			$vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
3240
		}
3241
3242
		if ( $title->isMainPage() ) {
3243
			$vars['wgIsMainPage'] = true;
3244
		}
3245
3246
		if ( $this->mRedirectedFrom ) {
3247
			$vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
3248
		}
3249
3250
		if ( $relevantUser ) {
3251
			$vars['wgRelevantUserName'] = $relevantUser->getName();
3252
		}
3253
3254
		// Allow extensions to add their custom variables to the mw.config map.
3255
		// Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
3256
		// page-dependant but site-wide (without state).
3257
		// Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
3258
		Hooks::run( 'MakeGlobalVariablesScript', [ &$vars, $this ] );
3259
3260
		// Merge in variables from addJsConfigVars last
3261
		return array_merge( $vars, $this->getJsConfigVars() );
3262
	}
3263
3264
	/**
3265
	 * To make it harder for someone to slip a user a fake
3266
	 * user-JavaScript or user-CSS preview, a random token
3267
	 * is associated with the login session. If it's not
3268
	 * passed back with the preview request, we won't render
3269
	 * the code.
3270
	 *
3271
	 * @return bool
3272
	 */
3273
	public function userCanPreview() {
3274
		$request = $this->getRequest();
3275
		if (
3276
			$request->getVal( 'action' ) !== 'submit' ||
3277
			!$request->getCheck( 'wpPreview' ) ||
3278
			!$request->wasPosted()
3279
		) {
3280
			return false;
3281
		}
3282
3283
		$user = $this->getUser();
3284
		if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
3285
			return false;
3286
		}
3287
3288
		$title = $this->getTitle();
3289
		if ( !$title->isJsSubpage() && !$title->isCssSubpage() ) {
3290
			return false;
3291
		}
3292
		if ( !$title->isSubpageOf( $user->getUserPage() ) ) {
3293
			// Don't execute another user's CSS or JS on preview (T85855)
3294
			return false;
3295
		}
3296
3297
		$errors = $title->getUserPermissionsErrors( 'edit', $user );
3298
		if ( count( $errors ) !== 0 ) {
3299
			return false;
3300
		}
3301
3302
		return true;
3303
	}
3304
3305
	/**
3306
	 * @return array Array in format "link name or number => 'link html'".
3307
	 */
3308
	public function getHeadLinksArray() {
3309
		global $wgVersion;
3310
3311
		$tags = [];
3312
		$config = $this->getConfig();
3313
3314
		$canonicalUrl = $this->mCanonicalUrl;
3315
3316
		$tags['meta-generator'] = Html::element( 'meta', [
3317
			'name' => 'generator',
3318
			'content' => "MediaWiki $wgVersion",
3319
		] );
3320
3321
		if ( $config->get( 'ReferrerPolicy' ) !== false ) {
3322
			$tags['meta-referrer'] = Html::element( 'meta', [
3323
				'name' => 'referrer',
3324
				'content' => $config->get( 'ReferrerPolicy' )
3325
			] );
3326
		}
3327
3328
		$p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
3329
		if ( $p !== 'index,follow' ) {
3330
			// http://www.robotstxt.org/wc/meta-user.html
3331
			// Only show if it's different from the default robots policy
3332
			$tags['meta-robots'] = Html::element( 'meta', [
3333
				'name' => 'robots',
3334
				'content' => $p,
3335
			] );
3336
		}
3337
3338
		foreach ( $this->mMetatags as $tag ) {
3339
			if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) {
3340
				$a = 'http-equiv';
3341
				$tag[0] = substr( $tag[0], 5 );
3342
			} else {
3343
				$a = 'name';
3344
			}
3345
			$tagName = "meta-{$tag[0]}";
3346
			if ( isset( $tags[$tagName] ) ) {
3347
				$tagName .= $tag[1];
3348
			}
3349
			$tags[$tagName] = Html::element( 'meta',
3350
				[
3351
					$a => $tag[0],
3352
					'content' => $tag[1]
3353
				]
3354
			);
3355
		}
3356
3357
		foreach ( $this->mLinktags as $tag ) {
3358
			$tags[] = Html::element( 'link', $tag );
3359
		}
3360
3361
		# Universal edit button
3362
		if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
3363
			$user = $this->getUser();
3364
			if ( $this->getTitle()->quickUserCan( 'edit', $user )
3365
				&& ( $this->getTitle()->exists() ||
3366
					$this->getTitle()->quickUserCan( 'create', $user ) )
3367
			) {
3368
				// Original UniversalEditButton
3369
				$msg = $this->msg( 'edit' )->text();
3370
				$tags['universal-edit-button'] = Html::element( 'link', [
3371
					'rel' => 'alternate',
3372
					'type' => 'application/x-wiki',
3373
					'title' => $msg,
3374
					'href' => $this->getTitle()->getEditURL(),
3375
				] );
3376
				// Alternate edit link
3377
				$tags['alternative-edit'] = Html::element( 'link', [
3378
					'rel' => 'edit',
3379
					'title' => $msg,
3380
					'href' => $this->getTitle()->getEditURL(),
3381
				] );
3382
			}
3383
		}
3384
3385
		# Generally the order of the favicon and apple-touch-icon links
3386
		# should not matter, but Konqueror (3.5.9 at least) incorrectly
3387
		# uses whichever one appears later in the HTML source. Make sure
3388
		# apple-touch-icon is specified first to avoid this.
3389
		if ( $config->get( 'AppleTouchIcon' ) !== false ) {
3390
			$tags['apple-touch-icon'] = Html::element( 'link', [
3391
				'rel' => 'apple-touch-icon',
3392
				'href' => $config->get( 'AppleTouchIcon' )
3393
			] );
3394
		}
3395
3396
		if ( $config->get( 'Favicon' ) !== false ) {
3397
			$tags['favicon'] = Html::element( 'link', [
3398
				'rel' => 'shortcut icon',
3399
				'href' => $config->get( 'Favicon' )
3400
			] );
3401
		}
3402
3403
		# OpenSearch description link
3404
		$tags['opensearch'] = Html::element( 'link', [
3405
			'rel' => 'search',
3406
			'type' => 'application/opensearchdescription+xml',
3407
			'href' => wfScript( 'opensearch_desc' ),
3408
			'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
3409
		] );
3410
3411
		if ( $config->get( 'EnableAPI' ) ) {
3412
			# Real Simple Discovery link, provides auto-discovery information
3413
			# for the MediaWiki API (and potentially additional custom API
3414
			# support such as WordPress or Twitter-compatible APIs for a
3415
			# blogging extension, etc)
3416
			$tags['rsd'] = Html::element( 'link', [
3417
				'rel' => 'EditURI',
3418
				'type' => 'application/rsd+xml',
3419
				// Output a protocol-relative URL here if $wgServer is protocol-relative.
3420
				// Whether RSD accepts relative or protocol-relative URLs is completely
3421
				// undocumented, though.
3422
				'href' => wfExpandUrl( wfAppendQuery(
3423
					wfScript( 'api' ),
3424
					[ 'action' => 'rsd' ] ),
3425
					PROTO_RELATIVE
3426
				),
3427
			] );
3428
		}
3429
3430
		# Language variants
3431
		if ( !$config->get( 'DisableLangConversion' ) ) {
3432
			$lang = $this->getTitle()->getPageLanguage();
3433
			if ( $lang->hasVariants() ) {
3434
				$variants = $lang->getVariants();
3435
				foreach ( $variants as $variant ) {
3436
					$tags["variant-$variant"] = Html::element( 'link', [
3437
						'rel' => 'alternate',
3438
						'hreflang' => wfBCP47( $variant ),
3439
						'href' => $this->getTitle()->getLocalURL(
3440
							[ 'variant' => $variant ] )
3441
						]
3442
					);
3443
				}
3444
				# x-default link per https://support.google.com/webmasters/answer/189077?hl=en
3445
				$tags["variant-x-default"] = Html::element( 'link', [
3446
					'rel' => 'alternate',
3447
					'hreflang' => 'x-default',
3448
					'href' => $this->getTitle()->getLocalURL() ] );
3449
			}
3450
		}
3451
3452
		# Copyright
3453
		if ( $this->copyrightUrl !== null ) {
3454
			$copyright = $this->copyrightUrl;
3455
		} else {
3456
			$copyright = '';
3457
			if ( $config->get( 'RightsPage' ) ) {
3458
				$copy = Title::newFromText( $config->get( 'RightsPage' ) );
3459
3460
				if ( $copy ) {
3461
					$copyright = $copy->getLocalURL();
3462
				}
3463
			}
3464
3465
			if ( !$copyright && $config->get( 'RightsUrl' ) ) {
3466
				$copyright = $config->get( 'RightsUrl' );
3467
			}
3468
		}
3469
3470
		if ( $copyright ) {
3471
			$tags['copyright'] = Html::element( 'link', [
3472
				'rel' => 'copyright',
3473
				'href' => $copyright ]
3474
			);
3475
		}
3476
3477
		# Feeds
3478
		if ( $config->get( 'Feed' ) ) {
3479
			$feedLinks = [];
3480
3481
			foreach ( $this->getSyndicationLinks() as $format => $link ) {
3482
				# Use the page name for the title.  In principle, this could
3483
				# lead to issues with having the same name for different feeds
3484
				# corresponding to the same page, but we can't avoid that at
3485
				# this low a level.
3486
3487
				$feedLinks[] = $this->feedLink(
3488
					$format,
3489
					$link,
3490
					# Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
3491
					$this->msg(
3492
						"page-{$format}-feed", $this->getTitle()->getPrefixedText()
3493
					)->text()
3494
				);
3495
			}
3496
3497
			# Recent changes feed should appear on every page (except recentchanges,
3498
			# that would be redundant). Put it after the per-page feed to avoid
3499
			# changing existing behavior. It's still available, probably via a
3500
			# menu in your browser. Some sites might have a different feed they'd
3501
			# like to promote instead of the RC feed (maybe like a "Recent New Articles"
3502
			# or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
3503
			# If so, use it instead.
3504
			$sitename = $config->get( 'Sitename' );
3505
			if ( $config->get( 'OverrideSiteFeed' ) ) {
3506
				foreach ( $config->get( 'OverrideSiteFeed' ) as $type => $feedUrl ) {
3507
					// Note, this->feedLink escapes the url.
3508
					$feedLinks[] = $this->feedLink(
3509
						$type,
3510
						$feedUrl,
3511
						$this->msg( "site-{$type}-feed", $sitename )->text()
3512
					);
3513
				}
3514
			} elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
3515
				$rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
3516
				foreach ( $config->get( 'AdvertisedFeedTypes' ) as $format ) {
3517
					$feedLinks[] = $this->feedLink(
3518
						$format,
3519
						$rctitle->getLocalURL( [ 'feed' => $format ] ),
3520
						# For grep: 'site-rss-feed', 'site-atom-feed'
3521
						$this->msg( "site-{$format}-feed", $sitename )->text()
3522
					);
3523
				}
3524
			}
3525
3526
			# Allow extensions to change the list pf feeds. This hook is primarily for changing,
3527
			# manipulating or removing existing feed tags. If you want to add new feeds, you should
3528
			# use OutputPage::addFeedLink() instead.
3529
			Hooks::run( 'AfterBuildFeedLinks', [ &$feedLinks ] );
3530
3531
			$tags += $feedLinks;
3532
		}
3533
3534
		# Canonical URL
3535
		if ( $config->get( 'EnableCanonicalServerLink' ) ) {
3536
			if ( $canonicalUrl !== false ) {
3537
				$canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL );
3538
			} else {
3539
				if ( $this->isArticleRelated() ) {
3540
					// This affects all requests where "setArticleRelated" is true. This is
3541
					// typically all requests that show content (query title, curid, oldid, diff),
3542
					// and all wikipage actions (edit, delete, purge, info, history etc.).
3543
					// It does not apply to File pages and Special pages.
3544
					// 'history' and 'info' actions address page metadata rather than the page
3545
					// content itself, so they may not be canonicalized to the view page url.
3546
					// TODO: this ought to be better encapsulated in the Action class.
3547
					$action = Action::getActionName( $this->getContext() );
3548
					if ( in_array( $action, [ 'history', 'info' ] ) ) {
3549
						$query = "action={$action}";
3550
					} else {
3551
						$query = '';
3552
					}
3553
					$canonicalUrl = $this->getTitle()->getCanonicalURL( $query );
3554
				} else {
3555
					$reqUrl = $this->getRequest()->getRequestURL();
3556
					$canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL );
3557
				}
3558
			}
3559
		}
3560
		if ( $canonicalUrl !== false ) {
3561
			$tags[] = Html::element( 'link', [
3562
				'rel' => 'canonical',
3563
				'href' => $canonicalUrl
3564
			] );
3565
		}
3566
3567
		return $tags;
3568
	}
3569
3570
	/**
3571
	 * @return string HTML tag links to be put in the header.
3572
	 * @deprecated since 1.24 Use OutputPage::headElement or if you have to,
3573
	 *   OutputPage::getHeadLinksArray directly.
3574
	 */
3575
	public function getHeadLinks() {
3576
		wfDeprecated( __METHOD__, '1.24' );
3577
		return implode( "\n", $this->getHeadLinksArray() );
3578
	}
3579
3580
	/**
3581
	 * Generate a "<link rel/>" for a feed.
3582
	 *
3583
	 * @param string $type Feed type
3584
	 * @param string $url URL to the feed
3585
	 * @param string $text Value of the "title" attribute
3586
	 * @return string HTML fragment
3587
	 */
3588
	private function feedLink( $type, $url, $text ) {
3589
		return Html::element( 'link', [
3590
			'rel' => 'alternate',
3591
			'type' => "application/$type+xml",
3592
			'title' => $text,
3593
			'href' => $url ]
3594
		);
3595
	}
3596
3597
	/**
3598
	 * Add a local or specified stylesheet, with the given media options.
3599
	 * Internal use only. Use OutputPage::addModuleStyles() if possible.
3600
	 *
3601
	 * @param string $style URL to the file
3602
	 * @param string $media To specify a media type, 'screen', 'printable', 'handheld' or any.
3603
	 * @param string $condition For IE conditional comments, specifying an IE version
3604
	 * @param string $dir Set to 'rtl' or 'ltr' for direction-specific sheets
3605
	 */
3606
	public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
3607
		$options = [];
3608
		if ( $media ) {
3609
			$options['media'] = $media;
3610
		}
3611
		if ( $condition ) {
3612
			$options['condition'] = $condition;
3613
		}
3614
		if ( $dir ) {
3615
			$options['dir'] = $dir;
3616
		}
3617
		$this->styles[$style] = $options;
3618
	}
3619
3620
	/**
3621
	 * Adds inline CSS styles
3622
	 * Internal use only. Use OutputPage::addModuleStyles() if possible.
3623
	 *
3624
	 * @param mixed $style_css Inline CSS
3625
	 * @param string $flip Set to 'flip' to flip the CSS if needed
3626
	 */
3627
	public function addInlineStyle( $style_css, $flip = 'noflip' ) {
3628
		if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
3629
			# If wanted, and the interface is right-to-left, flip the CSS
3630
			$style_css = CSSJanus::transform( $style_css, true, false );
3631
		}
3632
		$this->mInlineStyles .= Html::inlineStyle( $style_css );
3633
	}
3634
3635
	/**
3636
	 * Build a set of "<link>" elements for the stylesheets specified in the $this->styles array.
3637
	 * These will be applied to various media & IE conditionals.
3638
	 *
3639
	 * @return string
3640
	 */
3641
	public function buildCssLinks() {
3642
		global $wgContLang;
3643
3644
		$this->getSkin()->setupSkinUserCss( $this );
3645
3646
		// Add ResourceLoader styles
3647
		// Split the styles into these groups
3648
		$styles = [
3649
			'other' => [],
3650
			'user' => [],
3651
			'site' => [],
3652
			'private' => [],
3653
			'noscript' => []
3654
		];
3655
		$links = [];
3656
		$otherTags = []; // Tags to append after the normal <link> tags
3657
		$resourceLoader = $this->getResourceLoader();
3658
3659
		$moduleStyles = $this->getModuleStyles();
3660
3661
		// Per-site custom styles
3662
		$moduleStyles[] = 'site.styles';
3663
		$moduleStyles[] = 'noscript';
3664
3665
		// Per-user custom styles
3666
		if ( $this->getConfig()->get( 'AllowUserCss' ) && $this->getTitle()->isCssSubpage()
3667
			&& $this->userCanPreview()
3668
		) {
3669
			// We're on a preview of a CSS subpage
3670
			// Exclude this page from the user module in case it's in there (bug 26283)
3671
			$link = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES,
3672
				[ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
3673
			);
3674
			$otherTags = array_merge( $otherTags, $link['html'] );
3675
3676
			// Load the previewed CSS
3677
			// If needed, Janus it first. This is user-supplied CSS, so it's
3678
			// assumed to be right for the content language directionality.
3679
			$previewedCSS = $this->getRequest()->getText( 'wpTextbox1' );
3680
			if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) {
3681
				$previewedCSS = CSSJanus::transform( $previewedCSS, true, false );
3682
			}
3683
			$otherTags[] = Html::inlineStyle( $previewedCSS );
3684
		} else {
3685
			// Load the user styles normally
3686
			$moduleStyles[] = 'user';
3687
		}
3688
3689
		// Per-user preference styles
3690
		$moduleStyles[] = 'user.cssprefs';
3691
3692
		foreach ( $moduleStyles as $name ) {
3693
			$module = $resourceLoader->getModule( $name );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $module is correct as $resourceLoader->getModule($name) (which targets ResourceLoader::getModule()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3694
			if ( !$module ) {
3695
				continue;
3696
			}
3697
			if ( $name === 'site' ) {
3698
				// HACK: The site module shouldn't be fragmented with a cache group and
3699
				// http request. But in order to ensure its styles are separated and after the
3700
				// ResourceLoaderDynamicStyles marker, pretend it is in a group called 'site'.
3701
				// The scripts remain ungrouped and rides the bottom queue.
3702
				$styles['site'][] = $name;
3703
				continue;
3704
			}
3705
			$group = $module->getGroup();
3706
			// Modules in groups other than the ones needing special treatment
3707
			// (see $styles assignment)
3708
			// will be placed in the "other" style category.
3709
			$styles[isset( $styles[$group] ) ? $group : 'other'][] = $name;
3710
		}
3711
3712
		// We want site, private and user styles to override dynamically added
3713
		// styles from modules, but we want dynamically added styles to override
3714
		// statically added styles from other modules. So the order has to be
3715
		// other, dynamic, site, private, user. Add statically added styles for
3716
		// other modules
3717
		$links[] = $this->makeResourceLoaderLink(
3718
			$styles['other'],
3719
			ResourceLoaderModule::TYPE_STYLES
3720
		);
3721
		// Add normal styles added through addStyle()/addInlineStyle() here
3722
		$links[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3723
		// Add marker tag to mark the place where the client-side
3724
		// loader should inject dynamic styles
3725
		// We use a <meta> tag with a made-up name for this because that's valid HTML
3726
		$links[] = Html::element(
3727
			'meta',
3728
			[ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
3729
		);
3730
3731
		// Add site-specific and user-specific styles
3732
		// 'private' at present only contains user.options, so put that before 'user'
3733
		// Any future private modules will likely have a similar user-specific character
3734
		foreach ( [ 'site', 'noscript', 'private', 'user' ] as $group ) {
3735
			$links[] = $this->makeResourceLoaderLink( $styles[$group],
3736
				ResourceLoaderModule::TYPE_STYLES
3737
			);
3738
		}
3739
3740
		// Add stuff in $otherTags (previewed user CSS if applicable)
3741
		return self::getHtmlFromLoaderLinks( $links ) . implode( '', $otherTags );
3742
	}
3743
3744
	/**
3745
	 * @return array
3746
	 */
3747
	public function buildCssLinksArray() {
3748
		$links = [];
3749
3750
		// Add any extension CSS
3751
		foreach ( $this->mExtStyles as $url ) {
3752
			$this->addStyle( $url );
3753
		}
3754
		$this->mExtStyles = [];
3755
3756
		foreach ( $this->styles as $file => $options ) {
3757
			$link = $this->styleLink( $file, $options );
3758
			if ( $link ) {
3759
				$links[$file] = $link;
3760
			}
3761
		}
3762
		return $links;
3763
	}
3764
3765
	/**
3766
	 * Generate \<link\> tags for stylesheets
3767
	 *
3768
	 * @param string $style URL to the file
3769
	 * @param array $options Option, can contain 'condition', 'dir', 'media' keys
3770
	 * @return string HTML fragment
3771
	 */
3772
	protected function styleLink( $style, array $options ) {
3773
		if ( isset( $options['dir'] ) ) {
3774
			if ( $this->getLanguage()->getDir() != $options['dir'] ) {
3775
				return '';
3776
			}
3777
		}
3778
3779
		if ( isset( $options['media'] ) ) {
3780
			$media = self::transformCssMedia( $options['media'] );
3781
			if ( is_null( $media ) ) {
3782
				return '';
3783
			}
3784
		} else {
3785
			$media = 'all';
3786
		}
3787
3788
		if ( substr( $style, 0, 1 ) == '/' ||
3789
			substr( $style, 0, 5 ) == 'http:' ||
3790
			substr( $style, 0, 6 ) == 'https:' ) {
3791
			$url = $style;
3792
		} else {
3793
			$config = $this->getConfig();
3794
			$url = $config->get( 'StylePath' ) . '/' . $style . '?' .
3795
				$config->get( 'StyleVersion' );
3796
		}
3797
3798
		$link = Html::linkedStyle( $url, $media );
3799
3800
		if ( isset( $options['condition'] ) ) {
3801
			$condition = htmlspecialchars( $options['condition'] );
3802
			$link = "<!--[if $condition]>$link<![endif]-->";
3803
		}
3804
		return $link;
3805
	}
3806
3807
	/**
3808
	 * Transform path to web-accessible static resource.
3809
	 *
3810
	 * This is used to add a validation hash as query string.
3811
	 * This aids various behaviors:
3812
	 *
3813
	 * - Put long Cache-Control max-age headers on responses for improved
3814
	 *   cache performance.
3815
	 * - Get the correct version of a file as expected by the current page.
3816
	 * - Instantly get the updated version of a file after deployment.
3817
	 *
3818
	 * Avoid using this for urls included in HTML as otherwise clients may get different
3819
	 * versions of a resource when navigating the site depending on when the page was cached.
3820
	 * If changes to the url propagate, this is not a problem (e.g. if the url is in
3821
	 * an external stylesheet).
3822
	 *
3823
	 * @since 1.27
3824
	 * @param Config $config
3825
	 * @param string $path Path-absolute URL to file (from document root, must start with "/")
3826
	 * @return string URL
3827
	 */
3828
	public static function transformResourcePath( Config $config, $path ) {
3829
		global $IP;
3830
		$remotePathPrefix = $config->get( 'ResourceBasePath' );
3831
		if ( $remotePathPrefix === '' ) {
3832
			// The configured base path is required to be empty string for
3833
			// wikis in the domain root
3834
			$remotePath = '/';
3835
		} else {
3836
			$remotePath = $remotePathPrefix;
3837
		}
3838
		if ( strpos( $path, $remotePath ) !== 0 ) {
3839
			// Path is outside wgResourceBasePath, ignore.
3840
			return $path;
3841
		}
3842
		$path = RelPath\getRelativePath( $path, $remotePath );
3843
		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 3842 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...
3844
	}
3845
3846
	/**
3847
	 * Utility method for transformResourceFilePath().
3848
	 *
3849
	 * Caller is responsible for ensuring the file exists. Emits a PHP warning otherwise.
3850
	 *
3851
	 * @since 1.27
3852
	 * @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...
3853
	 * @param string $localPath File directory exposed at $remotePath
3854
	 * @param string $file Path to target file relative to $localPath
3855
	 * @return string URL
3856
	 */
3857
	public static function transformFilePath( $remotePathPrefix, $localPath, $file ) {
3858
		$hash = md5_file( "$localPath/$file" );
3859
		if ( $hash === false ) {
3860
			wfLogWarning( __METHOD__ . ": Failed to hash $localPath/$file" );
3861
			$hash = '';
3862
		}
3863
		return "$remotePathPrefix/$file?" . substr( $hash, 0, 5 );
3864
	}
3865
3866
	/**
3867
	 * Transform "media" attribute based on request parameters
3868
	 *
3869
	 * @param string $media Current value of the "media" attribute
3870
	 * @return string Modified value of the "media" attribute, or null to skip
3871
	 * this stylesheet
3872
	 */
3873
	public static function transformCssMedia( $media ) {
3874
		global $wgRequest;
3875
3876
		// http://www.w3.org/TR/css3-mediaqueries/#syntax
3877
		$screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
3878
3879
		// Switch in on-screen display for media testing
3880
		$switches = [
3881
			'printable' => 'print',
3882
			'handheld' => 'handheld',
3883
		];
3884
		foreach ( $switches as $switch => $targetMedia ) {
3885
			if ( $wgRequest->getBool( $switch ) ) {
3886
				if ( $media == $targetMedia ) {
3887
					$media = '';
3888
				} elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
3889
					/* This regex will not attempt to understand a comma-separated media_query_list
3890
					 *
3891
					 * Example supported values for $media:
3892
					 * 'screen', 'only screen', 'screen and (min-width: 982px)' ),
3893
					 * Example NOT supported value for $media:
3894
					 * '3d-glasses, screen, print and resolution > 90dpi'
3895
					 *
3896
					 * If it's a print request, we never want any kind of screen stylesheets
3897
					 * If it's a handheld request (currently the only other choice with a switch),
3898
					 * we don't want simple 'screen' but we might want screen queries that
3899
					 * have a max-width or something, so we'll pass all others on and let the
3900
					 * client do the query.
3901
					 */
3902
					if ( $targetMedia == 'print' || $media == 'screen' ) {
3903
						return null;
3904
					}
3905
				}
3906
			}
3907
		}
3908
3909
		return $media;
3910
	}
3911
3912
	/**
3913
	 * Add a wikitext-formatted message to the output.
3914
	 * This is equivalent to:
3915
	 *
3916
	 *    $wgOut->addWikiText( wfMessage( ... )->plain() )
3917
	 */
3918
	public function addWikiMsg( /*...*/ ) {
3919
		$args = func_get_args();
3920
		$name = array_shift( $args );
3921
		$this->addWikiMsgArray( $name, $args );
3922
	}
3923
3924
	/**
3925
	 * Add a wikitext-formatted message to the output.
3926
	 * Like addWikiMsg() except the parameters are taken as an array
3927
	 * instead of a variable argument list.
3928
	 *
3929
	 * @param string $name
3930
	 * @param array $args
3931
	 */
3932
	public function addWikiMsgArray( $name, $args ) {
3933
		$this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
3934
	}
3935
3936
	/**
3937
	 * This function takes a number of message/argument specifications, wraps them in
3938
	 * some overall structure, and then parses the result and adds it to the output.
3939
	 *
3940
	 * In the $wrap, $1 is replaced with the first message, $2 with the second,
3941
	 * and so on. The subsequent arguments may be either
3942
	 * 1) strings, in which case they are message names, or
3943
	 * 2) arrays, in which case, within each array, the first element is the message
3944
	 *    name, and subsequent elements are the parameters to that message.
3945
	 *
3946
	 * Don't use this for messages that are not in the user's interface language.
3947
	 *
3948
	 * For example:
3949
	 *
3950
	 *    $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>", 'some-error' );
3951
	 *
3952
	 * Is equivalent to:
3953
	 *
3954
	 *    $wgOut->addWikiText( "<div class='error'>\n"
3955
	 *        . wfMessage( 'some-error' )->plain() . "\n</div>" );
3956
	 *
3957
	 * The newline after the opening div is needed in some wikitext. See bug 19226.
3958
	 *
3959
	 * @param string $wrap
3960
	 */
3961
	public function wrapWikiMsg( $wrap /*, ...*/ ) {
3962
		$msgSpecs = func_get_args();
3963
		array_shift( $msgSpecs );
3964
		$msgSpecs = array_values( $msgSpecs );
3965
		$s = $wrap;
3966
		foreach ( $msgSpecs as $n => $spec ) {
3967
			if ( is_array( $spec ) ) {
3968
				$args = $spec;
3969
				$name = array_shift( $args );
3970
				if ( isset( $args['options'] ) ) {
3971
					unset( $args['options'] );
3972
					wfDeprecated(
3973
						'Adding "options" to ' . __METHOD__ . ' is no longer supported',
3974
						'1.20'
3975
					);
3976
				}
3977
			} else {
3978
				$args = [];
3979
				$name = $spec;
3980
			}
3981
			$s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
3982
		}
3983
		$this->addWikiText( $s );
3984
	}
3985
3986
	/**
3987
	 * Enables/disables TOC, doesn't override __NOTOC__
3988
	 * @param bool $flag
3989
	 * @since 1.22
3990
	 */
3991
	public function enableTOC( $flag = true ) {
3992
		$this->mEnableTOC = $flag;
3993
	}
3994
3995
	/**
3996
	 * @return bool
3997
	 * @since 1.22
3998
	 */
3999
	public function isTOCEnabled() {
4000
		return $this->mEnableTOC;
4001
	}
4002
4003
	/**
4004
	 * Enables/disables section edit links, doesn't override __NOEDITSECTION__
4005
	 * @param bool $flag
4006
	 * @since 1.23
4007
	 */
4008
	public function enableSectionEditLinks( $flag = true ) {
4009
		$this->mEnableSectionEditLinks = $flag;
4010
	}
4011
4012
	/**
4013
	 * @return bool
4014
	 * @since 1.23
4015
	 */
4016
	public function sectionEditLinksEnabled() {
4017
		return $this->mEnableSectionEditLinks;
4018
	}
4019
4020
	/**
4021
	 * Helper function to setup the PHP implementation of OOUI to use in this request.
4022
	 *
4023
	 * @since 1.26
4024
	 * @param String $skinName The Skin name to determine the correct OOUI theme
4025
	 * @param String $dir Language direction
4026
	 */
4027
	public static function setupOOUI( $skinName = '', $dir = 'ltr' ) {
4028
		$themes = ExtensionRegistry::getInstance()->getAttribute( 'SkinOOUIThemes' );
4029
		// Make keys (skin names) lowercase for case-insensitive matching.
4030
		$themes = array_change_key_case( $themes, CASE_LOWER );
4031
		$theme = isset( $themes[$skinName] ) ? $themes[$skinName] : 'MediaWiki';
4032
		// For example, 'OOUI\MediaWikiTheme'.
4033
		$themeClass = "OOUI\\{$theme}Theme";
4034
		OOUI\Theme::setSingleton( new $themeClass() );
4035
		OOUI\Element::setDefaultDir( $dir );
4036
	}
4037
4038
	/**
4039
	 * Add ResourceLoader module styles for OOUI and set up the PHP implementation of it for use with
4040
	 * MediaWiki and this OutputPage instance.
4041
	 *
4042
	 * @since 1.25
4043
	 */
4044
	public function enableOOUI() {
4045
		self::setupOOUI(
4046
			strtolower( $this->getSkin()->getSkinName() ),
4047
			$this->getLanguage()->getDir()
4048
		);
4049
		$this->addModuleStyles( [
4050
			'oojs-ui-core.styles',
4051
			'oojs-ui.styles.icons',
4052
			'oojs-ui.styles.indicators',
4053
			'oojs-ui.styles.textures',
4054
			'mediawiki.widgets.styles',
4055
		] );
4056
		// Used by 'skipFunction' of the four 'oojs-ui.styles.*' modules. Please don't treat this as a
4057
		// public API or you'll be severely disappointed when T87871 is fixed and it disappears.
4058
		$this->addMeta( 'X-OOUI-PHP', '1' );
4059
	}
4060
}
4061