Merge "mediawiki.api: Use then() in getToken instead of manual Deferred wrapping"
[mediawiki.git] / includes / specialpage / ChangesListSpecialPage.php
blobf7c95d1024abd900240842ab0ff93128ee1797da
1 <?php
2 /**
3 * Special page which uses a ChangesList to show query results.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
21 * @ingroup SpecialPage
24 /**
25 * Special page which uses a ChangesList to show query results.
26 * @todo Way too many public functions, most of them should be protected
28 * @ingroup SpecialPage
30 abstract class ChangesListSpecialPage extends SpecialPage {
31 /** @var string */
32 protected $rcSubpage;
34 /** @var FormOptions */
35 protected $rcOptions;
37 /** @var array */
38 protected $customFilters;
40 /**
41 * Main execution point
43 * @param string $subpage
45 public function execute( $subpage ) {
46 $this->rcSubpage = $subpage;
48 $this->setHeaders();
49 $this->outputHeader();
50 $this->addModules();
52 $rows = $this->getRows();
53 $opts = $this->getOptions();
54 if ( $rows === false ) {
55 if ( !$this->including() ) {
56 $this->doHeader( $opts );
59 return;
62 $batch = new LinkBatch;
63 foreach ( $rows as $row ) {
64 $batch->add( NS_USER, $row->rc_user_text );
65 $batch->add( NS_USER_TALK, $row->rc_user_text );
66 $batch->add( $row->rc_namespace, $row->rc_title );
68 $batch->execute();
70 $this->webOutput( $rows, $opts );
72 $rows->free();
75 /**
76 * Get the database result for this special page instance. Used by ApiFeedRecentChanges.
78 * @return bool|ResultWrapper Result or false
80 public function getRows() {
81 $opts = $this->getOptions();
82 $conds = $this->buildMainQueryConds( $opts );
84 return $this->doMainQuery( $conds, $opts );
87 /**
88 * Get the current FormOptions for this request
90 * @return FormOptions
92 public function getOptions() {
93 if ( $this->rcOptions === null ) {
94 $this->rcOptions = $this->setup( $this->rcSubpage );
97 return $this->rcOptions;
101 * Create a FormOptions object with options as specified by the user
103 * @param array $parameters
105 * @return FormOptions
107 public function setup( $parameters ) {
108 $opts = $this->getDefaultOptions();
109 foreach ( $this->getCustomFilters() as $key => $params ) {
110 $opts->add( $key, $params['default'] );
113 $opts = $this->fetchOptionsFromRequest( $opts );
115 // Give precedence to subpage syntax
116 if ( $parameters !== null ) {
117 $this->parseParameters( $parameters, $opts );
120 $this->validateOptions( $opts );
122 return $opts;
126 * Get a FormOptions object containing the default options. By default returns some basic options,
127 * you might want to not call parent method and discard them, or to override default values.
129 * @return FormOptions
131 public function getDefaultOptions() {
132 $opts = new FormOptions();
134 $opts->add( 'hideminor', false );
135 $opts->add( 'hidebots', false );
136 $opts->add( 'hideanons', false );
137 $opts->add( 'hideliu', false );
138 $opts->add( 'hidepatrolled', false );
139 $opts->add( 'hidemyself', false );
141 $opts->add( 'namespace', '', FormOptions::INTNULL );
142 $opts->add( 'invert', false );
143 $opts->add( 'associated', false );
145 return $opts;
149 * Get custom show/hide filters
151 * @return array Map of filter URL param names to properties (msg/default)
153 protected function getCustomFilters() {
154 if ( $this->customFilters === null ) {
155 $this->customFilters = array();
156 wfRunHooks( 'ChangesListSpecialPageFilters', array( $this, &$this->customFilters ) );
159 return $this->customFilters;
163 * Fetch values for a FormOptions object from the WebRequest associated with this instance.
165 * Intended for subclassing, e.g. to add a backwards-compatibility layer.
167 * @param FormOptions $opts
168 * @return FormOptions
170 protected function fetchOptionsFromRequest( $opts ) {
171 $opts->fetchValuesFromRequest( $this->getRequest() );
173 return $opts;
177 * Process $par and put options found in $opts. Used when including the page.
179 * @param string $par
180 * @param FormOptions $opts
182 public function parseParameters( $par, FormOptions $opts ) {
183 // nothing by default
187 * Validate a FormOptions object generated by getDefaultOptions() with values already populated.
189 * @param FormOptions $opts
191 public function validateOptions( FormOptions $opts ) {
192 // nothing by default
196 * Return an array of conditions depending of options set in $opts
198 * @param FormOptions $opts
199 * @return array
201 public function buildMainQueryConds( FormOptions $opts ) {
202 $dbr = $this->getDB();
203 $user = $this->getUser();
204 $conds = array();
206 // It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on
207 // what the user meant and either show only bots or force anons to be shown.
208 $botsonly = false;
209 $hideanons = $opts['hideanons'];
210 if ( $opts['hideanons'] && $opts['hideliu'] ) {
211 if ( $opts['hidebots'] ) {
212 $hideanons = false;
213 } else {
214 $botsonly = true;
218 // Toggles
219 if ( $opts['hideminor'] ) {
220 $conds['rc_minor'] = 0;
222 if ( $opts['hidebots'] ) {
223 $conds['rc_bot'] = 0;
225 if ( $user->useRCPatrol() && $opts['hidepatrolled'] ) {
226 $conds['rc_patrolled'] = 0;
228 if ( $botsonly ) {
229 $conds['rc_bot'] = 1;
230 } else {
231 if ( $opts['hideliu'] ) {
232 $conds[] = 'rc_user = 0';
234 if ( $hideanons ) {
235 $conds[] = 'rc_user != 0';
238 if ( $opts['hidemyself'] ) {
239 if ( $user->getId() ) {
240 $conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() );
241 } else {
242 $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
246 // Namespace filtering
247 if ( $opts['namespace'] !== '' ) {
248 $selectedNS = $dbr->addQuotes( $opts['namespace'] );
249 $operator = $opts['invert'] ? '!=' : '=';
250 $boolean = $opts['invert'] ? 'AND' : 'OR';
252 // Namespace association (bug 2429)
253 if ( !$opts['associated'] ) {
254 $condition = "rc_namespace $operator $selectedNS";
255 } else {
256 // Also add the associated namespace
257 $associatedNS = $dbr->addQuotes(
258 MWNamespace::getAssociated( $opts['namespace'] )
260 $condition = "(rc_namespace $operator $selectedNS "
261 . $boolean
262 . " rc_namespace $operator $associatedNS)";
265 $conds[] = $condition;
268 return $conds;
272 * Process the query
274 * @param array $conds
275 * @param FormOptions $opts
276 * @return bool|ResultWrapper Result or false
278 public function doMainQuery( $conds, $opts ) {
279 $tables = array( 'recentchanges' );
280 $fields = RecentChange::selectFields();
281 $query_options = array();
282 $join_conds = array();
284 ChangeTags::modifyDisplayQuery(
285 $tables,
286 $fields,
287 $conds,
288 $join_conds,
289 $query_options,
293 if ( !wfRunHooks( 'ChangesListSpecialPageQuery',
294 array( $this->getName(), &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ) )
296 return false;
299 $dbr = $this->getDB();
301 return $dbr->select(
302 $tables,
303 $fields,
304 $conds,
305 __METHOD__,
306 $query_options,
307 $join_conds
312 * Return a DatabaseBase object for reading
314 * @return DatabaseBase
316 protected function getDB() {
317 return wfGetDB( DB_SLAVE );
321 * Send output to the OutputPage object, only called if not used feeds
323 * @param ResultWrapper $rows Database rows
324 * @param FormOptions $opts
326 public function webOutput( $rows, $opts ) {
327 if ( !$this->including() ) {
328 $this->outputFeedLinks();
329 $this->doHeader( $opts );
332 $this->outputChangesList( $rows, $opts );
336 * Output feed links.
338 public function outputFeedLinks() {
339 // nothing by default
343 * Build and output the actual changes list.
345 * @param array $rows Database rows
346 * @param FormOptions $opts
348 abstract public function outputChangesList( $rows, $opts );
351 * Return the text to be displayed above the changes
353 * @param FormOptions $opts
354 * @return string XHTML
356 public function doHeader( $opts ) {
357 $this->setTopText( $opts );
359 // @todo Lots of stuff should be done here.
361 $this->setBottomText( $opts );
365 * Send the text to be displayed before the options. Should use $this->getOutput()->addWikiText()
366 * or similar methods to print the text.
368 * @param FormOptions $opts
370 function setTopText( FormOptions $opts ) {
371 // nothing by default
375 * Send the text to be displayed after the options. Should use $this->getOutput()->addWikiText()
376 * or similar methods to print the text.
378 * @param FormOptions $opts
380 function setBottomText( FormOptions $opts ) {
381 // nothing by default
385 * Get options to be displayed in a form
386 * @todo This should handle options returned by getDefaultOptions().
387 * @todo Not called by anything, should be called by something… doHeader() maybe?
389 * @param FormOptions $opts
390 * @return array
392 function getExtraOptions( $opts ) {
393 return array();
397 * Return the legend displayed within the fieldset
398 * @todo This should not be static, then we can drop the parameter
399 * @todo Not called by anything, should be called by doHeader()
401 * @param IContextSource $context The object available as $this in non-static functions
402 * @return string
404 public static function makeLegend( IContextSource $context ) {
405 global $wgRecentChangesFlags;
406 $user = $context->getUser();
407 # The legend showing what the letters and stuff mean
408 $legend = Html::openElement( 'dl' ) . "\n";
409 # Iterates through them and gets the messages for both letter and tooltip
410 $legendItems = $wgRecentChangesFlags;
411 if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
412 unset( $legendItems['unpatrolled'] );
414 foreach ( $legendItems as $key => $item ) { # generate items of the legend
415 $label = $item['title'];
416 $letter = $item['letter'];
417 $cssClass = isset( $item['class'] ) ? $item['class'] : $key;
419 $legend .= Html::element( 'dt',
420 array( 'class' => $cssClass ), $context->msg( $letter )->text()
421 ) . "\n";
422 if ( $key === 'newpage' ) {
423 $legend .= Html::openElement( 'dd' );
424 $legend .= $context->msg( $label )->escaped();
425 $legend .= ' ' . $context->msg( 'recentchanges-legend-newpage' )->parse();
426 $legend .= Html::closeElement( 'dd' ) . "\n";
427 } else {
428 $legend .= Html::element( 'dd', array(),
429 $context->msg( $label )->text()
430 ) . "\n";
433 # (+-123)
434 $legend .= Html::rawElement( 'dt',
435 array( 'class' => 'mw-plusminus-pos' ),
436 $context->msg( 'recentchanges-legend-plusminus' )->parse()
437 ) . "\n";
438 $legend .= Html::element(
439 'dd',
440 array( 'class' => 'mw-changeslist-legend-plusminus' ),
441 $context->msg( 'recentchanges-label-plusminus' )->text()
442 ) . "\n";
443 $legend .= Html::closeElement( 'dl' ) . "\n";
445 # Collapsibility
446 $legend =
447 '<div class="mw-changeslist-legend">' .
448 $context->msg( 'recentchanges-legend-heading' )->parse() .
449 '<div class="mw-collapsible-content">' . $legend . '</div>' .
450 '</div>';
452 return $legend;
456 * Add page-specific modules.
458 protected function addModules() {
459 $out = $this->getOutput();
460 // Styles and behavior for the legend box (see makeLegend())
461 $out->addModuleStyles( 'mediawiki.special.changeslist.legend' );
462 $out->addModules( 'mediawiki.special.changeslist.legend.js' );
465 protected function getGroupName() {
466 return 'changes';