Completed
Branch master (d7c4e6)
by
unknown
29:20
created

OutputPage::isUserCssPreview()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 5
c 2
b 0
f 0
nc 4
nop 0
dl 0
loc 6
rs 9.2
1
<?php
2
/**
3
 * Preparation for the final page rendering.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
use MediaWiki\Logger\LoggerFactory;
24
use MediaWiki\Session\SessionManager;
25
use WrappedString\WrappedString;
26
use WrappedString\WrappedStringList;
27
28
/**
29
 * This class should be covered by a general architecture document which does
30
 * not exist as of January 2011.  This is one of the Core classes and should
31
 * be read at least once by any new developers.
32
 *
33
 * This class is used to prepare the final rendering. A skin is then
34
 * applied to the output parameters (links, javascript, html, categories ...).
35
 *
36
 * @todo FIXME: Another class handles sending the whole page to the client.
37
 *
38
 * Some comments comes from a pairing session between Zak Greant and Antoine Musso
39
 * in November 2010.
40
 *
41
 * @todo document
42
 */
43
class OutputPage extends ContextSource {
44
	/** @var array Should be private. Used with addMeta() which adds "<meta>" */
45
	protected $mMetatags = [];
46
47
	/** @var array */
48
	protected $mLinktags = [];
49
50
	/** @var bool */
51
	protected $mCanonicalUrl = false;
52
53
	/**
54
	 * @var array Additional stylesheets. Looks like this is for extensions.
55
	 *   Might be replaced by ResourceLoader.
56
	 */
57
	protected $mExtStyles = [];
58
59
	/**
60
	 * @var string Should be private - has getter and setter. Contains
61
	 *   the HTML title */
62
	public $mPagetitle = '';
63
64
	/**
65
	 * @var string Contains all of the "<body>" content. Should be private we
66
	 *   got set/get accessors and the append() method.
67
	 */
68
	public $mBodytext = '';
69
70
	/**
71
	 * Holds the debug lines that will be output as comments in page source if
72
	 * $wgDebugComments is enabled. See also $wgShowDebug.
73
	 * @deprecated since 1.20; use MWDebug class instead.
74
	 */
75
	public $mDebugtext = '';
76
77
	/** @var string Stores contents of "<title>" tag */
78
	private $mHTMLtitle = '';
79
80
	/**
81
	 * @var bool Is the displayed content related to the source of the
82
	 *   corresponding wiki article.
83
	 */
84
	private $mIsarticle = false;
85
86
	/** @var bool Stores "article flag" toggle. */
87
	private $mIsArticleRelated = true;
88
89
	/**
90
	 * @var bool We have to set isPrintable(). Some pages should
91
	 * never be printed (ex: redirections).
92
	 */
93
	private $mPrintable = false;
94
95
	/**
96
	 * @var array Contains the page subtitle. Special pages usually have some
97
	 *   links here. Don't confuse with site subtitle added by skins.
98
	 */
99
	private $mSubtitle = [];
100
101
	/** @var string */
102
	public $mRedirect = '';
103
104
	/** @var int */
105
	protected $mStatusCode;
106
107
	/**
108
	 * @var string Used for sending cache control.
109
	 *   The whole caching system should probably be moved into its own class.
110
	 */
111
	protected $mLastModified = '';
112
113
	/** @var array */
114
	protected $mCategoryLinks = [];
115
116
	/** @var array */
117
	protected $mCategories = [];
118
119
	/** @var array */
120
	protected $mIndicators = [];
121
122
	/** @var array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page') */
123
	private $mLanguageLinks = [];
124
125
	/**
126
	 * Used for JavaScript (predates ResourceLoader)
127
	 * @todo We should split JS / CSS.
128
	 * mScripts content is inserted as is in "<head>" by Skin. This might
129
	 * contain either a link to a stylesheet or inline CSS.
130
	 */
131
	private $mScripts = '';
132
133
	/** @var string Inline CSS styles. Use addInlineStyle() sparingly */
134
	protected $mInlineStyles = '';
135
136
	/**
137
	 * @var string Used by skin template.
138
	 * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle );
139
	 */
140
	public $mPageLinkTitle = '';
141
142
	/** @var array Array of elements in "<head>". Parser might add its own headers! */
143
	protected $mHeadItems = [];
144
145
	/** @var array */
146
	protected $mModules = [];
147
148
	/** @var array */
149
	protected $mModuleScripts = [];
150
151
	/** @var array */
152
	protected $mModuleStyles = [];
153
154
	/** @var ResourceLoader */
155
	protected $mResourceLoader;
156
157
	/** @var ResourceLoaderClientHtml */
158
	private $rlClient;
159
160
	/** @var ResourceLoaderContext */
161
	private $rlClientContext;
162
163
	/** @var string */
164
	private $rlUserModuleState;
165
166
	/** @var array */
167
	private $rlExemptStyleModules;
168
169
	/** @var array */
170
	protected $mJsConfigVars = [];
171
172
	/** @var array */
173
	protected $mTemplateIds = [];
174
175
	/** @var array */
176
	protected $mImageTimeKeys = [];
177
178
	/** @var string */
179
	public $mRedirectCode = '';
180
181
	protected $mFeedLinksAppendQuery = null;
182
183
	/** @var array
184
	 * What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page?
185
	 * @see ResourceLoaderModule::$origin
186
	 * ResourceLoaderModule::ORIGIN_ALL is assumed unless overridden;
187
	 */
188
	protected $mAllowedModules = [
189
		ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
190
	];
191
192
	/** @var bool Whether output is disabled.  If this is true, the 'output' method will do nothing. */
193
	protected $mDoNothing = false;
194
195
	// Parser related.
196
197
	/** @var int */
198
	protected $mContainsNewMagic = 0;
199
200
	/**
201
	 * lazy initialised, use parserOptions()
202
	 * @var ParserOptions
203
	 */
204
	protected $mParserOptions = null;
205
206
	/**
207
	 * Handles the Atom / RSS links.
208
	 * We probably only support Atom in 2011.
209
	 * @see $wgAdvertisedFeedTypes
210
	 */
211
	private $mFeedLinks = [];
212
213
	// Gwicke work on squid caching? Roughly from 2003.
214
	protected $mEnableClientCache = true;
215
216
	/** @var bool Flag if output should only contain the body of the article. */
217
	private $mArticleBodyOnly = false;
218
219
	/** @var bool */
220
	protected $mNewSectionLink = false;
221
222
	/** @var bool */
223
	protected $mHideNewSectionLink = false;
224
225
	/**
226
	 * @var bool Comes from the parser. This was probably made to load CSS/JS
227
	 * only if we had "<gallery>". Used directly in CategoryPage.php.
228
	 * Looks like ResourceLoader can replace this.
229
	 */
230
	public $mNoGallery = false;
231
232
	/** @var string */
233
	private $mPageTitleActionText = '';
234
235
	/** @var int Cache stuff. Looks like mEnableClientCache */
236
	protected $mCdnMaxage = 0;
237
	/** @var int Upper limit on mCdnMaxage */
238
	protected $mCdnMaxageLimit = INF;
239
240
	/**
241
	 * @var bool Controls if anti-clickjacking / frame-breaking headers will
242
	 * be sent. This should be done for pages where edit actions are possible.
243
	 * Setters: $this->preventClickjacking() and $this->allowClickjacking().
244
	 */
245
	protected $mPreventClickjacking = true;
246
247
	/** @var int To include the variable {{REVISIONID}} */
248
	private $mRevisionId = null;
249
250
	/** @var string */
251
	private $mRevisionTimestamp = null;
252
253
	/** @var array */
254
	protected $mFileVersion = null;
255
256
	/**
257
	 * @var array An array of stylesheet filenames (relative from skins path),
258
	 * with options for CSS media, IE conditions, and RTL/LTR direction.
259
	 * For internal use; add settings in the skin via $this->addStyle()
260
	 *
261
	 * Style again! This seems like a code duplication since we already have
262
	 * mStyles. This is what makes Open Source amazing.
263
	 */
264
	protected $styles = [];
265
266
	private $mIndexPolicy = 'index';
267
	private $mFollowPolicy = 'follow';
268
	private $mVaryHeader = [
269
		'Accept-Encoding' => [ 'match=gzip' ],
270
	];
271
272
	/**
273
	 * If the current page was reached through a redirect, $mRedirectedFrom contains the Title
274
	 * of the redirect.
275
	 *
276
	 * @var Title
277
	 */
278
	private $mRedirectedFrom = null;
279
280
	/**
281
	 * Additional key => value data
282
	 */
283
	private $mProperties = [];
284
285
	/**
286
	 * @var string|null ResourceLoader target for load.php links. If null, will be omitted
287
	 */
288
	private $mTarget = null;
289
290
	/**
291
	 * @var bool Whether parser output should contain table of contents
292
	 */
293
	private $mEnableTOC = true;
294
295
	/**
296
	 * @var bool Whether parser output should contain section edit links
297
	 */
298
	private $mEnableSectionEditLinks = true;
299
300
	/**
301
	 * @var string|null The URL to send in a <link> element with rel=copyright
302
	 */
303
	private $copyrightUrl;
304
305
	/** @var array Profiling data */
306
	private $limitReportData = [];
307
308
	/**
309
	 * Constructor for OutputPage. This should not be called directly.
310
	 * Instead a new RequestContext should be created and it will implicitly create
311
	 * a OutputPage tied to that context.
312
	 * @param IContextSource|null $context
313
	 */
314
	function __construct( IContextSource $context = null ) {
315
		if ( $context === null ) {
316
			# Extensions should use `new RequestContext` instead of `new OutputPage` now.
317
			wfDeprecated( __METHOD__, '1.18' );
318
		} else {
319
			$this->setContext( $context );
320
		}
321
	}
322
323
	/**
324
	 * Redirect to $url rather than displaying the normal page
325
	 *
326
	 * @param string $url URL
327
	 * @param string $responsecode HTTP status code
328
	 */
329
	public function redirect( $url, $responsecode = '302' ) {
330
		# Strip newlines as a paranoia check for header injection in PHP<5.1.2
331
		$this->mRedirect = str_replace( "\n", '', $url );
332
		$this->mRedirectCode = $responsecode;
333
	}
334
335
	/**
336
	 * Get the URL to redirect to, or an empty string if not redirect URL set
337
	 *
338
	 * @return string
339
	 */
340
	public function getRedirect() {
341
		return $this->mRedirect;
342
	}
343
344
	/**
345
	 * Set the copyright URL to send with the output.
346
	 * Empty string to omit, null to reset.
347
	 *
348
	 * @since 1.26
349
	 *
350
	 * @param string|null $url
351
	 */
352
	public function setCopyrightUrl( $url ) {
353
		$this->copyrightUrl = $url;
354
	}
355
356
	/**
357
	 * Set the HTTP status code to send with the output.
358
	 *
359
	 * @param int $statusCode
360
	 */
361
	public function setStatusCode( $statusCode ) {
362
		$this->mStatusCode = $statusCode;
363
	}
364
365
	/**
366
	 * Add a new "<meta>" tag
367
	 * To add an http-equiv meta tag, precede the name with "http:"
368
	 *
369
	 * @param string $name Tag name
370
	 * @param string $val Tag value
371
	 */
372
	function addMeta( $name, $val ) {
373
		array_push( $this->mMetatags, [ $name, $val ] );
374
	}
375
376
	/**
377
	 * Returns the current <meta> tags
378
	 *
379
	 * @since 1.25
380
	 * @return array
381
	 */
382
	public function getMetaTags() {
383
		return $this->mMetatags;
384
	}
385
386
	/**
387
	 * Add a new \<link\> tag to the page header.
388
	 *
389
	 * Note: use setCanonicalUrl() for rel=canonical.
390
	 *
391
	 * @param array $linkarr Associative array of attributes.
392
	 */
393
	function addLink( array $linkarr ) {
394
		array_push( $this->mLinktags, $linkarr );
395
	}
396
397
	/**
398
	 * Returns the current <link> tags
399
	 *
400
	 * @since 1.25
401
	 * @return array
402
	 */
403
	public function getLinkTags() {
404
		return $this->mLinktags;
405
	}
406
407
	/**
408
	 * Add a new \<link\> with "rel" attribute set to "meta"
409
	 *
410
	 * @param array $linkarr Associative array mapping attribute names to their
411
	 *                 values, both keys and values will be escaped, and the
412
	 *                 "rel" attribute will be automatically added
413
	 */
414
	function addMetadataLink( array $linkarr ) {
415
		$linkarr['rel'] = $this->getMetadataAttribute();
416
		$this->addLink( $linkarr );
417
	}
418
419
	/**
420
	 * Set the URL to be used for the <link rel=canonical>. This should be used
421
	 * in preference to addLink(), to avoid duplicate link tags.
422
	 * @param string $url
423
	 */
424
	function setCanonicalUrl( $url ) {
425
		$this->mCanonicalUrl = $url;
0 ignored issues
show
Documentation Bug introduced by
The property $mCanonicalUrl was declared of type boolean, but $url is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
426
	}
427
428
	/**
429
	 * Returns the URL to be used for the <link rel=canonical> if
430
	 * one is set.
431
	 *
432
	 * @since 1.25
433
	 * @return bool|string
434
	 */
435
	public function getCanonicalUrl() {
436
		return $this->mCanonicalUrl;
437
	}
438
439
	/**
440
	 * Get the value of the "rel" attribute for metadata links
441
	 *
442
	 * @return string
443
	 */
444
	public function getMetadataAttribute() {
445
		# note: buggy CC software only reads first "meta" link
446
		static $haveMeta = false;
447
		if ( $haveMeta ) {
448
			return 'alternate meta';
449
		} else {
450
			$haveMeta = true;
451
			return 'meta';
452
		}
453
	}
454
455
	/**
456
	 * Add raw HTML to the list of scripts (including \<script\> tag, etc.)
457
	 * Internal use only. Use OutputPage::addModules() or OutputPage::addJsConfigVars()
458
	 * if possible.
459
	 *
460
	 * @param string $script Raw HTML
461
	 */
462
	function addScript( $script ) {
463
		$this->mScripts .= $script;
464
	}
465
466
	/**
467
	 * Register and add a stylesheet from an extension directory.
468
	 *
469
	 * @deprecated since 1.27 use addModuleStyles() or addStyle() instead
470
	 * @param string $url Path to sheet.  Provide either a full url (beginning
471
	 *             with 'http', etc) or a relative path from the document root
472
	 *             (beginning with '/').  Otherwise it behaves identically to
473
	 *             addStyle() and draws from the /skins folder.
474
	 */
475
	public function addExtensionStyle( $url ) {
476
		wfDeprecated( __METHOD__, '1.27' );
477
		array_push( $this->mExtStyles, $url );
478
	}
479
480
	/**
481
	 * Get all styles added by extensions
482
	 *
483
	 * @deprecated since 1.27
484
	 * @return array
485
	 */
486
	function getExtStyle() {
487
		wfDeprecated( __METHOD__, '1.27' );
488
		return $this->mExtStyles;
489
	}
490
491
	/**
492
	 * Add a JavaScript file out of skins/common, or a given relative path.
493
	 * Internal use only. Use OutputPage::addModules() if possible.
494
	 *
495
	 * @param string $file Filename in skins/common or complete on-server path
496
	 *              (/foo/bar.js)
497
	 * @param string $version Style version of the file. Defaults to $wgStyleVersion
498
	 */
499
	public function addScriptFile( $file, $version = null ) {
500
		// See if $file parameter is an absolute URL or begins with a slash
501
		if ( substr( $file, 0, 1 ) == '/' || preg_match( '#^[a-z]*://#i', $file ) ) {
502
			$path = $file;
503
		} else {
504
			$path = $this->getConfig()->get( 'StylePath' ) . "/common/{$file}";
505
		}
506
		if ( is_null( $version ) ) {
507
			$version = $this->getConfig()->get( 'StyleVersion' );
508
		}
509
		$this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ) ) );
