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