Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/specialpage/SpecialPageFactory.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Factory for handling the special page list and generating SpecialPage objects.
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
 * @ingroup SpecialPage
22
 * @defgroup SpecialPage SpecialPage
23
 */
24
use MediaWiki\Linker\LinkRenderer;
25
26
/**
27
 * Factory for handling the special page list and generating SpecialPage objects.
28
 *
29
 * To add a special page in an extension, add to $wgSpecialPages either
30
 * an object instance or an array containing the name and constructor
31
 * parameters. The latter is preferred for performance reasons.
32
 *
33
 * The object instantiated must be either an instance of SpecialPage or a
34
 * sub-class thereof. It must have an execute() method, which sends the HTML
35
 * for the special page to $wgOut. The parent class has an execute() method
36
 * which distributes the call to the historical global functions. Additionally,
37
 * execute() also checks if the user has the necessary access privileges
38
 * and bails out if not.
39
 *
40
 * To add a core special page, use the similar static list in
41
 * SpecialPageFactory::$list. To remove a core static special page at runtime, use
42
 * a SpecialPage_initList hook.
43
 *
44
 * @ingroup SpecialPage
45
 * @since 1.17
46
 */
47
class SpecialPageFactory {
48
	/**
49
	 * List of special page names to the subclass of SpecialPage which handles them.
50
	 */
51
	private static $coreList = [
52
		// Maintenance Reports
53
		'BrokenRedirects' => 'BrokenRedirectsPage',
54
		'Deadendpages' => 'DeadendPagesPage',
55
		'DoubleRedirects' => 'DoubleRedirectsPage',
56
		'Longpages' => 'LongPagesPage',
57
		'Ancientpages' => 'AncientPagesPage',
58
		'Lonelypages' => 'LonelyPagesPage',
59
		'Fewestrevisions' => 'FewestrevisionsPage',
60
		'Withoutinterwiki' => 'WithoutInterwikiPage',
61
		'Protectedpages' => 'SpecialProtectedpages',
62
		'Protectedtitles' => 'SpecialProtectedtitles',
63
		'Shortpages' => 'ShortPagesPage',
64
		'Uncategorizedcategories' => 'UncategorizedCategoriesPage',
65
		'Uncategorizedimages' => 'UncategorizedImagesPage',
66
		'Uncategorizedpages' => 'UncategorizedPagesPage',
67
		'Uncategorizedtemplates' => 'UncategorizedTemplatesPage',
68
		'Unusedcategories' => 'UnusedCategoriesPage',
69
		'Unusedimages' => 'UnusedimagesPage',
70
		'Unusedtemplates' => 'UnusedtemplatesPage',
71
		'Unwatchedpages' => 'UnwatchedpagesPage',
72
		'Wantedcategories' => 'WantedCategoriesPage',
73
		'Wantedfiles' => 'WantedFilesPage',
74
		'Wantedpages' => 'WantedPagesPage',
75
		'Wantedtemplates' => 'WantedTemplatesPage',
76
77
		// List of pages
78
		'Allpages' => 'SpecialAllPages',
79
		'Prefixindex' => 'SpecialPrefixindex',
80
		'Categories' => 'SpecialCategories',
81
		'Listredirects' => 'ListredirectsPage',
82
		'PagesWithProp' => 'SpecialPagesWithProp',
83
		'TrackingCategories' => 'SpecialTrackingCategories',
84
85
		// Authentication
86
		'Userlogin' => 'SpecialUserLogin',
87
		'Userlogout' => 'SpecialUserLogout',
88
		'CreateAccount' => 'SpecialCreateAccount',
89
		'LinkAccounts' => 'SpecialLinkAccounts',
90
		'UnlinkAccounts' => 'SpecialUnlinkAccounts',
91
		'ChangeCredentials' => 'SpecialChangeCredentials',
92
		'RemoveCredentials' => 'SpecialRemoveCredentials',
93
94
		// Users and rights
95
		'Activeusers' => 'SpecialActiveUsers',
96
		'Block' => 'SpecialBlock',
97
		'Unblock' => 'SpecialUnblock',
98
		'BlockList' => 'SpecialBlockList',
99
		'ChangePassword' => 'SpecialChangePassword',
100
		'BotPasswords' => 'SpecialBotPasswords',
101
		'PasswordReset' => 'SpecialPasswordReset',
102
		'DeletedContributions' => 'DeletedContributionsPage',
103
		'Preferences' => 'SpecialPreferences',
104
		'ResetTokens' => 'SpecialResetTokens',
105
		'Contributions' => 'SpecialContributions',
106
		'Listgrouprights' => 'SpecialListGroupRights',
107
		'Listgrants' => 'SpecialListGrants',
108
		'Listusers' => 'SpecialListUsers',
109
		'Listadmins' => 'SpecialListAdmins',
110
		'Listbots' => 'SpecialListBots',
111
		'Userrights' => 'UserrightsPage',
112
		'EditWatchlist' => 'SpecialEditWatchlist',
113
114
		// Recent changes and logs
115
		'Newimages' => 'SpecialNewFiles',
116
		'Log' => 'SpecialLog',
117
		'Watchlist' => 'SpecialWatchlist',
118
		'Newpages' => 'SpecialNewpages',
119
		'Recentchanges' => 'SpecialRecentChanges',
120
		'Recentchangeslinked' => 'SpecialRecentChangesLinked',
121
		'Tags' => 'SpecialTags',
122
123
		// Media reports and uploads
124
		'Listfiles' => 'SpecialListFiles',
125
		'Filepath' => 'SpecialFilepath',
126
		'MediaStatistics' => 'MediaStatisticsPage',
127
		'MIMEsearch' => 'MIMEsearchPage',
128
		'FileDuplicateSearch' => 'FileDuplicateSearchPage',
129
		'Upload' => 'SpecialUpload',
130
		'UploadStash' => 'SpecialUploadStash',
131
		'ListDuplicatedFiles' => 'ListDuplicatedFilesPage',
132
133
		// Data and tools
134
		'ApiSandbox' => 'SpecialApiSandbox',
135
		'Statistics' => 'SpecialStatistics',
136
		'Allmessages' => 'SpecialAllMessages',
137
		'Version' => 'SpecialVersion',
138
		'Lockdb' => 'SpecialLockdb',
139
		'Unlockdb' => 'SpecialUnlockdb',
140
141
		// Redirecting special pages
142
		'LinkSearch' => 'LinkSearchPage',
143
		'Randompage' => 'RandomPage',
144
		'RandomInCategory' => 'SpecialRandomInCategory',
145
		'Randomredirect' => 'SpecialRandomredirect',
146
		'Randomrootpage' => 'SpecialRandomrootpage',
147
148
		// High use pages
149
		'Mostlinkedcategories' => 'MostlinkedCategoriesPage',
150
		'Mostimages' => 'MostimagesPage',
151
		'Mostinterwikis' => 'MostinterwikisPage',
152
		'Mostlinked' => 'MostlinkedPage',
153
		'Mostlinkedtemplates' => 'MostlinkedTemplatesPage',
154
		'Mostcategories' => 'MostcategoriesPage',
155
		'Mostrevisions' => 'MostrevisionsPage',
156
157
		// Page tools
158
		'ComparePages' => 'SpecialComparePages',
159
		'Export' => 'SpecialExport',
160
		'Import' => 'SpecialImport',
161
		'Undelete' => 'SpecialUndelete',
162
		'Whatlinkshere' => 'SpecialWhatLinksHere',
163
		'MergeHistory' => 'SpecialMergeHistory',
164
		'ExpandTemplates' => 'SpecialExpandTemplates',
165
166
		// Other
167
		'Booksources' => 'SpecialBookSources',
168
169
		// Unlisted / redirects
170
		'ApiHelp' => 'SpecialApiHelp',
171
		'Blankpage' => 'SpecialBlankpage',
172
		'Diff' => 'SpecialDiff',
173
		'EditTags' => 'SpecialEditTags',
174
		'Emailuser' => 'SpecialEmailUser',
175
		'Movepage' => 'MovePageForm',
176
		'Mycontributions' => 'SpecialMycontributions',
177
		'MyLanguage' => 'SpecialMyLanguage',
178
		'Mypage' => 'SpecialMypage',
179
		'Mytalk' => 'SpecialMytalk',
180
		'Myuploads' => 'SpecialMyuploads',
181
		'AllMyUploads' => 'SpecialAllMyUploads',
182
		'PermanentLink' => 'SpecialPermanentLink',
183
		'Redirect' => 'SpecialRedirect',
184
		'Revisiondelete' => 'SpecialRevisionDelete',
185
		'RunJobs' => 'SpecialRunJobs',
186
		'Specialpages' => 'SpecialSpecialpages',
187
	];
188
189
	private static $list;
190
	private static $aliases;
191
192
	/**
193
	 * Reset the internal list of special pages. Useful when changing $wgSpecialPages after
194
	 * the internal list has already been initialized, e.g. during testing.
195
	 */
196
	public static function resetList() {
197
		self::$list = null;
198
		self::$aliases = null;
199
	}
200
201
	/**
202
	 * Returns a list of canonical special page names.
203
	 * May be used to iterate over all registered special pages.
204
	 *
205
	 * @return string[]
206
	 */
207
	public static function getNames() {
208
		return array_keys( self::getPageList() );
209
	}
210
211
	/**
212
	 * Get the special page list as an array
213
	 *
214
	 * @deprecated since 1.24, use getNames() instead.
215
	 * @return array
216
	 */
217
	public static function getList() {
218
		wfDeprecated( __FUNCTION__, '1.24' );
219
		return self::getPageList();
220
	}
221
222
	/**
223
	 * Get the special page list as an array
224
	 *
225
	 * @return array
226
	 */
227
	private static function getPageList() {
228
		global $wgSpecialPages;
229
		global $wgDisableInternalSearch, $wgEmailAuthentication;
230
		global $wgEnableEmail, $wgEnableJavaScriptTest;
231
		global $wgPageLanguageUseDB, $wgContentHandlerUseDB;
232
233
		if ( !is_array( self::$list ) ) {
234
235
			self::$list = self::$coreList;
236
237
			if ( !$wgDisableInternalSearch ) {
238
				self::$list['Search'] = 'SpecialSearch';
239
			}
240
241
			if ( $wgEmailAuthentication ) {
242
				self::$list['Confirmemail'] = 'EmailConfirmation';
243
				self::$list['Invalidateemail'] = 'EmailInvalidation';
244
			}
245
246
			if ( $wgEnableEmail ) {
247
				self::$list['ChangeEmail'] = 'SpecialChangeEmail';
248
			}
249
250
			if ( $wgEnableJavaScriptTest ) {
251
				self::$list['JavaScriptTest'] = 'SpecialJavaScriptTest';
252
			}
253
254
			if ( $wgPageLanguageUseDB ) {
255
				self::$list['PageLanguage'] = 'SpecialPageLanguage';
256
			}
257
			if ( $wgContentHandlerUseDB ) {
258
				self::$list['ChangeContentModel'] = 'SpecialChangeContentModel';
259
			}
260
261
			// Add extension special pages
262
			self::$list = array_merge( self::$list, $wgSpecialPages );
263
264
			// This hook can be used to disable unwanted core special pages
265
			// or conditionally register special pages.
266
			Hooks::run( 'SpecialPage_initList', [ &self::$list ] );
267
268
		}
269
270
		return self::$list;
271
	}
272
273
	/**
274
	 * Initialise and return the list of special page aliases. Returns an array where
275
	 * the key is an alias, and the value is the canonical name of the special page.
276
	 * All registered special pages are guaranteed to map to themselves.
277
	 * @return array
278
	 */
279
	private static function getAliasList() {
280
		if ( is_null( self::$aliases ) ) {
281
			global $wgContLang;
282
			$aliases = $wgContLang->getSpecialPageAliases();
283
			$pageList = self::getPageList();
284
285
			self::$aliases = [];
286
			$keepAlias = [];
287
288
			// Force every canonical name to be an alias for itself.
289
			foreach ( $pageList as $name => $stuff ) {
290
				$caseFoldedAlias = $wgContLang->caseFold( $name );
291
				self::$aliases[$caseFoldedAlias] = $name;
292
				$keepAlias[$caseFoldedAlias] = 'canonical';
293
			}
294
295
			// Check for $aliases being an array since Language::getSpecialPageAliases can return null
296
			if ( is_array( $aliases ) ) {
297
				foreach ( $aliases as $realName => $aliasList ) {
298
					$aliasList = array_values( $aliasList );
299
					foreach ( $aliasList as $i => $alias ) {
300
						$caseFoldedAlias = $wgContLang->caseFold( $alias );
301
302
						if ( isset( self::$aliases[$caseFoldedAlias] ) &&
303
							$realName === self::$aliases[$caseFoldedAlias]
304
						) {
305
							// Ignore same-realName conflicts
306
							continue;
307
						}
308
309
						if ( !isset( $keepAlias[$caseFoldedAlias] ) ) {
310
							self::$aliases[$caseFoldedAlias] = $realName;
311
							if ( !$i ) {
312
								$keepAlias[$caseFoldedAlias] = 'first';
313
							}
314
						} elseif ( !$i ) {
315
							wfWarn( "First alias '$alias' for $realName conflicts with " .
316
								"{$keepAlias[$caseFoldedAlias]} alias for " .
317
								self::$aliases[$caseFoldedAlias]
318
							);
319
						}
320
					}
321
				}
322
			}
323
		}
324
325
		return self::$aliases;
326
	}
327
328
	/**
329
	 * Given a special page name with a possible subpage, return an array
330
	 * where the first element is the special page name and the second is the
331
	 * subpage.
332
	 *
333
	 * @param string $alias
334
	 * @return array Array( String, String|null ), or array( null, null ) if the page is invalid
335
	 */
336
	public static function resolveAlias( $alias ) {
337
		global $wgContLang;
338
		$bits = explode( '/', $alias, 2 );
339
340
		$caseFoldedAlias = $wgContLang->caseFold( $bits[0] );
341
		$caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias );
342
		$aliases = self::getAliasList();
343
		if ( isset( $aliases[$caseFoldedAlias] ) ) {
344
			$name = $aliases[$caseFoldedAlias];
345
		} else {
346
			return [ null, null ];
347
		}
348
349 View Code Duplication
		if ( !isset( $bits[1] ) ) { // bug 2087
350
			$par = null;
351
		} else {
352
			$par = $bits[1];
353
		}
354
355
		return [ $name, $par ];
356
	}
