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() ); |
|
|
|
|
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
|
|
|
|
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.