MovePage.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @file
  19. */
  20. use MediaWiki\MediaWikiServices;
  21. /**
  22. * Handles the backend logic of moving a page from one title
  23. * to another.
  24. *
  25. * @since 1.24
  26. */
  27. class MovePage {
  28. /**
  29. * @var Title
  30. */
  31. protected $oldTitle;
  32. /**
  33. * @var Title
  34. */
  35. protected $newTitle;
  36. public function __construct( Title $oldTitle, Title $newTitle ) {
  37. $this->oldTitle = $oldTitle;
  38. $this->newTitle = $newTitle;
  39. }
  40. public function checkPermissions( User $user, $reason ) {
  41. $status = new Status();
  42. $errors = wfMergeErrorArrays(
  43. $this->oldTitle->getUserPermissionsErrors( 'move', $user ),
  44. $this->oldTitle->getUserPermissionsErrors( 'edit', $user ),
  45. $this->newTitle->getUserPermissionsErrors( 'move-target', $user ),
  46. $this->newTitle->getUserPermissionsErrors( 'edit', $user )
  47. );
  48. // Convert into a Status object
  49. if ( $errors ) {
  50. foreach ( $errors as $error ) {
  51. $status->fatal( ...$error );
  52. }
  53. }
  54. if ( EditPage::matchSummarySpamRegex( $reason ) !== false ) {
  55. // This is kind of lame, won't display nice
  56. $status->fatal( 'spamprotectiontext' );
  57. }
  58. $tp = $this->newTitle->getTitleProtection();
  59. if ( $tp !== false && !$user->isAllowed( $tp['permission'] ) ) {
  60. $status->fatal( 'cantmove-titleprotected' );
  61. }
  62. Hooks::run( 'MovePageCheckPermissions',
  63. [ $this->oldTitle, $this->newTitle, $user, $reason, $status ]
  64. );
  65. return $status;
  66. }
  67. /**
  68. * Does various sanity checks that the move is
  69. * valid. Only things based on the two titles
  70. * should be checked here.
  71. *
  72. * @return Status
  73. */
  74. public function isValidMove() {
  75. global $wgContentHandlerUseDB;
  76. $status = new Status();
  77. if ( $this->oldTitle->equals( $this->newTitle ) ) {
  78. $status->fatal( 'selfmove' );
  79. }
  80. if ( !$this->oldTitle->isMovable() ) {
  81. $status->fatal( 'immobile-source-namespace', $this->oldTitle->getNsText() );
  82. }
  83. if ( $this->newTitle->isExternal() ) {
  84. $status->fatal( 'immobile-target-namespace-iw' );
  85. }
  86. if ( !$this->newTitle->isMovable() ) {
  87. $status->fatal( 'immobile-target-namespace', $this->newTitle->getNsText() );
  88. }
  89. $oldid = $this->oldTitle->getArticleID();
  90. if ( strlen( $this->newTitle->getDBkey() ) < 1 ) {
  91. $status->fatal( 'articleexists' );
  92. }
  93. if (
  94. ( $this->oldTitle->getDBkey() == '' ) ||
  95. ( !$oldid ) ||
  96. ( $this->newTitle->getDBkey() == '' )
  97. ) {
  98. $status->fatal( 'badarticleerror' );
  99. }
  100. # The move is allowed only if (1) the target doesn't exist, or
  101. # (2) the target is a redirect to the source, and has no history
  102. # (so we can undo bad moves right after they're done).
  103. if ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) {
  104. $status->fatal( 'articleexists' );
  105. }
  106. // Content model checks
  107. if ( !$wgContentHandlerUseDB &&
  108. $this->oldTitle->getContentModel() !== $this->newTitle->getContentModel() ) {
  109. // can't move a page if that would change the page's content model
  110. $status->fatal(
  111. 'bad-target-model',
  112. ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
  113. ContentHandler::getLocalizedName( $this->newTitle->getContentModel() )
  114. );
  115. } elseif (
  116. !ContentHandler::getForTitle( $this->oldTitle )->canBeUsedOn( $this->newTitle )
  117. ) {
  118. $status->fatal(
  119. 'content-not-allowed-here',
  120. ContentHandler::getLocalizedName( $this->oldTitle->getContentModel() ),
  121. $this->newTitle->getPrefixedText()
  122. );
  123. }
  124. // Image-specific checks
  125. if ( $this->oldTitle->inNamespace( NS_FILE ) ) {
  126. $status->merge( $this->isValidFileMove() );
  127. }
  128. if ( $this->newTitle->inNamespace( NS_FILE ) && !$this->oldTitle->inNamespace( NS_FILE ) ) {
  129. $status->fatal( 'nonfile-cannot-move-to-file' );
  130. }
  131. // Hook for extensions to say a title can't be moved for technical reasons
  132. Hooks::run( 'MovePageIsValidMove', [ $this->oldTitle, $this->newTitle, $status ] );
  133. return $status;
  134. }
  135. /**
  136. * Sanity checks for when a file is being moved
  137. *
  138. * @return Status
  139. */
  140. protected function isValidFileMove() {
  141. $status = new Status();
  142. $file = wfLocalFile( $this->oldTitle );
  143. $file->load( File::READ_LATEST );
  144. if ( $file->exists() ) {
  145. if ( $this->newTitle->getText() != wfStripIllegalFilenameChars( $this->newTitle->getText() ) ) {
  146. $status->fatal( 'imageinvalidfilename' );
  147. }
  148. if ( !File::checkExtensionCompatibility( $file, $this->newTitle->getDBkey() ) ) {
  149. $status->fatal( 'imagetypemismatch' );
  150. }
  151. }
  152. if ( !$this->newTitle->inNamespace( NS_FILE ) ) {
  153. $status->fatal( 'imagenocrossnamespace' );
  154. }
  155. return $status;
  156. }
  157. /**
  158. * Checks if $this can be moved to a given Title
  159. * - Selects for update, so don't call it unless you mean business
  160. *
  161. * @since 1.25
  162. * @return bool
  163. */
  164. protected function isValidMoveTarget() {
  165. # Is it an existing file?
  166. if ( $this->newTitle->inNamespace( NS_FILE ) ) {
  167. $file = wfLocalFile( $this->newTitle );
  168. $file->load( File::READ_LATEST );
  169. if ( $file->exists() ) {
  170. wfDebug( __METHOD__ . ": file exists\n" );
  171. return false;
  172. }
  173. }
  174. # Is it a redirect with no history?
  175. if ( !$this->newTitle->isSingleRevRedirect() ) {
  176. wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
  177. return false;
  178. }
  179. # Get the article text
  180. $rev = Revision::newFromTitle( $this->newTitle, false, Revision::READ_LATEST );
  181. if ( !is_object( $rev ) ) {
  182. return false;
  183. }
  184. $content = $rev->getContent();
  185. # Does the redirect point to the source?
  186. # Or is it a broken self-redirect, usually caused by namespace collisions?
  187. $redirTitle = $content ? $content->getRedirectTarget() : null;
  188. if ( $redirTitle ) {
  189. if ( $redirTitle->getPrefixedDBkey() !== $this->oldTitle->getPrefixedDBkey() &&
  190. $redirTitle->getPrefixedDBkey() !== $this->newTitle->getPrefixedDBkey() ) {
  191. wfDebug( __METHOD__ . ": redirect points to other page\n" );
  192. return false;
  193. } else {
  194. return true;
  195. }
  196. } else {
  197. # Fail safe (not a redirect after all. strange.)
  198. wfDebug( __METHOD__ . ": failsafe: database says " . $this->newTitle->getPrefixedDBkey() .
  199. " is a redirect, but it doesn't contain a valid redirect.\n" );
  200. return false;
  201. }
  202. }
  203. /**
  204. * @param User $user
  205. * @param string $reason
  206. * @param bool $createRedirect
  207. * @param string[] $changeTags Change tags to apply to the entry in the move log. Caller
  208. * should perform permission checks with ChangeTags::canAddTagsAccompanyingChange
  209. * @return Status
  210. */
  211. public function move( User $user, $reason, $createRedirect, array $changeTags = [] ) {
  212. global $wgCategoryCollation;
  213. Hooks::run( 'TitleMove', [ $this->oldTitle, $this->newTitle, $user ] );
  214. // If it is a file, move it first.
  215. // It is done before all other moving stuff is done because it's hard to revert.
  216. $dbw = wfGetDB( DB_MASTER );
  217. if ( $this->oldTitle->getNamespace() == NS_FILE ) {
  218. $file = wfLocalFile( $this->oldTitle );
  219. $file->load( File::READ_LATEST );
  220. if ( $file->exists() ) {
  221. $status = $file->move( $this->newTitle );
  222. if ( !$status->isOK() ) {
  223. return $status;
  224. }
  225. }
  226. // Clear RepoGroup process cache
  227. RepoGroup::singleton()->clearCache( $this->oldTitle );
  228. RepoGroup::singleton()->clearCache( $this->newTitle ); # clear false negative cache
  229. }
  230. $dbw->startAtomic( __METHOD__ );
  231. Hooks::run( 'TitleMoveStarting', [ $this->oldTitle, $this->newTitle, $user ] );
  232. $pageid = $this->oldTitle->getArticleID( Title::GAID_FOR_UPDATE );
  233. $protected = $this->oldTitle->isProtected();
  234. // Do the actual move; if this fails, it will throw an MWException(!)
  235. $nullRevision = $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect,
  236. $changeTags );
  237. // Refresh the sortkey for this row. Be careful to avoid resetting
  238. // cl_timestamp, which may disturb time-based lists on some sites.
  239. // @todo This block should be killed, it's duplicating code
  240. // from LinksUpdate::getCategoryInsertions() and friends.
  241. $prefixes = $dbw->select(
  242. 'categorylinks',
  243. [ 'cl_sortkey_prefix', 'cl_to' ],
  244. [ 'cl_from' => $pageid ],
  245. __METHOD__
  246. );
  247. $type = MWNamespace::getCategoryLinkType( $this->newTitle->getNamespace() );
  248. foreach ( $prefixes as $prefixRow ) {
  249. $prefix = $prefixRow->cl_sortkey_prefix;
  250. $catTo = $prefixRow->cl_to;
  251. $dbw->update( 'categorylinks',
  252. [
  253. 'cl_sortkey' => Collation::singleton()->getSortKey(
  254. $this->newTitle->getCategorySortkey( $prefix ) ),
  255. 'cl_collation' => $wgCategoryCollation,
  256. 'cl_type' => $type,
  257. 'cl_timestamp=cl_timestamp' ],
  258. [
  259. 'cl_from' => $pageid,
  260. 'cl_to' => $catTo ],
  261. __METHOD__
  262. );
  263. }
  264. $redirid = $this->oldTitle->getArticleID();
  265. if ( $protected ) {
  266. # Protect the redirect title as the title used to be...
  267. $res = $dbw->select(
  268. 'page_restrictions',
  269. [ 'pr_type', 'pr_level', 'pr_cascade', 'pr_user', 'pr_expiry' ],
  270. [ 'pr_page' => $pageid ],
  271. __METHOD__,
  272. 'FOR UPDATE'
  273. );
  274. $rowsInsert = [];
  275. foreach ( $res as $row ) {
  276. $rowsInsert[] = [
  277. 'pr_page' => $redirid,
  278. 'pr_type' => $row->pr_type,
  279. 'pr_level' => $row->pr_level,
  280. 'pr_cascade' => $row->pr_cascade,
  281. 'pr_user' => $row->pr_user,
  282. 'pr_expiry' => $row->pr_expiry
  283. ];
  284. }
  285. $dbw->insert( 'page_restrictions', $rowsInsert, __METHOD__, [ 'IGNORE' ] );
  286. // Build comment for log
  287. $comment = wfMessage(
  288. 'prot_1movedto2',
  289. $this->oldTitle->getPrefixedText(),
  290. $this->newTitle->getPrefixedText()
  291. )->inContentLanguage()->text();
  292. if ( $reason ) {
  293. $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
  294. }
  295. // reread inserted pr_ids for log relation
  296. $insertedPrIds = $dbw->select(
  297. 'page_restrictions',
  298. 'pr_id',
  299. [ 'pr_page' => $redirid ],
  300. __METHOD__
  301. );
  302. $logRelationsValues = [];
  303. foreach ( $insertedPrIds as $prid ) {
  304. $logRelationsValues[] = $prid->pr_id;
  305. }
  306. // Update the protection log
  307. $logEntry = new ManualLogEntry( 'protect', 'move_prot' );
  308. $logEntry->setTarget( $this->newTitle );
  309. $logEntry->setComment( $comment );
  310. $logEntry->setPerformer( $user );
  311. $logEntry->setParameters( [
  312. '4::oldtitle' => $this->oldTitle->getPrefixedText(),
  313. ] );
  314. $logEntry->setRelations( [ 'pr_id' => $logRelationsValues ] );
  315. $logEntry->setTags( $changeTags );
  316. $logId = $logEntry->insert();
  317. $logEntry->publish( $logId );
  318. }
  319. // Update *_from_namespace fields as needed
  320. if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) {
  321. $dbw->update( 'pagelinks',
  322. [ 'pl_from_namespace' => $this->newTitle->getNamespace() ],
  323. [ 'pl_from' => $pageid ],
  324. __METHOD__
  325. );
  326. $dbw->update( 'templatelinks',
  327. [ 'tl_from_namespace' => $this->newTitle->getNamespace() ],
  328. [ 'tl_from' => $pageid ],
  329. __METHOD__
  330. );
  331. $dbw->update( 'imagelinks',
  332. [ 'il_from_namespace' => $this->newTitle->getNamespace() ],
  333. [ 'il_from' => $pageid ],
  334. __METHOD__
  335. );
  336. }
  337. # Update watchlists
  338. $oldtitle = $this->oldTitle->getDBkey();
  339. $newtitle = $this->newTitle->getDBkey();
  340. $oldsnamespace = MWNamespace::getSubject( $this->oldTitle->getNamespace() );
  341. $newsnamespace = MWNamespace::getSubject( $this->newTitle->getNamespace() );
  342. if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
  343. $store = MediaWikiServices::getInstance()->getWatchedItemStore();
  344. $store->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
  345. }
  346. Hooks::run(
  347. 'TitleMoveCompleting',
  348. [ $this->oldTitle, $this->newTitle,
  349. $user, $pageid, $redirid, $reason, $nullRevision ]
  350. );
  351. $dbw->endAtomic( __METHOD__ );
  352. $params = [
  353. &$this->oldTitle,
  354. &$this->newTitle,
  355. &$user,
  356. $pageid,
  357. $redirid,
  358. $reason,
  359. $nullRevision
  360. ];
  361. // Keep each single hook handler atomic
  362. DeferredUpdates::addUpdate(
  363. new AtomicSectionUpdate(
  364. $dbw,
  365. __METHOD__,
  366. // Hold onto $user to avoid HHVM bug where it no longer
  367. // becomes a reference (T118683)
  368. function () use ( $params, &$user ) {
  369. Hooks::run( 'TitleMoveComplete', $params );
  370. }
  371. )
  372. );
  373. return Status::newGood();
  374. }
  375. /**
  376. * Move page to a title which is either a redirect to the
  377. * source page or nonexistent
  378. *
  379. * @todo This was basically directly moved from Title, it should be split into
  380. * smaller functions
  381. * @param User $user the User doing the move
  382. * @param Title $nt The page to move to, which should be a redirect or non-existent
  383. * @param string $reason The reason for the move
  384. * @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
  385. * if the user has the suppressredirect right
  386. * @param string[] $changeTags Change tags to apply to the entry in the move log
  387. * @return Revision the revision created by the move
  388. * @throws MWException
  389. */
  390. private function moveToInternal( User $user, &$nt, $reason = '', $createRedirect = true,
  391. array $changeTags = []
  392. ) {
  393. if ( $nt->exists() ) {
  394. $moveOverRedirect = true;
  395. $logType = 'move_redir';
  396. } else {
  397. $moveOverRedirect = false;
  398. $logType = 'move';
  399. }
  400. if ( $moveOverRedirect ) {
  401. $overwriteMessage = wfMessage(
  402. 'delete_and_move_reason',
  403. $this->oldTitle->getPrefixedText()
  404. )->inContentLanguage()->text();
  405. $newpage = WikiPage::factory( $nt );
  406. $errs = [];
  407. $status = $newpage->doDeleteArticleReal(
  408. $overwriteMessage,
  409. /* $suppress */ false,
  410. $nt->getArticleID(),
  411. /* $commit */ false,
  412. $errs,
  413. $user,
  414. $changeTags,
  415. 'delete_redir'
  416. );
  417. if ( !$status->isGood() ) {
  418. throw new MWException( 'Failed to delete page-move revision: ' . $status );
  419. }
  420. $nt->resetArticleID( false );
  421. }
  422. if ( $createRedirect ) {
  423. if ( $this->oldTitle->getNamespace() == NS_CATEGORY
  424. && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
  425. ) {
  426. $redirectContent = new WikitextContent(
  427. wfMessage( 'category-move-redirect-override' )
  428. ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
  429. } else {
  430. $contentHandler = ContentHandler::getForTitle( $this->oldTitle );
  431. $redirectContent = $contentHandler->makeRedirectContent( $nt,
  432. wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
  433. }
  434. // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
  435. } else {
  436. $redirectContent = null;
  437. }
  438. // Figure out whether the content model is no longer the default
  439. $oldDefault = ContentHandler::getDefaultModelFor( $this->oldTitle );
  440. $contentModel = $this->oldTitle->getContentModel();
  441. $newDefault = ContentHandler::getDefaultModelFor( $nt );
  442. $defaultContentModelChanging = ( $oldDefault !== $newDefault
  443. && $oldDefault === $contentModel );
  444. // T59084: log_page should be the ID of the *moved* page
  445. $oldid = $this->oldTitle->getArticleID();
  446. $logTitle = clone $this->oldTitle;
  447. $logEntry = new ManualLogEntry( 'move', $logType );
  448. $logEntry->setPerformer( $user );
  449. $logEntry->setTarget( $logTitle );
  450. $logEntry->setComment( $reason );
  451. $logEntry->setParameters( [
  452. '4::target' => $nt->getPrefixedText(),
  453. '5::noredir' => $redirectContent ? '0' : '1',
  454. ] );
  455. $formatter = LogFormatter::newFromEntry( $logEntry );
  456. $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
  457. $comment = $formatter->getPlainActionText();
  458. if ( $reason ) {
  459. $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
  460. }
  461. $dbw = wfGetDB( DB_MASTER );
  462. $oldpage = WikiPage::factory( $this->oldTitle );
  463. $oldcountable = $oldpage->isCountable();
  464. $newpage = WikiPage::factory( $nt );
  465. # Change the name of the target page:
  466. $dbw->update( 'page',
  467. /* SET */ [
  468. 'page_namespace' => $nt->getNamespace(),
  469. 'page_title' => $nt->getDBkey(),
  470. ],
  471. /* WHERE */ [ 'page_id' => $oldid ],
  472. __METHOD__
  473. );
  474. # Save a null revision in the page's history notifying of the move
  475. $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $user );
  476. if ( !is_object( $nullRevision ) ) {
  477. throw new MWException( 'No valid null revision produced in ' . __METHOD__ );
  478. }
  479. $nullRevId = $nullRevision->insertOn( $dbw );
  480. $logEntry->setAssociatedRevId( $nullRevId );
  481. if ( !$redirectContent ) {
  482. // Clean up the old title *before* reset article id - T47348
  483. WikiPage::onArticleDelete( $this->oldTitle );
  484. }
  485. $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
  486. $nt->resetArticleID( $oldid );
  487. $newpage->loadPageData( WikiPage::READ_LOCKING ); // T48397
  488. $newpage->updateRevisionOn( $dbw, $nullRevision );
  489. Hooks::run( 'NewRevisionFromEditComplete',
  490. [ $newpage, $nullRevision, $nullRevision->getParentId(), $user ] );
  491. $newpage->doEditUpdates( $nullRevision, $user,
  492. [ 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ] );
  493. // If the default content model changes, we need to populate rev_content_model
  494. if ( $defaultContentModelChanging ) {
  495. $dbw->update(
  496. 'revision',
  497. [ 'rev_content_model' => $contentModel ],
  498. [ 'rev_page' => $nt->getArticleID(), 'rev_content_model IS NULL' ],
  499. __METHOD__
  500. );
  501. }
  502. WikiPage::onArticleCreate( $nt );
  503. # Recreate the redirect, this time in the other direction.
  504. if ( $redirectContent ) {
  505. $redirectArticle = WikiPage::factory( $this->oldTitle );
  506. $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // T48397
  507. $newid = $redirectArticle->insertOn( $dbw );
  508. if ( $newid ) { // sanity
  509. $this->oldTitle->resetArticleID( $newid );
  510. $redirectRevision = new Revision( [
  511. 'title' => $this->oldTitle, // for determining the default content model
  512. 'page' => $newid,
  513. 'user_text' => $user->getName(),
  514. 'user' => $user->getId(),
  515. 'comment' => $comment,
  516. 'content' => $redirectContent ] );
  517. $redirectRevId = $redirectRevision->insertOn( $dbw );
  518. $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
  519. Hooks::run( 'NewRevisionFromEditComplete',
  520. [ $redirectArticle, $redirectRevision, false, $user ] );
  521. $redirectArticle->doEditUpdates( $redirectRevision, $user, [ 'created' => true ] );
  522. // make a copy because of log entry below
  523. $redirectTags = $changeTags;
  524. if ( in_array( 'mw-new-redirect', ChangeTags::getSoftwareTags() ) ) {
  525. $redirectTags[] = 'mw-new-redirect';
  526. }
  527. ChangeTags::addTags( $redirectTags, null, $redirectRevId, null );
  528. }
  529. }
  530. # Log the move
  531. $logid = $logEntry->insert();
  532. $logEntry->setTags( $changeTags );
  533. $logEntry->publish( $logid );
  534. return $nullRevision;
  535. }
  536. }