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] ) ) {
209  if ( $err[0][0] == 'articleexists'
210  && $this->permManager->quickUserCan( 'delete', $user, $newTitle )
211  ) {
212  $out->wrapWikiMsg(
213  "<div class='warningbox'>\n$1\n</div>\n",
214  [ 'delete_and_move_text', $newTitle->getPrefixedText() ]
215  );
216  $deleteAndMove = true;
217  $err = [];
218  } elseif ( $err[0][0] == 'redirectexists' && (
219  // Any user that can delete normally can also delete a redirect here
220  $this->permManager->quickUserCan( 'delete-redirect', $user, $newTitle ) ||
221  $this->permManager->quickUserCan( 'delete', $user, $newTitle ) )
222  ) {
223  $out->wrapWikiMsg(
224  "<div class='warningbox'>\n$1\n</div>\n",
225  [ 'delete_redirect_and_move_text', $newTitle->getPrefixedText() ]
226  );
227  $deleteAndMove = true;
228  $err = [];
229  } elseif ( $err[0][0] == 'file-exists-sharedrepo'
230  && $this->permManager->userHasRight( $user, 'reupload-shared' )
231  ) {
232  $out->wrapWikiMsg(
233  "<div class='warningbox'>\n$1\n</div>\n",
234  [ 'move-over-sharedrepo', $newTitle->getPrefixedText() ]
235  );
236  $moveOverShared = true;
237  $err = [];
238  }
239  }
240 
241  $oldTalk = $this->oldTitle->getTalkPage();
242  $oldTitleSubpages = $this->oldTitle->hasSubpages();
243  $oldTitleTalkSubpages = $this->oldTitle->getTalkPage()->hasSubpages();
244 
245  $canMoveSubpage = ( $oldTitleSubpages || $oldTitleTalkSubpages ) &&
246  !count( $this->permManager->getPermissionErrors(
247  'move-subpages',
248  $user,
249  $this->oldTitle
250  ) );
251 
252  # We also want to be able to move assoc. subpage talk-pages even if base page
253  # has no associated talk page, so || with $oldTitleTalkSubpages.
254  $considerTalk = !$this->oldTitle->isTalkPage() &&
255  ( $oldTalk->exists()
256  || ( $oldTitleTalkSubpages && $canMoveSubpage ) );
257 
258  $dbr = wfGetDB( DB_REPLICA );
259  if ( $this->getConfig()->get( 'FixDoubleRedirects' ) ) {
260  $hasRedirects = $dbr->selectField( 'redirect', '1',
261  [
262  'rd_namespace' => $this->oldTitle->getNamespace(),
263  'rd_title' => $this->oldTitle->getDBkey(),
264  ], __METHOD__ );
265  } else {
266  $hasRedirects = false;
267  }
268 
269  if ( count( $err ) ) {
270  '@phan-var array[] $err';
271  if ( $isPermError ) {
272  $action_desc = $this->msg( 'action-move' )->plain();
273  $errMsgHtml = $this->msg( 'permissionserrorstext-withaction',
274  count( $err ), $action_desc )->parseAsBlock();
275  } else {
276  $errMsgHtml = $this->msg( 'cannotmove', count( $err ) )->parseAsBlock();
277  }
278 
279  if ( count( $err ) == 1 ) {
280  $errMsg = $err[0];
281  $errMsgName = array_shift( $errMsg );
282 
283  if ( $errMsgName == 'hookaborted' ) {
284  $errMsgHtml .= "<p>{$errMsg[0]}</p>\n";
285  } else {
286  $errMsgHtml .= $this->msg( $errMsgName, $errMsg )->parseAsBlock();
287  }
288  } else {
289  $errStr = [];
290 
291  foreach ( $err as $errMsg ) {
292  if ( $errMsg[0] == 'hookaborted' ) {
293  $errStr[] = $errMsg[1];
294  } else {
295  $errMsgName = array_shift( $errMsg );
296  $errStr[] = $this->msg( $errMsgName, $errMsg )->parse();
297  }
298  }
299 
300  $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n";
301  }
302  $out->addHTML( Html::errorBox( $errMsgHtml ) );
303  }
304 
305  if ( $this->oldTitle->isProtected( 'move' ) ) {
306  # Is the title semi-protected?
307  if ( $this->oldTitle->isSemiProtected( 'move' ) ) {
308  $noticeMsg = 'semiprotectedpagemovewarning';
309  } else {
310  # Then it must be protected based on static groups (regular)
311  $noticeMsg = 'protectedpagemovewarning';
312  }
313  $out->addHTML( "<div class='warningbox mw-warning-with-logexcerpt'>\n" );
314  $out->addWikiMsg( $noticeMsg );
316  $out,
317  'protect',
318  $this->oldTitle,
319  '',
320  [ 'lim' => 1 ]
321  );
322  $out->addHTML( "</div>\n" );
323  }
324 
325  // Length limit for wpReason and wpNewTitleMain is enforced in the
326  // mediawiki.special.movePage module
327 
328  $immovableNamespaces = [];
329  $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
330  foreach ( array_keys( $this->getLanguage()->getNamespaces() ) as $nsId ) {
331  if ( !$namespaceInfo->isMovable( $nsId ) ) {
332  $immovableNamespaces[] = $nsId;
333  }
334  }
335 
336  $out->enableOOUI();
337  $fields = [];
338 
339  $fields[] = new OOUI\FieldLayout(
340  new MediaWiki\Widget\ComplexTitleInputWidget( [
341  'id' => 'wpNewTitle',
342  'namespace' => [
343  'id' => 'wpNewTitleNs',
344  'name' => 'wpNewTitleNs',
345  'value' => $newTitle->getNamespace(),
346  'exclude' => $immovableNamespaces,
347  ],
348  'title' => [
349  'id' => 'wpNewTitleMain',
350  'name' => 'wpNewTitleMain',
351  'value' => $newTitle->getText(),
352  // Inappropriate, since we're expecting the user to input a non-existent page's title
353  'suggestions' => false,
354  ],
355  'infusable' => true,
356  ] ),
357  [
358  'label' => $this->msg( 'newtitle' )->text(),
359  'align' => 'top',
360  ]
361  );
362 
363  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
364  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
365  // Unicode codepoints.
366  $fields[] = new OOUI\FieldLayout(
367  new OOUI\TextInputWidget( [
368  'name' => 'wpReason',
369  'id' => 'wpReason',
371  'infusable' => true,
372  'value' => $this->reason,
373  ] ),
374  [
375  'label' => $this->msg( 'movereason' )->text(),
376  'align' => 'top',
377  ]
378  );
379 
380  if ( $considerTalk ) {
381  $fields[] = new OOUI\FieldLayout(
382  new OOUI\CheckboxInputWidget( [
383  'name' => 'wpMovetalk',
384  'id' => 'wpMovetalk',
385  'value' => '1',
386  'selected' => $this->moveTalk,
387  ] ),
388  [
389  'label' => $this->msg( 'movetalk' )->text(),
390  'help' => new OOUI\HtmlSnippet( $this->msg( 'movepagetalktext' )->parseAsBlock() ),
391  'helpInline' => true,
392  'align' => 'inline',
393  'id' => 'wpMovetalk-field',
394  ]
395  );
396  }
397 
398  if ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) {
399  if ( $handlerSupportsRedirects ) {
400  $isChecked = $this->leaveRedirect;
401  $isDisabled = false;
402  } else {
403  $isChecked = false;
404  $isDisabled = true;
405  }
406  $fields[] = new OOUI\FieldLayout(
407  new OOUI\CheckboxInputWidget( [
408  'name' => 'wpLeaveRedirect',
409  'id' => 'wpLeaveRedirect',
410  'value' => '1',
411  'selected' => $isChecked,
412  'disabled' => $isDisabled,
413  ] ),
414  [
415  'label' => $this->msg( 'move-leave-redirect' )->text(),
416  'align' => 'inline',
417  ]
418  );
419  }
420 
421  if ( $hasRedirects ) {
422  $fields[] = new OOUI\FieldLayout(
423  new OOUI\CheckboxInputWidget( [
424  'name' => 'wpFixRedirects',
425  'id' => 'wpFixRedirects',
426  'value' => '1',
427  'selected' => $this->fixRedirects,
428  ] ),
429  [
430  'label' => $this->msg( 'fix-double-redirects' )->text(),
431  'align' => 'inline',
432  ]
433  );
434  }
435 
436  if ( $canMoveSubpage ) {
437  $maximumMovedPages = $this->getConfig()->get( 'MaximumMovedPages' );
438  $fields[] = new OOUI\FieldLayout(
439  new OOUI\CheckboxInputWidget( [
440  'name' => 'wpMovesubpages',
441  'id' => 'wpMovesubpages',
442  'value' => '1',
443  'selected' => true, // T222953 Always check the box
444  ] ),
445  [
446  'label' => new OOUI\HtmlSnippet(
447  $this->msg(
448  ( $this->oldTitle->hasSubpages()
449  ? 'move-subpages'
450  : 'move-talk-subpages' )
451  )->numParams( $maximumMovedPages )->params( $maximumMovedPages )->parse()
452  ),
453  'align' => 'inline',
454  ]
455  );
456  }
457 
458  # Don't allow watching if user is not logged in
459  if ( $user->isLoggedIn() ) {
460  $watchChecked = $user->isLoggedIn() && ( $this->watch || $user->getBoolOption( 'watchmoves' )
461  || $user->isWatched( $this->oldTitle ) );
462  $fields[] = new OOUI\FieldLayout(
463  new OOUI\CheckboxInputWidget( [
464  'name' => 'wpWatch',
465  'id' => 'watch', # ew
466  'value' => '1',
467  'selected' => $watchChecked,
468  ] ),
469  [
470  'label' => $this->msg( 'move-watch' )->text(),
471  'align' => 'inline',
472  ]
473  );
474  }
475 
476  $hiddenFields = '';
477  if ( $moveOverShared ) {
478  $hiddenFields .= Html::hidden( 'wpMoveOverSharedFile', '1' );
479  }
480 
481  if ( $deleteAndMove ) {
482  $fields[] = new OOUI\FieldLayout(
483  new OOUI\CheckboxInputWidget( [
484  'name' => 'wpDeleteAndMove',
485  'id' => 'wpDeleteAndMove',
486  'value' => '1',
487  ] ),
488  [
489  'label' => $this->msg( 'delete_and_move_confirm' )->text(),
490  'align' => 'inline',
491  ]
492  );
493  }
494 
495  $fields[] = new OOUI\FieldLayout(
496  new OOUI\ButtonInputWidget( [
497  'name' => 'wpMove',
498  'value' => $this->msg( 'movepagebtn' )->text(),
499  'label' => $this->msg( 'movepagebtn' )->text(),
500  'flags' => [ 'primary', 'progressive' ],
501  'type' => 'submit',
502  ] ),
503  [
504  'align' => 'top',
505  ]
506  );
507 
508  $fieldset = new OOUI\FieldsetLayout( [
509  'label' => $this->msg( 'move-page-legend' )->text(),
510  'id' => 'mw-movepage-table',
511  'items' => $fields,
512  ] );
513 
514  $form = new OOUI\FormLayout( [
515  'method' => 'post',
516  'action' => $this->getPageTitle()->getLocalURL( 'action=submit' ),
517  'id' => 'movepage',
518  ] );
519  $form->appendContent(
520  $fieldset,
521  new OOUI\HtmlSnippet(
522  $hiddenFields .
523  Html::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) .
524  Html::hidden( 'wpEditToken', $user->getEditToken() )
525  )
526  );
527 
528  $out->addHTML(
529  new OOUI\PanelLayout( [
530  'classes' => [ 'movepage-wrapper' ],
531  'expanded' => false,
532  'padded' => true,
533  'framed' => true,
534  'content' => $form,
535  ] )
536  );
537 
538  $this->showLogFragment( $this->oldTitle );
539  $this->showSubpages( $this->oldTitle );
540  }
541 
542  private function doSubmit() {
543  $user = $this->getUser();
544 
545  if ( $user->pingLimiter( 'move' ) ) {
546  throw new ThrottledError;
547  }
548 
549  $ot = $this->oldTitle;
550  $nt = $this->newTitle;
551 
552  # don't allow moving to pages with # in
553  if ( !$nt || $nt->hasFragment() ) {
554  $this->showForm( [ [ 'badtitletext' ] ] );
555 
556  return;
557  }
558 
559  $services = MediaWikiServices::getInstance();
560 
561  # Show a warning if the target file exists on a shared repo
562  $repoGroup = $services->getRepoGroup();
563  if ( $nt->getNamespace() === NS_FILE
564  && !( $this->moveOverShared && $this->permManager->userHasRight( $user, 'reupload-shared' ) )
565  && !$repoGroup->getLocalRepo()->findFile( $nt )
566  && $repoGroup->findFile( $nt )
567  ) {
568  $this->showForm( [ [ 'file-exists-sharedrepo' ] ] );
569 
570  return;
571  }
572 
573  # Delete to make way if requested
574  if ( $this->deleteAndMove ) {
575  $redir2 = $nt->isSingleRevRedirect();
576 
577  $permErrors = $this->permManager->getPermissionErrors(
578  $redir2 ? 'delete-redirect' : 'delete',
579  $user, $nt
580  );
581  if ( count( $permErrors ) ) {
582  if ( $redir2 ) {
583  if ( count( $this->permManager->getPermissionErrors( 'delete', $user, $nt ) ) ) {
584  // Cannot delete-redirect, or delete normally
585  // Only show the first error
586  $this->showForm( $permErrors, true );
587  return;
588  } else {
589  // Cannot delete-redirect, but can delete normally,
590  // so log as a normal deletion
591  $redir2 = false;
592  }
593  } else {
594  // Cannot delete normally
595  // Only show first error
596  $this->showForm( $permErrors, true );
597  return;
598  }
599  }
600 
601  $page = WikiPage::factory( $nt );
602 
603  // Small safety margin to guard against concurrent edits
604  if ( $page->isBatchedDelete( 5 ) ) {
605  $this->showForm( [ [ 'movepage-delete-first' ] ] );
606 
607  return;
608  }
609 
610  $reason = $this->msg( 'delete_and_move_reason', $ot )->inContentLanguage()->text();
611 
612  // Delete an associated image if there is
613  if ( $nt->getNamespace() === NS_FILE ) {
614  $file = $repoGroup->getLocalRepo()->newFile( $nt );
615  $file->load( File::READ_LATEST );
616  if ( $file->exists() ) {
617  $file->deleteFile( $reason, $user, false );
618  }
619  }
620 
621  $error = ''; // passed by ref
622  $deletionLog = $redir2 ? 'delete_redir2' : 'delete';
623  $deleteStatus = $page->doDeleteArticleReal(
624  $reason, $user, false, null, $error,
625  null, [], $deletionLog
626  );
627  if ( !$deleteStatus->isGood() ) {
628  $this->showForm( $deleteStatus->getErrorsArray() );
629 
630  return;
631  }
632  }
633 
634  $handler = MediaWikiServices::getInstance()
635  ->getContentHandlerFactory()
636  ->getContentHandler( $ot->getContentModel() );
637 
638  if ( !$handler->supportsRedirects() ) {
639  $createRedirect = false;
640  } elseif ( $this->permManager->userHasRight( $user, 'suppressredirect' ) ) {
641  $createRedirect = $this->leaveRedirect;
642  } else {
643  $createRedirect = true;
644  }
645 
646  # Do the actual move.
647  $mp = new MovePage( $ot, $nt );
648 
649  # check whether the requested actions are permitted / possible
650  $userPermitted = $mp->checkPermissions( $user, $this->reason )->isOK();
651  if ( $ot->isTalkPage() || $nt->isTalkPage() ) {
652  $this->moveTalk = false;
653  }
654  if ( $this->moveSubpages ) {
655  $this->moveSubpages = $this->permManager->userCan( 'move-subpages', $user, $ot );
656  }
657 
658  $status = $mp->moveIfAllowed( $user, $this->reason, $createRedirect );
659  if ( !$status->isOK() ) {
660  $this->showForm( $status->getErrorsArray(), !$userPermitted );
661  return;
662  }
663 
664  if ( $this->getConfig()->get( 'FixDoubleRedirects' ) && $this->fixRedirects ) {
665  DoubleRedirectJob::fixRedirects( 'move', $ot, $nt );
666  }
667 
668  $out = $this->getOutput();
669  $out->setPageTitle( $this->msg( 'pagemovedsub' ) );
670 
671  $linkRenderer = $this->getLinkRenderer();
672  $oldLink = $linkRenderer->makeLink(
673  $ot,
674  null,
675  [ 'id' => 'movepage-oldlink' ],
676  [ 'redirect' => 'no' ]
677  );
678  $newLink = $linkRenderer->makeKnownLink(
679  $nt,
680  null,
681  [ 'id' => 'movepage-newlink' ]
682  );
683  $oldText = $ot->getPrefixedText();
684  $newText = $nt->getPrefixedText();
685 
686  if ( $ot->exists() ) {
687  // NOTE: we assume that if the old title exists, it's because it was re-created as
688  // a redirect to the new title. This is not safe, but what we did before was
689  // even worse: we just determined whether a redirect should have been created,
690  // and reported that it was created if it should have, without any checks.
691  // Also note that isRedirect() is unreliable because of T39209.
692  $msgName = 'movepage-moved-redirect';
693  } else {
694  $msgName = 'movepage-moved-noredirect';
695  }
696 
697  $out->addHTML( $this->msg( 'movepage-moved' )->rawParams( $oldLink,
698  $newLink )->params( $oldText, $newText )->parseAsBlock() );
699  $out->addWikiMsg( $msgName );
700 
701  $this->getHookRunner()->onSpecialMovepageAfterMove( $this, $ot, $nt );
702 
703  /*
704  * Now we move extra pages we've been asked to move: subpages and talk
705  * pages.
706  *
707  * First, make a list of id's. This might be marginally less efficient
708  * than a more direct method, but this is not a highly performance-cri-
709  * tical code path and readable code is more important here.
710  *
711  * If the target namespace doesn't allow subpages, moving with subpages
712  * would mean that you couldn't move them back in one operation, which
713  * is bad.
714  * @todo FIXME: A specific error message should be given in this case.
715  */
716 
717  // @todo FIXME: Use MovePage::moveSubpages() here
718  $nsInfo = $services->getNamespaceInfo();
719  $dbr = wfGetDB( DB_MASTER );
720  if ( $this->moveSubpages && (
721  $nsInfo->hasSubpages( $nt->getNamespace() ) || (
722  $this->moveTalk
723  && $nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() )
724  )
725  ) ) {
726  $conds = [
727  'page_title' . $dbr->buildLike( $ot->getDBkey() . '/', $dbr->anyString() )
728  . ' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
729  ];
730  $conds['page_namespace'] = [];
731  if ( $nsInfo->hasSubpages( $nt->getNamespace() ) ) {
732  $conds['page_namespace'][] = $ot->getNamespace();
733  }
734  if ( $this->moveTalk &&
735  $nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() )
736  ) {
737  $conds['page_namespace'][] = $ot->getTalkPage()->getNamespace();
738  }
739  } elseif ( $this->moveTalk ) {
740  $conds = [
741  'page_namespace' => $ot->getTalkPage()->getNamespace(),
742  'page_title' => $ot->getDBkey()
743  ];
744  } else {
745  # Skip the query
746  $conds = null;
747  }
748 
749  $extraPages = [];
750  if ( $conds !== null ) {
751  $extraPages = TitleArray::newFromResult(
752  $dbr->select( 'page',
753  [ 'page_id', 'page_namespace', 'page_title' ],
754  $conds,
755  __METHOD__
756  )
757  );
758  }
759 
760  $extraOutput = [];
761  $count = 1;
762  foreach ( $extraPages as $oldSubpage ) {
763  if ( $ot->equals( $oldSubpage ) || $nt->equals( $oldSubpage ) ) {
764  # Already did this one.
765  continue;
766  }
767 
768  $newPageName = preg_replace(
769  '#^' . preg_quote( $ot->getDBkey(), '#' ) . '#',
770  StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234
771  $oldSubpage->getDBkey()
772  );
773 
774  if ( $oldSubpage->isSubpage() && ( $ot->isTalkPage() xor $nt->isTalkPage() ) ) {
775  // Moving a subpage from a subject namespace to a talk namespace or vice-versa
776  $newNs = $nt->getNamespace();
777  } elseif ( $oldSubpage->isTalkPage() ) {
778  $newNs = $nt->getTalkPage()->getNamespace();
779  } else {
780  $newNs = $nt->getSubjectPage()->getNamespace();
781  }
782 
783  # T16385: we need makeTitleSafe because the new page names may
784  # be longer than 255 characters.
785  $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
786  if ( !$newSubpage ) {
787  $oldLink = $linkRenderer->makeKnownLink( $oldSubpage );
788  $extraOutput[] = $this->msg( 'movepage-page-unmoved' )->rawParams( $oldLink )
789  ->params( Title::makeName( $newNs, $newPageName ) )->escaped();
790  continue;
791  }
792 
793  $mp = new MovePage( $oldSubpage, $newSubpage );
794  # This was copy-pasted from Renameuser, bleh.
795  if ( $newSubpage->exists() && !$mp->isValidMove()->isOK() ) {
796  $link = $linkRenderer->makeKnownLink( $newSubpage );
797  $extraOutput[] = $this->msg( 'movepage-page-exists' )->rawParams( $link )->escaped();
798  } else {
799  $status = $mp->moveIfAllowed( $user, $this->reason, $createRedirect );
800 
801  if ( $status->isOK() ) {
802  if ( $this->fixRedirects ) {
803  DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage );
804  }
805  $oldLink = $linkRenderer->makeLink(
806  $oldSubpage,
807  null,
808  [],
809  [ 'redirect' => 'no' ]
810  );
811 
812  $newLink = $linkRenderer->makeKnownLink( $newSubpage );
813  $extraOutput[] = $this->msg( 'movepage-page-moved' )
814  ->rawParams( $oldLink, $newLink )->escaped();
815  ++$count;
816 
817  $maximumMovedPages = $this->getConfig()->get( 'MaximumMovedPages' );
818  if ( $count >= $maximumMovedPages ) {
819  $extraOutput[] = $this->msg( 'movepage-max-pages' )
820  ->numParams( $maximumMovedPages )->escaped();
821  break;
822  }
823  } else {
824  $oldLink = $linkRenderer->makeKnownLink( $oldSubpage );
825  $newLink = $linkRenderer->makeLink( $newSubpage );
826  $extraOutput[] = $this->msg( 'movepage-page-unmoved' )
827  ->rawParams( $oldLink, $newLink )->escaped();
828  }
829  }
830  }
831 
832  if ( $extraOutput !== [] ) {
833  $out->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
834  }
835 
836  # Deal with watches (we don't watch subpages)
837  WatchAction::doWatchOrUnwatch( $this->watch, $ot, $user );
838  WatchAction::doWatchOrUnwatch( $this->watch, $nt, $user );
839  }
840 
841  private function showLogFragment( $title ) {
842  $moveLogPage = new LogPage( 'move' );
843  $out = $this->getOutput();
844  $out->addHTML( Xml::element( 'h2', null, $moveLogPage->getName()->text() ) );
845  LogEventsList::showLogExtract( $out, 'move', $title );
846  }
847 
854  private function showSubpages( $title ) {
855  $nsHasSubpages = MediaWikiServices::getInstance()->getNamespaceInfo()->
856  hasSubpages( $title->getNamespace() );
857  $subpages = $title->getSubpages();
858  $count = $subpages instanceof TitleArray ? $subpages->count() : 0;
859 
860  $titleIsTalk = $title->isTalkPage();
861  $subpagesTalk = $title->getTalkPage()->getSubpages();
862  $countTalk = $subpagesTalk instanceof TitleArray ? $subpagesTalk->count() : 0;
863  $totalCount = $count + $countTalk;
864 
865  if ( !$nsHasSubpages && $countTalk == 0 ) {
866  return;
867  }
868 
869  $this->getOutput()->wrapWikiMsg(
870  '== $1 ==',
871  [ 'movesubpage', ( $titleIsTalk ? $count : $totalCount ) ]
872  );
873 
874  if ( $nsHasSubpages ) {
875  $this->showSubpagesList( $subpages, $count, 'movesubpagetext', true );
876  }
877 
878  if ( !$titleIsTalk && $countTalk > 0 ) {
879  $this->showSubpagesList( $subpagesTalk, $countTalk, 'movesubpagetalktext' );
880  }
881  }
882 
883  private function showSubpagesList( $subpages, $pagecount, $wikiMsg, $noSubpageMsg = false ) {
884  $out = $this->getOutput();
885 
886  # No subpages.
887  if ( $pagecount == 0 && $noSubpageMsg ) {
888  $out->addWikiMsg( 'movenosubpage' );
889  return;
890  }
891 
892  $out->addWikiMsg( $wikiMsg, $this->getLanguage()->formatNum( $pagecount ) );
893  $out->addHTML( "<ul>\n" );
894 
895  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
896  $linkBatch = $linkBatchFactory->newLinkBatch( $subpages );
897  $linkBatch->setCaller( __METHOD__ );
898  $linkBatch->execute();
899  $linkRenderer = $this->getLinkRenderer();
900 
901  foreach ( $subpages as $subpage ) {
902  $link = $linkRenderer->makeLink( $subpage );
903  $out->addHTML( "<li>$link</li>\n" );
904  }
905  $out->addHTML( "</ul>\n" );
906  }
907 
916  public function prefixSearchSubpages( $search, $limit, $offset ) {
917  return $this->prefixSearchString( $search, $limit, $offset );
918  }
919 
920  protected function getGroupName() {
921  return 'pagetools';
922  }
923 }
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:697
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:827
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:328
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:854
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:743
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:162
UnlistedSpecialPage
Shortcut to construct a special page which is unlisted by default.
Definition: UnlistedSpecialPage.php:31
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:920
Title\getPrefixedText
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1852
MovePageForm\prefixSearchSubpages
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
Definition: SpecialMovepage.php:916
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:763
SpecialPage\useTransactionalTimeLimit
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Definition: SpecialPage.php:934
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:31
SpecialPage\getLanguage
getLanguage()
Shortcut to get user's language.
Definition: SpecialPage.php:773
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:863
SpecialPage\prefixSearchString
prefixSearchString( $search, $limit, $offset)
Perform a regular substring search for prefixSearchSubpages.
Definition: SpecialPage.php:534
MovePageForm\doSubmit
doSubmit()
Definition: SpecialMovepage.php:542
SpecialPage\getHookRunner
getHookRunner()
Definition: SpecialPage.php:1010
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:793
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:159
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:2475
LogPage
Class to simplify the use of log pages.
Definition: LogPage.php:37
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:28
$title
$title
Definition: testCompression.php:38
SpecialPage\setHeaders
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!...
Definition: SpecialPage.php:571
SpecialPage\getUser
getUser()
Shortcut to get the User executing this instance.
Definition: SpecialPage.php:753
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:609
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:617
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:733
MovePageForm\doesWrites
doesWrites()
Indicates whether this special page may perform database writes.
Definition: SpecialMovepage.php:72
WatchAction\doWatchOrUnwatch
static doWatchOrUnwatch( $watch, Title $title, User $user, string $expiry=null)
Watch or unwatch a page.
Definition: WatchAction.php:208
MovePageForm\showSubpagesList
showSubpagesList( $subpages, $pagecount, $wikiMsg, $noSubpageMsg=false)
Definition: SpecialMovepage.php:883
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:48
SpecialPage\getLinkRenderer
getLinkRenderer()
Definition: SpecialPage.php:944
Title
Represents a title within MediaWiki.
Definition: Title.php:41
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:356
SpecialPage\$linkRenderer
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:71
ErrorPageError
An error page which can definitely be safely rendered using the OutputPage.
Definition: ErrorPageError.php:30
MovePageForm\showLogFragment
showLogFragment( $title)
Definition: SpecialMovepage.php:841
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:662
MovePageForm\execute
execute( $par)
Default execute method Checks user permissions.
Definition: SpecialMovepage.php:76