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