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