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