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