357
358
	/**
359
	 * Check if a given name exist as a special page or as a special page alias
360
	 *
361
	 * @param string $name Name of a special page
362
	 * @return bool True if a special page exists with this name
363
	 */
364
	public static function exists( $name ) {
365
		list( $title, /*...*/ ) = self::resolveAlias( $name );
366
367
		$specialPageList = self::getPageList();
368
		return isset( $specialPageList[$title] );
369
	}
370
371
	/**
372
	 * Find the object with a given name and return it (or NULL)
373
	 *
374
	 * @param string $name Special page name, may be localised and/or an alias
375
	 * @return SpecialPage|null SpecialPage object or null if the page doesn't exist
376
	 */
377
	public static function getPage( $name ) {
378
		list( $realName, /*...*/ ) = self::resolveAlias( $name );
379
380
		$specialPageList = self::getPageList();
381
382
		if ( isset( $specialPageList[$realName] ) ) {
383
			$rec = $specialPageList[$realName];
384
385
			if ( is_callable( $rec ) ) {
386
				// Use callback to instantiate the special page
387
				$page = call_user_func( $rec );
388
			} elseif ( is_string( $rec ) ) {
389
				$className = $rec;
390
				$page = new $className;
391
			} elseif ( is_array( $rec ) ) {
392
				$className = array_shift( $rec );
393
				// @deprecated, officially since 1.18, unofficially since forever
394
				wfDeprecated( "Array syntax for \$wgSpecialPages is deprecated ($className), " .
395
					"define a subclass of SpecialPage instead.", '1.18' );
396
				$page = ObjectFactory::getObjectFromSpec( [
397
					'class' => $className,
398
					'args' => $rec,
399
					'closure_expansion' => false,
400
				] );
401
			} elseif ( $rec instanceof SpecialPage ) {
402
				$page = $rec; // XXX: we should deep clone here
403
			} else {
404
				$page = null;
405
			}
406
407
			if ( $page instanceof SpecialPage ) {
408
				return $page;
409
			} else {
410
				// It's not a classname, nor a callback, nor a legacy constructor array,
411
				// nor a special page object. Give up.
412
				wfLogWarning( "Cannot instantiate special page $realName: bad spec!" );
413
				return null;
414
			}
415
416
		} else {
417
			return null;
418
		}
419
	}
