MediaWiki  master
ChangesListSpecialPage.php
Go to the documentation of this file.
1 <?php
26 use OOUI\IconWidget;
31 
38 abstract class ChangesListSpecialPage extends SpecialPage {
43  private const TAG_DESC_CHARACTER_LIMIT = 120;
44 
49  protected static $savedQueriesPreferenceName;
50 
55  protected static $daysPreferenceName;
56 
61  protected static $limitPreferenceName;
62 
67  protected static $collapsedPreferenceName;
68 
70  protected $rcSubpage;
71 
73  protected $rcOptions;
74 
75  // Order of both groups and filters is significant; first is top-most priority,
76  // descending from there.
77  // 'showHideSuffix' is a shortcut to and avoid spelling out
78  // details specific to subclasses here.
92 
98 
101 
104 
111  protected $filterGroups = [];
112 
113  public function __construct( $name, $restriction ) {
114  parent::__construct( $name, $restriction );
115 
116  $nonRevisionTypes = [ RC_LOG ];
117  $this->getHookRunner()->onSpecialWatchlistGetNonRevisionTypes( $nonRevisionTypes );
118 
119  $this->filterGroupDefinitions = [
120  [
121  'name' => 'registration',
122  'title' => 'rcfilters-filtergroup-registration',
123  'class' => ChangesListBooleanFilterGroup::class,
124  'filters' => [
125  [
126  'name' => 'hideliu',
127  // rcshowhideliu-show, rcshowhideliu-hide,
128  // wlshowhideliu
129  'showHideSuffix' => 'showhideliu',
130  'default' => false,
131  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
132  &$query_options, &$join_conds
133  ) {
134  $actorMigration = ActorMigration::newMigration();
135  $actorQuery = $actorMigration->getJoin( 'rc_user' );
136  $tables += $actorQuery['tables'];
137  $join_conds += $actorQuery['joins'];
138  $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
139  },
140  'isReplacedInStructuredUi' => true,
141 
142  ],
143  [
144  'name' => 'hideanons',
145  // rcshowhideanons-show, rcshowhideanons-hide,
146  // wlshowhideanons
147  'showHideSuffix' => 'showhideanons',
148  'default' => false,
149  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
150  &$query_options, &$join_conds
151  ) {
152  $actorMigration = ActorMigration::newMigration();
153  $actorQuery = $actorMigration->getJoin( 'rc_user' );
154  $tables += $actorQuery['tables'];
155  $join_conds += $actorQuery['joins'];
156  $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
157  },
158  'isReplacedInStructuredUi' => true,
159  ]
160  ],
161  ],
162 
163  [
164  'name' => 'userExpLevel',
165  'title' => 'rcfilters-filtergroup-user-experience-level',
166  'class' => ChangesListStringOptionsFilterGroup::class,
167  'isFullCoverage' => true,
168  'filters' => [
169  [
170  'name' => 'unregistered',
171  'label' => 'rcfilters-filter-user-experience-level-unregistered-label',
172  'description' => 'rcfilters-filter-user-experience-level-unregistered-description',
173  'cssClassSuffix' => 'user-unregistered',
174  'isRowApplicableCallable' => function ( $ctx, $rc ) {
175  return !$rc->getAttribute( 'rc_user' );
176  }
177  ],
178  [
179  'name' => 'registered',
180  'label' => 'rcfilters-filter-user-experience-level-registered-label',
181  'description' => 'rcfilters-filter-user-experience-level-registered-description',
182  'cssClassSuffix' => 'user-registered',
183  'isRowApplicableCallable' => function ( $ctx, $rc ) {
184  return $rc->getAttribute( 'rc_user' );
185  }
186  ],
187  [
188  'name' => 'newcomer',
189  'label' => 'rcfilters-filter-user-experience-level-newcomer-label',
190  'description' => 'rcfilters-filter-user-experience-level-newcomer-description',
191  'cssClassSuffix' => 'user-newcomer',
192  'isRowApplicableCallable' => function ( $ctx, $rc ) {
193  $performer = $rc->getPerformer();
194  return $performer && $performer->isRegistered() &&
195  $performer->getExperienceLevel() === 'newcomer';
196  }
197  ],
198  [
199  'name' => 'learner',
200  'label' => 'rcfilters-filter-user-experience-level-learner-label',
201  'description' => 'rcfilters-filter-user-experience-level-learner-description',
202  'cssClassSuffix' => 'user-learner',
203  'isRowApplicableCallable' => function ( $ctx, $rc ) {
204  $performer = $rc->getPerformer();
205  return $performer && $performer->isRegistered() &&
206  $performer->getExperienceLevel() === 'learner';
207  },
208  ],
209  [
210  'name' => 'experienced',
211  'label' => 'rcfilters-filter-user-experience-level-experienced-label',
212  'description' => 'rcfilters-filter-user-experience-level-experienced-description',
213  'cssClassSuffix' => 'user-experienced',
214  'isRowApplicableCallable' => function ( $ctx, $rc ) {
215  $performer = $rc->getPerformer();
216  return $performer && $performer->isRegistered() &&
217  $performer->getExperienceLevel() === 'experienced';
218  },
219  ]
220  ],
222  'queryCallable' => [ $this, 'filterOnUserExperienceLevel' ],
223  ],
224 
225  [
226  'name' => 'authorship',
227  'title' => 'rcfilters-filtergroup-authorship',
228  'class' => ChangesListBooleanFilterGroup::class,
229  'filters' => [
230  [
231  'name' => 'hidemyself',
232  'label' => 'rcfilters-filter-editsbyself-label',
233  'description' => 'rcfilters-filter-editsbyself-description',
234  // rcshowhidemine-show, rcshowhidemine-hide,
235  // wlshowhidemine
236  'showHideSuffix' => 'showhidemine',
237  'default' => false,
238  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
239  &$query_options, &$join_conds
240  ) {
241  $actorQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $ctx->getUser() );
242  $tables += $actorQuery['tables'];
243  $join_conds += $actorQuery['joins'];
244  $conds[] = 'NOT(' . $actorQuery['conds'] . ')';
245  },
246  'cssClassSuffix' => 'self',
247  'isRowApplicableCallable' => function ( $ctx, $rc ) {
248  return $ctx->getUser()->equals( $rc->getPerformer() );
249  },
250  ],
251  [
252  'name' => 'hidebyothers',
253  'label' => 'rcfilters-filter-editsbyother-label',
254  'description' => 'rcfilters-filter-editsbyother-description',
255  'default' => false,
256  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
257  &$query_options, &$join_conds
258  ) {
259  $actorQuery = ActorMigration::newMigration()
260  ->getWhere( $dbr, 'rc_user', $ctx->getUser(), false );
261  $tables += $actorQuery['tables'];
262  $join_conds += $actorQuery['joins'];
263  $conds[] = $actorQuery['conds'];
264  },
265  'cssClassSuffix' => 'others',
266  'isRowApplicableCallable' => function ( $ctx, $rc ) {
267  return !$ctx->getUser()->equals( $rc->getPerformer() );
268  },
269  ]
270  ]
271  ],
272 
273  [
274  'name' => 'automated',
275  'title' => 'rcfilters-filtergroup-automated',
276  'class' => ChangesListBooleanFilterGroup::class,
277  'filters' => [
278  [
279  'name' => 'hidebots',
280  'label' => 'rcfilters-filter-bots-label',
281  'description' => 'rcfilters-filter-bots-description',
282  // rcshowhidebots-show, rcshowhidebots-hide,
283  // wlshowhidebots
284  'showHideSuffix' => 'showhidebots',
285  'default' => false,
286  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
287  &$query_options, &$join_conds
288  ) {
289  $conds['rc_bot'] = 0;
290  },
291  'cssClassSuffix' => 'bot',
292  'isRowApplicableCallable' => function ( $ctx, $rc ) {
293  return $rc->getAttribute( 'rc_bot' );
294  },
295  ],
296  [
297  'name' => 'hidehumans',
298  'label' => 'rcfilters-filter-humans-label',
299  'description' => 'rcfilters-filter-humans-description',
300  'default' => false,
301  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
302  &$query_options, &$join_conds
303  ) {
304  $conds['rc_bot'] = 1;
305  },
306  'cssClassSuffix' => 'human',
307  'isRowApplicableCallable' => function ( $ctx, $rc ) {
308  return !$rc->getAttribute( 'rc_bot' );
309  },
310  ]
311  ]
312  ],
313 
314  // significance (conditional)
315 
316  [
317  'name' => 'significance',
318  'title' => 'rcfilters-filtergroup-significance',
319  'class' => ChangesListBooleanFilterGroup::class,
320  'priority' => -6,
321  'filters' => [
322  [
323  'name' => 'hideminor',
324  'label' => 'rcfilters-filter-minor-label',
325  'description' => 'rcfilters-filter-minor-description',
326  // rcshowhideminor-show, rcshowhideminor-hide,
327  // wlshowhideminor
328  'showHideSuffix' => 'showhideminor',
329  'default' => false,
330  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
331  &$query_options, &$join_conds
332  ) {
333  $conds[] = 'rc_minor = 0';
334  },
335  'cssClassSuffix' => 'minor',
336  'isRowApplicableCallable' => function ( $ctx, $rc ) {
337  return $rc->getAttribute( 'rc_minor' );
338  }
339  ],
340  [
341  'name' => 'hidemajor',
342  'label' => 'rcfilters-filter-major-label',
343  'description' => 'rcfilters-filter-major-description',
344  'default' => false,
345  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
346  &$query_options, &$join_conds
347  ) {
348  $conds[] = 'rc_minor = 1';
349  },
350  'cssClassSuffix' => 'major',
351  'isRowApplicableCallable' => function ( $ctx, $rc ) {
352  return !$rc->getAttribute( 'rc_minor' );
353  }
354  ]
355  ]
356  ],
357 
358  [
359  'name' => 'lastRevision',
360  'title' => 'rcfilters-filtergroup-lastrevision',
361  'class' => ChangesListBooleanFilterGroup::class,
362  'priority' => -7,
363  'filters' => [
364  [
365  'name' => 'hidelastrevision',
366  'label' => 'rcfilters-filter-lastrevision-label',
367  'description' => 'rcfilters-filter-lastrevision-description',
368  'default' => false,
369  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
370  &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
371  $conds[] = $dbr->makeList(
372  [
373  'rc_this_oldid <> page_latest',
374  'rc_type' => $nonRevisionTypes,
375  ],
376  LIST_OR
377  );
378  },
379  'cssClassSuffix' => 'last',
380  'isRowApplicableCallable' => function ( $ctx, $rc ) {
381  return $rc->getAttribute( 'rc_this_oldid' ) === $rc->getAttribute( 'page_latest' );
382  }
383  ],
384  [
385  'name' => 'hidepreviousrevisions',
386  'label' => 'rcfilters-filter-previousrevision-label',
387  'description' => 'rcfilters-filter-previousrevision-description',
388  'default' => false,
389  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
390  &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
391  $conds[] = $dbr->makeList(
392  [
393  'rc_this_oldid = page_latest',
394  'rc_type' => $nonRevisionTypes,
395  ],
396  LIST_OR
397  );
398  },
399  'cssClassSuffix' => 'previous',
400  'isRowApplicableCallable' => function ( $ctx, $rc ) {
401  return $rc->getAttribute( 'rc_this_oldid' ) !== $rc->getAttribute( 'page_latest' );
402  }
403  ]
404  ]
405  ],
406 
407  // With extensions, there can be change types that will not be hidden by any of these.
408  [
409  'name' => 'changeType',
410  'title' => 'rcfilters-filtergroup-changetype',
411  'class' => ChangesListBooleanFilterGroup::class,
412  'priority' => -8,
413  'filters' => [
414  [
415  'name' => 'hidepageedits',
416  'label' => 'rcfilters-filter-pageedits-label',
417  'description' => 'rcfilters-filter-pageedits-description',
418  'default' => false,
419  'priority' => -2,
420  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
421  &$query_options, &$join_conds
422  ) {
423  $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_EDIT );
424  },
425  'cssClassSuffix' => 'src-mw-edit',
426  'isRowApplicableCallable' => function ( $ctx, $rc ) {
427  return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_EDIT;
428  },
429  ],
430  [
431  'name' => 'hidenewpages',
432  'label' => 'rcfilters-filter-newpages-label',
433  'description' => 'rcfilters-filter-newpages-description',
434  'default' => false,
435  'priority' => -3,
436  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
437  &$query_options, &$join_conds
438  ) {
439  $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_NEW );
440  },
441  'cssClassSuffix' => 'src-mw-new',
442  'isRowApplicableCallable' => function ( $ctx, $rc ) {
443  return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW;
444  },
445  ],
446 
447  // hidecategorization
448 
449  [
450  'name' => 'hidelog',
451  'label' => 'rcfilters-filter-logactions-label',
452  'description' => 'rcfilters-filter-logactions-description',
453  'default' => false,
454  'priority' => -5,
455  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
456  &$query_options, &$join_conds
457  ) {
458  $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG );
459  },
460  'cssClassSuffix' => 'src-mw-log',
461  'isRowApplicableCallable' => function ( $ctx, $rc ) {
462  return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG;
463  }
464  ],
465  ],
466  ],
467 
468  ];
469 
470  $this->legacyReviewStatusFilterGroupDefinition = [
471  [
472  'name' => 'legacyReviewStatus',
473  'title' => 'rcfilters-filtergroup-reviewstatus',
474  'class' => ChangesListBooleanFilterGroup::class,
475  'filters' => [
476  [
477  'name' => 'hidepatrolled',
478  // rcshowhidepatr-show, rcshowhidepatr-hide
479  // wlshowhidepatr
480  'showHideSuffix' => 'showhidepatr',
481  'default' => false,
482  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
483  &$query_options, &$join_conds
484  ) {
485  $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
486  },
487  'isReplacedInStructuredUi' => true,
488  ],
489  [
490  'name' => 'hideunpatrolled',
491  'default' => false,
492  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
493  &$query_options, &$join_conds
494  ) {
495  $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
496  },
497  'isReplacedInStructuredUi' => true,
498  ],
499  ],
500  ]
501  ];
502 
503  $this->reviewStatusFilterGroupDefinition = [
504  [
505  'name' => 'reviewStatus',
506  'title' => 'rcfilters-filtergroup-reviewstatus',
507  'class' => ChangesListStringOptionsFilterGroup::class,
508  'isFullCoverage' => true,
509  'priority' => -5,
510  'filters' => [
511  [
512  'name' => 'unpatrolled',
513  'label' => 'rcfilters-filter-reviewstatus-unpatrolled-label',
514  'description' => 'rcfilters-filter-reviewstatus-unpatrolled-description',
515  'cssClassSuffix' => 'reviewstatus-unpatrolled',
516  'isRowApplicableCallable' => function ( $ctx, $rc ) {
517  return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED;
518  },
519  ],
520  [
521  'name' => 'manual',
522  'label' => 'rcfilters-filter-reviewstatus-manual-label',
523  'description' => 'rcfilters-filter-reviewstatus-manual-description',
524  'cssClassSuffix' => 'reviewstatus-manual',
525  'isRowApplicableCallable' => function ( $ctx, $rc ) {
526  return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_PATROLLED;
527  },
528  ],
529  [
530  'name' => 'auto',
531  'label' => 'rcfilters-filter-reviewstatus-auto-label',
532  'description' => 'rcfilters-filter-reviewstatus-auto-description',
533  'cssClassSuffix' => 'reviewstatus-auto',
534  'isRowApplicableCallable' => function ( $ctx, $rc ) {
535  return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED;
536  },
537  ],
538  ],
540  'queryCallable' => function ( $specialPageClassName, $ctx, $dbr,
541  &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
542  ) {
543  if ( $selected === [] ) {
544  return;
545  }
546  $rcPatrolledValues = [
547  'unpatrolled' => RecentChange::PRC_UNPATROLLED,
548  'manual' => RecentChange::PRC_PATROLLED,
550  ];
551  // e.g. rc_patrolled IN (0, 2)
552  $conds['rc_patrolled'] = array_map( function ( $s ) use ( $rcPatrolledValues ) {
553  return $rcPatrolledValues[ $s ];
554  }, $selected );
555  }
556  ]
557  ];
558 
559  $this->hideCategorizationFilterDefinition = [
560  'name' => 'hidecategorization',
561  'label' => 'rcfilters-filter-categorization-label',
562  'description' => 'rcfilters-filter-categorization-description',
563  // rcshowhidecategorization-show, rcshowhidecategorization-hide.
564  // wlshowhidecategorization
565  'showHideSuffix' => 'showhidecategorization',
566  'default' => false,
567  'priority' => -4,
568  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
569  &$query_options, &$join_conds
570  ) {
571  $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE );
572  },
573  'cssClassSuffix' => 'src-mw-categorize',
574  'isRowApplicableCallable' => function ( $ctx, $rc ) {
575  return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE;
576  },
577  ];
578  }
579 
585  protected function areFiltersInConflict() {
586  $opts = $this->getOptions();
588  foreach ( $this->getFilterGroups() as $group ) {
589  if ( $group->getConflictingGroups() ) {
590  wfLogWarning(
591  $group->getName() .
592  " specifies conflicts with other groups but these are not supported yet."
593  );
594  }
595 
597  foreach ( $group->getConflictingFilters() as $conflictingFilter ) {
598  if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
599  return true;
600  }
601  }
602 
604  foreach ( $group->getFilters() as $filter ) {
606  foreach ( $filter->getConflictingFilters() as $conflictingFilter ) {
607  if (
608  $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
609  $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
610  ) {
611  return true;
612  }
613  }
614 
615  }
616 
617  }
618 
619  return false;
620  }
621 
625  public function execute( $subpage ) {
626  $this->rcSubpage = $subpage;
627 
628  $this->considerActionsForDefaultSavedQuery( $subpage );
629 
630  // Enable OOUI and module for the clock icon.
631  if ( $this->getConfig()->get( 'WatchlistExpiry' ) ) {
632  $this->getOutput()->enableOOUI();
633  $this->getOutput()->addModules( 'mediawiki.special.changeslist.watchlistexpiry' );
634  }
635 
636  $opts = $this->getOptions();
637  try {
638  $rows = $this->getRows();
639  if ( $rows === false ) {
640  $rows = new FakeResultWrapper( [] );
641  }
642 
643  // Used by Structured UI app to get results without MW chrome
644  if ( $this->getRequest()->getVal( 'action' ) === 'render' ) {
645  $this->getOutput()->setArticleBodyOnly( true );
646  }
647 
648  // Used by "live update" and "view newest" to check
649  // if there's new changes with minimal data transfer
650  if ( $this->getRequest()->getBool( 'peek' ) ) {
651  $code = $rows->numRows() > 0 ? 200 : 204;
652  $this->getOutput()->setStatusCode( $code );
653 
654  if ( $this->getUser()->isAnon() !==
655  $this->getRequest()->getFuzzyBool( 'isAnon' )
656  ) {
657  $this->getOutput()->setStatusCode( 205 );
658  }
659 
660  return;
661  }
662 
663  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
664  $batch = $linkBatchFactory->newLinkBatch();
665  foreach ( $rows as $row ) {
666  $batch->add( NS_USER, $row->rc_user_text );
667  $batch->add( NS_USER_TALK, $row->rc_user_text );
668  $batch->add( $row->rc_namespace, $row->rc_title );
669  if ( $row->rc_source === RecentChange::SRC_LOG ) {
670  $formatter = LogFormatter::newFromRow( $row );
671  foreach ( $formatter->getPreloadTitles() as $title ) {
672  $batch->addObj( $title );
673  }
674  }
675  }
676  $batch->execute();
677 
678  $this->setHeaders();
679  $this->outputHeader();
680  $this->addModules();
681  $this->webOutput( $rows, $opts );
682 
683  $rows->free();
684  } catch ( DBQueryTimeoutError $timeoutException ) {
685  MWExceptionHandler::logException( $timeoutException );
686 
687  $this->setHeaders();
688  $this->outputHeader();
689  $this->addModules();
690 
691  $this->getOutput()->setStatusCode( 500 );
692  $this->webOutputHeader( 0, $opts );
693  $this->outputTimeout();
694  }
695 
696  if ( $this->getConfig()->get( 'EnableWANCacheReaper' ) ) {
697  // Clean up any bad page entries for titles showing up in RC
699  $this->getDB(),
700  LoggerFactory::getInstance( 'objectcache' )
701  ) );
702  }
703 
704  $this->includeRcFiltersApp();
705  }
706 
714  protected function considerActionsForDefaultSavedQuery( $subpage ) {
715  if ( !$this->isStructuredFilterUiEnabled() || $this->including() ) {
716  return;
717  }
718 
719  $knownParams = $this->getRequest()->getValues(
720  ...array_keys( $this->getOptions()->getAllValues() )
721  );
722 
723  // HACK: Temporarily until we can properly define "sticky" filters and parameters,
724  // we need to exclude several parameters we know should not be counted towards preventing
725  // the loading of defaults.
726  $excludedParams = [ 'limit' => '', 'days' => '', 'enhanced' => '', 'from' => '' ];
727  $knownParams = array_diff_key( $knownParams, $excludedParams );
728 
729  if (
730  // If there are NO known parameters in the URL request
731  // (that are not excluded) then we need to check into loading
732  // the default saved query
733  count( $knownParams ) === 0
734  ) {
735  // Get the saved queries data and parse it
736  $savedQueries = FormatJson::decode(
737  $this->getUser()->getOption( static::$savedQueriesPreferenceName ),
738  true
739  );
740 
741  if ( $savedQueries && isset( $savedQueries[ 'default' ] ) ) {
742  // Only load queries that are 'version' 2, since those
743  // have parameter representation
744  if ( isset( $savedQueries[ 'version' ] ) && $savedQueries[ 'version' ] === '2' ) {
745  $savedQueryDefaultID = $savedQueries[ 'default' ];
746  $defaultQuery = $savedQueries[ 'queries' ][ $savedQueryDefaultID ][ 'data' ];
747 
748  // Build the entire parameter list
749  $query = array_merge(
750  $defaultQuery[ 'params' ],
751  $defaultQuery[ 'highlights' ],
752  [
753  'urlversion' => '2',
754  ]
755  );
756  // Add to the query any parameters that we may have ignored before
757  // but are still valid and requested in the URL
758  $query = array_merge( $this->getRequest()->getValues(), $query );
759  unset( $query[ 'title' ] );
760  $this->getOutput()->redirect( $this->getPageTitle( $subpage )->getCanonicalURL( $query ) );
761  } else {
762  // There's a default, but the version is not 2, and the server can't
763  // actually recognize the query itself. This happens if it is before
764  // the conversion, so we need to tell the UI to reload saved query as
765  // it does the conversion to version 2
766  $this->getOutput()->addJsConfigVars(
767  'wgStructuredChangeFiltersDefaultSavedQueryExists',
768  true
769  );
770 
771  // Add the class that tells the frontend it is still loading
772  // another query
773  $this->getOutput()->addBodyClasses( 'mw-rcfilters-ui-loading' );
774  }
775  }
776  }
777  }
778 
784  protected function getLinkDays() {
785  $linkDays = $this->getConfig()->get( 'RCLinkDays' );
786  $filterByAge = $this->getConfig()->get( 'RCFilterByAge' );
787  $maxAge = $this->getConfig()->get( 'RCMaxAge' );
788  if ( $filterByAge ) {
789  // Trim it to only links which are within $wgRCMaxAge.
790  // Note that we allow one link higher than the max for things like
791  // "age 56 days" being accessible through the "60 days" link.
792  sort( $linkDays );
793 
794  $maxAgeDays = $maxAge / ( 3600 * 24 );
795  foreach ( $linkDays as $i => $days ) {
796  if ( $days >= $maxAgeDays ) {
797  array_splice( $linkDays, $i + 1 );
798  break;
799  }
800  }
801  }
802 
803  return $linkDays;
804  }
805 
812  protected function includeRcFiltersApp() {
813  $out = $this->getOutput();
814  if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
815  $jsData = $this->getStructuredFilterJsData();
816  $messages = [];
817  foreach ( $jsData['messageKeys'] as $key ) {
818  $messages[$key] = $this->msg( $key )->plain();
819  }
820 
821  $out->addBodyClasses( 'mw-rcfilters-enabled' );
822  $collapsed = $this->getUser()->getBoolOption( static::$collapsedPreferenceName );
823  if ( $collapsed ) {
824  $out->addBodyClasses( 'mw-rcfilters-collapsed' );
825  }
826 
827  // These config and message exports should be moved into a ResourceLoader data module (T201574)
828  $out->addJsConfigVars( 'wgStructuredChangeFilters', $jsData['groups'] );
829  $out->addJsConfigVars( 'wgStructuredChangeFiltersMessages', $messages );
830  $out->addJsConfigVars( 'wgStructuredChangeFiltersCollapsedState', $collapsed );
831 
832  $out->addJsConfigVars(
833  'StructuredChangeFiltersDisplayConfig',
834  [
835  'maxDays' => (int)$this->getConfig()->get( 'RCMaxAge' ) / ( 24 * 3600 ), // Translate to days
836  'limitArray' => $this->getConfig()->get( 'RCLinkLimits' ),
837  'limitDefault' => $this->getDefaultLimit(),
838  'daysArray' => $this->getLinkDays(),
839  'daysDefault' => $this->getDefaultDays(),
840  ]
841  );
842 
843  $out->addJsConfigVars(
844  'wgStructuredChangeFiltersSavedQueriesPreferenceName',
845  static::$savedQueriesPreferenceName
846  );
847  $out->addJsConfigVars(
848  'wgStructuredChangeFiltersLimitPreferenceName',
849  static::$limitPreferenceName
850  );
851  $out->addJsConfigVars(
852  'wgStructuredChangeFiltersDaysPreferenceName',
853  static::$daysPreferenceName
854  );
855  $out->addJsConfigVars(
856  'wgStructuredChangeFiltersCollapsedPreferenceName',
857  static::$collapsedPreferenceName
858  );
859  } else {
860  $out->addBodyClasses( 'mw-rcfilters-disabled' );
861  }
862  }
863 
872  public static function getRcFiltersConfigSummary( ResourceLoaderContext $context ) {
873  return [
874  // Reduce version computation by avoiding Message parsing
875  'RCFiltersChangeTags' => self::getChangeTagListSummary( $context ),
876  'StructuredChangeFiltersEditWatchlistUrl' =>
877  SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
878  ];
879  }
880 
888  public static function getRcFiltersConfigVars( ResourceLoaderContext $context ) {
889  return [
890  'RCFiltersChangeTags' => self::getChangeTagList( $context ),
891  'StructuredChangeFiltersEditWatchlistUrl' =>
892  SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
893  ];
894  }
895 
916  protected static function getChangeTagListSummary( ResourceLoaderContext $context ) {
917  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
918  return $cache->getWithSetCallback(
919  $cache->makeKey( 'ChangesListSpecialPage-changeTagListSummary', $context->getLanguage() ),
920  WANObjectCache::TTL_DAY,
921  function ( $oldValue, &$ttl, array &$setOpts ) use ( $context ) {
922  $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
923  $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
924 
925  $tagStats = ChangeTags::tagUsageStatistics();
926  $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
927 
928  $result = [];
929  foreach ( $tagHitCounts as $tagName => $hits ) {
930  if (
931  (
932  // Only get active tags
933  isset( $explicitlyDefinedTags[ $tagName ] ) ||
934  isset( $softwareActivatedTags[ $tagName ] )
935  ) &&
936  // Only get tags with more than 0 hits
937  $hits > 0
938  ) {
939  $labelMsg = ChangeTags::tagShortDescriptionMessage( $tagName, $context );
940  if ( $labelMsg === false ) {
941  // Tag is hidden, skip it
942  continue;
943  }
944  $descriptionMsg = ChangeTags::tagLongDescriptionMessage( $tagName, $context );
945  $result[] = [
946  'name' => $tagName,
947  'labelMsg' => $labelMsg,
948  'label' => $labelMsg->plain(),
949  'descriptionMsg' => $descriptionMsg,
950  'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
951  'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
952  'hits' => $hits,
953  ];
954  }
955  }
956  return $result;
957  }
958  );
959  }
960 
974  protected static function getChangeTagList( ResourceLoaderContext $context ) {
975  $tags = self::getChangeTagListSummary( $context );
976  $language = MediaWikiServices::getInstance()->getLanguageFactory()
977  ->getLanguage( $context->getLanguage() );
978  foreach ( $tags as &$tagInfo ) {
979  $tagInfo['label'] = Sanitizer::stripAllTags( $tagInfo['labelMsg']->parse() );
980  $tagInfo['description'] = $tagInfo['descriptionMsg'] ?
981  $language->truncateForVisual(
982  Sanitizer::stripAllTags( $tagInfo['descriptionMsg']->parse() ),
983  self::TAG_DESC_CHARACTER_LIMIT
984  ) :
985  '';
986  unset( $tagInfo['labelMsg'] );
987  unset( $tagInfo['descriptionMsg'] );
988  }
989 
990  // Instead of sorting by hit count (disabled for now), sort by display name
991  usort( $tags, function ( $a, $b ) {
992  return strcasecmp( $a['label'], $b['label'] );
993  } );
994  return $tags;
995  }
996 
1000  protected function outputNoResults() {
1001  $this->getOutput()->addHTML(
1002  '<div class="mw-changeslist-empty">' .
1003  $this->msg( 'recentchanges-noresult' )->parse() .
1004  '</div>'
1005  );
1006  }
1007 
1011  protected function outputTimeout() {
1012  $this->getOutput()->addHTML(
1013  '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
1014  $this->msg( 'recentchanges-timeout' )->parse() .
1015  '</div>'
1016  );
1017  }
1018 
1024  public function getRows() {
1025  $opts = $this->getOptions();
1026 
1027  $tables = [];
1028  $fields = [];
1029  $conds = [];
1030  $query_options = [];
1031  $join_conds = [];
1032  $this->buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1033 
1034  return $this->doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1035  }
1036 
1042  public function getOptions() {
1043  if ( $this->rcOptions === null ) {
1044  $this->rcOptions = $this->setup( $this->rcSubpage );
1045  }
1046 
1047  return $this->rcOptions;
1048  }
1049 
1059  protected function registerFilters() {
1060  $this->registerFiltersFromDefinitions( $this->filterGroupDefinitions );
1061 
1062  // Make sure this is not being transcluded (we don't want to show this
1063  // information to all users just because the user that saves the edit can
1064  // patrol or is logged in)
1065  if ( !$this->including() && $this->getUser()->useRCPatrol() ) {
1066  $this->registerFiltersFromDefinitions( $this->legacyReviewStatusFilterGroupDefinition );
1067  $this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition );
1068  }
1069 
1070  $changeTypeGroup = $this->getFilterGroup( 'changeType' );
1071 
1072  if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) {
1073  $transformedHideCategorizationDef = $this->transformFilterDefinition(
1074  $this->hideCategorizationFilterDefinition
1075  );
1076 
1077  $transformedHideCategorizationDef['group'] = $changeTypeGroup;
1078 
1079  $hideCategorization = new ChangesListBooleanFilter(
1080  $transformedHideCategorizationDef
1081  );
1082  }
1083 
1084  $this->getHookRunner()->onChangesListSpecialPageStructuredFilters( $this );
1085 
1086  $this->registerFiltersFromDefinitions( [] );
1087 
1088  $userExperienceLevel = $this->getFilterGroup( 'userExpLevel' );
1089  $registered = $userExperienceLevel->getFilter( 'registered' );
1090  $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'newcomer' ) );
1091  $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'learner' ) );
1092  $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'experienced' ) );
1093 
1094  $categoryFilter = $changeTypeGroup->getFilter( 'hidecategorization' );
1095  $logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' );
1096  $pagecreationFilter = $changeTypeGroup->getFilter( 'hidenewpages' );
1097 
1098  $significanceTypeGroup = $this->getFilterGroup( 'significance' );
1099  $hideMinorFilter = $significanceTypeGroup->getFilter( 'hideminor' );
1100 
1101  // categoryFilter is conditional; see registerFilters
1102  if ( $categoryFilter !== null ) {
1103  $hideMinorFilter->conflictsWith(
1104  $categoryFilter,
1105  'rcfilters-hideminor-conflicts-typeofchange-global',
1106  'rcfilters-hideminor-conflicts-typeofchange',
1107  'rcfilters-typeofchange-conflicts-hideminor'
1108  );
1109  }
1110  $hideMinorFilter->conflictsWith(
1111  $logactionsFilter,
1112  'rcfilters-hideminor-conflicts-typeofchange-global',
1113  'rcfilters-hideminor-conflicts-typeofchange',
1114  'rcfilters-typeofchange-conflicts-hideminor'
1115  );
1116  $hideMinorFilter->conflictsWith(
1117  $pagecreationFilter,
1118  'rcfilters-hideminor-conflicts-typeofchange-global',
1119  'rcfilters-hideminor-conflicts-typeofchange',
1120  'rcfilters-typeofchange-conflicts-hideminor'
1121  );
1122  }
1123 
1133  protected function transformFilterDefinition( array $filterDefinition ) {
1134  return $filterDefinition;
1135  }
1136 
1147  protected function registerFiltersFromDefinitions( array $definition ) {
1148  $autoFillPriority = -1;
1149  foreach ( $definition as $groupDefinition ) {
1150  if ( !isset( $groupDefinition['priority'] ) ) {
1151  $groupDefinition['priority'] = $autoFillPriority;
1152  } else {
1153  // If it's explicitly specified, start over the auto-fill
1154  $autoFillPriority = $groupDefinition['priority'];
1155  }
1156 
1157  $autoFillPriority--;
1158 
1159  $className = $groupDefinition['class'];
1160  unset( $groupDefinition['class'] );
1161 
1162  foreach ( $groupDefinition['filters'] as &$filterDefinition ) {
1163  $filterDefinition = $this->transformFilterDefinition( $filterDefinition );
1164  }
1165 
1166  $this->registerFilterGroup( new $className( $groupDefinition ) );
1167  }
1168  }
1169 
1173  protected function getLegacyShowHideFilters() {
1174  $filters = [];
1175  foreach ( $this->filterGroups as $group ) {
1176  if ( $group instanceof ChangesListBooleanFilterGroup ) {
1177  foreach ( $group->getFilters() as $key => $filter ) {
1178  if ( $filter->displaysOnUnstructuredUi() ) {
1179  $filters[ $key ] = $filter;
1180  }
1181  }
1182  }
1183  }
1184  return $filters;
1185  }
1186 
1195  public function setup( $parameters ) {
1196  $this->registerFilters();
1197 
1198  $opts = $this->getDefaultOptions();
1199 
1200  $opts = $this->fetchOptionsFromRequest( $opts );
1201 
1202  // Give precedence to subpage syntax
1203  if ( $parameters !== null ) {
1204  $this->parseParameters( $parameters, $opts );
1205  }
1206 
1207  $this->validateOptions( $opts );
1208 
1209  return $opts;
1210  }
1211 
1221  public function getDefaultOptions() {
1222  $opts = new FormOptions();
1223  $structuredUI = $this->isStructuredFilterUiEnabled();
1224  // If urlversion=2 is set, ignore the filter defaults and set them all to false/empty
1225  $useDefaults = $this->getRequest()->getInt( 'urlversion' ) !== 2;
1226 
1228  foreach ( $this->filterGroups as $filterGroup ) {
1229  $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1230  }
1231 
1232  $opts->add( 'namespace', '', FormOptions::STRING );
1233  $opts->add( 'invert', false );
1234  $opts->add( 'associated', false );
1235  $opts->add( 'urlversion', 1 );
1236  $opts->add( 'tagfilter', '' );
1237 
1238  $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT );
1239  $opts->add( 'limit', $this->getDefaultLimit(), FormOptions::INT );
1240 
1241  $opts->add( 'from', '' );
1242 
1243  return $opts;
1244  }
1245 
1251  public function registerFilterGroup( ChangesListFilterGroup $group ) {
1252  $groupName = $group->getName();
1253 
1254  $this->filterGroups[$groupName] = $group;
1255  }
1256 
1262  protected function getFilterGroups() {
1263  return $this->filterGroups;
1264  }
1265 
1273  public function getFilterGroup( $groupName ) {
1274  return $this->filterGroups[$groupName] ?? null;
1275  }
1276 
1277  // Currently, this intentionally only includes filters that display
1278  // in the structured UI. This can be changed easily, though, if we want
1279  // to include data on filters that use the unstructured UI. messageKeys is a
1280  // special top-level value, with the value being an array of the message keys to
1281  // send to the client.
1282 
1290  public function getStructuredFilterJsData() {
1291  $output = [
1292  'groups' => [],
1293  'messageKeys' => [],
1294  ];
1295 
1296  usort( $this->filterGroups, function ( $a, $b ) {
1297  return $b->getPriority() <=> $a->getPriority();
1298  } );
1299 
1300  foreach ( $this->filterGroups as $groupName => $group ) {
1301  $groupOutput = $group->getJsData();
1302  if ( $groupOutput !== null ) {
1303  $output['messageKeys'] = array_merge(
1304  $output['messageKeys'],
1305  $groupOutput['messageKeys']
1306  );
1307 
1308  unset( $groupOutput['messageKeys'] );
1309  $output['groups'][] = $groupOutput;
1310  }
1311  }
1312 
1313  return $output;
1314  }
1315 
1324  protected function fetchOptionsFromRequest( $opts ) {
1325  $opts->fetchValuesFromRequest( $this->getRequest() );
1326 
1327  return $opts;
1328  }
1329 
1336  public function parseParameters( $par, FormOptions $opts ) {
1337  $stringParameterNameSet = [];
1338  $hideParameterNameSet = [];
1339 
1340  // URL parameters can be per-group, like 'userExpLevel',
1341  // or per-filter, like 'hideminor'.
1342 
1343  foreach ( $this->filterGroups as $filterGroup ) {
1344  if ( $filterGroup instanceof ChangesListStringOptionsFilterGroup ) {
1345  $stringParameterNameSet[$filterGroup->getName()] = true;
1346  } elseif ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
1347  foreach ( $filterGroup->getFilters() as $filter ) {
1348  $hideParameterNameSet[$filter->getName()] = true;
1349  }
1350  }
1351  }
1352 
1353  $bits = preg_split( '/\s*,\s*/', trim( $par ) );
1354  foreach ( $bits as $bit ) {
1355  $m = [];
1356  if ( isset( $hideParameterNameSet[$bit] ) ) {
1357  // hidefoo => hidefoo=true
1358  $opts[$bit] = true;
1359  } elseif ( isset( $hideParameterNameSet["hide$bit"] ) ) {
1360  // foo => hidefoo=false
1361  $opts["hide$bit"] = false;
1362  } elseif ( preg_match( '/^(.*)=(.*)$/', $bit, $m ) ) {
1363  if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1364  $opts[$m[1]] = $m[2];
1365  }
1366  }
1367  }
1368  }
1369 
1375  public function validateOptions( FormOptions $opts ) {
1376  $isContradictory = $this->fixContradictoryOptions( $opts );
1377  $isReplaced = $this->replaceOldOptions( $opts );
1378 
1379  if ( $isContradictory || $isReplaced ) {
1380  $query = wfArrayToCgi( $this->convertParamsForLink( $opts->getChangedValues() ) );
1381  $this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) );
1382  }
1383 
1384  $opts->validateIntBounds( 'limit', 0, 5000 );
1385  $opts->validateBounds( 'days', 0, $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
1386  }
1387 
1394  private function fixContradictoryOptions( FormOptions $opts ) {
1395  $fixed = $this->fixBackwardsCompatibilityOptions( $opts );
1396 
1397  foreach ( $this->filterGroups as $filterGroup ) {
1398  if ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
1399  $filters = $filterGroup->getFilters();
1400 
1401  if ( count( $filters ) === 1 ) {
1402  // legacy boolean filters should not be considered
1403  continue;
1404  }
1405 
1406  $allInGroupEnabled = array_reduce(
1407  $filters,
1408  function ( $carry, $filter ) use ( $opts ) {
1409  return $carry && $opts[ $filter->getName() ];
1410  },
1411  /* initialValue */ count( $filters ) > 0
1412  );
1413 
1414  if ( $allInGroupEnabled ) {
1415  foreach ( $filters as $filter ) {
1416  $opts[ $filter->getName() ] = false;
1417  }
1418 
1419  $fixed = true;
1420  }
1421  }
1422  }
1423 
1424  return $fixed;
1425  }
1426 
1436  private function fixBackwardsCompatibilityOptions( FormOptions $opts ) {
1437  if ( $opts['hideanons'] && $opts['hideliu'] ) {
1438  $opts->reset( 'hideanons' );
1439  if ( !$opts['hidebots'] ) {
1440  $opts->reset( 'hideliu' );
1441  $opts['hidehumans'] = 1;
1442  }
1443 
1444  return true;
1445  }
1446 
1447  return false;
1448  }
1449 
1456  public function replaceOldOptions( FormOptions $opts ) {
1457  if ( !$this->isStructuredFilterUiEnabled() ) {
1458  return false;
1459  }
1460 
1461  $changed = false;
1462 
1463  // At this point 'hideanons' and 'hideliu' cannot be both true,
1464  // because fixBackwardsCompatibilityOptions resets (at least) 'hideanons' in such case
1465  if ( $opts[ 'hideanons' ] ) {
1466  $opts->reset( 'hideanons' );
1467  $opts[ 'userExpLevel' ] = 'registered';
1468  $changed = true;
1469  }
1470 
1471  if ( $opts[ 'hideliu' ] ) {
1472  $opts->reset( 'hideliu' );
1473  $opts[ 'userExpLevel' ] = 'unregistered';
1474  $changed = true;
1475  }
1476 
1477  if ( $this->getFilterGroup( 'legacyReviewStatus' ) ) {
1478  if ( $opts[ 'hidepatrolled' ] ) {
1479  $opts->reset( 'hidepatrolled' );
1480  $opts[ 'reviewStatus' ] = 'unpatrolled';
1481  $changed = true;
1482  }
1483 
1484  if ( $opts[ 'hideunpatrolled' ] ) {
1485  $opts->reset( 'hideunpatrolled' );
1486  $opts[ 'reviewStatus' ] = implode(
1488  [ 'manual', 'auto' ]
1489  );
1490  $changed = true;
1491  }
1492  }
1493 
1494  return $changed;
1495  }
1496 
1505  protected function convertParamsForLink( $params ) {
1506  foreach ( $params as &$value ) {
1507  if ( $value === false ) {
1508  $value = '0';
1509  }
1510  }
1511  unset( $value );
1512  return $params;
1513  }
1514 
1526  protected function buildQuery( &$tables, &$fields, &$conds, &$query_options,
1527  &$join_conds, FormOptions $opts
1528  ) {
1529  $dbr = $this->getDB();
1530  $isStructuredUI = $this->isStructuredFilterUiEnabled();
1531 
1533  foreach ( $this->filterGroups as $filterGroup ) {
1534  $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds,
1535  $query_options, $join_conds, $opts, $isStructuredUI );
1536  }
1537 
1538  // Namespace filtering
1539  if ( $opts[ 'namespace' ] !== '' ) {
1540  $namespaces = explode( ';', $opts[ 'namespace' ] );
1541 
1542  $namespaces = $this->expandSymbolicNamespaceFilters( $namespaces );
1543 
1544  $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1545  $namespaces = array_filter(
1546  $namespaces,
1547  function ( $ns ) use ( $namespaceInfo ) {
1548  return $namespaceInfo->exists( $ns );
1549  }
1550  );
1551 
1552  if ( $namespaces !== [] ) {
1553  // Namespaces are just ints, use them as int when acting with the database
1554  $namespaces = array_map( 'intval', $namespaces );
1555 
1556  if ( $opts[ 'associated' ] ) {
1557  $associatedNamespaces = array_map(
1558  function ( $ns ) use ( $namespaceInfo ){
1559  return $namespaceInfo->getAssociated( $ns );
1560  },
1561  array_filter(
1562  $namespaces,
1563  function ( $ns ) use ( $namespaceInfo ) {
1564  return $namespaceInfo->hasTalkNamespace( $ns );
1565  }
1566  )
1567  );
1568  $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
1569  }
1570 
1571  if ( count( $namespaces ) === 1 ) {
1572  $operator = $opts[ 'invert' ] ? '!=' : '=';
1573  $value = $dbr->addQuotes( reset( $namespaces ) );
1574  } else {
1575  $operator = $opts[ 'invert' ] ? 'NOT IN' : 'IN';
1576  sort( $namespaces );
1577  $value = '(' . $dbr->makeList( $namespaces ) . ')';
1578  }
1579  $conds[] = "rc_namespace $operator $value";
1580  }
1581  }
1582 
1583  // Calculate cutoff
1584  $cutoff_unixtime = time() - $opts['days'] * 3600 * 24;
1585  $cutoff = $dbr->timestamp( $cutoff_unixtime );
1586 
1587  $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
1588  if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
1589  $cutoff = $dbr->timestamp( $opts['from'] );
1590  } else {
1591  $opts->reset( 'from' );
1592  }
1593 
1594  $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
1595  }
1596 
1608  protected function doMainQuery( $tables, $fields, $conds,
1609  $query_options, $join_conds, FormOptions $opts
1610  ) {
1611  $rcQuery = RecentChange::getQueryInfo();
1612  $tables = array_merge( $tables, $rcQuery['tables'] );
1613  $fields = array_merge( $rcQuery['fields'], $fields );
1614  $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
1615 
1617  $tables,
1618  $fields,
1619  $conds,
1620  $join_conds,
1621  $query_options,
1622  ''
1623  );
1624 
1625  if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
1626  $opts )
1627  ) {
1628  return false;
1629  }
1630 
1631  $dbr = $this->getDB();
1632 
1633  return $dbr->select(
1634  $tables,
1635  $fields,
1636  $conds,
1637  __METHOD__,
1638  $query_options,
1639  $join_conds
1640  );
1641  }
1642 
1643  protected function runMainQueryHook( &$tables, &$fields, &$conds,
1644  &$query_options, &$join_conds, $opts
1645  ) {
1646  return $this->getHookRunner()->onChangesListSpecialPageQuery(
1647  $this->getName(), $tables, $fields, $conds, $query_options, $join_conds, $opts );
1648  }
1649 
1655  protected function getDB() {
1656  return wfGetDB( DB_REPLICA );
1657  }
1658 
1665  private function webOutputHeader( $rowCount, $opts ) {
1666  if ( !$this->including() ) {
1667  $this->outputFeedLinks();
1668  $this->doHeader( $opts, $rowCount );
1669  }
1670  }
1671 
1678  public function webOutput( $rows, $opts ) {
1679  $this->webOutputHeader( $rows->numRows(), $opts );
1680 
1681  $this->outputChangesList( $rows, $opts );
1682  }
1683 
1684  public function outputFeedLinks() {
1685  // nothing by default
1686  }
1687 
1694  abstract public function outputChangesList( $rows, $opts );
1695 
1702  public function doHeader( $opts, $numRows ) {
1703  $this->setTopText( $opts );
1704 
1705  // @todo Lots of stuff should be done here.
1706 
1707  $this->setBottomText( $opts );
1708  }
1709 
1717  public function setTopText( FormOptions $opts ) {
1718  // nothing by default
1719  }
1720 
1728  public function setBottomText( FormOptions $opts ) {
1729  // nothing by default
1730  }
1731 
1741  public function getExtraOptions( $opts ) {
1742  return [];
1743  }
1744 
1750  public function makeLegend() {
1751  $context = $this->getContext();
1752  $user = $context->getUser();
1753  # The legend showing what the letters and stuff mean
1754  $legend = Html::openElement( 'dl' ) . "\n";
1755  # Iterates through them and gets the messages for both letter and tooltip
1756  $legendItems = $context->getConfig()->get( 'RecentChangesFlags' );
1757  if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
1758  unset( $legendItems['unpatrolled'] );
1759  }
1760  foreach ( $legendItems as $key => $item ) { # generate items of the legend
1761  $label = $item['legend'] ?? $item['title'];
1762  $letter = $item['letter'];
1763  $cssClass = $item['class'] ?? $key;
1764 
1765  $legend .= Html::element( 'dt',
1766  [ 'class' => $cssClass ], $context->msg( $letter )->text()
1767  ) . "\n" .
1768  Html::rawElement( 'dd',
1769  [ 'class' => Sanitizer::escapeClass( 'mw-changeslist-legend-' . $key ) ],
1770  $context->msg( $label )->parse()
1771  ) . "\n";
1772  }
1773  # (+-123)
1774  $legend .= Html::rawElement( 'dt',
1775  [ 'class' => 'mw-plusminus-pos' ],
1776  $context->msg( 'recentchanges-legend-plusminus' )->parse()
1777  ) . "\n";
1778  $legend .= Html::element(
1779  'dd',
1780  [ 'class' => 'mw-changeslist-legend-plusminus' ],
1781  $context->msg( 'recentchanges-label-plusminus' )->text()
1782  ) . "\n";
1783  // Watchlist expiry clock icon.
1784  if ( $context->getConfig()->get( 'WatchlistExpiry' ) ) {
1785  $widget = new IconWidget( [
1786  'icon' => 'clock',
1787  'classes' => [ 'mw-changesList-watchlistExpiry' ],
1788  ] );
1789  // Link the image to its label for assistive technologies.
1790  $watchlistLabelId = 'mw-changeslist-watchlistExpiry-label';
1791  $widget->getIconElement()->setAttributes( [
1792  'role' => 'img',
1793  'aria-labelledby' => $watchlistLabelId,
1794  ] );
1795  $legend .= Html::rawElement(
1796  'dt',
1797  [ 'class' => 'mw-changeslist-legend-watchlistexpiry' ],
1798  $widget
1799  );
1800  $legend .= Html::element(
1801  'dd',
1802  [ 'class' => 'mw-changeslist-legend-watchlistexpiry', 'id' => $watchlistLabelId ],
1803  $context->msg( 'recentchanges-legend-watchlistexpiry' )->text()
1804  );
1805  }
1806  $legend .= Html::closeElement( 'dl' ) . "\n";
1807 
1808  $legendHeading = $this->isStructuredFilterUiEnabled() ?
1809  $context->msg( 'rcfilters-legend-heading' )->parse() :
1810  $context->msg( 'recentchanges-legend-heading' )->parse();
1811 
1812  # Collapsible
1813  $collapsedState = $this->getRequest()->getCookie( 'changeslist-state' );
1814  $collapsedClass = $collapsedState === 'collapsed' ? ' mw-collapsed' : '';
1815 
1816  $legend =
1817  '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass . '">' .
1818  $legendHeading .
1819  '<div class="mw-collapsible-content">' . $legend . '</div>' .
1820  '</div>';
1821 
1822  return $legend;
1823  }
1824 
1828  protected function addModules() {
1829  $out = $this->getOutput();
1830  // Styles and behavior for the legend box (see makeLegend())
1831  $out->addModuleStyles( [
1832  'mediawiki.interface.helpers.styles',
1833  'mediawiki.special.changeslist.legend',
1834  'mediawiki.special.changeslist',
1835  ] );
1836  $out->addModules( 'mediawiki.special.changeslist.legend.js' );
1837 
1838  if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
1839  $out->addModules( 'mediawiki.rcfilters.filters.ui' );
1840  $out->addModuleStyles( 'mediawiki.rcfilters.filters.base.styles' );
1841  }
1842  }
1843 
1844  protected function getGroupName() {
1845  return 'changes';
1846  }
1847 
1864  public function filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr,
1865  &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1866  ) {
1867  global $wgLearnerEdits,
1871 
1872  $LEVEL_COUNT = 5;
1873 
1874  // If all levels are selected, don't filter
1875  if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1876  return;
1877  }
1878 
1879  // both 'registered' and 'unregistered', experience levels, if any, are included in 'registered'
1880  if (
1881  in_array( 'registered', $selectedExpLevels ) &&
1882  in_array( 'unregistered', $selectedExpLevels )
1883  ) {
1884  return;
1885  }
1886 
1887  $actorMigration = ActorMigration::newMigration();
1888  $actorQuery = $actorMigration->getJoin( 'rc_user' );
1889  $tables += $actorQuery['tables'];
1890  $join_conds += $actorQuery['joins'];
1891 
1892  // 'registered' but not 'unregistered', experience levels, if any, are included in 'registered'
1893  if (
1894  in_array( 'registered', $selectedExpLevels ) &&
1895  !in_array( 'unregistered', $selectedExpLevels )
1896  ) {
1897  $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
1898  return;
1899  }
1900 
1901  if ( $selectedExpLevels === [ 'unregistered' ] ) {
1902  $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
1903  return;
1904  }
1905 
1906  $tables[] = 'user';
1907  $join_conds['user'] = [ 'LEFT JOIN', $actorQuery['fields']['rc_user'] . ' = user_id' ];
1908 
1909  if ( $now === 0 ) {
1910  $now = time();
1911  }
1912  $secondsPerDay = 86400;
1913  $learnerCutoff = $now - $wgLearnerMemberSince * $secondsPerDay;
1914  $experiencedUserCutoff = $now - $wgExperiencedUserMemberSince * $secondsPerDay;
1915 
1916  $aboveNewcomer = $dbr->makeList(
1917  [
1918  'user_editcount >= ' . intval( $wgLearnerEdits ),
1919  $dbr->makeList( [
1920  'user_registration IS NULL',
1921  'user_registration <= ' . $dbr->addQuotes( $dbr->timestamp( $learnerCutoff ) ),
1922  ], IDatabase::LIST_OR ),
1923  ],
1925  );
1926 
1927  $aboveLearner = $dbr->makeList(
1928  [
1929  'user_editcount >= ' . intval( $wgExperiencedUserEdits ),
1930  $dbr->makeList( [
1931  'user_registration IS NULL',
1932  'user_registration <= ' .
1933  $dbr->addQuotes( $dbr->timestamp( $experiencedUserCutoff ) ),
1934  ], IDatabase::LIST_OR ),
1935  ],
1937  );
1938 
1939  $conditions = [];
1940 
1941  if ( in_array( 'unregistered', $selectedExpLevels ) ) {
1942  $selectedExpLevels = array_diff( $selectedExpLevels, [ 'unregistered' ] );
1943  $conditions[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
1944  }
1945 
1946  if ( $selectedExpLevels === [ 'newcomer' ] ) {
1947  $conditions[] = "NOT ( $aboveNewcomer )";
1948  } elseif ( $selectedExpLevels === [ 'learner' ] ) {
1949  $conditions[] = $dbr->makeList(
1950  [ $aboveNewcomer, "NOT ( $aboveLearner )" ],
1952  );
1953  } elseif ( $selectedExpLevels === [ 'experienced' ] ) {
1954  $conditions[] = $aboveLearner;
1955  } elseif ( $selectedExpLevels === [ 'learner', 'newcomer' ] ) {
1956  $conditions[] = "NOT ( $aboveLearner )";
1957  } elseif ( $selectedExpLevels === [ 'experienced', 'newcomer' ] ) {
1958  $conditions[] = $dbr->makeList(
1959  [ "NOT ( $aboveNewcomer )", $aboveLearner ],
1961  );
1962  } elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) {
1963  $conditions[] = $aboveNewcomer;
1964  } elseif ( $selectedExpLevels === [ 'experienced', 'learner', 'newcomer' ] ) {
1965  $conditions[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
1966  }
1967 
1968  if ( count( $conditions ) > 1 ) {
1969  $conds[] = $dbr->makeList( $conditions, IDatabase::LIST_OR );
1970  } elseif ( count( $conditions ) === 1 ) {
1971  $conds[] = reset( $conditions );
1972  }
1973  }
1974 
1980  public function isStructuredFilterUiEnabled() {
1981  if ( $this->getRequest()->getBool( 'rcfilters' ) ) {
1982  return true;
1983  }
1984 
1985  return static::checkStructuredFilterUiEnabled( $this->getUser() );
1986  }
1987 
1995  public static function checkStructuredFilterUiEnabled( $user ) {
1996  if ( $user instanceof Config ) {
1997  wfDeprecated( __METHOD__ . ' with Config argument', '1.34' );
1998  $user = func_get_arg( 1 );
1999  }
2000  return !$user->getOption( 'rcenhancedfilters-disable' );
2001  }
2002 
2010  public function getDefaultLimit() {
2011  return $this->getUser()->getIntOption( static::$limitPreferenceName );
2012  }
2013 
2022  public function getDefaultDays() {
2023  return floatval( $this->getUser()->getOption( static::$daysPreferenceName ) );
2024  }
2025 
2026  private function expandSymbolicNamespaceFilters( array $namespaces ) {
2027  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2028  $symbolicFilters = [
2029  'all-contents' => $nsInfo->getSubjectNamespaces(),
2030  'all-discussions' => $nsInfo->getTalkNamespaces(),
2031  ];
2032  $additionalNamespaces = [];
2033  foreach ( $symbolicFilters as $name => $values ) {
2034  if ( in_array( $name, $namespaces ) ) {
2035  $additionalNamespaces = array_merge( $additionalNamespaces, $values );
2036  }
2037  }
2038  $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) );
2039  $namespaces = array_merge( $namespaces, $additionalNamespaces );
2040  return array_unique( $namespaces );
2041  }
2042 }
LIST_OR
const LIST_OR
Definition: Defines.php:45
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:742
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:33
RecentChange\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
Definition: RecentChange.php:247
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:900
ChangesListFilterGroup\getName
getName()
Definition: ChangesListFilterGroup.php:282
ChangesListSpecialPage\getExtraOptions
getExtraOptions( $opts)
Get options to be displayed in a form.
Definition: ChangesListSpecialPage.php:1741
ChangesListSpecialPage\__construct
__construct( $name, $restriction)
Definition: ChangesListSpecialPage.php:113
ChangesListSpecialPage\considerActionsForDefaultSavedQuery
considerActionsForDefaultSavedQuery( $subpage)
Check whether or not the page should load defaults, and if so, whether a default saved query is relev...
Definition: ChangesListSpecialPage.php:714
RC_EDIT
const RC_EDIT
Definition: Defines.php:125
Sanitizer\stripAllTags
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
Definition: Sanitizer.php:1574
FormOptions\FLOAT
const FLOAT
Float type, maps guessType() to WebRequest::getFloat()
Definition: FormOptions.php:49
ChangesListSpecialPage\includeRcFiltersApp
includeRcFiltersApp()
Include the modules and configuration for the RCFilters app.
Definition: ChangesListSpecialPage.php:812
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:788
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:166
$wgExperiencedUserMemberSince
$wgExperiencedUserMemberSince
Specify the difference engine to use.
Definition: DefaultSettings.php:9398
ChangesListSpecialPage\makeLegend
makeLegend()
Return the legend displayed within the fieldset.
Definition: ChangesListSpecialPage.php:1750
ChangesListSpecialPage\execute
execute( $subpage)
Definition: ChangesListSpecialPage.php:625
ChangesListSpecialPage\webOutputHeader
webOutputHeader( $rowCount, $opts)
Send header output to the OutputPage object, only called if not using feeds.
Definition: ChangesListSpecialPage.php:1665
ChangesListSpecialPage\fixContradictoryOptions
fixContradictoryOptions(FormOptions $opts)
Fix invalid options by resetting pairs that should never appear together.
Definition: ChangesListSpecialPage.php:1394
ChangesListSpecialPage\parseParameters
parseParameters( $par, FormOptions $opts)
Process $par and put options found in $opts.
Definition: ChangesListSpecialPage.php:1336
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1831
ChangesListSpecialPage\doMainQuery
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
Process the query.
Definition: ChangesListSpecialPage.php:1608
LIST_AND
const LIST_AND
Definition: Defines.php:42
ChangesListSpecialPage\expandSymbolicNamespaceFilters
expandSymbolicNamespaceFilters(array $namespaces)
Definition: ChangesListSpecialPage.php:2026
ChangesListSpecialPage\fixBackwardsCompatibilityOptions
fixBackwardsCompatibilityOptions(FormOptions $opts)
Fix a special case (hideanons=1 and hideliu=1) in a special way, for backwards compatibility.
Definition: ChangesListSpecialPage.php:1436
ChangesListSpecialPage
Special page which uses a ChangesList to show query results.
Definition: ChangesListSpecialPage.php:38
DeferredUpdates\addUpdate
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
Definition: DeferredUpdates.php:119
ChangesListSpecialPage\$legacyReviewStatusFilterGroupDefinition
array $legacyReviewStatusFilterGroupDefinition
Same format as filterGroupDefinitions, but for a single group (reviewStatus) that is registered condi...
Definition: ChangesListSpecialPage.php:97
ChangesListBooleanFilter
Represents a hide-based boolean filter (used on ChangesListSpecialPage and descendants)
Definition: ChangesListBooleanFilter.php:31
WANCacheReapUpdate
Class for fixing stale WANObjectCache keys using a purge event source.
Definition: WANCacheReapUpdate.php:26
ChangesListSpecialPage\getDefaultDays
getDefaultDays()
Get the default value of the number of days to display when loading the result set.
Definition: ChangesListSpecialPage.php:2022
$s
$s
Definition: mergeMessageFileList.php:186
SpecialPage\getTitleFor
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,...
Definition: SpecialPage.php:106
Sanitizer\escapeClass
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:976
FormOptions\reset
reset( $name)
Delete the option value.
Definition: FormOptions.php:207
ChangesListSpecialPage\fetchOptionsFromRequest
fetchOptionsFromRequest( $opts)
Fetch values for a FormOptions object from the WebRequest associated with this instance.
Definition: ChangesListSpecialPage.php:1324
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1093
FormOptions\validateIntBounds
validateIntBounds( $name, $min, $max)
Definition: FormOptions.php:255
ChangesListSpecialPage\getChangeTagListSummary
static getChangeTagListSummary(ResourceLoaderContext $context)
Get information about change tags, without parsing messages, for getRcFiltersConfigSummary().
Definition: ChangesListSpecialPage.php:916
RecentChange\SRC_CATEGORIZE
const SRC_CATEGORIZE
Definition: RecentChange.php:81
RC_NEW
const RC_NEW
Definition: Defines.php:126
SpecialPage\getName
getName()
Get the name of this Special Page.
Definition: SpecialPage.php:178
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
Definition: FakeResultWrapper.php:11
ChangesListFilterGroup
Represents a filter group (used on ChangesListSpecialPage and descendants)
Definition: ChangesListFilterGroup.php:37
RecentChange\SRC_LOG
const SRC_LOG
Definition: RecentChange.php:79
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:156
ChangesListSpecialPage\outputNoResults
outputNoResults()
Add the "no results" message to the output.
Definition: ChangesListSpecialPage.php:1000
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
ChangesListSpecialPage\doHeader
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
Definition: ChangesListSpecialPage.php:1702
ChangesListSpecialPage\checkStructuredFilterUiEnabled
static checkStructuredFilterUiEnabled( $user)
Static method to check whether StructuredFilter UI is enabled for the given user.
Definition: ChangesListSpecialPage.php:1995
$dbr
$dbr
Definition: testCompression.php:54
ChangeTags\tagShortDescriptionMessage
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
Definition: ChangeTags.php:229
$wgExperiencedUserEdits
$wgExperiencedUserEdits
Specify the difference engine to use.
Definition: DefaultSettings.php:9397
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:318
MWExceptionHandler\logException
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
Definition: MWExceptionHandler.php:666
RC_LOG
const RC_LOG
Definition: Defines.php:127
ChangesListSpecialPage\webOutput
webOutput( $rows, $opts)
Send output to the OutputPage object, only called if not used feeds.
Definition: ChangesListSpecialPage.php:1678
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:851
Config
Interface for configuration instances.
Definition: Config.php:30
ChangesListSpecialPage\TAG_DESC_CHARACTER_LIMIT
const TAG_DESC_CHARACTER_LIMIT
Maximum length of a tag description in UTF-8 characters.
Definition: ChangesListSpecialPage.php:43
FormatJson\decode
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
SpecialPage\getHookRunner
getHookRunner()
Definition: SpecialPage.php:1083
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:866
ChangesListSpecialPage\$filterGroupDefinitions
array $filterGroupDefinitions
Definition information for the filters and their groups.
Definition: ChangesListSpecialPage.php:91
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1033
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
LogFormatter\newFromRow
static newFromRow( $row)
Handy shortcut for constructing a formatter directly from database row.
Definition: LogFormatter.php:73
Wikimedia\Rdbms\DBQueryTimeoutError
Error thrown when a query times out.
Definition: DBQueryTimeoutError.php:30
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
ChangesListSpecialPage\convertParamsForLink
convertParamsForLink( $params)
Convert parameters values from true/false to 1/0 so they are not omitted by wfArrayToCgi() T38524.
Definition: ChangesListSpecialPage.php:1505
ChangesListSpecialPage\getRcFiltersConfigVars
static getRcFiltersConfigVars(ResourceLoaderContext $context)
Get config vars to export with the mediawiki.rcfilters.filters.ui module.
Definition: ChangesListSpecialPage.php:888
ChangesListSpecialPage\getRows
getRows()
Get the database result for this special page instance.
Definition: ChangesListSpecialPage.php:1024
ChangesListSpecialPage\outputFeedLinks
outputFeedLinks()
Definition: ChangesListSpecialPage.php:1684
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2466
ChangesListSpecialPage\runMainQueryHook
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
Definition: ChangesListSpecialPage.php:1643
ChangesListSpecialPage\isStructuredFilterUiEnabled
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
Definition: ChangesListSpecialPage.php:1980
ChangesListSpecialPage\$daysPreferenceName
static string $daysPreferenceName
Preference name for 'days'.
Definition: ChangesListSpecialPage.php:55
ChangesListSpecialPage\getStructuredFilterJsData
getStructuredFilterJsData()
Gets structured filter information needed by JS.
Definition: ChangesListSpecialPage.php:1290
ChangesListStringOptionsFilterGroup\SEPARATOR
const SEPARATOR
Delimiter.
Definition: ChangesListStringOptionsFilterGroup.php:46
ChangesListSpecialPage\outputChangesList
outputChangesList( $rows, $opts)
Build and output the actual changes list.
ChangesListSpecialPage\getFilterGroups
getFilterGroups()
Gets the currently registered filters groups.
Definition: ChangesListSpecialPage.php:1262
$title
$title
Definition: testCompression.php:38
SpecialPage\setHeaders
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!...
Definition: SpecialPage.php:616
SpecialPage\getUser
getUser()
Shortcut to get the User executing this instance.
Definition: SpecialPage.php:798
RecentChange\SRC_EDIT
const SRC_EDIT
Definition: RecentChange.php:77
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
ResourceLoaderContext\getLanguage
getLanguage()
Definition: ResourceLoaderContext.php:156
RecentChange\SRC_NEW
const SRC_NEW
Definition: RecentChange.php:78
ChangesListSpecialPage\buildQuery
buildQuery(&$tables, &$fields, &$conds, &$query_options, &$join_conds, FormOptions $opts)
Sets appropriate tables, fields, conditions, etc.
Definition: ChangesListSpecialPage.php:1526
ChangesListSpecialPage\getDefaultLimit
getDefaultLimit()
Get the default value of the number of changes to display when loading the result set.
Definition: ChangesListSpecialPage.php:2010
SpecialPage\getContext
getContext()
Gets the context this SpecialPage is executed in.
Definition: SpecialPage.php:762
RecentChange\PRC_PATROLLED
const PRC_PATROLLED
Definition: RecentChange.php:84
ChangeTags\listExplicitlyDefinedTags
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
Definition: ChangeTags.php:1550
ChangesListSpecialPage\$rcOptions
FormOptions $rcOptions
Definition: ChangesListSpecialPage.php:73
ChangesListSpecialPage\registerFilters
registerFilters()
Register all filters and their groups (including those from hooks), plus handle conflicts and default...
Definition: ChangesListSpecialPage.php:1059
ChangesListSpecialPage\$savedQueriesPreferenceName
static string $savedQueriesPreferenceName
Preference name for saved queries.
Definition: ChangesListSpecialPage.php:49
FormOptions\validateBounds
validateBounds( $name, $min, $max)
Constrain a numeric value for a given option to a given range.
Definition: FormOptions.php:270
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:42
ChangesListSpecialPage\setup
setup( $parameters)
Register all the filters, including legacy hook-driven ones.
Definition: ChangesListSpecialPage.php:1195
ChangesListSpecialPage\getChangeTagList
static getChangeTagList(ResourceLoaderContext $context)
Get information about change tags to export to JS via getRcFiltersConfigVars().
Definition: ChangesListSpecialPage.php:974
ChangesListSpecialPage\getFilterGroup
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
Definition: ChangesListSpecialPage.php:1273
ChangesListSpecialPage\outputTimeout
outputTimeout()
Add the "timeout" message to the output.
Definition: ChangesListSpecialPage.php:1011
ChangesListSpecialPage\$reviewStatusFilterGroupDefinition
array $reviewStatusFilterGroupDefinition
Single filter group registered conditionally.
Definition: ChangesListSpecialPage.php:100
SpecialPage\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: SpecialPage.php:778
ChangesListStringOptionsFilterGroup\NONE
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
Definition: ChangesListStringOptionsFilterGroup.php:59
NS_USER
const NS_USER
Definition: Defines.php:65
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:85
ChangeTags\listSoftwareActivatedTags
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
Definition: ChangeTags.php:1503
$wgLearnerEdits
$wgLearnerEdits
The following variables define 3 user experience levels:
Definition: DefaultSettings.php:9395
ChangesListSpecialPage\addModules
addModules()
Add page-specific modules.
Definition: ChangesListSpecialPage.php:1828
FormOptions\STRING
const STRING
String type, maps guessType() to WebRequest::getText()
Definition: FormOptions.php:43
ChangesListSpecialPage\getGroupName
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Definition: ChangesListSpecialPage.php:1844
ChangesListSpecialPage\areFiltersInConflict
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
Definition: ChangesListSpecialPage.php:585
ChangesListSpecialPage\getDefaultOptions
getDefaultOptions()
Get a FormOptions object containing the default options.
Definition: ChangesListSpecialPage.php:1221
ChangesListSpecialPage\transformFilterDefinition
transformFilterDefinition(array $filterDefinition)
Transforms filter definition to prepare it for constructor.
Definition: ChangesListSpecialPage.php:1133
ChangesListBooleanFilterGroup
If the group is active, any unchecked filters will translate to hide parameters in the URL.
Definition: ChangesListBooleanFilterGroup.php:13
$cache
$cache
Definition: mcc.php:33
RC_CATEGORIZE
const RC_CATEGORIZE
Definition: Defines.php:129
FormOptions\INT
const INT
Integer type, maps guessType() to WebRequest::getInt()
Definition: FormOptions.php:45
ChangesListSpecialPage\$hideCategorizationFilterDefinition
array $hideCategorizationFilterDefinition
Single filter group registered conditionally.
Definition: ChangesListSpecialPage.php:103
ChangesListSpecialPage\getDB
getDB()
Return a IDatabase object for reading.
Definition: ChangesListSpecialPage.php:1655
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:83
ChangesListSpecialPage\$filterGroups
ChangesListFilterGroup[] $filterGroups
Filter groups, and their contained filters This is an associative array (with group name as key) of C...
Definition: ChangesListSpecialPage.php:111
Html\openElement
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:254
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
ChangesListSpecialPage\$collapsedPreferenceName
static string $collapsedPreferenceName
Preference name for collapsing the active filter display.
Definition: ChangesListSpecialPage.php:67
ChangesListFilter
Represents a filter (used on ChangesListSpecialPage and descendants)
Definition: ChangesListFilter.php:29
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:66
$wgLearnerMemberSince
$wgLearnerMemberSince
Specify the difference engine to use.
Definition: DefaultSettings.php:9396
ChangesListSpecialPage\$rcSubpage
string $rcSubpage
Definition: ChangesListSpecialPage.php:70
ChangeTags\tagUsageStatistics
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
Definition: ChangeTags.php:1636
ChangesListSpecialPage\filterOnUserExperienceLevel
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.
Definition: ChangesListSpecialPage.php:1864
ChangesListSpecialPage\replaceOldOptions
replaceOldOptions(FormOptions $opts)
Replace old options with their structured UI equivalents.
Definition: ChangesListSpecialPage.php:1456
FormOptions
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:35
ChangesListSpecialPage\registerFiltersFromDefinitions
registerFiltersFromDefinitions(array $definition)
Register filters from a definition object.
Definition: ChangesListSpecialPage.php:1147
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:234
FormOptions\getChangedValues
getChangedValues()
Return options modified as an array ( name => value )
Definition: FormOptions.php:308
ChangesListSpecialPage\setBottomText
setBottomText(FormOptions $opts)
Send the text to be displayed after the options.
Definition: ChangesListSpecialPage.php:1728
ChangeTags\tagLongDescriptionMessage
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:280
ChangesListSpecialPage\validateOptions
validateOptions(FormOptions $opts)
Validate a FormOptions object generated by getDefaultOptions() with values already populated.
Definition: ChangesListSpecialPage.php:1375
ChangesListSpecialPage\registerFilterGroup
registerFilterGroup(ChangesListFilterGroup $group)
Register a structured changes list filter group.
Definition: ChangesListSpecialPage.php:1251
ChangesListStringOptionsFilterGroup
Represents a filter group with multiple string options.
Definition: ChangesListStringOptionsFilterGroup.php:37
ChangesListSpecialPage\getOptions
getOptions()
Get the current FormOptions for this request.
Definition: ChangesListSpecialPage.php:1042
ChangesListSpecialPage\getLinkDays
getLinkDays()
Definition: ChangesListSpecialPage.php:784
SpecialPage\outputHeader
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
Definition: SpecialPage.php:707
SpecialPage\including
including( $x=null)
Whether the special page is being evaluated via transclusion.
Definition: SpecialPage.php:265
ChangesListFilter\getConflictingFilters
getConflictingFilters()
Get filters conflicting with this filter.
Definition: ChangesListFilter.php:430
ChangesListSpecialPage\$limitPreferenceName
static string $limitPreferenceName
Preference name for 'limit'.
Definition: ChangesListSpecialPage.php:61
ChangesListSpecialPage\getLegacyShowHideFilters
getLegacyShowHideFilters()
Definition: ChangesListSpecialPage.php:1173
wfArrayToCgi
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
Definition: GlobalFunctions.php:350
ChangesListSpecialPage\setTopText
setTopText(FormOptions $opts)
Send the text to be displayed before the options.
Definition: ChangesListSpecialPage.php:1717
ChangesListSpecialPage\getRcFiltersConfigSummary
static getRcFiltersConfigSummary(ResourceLoaderContext $context)
Get essential data about getRcFiltersConfigVars() for change detection.
Definition: ChangesListSpecialPage.php:872