MediaWiki master
DoubleRedirectJob.php
Go to the documentation of this file.
1<?php
8
21
27class DoubleRedirectJob extends Job {
33 public const MAX_DR_JOBS_COUNTER = 10000;
34
38 private $redirTitle;
39
41 private static $user;
42
44 private $revisionLookup;
45
47 private $magicWordFactory;
48
50 private $wikiPageFactory;
51
64 public function __construct(
65 PageReference $page,
66 array $params,
67 RevisionLookup $revisionLookup,
68 MagicWordFactory $magicWordFactory,
69 WikiPageFactory $wikiPageFactory
70 ) {
71 parent::__construct( 'fixDoubleRedirect', $page, $params );
72 $this->redirTitle = Title::newFromText( $params['redirTitle'] );
73 $this->revisionLookup = $revisionLookup;
74 $this->magicWordFactory = $magicWordFactory;
75 $this->wikiPageFactory = $wikiPageFactory;
76 }
77
85 public static function fixRedirects( $reason, $redirTitle ) {
86 # Need to use the primary DB to get the redirect table updated in the same transaction
88 $dbw = $services->getConnectionProvider()->getPrimaryDatabase();
89 $res = $dbw->newSelectQueryBuilder()
90 ->select( [ 'page_namespace', 'page_title' ] )
91 ->from( 'redirect' )
92 ->join( 'page', null, 'page_id = rd_from' )
93 ->where( [ 'rd_namespace' => $redirTitle->getNamespace(), 'rd_title' => $redirTitle->getDBkey() ] )
94 ->andWhere( [ 'rd_interwiki' => '' ] )
95 ->caller( __METHOD__ )->fetchResultSet();
96 if ( !$res->numRows() ) {
97 return;
98 }
99 $jobs = [];
100 $jobQueueGroup = $services->getJobQueueGroup();
101 foreach ( $res as $row ) {
102 $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
103 if ( !$title || !$title->canExist() ) {
104 continue;
105 }
106
107 $jobs[] = new self(
108 $title,
109 [
110 'reason' => $reason,
111 'redirTitle' => $services->getTitleFormatter()
112 ->getPrefixedDBkey( $redirTitle )
113 ],
114 $services->getRevisionLookup(),
115 $services->getMagicWordFactory(),
116 $services->getWikiPageFactory()
117 );
118 # Avoid excessive memory usage
119 if ( count( $jobs ) > self::MAX_DR_JOBS_COUNTER ) {
120 $jobQueueGroup->push( $jobs );
121 $jobs = [];
122 }
123 }
124 $jobQueueGroup->push( $jobs );
125 }
126
130 public function run() {
131 if ( !$this->redirTitle ) {
132 $this->setLastError( 'Invalid title' );
133
134 return false;
135 }
136
137 if ( !$this->title->canExist() ) {
138 // Needs a proper title for WikiPageFactory::newFromTitle and RevisionStore::getRevisionByTitle
139 $this->setLastError( 'Cannot edit title' );
140
141 return false;
142 }
143
144 $targetRev = $this->revisionLookup
145 ->getRevisionByTitle( $this->title, 0, IDBAccessObject::READ_LATEST );
146 if ( !$targetRev ) {
147 wfDebug( __METHOD__ . ": target redirect already deleted, ignoring" );
148
149 return true;
150 }
151 $content = $targetRev->getContent( SlotRecord::MAIN );
152 $currentDest = $content ? $content->getRedirectTarget() : null;
153 if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) {
154 wfDebug( __METHOD__ . ": Redirect has changed since the job was queued" );
155
156 return true;
157 }
158
159 // Check for a suppression tag (used e.g. in periodically archived discussions)
160 $mw = $this->magicWordFactory->get( 'staticredirect' );
161 if ( $content->matchMagicWord( $mw ) ) {
162 wfDebug( __METHOD__ . ": skipping: suppressed with __STATICREDIRECT__" );
163
164 return true;
165 }
166
167 // Find the current final destination
168 $newTitle = self::getFinalDestination( $this->redirTitle );
169 if ( !$newTitle ) {
170 wfDebug( __METHOD__ .
171 ": skipping: single redirect, circular redirect or invalid redirect destination" );
172
173 return true;
174 }
175 if ( $newTitle->equals( $this->redirTitle ) ) {
176 // The redirect is already right, no need to change it
177 // This can happen if the page was moved back (say after vandalism)
178 wfDebug( __METHOD__ . " : skipping, already good" );
179 }
180
181 // Preserve fragment (T16904)
182 $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(),
183 $currentDest->getFragment(), $newTitle->getInterwiki() );
184
185 // Fix the text
186 $newContent = $content->updateRedirect( $newTitle );
187
188 if ( $newContent->equals( $content ) ) {
189 $this->setLastError( 'Content unchanged???' );
190
191 return false;
192 }
193
194 $user = $this->getUser();
195 if ( !$user ) {
196 $this->setLastError( 'Invalid user' );
197
198 return false;
199 }
200
201 // Save it
202 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser
203 global $wgUser;
204 $oldUser = $wgUser;
205 $wgUser = $user;
206 $article = $this->wikiPageFactory->newFromTitle( $this->title );
207
208 // Messages: double-redirect-fixed-move, double-redirect-fixed-maintenance
209 $reason = wfMessage( 'double-redirect-fixed-' . $this->params['reason'],
210 $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText()
211 )->inContentLanguage()->text();
212 // Avoid RC flood, and use minor to avoid email notifs
214 $article->doUserEditContent( $newContent, $user, $reason, $flags );
215 $wgUser = $oldUser;
216
217 return true;
218 }
219
228 public static function getFinalDestination( $title ) {
229 $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
230
231 // Circular redirect check
232 $seenTitles = [];
233 $dest = false;
234
235 while ( true ) {
236 $titleText = CacheKeyHelper::getKeyForPage( $title );
237 if ( isset( $seenTitles[$titleText] ) ) {
238 wfDebug( __METHOD__, "Circular redirect detected, aborting" );
239
240 return false;
241 }
242 $seenTitles[$titleText] = true;
243
244 if ( $title->isExternal() ) {
245 // If the target is interwiki, we have to break early (T42352).
246 // Otherwise it will look up a row in the local page table
247 // with the namespace/page of the interwiki target which can cause
248 // unexpected results (e.g. X -> foo:Bar -> Bar -> .. )
249 break;
250 }
251 $row = $dbw->newSelectQueryBuilder()
252 ->select( [ 'rd_namespace', 'rd_title', 'rd_interwiki' ] )
253 ->from( 'redirect' )
254 ->join( 'page', null, 'page_id = rd_from' )
255 ->where( [ 'page_namespace' => $title->getNamespace() ] )
256 ->andWhere( [ 'page_title' => $title->getDBkey() ] )
257 ->caller( __METHOD__ )->fetchRow();
258 if ( !$row ) {
259 # No redirect from here, chain terminates
260 break;
261 } else {
262 $dest = $title = Title::makeTitle(
263 $row->rd_namespace,
264 $row->rd_title,
265 '',
266 $row->rd_interwiki
267 );
268 }
269 }
270
271 return $dest;
272 }
273
281 private function getUser() {
282 if ( !self::$user ) {
283 $username = wfMessage( 'double-redirect-fixer' )->inContentLanguage()->text();
284 self::$user = User::newFromName( $username );
285 # User::newFromName() can return false on a badly configured wiki.
286 if ( self::$user && !self::$user->isRegistered() ) {
287 self::$user->addToDatabase();
288 }
289 }
290
291 return self::$user;
292 }
293}
294
296class_alias( DoubleRedirectJob::class, 'DoubleRedirectJob' );
const EDIT_INTERNAL
Signal that the page retrieve/save cycle happened entirely in this request.
Definition Defines.php:138
const EDIT_UPDATE
Article is assumed to be pre-existing, fail if it doesn't exist.
Definition Defines.php:117
const EDIT_SUPPRESS_RC
Definition Defines.php:126
const EDIT_MINOR
Mark this edit minor, if the user is allowed to do so.
Definition Defines.php:120
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$wgUser
Definition Setup.php:558
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
Describe and execute a background job.
Definition Job.php:28
array $params
Array of job parameters.
Definition Job.php:33
setLastError( $error)
Definition Job.php:425
Fix any double redirects after moving a page.
static getFinalDestination( $title)
Get the final destination of a redirect.
__construct(PageReference $page, array $params, RevisionLookup $revisionLookup, MagicWordFactory $magicWordFactory, WikiPageFactory $wikiPageFactory)
static fixRedirects( $reason, $redirTitle)
Insert jobs into the job queue to fix redirects to the given title.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Helper class for mapping page value objects to a string key.
Service for creating WikiPage objects.
Store information about magic words, and create/cache MagicWord objects.
Value object representing a content slot associated with a page revision.
Represents a title within MediaWiki.
Definition Title.php:69
canExist()
Can this title represent a page in the wiki's database?
Definition Title.php:1205
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1037
getDBkey()
Get the main part with underscores.
Definition Title.php:1028
User class for the MediaWiki software.
Definition User.php:130
Represents the target of a wiki link.
isExternal()
Whether this LinkTarget has an interwiki component.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Service for looking up page revisions.
Interface for database access objects.