Completed
Branch master (227f0c)
by
unknown
30:54
created

OutputPage::getTemplateIds()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
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 = [ 'page_id', 'page_namespace', 'page_title', 'page_len',
1281
			'page_is_redirect', 'page_latest', 'pp_value' ];
1282
1283
		if ( $this->getConfig()->get( 'ContentHandlerUseDB' ) ) {
1284
			$fields[] = 'page_content_model';
1285
		}
1286
		if ( $this->getConfig()->get( 'PageLanguageUseDB' ) ) {
1287
			$fields[] = 'page_lang';
1288
		}
1289
1290
		$res = $dbr->select( [ 'page', 'page_props' ],
1291
			$fields,
1292
			$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...
1293
			__METHOD__,
1294
			[],
1295
			[ 'page_props' => [ 'LEFT JOIN', [
1296
				'pp_propname' => 'hiddencat',
1297
				'pp_page = page_id'
1298
			] ] ]
1299
		);
1300
1301
		# Add the results to the link cache
1302
		$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 1290 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...
1303
1304
		# Set all the values to 'normal'.
1305
		$categories = array_fill_keys( array_keys( $categories ), 'normal' );
1306
1307
		# Mark hidden categories
1308
		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...
1309
			if ( isset( $row->pp_value ) ) {
1310
				$categories[$row->page_title] = 'hidden';
1311
			}
1312
		}
1313
1314
		# Add the remaining categories to the skin
1315
		if ( Hooks::run(
1316
			'OutputPageMakeCategoryLinks',
1317
			[ &$this, $categories, &$this->mCategoryLinks ] )
1318
		) {
1319
			foreach ( $categories as $category => $type ) {
1320
				// array keys will cast numeric category names to ints, so cast back to string
1321
				$category = (string)$category;
1322
				$origcategory = $category;
1323
				$title = Title::makeTitleSafe( NS_CATEGORY, $category );
1324
				if ( !$title ) {
1325
					continue;
1326
				}
1327
				$wgContLang->findVariantLink( $category, $title, true );
1328
				if ( $category != $origcategory && array_key_exists( $category, $categories ) ) {
1329
					continue;
1330
				}
1331
				$text = $wgContLang->convertHtml( $title->getText() );
1332
				$this->mCategories[] = $title->getText();
1333
				$this->mCategoryLinks[$type][] = Linker::link( $title, $text );
1334
			}
1335
		}
1336
	}
1337
1338
	/**
1339
	 * Reset the category links (but not the category list) and add $categories
1340
	 *
1341
	 * @param array $categories Mapping category name => sort key
1342
	 */
1343
	public function setCategoryLinks( array $categories ) {
1344
		$this->mCategoryLinks = [];
1345
		$this->addCategoryLinks( $categories );
1346
	}
1347
1348
	/**
1349
	 * Get the list of category links, in a 2-D array with the following format:
1350
	 * $arr[$type][] = $link, where $type is either "normal" or "hidden" (for
1351
	 * hidden categories) and $link a HTML fragment with a link to the category
1352
	 * page
1353
	 *
1354
	 * @return array
1355
	 */
1356
	public function getCategoryLinks() {
1357
		return $this->mCategoryLinks;
1358
	}
1359
1360
	/**
1361
	 * Get the list of category names this page belongs to
1362
	 *
1363
	 * @return array Array of strings
1364
	 */
1365
	public function getCategories() {
1366
		return $this->mCategories;
1367
	}
1368
1369
	/**
1370
	 * Add an array of indicators, with their identifiers as array
1371
	 * keys and HTML contents as values.
1372
	 *
1373
	 * In case of duplicate keys, existing values are overwritten.
1374
	 *
1375
	 * @param array $indicators
1376
	 * @since 1.25
1377
	 */
1378
	public function setIndicators( array $indicators ) {
1379
		$this->mIndicators = $indicators + $this->mIndicators;
1380
		// Keep ordered by key
1381
		ksort( $this->mIndicators );
1382
	}
1383
1384
	/**
1385
	 * Get the indicators associated with this page.
1386
	 *
1387
	 * The array will be internally ordered by item keys.
1388
	 *
1389
	 * @return array Keys: identifiers, values: HTML contents
1390
	 * @since 1.25
1391
	 */
1392
	public function getIndicators() {
1393
		return $this->mIndicators;
1394
	}
1395
1396
	/**
1397
	 * Adds help link with an icon via page indicators.
1398
	 * Link target can be overridden by a local message containing a wikilink:
1399
	 * the message key is: lowercase action or special page name + '-helppage'.
1400
	 * @param string $to Target MediaWiki.org page title or encoded URL.
1401
	 * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
1402
	 * @since 1.25
1403
	 */
1404
	public function addHelpLink( $to, $overrideBaseUrl = false ) {
1405
		$this->addModuleStyles( 'mediawiki.helplink' );
1406
		$text = $this->msg( 'helppage-top-gethelp' )->escaped();
1407
1408
		if ( $overrideBaseUrl ) {
1409
			$helpUrl = $to;
1410
		} else {
1411
			$toUrlencoded = wfUrlencode( str_replace( ' ', '_', $to ) );
1412
			$helpUrl = "//www.mediawiki.org/wiki/Special:MyLanguage/$toUrlencoded";
1413
		}
1414
1415
		$link = Html::rawElement(
1416
			'a',
1417
			[
1418
				'href' => $helpUrl,
1419
				'target' => '_blank',
1420
				'class' => 'mw-helplink',
1421
			],
1422
			$text
1423
		);
1424
1425
		$this->setIndicators( [ 'mw-helplink' => $link ] );
1426
	}
1427
1428
	/**
1429
	 * Do not allow scripts which can be modified by wiki users to load on this page;
1430
	 * only allow scripts bundled with, or generated by, the software.
1431
	 * Site-wide styles are controlled by a config setting, since they can be
1432
	 * used to create a custom skin/theme, but not user-specific ones.
1433
	 *
1434
	 * @todo this should be given a more accurate name
1435
	 */
1436
	public function disallowUserJs() {
1437
		$this->reduceAllowedModules(
1438
			ResourceLoaderModule::TYPE_SCRIPTS,
1439
			ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
1440
		);
1441
1442
		// Site-wide styles are controlled by a config setting, see bug 71621
1443
		// for background on why. User styles are never allowed.
1444
		if ( $this->getConfig()->get( 'AllowSiteCSSOnRestrictedPages' ) ) {
1445
			$styleOrigin = ResourceLoaderModule::ORIGIN_USER_SITEWIDE;
1446
		} else {
1447
			$styleOrigin = ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL;
1448
		}
1449
		$this->reduceAllowedModules(
1450
			ResourceLoaderModule::TYPE_STYLES,
1451
			$styleOrigin
1452
		);
1453
	}
1454
1455
	/**
1456
	 * Show what level of JavaScript / CSS untrustworthiness is allowed on this page
1457
	 * @see ResourceLoaderModule::$origin
1458
	 * @param string $type ResourceLoaderModule TYPE_ constant
1459
	 * @return int ResourceLoaderModule ORIGIN_ class constant
1460
	 */
1461
	public function getAllowedModules( $type ) {
1462
		if ( $type == ResourceLoaderModule::TYPE_COMBINED ) {
1463
			return min( array_values( $this->mAllowedModules ) );
1464
		} else {
1465
			return isset( $this->mAllowedModules[$type] )
1466
				? $this->mAllowedModules[$type]
1467
				: ResourceLoaderModule::ORIGIN_ALL;
1468
		}
1469
	}
1470
1471
	/**
1472
	 * Limit the highest level of CSS/JS untrustworthiness allowed.
1473
	 *
1474
	 * If passed the same or a higher level than the current level of untrustworthiness set, the
1475
	 * level will remain unchanged.
1476
	 *
1477
	 * @param string $type
1478
	 * @param int $level ResourceLoaderModule class constant
1479
	 */
1480
	public function reduceAllowedModules( $type, $level ) {
1481
		$this->mAllowedModules[$type] = min( $this->getAllowedModules( $type ), $level );
1482
	}
1483
1484
	/**
1485
	 * Prepend $text to the body HTML
1486
	 *
1487
	 * @param string $text HTML
1488
	 */
1489
	public function prependHTML( $text ) {
1490
		$this->mBodytext = $text . $this->mBodytext;
1491
	}
1492
1493
	/**
1494
	 * Append $text to the body HTML
1495
	 *
1496
	 * @param string $text HTML
1497
	 */
1498
	public function addHTML( $text ) {
1499
		$this->mBodytext .= $text;
1500
	}
1501
1502
	/**
1503
	 * Shortcut for adding an Html::element via addHTML.
1504
	 *
1505
	 * @since 1.19
1506
	 *
1507
	 * @param string $element
1508
	 * @param array $attribs
1509
	 * @param string $contents
1510
	 */
1511
	public function addElement( $element, array $attribs = [], $contents = '' ) {
1512
		$this->addHTML( Html::element( $element, $attribs, $contents ) );
1513
	}
1514
1515
	/**
1516
	 * Clear the body HTML
1517
	 */
1518
	public function clearHTML() {
1519
		$this->mBodytext = '';
1520
	}
1521
1522
	/**
1523
	 * Get the body HTML
1524
	 *
1525
	 * @return string HTML
1526
	 */
1527
	public function getHTML() {
1528
		return $this->mBodytext;
1529
	}
1530
1531
	/**
1532
	 * Get/set the ParserOptions object to use for wikitext parsing
1533
	 *
1534
	 * @param ParserOptions|null $options Either the ParserOption to use or null to only get the
1535
	 *   current ParserOption object
1536
	 * @return ParserOptions
1537
	 */
1538
	public function parserOptions( $options = null ) {
1539
		if ( $options !== null && !empty( $options->isBogus ) ) {
1540
			// Someone is trying to set a bogus pre-$wgUser PO. Check if it has
1541
			// been changed somehow, and keep it if so.
1542
			$anonPO = ParserOptions::newFromAnon();
1543
			$anonPO->setEditSection( false );
1544
			if ( !$options->matches( $anonPO ) ) {
1545
				wfLogWarning( __METHOD__ . ': Setting a changed bogus ParserOptions: ' . wfGetAllCallers( 5 ) );
1546
				$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...
1547
			}
1548
		}
1549
1550
		if ( !$this->mParserOptions ) {
1551
			if ( !$this->getContext()->getUser()->isSafeToLoad() ) {
1552
				// $wgUser isn't unstubbable yet, so don't try to get a
1553
				// ParserOptions for it. And don't cache this ParserOptions
1554
				// either.
1555
				$po = ParserOptions::newFromAnon();
1556
				$po->setEditSection( false );
1557
				$po->isBogus = true;
1558
				if ( $options !== null ) {
1559
					$this->mParserOptions = empty( $options->isBogus ) ? $options : null;
1560
				}
1561
				return $po;
1562
			}
1563
1564
			$this->mParserOptions = ParserOptions::newFromContext( $this->getContext() );
1565
			$this->mParserOptions->setEditSection( false );
1566
		}
1567
1568
		if ( $options !== null && !empty( $options->isBogus ) ) {
1569
			// They're trying to restore the bogus pre-$wgUser PO. Do the right
1570
			// thing.
1571
			return wfSetVar( $this->mParserOptions, null, true );
1572
		} else {
1573
			return wfSetVar( $this->mParserOptions, $options );
1574
		}
1575
	}
1576
1577
	/**
1578
	 * Set the revision ID which will be seen by the wiki text parser
1579
	 * for things such as embedded {{REVISIONID}} variable use.
1580
	 *
1581
	 * @param int|null $revid An positive integer, or null
1582
	 * @return mixed Previous value
1583
	 */
1584
	public function setRevisionId( $revid ) {
1585
		$val = is_null( $revid ) ? null : intval( $revid );
1586
		return wfSetVar( $this->mRevisionId, $val );
1587
	}
1588
1589
	/**
1590
	 * Get the displayed revision ID
1591
	 *
1592
	 * @return int
1593
	 */
1594
	public function getRevisionId() {
1595
		return $this->mRevisionId;
1596
	}
1597
1598
	/**
1599
	 * Set the timestamp of the revision which will be displayed. This is used
1600
	 * to avoid a extra DB call in Skin::lastModified().
1601
	 *
1602
	 * @param string|null $timestamp
1603
	 * @return mixed Previous value
1604
	 */
1605
	public function setRevisionTimestamp( $timestamp ) {
1606
		return wfSetVar( $this->mRevisionTimestamp, $timestamp );
1607
	}
1608
1609
	/**
1610
	 * Get the timestamp of displayed revision.
1611
	 * This will be null if not filled by setRevisionTimestamp().
1612
	 *
1613
	 * @return string|null
1614
	 */
1615
	public function getRevisionTimestamp() {
1616
		return $this->mRevisionTimestamp;
1617
	}
1618
1619
	/**
1620
	 * Set the displayed file version
1621
	 *
1622
	 * @param File|bool $file
1623
	 * @return mixed Previous value
1624
	 */
1625
	public function setFileVersion( $file ) {
1626
		$val = null;
1627
		if ( $file instanceof File && $file->exists() ) {
1628
			$val = [ 'time' => $file->getTimestamp(), 'sha1' => $file->getSha1() ];
1629
		}
1630
		return wfSetVar( $this->mFileVersion, $val, true );
1631
	}
1632
1633
	/**
1634
	 * Get the displayed file version
1635
	 *
1636
	 * @return array|null ('time' => MW timestamp, 'sha1' => sha1)
1637
	 */
1638
	public function getFileVersion() {
1639
		return $this->mFileVersion;
1640
	}
1641
1642
	/**
1643
	 * Get the templates used on this page
1644
	 *
1645
	 * @return array (namespace => dbKey => revId)
1646
	 * @since 1.18
1647
	 */
1648
	public function getTemplateIds() {
1649
		return $this->mTemplateIds;
1650
	}