510
	}
511
512
	/**
513
	 * Add a self-contained script tag with the given contents
514
	 * Internal use only. Use OutputPage::addModules() if possible.
515
	 *
516
	 * @param string $script JavaScript text, no script tags
517
	 */
518
	public function addInlineScript( $script ) {
519
		$this->mScripts .= Html::inlineScript( $script );
520
	}
521
522
	/**
523
	 * Filter an array of modules to remove insufficiently trustworthy members, and modules
524
	 * which are no longer registered (eg a page is cached before an extension is disabled)
525
	 * @param array $modules
526
	 * @param string|null $position If not null, only return modules with this position
527
	 * @param string $type
528
	 * @return array
529
	 */
530
	protected function filterModules( array $modules, $position = null,
531
		$type = ResourceLoaderModule::TYPE_COMBINED
532
	) {
533
		$resourceLoader = $this->getResourceLoader();
534
		$filteredModules = [];
535
		foreach ( $modules as $val ) {
536
			$module = $resourceLoader->getModule( $val );
537
			if ( $module instanceof ResourceLoaderModule
538
				&& $module->getOrigin() <= $this->getAllowedModules( $type )
539
				&& ( is_null( $position ) || $module->getPosition() == $position )
540
				&& ( !$this->mTarget || in_array( $this->mTarget, $module->getTargets() ) )
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->mTarget of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
541
			) {
542
				$filteredModules[] = $val;
543
			}
544
		}
545
		return $filteredModules;
546
	}
