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