1651
1652
	/**
1653
	 * Get the files used on this page
1654
	 *
1655
	 * @return array (dbKey => array('time' => MW timestamp or null, 'sha1' => sha1 or ''))
1656
	 * @since 1.18
1657
	 */
1658
	public function getFileSearchOptions() {
1659
		return $this->mImageTimeKeys;
1660
	}
1661
1662
	/**
1663
	 * Convert wikitext to HTML and add it to the buffer
1664
	 * Default assumes that the current page title will be used.
1665
	 *
1666
	 * @param string $text
1667
	 * @param bool $linestart Is this the start of a line?
1668
	 * @param bool $interface Is this text in the user interface language?
1669
	 * @throws MWException
1670
	 */
1671
	public function addWikiText( $text, $linestart = true, $interface = true ) {
1672
		$title = $this->getTitle(); // Work around E_STRICT
1673
		if ( !$title ) {
1674
			throw new MWException( 'Title is null' );
1675
		}
1676
		$this->addWikiTextTitle( $text, $title, $linestart, /*tidy*/false, $interface );
1677
	}
1678
1679
	/**
1680
	 * Add wikitext with a custom Title object
1681
	 *
1682
	 * @param string $text Wikitext
1683
	 * @param Title $title
1684
	 * @param bool $linestart Is this the start of a line?
1685
	 */
1686
	public function addWikiTextWithTitle( $text, &$title, $linestart = true ) {
1687
		$this->addWikiTextTitle( $text, $title, $linestart );
1688
	}
1689
1690
	/**
1691
	 * Add wikitext with a custom Title object and tidy enabled.
1692
	 *
1693
	 * @param string $text Wikitext
1694
	 * @param Title $title
1695
	 * @param bool $linestart Is this the start of a line?
1696
	 */
1697
	function addWikiTextTitleTidy( $text, &$title, $linestart = true ) {
1698
		$this->addWikiTextTitle( $text, $title, $linestart, true );
1699
	}
1700
1701
	/**
1702
	 * Add wikitext with tidy enabled
1703
	 *
1704
	 * @param string $text Wikitext
1705
	 * @param bool $linestart Is this the start of a line?
1706
	 */
1707
	public function addWikiTextTidy( $text, $linestart = true ) {
1708
		$title = $this->getTitle();
1709
		$this->addWikiTextTitleTidy( $text, $title, $linestart );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $this->getTitle() on line 1708 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...
1710
	}
1711
1712
	/**
1713
	 * Add wikitext with a custom Title object
1714
	 *
1715
	 * @param string $text Wikitext
1716
	 * @param Title $title
1717
	 * @param bool $linestart Is this the start of a line?
1718
	 * @param bool $tidy Whether to use tidy
1719
	 * @param bool $interface Whether it is an interface message
1720
	 *   (for example disables conversion)
1721
	 */
1722
	public function addWikiTextTitle( $text, Title $title, $linestart,
1723
		$tidy = false, $interface = false
1724
	) {
1725
		global $wgParser;
1726
1727
		$popts = $this->parserOptions();
1728
		$oldTidy = $popts->setTidy( $tidy );
1729
		$popts->setInterfaceMessage( (bool)$interface );
1730
1731
		$parserOutput = $wgParser->getFreshParser()->parse(
1732
			$text, $title, $popts,
1733
			$linestart, true, $this->mRevisionId
1734
		);
1735
1736
		$popts->setTidy( $oldTidy );
1737
1738
		$this->addParserOutput( $parserOutput );
1739
1740
	}
1741
1742
	/**
1743
	 * Add a ParserOutput object, but without Html.
1744
	 *
1745
	 * @deprecated since 1.24, use addParserOutputMetadata() instead.
1746
	 * @param ParserOutput $parserOutput
1747
	 */
1748
	public function addParserOutputNoText( $parserOutput ) {
1749
		wfDeprecated( __METHOD__, '1.24' );
1750
		$this->addParserOutputMetadata( $parserOutput );
1751
	}
1752
1753
	/**
1754
	 * Add all metadata associated with a ParserOutput object, but without the actual HTML. This
1755
	 * includes categories, language links, ResourceLoader modules, effects of certain magic words,
1756
	 * and so on.
1757
	 *
1758
	 * @since 1.24
1759
	 * @param ParserOutput $parserOutput
1760
	 */
1761
	public function addParserOutputMetadata( $parserOutput ) {
1762
		$this->mLanguageLinks += $parserOutput->getLanguageLinks();
1763
		$this->addCategoryLinks( $parserOutput->getCategories() );
1764
		$this->setIndicators( $parserOutput->getIndicators() );
1765
		$this->mNewSectionLink = $parserOutput->getNewSection();
1766
		$this->mHideNewSectionLink = $parserOutput->getHideNewSection();
1767
1768
		if ( !$parserOutput->isCacheable() ) {
1769
			$this->enableClientCache( false );
1770
		}
1771
		$this->mNoGallery = $parserOutput->getNoGallery();
1772
		$this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
1773
		$this->addModules( $parserOutput->getModules() );
1774
		$this->addModuleScripts( $parserOutput->getModuleScripts() );
1775
		$this->addModuleStyles( $parserOutput->getModuleStyles() );
1776
		$this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1777
		$this->mPreventClickjacking = $this->mPreventClickjacking
1778
			|| $parserOutput->preventClickjacking();
1779
1780
		// Template versioning...
1781
		foreach ( (array)$parserOutput->getTemplateIds() as $ns => $dbks ) {
1782
			if ( isset( $this->mTemplateIds[$ns] ) ) {
1783
				$this->mTemplateIds[$ns] = $dbks + $this->mTemplateIds[$ns];
1784
			} else {
1785
				$this->mTemplateIds[$ns] = $dbks;
1786
			}
1787
		}
1788
		// File versioning...
1789
		foreach ( (array)$parserOutput->getFileSearchOptions() as $dbk => $data ) {
1790
			$this->mImageTimeKeys[$dbk] = $data;
1791
		}
1792
1793
		// Hooks registered in the object
1794
		$parserOutputHooks = $this->getConfig()->get( 'ParserOutputHooks' );
1795
		foreach ( $parserOutput->getOutputHooks() as $hookInfo ) {
1796
			list( $hookName, $data ) = $hookInfo;
1797
			if ( isset( $parserOutputHooks[$hookName] ) ) {
1798
				call_user_func( $parserOutputHooks[$hookName], $this, $parserOutput, $data );
1799
			}
1800
		}
1801
1802
		// enable OOUI if requested via ParserOutput
1803
		if ( $parserOutput->getEnableOOUI() ) {
1804
			$this->enableOOUI();
1805
		}
1806
1807
		// Link flags are ignored for now, but may in the future be
1808
		// used to mark individual language links.
1809
		$linkFlags = [];
1810
		Hooks::run( 'LanguageLinks', [ $this->getTitle(), &$this->mLanguageLinks, &$linkFlags ] );
1811
		Hooks::run( 'OutputPageParserOutput', [ &$this, $parserOutput ] );
1812
	}
1813
1814
	/**
1815
	 * Add the HTML and enhancements for it (like ResourceLoader modules) associated with a
1816
	 * ParserOutput object, without any other metadata.
1817
	 *
1818
	 * @since 1.24
1819
	 * @param ParserOutput $parserOutput
1820
	 */
1821
	public function addParserOutputContent( $parserOutput ) {
1822
		$this->addParserOutputText( $parserOutput );
1823
1824
		$this->addModules( $parserOutput->getModules() );
1825
		$this->addModuleScripts( $parserOutput->getModuleScripts() );
1826
		$this->addModuleStyles( $parserOutput->getModuleStyles() );
1827
1828
		$this->addJsConfigVars( $parserOutput->getJsConfigVars() );
1829
	}
1830
1831
	/**
1832
	 * Add the HTML associated with a ParserOutput object, without any metadata.
1833
	 *
1834
	 * @since 1.24
1835
	 * @param ParserOutput $parserOutput
1836
	 */
1837
	public function addParserOutputText( $parserOutput ) {
1838
		$text = $parserOutput->getText();
1839
		Hooks::run( 'OutputPageBeforeHTML', [ &$this, &$text ] );
1840
		$this->addHTML( $text );
1841
	}
1842
1843
	/**
1844
	 * Add everything from a ParserOutput object.
1845
	 *
1846
	 * @param ParserOutput $parserOutput
1847
	 */
1848
	function addParserOutput( $parserOutput ) {
1849
		$this->addParserOutputMetadata( $parserOutput );
1850
		$parserOutput->setTOCEnabled( $this->mEnableTOC );
1851
1852
		// Touch section edit links only if not previously disabled
1853
		if ( $parserOutput->getEditSectionTokens() ) {
1854
			$parserOutput->setEditSectionTokens( $this->mEnableSectionEditLinks );
1855
		}
1856
1857
		$this->addParserOutputText( $parserOutput );
1858
	}
1859
1860
	/**
1861
	 * Add the output of a QuickTemplate to the output buffer
1862
	 *
1863
	 * @param QuickTemplate $template
1864
	 */
1865
	public function addTemplate( &$template ) {
1866
		$this->addHTML( $template->getHTML() );
1867
	}
1868
1869
	/**
1870
	 * Parse wikitext and return the HTML.
1871
	 *
1872
	 * @param string $text
1873
	 * @param bool $linestart Is this the start of a line?
1874
	 * @param bool $interface Use interface language ($wgLang instead of
1875
	 *   $wgContLang) while parsing language sensitive magic words like GRAMMAR and PLURAL.
1876
	 *   This also disables LanguageConverter.
1877
	 * @param Language $language Target language object, will override $interface
1878
	 * @throws MWException
1879
	 * @return string HTML
1880
	 */
1881
	public function parse( $text, $linestart = true, $interface = false, $language = null ) {
1882
		global $wgParser;
1883
1884
		if ( is_null( $this->getTitle() ) ) {
1885
			throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
1886
		}
1887
1888
		$popts = $this->parserOptions();
1889
		if ( $interface ) {
1890
			$popts->setInterfaceMessage( true );
1891
		}
1892
		if ( $language !== null ) {
1893
			$oldLang = $popts->setTargetLanguage( $language );
1894
		}
1895
1896
		$parserOutput = $wgParser->getFreshParser()->parse(
1897
			$text, $this->getTitle(), $popts,
1898
			$linestart, true, $this->mRevisionId
1899
		);
1900
1901
		if ( $interface ) {
1902
			$popts->setInterfaceMessage( false );
1903
		}
1904
		if ( $language !== null ) {
1905
			$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...
1906
		}
1907
1908
		return $parserOutput->getText();
1909
	}
1910
1911
	/**
1912
	 * Parse wikitext, strip paragraphs, and return the HTML.
1913
	 *
1914
	 * @param string $text
1915
	 * @param bool $linestart Is this the start of a line?
1916
	 * @param bool $interface Use interface language ($wgLang instead of
1917
	 *   $wgContLang) while parsing language sensitive magic
1918
	 *   words like GRAMMAR and PLURAL
1919
	 * @return string HTML
1920
	 */
1921
	public function parseInline( $text, $linestart = true, $interface = false ) {
1922
		$parsed = $this->parse( $text, $linestart, $interface );
1923
		return Parser::stripOuterParagraph( $parsed );
1924
	}
1925
1926
	/**
1927
	 * @param $maxage
1928
	 * @deprecated since 1.27 Use setCdnMaxage() instead
1929
	 */
1930
	public function setSquidMaxage( $maxage ) {
1931
		$this->setCdnMaxage( $maxage );
1932
	}
1933
1934
	/**
1935
	 * Set the value of the "s-maxage" part of the "Cache-control" HTTP header
1936
	 *
1937
	 * @param int $maxage Maximum cache time on the CDN, in seconds.
1938
	 */
1939
	public function setCdnMaxage( $maxage ) {
1940
		$this->mCdnMaxage = min( $maxage, $this->mCdnMaxageLimit );
1941
	}
1942
1943
	/**
1944
	 * Lower the value of the "s-maxage" part of the "Cache-control" HTTP header
1945
	 *
1946
	 * @param int $maxage Maximum cache time on the CDN, in seconds
1947
	 * @since 1.27
1948
	 */
1949
	public function lowerCdnMaxage( $maxage ) {
1950
		$this->mCdnMaxageLimit = min( $maxage, $this->mCdnMaxageLimit );
1951
		$this->setCdnMaxage( $this->mCdnMaxage );
1952
	}
1953
1954
	/**
1955
	 * Use enableClientCache(false) to force it to send nocache headers
1956
	 *
1957
	 * @param bool $state
1958
	 *
1959
	 * @return bool
1960
	 */
1961
	public function enableClientCache( $state ) {
1962
		return wfSetVar( $this->mEnableClientCache, $state );
1963
	}
1964
1965
	/**
1966
	 * Get the list of cookies that will influence on the cache
1967
	 *
1968
	 * @return array
1969
	 */
1970
	function getCacheVaryCookies() {
1971
		static $cookies;
1972
		if ( $cookies === null ) {
1973
			$config = $this->getConfig();
1974
			$cookies = array_merge(
1975
				SessionManager::singleton()->getVaryCookies(),
1976
				[
1977
					'forceHTTPS',
1978
				],
1979
				$config->get( 'CacheVaryCookies' )
1980
			);
1981
			Hooks::run( 'GetCacheVaryCookies', [ $this, &$cookies ] );
1982
		}
1983
		return $cookies;
1984
	}
1985
1986
	/**
1987
	 * Check if the request has a cache-varying cookie header
1988
	 * If it does, it's very important that we don't allow public caching
1989
	 *
1990
	 * @return bool
1991
	 */
1992
	function haveCacheVaryCookies() {
1993
		$request = $this->getRequest();
1994
		foreach ( $this->getCacheVaryCookies() as $cookieName ) {
1995
			if ( $request->getCookie( $cookieName, '', '' ) !== '' ) {
1996
				wfDebug( __METHOD__ . ": found $cookieName\n" );
1997
				return true;
1998
			}
1999
		}
2000
		wfDebug( __METHOD__ . ": no cache-varying cookies found\n" );
2001
		return false;
2002
	}