547
548
	/**
549
	 * Get the list of modules to include on this page
550
	 *
551
	 * @param bool $filter Whether to filter out insufficiently trustworthy modules
552
	 * @param string|null $position If not null, only return modules with this position
553
	 * @param string $param
554
	 * @return array Array of module names
555
	 */
556
	public function getModules( $filter = false, $position = null, $param = 'mModules',
557
		$type = ResourceLoaderModule::TYPE_COMBINED
558
	) {
559
		$modules = array_values( array_unique( $this->$param ) );
560
		return $filter
561
			? $this->filterModules( $modules, $position, $type )
562
			: $modules;
563
	}
564
565
	/**
566
	 * Add one or more modules recognized by ResourceLoader. Modules added
567
	 * through this function will be loaded by ResourceLoader when the
568
	 * page loads.
569
	 *
570
	 * @param string|array $modules Module name (string) or array of module names
571
	 */
572
	public function addModules( $modules ) {
573
		$this->mModules = array_merge( $this->mModules, (array)$modules );
574
	}
575
576
	/**
577
	 * Get the list of module JS to include on this page
578
	 *
579
	 * @param bool $filter
580
	 * @param string|null $position
581
	 * @return array Array of module names
582
	 */
583
	public function getModuleScripts( $filter = false, $position = null ) {
584
		return $this->getModules( $filter, $position, 'mModuleScripts',
585
			ResourceLoaderModule::TYPE_SCRIPTS
586
		);
587
	}
