99 $this->oldTitle = $oldTitle;
100 $this->newTitle = $newTitle;
103 MediaWikiServices::getInstance()->getMainConfig() );
104 $this->loadBalancer =
105 $loadBalancer ?? MediaWikiServices::getInstance()->getDBLoadBalancer();
106 $this->nsInfo =
$nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
107 $this->watchedItems =
108 $watchedItems ?? MediaWikiServices::getInstance()->getWatchedItemStore();
109 $this->permMgr =
$permMgr ?? MediaWikiServices::getInstance()->getPermissionManager();
110 $this->repoGroup =
$repoGroup ?? MediaWikiServices::getInstance()->getRepoGroup();
125 $this->permMgr->getPermissionErrors(
'move', $user, $this->oldTitle ),
126 $this->permMgr->getPermissionErrors(
'edit', $user, $this->oldTitle ),
127 $this->permMgr->getPermissionErrors(
'move-target', $user, $this->newTitle ),
128 $this->permMgr->getPermissionErrors(
'edit', $user, $this->newTitle )
133 foreach ( $errors as $error ) {
134 $status->fatal( ...$error );
140 $status->fatal(
'spamprotectiontext' );
143 $tp = $this->newTitle->getTitleProtection();
144 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
145 if ( $tp !==
false && !$permissionManager->userHasRight( $user, $tp[
'permission'] ) ) {
146 $status->fatal(
'cantmove-titleprotected' );
149 Hooks::run(
'MovePageCheckPermissions',
150 [ $this->oldTitle, $this->newTitle, $user, $reason, $status ]
166 if ( $this->oldTitle->equals( $this->newTitle ) ) {
167 $status->fatal(
'selfmove' );
168 } elseif ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) {
172 $status->fatal(
'articleexists' );
178 if ( $this->oldTitle->getDBkey() ==
'' ) {
179 $status->fatal(
'badarticleerror' );
180 } elseif ( $this->oldTitle->isExternal() ) {
181 $status->fatal(
'immobile-source-namespace-iw' );
182 } elseif ( !$this->oldTitle->isMovable() ) {
183 $status->fatal(
'immobile-source-namespace', $this->oldTitle->getNsText() );
184 } elseif ( !$this->oldTitle->exists() ) {
185 $status->fatal(
'movepage-source-doesnt-exist' );
188 if ( $this->newTitle->isExternal() ) {
189 $status->fatal(
'immobile-target-namespace-iw' );
190 } elseif ( !$this->newTitle->isMovable() ) {
191 $status->fatal(
'immobile-target-namespace', $this->newTitle->getNsText() );
193 if ( !$this->newTitle->isValid() ) {
194 $status->fatal(
'movepage-invalid-target-title' );
198 if ( !$this->options->get(
'ContentHandlerUseDB' ) &&
199 $this->oldTitle->getContentModel() !== $this->newTitle->getContentModel() ) {
203 ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
204 ContentHandler::getLocalizedName( $this->newTitle->getContentModel() )
207 !ContentHandler::getForTitle( $this->oldTitle )->canBeUsedOn( $this->newTitle )
210 'content-not-allowed-here',
211 ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
212 $this->newTitle->getPrefixedText(),
218 if ( $this->oldTitle->inNamespace(
NS_FILE ) ) {
222 if ( $this->newTitle->inNamespace(
NS_FILE ) && !$this->oldTitle->inNamespace(
NS_FILE ) ) {
223 $status->fatal(
'nonfile-cannot-move-to-file' );
227 Hooks::run(
'MovePageIsValidMove', [ $this->oldTitle, $this->newTitle, $status ] );
240 if ( !$this->newTitle->inNamespace(
NS_FILE ) ) {
241 $status->fatal(
'imagenocrossnamespace' );
246 $file = $this->repoGroup->getLocalRepo()->newFile( $this->oldTitle );
247 $file->load( File::READ_LATEST );
248 if (
$file->exists() ) {
250 $status->fatal(
'imageinvalidfilename' );
253 $status->fatal(
'imagetypemismatch' );
268 # Is it an existing file?
269 if ( $this->newTitle->inNamespace(
NS_FILE ) ) {
270 $file = $this->repoGroup->getLocalRepo()->newFile( $this->newTitle );
271 $file->load( File::READ_LATEST );
272 if (
$file->exists() ) {
273 wfDebug( __METHOD__ .
": file exists\n" );
277 # Is it a redirect with no history?
278 if ( !$this->newTitle->isSingleRevRedirect() ) {
279 wfDebug( __METHOD__ .
": not a one-rev redirect\n" );
282 # Get the article text
284 if ( !is_object( $rev ) ) {
288 # Does the redirect point to the source?
289 # Or is it a broken self-redirect, usually caused by namespace collisions?
293 if ( $redirTitle->getPrefixedDBkey() !== $this->oldTitle->getPrefixedDBkey() &&
294 $redirTitle->getPrefixedDBkey() !== $this->newTitle->getPrefixedDBkey() ) {
295 wfDebug( __METHOD__ .
": redirect points to other page\n" );
301 # Fail safe (not a redirect after all. strange.)
302 wfDebug( __METHOD__ .
": failsafe: database says " . $this->newTitle->getPrefixedDBkey() .
303 " is a redirect, but it doesn't contain a valid redirect.\n" );
320 User $user, $reason =
null, $createRedirect =
true, array $changeTags = []
323 if ( !$status->isOK() ) {
327 return $this->
moveUnsafe( $user, $reason, $createRedirect, $changeTags );
340 User $user, $reason =
null, $createRedirect =
true, array $changeTags = []
348 if ( !$status->isOK() ) {
355 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
356 if ( !$permissionManager->userHasRight( $user,
'suppressredirect' ) ) {
357 $createRedirect =
true;
360 return $this->
moveUnsafe( $user, $reason, $createRedirect, $changeTags );
378 User $user, $reason =
null, $createRedirect =
true, array $changeTags = []
397 User $user, $reason =
null, $createRedirect =
true, array $changeTags = []
411 $checkPermissions,
User $user, $reason, $createRedirect, array $changeTags
414 $services = MediaWikiServices::getInstance();
416 if ( $checkPermissions ) {
417 if ( !$services->getPermissionManager()->userCan(
418 'move-subpages', $user, $this->oldTitle )
420 return Status::newFatal(
'cant-move-subpages' );
424 $nsInfo = $services->getNamespaceInfo();
428 return Status::newFatal(
'namespace-nosubpages',
432 return Status::newFatal(
'namespace-nosubpages',
439 $topStatus = Status::newGood();
440 $perTitleStatus = [];
443 foreach ( $subpages as $oldSubpage ) {
447 $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
448 $topStatus->merge( $status );
449 $topStatus->setOK(
true );
455 if ( $oldSubpage->getArticleID() == $this->oldTitle->getArticleID() ||
456 $oldSubpage->getArticleID() == $this->newTitle->getArticleID()
461 $newPageName = preg_replace(
462 '#^' . preg_quote( $this->oldTitle->getDBkey(),
'#' ) .
'#',
464 $oldSubpage->getDBkey() );
465 if ( $oldSubpage->isTalkPage() ) {
466 $newNs = $this->newTitle->getTalkPage()->getNamespace();
468 $newNs = $this->newTitle->getSubjectPage()->getNamespace();
472 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
474 $mp =
new MovePage( $oldSubpage, $newSubpage );
475 $method = $checkPermissions ?
'moveIfAllowed' :
'move';
477 $status = $mp->$method( $user, $reason, $createRedirect, $changeTags );
478 if ( $status->isOK() ) {
479 $status->setResult(
true, $newSubpage->getPrefixedText() );
481 $perTitleStatus[$oldSubpage->getPrefixedText()] = $status;
482 $topStatus->merge( $status );
483 $topStatus->setOK(
true );
486 $topStatus->value = $perTitleStatus;
499 private function moveUnsafe(
User $user, $reason, $createRedirect, array $changeTags ) {
500 $status = Status::newGood();
501 Hooks::run(
'TitleMove', [ $this->oldTitle, $this->newTitle, $user, $reason, &$status ] );
502 if ( !$status->isOK() ) {
507 $dbw = $this->loadBalancer->getConnection(
DB_MASTER );
508 $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
510 Hooks::run(
'TitleMoveStarting', [ $this->oldTitle, $this->newTitle, $user ] );
512 $pageid = $this->oldTitle->getArticleID( Title::READ_LATEST );
513 $protected = $this->oldTitle->isProtected();
516 $nullRevision = $this->
moveToInternal( $user, $this->newTitle, $reason, $createRedirect,
523 $prefixes = $dbw->select(
525 [
'cl_sortkey_prefix',
'cl_to' ],
526 [
'cl_from' => $pageid ],
529 $type = $this->nsInfo->getCategoryLinkType( $this->newTitle->getNamespace() );
530 foreach ( $prefixes as $prefixRow ) {
531 $prefix = $prefixRow->cl_sortkey_prefix;
532 $catTo = $prefixRow->cl_to;
533 $dbw->update(
'categorylinks',
536 $this->newTitle->getCategorySortkey( $prefix ) ),
537 'cl_collation' => $this->options->get(
'CategoryCollation' ),
539 'cl_timestamp=cl_timestamp' ],
541 'cl_from' => $pageid,
547 $redirid = $this->oldTitle->getArticleID();
550 # Protect the redirect title as the title used to be...
553 [
'pr_type',
'pr_level',
'pr_cascade',
'pr_user',
'pr_expiry' ],
554 [
'pr_page' => $pageid ],
559 foreach (
$res as $row ) {
561 'pr_page' => $redirid,
562 'pr_type' => $row->pr_type,
563 'pr_level' => $row->pr_level,
564 'pr_cascade' => $row->pr_cascade,
565 'pr_user' => $row->pr_user,
566 'pr_expiry' => $row->pr_expiry
569 $dbw->insert(
'page_restrictions', $rowsInsert, __METHOD__, [
'IGNORE' ] );
574 $this->oldTitle->getPrefixedText(),
575 $this->newTitle->getPrefixedText()
576 )->inContentLanguage()->text();
578 $comment .=
wfMessage(
'colon-separator' )->inContentLanguage()->text() . $reason;
582 $insertedPrIds = $dbw->select(
585 [
'pr_page' => $redirid ],
588 $logRelationsValues = [];
589 foreach ( $insertedPrIds as $prid ) {
590 $logRelationsValues[] = $prid->pr_id;
595 $logEntry->setTarget( $this->newTitle );
596 $logEntry->setComment( $comment );
597 $logEntry->setPerformer( $user );
598 $logEntry->setParameters( [
599 '4::oldtitle' => $this->oldTitle->getPrefixedText(),
601 $logEntry->setRelations( [
'pr_id' => $logRelationsValues ] );
602 $logEntry->addTags( $changeTags );
603 $logId = $logEntry->insert();
604 $logEntry->publish( $logId );
608 if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) {
609 $dbw->update(
'pagelinks',
610 [
'pl_from_namespace' => $this->newTitle->getNamespace() ],
611 [
'pl_from' => $pageid ],
614 $dbw->update(
'templatelinks',
615 [
'tl_from_namespace' => $this->newTitle->getNamespace() ],
616 [
'tl_from' => $pageid ],
619 $dbw->update(
'imagelinks',
620 [
'il_from_namespace' => $this->newTitle->getNamespace() ],
621 [
'il_from' => $pageid ],
627 $oldtitle = $this->oldTitle->getDBkey();
628 $newtitle = $this->newTitle->getDBkey();
629 $oldsnamespace = $this->nsInfo->getSubject( $this->oldTitle->getNamespace() );
630 $newsnamespace = $this->nsInfo->getSubject( $this->newTitle->getNamespace() );
631 if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
632 $this->watchedItems->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
637 if ( $this->oldTitle->getNamespace() ==
NS_FILE ) {
638 $status = $this->
moveFile( $this->oldTitle, $this->newTitle );
639 if ( !$status->isOK() ) {
640 $dbw->cancelAtomic( __METHOD__ );
646 'TitleMoveCompleting',
647 [ $this->oldTitle, $this->newTitle,
648 $user, $pageid, $redirid, $reason, $nullRevision ]
651 $dbw->endAtomic( __METHOD__ );
663 DeferredUpdates::addUpdate(
669 function () use ( $params, &$user ) {
670 Hooks::run(
'TitleMoveComplete', $params );
675 return Status::newGood();
687 private function moveFile( $oldTitle, $newTitle ) {
688 $status = Status::newFatal(
694 $file->load( File::READ_LATEST );
695 if (
$file->exists() ) {
700 $this->repoGroup->clearCache(
$oldTitle );
701 $this->repoGroup->clearCache(
$newTitle ); # clear
false negative cache
721 array $changeTags = []
723 if ( $nt->exists() ) {
724 $moveOverRedirect =
true;
725 $logType =
'move_redir';
727 $moveOverRedirect =
false;
731 if ( $moveOverRedirect ) {
733 'delete_and_move_reason',
734 $this->oldTitle->getPrefixedText()
735 )->inContentLanguage()->text();
736 $newpage = WikiPage::factory( $nt );
738 $status = $newpage->doDeleteArticleReal(
749 if ( !$status->isGood() ) {
750 throw new MWException(
'Failed to delete page-move revision: '
751 . $status->getWikiText(
false,
false,
'en' ) );
754 $nt->resetArticleID(
false );
757 if ( $createRedirect ) {
758 if ( $this->oldTitle->getNamespace() ==
NS_CATEGORY
759 && !
wfMessage(
'category-move-redirect-override' )->inContentLanguage()->isDisabled()
762 wfMessage(
'category-move-redirect-override' )
763 ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
765 $contentHandler = ContentHandler::getForTitle( $this->oldTitle );
766 $redirectContent = $contentHandler->makeRedirectContent( $nt,
767 wfMessage(
'move-redirect-text' )->inContentLanguage()->plain() );
772 $redirectContent =
null;
776 $oldDefault = ContentHandler::getDefaultModelFor( $this->oldTitle );
777 $contentModel = $this->oldTitle->getContentModel();
778 $newDefault = ContentHandler::getDefaultModelFor( $nt );
779 $defaultContentModelChanging = ( $oldDefault !== $newDefault
780 && $oldDefault === $contentModel );
783 $oldid = $this->oldTitle->getArticleID();
784 $logTitle = clone $this->oldTitle;
787 $logEntry->setPerformer( $user );
788 $logEntry->setTarget( $logTitle );
789 $logEntry->setComment( $reason );
790 $logEntry->setParameters( [
791 '4::target' => $nt->getPrefixedText(),
792 '5::noredir' => $redirectContent ?
'0' :
'1',
796 $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
797 $comment = $formatter->getPlainActionText();
799 $comment .=
wfMessage(
'colon-separator' )->inContentLanguage()->text() . $reason;
802 $dbw = $this->loadBalancer->getConnection(
DB_MASTER );
804 $oldpage = WikiPage::factory( $this->oldTitle );
805 $oldcountable = $oldpage->isCountable();
807 $newpage = WikiPage::factory( $nt );
809 # Change the name of the target page:
810 $dbw->update(
'page',
812 'page_namespace' => $nt->getNamespace(),
813 'page_title' => $nt->getDBkey(),
815 [
'page_id' => $oldid ],
819 # Save a null revision in the page's history notifying of the move
821 if ( !is_object( $nullRevision ) ) {
822 throw new MWException(
'Failed to create null revision while moving page ID '
823 . $oldid .
' to ' . $nt->getPrefixedDBkey() );
826 $nullRevId = $nullRevision->insertOn( $dbw );
827 $logEntry->setAssociatedRevId( $nullRevId );
836 if ( !$redirectContent ) {
838 WikiPage::onArticleDelete( $this->oldTitle );
841 $this->oldTitle->resetArticleID( 0 );
842 $nt->resetArticleID( $oldid );
843 $newpage->loadPageData( WikiPage::READ_LOCKING );
845 $newpage->updateRevisionOn( $dbw, $nullRevision );
847 Hooks::run(
'NewRevisionFromEditComplete',
848 [ $newpage, $nullRevision, $nullRevision->getParentId(), $user ] );
850 $newpage->doEditUpdates( $nullRevision, $user,
851 [
'changed' =>
false,
'moved' =>
true,
'oldcountable' => $oldcountable ] );
854 if ( $defaultContentModelChanging ) {
857 [
'rev_content_model' => $contentModel ],
858 [
'rev_page' => $nt->getArticleID(),
'rev_content_model IS NULL' ],
863 WikiPage::onArticleCreate( $nt );
865 # Recreate the redirect, this time in the other direction.
866 if ( $redirectContent ) {
867 $redirectArticle = WikiPage::factory( $this->oldTitle );
868 $redirectArticle->loadFromRow(
false, WikiPage::READ_LOCKING );
869 $newid = $redirectArticle->insertOn( $dbw );
871 $this->oldTitle->resetArticleID( $newid );
873 'title' => $this->oldTitle,
875 'user_text' => $user->
getName(),
876 'user' => $user->
getId(),
877 'comment' => $comment,
878 'content' => $redirectContent ] );
879 $redirectRevId = $redirectRevision->insertOn( $dbw );
880 $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
882 Hooks::run(
'NewRevisionFromEditComplete',
883 [ $redirectArticle, $redirectRevision,
false, $user ] );
885 $redirectArticle->doEditUpdates( $redirectRevision, $user, [
'created' =>
true ] );
888 $redirectTags = $changeTags;
890 $redirectTags[] =
'mw-new-redirect';
897 $logid = $logEntry->insert();
899 $logEntry->addTags( $changeTags );
900 $logEntry->publish( $logid );
902 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 getUserPermissionsErrors, with duplicate removal e.g.
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()
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match.
static checkExtensionCompatibility(File $old, $new)
Checks if file extensions are compatible.
Class for creating new log entries and inserting them into the database.
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.
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
moveFile( $oldTitle, $newTitle)
Move a file associated with a page to a new location.
__construct(Title $oldTitle, Title $newTitle, ServiceOptions $options=null, ILoadBalancer $loadBalancer=null, NamespaceInfo $nsInfo=null, WatchedItemStoreInterface $watchedItems=null, PermissionManager $permMgr=null, RepoGroup $repoGroup=null)
Calling this directly is deprecated in 1.34.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
getCanonicalName( $index)
Returns the canonical (English) name for a given index.
hasSubpages( $index)
Does the namespace allow subpages?
Prioritized list of file repositories.
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
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.
getPrefixedText()
Get the prefixed title with spaces.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
getName()
Get the user name, or the IP of an anonymous user.
spreadAnyEditBlock()
If this user is logged-in and blocked, block any IP address they've successfully logged in from.
getId()
Get the user's ID.
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.