2003
2004
	/**
2005
	 * Add an HTTP header that will influence on the cache
2006
	 *
2007
	 * @param string $header Header name
2008
	 * @param string[]|null $option Options for the Key header. See
2009
	 * https://datatracker.ietf.org/doc/draft-fielding-http-key/
2010
	 * for the list of valid options.
2011
	 */
2012
	public function addVaryHeader( $header, array $option = null ) {
2013
		if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
2014
			$this->mVaryHeader[$header] = [];
2015
		}
2016
		if ( !is_array( $option ) ) {
2017
			$option = [];
2018
		}
2019
		$this->mVaryHeader[$header] = array_unique( array_merge( $this->mVaryHeader[$header], $option ) );
2020
	}
2021
2022
	/**
2023
	 * Return a Vary: header on which to vary caches. Based on the keys of $mVaryHeader,
2024
	 * such as Accept-Encoding or Cookie
2025
	 *
2026
	 * @return string
2027
	 */
2028
	public function getVaryHeader() {
2029
		// If we vary on cookies, let's make sure it's always included here too.
2030
		if ( $this->getCacheVaryCookies() ) {
2031
			$this->addVaryHeader( 'Cookie' );
2032
		}
2033
2034
		foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2035
			$this->addVaryHeader( $header, $options );
2036
		}
2037
		return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
2038
	}
2039
2040
	/**
2041
	 * Get a complete Key header
2042
	 *
2043
	 * @return string
2044
	 */
2045
	public function getKeyHeader() {
2046
		$cvCookies = $this->getCacheVaryCookies();
2047
2048
		$cookiesOption = [];
2049
		foreach ( $cvCookies as $cookieName ) {
2050
			$cookiesOption[] = 'param=' . $cookieName;
2051
		}
2052
		$this->addVaryHeader( 'Cookie', $cookiesOption );
2053
2054
		foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
2055
			$this->addVaryHeader( $header, $options );
2056
		}
2057
2058
		$headers = [];
2059
		foreach ( $this->mVaryHeader as $header => $option ) {
2060
			$newheader = $header;
2061
			if ( is_array( $option ) && count( $option ) > 0 ) {
2062
				$newheader .= ';' . implode( ';', $option );
2063
			}
2064
			$headers[] = $newheader;
2065
		}
2066
		$key = 'Key: ' . implode( ',', $headers );
2067
2068
		return $key;
2069
	}
2070
2071
	/**
2072
	 * T23672: Add Accept-Language to Vary and Key headers
2073
	 * if there's no 'variant' parameter existed in GET.
2074
	 *
2075
	 * For example:
2076
	 *   /w/index.php?title=Main_page should always be served; but
2077
	 *   /w/index.php?title=Main_page&variant=zh-cn should never be served.
2078
	 */
2079
	function addAcceptLanguage() {
2080
		$title = $this->getTitle();
2081
		if ( !$title instanceof Title ) {
2082
			return;
2083
		}
2084
2085
		$lang = $title->getPageLanguage();
2086
		if ( !$this->getRequest()->getCheck( 'variant' ) && $lang->hasVariants() ) {
2087
			$variants = $lang->getVariants();
2088
			$aloption = [];
2089
			foreach ( $variants as $variant ) {
2090
				if ( $variant === $lang->getCode() ) {
2091
					continue;
2092
				} else {
2093
					$aloption[] = 'substr=' . $variant;
2094
2095
					// IE and some other browsers use BCP 47 standards in
2096
					// their Accept-Language header, like "zh-CN" or "zh-Hant".
2097
					// We should handle these too.
2098
					$variantBCP47 = wfBCP47( $variant );
2099
					if ( $variantBCP47 !== $variant ) {
2100
						$aloption[] = 'substr=' . $variantBCP47;
2101
					}
2102
				}
2103
			}
2104
			$this->addVaryHeader( 'Accept-Language', $aloption );
2105
		}
2106
	}
2107
2108
	/**
2109
	 * Set a flag which will cause an X-Frame-Options header appropriate for
2110
	 * edit pages to be sent. The header value is controlled by
2111
	 * $wgEditPageFrameOptions.
2112
	 *
2113
	 * This is the default for special pages. If you display a CSRF-protected
2114
	 * form on an ordinary view page, then you need to call this function.
2115
	 *
2116
	 * @param bool $enable
2117
	 */
2118
	public function preventClickjacking( $enable = true ) {
2119
		$this->mPreventClickjacking = $enable;
2120
	}
2121
2122
	/**
2123
	 * Turn off frame-breaking. Alias for $this->preventClickjacking(false).
2124
	 * This can be called from pages which do not contain any CSRF-protected
2125
	 * HTML form.
2126
	 */
2127
	public function allowClickjacking() {
2128
		$this->mPreventClickjacking = false;
2129
	}
2130
2131
	/**
2132
	 * Get the prevent-clickjacking flag
2133
	 *
2134
	 * @since 1.24
2135
	 * @return bool
2136
	 */
2137
	public function getPreventClickjacking() {
2138
		return $this->mPreventClickjacking;
2139
	}
2140
2141
	/**
2142
	 * Get the X-Frame-Options header value (without the name part), or false
2143
	 * if there isn't one. This is used by Skin to determine whether to enable
2144
	 * JavaScript frame-breaking, for clients that don't support X-Frame-Options.
2145
	 *
2146
	 * @return string
2147
	 */
2148
	public function getFrameOptions() {
2149
		$config = $this->getConfig();
2150
		if ( $config->get( 'BreakFrames' ) ) {
2151
			return 'DENY';
2152
		} elseif ( $this->mPreventClickjacking && $config->get( 'EditPageFrameOptions' ) ) {
2153
			return $config->get( 'EditPageFrameOptions' );
2154
		}
2155
		return false;
2156
	}
2157
2158
	/**
2159
	 * Send cache control HTTP headers
2160
	 */
2161
	public function sendCacheControl() {
2162
		$response = $this->getRequest()->response();
2163
		$config = $this->getConfig();
2164
		if ( $config->get( 'UseETag' ) && $this->mETag ) {
2165
			$response->header( "ETag: $this->mETag" );
2166
		}
2167
2168
		$this->addVaryHeader( 'Cookie' );
2169
		$this->addAcceptLanguage();
2170
2171
		# don't serve compressed data to clients who can't handle it
2172
		# maintain different caches for logged-in users and non-logged in ones
2173
		$response->header( $this->getVaryHeader() );
2174
2175
		if ( $config->get( 'UseKeyHeader' ) ) {
2176
			$response->header( $this->getKeyHeader() );
2177
		}
2178
2179
		if ( $this->mEnableClientCache ) {
2180
			if (
2181
				$config->get( 'UseSquid' ) &&
2182
				!$response->hasCookies() &&
2183
				!SessionManager::getGlobalSession()->isPersistent() &&
2184
				!$this->isPrintable() &&
2185
				$this->mCdnMaxage != 0 &&
2186
				!$this->haveCacheVaryCookies()
2187
			) {
2188
				if ( $config->get( 'UseESI' ) ) {
2189
					# We'll purge the proxy cache explicitly, but require end user agents
2190
					# to revalidate against the proxy on each visit.
2191
					# Surrogate-Control controls our CDN, Cache-Control downstream caches
2192
					wfDebug( __METHOD__ . ": proxy caching with ESI; {$this->mLastModified} **", 'private' );
2193
					# start with a shorter timeout for initial testing
2194
					# header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"');
2195
					$response->header( 'Surrogate-Control: max-age=' . $config->get( 'SquidMaxage' )
2196
						. '+' . $this->mCdnMaxage . ', content="ESI/1.0"' );
2197
					$response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
2198
				} else {
2199
					# We'll purge the proxy cache for anons explicitly, but require end user agents
2200
					# to revalidate against the proxy on each visit.
2201
					# IMPORTANT! The CDN needs to replace the Cache-Control header with
2202
					# Cache-Control: s-maxage=0, must-revalidate, max-age=0
2203
					wfDebug( __METHOD__ . ": local proxy caching; {$this->mLastModified} **", 'private' );
2204
					# start with a shorter timeout for initial testing
2205
					# header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" );
2206
					$response->header( 'Cache-Control: s-maxage=' . $this->mCdnMaxage
2207
						. ', must-revalidate, max-age=0' );
2208
				}
2209 View Code Duplication
			} else {
2210
				# We do want clients to cache if they can, but they *must* check for updates
2211
				# on revisiting the page.
2212
				wfDebug( __METHOD__ . ": private caching; {$this->mLastModified} **", 'private' );
2213
				$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2214
				$response->header( "Cache-Control: private, must-revalidate, max-age=0" );
2215
			}
2216
			if ( $this->mLastModified ) {
2217
				$response->header( "Last-Modified: {$this->mLastModified}" );
2218
			}
2219 View Code Duplication
		} else {
2220
			wfDebug( __METHOD__ . ": no caching **", 'private' );
2221
2222
			# In general, the absence of a last modified header should be enough to prevent
2223
			# the client from using its cache. We send a few other things just to make sure.
2224
			$response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
2225
			$response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
2226
			$response->header( 'Pragma: no-cache' );
2227
		}
2228
	}
2229
2230
	/**
2231
	 * Finally, all the text has been munged and accumulated into
2232
	 * the object, let's actually output it:
2233
	 */
2234
	public function output() {
2235
		if ( $this->mDoNothing ) {
2236
			return;
2237
		}
2238
2239
		$response = $this->getRequest()->response();
2240
		$config = $this->getConfig();
2241
2242
		if ( $this->mRedirect != '' ) {
2243
			# Standards require redirect URLs to be absolute
2244
			$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...
2245
2246
			$redirect = $this->mRedirect;
2247
			$code = $this->mRedirectCode;
2248
2249
			if ( Hooks::run( "BeforePageRedirect", [ $this, &$redirect, &$code ] ) ) {
2250
				if ( $code == '301' || $code == '303' ) {
2251
					if ( !$config->get( 'DebugRedirects' ) ) {
2252
						$response->statusHeader( $code );
2253
					}
2254
					$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...
2255
				}
2256
				if ( $config->get( 'VaryOnXFP' ) ) {
2257
					$this->addVaryHeader( 'X-Forwarded-Proto' );
2258
				}
2259
				$this->sendCacheControl();
2260
2261
				$response->header( "Content-Type: text/html; charset=utf-8" );
2262
				if ( $config->get( 'DebugRedirects' ) ) {
2263
					$url = htmlspecialchars( $redirect );
2264
					print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
2265
					print "<p>Location: <a href=\"$url\">$url</a></p>\n";
2266
					print "</body>\n</html>\n";
2267
				} else {
2268
					$response->header( 'Location: ' . $redirect );
2269
				}
2270
			}
2271
2272
			return;
2273
		} elseif ( $this->mStatusCode ) {
2274
			$response->statusHeader( $this->mStatusCode );
2275
		}
2276
2277
		# Buffer output; final headers may depend on later processing
2278
		ob_start();
2279
2280
		$response->header( 'Content-type: ' . $config->get( 'MimeType' ) . '; charset=UTF-8' );
2281
		$response->header( 'Content-language: ' . $config->get( 'LanguageCode' ) );
2282
2283
		// Avoid Internet Explorer "compatibility view" in IE 8-10, so that
2284
		// jQuery etc. can work correctly.
2285
		$response->header( 'X-UA-Compatible: IE=Edge' );
2286
2287
		// Prevent framing, if requested
2288
		$frameOptions = $this->getFrameOptions();
2289
		if ( $frameOptions ) {
2290
			$response->header( "X-Frame-Options: $frameOptions" );
2291
		}
2292
2293
		if ( $this->mArticleBodyOnly ) {
2294
			echo $this->mBodytext;
2295
		} else {
2296
			$sk = $this->getSkin();
2297
			// add skin specific modules
2298
			$modules = $sk->getDefaultModules();
2299
2300
			// Enforce various default modules for all skins
2301
			$coreModules = [
2302
				// Keep this list as small as possible
2303
				'site',
2304
				'mediawiki.page.startup',
2305
				'mediawiki.user',
2306
			];
2307
2308
			// Support for high-density display images if enabled
2309
			if ( $config->get( 'ResponsiveImages' ) ) {
2310
				$coreModules[] = 'mediawiki.hidpi';
2311
			}
2312
2313
			$this->addModules( $coreModules );
2314
			foreach ( $modules as $group ) {
2315
				$this->addModules( $group );
2316
			}
2317
			MWDebug::addModules( $this );
2318
2319
			// Hook that allows last minute changes to the output page, e.g.
2320
			// adding of CSS or Javascript by extensions.
2321
			Hooks::run( 'BeforePageDisplay', [ &$this, &$sk ] );
2322
2323
			try {
2324
				$sk->outputPage();
2325
			} catch ( Exception $e ) {
2326
				ob_end_clean(); // bug T129657
2327
				throw $e;
2328
			}
2329
		}
2330
2331
		try {
2332
			// This hook allows last minute changes to final overall output by modifying output buffer
2333
			Hooks::run( 'AfterFinalPageOutput', [ $this ] );
2334
		} catch ( Exception $e ) {
2335
			ob_end_clean(); // bug T129657
2336
			throw $e;
2337
		}
2338
2339
		$this->sendCacheControl();
2340
2341
		ob_end_flush();
2342
2343
	}
2344
2345
	/**
2346
	 * Prepare this object to display an error page; disable caching and
2347
	 * indexing, clear the current text and redirect, set the page's title
2348
	 * and optionally an custom HTML title (content of the "<title>" tag).
2349
	 *
2350
	 * @param string|Message $pageTitle Will be passed directly to setPageTitle()
2351
	 * @param string|Message $htmlTitle Will be passed directly to setHTMLTitle();
2352
	 *                   optional, if not passed the "<title>" attribute will be
2353
	 *                   based on $pageTitle
2354
	 */
