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