OutputPage::setCanonicalUrl()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Bug introduced by
It seems like $lb->constructSet('page', $dbr) targeting LinkBatch::constructSet() can also be of type boolean; however, Database::select() does only seem to accept string, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2726
			ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $userBatch );
0 ignored issues
show
Bug introduced by
It seems like $dbr defined by wfGetDB(DB_REPLICA) on line 2724 can be null; however, ResourceLoaderWikiModule::preloadTitleInfo() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2727
2728
			// Filter out modules handled by buildExemptModules()
2729
			$moduleStyles = array_filter( $moduleStyles,
2730
				function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
2731
					$module = $rl->getModule( $name );
2732
					if ( $module ) {
2733
						if ( $name === 'user.styles' && $this->isUserCssPreview() ) {
2734
							$exemptStates[$name] = 'ready';
2735
							// Special case in buildExemptModules()
2736
							return false;
2737
						}
2738
						$group = $module->getGroup();
2739
						if ( isset( $exemptGroups[$group] ) ) {
2740
							$exemptStates[$name] = 'ready';
2741
							if ( !$module->isKnownEmpty( $context ) ) {
2742
								// E.g. Don't output empty <styles>
2743
								$exemptGroups[$group][] = $name;
2744
							}
2745
							return false;
2746
						}
2747
					}
2748
					return true;
2749
				}
2750
			);
2751
			$this->rlExemptStyleModules = $exemptGroups;
2752
2753
			$isUserModuleFiltered = !$this->filterModules( [ 'user' ] );
2754
			// If this page filters out 'user', makeResourceLoaderLink will drop it.
2755
			// Avoid indefinite "loading" state or untrue "ready" state (T145368).
2756
			if ( !$isUserModuleFiltered ) {
2757
				// Manually handled by getBottomScripts()
2758
				$userModule = $rl->getModule( 'user' );
2759
				$userState = $userModule->isKnownEmpty( $context ) && !$this->isUserJsPreview()
2760
					? 'ready'
2761
					: 'loading';
2762
				$this->rlUserModuleState = $exemptStates['user'] = $userState;
2763
			}
2764
2765
			$rlClient = new ResourceLoaderClientHtml( $context, $this->getTarget() );
0 ignored issues
show
Bug introduced by
It seems like $this->getTarget() targeting OutputPage::getTarget() can also be of type string; however, ResourceLoaderClientHtml::__construct() does only seem to accept object<aray>|null, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

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

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

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

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

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

Loading history...
3503
		$chunks = [];
3504
		// Things that go after the ResourceLoaderDynamicStyles marker
3505
		$append = [];
3506
3507
		// Exempt 'user' styles module (may need 'excludepages' for live preview)
3508
		if ( $this->isUserCssPreview() ) {
3509
			$append[] = $this->makeResourceLoaderLink(
3510
				'user.styles',
3511
				ResourceLoaderModule::TYPE_STYLES,
3512
				[ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
3513
			);
3514
3515
			// Load the previewed CSS. Janus it if needed.
3516
			// User-supplied CSS is assumed to in the wiki's content language.
3517
			$previewedCSS = $this->getRequest()->getText( 'wpTextbox1' );
3518
			if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) {
3519
				$previewedCSS = CSSJanus::transform( $previewedCSS, true, false );
3520
			}
3521
			$append[] = Html::inlineStyle( $previewedCSS );
3522
		}
3523
3524
		// We want site, private and user styles to override dynamically added styles from
3525
		// general modules, but we want dynamically added styles to override statically added
3526
		// style modules. So the order has to be:
3527
		// - page style modules (formatted by ResourceLoaderClientHtml::getHeadHtml())
3528
		// - dynamically loaded styles (added by mw.loader before ResourceLoaderDynamicStyles)
3529
		// - ResourceLoaderDynamicStyles marker
3530
		// - site/private/user styles
3531
3532
		// Add legacy styles added through addStyle()/addInlineStyle() here
