MediaWiki master
SpecialMovePage.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
41use OOUI\ButtonInputWidget;
42use OOUI\CheckboxInputWidget;
43use OOUI\DropdownInputWidget;
44use OOUI\FieldLayout;
45use OOUI\FieldsetLayout;
46use OOUI\FormLayout;
47use OOUI\HtmlSnippet;
48use OOUI\PanelLayout;
49use OOUI\TextInputWidget;
50use StatusValue;
54use Wikimedia\Timestamp\TimestampFormat as TS;
55
63 protected $oldTitle = null;
64
66 protected $newTitle;
67
69 protected $reason;
70
72 protected $moveTalk;
73
75 protected $deleteAndMove;
76
78 protected $moveSubpages;
79
81 protected $fixRedirects;
82
84 protected $leaveRedirect;
85
87 protected $moveOverShared;
88
89 private bool $moveOverProtection;
90
92 private $watch = false;
93
94 private MovePageFactory $movePageFactory;
95 private PermissionManager $permManager;
96 private UserOptionsLookup $userOptionsLookup;
97 private IConnectionProvider $dbProvider;
98 private IContentHandlerFactory $contentHandlerFactory;
99 private NamespaceInfo $nsInfo;
100 private LinkBatchFactory $linkBatchFactory;
101 private RepoGroup $repoGroup;
102 private WikiPageFactory $wikiPageFactory;
103 private SearchEngineFactory $searchEngineFactory;
104 private WatchlistManager $watchlistManager;
105 private WatchedItemStore $watchedItemStore;
106 private RestrictionStore $restrictionStore;
107 private TitleFactory $titleFactory;
108 private DeletePageFactory $deletePageFactory;
109
110 public function __construct(
111 MovePageFactory $movePageFactory,
112 PermissionManager $permManager,
113 UserOptionsLookup $userOptionsLookup,
114 IConnectionProvider $dbProvider,
115 IContentHandlerFactory $contentHandlerFactory,
116 NamespaceInfo $nsInfo,
117 LinkBatchFactory $linkBatchFactory,
118 RepoGroup $repoGroup,
119 WikiPageFactory $wikiPageFactory,
120 SearchEngineFactory $searchEngineFactory,
121 WatchlistManager $watchlistManager,
122 WatchedItemStore $watchedItemStore,
123 RestrictionStore $restrictionStore,
124 TitleFactory $titleFactory,
125 DeletePageFactory $deletePageFactory
126 ) {
127 parent::__construct( 'Movepage' );
128 $this->movePageFactory = $movePageFactory;
129 $this->permManager = $permManager;
130 $this->userOptionsLookup = $userOptionsLookup;
131 $this->dbProvider = $dbProvider;
132 $this->contentHandlerFactory = $contentHandlerFactory;
133 $this->nsInfo = $nsInfo;
134 $this->linkBatchFactory = $linkBatchFactory;
135 $this->repoGroup = $repoGroup;
136 $this->wikiPageFactory = $wikiPageFactory;
137 $this->searchEngineFactory = $searchEngineFactory;
138 $this->watchlistManager = $watchlistManager;
139 $this->watchedItemStore = $watchedItemStore;
140 $this->restrictionStore = $restrictionStore;
141 $this->titleFactory = $titleFactory;
142 $this->deletePageFactory = $deletePageFactory;
143 }
144
146 public function doesWrites() {
147 return true;
148 }
149
151 public function execute( $par ) {
153 $this->checkReadOnly();
154 $this->setHeaders();
155 $this->outputHeader();
156
157 $request = $this->getRequest();
158
159 // Beware: The use of WebRequest::getText() is wanted! See T22365
160 $target = $par ?? $request->getText( 'target' );
161 $oldTitleText = $request->getText( 'wpOldTitle', $target );
162 $this->oldTitle = Title::newFromText( $oldTitleText );
163
164 if ( !$this->oldTitle ) {
165 // Either oldTitle wasn't passed, or newFromText returned null
166 throw new ErrorPageError( 'notargettitle', 'notargettext' );
167 }
168 $this->getOutput()->addBacklinkSubtitle( $this->oldTitle );
169 // Various uses of Html::errorBox and Html::warningBox.
170 $this->getOutput()->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
171
172 if ( !$this->oldTitle->exists() ) {
173 throw new ErrorPageError( 'nopagetitle', 'nopagetext' );
174 }
175
176 $newTitleTextMain = $request->getText( 'wpNewTitleMain' );
177 $newTitleTextNs = $request->getInt( 'wpNewTitleNs', $this->oldTitle->getNamespace() );
178 // Backwards compatibility for forms submitting here from other sources
179 // which is more common than it should be.
180 $newTitleText_bc = $request->getText( 'wpNewTitle' );
181 $this->newTitle = strlen( $newTitleText_bc ) > 0
182 ? Title::newFromText( $newTitleText_bc )
183 : Title::makeTitleSafe( $newTitleTextNs, $newTitleTextMain );
184
185 $user = $this->getUser();
186 $isSubmit = $request->getRawVal( 'action' ) === 'submit' && $request->wasPosted();
187
188 $reasonList = $request->getText( 'wpReasonList', 'other' );
189 $reason = $request->getText( 'wpReason' );
190 if ( $reasonList === 'other' ) {
191 $this->reason = $reason;
192 } elseif ( $reason !== '' ) {
193 $this->reason = $reasonList . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $reason;
194 } else {
195 $this->reason = $reasonList;
196 }
197 // Default to checked, but don't fill in true during submission (browsers only submit checked values)
198 // TODO: Use HTMLForm to take care of this.
199 $def = !$isSubmit;
200 $this->moveTalk = $request->getBool( 'wpMovetalk', $def );
201 $this->fixRedirects = $request->getBool( 'wpFixRedirects', $def );
202 $this->leaveRedirect = $request->getBool( 'wpLeaveRedirect', $def );
203 // T222953: Tick the "move subpages" box by default
204 $this->moveSubpages = $request->getBool( 'wpMovesubpages', $def );
205 $this->deleteAndMove = $request->getBool( 'wpDeleteAndMove' );
206 $this->moveOverShared = $request->getBool( 'wpMoveOverSharedFile' );
207 $this->moveOverProtection = $request->getBool( 'wpMoveOverProtection' );
208 $this->watch = $request->getCheck( 'wpWatch' ) && $user->isRegistered();
209
210 // Similar to other SpecialPage/Action classes, when tokens fail (likely due to reset or expiry),
211 // do not show an error but show the form again for easy re-submit.
212 if ( $isSubmit && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
213 // Check rights
214 $permStatus = $this->permManager->getPermissionStatus( 'move', $user, $this->oldTitle,
215 PermissionManager::RIGOR_SECURE );
216 // If the account is "hard" blocked, auto-block IP
217 $user->scheduleSpreadBlock();
218 if ( !$permStatus->isGood() ) {
219 throw new PermissionsError( 'move', $permStatus );
220 }
221 $this->doSubmit();
222 } else {
223 // Avoid primary DB connection on form view (T283265)
224 $permStatus = $this->permManager->getPermissionStatus( 'move', $user, $this->oldTitle,
225 PermissionManager::RIGOR_FULL );
226 if ( !$permStatus->isGood() ) {
227 $user->scheduleSpreadBlock();
228 throw new PermissionsError( 'move', $permStatus );
229 }
230 $this->showForm();
231 }
232 }
233
241 private function showForm( ?StatusValue $status = null, ?StatusValue $talkStatus = null ) {
242 $this->getSkin()->setRelevantTitle( $this->oldTitle );
243
244 $out = $this->getOutput();
245 $out->setPageTitleMsg( $this->msg( 'move-page' )->plaintextParams( $this->oldTitle->getPrefixedText() ) );
246 $out->addModuleStyles( [
247 'mediawiki.special',
248 'mediawiki.interface.helpers.styles'
249 ] );
250 $out->addModules( 'mediawiki.misc-authed-ooui' );
251 $this->addHelpLink( 'Help:Moving a page' );
252
253 $handler = $this->contentHandlerFactory
254 ->getContentHandler( $this->oldTitle->getContentModel() );
255 $createRedirect = $handler->supportsRedirects() && !(
256 // Do not create redirects for wikitext message overrides (T376399).
257 // Maybe one day they will have a custom content model and this special case won't be needed.
258 $this->oldTitle->getNamespace() === NS_MEDIAWIKI &&
259 $this->oldTitle->getContentModel() === CONTENT_MODEL_WIKITEXT
260 );
261
262 if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) ) {
263 $out->addWikiMsg( 'movepagetext' );
264 } else {
265 $out->addWikiMsg( $createRedirect ?
266 'movepagetext-noredirectfixer' :
267 'movepagetext-noredirectsupport' );
268 }
269
270 if ( $this->oldTitle->getNamespace() === NS_USER && !$this->oldTitle->isSubpage() ) {
271 $out->addHTML(
272 Html::warningBox(
273 $out->msg( 'moveuserpage-warning' )->parse(),
274 'mw-moveuserpage-warning'
275 )
276 );
277 // Deselect moveTalk unless it's explicitly given
278 $this->moveTalk = $this->getRequest()->getBool( "wpMovetalk", false );
279 } elseif ( $this->oldTitle->getNamespace() === NS_CATEGORY ) {
280 $out->addHTML(
281 Html::warningBox(
282 $out->msg( 'movecategorypage-warning' )->parse(),
283 'mw-movecategorypage-warning'
284 )
285 );
286 }
287
288 $deleteAndMove = [];
289 $moveOverShared = false;
290
291 $user = $this->getUser();
292 $newTitle = $this->newTitle;
293 $oldTalk = $this->oldTitle->getTalkPageIfDefined();
294
295 if ( !$newTitle ) {
296 # Show the current title as a default
297 # when the form is first opened.
299 } elseif ( !$status ) {
300 # If a title was supplied, probably from the move log revert
301 # link, check for validity. We can then show some diagnostic
302 # information and save a click.
303 $mp = $this->movePageFactory->newMovePage( $this->oldTitle, $newTitle );
304 $status = $mp->isValidMove();
305 $status->merge( $mp->probablyCanMove( $this->getAuthority() ) );
306 if ( $this->moveTalk ) {
307 $newTalk = $newTitle->getTalkPageIfDefined();
308 if ( $oldTalk && $newTalk && $oldTalk->exists() ) {
309 $mpTalk = $this->movePageFactory->newMovePage( $oldTalk, $newTalk );
310 $talkStatus = $mpTalk->isValidMove();
311 $talkStatus->merge( $mpTalk->probablyCanMove( $this->getAuthority() ) );
312 }
313 }
314 }
315 if ( !$status ) {
316 // Caller (execute) is responsible for checking that you have permission to move the page somewhere
317 $status = StatusValue::newGood();
318 }
319 if ( !$talkStatus ) {
320 if ( $oldTalk ) {
321 // If you don't have permission to move the talk page anywhere then complain about that now
322 // rather than only after submitting the form to move the page
323 $talkStatus = $this->permManager->getPermissionStatus( 'move', $user, $oldTalk,
324 PermissionManager::RIGOR_QUICK );
325 } else {
326 // If there's no talk page to move (for example the old page is in a namespace with no talk page)
327 // then this needs to be set to something ...
328 $talkStatus = StatusValue::newGood();
329 }
330 }
331
332 $oldTalk = $this->oldTitle->getTalkPageIfDefined();
333 $oldTitleSubpages = $this->oldTitle->hasSubpages();
334 $oldTitleTalkSubpages = $this->oldTitle->getTalkPageIfDefined()->hasSubpages();
335
336 $canMoveSubpage = ( $oldTitleSubpages || $oldTitleTalkSubpages ) &&
337 $this->permManager->quickUserCan(
338 'move-subpages',
339 $user,
340 $this->oldTitle
341 );
342 # We also want to be able to move assoc. subpage talk-pages even if base page
343 # has no associated talk page, so || with $oldTitleTalkSubpages.
344 $considerTalk = !$this->oldTitle->isTalkPage() &&
345 ( $oldTalk->exists()
346 || ( $oldTitleTalkSubpages && $canMoveSubpage ) );
347
348 if ( $this->getConfig()->get( MainConfigNames::FixDoubleRedirects ) ) {
349 $queryBuilder = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
350 ->select( '1' )
351 ->from( 'redirect' )
352 ->where( [ 'rd_namespace' => $this->oldTitle->getNamespace() ] )
353 ->andWhere( [ 'rd_title' => $this->oldTitle->getDBkey() ] )
354 ->andWhere( [ 'rd_interwiki' => '' ] );
355
356 $hasRedirects = (bool)$queryBuilder->caller( __METHOD__ )->fetchField();
357 } else {
358 $hasRedirects = false;
359 }
360
361 $newTalkTitle = $newTitle->getTalkPageIfDefined();
362 $talkOK = $talkStatus->isOK();
363 $mainOK = $status->isOK();
364 $talkIsArticle = $talkIsRedirect = $mainIsArticle = $mainIsRedirect = false;
365 if ( count( $status->getMessages() ) == 1 ) {
366 $mainIsArticle = $status->hasMessage( 'articleexists' )
367 && $this->permManager->quickUserCan( 'delete', $user, $newTitle );
368 $mainIsRedirect = $status->hasMessage( 'redirectexists' ) && (
369 // Any user that can delete normally can also delete a redirect here
370 $this->permManager->quickUserCan( 'delete-redirect', $user, $newTitle ) ||
371 $this->permManager->quickUserCan( 'delete', $user, $newTitle ) );
372 if ( $status->hasMessage( 'file-exists-sharedrepo' )
373 && $this->permManager->userHasRight( $user, 'reupload-shared' )
374 ) {
375 $out->addHTML(
376 Html::warningBox(
377 $out->msg( 'move-over-sharedrepo', $newTitle->getPrefixedText() )->parse()
378 )
379 );
380 $moveOverShared = true;
381 $status = StatusValue::newGood();
382 }
383 }
384 if ( count( $talkStatus->getMessages() ) == 1 ) {
385 $talkIsArticle = $talkStatus->hasMessage( 'articleexists' )
386 && $this->permManager->quickUserCan( 'delete', $user, $newTitle );
387 $talkIsRedirect = $talkStatus->hasMessage( 'redirectexists' ) && (
388 // Any user that can delete normally can also delete a redirect here
389 $this->permManager->quickUserCan( 'delete-redirect', $user, $newTitle ) ||
390 $this->permManager->quickUserCan( 'delete', $user, $newTitle ) );
391 // Talk page is by definition not a file so can't be shared
392 }
393 $warning = null;
394 // Case 1: Two pages need deletions of full history
395 // Either both are articles or one is an article and one is a redirect
396 if ( ( $talkIsArticle && $mainIsArticle ) ||
397 ( $talkIsArticle && $mainIsRedirect ) ||
398 ( $talkIsRedirect && $mainIsArticle )
399 ) {
400 $warning = $out->msg( 'delete_and_move_text_2',
402 $newTalkTitle->getPrefixedText()
403 );
404 $deleteAndMove = [ $newTitle, $newTalkTitle ];
405 // Case 2: Both need simple deletes
406 } elseif ( $mainIsRedirect && $talkIsRedirect ) {
407 $warning = $out->msg( 'delete_redirect_and_move_text_2',
409 $newTalkTitle->getPrefixedText()
410 );
411 $deleteAndMove = [ $newTitle, $newTalkTitle ];
412 // Case 3: The main page needs a full delete, the talk doesn't exist
413 // (or is a single-rev redirect to the source we can silently ignore)
414 } elseif ( $mainIsArticle && $talkOK ) {
415 $warning = $out->msg( 'delete_and_move_text', $newTitle->getPrefixedText() );
417 // Case 4: The main page needs a simple delete, the talk doesn't exist
418 } elseif ( $mainIsRedirect && $talkOK ) {
419 $warning = $out->msg( 'delete_redirect_and_move_text', $newTitle->getPrefixedText() );
421 // Cases 5 and 6: The same for the talk page
422 } elseif ( $talkIsArticle && $mainOK ) {
423 $warning = $out->msg( 'delete_and_move_text', $newTalkTitle->getPrefixedText() );
424 $deleteAndMove = [ $newTalkTitle ];
425 } elseif ( $talkIsRedirect && $mainOK ) {
426 $warning = $out->msg( 'delete_redirect_and_move_text', $newTalkTitle->getPrefixedText() );
427 $deleteAndMove = [ $newTalkTitle ];
428 }
429 if ( $warning ) {
430 $out->addHTML( Html::warningBox( $warning->parse() ) );
431 } else {
432 $messages = $status->getMessages();
433 if ( $messages ) {
434 if ( $status instanceof PermissionStatus ) {
435 $action_desc = $this->msg( 'action-move' )->plain();
436 $errMsgHtml = $this->msg( 'permissionserrorstext-withaction',
437 count( $messages ), $action_desc )->parseAsBlock();
438 } else {
439 $errMsgHtml = $this->msg( 'cannotmove', count( $messages ) )->parseAsBlock();
440 }
441
442 if ( count( $messages ) == 1 ) {
443 $errMsgHtml .= $this->msg( $messages[0] )->parseAsBlock();
444 } else {
445 $errStr = [];
446
447 foreach ( $messages as $msg ) {
448 $errStr[] = $this->msg( $msg )->parse();
449 }
450
451 $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n";
452 }
453 $out->addHTML( Html::errorBox( $errMsgHtml ) );
454 }
455 $talkMessages = $talkStatus->getMessages();
456 if ( $talkMessages ) {
457 // Can't use permissionerrorstext here since there's no specific action for moving the talk page
458 $errMsgHtml = $this->msg( 'cannotmovetalk', count( $talkMessages ) )->parseAsBlock();
459
460 if ( count( $talkMessages ) == 1 ) {
461 $errMsgHtml .= $this->msg( $talkMessages[0] )->parseAsBlock();
462 } else {
463 $errStr = [];
464
465 foreach ( $talkMessages as $msg ) {
466 $errStr[] = $this->msg( $msg )->parse();
467 }
468
469 $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n";
470 }
471 $errMsgHtml .= $this->msg( 'movetalk-unselect' )->parse();
472 $out->addHTML( Html::errorBox( $errMsgHtml ) );
473 }
474 }
475
476 if ( $this->restrictionStore->isProtected( $this->oldTitle, 'move' ) ) {
477 # Is the title semi-protected?
478 if ( $this->restrictionStore->isSemiProtected( $this->oldTitle, 'move' ) ) {
479 $noticeMsg = 'semiprotectedpagemovewarning';
480 } else {
481 # Then it must be protected based on static groups (regular)
482 $noticeMsg = 'protectedpagemovewarning';
483 }
484 LogEventsList::showLogExtract(
485 $out,
486 'protect',
487 $this->oldTitle,
488 '',
489 [ 'lim' => 1, 'msgKey' => $noticeMsg ]
490 );
491 }
492 // Intentionally don't check moveTalk here since this is in the form before you specify whether
493 // to move the talk page
494 if ( $talkOK && $oldTalk && $oldTalk->exists() && $this->restrictionStore->isProtected( $oldTalk, 'move' ) ) {
495 # Is the title semi-protected?
496 if ( $this->restrictionStore->isSemiProtected( $oldTalk, 'move' ) ) {
497 $noticeMsg = 'semiprotectedtalkpagemovewarning';
498 } else {
499 # Then it must be protected based on static groups (regular)
500 $noticeMsg = 'protectedtalkpagemovewarning';
501 }
502 LogEventsList::showLogExtract(
503 $out,
504 'protect',
505 $oldTalk,
506 '',
507 [ 'lim' => 1, 'msgKey' => $noticeMsg ]
508 );
509 }
510
511 // Length limit for wpReason and wpNewTitleMain is enforced in the
512 // mediawiki.special.movePage module
513
514 $immovableNamespaces = [];
515 foreach ( $this->getLanguage()->getNamespaces() as $nsId => $_ ) {
516 if ( !$this->nsInfo->isMovable( $nsId ) ) {
517 $immovableNamespaces[] = $nsId;
518 }
519 }
520
521 $moveOverProtection = false;
522 if ( $this->newTitle ) {
523 if ( $this->restrictionStore->isProtected( $this->newTitle, 'create' ) ) {
524 # Is the title semi-protected?
525 if ( $this->restrictionStore->isSemiProtected( $this->newTitle, 'create' ) ) {
526 $noticeMsg = 'semiprotectedpagemovecreatewarning';
527 } else {
528 # Then it must be protected based on static groups (regular)
529 $noticeMsg = 'protectedpagemovecreatewarning';
530 }
531 LogEventsList::showLogExtract(
532 $out,
533 'protect',
534 $this->newTitle,
535 '',
536 [ 'lim' => 1, 'msgKey' => $noticeMsg ]
537 );
538 $moveOverProtection = true;
539 }
540 $newTalk = $newTitle->getTalkPageIfDefined();
541 if ( $oldTalk && $oldTalk->exists() && $talkOK &&
542 $newTalk && $this->restrictionStore->isProtected( $newTalk, 'create' )
543 ) {
544 # Is the title semi-protected?
545 if ( $this->restrictionStore->isSemiProtected( $newTalk, 'create' ) ) {
546 $noticeMsg = 'semiprotectedpagemovetalkcreatewarning';
547 } else {
548 # Then it must be protected based on static groups (regular)
549 $noticeMsg = 'protectedpagemovetalkcreatewarning';
550 }
551 LogEventsList::showLogExtract(
552 $out,
553 'protect',
554 $newTalk,
555 '',
556 [ 'lim' => 1, 'msgKey' => $noticeMsg ]
557 );
558 $moveOverProtection = true;
559 }
560 }
561
562 $out->enableOOUI();
563 $fields = [];
564
565 $fields[] = new FieldLayout(
566 new ComplexTitleInputWidget( [
567 'id' => 'wpNewTitle',
568 'namespace' => [
569 'id' => 'wpNewTitleNs',
570 'name' => 'wpNewTitleNs',
571 'value' => $newTitle->getNamespace(),
572 'exclude' => $immovableNamespaces,
573 ],
574 'title' => [
575 'id' => 'wpNewTitleMain',
576 'name' => 'wpNewTitleMain',
577 'value' => $newTitle->getText(),
578 // Inappropriate, since we're expecting the user to input a non-existent page's title
579 'suggestions' => false,
580 ],
581 'infusable' => true,
582 ] ),
583 [
584 'label' => $this->msg( 'newtitle' )->text(),
585 'align' => 'top',
586 ]
587 );
588
589 $options = Html::listDropdownOptions(
590 $this->msg( 'movepage-reason-dropdown' )
591 ->page( $this->oldTitle )
592 ->inContentLanguage()
593 ->text(),
594 [ 'other' => $this->msg( 'movereasonotherlist' )->text() ]
595 );
596 $options = Html::listDropdownOptionsOoui( $options );
597
598 $fields[] = new FieldLayout(
599 new DropdownInputWidget( [
600 'name' => 'wpReasonList',
601 'inputId' => 'wpReasonList',
602 'infusable' => true,
603 'value' => $this->getRequest()->getText( 'wpReasonList', 'other' ),
604 'options' => $options,
605 ] ),
606 [
607 'label' => $this->msg( 'movereason' )->text(),
608 'align' => 'top',
609 ]
610 );
611
612 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
613 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
614 // Unicode codepoints.
615 $fields[] = new FieldLayout(
616 new TextInputWidget( [
617 'name' => 'wpReason',
618 'id' => 'wpReason',
619 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
620 'infusable' => true,
621 'value' => $this->getRequest()->getText( 'wpReason' ),
622 ] ),
623 [
624 'label' => $this->msg( 'moveotherreason' )->text(),
625 'align' => 'top',
626 ]
627 );
628
629 if ( $considerTalk ) {
630 $fields[] = new FieldLayout(
631 new CheckboxInputWidget( [
632 'name' => 'wpMovetalk',
633 'id' => 'wpMovetalk',
634 'value' => '1',
635 // It's intentional that this box is still visible and checked by default even if you don't have
636 // permission to move the talk page; wanting to separate a base page from its talk page is so
637 // unusual that you should have to explicitly uncheck the box to do so
638 'selected' => $this->moveTalk,
639 ] ),
640 [
641 'label' => $this->msg( 'movetalk' )->text(),
642 'help' => new HtmlSnippet( $this->msg( 'movepagetalktext' )->parseAsBlock() ),
643 'helpInline' => true,
644 'align' => 'inline',
645 'id' => 'wpMovetalk-field',
646 ]
647 );
648 }
649
650 if ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) {
651 if ( $createRedirect ) {
652 $isChecked = $this->leaveRedirect;
653 $isDisabled = false;
654 } else {
655 $isChecked = false;
656 $isDisabled = true;
657 }
658 $fields[] = new FieldLayout(
659 new CheckboxInputWidget( [
660 'name' => 'wpLeaveRedirect',
661 'id' => 'wpLeaveRedirect',
662 'value' => '1',
663 'selected' => $isChecked,
664 'disabled' => $isDisabled,
665 ] ),
666 [
667 'label' => $this->msg( 'move-leave-redirect' )->text(),
668 'align' => 'inline',
669 ]
670 );
671 }
672
673 if ( $hasRedirects ) {
674 $fields[] = new FieldLayout(
675 new CheckboxInputWidget( [
676 'name' => 'wpFixRedirects',
677 'id' => 'wpFixRedirects',
678 'value' => '1',
679 'selected' => $this->fixRedirects,
680 ] ),
681 [
682 'label' => $this->msg( 'fix-double-redirects' )->text(),
683 'align' => 'inline',
684 ]
685 );
686 }
687
688 if ( $canMoveSubpage ) {
689 $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
690 $fields[] = new FieldLayout(
691 new CheckboxInputWidget( [
692 'name' => 'wpMovesubpages',
693 'id' => 'wpMovesubpages',
694 'value' => '1',
695 'selected' => $this->moveSubpages,
696 ] ),
697 [
698 'label' => new HtmlSnippet(
699 $this->msg(
700 ( $this->oldTitle->hasSubpages()
701 ? 'move-subpages'
702 : 'move-talk-subpages' )
703 )->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse()
704 ),
705 'align' => 'inline',
706 ]
707 );
708 }
709
710 # Don't allow watching if user is not logged in
711 if ( $user->isRegistered() ) {
712 $watchChecked = ( $this->watch || $this->userOptionsLookup->getBoolOption( $user, 'watchmoves' )
713 || $this->watchlistManager->isWatched( $user, $this->oldTitle ) );
714 $fields[] = new FieldLayout(
715 new CheckboxInputWidget( [
716 'name' => 'wpWatch',
717 'id' => 'watch', # ew
718 'infusable' => true,
719 'value' => '1',
720 'selected' => $watchChecked,
721 ] ),
722 [
723 'label' => $this->msg( 'move-watch' )->text(),
724 'align' => 'inline',
725 ]
726 );
727
728 # Add a dropdown for watchlist expiry times in the form, T261230
729 if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) ) {
730 $expiryOptions = WatchAction::getExpiryOptions(
731 $this->getContext(),
732 $this->watchedItemStore->getWatchedItem( $user, $this->oldTitle )
733 );
734 # Reformat the options to match what DropdownInputWidget wants.
735 $options = [];
736 foreach ( $expiryOptions['options'] as $label => $value ) {
737 $options[] = [ 'data' => $value, 'label' => $label ];
738 }
739
740 $fields[] = new FieldLayout(
741 new DropdownInputWidget( [
742 'name' => 'wpWatchlistExpiry',
743 'id' => 'wpWatchlistExpiry',
744 'infusable' => true,
745 'options' => $options,
746 ] ),
747 [
748 'label' => $this->msg( 'confirm-watch-label' )->text(),
749 'id' => 'wpWatchlistExpiryLabel',
750 'infusable' => true,
751 'align' => 'inline',
752 ]
753 );
754 }
755 }
756
757 $hiddenFields = '';
758 if ( $moveOverShared ) {
759 $hiddenFields .= Html::hidden( 'wpMoveOverSharedFile', '1' );
760 }
761
762 if ( $deleteAndMove ) {
763 // Suppress Phan false positives here - the array is either one or two elements, and is assigned above
764 // so the count clearly distinguishes the two cases
765 if ( count( $deleteAndMove ) == 2 ) {
766 $msg = $this->msg( 'delete_and_move_confirm_2',
767 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
768 $deleteAndMove[0]->getPrefixedText(),
769 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
770 $deleteAndMove[1]->getPrefixedText()
771 )->text();
772 } else {
773 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
774 $msg = $this->msg( 'delete_and_move_confirm', $deleteAndMove[0]->getPrefixedText() )->text();
775 }
776 $fields[] = new FieldLayout(
777 new CheckboxInputWidget( [
778 'name' => 'wpDeleteAndMove',
779 'id' => 'wpDeleteAndMove',
780 'value' => '1',
781 ] ),
782 [
783 'label' => $msg,
784 'align' => 'inline',
785 ]
786 );
787 }
788 if ( $moveOverProtection ) {
789 $fields[] = new FieldLayout(
790 new CheckboxInputWidget( [
791 'name' => 'wpMoveOverProtection',
792 'id' => 'wpMoveOverProtection',
793 'value' => '1',
794 ] ),
795 [
796 'label' => $this->msg( 'move_over_protection_confirm' )->text(),
797 'align' => 'inline',
798 ]
799 );
800 }
801
802 $fields[] = new FieldLayout(
803 new ButtonInputWidget( [
804 'name' => 'wpMove',
805 'value' => $this->msg( 'movepagebtn' )->text(),
806 'label' => $this->msg( 'movepagebtn' )->text(),
807 'flags' => [ 'primary', 'progressive' ],
808 'type' => 'submit',
809 'accessKey' => Linker::accesskey( 'move' ),
810 'title' => Linker::titleAttrib( 'move' ),
811 ] ),
812 [
813 'align' => 'top',
814 ]
815 );
816
817 $fieldset = new FieldsetLayout( [
818 'label' => $this->msg( 'move-page-legend' )->text(),
819 'id' => 'mw-movepage-table',
820 'items' => $fields,
821 ] );
822
823 $form = new FormLayout( [
824 'method' => 'post',
825 'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ),
826 'id' => 'movepage',
827 ] );
828 $form->appendContent(
829 $fieldset,
830 new HtmlSnippet(
831 $hiddenFields .
832 Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) .
833 Html::hidden( 'wpEditToken', $user->getEditToken() )
834 )
835 );
836
837 $out->addHTML(
838 ( new PanelLayout( [
839 'classes' => [ 'movepage-wrapper' ],
840 'expanded' => false,
841 'padded' => true,
842 'framed' => true,
843 'content' => $form,
844 ] ) )->toString()
845 );
846 if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) {
847 $link = $this->getLinkRenderer()->makeKnownLink(
848 $this->msg( 'movepage-reason-dropdown' )->inContentLanguage()->getTitle(),
849 $this->msg( 'movepage-edit-reasonlist' )->text(),
850 [],
851 [ 'action' => 'edit' ]
852 );
853 $out->addHTML( Html::rawElement( 'p', [ 'class' => 'mw-movepage-editreasons' ], $link ) );
854 }
855
856 $this->showLogFragment( $this->oldTitle );
857 $this->showSubpages( $this->oldTitle );
858 }
859
860 private function vacateTitle( Title $title, User $user, Title $oldTitle ): StatusValue {
861 if ( !$title->exists() ) {
862 return StatusValue::newGood();
863 }
864 $redir2 = $title->isSingleRevRedirect();
865
866 $permStatus = $this->permManager->getPermissionStatus(
867 $redir2 ? 'delete-redirect' : 'delete',
868 $user, $title
869 );
870 if ( !$permStatus->isGood() ) {
871 if ( $redir2 ) {
872 if ( !$this->permManager->userCan( 'delete', $user, $title ) ) {
873 // Cannot delete-redirect, or delete normally
874 return $permStatus;
875 } else {
876 // Cannot delete-redirect, but can delete normally,
877 // so log as a normal deletion
878 $redir2 = false;
879 }
880 } else {
881 // Cannot delete normally
882 return $permStatus;
883 }
884 }
885
886 $page = $this->wikiPageFactory->newFromTitle( $title );
887 $delPage = $this->deletePageFactory->newDeletePage( $page, $user );
888
889 // Small safety margin to guard against concurrent edits
890 if ( $delPage->isBatchedDelete( 5 ) ) {
891 return StatusValue::newFatal( 'movepage-delete-first' );
892 }
893
894 $reason = $this->msg( 'delete_and_move_reason', $oldTitle->getPrefixedText() )->inContentLanguage()->text();
895
896 // Delete an associated image if there is
897 if ( $title->getNamespace() === NS_FILE ) {
898 $file = $this->repoGroup->getLocalRepo()->newFile( $title );
899 $file->load( IDBAccessObject::READ_LATEST );
900 if ( $file->exists() ) {
901 $file->deleteFile( $reason, $user, false );
902 }
903 }
904
905 $deletionLog = $redir2 ? 'delete_redir2' : 'delete';
906 $deleteStatus = $delPage
907 ->setLogSubtype( $deletionLog )
908 // Should be redundant thanks to the isBatchedDelete check above.
909 ->forceImmediate( true )
910 ->deleteUnsafe( $reason );
911
912 return $deleteStatus;
913 }
914
915 private function doSubmit() {
916 $user = $this->getUser();
917
918 if ( $user->pingLimiter( 'move' ) ) {
919 throw new ThrottledError;
920 }
921
922 $ot = $this->oldTitle;
923 $nt = $this->newTitle;
924
925 # don't allow moving to pages with # in
926 if ( !$nt || $nt->hasFragment() ) {
927 $this->showForm( StatusValue::newFatal( 'badtitletext' ) );
928
929 return;
930 }
931
932 if ( $ot->isTalkPage() || $nt->isTalkPage() ) {
933 $this->moveTalk = false;
934 }
935
936 $oldTalk = $ot->getTalkPageIfDefined();
937 $newTalk = $nt->getTalkPageIfDefined();
938
939 # Show a warning if the target file exists on a shared repo
940 if ( $nt->getNamespace() === NS_FILE
941 && !( $this->moveOverShared && $this->permManager->userHasRight( $user, 'reupload-shared' ) )
942 && !$this->repoGroup->getLocalRepo()->findFile( $nt )
943 && $this->repoGroup->findFile( $nt )
944 ) {
945 $this->showForm( StatusValue::newFatal( 'file-exists-sharedrepo' ) );
946
947 return;
948 }
949
950 # Show a warning if procted (showForm handles the warning )
951 if ( !$this->moveOverProtection ) {
952 if ( $this->restrictionStore->isProtected( $nt, 'create' ) ) {
953 $this->showForm();
954 return;
955 }
956 if ( $this->moveTalk && $this->restrictionStore->isProtected( $newTalk, 'create' ) ) {
957 $this->showForm();
958 return;
959 }
960 }
961
962 $handler = $this->contentHandlerFactory->getContentHandler( $ot->getContentModel() );
963
964 if ( !$handler->supportsRedirects() || (
965 // Do not create redirects for wikitext message overrides (T376399).
966 // Maybe one day they will have a custom content model and this special case won't be needed.
967 $ot->getNamespace() === NS_MEDIAWIKI &&
968 $ot->getContentModel() === CONTENT_MODEL_WIKITEXT
969 ) ) {
970 $createRedirect = false;
971 } elseif ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) {
972 $createRedirect = $this->leaveRedirect;
973 } else {
974 $createRedirect = true;
975 }
976
977 // Check perms
978 $mp = $this->movePageFactory->newMovePage( $ot, $nt );
979 $permStatusMain = $mp->authorizeMove( $this->getAuthority(), $this->reason );
980 $permStatusMain->merge( $mp->isValidMove() );
981
982 $onlyMovingTalkSubpages = false;
983 if ( $this->moveTalk ) {
984 $mpTalk = $this->movePageFactory->newMovePage( $oldTalk, $newTalk );
985 $permStatusTalk = $mpTalk->authorizeMove( $this->getAuthority(), $this->reason );
986 $permStatusTalk->merge( $mpTalk->isValidMove() );
987 // Per the definition of $considerTalk in showForm you might be trying to move
988 // subpages of the talk even if the talk itself doesn't exist, so let that happen
989 if ( !$permStatusTalk->isOK() &&
990 !$permStatusTalk->hasMessagesExcept( 'movepage-source-doesnt-exist' )
991 ) {
992 $permStatusTalk->setOK( true );
993 $onlyMovingTalkSubpages = true;
994 }
995 } else {
996 $permStatusTalk = StatusValue::newGood();
997 $mpTalk = null;
998 }
999
1000 if ( $this->deleteAndMove ) {
1001 // This is done before the deletion (in order to minimize the impact of T265792)
1002 // so ignore "it already exists" checks (they will be repeated after the deletion)
1003 if ( $permStatusMain->hasMessagesExcept( 'redirectexists', 'articleexists' ) ||
1004 $permStatusTalk->hasMessagesExcept( 'redirectexists', 'articleexists' ) ) {
1005 $this->showForm( $permStatusMain, $permStatusTalk );
1006 return;
1007 }
1008 // If the code gets here, then it's passed all permission checks and the move should succeed
1009 // so start deleting things.
1010 // FIXME: This isn't atomic; it could delete things even if the move will later fail (T265792)
1011 // For example, if you manually specify deleteAndMove in the URL (the form UI won't show the checkbox)
1012 // and have `delete-redirect` and the main page is a single-revision redirect
1013 // but the talk page isn't it will delete the redirect and then fail, leaving it deleted
1014 $deleteStatus = $this->vacateTitle( $nt, $user, $ot );
1015 if ( !$deleteStatus->isGood() ) {
1016 $this->showForm( $deleteStatus );
1017 return;
1018 }
1019 if ( $this->moveTalk ) {
1020 $deleteStatus = $this->vacateTitle( $newTalk, $user, $oldTalk );
1021 if ( !$deleteStatus->isGood() ) {
1022 // Ideally we would specify that the subject page redirect was deleted
1023 // but see the FIXME above
1024 $this->showForm( StatusValue::newGood(), $deleteStatus );
1025 return;
1026 }
1027 }
1028 } elseif ( !$permStatusMain->isOK() || !$permStatusTalk->isOK() ) {
1029 // If we're not going to delete then bail on all errors
1030 $this->showForm( $permStatusMain, $permStatusTalk );
1031 return;
1032 }
1033
1034 // Now we've confirmed you can do all of the moves you want and proceeding won't leave things inconsistent
1035 // so actually move the main page
1036 $mainStatus = $mp->moveIfAllowed( $this->getAuthority(), $this->reason, $createRedirect );
1037 if ( !$mainStatus->isOK() ) {
1038 $this->showForm( $mainStatus );
1039 return;
1040 }
1041
1042 $fixRedirects = $this->fixRedirects && $this->getConfig()->get( MainConfigNames::FixDoubleRedirects );
1043 if ( $fixRedirects ) {
1044 DoubleRedirectJob::fixRedirects( 'move', $ot );
1045 }
1046
1047 // Now try to move the talk page
1048 $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
1049
1050 $moveStatuses = [];
1051 $talkStatus = null;
1052 if ( $this->moveTalk && !$onlyMovingTalkSubpages ) {
1053 $talkStatus = $mpTalk->moveIfAllowed( $this->getAuthority(), $this->reason, $createRedirect );
1054 // moveIfAllowed returns a Status with an array as a value, however moveSubpages per-title statuses
1055 // have strings as values. Massage this status into the moveSubpages format so it fits in with
1056 // the later calls
1057 '@phan-var Status<string> $talkStatus';
1058 $talkStatus->value = $newTalk->getPrefixedText();
1059 $moveStatuses[$oldTalk->getPrefixedText()] = $talkStatus;
1060 }
1061
1062 // Now try to move subpages if asked
1063 if ( $this->moveSubpages ) {
1064 if ( $this->permManager->userCan( 'move-subpages', $user, $ot ) ) {
1065 $mp->setMaximumMovedPages( $maximumMovedPages - count( $moveStatuses ) );
1066 $subpageStatus = $mp->moveSubpagesIfAllowed( $this->getAuthority(), $this->reason, $createRedirect );
1067 $moveStatuses = array_merge( $moveStatuses, $subpageStatus->value );
1068 }
1069 if ( $this->moveTalk && $maximumMovedPages > count( $moveStatuses ) &&
1070 $this->permManager->userCan( 'move-subpages', $user, $oldTalk ) &&
1071 ( $onlyMovingTalkSubpages || $talkStatus->isOK() )
1072 ) {
1073 $mpTalk->setMaximumMovedPages( $maximumMovedPages - count( $moveStatuses ) );
1074 $talkSubStatus = $mpTalk->moveSubpagesIfAllowed(
1075 $this->getAuthority(), $this->reason, $createRedirect
1076 );
1077 $moveStatuses = array_merge( $moveStatuses, $talkSubStatus->value );
1078 }
1079 }
1080
1081 // Now we've moved everything we're going to move, so post-process the output,
1082 // create the UI, and fix double redirects
1083 $out = $this->getOutput();
1084 $out->setPageTitleMsg( $this->msg( 'pagemovedsub' ) );
1085
1086 $linkRenderer = $this->getLinkRenderer();
1087 $oldLink = $linkRenderer->makeLink(
1088 $ot,
1089 null,
1090 [ 'id' => 'movepage-oldlink' ],
1091 [ 'redirect' => 'no' ]
1092 );
1093 $newLink = $linkRenderer->makeKnownLink(
1094 $nt,
1095 null,
1096 [ 'id' => 'movepage-newlink' ]
1097 );
1098 $oldText = $ot->getPrefixedText();
1099 $newText = $nt->getPrefixedText();
1100
1101 $out->addHTML( $this->msg( 'movepage-moved' )->rawParams( $oldLink,
1102 $newLink )->params( $oldText, $newText )->parseAsBlock() );
1103 $out->addWikiMsg( isset( $mainStatus->getValue()['redirectRevision'] ) ?
1104 'movepage-moved-redirect' :
1105 'movepage-moved-noredirect' );
1106
1107 $this->getHookRunner()->onSpecialMovepageAfterMove( $this, $ot, $nt );
1108
1109 $extraOutput = [];
1110 foreach ( $moveStatuses as $oldSubpage => $subpageStatus ) {
1111 if ( $subpageStatus->hasMessage( 'movepage-max-pages' ) ) {
1112 $extraOutput[] = $this->msg( 'movepage-max-pages' )
1113 ->numParams( $maximumMovedPages )->escaped();
1114 continue;
1115 }
1116 $oldSubpage = Title::newFromText( $oldSubpage );
1117 $newSubpage = Title::newFromText( $subpageStatus->value );
1118 if ( $subpageStatus->isGood() ) {
1119 if ( $fixRedirects ) {
1120 DoubleRedirectJob::fixRedirects( 'move', $oldSubpage );
1121 }
1122 $oldLink = $linkRenderer->makeLink( $oldSubpage, null, [], [ 'redirect' => "no" ] );
1123 $newLink = $linkRenderer->makeKnownLink( $newSubpage );
1124
1125 $extraOutput[] = $this->msg( 'movepage-page-moved' )->rawParams(
1126 $oldLink, $newLink
1127 )->escaped();
1128 } elseif ( $subpageStatus->hasMessage( 'articleexists' )
1129 || $subpageStatus->hasMessage( 'redirectexists' )
1130 ) {
1131 $link = $linkRenderer->makeKnownLink( $newSubpage );
1132 $extraOutput[] = $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped();
1133 } else {
1134 $oldLink = $linkRenderer->makeKnownLink( $oldSubpage );
1135 if ( $newSubpage ) {
1136 $newLink = $linkRenderer->makeLink( $newSubpage );
1137 } else {
1138 // It's not a valid title
1139 $newLink = htmlspecialchars( $subpageStatus->value );
1140 }
1141 $extraOutput[] = $this->msg( 'movepage-page-unmoved' )
1142 ->rawParams( $oldLink, $newLink )->escaped();
1143 }
1144 }
1145
1146 if ( $extraOutput !== [] ) {
1147 $out->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
1148 }
1149
1150 # Deal with watches (we don't watch subpages)
1151 # Get text from expiry selection dropdown, T261230
1152 $expiry = $this->getRequest()->getText( 'wpWatchlistExpiry' );
1153 if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) && $expiry !== '' ) {
1154 $expiry = ExpiryDef::normalizeExpiry( $expiry, TS::ISO_8601 );
1155 } else {
1156 $expiry = null;
1157 }
1158
1159 $this->watchlistManager->setWatch(
1160 $this->watch,
1161 $this->getAuthority(),
1162 $ot,
1163 $expiry
1164 );
1165
1166 $this->watchlistManager->setWatch(
1167 $this->watch,
1168 $this->getAuthority(),
1169 $nt,
1170 $expiry
1171 );
1172 }
1173
1174 private function showLogFragment( Title $title ) {
1175 $moveLogPage = new LogPage( 'move' );
1176 $out = $this->getOutput();
1177 $out->addHTML( Html::element( 'h2', [], $moveLogPage->getName()->text() ) );
1178 LogEventsList::showLogExtract( $out, 'move', $title );
1179 }
1180
1187 private function showSubpages( $title ) {
1188 $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
1189 $nsHasSubpages = $this->nsInfo->hasSubpages( $title->getNamespace() );
1190 $subpages = $title->getSubpages( $maximumMovedPages + 1 );
1191 $count = $subpages instanceof TitleArrayFromResult ? $subpages->count() : 0;
1192
1193 $titleIsTalk = $title->isTalkPage();
1194 $subpagesTalk = $title->getTalkPage()->getSubpages( $maximumMovedPages + 1 );
1195 $countTalk = $subpagesTalk instanceof TitleArrayFromResult ? $subpagesTalk->count() : 0;
1196 $totalCount = $count + $countTalk;
1197
1198 if ( !$nsHasSubpages && $countTalk == 0 ) {
1199 return;
1200 }
1201
1202 $this->getOutput()->wrapWikiMsg(
1203 '== $1 ==',
1204 [ 'movesubpage', ( $titleIsTalk ? $count : $totalCount ) ]
1205 );
1206
1207 if ( $nsHasSubpages ) {
1208 $this->showSubpagesList(
1209 $subpages, $count, 'movesubpagetext', 'movesubpagetext-truncated', true
1210 );
1211 }
1212
1213 if ( !$titleIsTalk && $countTalk > 0 ) {
1214 $this->showSubpagesList(
1215 $subpagesTalk, $countTalk, 'movesubpagetalktext', 'movesubpagetalktext-truncated'
1216 );
1217 }
1218 }
1219
1220 private function showSubpagesList(
1221 TitleArrayFromResult $subpages, int $pagecount, string $msg, string $truncatedMsg, bool $noSubpageMsg = false
1222 ) {
1223 $out = $this->getOutput();
1224
1225 # No subpages.
1226 if ( $pagecount == 0 && $noSubpageMsg ) {
1227 $out->addWikiMsg( 'movenosubpage' );
1228 return;
1229 }
1230
1231 $maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
1232
1233 if ( $pagecount > $maximumMovedPages ) {
1234 $subpages = $this->truncateSubpagesList( $subpages );
1235 $out->addWikiMsg( $truncatedMsg, $this->getLanguage()->formatNum( $maximumMovedPages ) );
1236 } else {
1237 $out->addWikiMsg( $msg, $this->getLanguage()->formatNum( $pagecount ) );
1238 }
1239 $out->addHTML( "<ul>\n" );
1240
1241 $this->linkBatchFactory->newLinkBatch( $subpages )
1242 ->setCaller( __METHOD__ )
1243 ->execute();
1244 $linkRenderer = $this->getLinkRenderer();
1245
1246 foreach ( $subpages as $subpage ) {
1247 $link = $linkRenderer->makeLink( $subpage );
1248 $out->addHTML( "<li>$link</li>\n" );
1249 }
1250 $out->addHTML( "</ul>\n" );
1251 }
1252
1253 private function truncateSubpagesList( iterable $subpages ): array {
1254 $returnArray = [];
1255 foreach ( $subpages as $subpage ) {
1256 $returnArray[] = $subpage;
1257 if ( count( $returnArray ) >= $this->getConfig()->get( MainConfigNames::MaximumMovedPages ) ) {
1258 break;
1259 }
1260 }
1261 return $returnArray;
1262 }
1263
1272 public function prefixSearchSubpages( $search, $limit, $offset ) {
1273 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
1274 }
1275
1277 protected function getGroupName() {
1278 return 'pagetools';
1279 }
1280}
1281
1286class_alias( SpecialMovePage::class, 'MovePageForm' );
const NS_USER
Definition Defines.php:53
const NS_FILE
Definition Defines.php:57
const NS_MEDIAWIKI
Definition Defines.php:59
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:235
const NS_CATEGORY
Definition Defines.php:65
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
Page addition to a user's watchlist.
Handle database storage of comments such as edit summaries and log reasons.
An error page which can definitely be safely rendered using the OutputPage.
Show an error when a user tries to do something they do not have the necessary permissions for.
Show an error when the user hits a rate limit.
Prioritized list of file repositories.
Definition RepoGroup.php:30
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
Fix any double redirects after moving a page.
static fixRedirects( $reason, $redirTitle)
Insert jobs into the job queue to fix redirects to the given title.
Some internal bits split of from Skin.php.
Definition Linker.php:47
Class to simplify the use of log pages.
Definition LogPage.php:35
A class containing constants representing the names of configuration variables.
const FixDoubleRedirects
Name constant for the FixDoubleRedirects setting, for use with Config::get()
const WatchlistExpiry
Name constant for the WatchlistExpiry setting, for use with Config::get()
const MaximumMovedPages
Name constant for the MaximumMovedPages setting, for use with Config::get()
Factory for LinkBatch objects to batch query page metadata.
Service for creating WikiPage objects.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
A StatusValue for permission errors.
Factory class for SearchEngine.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getSkin()
Shortcut to get the skin being used for this instance.
getUser()
Shortcut to get the User executing this instance.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Shortcut to construct a special page which is unlisted by default.
Implement Special:Movepage for changing page titles.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
__construct(MovePageFactory $movePageFactory, PermissionManager $permManager, UserOptionsLookup $userOptionsLookup, IConnectionProvider $dbProvider, IContentHandlerFactory $contentHandlerFactory, NamespaceInfo $nsInfo, LinkBatchFactory $linkBatchFactory, RepoGroup $repoGroup, WikiPageFactory $wikiPageFactory, SearchEngineFactory $searchEngineFactory, WatchlistManager $watchlistManager, WatchedItemStore $watchedItemStore, RestrictionStore $restrictionStore, TitleFactory $titleFactory, DeletePageFactory $deletePageFactory)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
execute( $par)
Default execute method Checks user permissions.This must be overridden by subclasses; it will be made...
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki....
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Creates Title objects.
Represents a title within MediaWiki.
Definition Title.php:69
getTalkPageIfDefined()
Get a Title object associated with the talk page of this article, if such a talk page can exist.
Definition Title.php:1645
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1037
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1010
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1857
Provides access to user options.
User class for the MediaWiki software.
Definition User.php:130
Storage layer class for WatchedItems.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.
Type definition for expiry timestamps.
Definition ExpiryDef.php:18
Service for page delete actions.
Service for page rename actions.
Provide primary and replica IDatabase connections.
Interface for database access objects.
element(SerializerNode $parent, SerializerNode $node, $contents)
msg( $key,... $params)