588
589
	/**
590
	 * Add only JS of one or more modules recognized by ResourceLoader. Module
591
	 * scripts added through this function will be loaded by ResourceLoader when
592
	 * the page loads.
593
	 *
594
	 * @param string|array $modules Module name (string) or array of module names
595
	 */
596
	public function addModuleScripts( $modules ) {
597
		$this->mModuleScripts = array_merge( $this->mModuleScripts, (array)$modules );
598
	}
599
600
	/**
601
	 * Get the list of module CSS to include on this page
602
	 *
603
	 * @param bool $filter
604
	 * @param string|null $position
605
	 * @return array Array of module names
606
	 */
607
	public function getModuleStyles( $filter = false, $position = null ) {
608
		return $this->getModules( $filter, $position, 'mModuleStyles',
609
			ResourceLoaderModule::TYPE_STYLES
610
		);
611
	}
612
613
	/**
614
	 * Add only CSS of one or more modules recognized by ResourceLoader.
615
	 *
616
	 * Module styles added through this function will be added using standard link CSS
617
	 * tags, rather than as a combined Javascript and CSS package. Thus, they will
618
	 * load when JavaScript is disabled (unless CSS also happens to be disabled).
619
	 *
620
	 * @param string|array $modules Module name (string) or array of module names
621
	 */
622
	public function addModuleStyles( $modules ) {
623
		$this->mModuleStyles = array_merge( $this->mModuleStyles, (array)$modules );
624
	}
625
626
	/**
627
	 * @return null|string ResourceLoader target
628
	 */
629
	public function getTarget() {
630
		return $this->mTarget;
631
	}
632
633
	/**
634
	 * Sets ResourceLoader target for load.php links. If null, will be omitted
635
	 *
636
	 * @param string|null $target
637
	 */
638
	public function setTarget( $target ) {
639
		$this->mTarget = $target;
640
	}
641
642
	/**
643
	 * Get an array of head items
644
	 *
645
	 * @return array
646
	 */
647
	function getHeadItemsArray() {
648
		return $this->mHeadItems;
649
	}
