SpecialPage::getFullTitle()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Parent class for all special pages.
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
 */
23
24
use MediaWiki\Auth\AuthManager;
25
use MediaWiki\Linker\LinkRenderer;
26
use MediaWiki\MediaWikiServices;
27
28
/**
29
 * Parent class for all special pages.
30
 *
31
 * Includes some static functions for handling the special page list deprecated
32
 * in favor of SpecialPageFactory.
33
 *
34
 * @ingroup SpecialPage
35
 */
36
class SpecialPage {
37
	// The canonical name of this special page
38
	// Also used for the default <h1> heading, @see getDescription()
39
	protected $mName;
40
41
	// The local name of this special page
42
	private $mLocalName;
43
44
	// Minimum user level required to access this page, or "" for anyone.
45
	// Also used to categorise the pages in Special:Specialpages
46
	protected $mRestriction;
47
48
	// Listed in Special:Specialpages?
49
	private $mListed;
50
51
	// Whether or not this special page is being included from an article
52
	protected $mIncluding;
53
54
	// Whether the special page can be included in an article
55
	protected $mIncludable;
56
57
	/**
58
	 * Current request context
59
	 * @var IContextSource
60
	 */
61
	protected $mContext;
62
63
	/**
64
	 * @var \MediaWiki\Linker\LinkRenderer|null
65
	 */
66
	private $linkRenderer;
67
68
	/**
69
	 * Get a localised Title object for a specified special page name
70
	 * If you don't need a full Title object, consider using TitleValue through
71
	 * getTitleValueFor() below.
72
	 *
73
	 * @since 1.9
74
	 * @since 1.21 $fragment parameter added
75
	 *
76
	 * @param string $name
77
	 * @param string|bool $subpage Subpage string, or false to not use a subpage
78
	 * @param string $fragment The link fragment (after the "#")
79
	 * @return Title
80
	 * @throws MWException
81
	 */
82
	public static function getTitleFor( $name, $subpage = false, $fragment = '' ) {
83
		return Title::newFromTitleValue(
84
			self::getTitleValueFor( $name, $subpage, $fragment )
85
		);
86
	}
87
88
	/**
89
	 * Get a localised TitleValue object for a specified special page name
90
	 *
91
	 * @since 1.28
92
	 * @param string $name
93
	 * @param string|bool $subpage Subpage string, or false to not use a subpage
94
	 * @param string $fragment The link fragment (after the "#")
95
	 * @return TitleValue
96
	 */
97
	public static function getTitleValueFor( $name, $subpage = false, $fragment = '' ) {
98
		$name = SpecialPageFactory::getLocalNameFor( $name, $subpage );
99
100
		return new TitleValue( NS_SPECIAL, $name, $fragment );
101
	}
102
103
	/**
104
	 * Get a localised Title object for a page name with a possibly unvalidated subpage
105
	 *
106
	 * @param string $name
107
	 * @param string|bool $subpage Subpage string, or false to not use a subpage
108
	 * @return Title|null Title object or null if the page doesn't exist
109
	 */
110
	public static function getSafeTitleFor( $name, $subpage = false ) {
111
		$name = SpecialPageFactory::getLocalNameFor( $name, $subpage );
112
		if ( $name ) {
113
			return Title::makeTitleSafe( NS_SPECIAL, $name );
114
		} else {
115
			return null;
116
		}
117
	}
118
119
	/**
120
	 * Default constructor for special pages
121
	 * Derivative classes should call this from their constructor
122
	 *     Note that if the user does not have the required level, an error message will
123
	 *     be displayed by the default execute() method, without the global function ever
124
	 *     being called.
125
	 *
126
	 *     If you override execute(), you can recover the default behavior with userCanExecute()
127
	 *     and displayRestrictionError()
128
	 *
129
	 * @param string $name Name of the special page, as seen in links and URLs
130
	 * @param string $restriction User right required, e.g. "block" or "delete"
131
	 * @param bool $listed Whether the page is listed in Special:Specialpages
132
	 * @param callable|bool $function Unused
133
	 * @param string $file Unused
134
	 * @param bool $includable Whether the page can be included in normal pages
135
	 */
136
	public function __construct(
137
		$name = '', $restriction = '', $listed = true,
138
		$function = false, $file = '', $includable = false
139
	) {
140
		$this->mName = $name;
141
		$this->mRestriction = $restriction;
142
		$this->mListed = $listed;
143
		$this->mIncludable = $includable;
144
	}
145
146
	/**
147
	 * Get the name of this Special Page.
148
	 * @return string
149
	 */
150
	function getName() {
151
		return $this->mName;
152
	}
153
154
	/**
155
	 * Get the permission that a user must have to execute this page
156
	 * @return string
157
	 */
158
	function getRestriction() {
159
		return $this->mRestriction;
160
	}
161
162
	// @todo FIXME: Decide which syntax to use for this, and stick to it
163
	/**
164
	 * Whether this special page is listed in Special:SpecialPages
165
	 * @since 1.3 (r3583)
166
	 * @return bool
167
	 */
168
	function isListed() {
169
		return $this->mListed;
170
	}
171
172
	/**
173
	 * Set whether this page is listed in Special:Specialpages, at run-time
174
	 * @since 1.3
175
	 * @param bool $listed
176
	 * @return bool
177
	 */
178
	function setListed( $listed ) {
179
		return wfSetVar( $this->mListed, $listed );
180
	}
181
182
	/**
183
	 * Get or set whether this special page is listed in Special:SpecialPages
184
	 * @since 1.6
185
	 * @param bool $x
186
	 * @return bool
187
	 */
188
	function listed( $x = null ) {
189
		return wfSetVar( $this->mListed, $x );
190
	}
191
192
	/**
193
	 * Whether it's allowed to transclude the special page via {{Special:Foo/params}}
194
	 * @return bool
195
	 */
196
	public function isIncludable() {
197
		return $this->mIncludable;
198
	}
199
200
	/**
201
	 * How long to cache page when it is being included.
202
	 *
203
	 * @note If cache time is not 0, then the current user becomes an anon
204
	 *   if you want to do any per-user customizations, than this method
205
	 *   must be overriden to return 0.
206
	 * @since 1.26
207
	 * @return int Time in seconds, 0 to disable caching altogether,
208
	 *  false to use the parent page's cache settings
209
	 */
210
	public function maxIncludeCacheTime() {
211
		return $this->getConfig()->get( 'MiserMode' ) ? $this->getCacheTTL() : 0;
212
	}
213
214
	/**
215
	 * @return int Seconds that this page can be cached
216
	 */
217
	protected function getCacheTTL() {
218
		return 60 * 60;
219
	}
220
221
	/**
222
	 * Whether the special page is being evaluated via transclusion
223
	 * @param bool $x
224
	 * @return bool
225
	 */
226
	function including( $x = null ) {
227
		return wfSetVar( $this->mIncluding, $x );
228
	}
229
230
	/**
231
	 * Get the localised name of the special page
232
	 * @return string
233
	 */
234
	function getLocalName() {
235
		if ( !isset( $this->mLocalName ) ) {
236
			$this->mLocalName = SpecialPageFactory::getLocalNameFor( $this->mName );
237
		}
238
239
		return $this->mLocalName;
240
	}
241
242
	/**
243
	 * Is this page expensive (for some definition of expensive)?
244
	 * Expensive pages are disabled or cached in miser mode.  Originally used
245
	 * (and still overridden) by QueryPage and subclasses, moved here so that
246
	 * Special:SpecialPages can safely call it for all special pages.
247
	 *
248
	 * @return bool
249
	 */
250
	public function isExpensive() {
251
		return false;
252
	}
253
254
	/**
255
	 * Is this page cached?
256
	 * Expensive pages are cached or disabled in miser mode.
257
	 * Used by QueryPage and subclasses, moved here so that
258
	 * Special:SpecialPages can safely call it for all special pages.
259
	 *
260
	 * @return bool
261
	 * @since 1.21
262
	 */
263
	public function isCached() {
264
		return false;
265
	}
266
267
	/**
268
	 * Can be overridden by subclasses with more complicated permissions
269
	 * schemes.
270
	 *
271
	 * @return bool Should the page be displayed with the restricted-access
272
	 *   pages?
273
	 */
274
	public function isRestricted() {
275
		// DWIM: If anons can do something, then it is not restricted
276
		return $this->mRestriction != '' && !User::groupHasPermission( '*', $this->mRestriction );
277
	}
278
279
	/**
280
	 * Checks if the given user (identified by an object) can execute this
281
	 * special page (as defined by $mRestriction).  Can be overridden by sub-
282
	 * classes with more complicated permissions schemes.
283
	 *
284
	 * @param User $user The user to check
285
	 * @return bool Does the user have permission to view the page?
286
	 */
287
	public function userCanExecute( User $user ) {
288
		return $user->isAllowed( $this->mRestriction );
289
	}
290
291
	/**
292
	 * Output an error message telling the user what access level they have to have
293
	 * @throws PermissionsError
294
	 */
295
	function displayRestrictionError() {
296
		throw new PermissionsError( $this->mRestriction );
297
	}
298
299
	/**
300
	 * Checks if userCanExecute, and if not throws a PermissionsError
301
	 *
302
	 * @since 1.19
303
	 * @return void
304
	 * @throws PermissionsError
305
	 */
306
	public function checkPermissions() {
307
		if ( !$this->userCanExecute( $this->getUser() ) ) {
308
			$this->displayRestrictionError();
309
		}
310
	}
311
312
	/**
313
	 * If the wiki is currently in readonly mode, throws a ReadOnlyError
314
	 *
315
	 * @since 1.19
316
	 * @return void
317
	 * @throws ReadOnlyError
318
	 */
319
	public function checkReadOnly() {
320
		if ( wfReadOnly() ) {
321
			throw new ReadOnlyError;
322
		}
323
	}
324
325
	/**
326
	 * If the user is not logged in, throws UserNotLoggedIn error
327
	 *
328
	 * The user will be redirected to Special:Userlogin with the given message as an error on
329
	 * the form.
330
	 *
331
	 * @since 1.23
332
	 * @param string $reasonMsg [optional] Message key to be displayed on login page
333
	 * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor
334
	 * @throws UserNotLoggedIn
335
	 */
336
	public function requireLogin(
337
		$reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin'
338
	) {
339
		if ( $this->getUser()->isAnon() ) {
340
			throw new UserNotLoggedIn( $reasonMsg, $titleMsg );
341
		}
342
	}
343
344
	/**
345
	 * Tells if the special page does something security-sensitive and needs extra defense against
346
	 * a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the
347
	 * authentication framework.
348
	 * @return bool|string False or the argument for AuthManager::securitySensitiveOperationStatus().
349
	 *   Typically a special page needing elevated security would return its name here.
350
	 */
351
	protected function getLoginSecurityLevel() {
352
		return false;
353
	}
354
355
	/**
356
	 * Verifies that the user meets the security level, possibly reauthenticating them in the process.
357
	 *
358
	 * This should be used when the page does something security-sensitive and needs extra defense
359
	 * against a stolen account (e.g. a reauthentication). The authentication framework will make
360
	 * an extra effort to make sure the user account is not compromised. What that exactly means
361
	 * will depend on the system and user settings; e.g. the user might be required to log in again
362
	 * unless their last login happened recently, or they might be given a second-factor challenge.
363
	 *
364
	 * Calling this method will result in one if these actions:
365
	 * - return true: all good.
366
	 * - return false and set a redirect: caller should abort; the redirect will take the user
367
	 *   to the login page for reauthentication, and back.
368
	 * - throw an exception if there is no way for the user to meet the requirements without using
369
	 *   a different access method (e.g. this functionality is only available from a specific IP).
370
	 *
371
	 * Note that this does not in any way check that the user is authorized to use this special page
372
	 * (use checkPermissions() for that).
373
	 *
374
	 * @param string $level A security level. Can be an arbitrary string, defaults to the page name.
375
	 * @return bool False means a redirect to the reauthentication page has been set and processing
376
	 *   of the special page should be aborted.
377
	 * @throws ErrorPageError If the security level cannot be met, even with reauthentication.
378
	 */
379
	protected function checkLoginSecurityLevel( $level = null ) {
380
		$level = $level ?: $this->getName();
381
		$securityStatus = AuthManager::singleton()->securitySensitiveOperationStatus( $level );
382
		if ( $securityStatus === AuthManager::SEC_OK ) {
383
			return true;
384
		} elseif ( $securityStatus === AuthManager::SEC_REAUTH ) {
385
			$request = $this->getRequest();
386
			$title = SpecialPage::getTitleFor( 'Userlogin' );
387
			$query = [
388
				'returnto' => $this->getFullTitle()->getPrefixedDBkey(),
389
				'returntoquery' => wfArrayToCgi( array_diff_key( $request->getQueryValues(),
390
					[ 'title' => true ] ) ),
391
				'force' => $level,
392
			];
393
			$url = $title->getFullURL( $query, false, PROTO_HTTPS );
394
395
			$this->getOutput()->redirect( $url );
396
			return false;
397
		}
398
399
		$titleMessage = wfMessage( 'specialpage-securitylevel-not-allowed-title' );
400
		$errorMessage = wfMessage( 'specialpage-securitylevel-not-allowed' );
401
		throw new ErrorPageError( $titleMessage, $errorMessage );
402
	}
403
404
	/**
405
	 * Return an array of subpages beginning with $search that this special page will accept.
406
	 *
407
	 * For example, if a page supports subpages "foo", "bar" and "baz" (as in Special:PageName/foo,
408
	 * etc.):
409
	 *
410
	 *   - `prefixSearchSubpages( "ba" )` should return `array( "bar", "baz" )`
411
	 *   - `prefixSearchSubpages( "f" )` should return `array( "foo" )`
412
	 *   - `prefixSearchSubpages( "z" )` should return `array()`
413
	 *   - `prefixSearchSubpages( "" )` should return `array( foo", "bar", "baz" )`
414
	 *
415
	 * @param string $search Prefix to search for
416
	 * @param int $limit Maximum number of results to return (usually 10)
417
	 * @param int $offset Number of results to skip (usually 0)
418
	 * @return string[] Matching subpages
419
	 */
420
	public function prefixSearchSubpages( $search, $limit, $offset ) {
421
		$subpages = $this->getSubpagesForPrefixSearch();
422
		if ( !$subpages ) {
423
			return [];
424
		}
425
426
		return self::prefixSearchArray( $search, $limit, $subpages, $offset );
427
	}
428
429
	/**
430
	 * Return an array of subpages that this special page will accept for prefix
431
	 * searches. If this method requires a query you might instead want to implement
432
	 * prefixSearchSubpages() directly so you can support $limit and $offset. This
433
	 * method is better for static-ish lists of things.
434
	 *
435
	 * @return string[] subpages to search from
436
	 */
437
	protected function getSubpagesForPrefixSearch() {
438
		return [];
439
	}
440
441
	/**
442
	 * Perform a regular substring search for prefixSearchSubpages
443
	 * @param string $search Prefix to search for
444
	 * @param int $limit Maximum number of results to return (usually 10)
445
	 * @param int $offset Number of results to skip (usually 0)
446
	 * @return string[] Matching subpages
447
	 */
448
	protected function prefixSearchString( $search, $limit, $offset ) {
449
		$title = Title::newFromText( $search );
450
		if ( !$title || !$title->canExist() ) {
451
			// No prefix suggestion in special and media namespace
452
			return [];
453
		}
454
455
		$searchEngine = MediaWikiServices::getInstance()->newSearchEngine();
456
		$searchEngine->setLimitOffset( $limit, $offset );
457
		$searchEngine->setNamespaces( [] );
458
		$result = $searchEngine->defaultPrefixSearch( $search );
459
		return array_map( function( Title $t ) {
460
			return $t->getPrefixedText();
461
		}, $result );
462
	}
463
464
	/**
465
	 * Helper function for implementations of prefixSearchSubpages() that
466
	 * filter the values in memory (as opposed to making a query).
467
	 *
468
	 * @since 1.24
469
	 * @param string $search
470
	 * @param int $limit
471
	 * @param array $subpages
472
	 * @param int $offset
473
	 * @return string[]
474
	 */
475
	protected static function prefixSearchArray( $search, $limit, array $subpages, $offset ) {
476
		$escaped = preg_quote( $search, '/' );
477
		return array_slice( preg_grep( "/^$escaped/i",
478
			array_slice( $subpages, $offset ) ), 0, $limit );
479
	}
480
481
	/**
482
	 * Sets headers - this should be called from the execute() method of all derived classes!
483
	 */
484
	function setHeaders() {
485
		$out = $this->getOutput();
486
		$out->setArticleRelated( false );
487
		$out->setRobotPolicy( $this->getRobotPolicy() );
488
		$out->setPageTitle( $this->getDescription() );
489
		if ( $this->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
490
			$out->addModuleStyles( [
491
				'mediawiki.ui.input',
492
				'mediawiki.ui.radio',
493
				'mediawiki.ui.checkbox',
494
			] );
495
		}
496
	}
497
498
	/**
499
	 * Entry point.
500
	 *
501
	 * @since 1.20
502
	 *
503
	 * @param string|null $subPage
504
	 */
505
	final public function run( $subPage ) {
506
		/**
507
		 * Gets called before @see SpecialPage::execute.
508
		 * Return false to prevent calling execute() (since 1.27+).
509
		 *
510
		 * @since 1.20
511
		 *
512
		 * @param SpecialPage $this
513
		 * @param string|null $subPage
514
		 */
515
		if ( !Hooks::run( 'SpecialPageBeforeExecute', [ $this, $subPage ] ) ) {
516
			return;
517
		}
518
519
		if ( $this->beforeExecute( $subPage ) === false ) {
520
			return;
521
		}
522
		$this->execute( $subPage );
523
		$this->afterExecute( $subPage );
524
525
		/**
526
		 * Gets called after @see SpecialPage::execute.
527
		 *
528
		 * @since 1.20
529
		 *
530
		 * @param SpecialPage $this
531
		 * @param string|null $subPage
532
		 */
533
		Hooks::run( 'SpecialPageAfterExecute', [ $this, $subPage ] );
534
	}
535
536
	/**
537
	 * Gets called before @see SpecialPage::execute.
538
	 * Return false to prevent calling execute() (since 1.27+).
539
	 *
540
	 * @since 1.20
541
	 *
542
	 * @param string|null $subPage
543
	 * @return bool|void
544
	 */
545
	protected function beforeExecute( $subPage ) {
546
		// No-op
547
	}
548
549
	/**
550
	 * Gets called after @see SpecialPage::execute.
551
	 *
552
	 * @since 1.20
553
	 *
554
	 * @param string|null $subPage
555
	 */
556
	protected function afterExecute( $subPage ) {
557
		// No-op
558
	}
559
560
	/**
561
	 * Default execute method
562
	 * Checks user permissions
563
	 *
564
	 * This must be overridden by subclasses; it will be made abstract in a future version
565
	 *
566
	 * @param string|null $subPage
567
	 */
568
	public function execute( $subPage ) {
569
		$this->setHeaders();
570
		$this->checkPermissions();
571
		$this->checkLoginSecurityLevel( $this->getLoginSecurityLevel() );
0 ignored issues
show
Bug introduced by
It seems like $this->getLoginSecurityLevel() targeting SpecialPage::getLoginSecurityLevel() can also be of type boolean; however, SpecialPage::checkLoginSecurityLevel() does only seem to accept string|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...
572
		$this->outputHeader();
573
	}
574
575
	/**
576
	 * Outputs a summary message on top of special pages
577
	 * Per default the message key is the canonical name of the special page
578
	 * May be overridden, i.e. by extensions to stick with the naming conventions
579
	 * for message keys: 'extensionname-xxx'
580
	 *
581
	 * @param string $summaryMessageKey Message key of the summary
582
	 */
583
	function outputHeader( $summaryMessageKey = '' ) {
584
		global $wgContLang;
585
586
		if ( $summaryMessageKey == '' ) {
587
			$msg = $wgContLang->lc( $this->getName() ) . '-summary';
588
		} else {
589
			$msg = $summaryMessageKey;
590
		}
591
		if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) {
592
			$this->getOutput()->wrapWikiMsg(
593
				"<div class='mw-specialpage-summary'>\n$1\n</div>", $msg );
594
		}
595
	}