420
421
	/**
422
	 * Return categorised listable special pages which are available
423
	 * for the current user, and everyone.
424
	 *
425
	 * @param User $user User object to check permissions, $wgUser will be used
426
	 *        if not provided
427
	 * @return array ( string => Specialpage )
428
	 */
429
	public static function getUsablePages( User $user = null ) {
430
		$pages = [];
431
		if ( $user === null ) {
432
			global $wgUser;
433
			$user = $wgUser;
434
		}
435
		foreach ( self::getPageList() as $name => $rec ) {
436
			$page = self::getPage( $name );
437
			if ( $page ) { // not null
438
				$page->setContext( RequestContext::getMain() );
439
				if ( $page->isListed()
440
					&& ( !$page->isRestricted() || $page->userCanExecute( $user ) )
441
				) {
442
					$pages[$name] = $page;
443
				}
444
			}
445
		}
446
447
		return $pages;
448
	}
449
450
	/**
451
	 * Return categorised listable special pages for all users
452
	 *
453
	 * @return array ( string => Specialpage )
454
	 */
455
	public static function getRegularPages() {
456
		$pages = [];
457 View Code Duplication
		foreach ( self::getPageList() as $name => $rec ) {
458
			$page = self::getPage( $name );
459
			if ( $page->isListed() && !$page->isRestricted() ) {
460
				$pages[$name] = $page;
461
			}
462
		}
463
464
		return $pages;
465
	}