650
651
	/**
652
	 * Add or replace a head item to the output
653
	 *
654
	 * Whenever possible, use more specific options like ResourceLoader modules,
655
	 * OutputPage::addLink(), OutputPage::addMetaLink() and OutputPage::addFeedLink()
656
	 * Fallback options for those are: OutputPage::addStyle, OutputPage::addScript(),
657
	 * OutputPage::addInlineScript() and OutputPage::addInlineStyle()
658
	 * This would be your very LAST fallback.
659
	 *
660
	 * @param string $name Item name
661
	 * @param string $value Raw HTML
662
	 */
663
	public function addHeadItem( $name, $value ) {
664
		$this->mHeadItems[$name] = $value;
665
	}
666
667
	/**
668
	 * Add one or more head items to the output
669
	 *
670
	 * @since 1.28
671
	 * @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...
672
	 */
673
	public function addHeadItems( $values ) {
674
		$this->mHeadItems = array_merge( $this->mHeadItems, (array)$values );
675
	}
676
677
	/**
678
	 * Check if the header item $name is already set
679
	 *
680
	 * @param string $name Item name
681
	 * @return bool
682
	 */
683
	public function hasHeadItem( $name ) {
684
		return isset( $this->mHeadItems[$name] );
685
	}
686
687
	/**
688
	 * @deprecated since 1.28 Obsolete - wgUseETag experiment was removed.
689
	 * @param string $tag
690
	 */
691
	public function setETag( $tag ) {
692
	}
693
694
	/**
695
	 * Set whether the output should only contain the body of the article,
696
	 * without any skin, sidebar, etc.
697
	 * Used e.g. when calling with "action=render".
698
	 *
699
	 * @param bool $only Whether to output only the body of the article
700
	 */
701
	public function setArticleBodyOnly( $only ) {
702
		$this->mArticleBodyOnly = $only;
703
	}
704
705
	/**
706
	 * Return whether the output will contain only the body of the article
707
	 *
708
	 * @return bool
709
	 */
710
	public function getArticleBodyOnly() {
711
		return $this->mArticleBodyOnly;
712
	}
713
714
	/**
715
	 * Set an additional output property
716
	 * @since 1.21
717
	 *
718
	 * @param string $name
719
	 * @param mixed $value
720
	 */
721
	public function setProperty( $name, $value ) {
722
		$this->mProperties[$name] = $value;
723
	}
724
725
	/**
726
	 * Get an additional output property
727
	 * @since 1.21
728
	 *
729
	 * @param string $name
730
	 * @return mixed Property value or null if not found
731
	 */
732
	public function getProperty( $name ) {
733
		if ( isset( $this->mProperties[$name] ) ) {
734
			return $this->mProperties[$name];
735
		} else {
736
			return null;
737
		}
738
	}
739
740
	/**
741
	 * checkLastModified tells the client to use the client-cached page if
742
	 * possible. If successful, the OutputPage is disabled so that
743
	 * any future call to OutputPage->output() have no effect.
744
	 *
745
	 * Side effect: sets mLastModified for Last-Modified header
746
	 *
747
	 * @param string $timestamp
748
	 *
749
	 * @return bool True if cache-ok headers was sent.
750
	 */
751
	public function checkLastModified( $timestamp ) {
752
		if ( !$timestamp || $timestamp == '19700101000000' ) {
753
			wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
754
			return false;
755
		}
756
		$config = $this->getConfig();
757
		if ( !$config->get( 'CachePages' ) ) {
758
			wfDebug( __METHOD__ . ": CACHE DISABLED\n" );
759
			return false;
760
		}
761
762
		$timestamp = wfTimestamp( TS_MW, $timestamp );
763
		$modifiedTimes = [
764
			'page' => $timestamp,
765
			'user' => $this->getUser()->getTouched(),
766
			'epoch' => $config->get( 'CacheEpoch' )
767
		];
768
		if ( $config->get( 'UseSquid' ) ) {
769
			// bug 44570: the core page itself may not change, but resources might
770
			$modifiedTimes['sepoch'] = wfTimestamp( TS_MW, time() - $config->get( 'SquidMaxage' ) );
771
		}
772
		Hooks::run( 'OutputPageCheckLastModified', [ &$modifiedTimes, $this ] );
773
774
		$maxModified = max( $modifiedTimes );
775
		$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...
776
777
		$clientHeader = $this->getRequest()->getHeader( 'If-Modified-Since' );
778
		if ( $clientHeader === false ) {
779
			wfDebug( __METHOD__ . ": client did not send If-Modified-Since header", 'private' );
780
			return false;
781
		}
782
783
		# IE sends sizes after the date like this:
784
		# Wed, 20 Aug 2003 06:51:19 GMT; length=5202
785
		# this breaks strtotime().
786
		$clientHeader = preg_replace( '/;.*$/', '', $clientHeader );
787
788
		MediaWiki\suppressWarnings(); // E_STRICT system time bitching
789
		$clientHeaderTime = strtotime( $clientHeader );
790
		MediaWiki\restoreWarnings();
791
		if ( !$clientHeaderTime ) {
792
			wfDebug( __METHOD__
793
				. ": unable to parse the client's If-Modified-Since header: $clientHeader\n" );
794
			return false;
795
		}
796
		$clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
797
798
		# Make debug info
799
		$info = '';
800
		foreach ( $modifiedTimes as $name => $value ) {
801
			if ( $info !== '' ) {
802
				$info .= ', ';
803
			}
804
			$info .= "$name=" . wfTimestamp( TS_ISO_8601, $value );
805
		}
806
807
		wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
808
			wfTimestamp( TS_ISO_8601, $clientHeaderTime ), 'private' );
