143 $this->oldTitle = $oldTitle;
144 $this->newTitle = $newTitle;
146 $services = MediaWikiServices::getInstance();
149 self::CONSTRUCTOR_OPTIONS,
150 $services->getMainConfig()
152 $this->loadBalancer =
$loadBalancer ?? $services->getDBLoadBalancer();
153 $this->nsInfo =
$nsInfo ?? $services->getNamespaceInfo();
154 $this->watchedItems =
$watchedItems ?? $services->getWatchedItemStore();
155 $this->permMgr =
$permMgr ?? $services->getPermissionManager();
156 $this->repoGroup =
$repoGroup ?? $services->getRepoGroup();
157 $this->contentHandlerFactory =
160 $this->revisionStore =
$revisionStore ?? $services->getRevisionStore();
161 $this->spamChecker =
$spamChecker ?? $services->getSpamChecker();
162 $this->hookContainer =
$hookContainer ?? $services->getHookContainer();
163 $this->hookRunner =
new HookRunner( $this->hookContainer );
178 $this->permMgr->getPermissionErrors(
'move', $user, $this->oldTitle ),
179 $this->permMgr->getPermissionErrors(
'edit', $user, $this->oldTitle ),
180 $this->permMgr->getPermissionErrors(
'move-target', $user, $this->newTitle ),
181 $this->permMgr->getPermissionErrors(
'edit', $user, $this->newTitle )
186 foreach ( $errors as $error ) {
187 $status->fatal( ...$error );
191 if ( $reason !==
null && $this->spamChecker->checkSummary( $reason ) !==
false ) {
193 $status->fatal(
'spamprotectiontext' );
196 $tp = $this->newTitle->getTitleProtection();
197 if ( $tp !==
false && !$this->permMgr->userHasRight( $user, $tp[
'permission'] ) ) {
198 $status->fatal(
'cantmove-titleprotected' );
201 $this->hookRunner->onMovePageCheckPermissions(
202 $this->oldTitle, $this->newTitle, $user, $reason, $status );
217 if ( $this->oldTitle->equals( $this->newTitle ) ) {
218 $status->fatal(
'selfmove' );
219 } elseif ( $this->newTitle->getArticleID( Title::READ_LATEST )
220 && !$this->isValidMoveTarget()
225 $status->fatal(
'articleexists', $this->newTitle->getPrefixedText() );
231 if ( $this->oldTitle->getDBkey() ==
'' ) {
232 $status->fatal(
'badarticleerror' );
233 } elseif ( $this->oldTitle->isExternal() ) {
234 $status->fatal(
'immobile-source-namespace-iw' );
235 } elseif ( !$this->oldTitle->isMovable() ) {
236 $nsText = $this->oldTitle->getNsText();
237 if ( $nsText ===
'' ) {
238 $nsText =
wfMessage(
'blanknamespace' )->text();
240 $status->fatal(
'immobile-source-namespace', $nsText );
241 } elseif ( !$this->oldTitle->exists() ) {
242 $status->fatal(
'movepage-source-doesnt-exist', $this->oldTitle->getPrefixedText() );
245 if ( $this->newTitle->isExternal() ) {
246 $status->fatal(
'immobile-target-namespace-iw' );
247 } elseif ( !$this->newTitle->isMovable() ) {
248 $nsText = $this->newTitle->getNsText();
249 if ( $nsText ===
'' ) {
250 $nsText =
wfMessage(
'blanknamespace' )->text();
252 $status->fatal(
'immobile-target-namespace', $nsText );
254 if ( !$this->newTitle->isValid() ) {
255 $status->fatal(
'movepage-invalid-target-title' );
259 if ( !$this->contentHandlerFactory
260 ->getContentHandler( $this->oldTitle->getContentModel() )
261 ->canBeUsedOn( $this->newTitle )
264 'content-not-allowed-here',
265 ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
266 $this->newTitle->getPrefixedText(),
272 if ( $this->oldTitle->inNamespace(
NS_FILE ) ) {
276 if ( $this->newTitle->inNamespace(
NS_FILE ) && !$this->oldTitle->inNamespace(
NS_FILE ) ) {
277 $status->fatal(
'nonfile-cannot-move-to-file' );
281 $this->hookRunner->onMovePageIsValidMove( $this->oldTitle, $this->newTitle, $status );
294 if ( !$this->newTitle->inNamespace(
NS_FILE ) ) {
295 $status->fatal(
'imagenocrossnamespace' );
300 $file = $this->repoGroup->getLocalRepo()->newFile( $this->oldTitle );
301 $file->load( File::READ_LATEST );
302 if (
$file->exists() ) {
304 $status->fatal(
'imageinvalidfilename' );
306 if ( !File::checkExtensionCompatibility(
$file, $this->newTitle->getDBkey() ) ) {
307 $status->fatal(
'imagetypemismatch' );
322 # Is it an existing file?
323 if ( $this->newTitle->inNamespace(
NS_FILE ) ) {
324 $file = $this->repoGroup->getLocalRepo()->newFile( $this->newTitle );
325 $file->load( File::READ_LATEST );
326 if (
$file->exists() ) {
327 wfDebug( __METHOD__ .
": file exists" );
331 # Is it a redirect with no history?
332 if ( !$this->newTitle->isSingleRevRedirect() ) {
333 wfDebug( __METHOD__ .
": not a one-rev redirect" );
336 # Get the article text
337 $rev = $this->revisionStore->getRevisionByTitle(
340 RevisionStore::READ_LATEST
342 if ( !is_object( $rev ) ) {
345 $content = $rev->getContent( SlotRecord::MAIN );
346 # Does the redirect point to the source?
347 # Or is it a broken self-redirect, usually caused by namespace collisions?
351 if ( $redirTitle->getPrefixedDBkey() !== $this->oldTitle->getPrefixedDBkey() &&
352 $redirTitle->getPrefixedDBkey() !== $this->newTitle->getPrefixedDBkey() ) {
353 wfDebug( __METHOD__ .
": redirect points to other page" );
359 # Fail safe (not a redirect after all. strange.)
360 wfDebug( __METHOD__ .
": failsafe: database says " . $this->newTitle->getPrefixedDBkey() .
361 " is a redirect, but it doesn't contain a valid redirect." );
378 User $user, $reason =
null, $createRedirect =
true, array $changeTags = []
381 if ( !$status->isOK() ) {
385 return $this->
moveUnsafe( $user, $reason, $createRedirect, $changeTags );
398 User $user, $reason =
null, $createRedirect =
true, array $changeTags = []
406 if ( !$status->isOK() ) {
413 if ( !$this->permMgr->userHasRight( $user,
'suppressredirect' ) ) {
414 $createRedirect =
true;
417 return $this->
moveUnsafe( $user, $reason, $createRedirect, $changeTags );
435 User $user, $reason =
null, $createRedirect =
true, array $changeTags = []
454 User $user, $reason =
null, $createRedirect =
true, array $changeTags = []
468 $checkPermissions,
User $user, $reason, $createRedirect, array $changeTags
472 if ( $checkPermissions ) {
473 if ( !$this->permMgr->userCan(
474 'move-subpages', $user, $this->oldTitle )
476 return Status::newFatal(
'cant-move-subpages' );
481 if ( !$this->nsInfo->hasSubpages( $this->oldTitle->getNamespace() ) ) {
482 return Status::newFatal(
'namespace-nosubpages',
483 $this->nsInfo->getCanonicalName( $this->oldTitle->getNamespace() ) );
485 if ( !$this->nsInfo->hasSubpages( $this->newTitle->getNamespace() ) ) {
486 return Status::newFatal(
'namespace-nosubpages',
487 $this->nsInfo->getCanonicalName( $this->newTitle->getNamespace() ) );
493 $topStatus = Status::newGood();
494 $perTitleStatus = [];
497 foreach ( $subpages as $oldSubpage ) {
501 $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
502 $topStatus->merge( $status );
503 $topStatus->setOK(
true );
509 if ( $oldSubpage->getArticleID() == $this->oldTitle->getArticleID() ||
510 $oldSubpage->getArticleID() == $this->newTitle->getArticleID()
515 $newPageName = preg_replace(
516 '#^' . preg_quote( $this->oldTitle->getDBkey(),
'#' ) .
'#',
518 $oldSubpage->getDBkey() );
519 if ( $oldSubpage->isTalkPage() ) {
520 $newNs = $this->newTitle->getTalkPage()->getNamespace();
522 $newNs = $this->newTitle->getSubjectPage()->getNamespace();
526 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
528 $mp =
new MovePage( $oldSubpage, $newSubpage );
529 $method = $checkPermissions ?
'moveIfAllowed' :
'move';
531 $status = $mp->$method( $user, $reason, $createRedirect, $changeTags );
532 if ( $status->isOK() ) {
533 $status->setResult(
true, $newSubpage->getPrefixedText() );
535 $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
536 $topStatus->merge( $status );
537 $topStatus->setOK(
true );
540 $topStatus->value = $perTitleStatus;
553 private function moveUnsafe(
User $user, $reason, $createRedirect, array $changeTags ) {
554 $status = Status::newGood();
555 $this->hookRunner->onTitleMove( $this->oldTitle, $this->newTitle, $user, $reason, $status );
556 if ( !$status->isOK() ) {
561 $dbw = $this->loadBalancer->getConnection(
DB_MASTER );
562 $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
564 $this->hookRunner->onTitleMoveStarting( $this->oldTitle, $this->newTitle, $user );
566 $pageid = $this->oldTitle->getArticleID( Title::READ_LATEST );
567 $protected = $this->oldTitle->isProtected();
570 $nullRevision = $this->
moveToInternal( $user, $this->newTitle, $reason, $createRedirect,
577 $prefixes = $dbw->select(
579 [
'cl_sortkey_prefix',
'cl_to' ],
580 [
'cl_from' => $pageid ],
583 $type = $this->nsInfo->getCategoryLinkType( $this->newTitle->getNamespace() );
584 foreach ( $prefixes as $prefixRow ) {
585 $prefix = $prefixRow->cl_sortkey_prefix;
586 $catTo = $prefixRow->cl_to;
587 $dbw->update(
'categorylinks',
589 'cl_sortkey' => Collation::singleton()->getSortKey(
590 $this->newTitle->getCategorySortkey( $prefix ) ),
591 'cl_collation' => $this->options->get(
'CategoryCollation' ),
593 'cl_timestamp=cl_timestamp' ],
595 'cl_from' => $pageid,
601 $redirid = $this->oldTitle->getArticleID();
604 # Protect the redirect title as the title used to be...
607 [
'pr_type',
'pr_level',
'pr_cascade',
'pr_user',
'pr_expiry' ],
608 [
'pr_page' => $pageid ],
613 foreach (
$res as $row ) {
615 'pr_page' => $redirid,
616 'pr_type' => $row->pr_type,
617 'pr_level' => $row->pr_level,
618 'pr_cascade' => $row->pr_cascade,
619 'pr_user' => $row->pr_user,
620 'pr_expiry' => $row->pr_expiry
623 $dbw->insert(
'page_restrictions', $rowsInsert, __METHOD__, [
'IGNORE' ] );
628 $this->oldTitle->getPrefixedText(),
629 $this->newTitle->getPrefixedText()
630 )->inContentLanguage()->text();
632 $comment .=
wfMessage(
'colon-separator' )->inContentLanguage()->text() . $reason;
636 $insertedPrIds = $dbw->select(
639 [
'pr_page' => $redirid ],
642 $logRelationsValues = [];
643 foreach ( $insertedPrIds as $prid ) {
644 $logRelationsValues[] = $prid->pr_id;
649 $logEntry->setTarget( $this->newTitle );
650 $logEntry->setComment( $comment );
651 $logEntry->setPerformer( $user );
652 $logEntry->setParameters( [
653 '4::oldtitle' => $this->oldTitle->getPrefixedText(),
655 $logEntry->setRelations( [
'pr_id' => $logRelationsValues ] );
656 $logEntry->addTags( $changeTags );
657 $logId = $logEntry->insert();
658 $logEntry->publish( $logId );
662 if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) {
663 $dbw->update(
'pagelinks',
664 [
'pl_from_namespace' => $this->newTitle->getNamespace() ],
665 [
'pl_from' => $pageid ],
668 $dbw->update(
'templatelinks',
669 [
'tl_from_namespace' => $this->newTitle->getNamespace() ],
670 [
'tl_from' => $pageid ],
673 $dbw->update(
'imagelinks',
674 [
'il_from_namespace' => $this->newTitle->getNamespace() ],
675 [
'il_from' => $pageid ],
681 $oldtitle = $this->oldTitle->getDBkey();
682 $newtitle = $this->newTitle->getDBkey();
683 $oldsnamespace = $this->nsInfo->getSubject( $this->oldTitle->getNamespace() );
684 $newsnamespace = $this->nsInfo->getSubject( $this->newTitle->getNamespace() );
685 if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
686 $this->watchedItems->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
691 if ( $this->oldTitle->getNamespace() ==
NS_FILE ) {
692 $status = $this->
moveFile( $this->oldTitle, $this->newTitle );
693 if ( !$status->isOK() ) {
694 $dbw->cancelAtomic( __METHOD__ );
699 $this->hookRunner->onPageMoveCompleting(
700 $this->oldTitle, $this->newTitle,
701 $user, $pageid, $redirid, $reason, $nullRevision
705 if ( $this->hookContainer->isRegistered(
'TitleMoveCompleting' ) ) {
707 $nullRevisionObj =
new Revision( $nullRevision );
708 $this->hookRunner->onTitleMoveCompleting(
719 $dbw->endAtomic( __METHOD__ );
722 DeferredUpdates::addUpdate(
726 function () use ( $user, $pageid, $redirid, $reason, $nullRevision ) {
727 $this->hookRunner->onPageMoveComplete(
737 if ( !$this->hookContainer->isRegistered(
'TitleMoveComplete' ) ) {
742 $nullRevisionObj =
new Revision( $nullRevision );
744 $this->hookRunner->onTitleMoveComplete(
756 return Status::newGood();
768 private function moveFile( $oldTitle, $newTitle ) {
770 $file->load( File::READ_LATEST );
771 if (
$file->exists() ) {
774 $status = Status::newGood();
778 $this->repoGroup->clearCache(
$oldTitle );
779 $this->repoGroup->clearCache(
$newTitle ); # clear
false negative cache
799 array $changeTags = []
801 if ( $nt->exists() ) {
802 $moveOverRedirect =
true;
803 $logType =
'move_redir';
805 $moveOverRedirect =
false;
809 if ( $moveOverRedirect ) {
811 'delete_and_move_reason',
812 $this->oldTitle->getPrefixedText()
813 )->inContentLanguage()->text();
814 $newpage = WikiPage::factory( $nt );
816 $status = $newpage->doDeleteArticleReal(
827 if ( !$status->isGood() ) {
828 throw new MWException(
'Failed to delete page-move revision: '
829 . $status->getWikiText(
false,
false,
'en' ) );
832 $nt->resetArticleID(
false );
835 if ( $createRedirect ) {
836 if ( $this->oldTitle->getNamespace() ==
NS_CATEGORY
837 && !
wfMessage(
'category-move-redirect-override' )->inContentLanguage()->isDisabled()
840 wfMessage(
'category-move-redirect-override' )
841 ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
843 $redirectContent = $this->contentHandlerFactory
844 ->getContentHandler( $this->oldTitle->getContentModel() )
845 ->makeRedirectContent(
847 wfMessage(
'move-redirect-text' )->inContentLanguage()->plain()
853 $redirectContent =
null;
857 $oldid = $this->oldTitle->getArticleID();
858 $logTitle = clone $this->oldTitle;
861 $logEntry->setPerformer( $user );
862 $logEntry->setTarget( $logTitle );
863 $logEntry->setComment( $reason );
864 $logEntry->setParameters( [
865 '4::target' => $nt->getPrefixedText(),
866 '5::noredir' => $redirectContent ?
'0' :
'1',
870 $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
871 $comment = $formatter->getPlainActionText();
873 $comment .=
wfMessage(
'colon-separator' )->inContentLanguage()->text() . $reason;
876 $dbw = $this->loadBalancer->getConnection(
DB_MASTER );
878 $oldpage = WikiPage::factory( $this->oldTitle );
879 $oldcountable = $oldpage->isCountable();
881 $newpage = WikiPage::factory( $nt );
883 # Change the name of the target page:
884 $dbw->update(
'page',
886 'page_namespace' => $nt->getNamespace(),
887 'page_title' => $nt->getDBkey(),
889 [
'page_id' => $oldid ],
895 $nt->resetArticleID( $oldid );
897 $commentObj = CommentStoreComment::newUnsavedComment( $comment );
898 # Save a null revision in the page's history notifying of the move
899 $nullRevision = $this->revisionStore->newNullRevision(
906 if ( !is_object( $nullRevision ) ) {
907 throw new MWException(
'Failed to create null revision while moving page ID '
908 . $oldid .
' to ' . $nt->getPrefixedDBkey() );
911 $nullRevision = $this->revisionStore->insertRevisionOn( $nullRevision, $dbw );
912 $logEntry->setAssociatedRevId( $nullRevision->getId() );
921 if ( !$redirectContent ) {
923 WikiPage::onArticleDelete( $this->oldTitle );
926 $this->oldTitle->resetArticleID( 0 );
927 $newpage->loadPageData( WikiPage::READ_LOCKING );
929 $newpage->updateRevisionOn( $dbw, $nullRevision );
932 $this->hookRunner->onRevisionFromEditComplete(
933 $newpage, $nullRevision, $nullRevision->getParentId(), $user, $fakeTags );
936 if ( $this->hookContainer->isRegistered(
'NewRevisionFromEditComplete' ) ) {
938 $nullRevisionObj =
new Revision( $nullRevision );
939 $this->hookRunner->onNewRevisionFromEditComplete(
942 $nullRevision->getParentId(),
948 $newpage->doEditUpdates( $nullRevision, $user,
949 [
'changed' =>
false,
'moved' =>
true,
'oldcountable' => $oldcountable ] );
951 WikiPage::onArticleCreate( $nt );
953 # Recreate the redirect, this time in the other direction.
954 if ( $redirectContent ) {
955 $redirectArticle = WikiPage::factory( $this->oldTitle );
956 $redirectArticle->loadFromRow(
false, WikiPage::READ_LOCKING );
957 $newid = $redirectArticle->insertOn( $dbw );
959 $this->oldTitle->resetArticleID( $newid );
961 $redirectRevisionRecord->setPageId( $newid );
962 $redirectRevisionRecord->setUser( $user );
963 $redirectRevisionRecord->setComment( $commentObj );
964 $redirectRevisionRecord->setContent( SlotRecord::MAIN, $redirectContent );
965 $redirectRevisionRecord->setTimestamp( MWTimestamp::now( TS_MW ) );
967 $inserted = $this->revisionStore->insertRevisionOn(
968 $redirectRevisionRecord,
971 $redirectRevId = $inserted->getId();
972 $redirectArticle->updateRevisionOn( $dbw, $inserted, 0 );
975 $this->hookRunner->onRevisionFromEditComplete(
984 if ( $this->hookContainer->isRegistered(
'NewRevisionFromEditComplete' ) ) {
986 $redirectRevisionObj =
new Revision( $inserted );
987 $this->hookRunner->onNewRevisionFromEditComplete(
989 $redirectRevisionObj,
996 $redirectArticle->doEditUpdates(
999 [
'created' =>
true ]
1003 $redirectTags = $changeTags;
1005 $redirectTags[] =
'mw-new-redirect';
1012 $logid = $logEntry->insert();
1014 $logEntry->addTags( $changeTags );
1015 $logEntry->publish( $logid );
1017 return $nullRevision;
$wgMaximumMovedPages
Maximum number of pages to move at once when moving subpages with a page.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfMergeErrorArrays(... $args)
Merge arrays in the style of PermissionManager::getPermissionErrors, with duplicate removal e....
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Deferrable Update for closure/callback updates via IDatabase::doAtomicSection()
Class for creating new log entries and inserting them into the database.
Service to check if text (either content or a summary) qualifies as spam.
Handles the backend logic of moving a page from one title to another.
checkPermissions(User $user, $reason)
Check if the user is allowed to perform the move.
moveSubpagesInternal( $checkPermissions, User $user, $reason, $createRedirect, array $changeTags)
moveIfAllowed(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Same as move(), but with permissions checks.
isValidFileMove()
Sanity checks for when a file is being moved.
moveUnsafe(User $user, $reason, $createRedirect, array $changeTags)
Moves without any sort of safety or sanity checks.
HookContainer $hookContainer
WatchedItemStoreInterface $watchedItems
moveSubpagesIfAllowed(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move the source page's subpages to be subpages of the target page, with user permission checks.
moveToInternal(User $user, &$nt, $reason='', $createRedirect=true, array $changeTags=[])
Move page to a title which is either a redirect to the source page or nonexistent.
ILoadBalancer $loadBalancer
isValidMove()
Does various sanity checks that the move is valid.
isValidMoveTarget()
Checks if $this can be moved to a given Title.
move(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move a page without taking user permissions into account.
moveSubpages(User $user, $reason=null, $createRedirect=true, array $changeTags=[])
Move the source page's subpages to be subpages of the target page, without checking user permissions.
PermissionManager $permMgr
IContentHandlerFactory $contentHandlerFactory
moveFile( $oldTitle, $newTitle)
Move a file associated with a page to a new location.
RevisionStore $revisionStore
__construct(Title $oldTitle, Title $newTitle, ServiceOptions $options=null, ILoadBalancer $loadBalancer=null, NamespaceInfo $nsInfo=null, WatchedItemStoreInterface $watchedItems=null, PermissionManager $permMgr=null, RepoGroup $repoGroup=null, IContentHandlerFactory $contentHandlerFactory=null, RevisionStore $revisionStore=null, SpamChecker $spamChecker=null, HookContainer $hookContainer=null)
Calling this directly is deprecated in 1.34.
const CONSTRUCTOR_OPTIONS
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Prioritized list of file repositories.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter.
Represents a title within MediaWiki.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
spreadAnyEditBlock()
If this user is logged-in and blocked, block any IP address they've successfully logged in from.
incEditCount()
Schedule a deferred update to update the user's edit count.
Content object for wiki text pages.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.