466
467
	/**
468
	 * Return categorised listable special pages which are available
469
	 * for the current user, but not for everyone
470
	 *
471
	 * @param User|null $user User object to use or null for $wgUser
472
	 * @return array ( string => Specialpage )
473
	 */
474
	public static function getRestrictedPages( User $user = null ) {
475
		$pages = [];
476
		if ( $user === null ) {
477
			global $wgUser;
478
			$user = $wgUser;
479
		}
480 View Code Duplication
		foreach ( self::getPageList() as $name => $rec ) {
481
			$page = self::getPage( $name );
482
			if (
483
				$page->isListed()
484
				&& $page->isRestricted()
485
				&& $page->userCanExecute( $user )
486
			) {
487
				$pages[$name] = $page;
488
			}
489
		}
490
491
		return $pages;
492
	}
493
494
	/**
495
	 * Execute a special page path.
496
	 * The path may contain parameters, e.g. Special:Name/Params
497
	 * Extracts the special page name and call the execute method, passing the parameters
498
	 *
499
	 * Returns a title object if the page is redirected, false if there was no such special
500
	 * page, and true if it was successful.
501
	 *
502
	 * @param Title $title
503
	 * @param IContextSource $context
504
	 * @param bool $including Bool output is being captured for use in {{special:whatever}}
505
	 * @param LinkRenderer|null $linkRenderer (since 1.28)
506
	 *
507
	 * @return bool
508
	 */