809
		wfDebug( __METHOD__ . ": effective Last-Modified: " .
810
			wfTimestamp( TS_ISO_8601, $maxModified ), 'private' );
811
		if ( $clientHeaderTime < $maxModified ) {
812
			wfDebug( __METHOD__ . ": STALE, $info", 'private' );
813
			return false;
814
		}
815
816
		# Not modified
817
		# Give a 304 Not Modified response code and disable body output
818
		wfDebug( __METHOD__ . ": NOT MODIFIED, $info", 'private' );
819
		ini_set( 'zlib.output_compression', 0 );
820
		$this->getRequest()->response()->statusHeader( 304 );
821
		$this->sendCacheControl();
822
		$this->disable();
823
824
		// Don't output a compressed blob when using ob_gzhandler;
825
		// it's technically against HTTP spec and seems to confuse
826
		// Firefox when the response gets split over two packets.
827
		wfClearOutputBuffers();
828
829
		return true;
830
	}
831
832
	/**
833
	 * Override the last modified timestamp
834
	 *
835
	 * @param string $timestamp New timestamp, in a format readable by
836
	 *        wfTimestamp()
837
	 */
838
	public function setLastModified( $timestamp ) {
839
		$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...
840
	}
841
842
	/**
843
	 * Set the robot policy for the page: <http://www.robotstxt.org/meta.html>
844
	 *
845
	 * @param string $policy The literal string to output as the contents of
846
	 *   the meta tag.  Will be parsed according to the spec and output in
847
	 *   standardized form.
848
	 * @return null
849
	 */
850
	public function setRobotPolicy( $policy ) {
851
		$policy = Article::formatRobotPolicy( $policy );
852
853
		if ( isset( $policy['index'] ) ) {
854
			$this->setIndexPolicy( $policy['index'] );
855
		}
856
		if ( isset( $policy['follow'] ) ) {
857
			$this->setFollowPolicy( $policy['follow'] );
858
		}
859
	}
860
861
	/**
862
	 * Set the index policy for the page, but leave the follow policy un-
863
	 * touched.
864
	 *
865
	 * @param string $policy Either 'index' or 'noindex'.
866
	 * @return null
867
	 */
868
	public function setIndexPolicy( $policy ) {
869
		$policy = trim( $policy );
870
		if ( in_array( $policy, [ 'index', 'noindex' ] ) ) {
871
			$this->mIndexPolicy = $policy;
872
		}
873
	}
874
875
	/**
876
	 * Set the follow policy for the page, but leave the index policy un-
877
	 * touched.
878
	 *
879
	 * @param string $policy Either 'follow' or 'nofollow'.
880
	 * @return null
881
	 */
882
	public function setFollowPolicy( $policy ) {
883
		$policy = trim( $policy );
884
		if ( in_array( $policy, [ 'follow', 'nofollow' ] ) ) {
885
			$this->mFollowPolicy = $policy;
886
		}
887
	}
888
889
	/**
890
	 * Set the new value of the "action text", this will be added to the
891
	 * "HTML title", separated from it with " - ".
892
	 *
893
	 * @param string $text New value of the "action text"
894
	 */
895
	public function setPageTitleActionText( $text ) {
896
		$this->mPageTitleActionText = $text;
897
	}
898
899
	/**
900
	 * Get the value of the "action text"
901
	 *
902
	 * @return string
903
	 */
904
	public function getPageTitleActionText() {
905
		return $this->mPageTitleActionText;
906
	}
907
908
	/**
909
	 * "HTML title" means the contents of "<title>".
910
	 * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file.
911
	 *
912
	 * @param string|Message $name
913
	 */
914
	public function setHTMLTitle( $name ) {
915
		if ( $name instanceof Message ) {
916
			$this->mHTMLtitle = $name->setContext( $this->getContext() )->text();
917
		} else {
918
			$this->mHTMLtitle = $name;
919
		}
920
	}
921
922
	/**
923
	 * Return the "HTML title", i.e. the content of the "<title>" tag.
924
	 *
925
	 * @return string
926
	 */
927
	public function getHTMLTitle() {
928
		return $this->mHTMLtitle;
929
	}
930
931
	/**
932
	 * Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
933
	 *
934
	 * @param Title $t
935
	 */
936
	public function setRedirectedFrom( $t ) {
937
		$this->mRedirectedFrom = $t;
938
	}
939
940
	/**
941
	 * "Page title" means the contents of \<h1\>. It is stored as a valid HTML
942
	 * fragment. This function allows good tags like \<sup\> in the \<h1\> tag,
943
	 * but not bad tags like \<script\>. This function automatically sets
944
	 * \<title\> to the same content as \<h1\> but with all tags removed. Bad
945
	 * tags that were escaped in \<h1\> will still be escaped in \<title\>, and
946
	 * good tags like \<i\> will be dropped entirely.
947
	 *
948
	 * @param string|Message $name
949
	 */
