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