3533
		$chunks[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
3534
3535
		$chunks[] = Html::element(
3536
			'meta',
3537
			[ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
3538
		);
3539
3540
		foreach ( $this->rlExemptStyleModules as $group => $moduleNames ) {
3541
			$chunks[] = $this->makeResourceLoaderLink( $moduleNames,
3542
				ResourceLoaderModule::TYPE_STYLES
3543
			);
3544
		}
3545
3546
		return self::combineWrappedStrings( array_merge( $chunks, $append ) );
3547
	}
3548
3549
	/**
3550
	 * @return array
3551
	 */
3552
	public function buildCssLinksArray() {
3553
		$links = [];
3554
3555
		// Add any extension CSS
3556
		foreach ( $this->mExtStyles as $url ) {
3557
			$this->addStyle( $url );
3558
		}
3559
		$this->mExtStyles = [];
3560
3561
		foreach ( $this->styles as $file => $options ) {
3562
			$link = $this->styleLink( $file, $options );
3563
			if ( $link ) {
3564
				$links[$file] = $link;
3565
			}
3566
		}
3567
		return $links;
3568
	}
3569
3570
	/**
3571
	 * Generate \<link\> tags for stylesheets
3572
	 *
3573
	 * @param string $style URL to the file
3574
	 * @param array $options Option, can contain 'condition', 'dir', 'media' keys
3575
	 * @return string HTML fragment
3576
	 */
3577
	protected function styleLink( $style, array $options ) {
3578
		if ( isset( $options['dir'] ) ) {
3579
			if ( $this->getLanguage()->getDir() != $options['dir'] ) {
3580
				return '';
3581
			}
3582
		}
3583
3584
		if ( isset( $options['media'] ) ) {
3585
			$media = self::transformCssMedia( $options['media'] );
3586
			if ( is_null( $media ) ) {
3587
				return '';
3588
			}
3589
		} else {
3590
			$media = 'all';
3591
		}
3592
3593
		if ( substr( $style, 0, 1 ) == '/' ||
3594
			substr( $style, 0, 5 ) == 'http:' ||
3595
			substr( $style, 0, 6 ) == 'https:' ) {
3596
			$url = $style;
3597
		} else {
3598
			$config = $this->getConfig();
3599
			$url = $config->get( 'StylePath' ) . '/' . $style . '?' .
3600
				$config->get( 'StyleVersion' );
3601
		}
3602
3603
		$link = Html::linkedStyle( $url, $media );
3604
3605
		if ( isset( $options['condition'] ) ) {
3606
			$condition = htmlspecialchars( $options['condition'] );
3607
			$link = "<!--[if $condition]>$link<![endif]-->";
3608
		}
3609
		return $link;
3610
	}
3611
3612
	/**
3613
	 * Transform path to web-accessible static resource.
3614
	 *
3615
	 * This is used to add a validation hash as query string.
3616
	 * This aids various behaviors:
3617
	 *
3618
	 * - Put long Cache-Control max-age headers on responses for improved
3619
	 *   cache performance.
3620
	 * - Get the correct version of a file as expected by the current page.
3621
	 * - Instantly get the updated version of a file after deployment.
3622
	 *
3623
	 * Avoid using this for urls included in HTML as otherwise clients may get different
3624
	 * versions of a resource when navigating the site depending on when the page was cached.
3625
	 * If changes to the url propagate, this is not a problem (e.g. if the url is in
3626
	 * an external stylesheet).
3627
	 *
3628
	 * @since 1.27
3629
	 * @param Config $config
3630
	 * @param string $path Path-absolute URL to file (from document root, must start with "/")
3631
	 * @return string URL
3632
	 */
3633
	public static function transformResourcePath( Config $config, $path ) {
3634
		global $IP;
3635
		$remotePathPrefix = $config->get( 'ResourceBasePath' );
3636
		if ( $remotePathPrefix === '' ) {
3637
			// The configured base path is required to be empty string for
3638
			// wikis in the domain root
3639
			$remotePath = '/';
3640
		} else {
3641
			$remotePath = $remotePathPrefix;
3642
		}
3643
		if ( strpos( $path, $remotePath ) !== 0 ) {
3644
			// Path is outside wgResourceBasePath, ignore.
3645
			return $path;
3646
		}
3647
		$path = RelPath\getRelativePath( $path, $remotePath );
3648
		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 3647 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...
3649
	}
3650
3651
	/**
3652
	 * Utility method for transformResourceFilePath().
3653
	 *
3654
	 * Caller is responsible for ensuring the file exists. Emits a PHP warning otherwise.
3655
	 *
3656
	 * @since 1.27
3657
	 * @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...
3658
	 * @param string $localPath File directory exposed at $remotePath
3659
	 * @param string $file Path to target file relative to $localPath
3660
	 * @return string URL
3661
	 */
3662
	public static function transformFilePath( $remotePathPrefix, $localPath, $file ) {
3663
		$hash = md5_file( "$localPath/$file" );
3664
		if ( $hash === false ) {
3665
			wfLogWarning( __METHOD__ . ": Failed to hash $localPath/$file" );
3666
			$hash = '';
3667
		}
3668
		return "$remotePathPrefix/$file?" . substr( $hash, 0, 5 );
3669
	}
3670
3671
	/**
3672
	 * Transform "media" attribute based on request parameters
3673
	 *
3674
	 * @param string $media Current value of the "media" attribute
3675
	 * @return string Modified value of the "media" attribute, or null to skip
3676
	 * this stylesheet
3677
	 */
3678
	public static function transformCssMedia( $media ) {
3679
		global $wgRequest;
3680
3681
		// https://www.w3.org/TR/css3-mediaqueries/#syntax
3682
		$screenMediaQueryRegex = '/^(?:only\s+)?screen\b/i';
3683
3684
		// Switch in on-screen display for media testing
3685
		$switches = [
3686
			'printable' => 'print',
3687
			'handheld' => 'handheld',
3688
		];
3689
		foreach ( $switches as $switch => $targetMedia ) {
3690
			if ( $wgRequest->getBool( $switch ) ) {
3691
				if ( $media == $targetMedia ) {
3692
					$media = '';
3693
				} elseif ( preg_match( $screenMediaQueryRegex, $media ) === 1 ) {
3694
					/* This regex will not attempt to understand a comma-separated media_query_list
3695
					 *
3696
					 * Example supported values for $media:
3697
					 * 'screen', 'only screen', 'screen and (min-width: 982px)' ),
3698
					 * Example NOT supported value for $media:
3699
					 * '3d-glasses, screen, print and resolution > 90dpi'
3700
					 *
3701
					 * If it's a print request, we never want any kind of screen stylesheets
3702
					 * If it's a handheld request (currently the only other choice with a switch),
3703
					 * we don't want simple 'screen' but we might want screen queries that
3704
					 * have a max-width or something, so we'll pass all others on and let the
3705
					 * client do the query.
3706
					 */
3707
					if ( $targetMedia == 'print' || $media == 'screen' ) {
3708
						return null;
3709
					}
3710
				}
3711
			}
3712
		}
3713
3714
		return $media;
3715
	}
3716
3717
	/**
3718
	 * Add a wikitext-formatted message to the output.
3719
	 * This is equivalent to:
3720
	 *
3721
	 *    $wgOut->addWikiText( wfMessage( ... )->plain() )
3722
	 */
3723
	public function addWikiMsg( /*...*/ ) {
3724
		$args = func_get_args();
3725
		$name = array_shift( $args );
3726
		$this->addWikiMsgArray( $name, $args );
3727
	}
3728
3729
	/**
3730
	 * Add a wikitext-formatted message to the output.
3731
	 * Like addWikiMsg() except the parameters are taken as an array
3732
	 * instead of a variable argument list.
3733
	 *
3734
	 * @param string $name
3735
	 * @param array $args
3736
	 */
3737
	public function addWikiMsgArray( $name, $args ) {
3738
		$this->addHTML( $this->msg( $name, $args )->parseAsBlock() );
3739
	}
3740
3741
	/**
3742
	 * This function takes a number of message/argument specifications, wraps them in
3743
	 * some overall structure, and then parses the result and adds it to the output.
3744
	 *
3745
	 * In the $wrap, $1 is replaced with the first message, $2 with the second,
3746
	 * and so on. The subsequent arguments may be either
3747
	 * 1) strings, in which case they are message names, or
3748
	 * 2) arrays, in which case, within each array, the first element is the message
3749
	 *    name, and subsequent elements are the parameters to that message.
3750
	 *
3751
	 * Don't use this for messages that are not in the user's interface language.
3752
	 *
3753
	 * For example:
3754
	 *
3755
	 *    $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>", 'some-error' );
3756
	 *
3757
	 * Is equivalent to:
3758
	 *
3759
	 *    $wgOut->addWikiText( "<div class='error'>\n"
3760
	 *        . wfMessage( 'some-error' )->plain() . "\n</div>" );
3761
	 *
3762
	 * The newline after the opening div is needed in some wikitext. See bug 19226.
3763
	 *
3764
	 * @param string $wrap
3765
	 */
3766
	public function wrapWikiMsg( $wrap /*, ...*/ ) {
3767
		$msgSpecs = func_get_args();
3768
		array_shift( $msgSpecs );
3769
		$msgSpecs = array_values( $msgSpecs );
3770
		$s = $wrap;
3771
		foreach ( $msgSpecs as $n => $spec ) {
3772
			if ( is_array( $spec ) ) {
3773
				$args = $spec;
3774
				$name = array_shift( $args );
3775
				if ( isset( $args['options'] ) ) {
3776
					unset( $args['options'] );
3777
					wfDeprecated(
3778
						'Adding "options" to ' . __METHOD__ . ' is no longer supported',
3779
						'1.20'
3780
					);
3781
				}
3782
			} else {
3783
				$args = [];
3784
				$name = $spec;
3785
			}
3786
			$s = str_replace( '$' . ( $n + 1 ), $this->msg( $name, $args )->plain(), $s );
3787
		}
3788
		$this->addWikiText( $s );
3789
	}
3790
3791
	/**
3792
	 * Enables/disables TOC, doesn't override __NOTOC__
3793
	 * @param bool $flag
3794
	 * @since 1.22
3795
	 */
3796
	public function enableTOC( $flag = true ) {
3797
		$this->mEnableTOC = $flag;
3798
	}
3799
3800
	/**
3801
	 * @return bool
3802
	 * @since 1.22
3803
	 */
3804
	public function isTOCEnabled() {
3805
		return $this->mEnableTOC;
3806
	}
3807
3808
	/**
3809
	 * Enables/disables section edit links, doesn't override __NOEDITSECTION__
3810
	 * @param bool $flag
3811
	 * @since 1.23
3812
	 */
3813
	public function enableSectionEditLinks( $flag = true ) {
3814
		$this->mEnableSectionEditLinks = $flag;
3815
	}
3816
3817
	/**
3818
	 * @return bool
3819
	 * @since 1.23
3820
	 */
3821
	public function sectionEditLinksEnabled() {
3822
		return $this->mEnableSectionEditLinks;
3823
	}
3824
3825
	/**
3826
	 * Helper function to setup the PHP implementation of OOUI to use in this request.
3827
	 *
3828
	 * @since 1.26
3829
	 * @param String $skinName The Skin name to determine the correct OOUI theme
3830
	 * @param String $dir Language direction
3831
	 */
3832
	public static function setupOOUI( $skinName = '', $dir = 'ltr' ) {
3833
		$themes = ExtensionRegistry::getInstance()->getAttribute( 'SkinOOUIThemes' );
3834
		// Make keys (skin names) lowercase for case-insensitive matching.
3835
		$themes = array_change_key_case( $themes, CASE_LOWER );
3836
		$theme = isset( $themes[$skinName] ) ? $themes[$skinName] : 'MediaWiki';
3837
		// For example, 'OOUI\MediaWikiTheme'.
3838
		$themeClass = "OOUI\\{$theme}Theme";
3839
		OOUI\Theme::setSingleton( new $themeClass() );
3840
		OOUI\Element::setDefaultDir( $dir );
3841
	}
3842
3843
	/**
3844
	 * Add ResourceLoader module styles for OOUI and set up the PHP implementation of it for use with
3845
	 * MediaWiki and this OutputPage instance.
3846
	 *
3847
	 * @since 1.25
3848
	 */
3849
	public function enableOOUI() {
3850
		self::setupOOUI(
3851
			strtolower( $this->getSkin()->getSkinName() ),
3852
			$this->getLanguage()->getDir()
3853
		);
3854
		$this->addModuleStyles( [
3855
			'oojs-ui-core.styles',
3856
			'oojs-ui.styles.icons',
3857
			'oojs-ui.styles.indicators',
3858
			'oojs-ui.styles.textures',
3859
			'mediawiki.widgets.styles',
3860
		] );
3861
	}
3862
}
3863