Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / jobqueue / jobs / DoubleRedirectJob.php
blobdd37b892f9a21face980bc8de934f589276dc939
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.
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.
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
18 * @file
21 use MediaWiki\Cache\CacheKeyHelper;
22 use MediaWiki\Linker\LinkTarget;
23 use MediaWiki\MediaWikiServices;
24 use MediaWiki\Page\PageReference;
25 use MediaWiki\Page\WikiPageFactory;
26 use MediaWiki\Parser\MagicWordFactory;
27 use MediaWiki\Revision\RevisionLookup;
28 use MediaWiki\Revision\SlotRecord;
29 use MediaWiki\Title\Title;
30 use MediaWiki\User\User;
31 use Wikimedia\Rdbms\IDBAccessObject;
33 /**
34 * Fix any double redirects after moving a page.
36 * @ingroup JobQueue
38 class DoubleRedirectJob extends Job {
39 /**
40 * @var int Max number of double redirect jobs counter.
41 * This is meant to avoid excessive memory usage. This is
42 * also used in fixDoubleRedirects.php script.
44 public const MAX_DR_JOBS_COUNTER = 10000;
46 /** @var Title The title which has changed, redirects pointing to this
47 * title are fixed
49 private $redirTitle;
51 /** @var User */
52 private static $user;
54 /** @var RevisionLookup */
55 private $revisionLookup;
57 /** @var MagicWordFactory */
58 private $magicWordFactory;
60 /** @var WikiPageFactory */
61 private $wikiPageFactory;
63 /**
64 * @param PageReference $page
65 * @param array $params Expected to contain these elements:
66 * - 'redirTitle' => string The title that changed and should be fixed.
67 * - 'reason' => string Reason for the change, can be "move" or "maintenance". Used as a suffix
68 * for the message keys "double-redirect-fixed-move" and
69 * "double-redirect-fixed-maintenance".
70 * ]
71 * @param RevisionLookup $revisionLookup
72 * @param MagicWordFactory $magicWordFactory
73 * @param WikiPageFactory $wikiPageFactory
75 public function __construct(
76 PageReference $page,
77 array $params,
78 RevisionLookup $revisionLookup,
79 MagicWordFactory $magicWordFactory,
80 WikiPageFactory $wikiPageFactory
81 ) {
82 parent::__construct( 'fixDoubleRedirect', $page, $params );
83 $this->redirTitle = Title::newFromText( $params['redirTitle'] );
84 $this->revisionLookup = $revisionLookup;
85 $this->magicWordFactory = $magicWordFactory;
86 $this->wikiPageFactory = $wikiPageFactory;
89 /**
90 * Insert jobs into the job queue to fix redirects to the given title
91 * @param string $reason The reason for the fix, see message
92 * "double-redirect-fixed-<reason>"
93 * @param LinkTarget $redirTitle The title which has changed, redirects
94 * pointing to this title are fixed
96 public static function fixRedirects( $reason, $redirTitle ) {
97 # Need to use the primary DB to get the redirect table updated in the same transaction
98 $services = MediaWikiServices::getInstance();
99 $dbw = $services->getConnectionProvider()->getPrimaryDatabase();
100 $res = $dbw->newSelectQueryBuilder()
101 ->select( [ 'page_namespace', 'page_title' ] )
102 ->from( 'redirect' )
103 ->join( 'page', null, 'page_id = rd_from' )
104 ->where( [ 'rd_namespace' => $redirTitle->getNamespace(), 'rd_title' => $redirTitle->getDBkey() ] )
105 ->andWhere( [ 'rd_interwiki' => '' ] )
106 ->caller( __METHOD__ )->fetchResultSet();
107 if ( !$res->numRows() ) {
108 return;
110 $jobs = [];
111 $jobQueueGroup = $services->getJobQueueGroup();
112 foreach ( $res as $row ) {
113 $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
114 if ( !$title || !$title->canExist() ) {
115 continue;
118 $jobs[] = new self(
119 $title,
121 'reason' => $reason,
122 'redirTitle' => $services->getTitleFormatter()
123 ->getPrefixedDBkey( $redirTitle )
125 $services->getRevisionLookup(),
126 $services->getMagicWordFactory(),
127 $services->getWikiPageFactory()
129 # Avoid excessive memory usage
130 if ( count( $jobs ) > self::MAX_DR_JOBS_COUNTER ) {
131 $jobQueueGroup->push( $jobs );
132 $jobs = [];
135 $jobQueueGroup->push( $jobs );
139 * @return bool
141 public function run() {
142 if ( !$this->redirTitle ) {
143 $this->setLastError( 'Invalid title' );
145 return false;
148 if ( !$this->title->canExist() ) {
149 // Needs a proper title for WikiPageFactory::newFromTitle and RevisionStore::getRevisionByTitle
150 $this->setLastError( 'Cannot edit title' );
152 return false;
155 $targetRev = $this->revisionLookup
156 ->getRevisionByTitle( $this->title, 0, IDBAccessObject::READ_LATEST );
157 if ( !$targetRev ) {
158 wfDebug( __METHOD__ . ": target redirect already deleted, ignoring" );
160 return true;
162 $content = $targetRev->getContent( SlotRecord::MAIN );
163 $currentDest = $content ? $content->getRedirectTarget() : null;
164 if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) {
165 wfDebug( __METHOD__ . ": Redirect has changed since the job was queued" );
167 return true;
170 // Check for a suppression tag (used e.g. in periodically archived discussions)
171 $mw = $this->magicWordFactory->get( 'staticredirect' );
172 if ( $content->matchMagicWord( $mw ) ) {
173 wfDebug( __METHOD__ . ": skipping: suppressed with __STATICREDIRECT__" );
175 return true;
178 // Find the current final destination
179 $newTitle = self::getFinalDestination( $this->redirTitle );
180 if ( !$newTitle ) {
181 wfDebug( __METHOD__ .
182 ": skipping: single redirect, circular redirect or invalid redirect destination" );
184 return true;
186 if ( $newTitle->equals( $this->redirTitle ) ) {
187 // The redirect is already right, no need to change it
188 // This can happen if the page was moved back (say after vandalism)
189 wfDebug( __METHOD__ . " : skipping, already good" );
192 // Preserve fragment (T16904)
193 $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(),
194 $currentDest->getFragment(), $newTitle->getInterwiki() );
196 // Fix the text
197 $newContent = $content->updateRedirect( $newTitle );
199 if ( $newContent->equals( $content ) ) {
200 $this->setLastError( 'Content unchanged???' );
202 return false;
205 $user = $this->getUser();
206 if ( !$user ) {
207 $this->setLastError( 'Invalid user' );
209 return false;
212 // Save it
213 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser
214 global $wgUser;
215 $oldUser = $wgUser;
216 $wgUser = $user;
217 $article = $this->wikiPageFactory->newFromTitle( $this->title );
219 // Messages: double-redirect-fixed-move, double-redirect-fixed-maintenance
220 $reason = wfMessage( 'double-redirect-fixed-' . $this->params['reason'],
221 $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText()
222 )->inContentLanguage()->text();
223 // Avoid RC flood, and use minor to avoid email notifs
224 $flags = EDIT_UPDATE | EDIT_SUPPRESS_RC | EDIT_INTERNAL | EDIT_MINOR;
225 $article->doUserEditContent( $newContent, $user, $reason, $flags );
226 $wgUser = $oldUser;
228 return true;
232 * Get the final destination of a redirect
234 * @param LinkTarget $title
236 * @return Title|false The final Title after following all redirects, or false if
237 * the page is not a redirect or the redirect loops.
239 public static function getFinalDestination( $title ) {
240 $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
242 // Circular redirect check
243 $seenTitles = [];
244 $dest = false;
246 while ( true ) {
247 $titleText = CacheKeyHelper::getKeyForPage( $title );
248 if ( isset( $seenTitles[$titleText] ) ) {
249 wfDebug( __METHOD__, "Circular redirect detected, aborting" );
251 return false;
253 $seenTitles[$titleText] = true;
255 if ( $title->isExternal() ) {
256 // If the target is interwiki, we have to break early (T42352).
257 // Otherwise it will look up a row in the local page table
258 // with the namespace/page of the interwiki target which can cause
259 // unexpected results (e.g. X -> foo:Bar -> Bar -> .. )
260 break;
262 $row = $dbw->newSelectQueryBuilder()
263 ->select( [ 'rd_namespace', 'rd_title', 'rd_interwiki' ] )
264 ->from( 'redirect' )
265 ->join( 'page', null, 'page_id = rd_from' )
266 ->where( [ 'page_namespace' => $title->getNamespace() ] )
267 ->andWhere( [ 'page_title' => $title->getDBkey() ] )
268 ->caller( __METHOD__ )->fetchRow();
269 if ( !$row ) {
270 # No redirect from here, chain terminates
271 break;
272 } else {
273 $dest = $title = Title::makeTitle(
274 $row->rd_namespace,
275 $row->rd_title,
277 $row->rd_interwiki
282 return $dest;
286 * Get a user object for doing edits, from a request-lifetime cache
287 * False will be returned if the user name specified in the
288 * 'double-redirect-fixer' message is invalid.
290 * @return User|false
292 private function getUser() {
293 if ( !self::$user ) {
294 $username = wfMessage( 'double-redirect-fixer' )->inContentLanguage()->text();
295 self::$user = User::newFromName( $username );
296 # User::newFromName() can return false on a badly configured wiki.
297 if ( self::$user && !self::$user->isRegistered() ) {
298 self::$user->addToDatabase();
302 return self::$user;