65 private $filterGroupDefinitions;
71 private $legacyReviewStatusFilterGroupDefinition;
74 private $reviewStatusFilterGroupDefinition;
77 private $hideCategorizationFilterDefinition;
85 protected $filterGroups = [];
88 parent::__construct( $name, $restriction );
90 $nonRevisionTypes = [
RC_LOG ];
91 $this->
getHookRunner()->onSpecialWatchlistGetNonRevisionTypes( $nonRevisionTypes );
93 $this->filterGroupDefinitions = [
95 'name' =>
'registration',
96 'title' =>
'rcfilters-filtergroup-registration',
97 'class' => ChangesListBooleanFilterGroup::class,
103 'showHideSuffix' =>
'showhideliu',
105 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
106 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
108 $conds[
'actor_user'] =
null;
109 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
111 'isReplacedInStructuredUi' =>
true,
115 'name' =>
'hideanons',
118 'showHideSuffix' =>
'showhideanons',
120 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
121 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
123 $conds[] =
'actor_user IS NOT NULL';
124 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
126 'isReplacedInStructuredUi' =>
true,
132 'name' =>
'userExpLevel',
133 'title' =>
'rcfilters-filtergroup-user-experience-level',
134 'class' => ChangesListStringOptionsFilterGroup::class,
135 'isFullCoverage' =>
true,
138 'name' =>
'unregistered',
139 'label' =>
'rcfilters-filter-user-experience-level-unregistered-label',
140 'description' =>
'rcfilters-filter-user-experience-level-unregistered-description',
141 'cssClassSuffix' =>
'user-unregistered',
143 return !$rc->getAttribute(
'rc_user' );
147 'name' =>
'registered',
148 'label' =>
'rcfilters-filter-user-experience-level-registered-label',
149 'description' =>
'rcfilters-filter-user-experience-level-registered-description',
150 'cssClassSuffix' =>
'user-registered',
152 return $rc->getAttribute(
'rc_user' );
156 'name' =>
'newcomer',
157 'label' =>
'rcfilters-filter-user-experience-level-newcomer-label',
158 'description' =>
'rcfilters-filter-user-experience-level-newcomer-description',
159 'cssClassSuffix' =>
'user-newcomer',
161 $performer = $rc->getPerformerIdentity();
162 return $performer->isRegistered() &&
163 MediaWikiServices::getInstance()
165 ->newFromUserIdentity( $performer )
166 ->getExperienceLevel() ===
'newcomer';
171 'label' =>
'rcfilters-filter-user-experience-level-learner-label',
172 'description' =>
'rcfilters-filter-user-experience-level-learner-description',
173 'cssClassSuffix' =>
'user-learner',
175 $performer = $rc->getPerformerIdentity();
176 return $performer->isRegistered() &&
177 MediaWikiServices::getInstance()
179 ->newFromUserIdentity( $performer )
180 ->getExperienceLevel() ===
'learner';
184 'name' =>
'experienced',
185 'label' =>
'rcfilters-filter-user-experience-level-experienced-label',
186 'description' =>
'rcfilters-filter-user-experience-level-experienced-description',
187 'cssClassSuffix' =>
'user-experienced',
189 $performer = $rc->getPerformerIdentity();
190 return $performer->isRegistered() &&
191 MediaWikiServices::getInstance()
193 ->newFromUserIdentity( $performer )
194 ->getExperienceLevel() ===
'experienced';
199 'queryCallable' => [ $this,
'filterOnUserExperienceLevel' ],
203 'name' =>
'authorship',
204 'title' =>
'rcfilters-filtergroup-authorship',
205 'class' => ChangesListBooleanFilterGroup::class,
208 'name' =>
'hidemyself',
209 'label' =>
'rcfilters-filter-editsbyself-label',
210 'description' =>
'rcfilters-filter-editsbyself-description',
213 'showHideSuffix' =>
'showhidemine',
215 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
216 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
218 $user = $ctx->getUser();
219 $conds[] =
'actor_name<>' .
$dbr->addQuotes( $user->getName() );
220 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
222 'cssClassSuffix' =>
'self',
224 return $ctx->getUser()->equals( $rc->getPerformerIdentity() );
228 'name' =>
'hidebyothers',
229 'label' =>
'rcfilters-filter-editsbyother-label',
230 'description' =>
'rcfilters-filter-editsbyother-description',
232 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
233 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
235 $user = $ctx->getUser();
236 if ( $user->isAnon() ) {
237 $conds[
'actor_name'] = $user->getName();
239 $conds[
'actor_user'] = $user->getId();
241 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
243 'cssClassSuffix' =>
'others',
245 return !$ctx->getUser()->equals( $rc->getPerformerIdentity() );
252 'name' =>
'automated',
253 'title' =>
'rcfilters-filtergroup-automated',
254 'class' => ChangesListBooleanFilterGroup::class,
257 'name' =>
'hidebots',
258 'label' =>
'rcfilters-filter-bots-label',
259 'description' =>
'rcfilters-filter-bots-description',
262 'showHideSuffix' =>
'showhidebots',
264 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
265 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
267 $conds[
'rc_bot'] = 0;
269 'cssClassSuffix' =>
'bot',
271 return $rc->getAttribute(
'rc_bot' );
275 'name' =>
'hidehumans',
276 'label' =>
'rcfilters-filter-humans-label',
277 'description' =>
'rcfilters-filter-humans-description',
279 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
280 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
282 $conds[
'rc_bot'] = 1;
284 'cssClassSuffix' =>
'human',
286 return !$rc->getAttribute(
'rc_bot' );
295 'name' =>
'significance',
296 'title' =>
'rcfilters-filtergroup-significance',
297 'class' => ChangesListBooleanFilterGroup::class,
301 'name' =>
'hideminor',
302 'label' =>
'rcfilters-filter-minor-label',
303 'description' =>
'rcfilters-filter-minor-description',
306 'showHideSuffix' =>
'showhideminor',
308 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
309 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
311 $conds[] =
'rc_minor = 0';
313 'cssClassSuffix' =>
'minor',
315 return $rc->getAttribute(
'rc_minor' );
319 'name' =>
'hidemajor',
320 'label' =>
'rcfilters-filter-major-label',
321 'description' =>
'rcfilters-filter-major-description',
323 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
324 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
326 $conds[] =
'rc_minor = 1';
328 'cssClassSuffix' =>
'major',
330 return !$rc->getAttribute(
'rc_minor' );
337 'name' =>
'lastRevision',
338 'title' =>
'rcfilters-filtergroup-lastrevision',
339 'class' => ChangesListBooleanFilterGroup::class,
343 'name' =>
'hidelastrevision',
344 'label' =>
'rcfilters-filter-lastrevision-label',
345 'description' =>
'rcfilters-filter-lastrevision-description',
347 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
348 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
349 ) use ( $nonRevisionTypes ) {
350 $conds[] =
$dbr->makeList(
352 'rc_this_oldid <> page_latest',
353 'rc_type' => $nonRevisionTypes,
358 'cssClassSuffix' =>
'last',
360 return $rc->getAttribute(
'rc_this_oldid' ) === $rc->getAttribute(
'page_latest' );
364 'name' =>
'hidepreviousrevisions',
365 'label' =>
'rcfilters-filter-previousrevision-label',
366 'description' =>
'rcfilters-filter-previousrevision-description',
368 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
369 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
370 ) use ( $nonRevisionTypes ) {
371 $conds[] =
$dbr->makeList(
373 'rc_this_oldid = page_latest',
374 'rc_type' => $nonRevisionTypes,
379 'cssClassSuffix' =>
'previous',
381 return $rc->getAttribute(
'rc_this_oldid' ) !== $rc->getAttribute(
'page_latest' );
389 'name' =>
'changeType',
390 'title' =>
'rcfilters-filtergroup-changetype',
391 'class' => ChangesListBooleanFilterGroup::class,
395 'name' =>
'hidepageedits',
396 'label' =>
'rcfilters-filter-pageedits-label',
397 'description' =>
'rcfilters-filter-pageedits-description',
400 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
401 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
403 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_EDIT );
405 'cssClassSuffix' =>
'src-mw-edit',
407 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_EDIT;
411 'name' =>
'hidenewpages',
412 'label' =>
'rcfilters-filter-newpages-label',
413 'description' =>
'rcfilters-filter-newpages-description',
416 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
417 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
419 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_NEW );
421 'cssClassSuffix' =>
'src-mw-new',
423 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_NEW;
431 'label' =>
'rcfilters-filter-logactions-label',
432 'description' =>
'rcfilters-filter-logactions-description',
435 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
436 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
438 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_LOG );
440 'cssClassSuffix' =>
'src-mw-log',
442 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_LOG;
450 $this->legacyReviewStatusFilterGroupDefinition = [
452 'name' =>
'legacyReviewStatus',
453 'title' =>
'rcfilters-filtergroup-reviewstatus',
454 'class' => ChangesListBooleanFilterGroup::class,
457 'name' =>
'hidepatrolled',
460 'showHideSuffix' =>
'showhidepatr',
462 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
463 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
465 $conds[
'rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
467 'isReplacedInStructuredUi' =>
true,
470 'name' =>
'hideunpatrolled',
472 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
473 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
475 $conds[] =
'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
477 'isReplacedInStructuredUi' =>
true,
483 $this->reviewStatusFilterGroupDefinition = [
485 'name' =>
'reviewStatus',
486 'title' =>
'rcfilters-filtergroup-reviewstatus',
487 'class' => ChangesListStringOptionsFilterGroup::class,
488 'isFullCoverage' =>
true,
492 'name' =>
'unpatrolled',
493 'label' =>
'rcfilters-filter-reviewstatus-unpatrolled-label',
494 'description' =>
'rcfilters-filter-reviewstatus-unpatrolled-description',
495 'cssClassSuffix' =>
'reviewstatus-unpatrolled',
497 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED;
502 'label' =>
'rcfilters-filter-reviewstatus-manual-label',
503 'description' =>
'rcfilters-filter-reviewstatus-manual-description',
504 'cssClassSuffix' =>
'reviewstatus-manual',
506 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_PATROLLED;
511 'label' =>
'rcfilters-filter-reviewstatus-auto-label',
512 'description' =>
'rcfilters-filter-reviewstatus-auto-description',
513 'cssClassSuffix' =>
'reviewstatus-auto',
515 return $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED;
520 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
521 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
523 if ( $selected === [] ) {
526 $rcPatrolledValues = [
527 'unpatrolled' => RecentChange::PRC_UNPATROLLED,
528 'manual' => RecentChange::PRC_PATROLLED,
529 'auto' => RecentChange::PRC_AUTOPATROLLED,
532 $conds[
'rc_patrolled'] = array_map(
static function (
$s ) use ( $rcPatrolledValues ) {
533 return $rcPatrolledValues[
$s ];
539 $this->hideCategorizationFilterDefinition = [
540 'name' =>
'hidecategorization',
541 'label' =>
'rcfilters-filter-categorization-label',
542 'description' =>
'rcfilters-filter-categorization-description',
545 'showHideSuffix' =>
'showhidecategorization',
548 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
549 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
553 'cssClassSuffix' =>
'src-mw-categorize',
555 return $rc->getAttribute(
'rc_source' ) === RecentChange::SRC_CATEGORIZE;
568 if ( $group->getConflictingGroups() ) {
571 " specifies conflicts with other groups but these are not supported yet."
575 foreach ( $group->getConflictingFilters() as $conflictingFilter ) {
576 if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
581 foreach ( $group->getFilters() as $filter ) {
582 foreach ( $filter->getConflictingFilters() as $conflictingFilter ) {
584 $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
585 $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
602 $this->rcSubpage = $subpage;
607 if ( $this->
getConfig()->
get( MainConfigNames::WatchlistExpiry ) ) {
609 $this->
getOutput()->addModules(
'mediawiki.special.changeslist.watchlistexpiry' );
615 if ( $rows ===
false ) {
620 if ( $this->
getRequest()->getRawVal(
'action' ) ===
'render' ) {
621 $this->
getOutput()->setArticleBodyOnly(
true );
626 if ( $this->
getRequest()->getBool(
'peek' ) ) {
627 $code = $rows->numRows() > 0 ? 200 : 204;
628 $this->
getOutput()->setStatusCode( $code );
630 if ( $this->
getUser()->isAnon() !==
633 $this->
getOutput()->setStatusCode( 205 );
639 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
640 $batch = $linkBatchFactory->newLinkBatch();
641 foreach ( $rows as $row ) {
642 $batch->add(
NS_USER, $row->rc_user_text );
644 $batch->add( $row->rc_namespace, $row->rc_title );
645 if ( $row->rc_source === RecentChange::SRC_LOG ) {
646 $formatter = LogFormatter::newFromRow( $row );
647 foreach ( $formatter->getPreloadTitles() as
$title ) {
661 MWExceptionHandler::logException( $timeoutException );
667 $this->
getOutput()->setStatusCode( 500 );
668 $this->webOutputHeader( 0, $opts );
672 if ( $this->
getConfig()->
get( MainConfigNames::EnableWANCacheReaper ) ) {
676 LoggerFactory::getInstance(
'objectcache' )
695 $knownParams = $this->
getRequest()->getValues(
696 ...array_keys( $this->
getOptions()->getAllValues() )
702 $excludedParams = [
'limit' =>
'',
'days' =>
'',
'enhanced' =>
'',
'from' =>
'' ];
703 $knownParams = array_diff_key( $knownParams, $excludedParams );
709 count( $knownParams ) === 0
711 $prefJson = MediaWikiServices::getInstance()
712 ->getUserOptionsLookup()
716 $savedQueries = $prefJson ? FormatJson::decode( $prefJson,
true ) :
false;
718 if ( $savedQueries && isset( $savedQueries[
'default' ] ) ) {
721 if ( isset( $savedQueries[
'version' ] ) && $savedQueries[
'version' ] ===
'2' ) {
722 $savedQueryDefaultID = $savedQueries[
'default' ];
723 $defaultQuery = $savedQueries[
'queries' ][ $savedQueryDefaultID ][
'data' ];
726 $query = array_merge(
727 $defaultQuery[
'params' ],
728 $defaultQuery[
'highlights' ],
735 $query = array_merge( $this->
getRequest()->getValues(), $query );
736 unset( $query[
'title' ] );
744 'wgStructuredChangeFiltersDefaultSavedQueryExists',
750 $this->
getOutput()->addBodyClasses(
'mw-rcfilters-ui-loading' );
761 $linkDays = $this->
getConfig()->get( MainConfigNames::RCLinkDays );
762 $filterByAge = $this->
getConfig()->get( MainConfigNames::RCFilterByAge );
763 $maxAge = $this->
getConfig()->get( MainConfigNames::RCMaxAge );
764 if ( $filterByAge ) {
770 $maxAgeDays = $maxAge / ( 3600 * 24 );
771 foreach ( $linkDays as $i => $days ) {
772 if ( $days >= $maxAgeDays ) {
773 array_splice( $linkDays, $i + 1 );
793 foreach ( $jsData[
'messageKeys'] as $key ) {
794 $messages[$key] = $this->
msg( $key )->plain();
797 $out->addBodyClasses(
'mw-rcfilters-enabled' );
798 $collapsed = MediaWikiServices::getInstance()->getUserOptionsLookup()
801 $out->addBodyClasses(
'mw-rcfilters-collapsed' );
805 $out->addJsConfigVars(
'wgStructuredChangeFilters', $jsData[
'groups'] );
806 $out->addJsConfigVars(
'wgStructuredChangeFiltersMessages', $messages );
807 $out->addJsConfigVars(
'wgStructuredChangeFiltersCollapsedState', $collapsed );
809 $out->addJsConfigVars(
810 'StructuredChangeFiltersDisplayConfig',
813 (
int)$this->
getConfig()->
get( MainConfigNames::RCMaxAge ) / ( 24 * 3600 ),
814 'limitArray' => $this->
getConfig()->
get( MainConfigNames::RCLinkLimits ),
821 $out->addJsConfigVars(
822 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
825 $out->addJsConfigVars(
826 'wgStructuredChangeFiltersLimitPreferenceName',
829 $out->addJsConfigVars(
830 'wgStructuredChangeFiltersDaysPreferenceName',
833 $out->addJsConfigVars(
834 'wgStructuredChangeFiltersCollapsedPreferenceName',
838 $out->addBodyClasses(
'mw-rcfilters-disabled' );
851 $lang = MediaWikiServices::getInstance()->getLanguageFactory()
852 ->getLanguage( $context->getLanguage() );
856 'StructuredChangeFiltersEditWatchlistUrl' =>
869 $lang = MediaWikiServices::getInstance()->getLanguageFactory()
870 ->getLanguage( $context->getLanguage() );
873 'StructuredChangeFiltersEditWatchlistUrl' =>
885 [
'class' =>
'mw-changeslist-empty' ],
886 $this->
msg(
'recentchanges-noresult' )->parse()
896 '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
897 $this->
msg(
'recentchanges-timeout' )->parse() .
915 $this->
buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
917 return $this->
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
926 if ( $this->rcOptions ===
null ) {
927 $this->rcOptions = $this->
setup( $this->rcSubpage );
930 return $this->rcOptions;
955 if ( $this->
getConfig()->
get( MainConfigNames::RCWatchCategoryMembership ) ) {
957 $this->hideCategorizationFilterDefinition
960 $transformedHideCategorizationDef[
'group'] = $changeTypeGroup;
963 $transformedHideCategorizationDef
967 $this->
getHookRunner()->onChangesListSpecialPageStructuredFilters( $this );
972 $registered = $userExperienceLevel->getFilter(
'registered' );
973 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'newcomer' ) );
974 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'learner' ) );
975 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'experienced' ) );
977 $categoryFilter = $changeTypeGroup->getFilter(
'hidecategorization' );
978 $logactionsFilter = $changeTypeGroup->getFilter(
'hidelog' );
979 $pagecreationFilter = $changeTypeGroup->getFilter(
'hidenewpages' );
982 $hideMinorFilter = $significanceTypeGroup->getFilter(
'hideminor' );
985 if ( $categoryFilter !==
null ) {
986 $hideMinorFilter->conflictsWith(
988 'rcfilters-hideminor-conflicts-typeofchange-global',
989 'rcfilters-hideminor-conflicts-typeofchange',
990 'rcfilters-typeofchange-conflicts-hideminor'
993 $hideMinorFilter->conflictsWith(
995 'rcfilters-hideminor-conflicts-typeofchange-global',
996 'rcfilters-hideminor-conflicts-typeofchange',
997 'rcfilters-typeofchange-conflicts-hideminor'
999 $hideMinorFilter->conflictsWith(
1000 $pagecreationFilter,
1001 'rcfilters-hideminor-conflicts-typeofchange-global',
1002 'rcfilters-hideminor-conflicts-typeofchange',
1003 'rcfilters-typeofchange-conflicts-hideminor'
1017 return $filterDefinition;
1031 $autoFillPriority = -1;
1032 foreach ( $definition as $groupDefinition ) {
1033 if ( !isset( $groupDefinition[
'priority'] ) ) {
1034 $groupDefinition[
'priority'] = $autoFillPriority;
1037 $autoFillPriority = $groupDefinition[
'priority'];
1040 $autoFillPriority--;
1042 $className = $groupDefinition[
'class'];
1043 unset( $groupDefinition[
'class'] );
1045 foreach ( $groupDefinition[
'filters'] as &$filterDefinition ) {
1058 foreach ( $this->filterGroups as $group ) {
1060 foreach ( $group->getFilters() as $key => $filter ) {
1062 $filters[ $key ] = $filter;
1086 if ( $parameters !==
null ) {
1108 $useDefaults = $this->
getRequest()->getInt(
'urlversion' ) !== 2;
1111 foreach ( $this->filterGroups as $filterGroup ) {
1112 $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1115 $opts->
add(
'namespace',
'', FormOptions::STRING );
1116 $opts->
add(
'invert',
false );
1117 $opts->
add(
'associated',
false );
1118 $opts->
add(
'urlversion', 1 );
1119 $opts->
add(
'tagfilter',
'' );
1124 $opts->
add(
'from',
'' );
1135 $groupName = $group->
getName();
1137 $this->filterGroups[$groupName] = $group;
1146 return $this->filterGroups;
1157 return $this->filterGroups[$groupName] ??
null;
1176 'messageKeys' => [],
1183 foreach ( $this->filterGroups as $groupName => $group ) {
1184 $groupOutput = $group->getJsData();
1185 if ( $groupOutput !==
null ) {
1186 $output[
'messageKeys'] = array_merge(
1187 $output[
'messageKeys'],
1188 $groupOutput[
'messageKeys']
1191 unset( $groupOutput[
'messageKeys'] );
1192 $output[
'groups'][] = $groupOutput;
1220 $stringParameterNameSet = [];
1221 $hideParameterNameSet = [];
1226 foreach ( $this->filterGroups as $filterGroup ) {
1228 $stringParameterNameSet[$filterGroup->getName()] =
true;
1230 foreach ( $filterGroup->getFilters() as $filter ) {
1231 $hideParameterNameSet[$filter->getName()] =
true;
1236 $bits = preg_split(
'/\s*,\s*/', trim( $par ) );
1237 foreach ( $bits as $bit ) {
1239 if ( isset( $hideParameterNameSet[$bit] ) ) {
1242 } elseif ( isset( $hideParameterNameSet[
"hide$bit"] ) ) {
1244 $opts[
"hide$bit"] =
false;
1245 } elseif ( preg_match(
'/^(.*)=(.*)$/', $bit, $m ) ) {
1246 if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1247 $opts[$m[1]] = $m[2];
1259 $isContradictory = $this->fixContradictoryOptions( $opts );
1262 if ( $isContradictory || $isReplaced ) {
1269 $this->
getConfig()->
get( MainConfigNames::RCMaxAge ) / ( 3600 * 24 ) );
1278 private function fixContradictoryOptions(
FormOptions $opts ) {
1279 $fixed = $this->fixBackwardsCompatibilityOptions( $opts );
1281 foreach ( $this->filterGroups as $filterGroup ) {
1283 $filters = $filterGroup->getFilters();
1285 if ( count( $filters ) === 1 ) {
1290 $allInGroupEnabled = array_reduce(
1293 return $carry && $opts[ $filter->
getName() ];
1295 count( $filters ) > 0
1298 if ( $allInGroupEnabled ) {
1299 foreach ( $filters as $filter ) {
1300 $opts[ $filter->getName() ] =
false;
1320 private function fixBackwardsCompatibilityOptions(
FormOptions $opts ) {
1321 if ( $opts[
'hideanons'] && $opts[
'hideliu'] ) {
1322 $opts->
reset(
'hideanons' );
1323 if ( !$opts[
'hidebots'] ) {
1324 $opts->
reset(
'hideliu' );
1325 $opts[
'hidehumans'] = 1;
1349 if ( $opts[
'hideanons' ] ) {
1350 $opts->
reset(
'hideanons' );
1351 $opts[
'userExpLevel' ] =
'registered';
1355 if ( $opts[
'hideliu' ] ) {
1356 $opts->
reset(
'hideliu' );
1357 $opts[
'userExpLevel' ] =
'unregistered';
1362 if ( $opts[
'hidepatrolled' ] ) {
1363 $opts->
reset(
'hidepatrolled' );
1364 $opts[
'reviewStatus' ] =
'unpatrolled';
1368 if ( $opts[
'hideunpatrolled' ] ) {
1369 $opts->
reset(
'hideunpatrolled' );
1370 $opts[
'reviewStatus' ] = implode(
1372 [
'manual',
'auto' ]
1390 foreach ( $params as &$value ) {
1391 if ( $value ===
false ) {
1410 protected function buildQuery( &$tables, &$fields, &$conds, &$query_options,
1417 foreach ( $this->filterGroups as $filterGroup ) {
1418 $filterGroup->modifyQuery(
$dbr, $this, $tables, $fields, $conds,
1419 $query_options, $join_conds, $opts, $isStructuredUI );
1423 if ( $opts[
'namespace' ] !==
'' ) {
1424 $namespaces = explode(
';', $opts[
'namespace' ] );
1426 $namespaces = $this->expandSymbolicNamespaceFilters( $namespaces );
1428 $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1429 $namespaces = array_filter( $namespaces, [ $namespaceInfo,
'exists' ] );
1431 if ( $namespaces !== [] ) {
1433 $namespaces = array_map(
'intval', $namespaces );
1435 if ( $opts[
'associated' ] ) {
1436 $associatedNamespaces = array_map(
1437 [ $namespaceInfo,
'getAssociated' ],
1438 array_filter( $namespaces, [ $namespaceInfo,
'hasTalkNamespace' ] )
1440 $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
1443 if ( count( $namespaces ) === 1 ) {
1444 $operator = $opts[
'invert' ] ?
'!=' :
'=';
1445 $value =
$dbr->addQuotes( reset( $namespaces ) );
1447 $operator = $opts[
'invert' ] ?
'NOT IN' :
'IN';
1448 sort( $namespaces );
1449 $value =
'(' .
$dbr->makeList( $namespaces ) .
')';
1451 $conds[] =
"rc_namespace $operator $value";
1456 $cutoff_unixtime = time() - $opts[
'days'] * 3600 * 24;
1457 $cutoff =
$dbr->timestamp( $cutoff_unixtime );
1459 $fromValid = preg_match(
'/^[0-9]{14}$/', $opts[
'from'] );
1460 if ( $fromValid && $opts[
'from'] >
wfTimestamp( TS_MW, $cutoff ) ) {
1461 $cutoff =
$dbr->timestamp( $opts[
'from'] );
1463 $opts->
reset(
'from' );
1466 $conds[] =
'rc_timestamp >= ' .
$dbr->addQuotes( $cutoff );
1483 $rcQuery = RecentChange::getQueryInfo();
1484 $tables = array_merge( $tables, $rcQuery[
'tables'] );
1485 $fields = array_merge( $rcQuery[
'fields'], $fields );
1486 $join_conds = array_merge( $join_conds, $rcQuery[
'joins'] );
1497 if ( !$this->
runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
1505 return $dbr->select(
1516 &$query_options, &$join_conds, $opts
1518 return $this->
getHookRunner()->onChangesListSpecialPageQuery(
1519 $this->
getName(), $tables, $fields, $conds, $query_options, $join_conds, $opts );
1537 private function webOutputHeader( $rowCount, $opts ) {
1538 if ( !$this->including() ) {
1539 $this->outputFeedLinks();
1540 $this->doHeader( $opts, $rowCount );
1551 $this->webOutputHeader( $rows->numRows(), $opts );
1624 $user = $context->getUser();
1625 # The legend showing what the letters and stuff mean
1626 $legend = Html::openElement(
'dl' ) .
"\n";
1627 # Iterates through them and gets the messages for both letter and tooltip
1628 $legendItems = $context->getConfig()->get( MainConfigNames::RecentChangesFlags );
1629 if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
1630 unset( $legendItems[
'unpatrolled'] );
1632 foreach ( $legendItems as $key => $item ) { # generate items of the legend
1633 $label = $item[
'legend'] ?? $item[
'title'];
1634 $letter = $item[
'letter'];
1635 $cssClass = $item[
'class'] ?? $key;
1637 $legend .= Html::element(
'dt',
1638 [
'class' => $cssClass ], $context->msg( $letter )->text()
1640 Html::rawElement(
'dd',
1641 [
'class' => Sanitizer::escapeClass(
'mw-changeslist-legend-' . $key ) ],
1642 $context->msg( $label )->parse()
1646 $legend .= Html::rawElement(
'dt',
1647 [
'class' =>
'mw-plusminus-pos' ],
1648 $context->msg(
'recentchanges-legend-plusminus' )->parse()
1650 $legend .= Html::element(
1652 [
'class' =>
'mw-changeslist-legend-plusminus' ],
1653 $context->msg(
'recentchanges-label-plusminus' )->text()
1656 if ( $context->getConfig()->get( MainConfigNames::WatchlistExpiry ) ) {
1657 $widget =
new IconWidget( [
1659 'classes' => [
'mw-changesList-watchlistExpiry' ],
1662 $watchlistLabelId =
'mw-changeslist-watchlistExpiry-label';
1663 $widget->getIconElement()->setAttributes( [
1665 'aria-labelledby' => $watchlistLabelId,
1667 $legend .= Html::rawElement(
1669 [
'class' =>
'mw-changeslist-legend-watchlistexpiry' ],
1672 $legend .= Html::element(
1674 [
'class' =>
'mw-changeslist-legend-watchlistexpiry',
'id' => $watchlistLabelId ],
1675 $context->msg(
'recentchanges-legend-watchlistexpiry' )->text()
1678 $legend .= Html::closeElement(
'dl' ) .
"\n";
1681 $context->msg(
'rcfilters-legend-heading' )->parse() :
1682 $context->msg(
'recentchanges-legend-heading' )->parse();
1685 $collapsedState = $this->
getRequest()->getCookie(
'changeslist-state' );
1686 $collapsedClass = $collapsedState ===
'collapsed' ?
'mw-collapsed' :
'';
1688 $legend = Html::rawElement(
1690 [
'class' => [
'mw-changeslist-legend',
'mw-collapsible', $collapsedClass ] ],
1692 Html::rawElement(
'div', [
'class' =>
'mw-collapsible-content' ], $legend )
1704 $out->addModuleStyles( [
1705 'mediawiki.interface.helpers.styles',
1706 'mediawiki.special.changeslist.legend',
1707 'mediawiki.special.changeslist',
1709 $out->addModules(
'mediawiki.special.changeslist.legend.js' );
1712 $out->addModules(
'mediawiki.rcfilters.filters.ui' );
1713 $out->addModuleStyles(
'mediawiki.rcfilters.filters.base.styles' );
1738 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1743 if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1749 in_array(
'registered', $selectedExpLevels ) &&
1750 in_array(
'unregistered', $selectedExpLevels )
1757 in_array(
'registered', $selectedExpLevels ) &&
1758 !in_array(
'unregistered', $selectedExpLevels )
1760 $conds[] =
'actor_user IS NOT NULL';
1761 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
1765 if ( $selectedExpLevels === [
'unregistered' ] ) {
1766 $conds[
'actor_user'] =
null;
1767 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
1772 $join_conds[
'user'] = [
'LEFT JOIN',
'actor_user=user_id' ];
1777 $secondsPerDay = 86400;
1780 $now - $config->get( MainConfigNames::LearnerMemberSince ) * $secondsPerDay;
1781 $experiencedUserCutoff =
1782 $now - $config->get( MainConfigNames::ExperiencedUserMemberSince ) * $secondsPerDay;
1784 $aboveNewcomer =
$dbr->makeList(
1786 'user_editcount >= ' . intval( $config->get( MainConfigNames::LearnerEdits ) ),
1788 'user_registration IS NULL',
1789 'user_registration <= ' .
$dbr->addQuotes(
$dbr->timestamp( $learnerCutoff ) ),
1790 ], IDatabase::LIST_OR ),
1795 $aboveLearner =
$dbr->makeList(
1797 'user_editcount >= ' . intval( $config->get( MainConfigNames::ExperiencedUserEdits ) ),
1799 'user_registration IS NULL',
1800 'user_registration <= ' .
1801 $dbr->addQuotes(
$dbr->timestamp( $experiencedUserCutoff ) ),
1802 ], IDatabase::LIST_OR ),
1809 if ( in_array(
'unregistered', $selectedExpLevels ) ) {
1810 $selectedExpLevels = array_diff( $selectedExpLevels, [
'unregistered' ] );
1811 $conditions[
'actor_user'] =
null;
1812 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
1815 if ( $selectedExpLevels === [
'newcomer' ] ) {
1816 $conditions[] =
"NOT ( $aboveNewcomer )";
1817 } elseif ( $selectedExpLevels === [
'learner' ] ) {
1818 $conditions[] =
$dbr->makeList(
1819 [ $aboveNewcomer,
"NOT ( $aboveLearner )" ],
1822 } elseif ( $selectedExpLevels === [
'experienced' ] ) {
1823 $conditions[] = $aboveLearner;
1824 } elseif ( $selectedExpLevels === [
'learner',
'newcomer' ] ) {
1825 $conditions[] =
"NOT ( $aboveLearner )";
1826 } elseif ( $selectedExpLevels === [
'experienced',
'newcomer' ] ) {
1827 $conditions[] =
$dbr->makeList(
1828 [
"NOT ( $aboveNewcomer )", $aboveLearner ],
1831 } elseif ( $selectedExpLevels === [
'experienced',
'learner' ] ) {
1832 $conditions[] = $aboveNewcomer;
1833 } elseif ( $selectedExpLevels === [
'experienced',
'learner',
'newcomer' ] ) {
1834 $conditions[] =
'actor_user IS NOT NULL';
1835 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
1838 if ( count( $conditions ) > 1 ) {
1839 $conds[] =
$dbr->makeList( $conditions, IDatabase::LIST_OR );
1840 } elseif ( count( $conditions ) === 1 ) {
1841 $conds[] = reset( $conditions );
1851 if ( $this->
getRequest()->getBool(
'rcfilters' ) ) {
1855 return static::checkStructuredFilterUiEnabled( $this->
getUser() );
1866 return !MediaWikiServices::getInstance()
1867 ->getUserOptionsLookup()
1868 ->getOption( $user,
'rcenhancedfilters-disable' );
1879 return MediaWikiServices::getInstance()
1880 ->getUserOptionsLookup()
1893 return floatval( MediaWikiServices::getInstance()
1894 ->getUserOptionsLookup()
1934 private function expandSymbolicNamespaceFilters( array $namespaces ) {
1935 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1936 $symbolicFilters = [
1937 'all-contents' => $nsInfo->getSubjectNamespaces(),
1938 'all-discussions' => $nsInfo->getTalkNamespaces(),
1940 $additionalNamespaces = [];
1941 foreach ( $symbolicFilters as $name => $values ) {
1942 if ( in_array( $name, $namespaces ) ) {
1943 $additionalNamespaces = array_merge( $additionalNamespaces, $values );
1946 $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) );
1947 $namespaces = array_merge( $namespaces, $additionalNamespaces );
1948 return array_unique( $namespaces );
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.
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)
displaysOnUnstructuredUi()
Checks whether the filter should display on the unstructured UI.bool Whether to display
Represents a filter group (used on ChangesListSpecialPage and descendants)
Special page which uses a ChangesList to show query results.
getSavedQueriesPreferenceName()
Preference name for saved queries.
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.
getDefaultDaysPreferenceName()
Preference name for 'days'.
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.
static getRcFiltersConfigSummary(RL\Context $context)
Get essential data about getRcFiltersConfigVars() for change detection.
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.
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.
getExtraOptions( $opts)
Get options to be displayed in a form.
setup( $parameters)
Register all the filters, including legacy hook-driven ones.
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.
getDefaultDays()
Get the default value of the number of days to display when loading the result set.
outputNoResults()
Add the "no results" message to the output.
static getRcFiltersConfigVars(RL\Context $context)
Get config vars to export with the mediawiki.rcfilters.filters.ui module.
getFilterGroups()
Gets the currently registered filters groups.
registerFilterGroup(ChangesListFilterGroup $group)
Register a structured changes list filter group.
addModules()
Add page-specific modules.
__construct( $name, $restriction)
getLegacyShowHideFilters()
outputTimeout()
Add the "timeout" message to the output.
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
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.
getCollapsedPreferenceName()
Preference name for collapsing the active filter display.
static checkStructuredFilterUiEnabled(UserIdentity $user)
Static method to check whether StructuredFilter UI is enabled for the given user.
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.
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'.
outputChangesList( $rows, $opts)
Build and output the actual changes list.
getRows()
Get the database result for this special page instance.
includeRcFiltersApp()
Include the modules and configuration for the RCFilters app.
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.
A class containing constants representing the names of configuration variables.
Utility class for creating new RC entries.
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.
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.
Class for fixing stale WANObjectCache keys using a purge event source.
Interface for objects which can provide a MediaWiki context on request.
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s
if(!isset( $args[0])) $lang