596
597
	/**
598
	 * Returns the name that goes in the \<h1\> in the special page itself, and
599
	 * also the name that will be listed in Special:Specialpages
600
	 *
601
	 * Derived classes can override this, but usually it is easier to keep the
602
	 * default behavior.
603
	 *
604
	 * @return string
605
	 */
606
	function getDescription() {
607
		return $this->msg( strtolower( $this->mName ) )->text();
608
	}
609
610
	/**
611
	 * Get a self-referential title object
612
	 *
613
	 * @param string|bool $subpage
614
	 * @return Title
615
	 * @deprecated since 1.23, use SpecialPage::getPageTitle
616
	 */
617
	function getTitle( $subpage = false ) {
618
		return $this->getPageTitle( $subpage );
619
	}
620
621
	/**
622
	 * Get a self-referential title object
623
	 *
624
	 * @param string|bool $subpage
625
	 * @return Title
626
	 * @since 1.23
627
	 */
628
	function getPageTitle( $subpage = false ) {
629
		return self::getTitleFor( $this->mName, $subpage );
630
	}
631
632
	/**
633
	 * Sets the context this SpecialPage is executed in
634
	 *
635
	 * @param IContextSource $context
636
	 * @since 1.18
637
	 */
638
	public function setContext( $context ) {
639
		$this->mContext = $context;
640
	}