509
	public static function executePath( Title &$title, IContextSource &$context, $including = false,
510
		LinkRenderer $linkRenderer = null
511
	) {
512
		// @todo FIXME: Redirects broken due to this call
513
		$bits = explode( '/', $title->getDBkey(), 2 );
514
		$name = $bits[0];
515 View Code Duplication
		if ( !isset( $bits[1] ) ) { // bug 2087
516
			$par = null;
517
		} else {
518
			$par = $bits[1];
519
		}
520
521
		$page = self::getPage( $name );
522
		if ( !$page ) {
523
			$context->getOutput()->setArticleRelated( false );
524
			$context->getOutput()->setRobotPolicy( 'noindex,nofollow' );
525
526
			global $wgSend404Code;
527
			if ( $wgSend404Code ) {
528
				$context->getOutput()->setStatusCode( 404 );
529
			}
530
531
			$context->getOutput()->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' );
532
533
			return false;
534
		}
535
536
		if ( !$including ) {
537
			// Narrow DB query expectations for this HTTP request
538
			$trxLimits = $context->getConfig()->get( 'TrxProfilerLimits' );
539
			$trxProfiler = Profiler::instance()->getTransactionProfiler();
540
			if ( $context->getRequest()->wasPosted() && !$page->doesWrites() ) {
541
				$trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ );
542
				$context->getRequest()->markAsSafeRequest();
543
			}
544
		}
