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