641
642
	/**
643
	 * Gets the context this SpecialPage is executed in
644
	 *
645
	 * @return IContextSource|RequestContext
646
	 * @since 1.18
647
	 */
648 View Code Duplication
	public function getContext() {
649
		if ( $this->mContext instanceof IContextSource ) {
650
			return $this->mContext;
651
		} else {
652
			wfDebug( __METHOD__ . " called and \$mContext is null. " .
653
				"Return RequestContext::getMain(); for sanity\n" );
654
655
			return RequestContext::getMain();
656
		}
657
	}
658
659
	/**
660
	 * Get the WebRequest being used for this instance
661
	 *
662
	 * @return WebRequest
663
	 * @since 1.18
664
	 */
665
	public function getRequest() {
666
		return $this->getContext()->getRequest();
667
	}
668
669
	/**
670
	 * Get the OutputPage being used for this instance
671
	 *
672
	 * @return OutputPage
673
	 * @since 1.18
674
	 */
675
	public function getOutput() {
676
		return $this->getContext()->getOutput();
677
	}
678
679
	/**
680
	 * Shortcut to get the User executing this instance
681
	 *
682
	 * @return User
683
	 * @since 1.18
684
	 */
685
	public function getUser() {
686
		return $this->getContext()->getUser();
687
	}
