42 public function __construct( $name =
'Recentchanges', $restriction =
'' ) {
43 parent::__construct( $name, $restriction );
45 $this->watchlistFilterGroupDefinition = [
46 'name' =>
'watchlist',
47 'title' =>
'rcfilters-filtergroup-watchlist',
48 'class' => ChangesListStringOptionsFilterGroup::class,
50 'isFullCoverage' =>
true,
54 'label' =>
'rcfilters-filter-watchlist-watched-label',
55 'description' =>
'rcfilters-filter-watchlist-watched-description',
56 'cssClassSuffix' =>
'watched',
57 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
58 return $rc->getAttribute(
'wl_user' );
62 'name' =>
'watchednew',
63 'label' =>
'rcfilters-filter-watchlist-watchednew-label',
64 'description' =>
'rcfilters-filter-watchlist-watchednew-description',
65 'cssClassSuffix' =>
'watchednew',
66 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
67 return $rc->getAttribute(
'wl_user' ) &&
68 $rc->getAttribute(
'rc_timestamp' ) &&
69 $rc->getAttribute(
'wl_notificationtimestamp' ) &&
70 $rc->getAttribute(
'rc_timestamp' ) >= $rc->getAttribute(
'wl_notificationtimestamp' );
74 'name' =>
'notwatched',
75 'label' =>
'rcfilters-filter-watchlist-notwatched-label',
76 'description' =>
'rcfilters-filter-watchlist-notwatched-description',
77 'cssClassSuffix' =>
'notwatched',
78 'isRowApplicableCallable' =>
function ( $ctx, $rc ) {
79 return $rc->getAttribute(
'wl_user' ) ===
null;
84 'queryCallable' =>
function ( $specialPageClassName,
$context,
$dbr,
85 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) {
86 sort( $selectedValues );
87 $notwatchedCond =
'wl_user IS NULL';
88 $watchedCond =
'wl_user IS NOT NULL';
89 $newCond =
'rc_timestamp >= wl_notificationtimestamp';
91 if ( $selectedValues === [
'notwatched' ] ) {
92 $conds[] = $notwatchedCond;
96 if ( $selectedValues === [
'watched' ] ) {
97 $conds[] = $watchedCond;
101 if ( $selectedValues === [
'watchednew' ] ) {
102 $conds[] =
$dbr->makeList( [
109 if ( $selectedValues === [
'notwatched',
'watched' ] ) {
114 if ( $selectedValues === [
'notwatched',
'watchednew' ] ) {
115 $conds[] =
$dbr->makeList( [
125 if ( $selectedValues === [
'watched',
'watchednew' ] ) {
126 $conds[] = $watchedCond;
130 if ( $selectedValues === [
'notwatched',
'watched',
'watchednew' ] ) {
143 $feedFormat = $this->
getRequest()->getVal(
'feed' );
144 if ( !$this->
including() && $feedFormat ) {
146 $query[
'feedformat'] = $feedFormat ===
'atom' ?
'atom' :
'rss';
154 $out->setCdnMaxage( 10 );
157 if ( $lastmod ===
false ) {
162 'https://meta.wikimedia.org/wiki/Special:MyLanguage/Help:Recent_changes',
165 parent::execute( $subpage );
172 if ( isset( $filterDefinition[
'showHideSuffix'] ) ) {
173 $filterDefinition[
'showHide'] =
'rc' . $filterDefinition[
'showHideSuffix'];
176 return $filterDefinition;
183 parent::registerFilters();
187 $this->
getUser()->isLoggedIn() &&
188 MediaWikiServices::getInstance()
190 ->userHasRight( $this->
getUser(),
'viewmywatchlist' )
194 $watchlistGroup->getFilter(
'watched' )->setAsSupersetOf(
195 $watchlistGroup->getFilter(
'watchednew' )
203 $hideMinor = $significance->getFilter(
'hideminor' );
204 '@phan-var ChangesListBooleanFilter $hideMinor';
205 $hideMinor->setDefault( $user->getBoolOption(
'hideminor' ) );
209 $hideBots = $automated->getFilter(
'hidebots' );
210 '@phan-var ChangesListBooleanFilter $hideBots';
211 $hideBots->setDefault(
true );
215 '@phan-var ChangesListStringOptionsFilterGroup|null $reviewStatus';
216 if ( $reviewStatus !==
null ) {
218 if ( $user->getBoolOption(
'hidepatrolled' ) ) {
219 $reviewStatus->setDefault(
'unpatrolled' );
220 $legacyReviewStatus = $this->
getFilterGroup(
'legacyReviewStatus' );
222 $legacyHidePatrolled = $legacyReviewStatus->getFilter(
'hidepatrolled' );
223 '@phan-var ChangesListBooleanFilter $legacyHidePatrolled';
224 $legacyHidePatrolled->setDefault(
true );
230 $hideCategorization = $changeType->getFilter(
'hidecategorization' );
231 '@phan-var ChangesListBooleanFilter $hideCategorization';
232 if ( $hideCategorization !==
null ) {
234 $hideCategorization->setDefault( $user->getBoolOption(
'hidecategorization' ) );
245 parent::parseParameters( $par, $opts );
247 $bits = preg_split(
'/\s*,\s*/', trim( $par ) );
248 foreach ( $bits as $bit ) {
249 if ( is_numeric( $bit ) ) {
250 $opts[
'limit'] = $bit;
254 if ( preg_match(
'/^limit=(\d+)$/', $bit, $m ) ) {
255 $opts[
'limit'] = $m[1];
257 if ( preg_match(
'/^days=(\d+(?:\.\d+)?)$/', $bit, $m ) ) {
258 $opts[
'days'] = $m[1];
260 if ( preg_match(
'/^namespace=(.*)$/', $bit, $m ) ) {
261 $opts[
'namespace'] = $m[1];
263 if ( preg_match(
'/^tagfilter=(.*)$/', $bit, $m ) ) {
264 $opts[
'tagfilter'] = $m[1];
272 protected function doMainQuery( $tables, $fields, $conds, $query_options,
278 $rcQuery = RecentChange::getQueryInfo();
279 $tables = array_merge( $tables, $rcQuery[
'tables'] );
280 $fields = array_merge( $rcQuery[
'fields'], $fields );
281 $join_conds = array_merge( $join_conds, $rcQuery[
'joins'] );
284 if ( $user->isLoggedIn() && MediaWikiServices::getInstance()
285 ->getPermissionManager()
286 ->userHasRight( $user,
'viewmywatchlist' )
288 $tables[] =
'watchlist';
289 $fields[] =
'wl_user';
290 $fields[] =
'wl_notificationtimestamp';
291 $join_conds[
'watchlist'] = [
'LEFT JOIN', [
292 'wl_user' => $user->getId(),
294 'wl_namespace=rc_namespace'
300 $fields[] =
'page_latest';
301 $join_conds[
'page'] = [
'LEFT JOIN',
'rc_cur_id=page_id' ];
303 $tagFilter = $opts[
'tagfilter'] ? explode(
'|', $opts[
'tagfilter'] ) : [];
313 if ( !$this->
runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
324 'ORDER BY' =>
'rc_timestamp DESC',
325 'LIMIT' => $opts[
'limit']
327 if ( in_array(
'DISTINCT', $query_options ) ) {
333 $orderByAndLimit[
'ORDER BY'] =
'rc_timestamp DESC, rc_id DESC';
334 $orderByAndLimit[
'GROUP BY'] =
'rc_timestamp, rc_id';
340 $query_options = array_merge( $orderByAndLimit, $query_options );
341 $rows =
$dbr->select(
346 $conds + [
'rc_new' => [ 0, 1 ] ],
369 $query = array_filter( $this->
getOptions()->getAllValues(),
function ( $value ) {
371 return $value !==
'';
373 $query[
'action'] =
'feedrecentchanges';
374 $feedLimit = $this->
getConfig()->get(
'FeedLimit' );
375 if ( $query[
'limit'] > $feedLimit ) {
376 $query[
'limit'] = $feedLimit;
389 $limit = $opts[
'limit'];
391 $showWatcherCount = $this->
getConfig()->get(
'RCShowWatchingUsers' )
392 && $this->
getUser()->getOption(
'shownumberswatching' );
397 $list->initChangesListRows( $rows );
399 $userShowHiddenCats = $this->
getUser()->getBoolOption(
'showhiddencats' );
400 $rclistOutput = $list->beginRecentChangesList();
405 foreach ( $rows as $obj ) {
409 $rc = RecentChange::newFromRow( $obj );
411 # Skip CatWatch entries for hidden cats based on user preference
414 !$userShowHiddenCats &&
415 $rc->getParam(
'hidden-cat' )
420 $rc->counter = $counter++;
421 # Check if the page has been updated since the last visit
422 if ( $this->
getConfig()->
get(
'ShowUpdatedMarker' )
423 && !empty( $obj->wl_notificationtimestamp )
425 $rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp );
427 $rc->notificationtimestamp =
false;
429 # Check the number of users watching the page
430 $rc->numberofWatchingusers = 0;
431 if ( $showWatcherCount && $obj->rc_namespace >= 0 ) {
432 if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
433 $watcherCache[$obj->rc_namespace][$obj->rc_title] =
434 MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchers(
435 new TitleValue( (
int)$obj->rc_namespace, $obj->rc_title )
438 $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
441 $changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
442 if ( $changeLine !==
false ) {
443 $rclistOutput .= $changeLine;
447 $rclistOutput .= $list->endRecentChangesList();
449 if ( $rows->numRows() === 0 ) {
452 $this->
getOutput()->setStatusCode( 404 );
455 $this->
getOutput()->addHTML( $rclistOutput );
468 $defaults = $opts->getAllValues();
469 $nondefaults = $opts->getChangedValues();
475 $panel[] = $this->
optionsPanel( $defaults, $nondefaults, $numRows );
479 $extraOptsCount = count( $extraOpts );
481 $submit =
' ' . Xml::submitButton( $this->
msg(
'recentchanges-submit' )->text() );
483 $out = Xml::openElement(
'table', [
'class' =>
'mw-recentchanges-table' ] );
484 foreach ( $extraOpts as $name => $optionRow ) {
485 # Add submit button to the last row only
487 $addSubmit = ( $count === $extraOptsCount ) ? $submit :
'';
489 $out .= Xml::openElement(
'tr', [
'class' => $name .
'Form' ] );
490 if ( is_array( $optionRow ) ) {
493 [
'class' =>
'mw-label mw-' . $name .
'-label' ],
498 [
'class' =>
'mw-input' ],
499 $optionRow[1] . $addSubmit
504 [
'class' =>
'mw-input',
'colspan' => 2 ],
505 $optionRow . $addSubmit
508 $out .= Xml::closeElement(
'tr' );
510 $out .= Xml::closeElement(
'table' );
512 $unconsumed = $opts->getUnconsumedValues();
513 foreach ( $unconsumed as $key => $value ) {
514 $out .= Html::hidden( $key, $value );
518 $out .= Html::hidden(
'title',
$t->getPrefixedText() );
519 $form = Xml::tags(
'form', [
'action' =>
wfScript() ], $out );
521 $panelString = implode(
"\n", $panel );
523 $rcoptions = Xml::fieldset(
524 $this->
msg(
'recentchanges-legend' )->text(),
526 [
'class' =>
'rcoptions cloptions' ]
531 $rcfilterContainer = Html::element(
534 [
'class' =>
'rcfilters-container mw-rcfilters-container' ]
537 $loadingContainer = Html::rawElement(
539 [
'class' =>
'mw-rcfilters-spinner' ],
542 [
'class' =>
'mw-rcfilters-spinner-bounce' ]
551 [
'class' =>
'rcfilters-head mw-rcfilters-head' ],
552 $rcfilterContainer . $rcoptions
557 $this->
getOutput()->addHTML( $loadingContainer );
559 $this->
getOutput()->addHTML( $rcoptions );
571 $message = $this->
msg(
'recentchangestext' )->inContentLanguage();
572 if ( !$message->isDisabled() ) {
573 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
577 $parserOutput = MessageCache::singleton()->parse(
579 $this->getPageTitle(),
586 $content = $parserOutput->getText( [
587 'enableSectionEditLinks' =>
false,
590 $this->
getOutput()->addParserOutputMetadata( $parserOutput );
593 'lang' => $contLang->getHtmlCode(),
594 'dir' => $contLang->getDir(),
597 $topLinksAttributes = [
'class' =>
'mw-recentchanges-toplinks' ];
601 $collapsedState = $this->
getRequest()->getCookie(
'rcfilters-toplinks-collapsed-state' );
603 $topLinksAttributes[
'class' ] .= $collapsedState !==
'expanded' ?
604 ' mw-recentchanges-toplinks-collapsed' :
'';
607 $contentTitle =
new OOUI\ButtonWidget( [
608 'classes' => [
'mw-recentchanges-toplinks-title' ],
609 'label' =>
new OOUI\HtmlSnippet( $this->
msg(
'rcfilters-other-review-tools' )->parse() ),
611 'indicator' => $collapsedState !==
'expanded' ?
'down' :
'up',
612 'flags' => [
'progressive' ],
615 $contentWrapper = Html::rawElement(
'div',
617 [
'class' =>
'mw-recentchanges-toplinks-content mw-collapsible-content' ],
622 $content = $contentTitle . $contentWrapper;
628 $topLinksAttributes = array_merge( $topLinksAttributes, $langAttributes );
632 Html::rawElement(
'div', $topLinksAttributes,
$content )
644 $opts->consumeValues( [
645 'namespace',
'invert',
'associated',
'tagfilter'
652 $opts[
'tagfilter'],
false, $this->
getContext() );
653 if ( count( $tagFilter ) ) {
654 $extraOpts[
'tagfilter'] = $tagFilter;
658 if ( $this->
getName() ===
'Recentchanges' ) {
659 Hooks::run(
'SpecialRecentChangesPanel', [ &$extraOpts, $opts ] );
669 parent::addModules();
671 $out->addModules(
'mediawiki.special.recentchanges' );
683 $lastmod =
$dbr->selectField(
'recentchanges',
'MAX(rc_timestamp)',
'', __METHOD__ );
695 $nsSelect = Html::namespaceSelector(
696 [
'selected' => $opts[
'namespace'],
'all' =>
'',
'in-user-lang' =>
true ],
697 [
'name' =>
'namespace',
'id' =>
'namespace' ]
699 $nsLabel = Xml::label( $this->
msg(
'namespace' )->text(),
'namespace' );
700 $attribs = [
'class' => [
'mw-input-with-label' ] ];
702 if ( $opts[
'namespace'] ===
'' ) {
703 $attribs[
'class'][] =
'mw-input-hidden';
705 $invert = Html::rawElement(
'span', $attribs, Xml::checkLabel(
706 $this->
msg(
'invert' )->text(),
'invert',
'nsinvert',
708 [
'title' => $this->
msg(
'tooltip-invert' )->text() ]
710 $associated = Html::rawElement(
'span', $attribs, Xml::checkLabel(
711 $this->
msg(
'namespace_association' )->text(),
'associated',
'nsassociated',
713 [
'title' => $this->
msg(
'tooltip-namespace_association' )->text() ]
716 return [ $nsLabel,
"$nsSelect $invert $associated" ];
730 $categories = array_map(
'trim', explode(
'|', $opts[
'categories'] ) );
732 if ( $categories === [] ) {
738 foreach ( $categories as $cat ) {
750 foreach ( $rows as $k => $r ) {
751 $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
752 $id = $nt->getArticleID();
754 continue; #
Page might have been deleted...
756 if ( !in_array( $id, $articles ) ) {
759 if ( !isset( $a2r[$id] ) ) {
767 if ( $articles === [] || $cats === [] ) {
773 $catFind->
seed( $articles, $cats, $opts[
'categories_any'] ?
'OR' :
'AND' );
774 $match = $catFind->run();
778 foreach ( $match as $id ) {
779 foreach ( $a2r[$id] as $rev ) {
781 $newrows[$k] = $rowsarr[$k];
804 'data-params' => json_encode( $override ),
805 'data-keys' => implode(
',', array_keys( $override ) ),
818 $options = $nondefaults + $defaults;
821 $msg = $this->
msg(
'rclegend' );
822 if ( !$msg->isDisabled() ) {
823 $note .= Html::rawElement(
825 [
'class' =>
'mw-rclegend' ],
833 if ( $options[
'from'] ) {
835 [
'from' =>
'' ], $nondefaults );
837 $noteFromMsg = $this->
msg(
'rcnotefrom' )
838 ->numParams( $options[
'limit'] )
840 $lang->userTimeAndDate( $options[
'from'], $user ),
841 $lang->userDate( $options[
'from'], $user ),
842 $lang->userTime( $options[
'from'], $user )
844 ->numParams( $numRows );
845 $note .= Html::rawElement(
847 [
'class' =>
'rcnotefrom' ],
848 $noteFromMsg->parse()
853 [
'class' =>
'rcoptions-listfromreset' ],
854 $this->
msg(
'parentheses' )->rawParams( $resetLink )->parse()
859 # Sort data for display and make sure it's unique after we've added user data.
860 $linkLimits = $config->get(
'RCLinkLimits' );
861 $linkLimits[] = $options[
'limit'];
863 $linkLimits = array_unique( $linkLimits );
866 $linkDays[] = $options[
'days'];
868 $linkDays = array_unique( $linkDays );
872 foreach ( $linkLimits as $value ) {
874 [
'limit' => $value ], $nondefaults, $value == $options[
'limit'] );
876 $cl =
$lang->pipeList( $cl );
880 foreach ( $linkDays as $value ) {
882 [
'days' => $value,
'from' =>
'' ], $nondefaults, $value == $options[
'days'] );
884 $dl =
$lang->pipeList( $dl );
886 $showhide = [
'show',
'hide' ];
892 $linkMessage = $this->
msg( $msg .
'-' . $showhide[1 - $options[$key]] );
895 if ( !$linkMessage->exists() ) {
896 $linkMessage = $this->
msg( $showhide[1 - $options[$key]] );
900 [ $key => 1 - $options[$key] ], $nondefaults );
903 'class' =>
"$msg rcshowhideoption clshowhideoption",
904 'data-filter-name' =>
$filter->getName(),
907 if (
$filter->isFeatureAvailableOnStructuredUi( $this ) ) {
908 $attribs[
'data-feature-in-structured-ui'] =
true;
911 $links[] = Html::rawElement(
914 $this->
msg( $msg )->rawParams( $link )->parse()
920 $now =
$lang->userTimeAndDate( $timestamp, $user );
921 $timenow =
$lang->userTime( $timestamp, $user );
922 $datenow =
$lang->userDate( $timestamp, $user );
923 $pipedLinks =
'<span class="rcshowhide">' .
$lang->pipeList( $links ) .
'</span>';
925 $rclinks = Html::rawElement(
927 [
'class' =>
'rclinks' ],
928 $this->
msg(
'rclinks' )->rawParams( $cl, $dl,
'' )->parse()
931 $rclistfrom = Html::rawElement(
933 [
'class' =>
'rclistfrom' ],
935 $this->
msg(
'rclistfrom' )->plaintextParams( $now, $timenow, $datenow )->parse(),
936 [
'from' => $timestamp,
'fromFormatted' => $now ],
941 return "{$note}$rclinks<br />$pipedLinks<br />$rclistfrom";
953 $systemPrefValue = $this->
getUser()->getIntOption(
'rclimit' );
956 return $this->
getUser()->getIntOption( static::$limitPreferenceName, $systemPrefValue );
960 return $systemPrefValue;
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
The "CategoryFinder" class takes a list of articles, creates an internal representation of all their ...
seed( $articleIds, $categories, $mode='AND', $maxdepth=-1)
Initializes the instance.
Special page which uses a ChangesList to show query results.
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
registerFiltersFromDefinitions(array $definition)
Register filters from a definition object.
convertParamsForLink( $params)
Convert parameters values from true/false to 1/0 so they are not omitted by wfArrayToCgi() T38524.
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
outputNoResults()
Add the "no results" message to the output.
getLegacyShowHideFilters()
getOptions()
Get the current FormOptions for this request.
setBottomText(FormOptions $opts)
Send the text to be displayed after the options.
makeLegend()
Return the legend displayed within the fieldset.
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
static newFromContext(IContextSource $context, array $groups=[])
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
Marks HTML that shouldn't be escaped.
getName()
Get the name of this Special Page.
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
addFeedLinks( $params)
Adds RSS/atom links.
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getLanguage()
Shortcut to get user's language.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
including( $x=null)
Whether the special page is being evaluated via transclusion.
A special page that lists last changes made to the wiki.
filterByCategories(&$rows, FormOptions $opts)
Filter $rows by categories set in $opts.
optionsPanel( $defaults, $nondefaults, $numRows)
Creates the options panel.
isIncludable()
Whether it's allowed to transclude the special page via {{Special:Foo/params}}.
setTopText(FormOptions $opts)
Send the text to be displayed above the options.
getExtraOptions( $opts)
Get options to be displayed in a form.
makeOptionsLink( $title, $override, $options, $active=false)
Makes change an option link which carries all the other options.
getDB()
Return a IDatabase object for reading.
static $savedQueriesPreferenceName
addModules()
Add page-specific modules.
getFeedQuery()
Get URL query parameters for action=feedrecentchanges API feed of current recent changes view.
static $daysPreferenceName
checkLastModified()
Get last modified date, for client caching Don't use this if we are using the patrol feature,...
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
static $limitPreferenceName
transformFilterDefinition(array $filterDefinition)
Transforms filter definition to prepare it for constructor.See overrides of this method as well....
parseParameters( $par, FormOptions $opts)
Process $par and put options found in $opts.
getDefaultLimit()
Get the default value of the number of changes to display when loading the result set.
__construct( $name='Recentchanges', $restriction='')
namespaceFilterForm(FormOptions $opts)
Creates the choose namespace selection.
outputChangesList( $rows, $opts)
Build and output the actual changes list.
outputFeedLinks()
Output feed links.
registerFilters()
Register all filters and their groups (including those from hooks), plus handle conflicts and default...
static $collapsedPreferenceName
$watchlistFilterGroupDefinition
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
Process the query.bool|IResultWrapper Result or false
Represents a page (or page fragment) title within MediaWiki.
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
if(!isset( $args[0])) $lang