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