688
689
	/**
690
	 * Shortcut to get the skin being used for this instance
691
	 *
692
	 * @return Skin
693
	 * @since 1.18
694
	 */
695
	public function getSkin() {
696
		return $this->getContext()->getSkin();
697
	}
698
699
	/**
700
	 * Shortcut to get user's language
701
	 *
702
	 * @return Language
703
	 * @since 1.19
704
	 */
705
	public function getLanguage() {
706
		return $this->getContext()->getLanguage();
707
	}
708
709
	/**
710
	 * Shortcut to get main config object
711
	 * @return Config
712
	 * @since 1.24
713
	 */
714
	public function getConfig() {
715
		return $this->getContext()->getConfig();
716
	}
717
718
	/**
719
	 * Return the full title, including $par
720
	 *
721
	 * @return Title
722
	 * @since 1.18
723
	 */
724
	public function getFullTitle() {
725
		return $this->getContext()->getTitle();
726
	}
727
728
	/**
729
	 * Return the robot policy. Derived classes that override this can change
730
	 * the robot policy set by setHeaders() from the default 'noindex,nofollow'.
731
	 *
732
	 * @return string
733
	 * @since 1.23
734
	 */
735
	protected function getRobotPolicy() {
736
		return 'noindex,nofollow';
737
	}
