105 protected $filterGroups = [];
108 parent::__construct( $name, $restriction );
110 $nonRevisionTypes = [
RC_LOG ];
111 $this->
getHookRunner()->onSpecialWatchlistGetNonRevisionTypes( $nonRevisionTypes );
113 $this->filterGroupDefinitions = [
115 'name' =>
'registration',
116 'title' =>
'rcfilters-filtergroup-registration',
117 'class' => ChangesListBooleanFilterGroup::class,
123 'showHideSuffix' =>
'showhideliu',
125 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
126 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
128 $conds[
'actor_user'] =
null;
130 'isReplacedInStructuredUi' =>
true,
134 'name' =>
'hideanons',
137 'showHideSuffix' =>
'showhideanons',
139 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
140 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
142 $conds[] =
'actor_user IS NOT NULL';
144 'isReplacedInStructuredUi' =>
true,
150 'name' =>
'userExpLevel',
151 'title' =>
'rcfilters-filtergroup-user-experience-level',
152 'class' => ChangesListStringOptionsFilterGroup::class,
153 'isFullCoverage' =>
true,
156 'name' =>
'unregistered',
157 'label' =>
'rcfilters-filter-user-experience-level-unregistered-label',
158 'description' =>
'rcfilters-filter-user-experience-level-unregistered-description',
159 'cssClassSuffix' =>
'user-unregistered',
161 return !$rc->getAttribute(
'rc_user' );
165 'name' =>
'registered',
166 'label' =>
'rcfilters-filter-user-experience-level-registered-label',
167 'description' =>
'rcfilters-filter-user-experience-level-registered-description',
168 'cssClassSuffix' =>
'user-registered',
170 return $rc->getAttribute(
'rc_user' );
174 'name' =>
'newcomer',
175 'label' =>
'rcfilters-filter-user-experience-level-newcomer-label',
176 'description' =>
'rcfilters-filter-user-experience-level-newcomer-description',
177 'cssClassSuffix' =>
'user-newcomer',
180 return $performer && $performer->isRegistered() &&
181 $performer->getExperienceLevel() ===
'newcomer';
186 'label' =>
'rcfilters-filter-user-experience-level-learner-label',
187 'description' =>
'rcfilters-filter-user-experience-level-learner-description',
188 'cssClassSuffix' =>
'user-learner',
191 return $performer && $performer->isRegistered() &&
192 $performer->getExperienceLevel() ===
'learner';
196 'name' =>
'experienced',
197 'label' =>
'rcfilters-filter-user-experience-level-experienced-label',
198 'description' =>
'rcfilters-filter-user-experience-level-experienced-description',
199 'cssClassSuffix' =>
'user-experienced',
202 return $performer && $performer->isRegistered() &&
203 $performer->getExperienceLevel() ===
'experienced';
208 'queryCallable' => [ $this,
'filterOnUserExperienceLevel' ],
212 'name' =>
'authorship',
213 'title' =>
'rcfilters-filtergroup-authorship',
214 'class' => ChangesListBooleanFilterGroup::class,
217 'name' =>
'hidemyself',
218 'label' =>
'rcfilters-filter-editsbyself-label',
219 'description' =>
'rcfilters-filter-editsbyself-description',
222 'showHideSuffix' =>
'showhidemine',
224 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
225 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
227 $user = $ctx->getUser();
228 $conds[] =
'actor_name<>' .
$dbr->addQuotes( $user->getName() );
230 'cssClassSuffix' =>
'self',
232 return $ctx->getUser()->equals( $rc->getPerformerIdentity() );
236 'name' =>
'hidebyothers',
237 'label' =>
'rcfilters-filter-editsbyother-label',
238 'description' =>
'rcfilters-filter-editsbyother-description',
240 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
241 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
243 $user = $ctx->getUser();
244 if ( $user->isAnon() ) {
245 $conds[
'actor_name'] = $user->getName();
247 $conds[
'actor_user'] = $user->getId();
250 'cssClassSuffix' =>
'others',
252 return !$ctx->getUser()->equals( $rc->getPerformerIdentity() );
259 'name' =>
'automated',
260 'title' =>
'rcfilters-filtergroup-automated',
261 'class' => ChangesListBooleanFilterGroup::class,
264 'name' =>
'hidebots',
265 'label' =>
'rcfilters-filter-bots-label',
266 'description' =>
'rcfilters-filter-bots-description',
269 'showHideSuffix' =>
'showhidebots',
271 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
272 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
274 $conds[
'rc_bot'] = 0;
276 'cssClassSuffix' =>
'bot',
278 return $rc->getAttribute(
'rc_bot' );
282 'name' =>
'hidehumans',
283 'label' =>
'rcfilters-filter-humans-label',
284 'description' =>
'rcfilters-filter-humans-description',
286 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
287 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
289 $conds[
'rc_bot'] = 1;
291 'cssClassSuffix' =>
'human',
293 return !$rc->getAttribute(
'rc_bot' );
302 'name' =>
'significance',
303 'title' =>
'rcfilters-filtergroup-significance',
304 'class' => ChangesListBooleanFilterGroup::class,
308 'name' =>
'hideminor',
309 'label' =>
'rcfilters-filter-minor-label',
310 'description' =>
'rcfilters-filter-minor-description',
313 'showHideSuffix' =>
'showhideminor',
315 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
316 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
318 $conds[] =
'rc_minor = 0';
320 'cssClassSuffix' =>
'minor',
322 return $rc->getAttribute(
'rc_minor' );
326 'name' =>
'hidemajor',
327 'label' =>
'rcfilters-filter-major-label',
328 'description' =>
'rcfilters-filter-major-description',
330 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
331 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
333 $conds[] =
'rc_minor = 1';
335 'cssClassSuffix' =>
'major',
337 return !$rc->getAttribute(
'rc_minor' );
344 'name' =>
'lastRevision',
345 'title' =>
'rcfilters-filtergroup-lastrevision',
346 'class' => ChangesListBooleanFilterGroup::class,
350 'name' =>
'hidelastrevision',
351 'label' =>
'rcfilters-filter-lastrevision-label',
352 'description' =>
'rcfilters-filter-lastrevision-description',
354 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
355 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
356 ) use ( $nonRevisionTypes ) {
357 $conds[] =
$dbr->makeList(
359 'rc_this_oldid <> page_latest',
360 'rc_type' => $nonRevisionTypes,
365 'cssClassSuffix' =>
'last',
367 return $rc->getAttribute(
'rc_this_oldid' ) === $rc->getAttribute(
'page_latest' );
371 'name' =>
'hidepreviousrevisions',
372 'label' =>
'rcfilters-filter-previousrevision-label',
373 'description' =>
'rcfilters-filter-previousrevision-description',
375 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
376 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
377 ) use ( $nonRevisionTypes ) {
378 $conds[] =
$dbr->makeList(
380 'rc_this_oldid = page_latest',
381 'rc_type' => $nonRevisionTypes,
386 'cssClassSuffix' =>
'previous',
388 return $rc->getAttribute(
'rc_this_oldid' ) !== $rc->getAttribute(
'page_latest' );
396 'name' =>
'changeType',
397 'title' =>
'rcfilters-filtergroup-changetype',
398 'class' => ChangesListBooleanFilterGroup::class,
402 'name' =>
'hidepageedits',
403 'label' =>
'rcfilters-filter-pageedits-label',
404 'description' =>
'rcfilters-filter-pageedits-description',
407 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
408 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
410 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_EDIT );
412 'cssClassSuffix' =>
'src-mw-edit',
414 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_EDIT;
418 'name' =>
'hidenewpages',
419 'label' =>
'rcfilters-filter-newpages-label',
420 'description' =>
'rcfilters-filter-newpages-description',
423 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
424 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
426 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_NEW );
428 'cssClassSuffix' =>
'src-mw-new',
430 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_NEW;
438 'label' =>
'rcfilters-filter-logactions-label',
439 'description' =>
'rcfilters-filter-logactions-description',
442 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
443 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
445 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_LOG );
447 'cssClassSuffix' =>
'src-mw-log',
449 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_LOG;
457 $this->legacyReviewStatusFilterGroupDefinition = [
459 'name' =>
'legacyReviewStatus',
460 'title' =>
'rcfilters-filtergroup-reviewstatus',
461 'class' => ChangesListBooleanFilterGroup::class,
464 'name' =>
'hidepatrolled',
467 'showHideSuffix' =>
'showhidepatr',
469 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
470 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
472 $conds[
'rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
474 'isReplacedInStructuredUi' =>
true,
477 'name' =>
'hideunpatrolled',
479 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
480 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
482 $conds[] =
'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
484 'isReplacedInStructuredUi' =>
true,
490 $this->reviewStatusFilterGroupDefinition = [
492 'name' =>
'reviewStatus',
493 'title' =>
'rcfilters-filtergroup-reviewstatus',
494 'class' => ChangesListStringOptionsFilterGroup::class,
495 'isFullCoverage' =>
true,
499 'name' =>
'unpatrolled',
500 'label' =>
'rcfilters-filter-reviewstatus-unpatrolled-label',
501 'description' =>
'rcfilters-filter-reviewstatus-unpatrolled-description',
502 'cssClassSuffix' =>
'reviewstatus-unpatrolled',
504 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED;
509 'label' =>
'rcfilters-filter-reviewstatus-manual-label',
510 'description' =>
'rcfilters-filter-reviewstatus-manual-description',
511 'cssClassSuffix' =>
'reviewstatus-manual',
513 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_PATROLLED;
518 'label' =>
'rcfilters-filter-reviewstatus-auto-label',
519 'description' =>
'rcfilters-filter-reviewstatus-auto-description',
520 'cssClassSuffix' =>
'reviewstatus-auto',
522 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED;
527 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
528 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
530 if ( $selected === [] ) {
533 $rcPatrolledValues = [
534 'unpatrolled' => RecentChange::PRC_UNPATROLLED,
535 'manual' => RecentChange::PRC_PATROLLED,
536 'auto' => RecentChange::PRC_AUTOPATROLLED,
539 $conds[
'rc_patrolled'] = array_map(
static function (
$s ) use ( $rcPatrolledValues ) {
540 return $rcPatrolledValues[
$s ];
546 $this->hideCategorizationFilterDefinition = [
547 'name' =>
'hidecategorization',
548 'label' =>
'rcfilters-filter-categorization-label',
549 'description' =>
'rcfilters-filter-categorization-description',
552 'showHideSuffix' =>
'showhidecategorization',
555 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
556 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
560 'cssClassSuffix' =>
'src-mw-categorize',
562 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_CATEGORIZE;
576 if ( $group->getConflictingGroups() ) {
579 " specifies conflicts with other groups but these are not supported yet."
584 foreach ( $group->getConflictingFilters() as $conflictingFilter ) {
585 if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
591 foreach ( $group->getFilters() as $filter ) {
593 foreach ( $filter->getConflictingFilters() as $conflictingFilter ) {
595 $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
596 $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
613 $this->rcSubpage = $subpage;
618 if ( $this->
getConfig()->
get(
'WatchlistExpiry' ) ) {
620 $this->
getOutput()->addModules(
'mediawiki.special.changeslist.watchlistexpiry' );
626 if ( $rows ===
false ) {
631 if ( $this->
getRequest()->getRawVal(
'action' ) ===
'render' ) {
632 $this->
getOutput()->setArticleBodyOnly(
true );
637 if ( $this->
getRequest()->getBool(
'peek' ) ) {
638 $code = $rows->numRows() > 0 ? 200 : 204;
639 $this->
getOutput()->setStatusCode( $code );
641 if ( $this->
getUser()->isAnon() !==
644 $this->
getOutput()->setStatusCode( 205 );
650 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
651 $batch = $linkBatchFactory->newLinkBatch();
652 foreach ( $rows as $row ) {
653 $batch->add(
NS_USER, $row->rc_user_text );
655 $batch->add( $row->rc_namespace, $row->rc_title );
656 if ( $row->rc_source === RecentChange::SRC_LOG ) {
658 foreach ( $formatter->getPreloadTitles() as
$title ) {
672 MWExceptionHandler::logException( $timeoutException );
678 $this->
getOutput()->setStatusCode( 500 );
683 if ( $this->
getConfig()->
get(
'EnableWANCacheReaper' ) ) {
687 LoggerFactory::getInstance(
'objectcache' )
706 $knownParams = $this->
getRequest()->getValues(
707 ...array_keys( $this->
getOptions()->getAllValues() )
713 $excludedParams = [
'limit' =>
'',
'days' =>
'',
'enhanced' =>
'',
'from' =>
'' ];
714 $knownParams = array_diff_key( $knownParams, $excludedParams );
720 count( $knownParams ) === 0
722 $prefJson = $this->
getUser()->getOption( static::$savedQueriesPreferenceName );
725 $savedQueries = $prefJson ? FormatJson::decode( $prefJson,
true ) :
false;
727 if ( $savedQueries && isset( $savedQueries[
'default' ] ) ) {
730 if ( isset( $savedQueries[
'version' ] ) && $savedQueries[
'version' ] ===
'2' ) {
731 $savedQueryDefaultID = $savedQueries[
'default' ];
732 $defaultQuery = $savedQueries[
'queries' ][ $savedQueryDefaultID ][
'data' ];
735 $query = array_merge(
736 $defaultQuery[
'params' ],
737 $defaultQuery[
'highlights' ],
744 $query = array_merge( $this->
getRequest()->getValues(), $query );
745 unset( $query[
'title' ] );
753 'wgStructuredChangeFiltersDefaultSavedQueryExists',
759 $this->
getOutput()->addBodyClasses(
'mw-rcfilters-ui-loading' );
771 $linkDays = $this->
getConfig()->get(
'RCLinkDays' );
772 $filterByAge = $this->
getConfig()->get(
'RCFilterByAge' );
773 $maxAge = $this->
getConfig()->get(
'RCMaxAge' );
774 if ( $filterByAge ) {
780 $maxAgeDays = $maxAge / ( 3600 * 24 );
781 foreach ( $linkDays as $i => $days ) {
782 if ( $days >= $maxAgeDays ) {
783 array_splice( $linkDays, $i + 1 );
803 foreach ( $jsData[
'messageKeys'] as $key ) {
804 $messages[$key] = $this->
msg( $key )->plain();
807 $out->addBodyClasses(
'mw-rcfilters-enabled' );
808 $collapsed = MediaWikiServices::getInstance()->getUserOptionsLookup()
809 ->getBoolOption( $this->
getUser(), static::$collapsedPreferenceName );
811 $out->addBodyClasses(
'mw-rcfilters-collapsed' );
815 $out->addJsConfigVars(
'wgStructuredChangeFilters', $jsData[
'groups'] );
816 $out->addJsConfigVars(
'wgStructuredChangeFiltersMessages', $messages );
817 $out->addJsConfigVars(
'wgStructuredChangeFiltersCollapsedState', $collapsed );
819 $out->addJsConfigVars(
820 'StructuredChangeFiltersDisplayConfig',
822 'maxDays' => (
int)$this->
getConfig()->
get(
'RCMaxAge' ) / ( 24 * 3600 ),
823 'limitArray' => $this->
getConfig()->
get(
'RCLinkLimits' ),
830 $out->addJsConfigVars(
831 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
832 static::$savedQueriesPreferenceName
834 $out->addJsConfigVars(
835 'wgStructuredChangeFiltersLimitPreferenceName',
838 $out->addJsConfigVars(
839 'wgStructuredChangeFiltersDaysPreferenceName',
840 static::$daysPreferenceName
842 $out->addJsConfigVars(
843 'wgStructuredChangeFiltersCollapsedPreferenceName',
844 static::$collapsedPreferenceName
847 $out->addBodyClasses(
'mw-rcfilters-disabled' );
862 'RCFiltersChangeTags' => self::getChangeTagListSummary( $context ),
863 'StructuredChangeFiltersEditWatchlistUrl' =>
877 'RCFiltersChangeTags' => self::getChangeTagList( $context ),
878 'StructuredChangeFiltersEditWatchlistUrl' =>
904 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
905 return $cache->getWithSetCallback(
906 $cache->makeKey(
'ChangesListSpecialPage-changeTagListSummary', $context->
getLanguage() ),
907 WANObjectCache::TTL_DAY,
908 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $context ) {
913 $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
916 foreach ( $tagHitCounts as $tagName => $hits ) {
920 isset( $explicitlyDefinedTags[ $tagName ] ) ||
921 isset( $softwareActivatedTags[ $tagName ] )
930 'labelMsg' => $labelMsg,
931 'label' => $labelMsg ? $labelMsg->plain() : $tagName,
932 'descriptionMsg' => $descriptionMsg,
933 'description' => $descriptionMsg ? $descriptionMsg->plain() :
'',
934 'cssClass' => Sanitizer::escapeClass(
'mw-tag-' . $tagName ),
958 $tags = self::getChangeTagListSummary( $context );
959 $language = MediaWikiServices::getInstance()->getLanguageFactory()
961 foreach ( $tags as &$tagInfo ) {
962 if ( $tagInfo[
'labelMsg'] ) {
963 $tagInfo[
'label'] = Sanitizer::stripAllTags( $tagInfo[
'labelMsg']->parse() );
965 $tagInfo[
'label'] = $context->
msg(
'rcfilters-tag-hidden', $tagInfo[
'name'] )->text();
967 $tagInfo[
'description'] = $tagInfo[
'descriptionMsg'] ?
968 $language->truncateForVisual(
969 Sanitizer::stripAllTags( $tagInfo[
'descriptionMsg']->parse() ),
970 self::TAG_DESC_CHARACTER_LIMIT
973 unset( $tagInfo[
'labelMsg'] );
974 unset( $tagInfo[
'descriptionMsg'] );
978 usort( $tags,
static function ( $a, $b ) {
979 return strcasecmp( $a[
'label'], $b[
'label'] );
991 [
'class' =>
'mw-changeslist-empty' ],
992 $this->
msg(
'recentchanges-noresult' )->parse()
1002 '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
1003 $this->
msg(
'recentchanges-timeout' )->parse() .
1019 $query_options = [];
1021 $this->
buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1023 return $this->
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1032 if ( $this->rcOptions ===
null ) {
1033 $this->rcOptions = $this->
setup( $this->rcSubpage );
1036 return $this->rcOptions;
1061 if ( $this->
getConfig()->
get(
'RCWatchCategoryMembership' ) ) {
1063 $this->hideCategorizationFilterDefinition
1066 $transformedHideCategorizationDef[
'group'] = $changeTypeGroup;
1069 $transformedHideCategorizationDef
1073 $this->
getHookRunner()->onChangesListSpecialPageStructuredFilters( $this );
1078 $registered = $userExperienceLevel->getFilter(
'registered' );
1079 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'newcomer' ) );
1080 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'learner' ) );
1081 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'experienced' ) );
1083 $categoryFilter = $changeTypeGroup->getFilter(
'hidecategorization' );
1084 $logactionsFilter = $changeTypeGroup->getFilter(
'hidelog' );
1085 $pagecreationFilter = $changeTypeGroup->getFilter(
'hidenewpages' );
1087 $significanceTypeGroup = $this->
getFilterGroup(
'significance' );
1088 $hideMinorFilter = $significanceTypeGroup->getFilter(
'hideminor' );
1091 if ( $categoryFilter !==
null ) {
1092 $hideMinorFilter->conflictsWith(
1094 'rcfilters-hideminor-conflicts-typeofchange-global',
1095 'rcfilters-hideminor-conflicts-typeofchange',
1096 'rcfilters-typeofchange-conflicts-hideminor'
1099 $hideMinorFilter->conflictsWith(
1101 'rcfilters-hideminor-conflicts-typeofchange-global',
1102 'rcfilters-hideminor-conflicts-typeofchange',
1103 'rcfilters-typeofchange-conflicts-hideminor'
1105 $hideMinorFilter->conflictsWith(
1106 $pagecreationFilter,
1107 'rcfilters-hideminor-conflicts-typeofchange-global',
1108 'rcfilters-hideminor-conflicts-typeofchange',
1109 'rcfilters-typeofchange-conflicts-hideminor'
1123 return $filterDefinition;
1137 $autoFillPriority = -1;
1138 foreach ( $definition as $groupDefinition ) {
1139 if ( !isset( $groupDefinition[
'priority'] ) ) {
1140 $groupDefinition[
'priority'] = $autoFillPriority;
1143 $autoFillPriority = $groupDefinition[
'priority'];
1146 $autoFillPriority--;
1148 $className = $groupDefinition[
'class'];
1149 unset( $groupDefinition[
'class'] );
1151 foreach ( $groupDefinition[
'filters'] as &$filterDefinition ) {
1164 foreach ( $this->filterGroups as $group ) {
1166 foreach ( $group->getFilters() as $key => $filter ) {
1167 if ( $filter->displaysOnUnstructuredUi() ) {
1168 $filters[ $key ] = $filter;
1192 if ( $parameters !==
null ) {
1214 $useDefaults = $this->
getRequest()->getInt(
'urlversion' ) !== 2;
1217 foreach ( $this->filterGroups as $filterGroup ) {
1218 $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1221 $opts->add(
'namespace',
'', FormOptions::STRING );
1222 $opts->add(
'invert',
false );
1223 $opts->add(
'associated',
false );
1224 $opts->add(
'urlversion', 1 );
1225 $opts->add(
'tagfilter',
'' );
1227 $opts->add(
'days', $this->
getDefaultDays(), FormOptions::FLOAT );
1230 $opts->add(
'from',
'' );
1241 $groupName = $group->
getName();
1243 $this->filterGroups[$groupName] = $group;
1252 return $this->filterGroups;
1263 return $this->filterGroups[$groupName] ??
null;
1282 'messageKeys' => [],
1289 foreach ( $this->filterGroups as $groupName => $group ) {
1290 $groupOutput = $group->getJsData();
1291 if ( $groupOutput !==
null ) {
1292 $output[
'messageKeys'] = array_merge(
1293 $output[
'messageKeys'],
1294 $groupOutput[
'messageKeys']
1297 unset( $groupOutput[
'messageKeys'] );
1298 $output[
'groups'][] = $groupOutput;
1314 $opts->fetchValuesFromRequest( $this->
getRequest() );
1326 $stringParameterNameSet = [];
1327 $hideParameterNameSet = [];
1332 foreach ( $this->filterGroups as $filterGroup ) {
1334 $stringParameterNameSet[$filterGroup->getName()] =
true;
1336 foreach ( $filterGroup->getFilters() as $filter ) {
1337 $hideParameterNameSet[$filter->getName()] =
true;
1342 $bits = preg_split(
'/\s*,\s*/', trim( $par ) );
1343 foreach ( $bits as $bit ) {
1345 if ( isset( $hideParameterNameSet[$bit] ) ) {
1348 } elseif ( isset( $hideParameterNameSet[
"hide$bit"] ) ) {
1350 $opts[
"hide$bit"] =
false;
1351 } elseif ( preg_match(
'/^(.*)=(.*)$/', $bit, $m ) ) {
1352 if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1353 $opts[$m[1]] = $m[2];
1368 if ( $isContradictory || $isReplaced ) {
1386 foreach ( $this->filterGroups as $filterGroup ) {
1388 $filters = $filterGroup->getFilters();
1390 if ( count( $filters ) === 1 ) {
1395 $allInGroupEnabled = array_reduce(
1398 return $carry && $opts[ $filter->
getName() ];
1400 count( $filters ) > 0
1403 if ( $allInGroupEnabled ) {
1404 foreach ( $filters as $filter ) {
1405 $opts[ $filter->getName() ] =
false;
1426 if ( $opts[
'hideanons'] && $opts[
'hideliu'] ) {
1427 $opts->
reset(
'hideanons' );
1428 if ( !$opts[
'hidebots'] ) {
1429 $opts->
reset(
'hideliu' );
1430 $opts[
'hidehumans'] = 1;
1454 if ( $opts[
'hideanons' ] ) {
1455 $opts->
reset(
'hideanons' );
1456 $opts[
'userExpLevel' ] =
'registered';
1460 if ( $opts[
'hideliu' ] ) {
1461 $opts->
reset(
'hideliu' );
1462 $opts[
'userExpLevel' ] =
'unregistered';
1467 if ( $opts[
'hidepatrolled' ] ) {
1468 $opts->
reset(
'hidepatrolled' );
1469 $opts[
'reviewStatus' ] =
'unpatrolled';
1473 if ( $opts[
'hideunpatrolled' ] ) {
1474 $opts->
reset(
'hideunpatrolled' );
1475 $opts[
'reviewStatus' ] = implode(
1477 [
'manual',
'auto' ]
1495 foreach ( $params as &$value ) {
1496 if ( $value ===
false ) {
1515 protected function buildQuery( &$tables, &$fields, &$conds, &$query_options,
1522 foreach ( $this->filterGroups as $filterGroup ) {
1523 $filterGroup->modifyQuery(
$dbr, $this, $tables, $fields, $conds,
1524 $query_options, $join_conds, $opts, $isStructuredUI );
1528 if ( $opts[
'namespace' ] !==
'' ) {
1529 $namespaces = explode(
';', $opts[
'namespace' ] );
1533 $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1534 $namespaces = array_filter(
1536 static function ( $ns ) use ( $namespaceInfo ) {
1537 return $namespaceInfo->exists( $ns );
1541 if ( $namespaces !== [] ) {
1543 $namespaces = array_map(
'intval', $namespaces );
1545 if ( $opts[
'associated' ] ) {
1546 $associatedNamespaces = array_map(
1547 static function ( $ns ) use ( $namespaceInfo ){
1548 return $namespaceInfo->getAssociated( $ns );
1552 static function ( $ns ) use ( $namespaceInfo ) {
1553 return $namespaceInfo->hasTalkNamespace( $ns );
1557 $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
1560 if ( count( $namespaces ) === 1 ) {
1561 $operator = $opts[
'invert' ] ?
'!=' :
'=';
1562 $value =
$dbr->addQuotes( reset( $namespaces ) );
1564 $operator = $opts[
'invert' ] ?
'NOT IN' :
'IN';
1565 sort( $namespaces );
1566 $value =
'(' .
$dbr->makeList( $namespaces ) .
')';
1568 $conds[] =
"rc_namespace $operator $value";
1573 $cutoff_unixtime = time() - $opts[
'days'] * 3600 * 24;
1574 $cutoff =
$dbr->timestamp( $cutoff_unixtime );
1576 $fromValid = preg_match(
'/^[0-9]{14}$/', $opts[
'from'] );
1577 if ( $fromValid && $opts[
'from'] >
wfTimestamp( TS_MW, $cutoff ) ) {
1578 $cutoff =
$dbr->timestamp( $opts[
'from'] );
1580 $opts->
reset(
'from' );
1583 $conds[] =
'rc_timestamp >= ' .
$dbr->addQuotes( $cutoff );
1600 $rcQuery = RecentChange::getQueryInfo();
1601 $tables = array_merge( $tables, $rcQuery[
'tables'] );
1602 $fields = array_merge( $rcQuery[
'fields'], $fields );
1603 $join_conds = array_merge( $join_conds, $rcQuery[
'joins'] );
1614 if ( !$this->
runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
1622 return $dbr->select(
1633 &$query_options, &$join_conds, $opts
1635 return $this->
getHookRunner()->onChangesListSpecialPageQuery(
1636 $this->
getName(), $tables, $fields, $conds, $query_options, $join_conds, $opts );
1657 $this->
doHeader( $opts, $rowCount );
1741 $user = $context->getUser();
1742 # The legend showing what the letters and stuff mean
1743 $legend = Html::openElement(
'dl' ) .
"\n";
1744 # Iterates through them and gets the messages for both letter and tooltip
1745 $legendItems = $context->getConfig()->get(
'RecentChangesFlags' );
1746 if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
1747 unset( $legendItems[
'unpatrolled'] );
1749 foreach ( $legendItems as $key => $item ) { # generate items of the legend
1750 $label = $item[
'legend'] ?? $item[
'title'];
1751 $letter = $item[
'letter'];
1752 $cssClass = $item[
'class'] ?? $key;
1754 $legend .= Html::element(
'dt',
1755 [
'class' => $cssClass ], $context->msg( $letter )->text()
1757 Html::rawElement(
'dd',
1758 [
'class' => Sanitizer::escapeClass(
'mw-changeslist-legend-' . $key ) ],
1759 $context->msg( $label )->parse()
1763 $legend .= Html::rawElement(
'dt',
1764 [
'class' =>
'mw-plusminus-pos' ],
1765 $context->msg(
'recentchanges-legend-plusminus' )->parse()
1767 $legend .= Html::element(
1769 [
'class' =>
'mw-changeslist-legend-plusminus' ],
1770 $context->msg(
'recentchanges-label-plusminus' )->text()
1773 if ( $context->getConfig()->get(
'WatchlistExpiry' ) ) {
1774 $widget =
new IconWidget( [
1776 'classes' => [
'mw-changesList-watchlistExpiry' ],
1779 $watchlistLabelId =
'mw-changeslist-watchlistExpiry-label';
1780 $widget->getIconElement()->setAttributes( [
1782 'aria-labelledby' => $watchlistLabelId,
1784 $legend .= Html::rawElement(
1786 [
'class' =>
'mw-changeslist-legend-watchlistexpiry' ],
1789 $legend .= Html::element(
1791 [
'class' =>
'mw-changeslist-legend-watchlistexpiry',
'id' => $watchlistLabelId ],
1792 $context->msg(
'recentchanges-legend-watchlistexpiry' )->text()
1795 $legend .= Html::closeElement(
'dl' ) .
"\n";
1798 $context->msg(
'rcfilters-legend-heading' )->parse() :
1799 $context->msg(
'recentchanges-legend-heading' )->parse();
1802 $collapsedState = $this->
getRequest()->getCookie(
'changeslist-state' );
1803 $collapsedClass = $collapsedState ===
'collapsed' ?
'mw-collapsed' :
'';
1805 $legend = Html::rawElement(
1807 [
'class' => [
'mw-changeslist-legend',
'mw-collapsible', $collapsedClass ] ],
1809 Html::rawElement(
'div', [
'class' =>
'mw-collapsible-content' ], $legend )
1821 $out->addModuleStyles( [
1822 'mediawiki.interface.helpers.styles',
1823 'mediawiki.special.changeslist.legend',
1824 'mediawiki.special.changeslist',
1826 $out->addModules(
'mediawiki.special.changeslist.legend.js' );
1829 $out->addModules(
'mediawiki.rcfilters.filters.ui' );
1830 $out->addModuleStyles(
'mediawiki.rcfilters.filters.base.styles' );
1855 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1865 if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1871 in_array(
'registered', $selectedExpLevels ) &&
1872 in_array(
'unregistered', $selectedExpLevels )
1879 in_array(
'registered', $selectedExpLevels ) &&
1880 !in_array(
'unregistered', $selectedExpLevels )
1882 $conds[] =
'actor_user IS NOT NULL';
1886 if ( $selectedExpLevels === [
'unregistered' ] ) {
1887 $conds[
'actor_user'] =
null;
1892 $join_conds[
'user'] = [
'LEFT JOIN',
'actor_user=user_id' ];
1897 $secondsPerDay = 86400;
1901 $aboveNewcomer =
$dbr->makeList(
1905 'user_registration IS NULL',
1906 'user_registration <= ' .
$dbr->addQuotes(
$dbr->timestamp( $learnerCutoff ) ),
1907 ], IDatabase::LIST_OR ),
1912 $aboveLearner =
$dbr->makeList(
1916 'user_registration IS NULL',
1917 'user_registration <= ' .
1918 $dbr->addQuotes(
$dbr->timestamp( $experiencedUserCutoff ) ),
1919 ], IDatabase::LIST_OR ),
1926 if ( in_array(
'unregistered', $selectedExpLevels ) ) {
1927 $selectedExpLevels = array_diff( $selectedExpLevels, [
'unregistered' ] );
1928 $conditions[
'actor_user'] =
null;
1931 if ( $selectedExpLevels === [
'newcomer' ] ) {
1932 $conditions[] =
"NOT ( $aboveNewcomer )";
1933 } elseif ( $selectedExpLevels === [
'learner' ] ) {
1934 $conditions[] =
$dbr->makeList(
1935 [ $aboveNewcomer,
"NOT ( $aboveLearner )" ],
1938 } elseif ( $selectedExpLevels === [
'experienced' ] ) {
1939 $conditions[] = $aboveLearner;
1940 } elseif ( $selectedExpLevels === [
'learner',
'newcomer' ] ) {
1941 $conditions[] =
"NOT ( $aboveLearner )";
1942 } elseif ( $selectedExpLevels === [
'experienced',
'newcomer' ] ) {
1943 $conditions[] =
$dbr->makeList(
1944 [
"NOT ( $aboveNewcomer )", $aboveLearner ],
1947 } elseif ( $selectedExpLevels === [
'experienced',
'learner' ] ) {
1948 $conditions[] = $aboveNewcomer;
1949 } elseif ( $selectedExpLevels === [
'experienced',
'learner',
'newcomer' ] ) {
1950 $conditions[] =
'actor_user IS NOT NULL';
1953 if ( count( $conditions ) > 1 ) {
1954 $conds[] =
$dbr->makeList( $conditions, IDatabase::LIST_OR );
1955 } elseif ( count( $conditions ) === 1 ) {
1956 $conds[] = reset( $conditions );
1966 if ( $this->
getRequest()->getBool(
'rcfilters' ) ) {
1970 return static::checkStructuredFilterUiEnabled( $this->
getUser() );
1981 if ( $user instanceof
Config ) {
1982 wfDeprecated( __METHOD__ .
' with Config argument',
'1.34' );
1983 $user = func_get_arg( 1 );
1985 return !$user->getOption(
'rcenhancedfilters-disable' );
1996 return MediaWikiServices::getInstance()
1997 ->getUserOptionsLookup()
2010 return floatval( $this->
getUser()->getOption( static::$daysPreferenceName ) );
2026 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2027 $symbolicFilters = [
2028 'all-contents' => $nsInfo->getSubjectNamespaces(),
2029 'all-discussions' => $nsInfo->getTalkNamespaces(),
2031 $additionalNamespaces = [];
2032 foreach ( $symbolicFilters as $name => $values ) {
2033 if ( in_array( $name, $namespaces ) ) {
2034 $additionalNamespaces = array_merge( $additionalNamespaces, $values );
2037 $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) );
2038 $namespaces = array_merge( $namespaces, $additionalNamespaces );
2039 return array_unique( $namespaces );
$wgLearnerMemberSince
Number of days the user must exist before becoming a learner.
$wgExperiencedUserMemberSince
Number of days the user must exist before becoming "experienced".
$wgLearnerEdits
The following variables define 3 user experience levels:
$wgExperiencedUserEdits
Number of edits the user must have before becoming "experienced".
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
If the group is active, any unchecked filters will translate to hide parameters in the URL.
Represents a hide-based boolean filter (used on ChangesListSpecialPage and descendants)
Represents a filter group (used on ChangesListSpecialPage and descendants)
Represents a filter (used on ChangesListSpecialPage and descendants)
getConflictingFilters()
Get filters conflicting with this filter.
Special page which uses a ChangesList to show query results.
static getRcFiltersConfigVars(ResourceLoaderContext $context)
Get config vars to export with the mediawiki.rcfilters.filters.ui module.
const TAG_DESC_CHARACTER_LIMIT
Maximum length of a tag description in UTF-8 characters.
validateOptions(FormOptions $opts)
Validate a FormOptions object generated by getDefaultOptions() with values already populated.
getDefaultOptions()
Get a FormOptions object containing the default options.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
setTopText(FormOptions $opts)
Send the text to be displayed before the options.
filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now=0)
Filter on users' experience levels; this will not be called if nothing is selected.
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
getDefaultLimit()
Get the default value of the number of changes to display when loading the result set.
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.
parseParameters( $par, FormOptions $opts)
Process $par and put options found in $opts.
static getChangeTagListSummary(ResourceLoaderContext $context)
Get information about change tags, without parsing messages, for getRcFiltersConfigSummary().
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
replaceOldOptions(FormOptions $opts)
Replace old options with their structured UI equivalents.
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
array $hideCategorizationFilterDefinition
Single filter group registered conditionally.
getExtraOptions( $opts)
Get options to be displayed in a form.
setup( $parameters)
Register all the filters, including legacy hook-driven ones.
static getChangeTagList(ResourceLoaderContext $context)
Get information about change tags to export to JS via getRcFiltersConfigVars().
static string $savedQueriesPreferenceName
Preference name for saved queries.
registerFilters()
Register all filters and their groups (including those from hooks), plus handle conflicts and default...
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
array $legacyReviewStatusFilterGroupDefinition
Same format as filterGroupDefinitions, but for a single group (reviewStatus) that is registered condi...
getDefaultDays()
Get the default value of the number of days to display when loading the result set.
fixBackwardsCompatibilityOptions(FormOptions $opts)
Fix a special case (hideanons=1 and hideliu=1) in a special way, for backwards compatibility.
expandSymbolicNamespaceFilters(array $namespaces)
outputNoResults()
Add the "no results" message to the output.
static string $daysPreferenceName
Preference name for 'days'.
getFilterGroups()
Gets the currently registered filters groups.
registerFilterGroup(ChangesListFilterGroup $group)
Register a structured changes list filter group.
addModules()
Add page-specific modules.
fixContradictoryOptions(FormOptions $opts)
Fix invalid options by resetting pairs that should never appear together.
static getRcFiltersConfigSummary(ResourceLoaderContext $context)
Get essential data about getRcFiltersConfigVars() for change detection.
__construct( $name, $restriction)
getLegacyShowHideFilters()
outputTimeout()
Add the "timeout" message to the output.
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
array $reviewStatusFilterGroupDefinition
Single filter group registered conditionally.
fetchOptionsFromRequest( $opts)
Fetch values for a FormOptions object from the WebRequest associated with this instance.
getOptions()
Get the current FormOptions for this request.
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
Process the query.
setBottomText(FormOptions $opts)
Send the text to be displayed after the options.
getStructuredFilterJsData()
Gets structured filter information needed by JS.
buildQuery(&$tables, &$fields, &$conds, &$query_options, &$join_conds, FormOptions $opts)
Sets appropriate tables, fields, conditions, etc.
webOutputHeader( $rowCount, $opts)
Send header output to the OutputPage object, only called if not using feeds.
ChangesListFilterGroup[] $filterGroups
Filter groups, and their contained filters This is an associative array (with group name as key) of C...
makeLegend()
Return the legend displayed within the fieldset.
webOutput( $rows, $opts)
Send output to the OutputPage object, only called if not used feeds.
considerActionsForDefaultSavedQuery( $subpage)
Check whether or not the page should load defaults, and if so, whether a default saved query is relev...
transformFilterDefinition(array $filterDefinition)
Transforms filter definition to prepare it for constructor.
getDB()
Return a IDatabase object for reading.
getLimitPreferenceName()
Getting the preference name for 'limit'.
static checkStructuredFilterUiEnabled( $user)
Static method to check whether StructuredFilter UI is enabled for the given user.
outputChangesList( $rows, $opts)
Build and output the actual changes list.
getRows()
Get the database result for this special page instance.
array $filterGroupDefinitions
Definition information for the filters and their groups.
includeRcFiltersApp()
Include the modules and configuration for the RCFilters app.
static string $collapsedPreferenceName
Preference name for collapsing the active filter display.
Represents a filter group with multiple string options.
const SEPARATOR
Delimiter.
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
Utility class for creating new RC entries.
Context object that contains information about the state of a specific ResourceLoader web request.
msg( $key,... $params)
Get a Message object with context set.
Parent class for all special pages.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
getName()
Get the name of this Special Page.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
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.
including( $x=null)
Whether the special page is being evaluated via transclusion.
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Class for fixing stale WANObjectCache keys using a purge event source.
Interface for configuration instances.
Interface for objects which can provide a MediaWiki context on request.
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s