2355
	public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
2356
		$this->setPageTitle( $pageTitle );
2357
		if ( $htmlTitle !== false ) {
2358
			$this->setHTMLTitle( $htmlTitle );
2359
		}
2360
		$this->setRobotPolicy( 'noindex,nofollow' );
2361
		$this->setArticleRelated( false );
2362
		$this->enableClientCache( false );
2363
		$this->mRedirect = '';
2364
		$this->clearSubtitle();
2365
		$this->clearHTML();
2366
	}
2367
2368
	/**
2369
	 * Output a standard error page
2370
	 *
2371
	 * showErrorPage( 'titlemsg', 'pagetextmsg' );
2372
	 * showErrorPage( 'titlemsg', 'pagetextmsg', array( 'param1', 'param2' ) );
2373
	 * showErrorPage( 'titlemsg', $messageObject );
2374
	 * showErrorPage( $titleMessageObject, $messageObject );
2375
	 *
2376
	 * @param string|Message $title Message key (string) for page title, or a Message object
2377
	 * @param string|Message $msg Message key (string) for page text, or a Message object
2378
	 * @param array $params Message parameters; ignored if $msg is a Message object
2379
	 */
2380
	public function showErrorPage( $title, $msg, $params = [] ) {
2381
		if ( !$title instanceof Message ) {
2382
			$title = $this->msg( $title );
2383
		}
2384
2385
		$this->prepareErrorPage( $title );
2386
2387
		if ( $msg instanceof Message ) {
2388
			if ( $params !== [] ) {
2389
				trigger_error( 'Argument ignored: $params. The message parameters argument '
2390
					. 'is discarded when the $msg argument is a Message object instead of '
2391
					. 'a string.', E_USER_NOTICE );
2392
			}
2393
			$this->addHTML( $msg->parseAsBlock() );
2394
		} else {
2395
			$this->addWikiMsgArray( $msg, $params );
2396
		}
2397
2398
		$this->returnToMain();
2399
	}
2400
2401
	/**
2402
	 * Output a standard permission error page
2403
	 *
2404
	 * @param array $errors Error message keys
2405
	 * @param string $action Action that was denied or null if unknown
2406
	 */
2407
	public function showPermissionsErrorPage( array $errors, $action = null ) {
2408
		// For some action (read, edit, create and upload), display a "login to do this action"
2409
		// error if all of the following conditions are met:
2410
		// 1. the user is not logged in
2411
		// 2. the only error is insufficient permissions (i.e. no block or something else)
2412
		// 3. the error can be avoided simply by logging in
2413
		if ( in_array( $action, [ 'read', 'edit', 'createpage', 'createtalk', 'upload' ] )
2414
			&& $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
2415
			&& ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
2416
			&& ( User::groupHasPermission( 'user', $action )
2417
			|| User::groupHasPermission( 'autoconfirmed', $action ) )
2418
		) {
2419
			$displayReturnto = null;
2420
2421
			# Due to bug 32276, if a user does not have read permissions,
2422
			# $this->getTitle() will just give Special:Badtitle, which is
2423
			# not especially useful as a returnto parameter. Use the title
2424
			# from the request instead, if there was one.
2425
			$request = $this->getRequest();
2426
			$returnto = Title::newFromText( $request->getVal( 'title', '' ) );
2427
			if ( $action == 'edit' ) {
2428
				$msg = 'whitelistedittext';
2429
				$displayReturnto = $returnto;
2430
			} elseif ( $action == 'createpage' || $action == 'createtalk' ) {
2431
				$msg = 'nocreatetext';
2432
			} elseif ( $action == 'upload' ) {
2433
				$msg = 'uploadnologintext';
2434
			} else { # Read
2435
				$msg = 'loginreqpagetext';
2436
				$displayReturnto = Title::newMainPage();
2437
			}
2438
2439
			$query = [];
2440
2441
			if ( $returnto ) {
2442
				$query['returnto'] = $returnto->getPrefixedText();
2443
2444 View Code Duplication
				if ( !$request->wasPosted() ) {
2445
					$returntoquery = $request->getValues();
2446
					unset( $returntoquery['title'] );
2447
					unset( $returntoquery['returnto'] );
2448
					unset( $returntoquery['returntoquery'] );
2449
					$query['returntoquery'] = wfArrayToCgi( $returntoquery );
2450
				}
2451
			}
2452
			$loginLink = Linker::linkKnown(
2453
				SpecialPage::getTitleFor( 'Userlogin' ),
2454
				$this->msg( 'loginreqlink' )->escaped(),
2455
				[],
2456
				$query
2457
			);
2458
2459
			$this->prepareErrorPage( $this->msg( 'loginreqtitle' ) );
2460
			$this->addHTML( $this->msg( $msg )->rawParams( $loginLink )->parse() );
2461
2462
			# Don't return to a page the user can't read otherwise
2463
			# we'll end up in a pointless loop
2464
			if ( $displayReturnto && $displayReturnto->userCan( 'read', $this->getUser() ) ) {
2465
				$this->returnToMain( null, $displayReturnto );
2466
			}
2467
		} else {
2468
			$this->prepareErrorPage( $this->msg( 'permissionserrors' ) );
2469
			$this->addWikiText( $this->formatPermissionsErrorMessage( $errors, $action ) );
2470
		}
2471
	}
2472
2473
	/**
2474
	 * Display an error page indicating that a given version of MediaWiki is
2475
	 * required to use it
2476
	 *
2477
	 * @param mixed $version The version of MediaWiki needed to use the page
2478
	 */
2479
	public function versionRequired( $version ) {
2480
		$this->prepareErrorPage( $this->msg( 'versionrequired', $version ) );
2481
2482
		$this->addWikiMsg( 'versionrequiredtext', $version );
2483
		$this->returnToMain();
2484
	}
2485
2486
	/**
2487
	 * Format a list of error messages
2488
	 *
2489
	 * @param array $errors Array of arrays returned by Title::getUserPermissionsErrors
2490
	 * @param string $action Action that was denied or null if unknown
2491
	 * @return string The wikitext error-messages, formatted into a list.
2492
	 */
2493
	public function formatPermissionsErrorMessage( array $errors, $action = null ) {
2494
		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...
2495
			$text = $this->msg( 'permissionserrorstext', count( $errors ) )->plain() . "\n\n";
2496
		} else {
2497
			$action_desc = $this->msg( "action-$action" )->plain();
2498
			$text = $this->msg(
2499
				'permissionserrorstext-withaction',
2500
				count( $errors ),
2501
				$action_desc
2502
			)->plain() . "\n\n";
2503
		}
2504
2505
		if ( count( $errors ) > 1 ) {
2506
			$text .= '<ul class="permissions-errors">' . "\n";
2507
2508
			foreach ( $errors as $error ) {
2509
				$text .= '<li>';
2510
				$text .= call_user_func_array( [ $this, 'msg' ], $error )->plain();
2511
				$text .= "</li>\n";
2512
			}
2513
			$text .= '</ul>';
2514
		} else {
2515
			$text .= "<div class=\"permissions-errors\">\n" .
2516
					call_user_func_array( [ $this, 'msg' ], reset( $errors ) )->plain() .
2517
					"\n</div>";
2518
		}
2519
2520
		return $text;
2521
	}
2522
2523
	/**
2524
	 * Display a page stating that the Wiki is in read-only mode.
2525
	 * Should only be called after wfReadOnly() has returned true.
2526
	 *
2527
	 * Historically, this function was used to show the source of the page that the user
2528
	 * was trying to edit and _also_ permissions error messages. The relevant code was
2529
	 * moved into EditPage in 1.19 (r102024 / d83c2a431c2a) and removed here in 1.25.
2530
	 *
2531
	 * @deprecated since 1.25; throw the exception directly
2532
	 * @throws ReadOnlyError
2533
	 */
2534
	public function readOnlyPage() {
2535
		if ( func_num_args() > 0 ) {
2536
			throw new MWException( __METHOD__ . ' no longer accepts arguments since 1.25.' );
2537
		}
2538
2539
		throw new ReadOnlyError;
2540
	}
2541
2542
	/**
2543
	 * Turn off regular page output and return an error response
2544
	 * for when rate limiting has triggered.
2545
	 *
2546
	 * @deprecated since 1.25; throw the exception directly
2547
	 */
2548
	public function rateLimited() {
2549
		wfDeprecated( __METHOD__, '1.25' );
2550
		throw new ThrottledError;
2551
	}
2552
2553
	/**
2554
	 * Show a warning about slave lag
2555
	 *
2556
	 * If the lag is higher than $wgSlaveLagCritical seconds,
2557
	 * then the warning is a bit more obvious. If the lag is
2558
	 * lower than $wgSlaveLagWarning, then no warning is shown.
2559
	 *
2560
	 * @param int $lag Slave lag
2561
	 */
2562
	public function showLagWarning( $lag ) {
2563
		$config = $this->getConfig();
2564
		if ( $lag >= $config->get( 'SlaveLagWarning' ) ) {
2565
			$message = $lag < $config->get( 'SlaveLagCritical' )
2566
				? 'lag-warn-normal'
2567
				: 'lag-warn-high';
2568
			$wrap = Html::rawElement( 'div', [ 'class' => "mw-{$message}" ], "\n$1\n" );
2569
			$this->wrapWikiMsg( "$wrap\n", [ $message, $this->getLanguage()->formatNum( $lag ) ] );
2570
		}
2571
	}
2572
2573
	public function showFatalError( $message ) {
2574
		$this->prepareErrorPage( $this->msg( 'internalerror' ) );
2575
2576
		$this->addHTML( $message );
2577
	}
2578
2579
	public function showUnexpectedValueError( $name, $val ) {
2580
		$this->showFatalError( $this->msg( 'unexpected', $name, $val )->text() );
2581
	}
2582
2583
	public function showFileCopyError( $old, $new ) {
2584
		$this->showFatalError( $this->msg( 'filecopyerror', $old, $new )->text() );
2585
	}
2586
2587
	public function showFileRenameError( $old, $new ) {
2588
		$this->showFatalError( $this->msg( 'filerenameerror', $old, $new )->text() );
2589
	}
2590
2591
	public function showFileDeleteError( $name ) {
2592
		$this->showFatalError( $this->msg( 'filedeleteerror', $name )->text() );
2593
	}
2594
2595
	public function showFileNotFoundError( $name ) {
2596
		$this->showFatalError( $this->msg( 'filenotfound', $name )->text() );
2597
	}
2598
2599
	/**
2600
	 * Add a "return to" link pointing to a specified title
2601
	 *
2602
	 * @param Title $title Title to link
2603
	 * @param array $query Query string parameters
2604
	 * @param string $text Text of the link (input is not escaped)
2605
	 * @param array $options Options array to pass to Linker
2606
	 */
2607
	public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) {
2608
		$link = $this->msg( 'returnto' )->rawParams(
2609
			Linker::link( $title, $text, [], $query, $options ) )->escaped();
2610
		$this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
2611
	}
2612
2613
	/**
2614
	 * Add a "return to" link pointing to a specified title,
2615
	 * or the title indicated in the request, or else the main page
2616
	 *
2617
	 * @param mixed $unused
2618
	 * @param Title|string $returnto Title or String to return to
2619
	 * @param string $returntoquery Query string for the return to link
2620
	 */
2621
	public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
2622
		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...
2623
			$returnto = $this->getRequest()->getText( 'returnto' );
2624
		}
2625
2626
		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...
2627
			$returntoquery = $this->getRequest()->getText( 'returntoquery' );
2628
		}
2629
2630
		if ( $returnto === '' ) {
2631
			$returnto = Title::newMainPage();
2632
		}
2633
2634
		if ( is_object( $returnto ) ) {
2635
			$titleObj = $returnto;
2636
		} else {
2637
			$titleObj = Title::newFromText( $returnto );
2638
		}
2639
		if ( !is_object( $titleObj ) ) {
2640
			$titleObj = Title::newMainPage();
2641
		}
2642
2643
		$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...
2644
	}
2645
2646
	/**
2647
	 * @param Skin $sk The given Skin
2648
	 * @param bool $includeStyle Unused
2649
	 * @return string The doctype, opening "<html>", and head element.
2650
	 */
