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 
153  public function __construct(
156  ServiceOptions $options = null,
158  NamespaceInfo $nsInfo = null,
160  RepoGroup $repoGroup = null,
163  SpamChecker $spamChecker = null,
164  HookContainer $hookContainer = null,
166  UserFactory $userFactory = null,
170  ) {
171  if ( !$options ) {
173  __METHOD__ . ' without providing all services is deprecated',
174  '1.34'
175  );
176  }
177 
178  $this->oldTitle = $oldTitle;
179  $this->newTitle = $newTitle;
180 
181  $services = static function () {
182  // BC hack. Use a closure so this can be unit-tested.
183  return MediaWikiServices::getInstance();
184  };
185  $this->options = $options ??
186  new ServiceOptions(
187  self::CONSTRUCTOR_OPTIONS,
188  $services()->getMainConfig()
189  );
190  $this->loadBalancer = $loadBalancer ?? $services()->getDBLoadBalancer();
191  $this->nsInfo = $nsInfo ?? $services()->getNamespaceInfo();
192  $this->watchedItems = $watchedItems ?? $services()->getWatchedItemStore();
193  $this->repoGroup = $repoGroup ?? $services()->getRepoGroup();
194  $this->contentHandlerFactory =
195  $contentHandlerFactory ?? $services()->getContentHandlerFactory();
196 
197  $this->revisionStore = $revisionStore ?? $services()->getRevisionStore();
198  $this->spamChecker = $spamChecker ?? $services()->getSpamChecker();
199  $this->hookRunner = new HookRunner( $hookContainer ?? $services()->getHookContainer() );
200  $this->wikiPageFactory = $wikiPageFactory ?? $services()->getWikiPageFactory();
201  $this->userFactory = $userFactory ?? $services()->getUserFactory();
202  $this->userEditTracker = $userEditTracker ?? $services()->getUserEditTracker();
203  $this->movePageFactory = $movePageFactory ?? $services()->getMovePageFactory();
204  $this->collationFactory = $collationFactory ?? $services()->getCollationFactory();
205  }
206 
213  private function authorizeInternal(
214  callable $authorizer,
215  Authority $performer,
216  ?string $reason
217  ): PermissionStatus {
218  $status = PermissionStatus::newEmpty();
219 
220  $authorizer( 'move', $this->oldTitle, $status );
221  $authorizer( 'edit', $this->oldTitle, $status );
222  $authorizer( 'move-target', $this->newTitle, $status );
223  $authorizer( 'edit', $this->newTitle, $status );
224 
225  if ( $reason !== null && $this->spamChecker->checkSummary( $reason ) !== false ) {
226  // This is kind of lame, won't display nice
227  $status->fatal( 'spamprotectiontext' );
228  }
229 
230  $tp = $this->newTitle->getTitleProtection();
231  if ( $tp !== false && !$performer->isAllowed( $tp['permission'] ) ) {
232  $status->fatal( 'cantmove-titleprotected' );
233  }
234 
235  // TODO: change hook signature to accept Authority and PermissionStatus
236  $user = $this->userFactory->newFromAuthority( $performer );
237  $status = Status::wrap( $status );
238  $this->hookRunner->onMovePageCheckPermissions(
239  $this->oldTitle, $this->newTitle, $user, $reason, $status );
240  // TODO: remove conversion code after hook signature is changed.
241  $permissionStatus = PermissionStatus::newEmpty();
242  foreach ( $status->getErrorsArray() as $error ) {
243  $permissionStatus->fatal( ...$error );
244  }
245  return $permissionStatus;
246  }
247 
259  public function probablyCanMove( Authority $performer, string $reason = null ): PermissionStatus {
260  return $this->authorizeInternal(
261  static function ( string $action, PageIdentity $target, PermissionStatus $status ) use ( $performer ) {
262  return $performer->probablyCan( $action, $target, $status );
263  },
264  $performer,
265  $reason
266  );
267  }
268 
280  public function authorizeMove( Authority $performer, string $reason = null ): PermissionStatus {
281  return $this->authorizeInternal(
282  static function ( string $action, PageIdentity $target, PermissionStatus $status ) use ( $performer ) {
283  return $performer->authorizeWrite( $action, $target, $status );
284  },
285  $performer,
286  $reason
287  );
288  }
289 
299  public function checkPermissions( Authority $performer, $reason ) {
300  $permissionStatus = $this->authorizeInternal(
301  static function ( string $action, PageIdentity $target, PermissionStatus $status ) use ( $performer ) {
302  return $performer->definitelyCan( $action, $target, $status );
303  },
304  $performer,
305  $reason
306  );
307  return Status::wrap( $permissionStatus );
308  }
309 
317  public function isValidMove() {
318  $status = new Status();
319 
320  if ( $this->oldTitle->equals( $this->newTitle ) ) {
321  $status->fatal( 'selfmove' );
322  } elseif ( $this->newTitle->getArticleID( Title::READ_LATEST /* T272386 */ )
323  && !$this->isValidMoveTarget()
324  ) {
325  // The move is allowed only if (1) the target doesn't exist, or (2) the target is a
326  // redirect to the source, and has no history (so we can undo bad moves right after
327  // they're done). If the target is a single revision redirect to a different page,
328  // it can be deleted with just `delete-redirect` rights (i.e. without needing
329  // `delete`) - see T239277
330  $fatal = $this->newTitle->isSingleRevRedirect() ? 'redirectexists' : 'articleexists';
331  $status->fatal( $fatal, $this->newTitle->getPrefixedText() );
332  }
333 
334  // @todo If the old title is invalid, maybe we should check if it somehow exists in the
335  // database and allow moving it to a valid name? Why prohibit the move from an empty name
336  // without checking in the database?
337  if ( $this->oldTitle->getDBkey() == '' ) {
338  $status->fatal( 'badarticleerror' );
339  } elseif ( $this->oldTitle->isExternal() ) {
340  $status->fatal( 'immobile-source-namespace-iw' );
341  } elseif ( !$this->oldTitle->isMovable() ) {
342  $nsText = $this->oldTitle->getNsText();
343  if ( $nsText === '' ) {
344  $nsText = wfMessage( 'blanknamespace' )->text();
345  }
346  $status->fatal( 'immobile-source-namespace', $nsText );
347  } elseif ( !$this->oldTitle->exists() ) {
348  $status->fatal( 'movepage-source-doesnt-exist' );
349  }
350 
351  if ( $this->newTitle->isExternal() ) {
352  $status->fatal( 'immobile-target-namespace-iw' );
353  } elseif ( !$this->newTitle->isMovable() ) {
354  $nsText = $this->newTitle->getNsText();
355  if ( $nsText === '' ) {
356  $nsText = wfMessage( 'blanknamespace' )->text();
357  }
358  $status->fatal( 'immobile-target-namespace', $nsText );
359  }
360  if ( !$this->newTitle->isValid() ) {
361  $status->fatal( 'movepage-invalid-target-title' );
362  }
363 
364  // Content model checks
365  if ( !$this->contentHandlerFactory
366  ->getContentHandler( $this->oldTitle->getContentModel() )
367  ->canBeUsedOn( $this->newTitle )
368  ) {
369  $status->fatal(
370  'content-not-allowed-here',
371  ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
372  $this->newTitle->getPrefixedText(),
373  SlotRecord::MAIN
374  );
375  }
376 
377  // Image-specific checks
378  if ( $this->oldTitle->inNamespace( NS_FILE ) ) {
379  $status->merge( $this->isValidFileMove() );
380  }
381 
382  if ( $this->newTitle->inNamespace( NS_FILE ) && !$this->oldTitle->inNamespace( NS_FILE ) ) {
383  $status->fatal( 'nonfile-cannot-move-to-file' );
384  }
385 
386  // Hook for extensions to say a title can't be moved for technical reasons
387  $this->hookRunner->onMovePageIsValidMove( $this->oldTitle, $this->newTitle, $status );
388 
389  return $status;
390  }
391 
397  protected function isValidFileMove() {
398  $status = new Status();
399 
400  if ( !$this->newTitle->inNamespace( NS_FILE ) ) {
401  // No need for further errors about the target filename being wrong
402  return $status->fatal( 'imagenocrossnamespace' );
403  }
404 
405  $file = $this->repoGroup->getLocalRepo()->newFile( $this->oldTitle );
406  $file->load( File::READ_LATEST );
407  if ( $file->exists() ) {
408  if ( $this->newTitle->getText() != wfStripIllegalFilenameChars( $this->newTitle->getText() ) ) {
409  $status->fatal( 'imageinvalidfilename' );
410  }
411  if ( !File::checkExtensionCompatibility( $file, $this->newTitle->getDBkey() ) ) {
412  $status->fatal( 'imagetypemismatch' );
413  }
414  }
415 
416  return $status;
417  }
418 
426  protected function isValidMoveTarget() {
427  # Is it an existing file?
428  if ( $this->newTitle->inNamespace( NS_FILE ) ) {
429  $file = $this->repoGroup->getLocalRepo()->newFile( $this->newTitle );
430  $file->load( File::READ_LATEST );
431  if ( $file->exists() ) {
432  wfDebug( __METHOD__ . ": file exists" );
433  return false;
434  }
435  }
436  # Is it a redirect with no history?
437  if ( !$this->newTitle->isSingleRevRedirect() ) {
438  wfDebug( __METHOD__ . ": not a one-rev redirect" );
439  return false;
440  }
441  # Get the article text
442  $rev = $this->revisionStore->getRevisionByTitle(
443  $this->newTitle,
444  0,
445  RevisionStore::READ_LATEST
446  );
447  if ( !is_object( $rev ) ) {
448  return false;
449  }
450  $content = $rev->getContent( SlotRecord::MAIN );
451  # Does the redirect point to the source?
452  # Or is it a broken self-redirect, usually caused by namespace collisions?
453  $redirTitle = $content ? $content->getRedirectTarget() : null;
454 
455  if ( $redirTitle ) {
456  if ( $redirTitle->getPrefixedDBkey() !== $this->oldTitle->getPrefixedDBkey() &&
457  $redirTitle->getPrefixedDBkey() !== $this->newTitle->getPrefixedDBkey() ) {
458  wfDebug( __METHOD__ . ": redirect points to other page" );
459  return false;
460  } else {
461  return true;
462  }
463  } else {
464  # Fail safe (not a redirect after all. strange.)
465  wfDebug( __METHOD__ . ": failsafe: database says " . $this->newTitle->getPrefixedDBkey() .
466  " is a redirect, but it doesn't contain a valid redirect." );
467  return false;
468  }
469  }
470 
482  public function move(
483  UserIdentity $user, $reason = null, $createRedirect = true, array $changeTags = []
484  ) {
485  $status = $this->isValidMove();
486  if ( !$status->isOK() ) {
487  return $status;
488  }
489 
490  return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
491  }
492 
502  public function moveIfAllowed(
503  Authority $performer, $reason = null, $createRedirect = true, array $changeTags = []
504  ) {
505  $status = $this->isValidMove();
506  $status->merge( $this->authorizeMove( $performer, $reason ) );
507  if ( $changeTags ) {
508  $status->merge( ChangeTags::canAddTagsAccompanyingChange( $changeTags, $performer ) );
509  }
510 
511  if ( !$status->isOK() ) {
512  // TODO: wrap block spreading into Authority side-effect?
513  $user = $this->userFactory->newFromAuthority( $performer );
514  // Auto-block user's IP if the account was "hard" blocked
515  $user->spreadAnyEditBlock();
516  return $status;
517  }
518 
519  // Check suppressredirect permission
520  if ( !$performer->isAllowed( 'suppressredirect' ) ) {
521  $createRedirect = true;
522  }
523 
524  return $this->moveUnsafe( $performer->getUser(), $reason, $createRedirect, $changeTags );
525  }
526 
541  public function moveSubpages(
542  UserIdentity $user, $reason = null, $createRedirect = true, array $changeTags = []
543  ) {
544  return $this->moveSubpagesInternal(
545  function ( Title $oldSubpage, Title $newSubpage )
546  use ( $user, $reason, $createRedirect, $changeTags ) {
547  $mp = $this->movePageFactory->newMovePage( $oldSubpage, $newSubpage );
548  return $mp->move( $user, $reason, $createRedirect, $changeTags );
549  }
550  );
551  }
552 
566  public function moveSubpagesIfAllowed(
567  Authority $performer, $reason = null, $createRedirect = true, array $changeTags = []
568  ) {
569  if ( !$performer->authorizeWrite( 'move-subpages', $this->oldTitle ) ) {
570  return Status::newFatal( 'cant-move-subpages' );
571  }
572  return $this->moveSubpagesInternal(
573  function ( Title $oldSubpage, Title $newSubpage )
574  use ( $performer, $reason, $createRedirect, $changeTags ) {
575  $mp = $this->movePageFactory->newMovePage( $oldSubpage, $newSubpage );
576  return $mp->moveIfAllowed( $performer, $reason, $createRedirect, $changeTags );
577  }
578  );
579  }
580 
586  private function moveSubpagesInternal( callable $subpageMoveCallback ) {
587  // Do the source and target namespaces support subpages?
588  if ( !$this->nsInfo->hasSubpages( $this->oldTitle->getNamespace() ) ) {
589  return Status::newFatal( 'namespace-nosubpages',
590  $this->nsInfo->getCanonicalName( $this->oldTitle->getNamespace() ) );
591  }
592  if ( !$this->nsInfo->hasSubpages( $this->newTitle->getNamespace() ) ) {
593  return Status::newFatal( 'namespace-nosubpages',
594  $this->nsInfo->getCanonicalName( $this->newTitle->getNamespace() ) );
595  }
596 
597  // Return a status for the overall result. Its value will be an array with per-title
598  // status for each subpage. Merge any errors from the per-title statuses into the
599  // top-level status without resetting the overall result.
600  $maximumMovedPages = $this->options->get( 'MaximumMovedPages' );
601  $topStatus = Status::newGood();
602  $perTitleStatus = [];
603  $subpages = $this->oldTitle->getSubpages( $maximumMovedPages + 1 );
604  $count = 0;
605  foreach ( $subpages as $oldSubpage ) {
606  $count++;
607  if ( $count > $maximumMovedPages ) {
608  $status = Status::newFatal( 'movepage-max-pages', $maximumMovedPages );
609  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
610  $topStatus->merge( $status );
611  $topStatus->setOK( true );
612  break;
613  }
614 
615  // We don't know whether this function was called before or after moving the root page,
616  // so check both titles
617  if ( $oldSubpage->getArticleID() == $this->oldTitle->getArticleID() ||
618  $oldSubpage->getArticleID() == $this->newTitle->getArticleID()
619  ) {
620  // When moving a page to a subpage of itself, don't move it twice
621  continue;
622  }
623  $newPageName = preg_replace(
624  '#^' . preg_quote( $this->oldTitle->getDBkey(), '#' ) . '#',
625  StringUtils::escapeRegexReplacement( $this->newTitle->getDBkey() ), # T23234
626  $oldSubpage->getDBkey() );
627  if ( $oldSubpage->isTalkPage() ) {
628  $newNs = $this->newTitle->getTalkPage()->getNamespace();
629  } else {
630  $newNs = $this->newTitle->getSubjectPage()->getNamespace();
631  }
632  // T16385: we need makeTitleSafe because the new page names may be longer than 255
633  // characters.
634  $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
635  $status = $subpageMoveCallback( $oldSubpage, $newSubpage );
636  if ( $status->isOK() ) {
637  $status->setResult( true, $newSubpage->getPrefixedText() );
638  }
639  $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
640  $topStatus->merge( $status );
641  $topStatus->setOK( true );
642  }
643 
644  $topStatus->value = $perTitleStatus;
645  return $topStatus;
646  }
647 
657  private function moveUnsafe( UserIdentity $user, $reason, $createRedirect, array $changeTags ) {
658  $status = Status::newGood();
659 
660  // TODO: make hooks accept UserIdentity
661  $userObj = $this->userFactory->newFromUserIdentity( $user );
662  $this->hookRunner->onTitleMove( $this->oldTitle, $this->newTitle, $userObj, $reason, $status );
663  if ( !$status->isOK() ) {
664  // Move was aborted by the hook
665  return $status;
666  }
667 
668  $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
669  $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
670 
671  $this->hookRunner->onTitleMoveStarting( $this->oldTitle, $this->newTitle, $userObj );
672 
673  $pageid = $this->oldTitle->getArticleID( Title::READ_LATEST );
674  $protected = $this->oldTitle->isProtected();
675 
676  // Attempt the actual move
677  $moveAttemptResult = $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect,
678  $changeTags );
679 
680  if ( $moveAttemptResult instanceof Status ) {
681  // T265779: Attempt to delete target page failed
682  $dbw->cancelAtomic( __METHOD__ );
683  return $moveAttemptResult;
684  } else {
685  $nullRevision = $moveAttemptResult;
686  }
687 
688  // Refresh the sortkey for this row. Be careful to avoid resetting
689  // cl_timestamp, which may disturb time-based lists on some sites.
690  // @todo This block should be killed, it's duplicating code
691  // from LinksUpdate::getCategoryInsertions() and friends.
692  $prefixes = $dbw->select(
693  'categorylinks',
694  [ 'cl_sortkey_prefix', 'cl_to' ],
695  [ 'cl_from' => $pageid ],
696  __METHOD__
697  );
698  $type = $this->nsInfo->getCategoryLinkType( $this->newTitle->getNamespace() );
699  $collation = $this->collationFactory->getCategoryCollation();
700  foreach ( $prefixes as $prefixRow ) {
701  $prefix = $prefixRow->cl_sortkey_prefix;
702  $catTo = $prefixRow->cl_to;
703  $dbw->update( 'categorylinks',
704  [
705  'cl_sortkey' => $collation->getSortKey(
706  $this->newTitle->getCategorySortkey( $prefix ) ),
707  'cl_collation' => $this->options->get( 'CategoryCollation' ),
708  'cl_type' => $type,
709  'cl_timestamp=cl_timestamp' ],
710  [
711  'cl_from' => $pageid,
712  'cl_to' => $catTo ],
713  __METHOD__
714  );
715  }
716 
717  $redirid = $this->oldTitle->getArticleID();
718 
719  if ( $protected ) {
720  # Protect the redirect title as the title used to be...
721  $res = $dbw->select(
722  'page_restrictions',
723  [ 'pr_type', 'pr_level', 'pr_cascade', 'pr_user', 'pr_expiry' ],
724  [ 'pr_page' => $pageid ],
725  __METHOD__,
726  'FOR UPDATE'
727  );
728  $rowsInsert = [];
729  foreach ( $res as $row ) {
730  $rowsInsert[] = [
731  'pr_page' => $redirid,
732  'pr_type' => $row->pr_type,
733  'pr_level' => $row->pr_level,
734  'pr_cascade' => $row->pr_cascade,
735  'pr_user' => $row->pr_user,
736  'pr_expiry' => $row->pr_expiry
737  ];
738  }
739  $dbw->insert( 'page_restrictions', $rowsInsert, __METHOD__, [ 'IGNORE' ] );
740 
741  // Build comment for log
742  $comment = wfMessage(
743  'prot_1movedto2',
744  $this->oldTitle->getPrefixedText(),
745  $this->newTitle->getPrefixedText()
746  )->inContentLanguage()->text();
747  if ( $reason ) {
748  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
749  }
750 
751  // reread inserted pr_ids for log relation
752  $insertedPrIds = $dbw->select(
753  'page_restrictions',
754  'pr_id',
755  [ 'pr_page' => $redirid ],
756  __METHOD__
757  );
758  $logRelationsValues = [];
759  foreach ( $insertedPrIds as $prid ) {
760  $logRelationsValues[] = $prid->pr_id;
761  }
762 
763  // Update the protection log
764  $logEntry = new ManualLogEntry( 'protect', 'move_prot' );
765  $logEntry->setTarget( $this->newTitle );
766  $logEntry->setComment( $comment );
767  $logEntry->setPerformer( $user );
768  $logEntry->setParameters( [
769  '4::oldtitle' => $this->oldTitle->getPrefixedText(),
770  ] );
771  $logEntry->setRelations( [ 'pr_id' => $logRelationsValues ] );
772  $logEntry->addTags( $changeTags );
773  $logId = $logEntry->insert();
774  $logEntry->publish( $logId );
775  }
776 
777  // Update *_from_namespace fields as needed
778  if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) {
779  $dbw->update( 'pagelinks',
780  [ 'pl_from_namespace' => $this->newTitle->getNamespace() ],
781  [ 'pl_from' => $pageid ],
782  __METHOD__
783  );
784  $dbw->update( 'templatelinks',
785  [ 'tl_from_namespace' => $this->newTitle->getNamespace() ],
786  [ 'tl_from' => $pageid ],
787  __METHOD__
788  );
789  $dbw->update( 'imagelinks',
790  [ 'il_from_namespace' => $this->newTitle->getNamespace() ],
791  [ 'il_from' => $pageid ],
792  __METHOD__
793  );
794  }
795 
796  # Update watchlists
797  $oldtitle = $this->oldTitle->getDBkey();
798  $newtitle = $this->newTitle->getDBkey();
799  $oldsnamespace = $this->nsInfo->getSubject( $this->oldTitle->getNamespace() );
800  $newsnamespace = $this->nsInfo->getSubject( $this->newTitle->getNamespace() );
801  if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
802  $this->watchedItems->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
803  }
804 
805  // If it is a file then move it last.
806  // This is done after all database changes so that file system errors cancel the transaction.
807  if ( $this->oldTitle->getNamespace() === NS_FILE ) {
808  $status = $this->moveFile( $this->oldTitle, $this->newTitle );
809  if ( !$status->isOK() ) {
810  $dbw->cancelAtomic( __METHOD__ );
811  return $status;
812  }
813  }
814 
815  $this->hookRunner->onPageMoveCompleting(
816  $this->oldTitle, $this->newTitle,
817  $user, $pageid, $redirid, $reason, $nullRevision
818  );
819 
820  $dbw->endAtomic( __METHOD__ );
821 
822  // Keep each single hook handler atomic
825  $dbw,
826  __METHOD__,
827  function () use ( $user, $pageid, $redirid, $reason, $nullRevision ) {
828  $this->hookRunner->onPageMoveComplete(
829  $this->oldTitle,
830  $this->newTitle,
831  $user,
832  $pageid,
833  $redirid,
834  $reason,
835  $nullRevision
836  );
837  }
838  )
839  );
840 
841  return Status::newGood();
842  }
843 
853  private function moveFile( $oldTitle, $newTitle ) {
854  $file = $this->repoGroup->getLocalRepo()->newFile( $oldTitle );
855  $file->load( File::READ_LATEST );
856  if ( $file->exists() ) {
857  $status = $file->move( $newTitle );
858  } else {
859  $status = Status::newGood();
860  }
861 
862  // Clear RepoGroup process cache
863  $this->repoGroup->clearCache( $oldTitle );
864  $this->repoGroup->clearCache( $newTitle ); # clear false negative cache
865  return $status;
866  }
867 
882  private function moveToInternal( UserIdentity $user, &$nt, $reason = '', $createRedirect = true,
883  array $changeTags = []
884  ) {
885  if ( $nt->getArticleId( Title::READ_LATEST ) ) {
886  $moveOverRedirect = true;
887  $logType = 'move_redir';
888  } else {
889  $moveOverRedirect = false;
890  $logType = 'move';
891  }
892 
893  if ( $moveOverRedirect ) {
894  $overwriteMessage = wfMessage(
895  'delete_and_move_reason',
896  $this->oldTitle->getPrefixedText()
897  )->inContentLanguage()->text();
898  $newpage = $this->wikiPageFactory->newFromTitle( $nt );
899  $errs = [];
900  $status = $newpage->doDeleteArticleReal(
901  $overwriteMessage,
902  $user,
903  /* $suppress */ false,
904  /* unused */ null,
905  $errs,
906  /* unused */ null,
907  $changeTags,
908  'delete_redir'
909  );
910 
911  if ( !$status->isGood() ) {
912  return $status;
913  }
914 
915  $nt->resetArticleID( false );
916  }
917 
918  if ( $createRedirect ) {
919  if ( $this->oldTitle->getNamespace() === NS_CATEGORY
920  && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
921  ) {
922  $redirectContent = new WikitextContent(
923  wfMessage( 'category-move-redirect-override' )
924  ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
925  } else {
926  $redirectContent = $this->contentHandlerFactory
927  ->getContentHandler( $this->oldTitle->getContentModel() )
928  ->makeRedirectContent(
929  $nt,
930  wfMessage( 'move-redirect-text' )->inContentLanguage()->plain()
931  );
932  }
933 
934  // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
935  } else {
936  $redirectContent = null;
937  }
938 
939  // T59084: log_page should be the ID of the *moved* page
940  $oldid = $this->oldTitle->getArticleID();
941  $logTitle = clone $this->oldTitle;
942 
943  $logEntry = new ManualLogEntry( 'move', $logType );
944  $logEntry->setPerformer( $user );
945  $logEntry->setTarget( $logTitle );
946  $logEntry->setComment( $reason );
947  $logEntry->setParameters( [
948  '4::target' => $nt->getPrefixedText(),
949  '5::noredir' => $redirectContent ? '0' : '1',
950  ] );
951 
952  $formatter = LogFormatter::newFromEntry( $logEntry );
953  $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
954  $comment = $formatter->getPlainActionText();
955  if ( $reason ) {
956  $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
957  }
958 
959  $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
960 
961  $oldpage = $this->wikiPageFactory->newFromTitle( $this->oldTitle );
962  $oldcountable = $oldpage->isCountable();
963 
964  $newpage = $this->wikiPageFactory->newFromTitle( $nt );
965 
966  # Change the name of the target page:
967  $dbw->update( 'page',
968  /* SET */ [
969  'page_namespace' => $nt->getNamespace(),
970  'page_title' => $nt->getDBkey(),
971  ],
972  /* WHERE */ [ 'page_id' => $oldid ],
973  __METHOD__
974  );
975 
976  // Reset $nt before using it to create the null revision (T248789).
977  // But not $this->oldTitle yet, see below (T47348).
978  $nt->resetArticleID( $oldid );
979 
980  $commentObj = CommentStoreComment::newUnsavedComment( $comment );
981  # Save a null revision in the page's history notifying of the move
982  $nullRevision = $this->revisionStore->newNullRevision(
983  $dbw,
984  $nt,
985  $commentObj,
986  true,
987  $user
988  );
989  if ( $nullRevision === null ) {
990  $id = $nt->getArticleID( Title::READ_EXCLUSIVE );
991  $msg = 'Failed to create null revision while moving page ID ' .
992  $oldid . ' to ' . $nt->getPrefixedDBkey() . " (page ID $id)";
993 
994  throw new MWException( $msg );
995  }
996 
997  $nullRevision = $this->revisionStore->insertRevisionOn( $nullRevision, $dbw );
998  $logEntry->setAssociatedRevId( $nullRevision->getId() );
999 
1005  $this->userEditTracker->incrementUserEditCount( $user );
1006 
1007  if ( !$redirectContent ) {
1008  // Clean up the old title *before* reset article id - T47348
1009  WikiPage::onArticleDelete( $this->oldTitle );
1010  }
1011 
1012  $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
1013  $newpage->loadPageData( WikiPage::READ_LOCKING ); // T48397
1014 
1015  $newpage->updateRevisionOn( $dbw, $nullRevision );
1016 
1017  $fakeTags = [];
1018  $this->hookRunner->onRevisionFromEditComplete(
1019  $newpage, $nullRevision, $nullRevision->getParentId(), $user, $fakeTags );
1020 
1021  $newpage->doEditUpdates( $nullRevision, $user,
1022  [ 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ] );
1023 
1025 
1026  # Recreate the redirect, this time in the other direction.
1027  if ( $redirectContent ) {
1028  $redirectArticle = $this->wikiPageFactory->newFromTitle( $this->oldTitle );
1029  $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // T48397
1030  $newid = $redirectArticle->insertOn( $dbw );
1031  if ( $newid ) { // sanity
1032  $this->oldTitle->resetArticleID( $newid );
1033  $redirectRevRecord = new MutableRevisionRecord( $this->oldTitle );
1034  $redirectRevRecord->setPageId( $newid )
1035  ->setUser( $user )
1036  ->setComment( $commentObj )
1037  ->setContent( SlotRecord::MAIN, $redirectContent )
1038  ->setTimestamp( MWTimestamp::now( TS_MW ) );
1039 
1040  $inserted = $this->revisionStore->insertRevisionOn(
1041  $redirectRevRecord,
1042  $dbw
1043  );
1044  $redirectRevId = $inserted->getId();
1045  $redirectArticle->updateRevisionOn( $dbw, $inserted, 0 );
1046 
1047  $fakeTags = [];
1048  $this->hookRunner->onRevisionFromEditComplete(
1049  $redirectArticle,
1050  $inserted,
1051  false,
1052  $user,
1053  $fakeTags
1054  );
1055 
1056  // Clear all caches to make sure no stale information is used
1057  // when parsing the newly created redirect. Without this, moves would fail
1058  // under certain conditions when Lua core runs on the new page.
1059  // It is not entirely clear why this is needed, we just found that
1060  // it fixes the issue at hand (T279832).
1062 
1063  $redirectArticle->doEditUpdates(
1064  $inserted,
1065  $user,
1066  [ 'created' => true ]
1067  );
1068 
1069  // make a copy because of log entry below
1070  $redirectTags = $changeTags;
1071  if ( in_array( 'mw-new-redirect', ChangeTags::getSoftwareTags() ) ) {
1072  $redirectTags[] = 'mw-new-redirect';
1073  }
1074  ChangeTags::addTags( $redirectTags, null, $redirectRevId, null );
1075  }
1076  }
1077 
1078  # Log the move
1079  $logid = $logEntry->insert();
1080 
1081  $logEntry->addTags( $changeTags );
1082  $logEntry->publish( $logid );
1083 
1084  return $nullRevision;
1085  }
1086 }
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
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:259
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:279
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:2793
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:541
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:426
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
MovePage\$oldTitle
Title $oldTitle
Definition: MovePage.php:55
Title\clearCaches
static clearCaches()
Definition: Title.php:3039
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
MovePage\$revisionStore
RevisionStore $revisionStore
Definition: MovePage.php:95
MovePage\authorizeMove
authorizeMove(Authority $performer, string $reason=null)
Authorize the move by $performer.
Definition: MovePage.php:280
MovePage\moveFile
moveFile( $oldTitle, $newTitle)
Move a file associated with a page to a new location.
Definition: MovePage.php:853
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:119
MovePage\isValidMove
isValidMove()
Does various sanity checks that the move is valid.
Definition: MovePage.php:317
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:1182
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:299
MovePage\moveIfAllowed
moveIfAllowed(Authority $performer, $reason=null, $createRedirect=true, array $changeTags=[])
Same as move(), but with permissions checks.
Definition: MovePage.php:502
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:482
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:2371
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:566
MovePage\$nsInfo
NamespaceInfo $nsInfo
Definition: MovePage.php:75
WikiPage\onArticleDelete
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:2829
MovePage\$hookRunner
HookRunner $hookRunner
Definition: MovePage.php:105
Page\WikiPageFactory
Definition: WikiPageFactory.php:20
ChangeTags\getSoftwareTags
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:158
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:882
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:37
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:625
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:677
$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:307
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
MediaWiki\Revision\MutableRevisionRecord
Definition: MutableRevisionRecord.php:44
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 sanity checks.
Definition: MovePage.php:657
MovePage\$watchedItems
WatchedItemStoreInterface $watchedItems
Definition: MovePage.php:80
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:48
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\__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)
Definition: MovePage.php:153
MovePage\$userFactory
UserFactory $userFactory
Definition: MovePage.php:115
MovePage\moveSubpagesInternal
moveSubpagesInternal(callable $subpageMoveCallback)
Definition: MovePage.php:586
RepoGroup
Prioritized list of file repositories.
Definition: RepoGroup.php:32
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
MovePage\isValidFileMove
isValidFileMove()
Sanity checks for when a file is being moved.
Definition: MovePage.php:397
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:44
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:558
MovePage\authorizeInternal
authorizeInternal(callable $authorizer, Authority $performer, ?string $reason)
Definition: MovePage.php:213
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
ChangeTags\addTags
static addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params=null, RecentChange $rc=null)
Add tags to a change given its rc_id, rev_id and/or log_id.
Definition: ChangeTags.php:328
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:54
$type
$type
Definition: testCompression.php:52