545
546
		// Page exists, set the context
547
		$page->setContext( $context );
548
549
		if ( !$including ) {
550
			// Redirect to canonical alias for GET commands
551
			// Not for POST, we'd lose the post data, so it's best to just distribute
552
			// the request. Such POST requests are possible for old extensions that
553
			// generate self-links without being aware that their default name has
554
			// changed.
555
			if ( $name != $page->getLocalName() && !$context->getRequest()->wasPosted() ) {
556
				$query = $context->getRequest()->getQueryValues();
557
				unset( $query['title'] );
558
				$title = $page->getPageTitle( $par );
559
				$url = $title->getFullURL( $query );
560
				$context->getOutput()->redirect( $url );
561
562
				return $title;
563
			} else {
564
				$context->setTitle( $page->getPageTitle( $par ) );
0 ignored issues
show
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...
565
			}
566
		} elseif ( !$page->isIncludable() ) {
567
			return false;
568
		}
569
570
		$page->including( $including );
571
		if ( $linkRenderer ) {
572
			$page->setLinkRenderer( $linkRenderer );
573
		}
574
575
		// Execute special page
576
		$page->run( $par );
577
578
		return true;
579
	}
580
581
	/**
582
	 * Just like executePath() but will override global variables and execute
583
	 * the page in "inclusion" mode. Returns true if the execution was
584
	 * successful or false if there was no such special page, or a title object
585
	 * if it was a redirect.
586
	 *
587
	 * Also saves the current $wgTitle, $wgOut, $wgRequest, $wgUser and $wgLang
588
	 * variables so that the special page will get the context it'd expect on a
589
	 * normal request, and then restores them to their previous values after.
590
	 *
591
	 * @param Title $title
592
	 * @param IContextSource $context
593
	 * @param LinkRenderer|null $linkRenderer (since 1.28)
594
	 * @return string HTML fragment
595
	 */
