Completed
Branch master (726f70)
by
unknown
25:29
created

OutputPage::setLimitReportData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
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
use WrappedString\WrappedStringList;
27
28
/**
29
 * This class should be covered by a general architecture document which does
30
 * not exist as of January 2011.  This is one of the Core classes and should
31
 * be read at least once by any new developers.
32
 *
33
 * This class is used to prepare the final rendering. A skin is then
34
 * applied to the output parameters (links, javascript, html, categories ...).
35
 *
36
 * @todo FIXME: Another class handles sending the whole page to the client.
37
 *
38
 * Some comments comes from a pairing session between Zak Greant and Antoine Musso
39
 * in November 2010.
40
 *
41
 * @todo document
42
 */
43
class OutputPage extends ContextSource {
44
	/** @var array Should be private. Used with addMeta() which adds "<meta>" */
45
	protected $mMetatags = [];
46
47
	/** @var array */
48
	protected $mLinktags = [];
49
50
	/** @var bool */
51
	protected $mCanonicalUrl = false;
52
53
	/**
54
	 * @var array Additional stylesheets. Looks like this is for extensions.
55
	 *   Might be replaced by ResourceLoader.
56
	 */
57
	protected $mExtStyles = [];
58
59
	/**
60
	 * @var string Should be private - has getter and setter. Contains
61
	 *   the HTML title */
62
	public $mPagetitle = '';
63
64
	/**
65
	 * @var string Contains all of the "<body>" content. Should be private we
66
	 *   got set/get accessors and the append() method.
67
	 */
68
	public $mBodytext = '';
69
70
	/**
71
	 * Holds the debug lines that will be output as comments in page source if
72
	 * $wgDebugComments is enabled. See also $wgShowDebug.
73
	 * @deprecated since 1.20; use MWDebug class instead.
74
	 */
75
	public $mDebugtext = '';
76
77
	/** @var string Stores contents of "<title>" tag */
78
	private $mHTMLtitle = '';
79
80
	/**
81
	 * @var bool Is the displayed content related to the source of the
82
	 *   corresponding wiki article.
83
	 */
84
	private $mIsarticle = false;
85
86
	/** @var bool Stores "article flag" toggle. */
87
	private $mIsArticleRelated = true;
88
89
	/**
90
	 * @var bool We have to set isPrintable(). Some pages should
91
	 * never be printed (ex: redirections).
92
	 */
93
	private $mPrintable = false;
94
95
	/**
96
	 * @var array Contains the page subtitle. Special pages usually have some
97
	 *   links here. Don't confuse with site subtitle added by skins.
98
	 */
99
	private $mSubtitle = [];
100
101
	/** @var string */
102
	public $mRedirect = '';
103
104
	/** @var int */
105
	protected $mStatusCode;
106
107
	/**
108
	 * @var string Used for sending cache control.
109
	 *   The whole caching system should probably be moved into its own class.
110
	 */
111
	protected $mLastModified = '';
112
113
	/** @var array */
114
	protected $mCategoryLinks = [];
115
116
	/** @var array */
117
	protected $mCategories = [];
118
119
	/** @var array */
120
	protected $mIndicators = [];
121
122
	/** @var array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page') */
123
	private $mLanguageLinks = [];
124
125
	/**
126
	 * Used for JavaScript (predates ResourceLoader)
127
	 * @todo We should split JS / CSS.
128
	 * mScripts content is inserted as is in "<head>" by Skin. This might
129
	 * contain either a link to a stylesheet or inline CSS.
130
	 */
131
	private $mScripts = '';
132
133
	/** @var string Inline CSS styles. Use addInlineStyle() sparingly */
134
	protected $mInlineStyles = '';
135
136
	/**
137
	 * @var string Used by skin template.
138
	 * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle );
139
	 */
140
	public $mPageLinkTitle = '';
141
142
	/** @var array Array of elements in "<head>". Parser might add its own headers! */
143
	protected $mHeadItems = [];
144
145
	/** @var array */
146
	protected $mModules = [];
147
148
	/** @var array */
149
	protected $mModuleScripts = [];
150
151
	/** @var array */
152
	protected $mModuleStyles = [];
153
154
	/** @var ResourceLoader */
155
	protected $mResourceLoader;
156
157
	/** @var array */
158
	protected $mJsConfigVars = [];
159
160
	/** @var array */
161
	protected $mTemplateIds = [];
162
163
	/** @var array */
164
	protected $mImageTimeKeys = [];
165
166
	/** @var string */
167
	public $mRedirectCode = '';
168
169
	protected $mFeedLinksAppendQuery = null;
170
171
	/** @var array
172
	 * What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page?
173
	 * @see ResourceLoaderModule::$origin
174
	 * ResourceLoaderModule::ORIGIN_ALL is assumed unless overridden;
175
	 */
176
	protected $mAllowedModules = [
177
		ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
178
	];
179
180
	/** @var bool Whether output is disabled.  If this is true, the 'output' method will do nothing. */
181
	protected $mDoNothing = false;
182
183
	// Parser related.
184
185
	/** @var int */
186
	protected $mContainsNewMagic = 0;
187
188
	/**
189
	 * lazy initialised, use parserOptions()
190
	 * @var ParserOptions
191
	 */
192
	protected $mParserOptions = null;
193
194
	/**
195
	 * Handles the Atom / RSS links.
196
	 * We probably only support Atom in 2011.
197
	 * @see $wgAdvertisedFeedTypes
198
	 */
199
	private $mFeedLinks = [];
200
201
	// Gwicke work on squid caching? Roughly from 2003.
202
	protected $mEnableClientCache = true;
203
204
	/** @var bool Flag if output should only contain the body of the article. */
205
	private $mArticleBodyOnly = false;
206
207
	/** @var bool */
208
	protected $mNewSectionLink = false;
209
210
	/** @var bool */
211
	protected $mHideNewSectionLink = false;
212
213
	/**
214
	 * @var bool Comes from the parser. This was probably made to load CSS/JS
215
	 * only if we had "<gallery>". Used directly in CategoryPage.php.
216
	 * Looks like ResourceLoader can replace this.
217
	 */
218
	public $mNoGallery = false;
219
220
	/** @var string */
221
	private $mPageTitleActionText = '';
222
223
	/** @var int Cache stuff. Looks like mEnableClientCache */
224
	protected $mCdnMaxage = 0;
225
	/** @var int Upper limit on mCdnMaxage */
226
	protected $mCdnMaxageLimit = INF;
227
228
	/**
229
	 * @var bool Controls if anti-clickjacking / frame-breaking headers will
230
	 * be sent. This should be done for pages where edit actions are possible.
231
	 * Setters: $this->preventClickjacking() and $this->allowClickjacking().
232
	 */
233
	protected $mPreventClickjacking = true;
234
235
	/** @var int To include the variable {{REVISIONID}} */
236
	private $mRevisionId = null;
237
238
	/** @var string */
239
	private $mRevisionTimestamp = null;
240
241
	/** @var array */
242
	protected $mFileVersion = null;
243
244
	/**
245
	 * @var array An array of stylesheet filenames (relative from skins path),
246
	 * with options for CSS media, IE conditions, and RTL/LTR direction.
247
	 * For internal use; add settings in the skin via $this->addStyle()
248
	 *
249
	 * Style again! This seems like a code duplication since we already have
250
	 * mStyles. This is what makes Open Source amazing.
251
	 */
252
	protected $styles = [];
253
254
	private $mIndexPolicy = 'index';
255
	private $mFollowPolicy = 'follow';
256
	private $mVaryHeader = [
257
		'Accept-Encoding' => [ 'match=gzip' ],
258
	];
259
260
	/**
261
	 * If the current page was reached through a redirect, $mRedirectedFrom contains the Title
262
	 * of the redirect.
263
	 *
264
	 * @var Title
265
	 */
266
	private $mRedirectedFrom = null;
267
268
	/**
269
	 * Additional key => value data
270
	 */
271
	private $mProperties = [];
272
273
	/**
274
	 * @var string|null ResourceLoader target for load.php links. If null, will be omitted
275
	 */
276
	private $mTarget = null;
277
278
	/**
279
	 * @var bool Whether parser output should contain table of contents
280
	 */
281
	private $mEnableTOC = true;
282
283
	/**
284
	 * @var bool Whether parser output should contain section edit links
285
	 */
286
	private $mEnableSectionEditLinks = true;
287
288
	/**
289
	 * @var string|null The URL to send in a <link> element with rel=copyright
290
	 */
291
	private $copyrightUrl;
292
293
	/** @var array Profiling data */
294
	private $limitReportData = [];
295
296
	/**
297
	 * Constructor for OutputPage. This should not be called directly.
298
	 * Instead a new RequestContext should be created and it will implicitly create
299
	 * a OutputPage tied to that context.
300
	 * @param IContextSource|null $context
301
	 */
302
	function __construct( IContextSource $context = null ) {
303
		if ( $context === null ) {
304
			# Extensions should use `new RequestContext` instead of `new OutputPage` now.
305
			wfDeprecated( __METHOD__, '1.18' );
306
		} else {
307
			$this->setContext( $context );
308
		}
309
	}
310
311
	/**
312
	 * Redirect to $url rather than displaying the normal page
313
	 *
314
	 * @param string $url URL
315
	 * @param string $responsecode HTTP status code
316
	 */
317
	public function redirect( $url, $responsecode = '302' ) {
318
		# Strip newlines as a paranoia check for header injection in PHP<5.1.2
319
		$this->mRedirect = str_replace( "\n", '', $url );
320
		$this->mRedirectCode = $responsecode;
321
	}
322
323
	/**
324
	 * Get the URL to redirect to, or an empty string if not redirect URL set
325
	 *
326
	 * @return string
327
	 */
328
	public function getRedirect() {
329
		return $this->mRedirect;
330
	}
331
332
	/**
333
	 * Set the copyright URL to send with the output.
334
	 * Empty string to omit, null to reset.
335
	 *
336
	 * @since 1.26
337
	 *
338
	 * @param string|null $url
339
	 */
340
	public function setCopyrightUrl( $url ) {
341
		$this->copyrightUrl = $url;
342
	}
343
344
	/**
345
	 * Set the HTTP status code to send with the output.
346
	 *
347
	 * @param int $statusCode
348
	 */
349
	public function setStatusCode( $statusCode ) {
350
		$this->mStatusCode = $statusCode;
351
	}
352
353
	/**
354
	 * Add a new "<meta>" tag
355
	 * To add an http-equiv meta tag, precede the name with "http:"
356
	 *
357
	 * @param string $name Tag name
358
	 * @param string $val Tag value
359
	 */
360
	function addMeta( $name, $val ) {
361
		array_push( $this->mMetatags, [ $name, $val ] );
362
	}
363
364
	/**
365
	 * Returns the current <meta> tags
366
	 *
367
	 * @since 1.25
368
	 * @return array
369
	 */
370
	public function getMetaTags() {
371
		return $this->mMetatags;
372
	}
373
374
	/**
375
	 * Add a new \<link\> tag to the page header.
376
	 *
377
	 * Note: use setCanonicalUrl() for rel=canonical.
378
	 *
379
	 * @param array $linkarr Associative array of attributes.
380
	 */
381
	function addLink( array $linkarr ) {
382
		array_push( $this->mLinktags, $linkarr );
383
	}
384
385
	/**
386
	 * Returns the current <link> tags
387
	 *
388
	 * @since 1.25
389
	 * @return array
390
	 */
391
	public function getLinkTags() {
392
		return $this->mLinktags;
393
	}
394
395
	/**
396
	 * Add a new \<link\> with "rel" attribute set to "meta"
397
	 *
398
	 * @param array $linkarr Associative array mapping attribute names to their
399
	 *                 values, both keys and values will be escaped, and the
400
	 *                 "rel" attribute will be automatically added
401
	 */
402
	function addMetadataLink( array $linkarr ) {
403
		$linkarr['rel'] = $this->getMetadataAttribute();
404
		$this->addLink( $linkarr );
405
	}
406
407
	/**
408
	 * Set the URL to be used for the <link rel=canonical>. This should be used
409
	 * in preference to addLink(), to avoid duplicate link tags.
410
	 * @param string $url
411
	 */
412
	function setCanonicalUrl( $url ) {
413
		$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...
414
	}
415
416
	/**
417
	 * Returns the URL to be used for the <link rel=canonical> if
418
	 * one is set.
419
	 *
420
	 * @since 1.25
421
	 * @return bool|string
422
	 */
423
	public function getCanonicalUrl() {
424
		return $this->mCanonicalUrl;
425
	}
426
427
	/**
428
	 * Get the value of the "rel" attribute for metadata links
429
	 *
430
	 * @return string
431
	 */
432
	public function getMetadataAttribute() {
433
		# note: buggy CC software only reads first "meta" link
434
		static $haveMeta = false;
435
		if ( $haveMeta ) {
436
			return 'alternate meta';
437
		} else {
438
			$haveMeta = true;
439
			return 'meta';
440
		}
441
	}
442
443
	/**
444
	 * Add raw HTML to the list of scripts (including \<script\> tag, etc.)
445
	 * Internal use only. Use OutputPage::addModules() or OutputPage::addJsConfigVars()
446
	 * if possible.
447
	 *
448
	 * @param string $script Raw HTML
449
	 */
450
	function addScript( $script ) {
451
		$this->mScripts .= $script;
452
	}
453
454
	/**
455
	 * Register and add a stylesheet from an extension directory.
456
	 *
457
	 * @deprecated since 1.27 use addModuleStyles() or addStyle() instead
458
	 * @param string $url Path to sheet.  Provide either a full url (beginning
459
	 *             with 'http', etc) or a relative path from the document root
460
	 *             (beginning with '/').  Otherwise it behaves identically to
461
	 *             addStyle() and draws from the /skins folder.
462
	 */
463
	public function addExtensionStyle( $url ) {
464
		wfDeprecated( __METHOD__, '1.27' );
465
		array_push( $this->mExtStyles, $url );
466
	}
467
468
	/**
469
	 * Get all styles added by extensions
470
	 *
471
	 * @deprecated since 1.27
472
	 * @return array
473
	 */
474
	function getExtStyle() {
475
		wfDeprecated( __METHOD__, '1.27' );
476
		return $this->mExtStyles;
477
	}
478
479
	/**
480
	 * Add a JavaScript file out of skins/common, or a given relative path.
481
	 * Internal use only. Use OutputPage::addModules() if possible.
482
	 *
483
	 * @param string $file Filename in skins/common or complete on-server path
484
	 *              (/foo/bar.js)
485
	 * @param string $version Style version of the file. Defaults to $wgStyleVersion
486
	 */
487
	public function addScriptFile( $file, $version = null ) {
488
		// See if $file parameter is an absolute URL or begins with a slash
489
		if ( substr( $file, 0, 1 ) == '/' || preg_match( '#^[a-z]*://#i', $file ) ) {
490
			$path = $file;
491
		} else {
492
			$path = $this->getConfig()->get( 'StylePath' ) . "/common/{$file}";
493
		}
494
		if ( is_null( $version ) ) {
495
			$version = $this->getConfig()->get( 'StyleVersion' );
496
		}
497
		$this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ) ) );
