MediaWiki  master
SpecialRecentChangesLinked.php
Go to the documentation of this file.
1 <?php
29 
37  protected $rclTargetTitle;
38 
40  private $loadBalancer;
41 
43  private $searchEngineFactory;
44 
52  public function __construct(
53  WatchedItemStoreInterface $watchedItemStore,
54  MessageCache $messageCache,
55  ILoadBalancer $loadBalancer,
56  UserOptionsLookup $userOptionsLookup,
57  SearchEngineFactory $searchEngineFactory
58  ) {
59  parent::__construct(
60  $watchedItemStore,
61  $messageCache,
62  $loadBalancer,
63  $userOptionsLookup
64  );
65  $this->mName = 'Recentchangeslinked';
66  $this->loadBalancer = $loadBalancer;
67  $this->searchEngineFactory = $searchEngineFactory;
68  }
69 
70  public function getDefaultOptions() {
71  $opts = parent::getDefaultOptions();
72  $opts->add( 'target', '' );
73  $opts->add( 'showlinkedto', false );
74 
75  return $opts;
76  }
77 
78  public function parseParameters( $par, FormOptions $opts ) {
79  $opts['target'] = $par;
80  }
81 
85  protected function doMainQuery( $tables, $select, $conds, $query_options,
86  $join_conds, FormOptions $opts
87  ) {
88  $target = $opts['target'];
89  $showlinkedto = $opts['showlinkedto'];
90  $limit = $opts['limit'];
91 
92  if ( $target === '' ) {
93  return false;
94  }
95  $outputPage = $this->getOutput();
96  $title = Title::newFromText( $target );
97  if ( !$title || $title->isExternal() ) {
98  $outputPage->addHTML(
99  Html::errorBox( $this->msg( 'allpagesbadtitle' )->parse(), '', 'mw-recentchangeslinked-errorbox' )
100  );
101  return false;
102  }
103 
104  $outputPage->setPageTitle( $this->msg( 'recentchangeslinked-title', $title->getPrefixedText() ) );
105 
106  /*
107  * Ordinary links are in the pagelinks table, while transclusions are
108  * in the templatelinks table, categorizations in categorylinks and
109  * image use in imagelinks. We need to somehow combine all these.
110  * Special:Whatlinkshere does this by firing multiple queries and
111  * merging the results, but the code we inherit from our parent class
112  * expects only one result set so we use UNION instead.
113  */
114 
115  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA, 'recentchangeslinked' );
116  $id = $title->getArticleID();
117  $ns = $title->getNamespace();
118  $dbkey = $title->getDBkey();
119 
120  $rcQuery = RecentChange::getQueryInfo();
121  $tables = array_unique( array_merge( $rcQuery['tables'], $tables ) );
122  $select = array_unique( array_merge( $rcQuery['fields'], $select ) );
123  $join_conds = array_merge( $rcQuery['joins'], $join_conds );
124 
125  // Join with watchlist and watchlist_expiry tables to highlight watched rows.
126  $this->addWatchlistJoins( $dbr, $tables, $select, $join_conds, $conds );
127 
128  // JOIN on page, used for 'last revision' filter highlight
129  $tables[] = 'page';
130  $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
131  $select[] = 'page_latest';
132 
133  $tagFilter = $opts['tagfilter'] !== '' ? explode( '|', $opts['tagfilter'] ) : [];
135  $tables,
136  $select,
137  $conds,
138  $join_conds,
139  $query_options,
140  $tagFilter
141  );
142 
143  if ( $dbr->unionSupportsOrderAndLimit() ) {
144  if ( count( $tagFilter ) > 1 ) {
145  // ChangeTags::modifyDisplayQuery() will have added DISTINCT.
146  // To prevent this from causing query performance problems, we need to add
147  // a GROUP BY, and add rc_id to the ORDER BY.
148  $order = [
149  'GROUP BY' => [ 'rc_timestamp', 'rc_id' ],
150  'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ]
151  ];
152  } else {
153  $order = [ 'ORDER BY' => 'rc_timestamp DESC' ];
154  }
155  } else {
156  $order = [];
157  }
158 
159  if ( !$this->runMainQueryHook( $tables, $select, $conds, $query_options, $join_conds,
160  $opts )
161  ) {
162  return false;
163  }
164 
165  if ( $ns === NS_CATEGORY && !$showlinkedto ) {
166  // special handling for categories
167  // XXX: should try to make this less kludgy
168  $link_tables = [ 'categorylinks' ];
169  $showlinkedto = true;
170  } else {
171  // for now, always join on these tables; really should be configurable as in whatlinkshere
172  $link_tables = [ 'pagelinks', 'templatelinks' ];
173  // imagelinks only contains links to pages in NS_FILE
174  if ( $ns === NS_FILE || !$showlinkedto ) {
175  $link_tables[] = 'imagelinks';
176  }
177  }
178 
179  if ( $id == 0 && !$showlinkedto ) {
180  return false; // nonexistent pages can't link to any pages
181  }
182 
183  // field name prefixes for all the various tables we might want to join with
184  $prefix = [
185  'pagelinks' => 'pl',
186  'templatelinks' => 'tl',
187  'categorylinks' => 'cl',
188  'imagelinks' => 'il'
189  ];
190 
191  $subsql = []; // SELECT statements to combine with UNION
192 
193  foreach ( $link_tables as $link_table ) {
194  $queryBuilder = $dbr->newSelectQueryBuilder();
195  $linksMigration = \MediaWiki\MediaWikiServices::getInstance()->getLinksMigration();
196  $queryBuilder = $queryBuilder
197  ->tables( $tables )
198  ->fields( $select )
199  ->where( $conds )
200  ->caller( __METHOD__ )
201  ->options( $order + $query_options )
202  ->joinConds( $join_conds );
203  $pfx = $prefix[$link_table];
204 
205  // imagelinks and categorylinks tables have no xx_namespace field,
206  // and have xx_to instead of xx_title
207  if ( $link_table == 'imagelinks' ) {
208  $link_ns = NS_FILE;
209  } elseif ( $link_table == 'categorylinks' ) {
210  $link_ns = NS_CATEGORY;
211  } else {
212  $link_ns = 0;
213  }
214 
215  if ( $showlinkedto ) {
216  // find changes to pages linking to this page
217  if ( $link_ns ) {
218  if ( $ns != $link_ns ) {
219  continue;
220  } // should never happen, but check anyway
221  $queryBuilder->where( [ "{$pfx}_to" => $dbkey ] );
222  } else {
223  if ( isset( $linksMigration::$mapping[$link_table] ) ) {
224  $queryBuilder->where( $linksMigration->getLinksConditions( $link_table, $title ) );
225  } else {
226  $queryBuilder->where( [ "{$pfx}_namespace" => $ns, "{$pfx}_title" => $dbkey ] );
227  }
228  }
229  $queryBuilder->join( $link_table, null, "rc_cur_id = {$pfx}_from" );
230  } else {
231  // find changes to pages linked from this page
232  $queryBuilder->where( [ "{$pfx}_from" => $id ] );
233  if ( $link_table == 'imagelinks' || $link_table == 'categorylinks' ) {
234  $queryBuilder->where( [ "rc_namespace" => $link_ns ] );
235  $queryBuilder->join( $link_table, null, "rc_title = {$pfx}_to" );
236  } else {
237  // TODO: Move this to LinksMigration
238  if ( isset( $linksMigration::$mapping[$link_table] ) ) {
239  $queryInfo = $linksMigration->getQueryInfo( $link_table, $link_table );
240  [ $nsField, $titleField ] = $linksMigration->getTitleFields( $link_table );
241  if ( in_array( 'linktarget', $queryInfo['tables'] ) ) {
242  $joinTable = 'linktarget';
243  } else {
244  $joinTable = $link_table;
245  }
246  $queryBuilder->join(
247  $joinTable,
248  null,
249  [ "rc_namespace = {$nsField}", "rc_title = {$titleField}" ]
250  );
251  if ( in_array( 'linktarget', $queryInfo['tables'] ) ) {
252  $queryBuilder->joinConds( $queryInfo['joins'] );
253  $queryBuilder->table( $link_table );
254  }
255  } else {
256  $queryBuilder->join(
257  $link_table,
258  null,
259  [ "rc_namespace = {$pfx}_namespace", "rc_title = {$pfx}_title" ]
260  );
261  }
262  }
263  }
264 
265  if ( $dbr->unionSupportsOrderAndLimit() ) {
266  $queryBuilder->limit( $limit );
267  }
268 
269  $subsql[] = $queryBuilder;
270  }
271 
272  if ( count( $subsql ) == 0 ) {
273  return false; // should never happen
274  }
275  if ( count( $subsql ) == 1 && $dbr->unionSupportsOrderAndLimit() ) {
276  $sql = $subsql[0]
277  ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) )
278  ->getSQL();
279  } else {
280  $sqls = array_map( static function ( $queryBuilder ) {
281  return $queryBuilder->getSQL();
282  }, $subsql );
283  $queryBuilder = $dbr->newSelectQueryBuilder()
284  ->select( '*' )
285  ->from(
286  new Subquery( $dbr->unionQueries( $sqls, $dbr::UNION_DISTINCT ) ),
287  'main'
288  )
289  ->orderBy( 'rc_timestamp', SelectQueryBuilder::SORT_DESC )
290  ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) )
291  ->limit( $limit );
292  $sql = $queryBuilder->getSQL();
293  }
294  return $dbr->query( $sql, __METHOD__ );
295  }
296 
297  public function setTopText( FormOptions $opts ) {
298  $target = $this->getTargetTitle();
299  if ( $target ) {
300  $this->getOutput()->addBacklinkSubtitle( $target );
301  $this->getSkin()->setRelevantTitle( $target );
302  }
303  }
304 
311  public function getExtraOptions( $opts ) {
312  $extraOpts = parent::getExtraOptions( $opts );
313 
314  $opts->consumeValues( [ 'showlinkedto', 'target' ] );
315 
316  $extraOpts['target'] = [ $this->msg( 'recentchangeslinked-page' )->escaped(),
317  Xml::input( 'target', 40, str_replace( '_', ' ', $opts['target'] ) ) .
318  Xml::check( 'showlinkedto', $opts['showlinkedto'], [ 'id' => 'showlinkedto' ] ) . ' ' .
319  Xml::label( $this->msg( 'recentchangeslinked-to' )->text(), 'showlinkedto' ) ];
320 
321  $this->addHelpLink( 'Help:Related changes' );
322  return $extraOpts;
323  }
324 
328  private function getTargetTitle() {
329  if ( $this->rclTargetTitle === null ) {
330  $opts = $this->getOptions();
331  if ( isset( $opts['target'] ) && $opts['target'] !== '' ) {
332  $this->rclTargetTitle = Title::newFromText( $opts['target'] );
333  } else {
334  $this->rclTargetTitle = false;
335  }
336  }
337 
338  return $this->rclTargetTitle;
339  }
340 
349  public function prefixSearchSubpages( $search, $limit, $offset ) {
350  return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
351  }
352 
353  protected function outputNoResults() {
354  $targetTitle = $this->getTargetTitle();
355  if ( $targetTitle === false ) {
356  $this->getOutput()->addHTML(
358  'div',
359  [ 'class' => 'mw-changeslist-empty mw-changeslist-notargetpage' ],
360  $this->msg( 'recentchanges-notargetpage' )->parse()
361  )
362  );
363  } elseif ( !$targetTitle || $targetTitle->isExternal() ) {
364  $this->getOutput()->addHTML(
366  'div',
367  [ 'class' => 'mw-changeslist-empty mw-changeslist-invalidtargetpage' ],
368  $this->msg( 'allpagesbadtitle' )->parse()
369  )
370  );
371  } else {
372  parent::outputNoResults();
373  }
374  }
375 }
const NS_FILE
Definition: Defines.php:70
const NS_CATEGORY
Definition: Defines.php:78
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:906
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
getOptions()
Get the current FormOptions for this request.
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:35
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:788
A class containing constants representing the names of configuration variables.
Provides access to user options.
Cache messages that are defined by MediaWiki-namespace pages or by hooks.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
Factory class for SearchEngine.
getOutput()
Get the OutputPage being used for this instance.
getSkin()
Shortcut to get the skin being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
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.
setTopText(FormOptions $opts)
Send the text to be displayed above the options.
doMainQuery( $tables, $select, $conds, $query_options, $join_conds, FormOptions $opts)
Process the query.Array of tables; see IDatabase::select $table Array of fields; see IDatabase::selec...
parseParameters( $par, FormOptions $opts)
Process $par and put options found in $opts.
getDefaultOptions()
Get a FormOptions object containing the default options.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
outputNoResults()
Add the "no results" message to the output.
__construct(WatchedItemStoreInterface $watchedItemStore, MessageCache $messageCache, ILoadBalancer $loadBalancer, UserOptionsLookup $userOptionsLookup, SearchEngineFactory $searchEngineFactory)
getExtraOptions( $opts)
Get options to be displayed in a form.
A special page that lists last changes made to the wiki.
addWatchlistJoins(IDatabase $dbr, &$tables, &$fields, &$joinConds, &$conds)
Add required values to a query's $tables, $fields, $joinConds, and $conds arrays to join to the watch...
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:373
A query builder for SELECT queries with a fluent interface.
static check( $name, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox.
Definition: Xml.php:332
static label( $label, $id, $attribs=[])
Convenience function to build an HTML form label.
Definition: Xml.php:367
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:283
Create and track the database connections and transactions for a given database cluster.
const DB_REPLICA
Definition: defines.php:26