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
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 $rcSubpage, $rcOptions; // @todo Rename these, make protected
32 protected $customFilters;
35 * The feed format to output as (either 'rss' or 'atom'), or null if no
36 * feed output was requested
38 * @var string $feedFormat
40 protected $feedFormat;
43 * Main execution point
45 * @param string $subpage
47 public function execute( $subpage ) {
48 $this->rcSubpage
= $subpage;
49 $this->feedFormat
= $this->including() ?
null : $this->getRequest()->getVal( 'feed' );
50 if ( $this->feedFormat
!== 'atom' && $this->feedFormat
!== 'rss' ) {
51 $this->feedFormat
= null;
55 $this->outputHeader();
58 $opts = $this->getOptions();
59 // Fetch results, prepare a batch link existence check query
60 $conds = $this->buildMainQueryConds( $opts );
61 $rows = $this->doMainQuery( $conds, $opts );
62 if ( $rows === false ) {
63 if ( !$this->including() ) {
64 $this->doHeader( $opts );
70 if ( !$this->feedFormat
) {
71 $batch = new LinkBatch
;
72 foreach ( $rows as $row ) {
73 $batch->add( NS_USER
, $row->rc_user_text
);
74 $batch->add( NS_USER_TALK
, $row->rc_user_text
);
75 $batch->add( $row->rc_namespace
, $row->rc_title
);
79 if ( $this->feedFormat
) {
80 list( $changesFeed, $formatter ) = $this->getFeedObject( $this->feedFormat
);
81 /** @var ChangesFeed $changesFeed */
82 $changesFeed->execute( $formatter, $rows, $this->checkLastModified( $this->feedFormat
), $opts );
84 $this->webOutput( $rows, $opts );
91 * Get the current FormOptions for this request
95 public function getOptions() {
96 if ( $this->rcOptions
=== null ) {
97 $this->rcOptions
= $this->setup( $this->rcSubpage
);
100 return $this->rcOptions
;
104 * Create a FormOptions object with options as specified by the user
106 * @param array $parameters
108 * @return FormOptions
110 public function setup( $parameters ) {
111 $opts = $this->getDefaultOptions();
112 foreach ( $this->getCustomFilters() as $key => $params ) {
113 $opts->add( $key, $params['default'] );
116 $opts = $this->fetchOptionsFromRequest( $opts );
118 // Give precedence to subpage syntax
119 if ( $parameters !== null ) {
120 $this->parseParameters( $parameters, $opts );
123 $this->validateOptions( $opts );
129 * Get a FormOptions object containing the default options. By default returns some basic options,
130 * you might want to not call parent method and discard them, or to override default values.
132 * @return FormOptions
134 public function getDefaultOptions() {
135 $opts = new FormOptions();
137 $opts->add( 'hideminor', false );
138 $opts->add( 'hidebots', false );
139 $opts->add( 'hideanons', false );
140 $opts->add( 'hideliu', false );
141 $opts->add( 'hidepatrolled', false );
142 $opts->add( 'hidemyself', false );
144 $opts->add( 'namespace', '', FormOptions
::INTNULL
);
145 $opts->add( 'invert', false );
146 $opts->add( 'associated', false );
152 * Get custom show/hide filters
154 * @return array Map of filter URL param names to properties (msg/default)
156 protected function getCustomFilters() {
157 // @todo Fire a Special{$this->getName()}Filters hook here
162 * Fetch values for a FormOptions object from the WebRequest associated with this instance.
164 * Intended for subclassing, e.g. to add a backwards-compatibility layer.
166 * @param FormOptions $parameters
167 * @return FormOptions
169 protected function fetchOptionsFromRequest( $opts ) {
170 $opts->fetchValuesFromRequest( $this->getRequest() );
175 * Process $par and put options found in $opts. Used when including the page.
178 * @param FormOptions $opts
180 public function parseParameters( $par, FormOptions
$opts ) {
181 // nothing by default
185 * Validate a FormOptions object generated by getDefaultOptions() with values already populated.
187 * @param FormOptions $opts
189 public function validateOptions( FormOptions
$opts ) {
190 // nothing by default
194 * Return an array of conditions depending of options set in $opts
196 * @param FormOptions $opts
199 public function buildMainQueryConds( FormOptions
$opts ) {
200 $dbr = $this->getDB();
201 $user = $this->getUser();
204 // It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on
205 // what the user meant and either show only bots or force anons to be shown.
207 $hideanons = $opts['hideanons'];
208 if ( $opts['hideanons'] && $opts['hideliu'] ) {
209 if ( $opts['hidebots'] ) {
217 if ( $opts['hideminor'] ) {
218 $conds['rc_minor'] = 0;
220 if ( $opts['hidebots'] ) {
221 $conds['rc_bot'] = 0;
223 if ( $user->useRCPatrol() && $opts['hidepatrolled'] ) {
224 $conds['rc_patrolled'] = 0;
227 $conds['rc_bot'] = 1;
229 if ( $opts['hideliu'] ) {
230 $conds[] = 'rc_user = 0';
233 $conds[] = 'rc_user != 0';
236 if ( $opts['hidemyself'] ) {
237 if ( $user->getId() ) {
238 $conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() );
240 $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
244 // Namespace filtering
245 if ( $opts['namespace'] !== '' ) {
246 $selectedNS = $dbr->addQuotes( $opts['namespace'] );
247 $operator = $opts['invert'] ?
'!=' : '=';
248 $boolean = $opts['invert'] ?
'AND' : 'OR';
250 // Namespace association (bug 2429)
251 if ( !$opts['associated'] ) {
252 $condition = "rc_namespace $operator $selectedNS";
254 // Also add the associated namespace
255 $associatedNS = $dbr->addQuotes(
256 MWNamespace
::getAssociated( $opts['namespace'] )
258 $condition = "(rc_namespace $operator $selectedNS "
260 . " rc_namespace $operator $associatedNS)";
263 $conds[] = $condition;
272 * @param array $conds
273 * @param FormOptions $opts
274 * @return bool|ResultWrapper Result or false
276 public function doMainQuery( $conds, $opts ) {
277 $tables = array( 'recentchanges' );
278 $fields = RecentChange
::selectFields();
279 $query_options = array();
280 $join_conds = array();
282 ChangeTags
::modifyDisplayQuery(
291 // @todo Fire a Special{$this->getName()}Query hook here
292 // @todo Uncomment and document
293 // if ( !wfRunHooks( 'ChangesListSpecialPageQuery',
294 // array( &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ) )
299 $dbr = $this->getDB();
311 * Return a DatabaseBase object for reading
313 * @return DatabaseBase
315 protected function getDB() {
316 return wfGetDB( DB_SLAVE
);
320 * Send output to the OutputPage object, only called if not used feeds
322 * @param ResultWrapper $rows Database rows
323 * @param FormOptions $opts
325 public function webOutput( $rows, $opts ) {
326 if ( !$this->including() ) {
327 $this->outputFeedLinks();
328 $this->doHeader( $opts );
331 $this->outputChangesList( $rows, $opts );
337 public function outputFeedLinks() {
338 // nothing by default
342 * Build and output the actual changes list.
344 * @param array $rows Database rows
345 * @param FormOptions $opts
347 abstract public function outputChangesList( $rows, $opts );
350 * Return the text to be displayed above the changes
352 * @param FormOptions $opts
353 * @return string XHTML
355 public function doHeader( $opts ) {
356 $this->setTopText( $opts );
358 // @todo Lots of stuff should be done here.
360 $this->setBottomText( $opts );
364 * Send the text to be displayed before the options. Should use $this->getOutput()->addWikiText()
365 * or similar methods to print the text.
367 * @param FormOptions $opts
369 function setTopText( FormOptions
$opts ) {
370 // nothing by default
374 * Send the text to be displayed after the options. Should use $this->getOutput()->addWikiText()
375 * or similar methods to print the text.
377 * @param FormOptions $opts
379 function setBottomText( FormOptions
$opts ) {
380 // nothing by default
384 * Get options to be displayed in a form
385 * @todo This should handle options returned by getDefaultOptions().
386 * @todo Not called by anything, should be called by something… doHeader() maybe?
388 * @param FormOptions $opts
391 function getExtraOptions( $opts ) {
396 * Return the legend displayed within the fieldset
397 * @todo This should not be static, then we can drop the parameter
398 * @todo Not called by anything, should be called by doHeader()
400 * @param $context the object available as $this in non-static functions
403 public static function makeLegend( IContextSource
$context ) {
404 global $wgRecentChangesFlags;
405 $user = $context->getUser();
406 # The legend showing what the letters and stuff mean
407 $legend = Xml
::openElement( 'dl' ) . "\n";
408 # Iterates through them and gets the messages for both letter and tooltip
409 $legendItems = $wgRecentChangesFlags;
410 if ( !$user->useRCPatrol() ) {
411 unset( $legendItems['unpatrolled'] );
413 foreach ( $legendItems as $key => $legendInfo ) { # generate items of the legend
414 $label = $legendInfo['title'];
415 $letter = $legendInfo['letter'];
416 $cssClass = isset( $legendInfo['class'] ) ?
$legendInfo['class'] : $key;
418 $legend .= Xml
::element( 'dt',
419 array( 'class' => $cssClass ), $context->msg( $letter )->text()
421 if ( $key === 'newpage' ) {
422 $legend .= Xml
::openElement( 'dd' );
423 $legend .= $context->msg( $label )->escaped();
424 $legend .= ' ' . $context->msg( 'recentchanges-legend-newpage' )->parse();
425 $legend .= Xml
::closeElement( 'dd' ) . "\n";
427 $legend .= Xml
::element( 'dd', array(),
428 $context->msg( $label )->text()
433 $legend .= Xml
::tags( 'dt',
434 array( 'class' => 'mw-plusminus-pos' ),
435 $context->msg( 'recentchanges-legend-plusminus' )->parse()
437 $legend .= Xml
::element(
439 array( 'class' => 'mw-changeslist-legend-plusminus' ),
440 $context->msg( 'recentchanges-label-plusminus' )->text()
442 $legend .= Xml
::closeElement( 'dl' ) . "\n";
446 '<div class="mw-changeslist-legend">' .
447 $context->msg( 'recentchanges-legend-heading' )->parse() .
448 '<div class="mw-collapsible-content">' . $legend . '</div>' .
455 * Add page-specific modules.
457 protected function addModules() {
458 $out = $this->getOutput();
459 // Styles and behavior for the legend box (see makeLegend())
460 $out->addModuleStyles( 'mediawiki.special.changeslist.legend' );
461 $out->addModules( 'mediawiki.special.changeslist.legend.js' );
465 * Return an array with a ChangesFeed object and ChannelFeed object.
467 * This is intentionally not abstract not to require subclasses which don't
468 * use feeds functionality to implement it.
470 * @param string $feedFormat Feed's format (either 'rss' or 'atom')
473 public function getFeedObject( $feedFormat ) {
474 throw new MWException( "Not implemented" );
478 * Get last-modified date, for client caching. Not implemented by default
479 * (returns current time).
481 * @param string $feedFormat
482 * @return string|bool
484 public function checkLastModified( $feedFormat ) {
485 return wfTimestampNow();
488 protected function getGroupName() {