MediaWiki master
SpecialRecentChangesLinked.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
33use MessageCache;
34use RecentChange;
38
46 protected $rclTargetTitle;
47
48 private SearchEngineFactory $searchEngineFactory;
49 private ChangeTagsStore $changeTagsStore;
50
57 public function __construct(
58 WatchedItemStoreInterface $watchedItemStore,
59 MessageCache $messageCache,
60 UserOptionsLookup $userOptionsLookup,
61 SearchEngineFactory $searchEngineFactory,
62 ChangeTagsStore $changeTagsStore,
65 ) {
66 parent::__construct(
67 $watchedItemStore,
68 $messageCache,
69 $userOptionsLookup,
70 $changeTagsStore,
73 );
74 $this->mName = 'Recentchangeslinked';
75 $this->searchEngineFactory = $searchEngineFactory;
76 $this->changeTagsStore = $changeTagsStore;
77 }
78
79 public function getDefaultOptions() {
80 $opts = parent::getDefaultOptions();
81 $opts->add( 'target', '' );
82 $opts->add( 'showlinkedto', false );
83
84 return $opts;
85 }
86
87 public function parseParameters( $par, FormOptions $opts ) {
88 $opts['target'] = $par;
89 }
90
96 protected function doMainQuery( $tables, $select, $conds, $query_options,
97 $join_conds, FormOptions $opts
98 ) {
99 $target = $opts['target'];
100 $showlinkedto = $opts['showlinkedto'];
101 $limit = $opts['limit'];
102
103 if ( $target === '' ) {
104 return false;
105 }
106 $outputPage = $this->getOutput();
107 $title = Title::newFromText( $target );
108 if ( !$title || $title->isExternal() ) {
109 $outputPage->addHTML(
110 Html::errorBox( $this->msg( 'allpagesbadtitle' )->parse(), '', 'mw-recentchangeslinked-errorbox' )
111 );
112 return false;
113 }
114
115 $outputPage->setPageTitleMsg(
116 $this->msg( 'recentchangeslinked-title' )->plaintextParams( $title->getPrefixedText() )
117 );
118
119 /*
120 * Ordinary links are in the pagelinks table, while transclusions are
121 * in the templatelinks table, categorizations in categorylinks and
122 * image use in imagelinks. We need to somehow combine all these.
123 * Special:Whatlinkshere does this by firing multiple queries and
124 * merging the results, but the code we inherit from our parent class
125 * expects only one result set so we use UNION instead.
126 */
127 $dbr = $this->getDB();
128 $id = $title->getArticleID();
129 $ns = $title->getNamespace();
130 $dbkey = $title->getDBkey();
131
132 $rcQuery = RecentChange::getQueryInfo();
133 $tables = array_unique( array_merge( $rcQuery['tables'], $tables ) );
134 $select = array_unique( array_merge( $rcQuery['fields'], $select ) );
135 $join_conds = array_merge( $rcQuery['joins'], $join_conds );
136
137 // Join with watchlist and watchlist_expiry tables to highlight watched rows.
138 $this->addWatchlistJoins( $dbr, $tables, $select, $join_conds, $conds );
139
140 // JOIN on page, used for 'last revision' filter highlight
141 $tables[] = 'page';
142 $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
143 $select[] = 'page_latest';
144
145 $tagFilter = $opts['tagfilter'] !== '' ? explode( '|', $opts['tagfilter'] ) : [];
146 $this->changeTagsStore->modifyDisplayQuery(
147 $tables,
148 $select,
149 $conds,
150 $join_conds,
151 $query_options,
152 $tagFilter,
153 $opts['inverttags']
154 );
155
156 if ( $dbr->unionSupportsOrderAndLimit() ) {
157 if ( in_array( 'DISTINCT', $query_options ) ) {
158 // ChangeTagsStore::modifyDisplayQuery() will have added DISTINCT.
159 // To prevent this from causing query performance problems, we need to add
160 // a GROUP BY, and add rc_id to the ORDER BY.
161 $order = [
162 'GROUP BY' => [ 'rc_timestamp', 'rc_id' ],
163 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ]
164 ];
165 } else {
166 $order = [ 'ORDER BY' => 'rc_timestamp DESC' ];
167 }
168 } else {
169 $order = [];
170 }
171
172 if ( !$this->runMainQueryHook( $tables, $select, $conds, $query_options, $join_conds,
173 $opts )
174 ) {
175 return false;
176 }
177
178 if ( $ns === NS_CATEGORY && !$showlinkedto ) {
179 // special handling for categories
180 // XXX: should try to make this less kludgy
181 $link_tables = [ 'categorylinks' ];
182 $showlinkedto = true;
183 } else {
184 // for now, always join on these tables; really should be configurable as in whatlinkshere
185 $link_tables = [ 'pagelinks', 'templatelinks' ];
186 // imagelinks only contains links to pages in NS_FILE
187 if ( $ns === NS_FILE || !$showlinkedto ) {
188 $link_tables[] = 'imagelinks';
189 }
190 }
191
192 if ( $id == 0 && !$showlinkedto ) {
193 return false; // nonexistent pages can't link to any pages
194 }
195
196 // field name prefixes for all the various tables we might want to join with
197 $prefix = [
198 'pagelinks' => 'pl',
199 'templatelinks' => 'tl',
200 'categorylinks' => 'cl',
201 'imagelinks' => 'il'
202 ];
203
204 $subsql = []; // SELECT statements to combine with UNION
205
206 foreach ( $link_tables as $link_table ) {
207 $queryBuilder = $dbr->newSelectQueryBuilder();
208 $linksMigration = \MediaWiki\MediaWikiServices::getInstance()->getLinksMigration();
209 $queryBuilder = $queryBuilder
210 ->tables( $tables )
211 ->fields( $select )
212 ->where( $conds )
213 ->caller( __METHOD__ )
214 ->options( $order + $query_options )
215 ->joinConds( $join_conds );
216 $pfx = $prefix[$link_table];
217
218 // imagelinks and categorylinks tables have no xx_namespace field,
219 // and have xx_to instead of xx_title
220 if ( $link_table == 'imagelinks' ) {
221 $link_ns = NS_FILE;
222 } elseif ( $link_table == 'categorylinks' ) {
223 $link_ns = NS_CATEGORY;
224 } else {
225 $link_ns = 0;
226 }
227
228 if ( $showlinkedto ) {
229 // find changes to pages linking to this page
230 if ( $link_ns ) {
231 if ( $ns != $link_ns ) {
232 continue;
233 } // should never happen, but check anyway
234 $queryBuilder->where( [ "{$pfx}_to" => $dbkey ] );
235 } else {
236 if ( isset( $linksMigration::$mapping[$link_table] ) ) {
237 $queryBuilder->where( $linksMigration->getLinksConditions( $link_table, $title ) );
238 } else {
239 $queryBuilder->where( [ "{$pfx}_namespace" => $ns, "{$pfx}_title" => $dbkey ] );
240 }
241 }
242 $queryBuilder->join( $link_table, null, "rc_cur_id = {$pfx}_from" );
243 } else {
244 // find changes to pages linked from this page
245 $queryBuilder->where( [ "{$pfx}_from" => $id ] );
246 if ( $link_table == 'imagelinks' || $link_table == 'categorylinks' ) {
247 $queryBuilder->where( [ "rc_namespace" => $link_ns ] );
248 $queryBuilder->join( $link_table, null, "rc_title = {$pfx}_to" );
249 } else {
250 // TODO: Move this to LinksMigration
251 if ( isset( $linksMigration::$mapping[$link_table] ) ) {
252 $queryInfo = $linksMigration->getQueryInfo( $link_table, $link_table );
253 [ $nsField, $titleField ] = $linksMigration->getTitleFields( $link_table );
254 if ( in_array( 'linktarget', $queryInfo['tables'] ) ) {
255 $joinTable = 'linktarget';
256 } else {
257 $joinTable = $link_table;
258 }
259 $queryBuilder->join(
260 $joinTable,
261 null,
262 [ "rc_namespace = {$nsField}", "rc_title = {$titleField}" ]
263 );
264 if ( in_array( 'linktarget', $queryInfo['tables'] ) ) {
265 $queryBuilder->joinConds( $queryInfo['joins'] );
266 $queryBuilder->table( $link_table );
267 }
268 } else {
269 $queryBuilder->join(
270 $link_table,
271 null,
272 [ "rc_namespace = {$pfx}_namespace", "rc_title = {$pfx}_title" ]
273 );
274 }
275 }
276 }
277
278 if ( $dbr->unionSupportsOrderAndLimit() ) {
279 $queryBuilder->limit( $limit );
280 }
281
282 $subsql[] = $queryBuilder;
283 }
284
285 if ( count( $subsql ) == 0 ) {
286 return false; // should never happen
287 }
288 if ( count( $subsql ) == 1 && $dbr->unionSupportsOrderAndLimit() ) {
289 return $subsql[0]
290 ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) )
291 ->caller( __METHOD__ )->fetchResultSet();
292 } else {
293 $unionQueryBuilder = $dbr->newUnionQueryBuilder()->caller( __METHOD__ );
294 foreach ( $subsql as $selectQueryBuilder ) {
295 $unionQueryBuilder->add( $selectQueryBuilder );
296 }
297 return $dbr->newSelectQueryBuilder()
298 ->select( '*' )
299 ->from(
300 new Subquery( $unionQueryBuilder->getSQL() ),
301 'main'
302 )
303 ->orderBy( 'rc_timestamp', SelectQueryBuilder::SORT_DESC )
304 ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) )
305 ->limit( $limit )
306 ->caller( __METHOD__ )->fetchResultSet();
307 }
308 }
309
310 public function setTopText( FormOptions $opts ) {
311 $target = $this->getTargetTitle();
312 if ( $target ) {
313 $this->getOutput()->addBacklinkSubtitle( $target );
314 $this->getSkin()->setRelevantTitle( $target );
315 }
316 }
317
324 public function getExtraOptions( $opts ) {
325 $extraOpts = parent::getExtraOptions( $opts );
326
327 $opts->consumeValues( [ 'showlinkedto', 'target' ] );
328
329 $extraOpts['target'] = [ $this->msg( 'recentchangeslinked-page' )->escaped(),
330 Xml::input( 'target', 40, str_replace( '_', ' ', $opts['target'] ) ) . ' ' .
331 Xml::check( 'showlinkedto', $opts['showlinkedto'], [ 'id' => 'showlinkedto' ] ) . ' ' .
332 Xml::label( $this->msg( 'recentchangeslinked-to' )->text(), 'showlinkedto' ) ];
333
334 $this->addHelpLink( 'Help:Related changes' );
335 return $extraOpts;
336 }
337
341 private function getTargetTitle() {
342 if ( $this->rclTargetTitle === null ) {
343 $opts = $this->getOptions();
344 if ( isset( $opts['target'] ) && $opts['target'] !== '' ) {
345 $this->rclTargetTitle = Title::newFromText( $opts['target'] );
346 } else {
347 $this->rclTargetTitle = false;
348 }
349 }
350
352 }
353
362 public function prefixSearchSubpages( $search, $limit, $offset ) {
363 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
364 }
365
366 protected function outputNoResults() {
367 $targetTitle = $this->getTargetTitle();
368 if ( $targetTitle === false ) {
369 $this->getOutput()->addHTML(
370 Html::rawElement(
371 'div',
372 [ 'class' => 'mw-changeslist-empty mw-changeslist-notargetpage' ],
373 $this->msg( 'recentchanges-notargetpage' )->parse()
374 )
375 );
376 } elseif ( !$targetTitle || $targetTitle->isExternal() ) {
377 $this->getOutput()->addHTML(
378 Html::rawElement(
379 'div',
380 [ 'class' => 'mw-changeslist-empty mw-changeslist-invalidtargetpage' ],
381 $this->msg( 'allpagesbadtitle' )->parse()
382 )
383 );
384 } else {
385 parent::outputNoResults();
386 }
387 }
388}
389
394class_alias( SpecialRecentChangesLinked::class, 'SpecialRecentChangesLinked' );
const NS_FILE
Definition Defines.php:71
const NS_CATEGORY
Definition Defines.php:79
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
static check( $name, $checked=false, array $attribs=[])
Convenience function to produce a checkbox (input element with type=checkbox)
Definition Html.php:672
static label( $label, $id, array $attribs=[])
Convenience function for generating a label for inputs.
Definition Html.php:815
static input( $name, $value='', $type='text', array $attribs=[])
Convenience function to produce an <input> element.
Definition Html.php:657
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)
List of the 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:79
Provides access to user options.
Convenience functions for interpreting UserIdentity objects using additional services or config.
Module of static functions for generating XML.
Definition Xml.php:37
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.
Interface for temporary user creation config and name matching.