950
	public function setPageTitle( $name ) {
951
		if ( $name instanceof Message ) {
952
			$name = $name->setContext( $this->getContext() )->text();
953
		}
954
955
		# change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
956
		# but leave "<i>foobar</i>" alone
957
		$nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
958
		$this->mPagetitle = $nameWithTags;
959
960
		# change "<i>foo&amp;bar</i>" to "foo&bar"
961
		$this->setHTMLTitle(
962
			$this->msg( 'pagetitle' )->rawParams( Sanitizer::stripAllTags( $nameWithTags ) )
963
				->inContentLanguage()
964
		);
965
	}
966
967
	/**
968
	 * Return the "page title", i.e. the content of the \<h1\> tag.
969
	 *
970
	 * @return string
971
	 */
972
	public function getPageTitle() {
973
		return $this->mPagetitle;
974
	}
975
976
	/**
977
	 * Set the Title object to use
978
	 *
979
	 * @param Title $t
980
	 */
981
	public function setTitle( Title $t ) {
982
		$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...
983
	}
984
985
	/**
986
	 * Replace the subtitle with $str
987
	 *
988
	 * @param string|Message $str New value of the subtitle. String should be safe HTML.
989
	 */
990
	public function setSubtitle( $str ) {
991
		$this->clearSubtitle();
992
		$this->addSubtitle( $str );
993
	}
994
995
	/**
996
	 * Add $str to the subtitle
997
	 *
998
	 * @param string|Message $str String or Message to add to the subtitle. String should be safe HTML.
999
	 */
1000
	public function addSubtitle( $str ) {
1001
		if ( $str instanceof Message ) {
1002
			$this->mSubtitle[] = $str->setContext( $this->getContext() )->parse();
1003
		} else {
1004
			$this->mSubtitle[] = $str;
1005
		}
1006
	}
1007
1008
	/**
1009
	 * Build message object for a subtitle containing a backlink to a page
1010
	 *
1011
	 * @param Title $title Title to link to
1012
	 * @param array $query Array of additional parameters to include in the link
1013
	 * @return Message
1014
	 * @since 1.25
1015
	 */
1016
	public static function buildBacklinkSubtitle( Title $title, $query = [] ) {
1017
		if ( $title->isRedirect() ) {
1018
			$query['redirect'] = 'no';
1019
		}
1020
		return wfMessage( 'backlinksubtitle' )
1021
			->rawParams( Linker::link( $title, null, [], $query ) );
1022
	}
1023
1024
	/**
1025
	 * Add a subtitle containing a backlink to a page
1026
	 *
1027
	 * @param Title $title Title to link to
1028
	 * @param array $query Array of additional parameters to include in the link
1029
	 */
1030
	public function addBacklinkSubtitle( Title $title, $query = [] ) {
1031
		$this->addSubtitle( self::buildBacklinkSubtitle( $title, $query ) );
1032
	}
1033
1034
	/**
1035
	 * Clear the subtitles
1036
	 */
1037
	public function clearSubtitle() {
1038
		$this->mSubtitle = [];
1039
	}
1040
1041
	/**
1042
	 * Get the subtitle
1043
	 *
1044
	 * @return string
1045
	 */
1046
	public function getSubtitle() {
1047
		return implode( "<br />\n\t\t\t\t", $this->mSubtitle );
1048
	}
1049
1050
	/**
1051
	 * Set the page as printable, i.e. it'll be displayed with all
1052
	 * print styles included
1053
	 */
1054
	public function setPrintable() {
1055
		$this->mPrintable = true;
1056
	}
1057
1058
	/**
1059
	 * Return whether the page is "printable"
1060
	 *
1061
	 * @return bool
1062
	 */
1063
	public function isPrintable() {
1064
		return $this->mPrintable;
1065
	}
1066
1067
	/**
1068
	 * Disable output completely, i.e. calling output() will have no effect
1069
	 */
1070
	public function disable() {
1071
		$this->mDoNothing = true;
1072
	}
1073
1074
	/**
1075
	 * Return whether the output will be completely disabled
1076
	 *
1077
	 * @return bool
1078
	 */
1079
	public function isDisabled() {
1080
		return $this->mDoNothing;
1081
	}
1082
1083
	/**
1084
	 * Show an "add new section" link?
1085
	 *
1086
	 * @return bool
1087
	 */
1088
	public function showNewSectionLink() {
1089
		return $this->mNewSectionLink;
1090
	}
1091
1092
	/**
1093
	 * Forcibly hide the new section link?
1094
	 *
1095
	 * @return bool
1096
	 */
1097
	public function forceHideNewSectionLink() {
1098
		return $this->mHideNewSectionLink;
1099
	}
1100
1101
	/**
1102
	 * Add or remove feed links in the page header
1103
	 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
1104
	 * for the new version
1105
	 * @see addFeedLink()
1106
	 *
1107
	 * @param bool $show True: add default feeds, false: remove all feeds
1108
	 */
1109
	public function setSyndicated( $show = true ) {
1110
		if ( $show ) {
1111
			$this->setFeedAppendQuery( false );
1112
		} else {
1113
			$this->mFeedLinks = [];
1114
		}
1115
	}
1116
1117
	/**
1118
	 * Add default feeds to the page header
1119
	 * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
1120
	 * for the new version
1121
	 * @see addFeedLink()
1122
	 *
1123
	 * @param string $val Query to append to feed links or false to output
1124
	 *        default links
1125
	 */
