Completed
Branch master (537795)
by
unknown
33:10
created

OutputPage::adaptCdnTTL()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $dbr defined by wfGetDB(DB_REPLICA) on line 1261 can be null; however, LinkBatch::constructSet() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

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

An additional type check may prevent trouble.

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

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

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

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

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

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