596
	public static function capturePath(
597
		Title $title, IContextSource $context, LinkRenderer $linkRenderer = null
598
	) {
599
		global $wgTitle, $wgOut, $wgRequest, $wgUser, $wgLang;
600
		$main = RequestContext::getMain();
601
602
		// Save current globals and main context
603
		$glob = [
604
			'title' => $wgTitle,
605
			'output' => $wgOut,
606
			'request' => $wgRequest,
607
			'user' => $wgUser,
608
			'language' => $wgLang,
609
		];
610
		$ctx = [
611
			'title' => $main->getTitle(),
612
			'output' => $main->getOutput(),
613
			'request' => $main->getRequest(),
614
			'user' => $main->getUser(),
615
			'language' => $main->getLanguage(),
616
		];
617
618
		// Override
619
		$wgTitle = $title;
620
		$wgOut = $context->getOutput();
621
		$wgRequest = $context->getRequest();
622
		$wgUser = $context->getUser();
623
		$wgLang = $context->getLanguage();
624
		$main->setTitle( $title );
625
		$main->setOutput( $context->getOutput() );
626
		$main->setRequest( $context->getRequest() );
627
		$main->setUser( $context->getUser() );
628
		$main->setLanguage( $context->getLanguage() );
629
630
		// The useful part
631
		$ret = self::executePath( $title, $context, true, $linkRenderer );
632
633
		// Restore old globals and context
634
		$wgTitle = $glob['title'];
635
		$wgOut = $glob['output'];
636
		$wgRequest = $glob['request'];
637
		$wgUser = $glob['user'];
638
		$wgLang = $glob['language'];
639
		$main->setTitle( $ctx['title'] );
640
		$main->setOutput( $ctx['output'] );
641
		$main->setRequest( $ctx['request'] );
642
		$main->setUser( $ctx['user'] );
643
		$main->setLanguage( $ctx['language'] );
644
645
		return $ret;
646
	}
647
648
	/**
649
	 * Get the local name for a specified canonical name
650
	 *
651
	 * @param string $name
652
	 * @param string|bool $subpage
653
	 * @return string
654
	 */
655
	public static function getLocalNameFor( $name, $subpage = false ) {
656
		global $wgContLang;
657
		$aliases = $wgContLang->getSpecialPageAliases();
658
		$aliasList = self::getAliasList();
659
660
		// Find the first alias that maps back to $name
661
		if ( isset( $aliases[$name] ) ) {
662
			$found = false;
663
			foreach ( $aliases[$name] as $alias ) {
664
				$caseFoldedAlias = $wgContLang->caseFold( $alias );
665
				$caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias );
666
				if ( isset( $aliasList[$caseFoldedAlias] ) &&
667
					$aliasList[$caseFoldedAlias] === $name
668
				) {
669
					$name = $alias;
670
					$found = true;
671
					break;
672
				}
673
			}
674
			if ( !$found ) {
675
				wfWarn( "Did not find a usable alias for special page '$name'. " .
676
					"It seems all defined aliases conflict?" );
677
			}
678
		} else {
679
			// Check if someone misspelled the correct casing
680
			if ( is_array( $aliases ) ) {
681
				foreach ( $aliases as $n => $values ) {
682
					if ( strcasecmp( $name, $n ) === 0 ) {
683
						wfWarn( "Found alias defined for $n when searching for " .
684
							"special page aliases for $name. Case mismatch?" );
685
						return self::getLocalNameFor( $n, $subpage );
686
					}
687
				}
688
			}
689
690
			wfWarn( "Did not find alias for special page '$name'. " .
691
				"Perhaps no aliases are defined for it?" );
692
		}
693
694
		if ( $subpage !== false && !is_null( $subpage ) ) {
695
			// Make sure it's in dbkey form
696
			$subpage = str_replace( ' ', '_', $subpage );
697
			$name = "$name/$subpage";
698
		}
699
700
		return $wgContLang->ucfirst( $name );
701
	}
702
703
	/**
704
	 * Get a title for a given alias
705
	 *
706
	 * @param string $alias
707
	 * @return Title|null Title or null if there is no such alias
708
	 */
709
	public static function getTitleForAlias( $alias ) {
710
		list( $name, $subpage ) = self::resolveAlias( $alias );
711
		if ( $name != null ) {
712
			return SpecialPage::getTitleFor( $name, $subpage );
713
		} else {
714
			return null;
715
		}
716
	}
717
}
718