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,
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 ) {
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 );
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 :
'';
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
512 $unconsumed = $opts->getUnconsumedValues();
513 foreach ( $unconsumed as $key => $value ) {
514 $out .= Html::hidden( $key, $value );
518 $out .= Html::hidden(
'title',
$t->getPrefixedText() );
521 $panelString = implode(
"\n", $panel );
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();
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';
706 $this->
msg(
'invert' )->text(),
'invert',
'nsinvert',
708 [
'title' => $this->
msg(
'tooltip-invert' )->text() ]
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 ) {
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;