MediaWiki  master
MovePage.php
Go to the documentation of this file.
1 <?php
2 
43 
50 class MovePage {
51 
55  protected $oldTitle;
56 
60  protected $newTitle;
61 
65  protected $options;
66 
70  protected $loadBalancer;
71 
75  protected $nsInfo;
76 
80  protected $watchedItems;
81 
85  protected $repoGroup;
86 
91 
95  private $revisionStore;
96 
100  private $spamChecker;
101 
105  private $hookRunner;
106 
111 
115  private $userFactory;
116 
119 
122 
125 
129  public const CONSTRUCTOR_OPTIONS = [
130  'CategoryCollation',
131  'MaximumMovedPages',
132  ];
133 
136 
159  public function __construct(
162  ServiceOptions $options = null,
164  NamespaceInfo $nsInfo = null,
166  RepoGroup $repoGroup = null,
169  SpamChecker $spamChecker = null,
170  HookContainer $hookContainer = null,
172  UserFactory $userFactory = null,
177  ) {
178  if ( !$options ) {
180  __METHOD__ . ' without providing all services is deprecated',
181  '1.34'
182  );
183  }
184 
185  $this->oldTitle = $oldTitle;
186  $this->newTitle = $newTitle;
187 
188  $services = static function () {
189  // BC hack. Use a closure so this can be unit-tested.
190  return MediaWikiServices::getInstance();
191  };
192  $this->options = $options ??
193  new ServiceOptions(
194  self::CONSTRUCTOR_OPTIONS,
195  $services()->getMainConfig()
196  );
197  $this->loadBalancer = $loadBalancer ?? $services()->getDBLoadBalancer();
198  $this->nsInfo = $nsInfo ?? $services()->getNamespaceInfo();
199  $this->watchedItems = $watchedItems ?? $services()->getWatchedItemStore();
200  $this->repoGroup = $repoGroup ?? $services()->getRepoGroup();
201  $this->contentHandlerFactory =
202  $contentHandlerFactory ?? $services()->getContentHandlerFactory();
203 
204  $this->revisionStore = $revisionStore ?? $services()->getRevisionStore();
205  $this->spamChecker = $spamChecker ?? $services()->getSpamChecker();
206  $this->hookRunner = new HookRunner( $hookContainer ?? $services()->getHookContainer() );
207  $this->wikiPageFactory = $wikiPageFactory ?? $services()->getWikiPageFactory();
208  $this->userFactory = $userFactory ?? $services()->getUserFactory();
209  $this->userEditTracker = $userEditTracker ?? $services()->getUserEditTracker();
210  $this->movePageFactory = $movePageFactory ?? $services()->getMovePageFactory();
211  $this->collationFactory = $collationFactory ?? $services()->getCollationFactory();
212  $this->pageUpdaterFactory = $pageUpdaterFactory ?? $services()->getPageUpdaterFactory();
213  }
214 
221  private function authorizeInternal(
222  callable $authorizer,
223  Authority $performer,
224  ?string $reason
225  ): PermissionStatus {
226  $status = PermissionStatus::newEmpty();
227 
228  $authorizer( 'move', $this->oldTitle, $status );
229  $authorizer( 'edit', $this->oldTitle, $status );
230  $authorizer( 'move-target', $this->newTitle, $status );
231  $authorizer( 'edit', $this->newTitle, $status );
232 
233  if ( $reason !== null && $this->spamChecker->checkSummary( $reason ) !== false ) {
234  // This is kind of lame, won't display nice
235  $status->fatal( 'spamprotectiontext' );
236  }
237 
238  $tp = $this->newTitle->getTitleProtection();
239  if ( $tp !== false && !$performer->isAllowed( $tp['permission'] ) ) {
240  $status->fatal( 'cantmove-titleprotected' );
241  }
242 
243  // TODO: change hook signature to accept Authority and PermissionStatus
244  $user = $this->userFactory->newFromAuthority( $performer );
245  $status = Status::wrap( $status );
246  $this->hookRunner->onMovePageCheckPermissions(
247  $this->oldTitle, $this->newTitle, $user, $reason, $status );
248  // TODO: remove conversion code after hook signature is changed.
249  $permissionStatus = PermissionStatus::newEmpty();
250  foreach ( $status->getErrorsArray() as $error ) {
251  $permissionStatus->fatal( ...$error );
252  }
253  return $permissionStatus;
254  }
255 
267  public function probablyCanMove( Authority $performer, string $reason = null ): PermissionStatus {
268  return $this->authorizeInternal(
269  static function ( string $action, PageIdentity $target, PermissionStatus $status ) use ( $performer ) {
270  return $performer->probablyCan( $action, $target, $status );
271  },
272  $performer,
273  $reason
274  );
275  }
276 
288  public function authorizeMove( Authority $performer, string $reason = null ): PermissionStatus {
289  return $this->authorizeInternal(
290  static function ( string $action, PageIdentity $target, PermissionStatus $status ) use ( $performer ) {
291  return $performer->authorizeWrite( $action, $target, $status );
292  },
293  $performer,
294  $reason
295  );
296  }
297 
307  public function checkPermissions( Authority $performer, $reason ) {
308  $permissionStatus = $this->authorizeInternal(
309  static function ( string $action, PageIdentity $target, PermissionStatus $status ) use ( $performer ) {
310  return $performer->definitelyCan( $action, $target, $status );
311  },
312  $performer,
313  $reason
314  );
315  return Status::wrap( $permissionStatus );
316  }
317 
325  public function isValidMove() {
326  $status = new Status();
327 
328  if ( $this->oldTitle->equals( $this->newTitle ) ) {
329  $status->fatal( 'selfmove' );
330  } elseif ( $this->newTitle->getArticleID( Title::READ_LATEST /* T272386 */ )
331  && !$this->isValidMoveTarget()
332  ) {
333  // The move is allowed only if (1) the target doesn't exist, or (2) the target is a
334  // redirect to the source, and has no history (so we can undo bad moves right after
335  // they're done). If the target is a single revision redirect to a different page,
336  // it can be deleted with just `delete-redirect` rights (i.e. without needing
337  // `delete`) - see T239277
338  $fatal = $this->newTitle->isSingleRevRedirect() ? 'redirectexists' : 'articleexists';
339  $status->fatal( $fatal, $this->newTitle->getPrefixedText() );
340  }
341 
342  // @todo If the old title is invalid, maybe we should check if it somehow exists in the
343  // database and allow moving it to a valid name? Why prohibit the move from an empty name
344  // without checking in the database?
345  if ( $this->oldTitle->getDBkey() == '' ) {
346  $status->fatal( 'badarticleerror' );
347  } elseif ( $this->oldTitle->isExternal() ) {
348  $status->fatal( 'immobile-source-namespace-iw' );
349  } elseif ( !$this->oldTitle->isMovable() ) {
350  $nsText = $this->oldTitle->getNsText();
351  if ( $nsText === '' ) {
352  $nsText = wfMessage( 'blanknamespace' )->text();
353  }
354  $status->fatal( 'immobile-source-namespace', $nsText );
355  } elseif ( !$this->oldTitle->exists() ) {
356  $status->fatal( 'movepage-source-doesnt-exist' );
357  }
358 
359  if ( $this->newTitle->isExternal() ) {
360  $status->fatal( 'immobile-target-namespace-iw' );
361  } elseif ( !$this->newTitle->isMovable() ) {
362  $nsText = $this->newTitle->getNsText();
363  if ( $nsText === '' ) {
364  $nsText = wfMessage( 'blanknamespace' )->text();
365  }
366  $status->fatal( 'immobile-target-namespace', $nsText );
367  }
368  if ( !$this->newTitle->isValid() ) {
369  $status->fatal( 'movepage-invalid-target-title' );
370  }
371 
372  // Content model checks
373  if ( !$this->contentHandlerFactory
374  ->getContentHandler( $this->oldTitle->getContentModel() )
375  ->canBeUsedOn( $this->newTitle )
376  ) {
377  $status->fatal(
378  'content-not-allowed-here',
379  ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
380  $this->newTitle->getPrefixedText(),
381  SlotRecord::MAIN
382  );
383  }
384 
385  // Image-specific checks
386  if ( $this->oldTitle->inNamespace( NS_FILE ) ) {
387  $status->merge( $this->isValidFileMove() );
388  }
389 
390  if ( $this->newTitle->inNamespace( NS_FILE ) && !$this->oldTitle->inNamespace( NS_FILE ) ) {
391  $status->fatal( 'nonfile-cannot-move-to-file' );
392  }
393 
394  // Hook for extensions to say a title can't be moved for technical reasons
395  $this->hookRunner->onMovePageIsValidMove( $this->oldTitle, $this->newTitle, $status );
396 
397  return $status;
398  }
399 
405  protected function isValidFileMove() {
406  $status = new Status();
407 
408  if ( !$this->newTitle->inNamespace( NS_FILE ) ) {
409  // No need for further errors about the target filename being wrong
410  return $status->fatal( 'imagenocrossnamespace' );
411  }
412 
413  $file = $this->repoGroup->getLocalRepo()->newFile( $this->oldTitle );
414  $file->load( File::READ_LATEST );
415  if ( $file->exists() ) {
416  if ( $this->newTitle->getText() != wfStripIllegalFilenameChars( $this->newTitle->getText() ) ) {
417  $status->fatal( 'imageinvalidfilename' );
418  }
419  if ( !File::checkExtensionCompatibility( $file, $this->newTitle->getDBkey() ) ) {
420  $status->fatal( 'imagetypemismatch' );
421  }
422  }
423 
424  return $status;
425  }
426 
434  protected function isValidMoveTarget() {
435  # Is it an existing file?
436  if ( $this->newTitle->inNamespace( NS_FILE ) ) {
437  $file = $this->repoGroup->getLocalRepo()->newFile( $this->newTitle );
438  $file->load( File::READ_LATEST );
439  if ( $file->exists() ) {
440  wfDebug( __METHOD__ . ": file exists" );
441  return false;
442  }
443  }
444  # Is it a redirect with no history?
445  if ( !$this->newTitle->isSingleRevRedirect() ) {
446  wfDebug( __METHOD__ . ": not a one-rev redirect" );
447  return false;
448  }
449  # Get the article text
450  $rev = $this->revisionStore->getRevisionByTitle(
451  $this->newTitle,
452  0,
453  RevisionStore::READ_LATEST
454  );
455  if ( !is_object( $rev ) ) {
456  return false;
457  }
458  $content = $rev->getContent( SlotRecord::MAIN );
459  # Does the redirect point to the source?
460  # Or is it a broken self-redirect, usually caused by namespace collisions?
461  $redirTitle = $content ? $content->getRedirectTarget() : null;
462 
463  if ( $redirTitle ) {
464  if ( $redirTitle->getPrefixedDBkey() !== $this->oldTitle->getPrefixedDBkey() &&
465  $redirTitle->getPrefixedDBkey() !== $this->newTitle->getPrefixedDBkey() ) {
466  wfDebug( __METHOD__ . ": redirect points to other page" );
467  return false;
468  } else {
469  return true;
470  }
471  } else {
472  # Fail safe (not a redirect after all. strange.)
473  wfDebug( __METHOD__ . ": failsafe: database says " . $this->newTitle->getPrefixedDBkey() .
474  " is a redirect, but it doesn't contain a valid redirect." );
475  return false;
476  }
477  }
478 
490  public function move(
491  UserIdentity $user, $reason = null, $createRedirect = true, array $changeTags = []
492  ) {
493  $status = $this->isValidMove();
494  if ( !$status->isOK() ) {
495  return $status;
496  }
497 
498  return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
499  }
500 
510  public function moveIfAllowed(
511  Authority $performer, $reason = null, $createRedirect = true, array $changeTags = []
512  ) {
513  $status = $this->isValidMove();
514  $status->merge( $this->authorizeMove( $performer, $reason ) );
515  if ( $changeTags ) {
516  $status->merge( ChangeTags::canAddTagsAccompanyingChange( $changeTags, $performer ) );
517  }
518 
519  if ( !$status->isOK() ) {
520  // TODO: wrap block spreading into Authority side-effect?
521  $user = $this->userFactory->newFromAuthority( $performer );
522  // Auto-block user's IP if the account was "hard" blocked
523  $user->spreadAnyEditBlock();
524  return $status;
525  }
526 
527  // Check suppressredirect permission
528  if ( !$performer->isAllowed( 'suppressredirect' ) ) {
529  $createRedirect = true;
530  }
531 
532  return $this->moveUnsafe( $performer->getUser(), $reason, $createRedirect, $changeTags );
533  }
534 
549  public function moveSubpages(
550  UserIdentity $user, $reason = null, $createRedirect = true, array $changeTags = []
551  ) {
552  return $this->moveSubpagesInternal(
553  function ( Title $oldSubpage, Title $newSubpage )
554  use ( $user, $reason, $createRedirect, $changeTags ) {
555  $mp = $this->movePageFactory->newMovePage( $oldSubpage, $newSubpage );
556  return $mp->move( $user, $reason, $createRedirect, $changeTags );
557  }
558  );
559  }
560 
574  public function moveSubpagesIfAllowed(
575  Authority $performer, $reason = null, $createRedirect = true, array $changeTags = []
576  ) {
577  if ( !$performer->authorizeWrite( 'move-subpages', $this->oldTitle ) ) {
578  return Status::newFatal( 'cant-move-subpages' );
579  }
580  return $this->moveSubpagesInternal(
581  function ( Title $oldSubpage, Title $newSubpage )
582  use ( $performer, $reason, $createRedirect, $changeTags ) {
583  $mp = $this->movePageFactory->newMovePage( $oldSubpage, $newSubpage );
584  return $mp->moveIfAllowed( $performer, $reason, $createRedirect, $changeTags );
585  }
586  );
587  }
588 
594  private function moveSubpagesInternal( callable $subpageMoveCallback ) {
595  // Do the source and target namespaces support subpages?
596  if ( !$this->nsInfo->hasSubpages( $this->oldTitle->getNamespace() ) ) {
597  return Status::newFatal( 'namespace-nosubpages',
598  $this->nsInfo->getCanonicalName( $this->oldTitle->getNamespace() ) );
599  }
600  if ( !$this->nsInfo->hasSubpages( $this->newTitle->getNamespace() ) ) {
601  return Status::newFatal( 'namespace-nosubpages',
602  $this->nsInfo->getCanonicalName( $this->newTitle->getNamespace() ) );
603  }
604 
605  // Return a status for the overall result. Its value will be an array with per-title
606  // status for each subpage. Merge any errors from the per-title statuses into the
607  // top-level status without resetting the overall result.
608  $maximumMovedPages = $this->options->get( 'MaximumMovedPages' );
609  $topStatus = Status::newGood();
610  $perTitleStatus = [];
611  $subpages = $this->oldTitle->getSubpages( $maximumMovedPages + 1 );
612  $count = 0;
613  foreach ( $subpages as $oldSubpage ) {
614  $count++;
615  if ( $count > $maximumMovedPages ) {
616  $status = Status::newFatal( 'movepage-max-pages', $maximumMovedPages );
617  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
618  $topStatus->merge( $status );
619  $topStatus->setOK( true );
620  break;
621  }
622 
623  // We don't know whether this function was called before or after moving the root page,
624  // so check both titles
625  if ( $oldSubpage->getArticleID() == $this->oldTitle->getArticleID() ||
626  $oldSubpage->getArticleID() == $this->newTitle->getArticleID()
627  ) {
628  // When moving a page to a subpage of itself, don't move it twice
629  continue;
630  }
631  $newPageName = preg_replace(
632  '#^' . preg_quote( $this->oldTitle->getDBkey(), '#' ) . '#',
633  StringUtils::escapeRegexReplacement( $this->newTitle->getDBkey() ), # T23234
634  $oldSubpage->getDBkey() );
635  if ( $oldSubpage->isTalkPage() ) {
636  $newNs = $this->newTitle->getTalkPage()->getNamespace();
637  } else {
638  $newNs = $this->newTitle->getSubjectPage()->getNamespace();
639  }
640  // T16385: we need makeTitleSafe because the new page names may be longer than 255
641  // characters.
642  $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
643  $status = $subpageMoveCallback( $oldSubpage, $newSubpage );
644  if ( $status->isOK() ) {
645  $status->setResult( true, $newSubpage->getPrefixedText() );
646  }
647  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
648  $topStatus->merge( $status );
649  $topStatus->setOK( true );
650  }
651 
652  $topStatus->value = $perTitleStatus;
653  return $topStatus;
654  }
655 
665  private function moveUnsafe( UserIdentity $user, $reason, $createRedirect, array $changeTags ) {
666  $status = Status::newGood();
667 
668  // TODO: make hooks accept UserIdentity
669  $userObj = $this->userFactory->newFromUserIdentity( $user );
670  $this->hookRunner->onTitleMove( $this->oldTitle, $this->newTitle, $userObj, $reason, $status );
671  if ( !$status->isOK() ) {
672  // Move was aborted by the hook
673  return $status;
674  }
675 
676  $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
677  $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
678 
679  $this->hookRunner->onTitleMoveStarting( $this->oldTitle, $this->newTitle, $userObj );
680 
681  $pageid = $this->oldTitle->getArticleID( Title::READ_LATEST );
682  $protected = $this->oldTitle->isProtected();
683 
684  // Attempt the actual move
685  $moveAttemptResult = $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect,
686  $changeTags );
687 
688  if ( $moveAttemptResult instanceof Status ) {
689  // T265779: Attempt to delete target page failed
690  $dbw->cancelAtomic( __METHOD__ );
691  return $moveAttemptResult;
692  } else {
693  $nullRevision = $moveAttemptResult;
694  }
695 
696  $redirid = $this->oldTitle->getArticleID();
697 
698  if ( $protected ) {
699  # Protect the redirect title as the title used to be...
700  $res = $dbw->select(
701  'page_restrictions',
702  [ 'pr_type', 'pr_level', 'pr_cascade', 'pr_expiry' ],
703  [ 'pr_page' => $pageid ],
704  __METHOD__,
705  'FOR UPDATE'
706  );
707  $rowsInsert = [];
708  foreach ( $res as $row ) {
709  $rowsInsert[] = [
710  'pr_page' => $redirid,
711  'pr_type' => $row->pr_type,
712  'pr_level' => $row->pr_level,
713  'pr_cascade' => $row->pr_cascade,
714  'pr_expiry' => $row->pr_expiry
715  ];
716  }
717  $dbw->insert( 'page_restrictions', $rowsInsert, __METHOD__, [ 'IGNORE' ] );
718 
719  // Build comment for log
720  $comment = wfMessage(
721  'prot_1movedto2',
722  $this->oldTitle->getPrefixedText(),
723  $this->newTitle->getPrefixedText()
724  )->inContentLanguage()->text();
725  if ( $reason ) {
726  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
727  }
728 
729  // reread inserted pr_ids for log relation
730  $insertedPrIds = $dbw->select(
731  'page_restrictions',
732  'pr_id',
733  [ 'pr_page' => $redirid ],
734  __METHOD__
735  );
736  $logRelationsValues = [];
737  foreach ( $insertedPrIds as $prid ) {
738  $logRelationsValues[] = $prid->pr_id;
739  }
740 
741  // Update the protection log
742  $logEntry = new ManualLogEntry( 'protect', 'move_prot' );
743  $logEntry->setTarget( $this->newTitle );
744  $logEntry->setComment( $comment );
745  $logEntry->setPerformer( $user );
746  $logEntry->setParameters( [
747  '4::oldtitle' => $this->oldTitle->getPrefixedText(),
748  ] );
749  $logEntry->setRelations( [ 'pr_id' => $logRelationsValues ] );
750  $logEntry->addTags( $changeTags );
751  $logId = $logEntry->insert();
752  $logEntry->publish( $logId );
753  }
754 
755  # Update watchlists
756  $oldtitle = $this->oldTitle->getDBkey();
757  $newtitle = $this->newTitle->getDBkey();
758  $oldsnamespace = $this->nsInfo->getSubject( $this->oldTitle->getNamespace() );
759  $newsnamespace = $this->nsInfo->getSubject( $this->newTitle->getNamespace() );
760  if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
761  $this->watchedItems->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
762  }
763 
764  // If it is a file then move it last.
765  // This is done after all database changes so that file system errors cancel the transaction.
766  if ( $this->oldTitle->getNamespace() === NS_FILE ) {
767  $status = $this->moveFile( $this->oldTitle, $this->newTitle );
768  if ( !$status->isOK() ) {
769  $dbw->cancelAtomic( __METHOD__ );
770  return $status;
771  }
772  }
773 
774  $this->hookRunner->onPageMoveCompleting(
775  $this->oldTitle, $this->newTitle,
776  $user, $pageid, $redirid, $reason, $nullRevision
777  );
778 
779  $dbw->endAtomic( __METHOD__ );
780 
781  // Keep each single hook handler atomic
784  $dbw,
785  __METHOD__,
786  function () use ( $user, $pageid, $redirid, $reason, $nullRevision ) {
787  $this->hookRunner->onPageMoveComplete(
788  $this->oldTitle,
789  $this->newTitle,
790  $user,
791  $pageid,
792  $redirid,
793  $reason,
794  $nullRevision
795  );
796  }
797  )
798  );
799 
800  return Status::newGood();
801  }
802 
812  private function moveFile( $oldTitle, $newTitle ) {
813  $file = $this->repoGroup->getLocalRepo()->newFile( $oldTitle );
814  $file->load( File::READ_LATEST );
815  if ( $file->exists() ) {
816  $status = $file->move( $newTitle );
817  } else {
818  $status = Status::newGood();
819  }
820 
821  // Clear RepoGroup process cache
822  $this->repoGroup->clearCache( $oldTitle );
823  $this->repoGroup->clearCache( $newTitle ); # clear false negative cache
824  return $status;
825  }
826 
841  private function moveToInternal( UserIdentity $user, &$nt, $reason = '', $createRedirect = true,
842  array $changeTags = []
843  ) {
844  if ( $nt->getArticleId( Title::READ_LATEST ) ) {
845  $moveOverRedirect = true;
846  $logType = 'move_redir';
847  } else {
848  $moveOverRedirect = false;
849  $logType = 'move';
850  }
851 
852  if ( $moveOverRedirect ) {
853  $overwriteMessage = wfMessage(
854  'delete_and_move_reason',
855  $this->oldTitle->getPrefixedText()
856  )->inContentLanguage()->text();
857  $newpage = $this->wikiPageFactory->newFromTitle( $nt );
858  $errs = [];
859  $status = $newpage->doDeleteArticleReal(
860  $overwriteMessage,
861  $user,
862  /* $suppress */ false,
863  /* unused */ null,
864  $errs,
865  /* unused */ null,
866  $changeTags,
867  'delete_redir'
868  );
869 
870  if ( !$status->isGood() ) {
871  return $status;
872  }
873 
874  $nt->resetArticleID( false );
875  }
876 
877  if ( $createRedirect ) {
878  if ( $this->oldTitle->getNamespace() === NS_CATEGORY
879  && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
880  ) {
881  $redirectContent = new WikitextContent(
882  wfMessage( 'category-move-redirect-override' )
883  ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
884  } else {
885  $redirectContent = $this->contentHandlerFactory
886  ->getContentHandler( $this->oldTitle->getContentModel() )
887  ->makeRedirectContent(
888  $nt,
889  wfMessage( 'move-redirect-text' )->inContentLanguage()->plain()
890  );
891  }
892 
893  // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
894  } else {
895  $redirectContent = null;
896  }
897 
898  // T59084: log_page should be the ID of the *moved* page
899  $oldid = $this->oldTitle->getArticleID();
900  $logTitle = clone $this->oldTitle;
901 
902  $logEntry = new ManualLogEntry( 'move', $logType );
903  $logEntry->setPerformer( $user );
904  $logEntry->setTarget( $logTitle );
905  $logEntry->setComment( $reason );
906  $logEntry->setParameters( [
907  '4::target' => $nt->getPrefixedText(),
908  '5::noredir' => $redirectContent ? '0' : '1',
909  ] );
910 
911  $formatter = LogFormatter::newFromEntry( $logEntry );
912  $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
913  $comment = $formatter->getPlainActionText();
914  if ( $reason ) {
915  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
916  }
917 
918  $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
919 
920  $oldpage = $this->wikiPageFactory->newFromTitle( $this->oldTitle );
921  $oldcountable = $oldpage->isCountable();
922 
923  $newpage = $this->wikiPageFactory->newFromTitle( $nt );
924 
925  # Change the name of the target page:
926  $dbw->update( 'page',
927  /* SET */ [
928  'page_namespace' => $nt->getNamespace(),
929  'page_title' => $nt->getDBkey(),
930  ],
931  /* WHERE */ [ 'page_id' => $oldid ],
932  __METHOD__
933  );
934 
935  // Reset $nt before using it to create the null revision (T248789).
936  // But not $this->oldTitle yet, see below (T47348).
937  $nt->resetArticleID( $oldid );
938 
939  $commentObj = CommentStoreComment::newUnsavedComment( $comment );
940  # Save a null revision in the page's history notifying of the move
941  $nullRevision = $this->revisionStore->newNullRevision(
942  $dbw,
943  $nt,
944  $commentObj,
945  true,
946  $user
947  );
948  if ( $nullRevision === null ) {
949  $id = $nt->getArticleID( Title::READ_EXCLUSIVE );
950  $msg = 'Failed to create null revision while moving page ID ' .
951  $oldid . ' to ' . $nt->getPrefixedDBkey() . " (page ID $id)";
952 
953  throw new MWException( $msg );
954  }
955 
956  $nullRevision = $this->revisionStore->insertRevisionOn( $nullRevision, $dbw );
957  $logEntry->setAssociatedRevId( $nullRevision->getId() );
958 
964  $this->userEditTracker->incrementUserEditCount( $user );
965 
966  if ( !$redirectContent ) {
967  // Clean up the old title *before* reset article id - T47348
968  WikiPage::onArticleDelete( $this->oldTitle );
969  }
970 
971  $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
972  $newpage->loadPageData( WikiPage::READ_LOCKING ); // T48397
973 
974  $newpage->updateRevisionOn( $dbw, $nullRevision );
975 
976  $fakeTags = [];
977  $this->hookRunner->onRevisionFromEditComplete(
978  $newpage, $nullRevision, $nullRevision->getParentId(), $user, $fakeTags );
979 
980  $options = [
981  'changed' => false,
982  'moved' => true,
983  'oldtitle' => $this->oldTitle,
984  'oldcountable' => $oldcountable,
985  'causeAction' => 'edit-page',
986  'causeAgent' => $user->getName(),
987  ];
988 
989  $updater = $this->pageUpdaterFactory->newDerivedPageDataUpdater( $newpage );
990  $updater->prepareUpdate( $nullRevision, $options );
991  $updater->doUpdates();
992 
994 
995  # Recreate the redirect, this time in the other direction.
996  if ( $redirectContent ) {
997  $redirectArticle = $this->wikiPageFactory->newFromTitle( $this->oldTitle );
998  $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // T48397
999  $redirectArticle->newPageUpdater( $user )
1000  ->setContent( SlotRecord::MAIN, $redirectContent )
1001  ->addTags( $changeTags )
1002  ->addSoftwareTag( 'mw-new-redirect' )
1003  ->setUsePageCreationLog( false )
1004  ->setFlags( EDIT_SUPPRESS_RC )
1005  ->saveRevision( $commentObj );
1006  }
1007 
1008  # Log the move
1009  $logid = $logEntry->insert();
1010 
1011  $logEntry->addTags( $changeTags );
1012  $logEntry->publish( $logid );
1013 
1014  return $nullRevision;
1015  }
1016 }
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
MovePage\$pageUpdaterFactory
PageUpdaterFactory $pageUpdaterFactory
Definition: MovePage.php:135
MovePage\$collationFactory
CollationFactory $collationFactory
Definition: MovePage.php:124
MovePage\probablyCanMove
probablyCanMove(Authority $performer, string $reason=null)
Check whether $performer can execute the move.
Definition: MovePage.php:267
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
File\checkExtensionCompatibility
static checkExtensionCompatibility(File $old, $new)
Checks if file extensions are compatible.
Definition: File.php:280
WikiPage\onArticleCreate
static onArticleCreate(Title $title)
The onArticle*() functions are supposed to be a kind of hooks which should be called whenever any of ...
Definition: WikiPage.php:2844
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
MovePage\moveSubpages
moveSubpages(UserIdentity $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move the source page's subpages to be subpages of the target page, without checking user permissions.
Definition: MovePage.php:549
MovePage\$loadBalancer
ILoadBalancer $loadBalancer
Definition: MovePage.php:70
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
MovePage\isValidMoveTarget
isValidMoveTarget()
Checks if $this can be moved to a given Title.
Definition: MovePage.php:434
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:204
MovePage\$oldTitle
Title $oldTitle
Definition: MovePage.php:55
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:89
MovePage\$revisionStore
RevisionStore $revisionStore
Definition: MovePage.php:95
MovePage\authorizeMove
authorizeMove(Authority $performer, string $reason=null)
Authorize the move by $performer.
Definition: MovePage.php:288
MovePage\moveFile
moveFile( $oldTitle, $newTitle)
Move a file associated with a page to a new location.
Definition: MovePage.php:812
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
Definition: DeferredUpdates.php:125
MovePage\isValidMove
isValidMove()
Does various checks that the move is valid.
Definition: MovePage.php:325
StringUtils\escapeRegexReplacement
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter.
Definition: StringUtils.php:314
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
MovePage\$userEditTracker
UserEditTracker $userEditTracker
Definition: MovePage.php:118
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1167
RequestContext\newExtraneousContext
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
Definition: RequestContext.php:658
$res
$res
Definition: testCompression.php:57
MediaWiki\Permissions\Authority\probablyCan
probablyCan(string $action, PageIdentity $target, PermissionStatus $status=null)
Checks whether this authority can probably perform the given action on the given target page.
MediaWiki\Permissions\Authority\getUser
getUser()
Returns the performer of the actions associated with this authority.
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
MovePage\checkPermissions
checkPermissions(Authority $performer, $reason)
Check if the user is allowed to perform the move.
Definition: MovePage.php:307
MovePage\moveIfAllowed
moveIfAllowed(Authority $performer, $reason=null, $createRedirect=true, array $changeTags=[])
Same as move(), but with permissions checks.
Definition: MovePage.php:510
MovePage\__construct
__construct(Title $oldTitle, Title $newTitle, ServiceOptions $options=null, ILoadBalancer $loadBalancer=null, NamespaceInfo $nsInfo=null, WatchedItemStoreInterface $watchedItems=null, RepoGroup $repoGroup=null, IContentHandlerFactory $contentHandlerFactory=null, RevisionStore $revisionStore=null, SpamChecker $spamChecker=null, HookContainer $hookContainer=null, WikiPageFactory $wikiPageFactory=null, UserFactory $userFactory=null, UserEditTracker $userEditTracker=null, MovePageFactory $movePageFactory=null, CollationFactory $collationFactory=null, PageUpdaterFactory $pageUpdaterFactory=null)
Definition: MovePage.php:159
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
MovePage\move
move(UserIdentity $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move a page without taking user permissions into account.
Definition: MovePage.php:490
MovePage\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: MovePage.php:110
MovePage\$options
ServiceOptions $options
Definition: MovePage.php:65
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1028
MWException
MediaWiki exception.
Definition: MWException.php:29
wfStripIllegalFilenameChars
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
Definition: GlobalFunctions.php:2332
MovePage\$repoGroup
RepoGroup $repoGroup
Definition: MovePage.php:85
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:27
MovePage
Handles the backend logic of moving a page from one title to another.
Definition: MovePage.php:50
Status\wrap
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:62
MovePage\moveSubpagesIfAllowed
moveSubpagesIfAllowed(Authority $performer, $reason=null, $createRedirect=true, array $changeTags=[])
Move the source page's subpages to be subpages of the target page, with user permission checks.
Definition: MovePage.php:574
MovePage\$nsInfo
NamespaceInfo $nsInfo
Definition: MovePage.php:75
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:2880
MovePage\$hookRunner
HookRunner $hookRunner
Definition: MovePage.php:105
Page\WikiPageFactory
Definition: WikiPageFactory.php:19
MediaWiki\User\UserIdentity\getName
getName()
MovePage\moveToInternal
moveToInternal(UserIdentity $user, &$nt, $reason='', $createRedirect=true, array $changeTags=[])
Move page to a title which is either a redirect to the source page or nonexistent.
Definition: MovePage.php:841
MediaWiki\Permissions\Authority\authorizeWrite
authorizeWrite(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize write access.
WikitextContent
Content object for wiki text pages.
Definition: WikitextContent.php:36
MediaWiki\Collation\CollationFactory
Common factory to construct collation classes.
Definition: CollationFactory.php:36
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
MediaWiki\Permissions\Authority\definitelyCan
definitelyCan(string $action, PageIdentity $target, PermissionStatus $status=null)
Checks whether this authority can perform the given action on the given target page.
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
AtomicSectionUpdate
Deferrable Update for closure/callback updates via IDatabase::doAtomicSection()
Definition: AtomicSectionUpdate.php:9
ChangeTags\canAddTagsAccompanyingChange
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Definition: ChangeTags.php:630
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:674
$content
$content
Definition: router.php:76
ContentHandler\getLocalizedName
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
Definition: ContentHandler.php:310
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
DB_PRIMARY
const DB_PRIMARY
Definition: defines.php:27
MediaWiki\EditPage\SpamChecker
Service to check if text (either content or a summary) qualifies as spam.
Definition: SpamChecker.php:14
MovePage\$newTitle
Title $newTitle
Definition: MovePage.php:60
MovePage\moveUnsafe
moveUnsafe(UserIdentity $user, $reason, $createRedirect, array $changeTags)
Moves without any sort of safety or other checks.
Definition: MovePage.php:665
MovePage\$watchedItems
WatchedItemStoreInterface $watchedItems
Definition: MovePage.php:80
EDIT_SUPPRESS_RC
const EDIT_SUPPRESS_RC
Definition: Defines.php:128
MovePage\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: MovePage.php:90
MovePage\$movePageFactory
MovePageFactory $movePageFactory
Definition: MovePage.php:121
Page\MovePageFactory
Definition: MovePageFactory.php:30
MediaWiki\Permissions\PermissionStatus
A StatusValue for permission errors.
Definition: PermissionStatus.php:35
Title
Represents a title within MediaWiki.
Definition: Title.php:47
MediaWiki\Permissions\Authority\isAllowed
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
MediaWiki\User\UserEditTracker
Track info about user edit counts and timings.
Definition: UserEditTracker.php:21
MovePage\$userFactory
UserFactory $userFactory
Definition: MovePage.php:115
MovePage\moveSubpagesInternal
moveSubpagesInternal(callable $subpageMoveCallback)
Definition: MovePage.php:594
RepoGroup
Prioritized list of file repositories.
Definition: RepoGroup.php:32
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
MediaWiki\Storage\PageUpdaterFactory
A factory for PageUpdater instances.
Definition: PageUpdaterFactory.php:56
MovePage\isValidFileMove
isValidFileMove()
Checks for when a file is being moved.
Definition: MovePage.php:405
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:45
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:35
MovePage\$spamChecker
SpamChecker $spamChecker
Definition: MovePage.php:100
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:557
MovePage\authorizeInternal
authorizeInternal(callable $authorizer, Authority $performer, ?string $reason)
Definition: MovePage.php:221
NS_FILE
const NS_FILE
Definition: Defines.php:70
WatchedItemStoreInterface
Definition: WatchedItemStoreInterface.php:31
MovePage\CONSTRUCTOR_OPTIONS
const CONSTRUCTOR_OPTIONS
Definition: MovePage.php:129
MediaWiki\User\UserFactory
Creates User objects.
Definition: UserFactory.php:41
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
LogFormatter\newFromEntry
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
Definition: LogFormatter.php:55