1126
	public function setFeedAppendQuery( $val ) {
1127
		$this->mFeedLinks = [];
1128
1129
		foreach ( $this->getConfig()->get( 'AdvertisedFeedTypes' ) as $type ) {
1130
			$query = "feed=$type";
1131
			if ( is_string( $val ) ) {
1132
				$query .= '&' . $val;
1133
			}
1134
			$this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
1135
		}
1136
	}
1137
1138
	/**
1139
	 * Add a feed link to the page header
1140
	 *
1141
	 * @param string $format Feed type, should be a key of $wgFeedClasses
1142
	 * @param string $href URL
1143
	 */
1144
	public function addFeedLink( $format, $href ) {
1145
		if ( in_array( $format, $this->getConfig()->get( 'AdvertisedFeedTypes' ) ) ) {
1146
			$this->mFeedLinks[$format] = $href;
1147
		}
1148
	}
1149
1150
	/**
1151
	 * Should we output feed links for this page?
1152
	 * @return bool
1153
	 */
1154
	public function isSyndicated() {
1155
		return count( $this->mFeedLinks ) > 0;
1156
	}
1157
1158
	/**
1159
	 * Return URLs for each supported syndication format for this page.
1160
	 * @return array Associating format keys with URLs
1161
	 */
1162
	public function getSyndicationLinks() {
1163
		return $this->mFeedLinks;
1164
	}
1165
1166
	/**
1167
	 * Will currently always return null
1168
	 *
1169
	 * @return null
1170
	 */
1171
	public function getFeedAppendQuery() {
1172
		return $this->mFeedLinksAppendQuery;
1173
	}
1174
1175
	/**
1176
	 * Set whether the displayed content is related to the source of the
1177
	 * corresponding article on the wiki
1178
	 * Setting true will cause the change "article related" toggle to true
1179
	 *
1180
	 * @param bool $v
1181
	 */
1182
	public function setArticleFlag( $v ) {
1183
		$this->mIsarticle = $v;
1184
		if ( $v ) {
1185
			$this->mIsArticleRelated = $v;
1186
		}
1187
	}
1188
1189
	/**
1190
	 * Return whether the content displayed page is related to the source of
1191
	 * the corresponding article on the wiki
1192
	 *
1193
	 * @return bool
1194
	 */
1195
	public function isArticle() {
1196
		return $this->mIsarticle;
1197
	}
1198
1199
	/**
1200
	 * Set whether this page is related an article on the wiki
1201
	 * Setting false will cause the change of "article flag" toggle to false
1202
	 *
1203
	 * @param bool $v
1204
	 */
1205
	public function setArticleRelated( $v ) {
1206
		$this->mIsArticleRelated = $v;
1207
		if ( !$v ) {
1208
			$this->mIsarticle = false;
1209
		}
1210
	}
1211
1212
	/**
1213
	 * Return whether this page is related an article on the wiki
1214
	 *
1215
	 * @return bool
1216
	 */
1217
	public function isArticleRelated() {
1218
		return $this->mIsArticleRelated;
1219
	}
1220
1221
	/**
1222
	 * Add new language links
1223
	 *
1224
	 * @param array $newLinkArray Associative array mapping language code to the page
1225
	 *                      name
1226
	 */
1227
	public function addLanguageLinks( array $newLinkArray ) {
1228
		$this->mLanguageLinks += $newLinkArray;
1229
	}
1230
1231
	/**
1232
	 * Reset the language links and add new language links
1233
	 *
1234
	 * @param array $newLinkArray Associative array mapping language code to the page
1235
	 *                      name
1236
	 */
1237
	public function setLanguageLinks( array $newLinkArray ) {
1238
		$this->mLanguageLinks = $newLinkArray;
1239
	}
1240
1241
	/**
1242
	 * Get the list of language links
1243
	 *
1244
	 * @return array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page')
1245
	 */
1246
	public function getLanguageLinks() {
1247
		return $this->mLanguageLinks;
1248
	}
1249
1250
	/**
1251
	 * Add an array of categories, with names in the keys
1252
	 *
1253
	 * @param array $categories Mapping category name => sort key
1254
	 */
1255
	public function addCategoryLinks( array $categories ) {
1256
		global $wgContLang;
1257
1258
		if ( !is_array( $categories ) || count( $categories ) == 0 ) {
1259
			return;
1260
		}
1261
1262
		# Add the links to a LinkBatch
1263
		$arr = [ NS_CATEGORY => $categories ];
1264
		$lb = new LinkBatch;
1265
		$lb->setArray( $arr );
1266
1267
		# Fetch existence plus the hiddencat property
1268
		$dbr = wfGetDB( DB_SLAVE );
1269
		$fields = array_merge(
1270
			LinkCache::getSelectFields(),
1271
			[ 'page_namespace', 'page_title', 'pp_value' ]
1272
		);
1273
1274
		$res = $dbr->select( [ 'page', 'page_props' ],
1275
			$fields,
1276
			$lb->constructSet( 'page', $dbr ),
0 ignored issues
show
Bug introduced by
It seems like $lb->constructSet('page', $dbr) targeting LinkBatch::constructSet() can also be of type boolean; however, DatabaseBase::select() does only seem to accept string, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

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

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

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

    return array();
}

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

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

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

There are different options of fixing this problem.

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

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

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

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