3 * Implements Special:Recentchanges
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 * A special page that lists last changes made to the wiki
27 * @ingroup SpecialPage
29 class SpecialRecentChanges
extends IncludableSpecialPage
{
30 var $rcOptions, $rcSubpage;
31 protected $customFilters;
34 * The feed format to output as (either 'rss' or 'atom'), or null if no
35 * feed output was requested
37 * @var string $feedFormat
39 protected $feedFormat;
41 public function __construct( $name = 'Recentchanges' ) {
42 parent
::__construct( $name );
46 * Get a FormOptions object containing the default options
50 public function getDefaultOptions() {
51 $opts = new FormOptions();
52 $user = $this->getUser();
54 $opts->add( 'days', $user->getIntOption( 'rcdays' ) );
55 $opts->add( 'limit', $user->getIntOption( 'rclimit' ) );
56 $opts->add( 'from', '' );
58 $opts->add( 'hideminor', $user->getBoolOption( 'hideminor' ) );
59 $opts->add( 'hidebots', true );
60 $opts->add( 'hideanons', false );
61 $opts->add( 'hideliu', false );
62 $opts->add( 'hidepatrolled', $user->getBoolOption( 'hidepatrolled' ) );
63 $opts->add( 'hidemyself', false );
65 $opts->add( 'namespace', '', FormOptions
::INTNULL
);
66 $opts->add( 'invert', false );
67 $opts->add( 'associated', false );
69 $opts->add( 'categories', '' );
70 $opts->add( 'categories_any', false );
71 $opts->add( 'tagfilter', '' );
77 * Create a FormOptions object with options as specified by the user
79 * @param array $parameters
82 public function setup( $parameters ) {
85 $opts = $this->getDefaultOptions();
87 foreach ( $this->getCustomFilters() as $key => $params ) {
88 $opts->add( $key, $params['default'] );
91 $opts->fetchValuesFromRequest( $this->getRequest() );
93 // Give precedence to subpage syntax
94 if ( $parameters !== null ) {
95 $this->parseParameters( $parameters, $opts );
98 $opts->validateIntBounds( 'limit', 0, $this->feedFormat ?
$wgFeedLimit : 5000 );
104 * Get custom show/hide filters
106 * @return array Map of filter URL param names to properties (msg/default)
108 protected function getCustomFilters() {
109 if ( $this->customFilters
=== null ) {
110 $this->customFilters
= array();
111 wfRunHooks( 'SpecialRecentChangesFilters', array( $this, &$this->customFilters
) );
114 return $this->customFilters
;
118 * Get the current FormOptions for this request
120 public function getOptions() {
121 if ( $this->rcOptions
=== null ) {
122 $this->rcOptions
= $this->setup( $this->rcSubpage
);
125 return $this->rcOptions
;
129 * Main execution point
131 * @param string $subpage
133 public function execute( $subpage ) {
134 $this->rcSubpage
= $subpage;
135 $this->feedFormat
= $this->including() ?
null : $this->getRequest()->getVal( 'feed' );
137 # 10 seconds server-side caching max
138 $this->getOutput()->setSquidMaxage( 10 );
139 # Check if the client has a cached version
140 $lastmod = $this->checkLastModified( $this->feedFormat
);
141 if ( $lastmod === false ) {
145 $opts = $this->getOptions();
147 $this->outputHeader();
150 // Fetch results, prepare a batch link existence check query
151 $conds = $this->buildMainQueryConds( $opts );
152 $rows = $this->doMainQuery( $conds, $opts );
153 if ( $rows === false ) {
154 if ( !$this->including() ) {
155 $this->doHeader( $opts );
161 if ( !$this->feedFormat
) {
162 $batch = new LinkBatch
;
163 foreach ( $rows as $row ) {
164 $batch->add( NS_USER
, $row->rc_user_text
);
165 $batch->add( NS_USER_TALK
, $row->rc_user_text
);
166 $batch->add( $row->rc_namespace
, $row->rc_title
);
170 if ( $this->feedFormat
) {
171 list( $changesFeed, $formatter ) = $this->getFeedObject( $this->feedFormat
);
172 /** @var ChangesFeed $changesFeed */
173 $changesFeed->execute( $formatter, $rows, $lastmod, $opts );
175 $this->webOutput( $rows, $opts );
182 * Return an array with a ChangesFeed object and ChannelFeed object
184 * @param string $feedFormat Feed's format (either 'rss' or 'atom')
187 public function getFeedObject( $feedFormat ) {
188 $changesFeed = new ChangesFeed( $feedFormat, 'rcfeed' );
189 $formatter = $changesFeed->getFeedObject(
190 $this->msg( 'recentchanges' )->inContentLanguage()->text(),
191 $this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(),
192 $this->getPageTitle()->getFullURL()
195 return array( $changesFeed, $formatter );
199 * Process $par and put options found if $opts
200 * Mainly used when including the page
203 * @param FormOptions $opts
205 public function parseParameters( $par, FormOptions
$opts ) {
206 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
207 foreach ( $bits as $bit ) {
208 if ( 'hidebots' === $bit ) {
209 $opts['hidebots'] = true;
211 if ( 'bots' === $bit ) {
212 $opts['hidebots'] = false;
214 if ( 'hideminor' === $bit ) {
215 $opts['hideminor'] = true;
217 if ( 'minor' === $bit ) {
218 $opts['hideminor'] = false;
220 if ( 'hideliu' === $bit ) {
221 $opts['hideliu'] = true;
223 if ( 'hidepatrolled' === $bit ) {
224 $opts['hidepatrolled'] = true;
226 if ( 'hideanons' === $bit ) {
227 $opts['hideanons'] = true;
229 if ( 'hidemyself' === $bit ) {
230 $opts['hidemyself'] = true;
233 if ( is_numeric( $bit ) ) {
234 $opts['limit'] = $bit;
238 if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
239 $opts['limit'] = $m[1];
241 if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) {
242 $opts['days'] = $m[1];
244 if ( preg_match( '/^namespace=(\d+)$/', $bit, $m ) ) {
245 $opts['namespace'] = $m[1];
251 * Get last modified date, for client caching
252 * Don't use this if we are using the patrol feature, patrol changes don't
253 * update the timestamp
255 * @param string $feedFormat
256 * @return string|bool
258 public function checkLastModified( $feedFormat ) {
259 $dbr = wfGetDB( DB_SLAVE
);
260 $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__
);
261 if ( $feedFormat ||
!$this->getUser()->useRCPatrol() ) {
262 if ( $lastmod && $this->getOutput()->checkLastModified( $lastmod ) ) {
263 # Client cache fresh and headers sent, nothing more to do.
272 * Return an array of conditions depending of options set in $opts
274 * @param FormOptions $opts
277 public function buildMainQueryConds( FormOptions
$opts ) {
278 $dbr = wfGetDB( DB_SLAVE
);
281 # It makes no sense to hide both anons and logged-in users
282 # Where this occurs, force anons to be shown
284 if ( $opts['hideanons'] && $opts['hideliu'] ) {
285 # Check if the user wants to show bots only
286 if ( $opts['hidebots'] ) {
287 $opts['hideanons'] = false;
290 $opts['hidebots'] = false;
295 $cutoff_unixtime = time() - ( $opts['days'] * 86400 );
296 $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime %
86400 );
297 $cutoff = $dbr->timestamp( $cutoff_unixtime );
299 $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
300 if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW
, $cutoff ) ) {
301 $cutoff = $dbr->timestamp( $opts['from'] );
303 $opts->reset( 'from' );
306 $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
308 $hidePatrol = $this->getUser()->useRCPatrol() && $opts['hidepatrolled'];
309 $hideLoggedInUsers = $opts['hideliu'] && !$forcebot;
310 $hideAnonymousUsers = $opts['hideanons'] && !$forcebot;
312 if ( $opts['hideminor'] ) {
313 $conds['rc_minor'] = 0;
315 if ( $opts['hidebots'] ) {
316 $conds['rc_bot'] = 0;
319 $conds['rc_patrolled'] = 0;
322 $conds['rc_bot'] = 1;
324 if ( $hideLoggedInUsers ) {
325 $conds[] = 'rc_user = 0';
327 if ( $hideAnonymousUsers ) {
328 $conds[] = 'rc_user != 0';
331 if ( $opts['hidemyself'] ) {
332 if ( $this->getUser()->getId() ) {
333 $conds[] = 'rc_user != ' . $dbr->addQuotes( $this->getUser()->getId() );
335 $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $this->getUser()->getName() );
339 # Namespace filtering
340 if ( $opts['namespace'] !== '' ) {
341 $selectedNS = $dbr->addQuotes( $opts['namespace'] );
342 $operator = $opts['invert'] ?
'!=' : '=';
343 $boolean = $opts['invert'] ?
'AND' : 'OR';
345 # namespace association (bug 2429)
346 if ( !$opts['associated'] ) {
347 $condition = "rc_namespace $operator $selectedNS";
349 # Also add the associated namespace
350 $associatedNS = $dbr->addQuotes(
351 MWNamespace
::getAssociated( $opts['namespace'] )
353 $condition = "(rc_namespace $operator $selectedNS "
355 . " rc_namespace $operator $associatedNS)";
358 $conds[] = $condition;
367 * @param array $conds
368 * @param FormOptions $opts
369 * @return bool|ResultWrapper Result or false (for Recentchangeslinked only)
371 public function doMainQuery( $conds, $opts ) {
372 $tables = array( 'recentchanges' );
373 $join_conds = array();
374 $query_options = array();
376 $uid = $this->getUser()->getId();
377 $dbr = wfGetDB( DB_SLAVE
);
378 $limit = $opts['limit'];
379 $namespace = $opts['namespace'];
380 $invert = $opts['invert'];
381 $associated = $opts['associated'];
383 $fields = RecentChange
::selectFields();
384 // JOIN on watchlist for users
385 if ( $uid && $this->getUser()->isAllowed( 'viewmywatchlist' ) ) {
386 $tables[] = 'watchlist';
387 $fields[] = 'wl_user';
388 $fields[] = 'wl_notificationtimestamp';
389 $join_conds['watchlist'] = array( 'LEFT JOIN', array(
392 'wl_namespace=rc_namespace'
395 if ( $this->getUser()->isAllowed( 'rollback' ) ) {
397 $fields[] = 'page_latest';
398 $join_conds['page'] = array( 'LEFT JOIN', 'rc_cur_id=page_id' );
401 ChangeTags
::modifyDisplayQuery(
410 if ( !wfRunHooks( 'SpecialRecentChangesQuery',
411 array( &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ) )
416 // rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough
417 // knowledge to use an index merge if it wants (it may use some other index though).
421 $conds +
array( 'rc_new' => array( 0, 1 ) ),
423 array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ) +
$query_options,
429 * Send output to the OutputPage object, only called if not used feeds
431 * @param array $rows Database rows
432 * @param FormOptions $opts
434 public function webOutput( $rows, $opts ) {
435 global $wgRCShowWatchingUsers, $wgShowUpdatedMarker, $wgAllowCategorizedRecentChanges;
437 // Build the final data
439 if ( $wgAllowCategorizedRecentChanges ) {
440 $this->filterByCategories( $rows, $opts );
443 $limit = $opts['limit'];
445 $showWatcherCount = $wgRCShowWatchingUsers && $this->getUser()->getOption( 'shownumberswatching' );
446 $watcherCache = array();
448 $dbr = wfGetDB( DB_SLAVE
);
451 $list = ChangesList
::newFromContext( $this->getContext() );
453 $rclistOutput = $list->beginRecentChangesList();
454 foreach ( $rows as $obj ) {
458 $rc = RecentChange
::newFromRow( $obj );
459 $rc->counter
= $counter++
;
460 # Check if the page has been updated since the last visit
461 if ( $wgShowUpdatedMarker && !empty( $obj->wl_notificationtimestamp
) ) {
462 $rc->notificationtimestamp
= ( $obj->rc_timestamp
>= $obj->wl_notificationtimestamp
);
464 $rc->notificationtimestamp
= false; // Default
466 # Check the number of users watching the page
467 $rc->numberofWatchingusers
= 0; // Default
468 if ( $showWatcherCount && $obj->rc_namespace
>= 0 ) {
469 if ( !isset( $watcherCache[$obj->rc_namespace
][$obj->rc_title
] ) ) {
470 $watcherCache[$obj->rc_namespace
][$obj->rc_title
] =
475 'wl_namespace' => $obj->rc_namespace
,
476 'wl_title' => $obj->rc_title
,
478 __METHOD__
. '-watchers'
481 $rc->numberofWatchingusers
= $watcherCache[$obj->rc_namespace
][$obj->rc_title
];
484 $changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user
), $counter );
485 if ( $changeLine !== false ) {
486 $rclistOutput .= $changeLine;
490 $rclistOutput .= $list->endRecentChangesList();
494 if ( !$this->including() ) {
495 // Output options box
496 $this->doHeader( $opts );
499 // And now for the content
500 $feedQuery = $this->getFeedQuery();
501 if ( $feedQuery !== '' ) {
502 $this->getOutput()->setFeedAppendQuery( $feedQuery );
504 $this->getOutput()->setFeedAppendQuery( false );
507 if ( $rows->numRows() === 0 ) {
508 $this->getOutput()->addHtml(
509 '<div class="mw-changeslist-empty">' . $this->msg( 'recentchanges-noresult' )->parse() . '</div>'
512 $this->getOutput()->addHTML( $rclistOutput );
517 * Get the query string to append to feed link URLs.
521 public function getFeedQuery() {
524 $this->getOptions()->validateIntBounds( 'limit', 0, $wgFeedLimit );
525 $options = $this->getOptions()->getChangedValues();
527 // wfArrayToCgi() omits options set to null or false
528 foreach ( $options as &$value ) {
529 if ( $value === false ) {
535 return wfArrayToCgi( $options );
539 * Return the text to be displayed above the changes
541 * @param FormOptions $opts
542 * @return string XHTML
544 public function doHeader( $opts ) {
547 $this->setTopText( $opts );
549 $defaults = $opts->getAllValues();
550 $nondefaults = $opts->getChangedValues();
553 $panel[] = self
::makeLegend( $this->getContext() );
554 $panel[] = $this->optionsPanel( $defaults, $nondefaults );
557 $extraOpts = $this->getExtraOptions( $opts );
558 $extraOptsCount = count( $extraOpts );
560 $submit = ' ' . Xml
::submitbutton( $this->msg( 'allpagessubmit' )->text() );
562 $out = Xml
::openElement( 'table', array( 'class' => 'mw-recentchanges-table' ) );
563 foreach ( $extraOpts as $name => $optionRow ) {
564 # Add submit button to the last row only
566 $addSubmit = ( $count === $extraOptsCount ) ?
$submit : '';
568 $out .= Xml
::openElement( 'tr' );
569 if ( is_array( $optionRow ) ) {
572 array( 'class' => 'mw-label mw-' . $name . '-label' ),
577 array( 'class' => 'mw-input' ),
578 $optionRow[1] . $addSubmit
583 array( 'class' => 'mw-input', 'colspan' => 2 ),
584 $optionRow . $addSubmit
587 $out .= Xml
::closeElement( 'tr' );
589 $out .= Xml
::closeElement( 'table' );
591 $unconsumed = $opts->getUnconsumedValues();
592 foreach ( $unconsumed as $key => $value ) {
593 $out .= Html
::hidden( $key, $value );
596 $t = $this->getPageTitle();
597 $out .= Html
::hidden( 'title', $t->getPrefixedText() );
598 $form = Xml
::tags( 'form', array( 'action' => $wgScript ), $out );
600 $panelString = implode( "\n", $panel );
602 $this->getOutput()->addHTML(
604 $this->msg( 'recentchanges-legend' )->text(),
606 array( 'class' => 'rcoptions' )
610 $this->setBottomText( $opts );
614 * Get options to be displayed in a form
616 * @param FormOptions $opts
619 function getExtraOptions( $opts ) {
620 $opts->consumeValues( array(
621 'namespace', 'invert', 'associated', 'tagfilter', 'categories', 'categories_any'
624 $extraOpts = array();
625 $extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
627 global $wgAllowCategorizedRecentChanges;
628 if ( $wgAllowCategorizedRecentChanges ) {
629 $extraOpts['category'] = $this->categoryFilterForm( $opts );
632 $tagFilter = ChangeTags
::buildTagFilterSelector( $opts['tagfilter'] );
633 if ( count( $tagFilter ) ) {
634 $extraOpts['tagfilter'] = $tagFilter;
637 // Don't fire the hook for subclasses. (Or should we?)
638 if ( $this->getName() === 'Recentchanges' ) {
639 wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) );
646 * Return the legend displayed within the fieldset.
648 * This method is also called from SpecialWatchlist.
650 * @param $context the object available as $this in non-static functions
653 public static function makeLegend( IContextSource
$context ) {
654 global $wgRecentChangesFlags;
655 $user = $context->getUser();
656 # The legend showing what the letters and stuff mean
657 $legend = Xml
::openElement( 'dl' ) . "\n";
658 # Iterates through them and gets the messages for both letter and tooltip
659 $legendItems = $wgRecentChangesFlags;
660 if ( !$user->useRCPatrol() ) {
661 unset( $legendItems['unpatrolled'] );
663 foreach ( $legendItems as $key => $legendInfo ) { # generate items of the legend
664 $label = $legendInfo['title'];
665 $letter = $legendInfo['letter'];
666 $cssClass = isset( $legendInfo['class'] ) ?
$legendInfo['class'] : $key;
668 $legend .= Xml
::element( 'dt',
669 array( 'class' => $cssClass ), $context->msg( $letter )->text()
671 if ( $key === 'newpage' ) {
672 $legend .= Xml
::openElement( 'dd' );
673 $legend .= $context->msg( $label )->escaped();
674 $legend .= ' ' . $context->msg( 'recentchanges-legend-newpage' )->parse();
675 $legend .= Xml
::closeElement( 'dd' ) . "\n";
677 $legend .= Xml
::element( 'dd', array(),
678 $context->msg( $label )->text()
683 $legend .= Xml
::tags( 'dt',
684 array( 'class' => 'mw-plusminus-pos' ),
685 $context->msg( 'recentchanges-legend-plusminus' )->parse()
687 $legend .= Xml
::element(
689 array( 'class' => 'mw-changeslist-legend-plusminus' ),
690 $context->msg( 'recentchanges-label-plusminus' )->text()
692 $legend .= Xml
::closeElement( 'dl' ) . "\n";
696 '<div class="mw-changeslist-legend">' .
697 $context->msg( 'recentchanges-legend-heading' )->parse() .
698 '<div class="mw-collapsible-content">' . $legend . '</div>' .
705 * Send the text to be displayed above the options
707 * @param FormOptions $opts Unused
709 function setTopText( FormOptions
$opts ) {
712 $message = $this->msg( 'recentchangestext' )->inContentLanguage();
713 if ( !$message->isDisabled() ) {
714 $this->getOutput()->addWikiText(
715 Html
::rawElement( 'p',
716 array( 'lang' => $wgContLang->getCode(), 'dir' => $wgContLang->getDir() ),
717 "\n" . $message->plain() . "\n"
719 /* $lineStart */ false,
720 /* $interface */ false
726 * Send the text to be displayed after the options, for use in subclasses.
728 * @param FormOptions $opts
730 function setBottomText( FormOptions
$opts ) {
734 * Creates the choose namespace selection
736 * @param FormOptions $opts
739 protected function namespaceFilterForm( FormOptions
$opts ) {
740 $nsSelect = Html
::namespaceSelector(
741 array( 'selected' => $opts['namespace'], 'all' => '' ),
742 array( 'name' => 'namespace', 'id' => 'namespace' )
744 $nsLabel = Xml
::label( $this->msg( 'namespace' )->text(), 'namespace' );
745 $invert = Xml
::checkLabel(
746 $this->msg( 'invert' )->text(), 'invert', 'nsinvert',
748 array( 'title' => $this->msg( 'tooltip-invert' )->text() )
750 $associated = Xml
::checkLabel(
751 $this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated',
753 array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() )
756 return array( $nsLabel, "$nsSelect $invert $associated" );
760 * Create a input to filter changes by categories
762 * @param FormOptions $opts
765 protected function categoryFilterForm( FormOptions
$opts ) {
766 list( $label, $input ) = Xml
::inputLabelSep( $this->msg( 'rc_categories' )->text(),
767 'categories', 'mw-categories', false, $opts['categories'] );
769 $input .= ' ' . Xml
::checkLabel( $this->msg( 'rc_categories_any' )->text(),
770 'categories_any', 'mw-categories_any', $opts['categories_any'] );
772 return array( $label, $input );
776 * Filter $rows by categories set in $opts
778 * @param array $rows Database rows
779 * @param FormOptions $opts
781 function filterByCategories( &$rows, FormOptions
$opts ) {
782 $categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
784 if ( !count( $categories ) ) {
790 foreach ( $categories as $cat ) {
802 foreach ( $rows as $k => $r ) {
803 $nt = Title
::makeTitle( $r->rc_namespace
, $r->rc_title
);
804 $id = $nt->getArticleID();
806 continue; # Page might have been deleted...
808 if ( !in_array( $id, $articles ) ) {
811 if ( !isset( $a2r[$id] ) ) {
819 if ( !count( $articles ) ||
!count( $cats ) ) {
824 $c = new Categoryfinder
;
825 $c->seed( $articles, $cats, $opts['categories_any'] ?
'OR' : 'AND' );
830 foreach ( $match as $id ) {
831 foreach ( $a2r[$id] as $rev ) {
833 $newrows[$k] = $rowsarr[$k];
840 * Makes change an option link which carries all the other options
842 * @param string $title Title
843 * @param array $override Options to override
844 * @param array $options Current options
845 * @param bool $active Whether to show the link in bold
848 function makeOptionsLink( $title, $override, $options, $active = false ) {
849 $params = $override +
$options;
851 // Bug 36524: false values have be converted to "0" otherwise
852 // wfArrayToCgi() will omit it them.
853 foreach ( $params as &$value ) {
854 if ( $value === false ) {
860 $text = htmlspecialchars( $title );
862 $text = '<strong>' . $text . '</strong>';
865 return Linker
::linkKnown( $this->getPageTitle(), $text, array(), $params );
869 * Creates the options panel.
871 * @param array $defaults
872 * @param array $nondefaults
875 function optionsPanel( $defaults, $nondefaults ) {
876 global $wgRCLinkLimits, $wgRCLinkDays;
878 $options = $nondefaults +
$defaults;
881 $msg = $this->msg( 'rclegend' );
882 if ( !$msg->isDisabled() ) {
883 $note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n";
886 $lang = $this->getLanguage();
887 $user = $this->getUser();
888 if ( $options['from'] ) {
889 $note .= $this->msg( 'rcnotefrom' )->numParams( $options['limit'] )->params(
890 $lang->userTimeAndDate( $options['from'], $user ),
891 $lang->userDate( $options['from'], $user ),
892 $lang->userTime( $options['from'], $user ) )->parse() . '<br />';
895 # Sort data for display and make sure it's unique after we've added user data.
896 $linkLimits = $wgRCLinkLimits;
897 $linkLimits[] = $options['limit'];
899 $linkLimits = array_unique( $linkLimits );
901 $linkDays = $wgRCLinkDays;
902 $linkDays[] = $options['days'];
904 $linkDays = array_unique( $linkDays );
908 foreach ( $linkLimits as $value ) {
909 $cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
910 array( 'limit' => $value ), $nondefaults, $value == $options['limit'] );
912 $cl = $lang->pipeList( $cl );
914 // day links, reset 'from' to none
916 foreach ( $linkDays as $value ) {
917 $dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
918 array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] );
920 $dl = $lang->pipeList( $dl );
923 $showhide = array( $this->msg( 'show' )->text(), $this->msg( 'hide' )->text() );
925 'hideminor' => 'rcshowhideminor',
926 'hidebots' => 'rcshowhidebots',
927 'hideanons' => 'rcshowhideanons',
928 'hideliu' => 'rcshowhideliu',
929 'hidepatrolled' => 'rcshowhidepatr',
930 'hidemyself' => 'rcshowhidemine'
932 foreach ( $this->getCustomFilters() as $key => $params ) {
933 $filters[$key] = $params['msg'];
935 // Disable some if needed
936 if ( !$user->useRCPatrol() ) {
937 unset( $filters['hidepatrolled'] );
941 foreach ( $filters as $key => $msg ) {
942 $link = $this->makeOptionsLink( $showhide[1 - $options[$key]],
943 array( $key => 1 - $options[$key] ), $nondefaults );
944 $links[] = $this->msg( $msg )->rawParams( $link )->escaped();
947 // show from this onward link
948 $timestamp = wfTimestampNow();
949 $now = $lang->userTimeAndDate( $timestamp, $user );
950 $tl = $this->makeOptionsLink(
951 $now, array( 'from' => $timestamp ), $nondefaults
954 $rclinks = $this->msg( 'rclinks' )->rawParams( $cl, $dl, $lang->pipeList( $links ) )
956 $rclistfrom = $this->msg( 'rclistfrom' )->rawParams( $tl )->parse();
958 return "{$note}$rclinks<br />$rclistfrom";
962 * Add page-specific modules.
964 protected function addModules() {
965 $this->getOutput()->addModules( array(
966 'mediawiki.special.recentchanges',
970 protected function getGroupName() {