498
	}
499
500
	/**
501
	 * Add a self-contained script tag with the given contents
502
	 * Internal use only. Use OutputPage::addModules() if possible.
503
	 *
504
	 * @param string $script JavaScript text, no "<script>" tags
505
	 */
506
	public function addInlineScript( $script ) {
507
		$this->mScripts .= Html::inlineScript( $script );
508
	}
509
510
	/**
511
	 * Filter an array of modules to remove insufficiently trustworthy members, and modules
512
	 * which are no longer registered (eg a page is cached before an extension is disabled)
513
	 * @param array $modules
514
	 * @param string|null $position If not null, only return modules with this position
515
	 * @param string $type
516
	 * @return array
517
	 */
518
	protected function filterModules( array $modules, $position = null,
519
		$type = ResourceLoaderModule::TYPE_COMBINED
520
	) {
521
		$resourceLoader = $this->getResourceLoader();
522
		$filteredModules = [];
523
		foreach ( $modules as $val ) {
524
			$module = $resourceLoader->getModule( $val );
525
			if ( $module instanceof ResourceLoaderModule
526
				&& $module->getOrigin() <= $this->getAllowedModules( $type )
527
				&& ( is_null( $position ) || $module->getPosition() == $position )
528
				&& ( !$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...
529
			) {
530
				$filteredModules[] = $val;
531
			}
532
		}
533
		return $filteredModules;
534
	}
535
536
	/**
537
	 * Get the list of modules to include on this page
538
	 *
539
	 * @param bool $filter Whether to filter out insufficiently trustworthy modules
540
	 * @param string|null $position If not null, only return modules with this position
541
	 * @param string $param
542
	 * @return array Array of module names
543
	 */
544
	public function getModules( $filter = false, $position = null, $param = 'mModules' ) {
545
		$modules = array_values( array_unique( $this->$param ) );
546
		return $filter
547
			? $this->filterModules( $modules, $position )
548
			: $modules;
549
	}
550
551
	/**
552
	 * Add one or more modules recognized by ResourceLoader. Modules added
553
	 * through this function will be loaded by ResourceLoader when the
554
	 * page loads.
555
	 *
556
	 * @param string|array $modules Module name (string) or array of module names
557
	 */
558
	public function addModules( $modules ) {
559
		$this->mModules = array_merge( $this->mModules, (array)$modules );
560
	}
561
562
	/**
563
	 * Get the list of module JS to include on this page
564
	 *
565
	 * @param bool $filter
566
	 * @param string|null $position
567
	 *
568
	 * @return array Array of module names
569
	 */
570
	public function getModuleScripts( $filter = false, $position = null ) {
571
		return $this->getModules( $filter, $position, 'mModuleScripts' );
572
	}
573
574
	/**
575
	 * Add only JS of one or more modules recognized by ResourceLoader. Module
576
	 * scripts added through this function will be loaded by ResourceLoader when
577
	 * the page loads.
578
	 *
579
	 * @param string|array $modules Module name (string) or array of module names
580
	 */
581
	public function addModuleScripts( $modules ) {
582
		$this->mModuleScripts = array_merge( $this->mModuleScripts, (array)$modules );
583
	}
584
585
	/**
586
	 * Get the list of module CSS to include on this page
587
	 *
588
	 * @param bool $filter
589
	 * @param string|null $position
590
	 *
591
	 * @return array Array of module names
592
	 */
593
	public function getModuleStyles( $filter = false, $position = null ) {
594
		return $this->getModules( $filter, $position, 'mModuleStyles' );
595
	}
596
597
	/**
598
	 * Add only CSS of one or more modules recognized by ResourceLoader.
599
	 *
600
	 * Module styles added through this function will be added using standard link CSS
601
	 * tags, rather than as a combined Javascript and CSS package. Thus, they will
602
	 * load when JavaScript is disabled (unless CSS also happens to be disabled).
603
	 *
604
	 * @param string|array $modules Module name (string) or array of module names
605
	 */
606
	public function addModuleStyles( $modules ) {
607
		$this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
608
	}
609
610
	/**
611
	 * @return null|string ResourceLoader target
612
	 */
613
	public function getTarget() {
614
		return $this->mTarget;
615
	}
616
617
	/**
618
	 * Sets ResourceLoader target for load.php links. If null, will be omitted
619
	 *
620
	 * @param string|null $target
621
	 */
622
	public function setTarget( $target ) {
623
		$this->mTarget = $target;
624
	}
625
626
	/**
627
	 * Get an array of head items
628
	 *
629
	 * @return array
630
	 */
631
	function getHeadItemsArray() {
632
		return $this->mHeadItems;
633
	}
634
635
	/**
636
	 * Add or replace a head item to the output
637
	 *
638
	 * Whenever possible, use more specific options like ResourceLoader modules,
639
	 * OutputPage::addLink(), OutputPage::addMetaLink() and OutputPage::addFeedLink()
640
	 * Fallback options for those are: OutputPage::addStyle, OutputPage::addScript(),
641
	 * OutputPage::addInlineScript() and OutputPage::addInlineStyle()
642
	 * This would be your very LAST fallback.
643
	 *
644
	 * @param string $name Item name
645
	 * @param string $value Raw HTML
646
	 */
647
	public function addHeadItem( $name, $value ) {
648
		$this->mHeadItems[$name] = $value;
649
	}
650
651
	/**
652
	 * Add one or more head items to the output
653
	 *
654
	 * @since 1.28
655
	 * @param string|string[] $value Raw HTML
0 ignored issues
show
Documentation introduced by
There is no parameter named $value. Did you maybe mean $values?

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

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

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

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

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