MediaWiki master
DoubleRedirectJob.php
Go to the documentation of this file.
1<?php
32
38class DoubleRedirectJob extends Job {
44 public const MAX_DR_JOBS_COUNTER = 10000;
45
49 private $redirTitle;
50
52 private static $user;
53
55 private $revisionLookup;
56
58 private $magicWordFactory;
59
61 private $wikiPageFactory;
62
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;
87 }
88
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;
109 }
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;
116 }
117
118 $jobs[] = new self(
119 $title,
120 [
121 'reason' => $reason,
122 'redirTitle' => $services->getTitleFormatter()
123 ->getPrefixedDBkey( $redirTitle )
124 ],
125 $services->getRevisionLookup(),
126 $services->getMagicWordFactory(),
127 $services->getWikiPageFactory()
128 );
129 # Avoid excessive memory usage
130 if ( count( $jobs ) > self::MAX_DR_JOBS_COUNTER ) {
131 $jobQueueGroup->push( $jobs );
132 $jobs = [];
133 }
134 }
135 $jobQueueGroup->push( $jobs );
136 }
137
141 public function run() {
142 if ( !$this->redirTitle ) {
143 $this->setLastError( 'Invalid title' );
144
145 return false;
146 }
147
148 if ( !$this->title->canExist() ) {
149 // Needs a proper title for WikiPageFactory::newFromTitle and RevisionStore::getRevisionByTitle
150 $this->setLastError( 'Cannot edit title' );
151
152 return false;
153 }
154
155 $targetRev = $this->revisionLookup
156 ->getRevisionByTitle( $this->title, 0, IDBAccessObject::READ_LATEST );
157 if ( !$targetRev ) {
158 wfDebug( __METHOD__ . ": target redirect already deleted, ignoring" );
159
160 return true;
161 }
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" );
166
167 return true;
168 }
169
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__" );
174
175 return true;
176 }
177
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" );
183
184 return true;
185 }
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" );
190 }
191
192 // Preserve fragment (T16904)
193 $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(),
194 $currentDest->getFragment(), $newTitle->getInterwiki() );
195
196 // Fix the text
197 $newContent = $content->updateRedirect( $newTitle );
198
199 if ( $newContent->equals( $content ) ) {
200 $this->setLastError( 'Content unchanged???' );
201
202 return false;
203 }
204
205 $user = $this->getUser();
206 if ( !$user ) {
207 $this->setLastError( 'Invalid user' );
208
209 return false;
210 }
211
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 );
218
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
225 $article->doUserEditContent( $newContent, $user, $reason, $flags );
226 $wgUser = $oldUser;
227
228 return true;
229 }
230
239 public static function getFinalDestination( $title ) {
240 $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
241
242 // Circular redirect check
243 $seenTitles = [];
244 $dest = false;
245
246 while ( true ) {
247 $titleText = CacheKeyHelper::getKeyForPage( $title );
248 if ( isset( $seenTitles[$titleText] ) ) {
249 wfDebug( __METHOD__, "Circular redirect detected, aborting" );
250
251 return false;
252 }
253 $seenTitles[$titleText] = true;
254
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;
261 }
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,
276 '',
277 $row->rd_interwiki
278 );
279 }
280 }
281
282 return $dest;
283 }
284
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();
299 }
300 }
301
302 return self::$user;
303 }
304}
const EDIT_INTERNAL
Definition Defines.php:134
const EDIT_UPDATE
Definition Defines.php:128
const EDIT_SUPPRESS_RC
Definition Defines.php:130
const EDIT_MINOR
Definition Defines.php:129
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:547
array $params
The job parameters.
setLastError( $error)
This is actually implemented in the Job class.
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.
Describe and execute a background job.
Definition Job.php:41
Title $title
Definition Job.php:52
Helper class for mapping value objects representing basic entities to cache keys.
Service locator for MediaWiki core services.
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:78
canExist()
Can this title represent a page in the wiki's database?
Definition Title.php:1211
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1043
getDBkey()
Get the main part with underscores.
Definition Title.php:1034
internal since 1.36
Definition User.php:93
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.