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' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
132  &$query_options, &$join_conds
133  ) {
134  $conds['actor_user'] = null;
135  },
136  'isReplacedInStructuredUi' => true,
137 
138  ],
139  [
140  'name' => 'hideanons',
141  // rcshowhideanons-show, rcshowhideanons-hide,
142  // wlshowhideanons
143  'showHideSuffix' => 'showhideanons',
144  'default' => false,
145  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
146  &$query_options, &$join_conds
147  ) {
148  $conds[] = 'actor_user IS NOT NULL';
149  },
150  'isReplacedInStructuredUi' => true,
151  ]
152  ],
153  ],
154 
155  [
156  'name' => 'userExpLevel',
157  'title' => 'rcfilters-filtergroup-user-experience-level',
158  'class' => ChangesListStringOptionsFilterGroup::class,
159  'isFullCoverage' => true,
160  'filters' => [
161  [
162  'name' => 'unregistered',
163  'label' => 'rcfilters-filter-user-experience-level-unregistered-label',
164  'description' => 'rcfilters-filter-user-experience-level-unregistered-description',
165  'cssClassSuffix' => 'user-unregistered',
166  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
167  return !$rc->getAttribute( 'rc_user' );
168  }
169  ],
170  [
171  'name' => 'registered',
172  'label' => 'rcfilters-filter-user-experience-level-registered-label',
173  'description' => 'rcfilters-filter-user-experience-level-registered-description',
174  'cssClassSuffix' => 'user-registered',
175  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
176  return $rc->getAttribute( 'rc_user' );
177  }
178  ],
179  [
180  'name' => 'newcomer',
181  'label' => 'rcfilters-filter-user-experience-level-newcomer-label',
182  'description' => 'rcfilters-filter-user-experience-level-newcomer-description',
183  'cssClassSuffix' => 'user-newcomer',
184  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
185  $performer = User::newFromIdentity( $rc->getPerformerIdentity() );
186  return $performer && $performer->isRegistered() &&
187  $performer->getExperienceLevel() === 'newcomer';
188  }
189  ],
190  [
191  'name' => 'learner',
192  'label' => 'rcfilters-filter-user-experience-level-learner-label',
193  'description' => 'rcfilters-filter-user-experience-level-learner-description',
194  'cssClassSuffix' => 'user-learner',
195  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
196  $performer = User::newFromIdentity( $rc->getPerformerIdentity() );
197  return $performer && $performer->isRegistered() &&
198  $performer->getExperienceLevel() === 'learner';
199  },
200  ],
201  [
202  'name' => 'experienced',
203  'label' => 'rcfilters-filter-user-experience-level-experienced-label',
204  'description' => 'rcfilters-filter-user-experience-level-experienced-description',
205  'cssClassSuffix' => 'user-experienced',
206  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
207  $performer = User::newFromIdentity( $rc->getPerformerIdentity() );
208  return $performer && $performer->isRegistered() &&
209  $performer->getExperienceLevel() === 'experienced';
210  },
211  ]
212  ],
214  'queryCallable' => [ $this, 'filterOnUserExperienceLevel' ],
215  ],
216 
217  [
218  'name' => 'authorship',
219  'title' => 'rcfilters-filtergroup-authorship',
220  'class' => ChangesListBooleanFilterGroup::class,
221  'filters' => [
222  [
223  'name' => 'hidemyself',
224  'label' => 'rcfilters-filter-editsbyself-label',
225  'description' => 'rcfilters-filter-editsbyself-description',
226  // rcshowhidemine-show, rcshowhidemine-hide,
227  // wlshowhidemine
228  'showHideSuffix' => 'showhidemine',
229  'default' => false,
230  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
231  &$query_options, &$join_conds
232  ) {
234  $user = $ctx->getUser();
235  $conds[] = 'actor_name<>' . $dbr->addQuotes( $user->getName() );
236  },
237  'cssClassSuffix' => 'self',
238  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
239  return $ctx->getUser()->equals( $rc->getPerformerIdentity() );
240  },
241  ],
242  [
243  'name' => 'hidebyothers',
244  'label' => 'rcfilters-filter-editsbyother-label',
245  'description' => 'rcfilters-filter-editsbyother-description',
246  'default' => false,
247  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
248  &$query_options, &$join_conds
249  ) {
251  $user = $ctx->getUser();
252  if ( $user->isAnon() ) {
253  $conds['actor_name'] = $user->getName();
254  } else {
255  $conds['actor_user'] = $user->getId();
256  }
257  },
258  'cssClassSuffix' => 'others',
259  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
260  return !$ctx->getUser()->equals( $rc->getPerformerIdentity() );
261  },
262  ]
263  ]
264  ],
265 
266  [
267  'name' => 'automated',
268  'title' => 'rcfilters-filtergroup-automated',
269  'class' => ChangesListBooleanFilterGroup::class,
270  'filters' => [
271  [
272  'name' => 'hidebots',
273  'label' => 'rcfilters-filter-bots-label',
274  'description' => 'rcfilters-filter-bots-description',
275  // rcshowhidebots-show, rcshowhidebots-hide,
276  // wlshowhidebots
277  'showHideSuffix' => 'showhidebots',
278  'default' => false,
279  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
280  &$query_options, &$join_conds
281  ) {
282  $conds['rc_bot'] = 0;
283  },
284  'cssClassSuffix' => 'bot',
285  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
286  return $rc->getAttribute( 'rc_bot' );
287  },
288  ],
289  [
290  'name' => 'hidehumans',
291  'label' => 'rcfilters-filter-humans-label',
292  'description' => 'rcfilters-filter-humans-description',
293  'default' => false,
294  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
295  &$query_options, &$join_conds
296  ) {
297  $conds['rc_bot'] = 1;
298  },
299  'cssClassSuffix' => 'human',
300  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
301  return !$rc->getAttribute( 'rc_bot' );
302  },
303  ]
304  ]
305  ],
306 
307  // significance (conditional)
308 
309  [
310  'name' => 'significance',
311  'title' => 'rcfilters-filtergroup-significance',
312  'class' => ChangesListBooleanFilterGroup::class,
313  'priority' => -6,
314  'filters' => [
315  [
316  'name' => 'hideminor',
317  'label' => 'rcfilters-filter-minor-label',
318  'description' => 'rcfilters-filter-minor-description',
319  // rcshowhideminor-show, rcshowhideminor-hide,
320  // wlshowhideminor
321  'showHideSuffix' => 'showhideminor',
322  'default' => false,
323  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
324  &$query_options, &$join_conds
325  ) {
326  $conds[] = 'rc_minor = 0';
327  },
328  'cssClassSuffix' => 'minor',
329  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
330  return $rc->getAttribute( 'rc_minor' );
331  }
332  ],
333  [
334  'name' => 'hidemajor',
335  'label' => 'rcfilters-filter-major-label',
336  'description' => 'rcfilters-filter-major-description',
337  'default' => false,
338  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
339  &$query_options, &$join_conds
340  ) {
341  $conds[] = 'rc_minor = 1';
342  },
343  'cssClassSuffix' => 'major',
344  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
345  return !$rc->getAttribute( 'rc_minor' );
346  }
347  ]
348  ]
349  ],
350 
351  [
352  'name' => 'lastRevision',
353  'title' => 'rcfilters-filtergroup-lastrevision',
354  'class' => ChangesListBooleanFilterGroup::class,
355  'priority' => -7,
356  'filters' => [
357  [
358  'name' => 'hidelastrevision',
359  'label' => 'rcfilters-filter-lastrevision-label',
360  'description' => 'rcfilters-filter-lastrevision-description',
361  'default' => false,
362  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
363  &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
364  $conds[] = $dbr->makeList(
365  [
366  'rc_this_oldid <> page_latest',
367  'rc_type' => $nonRevisionTypes,
368  ],
369  LIST_OR
370  );
371  },
372  'cssClassSuffix' => 'last',
373  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
374  return $rc->getAttribute( 'rc_this_oldid' ) === $rc->getAttribute( 'page_latest' );
375  }
376  ],
377  [
378  'name' => 'hidepreviousrevisions',
379  'label' => 'rcfilters-filter-previousrevision-label',
380  'description' => 'rcfilters-filter-previousrevision-description',
381  'default' => false,
382  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
383  &$query_options, &$join_conds ) use ( $nonRevisionTypes ) {
384  $conds[] = $dbr->makeList(
385  [
386  'rc_this_oldid = page_latest',
387  'rc_type' => $nonRevisionTypes,
388  ],
389  LIST_OR
390  );
391  },
392  'cssClassSuffix' => 'previous',
393  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
394  return $rc->getAttribute( 'rc_this_oldid' ) !== $rc->getAttribute( 'page_latest' );
395  }
396  ]
397  ]
398  ],
399 
400  // With extensions, there can be change types that will not be hidden by any of these.
401  [
402  'name' => 'changeType',
403  'title' => 'rcfilters-filtergroup-changetype',
404  'class' => ChangesListBooleanFilterGroup::class,
405  'priority' => -8,
406  'filters' => [
407  [
408  'name' => 'hidepageedits',
409  'label' => 'rcfilters-filter-pageedits-label',
410  'description' => 'rcfilters-filter-pageedits-description',
411  'default' => false,
412  'priority' => -2,
413  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
414  &$query_options, &$join_conds
415  ) {
416  $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_EDIT );
417  },
418  'cssClassSuffix' => 'src-mw-edit',
419  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
420  return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_EDIT;
421  },
422  ],
423  [
424  'name' => 'hidenewpages',
425  'label' => 'rcfilters-filter-newpages-label',
426  'description' => 'rcfilters-filter-newpages-description',
427  'default' => false,
428  'priority' => -3,
429  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
430  &$query_options, &$join_conds
431  ) {
432  $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_NEW );
433  },
434  'cssClassSuffix' => 'src-mw-new',
435  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
436  return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW;
437  },
438  ],
439 
440  // hidecategorization
441 
442  [
443  'name' => 'hidelog',
444  'label' => 'rcfilters-filter-logactions-label',
445  'description' => 'rcfilters-filter-logactions-description',
446  'default' => false,
447  'priority' => -5,
448  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
449  &$query_options, &$join_conds
450  ) {
451  $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_LOG );
452  },
453  'cssClassSuffix' => 'src-mw-log',
454  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
455  return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG;
456  }
457  ],
458  ],
459  ],
460 
461  ];
462 
463  $this->legacyReviewStatusFilterGroupDefinition = [
464  [
465  'name' => 'legacyReviewStatus',
466  'title' => 'rcfilters-filtergroup-reviewstatus',
467  'class' => ChangesListBooleanFilterGroup::class,
468  'filters' => [
469  [
470  'name' => 'hidepatrolled',
471  // rcshowhidepatr-show, rcshowhidepatr-hide
472  // wlshowhidepatr
473  'showHideSuffix' => 'showhidepatr',
474  'default' => false,
475  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
476  &$query_options, &$join_conds
477  ) {
478  $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
479  },
480  'isReplacedInStructuredUi' => true,
481  ],
482  [
483  'name' => 'hideunpatrolled',
484  'default' => false,
485  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
486  &$query_options, &$join_conds
487  ) {
488  $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
489  },
490  'isReplacedInStructuredUi' => true,
491  ],
492  ],
493  ]
494  ];
495 
496  $this->reviewStatusFilterGroupDefinition = [
497  [
498  'name' => 'reviewStatus',
499  'title' => 'rcfilters-filtergroup-reviewstatus',
500  'class' => ChangesListStringOptionsFilterGroup::class,
501  'isFullCoverage' => true,
502  'priority' => -5,
503  'filters' => [
504  [
505  'name' => 'unpatrolled',
506  'label' => 'rcfilters-filter-reviewstatus-unpatrolled-label',
507  'description' => 'rcfilters-filter-reviewstatus-unpatrolled-description',
508  'cssClassSuffix' => 'reviewstatus-unpatrolled',
509  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
510  return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED;
511  },
512  ],
513  [
514  'name' => 'manual',
515  'label' => 'rcfilters-filter-reviewstatus-manual-label',
516  'description' => 'rcfilters-filter-reviewstatus-manual-description',
517  'cssClassSuffix' => 'reviewstatus-manual',
518  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
519  return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_PATROLLED;
520  },
521  ],
522  [
523  'name' => 'auto',
524  'label' => 'rcfilters-filter-reviewstatus-auto-label',
525  'description' => 'rcfilters-filter-reviewstatus-auto-description',
526  'cssClassSuffix' => 'reviewstatus-auto',
527  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
528  return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED;
529  },
530  ],
531  ],
533  'queryCallable' => static function ( $specialPageClassName, $ctx, $dbr,
534  &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected
535  ) {
536  if ( $selected === [] ) {
537  return;
538  }
539  $rcPatrolledValues = [
540  'unpatrolled' => RecentChange::PRC_UNPATROLLED,
541  'manual' => RecentChange::PRC_PATROLLED,
543  ];
544  // e.g. rc_patrolled IN (0, 2)
545  $conds['rc_patrolled'] = array_map( static function ( $s ) use ( $rcPatrolledValues ) {
546  return $rcPatrolledValues[ $s ];
547  }, $selected );
548  }
549  ]
550  ];
551 
552  $this->hideCategorizationFilterDefinition = [
553  'name' => 'hidecategorization',
554  'label' => 'rcfilters-filter-categorization-label',
555  'description' => 'rcfilters-filter-categorization-description',
556  // rcshowhidecategorization-show, rcshowhidecategorization-hide.
557  // wlshowhidecategorization
558  'showHideSuffix' => 'showhidecategorization',
559  'default' => false,
560  'priority' => -4,
561  'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
562  &$query_options, &$join_conds
563  ) {
564  $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE );
565  },
566  'cssClassSuffix' => 'src-mw-categorize',
567  'isRowApplicableCallable' => static function ( $ctx, $rc ) {
568  return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE;
569  },
570  ];
571  }
572 
578  protected function areFiltersInConflict() {
579  $opts = $this->getOptions();
581  foreach ( $this->getFilterGroups() as $group ) {
582  if ( $group->getConflictingGroups() ) {
583  wfLogWarning(
584  $group->getName() .
585  " specifies conflicts with other groups but these are not supported yet."
586  );
587  }
588 
590  foreach ( $group->getConflictingFilters() as $conflictingFilter ) {
591  if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) {
592  return true;
593  }
594  }
595 
597  foreach ( $group->getFilters() as $filter ) {
599  foreach ( $filter->getConflictingFilters() as $conflictingFilter ) {
600  if (
601  $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) &&
602  $filter->activelyInConflictWithFilter( $conflictingFilter, $opts )
603  ) {
604  return true;
605  }
606  }
607 
608  }
609 
610  }
611 
612  return false;
613  }
614 
618  public function execute( $subpage ) {
619  $this->rcSubpage = $subpage;
620 
621  $this->considerActionsForDefaultSavedQuery( $subpage );
622 
623  // Enable OOUI and module for the clock icon.
624  if ( $this->getConfig()->get( 'WatchlistExpiry' ) ) {
625  $this->getOutput()->enableOOUI();
626  $this->getOutput()->addModules( 'mediawiki.special.changeslist.watchlistexpiry' );
627  }
628 
629  $opts = $this->getOptions();
630  try {
631  $rows = $this->getRows();
632  if ( $rows === false ) {
633  $rows = new FakeResultWrapper( [] );
634  }
635 
636  // Used by Structured UI app to get results without MW chrome
637  if ( $this->getRequest()->getVal( 'action' ) === 'render' ) {
638  $this->getOutput()->setArticleBodyOnly( true );
639  }
640 
641  // Used by "live update" and "view newest" to check
642  // if there's new changes with minimal data transfer
643  if ( $this->getRequest()->getBool( 'peek' ) ) {
644  $code = $rows->numRows() > 0 ? 200 : 204;
645  $this->getOutput()->setStatusCode( $code );
646 
647  if ( $this->getUser()->isAnon() !==
648  $this->getRequest()->getFuzzyBool( 'isAnon' )
649  ) {
650  $this->getOutput()->setStatusCode( 205 );
651  }
652 
653  return;
654  }
655 
656  $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
657  $batch = $linkBatchFactory->newLinkBatch();
658  foreach ( $rows as $row ) {
659  $batch->add( NS_USER, $row->rc_user_text );
660  $batch->add( NS_USER_TALK, $row->rc_user_text );
661  $batch->add( $row->rc_namespace, $row->rc_title );
662  if ( $row->rc_source === RecentChange::SRC_LOG ) {
663  $formatter = LogFormatter::newFromRow( $row );
664  foreach ( $formatter->getPreloadTitles() as $title ) {
665  $batch->addObj( $title );
666  }
667  }
668  }
669  $batch->execute();
670 
671  $this->setHeaders();
672  $this->outputHeader();
673  $this->addModules();
674  $this->webOutput( $rows, $opts );
675 
676  $rows->free();
677  } catch ( DBQueryTimeoutError $timeoutException ) {
678  MWExceptionHandler::logException( $timeoutException );
679 
680  $this->setHeaders();
681  $this->outputHeader();
682  $this->addModules();
683 
684  $this->getOutput()->setStatusCode( 500 );
685  $this->webOutputHeader( 0, $opts );
686  $this->outputTimeout();
687  }
688 
689  if ( $this->getConfig()->get( 'EnableWANCacheReaper' ) ) {
690  // Clean up any bad page entries for titles showing up in RC
692  $this->getDB(),
693  LoggerFactory::getInstance( 'objectcache' )
694  ) );
695  }
696 
697  $this->includeRcFiltersApp();
698  }
699 
707  protected function considerActionsForDefaultSavedQuery( $subpage ) {
708  if ( !$this->isStructuredFilterUiEnabled() || $this->including() ) {
709  return;
710  }
711 
712  $knownParams = $this->getRequest()->getValues(
713  ...array_keys( $this->getOptions()->getAllValues() )
714  );
715 
716  // HACK: Temporarily until we can properly define "sticky" filters and parameters,
717  // we need to exclude several parameters we know should not be counted towards preventing
718  // the loading of defaults.
719  $excludedParams = [ 'limit' => '', 'days' => '', 'enhanced' => '', 'from' => '' ];
720  $knownParams = array_diff_key( $knownParams, $excludedParams );
721 
722  if (
723  // If there are NO known parameters in the URL request
724  // (that are not excluded) then we need to check into loading
725  // the default saved query
726  count( $knownParams ) === 0
727  ) {
728  // Get the saved queries data and parse it
729  $savedQueries = FormatJson::decode(
730  $this->getUser()->getOption( static::$savedQueriesPreferenceName ),
731  true
732  );
733 
734  if ( $savedQueries && isset( $savedQueries[ 'default' ] ) ) {
735  // Only load queries that are 'version' 2, since those
736  // have parameter representation
737  if ( isset( $savedQueries[ 'version' ] ) && $savedQueries[ 'version' ] === '2' ) {
738  $savedQueryDefaultID = $savedQueries[ 'default' ];
739  $defaultQuery = $savedQueries[ 'queries' ][ $savedQueryDefaultID ][ 'data' ];
740 
741  // Build the entire parameter list
742  $query = array_merge(
743  $defaultQuery[ 'params' ],
744  $defaultQuery[ 'highlights' ],
745  [
746  'urlversion' => '2',
747  ]
748  );
749  // Add to the query any parameters that we may have ignored before
750  // but are still valid and requested in the URL
751  $query = array_merge( $this->getRequest()->getValues(), $query );
752  unset( $query[ 'title' ] );
753  $this->getOutput()->redirect( $this->getPageTitle( $subpage )->getCanonicalURL( $query ) );
754  } else {
755  // There's a default, but the version is not 2, and the server can't
756  // actually recognize the query itself. This happens if it is before
757  // the conversion, so we need to tell the UI to reload saved query as
758  // it does the conversion to version 2
759  $this->getOutput()->addJsConfigVars(
760  'wgStructuredChangeFiltersDefaultSavedQueryExists',
761  true
762  );
763 
764  // Add the class that tells the frontend it is still loading
765  // another query
766  $this->getOutput()->addBodyClasses( 'mw-rcfilters-ui-loading' );
767  }
768  }
769  }
770  }
771 
777  protected function getLinkDays() {
778  $linkDays = $this->getConfig()->get( 'RCLinkDays' );
779  $filterByAge = $this->getConfig()->get( 'RCFilterByAge' );
780  $maxAge = $this->getConfig()->get( 'RCMaxAge' );
781  if ( $filterByAge ) {
782  // Trim it to only links which are within $wgRCMaxAge.
783  // Note that we allow one link higher than the max for things like
784  // "age 56 days" being accessible through the "60 days" link.
785  sort( $linkDays );
786 
787  $maxAgeDays = $maxAge / ( 3600 * 24 );
788  foreach ( $linkDays as $i => $days ) {
789  if ( $days >= $maxAgeDays ) {
790  array_splice( $linkDays, $i + 1 );
791  break;
792  }
793  }
794  }
795 
796  return $linkDays;
797  }
798 
805  protected function includeRcFiltersApp() {
806  $out = $this->getOutput();
807  if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
808  $jsData = $this->getStructuredFilterJsData();
809  $messages = [];
810  foreach ( $jsData['messageKeys'] as $key ) {
811  $messages[$key] = $this->msg( $key )->plain();
812  }
813 
814  $out->addBodyClasses( 'mw-rcfilters-enabled' );
815  $collapsed = MediaWikiServices::getInstance()->getUserOptionsLookup()
816  ->getBoolOption( $this->getUser(), static::$collapsedPreferenceName );
817  if ( $collapsed ) {
818  $out->addBodyClasses( 'mw-rcfilters-collapsed' );
819  }
820 
821  // These config and message exports should be moved into a ResourceLoader data module (T201574)
822  $out->addJsConfigVars( 'wgStructuredChangeFilters', $jsData['groups'] );
823  $out->addJsConfigVars( 'wgStructuredChangeFiltersMessages', $messages );
824  $out->addJsConfigVars( 'wgStructuredChangeFiltersCollapsedState', $collapsed );
825 
826  $out->addJsConfigVars(
827  'StructuredChangeFiltersDisplayConfig',
828  [
829  'maxDays' => (int)$this->getConfig()->get( 'RCMaxAge' ) / ( 24 * 3600 ), // Translate to days
830  'limitArray' => $this->getConfig()->get( 'RCLinkLimits' ),
831  'limitDefault' => $this->getDefaultLimit(),
832  'daysArray' => $this->getLinkDays(),
833  'daysDefault' => $this->getDefaultDays(),
834  ]
835  );
836 
837  $out->addJsConfigVars(
838  'wgStructuredChangeFiltersSavedQueriesPreferenceName',
839  static::$savedQueriesPreferenceName
840  );
841  $out->addJsConfigVars(
842  'wgStructuredChangeFiltersLimitPreferenceName',
843  static::$limitPreferenceName
844  );
845  $out->addJsConfigVars(
846  'wgStructuredChangeFiltersDaysPreferenceName',
847  static::$daysPreferenceName
848  );
849  $out->addJsConfigVars(
850  'wgStructuredChangeFiltersCollapsedPreferenceName',
851  static::$collapsedPreferenceName
852  );
853  } else {
854  $out->addBodyClasses( 'mw-rcfilters-disabled' );
855  }
856  }
857 
866  public static function getRcFiltersConfigSummary( ResourceLoaderContext $context ) {
867  return [
868  // Reduce version computation by avoiding Message parsing
869  'RCFiltersChangeTags' => self::getChangeTagListSummary( $context ),
870  'StructuredChangeFiltersEditWatchlistUrl' =>
871  SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
872  ];
873  }
874 
882  public static function getRcFiltersConfigVars( ResourceLoaderContext $context ) {
883  return [
884  'RCFiltersChangeTags' => self::getChangeTagList( $context ),
885  'StructuredChangeFiltersEditWatchlistUrl' =>
886  SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
887  ];
888  }
889 
910  protected static function getChangeTagListSummary( ResourceLoaderContext $context ) {
911  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
912  return $cache->getWithSetCallback(
913  $cache->makeKey( 'ChangesListSpecialPage-changeTagListSummary', $context->getLanguage() ),
914  WANObjectCache::TTL_DAY,
915  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $context ) {
916  $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
917  $softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
918 
919  $tagStats = ChangeTags::tagUsageStatistics();
920  $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags, $tagStats );
921 
922  $result = [];
923  foreach ( $tagHitCounts as $tagName => $hits ) {
924  if (
925  (
926  // Only get active tags
927  isset( $explicitlyDefinedTags[ $tagName ] ) ||
928  isset( $softwareActivatedTags[ $tagName ] )
929  ) &&
930  // Only get tags with more than 0 hits
931  $hits > 0
932  ) {
933  $labelMsg = ChangeTags::tagShortDescriptionMessage( $tagName, $context );
934  if ( $labelMsg === false ) {
935  // Tag is hidden, skip it
936  continue;
937  }
938  $descriptionMsg = ChangeTags::tagLongDescriptionMessage( $tagName, $context );
939  $result[] = [
940  'name' => $tagName,
941  'labelMsg' => $labelMsg,
942  'label' => $labelMsg->plain(),
943  'descriptionMsg' => $descriptionMsg,
944  'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
945  'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
946  'hits' => $hits,
947  ];
948  }
949  }
950  return $result;
951  }
952  );
953  }
954 
968  protected static function getChangeTagList( ResourceLoaderContext $context ) {
969  $tags = self::getChangeTagListSummary( $context );
970  $language = MediaWikiServices::getInstance()->getLanguageFactory()
971  ->getLanguage( $context->getLanguage() );
972  foreach ( $tags as &$tagInfo ) {
973  $tagInfo['label'] = Sanitizer::stripAllTags( $tagInfo['labelMsg']->parse() );
974  $tagInfo['description'] = $tagInfo['descriptionMsg'] ?
975  $language->truncateForVisual(
976  Sanitizer::stripAllTags( $tagInfo['descriptionMsg']->parse() ),
977  self::TAG_DESC_CHARACTER_LIMIT
978  ) :
979  '';
980  unset( $tagInfo['labelMsg'] );
981  unset( $tagInfo['descriptionMsg'] );
982  }
983 
984  // Instead of sorting by hit count (disabled for now), sort by display name
985  usort( $tags, static function ( $a, $b ) {
986  return strcasecmp( $a['label'], $b['label'] );
987  } );
988  return $tags;
989  }
990 
994  protected function outputNoResults() {
995  $this->getOutput()->addHTML(
996  '<div class="mw-changeslist-empty">' .
997  $this->msg( 'recentchanges-noresult' )->parse() .
998  '</div>'
999  );
1000  }
1001 
1005  protected function outputTimeout() {
1006  $this->getOutput()->addHTML(
1007  '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
1008  $this->msg( 'recentchanges-timeout' )->parse() .
1009  '</div>'
1010  );
1011  }
1012 
1018  public function getRows() {
1019  $opts = $this->getOptions();
1020 
1021  $tables = [];
1022  $fields = [];
1023  $conds = [];
1024  $query_options = [];
1025  $join_conds = [];
1026  $this->buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1027 
1028  return $this->doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts );
1029  }
1030 
1036  public function getOptions() {
1037  if ( $this->rcOptions === null ) {
1038  $this->rcOptions = $this->setup( $this->rcSubpage );
1039  }
1040 
1041  return $this->rcOptions;
1042  }
1043 
1053  protected function registerFilters() {
1054  $this->registerFiltersFromDefinitions( $this->filterGroupDefinitions );
1055 
1056  // Make sure this is not being transcluded (we don't want to show this
1057  // information to all users just because the user that saves the edit can
1058  // patrol or is logged in)
1059  if ( !$this->including() && $this->getUser()->useRCPatrol() ) {
1060  $this->registerFiltersFromDefinitions( $this->legacyReviewStatusFilterGroupDefinition );
1061  $this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition );
1062  }
1063 
1064  $changeTypeGroup = $this->getFilterGroup( 'changeType' );
1065 
1066  if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) {
1067  $transformedHideCategorizationDef = $this->transformFilterDefinition(
1068  $this->hideCategorizationFilterDefinition
1069  );
1070 
1071  $transformedHideCategorizationDef['group'] = $changeTypeGroup;
1072 
1073  $hideCategorization = new ChangesListBooleanFilter(
1074  $transformedHideCategorizationDef
1075  );
1076  }
1077 
1078  $this->getHookRunner()->onChangesListSpecialPageStructuredFilters( $this );
1079 
1080  $this->registerFiltersFromDefinitions( [] );
1081 
1082  $userExperienceLevel = $this->getFilterGroup( 'userExpLevel' );
1083  $registered = $userExperienceLevel->getFilter( 'registered' );
1084  $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'newcomer' ) );
1085  $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'learner' ) );
1086  $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'experienced' ) );
1087 
1088  $categoryFilter = $changeTypeGroup->getFilter( 'hidecategorization' );
1089  $logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' );
1090  $pagecreationFilter = $changeTypeGroup->getFilter( 'hidenewpages' );
1091 
1092  $significanceTypeGroup = $this->getFilterGroup( 'significance' );
1093  $hideMinorFilter = $significanceTypeGroup->getFilter( 'hideminor' );
1094 
1095  // categoryFilter is conditional; see registerFilters
1096  if ( $categoryFilter !== null ) {
1097  $hideMinorFilter->conflictsWith(
1098  $categoryFilter,
1099  'rcfilters-hideminor-conflicts-typeofchange-global',
1100  'rcfilters-hideminor-conflicts-typeofchange',
1101  'rcfilters-typeofchange-conflicts-hideminor'
1102  );
1103  }
1104  $hideMinorFilter->conflictsWith(
1105  $logactionsFilter,
1106  'rcfilters-hideminor-conflicts-typeofchange-global',
1107  'rcfilters-hideminor-conflicts-typeofchange',
1108  'rcfilters-typeofchange-conflicts-hideminor'
1109  );
1110  $hideMinorFilter->conflictsWith(
1111  $pagecreationFilter,
1112  'rcfilters-hideminor-conflicts-typeofchange-global',
1113  'rcfilters-hideminor-conflicts-typeofchange',
1114  'rcfilters-typeofchange-conflicts-hideminor'
1115  );
1116  }
1117 
1127  protected function transformFilterDefinition( array $filterDefinition ) {
1128  return $filterDefinition;
1129  }
1130 
1141  protected function registerFiltersFromDefinitions( array $definition ) {
1142  $autoFillPriority = -1;
1143  foreach ( $definition as $groupDefinition ) {
1144  if ( !isset( $groupDefinition['priority'] ) ) {
1145  $groupDefinition['priority'] = $autoFillPriority;
1146  } else {
1147  // If it's explicitly specified, start over the auto-fill
1148  $autoFillPriority = $groupDefinition['priority'];
1149  }
1150 
1151  $autoFillPriority--;
1152 
1153  $className = $groupDefinition['class'];
1154  unset( $groupDefinition['class'] );
1155 
1156  foreach ( $groupDefinition['filters'] as &$filterDefinition ) {
1157  $filterDefinition = $this->transformFilterDefinition( $filterDefinition );
1158  }
1159 
1160  $this->registerFilterGroup( new $className( $groupDefinition ) );
1161  }
1162  }
1163 
1167  protected function getLegacyShowHideFilters() {
1168  $filters = [];
1169  foreach ( $this->filterGroups as $group ) {
1170  if ( $group instanceof ChangesListBooleanFilterGroup ) {
1171  foreach ( $group->getFilters() as $key => $filter ) {
1172  if ( $filter->displaysOnUnstructuredUi() ) {
1173  $filters[ $key ] = $filter;
1174  }
1175  }
1176  }
1177  }
1178  return $filters;
1179  }
1180 
1189  public function setup( $parameters ) {
1190  $this->registerFilters();
1191 
1192  $opts = $this->getDefaultOptions();
1193 
1194  $opts = $this->fetchOptionsFromRequest( $opts );
1195 
1196  // Give precedence to subpage syntax
1197  if ( $parameters !== null ) {
1198  $this->parseParameters( $parameters, $opts );
1199  }
1200 
1201  $this->validateOptions( $opts );
1202 
1203  return $opts;
1204  }
1205 
1215  public function getDefaultOptions() {
1216  $opts = new FormOptions();
1217  $structuredUI = $this->isStructuredFilterUiEnabled();
1218  // If urlversion=2 is set, ignore the filter defaults and set them all to false/empty
1219  $useDefaults = $this->getRequest()->getInt( 'urlversion' ) !== 2;
1220 
1222  foreach ( $this->filterGroups as $filterGroup ) {
1223  $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
1224  }
1225 
1226  $opts->add( 'namespace', '', FormOptions::STRING );
1227  $opts->add( 'invert', false );
1228  $opts->add( 'associated', false );
1229  $opts->add( 'urlversion', 1 );
1230  $opts->add( 'tagfilter', '' );
1231 
1232  $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT );
1233  $opts->add( 'limit', $this->getDefaultLimit(), FormOptions::INT );
1234 
1235  $opts->add( 'from', '' );
1236 
1237  return $opts;
1238  }
1239 
1245  public function registerFilterGroup( ChangesListFilterGroup $group ) {
1246  $groupName = $group->getName();
1247 
1248  $this->filterGroups[$groupName] = $group;
1249  }
1250 
1256  protected function getFilterGroups() {
1257  return $this->filterGroups;
1258  }
1259 
1267  public function getFilterGroup( $groupName ) {
1268  return $this->filterGroups[$groupName] ?? null;
1269  }
1270 
1271  // Currently, this intentionally only includes filters that display
1272  // in the structured UI. This can be changed easily, though, if we want
1273  // to include data on filters that use the unstructured UI. messageKeys is a
1274  // special top-level value, with the value being an array of the message keys to
1275  // send to the client.
1276 
1284  public function getStructuredFilterJsData() {
1285  $output = [
1286  'groups' => [],
1287  'messageKeys' => [],
1288  ];
1289 
1290  usort( $this->filterGroups, static function ( $a, $b ) {
1291  return $b->getPriority() <=> $a->getPriority();
1292  } );
1293 
1294  foreach ( $this->filterGroups as $groupName => $group ) {
1295  $groupOutput = $group->getJsData();
1296  if ( $groupOutput !== null ) {
1297  $output['messageKeys'] = array_merge(
1298  $output['messageKeys'],
1299  $groupOutput['messageKeys']
1300  );
1301 
1302  unset( $groupOutput['messageKeys'] );
1303  $output['groups'][] = $groupOutput;
1304  }
1305  }
1306 
1307  return $output;
1308  }
1309 
1318  protected function fetchOptionsFromRequest( $opts ) {
1319  $opts->fetchValuesFromRequest( $this->getRequest() );
1320 
1321  return $opts;
1322  }
1323 
1330  public function parseParameters( $par, FormOptions $opts ) {
1331  $stringParameterNameSet = [];
1332  $hideParameterNameSet = [];
1333 
1334  // URL parameters can be per-group, like 'userExpLevel',
1335  // or per-filter, like 'hideminor'.
1336 
1337  foreach ( $this->filterGroups as $filterGroup ) {
1338  if ( $filterGroup instanceof ChangesListStringOptionsFilterGroup ) {
1339  $stringParameterNameSet[$filterGroup->getName()] = true;
1340  } elseif ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
1341  foreach ( $filterGroup->getFilters() as $filter ) {
1342  $hideParameterNameSet[$filter->getName()] = true;
1343  }
1344  }
1345  }
1346 
1347  $bits = preg_split( '/\s*,\s*/', trim( $par ) );
1348  foreach ( $bits as $bit ) {
1349  $m = [];
1350  if ( isset( $hideParameterNameSet[$bit] ) ) {
1351  // hidefoo => hidefoo=true
1352  $opts[$bit] = true;
1353  } elseif ( isset( $hideParameterNameSet["hide$bit"] ) ) {
1354  // foo => hidefoo=false
1355  $opts["hide$bit"] = false;
1356  } elseif ( preg_match( '/^(.*)=(.*)$/', $bit, $m ) ) {
1357  if ( isset( $stringParameterNameSet[$m[1]] ) ) {
1358  $opts[$m[1]] = $m[2];
1359  }
1360  }
1361  }
1362  }
1363 
1369  public function validateOptions( FormOptions $opts ) {
1370  $isContradictory = $this->fixContradictoryOptions( $opts );
1371  $isReplaced = $this->replaceOldOptions( $opts );
1372 
1373  if ( $isContradictory || $isReplaced ) {
1374  $query = wfArrayToCgi( $this->convertParamsForLink( $opts->getChangedValues() ) );
1375  $this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) );
1376  }
1377 
1378  $opts->validateIntBounds( 'limit', 0, 5000 );
1379  $opts->validateBounds( 'days', 0, $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
1380  }
1381 
1388  private function fixContradictoryOptions( FormOptions $opts ) {
1389  $fixed = $this->fixBackwardsCompatibilityOptions( $opts );
1390 
1391  foreach ( $this->filterGroups as $filterGroup ) {
1392  if ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
1393  $filters = $filterGroup->getFilters();
1394 
1395  if ( count( $filters ) === 1 ) {
1396  // legacy boolean filters should not be considered
1397  continue;
1398  }
1399 
1400  $allInGroupEnabled = array_reduce(
1401  $filters,
1402  static function ( $carry, $filter ) use ( $opts ) {
1403  return $carry && $opts[ $filter->getName() ];
1404  },
1405  /* initialValue */ count( $filters ) > 0
1406  );
1407 
1408  if ( $allInGroupEnabled ) {
1409  foreach ( $filters as $filter ) {
1410  $opts[ $filter->getName() ] = false;
1411  }
1412 
1413  $fixed = true;
1414  }
1415  }
1416  }
1417 
1418  return $fixed;
1419  }
1420 
1430  private function fixBackwardsCompatibilityOptions( FormOptions $opts ) {
1431  if ( $opts['hideanons'] && $opts['hideliu'] ) {
1432  $opts->reset( 'hideanons' );
1433  if ( !$opts['hidebots'] ) {
1434  $opts->reset( 'hideliu' );
1435  $opts['hidehumans'] = 1;
1436  }
1437 
1438  return true;
1439  }
1440 
1441  return false;
1442  }
1443 
1450  public function replaceOldOptions( FormOptions $opts ) {
1451  if ( !$this->isStructuredFilterUiEnabled() ) {
1452  return false;
1453  }
1454 
1455  $changed = false;
1456 
1457  // At this point 'hideanons' and 'hideliu' cannot be both true,
1458  // because fixBackwardsCompatibilityOptions resets (at least) 'hideanons' in such case
1459  if ( $opts[ 'hideanons' ] ) {
1460  $opts->reset( 'hideanons' );
1461  $opts[ 'userExpLevel' ] = 'registered';
1462  $changed = true;
1463  }
1464 
1465  if ( $opts[ 'hideliu' ] ) {
1466  $opts->reset( 'hideliu' );
1467  $opts[ 'userExpLevel' ] = 'unregistered';
1468  $changed = true;
1469  }
1470 
1471  if ( $this->getFilterGroup( 'legacyReviewStatus' ) ) {
1472  if ( $opts[ 'hidepatrolled' ] ) {
1473  $opts->reset( 'hidepatrolled' );
1474  $opts[ 'reviewStatus' ] = 'unpatrolled';
1475  $changed = true;
1476  }
1477 
1478  if ( $opts[ 'hideunpatrolled' ] ) {
1479  $opts->reset( 'hideunpatrolled' );
1480  $opts[ 'reviewStatus' ] = implode(
1482  [ 'manual', 'auto' ]
1483  );
1484  $changed = true;
1485  }
1486  }
1487 
1488  return $changed;
1489  }
1490 
1499  protected function convertParamsForLink( $params ) {
1500  foreach ( $params as &$value ) {
1501  if ( $value === false ) {
1502  $value = '0';
1503  }
1504  }
1505  unset( $value );
1506  return $params;
1507  }
1508 
1520  protected function buildQuery( &$tables, &$fields, &$conds, &$query_options,
1521  &$join_conds, FormOptions $opts
1522  ) {
1523  $dbr = $this->getDB();
1524  $isStructuredUI = $this->isStructuredFilterUiEnabled();
1525 
1527  foreach ( $this->filterGroups as $filterGroup ) {
1528  $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds,
1529  $query_options, $join_conds, $opts, $isStructuredUI );
1530  }
1531 
1532  // Namespace filtering
1533  if ( $opts[ 'namespace' ] !== '' ) {
1534  $namespaces = explode( ';', $opts[ 'namespace' ] );
1535 
1536  $namespaces = $this->expandSymbolicNamespaceFilters( $namespaces );
1537 
1538  $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1539  $namespaces = array_filter(
1540  $namespaces,
1541  static function ( $ns ) use ( $namespaceInfo ) {
1542  return $namespaceInfo->exists( $ns );
1543  }
1544  );
1545 
1546  if ( $namespaces !== [] ) {
1547  // Namespaces are just ints, use them as int when acting with the database
1548  $namespaces = array_map( 'intval', $namespaces );
1549 
1550  if ( $opts[ 'associated' ] ) {
1551  $associatedNamespaces = array_map(
1552  static function ( $ns ) use ( $namespaceInfo ){
1553  return $namespaceInfo->getAssociated( $ns );
1554  },
1555  array_filter(
1556  $namespaces,
1557  static function ( $ns ) use ( $namespaceInfo ) {
1558  return $namespaceInfo->hasTalkNamespace( $ns );
1559  }
1560  )
1561  );
1562  $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) );
1563  }
1564 
1565  if ( count( $namespaces ) === 1 ) {
1566  $operator = $opts[ 'invert' ] ? '!=' : '=';
1567  $value = $dbr->addQuotes( reset( $namespaces ) );
1568  } else {
1569  $operator = $opts[ 'invert' ] ? 'NOT IN' : 'IN';
1570  sort( $namespaces );
1571  $value = '(' . $dbr->makeList( $namespaces ) . ')';
1572  }
1573  $conds[] = "rc_namespace $operator $value";
1574  }
1575  }
1576 
1577  // Calculate cutoff
1578  $cutoff_unixtime = time() - $opts['days'] * 3600 * 24;
1579  $cutoff = $dbr->timestamp( $cutoff_unixtime );
1580 
1581  $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
1582  if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
1583  $cutoff = $dbr->timestamp( $opts['from'] );
1584  } else {
1585  $opts->reset( 'from' );
1586  }
1587 
1588  $conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
1589  }
1590 
1602  protected function doMainQuery( $tables, $fields, $conds,
1603  $query_options, $join_conds, FormOptions $opts
1604  ) {
1605  $rcQuery = RecentChange::getQueryInfo();
1606  $tables = array_merge( $tables, $rcQuery['tables'] );
1607  $fields = array_merge( $rcQuery['fields'], $fields );
1608  $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
1609 
1611  $tables,
1612  $fields,
1613  $conds,
1614  $join_conds,
1615  $query_options,
1616  ''
1617  );
1618 
1619  if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
1620  $opts )
1621  ) {
1622  return false;
1623  }
1624 
1625  $dbr = $this->getDB();
1626 
1627  return $dbr->select(
1628  $tables,
1629  $fields,
1630  $conds,
1631  __METHOD__,
1632  $query_options,
1633  $join_conds
1634  );
1635  }
1636 
1637  protected function runMainQueryHook( &$tables, &$fields, &$conds,
1638  &$query_options, &$join_conds, $opts
1639  ) {
1640  return $this->getHookRunner()->onChangesListSpecialPageQuery(
1641  $this->getName(), $tables, $fields, $conds, $query_options, $join_conds, $opts );
1642  }
1643 
1649  protected function getDB() {
1650  return wfGetDB( DB_REPLICA );
1651  }
1652 
1659  private function webOutputHeader( $rowCount, $opts ) {
1660  if ( !$this->including() ) {
1661  $this->outputFeedLinks();
1662  $this->doHeader( $opts, $rowCount );
1663  }
1664  }
1665 
1672  public function webOutput( $rows, $opts ) {
1673  $this->webOutputHeader( $rows->numRows(), $opts );
1674 
1675  $this->outputChangesList( $rows, $opts );
1676  }
1677 
1678  public function outputFeedLinks() {
1679  // nothing by default
1680  }
1681 
1688  abstract public function outputChangesList( $rows, $opts );
1689 
1696  public function doHeader( $opts, $numRows ) {
1697  $this->setTopText( $opts );
1698 
1699  // @todo Lots of stuff should be done here.
1700 
1701  $this->setBottomText( $opts );
1702  }
1703 
1711  public function setTopText( FormOptions $opts ) {
1712  // nothing by default
1713  }
1714 
1722  public function setBottomText( FormOptions $opts ) {
1723  // nothing by default
1724  }
1725 
1735  public function getExtraOptions( $opts ) {
1736  return [];
1737  }
1738 
1744  public function makeLegend() {
1745  $context = $this->getContext();
1746  $user = $context->getUser();
1747  # The legend showing what the letters and stuff mean
1748  $legend = Html::openElement( 'dl' ) . "\n";
1749  # Iterates through them and gets the messages for both letter and tooltip
1750  $legendItems = $context->getConfig()->get( 'RecentChangesFlags' );
1751  if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
1752  unset( $legendItems['unpatrolled'] );
1753  }
1754  foreach ( $legendItems as $key => $item ) { # generate items of the legend
1755  $label = $item['legend'] ?? $item['title'];
1756  $letter = $item['letter'];
1757  $cssClass = $item['class'] ?? $key;
1758 
1759  $legend .= Html::element( 'dt',
1760  [ 'class' => $cssClass ], $context->msg( $letter )->text()
1761  ) . "\n" .
1762  Html::rawElement( 'dd',
1763  [ 'class' => Sanitizer::escapeClass( 'mw-changeslist-legend-' . $key ) ],
1764  $context->msg( $label )->parse()
1765  ) . "\n";
1766  }
1767  # (+-123)
1768  $legend .= Html::rawElement( 'dt',
1769  [ 'class' => 'mw-plusminus-pos' ],
1770  $context->msg( 'recentchanges-legend-plusminus' )->parse()
1771  ) . "\n";
1772  $legend .= Html::element(
1773  'dd',
1774  [ 'class' => 'mw-changeslist-legend-plusminus' ],
1775  $context->msg( 'recentchanges-label-plusminus' )->text()
1776  ) . "\n";
1777  // Watchlist expiry clock icon.
1778  if ( $context->getConfig()->get( 'WatchlistExpiry' ) ) {
1779  $widget = new IconWidget( [
1780  'icon' => 'clock',
1781  'classes' => [ 'mw-changesList-watchlistExpiry' ],
1782  ] );
1783  // Link the image to its label for assistive technologies.
1784  $watchlistLabelId = 'mw-changeslist-watchlistExpiry-label';
1785  $widget->getIconElement()->setAttributes( [
1786  'role' => 'img',
1787  'aria-labelledby' => $watchlistLabelId,
1788  ] );
1789  $legend .= Html::rawElement(
1790  'dt',
1791  [ 'class' => 'mw-changeslist-legend-watchlistexpiry' ],
1792  $widget
1793  );
1794  $legend .= Html::element(
1795  'dd',
1796  [ 'class' => 'mw-changeslist-legend-watchlistexpiry', 'id' => $watchlistLabelId ],
1797  $context->msg( 'recentchanges-legend-watchlistexpiry' )->text()
1798  );
1799  }
1800  $legend .= Html::closeElement( 'dl' ) . "\n";
1801 
1802  $legendHeading = $this->isStructuredFilterUiEnabled() ?
1803  $context->msg( 'rcfilters-legend-heading' )->parse() :
1804  $context->msg( 'recentchanges-legend-heading' )->parse();
1805 
1806  # Collapsible
1807  $collapsedState = $this->getRequest()->getCookie( 'changeslist-state' );
1808  $collapsedClass = $collapsedState === 'collapsed' ? ' mw-collapsed' : '';
1809 
1810  $legend =
1811  '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass . '">' .
1812  $legendHeading .
1813  '<div class="mw-collapsible-content">' . $legend . '</div>' .
1814  '</div>';
1815 
1816  return $legend;
1817  }
1818 
1822  protected function addModules() {
1823  $out = $this->getOutput();
1824  // Styles and behavior for the legend box (see makeLegend())
1825  $out->addModuleStyles( [
1826  'mediawiki.interface.helpers.styles',
1827  'mediawiki.special.changeslist.legend',
1828  'mediawiki.special.changeslist',
1829  ] );
1830  $out->addModules( 'mediawiki.special.changeslist.legend.js' );
1831 
1832  if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
1833  $out->addModules( 'mediawiki.rcfilters.filters.ui' );
1834  $out->addModuleStyles( 'mediawiki.rcfilters.filters.base.styles' );
1835  }
1836  }
1837 
1838  protected function getGroupName() {
1839  return 'changes';
1840  }
1841 
1858  public function filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr,
1859  &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0
1860  ) {
1861  global $wgLearnerEdits,
1865 
1866  $LEVEL_COUNT = 5;
1867 
1868  // If all levels are selected, don't filter
1869  if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
1870  return;
1871  }
1872 
1873  // both 'registered' and 'unregistered', experience levels, if any, are included in 'registered'
1874  if (
1875  in_array( 'registered', $selectedExpLevels ) &&
1876  in_array( 'unregistered', $selectedExpLevels )
1877  ) {
1878  return;
1879  }
1880 
1881  // 'registered' but not 'unregistered', experience levels, if any, are included in 'registered'
1882  if (
1883  in_array( 'registered', $selectedExpLevels ) &&
1884  !in_array( 'unregistered', $selectedExpLevels )
1885  ) {
1886  $conds[] = 'actor_user IS NOT NULL';
1887  return;
1888  }
1889 
1890  if ( $selectedExpLevels === [ 'unregistered' ] ) {
1891  $conds['actor_user'] = null;
1892  return;
1893  }
1894 
1895  $tables[] = 'user';
1896  $join_conds['user'] = [ 'LEFT JOIN', 'actor_user=user_id' ];
1897 
1898  if ( $now === 0 ) {
1899  $now = time();
1900  }
1901  $secondsPerDay = 86400;
1902  $learnerCutoff = $now - $wgLearnerMemberSince * $secondsPerDay;
1903  $experiencedUserCutoff = $now - $wgExperiencedUserMemberSince * $secondsPerDay;
1904 
1905  $aboveNewcomer = $dbr->makeList(
1906  [
1907  'user_editcount >= ' . intval( $wgLearnerEdits ),
1908  $dbr->makeList( [
1909  'user_registration IS NULL',
1910  'user_registration <= ' . $dbr->addQuotes( $dbr->timestamp( $learnerCutoff ) ),
1911  ], IDatabase::LIST_OR ),
1912  ],
1914  );
1915 
1916  $aboveLearner = $dbr->makeList(
1917  [
1918  'user_editcount >= ' . intval( $wgExperiencedUserEdits ),
1919  $dbr->makeList( [
1920  'user_registration IS NULL',
1921  'user_registration <= ' .
1922  $dbr->addQuotes( $dbr->timestamp( $experiencedUserCutoff ) ),
1923  ], IDatabase::LIST_OR ),
1924  ],
1926  );
1927 
1928  $conditions = [];
1929 
1930  if ( in_array( 'unregistered', $selectedExpLevels ) ) {
1931  $selectedExpLevels = array_diff( $selectedExpLevels, [ 'unregistered' ] );
1932  $conditions['actor_user'] = null;
1933  }
1934 
1935  if ( $selectedExpLevels === [ 'newcomer' ] ) {
1936  $conditions[] = "NOT ( $aboveNewcomer )";
1937  } elseif ( $selectedExpLevels === [ 'learner' ] ) {
1938  $conditions[] = $dbr->makeList(
1939  [ $aboveNewcomer, "NOT ( $aboveLearner )" ],
1941  );
1942  } elseif ( $selectedExpLevels === [ 'experienced' ] ) {
1943  $conditions[] = $aboveLearner;
1944  } elseif ( $selectedExpLevels === [ 'learner', 'newcomer' ] ) {
1945  $conditions[] = "NOT ( $aboveLearner )";
1946  } elseif ( $selectedExpLevels === [ 'experienced', 'newcomer' ] ) {
1947  $conditions[] = $dbr->makeList(
1948  [ "NOT ( $aboveNewcomer )", $aboveLearner ],
1950  );
1951  } elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) {
1952  $conditions[] = $aboveNewcomer;
1953  } elseif ( $selectedExpLevels === [ 'experienced', 'learner', 'newcomer' ] ) {
1954  $conditions[] = 'actor_user IS NOT NULL';
1955  }
1956 
1957  if ( count( $conditions ) > 1 ) {
1958  $conds[] = $dbr->makeList( $conditions, IDatabase::LIST_OR );
1959  } elseif ( count( $conditions ) === 1 ) {
1960  $conds[] = reset( $conditions );
1961  }
1962  }
1963 
1969  public function isStructuredFilterUiEnabled() {
1970  if ( $this->getRequest()->getBool( 'rcfilters' ) ) {
1971  return true;
1972  }
1973 
1974  return static::checkStructuredFilterUiEnabled( $this->getUser() );
1975  }
1976 
1984  public static function checkStructuredFilterUiEnabled( $user ) {
1985  if ( $user instanceof Config ) {
1986  wfDeprecated( __METHOD__ . ' with Config argument', '1.34' );
1987  $user = func_get_arg( 1 );
1988  }
1989  return !$user->getOption( 'rcenhancedfilters-disable' );
1990  }
1991 
1999  public function getDefaultLimit() {
2000  return $this->getUser()->getIntOption( static::$limitPreferenceName );
2001  }
2002 
2011  public function getDefaultDays() {
2012  return floatval( $this->getUser()->getOption( static::$daysPreferenceName ) );
2013  }
2014 
2015  private function expandSymbolicNamespaceFilters( array $namespaces ) {
2016  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2017  $symbolicFilters = [
2018  'all-contents' => $nsInfo->getSubjectNamespaces(),
2019  'all-discussions' => $nsInfo->getTalkNamespaces(),
2020  ];
2021  $additionalNamespaces = [];
2022  foreach ( $symbolicFilters as $name => $values ) {
2023  if ( in_array( $name, $namespaces ) ) {
2024  $additionalNamespaces = array_merge( $additionalNamespaces, $values );
2025  }
2026  }
2027  $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) );
2028  $namespaces = array_merge( $namespaces, $additionalNamespaces );
2029  return array_unique( $namespaces );
2030  }
2031 }
LIST_OR
const LIST_OR
Definition: Defines.php:46
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:743
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:34
RecentChange\getQueryInfo
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object.
Definition: RecentChange.php:257
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:911
ChangesListFilterGroup\getName
getName()
Definition: ChangesListFilterGroup.php:282
ChangesListSpecialPage\getExtraOptions
getExtraOptions( $opts)
Get options to be displayed in a form.
Definition: ChangesListSpecialPage.php:1735
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:707
RC_EDIT
const RC_EDIT
Definition: Defines.php:115
Sanitizer\stripAllTags
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
Definition: Sanitizer.php:1576
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:805
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:789
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:186
$wgExperiencedUserMemberSince
$wgExperiencedUserMemberSince
Number of days the user must exist before becoming "experienced".
Definition: DefaultSettings.php:5149
ChangesListSpecialPage\makeLegend
makeLegend()
Return the legend displayed within the fieldset.
Definition: ChangesListSpecialPage.php:1744
ChangesListSpecialPage\execute
execute( $subpage)
Definition: ChangesListSpecialPage.php:618
ChangesListSpecialPage\webOutputHeader
webOutputHeader( $rowCount, $opts)
Send header output to the OutputPage object, only called if not using feeds.
Definition: ChangesListSpecialPage.php:1659
ChangesListSpecialPage\fixContradictoryOptions
fixContradictoryOptions(FormOptions $opts)
Fix invalid options by resetting pairs that should never appear together.
Definition: ChangesListSpecialPage.php:1388
ChangesListSpecialPage\parseParameters
parseParameters( $par, FormOptions $opts)
Process $par and put options found in $opts.
Definition: ChangesListSpecialPage.php:1330
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1692
ChangesListSpecialPage\doMainQuery
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
Process the query.
Definition: ChangesListSpecialPage.php:1602
LIST_AND
const LIST_AND
Definition: Defines.php:43
ChangesListSpecialPage\expandSymbolicNamespaceFilters
expandSymbolicNamespaceFilters(array $namespaces)
Definition: ChangesListSpecialPage.php:2015
ChangesListSpecialPage\fixBackwardsCompatibilityOptions
fixBackwardsCompatibilityOptions(FormOptions $opts)
Fix a special case (hideanons=1 and hideliu=1) in a special way, for backwards compatibility.
Definition: ChangesListSpecialPage.php:1430
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:2011
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:107
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:972
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:1318
User\newFromIdentity
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:679
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1056
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:910
RecentChange\SRC_CATEGORIZE
const SRC_CATEGORIZE
Definition: RecentChange.php:89
RC_NEW
const RC_NEW
Definition: Defines.php:116
SpecialPage\getName
getName()
Get the name of this Special Page.
Definition: SpecialPage.php:179
Wikimedia\Rdbms\FakeResultWrapper
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Definition: FakeResultWrapper.php:12
ChangesListFilterGroup
Represents a filter group (used on ChangesListSpecialPage and descendants)
Definition: ChangesListFilterGroup.php:37
RecentChange\SRC_LOG
const SRC_LOG
Definition: RecentChange.php:87
ChangesListSpecialPage\outputNoResults
outputNoResults()
Add the "no results" message to the output.
Definition: ChangesListSpecialPage.php:994
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:1696
ChangesListSpecialPage\checkStructuredFilterUiEnabled
static checkStructuredFilterUiEnabled( $user)
Static method to check whether StructuredFilter UI is enabled for the given user.
Definition: ChangesListSpecialPage.php:1984
$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:249
$wgExperiencedUserEdits
$wgExperiencedUserEdits
Number of edits the user must have before becoming "experienced".
Definition: DefaultSettings.php:5143
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:316
MWExceptionHandler\logException
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
Definition: MWExceptionHandler.php:684
RC_LOG
const RC_LOG
Definition: Defines.php:117
ChangesListSpecialPage\webOutput
webOutput( $rows, $opts)
Send output to the OutputPage object, only called if not used feeds.
Definition: ChangesListSpecialPage.php:1672
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:891
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:1094
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:877
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:996
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:74
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:25
ChangesListSpecialPage\convertParamsForLink
convertParamsForLink( $params)
Convert parameters values from true/false to 1/0 so they are not omitted by wfArrayToCgi() T38524.
Definition: ChangesListSpecialPage.php:1499
ChangesListSpecialPage\getRcFiltersConfigVars
static getRcFiltersConfigVars(ResourceLoaderContext $context)
Get config vars to export with the mediawiki.rcfilters.filters.ui module.
Definition: ChangesListSpecialPage.php:882
ChangesListSpecialPage\getRows
getRows()
Get the database result for this special page instance.
Definition: ChangesListSpecialPage.php:1018
ChangesListSpecialPage\outputFeedLinks
outputFeedLinks()
Definition: ChangesListSpecialPage.php:1678
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2226
ChangesListSpecialPage\runMainQueryHook
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
Definition: ChangesListSpecialPage.php:1637
ChangesListSpecialPage\isStructuredFilterUiEnabled
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
Definition: ChangesListSpecialPage.php:1969
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:1284
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:1256
$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:617
SpecialPage\getUser
getUser()
Shortcut to get the User executing this instance.
Definition: SpecialPage.php:799
RecentChange\SRC_EDIT
const SRC_EDIT
Definition: RecentChange.php:85
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
ResourceLoaderContext\getLanguage
getLanguage()
Definition: ResourceLoaderContext.php:183
RecentChange\SRC_NEW
const SRC_NEW
Definition: RecentChange.php:86
ChangesListSpecialPage\buildQuery
buildQuery(&$tables, &$fields, &$conds, &$query_options, &$join_conds, FormOptions $opts)
Sets appropriate tables, fields, conditions, etc.
Definition: ChangesListSpecialPage.php:1520
ChangesListSpecialPage\getDefaultLimit
getDefaultLimit()
Get the default value of the number of changes to display when loading the result set.
Definition: ChangesListSpecialPage.php:1999
SpecialPage\getContext
getContext()
Gets the context this SpecialPage is executed in.
Definition: SpecialPage.php:763
RecentChange\PRC_PATROLLED
const PRC_PATROLLED
Definition: RecentChange.php:92
ChangeTags\listExplicitlyDefinedTags
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
Definition: ChangeTags.php:1606
ChangesListSpecialPage\$rcOptions
FormOptions $rcOptions
Definition: ChangesListSpecialPage.php:73
$s
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
Definition: mergeMessageFileList.php:206
ChangesListSpecialPage\registerFilters
registerFilters()
Register all filters and their groups (including those from hooks), plus handle conflicts and default...
Definition: ChangesListSpecialPage.php:1053
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:43
ChangesListSpecialPage\setup
setup( $parameters)
Register all the filters, including legacy hook-driven ones.
Definition: ChangesListSpecialPage.php:1189
ChangesListSpecialPage\getChangeTagList
static getChangeTagList(ResourceLoaderContext $context)
Get information about change tags to export to JS via getRcFiltersConfigVars().
Definition: ChangesListSpecialPage.php:968
ChangesListSpecialPage\getFilterGroup
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
Definition: ChangesListSpecialPage.php:1267
ChangesListSpecialPage\outputTimeout
outputTimeout()
Add the "timeout" message to the output.
Definition: ChangesListSpecialPage.php:1005
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:779
ChangesListStringOptionsFilterGroup\NONE
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
Definition: ChangesListStringOptionsFilterGroup.php:59
IContextSource\getUser
getUser()
NS_USER
const NS_USER
Definition: Defines.php:66
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:93
ChangeTags\listSoftwareActivatedTags
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
Definition: ChangeTags.php:1558
$wgLearnerEdits
$wgLearnerEdits
The following variables define 3 user experience levels:
Definition: DefaultSettings.php:5131
ChangesListSpecialPage\addModules
addModules()
Add page-specific modules.
Definition: ChangesListSpecialPage.php:1822
FormOptions\STRING
const STRING
String type, maps guessType() to WebRequest::getText()
Definition: FormOptions.php:43
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:58
ChangesListSpecialPage\getGroupName
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Definition: ChangesListSpecialPage.php:1838
ChangesListSpecialPage\areFiltersInConflict
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
Definition: ChangesListSpecialPage.php:578
ChangesListSpecialPage\getDefaultOptions
getDefaultOptions()
Get a FormOptions object containing the default options.
Definition: ChangesListSpecialPage.php:1215
ChangesListSpecialPage\transformFilterDefinition
transformFilterDefinition(array $filterDefinition)
Transforms filter definition to prepare it for constructor.
Definition: ChangesListSpecialPage.php:1127
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:119
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:1649
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:91
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:252
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
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:67
$wgLearnerMemberSince
$wgLearnerMemberSince
Number of days the user must exist before becoming a learner.
Definition: DefaultSettings.php:5137
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:1692
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:1858
ChangesListSpecialPage\replaceOldOptions
replaceOldOptions(FormOptions $opts)
Replace old options with their structured UI equivalents.
Definition: ChangesListSpecialPage.php:1450
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:1141
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
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:1722
ChangeTags\tagLongDescriptionMessage
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:300
ChangesListSpecialPage\validateOptions
validateOptions(FormOptions $opts)
Validate a FormOptions object generated by getDefaultOptions() with values already populated.
Definition: ChangesListSpecialPage.php:1369
ChangesListSpecialPage\registerFilterGroup
registerFilterGroup(ChangesListFilterGroup $group)
Register a structured changes list filter group.
Definition: ChangesListSpecialPage.php:1245
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:1036
ChangesListSpecialPage\getLinkDays
getLinkDays()
Definition: ChangesListSpecialPage.php:777
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:708
SpecialPage\including
including( $x=null)
Whether the special page is being evaluated via transclusion.
Definition: SpecialPage.php:266
ChangesListFilter\getConflictingFilters
getConflictingFilters()
Get filters conflicting with this filter.
Definition: ChangesListFilter.php:429
ChangesListSpecialPage\$limitPreferenceName
static string $limitPreferenceName
Preference name for 'limit'.
Definition: ChangesListSpecialPage.php:61
ChangesListSpecialPage\getLegacyShowHideFilters
getLegacyShowHideFilters()
Definition: ChangesListSpecialPage.php:1167
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:330
ChangesListSpecialPage\setTopText
setTopText(FormOptions $opts)
Send the text to be displayed before the options.
Definition: ChangesListSpecialPage.php:1711
ChangesListSpecialPage\getRcFiltersConfigSummary
static getRcFiltersConfigSummary(ResourceLoaderContext $context)
Get essential data about getRcFiltersConfigVars() for change detection.
Definition: ChangesListSpecialPage.php:866