62 private $filterGroupDefinitions;
68 private $legacyReviewStatusFilterGroupDefinition;
71 private $reviewStatusFilterGroupDefinition;
74 private $hideCategorizationFilterDefinition;
85 parent::__construct( $name, $restriction );
87 $nonRevisionTypes = [
RC_LOG ];
88 $this->
getHookRunner()->onSpecialWatchlistGetNonRevisionTypes( $nonRevisionTypes );
90 $this->filterGroupDefinitions = [
92 'name' =>
'registration',
93 'title' =>
'rcfilters-filtergroup-registration',
94 'class' => ChangesListBooleanFilterGroup::class,
100 'showHideSuffix' =>
'showhideliu',
102 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
103 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
105 $conds[
'actor_user'] =
null;
106 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
108 'isReplacedInStructuredUi' =>
true,
112 'name' =>
'hideanons',
115 'showHideSuffix' =>
'showhideanons',
117 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
118 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
120 $conds[] =
'actor_user IS NOT NULL';
121 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
123 'isReplacedInStructuredUi' =>
true,
129 'name' =>
'userExpLevel',
130 'title' =>
'rcfilters-filtergroup-user-experience-level',
131 'class' => ChangesListStringOptionsFilterGroup::class,
132 'isFullCoverage' =>
true,
135 'name' =>
'unregistered',
136 'label' =>
'rcfilters-filter-user-experience-level-unregistered-label',
137 'description' =>
'rcfilters-filter-user-experience-level-unregistered-description',
138 'cssClassSuffix' =>
'user-unregistered',
140 return !$rc->getAttribute(
'rc_user' );
144 'name' =>
'registered',
145 'label' =>
'rcfilters-filter-user-experience-level-registered-label',
146 'description' =>
'rcfilters-filter-user-experience-level-registered-description',
147 'cssClassSuffix' =>
'user-registered',
149 return $rc->getAttribute(
'rc_user' );
153 'name' =>
'newcomer',
154 'label' =>
'rcfilters-filter-user-experience-level-newcomer-label',
155 'description' =>
'rcfilters-filter-user-experience-level-newcomer-description',
156 'cssClassSuffix' =>
'user-newcomer',
158 $performer = $rc->getPerformerIdentity();
159 return $performer->isRegistered() &&
160 MediaWikiServices::getInstance()
162 ->newFromUserIdentity( $performer )
163 ->getExperienceLevel() ===
'newcomer';
168 'label' =>
'rcfilters-filter-user-experience-level-learner-label',
169 'description' =>
'rcfilters-filter-user-experience-level-learner-description',
170 'cssClassSuffix' =>
'user-learner',
172 $performer = $rc->getPerformerIdentity();
173 return $performer->isRegistered() &&
174 MediaWikiServices::getInstance()
176 ->newFromUserIdentity( $performer )
177 ->getExperienceLevel() ===
'learner';
181 'name' =>
'experienced',
182 'label' =>
'rcfilters-filter-user-experience-level-experienced-label',
183 'description' =>
'rcfilters-filter-user-experience-level-experienced-description',
184 'cssClassSuffix' =>
'user-experienced',
186 $performer = $rc->getPerformerIdentity();
187 return $performer->isRegistered() &&
188 MediaWikiServices::getInstance()
190 ->newFromUserIdentity( $performer )
191 ->getExperienceLevel() ===
'experienced';
196 'queryCallable' => [ $this,
'filterOnUserExperienceLevel' ],
200 'name' =>
'authorship',
201 'title' =>
'rcfilters-filtergroup-authorship',
202 'class' => ChangesListBooleanFilterGroup::class,
205 'name' =>
'hidemyself',
206 'label' =>
'rcfilters-filter-editsbyself-label',
207 'description' =>
'rcfilters-filter-editsbyself-description',
210 'showHideSuffix' =>
'showhidemine',
212 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
213 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
215 $user = $ctx->getUser();
216 $conds[] =
'actor_name<>' .
$dbr->addQuotes( $user->getName() );
217 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
219 'cssClassSuffix' =>
'self',
221 return $ctx->getUser()->equals( $rc->getPerformerIdentity() );
225 'name' =>
'hidebyothers',
226 'label' =>
'rcfilters-filter-editsbyother-label',
227 'description' =>
'rcfilters-filter-editsbyother-description',
229 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
230 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
232 $user = $ctx->getUser();
233 if ( $user->isAnon() ) {
234 $conds[
'actor_name'] = $user->getName();
236 $conds[
'actor_user'] = $user->getId();
238 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
240 'cssClassSuffix' =>
'others',
242 return !$ctx->getUser()->equals( $rc->getPerformerIdentity() );
249 'name' =>
'automated',
250 'title' =>
'rcfilters-filtergroup-automated',
251 'class' => ChangesListBooleanFilterGroup::class,
254 'name' =>
'hidebots',
255 'label' =>
'rcfilters-filter-bots-label',
256 'description' =>
'rcfilters-filter-bots-description',
259 'showHideSuffix' =>
'showhidebots',
261 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
262 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
264 $conds[
'rc_bot'] = 0;
266 'cssClassSuffix' =>
'bot',
268 return $rc->getAttribute(
'rc_bot' );
272 'name' =>
'hidehumans',
273 'label' =>
'rcfilters-filter-humans-label',
274 'description' =>
'rcfilters-filter-humans-description',
276 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
277 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
279 $conds[
'rc_bot'] = 1;
281 'cssClassSuffix' =>
'human',
283 return !$rc->getAttribute(
'rc_bot' );
292 'name' =>
'significance',
293 'title' =>
'rcfilters-filtergroup-significance',
294 'class' => ChangesListBooleanFilterGroup::class,
298 'name' =>
'hideminor',
299 'label' =>
'rcfilters-filter-minor-label',
300 'description' =>
'rcfilters-filter-minor-description',
303 'showHideSuffix' =>
'showhideminor',
305 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
306 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
308 $conds[] =
'rc_minor = 0';
310 'cssClassSuffix' =>
'minor',
312 return $rc->getAttribute(
'rc_minor' );
316 'name' =>
'hidemajor',
317 'label' =>
'rcfilters-filter-major-label',
318 'description' =>
'rcfilters-filter-major-description',
320 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
321 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
323 $conds[] =
'rc_minor = 1';
325 'cssClassSuffix' =>
'major',
327 return !$rc->getAttribute(
'rc_minor' );
334 'name' =>
'lastRevision',
335 'title' =>
'rcfilters-filtergroup-lastrevision',
336 'class' => ChangesListBooleanFilterGroup::class,
340 'name' =>
'hidelastrevision',
341 'label' =>
'rcfilters-filter-lastrevision-label',
342 'description' =>
'rcfilters-filter-lastrevision-description',
344 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
345 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
346 ) use ( $nonRevisionTypes ) {
347 $conds[] =
$dbr->makeList(
349 'rc_this_oldid <> page_latest',
350 'rc_type' => $nonRevisionTypes,
355 'cssClassSuffix' =>
'last',
357 return $rc->getAttribute(
'rc_this_oldid' ) === $rc->getAttribute(
'page_latest' );
361 'name' =>
'hidepreviousrevisions',
362 'label' =>
'rcfilters-filter-previousrevision-label',
363 'description' =>
'rcfilters-filter-previousrevision-description',
365 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
366 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
367 ) use ( $nonRevisionTypes ) {
368 $conds[] =
$dbr->makeList(
370 'rc_this_oldid = page_latest',
371 'rc_type' => $nonRevisionTypes,
376 'cssClassSuffix' =>
'previous',
378 return $rc->getAttribute(
'rc_this_oldid' ) !== $rc->getAttribute(
'page_latest' );
386 'name' =>
'changeType',
387 'title' =>
'rcfilters-filtergroup-changetype',
388 'class' => ChangesListBooleanFilterGroup::class,
392 'name' =>
'hidepageedits',
393 'label' =>
'rcfilters-filter-pageedits-label',
394 'description' =>
'rcfilters-filter-pageedits-description',
397 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
398 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
400 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_EDIT );
402 'cssClassSuffix' =>
'src-mw-edit',
408 'name' =>
'hidenewpages',
409 'label' =>
'rcfilters-filter-newpages-label',
410 'description' =>
'rcfilters-filter-newpages-description',
413 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
414 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
416 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_NEW );
418 'cssClassSuffix' =>
'src-mw-new',
428 'label' =>
'rcfilters-filter-logactions-label',
429 'description' =>
'rcfilters-filter-logactions-description',
432 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
433 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
435 $conds[] =
'rc_type != ' .
$dbr->addQuotes(
RC_LOG );
437 'cssClassSuffix' =>
'src-mw-log',
443 'name' =>
'hidenewuserlog',
444 'label' =>
'rcfilters-filter-newuserlogactions-label',
445 'description' =>
'rcfilters-filter-newuserlogactions-description',
448 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
449 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
451 $conds[] =
$dbr->makeList(
453 'rc_log_type != ' .
$dbr->addQuotes(
'newusers' ),
454 'rc_log_type' => null
459 'cssClassSuffix' =>
'src-mw-newuserlog',
461 return $rc->getAttribute(
'rc_log_type' ) ===
"newusers";
469 $this->legacyReviewStatusFilterGroupDefinition = [
471 'name' =>
'legacyReviewStatus',
472 'title' =>
'rcfilters-filtergroup-reviewstatus',
473 'class' => ChangesListBooleanFilterGroup::class,
476 'name' =>
'hidepatrolled',
479 'showHideSuffix' =>
'showhidepatr',
481 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
482 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
486 'isReplacedInStructuredUi' =>
true,
489 'name' =>
'hideunpatrolled',
491 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
492 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
496 'isReplacedInStructuredUi' =>
true,
502 $this->reviewStatusFilterGroupDefinition = [
504 'name' =>
'reviewStatus',
505 'title' =>
'rcfilters-filtergroup-reviewstatus',
506 'class' => ChangesListStringOptionsFilterGroup::class,
507 'isFullCoverage' =>
true,
511 'name' =>
'unpatrolled',
512 'label' =>
'rcfilters-filter-reviewstatus-unpatrolled-label',
513 'description' =>
'rcfilters-filter-reviewstatus-unpatrolled-description',
514 'cssClassSuffix' =>
'reviewstatus-unpatrolled',
521 'label' =>
'rcfilters-filter-reviewstatus-manual-label',
522 'description' =>
'rcfilters-filter-reviewstatus-manual-description',
523 'cssClassSuffix' =>
'reviewstatus-manual',
530 'label' =>
'rcfilters-filter-reviewstatus-auto-label',
531 'description' =>
'rcfilters-filter-reviewstatus-auto-description',
532 'cssClassSuffix' =>
'reviewstatus-auto',
539 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
540 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
542 if ( $selected === [] ) {
545 $rcPatrolledValues = [
551 $conds[
'rc_patrolled'] = array_map(
static function (
$s ) use ( $rcPatrolledValues ) {
552 return $rcPatrolledValues[
$s ];
558 $this->hideCategorizationFilterDefinition = [
559 'name' =>
'hidecategorization',
560 'label' =>
'rcfilters-filter-categorization-label',
561 'description' =>
'rcfilters-filter-categorization-description',
564 'showHideSuffix' =>
'showhidecategorization',
567 'queryCallable' =>
static function (
string $specialClassName,
IContextSource $ctx,
568 IDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
572 'cssClassSuffix' =>
'src-mw-categorize',
587 if ( $group->getConflictingGroups() ) {
590 " specifies conflicts with other groups but these are not supported yet."
594 foreach ( $group->getConflictingFilters() as $conflictingFilter ) {
595 if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
600 foreach ( $group->getFilters() as $filter ) {
601 foreach ( $filter->getConflictingFilters() as $conflictingFilter ) {
603 $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
604 $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
621 $this->rcSubpage = $subpage;
626 if ( $this->
getConfig()->
get( MainConfigNames::WatchlistExpiry ) ) {
628 $this->
getOutput()->addModules(
'mediawiki.special.changeslist.watchlistexpiry' );
634 if ( $rows ===
false ) {
639 if ( $this->
getRequest()->getRawVal(
'action' ) ===
'render' ) {
640 $this->
getOutput()->setArticleBodyOnly(
true );
645 if ( $this->
getRequest()->getBool(
'peek' ) ) {
646 $code = $rows->numRows() > 0 ? 200 : 204;
647 $this->
getOutput()->setStatusCode( $code );
649 if ( $this->
getUser()->isAnon() !==
652 $this->
getOutput()->setStatusCode( 205 );
658 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
659 $batch = $linkBatchFactory->newLinkBatch();
660 foreach ( $rows as $row ) {
661 $batch->add(
NS_USER, $row->rc_user_text );
663 $batch->add( $row->rc_namespace, $row->rc_title );
666 foreach ( $formatter->getPreloadTitles() as
$title ) {
686 $this->
getOutput()->setStatusCode( 500 );
687 $this->webOutputHeader( 0, $opts );
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 = MediaWikiServices::getInstance()
723 ->getUserOptionsLookup()
729 if ( $savedQueries && isset( $savedQueries[
'default' ] ) ) {
732 if ( isset( $savedQueries[
'version' ] ) && $savedQueries[
'version' ] ===
'2' ) {
733 $savedQueryDefaultID = $savedQueries[
'default' ];
734 $defaultQuery = $savedQueries[
'queries' ][ $savedQueryDefaultID ][
'data' ];
737 $query = array_merge(
738 $defaultQuery[
'params' ],
739 $defaultQuery[
'highlights' ],
746 $query = array_merge( $this->
getRequest()->getValues(), $query );
747 unset( $query[
'title' ] );
755 'wgStructuredChangeFiltersDefaultSavedQueryExists',
761 $this->
getOutput()->addBodyClasses(
'mw-rcfilters-ui-loading' );
772 $linkDays = $this->
getConfig()->get( MainConfigNames::RCLinkDays );
773 $filterByAge = $this->
getConfig()->get( MainConfigNames::RCFilterByAge );
774 $maxAge = $this->
getConfig()->get( MainConfigNames::RCMaxAge );
775 if ( $filterByAge ) {
781 $maxAgeDays = $maxAge / ( 3600 * 24 );
782 foreach ( $linkDays as $i => $days ) {
783 if ( $days >= $maxAgeDays ) {
784 array_splice( $linkDays, $i + 1 );
804 foreach ( $jsData[
'messageKeys'] as $key ) {
805 $messages[$key] = $this->
msg( $key )->plain();
808 $out->addBodyClasses(
'mw-rcfilters-enabled' );
809 $collapsed = MediaWikiServices::getInstance()->getUserOptionsLookup()
812 $out->addBodyClasses(
'mw-rcfilters-collapsed' );
816 $out->addJsConfigVars(
'wgStructuredChangeFilters', $jsData[
'groups'] );
817 $out->addJsConfigVars(
'wgStructuredChangeFiltersMessages', $messages );
818 $out->addJsConfigVars(
'wgStructuredChangeFiltersCollapsedState', $collapsed );
820 $out->addJsConfigVars(
821 'StructuredChangeFiltersDisplayConfig',
824 (
int)$this->
getConfig()->
get( MainConfigNames::RCMaxAge ) / ( 24 * 3600 ),
825 'limitArray' => $this->
getConfig()->
get( MainConfigNames::RCLinkLimits ),
832 $out->addJsConfigVars(
833 'wgStructuredChangeFiltersSavedQueriesPreferenceName',
836 $out->addJsConfigVars(
837 'wgStructuredChangeFiltersLimitPreferenceName',
840 $out->addJsConfigVars(
841 'wgStructuredChangeFiltersDaysPreferenceName',
844 $out->addJsConfigVars(
845 'wgStructuredChangeFiltersCollapsedPreferenceName',
849 $out->addBodyClasses(
'mw-rcfilters-disabled' );
862 $lang = MediaWikiServices::getInstance()->getLanguageFactory()
863 ->getLanguage( $context->getLanguage() );
867 'StructuredChangeFiltersEditWatchlistUrl' =>
880 $lang = MediaWikiServices::getInstance()->getLanguageFactory()
881 ->getLanguage( $context->getLanguage() );
884 'StructuredChangeFiltersEditWatchlistUrl' =>
896 [
'class' =>
'mw-changeslist-empty' ],
897 $this->
msg(
'recentchanges-noresult' )->parse()
907 '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
908 $this->
msg(
'recentchanges-timeout' )->parse() .
926 $this->
buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
928 return $this->
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
937 if ( $this->rcOptions ===
null ) {
938 $this->rcOptions = $this->
setup( $this->rcSubpage );
966 if ( $this->
getConfig()->
get( MainConfigNames::RCWatchCategoryMembership ) ) {
968 $this->hideCategorizationFilterDefinition
971 $transformedHideCategorizationDef[
'group'] = $changeTypeGroup;
974 $transformedHideCategorizationDef
978 $this->
getHookRunner()->onChangesListSpecialPageStructuredFilters( $this );
983 $registered = $userExperienceLevel->getFilter(
'registered' );
984 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'newcomer' ) );
985 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'learner' ) );
986 $registered->setAsSupersetOf( $userExperienceLevel->getFilter(
'experienced' ) );
988 $categoryFilter = $changeTypeGroup->getFilter(
'hidecategorization' );
989 $logactionsFilter = $changeTypeGroup->getFilter(
'hidelog' );
990 $lognewuserFilter = $changeTypeGroup->getFilter(
'hidenewuserlog' );
991 $pagecreationFilter = $changeTypeGroup->getFilter(
'hidenewpages' );
994 $hideMinorFilter = $significanceTypeGroup->getFilter(
'hideminor' );
997 if ( $categoryFilter !==
null ) {
998 $hideMinorFilter->conflictsWith(
1000 'rcfilters-hideminor-conflicts-typeofchange-global',
1001 'rcfilters-hideminor-conflicts-typeofchange',
1002 'rcfilters-typeofchange-conflicts-hideminor'
1005 $hideMinorFilter->conflictsWith(
1007 'rcfilters-hideminor-conflicts-typeofchange-global',
1008 'rcfilters-hideminor-conflicts-typeofchange',
1009 'rcfilters-typeofchange-conflicts-hideminor'
1011 $hideMinorFilter->conflictsWith(
1013 'rcfilters-hideminor-conflicts-typeofchange-global',
1014 'rcfilters-hideminor-conflicts-typeofchange',
1015 'rcfilters-typeofchange-conflicts-hideminor'
1017 $hideMinorFilter->conflictsWith(
1018 $pagecreationFilter,
1019 'rcfilters-hideminor-conflicts-typeofchange-global',
1020 'rcfilters-hideminor-conflicts-typeofchange',
1021 'rcfilters-typeofchange-conflicts-hideminor'
1035 return $filterDefinition;
1049 $autoFillPriority = -1;
1050 foreach ( $definition as $groupDefinition ) {
1051 if ( !isset( $groupDefinition[
'priority'] ) ) {
1052 $groupDefinition[
'priority'] = $autoFillPriority;
1055 $autoFillPriority = $groupDefinition[
'priority'];
1058 $autoFillPriority--;
1060 $className = $groupDefinition[
'class'];
1061 unset( $groupDefinition[
'class'] );
1063 foreach ( $groupDefinition[
'filters'] as &$filterDefinition ) {
1076 foreach ( $this->filterGroups as $group ) {
1078 foreach ( $group->getFilters() as $key => $filter ) {
1080 $filters[ $key ] = $filter;
1104 if ( $parameters !==
null ) {
1126 $useDefaults = $this->
getRequest()->getInt(
'urlversion' ) !== 2;
1129 foreach ( $this->filterGroups as $filterGroup ) {
1130 $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1135 $opts->
add(
'invert',
false );
1136 $opts->
add(
'associated',
false );
1137 $opts->
add(
'urlversion', 1 );
1138 $opts->
add(
'tagfilter',
'' );
1139 $opts->
add(
'inverttags',
false );
1144 $opts->
add(
'from',
'' );
1155 $groupName = $group->
getName();
1157 $this->filterGroups[$groupName] = $group;
1177 return $this->filterGroups[$groupName] ??
null;
1196 'messageKeys' => [],
1203 foreach ( $this->filterGroups as $group ) {
1204 $groupOutput = $group->getJsData();
1205 if ( $groupOutput !==
null ) {
1206 $output[
'messageKeys'] = array_merge(
1207 $output[
'messageKeys'],
1208 $groupOutput[
'messageKeys']
1211 unset( $groupOutput[
'messageKeys'] );
1212 $output[
'groups'][] = $groupOutput;
1240 $stringParameterNameSet = [];
1241 $hideParameterNameSet = [];
1246 foreach ( $this->filterGroups as $filterGroup ) {
1248 $stringParameterNameSet[$filterGroup->getName()] =
true;
1250 foreach ( $filterGroup->getFilters() as $filter ) {
1251 $hideParameterNameSet[$filter->getName()] =
true;
1256 $bits = preg_split(
'/\s*,\s*/', trim( $par ) );
1257 foreach ( $bits as $bit ) {
1259 if ( isset( $hideParameterNameSet[$bit] ) ) {
1262 } elseif ( isset( $hideParameterNameSet[
"hide$bit"] ) ) {
1264 $opts[
"hide$bit"] =
false;
1265 } elseif ( preg_match(
'/^(.*)=(.*)$/', $bit, $m ) ) {
1266 if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1267 $opts[$m[1]] = $m[2];
1279 $isContradictory = $this->fixContradictoryOptions( $opts );
1282 if ( $isContradictory || $isReplaced ) {
1289 $this->
getConfig()->
get( MainConfigNames::RCMaxAge ) / ( 3600 * 24 ) );
1298 private function fixContradictoryOptions(
FormOptions $opts ) {
1299 $fixed = $this->fixBackwardsCompatibilityOptions( $opts );
1301 foreach ( $this->filterGroups as $filterGroup ) {
1303 $filters = $filterGroup->getFilters();
1305 if ( count( $filters ) === 1 ) {
1310 $allInGroupEnabled = array_reduce(
1313 return $carry && $opts[ $filter->
getName() ];
1315 count( $filters ) > 0
1318 if ( $allInGroupEnabled ) {
1319 foreach ( $filters as $filter ) {
1320 $opts[ $filter->getName() ] =
false;
1340 private function fixBackwardsCompatibilityOptions(
FormOptions $opts ) {
1341 if ( $opts[
'hideanons'] && $opts[
'hideliu'] ) {
1342 $opts->
reset(
'hideanons' );
1343 if ( !$opts[
'hidebots'] ) {
1344 $opts->
reset(
'hideliu' );
1345 $opts[
'hidehumans'] = 1;
1369 if ( $opts[
'hideanons' ] ) {
1370 $opts->
reset(
'hideanons' );
1371 $opts[
'userExpLevel' ] =
'registered';
1375 if ( $opts[
'hideliu' ] ) {
1376 $opts->
reset(
'hideliu' );
1377 $opts[
'userExpLevel' ] =
'unregistered';
1382 if ( $opts[
'hidepatrolled' ] ) {
1383 $opts->
reset(
'hidepatrolled' );
1384 $opts[
'reviewStatus' ] =
'unpatrolled';
1388 if ( $opts[
'hideunpatrolled' ] ) {
1389 $opts->
reset(
'hideunpatrolled' );
1390 $opts[
'reviewStatus' ] = implode(
1392 [
'manual',
'auto' ]
1410 foreach ( $params as &$value ) {
1411 if ( $value ===
false ) {
1430 protected function buildQuery( &$tables, &$fields, &$conds, &$query_options,
1437 foreach ( $this->filterGroups as $filterGroup ) {
1438 $filterGroup->modifyQuery(
$dbr, $this, $tables, $fields, $conds,
1439 $query_options, $join_conds, $opts, $isStructuredUI );
1443 if ( $opts[
'namespace' ] !==
'' ) {
1444 $namespaces = explode(
';', $opts[
'namespace' ] );
1446 $namespaces = $this->expandSymbolicNamespaceFilters( $namespaces );
1448 $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1449 $namespaces = array_filter( $namespaces, [ $namespaceInfo,
'exists' ] );
1451 if ( $namespaces !== [] ) {
1453 $namespaces = array_map(
'intval', $namespaces );
1455 if ( $opts[
'associated' ] ) {
1456 $associatedNamespaces = array_map(
1457 [ $namespaceInfo,
'getAssociated' ],
1458 array_filter( $namespaces, [ $namespaceInfo,
'hasTalkNamespace' ] )
1460 $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
1463 if ( count( $namespaces ) === 1 ) {
1464 $operator = $opts[
'invert' ] ?
'!=' :
'=';
1465 $value =
$dbr->addQuotes( reset( $namespaces ) );
1467 $operator = $opts[
'invert' ] ?
'NOT IN' :
'IN';
1468 sort( $namespaces );
1469 $value =
'(' .
$dbr->makeList( $namespaces ) .
')';
1471 $conds[] =
"rc_namespace $operator $value";
1476 $cutoff_unixtime = time() - $opts[
'days'] * 3600 * 24;
1477 $cutoff =
$dbr->timestamp( $cutoff_unixtime );
1479 $fromValid = preg_match(
'/^[0-9]{14}$/', $opts[
'from'] );
1480 if ( $fromValid && $opts[
'from'] >
wfTimestamp( TS_MW, $cutoff ) ) {
1481 $cutoff =
$dbr->timestamp( $opts[
'from'] );
1483 $opts->
reset(
'from' );
1486 $conds[] =
'rc_timestamp >= ' .
$dbr->addQuotes( $cutoff );
1504 $tables = array_merge( $tables, $rcQuery[
'tables'] );
1505 $fields = array_merge( $rcQuery[
'fields'], $fields );
1506 $join_conds = array_merge( $join_conds, $rcQuery[
'joins'] );
1515 $opts[
'inverttags' ]
1519 !$this->
runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts )
1526 return $dbr->select(
1537 &$query_options, &$join_conds, $opts
1539 return $this->
getHookRunner()->onChangesListSpecialPageQuery(
1540 $this->
getName(), $tables, $fields, $conds, $query_options, $join_conds, $opts );
1558 private function webOutputHeader( $rowCount, $opts ) {
1561 $this->
doHeader( $opts, $rowCount );
1572 $this->webOutputHeader( $rows->numRows(), $opts );
1645 $user = $context->getUser();
1646 # The legend showing what the letters and stuff mean
1648 # Iterates through them and gets the messages for both letter and tooltip
1649 $legendItems = $context->getConfig()->get( MainConfigNames::RecentChangesFlags );
1650 if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
1651 unset( $legendItems[
'unpatrolled'] );
1653 foreach ( $legendItems as $key => $item ) { # generate items of the legend
1654 $label = $item[
'legend'] ?? $item[
'title'];
1655 $letter = $item[
'letter'];
1656 $cssClass = $item[
'class'] ?? $key;
1659 [
'class' => $cssClass ], $context->msg( $letter )->text()
1663 $context->msg( $label )->parse()
1668 [
'class' =>
'mw-plusminus-pos' ],
1669 $context->msg(
'recentchanges-legend-plusminus' )->parse()
1673 [
'class' =>
'mw-changeslist-legend-plusminus' ],
1674 $context->msg(
'recentchanges-label-plusminus' )->text()
1677 if ( $context->getConfig()->get( MainConfigNames::WatchlistExpiry ) ) {
1678 $widget =
new IconWidget( [
1680 'classes' => [
'mw-changesList-watchlistExpiry' ],
1683 $watchlistLabelId =
'mw-changeslist-watchlistExpiry-label';
1684 $widget->getIconElement()->setAttributes( [
1686 'aria-labelledby' => $watchlistLabelId,
1690 [
'class' =>
'mw-changeslist-legend-watchlistexpiry' ],
1695 [
'class' =>
'mw-changeslist-legend-watchlistexpiry',
'id' => $watchlistLabelId ],
1696 $context->msg(
'recentchanges-legend-watchlistexpiry' )->text()
1702 $context->msg(
'rcfilters-legend-heading' )->parse() :
1703 $context->msg(
'recentchanges-legend-heading' )->parse();
1706 $collapsedState = $this->
getRequest()->getCookie(
'changeslist-state' );
1707 $collapsedClass = $collapsedState ===
'collapsed' ?
'mw-collapsed' :
'';
1711 [
'class' => [
'mw-changeslist-legend',
'mw-collapsible', $collapsedClass ] ],
1713 Html::rawElement(
'div', [
'class' =>
'mw-collapsible-content' ], $legend )
1725 $out->addModuleStyles( [
1726 'mediawiki.interface.helpers.styles',
1727 'mediawiki.special.changeslist.legend',
1728 'mediawiki.special.changeslist',
1730 $out->addModules(
'mediawiki.special.changeslist.legend.js' );
1733 $out->addModules(
'mediawiki.rcfilters.filters.ui' );
1734 $out->addModuleStyles(
'mediawiki.rcfilters.filters.base.styles' );
1759 &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1764 if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1770 in_array(
'registered', $selectedExpLevels ) &&
1771 in_array(
'unregistered', $selectedExpLevels )
1778 in_array(
'registered', $selectedExpLevels ) &&
1779 !in_array(
'unregistered', $selectedExpLevels )
1781 $conds[] =
'actor_user IS NOT NULL';
1782 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
1786 if ( $selectedExpLevels === [
'unregistered' ] ) {
1787 $conds[
'actor_user'] =
null;
1788 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
1793 $join_conds[
'user'] = [
'LEFT JOIN',
'actor_user=user_id' ];
1798 $secondsPerDay = 86400;
1801 $now - $config->get( MainConfigNames::LearnerMemberSince ) * $secondsPerDay;
1802 $experiencedUserCutoff =
1803 $now - $config->get( MainConfigNames::ExperiencedUserMemberSince ) * $secondsPerDay;
1805 $aboveNewcomer =
$dbr->makeList(
1807 'user_editcount >= ' . intval( $config->get( MainConfigNames::LearnerEdits ) ),
1809 'user_registration IS NULL',
1810 'user_registration <= ' .
$dbr->addQuotes(
$dbr->timestamp( $learnerCutoff ) ),
1816 $aboveLearner =
$dbr->makeList(
1818 'user_editcount >= ' . intval( $config->get( MainConfigNames::ExperiencedUserEdits ) ),
1820 'user_registration IS NULL',
1821 'user_registration <= ' .
1822 $dbr->addQuotes(
$dbr->timestamp( $experiencedUserCutoff ) ),
1830 if ( in_array(
'unregistered', $selectedExpLevels ) ) {
1831 $selectedExpLevels = array_diff( $selectedExpLevels, [
'unregistered' ] );
1832 $conditions[
'actor_user'] =
null;
1833 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
1836 if ( $selectedExpLevels === [
'newcomer' ] ) {
1837 $conditions[] =
"NOT ( $aboveNewcomer )";
1838 } elseif ( $selectedExpLevels === [
'learner' ] ) {
1839 $conditions[] =
$dbr->makeList(
1840 [ $aboveNewcomer,
"NOT ( $aboveLearner )" ],
1843 } elseif ( $selectedExpLevels === [
'experienced' ] ) {
1844 $conditions[] = $aboveLearner;
1845 } elseif ( $selectedExpLevels === [
'learner',
'newcomer' ] ) {
1846 $conditions[] =
"NOT ( $aboveLearner )";
1847 } elseif ( $selectedExpLevels === [
'experienced',
'newcomer' ] ) {
1848 $conditions[] =
$dbr->makeList(
1849 [
"NOT ( $aboveNewcomer )", $aboveLearner ],
1852 } elseif ( $selectedExpLevels === [
'experienced',
'learner' ] ) {
1853 $conditions[] = $aboveNewcomer;
1854 } elseif ( $selectedExpLevels === [
'experienced',
'learner',
'newcomer' ] ) {
1855 $conditions[] =
'actor_user IS NOT NULL';
1856 $join_conds[
'recentchanges_actor'] = [
'JOIN',
'actor_id=rc_actor' ];
1859 if ( count( $conditions ) > 1 ) {
1861 } elseif ( count( $conditions ) === 1 ) {
1862 $conds[] = reset( $conditions );
1872 if ( $this->
getRequest()->getBool(
'rcfilters' ) ) {
1876 return static::checkStructuredFilterUiEnabled( $this->
getUser() );
1887 return !MediaWikiServices::getInstance()
1888 ->getUserOptionsLookup()
1889 ->getOption( $user,
'rcenhancedfilters-disable' );
1900 return MediaWikiServices::getInstance()
1901 ->getUserOptionsLookup()
1914 return floatval( MediaWikiServices::getInstance()
1915 ->getUserOptionsLookup()
1955 private function expandSymbolicNamespaceFilters( array $namespaces ) {
1956 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1957 $symbolicFilters = [
1958 'all-contents' => $nsInfo->getSubjectNamespaces(),
1959 'all-discussions' => $nsInfo->getTalkNamespaces(),
1961 $additionalNamespaces = [];
1962 foreach ( $symbolicFilters as $name => $values ) {
1963 if ( in_array( $name, $namespaces ) ) {
1964 $additionalNamespaces = array_merge( $additionalNamespaces, $values );
1967 $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) );
1968 $namespaces = array_merge( $namespaces, $additionalNamespaces );
1969 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.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
static closeElement( $element)
Returns "</$element>".
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
A class containing constants representing the names of configuration variables.
Utility class for creating new RC entries.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
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,...
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
including( $x=null)
Whether the special page is being evaluated via transclusion.
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