738
739
	/**
740
	 * Wrapper around wfMessage that sets the current context.
741
	 *
742
	 * @since 1.16
743
	 * @return Message
744
	 * @see wfMessage
745
	 */
746
	public function msg( /* $args */ ) {
747
		$message = call_user_func_array(
748
			[ $this->getContext(), 'msg' ],
749
			func_get_args()
750
		);
751
		// RequestContext passes context to wfMessage, and the language is set from
752
		// the context, but setting the language for Message class removes the
753
		// interface message status, which breaks for example usernameless gender
754
		// invocations. Restore the flag when not including special page in content.
755
		if ( $this->including() ) {
756
			$message->setInterfaceMessageFlag( false );
757
		}
758
759
		return $message;
760
	}
761
762
	/**
763
	 * Adds RSS/atom links
764
	 *
765
	 * @param array $params
766
	 */
767
	protected function addFeedLinks( $params ) {
768
		$feedTemplate = wfScript( 'api' );
769
770
		foreach ( $this->getConfig()->get( 'FeedClasses' ) as $format => $class ) {
771
			$theseParams = $params + [ 'feedformat' => $format ];
772
			$url = wfAppendQuery( $feedTemplate, $theseParams );
773
			$this->getOutput()->addFeedLink( $format, $url );
774
		}
775
	}
