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