Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
48.56% |
304 / 626 |
|
27.27% |
3 / 11 |
CRAP | |
0.00% |
0 / 1 |
SpecialMovePage | |
48.64% |
304 / 625 |
|
27.27% |
3 / 11 |
2419.61 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
75.51% |
37 / 49 |
|
0.00% |
0 / 1 |
14.12 | |||
showForm | |
62.76% |
209 / 333 |
|
0.00% |
0 / 1 |
155.26 | |||
doSubmit | |
0.00% |
0 / 176 |
|
0.00% |
0 / 1 |
2550 | |||
showLogFragment | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
showSubpages | |
81.82% |
18 / 22 |
|
0.00% |
0 / 1 |
9.49 | |||
showSubpagesList | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
5.03 | |||
truncateSubpagesList | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
prefixSearchSubpages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @files |
19 | */ |
20 | |
21 | namespace MediaWiki\Specials; |
22 | |
23 | use DoubleRedirectJob; |
24 | use ErrorPageError; |
25 | use IDBAccessObject; |
26 | use LogEventsList; |
27 | use LogPage; |
28 | use MediaWiki\Cache\LinkBatchFactory; |
29 | use MediaWiki\CommentStore\CommentStore; |
30 | use MediaWiki\Content\IContentHandlerFactory; |
31 | use MediaWiki\Deferred\DeferredUpdates; |
32 | use MediaWiki\Html\Html; |
33 | use MediaWiki\MainConfigNames; |
34 | use MediaWiki\Page\MovePageFactory; |
35 | use MediaWiki\Page\WikiPageFactory; |
36 | use MediaWiki\Permissions\PermissionManager; |
37 | use MediaWiki\Permissions\RestrictionStore; |
38 | use MediaWiki\SpecialPage\UnlistedSpecialPage; |
39 | use MediaWiki\Title\NamespaceInfo; |
40 | use MediaWiki\Title\Title; |
41 | use MediaWiki\Title\TitleArrayFromResult; |
42 | use MediaWiki\Title\TitleFactory; |
43 | use MediaWiki\User\Options\UserOptionsLookup; |
44 | use MediaWiki\Watchlist\WatchlistManager; |
45 | use MediaWiki\Widget\ComplexTitleInputWidget; |
46 | use OOUI\ButtonInputWidget; |
47 | use OOUI\CheckboxInputWidget; |
48 | use OOUI\DropdownInputWidget; |
49 | use OOUI\FieldLayout; |
50 | use OOUI\FieldsetLayout; |
51 | use OOUI\FormLayout; |
52 | use OOUI\HtmlSnippet; |
53 | use OOUI\PanelLayout; |
54 | use OOUI\TextInputWidget; |
55 | use PermissionsError; |
56 | use RepoGroup; |
57 | use SearchEngineFactory; |
58 | use StringUtils; |
59 | use ThrottledError; |
60 | use Wikimedia\Rdbms\IConnectionProvider; |
61 | use Wikimedia\Rdbms\IExpression; |
62 | use Wikimedia\Rdbms\LikeValue; |
63 | use Xml; |
64 | |
65 | /** |
66 | * Implement Special:Movepage for changing page titles |
67 | * |
68 | * @ingroup SpecialPage |
69 | */ |
70 | class SpecialMovePage extends UnlistedSpecialPage { |
71 | /** @var Title */ |
72 | protected $oldTitle = null; |
73 | |
74 | /** @var Title */ |
75 | protected $newTitle; |
76 | |
77 | /** @var string Text input */ |
78 | protected $reason; |
79 | |
80 | /** @var bool */ |
81 | protected $moveTalk; |
82 | |
83 | /** @var bool */ |
84 | protected $deleteAndMove; |
85 | |
86 | /** @var bool */ |
87 | protected $moveSubpages; |
88 | |
89 | /** @var bool */ |
90 | protected $fixRedirects; |
91 | |
92 | /** @var bool */ |
93 | protected $leaveRedirect; |
94 | |
95 | /** @var bool */ |
96 | protected $moveOverShared; |
97 | |
98 | private $watch = false; |
99 | |
100 | private MovePageFactory $movePageFactory; |
101 | private PermissionManager $permManager; |
102 | private UserOptionsLookup $userOptionsLookup; |
103 | private IConnectionProvider $dbProvider; |
104 | private IContentHandlerFactory $contentHandlerFactory; |
105 | private NamespaceInfo $nsInfo; |
106 | private LinkBatchFactory $linkBatchFactory; |
107 | private RepoGroup $repoGroup; |
108 | private WikiPageFactory $wikiPageFactory; |
109 | private SearchEngineFactory $searchEngineFactory; |
110 | private WatchlistManager $watchlistManager; |
111 | private RestrictionStore $restrictionStore; |
112 | private TitleFactory $titleFactory; |
113 | |
114 | /** |
115 | * @param MovePageFactory $movePageFactory |
116 | * @param PermissionManager $permManager |
117 | * @param UserOptionsLookup $userOptionsLookup |
118 | * @param IConnectionProvider $dbProvider |
119 | * @param IContentHandlerFactory $contentHandlerFactory |
120 | * @param NamespaceInfo $nsInfo |
121 | * @param LinkBatchFactory $linkBatchFactory |
122 | * @param RepoGroup $repoGroup |
123 | * @param WikiPageFactory $wikiPageFactory |
124 | * @param SearchEngineFactory $searchEngineFactory |
125 | * @param WatchlistManager $watchlistManager |
126 | * @param RestrictionStore $restrictionStore |
127 | * @param TitleFactory $titleFactory |
128 | */ |
129 | public function __construct( |
130 | MovePageFactory $movePageFactory, |
131 | PermissionManager $permManager, |
132 | UserOptionsLookup $userOptionsLookup, |
133 | IConnectionProvider $dbProvider, |
134 | IContentHandlerFactory $contentHandlerFactory, |
135 | NamespaceInfo $nsInfo, |
136 | LinkBatchFactory $linkBatchFactory, |
137 | RepoGroup $repoGroup, |
138 | WikiPageFactory $wikiPageFactory, |
139 | SearchEngineFactory $searchEngineFactory, |
140 | WatchlistManager $watchlistManager, |
141 | RestrictionStore $restrictionStore, |
142 | TitleFactory $titleFactory |
143 | ) { |
144 | parent::__construct( 'Movepage' ); |
145 | $this->movePageFactory = $movePageFactory; |
146 | $this->permManager = $permManager; |
147 | $this->userOptionsLookup = $userOptionsLookup; |
148 | $this->dbProvider = $dbProvider; |
149 | $this->contentHandlerFactory = $contentHandlerFactory; |
150 | $this->nsInfo = $nsInfo; |
151 | $this->linkBatchFactory = $linkBatchFactory; |
152 | $this->repoGroup = $repoGroup; |
153 | $this->wikiPageFactory = $wikiPageFactory; |
154 | $this->searchEngineFactory = $searchEngineFactory; |
155 | $this->watchlistManager = $watchlistManager; |
156 | $this->restrictionStore = $restrictionStore; |
157 | $this->titleFactory = $titleFactory; |
158 | } |
159 | |
160 | public function doesWrites() { |
161 | return true; |
162 | } |
163 | |
164 | public function execute( $par ) { |
165 | $this->useTransactionalTimeLimit(); |
166 | $this->checkReadOnly(); |
167 | $this->setHeaders(); |
168 | $this->outputHeader(); |
169 | |
170 | $request = $this->getRequest(); |
171 | |
172 | // Beware: The use of WebRequest::getText() is wanted! See T22365 |
173 | $target = $par ?? $request->getText( 'target' ); |
174 | $oldTitleText = $request->getText( 'wpOldTitle', $target ); |
175 | $this->oldTitle = Title::newFromText( $oldTitleText ); |
176 | |
177 | if ( !$this->oldTitle ) { |
178 | // Either oldTitle wasn't passed, or newFromText returned null |
179 | throw new ErrorPageError( 'notargettitle', 'notargettext' ); |
180 | } |
181 | $this->getOutput()->addBacklinkSubtitle( $this->oldTitle ); |
182 | |
183 | if ( !$this->oldTitle->exists() ) { |
184 | throw new ErrorPageError( 'nopagetitle', 'nopagetext' ); |
185 | } |
186 | |
187 | $newTitleTextMain = $request->getText( 'wpNewTitleMain' ); |
188 | $newTitleTextNs = $request->getInt( 'wpNewTitleNs', $this->oldTitle->getNamespace() ); |
189 | // Backwards compatibility for forms submitting here from other sources |
190 | // which is more common than it should be. |
191 | $newTitleText_bc = $request->getText( 'wpNewTitle' ); |
192 | $this->newTitle = strlen( $newTitleText_bc ) > 0 |
193 | ? Title::newFromText( $newTitleText_bc ) |
194 | : Title::makeTitleSafe( $newTitleTextNs, $newTitleTextMain ); |
195 | |
196 | $user = $this->getUser(); |
197 | $isSubmit = $request->getRawVal( 'action' ) === 'submit' && $request->wasPosted(); |
198 | |
199 | $reasonList = $request->getText( 'wpReasonList', 'other' ); |
200 | $reason = $request->getText( 'wpReason' ); |
201 | if ( $reasonList === 'other' ) { |
202 | $this->reason = $reason; |
203 | } elseif ( $reason !== '' ) { |
204 | $this->reason = $reasonList . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $reason; |
205 | } else { |
206 | $this->reason = $reasonList; |
207 | } |
208 | // Default to checked, but don't fill in true during submission (browsers only submit checked values) |
209 | // TODO: Use HTMLForm to take care of this. |
210 | $def = !$isSubmit; |
211 | $this->moveTalk = $request->getBool( 'wpMovetalk', $def ); |
212 | $this->fixRedirects = $request->getBool( 'wpFixRedirects', $def ); |
213 | $this->leaveRedirect = $request->getBool( 'wpLeaveRedirect', $def ); |
214 | // T222953: Tick the "move subpages" box by default |
215 | $this->moveSubpages = $request->getBool( 'wpMovesubpages', $def ); |
216 | $this->deleteAndMove = $request->getBool( 'wpDeleteAndMove' ); |
217 | $this->moveOverShared = $request->getBool( 'wpMoveOverSharedFile' ); |
218 | $this->watch = $request->getCheck( 'wpWatch' ) && $user->isRegistered(); |
219 | |
220 | // Similar to other SpecialPage/Action classes, when tokens fail (likely due to reset or expiry), |
221 | // do not show an error but show the form again for easy re-submit. |
222 | if ( $isSubmit && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) { |
223 | // Check rights |
224 | $permErrors = $this->permManager->getPermissionErrors( 'move', $user, $this->oldTitle, |
225 | PermissionManager::RIGOR_SECURE ); |
226 | // If the account is "hard" blocked, auto-block IP |
227 | DeferredUpdates::addCallableUpdate( [ $user, 'spreadAnyEditBlock' ] ); |
228 | if ( $permErrors ) { |
229 | throw new PermissionsError( 'move', $permErrors ); |
230 | } |
231 | $this->doSubmit(); |
232 | } else { |
233 | // Avoid primary DB connection on form view (T283265) |
234 | $permErrors = $this->permManager->getPermissionErrors( 'move', $user, $this->oldTitle, |
235 | PermissionManager::RIGOR_FULL ); |
236 | if ( $permErrors ) { |
237 | DeferredUpdates::addCallableUpdate( [ $user, 'spreadAnyEditBlock' ] ); |
238 | throw new PermissionsError( 'move', $permErrors ); |
239 | } |
240 | $this->showForm( [] ); |
241 | } |
242 | } |
243 | |
244 | /** |
245 | * Show the form |
246 | * |
247 | * @param (string|array)[] $err Error messages. Each item is an error message. |
248 | * It may either be a string message name or array message name and |
249 | * parameters, like the second argument to OutputPage::wrapWikiMsg(). |
250 | * @param bool $isPermError Whether the error message is about user permissions. |
251 | */ |
252 | protected function showForm( $err, $isPermError = false ) { |
253 | $this->getSkin()->setRelevantTitle( $this->oldTitle ); |
254 | |
255 | $out = $this->getOutput(); |
256 | $out->setPageTitleMsg( $this->msg( 'move-page' )->plaintextParams( $this->oldTitle->getPrefixedText() ) ); |
257 | $out->addModuleStyles( [ |
258 | 'mediawiki.special', |
259 | 'mediawiki.interface.helpers.styles' |
260 | ] ); |
261 | $out->addModules( 'mediawiki.misc-authed-ooui' ); |
262 | $this->addHelpLink( 'Help:Moving a page' ); |
263 | |
264 | $handlerSupportsRedirects = $this->contentHandlerFactory |
265 | ->getContentHandler( $this->oldTitle->getContentModel() ) |
266 | ->supportsRedirects(); |
267 | |
268 | if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) ) { |
269 | $out->addWikiMsg( 'movepagetext' ); |
270 | } else { |
271 | $out->addWikiMsg( $handlerSupportsRedirects ? |
272 | 'movepagetext-noredirectfixer' : |
273 | 'movepagetext-noredirectsupport' ); |
274 | } |
275 | |
276 | if ( $this->oldTitle->getNamespace() === NS_USER && !$this->oldTitle->isSubpage() ) { |
277 | $out->addHTML( |
278 | Html::warningBox( |
279 | $out->msg( 'moveuserpage-warning' )->parse(), |
280 | 'mw-moveuserpage-warning' |
281 | ) |
282 | ); |
283 | } elseif ( $this->oldTitle->getNamespace() === NS_CATEGORY ) { |
284 | $out->addHTML( |
285 | Html::warningBox( |
286 | $out->msg( 'movecategorypage-warning' )->parse(), |
287 | 'mw-movecategorypage-warning' |
288 | ) |
289 | ); |
290 | } |
291 | |
292 | $deleteAndMove = false; |
293 | $moveOverShared = false; |
294 | |
295 | $user = $this->getUser(); |
296 | $newTitle = $this->newTitle; |
297 | |
298 | if ( !$newTitle ) { |
299 | # Show the current title as a default |
300 | # when the form is first opened. |
301 | $newTitle = $this->oldTitle; |
302 | } elseif ( !count( $err ) ) { |
303 | # If a title was supplied, probably from the move log revert |
304 | # link, check for validity. We can then show some diagnostic |
305 | # information and save a click. |
306 | $mp = $this->movePageFactory->newMovePage( $this->oldTitle, $newTitle ); |
307 | $status = $mp->isValidMove(); |
308 | $status->merge( $mp->probablyCanMove( $this->getAuthority() ) ); |
309 | if ( $status->getErrors() ) { |
310 | $err = $status->getErrorsArray(); |
311 | } |
312 | } |
313 | |
314 | if ( count( $err ) == 1 && isset( $err[0][0] ) ) { |
315 | if ( $err[0][0] == 'articleexists' |
316 | && $this->permManager->quickUserCan( 'delete', $user, $newTitle ) |
317 | ) { |
318 | $out->addHTML( |
319 | Html::warningBox( |
320 | $out->msg( 'delete_and_move_text', $newTitle->getPrefixedText() )->parse() |
321 | ) |
322 | ); |
323 | $deleteAndMove = true; |
324 | $err = []; |
325 | } elseif ( $err[0][0] == 'redirectexists' && ( |
326 | // Any user that can delete normally can also delete a redirect here |
327 | $this->permManager->quickUserCan( 'delete-redirect', $user, $newTitle ) || |
328 | $this->permManager->quickUserCan( 'delete', $user, $newTitle ) ) |
329 | ) { |
330 | $out->addHTML( |
331 | Html::warningBox( |
332 | $out->msg( 'delete_redirect_and_move_text', $newTitle->getPrefixedText() )->parse() |
333 | ) |
334 | ); |
335 | $deleteAndMove = true; |
336 | $err = []; |
337 | } elseif ( $err[0][0] == 'file-exists-sharedrepo' |
338 | && $this->permManager->userHasRight( $user, 'reupload-shared' ) |
339 | ) { |
340 | $out->addHTML( |
341 | Html::warningBox( |
342 | $out->msg( 'move-over-sharedrepo', $newTitle->getPrefixedText() )->parse() |
343 | ) |
344 | ); |
345 | $moveOverShared = true; |
346 | $err = []; |
347 | } |
348 | } |
349 | |
350 | $oldTalk = $this->oldTitle->getTalkPage(); |
351 | $oldTitleSubpages = $this->oldTitle->hasSubpages(); |
352 | $oldTitleTalkSubpages = $this->oldTitle->getTalkPage()->hasSubpages(); |
353 | |
354 | $canMoveSubpage = ( $oldTitleSubpages || $oldTitleTalkSubpages ) && |
355 | $this->permManager->quickUserCan( |
356 | 'move-subpages', |
357 | $user, |
358 | $this->oldTitle |
359 | ); |
360 | |
361 | # We also want to be able to move assoc. subpage talk-pages even if base page |
362 | # has no associated talk page, so || with $oldTitleTalkSubpages. |
363 | $considerTalk = !$this->oldTitle->isTalkPage() && |
364 | ( $oldTalk->exists() |
365 | || ( $oldTitleTalkSubpages && $canMoveSubpage ) ); |
366 | |
367 | if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) ) { |
368 | $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() |
369 | ->select( '1' ) |
370 | ->from( 'redirect' ) |
371 | ->where( [ 'rd_namespace' => $this->oldTitle->getNamespace() ] ) |
372 | ->andWhere( [ 'rd_title' => $this->oldTitle->getDBkey() ] ) |
373 | ->andWhere( [ 'rd_interwiki' => '' ] ); |
374 | |
375 | $hasRedirects = (bool)$queryBuilder->caller( __METHOD__ )->fetchField(); |
376 | } else { |
377 | $hasRedirects = false; |
378 | } |
379 | |
380 | if ( count( $err ) ) { |
381 | '@phan-var array[] $err'; |
382 | if ( $isPermError ) { |
383 | $action_desc = $this->msg( 'action-move' )->plain(); |
384 | $errMsgHtml = $this->msg( 'permissionserrorstext-withaction', |
385 | count( $err ), $action_desc )->parseAsBlock(); |
386 | } else { |
387 | $errMsgHtml = $this->msg( 'cannotmove', count( $err ) )->parseAsBlock(); |
388 | } |
389 | |
390 | if ( count( $err ) == 1 ) { |
391 | $errMsg = $err[0]; |
392 | $errMsgName = array_shift( $errMsg ); |
393 | |
394 | if ( $errMsgName == 'hookaborted' ) { |
395 | $errMsgHtml .= "<p>{$errMsg[0]}</p>\n"; |
396 | } else { |
397 | $errMsgHtml .= $this->msg( $errMsgName, $errMsg )->parseAsBlock(); |
398 | } |
399 | } else { |
400 | $errStr = []; |
401 | |
402 | foreach ( $err as $errMsg ) { |
403 | if ( $errMsg[0] == 'hookaborted' ) { |
404 | $errStr[] = $errMsg[1]; |
405 | } else { |
406 | $errMsgName = array_shift( $errMsg ); |
407 | $errStr[] = $this->msg( $errMsgName, $errMsg )->parse(); |
408 | } |
409 | } |
410 | |
411 | $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n"; |
412 | } |
413 | $out->addHTML( Html::errorBox( $errMsgHtml ) ); |
414 | } |
415 | |
416 | if ( $this->restrictionStore->isProtected( $this->oldTitle, 'move' ) ) { |
417 | # Is the title semi-protected? |
418 | if ( $this->restrictionStore->isSemiProtected( $this->oldTitle, 'move' ) ) { |
419 | $noticeMsg = 'semiprotectedpagemovewarning'; |
420 | } else { |
421 | # Then it must be protected based on static groups (regular) |
422 | $noticeMsg = 'protectedpagemovewarning'; |
423 | } |
424 | LogEventsList::showLogExtract( |
425 | $out, |
426 | 'protect', |
427 | $this->oldTitle, |
428 | '', |
429 | [ 'lim' => 1, 'msgKey' => $noticeMsg ] |
430 | ); |
431 | } |
432 | |
433 | // Length limit for wpReason and wpNewTitleMain is enforced in the |
434 | // mediawiki.special.movePage module |
435 | |
436 | $immovableNamespaces = []; |
437 | foreach ( $this->getLanguage()->getNamespaces() as $nsId => $_ ) { |
438 | if ( !$this->nsInfo->isMovable( $nsId ) ) { |
439 | $immovableNamespaces[] = $nsId; |
440 | } |
441 | } |
442 | |
443 | $out->enableOOUI(); |
444 | $fields = []; |
445 | |
446 | $fields[] = new FieldLayout( |
447 | new ComplexTitleInputWidget( [ |
448 | 'id' => 'wpNewTitle', |
449 | 'namespace' => [ |
450 | 'id' => 'wpNewTitleNs', |
451 | 'name' => 'wpNewTitleNs', |
452 | 'value' => $newTitle->getNamespace(), |
453 | 'exclude' => $immovableNamespaces, |
454 | ], |
455 | 'title' => [ |
456 | 'id' => 'wpNewTitleMain', |
457 | 'name' => 'wpNewTitleMain', |
458 | 'value' => $newTitle->getText(), |
459 | // Inappropriate, since we're expecting the user to input a non-existent page's title |
460 | 'suggestions' => false, |
461 | ], |
462 | 'infusable' => true, |
463 | ] ), |
464 | [ |
465 | 'label' => $this->msg( 'newtitle' )->text(), |
466 | 'align' => 'top', |
467 | ] |
468 | ); |
469 | |
470 | $options = Html::listDropdownOptions( |
471 | $this->msg( 'movepage-reason-dropdown' ) |
472 | ->page( $this->oldTitle ) |
473 | ->inContentLanguage() |
474 | ->text(), |
475 | [ 'other' => $this->msg( 'movereasonotherlist' )->text() ] |
476 | ); |
477 | $options = Html::listDropdownOptionsOoui( $options ); |
478 | |
479 | $fields[] = new FieldLayout( |
480 | new DropdownInputWidget( [ |
481 | 'name' => 'wpReasonList', |
482 | 'inputId' => 'wpReasonList', |
483 | 'infusable' => true, |
484 | 'value' => $this->getRequest()->getText( 'wpReasonList', 'other' ), |
485 | 'options' => $options, |
486 | ] ), |
487 | [ |
488 | 'label' => $this->msg( 'movereason' )->text(), |
489 | 'align' => 'top', |
490 | ] |
491 | ); |
492 | |
493 | // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP |
494 | // (e.g. emojis) count for two each. This limit is overridden in JS to instead count |
495 | // Unicode codepoints. |
496 | $fields[] = new FieldLayout( |
497 | new TextInputWidget( [ |
498 | 'name' => 'wpReason', |
499 | 'id' => 'wpReason', |
500 | 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
501 | 'infusable' => true, |
502 | 'value' => $this->getRequest()->getText( 'wpReason' ), |
503 | ] ), |
504 | [ |
505 | 'label' => $this->msg( 'moveotherreason' )->text(), |
506 | 'align' => 'top', |
507 | ] |
508 | ); |
509 | |
510 | if ( $considerTalk ) { |
511 | $fields[] = new FieldLayout( |
512 | new CheckboxInputWidget( [ |
513 | 'name' => 'wpMovetalk', |
514 | 'id' => 'wpMovetalk', |
515 | 'value' => '1', |
516 | 'selected' => $this->moveTalk, |
517 | ] ), |
518 | [ |
519 | 'label' => $this->msg( 'movetalk' )->text(), |
520 | 'help' => new HtmlSnippet( $this->msg( 'movepagetalktext' )->parseAsBlock() ), |
521 | 'helpInline' => true, |
522 | 'align' => 'inline', |
523 | 'id' => 'wpMovetalk-field', |
524 | ] |
525 | ); |
526 | } |
527 | |
528 | if ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) { |
529 | if ( $handlerSupportsRedirects ) { |
530 | $isChecked = $this->leaveRedirect; |
531 | $isDisabled = false; |
532 | } else { |
533 | $isChecked = false; |
534 | $isDisabled = true; |
535 | } |
536 | $fields[] = new FieldLayout( |
537 | new CheckboxInputWidget( [ |
538 | 'name' => 'wpLeaveRedirect', |
539 | 'id' => 'wpLeaveRedirect', |
540 | 'value' => '1', |
541 | 'selected' => $isChecked, |
542 | 'disabled' => $isDisabled, |
543 | ] ), |
544 | [ |
545 | 'label' => $this->msg( 'move-leave-redirect' )->text(), |
546 | 'align' => 'inline', |
547 | ] |
548 | ); |
549 | } |
550 | |
551 | if ( $hasRedirects ) { |
552 | $fields[] = new FieldLayout( |
553 | new CheckboxInputWidget( [ |
554 | 'name' => 'wpFixRedirects', |
555 | 'id' => 'wpFixRedirects', |
556 | 'value' => '1', |
557 | 'selected' => $this->fixRedirects, |
558 | ] ), |
559 | [ |
560 | 'label' => $this->msg( 'fix-double-redirects' )->text(), |
561 | 'align' => 'inline', |
562 | ] |
563 | ); |
564 | } |
565 | |
566 | if ( $canMoveSubpage ) { |
567 | $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages ); |
568 | $fields[] = new FieldLayout( |
569 | new CheckboxInputWidget( [ |
570 | 'name' => 'wpMovesubpages', |
571 | 'id' => 'wpMovesubpages', |
572 | 'value' => '1', |
573 | 'selected' => $this->moveSubpages, |
574 | ] ), |
575 | [ |
576 | 'label' => new HtmlSnippet( |
577 | $this->msg( |
578 | ( $this->oldTitle->hasSubpages() |
579 | ? 'move-subpages' |
580 | : 'move-talk-subpages' ) |
581 | )->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse() |
582 | ), |
583 | 'align' => 'inline', |
584 | ] |
585 | ); |
586 | } |
587 | |
588 | # Don't allow watching if user is not logged in |
589 | if ( $user->isRegistered() ) { |
590 | $watchChecked = ( $this->watch || $this->userOptionsLookup->getBoolOption( $user, 'watchmoves' ) |
591 | || $this->watchlistManager->isWatched( $user, $this->oldTitle ) ); |
592 | $fields[] = new FieldLayout( |
593 | new CheckboxInputWidget( [ |
594 | 'name' => 'wpWatch', |
595 | 'id' => 'watch', # ew |
596 | 'value' => '1', |
597 | 'selected' => $watchChecked, |
598 | ] ), |
599 | [ |
600 | 'label' => $this->msg( 'move-watch' )->text(), |
601 | 'align' => 'inline', |
602 | ] |
603 | ); |
604 | } |
605 | |
606 | $hiddenFields = ''; |
607 | if ( $moveOverShared ) { |
608 | $hiddenFields .= Html::hidden( 'wpMoveOverSharedFile', '1' ); |
609 | } |
610 | |
611 | if ( $deleteAndMove ) { |
612 | $fields[] = new FieldLayout( |
613 | new CheckboxInputWidget( [ |
614 | 'name' => 'wpDeleteAndMove', |
615 | 'id' => 'wpDeleteAndMove', |
616 | 'value' => '1', |
617 | ] ), |
618 | [ |
619 | 'label' => $this->msg( 'delete_and_move_confirm' )->text(), |
620 | 'align' => 'inline', |
621 | ] |
622 | ); |
623 | } |
624 | |
625 | $fields[] = new FieldLayout( |
626 | new ButtonInputWidget( [ |
627 | 'name' => 'wpMove', |
628 | 'value' => $this->msg( 'movepagebtn' )->text(), |
629 | 'label' => $this->msg( 'movepagebtn' )->text(), |
630 | 'flags' => [ 'primary', 'progressive' ], |
631 | 'type' => 'submit', |
632 | ] ), |
633 | [ |
634 | 'align' => 'top', |
635 | ] |
636 | ); |
637 | |
638 | $fieldset = new FieldsetLayout( [ |
639 | 'label' => $this->msg( 'move-page-legend' )->text(), |
640 | 'id' => 'mw-movepage-table', |
641 | 'items' => $fields, |
642 | ] ); |
643 | |
644 | $form = new FormLayout( [ |
645 | 'method' => 'post', |
646 | 'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ), |
647 | 'id' => 'movepage', |
648 | ] ); |
649 | $form->appendContent( |
650 | $fieldset, |
651 | new HtmlSnippet( |
652 | $hiddenFields . |
653 | Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) . |
654 | Html::hidden( 'wpEditToken', $user->getEditToken() ) |
655 | ) |
656 | ); |
657 | |
658 | $out->addHTML( |
659 | new PanelLayout( [ |
660 | 'classes' => [ 'movepage-wrapper' ], |
661 | 'expanded' => false, |
662 | 'padded' => true, |
663 | 'framed' => true, |
664 | 'content' => $form, |
665 | ] ) |
666 | ); |
667 | if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) { |
668 | $link = $this->getLinkRenderer()->makeKnownLink( |
669 | $this->msg( 'movepage-reason-dropdown' )->inContentLanguage()->getTitle(), |
670 | $this->msg( 'movepage-edit-reasonlist' )->text(), |
671 | [], |
672 | [ 'action' => 'edit' ] |
673 | ); |
674 | $out->addHTML( Html::rawElement( 'p', [ 'class' => 'mw-movepage-editreasons' ], $link ) ); |
675 | } |
676 | |
677 | $this->showLogFragment( $this->oldTitle ); |
678 | $this->showSubpages( $this->oldTitle ); |
679 | } |
680 | |
681 | private function doSubmit() { |
682 | $user = $this->getUser(); |
683 | |
684 | if ( $user->pingLimiter( 'move' ) ) { |
685 | throw new ThrottledError; |
686 | } |
687 | |
688 | $ot = $this->oldTitle; |
689 | $nt = $this->newTitle; |
690 | |
691 | # don't allow moving to pages with # in |
692 | if ( !$nt || $nt->hasFragment() ) { |
693 | $this->showForm( [ [ 'badtitletext' ] ] ); |
694 | |
695 | return; |
696 | } |
697 | |
698 | # Show a warning if the target file exists on a shared repo |
699 | if ( $nt->getNamespace() === NS_FILE |
700 | && !( $this->moveOverShared && $this->permManager->userHasRight( $user, 'reupload-shared' ) ) |
701 | && !$this->repoGroup->getLocalRepo()->findFile( $nt ) |
702 | && $this->repoGroup->findFile( $nt ) |
703 | ) { |
704 | $this->showForm( [ [ 'file-exists-sharedrepo' ] ] ); |
705 | |
706 | return; |
707 | } |
708 | |
709 | # Delete to make way if requested |
710 | if ( $this->deleteAndMove ) { |
711 | $redir2 = $nt->isSingleRevRedirect(); |
712 | |
713 | $permErrors = $this->permManager->getPermissionErrors( |
714 | $redir2 ? 'delete-redirect' : 'delete', |
715 | $user, $nt |
716 | ); |
717 | if ( count( $permErrors ) ) { |
718 | if ( $redir2 ) { |
719 | if ( count( $this->permManager->getPermissionErrors( 'delete', $user, $nt ) ) ) { |
720 | // Cannot delete-redirect, or delete normally |
721 | // Only show the first error |
722 | $this->showForm( $permErrors, true ); |
723 | return; |
724 | } else { |
725 | // Cannot delete-redirect, but can delete normally, |
726 | // so log as a normal deletion |
727 | $redir2 = false; |
728 | } |
729 | } else { |
730 | // Cannot delete normally |
731 | // Only show first error |
732 | $this->showForm( $permErrors, true ); |
733 | return; |
734 | } |
735 | } |
736 | |
737 | $page = $this->wikiPageFactory->newFromTitle( $nt ); |
738 | |
739 | // Small safety margin to guard against concurrent edits |
740 | if ( $page->isBatchedDelete( 5 ) ) { |
741 | $this->showForm( [ [ 'movepage-delete-first' ] ] ); |
742 | |
743 | return; |
744 | } |
745 | |
746 | $reason = $this->msg( 'delete_and_move_reason', $ot )->inContentLanguage()->text(); |
747 | |
748 | // Delete an associated image if there is |
749 | if ( $nt->getNamespace() === NS_FILE ) { |
750 | $file = $this->repoGroup->getLocalRepo()->newFile( $nt ); |
751 | $file->load( IDBAccessObject::READ_LATEST ); |
752 | if ( $file->exists() ) { |
753 | $file->deleteFile( $reason, $user, false ); |
754 | } |
755 | } |
756 | |
757 | $error = ''; // passed by ref |
758 | $deletionLog = $redir2 ? 'delete_redir2' : 'delete'; |
759 | $deleteStatus = $page->doDeleteArticleReal( |
760 | $reason, $user, false, null, $error, |
761 | null, [], $deletionLog |
762 | ); |
763 | if ( !$deleteStatus->isGood() ) { |
764 | $this->showForm( $deleteStatus->getErrorsArray() ); |
765 | |
766 | return; |
767 | } |
768 | } |
769 | |
770 | $handler = $this->contentHandlerFactory->getContentHandler( $ot->getContentModel() ); |
771 | |
772 | if ( !$handler->supportsRedirects() ) { |
773 | $createRedirect = false; |
774 | } elseif ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) { |
775 | $createRedirect = $this->leaveRedirect; |
776 | } else { |
777 | $createRedirect = true; |
778 | } |
779 | |
780 | # Do the actual move. |
781 | $mp = $this->movePageFactory->newMovePage( $ot, $nt ); |
782 | |
783 | # check whether the requested actions are permitted / possible |
784 | $userPermitted = $mp->authorizeMove( $this->getAuthority(), $this->reason )->isOK(); |
785 | if ( $ot->isTalkPage() || $nt->isTalkPage() ) { |
786 | $this->moveTalk = false; |
787 | } |
788 | if ( $this->moveSubpages ) { |
789 | $this->moveSubpages = $this->permManager->userCan( 'move-subpages', $user, $ot ); |
790 | } |
791 | |
792 | $status = $mp->moveIfAllowed( $this->getAuthority(), $this->reason, $createRedirect ); |
793 | if ( !$status->isOK() ) { |
794 | $this->showForm( $status->getErrorsArray(), !$userPermitted ); |
795 | return; |
796 | } |
797 | |
798 | if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) && |
799 | $this->fixRedirects ) { |
800 | DoubleRedirectJob::fixRedirects( 'move', $ot ); |
801 | } |
802 | |
803 | $out = $this->getOutput(); |
804 | $out->setPageTitleMsg( $this->msg( 'pagemovedsub' ) ); |
805 | |
806 | $linkRenderer = $this->getLinkRenderer(); |
807 | $oldLink = $linkRenderer->makeLink( |
808 | $ot, |
809 | null, |
810 | [ 'id' => 'movepage-oldlink' ], |
811 | [ 'redirect' => 'no' ] |
812 | ); |
813 | $newLink = $linkRenderer->makeKnownLink( |
814 | $nt, |
815 | null, |
816 | [ 'id' => 'movepage-newlink' ] |
817 | ); |
818 | $oldText = $ot->getPrefixedText(); |
819 | $newText = $nt->getPrefixedText(); |
820 | |
821 | if ( $status->getValue()['redirectRevision'] !== null ) { |
822 | $msgName = 'movepage-moved-redirect'; |
823 | } else { |
824 | $msgName = 'movepage-moved-noredirect'; |
825 | } |
826 | |
827 | $out->addHTML( $this->msg( 'movepage-moved' )->rawParams( $oldLink, |
828 | $newLink )->params( $oldText, $newText )->parseAsBlock() ); |
829 | $out->addWikiMsg( $msgName ); |
830 | |
831 | $this->getHookRunner()->onSpecialMovepageAfterMove( $this, $ot, $nt ); |
832 | |
833 | /* |
834 | * Now we move extra pages we've been asked to move: subpages and talk |
835 | * pages. |
836 | * |
837 | * First, make a list of id's. This might be marginally less efficient |
838 | * than a more direct method, but this is not a highly performance-cri- |
839 | * tical code path and readable code is more important here. |
840 | * |
841 | * If the target namespace doesn't allow subpages, moving with subpages |
842 | * would mean that you couldn't move them back in one operation, which |
843 | * is bad. |
844 | * @todo FIXME: A specific error message should be given in this case. |
845 | */ |
846 | |
847 | // @todo FIXME: Use MovePage::moveSubpages() here |
848 | $dbr = $this->dbProvider->getReplicaDatabase(); |
849 | if ( $this->moveSubpages && ( |
850 | $this->nsInfo->hasSubpages( $nt->getNamespace() ) || ( |
851 | $this->moveTalk |
852 | && $this->nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() ) |
853 | ) |
854 | ) ) { |
855 | $conds = [ |
856 | $dbr->expr( |
857 | 'page_title', |
858 | IExpression::LIKE, |
859 | new LikeValue( $ot->getDBkey() . '/', $dbr->anyString() ) |
860 | )->or( 'page_title', '=', $ot->getDBkey() ) |
861 | ]; |
862 | $conds['page_namespace'] = []; |
863 | if ( $this->nsInfo->hasSubpages( $nt->getNamespace() ) ) { |
864 | $conds['page_namespace'][] = $ot->getNamespace(); |
865 | } |
866 | if ( $this->moveTalk && |
867 | $this->nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() ) |
868 | ) { |
869 | $conds['page_namespace'][] = $ot->getTalkPage()->getNamespace(); |
870 | } |
871 | } elseif ( $this->moveTalk ) { |
872 | $conds = [ |
873 | 'page_namespace' => $ot->getTalkPage()->getNamespace(), |
874 | 'page_title' => $ot->getDBkey() |
875 | ]; |
876 | } else { |
877 | # Skip the query |
878 | $conds = null; |
879 | } |
880 | |
881 | $extraPages = []; |
882 | if ( $conds !== null ) { |
883 | $extraPages = $this->titleFactory->newTitleArrayFromResult( |
884 | $dbr->newSelectQueryBuilder() |
885 | ->select( [ 'page_id', 'page_namespace', 'page_title' ] ) |
886 | ->from( 'page' ) |
887 | ->where( $conds ) |
888 | ->caller( __METHOD__ )->fetchResultSet() |
889 | ); |
890 | } |
891 | |
892 | $extraOutput = []; |
893 | $count = 1; |
894 | foreach ( $extraPages as $oldSubpage ) { |
895 | if ( $ot->equals( $oldSubpage ) || $nt->equals( $oldSubpage ) ) { |
896 | # Already did this one. |
897 | continue; |
898 | } |
899 | |
900 | $newPageName = preg_replace( |
901 | '#^' . preg_quote( $ot->getDBkey(), '#' ) . '#', |
902 | StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234 |
903 | $oldSubpage->getDBkey() |
904 | ); |
905 | |
906 | if ( $oldSubpage->isSubpage() && ( $ot->isTalkPage() xor $nt->isTalkPage() ) ) { |
907 | // Moving a subpage from a subject namespace to a talk namespace or vice-versa |
908 | $newNs = $nt->getNamespace(); |
909 | } elseif ( $oldSubpage->isTalkPage() ) { |
910 | $newNs = $nt->getTalkPage()->getNamespace(); |
911 | } else { |
912 | $newNs = $nt->getSubjectPage()->getNamespace(); |
913 | } |
914 | |
915 | # T16385: we need makeTitleSafe because the new page names may |
916 | # be longer than 255 characters. |
917 | $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); |
918 | if ( !$newSubpage ) { |
919 | $oldLink = $linkRenderer->makeKnownLink( $oldSubpage ); |
920 | $extraOutput[] = $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink ) |
921 | ->params( Title::makeName( $newNs, $newPageName ) )->escaped(); |
922 | continue; |
923 | } |
924 | |
925 | $mp = $this->movePageFactory->newMovePage( $oldSubpage, $newSubpage ); |
926 | # This was copy-pasted from Renameuser, bleh. |
927 | if ( $newSubpage->exists() && !$mp->isValidMove()->isOK() ) { |
928 | $link = $linkRenderer->makeKnownLink( $newSubpage ); |
929 | $extraOutput[] = $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped(); |
930 | } else { |
931 | $status = $mp->moveIfAllowed( $this->getAuthority(), $this->reason, $createRedirect ); |
932 | |
933 | if ( $status->isOK() ) { |
934 | if ( $this->fixRedirects ) { |
935 | DoubleRedirectJob::fixRedirects( 'move', $oldSubpage ); |
936 | } |
937 | $oldLink = $linkRenderer->makeLink( |
938 | $oldSubpage, |
939 | null, |
940 | [], |
941 | [ 'redirect' => 'no' ] |
942 | ); |
943 | |
944 | $newLink = $linkRenderer->makeKnownLink( $newSubpage ); |
945 | $extraOutput[] = $this->msg( 'movepage-page-moved' ) |
946 | ->rawParams( $oldLink, $newLink )->escaped(); |
947 | ++$count; |
948 | |
949 | $maximumMovedPages = |
950 | $this->getConfig()->get( MainConfigNames::MaximumMovedPages ); |
951 | if ( $count >= $maximumMovedPages ) { |
952 | $extraOutput[] = $this->msg( 'movepage-max-pages' ) |
953 | ->numParams( $maximumMovedPages )->escaped(); |
954 | break; |
955 | } |
956 | } else { |
957 | $oldLink = $linkRenderer->makeKnownLink( $oldSubpage ); |
958 | $newLink = $linkRenderer->makeLink( $newSubpage ); |
959 | $extraOutput[] = $this->msg( 'movepage-page-unmoved' ) |
960 | ->rawParams( $oldLink, $newLink )->escaped(); |
961 | } |
962 | } |
963 | } |
964 | |
965 | if ( $extraOutput !== [] ) { |
966 | $out->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" ); |
967 | } |
968 | |
969 | # Deal with watches (we don't watch subpages) |
970 | $this->watchlistManager->setWatch( $this->watch, $this->getAuthority(), $ot ); |
971 | $this->watchlistManager->setWatch( $this->watch, $this->getAuthority(), $nt ); |
972 | } |
973 | |
974 | private function showLogFragment( $title ) { |
975 | $moveLogPage = new LogPage( 'move' ); |
976 | $out = $this->getOutput(); |
977 | $out->addHTML( Xml::element( 'h2', null, $moveLogPage->getName()->text() ) ); |
978 | LogEventsList::showLogExtract( $out, 'move', $title ); |
979 | } |
980 | |
981 | /** |
982 | * Show subpages of the page being moved. Section is not shown if both current |
983 | * namespace does not support subpages and no talk subpages were found. |
984 | * |
985 | * @param Title $title Page being moved. |
986 | */ |
987 | private function showSubpages( $title ) { |
988 | $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages ); |
989 | $nsHasSubpages = $this->nsInfo->hasSubpages( $title->getNamespace() ); |
990 | $subpages = $title->getSubpages( $maximumMovedPages + 1 ); |
991 | $count = $subpages instanceof TitleArrayFromResult ? $subpages->count() : 0; |
992 | |
993 | $titleIsTalk = $title->isTalkPage(); |
994 | $subpagesTalk = $title->getTalkPage()->getSubpages( $maximumMovedPages + 1 ); |
995 | $countTalk = $subpagesTalk instanceof TitleArrayFromResult ? $subpagesTalk->count() : 0; |
996 | $totalCount = $count + $countTalk; |
997 | |
998 | if ( !$nsHasSubpages && $countTalk == 0 ) { |
999 | return; |
1000 | } |
1001 | |
1002 | $this->getOutput()->wrapWikiMsg( |
1003 | '== $1 ==', |
1004 | [ 'movesubpage', ( $titleIsTalk ? $count : $totalCount ) ] |
1005 | ); |
1006 | |
1007 | if ( $nsHasSubpages ) { |
1008 | $this->showSubpagesList( |
1009 | $subpages, $count, 'movesubpagetext', 'movesubpagetext-truncated', true |
1010 | ); |
1011 | } |
1012 | |
1013 | if ( !$titleIsTalk && $countTalk > 0 ) { |
1014 | $this->showSubpagesList( |
1015 | $subpagesTalk, $countTalk, 'movesubpagetalktext', 'movesubpagetalktext-truncated' |
1016 | ); |
1017 | } |
1018 | } |
1019 | |
1020 | private function showSubpagesList( $subpages, $pagecount, $msg, $truncatedMsg, $noSubpageMsg = false ) { |
1021 | $out = $this->getOutput(); |
1022 | |
1023 | # No subpages. |
1024 | if ( $pagecount == 0 && $noSubpageMsg ) { |
1025 | $out->addWikiMsg( 'movenosubpage' ); |
1026 | return; |
1027 | } |
1028 | |
1029 | $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages ); |
1030 | |
1031 | if ( $pagecount > $maximumMovedPages ) { |
1032 | $subpages = $this->truncateSubpagesList( $subpages ); |
1033 | $out->addWikiMsg( $truncatedMsg, $this->getLanguage()->formatNum( $maximumMovedPages ) ); |
1034 | } else { |
1035 | $out->addWikiMsg( $msg, $this->getLanguage()->formatNum( $pagecount ) ); |
1036 | } |
1037 | $out->addHTML( "<ul>\n" ); |
1038 | |
1039 | $linkBatch = $this->linkBatchFactory->newLinkBatch( $subpages ); |
1040 | $linkBatch->setCaller( __METHOD__ ); |
1041 | $linkBatch->execute(); |
1042 | $linkRenderer = $this->getLinkRenderer(); |
1043 | |
1044 | foreach ( $subpages as $subpage ) { |
1045 | $link = $linkRenderer->makeLink( $subpage ); |
1046 | $out->addHTML( "<li>$link</li>\n" ); |
1047 | } |
1048 | $out->addHTML( "</ul>\n" ); |
1049 | } |
1050 | |
1051 | private function truncateSubpagesList( iterable $subpages ): array { |
1052 | $returnArray = []; |
1053 | foreach ( $subpages as $subpage ) { |
1054 | $returnArray[] = $subpage; |
1055 | if ( count( $returnArray ) >= $this->getConfig()->get( MainConfigNames::MaximumMovedPages ) ) { |
1056 | break; |
1057 | } |
1058 | } |
1059 | return $returnArray; |
1060 | } |
1061 | |
1062 | /** |
1063 | * Return an array of subpages beginning with $search that this special page will accept. |
1064 | * |
1065 | * @param string $search Prefix to search for |
1066 | * @param int $limit Maximum number of results to return (usually 10) |
1067 | * @param int $offset Number of results to skip (usually 0) |
1068 | * @return string[] Matching subpages |
1069 | */ |
1070 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
1071 | return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory ); |
1072 | } |
1073 | |
1074 | protected function getGroupName() { |
1075 | return 'pagetools'; |
1076 | } |
1077 | } |
1078 | |
1079 | /** |
1080 | * Retain the old class name for backwards compatibility. |
1081 | * @deprecated since 1.40 |
1082 | */ |
1083 | class_alias( SpecialMovePage::class, 'MovePageForm' ); |