DoubleRedirectJob.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <?php
  2. /**
  3. * Job to fix double redirects after moving a page.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup JobQueue
  22. */
  23. use MediaWiki\MediaWikiServices;
  24. /**
  25. * Job to fix double redirects after moving a page
  26. *
  27. * @ingroup JobQueue
  28. */
  29. class DoubleRedirectJob extends Job {
  30. /** @var Title The title which has changed, redirects pointing to this
  31. * title are fixed
  32. */
  33. private $redirTitle;
  34. /** @var User */
  35. private static $user;
  36. /**
  37. * @param Title $title
  38. * @param array $params Expected to contain these elements:
  39. * - 'redirTitle' => string The title that changed and should be fixed.
  40. * - 'reason' => string Reason for the change, can be "move" or "maintenance". Used as a suffix
  41. * for the message keys "double-redirect-fixed-move" and
  42. * "double-redirect-fixed-maintenance".
  43. * ]
  44. */
  45. function __construct( Title $title, array $params ) {
  46. parent::__construct( 'fixDoubleRedirect', $title, $params );
  47. $this->redirTitle = Title::newFromText( $params['redirTitle'] );
  48. }
  49. /**
  50. * Insert jobs into the job queue to fix redirects to the given title
  51. * @param string $reason The reason for the fix, see message
  52. * "double-redirect-fixed-<reason>"
  53. * @param Title $redirTitle The title which has changed, redirects
  54. * pointing to this title are fixed
  55. * @param bool $destTitle Not used
  56. */
  57. public static function fixRedirects( $reason, $redirTitle, $destTitle = false ) {
  58. # Need to use the master to get the redirect table updated in the same transaction
  59. $dbw = wfGetDB( DB_MASTER );
  60. $res = $dbw->select(
  61. [ 'redirect', 'page' ],
  62. [ 'page_namespace', 'page_title' ],
  63. [
  64. 'page_id = rd_from',
  65. 'rd_namespace' => $redirTitle->getNamespace(),
  66. 'rd_title' => $redirTitle->getDBkey()
  67. ], __METHOD__ );
  68. if ( !$res->numRows() ) {
  69. return;
  70. }
  71. $jobs = [];
  72. foreach ( $res as $row ) {
  73. $title = Title::makeTitle( $row->page_namespace, $row->page_title );
  74. if ( !$title ) {
  75. continue;
  76. }
  77. $jobs[] = new self( $title, [
  78. 'reason' => $reason,
  79. 'redirTitle' => $redirTitle->getPrefixedDBkey() ] );
  80. # Avoid excessive memory usage
  81. if ( count( $jobs ) > 10000 ) {
  82. JobQueueGroup::singleton()->push( $jobs );
  83. $jobs = [];
  84. }
  85. }
  86. JobQueueGroup::singleton()->push( $jobs );
  87. }
  88. /**
  89. * @return bool
  90. */
  91. function run() {
  92. if ( !$this->redirTitle ) {
  93. $this->setLastError( 'Invalid title' );
  94. return false;
  95. }
  96. $targetRev = Revision::newFromTitle( $this->title, false, Revision::READ_LATEST );
  97. if ( !$targetRev ) {
  98. wfDebug( __METHOD__ . ": target redirect already deleted, ignoring\n" );
  99. return true;
  100. }
  101. $content = $targetRev->getContent();
  102. $currentDest = $content ? $content->getRedirectTarget() : null;
  103. if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) {
  104. wfDebug( __METHOD__ . ": Redirect has changed since the job was queued\n" );
  105. return true;
  106. }
  107. // Check for a suppression tag (used e.g. in periodically archived discussions)
  108. $mw = MediaWikiServices::getInstance()->getMagicWordFactory()->get( 'staticredirect' );
  109. if ( $content->matchMagicWord( $mw ) ) {
  110. wfDebug( __METHOD__ . ": skipping: suppressed with __STATICREDIRECT__\n" );
  111. return true;
  112. }
  113. // Find the current final destination
  114. $newTitle = self::getFinalDestination( $this->redirTitle );
  115. if ( !$newTitle ) {
  116. wfDebug( __METHOD__ .
  117. ": skipping: single redirect, circular redirect or invalid redirect destination\n" );
  118. return true;
  119. }
  120. if ( $newTitle->equals( $this->redirTitle ) ) {
  121. // The redirect is already right, no need to change it
  122. // This can happen if the page was moved back (say after vandalism)
  123. wfDebug( __METHOD__ . " : skipping, already good\n" );
  124. }
  125. // Preserve fragment (T16904)
  126. $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(),
  127. $currentDest->getFragment(), $newTitle->getInterwiki() );
  128. // Fix the text
  129. $newContent = $content->updateRedirect( $newTitle );
  130. if ( $newContent->equals( $content ) ) {
  131. $this->setLastError( 'Content unchanged???' );
  132. return false;
  133. }
  134. $user = $this->getUser();
  135. if ( !$user ) {
  136. $this->setLastError( 'Invalid user' );
  137. return false;
  138. }
  139. // Save it
  140. global $wgUser;
  141. $oldUser = $wgUser;
  142. $wgUser = $user;
  143. $article = WikiPage::factory( $this->title );
  144. // Messages: double-redirect-fixed-move, double-redirect-fixed-maintenance
  145. $reason = wfMessage( 'double-redirect-fixed-' . $this->params['reason'],
  146. $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText()
  147. )->inContentLanguage()->text();
  148. $flags = EDIT_UPDATE | EDIT_SUPPRESS_RC | EDIT_INTERNAL;
  149. $article->doEditContent( $newContent, $reason, $flags, false, $user );
  150. $wgUser = $oldUser;
  151. return true;
  152. }
  153. /**
  154. * Get the final destination of a redirect
  155. *
  156. * @param Title $title
  157. *
  158. * @return Title|bool The final Title after following all redirects, or false if
  159. * the page is not a redirect or the redirect loops.
  160. */
  161. public static function getFinalDestination( $title ) {
  162. $dbw = wfGetDB( DB_MASTER );
  163. // Circular redirect check
  164. $seenTitles = [];
  165. $dest = false;
  166. while ( true ) {
  167. $titleText = $title->getPrefixedDBkey();
  168. if ( isset( $seenTitles[$titleText] ) ) {
  169. wfDebug( __METHOD__, "Circular redirect detected, aborting\n" );
  170. return false;
  171. }
  172. $seenTitles[$titleText] = true;
  173. if ( $title->isExternal() ) {
  174. // If the target is interwiki, we have to break early (T42352).
  175. // Otherwise it will look up a row in the local page table
  176. // with the namespace/page of the interwiki target which can cause
  177. // unexpected results (e.g. X -> foo:Bar -> Bar -> .. )
  178. break;
  179. }
  180. $row = $dbw->selectRow(
  181. [ 'redirect', 'page' ],
  182. [ 'rd_namespace', 'rd_title', 'rd_interwiki' ],
  183. [
  184. 'rd_from=page_id',
  185. 'page_namespace' => $title->getNamespace(),
  186. 'page_title' => $title->getDBkey()
  187. ], __METHOD__ );
  188. if ( !$row ) {
  189. # No redirect from here, chain terminates
  190. break;
  191. } else {
  192. $dest = $title = Title::makeTitle(
  193. $row->rd_namespace,
  194. $row->rd_title,
  195. '',
  196. $row->rd_interwiki
  197. );
  198. }
  199. }
  200. return $dest;
  201. }
  202. /**
  203. * Get a user object for doing edits, from a request-lifetime cache
  204. * False will be returned if the user name specified in the
  205. * 'double-redirect-fixer' message is invalid.
  206. *
  207. * @return User|bool
  208. */
  209. function getUser() {
  210. if ( !self::$user ) {
  211. $username = wfMessage( 'double-redirect-fixer' )->inContentLanguage()->text();
  212. self::$user = User::newFromName( $username );
  213. # User::newFromName() can return false on a badly configured wiki.
  214. if ( self::$user && !self::$user->isLoggedIn() ) {
  215. self::$user->addToDatabase();
  216. }
  217. }
  218. return self::$user;
  219. }
  220. }