2651
	public function headElement( Skin $sk, $includeStyle = true ) {
2652
		global $wgContLang;
2653
2654
		$userdir = $this->getLanguage()->getDir();
2655
		$sitedir = $wgContLang->getDir();
2656
2657
		$ret = Html::htmlHeader( $sk->getHtmlElementAttributes() );
2658
2659
		if ( $this->getHTMLTitle() == '' ) {
2660
			$this->setHTMLTitle( $this->msg( 'pagetitle', $this->getPageTitle() )->inContentLanguage() );
2661
		}
2662
2663
		$openHead = Html::openElement( 'head' );
2664
		if ( $openHead ) {
2665
			# Don't bother with the newline if $head == ''
2666
			$ret .= "$openHead\n";
2667
		}
2668
2669
		if ( !Html::isXmlMimeType( $this->getConfig()->get( 'MimeType' ) ) ) {
2670
			// Add <meta charset="UTF-8">
2671
			// This should be before <title> since it defines the charset used by
2672
			// text including the text inside <title>.
2673
			// The spec recommends defining XHTML5's charset using the XML declaration
2674
			// instead of meta.
2675
			// Our XML declaration is output by Html::htmlHeader.
2676
			// http://www.whatwg.org/html/semantics.html#attr-meta-http-equiv-content-type
2677
			// http://www.whatwg.org/html/semantics.html#charset
2678
			$ret .= Html::element( 'meta', [ 'charset' => 'UTF-8' ] ) . "\n";
2679
		}
2680
2681
		$ret .= Html::element( 'title', null, $this->getHTMLTitle() ) . "\n";
2682
		$ret .= $this->getInlineHeadScripts() . "\n";
2683
		$ret .= $this->buildCssLinks() . "\n";
2684
		$ret .= $this->getExternalHeadScripts() . "\n";
2685
2686
		foreach ( $this->getHeadLinksArray() as $item ) {
2687
			$ret .= $item . "\n";
2688
		}
2689
2690
		foreach ( $this->mHeadItems as $item ) {
2691
			$ret .= $item . "\n";
2692
		}
2693
2694
		$closeHead = Html::closeElement( 'head' );
2695
		if ( $closeHead ) {
2696
			$ret .= "$closeHead\n";
2697
		}
2698
2699
		$bodyClasses = [];
2700
		$bodyClasses[] = 'mediawiki';
2701
2702
		# Classes for LTR/RTL directionality support
2703
		$bodyClasses[] = $userdir;
2704
		$bodyClasses[] = "sitedir-$sitedir";
2705
2706
		if ( $this->getLanguage()->capitalizeAllNouns() ) {
2707
			# A <body> class is probably not the best way to do this . . .
2708
			$bodyClasses[] = 'capitalize-all-nouns';
2709
		}
2710
2711
		$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...
2712
		$bodyClasses[] = 'skin-' . Sanitizer::escapeClass( $sk->getSkinName() );
2713
		$bodyClasses[] =
2714
			'action-' . Sanitizer::escapeClass( Action::getActionName( $this->getContext() ) );
2715
2716
		$bodyAttrs = [];
2717
		// While the implode() is not strictly needed, it's used for backwards compatibility
2718
		// (this used to be built as a string and hooks likely still expect that).
2719
		$bodyAttrs['class'] = implode( ' ', $bodyClasses );
2720
2721
		// Allow skins and extensions to add body attributes they need
2722
		$sk->addToBodyAttributes( $this, $bodyAttrs );
2723
		Hooks::run( 'OutputPageBodyAttributes', [ $this, $sk, &$bodyAttrs ] );
2724
2725
		$ret .= Html::openElement( 'body', $bodyAttrs ) . "\n";
2726
2727
		return $ret;
2728
	}
2729
2730
	/**
2731
	 * Get a ResourceLoader object associated with this OutputPage
2732
	 *
2733
	 * @return ResourceLoader
2734
	 */
2735
	public function getResourceLoader() {
2736
		if ( is_null( $this->mResourceLoader ) ) {
2737
			$this->mResourceLoader = new ResourceLoader(
2738
				$this->getConfig(),
2739
				LoggerFactory::getInstance( 'resourceloader' )
2740
			);
2741
		}
2742
		return $this->mResourceLoader;
2743
	}
2744
2745
	/**
2746
	 * Construct neccecary html and loader preset states to load modules on a page.
2747
	 *
2748
	 * Use getHtmlFromLoaderLinks() to convert this array to HTML.
2749
	 *
2750
	 * @param array|string $modules One or more module names
2751
	 * @param string $only ResourceLoaderModule TYPE_ class constant
2752
	 * @param array $extraQuery [optional] Array with extra query parameters for the request
2753
	 * @return array A list of HTML strings and array of client loader preset states
2754
	 */
2755
	public function makeResourceLoaderLink( $modules, $only, array $extraQuery = [] ) {
2756
		$modules = (array)$modules;
2757
2758
		$links = [
2759
			// List of html strings
2760
			'html' => [],
2761
			// Associative array of module names and their states
2762
			'states' => [],
2763
		];
2764
2765
		if ( !count( $modules ) ) {
2766
			return $links;
2767
		}
2768
2769
		if ( count( $modules ) > 1 ) {
2770
			// Remove duplicate module requests
2771
			$modules = array_unique( $modules );
2772
			// Sort module names so requests are more uniform
2773
			sort( $modules );
2774
2775
			if ( ResourceLoader::inDebugMode() ) {
2776
				// Recursively call us for every item
2777
				foreach ( $modules as $name ) {
2778
					$link = $this->makeResourceLoaderLink( $name, $only, $extraQuery );
2779
					$links['html'] = array_merge( $links['html'], $link['html'] );
2780
					$links['states'] += $link['states'];
2781
				}
2782
				return $links;
2783
			}
2784
		}
2785
2786
		if ( !is_null( $this->mTarget ) ) {
2787
			$extraQuery['target'] = $this->mTarget;
2788
		}
2789
2790
		// Create keyed-by-source and then keyed-by-group list of module objects from modules list
2791
		$sortedModules = [];
2792
		$resourceLoader = $this->getResourceLoader();
2793
		foreach ( $modules as $name ) {
2794
			$module = $resourceLoader->getModule( $name );
2795
			# Check that we're allowed to include this module on this page
2796
			if ( !$module
2797
				|| ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS )
2798
					&& $only == ResourceLoaderModule::TYPE_SCRIPTS )
2799
				|| ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_STYLES )
2800
					&& $only == ResourceLoaderModule::TYPE_STYLES )
2801
				|| ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
2802
					&& $only == ResourceLoaderModule::TYPE_COMBINED )
2803
				|| ( $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...
2804
			) {
2805
				continue;
2806
			}
2807
2808
			$sortedModules[$module->getSource()][$module->getGroup()][$name] = $module;
2809
		}
2810
2811
		foreach ( $sortedModules as $source => $groups ) {
2812
			foreach ( $groups as $group => $grpModules ) {
2813
				// Special handling for user-specific groups
2814
				$user = null;
2815
				if ( ( $group === 'user' || $group === 'private' ) && $this->getUser()->isLoggedIn() ) {
2816
					$user = $this->getUser()->getName();
2817
				}
2818
2819
				// Create a fake request based on the one we are about to make so modules return
2820
				// correct timestamp and emptiness data
2821
				$query = ResourceLoader::makeLoaderQuery(
2822
					[], // modules; not determined yet
2823
					$this->getLanguage()->getCode(),
2824
					$this->getSkin()->getSkinName(),
2825
					$user,
2826
					null, // version; not determined yet
2827
					ResourceLoader::inDebugMode(),
2828
					$only === ResourceLoaderModule::TYPE_COMBINED ? null : $only,
2829
					$this->isPrintable(),
2830
					$this->getRequest()->getBool( 'handheld' ),
2831
					$extraQuery
2832
				);
2833
				$context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
2834
2835
				// Extract modules that know they're empty and see if we have one or more
2836
				// raw modules
2837
				$isRaw = false;
2838
				foreach ( $grpModules as $key => $module ) {
2839
					// Inline empty modules: since they're empty, just mark them as 'ready' (bug 46857)
2840
					// If we're only getting the styles, we don't need to do anything for empty modules.
2841
					if ( $module->isKnownEmpty( $context ) ) {
2842
						unset( $grpModules[$key] );
2843
						if ( $only !== ResourceLoaderModule::TYPE_STYLES ) {
2844
							$links['states'][$key] = 'ready';
2845
						}
2846
					}
2847
2848
					$isRaw |= $module->isRaw();
2849
				}
2850
2851
				// If there are no non-empty modules, skip this group
2852
				if ( count( $grpModules ) === 0 ) {
2853
					continue;
2854
				}
2855
2856
				// Inline private modules. These can't be loaded through load.php for security
2857
				// reasons, see bug 34907. Note that these modules should be loaded from
2858
				// getExternalHeadScripts() before the first loader call. Otherwise other modules can't
2859
				// properly use them as dependencies (bug 30914)
2860
				if ( $group === 'private' ) {
2861
					if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
2862
						$links['html'][] = Html::inlineStyle(
2863
							$resourceLoader->makeModuleResponse( $context, $grpModules )
2864
						);
2865
					} else {
2866
						$links['html'][] = ResourceLoader::makeInlineScript(
2867
							$resourceLoader->makeModuleResponse( $context, $grpModules )
2868
						);
2869
					}
2870
					continue;
2871
				}
2872
2873
				// Special handling for the user group; because users might change their stuff
2874
				// on-wiki like user pages, or user preferences; we need to find the highest
2875
				// timestamp of these user-changeable modules so we can ensure cache misses on change
2876
				// This should NOT be done for the site group (bug 27564) because anons get that too
2877
				// and we shouldn't be putting timestamps in CDN-cached HTML
2878
				$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...
2879
				if ( $group === 'user' ) {
2880
					$query['version'] = $resourceLoader->getCombinedVersion( $context, array_keys( $grpModules ) );
2881
				}
2882
2883
				$query['modules'] = ResourceLoader::makePackedModulesString( array_keys( $grpModules ) );
2884
				$moduleContext = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
2885
				$url = $resourceLoader->createLoaderURL( $source, $moduleContext, $extraQuery );
2886
2887
				// Automatically select style/script elements
2888
				if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
2889
					$link = Html::linkedStyle( $url );
2890
				} else {
2891
					if ( $context->getRaw() || $isRaw ) {
2892
						// Startup module can't load itself, needs to use <script> instead of mw.loader.load
2893
						$link = Html::element( 'script', [
2894
							// In SpecialJavaScriptTest, QUnit must load synchronous
2895
							'async' => !isset( $extraQuery['sync'] ),
2896
							'src' => $url
2897
						] );
2898
					} else {
2899
						$link = ResourceLoader::makeInlineScript(
2900
							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...
2901
						);
2902
					}
2903
2904
					// For modules requested directly in the html via <script> or mw.loader.load
2905
					// tell mw.loader they are being loading to prevent duplicate requests.
2906
					foreach ( $grpModules as $key => $module ) {
2907
						// Don't output state=loading for the startup module.
2908
						if ( $key !== 'startup' ) {
2909
							$links['states'][$key] = 'loading';
2910
						}
2911
					}
2912
				}
2913
2914
				if ( $group == 'noscript' ) {
2915
					$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 2899 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...
2916
				} else {
2917
					$links['html'][] = $link;
2918
				}
2919
			}
2920
		}
2921
2922
		return $links;
2923
	}
2924
2925
	/**
2926
	 * Build html output from an array of links from makeResourceLoaderLink.
2927
	 * @param array $links
2928
	 * @return string HTML
2929
	 */
2930
	protected static function getHtmlFromLoaderLinks( array $links ) {
2931
		$html = [];
2932
		$states = [];
2933
		foreach ( $links as $link ) {
2934
			if ( !is_array( $link ) ) {
2935
				$html[] = $link;
2936
			} else {
2937
				$html = array_merge( $html, $link['html'] );
2938
				$states += $link['states'];
2939
			}
2940
		}
2941
		// Filter out empty values
2942
		$html = array_filter( $html, 'strlen' );
2943
2944
		if ( count( $states ) ) {
2945
			array_unshift( $html, ResourceLoader::makeInlineScript(
2946
				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...
2947
			) );
2948
		}
2949
2950
		return WrappedString::join( "\n", $html );
2951
	}
2952
2953
	/**
2954
	 * JS stuff to put in the "<head>". This is the startup module, config
2955
	 * vars and modules marked with position 'top'
2956
	 *
2957
	 * @return string HTML fragment
2958
	 */
2959
	function getHeadScripts() {
2960
		return $this->getInlineHeadScripts() . $this->getExternalHeadScripts();
2961
	}
2962
2963
	/**
2964
	 * <script src="..."> tags for "<head>". This is the startup module
2965
	 * and other modules marked with position 'top'.
2966
	 *
2967
	 * @return string HTML fragment
2968
	 */
2969
	function getExternalHeadScripts() {
2970
		$links = [];
2971
2972
		// Startup - this provides the client with the module
2973
		// manifest and loads jquery and mediawiki base modules
2974
		$links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS );
2975
2976
		return self::getHtmlFromLoaderLinks( $links );
2977
	}
2978
2979
	/**
2980
	 * <script>...</script> tags to put in "<head>".
2981
	 *
2982
	 * @return string HTML fragment
2983
	 */
2984
	function getInlineHeadScripts() {
2985
		$links = [];
2986
2987
		// Client profile classes for <html>. Allows for easy hiding/showing of UI components.
2988
		// Must be done synchronously on every page to avoid flashes of wrong content.
2989
		// Note: This class distinguishes MediaWiki-supported JavaScript from the rest.
2990
		// The "rest" includes browsers that support JavaScript but not supported by our runtime.
2991
		// For the performance benefit of the majority, this is added unconditionally here and is
2992
		// then fixed up by the startup module for unsupported browsers.
2993
		$links[] = Html::inlineScript(
2994
			'document.documentElement.className = document.documentElement.className'
2995
			. '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );'
2996
		);
2997
2998
		// Load config before anything else
2999
		$links[] = ResourceLoader::makeInlineScript(
3000
			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...
3001
		);
3002
3003
		// Load embeddable private modules before any loader links
3004
		// This needs to be TYPE_COMBINED so these modules are properly wrapped
3005
		// in mw.loader.implement() calls and deferred until mw.user is available
3006
		$embedScripts = [ 'user.options' ];
3007
		$links[] = $this->makeResourceLoaderLink(
3008
			$embedScripts,
3009
			ResourceLoaderModule::TYPE_COMBINED
3010
		);
3011
		// Separate user.tokens as otherwise caching will be allowed (T84960)
3012
		$links[] = $this->makeResourceLoaderLink(
3013
			'user.tokens',
3014
			ResourceLoaderModule::TYPE_COMBINED
3015
		);
3016
3017
		// Modules requests - let the client calculate dependencies and batch requests as it likes
3018
		// Only load modules that have marked themselves for loading at the top
3019
		$modules = $this->getModules( true, 'top' );
3020
		if ( $modules ) {
3021
			$links[] = ResourceLoader::makeInlineScript(
3022
				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...
3023
			);
3024
		}
3025
3026
		// "Scripts only" modules marked for top inclusion
3027
		$links[] = $this->makeResourceLoaderLink(
3028
			$this->getModuleScripts( true, 'top' ),
3029
			ResourceLoaderModule::TYPE_SCRIPTS
3030
		);
3031
3032
		return self::getHtmlFromLoaderLinks( $links );
3033
	}