776
777
	/**
778
	 * Adds help link with an icon via page indicators.
779
	 * Link target can be overridden by a local message containing a wikilink:
780
	 * the message key is: lowercase special page name + '-helppage'.
781
	 * @param string $to Target MediaWiki.org page title or encoded URL.
782
	 * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
783
	 * @since 1.25
784
	 */
785 View Code Duplication
	public function addHelpLink( $to, $overrideBaseUrl = false ) {
786
		global $wgContLang;
787
		$msg = $this->msg( $wgContLang->lc( $this->getName() ) . '-helppage' );
788
789
		if ( !$msg->isDisabled() ) {
790
			$helpUrl = Skin::makeUrl( $msg->plain() );
791
			$this->getOutput()->addHelpLink( $helpUrl, true );
792
		} else {
793
			$this->getOutput()->addHelpLink( $to, $overrideBaseUrl );
794
		}
795
	}
796
797
	/**
798
	 * Get the group that the special page belongs in on Special:SpecialPage
799
	 * Use this method, instead of getGroupName to allow customization
800
	 * of the group name from the wiki side
801
	 *
802
	 * @return string Group of this special page
803
	 * @since 1.21
804
	 */
805
	public function getFinalGroupName() {
806
		$name = $this->getName();
807
808
		// Allow overbidding the group from the wiki side
809
		$msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage();
810
		if ( !$msg->isBlank() ) {
811
			$group = $msg->text();
812
		} else {
813
			// Than use the group from this object
814
			$group = $this->getGroupName();
815
		}
816
817
		return $group;
818
	}
