Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
13.98% |
33 / 236 |
|
18.57% |
13 / 70 |
CRAP | |
0.00% |
0 / 1 |
SpecialPage | |
14.04% |
33 / 235 |
|
18.57% |
13 / 70 |
8662.07 | |
0.00% |
0 / 1 |
newSearchPage | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getTitleFor | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTitleValueFor | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getSafeTitleFor | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRestriction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isListed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isIncludable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
maxIncludeCacheTime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
getCacheTTL | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
including | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLocalName | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isExpensive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isCached | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isRestricted | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
userCanExecute | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
authorizeAction | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
displayRestrictionError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkPermissions | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
checkReadOnly | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
requireLogin | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
requireNamedUser | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getLoginSecurityLevel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setReauthPostData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkLoginSecurityLevel | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
72 | |||
setAuthManager | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAuthManager | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
prefixSearchSubpages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getSubpagesForPrefixSearch | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAssociatedNavigationLinks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
prefixSearchString | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
prefixSearchArray | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
setHeaders | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
run | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
beforeExecute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
afterExecute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
outputHeader | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getShortDescription | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getPageTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setContext | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getContext | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getRequest | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getOutput | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAuthority | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSkin | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getContentLanguage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setContentLanguage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFullTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRobotPolicy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
msg | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
addFeedLinks | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
addHelpLink | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getFinalGroupName | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
useTransactionalTimeLimit | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getLinkRenderer | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
setLinkRenderer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildPrevNextNavigation | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
setHookContainer | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getHookContainer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getHookRunner | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
setSpecialPageFactory | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSpecialPageFactory | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
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 | namespace MediaWiki\SpecialPage; |
25 | |
26 | use ErrorPageError; |
27 | use Language; |
28 | use MediaWiki\Auth\AuthManager; |
29 | use MediaWiki\Config\Config; |
30 | use MediaWiki\Context\IContextSource; |
31 | use MediaWiki\Context\RequestContext; |
32 | use MediaWiki\HookContainer\HookContainer; |
33 | use MediaWiki\HookContainer\HookRunner; |
34 | use MediaWiki\Language\RawMessage; |
35 | use MediaWiki\Linker\LinkRenderer; |
36 | use MediaWiki\MainConfigNames; |
37 | use MediaWiki\MediaWikiServices; |
38 | use MediaWiki\Message\Message; |
39 | use MediaWiki\Navigation\PagerNavigationBuilder; |
40 | use MediaWiki\Output\OutputPage; |
41 | use MediaWiki\Permissions\Authority; |
42 | use MediaWiki\Permissions\PermissionStatus; |
43 | use MediaWiki\Request\WebRequest; |
44 | use MediaWiki\Title\Title; |
45 | use MediaWiki\Title\TitleValue; |
46 | use MediaWiki\User\User; |
47 | use MessageLocalizer; |
48 | use MessageSpecifier; |
49 | use MWCryptRand; |
50 | use PermissionsError; |
51 | use ReadOnlyError; |
52 | use SearchEngineFactory; |
53 | use Skin; |
54 | use UserNotLoggedIn; |
55 | |
56 | /** |
57 | * Parent class for all special pages. |
58 | * |
59 | * Includes some static functions for handling the special page list deprecated |
60 | * in favor of SpecialPageFactory. |
61 | * |
62 | * @stable to extend |
63 | * |
64 | * @ingroup SpecialPage |
65 | */ |
66 | class SpecialPage implements MessageLocalizer { |
67 | /** |
68 | * @var string The canonical name of this special page |
69 | * Also used as the message key for the default <h1> heading, |
70 | * @see getDescription() |
71 | */ |
72 | protected $mName; |
73 | |
74 | /** @var string The local name of this special page */ |
75 | private $mLocalName; |
76 | |
77 | /** |
78 | * @var string Minimum user level required to access this page, or "" for anyone. |
79 | * Also used to categorise the pages in Special:Specialpages |
80 | */ |
81 | protected $mRestriction; |
82 | |
83 | /** @var bool Listed in Special:Specialpages? */ |
84 | private $mListed; |
85 | |
86 | /** @var bool Whether or not this special page is being included from an article */ |
87 | protected $mIncluding; |
88 | |
89 | /** @var bool Whether the special page can be included in an article */ |
90 | protected $mIncludable; |
91 | |
92 | /** |
93 | * Current request context |
94 | * @var IContextSource |
95 | */ |
96 | protected $mContext; |
97 | |
98 | /** @var Language|null */ |
99 | private $contentLanguage; |
100 | |
101 | /** |
102 | * @var LinkRenderer|null |
103 | */ |
104 | private $linkRenderer = null; |
105 | |
106 | /** @var HookContainer|null */ |
107 | private $hookContainer; |
108 | /** @var HookRunner|null */ |
109 | private $hookRunner; |
110 | |
111 | /** @var AuthManager|null */ |
112 | private $authManager = null; |
113 | |
114 | /** @var SpecialPageFactory */ |
115 | private $specialPageFactory; |
116 | |
117 | /** |
118 | * Get the users preferred search page. |
119 | * |
120 | * It will fall back to Special:Search if the preference points to a page |
121 | * that doesn't exist or is not defined. |
122 | * |
123 | * @since 1.38 |
124 | * @param User $user Search page can be customized by user preference. |
125 | * @return Title |
126 | */ |
127 | public static function newSearchPage( User $user ) { |
128 | // Try user preference first |
129 | $userOptionsManager = MediaWikiServices::getInstance()->getUserOptionsManager(); |
130 | $title = $userOptionsManager->getOption( $user, 'search-special-page' ); |
131 | if ( $title ) { |
132 | $page = self::getTitleFor( $title ); |
133 | $factory = MediaWikiServices::getInstance()->getSpecialPageFactory(); |
134 | if ( $factory->exists( $page->getText() ) ) { |
135 | return $page; |
136 | } |
137 | } |
138 | return self::getTitleFor( 'Search' ); |
139 | } |
140 | |
141 | /** |
142 | * Get a localised Title object for a specified special page name |
143 | * If you don't need a full Title object, consider using TitleValue through |
144 | * getTitleValueFor() below. |
145 | * |
146 | * @since 1.9 |
147 | * @since 1.21 $fragment parameter added |
148 | * |
149 | * @param string $name |
150 | * @param string|false|null $subpage Subpage string, or false/null to not use a subpage |
151 | * @param string $fragment The link fragment (after the "#") |
152 | * @return Title |
153 | */ |
154 | public static function getTitleFor( $name, $subpage = false, $fragment = '' ) { |
155 | return Title::newFromLinkTarget( |
156 | self::getTitleValueFor( $name, $subpage, $fragment ) |
157 | ); |
158 | } |
159 | |
160 | /** |
161 | * Get a localised TitleValue object for a specified special page name |
162 | * |
163 | * @since 1.28 |
164 | * @param string $name |
165 | * @param string|false|null $subpage Subpage string, or false/null to not use a subpage |
166 | * @param string $fragment The link fragment (after the "#") |
167 | * @return TitleValue |
168 | */ |
169 | public static function getTitleValueFor( $name, $subpage = false, $fragment = '' ) { |
170 | $name = MediaWikiServices::getInstance()->getSpecialPageFactory()-> |
171 | getLocalNameFor( $name, $subpage ); |
172 | |
173 | return new TitleValue( NS_SPECIAL, $name, $fragment ); |
174 | } |
175 | |
176 | /** |
177 | * Get a localised Title object for a page name with a possibly unvalidated subpage |
178 | * |
179 | * @param string $name |
180 | * @param string|false $subpage Subpage string, or false to not use a subpage |
181 | * @return Title|null Title object or null if the page doesn't exist |
182 | */ |
183 | public static function getSafeTitleFor( $name, $subpage = false ) { |
184 | $name = MediaWikiServices::getInstance()->getSpecialPageFactory()-> |
185 | getLocalNameFor( $name, $subpage ); |
186 | if ( $name ) { |
187 | return Title::makeTitleSafe( NS_SPECIAL, $name ); |
188 | } else { |
189 | return null; |
190 | } |
191 | } |
192 | |
193 | /** |
194 | * Default constructor for special pages |
195 | * Derivative classes should call this from their constructor |
196 | * Note that if the user does not have the required level, an error message will |
197 | * be displayed by the default execute() method, without the global function ever |
198 | * being called. |
199 | * |
200 | * If you override execute(), you can recover the default behavior with userCanExecute() |
201 | * and displayRestrictionError() |
202 | * |
203 | * @stable to call |
204 | * |
205 | * @param string $name Name of the special page, as seen in links and URLs |
206 | * @param string $restriction User right required, e.g. "block" or "delete" |
207 | * @param bool $listed Whether the page is listed in Special:Specialpages |
208 | * @param callable|bool $function Unused |
209 | * @param string $file Unused |
210 | * @param bool $includable Whether the page can be included in normal pages |
211 | */ |
212 | public function __construct( |
213 | $name = '', $restriction = '', $listed = true, |
214 | $function = false, $file = '', $includable = false |
215 | ) { |
216 | $this->mName = $name; |
217 | $this->mRestriction = $restriction; |
218 | $this->mListed = $listed; |
219 | $this->mIncludable = $includable; |
220 | } |
221 | |
222 | /** |
223 | * Get the canonical, unlocalized name of this special page without namespace. |
224 | * @return string |
225 | */ |
226 | public function getName() { |
227 | return $this->mName; |
228 | } |
229 | |
230 | /** |
231 | * Get the permission that a user must have to execute this page |
232 | * @return string |
233 | */ |
234 | public function getRestriction() { |
235 | return $this->mRestriction; |
236 | } |
237 | |
238 | // @todo FIXME: Decide which syntax to use for this, and stick to it |
239 | |
240 | /** |
241 | * Whether this special page is listed in Special:SpecialPages |
242 | * @stable to override |
243 | * @since 1.3 (r3583) |
244 | * @return bool |
245 | */ |
246 | public function isListed() { |
247 | return $this->mListed; |
248 | } |
249 | |
250 | /** |
251 | * Whether it's allowed to transclude the special page via {{Special:Foo/params}} |
252 | * @stable to override |
253 | * @return bool |
254 | */ |
255 | public function isIncludable() { |
256 | return $this->mIncludable; |
257 | } |
258 | |
259 | /** |
260 | * How long to cache page when it is being included. |
261 | * |
262 | * @note If cache time is not 0, then the current user becomes an anon |
263 | * if you want to do any per-user customizations, than this method |
264 | * must be overridden to return 0. |
265 | * @since 1.26 |
266 | * @stable to override |
267 | * @return int Time in seconds, 0 to disable caching altogether, |
268 | * false to use the parent page's cache settings |
269 | */ |
270 | public function maxIncludeCacheTime() { |
271 | return $this->getConfig()->get( MainConfigNames::MiserMode ) ? $this->getCacheTTL() : 0; |
272 | } |
273 | |
274 | /** |
275 | * @stable to override |
276 | * @return int Seconds that this page can be cached |
277 | */ |
278 | protected function getCacheTTL() { |
279 | return 60 * 60; |
280 | } |
281 | |
282 | /** |
283 | * Whether the special page is being evaluated via transclusion |
284 | * @param bool|null $x |
285 | * @return bool |
286 | */ |
287 | public function including( $x = null ) { |
288 | return wfSetVar( $this->mIncluding, $x ); |
289 | } |
290 | |
291 | /** |
292 | * Get the localised name of the special page |
293 | * @stable to override |
294 | * @return string |
295 | */ |
296 | public function getLocalName() { |
297 | if ( !isset( $this->mLocalName ) ) { |
298 | $this->mLocalName = $this->getSpecialPageFactory()->getLocalNameFor( $this->mName ); |
299 | } |
300 | |
301 | return $this->mLocalName; |
302 | } |
303 | |
304 | /** |
305 | * Is this page expensive (for some definition of expensive)? |
306 | * Expensive pages are disabled or cached in miser mode. Originally used |
307 | * (and still overridden) by QueryPage and subclasses, moved here so that |
308 | * Special:SpecialPages can safely call it for all special pages. |
309 | * |
310 | * @stable to override |
311 | * @return bool |
312 | */ |
313 | public function isExpensive() { |
314 | return false; |
315 | } |
316 | |
317 | /** |
318 | * Is this page cached? |
319 | * Expensive pages are cached or disabled in miser mode. |
320 | * Used by QueryPage and subclasses, moved here so that |
321 | * Special:SpecialPages can safely call it for all special pages. |
322 | * |
323 | * @stable to override |
324 | * @return bool |
325 | * @since 1.21 |
326 | */ |
327 | public function isCached() { |
328 | return false; |
329 | } |
330 | |
331 | /** |
332 | * Can be overridden by subclasses with more complicated permissions |
333 | * schemes. |
334 | * |
335 | * @stable to override |
336 | * @return bool Should the page be displayed with the restricted-access |
337 | * pages? |
338 | */ |
339 | public function isRestricted() { |
340 | // DWIM: If anons can do something, then it is not restricted |
341 | return $this->mRestriction != '' && !MediaWikiServices::getInstance() |
342 | ->getGroupPermissionsLookup() |
343 | ->groupHasPermission( '*', $this->mRestriction ); |
344 | } |
345 | |
346 | /** |
347 | * Checks if the given user (identified by an object) can execute this |
348 | * special page (as defined by $mRestriction). Can be overridden by sub- |
349 | * classes with more complicated permissions schemes. |
350 | * |
351 | * @stable to override |
352 | * @param User $user The user to check |
353 | * @return bool Does the user have permission to view the page? |
354 | */ |
355 | public function userCanExecute( User $user ) { |
356 | return MediaWikiServices::getInstance() |
357 | ->getPermissionManager() |
358 | ->userHasRight( $user, $this->mRestriction ); |
359 | } |
360 | |
361 | /** |
362 | * Utility function for authorizing an action to be performed by the special |
363 | * page. User blocks and rate limits are enforced implicitly. |
364 | * |
365 | * @see Authority::authorizeAction. |
366 | * |
367 | * @param ?string $action If not given, the action returned by |
368 | * getRestriction() will be used. |
369 | * |
370 | * @return PermissionStatus |
371 | */ |
372 | protected function authorizeAction( ?string $action = null ): PermissionStatus { |
373 | $action ??= $this->getRestriction(); |
374 | |
375 | if ( !$action ) { |
376 | return PermissionStatus::newGood(); |
377 | } |
378 | |
379 | $status = PermissionStatus::newEmpty(); |
380 | $this->getAuthority()->authorizeAction( $action, $status ); |
381 | return $status; |
382 | } |
383 | |
384 | /** |
385 | * Output an error message telling the user what access level they have to have |
386 | * @stable to override |
387 | * @throws PermissionsError |
388 | * @return never |
389 | */ |
390 | protected function displayRestrictionError() { |
391 | throw new PermissionsError( $this->mRestriction ); |
392 | } |
393 | |
394 | /** |
395 | * Checks if userCanExecute, and if not throws a PermissionsError |
396 | * |
397 | * @stable to override |
398 | * @since 1.19 |
399 | * @return void |
400 | * @throws PermissionsError |
401 | */ |
402 | public function checkPermissions() { |
403 | if ( !$this->userCanExecute( $this->getUser() ) ) { |
404 | $this->displayRestrictionError(); |
405 | } |
406 | } |
407 | |
408 | /** |
409 | * If the wiki is currently in readonly mode, throws a ReadOnlyError |
410 | * |
411 | * @since 1.19 |
412 | * @return void |
413 | * @throws ReadOnlyError |
414 | */ |
415 | public function checkReadOnly() { |
416 | // Can not inject the ReadOnlyMode as it would break the installer since |
417 | // it instantiates SpecialPageFactory before the DB (via ParserFactory for message parsing) |
418 | if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) { |
419 | throw new ReadOnlyError; |
420 | } |
421 | } |
422 | |
423 | /** |
424 | * If the user is not logged in, throws UserNotLoggedIn error |
425 | * |
426 | * The user will be redirected to Special:Userlogin with the given message as an error on |
427 | * the form. |
428 | * |
429 | * @since 1.23 |
430 | * @param string $reasonMsg [optional] Message key to be displayed on login page |
431 | * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor |
432 | * @throws UserNotLoggedIn |
433 | */ |
434 | public function requireLogin( |
435 | $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin' |
436 | ) { |
437 | if ( $this->getUser()->isAnon() ) { |
438 | throw new UserNotLoggedIn( $reasonMsg, $titleMsg ); |
439 | } |
440 | } |
441 | |
442 | /** |
443 | * If the user is not logged in or is a temporary user, throws UserNotLoggedIn |
444 | * |
445 | * @since 1.39 |
446 | * @param string $reasonMsg [optional] Message key to be displayed on login page |
447 | * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor |
448 | * @throws UserNotLoggedIn |
449 | */ |
450 | public function requireNamedUser( |
451 | $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin' |
452 | ) { |
453 | if ( !$this->getUser()->isNamed() ) { |
454 | throw new UserNotLoggedIn( $reasonMsg, $titleMsg ); |
455 | } |
456 | } |
457 | |
458 | /** |
459 | * Tells if the special page does something security-sensitive and needs extra defense against |
460 | * a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the |
461 | * authentication framework. |
462 | * @stable to override |
463 | * @return string|false False or the argument for AuthManager::securitySensitiveOperationStatus(). |
464 | * Typically a special page needing elevated security would return its name here. |
465 | */ |
466 | protected function getLoginSecurityLevel() { |
467 | return false; |
468 | } |
469 | |
470 | /** |
471 | * Record preserved POST data after a reauthentication. |
472 | * |
473 | * This is called from checkLoginSecurityLevel() when returning from the |
474 | * redirect for reauthentication, if the redirect had been served in |
475 | * response to a POST request. |
476 | * |
477 | * The base SpecialPage implementation does nothing. If your subclass uses |
478 | * getLoginSecurityLevel() or checkLoginSecurityLevel(), it should probably |
479 | * implement this to do something with the data. |
480 | * |
481 | * @note Call self::setAuthManager from special page constructor when overriding |
482 | * |
483 | * @stable to override |
484 | * @since 1.32 |
485 | * @param array $data |
486 | */ |
487 | protected function setReauthPostData( array $data ) { |
488 | } |
489 | |
490 | /** |
491 | * Verifies that the user meets the security level, possibly reauthenticating them in the process. |
492 | * |
493 | * This should be used when the page does something security-sensitive and needs extra defense |
494 | * against a stolen account (e.g. a reauthentication). The authentication framework will make |
495 | * an extra effort to make sure the user account is not compromised. What that exactly means |
496 | * will depend on the system and user settings; e.g. the user might be required to log in again |
497 | * unless their last login happened recently, or they might be given a second-factor challenge. |
498 | * |
499 | * Calling this method will result in one if these actions: |
500 | * - return true: all good. |
501 | * - return false and set a redirect: caller should abort; the redirect will take the user |
502 | * to the login page for reauthentication, and back. |
503 | * - throw an exception if there is no way for the user to meet the requirements without using |
504 | * a different access method (e.g. this functionality is only available from a specific IP). |
505 | * |
506 | * Note that this does not in any way check that the user is authorized to use this special page |
507 | * (use checkPermissions() for that). |
508 | * |
509 | * @param string|null $level A security level. Can be an arbitrary string, defaults to the page |
510 | * name. |
511 | * @return bool False means a redirect to the reauthentication page has been set and processing |
512 | * of the special page should be aborted. |
513 | * @throws ErrorPageError If the security level cannot be met, even with reauthentication. |
514 | */ |
515 | protected function checkLoginSecurityLevel( $level = null ) { |
516 | $level = $level ?: $this->getName(); |
517 | $key = 'SpecialPage:reauth:' . $this->getName(); |
518 | $request = $this->getRequest(); |
519 | |
520 | $securityStatus = $this->getAuthManager()->securitySensitiveOperationStatus( $level ); |
521 | if ( $securityStatus === AuthManager::SEC_OK ) { |
522 | $uniqueId = $request->getVal( 'postUniqueId' ); |
523 | if ( $uniqueId ) { |
524 | $key .= ':' . $uniqueId; |
525 | $session = $request->getSession(); |
526 | $data = $session->getSecret( $key ); |
527 | if ( $data ) { |
528 | $session->remove( $key ); |
529 | $this->setReauthPostData( $data ); |
530 | } |
531 | } |
532 | return true; |
533 | } elseif ( $securityStatus === AuthManager::SEC_REAUTH ) { |
534 | $title = self::getTitleFor( 'Userlogin' ); |
535 | $queryParams = $request->getQueryValues(); |
536 | |
537 | if ( $request->wasPosted() ) { |
538 | $data = array_diff_assoc( $request->getValues(), $request->getQueryValues() ); |
539 | if ( $data ) { |
540 | // unique ID in case the same special page is open in multiple browser tabs |
541 | $uniqueId = MWCryptRand::generateHex( 6 ); |
542 | $key .= ':' . $uniqueId; |
543 | $queryParams['postUniqueId'] = $uniqueId; |
544 | $session = $request->getSession(); |
545 | $session->persist(); // Just in case |
546 | $session->setSecret( $key, $data ); |
547 | } |
548 | } |
549 | |
550 | $query = [ |
551 | 'returnto' => $this->getFullTitle()->getPrefixedDBkey(), |
552 | 'returntoquery' => wfArrayToCgi( array_diff_key( $queryParams, [ 'title' => true ] ) ), |
553 | 'force' => $level, |
554 | ]; |
555 | $url = $title->getFullURL( $query, false, PROTO_HTTPS ); |
556 | |
557 | $this->getOutput()->redirect( $url ); |
558 | return false; |
559 | } |
560 | |
561 | $titleMessage = wfMessage( 'specialpage-securitylevel-not-allowed-title' ); |
562 | $errorMessage = wfMessage( 'specialpage-securitylevel-not-allowed' ); |
563 | throw new ErrorPageError( $titleMessage, $errorMessage ); |
564 | } |
565 | |
566 | /** |
567 | * Set the injected AuthManager from the special page constructor |
568 | * |
569 | * @since 1.36 |
570 | * @param AuthManager $authManager |
571 | */ |
572 | final protected function setAuthManager( AuthManager $authManager ) { |
573 | $this->authManager = $authManager; |
574 | } |
575 | |
576 | /** |
577 | * @note Call self::setAuthManager from special page constructor when using |
578 | * |
579 | * @since 1.36 |
580 | * @return AuthManager |
581 | */ |
582 | final protected function getAuthManager(): AuthManager { |
583 | if ( $this->authManager === null ) { |
584 | // Fallback if not provided |
585 | // TODO Change to wfWarn in a future release |
586 | $this->authManager = MediaWikiServices::getInstance()->getAuthManager(); |
587 | } |
588 | return $this->authManager; |
589 | } |
590 | |
591 | /** |
592 | * Return an array of subpages beginning with $search that this special page will accept. |
593 | * |
594 | * For example, if a page supports subpages "foo", "bar" and "baz" (as in Special:PageName/foo, |
595 | * etc.): |
596 | * |
597 | * - `prefixSearchSubpages( "ba" )` should return `[ "bar", "baz" ]` |
598 | * - `prefixSearchSubpages( "f" )` should return `[ "foo" ]` |
599 | * - `prefixSearchSubpages( "z" )` should return `[]` |
600 | * - `prefixSearchSubpages( "" )` should return `[ foo", "bar", "baz" ]` |
601 | * |
602 | * @stable to override |
603 | * @param string $search Prefix to search for |
604 | * @param int $limit Maximum number of results to return (usually 10) |
605 | * @param int $offset Number of results to skip (usually 0) |
606 | * @return string[] Matching subpages |
607 | */ |
608 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
609 | $subpages = $this->getSubpagesForPrefixSearch(); |
610 | if ( !$subpages ) { |
611 | return []; |
612 | } |
613 | |
614 | return self::prefixSearchArray( $search, $limit, $subpages, $offset ); |
615 | } |
616 | |
617 | /** |
618 | * Return an array of subpages that this special page will accept for prefix |
619 | * searches. If this method requires a query you might instead want to implement |
620 | * prefixSearchSubpages() directly so you can support $limit and $offset. This |
621 | * method is better for static-ish lists of things. |
622 | * |
623 | * @stable to override |
624 | * @return string[] subpages to search from |
625 | */ |
626 | protected function getSubpagesForPrefixSearch() { |
627 | return []; |
628 | } |
629 | |
630 | /** |
631 | * Return an array of strings representing page titles that are discoverable to end users via UI. |
632 | * |
633 | * @since 1.39 |
634 | * @stable to call or override |
635 | * @return string[] strings representing page titles that can be rendered by skins if required. |
636 | */ |
637 | public function getAssociatedNavigationLinks() { |
638 | return []; |
639 | } |
640 | |
641 | /** |
642 | * Perform a regular substring search for prefixSearchSubpages |
643 | * @since 1.36 Added $searchEngineFactory parameter |
644 | * @param string $search Prefix to search for |
645 | * @param int $limit Maximum number of results to return (usually 10) |
646 | * @param int $offset Number of results to skip (usually 0) |
647 | * @param SearchEngineFactory|null $searchEngineFactory Provide the service |
648 | * @return string[] Matching subpages |
649 | */ |
650 | protected function prefixSearchString( $search, $limit, $offset, SearchEngineFactory $searchEngineFactory = null ) { |
651 | $title = Title::newFromText( $search ); |
652 | if ( !$title || !$title->canExist() ) { |
653 | // No prefix suggestion in special and media namespace |
654 | return []; |
655 | } |
656 | |
657 | $searchEngine = $searchEngineFactory |
658 | ? $searchEngineFactory->create() |
659 | // Fallback if not provided |
660 | // TODO Change to wfWarn in a future release |
661 | : MediaWikiServices::getInstance()->newSearchEngine(); |
662 | $searchEngine->setLimitOffset( $limit, $offset ); |
663 | $searchEngine->setNamespaces( [] ); |
664 | $result = $searchEngine->defaultPrefixSearch( $search ); |
665 | return array_map( static function ( Title $t ) { |
666 | return $t->getPrefixedText(); |
667 | }, $result ); |
668 | } |
669 | |
670 | /** |
671 | * Helper function for implementations of prefixSearchSubpages() that |
672 | * filter the values in memory (as opposed to making a query). |
673 | * |
674 | * @since 1.24 |
675 | * @param string $search |
676 | * @param int $limit |
677 | * @param array $subpages |
678 | * @param int $offset |
679 | * @return string[] |
680 | */ |
681 | protected static function prefixSearchArray( $search, $limit, array $subpages, $offset ) { |
682 | $escaped = preg_quote( $search, '/' ); |
683 | return array_slice( preg_grep( "/^$escaped/i", |
684 | array_slice( $subpages, $offset ) ), 0, $limit ); |
685 | } |
686 | |
687 | /** |
688 | * Sets headers - this should be called from the execute() method of all derived classes! |
689 | * @stable to override |
690 | */ |
691 | protected function setHeaders() { |
692 | $out = $this->getOutput(); |
693 | $out->setArticleRelated( false ); |
694 | $out->setRobotPolicy( $this->getRobotPolicy() ); |
695 | $title = $this->getDescription(); |
696 | if ( is_string( $title ) ) { // T343849 |
697 | wfDeprecated( 'string return from SpecialPage::getDescription()', '1.41' ); |
698 | $title = ( new RawMessage( '$1' ) )->rawParams( $title ); |
699 | } |
700 | $out->setPageTitleMsg( $title ); |
701 | } |
702 | |
703 | /** |
704 | * Entry point. |
705 | * |
706 | * @since 1.20 |
707 | * |
708 | * @param string|null $subPage |
709 | */ |
710 | final public function run( $subPage ) { |
711 | if ( !$this->getHookRunner()->onSpecialPageBeforeExecute( $this, $subPage ) ) { |
712 | return; |
713 | } |
714 | |
715 | if ( $this->beforeExecute( $subPage ) === false ) { |
716 | return; |
717 | } |
718 | $this->execute( $subPage ); |
719 | $this->afterExecute( $subPage ); |
720 | |
721 | $this->getHookRunner()->onSpecialPageAfterExecute( $this, $subPage ); |
722 | } |
723 | |
724 | /** |
725 | * Gets called before @see SpecialPage::execute. |
726 | * Return false to prevent calling execute() (since 1.27+). |
727 | * |
728 | * @stable to override |
729 | * @since 1.20 |
730 | * |
731 | * @param string|null $subPage |
732 | * @return bool|void |
733 | */ |
734 | protected function beforeExecute( $subPage ) { |
735 | // No-op |
736 | } |
737 | |
738 | /** |
739 | * Gets called after @see SpecialPage::execute. |
740 | * |
741 | * @stable to override |
742 | * @since 1.20 |
743 | * |
744 | * @param string|null $subPage |
745 | */ |
746 | protected function afterExecute( $subPage ) { |
747 | // No-op |
748 | } |
749 | |
750 | /** |
751 | * Default execute method |
752 | * Checks user permissions |
753 | * |
754 | * This must be overridden by subclasses; it will be made abstract in a future version |
755 | * |
756 | * @stable to override |
757 | * |
758 | * @param string|null $subPage |
759 | */ |
760 | public function execute( $subPage ) { |
761 | $this->setHeaders(); |
762 | $this->checkPermissions(); |
763 | $securityLevel = $this->getLoginSecurityLevel(); |
764 | if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) { |
765 | return; |
766 | } |
767 | $this->outputHeader(); |
768 | } |
769 | |
770 | /** |
771 | * Outputs a summary message on top of special pages |
772 | * Per default the message key is the canonical name of the special page |
773 | * May be overridden, i.e. by extensions to stick with the naming conventions |
774 | * for message keys: 'extensionname-xxx' |
775 | * |
776 | * @stable to override |
777 | * |
778 | * @param string $summaryMessageKey Message key of the summary |
779 | */ |
780 | protected function outputHeader( $summaryMessageKey = '' ) { |
781 | if ( $summaryMessageKey == '' ) { |
782 | $msg = strtolower( $this->getName() ) . '-summary'; |
783 | } else { |
784 | $msg = $summaryMessageKey; |
785 | } |
786 | if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) { |
787 | $this->getOutput()->wrapWikiMsg( |
788 | "<div class='mw-specialpage-summary'>\n$1\n</div>", $msg ); |
789 | } |
790 | } |
791 | |
792 | /** |
793 | * Returns the name that goes in the \<h1\> in the special page itself, and |
794 | * also the name that will be listed in Special:Specialpages |
795 | * |
796 | * Derived classes can override this, but usually it is easier to keep the |
797 | * default behavior. |
798 | * |
799 | * Returning a string from this method has been deprecated since 1.41. |
800 | * |
801 | * @stable to override |
802 | * |
803 | * @return string|Message |
804 | */ |
805 | public function getDescription() { |
806 | return $this->msg( strtolower( $this->mName ) ); |
807 | } |
808 | |
809 | /** |
810 | * Similar to getDescription but takes into account sub pages and designed for display |
811 | * in tabs. |
812 | * |
813 | * @since 1.39 |
814 | * @stable to override if special page has complex parameter handling. Use default message keys |
815 | * where possible. |
816 | * |
817 | * @param string $path (optional) |
818 | * @return string |
819 | */ |
820 | public function getShortDescription( string $path = '' ): string { |
821 | $lowerPath = strtolower( str_replace( '/', '-', $path ) ); |
822 | $shortKey = 'special-tab-' . $lowerPath; |
823 | $shortKey .= '-short'; |
824 | $msgShort = $this->msg( $shortKey ); |
825 | return $msgShort->text(); |
826 | } |
827 | |
828 | /** |
829 | * Get a self-referential title object |
830 | * |
831 | * @param string|false|null $subpage |
832 | * @return Title |
833 | * @since 1.23 |
834 | */ |
835 | public function getPageTitle( $subpage = false ) { |
836 | return self::getTitleFor( $this->mName, $subpage ); |
837 | } |
838 | |
839 | /** |
840 | * Sets the context this SpecialPage is executed in |
841 | * |
842 | * @param IContextSource $context |
843 | * @since 1.18 |
844 | */ |
845 | public function setContext( $context ) { |
846 | $this->mContext = $context; |
847 | } |
848 | |
849 | /** |
850 | * Gets the context this SpecialPage is executed in |
851 | * |
852 | * @return IContextSource|RequestContext |
853 | * @since 1.18 |
854 | */ |
855 | public function getContext() { |
856 | if ( !( $this->mContext instanceof IContextSource ) ) { |
857 | wfDebug( __METHOD__ . " called and \$mContext is null. " . |
858 | "Using RequestContext::getMain()" ); |
859 | |
860 | $this->mContext = RequestContext::getMain(); |
861 | } |
862 | return $this->mContext; |
863 | } |
864 | |
865 | /** |
866 | * Get the WebRequest being used for this instance |
867 | * |
868 | * @return WebRequest |
869 | * @since 1.18 |
870 | */ |
871 | public function getRequest() { |
872 | return $this->getContext()->getRequest(); |
873 | } |
874 | |
875 | /** |
876 | * Get the OutputPage being used for this instance |
877 | * |
878 | * @return OutputPage |
879 | * @since 1.18 |
880 | */ |
881 | public function getOutput() { |
882 | return $this->getContext()->getOutput(); |
883 | } |
884 | |
885 | /** |
886 | * Shortcut to get the User executing this instance |
887 | * |
888 | * @return User |
889 | * @since 1.18 |
890 | */ |
891 | public function getUser() { |
892 | return $this->getContext()->getUser(); |
893 | } |
894 | |
895 | /** |
896 | * Shortcut to get the Authority executing this instance |
897 | * |
898 | * @return Authority |
899 | * @since 1.36 |
900 | */ |
901 | public function getAuthority(): Authority { |
902 | return $this->getContext()->getAuthority(); |
903 | } |
904 | |
905 | /** |
906 | * Shortcut to get the skin being used for this instance |
907 | * |
908 | * @return Skin |
909 | * @since 1.18 |
910 | */ |
911 | public function getSkin() { |
912 | return $this->getContext()->getSkin(); |
913 | } |
914 | |
915 | /** |
916 | * Shortcut to get user's language |
917 | * |
918 | * @return Language |
919 | * @since 1.19 |
920 | */ |
921 | public function getLanguage() { |
922 | return $this->getContext()->getLanguage(); |
923 | } |
924 | |
925 | /** |
926 | * Shortcut to get content language |
927 | * |
928 | * @return Language |
929 | * @since 1.36 |
930 | */ |
931 | final public function getContentLanguage(): Language { |
932 | if ( $this->contentLanguage === null ) { |
933 | // Fallback if not provided |
934 | // TODO Change to wfWarn in a future release |
935 | $this->contentLanguage = MediaWikiServices::getInstance()->getContentLanguage(); |
936 | } |
937 | return $this->contentLanguage; |
938 | } |
939 | |
940 | /** |
941 | * Set content language |
942 | * |
943 | * @internal For factory only |
944 | * @param Language $contentLanguage |
945 | * @since 1.36 |
946 | */ |
947 | final public function setContentLanguage( Language $contentLanguage ) { |
948 | $this->contentLanguage = $contentLanguage; |
949 | } |
950 | |
951 | /** |
952 | * Shortcut to get main config object |
953 | * @return Config |
954 | * @since 1.24 |
955 | */ |
956 | public function getConfig() { |
957 | return $this->getContext()->getConfig(); |
958 | } |
959 | |
960 | /** |
961 | * Return the full title, including $par |
962 | * |
963 | * @return Title |
964 | * @since 1.18 |
965 | */ |
966 | public function getFullTitle() { |
967 | return $this->getContext()->getTitle(); |
968 | } |
969 | |
970 | /** |
971 | * Return the robot policy. Derived classes that override this can change |
972 | * the robot policy set by setHeaders() from the default 'noindex,nofollow'. |
973 | * |
974 | * @return string |
975 | * @since 1.23 |
976 | */ |
977 | protected function getRobotPolicy() { |
978 | return 'noindex,nofollow'; |
979 | } |
980 | |
981 | /** |
982 | * Wrapper around wfMessage that sets the current context. |
983 | * |
984 | * @since 1.16 |
985 | * @param string|string[]|MessageSpecifier $key |
986 | * @param mixed ...$params |
987 | * @return Message |
988 | * @see wfMessage |
989 | */ |
990 | public function msg( $key, ...$params ) { |
991 | $message = $this->getContext()->msg( $key, ...$params ); |
992 | // RequestContext passes context to wfMessage, and the language is set from |
993 | // the context, but setting the language for Message class removes the |
994 | // interface message status, which breaks for example usernameless gender |
995 | // invocations. Restore the flag when not including special page in content. |
996 | if ( $this->including() ) { |
997 | $message->setInterfaceMessageFlag( false ); |
998 | } |
999 | |
1000 | return $message; |
1001 | } |
1002 | |
1003 | /** |
1004 | * Adds RSS/atom links |
1005 | * |
1006 | * @param array $params |
1007 | */ |
1008 | protected function addFeedLinks( $params ) { |
1009 | $feedTemplate = wfScript( 'api' ); |
1010 | |
1011 | foreach ( $this->getConfig()->get( MainConfigNames::FeedClasses ) as $format => $class ) { |
1012 | $theseParams = $params + [ 'feedformat' => $format ]; |
1013 | $url = wfAppendQuery( $feedTemplate, $theseParams ); |
1014 | $this->getOutput()->addFeedLink( $format, $url ); |
1015 | } |
1016 | } |
1017 | |
1018 | /** |
1019 | * Adds help link with an icon via page indicators. |
1020 | * Link target can be overridden by a local message containing a wikilink: |
1021 | * the message key is: lowercase special page name + '-helppage'. |
1022 | * @param string $to Target MediaWiki.org page title or encoded URL. |
1023 | * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o. |
1024 | * @since 1.25 |
1025 | */ |
1026 | public function addHelpLink( $to, $overrideBaseUrl = false ) { |
1027 | if ( $this->including() ) { |
1028 | return; |
1029 | } |
1030 | |
1031 | $msg = $this->msg( strtolower( $this->getName() ) . '-helppage' ); |
1032 | |
1033 | if ( !$msg->isDisabled() ) { |
1034 | $title = Title::newFromText( $msg->plain() ); |
1035 | if ( $title instanceof Title ) { |
1036 | $this->getOutput()->addHelpLink( $title->getLocalURL(), true ); |
1037 | } |
1038 | } else { |
1039 | $this->getOutput()->addHelpLink( $to, $overrideBaseUrl ); |
1040 | } |
1041 | } |
1042 | |
1043 | /** |
1044 | * Get the group that the special page belongs in on Special:SpecialPage |
1045 | * Use this method, instead of getGroupName to allow customization |
1046 | * of the group name from the wiki side |
1047 | * |
1048 | * @return string Group of this special page |
1049 | * @since 1.21 |
1050 | */ |
1051 | public function getFinalGroupName() { |
1052 | $name = $this->getName(); |
1053 | |
1054 | // Allow overriding the group from the wiki side |
1055 | $msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage(); |
1056 | if ( !$msg->isBlank() ) { |
1057 | $group = $msg->text(); |
1058 | } else { |
1059 | // Than use the group from this object |
1060 | $group = $this->getGroupName(); |
1061 | } |
1062 | |
1063 | return $group; |
1064 | } |
1065 | |
1066 | /** |
1067 | * Indicates whether this special page may perform database writes |
1068 | * |
1069 | * @stable to override |
1070 | * |
1071 | * @return bool |
1072 | * @since 1.27 |
1073 | */ |
1074 | public function doesWrites() { |
1075 | return false; |
1076 | } |
1077 | |
1078 | /** |
1079 | * Under which header this special page is listed in Special:SpecialPages |
1080 | * See messages 'specialpages-group-*' for valid names |
1081 | * This method defaults to group 'other' |
1082 | * |
1083 | * @stable to override |
1084 | * |
1085 | * @return string |
1086 | * @since 1.21 |
1087 | */ |
1088 | protected function getGroupName() { |
1089 | return 'other'; |
1090 | } |
1091 | |
1092 | /** |
1093 | * Call wfTransactionalTimeLimit() if this request was POSTed |
1094 | * @since 1.26 |
1095 | */ |
1096 | protected function useTransactionalTimeLimit() { |
1097 | if ( $this->getRequest()->wasPosted() ) { |
1098 | wfTransactionalTimeLimit(); |
1099 | } |
1100 | } |
1101 | |
1102 | /** |
1103 | * @since 1.28 |
1104 | * @return LinkRenderer |
1105 | */ |
1106 | public function getLinkRenderer(): LinkRenderer { |
1107 | if ( $this->linkRenderer === null ) { |
1108 | // TODO Inject the service |
1109 | $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRendererFactory() |
1110 | ->create(); |
1111 | } |
1112 | return $this->linkRenderer; |
1113 | } |
1114 | |
1115 | /** |
1116 | * @since 1.28 |
1117 | * @param LinkRenderer $linkRenderer |
1118 | */ |
1119 | public function setLinkRenderer( LinkRenderer $linkRenderer ) { |
1120 | $this->linkRenderer = $linkRenderer; |
1121 | } |
1122 | |
1123 | /** |
1124 | * Generate (prev x| next x) (20|50|100...) type links for paging |
1125 | * |
1126 | * @param int $offset |
1127 | * @param int $limit |
1128 | * @param array $query Optional URL query parameter string |
1129 | * @param bool $atend Optional param for specified if this is the last page |
1130 | * @param string|false $subpage Optional param for specifying subpage |
1131 | * @return string |
1132 | */ |
1133 | protected function buildPrevNextNavigation( |
1134 | $offset, |
1135 | $limit, |
1136 | array $query = [], |
1137 | $atend = false, |
1138 | $subpage = false |
1139 | ) { |
1140 | $navBuilder = new PagerNavigationBuilder( $this ); |
1141 | $navBuilder |
1142 | ->setPage( $this->getPageTitle( $subpage ) ) |
1143 | ->setLinkQuery( [ 'limit' => $limit, 'offset' => $offset ] + $query ) |
1144 | ->setLimitLinkQueryParam( 'limit' ) |
1145 | ->setCurrentLimit( $limit ) |
1146 | ->setPrevTooltipMsg( 'prevn-title' ) |
1147 | ->setNextTooltipMsg( 'nextn-title' ) |
1148 | ->setLimitTooltipMsg( 'shown-title' ); |
1149 | |
1150 | if ( $offset > 0 ) { |
1151 | $navBuilder->setPrevLinkQuery( [ 'offset' => (string)max( $offset - $limit, 0 ) ] ); |
1152 | } |
1153 | if ( !$atend ) { |
1154 | $navBuilder->setNextLinkQuery( [ 'offset' => (string)( $offset + $limit ) ] ); |
1155 | } |
1156 | |
1157 | return $navBuilder->getHtml(); |
1158 | } |
1159 | |
1160 | /** |
1161 | * @since 1.35 |
1162 | * @internal |
1163 | * @param HookContainer $hookContainer |
1164 | */ |
1165 | public function setHookContainer( HookContainer $hookContainer ) { |
1166 | $this->hookContainer = $hookContainer; |
1167 | $this->hookRunner = new HookRunner( $hookContainer ); |
1168 | } |
1169 | |
1170 | /** |
1171 | * @since 1.35 |
1172 | * @return HookContainer |
1173 | */ |
1174 | protected function getHookContainer() { |
1175 | if ( !$this->hookContainer ) { |
1176 | $this->hookContainer = MediaWikiServices::getInstance()->getHookContainer(); |
1177 | } |
1178 | return $this->hookContainer; |
1179 | } |
1180 | |
1181 | /** |
1182 | * @internal This is for use by core only. Hook interfaces may be removed |
1183 | * without notice. |
1184 | * @since 1.35 |
1185 | * @return HookRunner |
1186 | */ |
1187 | protected function getHookRunner() { |
1188 | if ( !$this->hookRunner ) { |
1189 | $this->hookRunner = new HookRunner( $this->getHookContainer() ); |
1190 | } |
1191 | return $this->hookRunner; |
1192 | } |
1193 | |
1194 | /** |
1195 | * @internal For factory only |
1196 | * @since 1.36 |
1197 | * @param SpecialPageFactory $specialPageFactory |
1198 | */ |
1199 | final public function setSpecialPageFactory( SpecialPageFactory $specialPageFactory ) { |
1200 | $this->specialPageFactory = $specialPageFactory; |
1201 | } |
1202 | |
1203 | /** |
1204 | * @since 1.36 |
1205 | * @return SpecialPageFactory |
1206 | */ |
1207 | final protected function getSpecialPageFactory(): SpecialPageFactory { |
1208 | if ( !$this->specialPageFactory ) { |
1209 | // Fallback if not provided |
1210 | // TODO Change to wfWarn in a future release |
1211 | $this->specialPageFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); |
1212 | } |
1213 | return $this->specialPageFactory; |
1214 | } |
1215 | } |
1216 | |
1217 | /** @deprecated class alias since 1.41 */ |
1218 | class_alias( SpecialPage::class, 'SpecialPage' ); |