3034
3035
	/**
3036
	 * JS stuff to put at the 'bottom', which goes at the bottom of the `<body>`.
3037
	 * These are modules marked with position 'bottom', legacy scripts ($this->mScripts),
3038
	 * site JS, and user JS.
3039
	 *
3040
	 * @param bool $unused Previously used to let this method change its output based
3041
	 *  on whether it was called by getExternalHeadScripts() or getBottomScripts().
3042
	 * @return string
3043
	 */
3044
	function getScriptsForBottomQueue( $unused = null ) {
3045
		// Scripts "only" requests marked for bottom inclusion
3046
		// If we're in the <head>, use load() calls rather than <script src="..."> tags
3047
		$links = [];
3048
3049
		$links[] = $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ),
3050
			ResourceLoaderModule::TYPE_SCRIPTS
3051
		);
3052
3053
		// Modules requests - let the client calculate dependencies and batch requests as it likes
3054
		// Only load modules that have marked themselves for loading at the bottom
3055
		$modules = $this->getModules( true, 'bottom' );
3056
		if ( $modules ) {
3057
			$links[] = ResourceLoader::makeInlineScript(
3058
				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...
3059
			);
3060
		}
3061
3062
		// Legacy Scripts
3063
		$links[] = $this->mScripts;
3064
3065
		// Add user JS if enabled
3066
		// This must use TYPE_COMBINED instead of only=scripts so that its request is handled by
3067
		// mw.loader.implement() which ensures that execution is scheduled after the "site" module.
3068
		if ( $this->getConfig()->get( 'AllowUserJs' )
3069
			&& $this->getUser()->isLoggedIn()
3070
			&& $this->getTitle()
3071
			&& $this->getTitle()->isJsSubpage()
3072
			&& $this->userCanPreview()
3073
		) {
3074
			// We're on a preview of a JS subpage. Exclude this page from the user module (T28283)
3075
			// and include the draft contents as a raw script instead.
3076
			$links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED,
3077
				[ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
3078
			);
3079
			// Load the previewed JS
3080
			$links[] = ResourceLoader::makeInlineScript(
3081
				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...
3082
					[ 'user', 'site' ],
3083
					new XmlJsCode(
3084
						'function () {'
3085
							. Xml::encodeJsCall( '$.globalEval', [
3086
								$this->getRequest()->getText( 'wpTextbox1' )
3087
							] )
3088
							. '}'
3089
					)
3090
				] )
3091
			);
3092
3093
			// FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded
3094
			// asynchronously and may arrive *after* the inline script here. So the previewed code
3095
			// may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js.
3096
			// Similarly, when previewing ./common.js and the user module does arrive first,
3097
			// it will arrive without common.js and the inline script runs after.
3098
			// Thus running common after the excluded subpage.
3099
		} else {
3100
			// Include the user module normally, i.e., raw to avoid it being wrapped in a closure.
3101
			$links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED );
3102
		}
3103
3104
		return self::getHtmlFromLoaderLinks( $links );
3105
	}
3106
3107
	/**
3108
	 * JS stuff to put at the bottom of the "<body>"
3109
	 * @return string
3110
	 */
3111
	function getBottomScripts() {
3112
		return $this->getScriptsForBottomQueue();
3113
	}
3114
3115
	/**
3116
	 * Get the javascript config vars to include on this page
3117
	 *
3118
	 * @return array Array of javascript config vars
3119
	 * @since 1.23
3120
	 */
3121
	public function getJsConfigVars() {
3122
		return $this->mJsConfigVars;
3123
	}
3124
3125
	/**
3126
	 * Add one or more variables to be set in mw.config in JavaScript
3127
	 *
3128
	 * @param string|array $keys Key or array of key/value pairs
3129
	 * @param mixed $value [optional] Value of the configuration variable
3130
	 */
3131 View Code Duplication
	public function addJsConfigVars( $keys, $value = null ) {
3132
		if ( is_array( $keys ) ) {
3133
			foreach ( $keys as $key => $value ) {
3134
				$this->mJsConfigVars[$key] = $value;
3135
			}
3136
			return;
3137
		}
3138
3139
		$this->mJsConfigVars[$keys] = $value;
3140
	}
3141
3142
	/**
3143
	 * Get an array containing the variables to be set in mw.config in JavaScript.
3144
	 *
3145
	 * Do not add things here which can be evaluated in ResourceLoaderStartUpModule
3146
	 * - in other words, page-independent/site-wide variables (without state).
3147
	 * You will only be adding bloat to the html page and causing page caches to
3148
	 * have to be purged on configuration changes.
3149
	 * @return array
3150
	 */
3151
	public function getJSVars() {
3152
		global $wgContLang;
3153
3154
		$curRevisionId = 0;
3155
		$articleId = 0;
3156
		$canonicalSpecialPageName = false; # bug 21115
3157
3158
		$title = $this->getTitle();
3159
		$ns = $title->getNamespace();
3160
		$canonicalNamespace = MWNamespace::exists( $ns )
3161
			? MWNamespace::getCanonicalName( $ns )
3162
			: $title->getNsText();
3163
3164
		$sk = $this->getSkin();
3165
		// Get the relevant title so that AJAX features can use the correct page name
3166
		// when making API requests from certain special pages (bug 34972).
3167
		$relevantTitle = $sk->getRelevantTitle();
3168
		$relevantUser = $sk->getRelevantUser();
3169
3170
		if ( $ns == NS_SPECIAL ) {
3171
			list( $canonicalSpecialPageName, /*...*/ ) =
3172
				SpecialPageFactory::resolveAlias( $title->getDBkey() );
3173
		} elseif ( $this->canUseWikiPage() ) {
3174
			$wikiPage = $this->getWikiPage();
3175
			$curRevisionId = $wikiPage->getLatest();
3176
			$articleId = $wikiPage->getId();
3177
		}
3178
3179
		$lang = $title->getPageViewLanguage();
3180
3181
		// Pre-process information
3182
		$separatorTransTable = $lang->separatorTransformTable();
3183
		$separatorTransTable = $separatorTransTable ? $separatorTransTable : [];
3184
		$compactSeparatorTransTable = [
3185
			implode( "\t", array_keys( $separatorTransTable ) ),
3186
			implode( "\t", $separatorTransTable ),
3187
		];
3188
		$digitTransTable = $lang->digitTransformTable();
3189
		$digitTransTable = $digitTransTable ? $digitTransTable : [];
3190
		$compactDigitTransTable = [
3191
			implode( "\t", array_keys( $digitTransTable ) ),
3192
			implode( "\t", $digitTransTable ),
3193
		];
3194
3195
		$user = $this->getUser();
3196
3197
		$vars = [
3198
			'wgCanonicalNamespace' => $canonicalNamespace,
3199
			'wgCanonicalSpecialPageName' => $canonicalSpecialPageName,
3200
			'wgNamespaceNumber' => $title->getNamespace(),
3201
			'wgPageName' => $title->getPrefixedDBkey(),
3202
			'wgTitle' => $title->getText(),
3203
			'wgCurRevisionId' => $curRevisionId,
3204
			'wgRevisionId' => (int)$this->getRevisionId(),
3205
			'wgArticleId' => $articleId,
3206
			'wgIsArticle' => $this->isArticle(),
3207
			'wgIsRedirect' => $title->isRedirect(),
3208
			'wgAction' => Action::getActionName( $this->getContext() ),
3209
			'wgUserName' => $user->isAnon() ? null : $user->getName(),
3210
			'wgUserGroups' => $user->getEffectiveGroups(),
3211
			'wgCategories' => $this->getCategories(),
3212
			'wgBreakFrames' => $this->getFrameOptions() == 'DENY',
3213
			'wgPageContentLanguage' => $lang->getCode(),
3214
			'wgPageContentModel' => $title->getContentModel(),
3215
			'wgSeparatorTransformTable' => $compactSeparatorTransTable,
3216
			'wgDigitTransformTable' => $compactDigitTransTable,
3217
			'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
3218
			'wgMonthNames' => $lang->getMonthNamesArray(),
3219
			'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(),
3220
			'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
3221
			'wgRelevantArticleId' => $relevantTitle->getArticleID(),
3222
			'wgRequestId' => WebRequest::getRequestId(),
3223
		];
3224
3225
		if ( $user->isLoggedIn() ) {
3226
			$vars['wgUserId'] = $user->getId();
3227
			$vars['wgUserEditCount'] = $user->getEditCount();
3228
			$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...
3229
			$vars['wgUserRegistration'] = $userReg !== null ? ( $userReg * 1000 ) : null;
3230
			// Get the revision ID of the oldest new message on the user's talk
3231
			// page. This can be used for constructing new message alerts on
3232
			// the client side.
3233
			$vars['wgUserNewMsgRevisionId'] = $user->getNewMessageRevisionId();
3234
		}
3235
3236
		if ( $wgContLang->hasVariants() ) {
3237
			$vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
3238
		}
3239
		// Same test as SkinTemplate
3240
		$vars['wgIsProbablyEditable'] = $title->quickUserCan( 'edit', $user )
3241
			&& ( $title->exists() || $title->quickUserCan( 'create', $user ) );
3242
3243
		foreach ( $title->getRestrictionTypes() as $type ) {
3244
			$vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
3245
		}
3246
3247
		if ( $title->isMainPage() ) {
3248
			$vars['wgIsMainPage'] = true;
3249
		}
3250
3251
		if ( $this->mRedirectedFrom ) {
3252
			$vars['wgRedirectedFrom'] = $this->mRedirectedFrom->getPrefixedDBkey();
3253
		}
3254
3255
		if ( $relevantUser ) {
3256
			$vars['wgRelevantUserName'] = $relevantUser->getName();
3257
		}
3258
3259
		// Allow extensions to add their custom variables to the mw.config map.
3260
		// Use the 'ResourceLoaderGetConfigVars' hook if the variable is not
3261
		// page-dependant but site-wide (without state).
3262
		// Alternatively, you may want to use OutputPage->addJsConfigVars() instead.
3263
		Hooks::run( 'MakeGlobalVariablesScript', [ &$vars, $this ] );
3264
3265
		// Merge in variables from addJsConfigVars last
3266
		return array_merge( $vars, $this->getJsConfigVars() );
3267
	}
3268
3269
	/**
3270
	 * To make it harder for someone to slip a user a fake
3271
	 * user-JavaScript or user-CSS preview, a random token
3272
	 * is associated with the login session. If it's not
3273
	 * passed back with the preview request, we won't render
3274
	 * the code.
3275
	 *
3276
	 * @return bool
3277
	 */
3278
	public function userCanPreview() {
3279
		$request = $this->getRequest();
3280
		if (
3281
			$request->getVal( 'action' ) !== 'submit' ||
3282
			!$request->getCheck( 'wpPreview' ) ||
3283
			!$request->wasPosted()
3284
		) {
3285
			return false;
3286
		}
3287
3288
		$user = $this->getUser();
3289
		if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
3290
			return false;
3291
		}
3292
3293
		$title = $this->getTitle();
3294
		if ( !$title->isJsSubpage() && !$title->isCssSubpage() ) {
3295
			return false;
3296
		}
3297
		if ( !$title->isSubpageOf( $user->getUserPage() ) ) {
3298
			// Don't execute another user's CSS or JS on preview (T85855)
3299
			return false;
3300
		}
3301
3302
		$errors = $title->getUserPermissionsErrors( 'edit', $user );
3303
		if ( count( $errors ) !== 0 ) {
3304
			return false;
3305
		}
3306
3307
		return true;
3308
	}
3309
3310
	/**
3311
	 * @return array Array in format "link name or number => 'link html'".
3312
	 */
3313
	public function getHeadLinksArray() {
3314
		global $wgVersion;
3315
3316
		$tags = [];
3317
		$config = $this->getConfig();
3318
3319
		$canonicalUrl = $this->mCanonicalUrl;
3320
3321
		$tags['meta-generator'] = Html::element( 'meta', [
3322
			'name' => 'generator',
3323
			'content' => "MediaWiki $wgVersion",
3324
		] );
3325
3326
		if ( $config->get( 'ReferrerPolicy' ) !== false ) {
3327
			$tags['meta-referrer'] = Html::element( 'meta', [
3328
				'name' => 'referrer',
3329
				'content' => $config->get( 'ReferrerPolicy' )
3330
			] );
3331
		}
3332
3333
		$p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
3334
		if ( $p !== 'index,follow' ) {
3335
			// http://www.robotstxt.org/wc/meta-user.html
3336
			// Only show if it's different from the default robots policy
3337
			$tags['meta-robots'] = Html::element( 'meta', [
3338
				'name' => 'robots',
3339
				'content' => $p,
3340
			] );
3341
		}
3342
3343
		foreach ( $this->mMetatags as $tag ) {
3344
			if ( 0 == strcasecmp( 'http:', substr( $tag[0], 0, 5 ) ) ) {
3345
				$a = 'http-equiv';
3346
				$tag[0] = substr( $tag[0], 5 );
3347
			} else {
3348
				$a = 'name';
3349
			}
3350
			$tagName = "meta-{$tag[0]}";
3351
			if ( isset( $tags[$tagName] ) ) {
3352
				$tagName .= $tag[1];
3353
			}
3354
			$tags[$tagName] = Html::element( 'meta',
3355
				[
3356
					$a => $tag[0],
3357
					'content' => $tag[1]
3358
				]
3359
			);
3360
		}
3361
3362
		foreach ( $this->mLinktags as $tag ) {
3363
			$tags[] = Html::element( 'link', $tag );
3364
		}
3365
3366
		# Universal edit button
3367
		if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
3368
			$user = $this->getUser();
3369
			if ( $this->getTitle()->quickUserCan( 'edit', $user )
3370
				&& ( $this->getTitle()->exists() ||
3371
					$this->getTitle()->quickUserCan( 'create', $user ) )
3372
			) {
3373
				// Original UniversalEditButton
3374
				$msg = $this->msg( 'edit' )->text();
3375
				$tags['universal-edit-button'] = Html::element( 'link', [
3376
					'rel' => 'alternate',
3377
					'type' => 'application/x-wiki',
3378
					'title' => $msg,
3379
					'href' => $this->getTitle()->getEditURL(),
3380
				] );
3381
				// Alternate edit link
3382
				$tags['alternative-edit'] = Html::element( 'link', [
3383
					'rel' => 'edit',
3384
					'title' => $msg,
3385
					'href' => $this->getTitle()->getEditURL(),
3386
				] );
3387
			}
3388
		}