819
820
	/**
821
	 * Indicates whether this special page may perform database writes
822
	 *
823
	 * @return bool
824
	 * @since 1.27
825
	 */
826
	public function doesWrites() {
827
		return false;
828
	}
829
830
	/**
831
	 * Under which header this special page is listed in Special:SpecialPages
832
	 * See messages 'specialpages-group-*' for valid names
833
	 * This method defaults to group 'other'
834
	 *
835
	 * @return string
836
	 * @since 1.21
837
	 */
838
	protected function getGroupName() {
839
		return 'other';
840
	}
841
842
	/**
843
	 * Call wfTransactionalTimeLimit() if this request was POSTed
844
	 * @since 1.26
845
	 */
846
	protected function useTransactionalTimeLimit() {
847
		if ( $this->getRequest()->wasPosted() ) {
848
			wfTransactionalTimeLimit();
849
		}
850
	}
851
852
	/**
853
	 * @since 1.28
854
	 * @return \MediaWiki\Linker\LinkRenderer
855
	 */
856
	protected function getLinkRenderer() {
857
		if ( $this->linkRenderer ) {
858
			return $this->linkRenderer;
859
		} else {
860
			return MediaWikiServices::getInstance()->getLinkRenderer();
861
		}
862
	}
863
864
	/**
865
	 * @since 1.28
866
	 * @param \MediaWiki\Linker\LinkRenderer $linkRenderer
867
	 */
868
	public function setLinkRenderer( LinkRenderer $linkRenderer ) {
869
		$this->linkRenderer = $linkRenderer;
870
	}
871
}
872