MediaWiki master
SpecialRecentChangesLinked.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Specials;
25
34use MessageCache;
35use RecentChange;
40use Xml;
41
49 protected $rclTargetTitle;
50
51 private SearchEngineFactory $searchEngineFactory;
52 private ChangeTagsStore $changeTagsStore;
53
60 public function __construct(
61 WatchedItemStoreInterface $watchedItemStore,
62 MessageCache $messageCache,
63 UserOptionsLookup $userOptionsLookup,
64 SearchEngineFactory $searchEngineFactory,
65 ChangeTagsStore $changeTagsStore,
68 ) {
69 parent::__construct(
70 $watchedItemStore,
71 $messageCache,
72 $userOptionsLookup,
73 $changeTagsStore,
76 );
77 $this->mName = 'Recentchangeslinked';
78 $this->searchEngineFactory = $searchEngineFactory;
79 $this->changeTagsStore = $changeTagsStore;
80 }
81
82 public function getDefaultOptions() {
83 $opts = parent::getDefaultOptions();
84 $opts->add( 'target', '' );
85 $opts->add( 'showlinkedto', false );
86
87 return $opts;
88 }
89
90 public function parseParameters( $par, FormOptions $opts ) {
91 $opts['target'] = $par;
92 }
93
99 protected function doMainQuery( $tables, $select, $conds, $query_options,
100 $join_conds, FormOptions $opts
101 ) {
102 $target = $opts['target'];
103 $showlinkedto = $opts['showlinkedto'];
104 $limit = $opts['limit'];
105
106 if ( $target === '' ) {
107 return false;
108 }
109 $outputPage = $this->getOutput();
110 $title = Title::newFromText( $target );
111 if ( !$title || $title->isExternal() ) {
112 $outputPage->addHTML(
113 Html::errorBox( $this->msg( 'allpagesbadtitle' )->parse(), '', 'mw-recentchangeslinked-errorbox' )
114 );
115 return false;
116 }
117
118 $outputPage->setPageTitleMsg(
119 $this->msg( 'recentchangeslinked-title' )->plaintextParams( $title->getPrefixedText() )
120 );
121
122 /*
123 * Ordinary links are in the pagelinks table, while transclusions are
124 * in the templatelinks table, categorizations in categorylinks and
125 * image use in imagelinks. We need to somehow combine all these.
126 * Special:Whatlinkshere does this by firing multiple queries and
127 * merging the results, but the code we inherit from our parent class
128 * expects only one result set so we use UNION instead.
129 */
130 $dbr = $this->getDB();
131 $id = $title->getArticleID();
132 $ns = $title->getNamespace();
133 $dbkey = $title->getDBkey();
134
135 $rcQuery = RecentChange::getQueryInfo();
136 $tables = array_unique( array_merge( $rcQuery['tables'], $tables ) );
137 $select = array_unique( array_merge( $rcQuery['fields'], $select ) );
138 $join_conds = array_merge( $rcQuery['joins'], $join_conds );
139
140 // Join with watchlist and watchlist_expiry tables to highlight watched rows.
141 $this->addWatchlistJoins( $dbr, $tables, $select, $join_conds, $conds );
142
143 // JOIN on page, used for 'last revision' filter highlight
144 $tables[] = 'page';
145 $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
146 $select[] = 'page_latest';
147
148 $tagFilter = $opts['tagfilter'] !== '' ? explode( '|', $opts['tagfilter'] ) : [];
149 $this->changeTagsStore->modifyDisplayQuery(
150 $tables,
151 $select,
152 $conds,
153 $join_conds,
154 $query_options,
155 $tagFilter,
156 $opts['inverttags']
157 );
158
159 if ( $dbr->unionSupportsOrderAndLimit() ) {
160 if ( in_array( 'DISTINCT', $query_options ) ) {
161 // ChangeTagsStore::modifyDisplayQuery() will have added DISTINCT.
162 // To prevent this from causing query performance problems, we need to add
163 // a GROUP BY, and add rc_id to the ORDER BY.
164 $order = [
165 'GROUP BY' => [ 'rc_timestamp', 'rc_id' ],
166 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ]
167 ];
168 } else {
169 $order = [ 'ORDER BY' => 'rc_timestamp DESC' ];
170 }
171 } else {
172 $order = [];
173 }
174
175 if ( !$this->runMainQueryHook( $tables, $select, $conds, $query_options, $join_conds,
176 $opts )
177 ) {
178 return false;
179 }
180
181 if ( $ns === NS_CATEGORY && !$showlinkedto ) {
182 // special handling for categories
183 // XXX: should try to make this less kludgy
184 $link_tables = [ 'categorylinks' ];
185 $showlinkedto = true;
186 } else {
187 // for now, always join on these tables; really should be configurable as in whatlinkshere
188 $link_tables = [ 'pagelinks', 'templatelinks' ];
189 // imagelinks only contains links to pages in NS_FILE
190 if ( $ns === NS_FILE || !$showlinkedto ) {
191 $link_tables[] = 'imagelinks';
192 }
193 }
194
195 if ( $id == 0 && !$showlinkedto ) {
196 return false; // nonexistent pages can't link to any pages
197 }
198
199 // field name prefixes for all the various tables we might want to join with
200 $prefix = [
201 'pagelinks' => 'pl',
202 'templatelinks' => 'tl',
203 'categorylinks' => 'cl',
204 'imagelinks' => 'il'
205 ];
206
207 $subsql = []; // SELECT statements to combine with UNION
208
209 foreach ( $link_tables as $link_table ) {
210 $queryBuilder = $dbr->newSelectQueryBuilder();
211 $linksMigration = \MediaWiki\MediaWikiServices::getInstance()->getLinksMigration();
212 $queryBuilder = $queryBuilder
213 ->tables( $tables )
214 ->fields( $select )
215 ->where( $conds )
216 ->caller( __METHOD__ )
217 ->options( $order + $query_options )
218 ->joinConds( $join_conds );
219 $pfx = $prefix[$link_table];
220
221 // imagelinks and categorylinks tables have no xx_namespace field,
222 // and have xx_to instead of xx_title
223 if ( $link_table == 'imagelinks' ) {
224 $link_ns = NS_FILE;
225 } elseif ( $link_table == 'categorylinks' ) {
226 $link_ns = NS_CATEGORY;
227 } else {
228 $link_ns = 0;
229 }
230
231 if ( $showlinkedto ) {
232 // find changes to pages linking to this page
233 if ( $link_ns ) {
234 if ( $ns != $link_ns ) {
235 continue;
236 } // should never happen, but check anyway
237 $queryBuilder->where( [ "{$pfx}_to" => $dbkey ] );
238 } else {
239 if ( isset( $linksMigration::$mapping[$link_table] ) ) {
240 $queryBuilder->where( $linksMigration->getLinksConditions( $link_table, $title ) );
241 } else {
242 $queryBuilder->where( [ "{$pfx}_namespace" => $ns, "{$pfx}_title" => $dbkey ] );
243 }
244 }
245 $queryBuilder->join( $link_table, null, "rc_cur_id = {$pfx}_from" );
246 } else {
247 // find changes to pages linked from this page
248 $queryBuilder->where( [ "{$pfx}_from" => $id ] );
249 if ( $link_table == 'imagelinks' || $link_table == 'categorylinks' ) {
250 $queryBuilder->where( [ "rc_namespace" => $link_ns ] );
251 $queryBuilder->join( $link_table, null, "rc_title = {$pfx}_to" );
252 } else {
253 // TODO: Move this to LinksMigration
254 if ( isset( $linksMigration::$mapping[$link_table] ) ) {
255 $queryInfo = $linksMigration->getQueryInfo( $link_table, $link_table );
256 [ $nsField, $titleField ] = $linksMigration->getTitleFields( $link_table );
257 if ( in_array( 'linktarget', $queryInfo['tables'] ) ) {
258 $joinTable = 'linktarget';
259 } else {
260 $joinTable = $link_table;
261 }
262 $queryBuilder->join(
263 $joinTable,
264 null,
265 [ "rc_namespace = {$nsField}", "rc_title = {$titleField}" ]
266 );
267 if ( in_array( 'linktarget', $queryInfo['tables'] ) ) {
268 $queryBuilder->joinConds( $queryInfo['joins'] );
269 $queryBuilder->table( $link_table );
270 }
271 } else {
272 $queryBuilder->join(
273 $link_table,
274 null,
275 [ "rc_namespace = {$pfx}_namespace", "rc_title = {$pfx}_title" ]
276 );
277 }
278 }
279 }
280
281 if ( $dbr->unionSupportsOrderAndLimit() ) {
282 $queryBuilder->limit( $limit );
283 }
284
285 $subsql[] = $queryBuilder;
286 }
287
288 if ( count( $subsql ) == 0 ) {
289 return false; // should never happen
290 }
291 if ( count( $subsql ) == 1 && $dbr->unionSupportsOrderAndLimit() ) {
292 return $subsql[0]
293 ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) )
294 ->caller( __METHOD__ )->fetchResultSet();
295 } else {
296 $sqls = array_map( static function ( $queryBuilder ) {
297 return $queryBuilder->getSQL();
298 }, $subsql );
299 return $dbr->newSelectQueryBuilder()
300 ->select( '*' )
301 ->from(
302 new Subquery( $dbr->unionQueries( $sqls, $dbr::UNION_DISTINCT ) ),
303 'main'
304 )
305 ->orderBy( 'rc_timestamp', SelectQueryBuilder::SORT_DESC )
306 ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) )
307 ->limit( $limit )
308 ->caller( __METHOD__ )->fetchResultSet();
309 }
310 }
311
312 public function setTopText( FormOptions $opts ) {
313 $target = $this->getTargetTitle();
314 if ( $target ) {
315 $this->getOutput()->addBacklinkSubtitle( $target );
316 $this->getSkin()->setRelevantTitle( $target );
317 }
318 }
319
326 public function getExtraOptions( $opts ) {
327 $extraOpts = parent::getExtraOptions( $opts );
328
329 $opts->consumeValues( [ 'showlinkedto', 'target' ] );
330
331 $extraOpts['target'] = [ $this->msg( 'recentchangeslinked-page' )->escaped(),
332 Xml::input( 'target', 40, str_replace( '_', ' ', $opts['target'] ) ) . ' ' .
333 Xml::check( 'showlinkedto', $opts['showlinkedto'], [ 'id' => 'showlinkedto' ] ) . ' ' .
334 Xml::label( $this->msg( 'recentchangeslinked-to' )->text(), 'showlinkedto' ) ];
335
336 $this->addHelpLink( 'Help:Related changes' );
337 return $extraOpts;
338 }
339
343 private function getTargetTitle() {
344 if ( $this->rclTargetTitle === null ) {
345 $opts = $this->getOptions();
346 if ( isset( $opts['target'] ) && $opts['target'] !== '' ) {
347 $this->rclTargetTitle = Title::newFromText( $opts['target'] );
348 } else {
349 $this->rclTargetTitle = false;
350 }
351 }
352
354 }
355
364 public function prefixSearchSubpages( $search, $limit, $offset ) {
365 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
366 }
367
368 protected function outputNoResults() {
369 $targetTitle = $this->getTargetTitle();
370 if ( $targetTitle === false ) {
371 $this->getOutput()->addHTML(
372 Html::rawElement(
373 'div',
374 [ 'class' => 'mw-changeslist-empty mw-changeslist-notargetpage' ],
375 $this->msg( 'recentchanges-notargetpage' )->parse()
376 )
377 );
378 } elseif ( !$targetTitle || $targetTitle->isExternal() ) {
379 $this->getOutput()->addHTML(
380 Html::rawElement(
381 'div',
382 [ 'class' => 'mw-changeslist-empty mw-changeslist-invalidtargetpage' ],
383 $this->msg( 'allpagesbadtitle' )->parse()
384 )
385 );
386 } else {
387 parent::outputNoResults();
388 }
389 }
390}
391
396class_alias( SpecialRecentChangesLinked::class, 'SpecialRecentChangesLinked' );
const NS_FILE
Definition Defines.php:70
const NS_CATEGORY
Definition Defines.php:78
Gateway class for change_tags table.
Helper class to keep track of options when mixing links and form elements.
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
A class containing constants representing the names of configuration variables.
const MaxExecutionTimeForExpensiveQueries
Name constant for the MaxExecutionTimeForExpensiveQueries setting, for use with Config::get()
getDB()
Which database to use for read queries.
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
getOptions()
Get the current FormOptions for this request.
getSkin()
Shortcut to get the skin being used for this instance.
prefixSearchString( $search, $limit, $offset, SearchEngineFactory $searchEngineFactory=null)
Perform a regular substring search for prefixSearchSubpages.
getConfig()
Shortcut to get main config object.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
This is to display changes made to all articles linked in an article.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
doMainQuery( $tables, $select, $conds, $query_options, $join_conds, FormOptions $opts)
FIXME: Port useful changes from SpecialRecentChanges.
setTopText(FormOptions $opts)
Send the text to be displayed above the options.
parseParameters( $par, FormOptions $opts)
Process $par and put options found in $opts.
outputNoResults()
Add the "no results" message to the output.
getDefaultOptions()
Get a FormOptions object containing the default options.
getExtraOptions( $opts)
Get options to be displayed in a form.
__construct(WatchedItemStoreInterface $watchedItemStore, MessageCache $messageCache, UserOptionsLookup $userOptionsLookup, SearchEngineFactory $searchEngineFactory, ChangeTagsStore $changeTagsStore, UserIdentityUtils $userIdentityUtils, TempUserConfig $tempUserConfig)
A special page that lists last changes made to the wiki.
addWatchlistJoins(IReadableDatabase $dbr, &$tables, &$fields, &$joinConds, &$conds)
Add required values to a query's $tables, $fields, $joinConds, and $conds arrays to join to the watch...
Represents a title within MediaWiki.
Definition Title.php:78
Provides access to user options.
Convenience functions for interpreting UserIdentity objects using additional services or config.
Cache messages that are defined by MediaWiki-namespace pages or by hooks.
Utility class for creating new RC entries.
Factory class for SearchEngine.
Build SELECT queries with a fluent interface.
Module of static functions for generating XML.
Definition Xml.php:33
Interface for temporary user creation config and name matching.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...