MovePage.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  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. call_user_func_array( [ $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. if ( $this->newTitle->getNamespace() == NS_CATEGORY ) {
  248. $type = 'subcat';
  249. } elseif ( $this->newTitle->getNamespace() == NS_FILE ) {
  250. $type = 'file';
  251. } else {
  252. $type = 'page';
  253. }
  254. foreach ( $prefixes as $prefixRow ) {
  255. $prefix = $prefixRow->cl_sortkey_prefix;
  256. $catTo = $prefixRow->cl_to;
  257. $dbw->update( 'categorylinks',
  258. [
  259. 'cl_sortkey' => Collation::singleton()->getSortKey(
  260. $this->newTitle->getCategorySortkey( $prefix ) ),
  261. 'cl_collation' => $wgCategoryCollation,
  262. 'cl_type' => $type,
  263. 'cl_timestamp=cl_timestamp' ],
  264. [
  265. 'cl_from' => $pageid,
  266. 'cl_to' => $catTo ],
  267. __METHOD__
  268. );
  269. }
  270. $redirid = $this->oldTitle->getArticleID();
  271. if ( $protected ) {
  272. # Protect the redirect title as the title used to be...
  273. $res = $dbw->select(
  274. 'page_restrictions',
  275. [ 'pr_type', 'pr_level', 'pr_cascade', 'pr_user', 'pr_expiry' ],
  276. [ 'pr_page' => $pageid ],
  277. __METHOD__,
  278. 'FOR UPDATE'
  279. );
  280. $rowsInsert = [];
  281. foreach ( $res as $row ) {
  282. $rowsInsert[] = [
  283. 'pr_page' => $redirid,
  284. 'pr_type' => $row->pr_type,
  285. 'pr_level' => $row->pr_level,
  286. 'pr_cascade' => $row->pr_cascade,
  287. 'pr_user' => $row->pr_user,
  288. 'pr_expiry' => $row->pr_expiry
  289. ];
  290. }
  291. $dbw->insert( 'page_restrictions', $rowsInsert, __METHOD__, [ 'IGNORE' ] );
  292. // Build comment for log
  293. $comment = wfMessage(
  294. 'prot_1movedto2',
  295. $this->oldTitle->getPrefixedText(),
  296. $this->newTitle->getPrefixedText()
  297. )->inContentLanguage()->text();
  298. if ( $reason ) {
  299. $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
  300. }
  301. // reread inserted pr_ids for log relation
  302. $insertedPrIds = $dbw->select(
  303. 'page_restrictions',
  304. 'pr_id',
  305. [ 'pr_page' => $redirid ],
  306. __METHOD__
  307. );
  308. $logRelationsValues = [];
  309. foreach ( $insertedPrIds as $prid ) {
  310. $logRelationsValues[] = $prid->pr_id;
  311. }
  312. // Update the protection log
  313. $logEntry = new ManualLogEntry( 'protect', 'move_prot' );
  314. $logEntry->setTarget( $this->newTitle );
  315. $logEntry->setComment( $comment );
  316. $logEntry->setPerformer( $user );
  317. $logEntry->setParameters( [
  318. '4::oldtitle' => $this->oldTitle->getPrefixedText(),
  319. ] );
  320. $logEntry->setRelations( [ 'pr_id' => $logRelationsValues ] );
  321. $logEntry->setTags( $changeTags );
  322. $logId = $logEntry->insert();
  323. $logEntry->publish( $logId );
  324. }
  325. // Update *_from_namespace fields as needed
  326. if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) {
  327. $dbw->update( 'pagelinks',
  328. [ 'pl_from_namespace' => $this->newTitle->getNamespace() ],
  329. [ 'pl_from' => $pageid ],
  330. __METHOD__
  331. );
  332. $dbw->update( 'templatelinks',
  333. [ 'tl_from_namespace' => $this->newTitle->getNamespace() ],
  334. [ 'tl_from' => $pageid ],
  335. __METHOD__
  336. );
  337. $dbw->update( 'imagelinks',
  338. [ 'il_from_namespace' => $this->newTitle->getNamespace() ],
  339. [ 'il_from' => $pageid ],
  340. __METHOD__
  341. );
  342. }
  343. # Update watchlists
  344. $oldtitle = $this->oldTitle->getDBkey();
  345. $newtitle = $this->newTitle->getDBkey();
  346. $oldsnamespace = MWNamespace::getSubject( $this->oldTitle->getNamespace() );
  347. $newsnamespace = MWNamespace::getSubject( $this->newTitle->getNamespace() );
  348. if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
  349. $store = MediaWikiServices::getInstance()->getWatchedItemStore();
  350. $store->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
  351. }
  352. Hooks::run(
  353. 'TitleMoveCompleting',
  354. [ $this->oldTitle, $this->newTitle,
  355. $user, $pageid, $redirid, $reason, $nullRevision ]
  356. );
  357. $dbw->endAtomic( __METHOD__ );
  358. $params = [
  359. &$this->oldTitle,
  360. &$this->newTitle,
  361. &$user,
  362. $pageid,
  363. $redirid,
  364. $reason,
  365. $nullRevision
  366. ];
  367. // Keep each single hook handler atomic
  368. DeferredUpdates::addUpdate(
  369. new AtomicSectionUpdate(
  370. $dbw,
  371. __METHOD__,
  372. // Hold onto $user to avoid HHVM bug where it no longer
  373. // becomes a reference (T118683)
  374. function () use ( $params, &$user ) {
  375. Hooks::run( 'TitleMoveComplete', $params );
  376. }
  377. )
  378. );
  379. return Status::newGood();
  380. }
  381. /**
  382. * Move page to a title which is either a redirect to the
  383. * source page or nonexistent
  384. *
  385. * @todo This was basically directly moved from Title, it should be split into
  386. * smaller functions
  387. * @param User $user the User doing the move
  388. * @param Title $nt The page to move to, which should be a redirect or non-existent
  389. * @param string $reason The reason for the move
  390. * @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
  391. * if the user has the suppressredirect right
  392. * @param string[] $changeTags Change tags to apply to the entry in the move log
  393. * @return Revision the revision created by the move
  394. * @throws MWException
  395. */
  396. private function moveToInternal( User $user, &$nt, $reason = '', $createRedirect = true,
  397. array $changeTags = []
  398. ) {
  399. if ( $nt->exists() ) {
  400. $moveOverRedirect = true;
  401. $logType = 'move_redir';
  402. } else {
  403. $moveOverRedirect = false;
  404. $logType = 'move';
  405. }
  406. if ( $moveOverRedirect ) {
  407. $overwriteMessage = wfMessage(
  408. 'delete_and_move_reason',
  409. $this->oldTitle->getPrefixedText()
  410. )->inContentLanguage()->text();
  411. $newpage = WikiPage::factory( $nt );
  412. $errs = [];
  413. $status = $newpage->doDeleteArticleReal(
  414. $overwriteMessage,
  415. /* $suppress */ false,
  416. $nt->getArticleID(),
  417. /* $commit */ false,
  418. $errs,
  419. $user,
  420. $changeTags,
  421. 'delete_redir'
  422. );
  423. if ( !$status->isGood() ) {
  424. throw new MWException( 'Failed to delete page-move revision: ' . $status );
  425. }
  426. $nt->resetArticleID( false );
  427. }
  428. if ( $createRedirect ) {
  429. if ( $this->oldTitle->getNamespace() == NS_CATEGORY
  430. && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
  431. ) {
  432. $redirectContent = new WikitextContent(
  433. wfMessage( 'category-move-redirect-override' )
  434. ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
  435. } else {
  436. $contentHandler = ContentHandler::getForTitle( $this->oldTitle );
  437. $redirectContent = $contentHandler->makeRedirectContent( $nt,
  438. wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
  439. }
  440. // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
  441. } else {
  442. $redirectContent = null;
  443. }
  444. // Figure out whether the content model is no longer the default
  445. $oldDefault = ContentHandler::getDefaultModelFor( $this->oldTitle );
  446. $contentModel = $this->oldTitle->getContentModel();
  447. $newDefault = ContentHandler::getDefaultModelFor( $nt );
  448. $defaultContentModelChanging = ( $oldDefault !== $newDefault
  449. && $oldDefault === $contentModel );
  450. // T59084: log_page should be the ID of the *moved* page
  451. $oldid = $this->oldTitle->getArticleID();
  452. $logTitle = clone $this->oldTitle;
  453. $logEntry = new ManualLogEntry( 'move', $logType );
  454. $logEntry->setPerformer( $user );
  455. $logEntry->setTarget( $logTitle );
  456. $logEntry->setComment( $reason );
  457. $logEntry->setParameters( [
  458. '4::target' => $nt->getPrefixedText(),
  459. '5::noredir' => $redirectContent ? '0' : '1',
  460. ] );
  461. $formatter = LogFormatter::newFromEntry( $logEntry );
  462. $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
  463. $comment = $formatter->getPlainActionText();
  464. if ( $reason ) {
  465. $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
  466. }
  467. $dbw = wfGetDB( DB_MASTER );
  468. $oldpage = WikiPage::factory( $this->oldTitle );
  469. $oldcountable = $oldpage->isCountable();
  470. $newpage = WikiPage::factory( $nt );
  471. # Save a null revision in the page's history notifying of the move
  472. $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $user );
  473. if ( !is_object( $nullRevision ) ) {
  474. throw new MWException( 'No valid null revision produced in ' . __METHOD__ );
  475. }
  476. $nullRevId = $nullRevision->insertOn( $dbw );
  477. $logEntry->setAssociatedRevId( $nullRevId );
  478. # Change the name of the target page:
  479. $dbw->update( 'page',
  480. /* SET */ [
  481. 'page_namespace' => $nt->getNamespace(),
  482. 'page_title' => $nt->getDBkey(),
  483. ],
  484. /* WHERE */ [ 'page_id' => $oldid ],
  485. __METHOD__
  486. );
  487. if ( !$redirectContent ) {
  488. // Clean up the old title *before* reset article id - T47348
  489. WikiPage::onArticleDelete( $this->oldTitle );
  490. }
  491. $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
  492. $nt->resetArticleID( $oldid );
  493. $newpage->loadPageData( WikiPage::READ_LOCKING ); // T48397
  494. $newpage->updateRevisionOn( $dbw, $nullRevision );
  495. Hooks::run( 'NewRevisionFromEditComplete',
  496. [ $newpage, $nullRevision, $nullRevision->getParentId(), $user ] );
  497. $newpage->doEditUpdates( $nullRevision, $user,
  498. [ 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ] );
  499. // If the default content model changes, we need to populate rev_content_model
  500. if ( $defaultContentModelChanging ) {
  501. $dbw->update(
  502. 'revision',
  503. [ 'rev_content_model' => $contentModel ],
  504. [ 'rev_page' => $nt->getArticleID(), 'rev_content_model IS NULL' ],
  505. __METHOD__
  506. );
  507. }
  508. WikiPage::onArticleCreate( $nt );
  509. # Recreate the redirect, this time in the other direction.
  510. if ( $redirectContent ) {
  511. $redirectArticle = WikiPage::factory( $this->oldTitle );
  512. $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // T48397
  513. $newid = $redirectArticle->insertOn( $dbw );
  514. if ( $newid ) { // sanity
  515. $this->oldTitle->resetArticleID( $newid );
  516. $redirectRevision = new Revision( [
  517. 'title' => $this->oldTitle, // for determining the default content model
  518. 'page' => $newid,
  519. 'user_text' => $user->getName(),
  520. 'user' => $user->getId(),
  521. 'comment' => $comment,
  522. 'content' => $redirectContent ] );
  523. $redirectRevId = $redirectRevision->insertOn( $dbw );
  524. $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
  525. Hooks::run( 'NewRevisionFromEditComplete',
  526. [ $redirectArticle, $redirectRevision, false, $user ] );
  527. $redirectArticle->doEditUpdates( $redirectRevision, $user, [ 'created' => true ] );
  528. // make a copy because of log entry below
  529. $redirectTags = $changeTags;
  530. if ( in_array( 'mw-new-redirect', ChangeTags::getSoftwareTags() ) ) {
  531. $redirectTags[] = 'mw-new-redirect';
  532. }
  533. ChangeTags::addTags( $redirectTags, null, $redirectRevId, null );
  534. }
  535. }
  536. # Log the move
  537. $logid = $logEntry->insert();
  538. $logEntry->setTags( $changeTags );
  539. $logEntry->publish( $logid );
  540. return $nullRevision;
  541. }
  542. }