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
21 * @ingroup SpecialPage
23 use MediaWiki\Logger\LoggerFactory
;
26 * Special page which uses a ChangesList to show query results.
27 * @todo Way too many public functions, most of them should be protected
29 * @ingroup SpecialPage
31 abstract class ChangesListSpecialPage
extends SpecialPage
{
35 /** @var FormOptions */
39 protected $customFilters;
42 * Main execution point
44 * @param string $subpage
46 public function execute( $subpage ) {
47 $this->rcSubpage
= $subpage;
50 $this->outputHeader();
53 $rows = $this->getRows();
54 $opts = $this->getOptions();
55 if ( $rows === false ) {
56 if ( !$this->including() ) {
57 $this->doHeader( $opts, 0 );
58 $this->getOutput()->setStatusCode( 404 );
64 $batch = new LinkBatch
;
65 foreach ( $rows as $row ) {
66 $batch->add( NS_USER
, $row->rc_user_text
);
67 $batch->add( NS_USER_TALK
, $row->rc_user_text
);
68 $batch->add( $row->rc_namespace
, $row->rc_title
);
69 if ( $row->rc_source
=== RecentChange
::SRC_LOG
) {
70 $formatter = LogFormatter
::newFromRow( $row );
71 foreach ( $formatter->getPreloadTitles() as $title ) {
72 $batch->addObj( $title );
78 $this->webOutput( $rows, $opts );
82 if ( $this->getConfig()->get( 'EnableWANCacheReaper' ) ) {
83 // Clean up any bad page entries for titles showing up in RC
84 DeferredUpdates
::addUpdate( new WANCacheReapUpdate(
86 LoggerFactory
::getInstance( 'objectcache' )
92 * Get the database result for this special page instance. Used by ApiFeedRecentChanges.
94 * @return bool|ResultWrapper Result or false
96 public function getRows() {
97 $opts = $this->getOptions();
98 $conds = $this->buildMainQueryConds( $opts );
100 return $this->doMainQuery( $conds, $opts );
104 * Get the current FormOptions for this request
106 * @return FormOptions
108 public function getOptions() {
109 if ( $this->rcOptions
=== null ) {
110 $this->rcOptions
= $this->setup( $this->rcSubpage
);
113 return $this->rcOptions
;
117 * Create a FormOptions object with options as specified by the user
119 * @param array $parameters
121 * @return FormOptions
123 public function setup( $parameters ) {
124 $opts = $this->getDefaultOptions();
125 foreach ( $this->getCustomFilters() as $key => $params ) {
126 $opts->add( $key, $params['default'] );
129 $opts = $this->fetchOptionsFromRequest( $opts );
131 // Give precedence to subpage syntax
132 if ( $parameters !== null ) {
133 $this->parseParameters( $parameters, $opts );
136 $this->validateOptions( $opts );
142 * Get a FormOptions object containing the default options. By default returns some basic options,
143 * you might want to not call parent method and discard them, or to override default values.
145 * @return FormOptions
147 public function getDefaultOptions() {
148 $config = $this->getConfig();
149 $opts = new FormOptions();
151 $opts->add( 'hideminor', false );
152 $opts->add( 'hidemajor', false );
153 $opts->add( 'hidebots', false );
154 $opts->add( 'hidehumans', false );
155 $opts->add( 'hideanons', false );
156 $opts->add( 'hideliu', false );
157 $opts->add( 'hidepatrolled', false );
158 $opts->add( 'hideunpatrolled', false );
159 $opts->add( 'hidemyself', false );
160 $opts->add( 'hidebyothers', false );
162 if ( $config->get( 'RCWatchCategoryMembership' ) ) {
163 $opts->add( 'hidecategorization', false );
165 $opts->add( 'hidepageedits', false );
166 $opts->add( 'hidenewpages', false );
167 $opts->add( 'hidelog', false );
169 $opts->add( 'namespace', '', FormOptions
::INTNULL
);
170 $opts->add( 'invert', false );
171 $opts->add( 'associated', false );
177 * Get custom show/hide filters
179 * @return array Map of filter URL param names to properties (msg/default)
181 protected function getCustomFilters() {
182 if ( $this->customFilters
=== null ) {
183 $this->customFilters
= [];
184 Hooks
::run( 'ChangesListSpecialPageFilters', [ $this, &$this->customFilters
] );
187 return $this->customFilters
;
191 * Fetch values for a FormOptions object from the WebRequest associated with this instance.
193 * Intended for subclassing, e.g. to add a backwards-compatibility layer.
195 * @param FormOptions $opts
196 * @return FormOptions
198 protected function fetchOptionsFromRequest( $opts ) {
199 $opts->fetchValuesFromRequest( $this->getRequest() );
205 * Process $par and put options found in $opts. Used when including the page.
208 * @param FormOptions $opts
210 public function parseParameters( $par, FormOptions
$opts ) {
211 // nothing by default
215 * Validate a FormOptions object generated by getDefaultOptions() with values already populated.
217 * @param FormOptions $opts
219 public function validateOptions( FormOptions
$opts ) {
220 // nothing by default
224 * Return an array of conditions depending of options set in $opts
226 * @param FormOptions $opts
229 public function buildMainQueryConds( FormOptions
$opts ) {
230 $dbr = $this->getDB();
231 $user = $this->getUser();
234 // It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on
235 // what the user meant and either show only bots or force anons to be shown.
237 $hideanons = $opts['hideanons'];
238 if ( $opts['hideanons'] && $opts['hideliu'] ) {
239 if ( $opts['hidebots'] ) {
247 if ( $opts['hideminor'] ) {
248 $conds[] = 'rc_minor = 0';
250 if ( $opts['hidemajor'] ) {
251 $conds[] = 'rc_minor = 1';
253 if ( $opts['hidebots'] ) {
254 $conds['rc_bot'] = 0;
256 if ( $opts['hidehumans'] ) {
257 $conds[] = 'rc_bot = 1';
259 if ( $user->useRCPatrol() ) {
260 if ( $opts['hidepatrolled'] ) {
261 $conds[] = 'rc_patrolled = 0';
263 if ( $opts['hideunpatrolled'] ) {
264 $conds[] = 'rc_patrolled = 1';
268 $conds['rc_bot'] = 1;
270 if ( $opts['hideliu'] ) {
271 $conds[] = 'rc_user = 0';
274 $conds[] = 'rc_user != 0';
278 if ( $opts['hidemyself'] ) {
279 if ( $user->getId() ) {
280 $conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() );
282 $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
285 if ( $opts['hidebyothers'] ) {
286 if ( $user->getId() ) {
287 $conds[] = 'rc_user = ' . $dbr->addQuotes( $user->getId() );
289 $conds[] = 'rc_user_text = ' . $dbr->addQuotes( $user->getName() );
293 if ( $this->getConfig()->get( 'RCWatchCategoryMembership' )
294 && $opts['hidecategorization'] === true
296 $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE
);
298 if ( $opts['hidepageedits'] ) {
299 $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_EDIT
);
301 if ( $opts['hidenewpages'] ) {
302 $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_NEW
);
304 if ( $opts['hidelog'] ) {
305 $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG
);
308 // Namespace filtering
309 if ( $opts['namespace'] !== '' ) {
310 $selectedNS = $dbr->addQuotes( $opts['namespace'] );
311 $operator = $opts['invert'] ?
'!=' : '=';
312 $boolean = $opts['invert'] ?
'AND' : 'OR';
314 // Namespace association (bug 2429)
315 if ( !$opts['associated'] ) {
316 $condition = "rc_namespace $operator $selectedNS";
318 // Also add the associated namespace
319 $associatedNS = $dbr->addQuotes(
320 MWNamespace
::getAssociated( $opts['namespace'] )
322 $condition = "(rc_namespace $operator $selectedNS "
324 . " rc_namespace $operator $associatedNS)";
327 $conds[] = $condition;
336 * @param array $conds
337 * @param FormOptions $opts
338 * @return bool|ResultWrapper Result or false
340 public function doMainQuery( $conds, $opts ) {
341 $tables = [ 'recentchanges' ];
342 $fields = RecentChange
::selectFields();
346 ChangeTags
::modifyDisplayQuery(
355 if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
361 $dbr = $this->getDB();
373 protected function runMainQueryHook( &$tables, &$fields, &$conds,
374 &$query_options, &$join_conds, $opts
377 'ChangesListSpecialPageQuery',
378 [ $this->getName(), &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ]
383 * Return a IDatabase object for reading
387 protected function getDB() {
388 return wfGetDB( DB_REPLICA
);
392 * Send output to the OutputPage object, only called if not used feeds
394 * @param ResultWrapper $rows Database rows
395 * @param FormOptions $opts
397 public function webOutput( $rows, $opts ) {
398 if ( !$this->including() ) {
399 $this->outputFeedLinks();
400 $this->doHeader( $opts, $rows->numRows() );
403 $this->outputChangesList( $rows, $opts );
409 public function outputFeedLinks() {
410 // nothing by default
414 * Build and output the actual changes list.
416 * @param ResultWrapper $rows Database rows
417 * @param FormOptions $opts
419 abstract public function outputChangesList( $rows, $opts );
422 * Set the text to be displayed above the changes
424 * @param FormOptions $opts
425 * @param int $numRows Number of rows in the result to show after this header
427 public function doHeader( $opts, $numRows ) {
428 $this->setTopText( $opts );
430 // @todo Lots of stuff should be done here.
432 $this->setBottomText( $opts );
436 * Send the text to be displayed before the options. Should use $this->getOutput()->addWikiText()
437 * or similar methods to print the text.
439 * @param FormOptions $opts
441 public function setTopText( FormOptions
$opts ) {
442 // nothing by default
446 * Send the text to be displayed after the options. Should use $this->getOutput()->addWikiText()
447 * or similar methods to print the text.
449 * @param FormOptions $opts
451 public function setBottomText( FormOptions
$opts ) {
452 // nothing by default
456 * Get options to be displayed in a form
457 * @todo This should handle options returned by getDefaultOptions().
458 * @todo Not called by anything, should be called by something… doHeader() maybe?
460 * @param FormOptions $opts
463 public function getExtraOptions( $opts ) {
468 * Return the legend displayed within the fieldset
472 public function makeLegend() {
473 $context = $this->getContext();
474 $user = $context->getUser();
475 # The legend showing what the letters and stuff mean
476 $legend = Html
::openElement( 'dl' ) . "\n";
477 # Iterates through them and gets the messages for both letter and tooltip
478 $legendItems = $context->getConfig()->get( 'RecentChangesFlags' );
479 if ( !( $user->useRCPatrol() ||
$user->useNPPatrol() ) ) {
480 unset( $legendItems['unpatrolled'] );
482 foreach ( $legendItems as $key => $item ) { # generate items of the legend
483 $label = isset( $item['legend'] ) ?
$item['legend'] : $item['title'];
484 $letter = $item['letter'];
485 $cssClass = isset( $item['class'] ) ?
$item['class'] : $key;
487 $legend .= Html
::element( 'dt',
488 [ 'class' => $cssClass ], $context->msg( $letter )->text()
490 Html
::rawElement( 'dd',
491 [ 'class' => Sanitizer
::escapeClass( 'mw-changeslist-legend-' . $key ) ],
492 $context->msg( $label )->parse()
496 $legend .= Html
::rawElement( 'dt',
497 [ 'class' => 'mw-plusminus-pos' ],
498 $context->msg( 'recentchanges-legend-plusminus' )->parse()
500 $legend .= Html
::element(
502 [ 'class' => 'mw-changeslist-legend-plusminus' ],
503 $context->msg( 'recentchanges-label-plusminus' )->text()
505 $legend .= Html
::closeElement( 'dl' ) . "\n";
509 '<div class="mw-changeslist-legend">' .
510 $context->msg( 'recentchanges-legend-heading' )->parse() .
511 '<div class="mw-collapsible-content">' . $legend . '</div>' .
518 * Add page-specific modules.
520 protected function addModules() {
521 $out = $this->getOutput();
522 // Styles and behavior for the legend box (see makeLegend())
523 $out->addModuleStyles( [
524 'mediawiki.special.changeslist.legend',
525 'mediawiki.special.changeslist',
527 $out->addModules( 'mediawiki.special.changeslist.legend.js' );
530 protected function getGroupName() {
535 * Get filters that can be rendered.
537 * Filters with 'msg' => false can be used to filter data but won't
538 * be presented as show/hide toggles in the UI. They are not returned
541 * @param array $allFilters Map of filter URL param names to properties (msg/default)
542 * @return array Map of filter URL param names to properties (msg/default)
544 protected function getRenderableCustomFilters( $allFilters ) {
547 function( $filter ) {
548 return isset( $filter['msg'] ) && ( $filter['msg'] !== false );