3389
3390
		# Generally the order of the favicon and apple-touch-icon links
3391
		# should not matter, but Konqueror (3.5.9 at least) incorrectly
3392
		# uses whichever one appears later in the HTML source. Make sure
3393
		# apple-touch-icon is specified first to avoid this.
3394
		if ( $config->get( 'AppleTouchIcon' ) !== false ) {
3395
			$tags['apple-touch-icon'] = Html::element( 'link', [
3396
				'rel' => 'apple-touch-icon',
3397
				'href' => $config->get( 'AppleTouchIcon' )
3398
			] );
3399
		}
3400
3401
		if ( $config->get( 'Favicon' ) !== false ) {
3402
			$tags['favicon'] = Html::element( 'link', [
3403
				'rel' => 'shortcut icon',
3404
				'href' => $config->get( 'Favicon' )
3405
			] );
3406
		}
3407
3408
		# OpenSearch description link
3409
		$tags['opensearch'] = Html::element( 'link', [
3410
			'rel' => 'search',
3411
			'type' => 'application/opensearchdescription+xml',
3412
			'href' => wfScript( 'opensearch_desc' ),
3413
			'title' => $this->msg( 'opensearch-desc' )->inContentLanguage()->text(),
3414
		] );
3415
3416
		if ( $config->get( 'EnableAPI' ) ) {
3417
			# Real Simple Discovery link, provides auto-discovery information
3418
			# for the MediaWiki API (and potentially additional custom API
3419
			# support such as WordPress or Twitter-compatible APIs for a
3420
			# blogging extension, etc)
3421
			$tags['rsd'] = Html::element( 'link', [
3422
				'rel' => 'EditURI',
3423
				'type' => 'application/rsd+xml',
3424
				// Output a protocol-relative URL here if $wgServer is protocol-relative.
3425
				// Whether RSD accepts relative or protocol-relative URLs is completely
3426
				// undocumented, though.
3427
				'href' => wfExpandUrl( wfAppendQuery(
3428
					wfScript( 'api' ),
3429
					[ 'action' => 'rsd' ] ),
3430
					PROTO_RELATIVE
3431
				),
3432
			] );
3433
		}
3434
3435
		# Language variants
3436
		if ( !$config->get( 'DisableLangConversion' ) ) {
3437
			$lang = $this->getTitle()->getPageLanguage();
3438
			if ( $lang->hasVariants() ) {
3439
				$variants = $lang->getVariants();
3440
				foreach ( $variants as $variant ) {
3441
					$tags["variant-$variant"] = Html::element( 'link', [
3442
						'rel' => 'alternate',
3443
						'hreflang' => wfBCP47( $variant ),
3444
						'href' => $this->getTitle()->getLocalURL(
3445
							[ 'variant' => $variant ] )
3446
						]
3447
					);
3448
				}
3449
				# x-default link per https://support.google.com/webmasters/answer/189077?hl=en
3450
				$tags["variant-x-default"] = Html::element( 'link', [
3451
					'rel' => 'alternate',
3452
					'hreflang' => 'x-default',
3453
					'href' => $this->getTitle()->getLocalURL() ] );
3454
			}
3455
		}
3456
3457
		# Copyright
3458
		if ( $this->copyrightUrl !== null ) {
3459
			$copyright = $this->copyrightUrl;
3460
		} else {
3461
			$copyright = '';
3462
			if ( $config->get( 'RightsPage' ) ) {
3463
				$copy = Title::newFromText( $config->get( 'RightsPage' ) );
3464
3465
				if ( $copy ) {
3466
					$copyright = $copy->getLocalURL();
3467
				}
3468
			}
3469
3470
			if ( !$copyright && $config->get( 'RightsUrl' ) ) {
3471
				$copyright = $config->get( 'RightsUrl' );
3472
			}
3473
		}
3474
3475
		if ( $copyright ) {
3476
			$tags['copyright'] = Html::element( 'link', [
3477
				'rel' => 'copyright',
3478
				'href' => $copyright ]
3479
			);
3480
		}
3481
3482
		# Feeds
3483
		if ( $config->get( 'Feed' ) ) {
3484
			$feedLinks = [];
3485
3486
			foreach ( $this->getSyndicationLinks() as $format => $link ) {
3487
				# Use the page name for the title.  In principle, this could
3488
				# lead to issues with having the same name for different feeds
3489
				# corresponding to the same page, but we can't avoid that at
3490
				# this low a level.
3491
3492
				$feedLinks[] = $this->feedLink(
3493
					$format,
3494
					$link,
3495
					# Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
3496
					$this->msg(
3497
						"page-{$format}-feed", $this->getTitle()->getPrefixedText()
3498
					)->text()
3499
				);
3500
			}
3501
3502
			# Recent changes feed should appear on every page (except recentchanges,
3503
			# that would be redundant). Put it after the per-page feed to avoid
3504
			# changing existing behavior. It's still available, probably via a
3505
			# menu in your browser. Some sites might have a different feed they'd
3506
			# like to promote instead of the RC feed (maybe like a "Recent New Articles"
3507
			# or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
3508
			# If so, use it instead.
3509
			$sitename = $config->get( 'Sitename' );
3510
			if ( $config->get( 'OverrideSiteFeed' ) ) {
3511
				foreach ( $config->get( 'OverrideSiteFeed' ) as $type => $feedUrl ) {
3512
					// Note, this->feedLink escapes the url.
3513
					$feedLinks[] = $this->feedLink(
3514
						$type,
3515
						$feedUrl,
3516
						$this->msg( "site-{$type}-feed", $sitename )->text()
3517
					);
3518
				}
3519
			} elseif ( !$this->getTitle()->isSpecial( 'Recentchanges' ) ) {
3520
				$rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
3521
				foreach ( $config->get( 'AdvertisedFeedTypes' ) as $format ) {
3522
					$feedLinks[] = $this->feedLink(
3523
						$format,
3524
						$rctitle->getLocalURL( [ 'feed' => $format ] ),
3525
						# For grep: 'site-rss-feed', 'site-atom-feed'
3526
						$this->msg( "site-{$format}-feed", $sitename )->text()
3527
					);
3528
				}
3529
			}
3530
3531
			# Allow extensions to change the list pf feeds. This hook is primarily for changing,
3532
			# manipulating or removing existing feed tags. If you want to add new feeds, you should
3533
			# use OutputPage::addFeedLink() instead.
3534
			Hooks::run( 'AfterBuildFeedLinks', [ &$feedLinks ] );
3535
3536
			$tags += $feedLinks;
3537
		}
3538
3539
		# Canonical URL
3540
		if ( $config->get( 'EnableCanonicalServerLink' ) ) {
3541
			if ( $canonicalUrl !== false ) {
3542
				$canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL );
3543
			} else {
3544
				if ( $this->isArticleRelated() ) {
3545
					// This affects all requests where "setArticleRelated" is true. This is
3546
					// typically all requests that show content (query title, curid, oldid, diff),
3547
					// and all wikipage actions (edit, delete, purge, info, history etc.).
3548
					// It does not apply to File pages and Special pages.
3549
					// 'history' and 'info' actions address page metadata rather than the page
3550
					// content itself, so they may not be canonicalized to the view page url.
3551
					// TODO: this ought to be better encapsulated in the Action class.
3552
					$action = Action::getActionName( $this->getContext() );
3553
					if ( in_array( $action, [ 'history', 'info' ] ) ) {
3554
						$query = "action={$action}";
3555
					} else {
3556
						$query = '';
3557
					}
3558
					$canonicalUrl = $this->getTitle()->getCanonicalURL( $query );
3559
				} else {
3560
					$reqUrl = $this->getRequest()->getRequestURL();
3561
					$canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL );
3562
				}
3563
			}
3564
		}
3565
		if ( $canonicalUrl !== false ) {
3566
			$tags[] = Html::element( 'link', [
3567
				'rel' => 'canonical',
3568
				'href' => $canonicalUrl
3569
			] );
3570
		}
3571
3572
		return $tags;
3573
	}
3574
3575
	/**
3576
	 * @return string HTML tag links to be put in the header.
3577
	 * @deprecated since 1.24 Use OutputPage::headElement or if you have to,
3578
	 *   OutputPage::getHeadLinksArray directly.
3579
	 */
3580
	public function getHeadLinks() {
3581
		wfDeprecated( __METHOD__, '1.24' );
3582
		return implode( "\n", $this->getHeadLinksArray() );
3583
	}
3584
3585
	/**
3586
	 * Generate a "<link rel/>" for a feed.
3587
	 *
3588
	 * @param string $type Feed type
3589
	 * @param string $url URL to the feed
3590
	 * @param string $text Value of the "title" attribute
3591
	 * @return string HTML fragment
3592
	 */
3593
	private function feedLink( $type, $url, $text ) {
3594
		return Html::element( 'link', [
3595
			'rel' => 'alternate',
3596
			'type' => "application/$type+xml",
3597
			'title' => $text,
3598
			'href' => $url ]
3599
		);
3600
	}
3601
3602
	/**
3603
	 * Add a local or specified stylesheet, with the given media options.
3604
	 * Internal use only. Use OutputPage::addModuleStyles() if possible.
3605
	 *
3606
	 * @param string $style URL to the file
3607
	 * @param string $media To specify a media type, 'screen', 'printable', 'handheld' or any.
3608
	 * @param string $condition For IE conditional comments, specifying an IE version
3609
	 * @param string $dir Set to 'rtl' or 'ltr' for direction-specific sheets
3610
	 */
3611
	public function addStyle( $style, $media = '', $condition = '', $dir = '' ) {
3612
		$options = [];
3613
		if ( $media ) {
3614
			$options['media'] = $media;
3615
		}
3616
		if ( $condition ) {
3617
			$options['condition'] = $condition;
3618
		}
3619
		if ( $dir ) {
3620
			$options['dir'] = $dir;
3621
		}
3622
		$this->styles[$style] = $options;
3623
	}
3624
3625
	/**
3626
	 * Adds inline CSS styles
3627
	 * Internal use only. Use OutputPage::addModuleStyles() if possible.
3628
	 *
3629
	 * @param mixed $style_css Inline CSS
3630
	 * @param string $flip Set to 'flip' to flip the CSS if needed
3631
	 */
3632
	public function addInlineStyle( $style_css, $flip = 'noflip' ) {
3633
		if ( $flip === 'flip' && $this->getLanguage()->isRTL() ) {
3634
			# If wanted, and the interface is right-to-left, flip the CSS
3635
			$style_css = CSSJanus::transform( $style_css, true, false );
3636
		}
3637
		$this->mInlineStyles .= Html::inlineStyle( $style_css );
3638
	}
3639
3640
	/**
3641
	 * Build a set of "<link>" elements for the stylesheets specified in the $this->styles array.
3642
	 * These will be applied to various media & IE conditionals.
3643
	 *
3644
	 * @return string
3645
	 */
3646
	public function buildCssLinks() {
3647
		global $wgContLang;
3648
3649
		$this->getSkin()->setupSkinUserCss( $this );
3650
3651
		// Add ResourceLoader styles
3652
		// Split the styles into these groups
3653
		$styles = [
3654
			'other' => [],
3655
			'user' => [],
3656
			'site' => [],
3657
			'private' => [],
3658
			'noscript' => []
3659
		];
3660
		$links = [];
3661
		$otherTags = []; // Tags to append after the normal <link> tags
3662
		$resourceLoader = $this->getResourceLoader();
3663
3664
		$moduleStyles = $this->getModuleStyles();
3665
3666
		// Per-site custom styles
3667
		$moduleStyles[] = 'site';
3668
		$moduleStyles[] = 'noscript';
3669
3670
		// Per-user custom styles
3671
		if ( $this->getConfig()->get( 'AllowUserCss' ) && $this->getTitle()->isCssSubpage()
3672
			&& $this->userCanPreview()
3673
		) {
3674
			// We're on a preview of a CSS subpage
3675
			// Exclude this page from the user module in case it's in there (bug 26283)
3676
			$link = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES,
3677
				[ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
3678
			);
3679
			$otherTags = array_merge( $otherTags, $link['html'] );
3680
3681
			// Load the previewed CSS
3682
			// If needed, Janus it first. This is user-supplied CSS, so it's
3683
			// assumed to be right for the content language directionality.
3684
			$previewedCSS = $this->getRequest()->getText( 'wpTextbox1' );
3685
			if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) {
3686
				$previewedCSS = CSSJanus::transform( $previewedCSS, true, false );
3687
			}
3688
			$otherTags[] = Html::inlineStyle( $previewedCSS );
3689
		} else {
3690
			// Load the user styles normally
3691
			$moduleStyles[] = 'user';
3692
		}
3693
3694
		// Per-user preference styles
3695
		$moduleStyles[] = 'user.cssprefs';
3696
3697
		foreach ( $moduleStyles as $name ) {
3698
			$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...
3699
			if ( !$module ) {
3700
				continue;
3701
			}
3702
			if ( $name === 'site' ) {
3703
				// HACK: The site module shouldn't be fragmented with a cache group and
3704
				// http request. But in order to ensure its styles are separated and after the
3705
				// ResourceLoaderDynamicStyles marker, pretend it is in a group called 'site'.
3706
				// The scripts remain ungrouped and rides the bottom queue.
3707
				$styles['site'][] = $name;
3708
				continue;
3709
			}
3710
			$group = $module->getGroup();
3711
			// Modules in groups other than the ones needing special treatment
3712
			// (see $styles assignment)
3713
			// will be placed in the "other" style category.
3714
			$styles[isset( $styles[$group] ) ? $group : 'other'][] = $name;
3715
		}
