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