MediaWiki  master
SpecialRecentChangesLinked.php
Go to the documentation of this file.
1 <?php
26 
34  protected $rclTargetTitle;
35 
37  private $loadBalancer;
38 
41 
49  public function __construct(
55  ) {
56  parent::__construct(
61  );
62  $this->mName = 'Recentchangeslinked';
63  $this->loadBalancer = $loadBalancer;
64  $this->searchEngineFactory = $searchEngineFactory;
65  }
66 
67  public function getDefaultOptions() {
68  $opts = parent::getDefaultOptions();
69  $opts->add( 'target', '' );
70  $opts->add( 'showlinkedto', false );
71 
72  return $opts;
73  }
74 
75  public function parseParameters( $par, FormOptions $opts ) {
76  $opts['target'] = $par;
77  }
78 
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 
112  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA, 'recentchangeslinked' );
113  $id = $title->getArticleID();
114  $ns = $title->getNamespace();
115  $dbkey = $title->getDBkey();
116 
117  $rcQuery = RecentChange::getQueryInfo();
118  $tables = array_merge( $rcQuery['tables'], $tables );
119  $select = array_merge( $rcQuery['fields'], $select );
120  $join_conds = array_merge( $rcQuery['joins'], $join_conds );
121 
122  // Join with watchlist and watchlist_expiry tables to highlight watched rows.
123  $this->addWatchlistJoins( $dbr, $tables, $select, $join_conds, $conds );
124 
125  // JOIN on page, used for 'last revision' filter highlight
126  $tables[] = 'page';
127  $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
128  $select[] = 'page_latest';
129 
130  $tagFilter = $opts['tagfilter'] !== '' ? explode( '|', $opts['tagfilter'] ) : [];
132  $tables,
133  $select,
134  $conds,
135  $join_conds,
136  $query_options,
137  $tagFilter
138  );
139 
140  if ( $dbr->unionSupportsOrderAndLimit() ) {
141  if ( count( $tagFilter ) > 1 ) {
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  $pfx = $prefix[$link_table];
192 
193  // imagelinks and categorylinks tables have no xx_namespace field,
194  // and have xx_to instead of xx_title
195  if ( $link_table == 'imagelinks' ) {
196  $link_ns = NS_FILE;
197  } elseif ( $link_table == 'categorylinks' ) {
198  $link_ns = NS_CATEGORY;
199  } else {
200  $link_ns = 0;
201  }
202 
203  if ( $showlinkedto ) {
204  // find changes to pages linking to this page
205  if ( $link_ns ) {
206  if ( $ns != $link_ns ) {
207  continue;
208  } // should never happen, but check anyway
209  $subconds = [ "{$pfx}_to" => $dbkey ];
210  } else {
211  $subconds = [ "{$pfx}_namespace" => $ns, "{$pfx}_title" => $dbkey ];
212  }
213  $subjoin = "rc_cur_id = {$pfx}_from";
214  } else {
215  // find changes to pages linked from this page
216  $subconds = [ "{$pfx}_from" => $id ];
217  if ( $link_table == 'imagelinks' || $link_table == 'categorylinks' ) {
218  $subconds["rc_namespace"] = $link_ns;
219  $subjoin = "rc_title = {$pfx}_to";
220  } else {
221  $subjoin = [ "rc_namespace = {$pfx}_namespace", "rc_title = {$pfx}_title" ];
222  }
223  }
224 
225  $query = $dbr->selectSQLText(
226  array_merge( $tables, [ $link_table ] ),
227  $select,
228  $conds + $subconds,
229  __METHOD__,
230  $order + $query_options,
231  $join_conds + [ $link_table => [ 'JOIN', $subjoin ] ]
232  );
233 
234  if ( $dbr->unionSupportsOrderAndLimit() ) {
235  $query = $dbr->limitResult( $query, $limit );
236  }
237 
238  $subsql[] = $query;
239  }
240 
241  if ( count( $subsql ) == 0 ) {
242  return false; // should never happen
243  }
244  if ( count( $subsql ) == 1 && $dbr->unionSupportsOrderAndLimit() ) {
245  $sql = $subsql[0];
246  } else {
247  // need to resort and relimit after union
248  $sql = $dbr->unionQueries( $subsql, $dbr::UNION_DISTINCT ) .
249  ' ORDER BY rc_timestamp DESC';
250  $sql = $dbr->limitResult( $sql, $limit, false );
251  }
252  return $dbr->query( $sql, __METHOD__ );
253  }
254 
255  public function setTopText( FormOptions $opts ) {
256  $target = $this->getTargetTitle();
257  if ( $target ) {
258  $this->getOutput()->addBacklinkSubtitle( $target );
259  $this->getSkin()->setRelevantTitle( $target );
260  }
261  }
262 
269  public function getExtraOptions( $opts ) {
270  $extraOpts = parent::getExtraOptions( $opts );
271 
272  $opts->consumeValues( [ 'showlinkedto', 'target' ] );
273 
274  $extraOpts['target'] = [ $this->msg( 'recentchangeslinked-page' )->escaped(),
275  Xml::input( 'target', 40, str_replace( '_', ' ', $opts['target'] ) ) .
276  Xml::check( 'showlinkedto', $opts['showlinkedto'], [ 'id' => 'showlinkedto' ] ) . ' ' .
277  Xml::label( $this->msg( 'recentchangeslinked-to' )->text(), 'showlinkedto' ) ];
278 
279  $this->addHelpLink( 'Help:Related changes' );
280  return $extraOpts;
281  }
282 
286  private function getTargetTitle() {
287  if ( $this->rclTargetTitle === null ) {
288  $opts = $this->getOptions();
289  if ( isset( $opts['target'] ) && $opts['target'] !== '' ) {
290  $this->rclTargetTitle = Title::newFromText( $opts['target'] );
291  } else {
292  $this->rclTargetTitle = false;
293  }
294  }
295 
296  return $this->rclTargetTitle;
297  }
298 
307  public function prefixSearchSubpages( $search, $limit, $offset ) {
308  return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
309  }
310 
311  protected function outputNoResults() {
312  $targetTitle = $this->getTargetTitle();
313  if ( $targetTitle === false ) {
314  $this->getOutput()->addHTML(
316  'div',
317  [ 'class' => 'mw-changeslist-empty mw-changeslist-notargetpage' ],
318  $this->msg( 'recentchanges-notargetpage' )->parse()
319  )
320  );
321  } elseif ( !$targetTitle || $targetTitle->isExternal() ) {
322  $this->getOutput()->addHTML(
324  'div',
325  [ 'class' => 'mw-changeslist-empty mw-changeslist-invalidtargetpage' ],
326  $this->msg( 'allpagesbadtitle' )->parse()
327  )
328  );
329  } else {
330  parent::outputNoResults();
331  }
332  }
333 }
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:900
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
Provides access to user options.
Cache of 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.
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...
UserOptionsLookup $userOptionsLookup
WatchedItemStoreInterface $watchedItemStore
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:369
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
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition: defines.php:25