3716
3717
		// We want site, private and user styles to override dynamically added
3718
		// styles from modules, but we want dynamically added styles to override
3719
		// statically added styles from other modules. So the order has to be
3720
		// other, dynamic, site, private, user. Add statically added styles for
3721
		// other modules
3722
		$links[] = $this->makeResourceLoaderLink(
3723
			$styles['other'],
3724
			ResourceLoaderModule::TYPE_STYLES
3725
		);
3726
		// Add normal styles added through addStyle()/addInlineStyle() here
3727
		$links[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3728
		// Add marker tag to mark the place where the client-side
3729
		// loader should inject dynamic styles
3730
		// We use a <meta> tag with a made-up name for this because that's valid HTML
3731
		$links[] = Html::element(
3732
			'meta',
3733
			[ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
3734
		);
3735
3736
		// Add site-specific and user-specific styles
3737
		// 'private' at present only contains user.options, so put that before 'user'
3738
		// Any future private modules will likely have a similar user-specific character
3739
		foreach ( [ 'site', 'noscript', 'private', 'user' ] as $group ) {
3740
			$links[] = $this->makeResourceLoaderLink( $styles[$group],
3741
				ResourceLoaderModule::TYPE_STYLES
3742
			);
3743
		}
3744
3745
		// Add stuff in $otherTags (previewed user CSS if applicable)
3746
		return self::getHtmlFromLoaderLinks( $links ) . implode( '', $otherTags );
3747
	}
3748
3749
	/**
3750
	 * @return array
3751
	 */
3752
	public function buildCssLinksArray() {
3753
		$links = [];
3754
3755
		// Add any extension CSS
3756
		foreach ( $this->mExtStyles as $url ) {
3757
			$this->addStyle( $url );
3758
		}
3759
		$this->mExtStyles = [];
3760
3761
		foreach ( $this->styles as $file => $options ) {
3762
			$link = $this->styleLink( $file, $options );
3763
			if ( $link ) {
3764
				$links[$file] = $link;
3765
			}
3766
		}
3767
		return $links;
3768
	}
3769
3770
	/**
3771
	 * Generate \<link\> tags for stylesheets
3772
	 *
3773
	 * @param string $style URL to the file
3774
	 * @param array $options Option, can contain 'condition', 'dir', 'media' keys
3775
	 * @return string HTML fragment
3776
	 */
3777
	protected function styleLink( $style, array $options ) {
3778
		if ( isset( $options['dir'] ) ) {
3779
			if ( $this->getLanguage()->getDir() != $options['dir'] ) {
3780
				return '';
3781
			}
3782
		}
3783
3784
		if ( isset( $options['media'] ) ) {
3785
			$media = self::transformCssMedia( $options['media'] );
3786
			if ( is_null( $media ) ) {
3787
				return '';
3788
			}
3789
		} else {
3790
			$media = 'all';
3791
		}
3792
3793
		if ( substr( $style, 0, 1 ) == '/' ||
3794
			substr( $style, 0, 5 ) == 'http:' ||
3795
			substr( $style, 0, 6 ) == 'https:' ) {
3796
			$url = $style;
3797
		} else {
3798
			$config = $this->getConfig();
3799
			$url = $config->get( 'StylePath' ) . '/' . $style . '?' .
3800
				$config->get( 'StyleVersion' );
3801
		}
3802
3803
		$link = Html::linkedStyle( $url, $media );
3804
3805
		if ( isset( $options['condition'] ) ) {
3806
			$condition = htmlspecialchars( $options['condition'] );
3807
			$link = "<!--[if $condition]>$link<![endif]-->";
3808
		}
3809
		return $link;
3810
	}
3811
3812
	/**
3813
	 * Transform path to web-accessible static resource.
3814
	 *
3815
	 * This is used to add a validation hash as query string.
3816
	 * This aids various behaviors:
3817
	 *
3818
	 * - Put long Cache-Control max-age headers on responses for improved
3819
	 *   cache performance.
3820
	 * - Get the correct version of a file as expected by the current page.
3821
	 * - Instantly get the updated version of a file after deployment.
3822
	 *
3823
	 * Avoid using this for urls included in HTML as otherwise clients may get different
3824
	 * versions of a resource when navigating the site depending on when the page was cached.
3825
	 * If changes to the url propagate, this is not a problem (e.g. if the url is in
3826
	 * an external stylesheet).
3827
	 *
3828
	 * @since 1.27
3829
	 * @param Config $config
3830
	 * @param string $path Path-absolute URL to file (from document root, must start with "/")
3831
	 * @return string URL
3832
	 */
3833
	public static function transformResourcePath( Config $config, $path ) {
3834
		global $IP;
3835
		$remotePathPrefix = $config->get( 'ResourceBasePath' );
3836
		if ( $remotePathPrefix === '' ) {
3837
			// The configured base path is required to be empty string for
3838
			// wikis in the domain root
3839
			$remotePath = '/';
3840
		} else {
3841
			$remotePath = $remotePathPrefix;
3842
		}
3843
		if ( strpos( $path, $remotePath ) !== 0 ) {
3844
			// Path is outside wgResourceBasePath, ignore.
3845
			return $path;
3846
		}
3847
		$path = RelPath\getRelativePath( $path, $remotePath );
3848
		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 3847 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...
3849
	}
3850
3851
	/**
3852
	 * Utility method for transformResourceFilePath().
3853
	 *
3854
	 * Caller is responsible for ensuring the file exists. Emits a PHP warning otherwise.
3855
	 *
3856
	 * @since 1.27
3857
	 * @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...
3858
	 * @param string $localPath File directory exposed at $remotePath
3859
	 * @param string $file Path to target file relative to $localPath
3860
	 * @return string URL
3861
	 */
3862
	public static function transformFilePath( $remotePathPrefix, $localPath, $file ) {
3863
		$hash = md5_file( "$localPath/$file" );
3864
		if ( $hash === false ) {
3865
			wfLogWarning( __METHOD__ . ": Failed to hash $localPath/$file" );
3866
			$hash = '';
3867
		}
3868
		return "$remotePathPrefix/$file?" . substr( $hash, 0, 5 );
3869
	}
3870
3871
	/**
3872
	 * Transform "media" attribute based on request parameters
3873
	 *
3874
	 * @param string $media Current value of the "media" attribute
3875
	 * @return string Modified value of the "media" attribute, or null to skip
3876
	 * this stylesheet
3877
	 */
3878
	public static function transformCssMedia( $media ) {
3879
		global $wgRequest;
3880
3881
		// http://www.w3.org/TR/css3-mediaqueries/#syntax
3882
		$screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
3883
3884
		// Switch in on-screen display for media testing
3885
		$switches = [
3886
			'printable' => 'print',
3887
			'handheld' => 'handheld',
3888
		];
3889
		foreach ( $switches as $switch => $targetMedia ) {
3890
			if ( $wgRequest->getBool( $switch ) ) {
3891
				if ( $media == $targetMedia ) {
3892
					$media = '';
3893
				} elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
3894
					/* This regex will not attempt to understand a comma-separated media_query_list
3895
					 *
3896
					 * Example supported values for $media:
3897
					 * 'screen', 'only screen', 'screen and (min-width: 982px)' ),
3898
					 * Example NOT supported value for $media:
3899
					 * '3d-glasses, screen, print and resolution > 90dpi'
3900
					 *
3901
					 * If it's a print request, we never want any kind of screen stylesheets
3902
					 * If it's a handheld request (currently the only other choice with a switch),
3903
					 * we don't want simple 'screen' but we might want screen queries that
3904
					 * have a max-width or something, so we'll pass all others on and let the
3905
					 * client do the query.
3906
					 */
3907
					if ( $targetMedia == 'print' || $media == 'screen' ) {
3908
						return null;
3909
					}
3910
				}
3911
			}
3912
		}
3913
3914
		return $media;
3915
	}
3916
3917
	/**
3918
	 * Add a wikitext-formatted message to the output.
3919
	 * This is equivalent to:
3920
	 *
3921
	 *    $wgOut->addWikiText( wfMessage( ... )->plain() )
3922
	 */
3923
	public function addWikiMsg( /*...*/ ) {
3924
		$args = func_get_args();
3925
		$name = array_shift( $args );
3926
		$this->addWikiMsgArray( $name, $args );
3927
	}
3928
3929
	/**
3930
	 * Add a wikitext-formatted message to the output.
3931
	 * Like addWikiMsg() except the parameters are taken as an array
3932
	 * instead of a variable argument list.
3933
	 *
3934
	 * @param string $name
3935
	 * @param array $args
3936
	 */
3937
	public function addWikiMsgArray( $name, $args ) {
3938
		$this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
3939
	}
3940
3941
	/**
3942
	 * This function takes a number of message/argument specifications, wraps them in
3943
	 * some overall structure, and then parses the result and adds it to the output.
3944
	 *
3945
	 * In the $wrap, $1 is replaced with the first message, $2 with the second,
3946
	 * and so on. The subsequent arguments may be either
3947
	 * 1) strings, in which case they are message names, or
3948
	 * 2) arrays, in which case, within each array, the first element is the message
3949
	 *    name, and subsequent elements are the parameters to that message.
3950
	 *
3951
	 * Don't use this for messages that are not in the user's interface language.
3952
	 *
3953
	 * For example:
3954
	 *
3955
	 *    $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>", 'some-error' );
3956
	 *
3957
	 * Is equivalent to:
3958
	 *
3959
	 *    $wgOut->addWikiText( "<div class='error'>\n"
3960
	 *        . wfMessage( 'some-error' )->plain() . "\n</div>" );
3961
	 *
3962
	 * The newline after the opening div is needed in some wikitext. See bug 19226.
3963
	 *
3964
	 * @param string $wrap
3965
	 */
3966
	public function wrapWikiMsg( $wrap /*, ...*/ ) {
3967
		$msgSpecs = func_get_args();
3968
		array_shift( $msgSpecs );
3969
		$msgSpecs = array_values( $msgSpecs );
3970
		$s = $wrap;
3971
		foreach ( $msgSpecs as $n => $spec ) {
3972
			if ( is_array( $spec ) ) {
3973
				$args = $spec;
3974
				$name = array_shift( $args );
3975
				if ( isset( $args['options'] ) ) {
3976
					unset( $args['options'] );
3977
					wfDeprecated(
3978
						'Adding "options" to ' . __METHOD__ . ' is no longer supported',
3979
						'1.20'
3980
					);
3981
				}
3982
			} else {
3983
				$args = [];
3984
				$name = $spec;
3985
			}
3986
			$s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
3987
		}
3988
		$this->addWikiText( $s );
3989
	}
3990
3991
	/**
3992
	 * Enables/disables TOC, doesn't override __NOTOC__
3993
	 * @param bool $flag
3994
	 * @since 1.22
3995
	 */
3996
	public function enableTOC( $flag = true ) {
3997
		$this->mEnableTOC = $flag;
3998
	}
3999
4000
	/**
4001
	 * @return bool
4002
	 * @since 1.22
4003
	 */
4004
	public function isTOCEnabled() {
4005
		return $this->mEnableTOC;
4006
	}
4007
4008
	/**
4009
	 * Enables/disables section edit links, doesn't override __NOEDITSECTION__
4010
	 * @param bool $flag
4011
	 * @since 1.23
4012
	 */
4013
	public function enableSectionEditLinks( $flag = true ) {
4014
		$this->mEnableSectionEditLinks = $flag;
4015
	}
4016
4017
	/**
4018
	 * @return bool
4019
	 * @since 1.23
4020
	 */
4021
	public function sectionEditLinksEnabled() {
4022
		return $this->mEnableSectionEditLinks;
4023
	}
4024
4025
	/**
4026
	 * Helper function to setup the PHP implementation of OOUI to use in this request.
4027
	 *
4028
	 * @since 1.26
4029
	 * @param String $skinName The Skin name to determine the correct OOUI theme
4030
	 * @param String $dir Language direction
4031
	 */
4032
	public static function setupOOUI( $skinName = '', $dir = 'ltr' ) {
4033
		$themes = ExtensionRegistry::getInstance()->getAttribute( 'SkinOOUIThemes' );
4034
		// Make keys (skin names) lowercase for case-insensitive matching.
4035
		$themes = array_change_key_case( $themes, CASE_LOWER );
4036
		$theme = isset( $themes[$skinName] ) ? $themes[$skinName] : 'MediaWiki';
4037
		// For example, 'OOUI\MediaWikiTheme'.
4038
		$themeClass = "OOUI\\{$theme}Theme";
4039
		OOUI\Theme::setSingleton( new $themeClass() );
4040
		OOUI\Element::setDefaultDir( $dir );
4041
	}
4042
4043
	/**
4044
	 * Add ResourceLoader module styles for OOUI and set up the PHP implementation of it for use with
4045
	 * MediaWiki and this OutputPage instance.
4046
	 *
4047
	 * @since 1.25
4048
	 */
4049
	public function enableOOUI() {
4050
		self::setupOOUI(
4051
			strtolower( $this->getSkin()->getSkinName() ),
4052
			$this->getLanguage()->getDir()
4053
		);
4054
		$this->addModuleStyles( [
4055
			'oojs-ui-core.styles',
4056
			'oojs-ui.styles.icons',
4057
			'oojs-ui.styles.indicators',
4058
			'oojs-ui.styles.textures',
4059
			'mediawiki.widgets.styles',
4060
		] );
4061
		// Used by 'skipFunction' of the four 'oojs-ui.styles.*' modules. Please don't treat this as a
4062
		// public API or you'll be severely disappointed when T87871 is fixed and it disappears.
4063
		$this->addMeta( 'X-OOUI-PHP', '1' );
4064
	}
4065
}
4066