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