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