Completed
Branch master (bbf110)
by
unknown
25:51
created

OutputPage::preventClickjacking()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

An additional type check may prevent trouble.

Loading history...
1274
			__METHOD__,
1275
			[],
1276
			[ 'page_props' => [ 'LEFT JOIN', [
1277
				'pp_propname' => 'hiddencat',
1278
				'pp_page = page_id'
1279
			] ] ]
1280
		);
1281
1282
		# Add the results to the link cache
1283
		$lb->addResultToCache( LinkCache::singleton(), $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $dbr->select(array('page...'pp_page = page_id')))) on line 1271 can also be of type boolean; however, LinkBatch::addResultToCache() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

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

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

    return array();
}

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

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

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

There are different options of fixing this problem.

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

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

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

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