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