MediaWiki  master
SpecialWatchlist.php
Go to the documentation of this file.
1 <?php
27 
35  protected static $savedQueriesPreferenceName = 'rcfilters-wl-saved-queries';
36  protected static $daysPreferenceName = 'watchlistdays';
37  protected static $limitPreferenceName = 'wllimit';
38  protected static $collapsedPreferenceName = 'rcfilters-wl-collapsed';
39 
41  private $maxDays;
43  private $watchStore;
44 
45  public function __construct( $page = 'Watchlist', $restriction = 'viewmywatchlist' ) {
46  parent::__construct( $page, $restriction );
47 
48  $this->maxDays = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 );
49  $this->watchStore = MediaWikiServices::getInstance()->getWatchedItemStore();
50  }
51 
52  public function doesWrites() {
53  return true;
54  }
55 
61  function execute( $subpage ) {
62  // Anons don't get a watchlist
63  $this->requireLogin( 'watchlistanontext' );
64 
65  $output = $this->getOutput();
66  $request = $this->getRequest();
67  $this->addHelpLink( 'Help:Watching pages' );
68  $output->addModuleStyles( [ 'mediawiki.special' ] );
69  $output->addModules( [
70  'mediawiki.special.recentchanges',
71  'mediawiki.special.watchlist',
72  ] );
73 
74  $mode = SpecialEditWatchlist::getMode( $request, $subpage );
75  if ( $mode !== false ) {
76  if ( $mode === SpecialEditWatchlist::EDIT_RAW ) {
77  $title = SpecialPage::getTitleFor( 'EditWatchlist', 'raw' );
78  } elseif ( $mode === SpecialEditWatchlist::EDIT_CLEAR ) {
79  $title = SpecialPage::getTitleFor( 'EditWatchlist', 'clear' );
80  } else {
81  $title = SpecialPage::getTitleFor( 'EditWatchlist' );
82  }
83 
84  $output->redirect( $title->getLocalURL() );
85 
86  return;
87  }
88 
89  $this->checkPermissions();
90 
91  $user = $this->getUser();
92  $opts = $this->getOptions();
93 
94  $config = $this->getConfig();
95  if ( ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) )
96  && $request->getVal( 'reset' )
97  && $request->wasPosted()
98  && $user->matchEditToken( $request->getVal( 'token' ) )
99  ) {
100  $user->clearAllNotifications();
101  $output->redirect( $this->getPageTitle()->getFullURL( $opts->getChangedValues() ) );
102 
103  return;
104  }
105 
106  parent::execute( $subpage );
107 
108  if ( $this->isStructuredFilterUiEnabled() ) {
109  $output->addModuleStyles( [ 'mediawiki.rcfilters.highlightCircles.seenunseen.styles' ] );
110  }
111  }
112 
116  public static function checkStructuredFilterUiEnabled( $user ) {
117  if ( $user instanceof Config ) {
118  wfDeprecated( __METHOD__ . ' with Config argument', '1.34' );
119  $user = func_get_arg( 1 );
120  }
121  return !$user->getOption( 'wlenhancedfilters-disable' );
122  }
123 
130  public function getSubpagesForPrefixSearch() {
131  return [
132  'clear',
133  'edit',
134  'raw',
135  ];
136  }
137 
141  protected function transformFilterDefinition( array $filterDefinition ) {
142  if ( isset( $filterDefinition['showHideSuffix'] ) ) {
143  $filterDefinition['showHide'] = 'wl' . $filterDefinition['showHideSuffix'];
144  }
145 
146  return $filterDefinition;
147  }
148 
153  protected function registerFilters() {
154  parent::registerFilters();
155 
156  // legacy 'extended' filter
158  'name' => 'extended-group',
159  'filters' => [
160  [
161  'name' => 'extended',
162  'isReplacedInStructuredUi' => true,
163  'activeValue' => false,
164  'default' => $this->getUser()->getBoolOption( 'extendwatchlist' ),
165  'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables,
166  &$fields, &$conds, &$query_options, &$join_conds ) {
167  $nonRevisionTypes = [ RC_LOG ];
168  Hooks::run( 'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
169  if ( $nonRevisionTypes ) {
170  $conds[] = $dbr->makeList(
171  [
172  'rc_this_oldid=page_latest',
173  'rc_type' => $nonRevisionTypes,
174  ],
175  LIST_OR
176  );
177  }
178  },
179  ]
180  ],
181 
182  ] ) );
183 
184  if ( $this->isStructuredFilterUiEnabled() ) {
185  $this->getFilterGroup( 'lastRevision' )
186  ->getFilter( 'hidepreviousrevisions' )
187  ->setDefault( !$this->getUser()->getBoolOption( 'extendwatchlist' ) );
188  }
189 
191  'name' => 'watchlistactivity',
192  'title' => 'rcfilters-filtergroup-watchlistactivity',
193  'class' => ChangesListStringOptionsFilterGroup::class,
194  'priority' => 3,
195  'isFullCoverage' => true,
196  'filters' => [
197  [
198  'name' => 'unseen',
199  'label' => 'rcfilters-filter-watchlistactivity-unseen-label',
200  'description' => 'rcfilters-filter-watchlistactivity-unseen-description',
201  'cssClassSuffix' => 'watchedunseen',
202  'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
203  return !$this->isChangeEffectivelySeen( $rc );
204  },
205  ],
206  [
207  'name' => 'seen',
208  'label' => 'rcfilters-filter-watchlistactivity-seen-label',
209  'description' => 'rcfilters-filter-watchlistactivity-seen-description',
210  'cssClassSuffix' => 'watchedseen',
211  'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
212  return $this->isChangeEffectivelySeen( $rc );
213  }
214  ],
215  ],
217  'queryCallable' => function (
218  $specialPageClassName,
219  $context,
220  IDatabase $dbr,
221  &$tables,
222  &$fields,
223  &$conds,
224  &$query_options,
225  &$join_conds,
226  $selectedValues
227  ) {
228  if ( $selectedValues === [ 'seen' ] ) {
229  $conds[] = $dbr->makeList( [
230  'wl_notificationtimestamp IS NULL',
231  'rc_timestamp < wl_notificationtimestamp'
232  ], LIST_OR );
233  } elseif ( $selectedValues === [ 'unseen' ] ) {
234  $conds[] = $dbr->makeList( [
235  'wl_notificationtimestamp IS NOT NULL',
236  'rc_timestamp >= wl_notificationtimestamp'
237  ], LIST_AND );
238  }
239  }
240  ] ) );
241 
242  $user = $this->getUser();
243 
244  $significance = $this->getFilterGroup( 'significance' );
245  $hideMinor = $significance->getFilter( 'hideminor' );
246  $hideMinor->setDefault( $user->getBoolOption( 'watchlisthideminor' ) );
247 
248  $automated = $this->getFilterGroup( 'automated' );
249  $hideBots = $automated->getFilter( 'hidebots' );
250  $hideBots->setDefault( $user->getBoolOption( 'watchlisthidebots' ) );
251 
252  $registration = $this->getFilterGroup( 'registration' );
253  $hideAnons = $registration->getFilter( 'hideanons' );
254  $hideAnons->setDefault( $user->getBoolOption( 'watchlisthideanons' ) );
255  $hideLiu = $registration->getFilter( 'hideliu' );
256  $hideLiu->setDefault( $user->getBoolOption( 'watchlisthideliu' ) );
257 
258  // Selecting both hideanons and hideliu on watchlist preferances
259  // gives mutually exclusive filters, so those are ignored
260  if ( $user->getBoolOption( 'watchlisthideanons' ) &&
261  !$user->getBoolOption( 'watchlisthideliu' )
262  ) {
263  $this->getFilterGroup( 'userExpLevel' )
264  ->setDefault( 'registered' );
265  }
266 
267  if ( $user->getBoolOption( 'watchlisthideliu' ) &&
268  !$user->getBoolOption( 'watchlisthideanons' )
269  ) {
270  $this->getFilterGroup( 'userExpLevel' )
271  ->setDefault( 'unregistered' );
272  }
273 
274  $reviewStatus = $this->getFilterGroup( 'reviewStatus' );
275  if ( $reviewStatus !== null ) {
276  // Conditional on feature being available and rights
277  if ( $user->getBoolOption( 'watchlisthidepatrolled' ) ) {
278  $reviewStatus->setDefault( 'unpatrolled' );
279  $legacyReviewStatus = $this->getFilterGroup( 'legacyReviewStatus' );
280  $legacyHidePatrolled = $legacyReviewStatus->getFilter( 'hidepatrolled' );
281  $legacyHidePatrolled->setDefault( true );
282  }
283  }
284 
285  $authorship = $this->getFilterGroup( 'authorship' );
286  $hideMyself = $authorship->getFilter( 'hidemyself' );
287  $hideMyself->setDefault( $user->getBoolOption( 'watchlisthideown' ) );
288 
289  $changeType = $this->getFilterGroup( 'changeType' );
290  $hideCategorization = $changeType->getFilter( 'hidecategorization' );
291  if ( $hideCategorization !== null ) {
292  // Conditional on feature being available
293  $hideCategorization->setDefault( $user->getBoolOption( 'watchlisthidecategorization' ) );
294  }
295  }
296 
306  protected function fetchOptionsFromRequest( $opts ) {
307  static $compatibilityMap = [
308  'hideMinor' => 'hideminor',
309  'hideBots' => 'hidebots',
310  'hideAnons' => 'hideanons',
311  'hideLiu' => 'hideliu',
312  'hidePatrolled' => 'hidepatrolled',
313  'hideOwn' => 'hidemyself',
314  ];
315 
316  $params = $this->getRequest()->getValues();
317  foreach ( $compatibilityMap as $from => $to ) {
318  if ( isset( $params[$from] ) ) {
319  $params[$to] = $params[$from];
320  unset( $params[$from] );
321  }
322  }
323 
324  if ( $this->getRequest()->getVal( 'action' ) == 'submit' ) {
325  $allBooleansFalse = [];
326 
327  // If the user submitted the form, start with a baseline of "all
328  // booleans are false", then change the ones they checked. This
329  // means we ignore the defaults.
330 
331  // This is how we handle the fact that HTML forms don't submit
332  // unchecked boxes.
333  foreach ( $this->getLegacyShowHideFilters() as $filter ) {
334  $allBooleansFalse[ $filter->getName() ] = false;
335  }
336 
337  $params = $params + $allBooleansFalse;
338  }
339 
340  // Not the prettiest way to achieve this… FormOptions internally depends on data sanitization
341  // methods defined on WebRequest and removing this dependency would cause some code duplication.
342  $request = new DerivativeRequest( $this->getRequest(), $params );
343  $opts->fetchValuesFromRequest( $request );
344 
345  return $opts;
346  }
347 
351  protected function doMainQuery( $tables, $fields, $conds, $query_options,
352  $join_conds, FormOptions $opts
353  ) {
354  $dbr = $this->getDB();
355  $user = $this->getUser();
356 
357  $rcQuery = RecentChange::getQueryInfo();
358  $tables = array_merge( $tables, $rcQuery['tables'], [ 'watchlist' ] );
359  $fields = array_merge( $rcQuery['fields'], $fields );
360 
361  $join_conds = array_merge(
362  [
363  'watchlist' => [
364  'JOIN',
365  [
366  'wl_user' => $user->getId(),
367  'wl_namespace=rc_namespace',
368  'wl_title=rc_title'
369  ],
370  ],
371  ],
372  $rcQuery['joins'],
373  $join_conds
374  );
375 
376  $tables[] = 'page';
377  $fields[] = 'page_latest';
378  $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
379 
380  $fields[] = 'wl_notificationtimestamp';
381 
382  // Log entries with DELETED_ACTION must not show up unless the user has
383  // the necessary rights.
384  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
385  if ( !$permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
386  $bitmask = LogPage::DELETED_ACTION;
387  } elseif ( !$permissionManager->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' ) ) {
389  } else {
390  $bitmask = 0;
391  }
392  if ( $bitmask ) {
393  $conds[] = $dbr->makeList( [
394  'rc_type != ' . RC_LOG,
395  $dbr->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
396  ], LIST_OR );
397  }
398 
399  $tagFilter = $opts['tagfilter'] ? explode( '|', $opts['tagfilter'] ) : [];
401  $tables,
402  $fields,
403  $conds,
404  $join_conds,
405  $query_options,
406  $tagFilter
407  );
408 
409  $this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts );
410 
411  if ( $this->areFiltersInConflict() ) {
412  return false;
413  }
414 
415  $orderByAndLimit = [
416  'ORDER BY' => 'rc_timestamp DESC',
417  'LIMIT' => $opts['limit']
418  ];
419  if ( in_array( 'DISTINCT', $query_options ) ) {
420  // ChangeTags::modifyDisplayQuery() adds DISTINCT when filtering on multiple tags.
421  // In order to prevent DISTINCT from causing query performance problems,
422  // we have to GROUP BY the primary key. This in turn requires us to add
423  // the primary key to the end of the ORDER BY, and the old ORDER BY to the
424  // start of the GROUP BY
425  $orderByAndLimit['ORDER BY'] = 'rc_timestamp DESC, rc_id DESC';
426  $orderByAndLimit['GROUP BY'] = 'rc_timestamp, rc_id';
427  }
428  // array_merge() is used intentionally here so that hooks can, should
429  // they so desire, override the ORDER BY / LIMIT condition(s)
430  $query_options = array_merge( $orderByAndLimit, $query_options );
431 
432  return $dbr->select(
433  $tables,
434  $fields,
435  $conds,
436  __METHOD__,
437  $query_options,
438  $join_conds
439  );
440  }
441 
447  protected function getDB() {
448  return wfGetDB( DB_REPLICA, 'watchlist' );
449  }
450 
454  public function outputFeedLinks() {
455  $user = $this->getUser();
456  $wlToken = $user->getTokenFromOption( 'watchlisttoken' );
457  if ( $wlToken ) {
458  $this->addFeedLinks( [
459  'action' => 'feedwatchlist',
460  'allrev' => 1,
461  'wlowner' => $user->getName(),
462  'wltoken' => $wlToken,
463  ] );
464  }
465  }
466 
473  public function outputChangesList( $rows, $opts ) {
474  $dbr = $this->getDB();
475  $user = $this->getUser();
476  $output = $this->getOutput();
477  $services = MediaWikiServices::getInstance();
478 
479  # Show a message about replica DB lag, if applicable
480  $lag = $dbr->getSessionLagStatus()['lag'];
481  if ( $lag > 0 ) {
482  $output->showLagWarning( $lag );
483  }
484 
485  # If no rows to display, show message before try to render the list
486  if ( $rows->numRows() == 0 ) {
487  $output->wrapWikiMsg(
488  "<div class='mw-changeslist-empty'>\n$1\n</div>", 'recentchanges-noresult'
489  );
490  return;
491  }
492 
493  $dbr->dataSeek( $rows, 0 );
494 
495  $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups );
496  $list->setWatchlistDivs();
497  $list->initChangesListRows( $rows );
498  if ( $user->getOption( 'watchlistunwatchlinks' ) ) {
499  $list->setChangeLinePrefixer( function ( RecentChange $rc, ChangesList $cl, $grouped ) {
500  // Don't show unwatch link if the line is a grouped log entry using EnhancedChangesList,
501  // since EnhancedChangesList groups log entries by performer rather than by target article
502  if ( $rc->mAttribs['rc_type'] == RC_LOG && $cl instanceof EnhancedChangesList &&
503  $grouped ) {
504  return '';
505  } else {
506  return $this->getLinkRenderer()
507  ->makeKnownLink( $rc->getTitle(),
508  $this->msg( 'watchlist-unwatch' )->text(), [
509  'class' => 'mw-unwatch-link',
510  'title' => $this->msg( 'tooltip-ca-unwatch' )->text()
511  ], [ 'action' => 'unwatch' ] ) . "\u{00A0}";
512  }
513  } );
514  }
515  $dbr->dataSeek( $rows, 0 );
516 
517  if ( $this->getConfig()->get( 'RCShowWatchingUsers' )
518  && $user->getOption( 'shownumberswatching' )
519  ) {
520  $watchedItemStore = $services->getWatchedItemStore();
521  }
522 
523  $s = $list->beginRecentChangesList();
524 
525  if ( $this->isStructuredFilterUiEnabled() ) {
526  $s .= $this->makeLegend();
527  }
528 
529  $userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
530  $counter = 1;
531  foreach ( $rows as $obj ) {
532  # Make RC entry
533  $rc = RecentChange::newFromRow( $obj );
534 
535  # Skip CatWatch entries for hidden cats based on user preference
536  if (
537  $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
538  !$userShowHiddenCats &&
539  $rc->getParam( 'hidden-cat' )
540  ) {
541  continue;
542  }
543 
544  $rc->counter = $counter++;
545 
546  if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
547  $unseen = !$this->isChangeEffectivelySeen( $rc );
548  } else {
549  $unseen = false;
550  }
551 
552  if ( isset( $watchedItemStore ) ) {
553  $rcTitleValue = new TitleValue( (int)$obj->rc_namespace, $obj->rc_title );
554  $rc->numberofWatchingusers = $watchedItemStore->countWatchers( $rcTitleValue );
555  } else {
556  $rc->numberofWatchingusers = 0;
557  }
558 
559  // XXX: this treats pages with no unseen changes as "not on the watchlist" since
560  // everything is on the watchlist and it is an easy way to make pages with unseen
561  // changes appear bold. @TODO: clean this up.
562  $changeLine = $list->recentChangesLine( $rc, $unseen, $counter );
563  if ( $changeLine !== false ) {
564  $s .= $changeLine;
565  }
566  }
567  $s .= $list->endRecentChangesList();
568 
569  $output->addHTML( $s );
570  }
571 
578  public function doHeader( $opts, $numRows ) {
579  $user = $this->getUser();
580  $out = $this->getOutput();
581 
582  $out->addSubtitle(
583  $this->msg( 'watchlistfor2', $user->getName() )
585  $this->getLanguage(),
586  $this->getLinkRenderer()
587  ) )
588  );
589 
590  $this->setTopText( $opts );
591 
592  $form = '';
593 
594  $form .= Xml::openElement( 'form', [
595  'method' => 'get',
596  'action' => wfScript(),
597  'id' => 'mw-watchlist-form'
598  ] );
599  $form .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
600  $form .= Xml::openElement(
601  'fieldset',
602  [ 'id' => 'mw-watchlist-options', 'class' => 'cloptions' ]
603  );
604  $form .= Xml::element(
605  'legend', null, $this->msg( 'watchlist-options' )->text()
606  );
607 
608  if ( !$this->isStructuredFilterUiEnabled() ) {
609  $form .= $this->makeLegend();
610  }
611 
612  $lang = $this->getLanguage();
613  $timestamp = wfTimestampNow();
614  $now = $lang->userTimeAndDate( $timestamp, $user );
615  $wlInfo = Html::rawElement(
616  'span',
617  [
618  'class' => 'wlinfo',
619  'data-params' => json_encode( [ 'from' => $timestamp, 'fromFormatted' => $now ] ),
620  ],
621  $this->msg( 'wlnote' )->numParams( $numRows, round( $opts['days'] * 24 ) )->params(
622  $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user )
623  )->parse()
624  ) . "<br />\n";
625 
626  $nondefaults = $opts->getChangedValues();
627  $cutofflinks = Html::rawElement(
628  'span',
629  [ 'class' => 'cldays cloption' ],
630  $this->msg( 'wlshowtime' ) . ' ' . $this->cutoffselector( $opts )
631  );
632 
633  # Spit out some control panel links
634  $links = [];
635  $namesOfDisplayedFilters = [];
636  foreach ( $this->getLegacyShowHideFilters() as $filterName => $filter ) {
637  $namesOfDisplayedFilters[] = $filterName;
638  $links[] = $this->showHideCheck(
639  $nondefaults,
640  $filter->getShowHide(),
641  $filterName,
642  $opts[ $filterName ],
643  $filter->isFeatureAvailableOnStructuredUi( $this )
644  );
645  }
646 
647  $hiddenFields = $nondefaults;
648  $hiddenFields['action'] = 'submit';
649  unset( $hiddenFields['namespace'] );
650  unset( $hiddenFields['invert'] );
651  unset( $hiddenFields['associated'] );
652  unset( $hiddenFields['days'] );
653  foreach ( $namesOfDisplayedFilters as $filterName ) {
654  unset( $hiddenFields[$filterName] );
655  }
656 
657  # Namespace filter and put the whole form together.
658  $form .= $wlInfo;
659  $form .= $cutofflinks;
660  $form .= Html::rawElement(
661  'span',
662  [ 'class' => 'clshowhide' ],
663  $this->msg( 'watchlist-hide' ) .
664  $this->msg( 'colon-separator' )->escaped() .
665  implode( ' ', $links )
666  );
667  $form .= "\n<br />\n";
668 
669  $namespaceForm = Html::namespaceSelector(
670  [
671  'selected' => $opts['namespace'],
672  'all' => '',
673  'label' => $this->msg( 'namespace' )->text(),
674  'in-user-lang' => true,
675  ], [
676  'name' => 'namespace',
677  'id' => 'namespace',
678  'class' => 'namespaceselector',
679  ]
680  ) . "\n";
681  $hidden = $opts['namespace'] === '' ? ' mw-input-hidden' : '';
682  $namespaceForm .= '<span class="mw-input-with-label' . $hidden . '">' . Xml::checkLabel(
683  $this->msg( 'invert' )->text(),
684  'invert',
685  'nsinvert',
686  $opts['invert'],
687  [ 'title' => $this->msg( 'tooltip-invert' )->text() ]
688  ) . "</span>\n";
689  $namespaceForm .= '<span class="mw-input-with-label' . $hidden . '">' . Xml::checkLabel(
690  $this->msg( 'namespace_association' )->text(),
691  'associated',
692  'nsassociated',
693  $opts['associated'],
694  [ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
695  ) . "</span>\n";
696  $form .= Html::rawElement(
697  'span',
698  [ 'class' => 'namespaceForm cloption' ],
699  $namespaceForm
700  );
701 
702  $form .= Xml::submitButton(
703  $this->msg( 'watchlist-submit' )->text(),
704  [ 'class' => 'cloption-submit' ]
705  ) . "\n";
706  foreach ( $hiddenFields as $key => $value ) {
707  $form .= Html::hidden( $key, $value ) . "\n";
708  }
709  $form .= Xml::closeElement( 'fieldset' ) . "\n";
710  $form .= Xml::closeElement( 'form' ) . "\n";
711 
712  // Insert a placeholder for RCFilters
713  if ( $this->isStructuredFilterUiEnabled() ) {
714  $rcfilterContainer = Html::element(
715  'div',
716  // TODO: Remove deprecated rcfilters-container class
717  [ 'class' => 'rcfilters-container mw-rcfilters-container' ]
718  );
719 
720  $loadingContainer = Html::rawElement(
721  'div',
722  [ 'class' => 'mw-rcfilters-spinner' ],
724  'div',
725  [ 'class' => 'mw-rcfilters-spinner-bounce' ]
726  )
727  );
728 
729  // Wrap both with rcfilters-head
730  $this->getOutput()->addHTML(
732  'div',
733  // TODO: Remove deprecated rcfilters-head class
734  [ 'class' => 'rcfilters-head mw-rcfilters-head' ],
735  $rcfilterContainer . $form
736  )
737  );
738 
739  // Add spinner
740  $this->getOutput()->addHTML( $loadingContainer );
741  } else {
742  $this->getOutput()->addHTML( $form );
743  }
744 
745  $this->setBottomText( $opts );
746  }
747 
748  function cutoffselector( $options ) {
749  $selected = (float)$options['days'];
750  if ( $selected <= 0 ) {
751  $selected = $this->maxDays;
752  }
753 
754  $selectedHours = round( $selected * 24 );
755 
756  $hours = array_unique( array_filter( [
757  1,
758  2,
759  6,
760  12,
761  24,
762  72,
763  168,
764  24 * (float)$this->getUser()->getOption( 'watchlistdays', 0 ),
765  24 * $this->maxDays,
766  $selectedHours
767  ] ) );
768  asort( $hours );
769 
770  $select = new XmlSelect( 'days', 'days', (float)( $selectedHours / 24 ) );
771 
772  foreach ( $hours as $value ) {
773  if ( $value < 24 ) {
774  $name = $this->msg( 'hours' )->numParams( $value )->text();
775  } else {
776  $name = $this->msg( 'days' )->numParams( $value / 24 )->text();
777  }
778  $select->addOption( $name, (float)( $value / 24 ) );
779  }
780 
781  return $select->getHTML() . "\n<br />\n";
782  }
783 
784  function setTopText( FormOptions $opts ) {
785  $nondefaults = $opts->getChangedValues();
786  $form = '';
787  $user = $this->getUser();
788 
789  $numItems = $this->countItems();
790  $showUpdatedMarker = $this->getConfig()->get( 'ShowUpdatedMarker' );
791 
792  // Show watchlist header
793  $watchlistHeader = '';
794  if ( $numItems == 0 ) {
795  $watchlistHeader = $this->msg( 'nowatchlist' )->parse();
796  } else {
797  $watchlistHeader .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n";
798  if ( $this->getConfig()->get( 'EnotifWatchlist' )
799  && $user->getOption( 'enotifwatchlistpages' )
800  ) {
801  $watchlistHeader .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
802  }
803  if ( $showUpdatedMarker ) {
804  $watchlistHeader .= $this->msg(
805  $this->isStructuredFilterUiEnabled() ?
806  'rcfilters-watchlist-showupdated' :
807  'wlheader-showupdated'
808  )->parse() . "\n";
809  }
810  }
811  $form .= Html::rawElement(
812  'div',
813  [ 'class' => 'watchlistDetails' ],
814  $watchlistHeader
815  );
816 
817  if ( $numItems > 0 && $showUpdatedMarker ) {
818  $form .= Xml::openElement( 'form', [ 'method' => 'post',
819  'action' => $this->getPageTitle()->getLocalURL(),
820  'id' => 'mw-watchlist-resetbutton' ] ) . "\n" .
821  Xml::submitButton( $this->msg( 'enotif_reset' )->text(),
822  [ 'name' => 'mw-watchlist-reset-submit' ] ) . "\n" .
823  Html::hidden( 'token', $user->getEditToken() ) . "\n" .
824  Html::hidden( 'reset', 'all' ) . "\n";
825  foreach ( $nondefaults as $key => $value ) {
826  $form .= Html::hidden( $key, $value ) . "\n";
827  }
828  $form .= Xml::closeElement( 'form' ) . "\n";
829  }
830 
831  $this->getOutput()->addHTML( $form );
832  }
833 
834  protected function showHideCheck( $options, $message, $name, $value, $inStructuredUi ) {
835  $options[$name] = 1 - (int)$value;
836 
837  $attribs = [ 'class' => 'mw-input-with-label clshowhideoption cloption' ];
838  if ( $inStructuredUi ) {
839  $attribs[ 'data-feature-in-structured-ui' ] = true;
840  }
841 
842  return Html::rawElement(
843  'span',
844  $attribs,
845  // not using Html::checkLabel because that would escape the contents
846  Html::check( $name, (int)$value, [ 'id' => $name ] ) . Html::rawElement(
847  'label',
848  $attribs + [ 'for' => $name ],
849  // <nowiki/> at beginning to avoid messages with "$1 ..." being parsed as pre tags
850  $this->msg( $message, '<nowiki/>' )->parse()
851  )
852  );
853  }
854 
862  protected function countItems() {
863  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
864  $count = $store->countWatchedItems( $this->getUser() );
865  return floor( $count / 2 );
866  }
867 
872  protected function isChangeEffectivelySeen( RecentChange $rc ) {
873  $firstUnseen = $this->getLatestNotificationTimestamp( $rc );
874 
875  return ( $firstUnseen === null || $firstUnseen > $rc->getAttribute( 'rc_timestamp' ) );
876  }
877 
883  return $this->watchStore->getLatestNotificationTimestamp(
884  $rc->getAttribute( 'wl_notificationtimestamp' ),
885  $this->getUser(),
886  $rc->getTitle()
887  );
888  }
889 }
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:772
getFilterGroup( $groupName)
Gets a specified ChangesListFilterGroup by name.
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:35
const RC_CATEGORIZE
Definition: Defines.php:126
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
$context
Definition: load.php:45
getContext()
Gets the context this SpecialPage is executed in.
areFiltersInConflict()
Check if filters are in conflict and guaranteed to return no results.
outputChangesList( $rows, $opts)
Build and output the actual changes list.
static $collapsedPreferenceName
const EDIT_CLEAR
Editing modes.
static $savedQueriesPreferenceName
if(!isset( $args[0])) $lang
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
getAttribute( $name)
Get an attribute value.
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
Class for generating HTML <select> or <datalist> elements.
Definition: XmlSelect.php:26
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
$watchStore
WatchedItemStore.
getDB()
Return a IDatabase object for reading.
static checkStructuredFilterUiEnabled( $user)
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:36
static getMode( $request, $par)
Determine whether we are editing the watchlist, and if so, what kind of editing operation.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
static buildTools( $lang, LinkRenderer $linkRenderer=null)
Build a set of links for convenient navigation between watchlist viewing and editing modes...
getOutput()
Get the OutputPage being used for this instance.
fetchOptionsFromRequest( $opts)
Fetch values for a FormOptions object from the WebRequest associated with this instance.
isChangeEffectivelySeen(RecentChange $rc)
getOptions()
Get the current FormOptions for this request.
getLatestNotificationTimestamp(RecentChange $rc)
__construct( $page='Watchlist', $restriction='viewmywatchlist')
doHeader( $opts, $numRows)
Set the text to be displayed above the changes.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
transformFilterDefinition(array $filterDefinition)
doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, FormOptions $opts)
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:108
Interface for configuration instances.
Definition: Config.php:28
const LIST_AND
Definition: Defines.php:39
showHideCheck( $options, $message, $name, $value, $inStructuredUi)
countItems()
Count the number of paired items on a user&#39;s watchlist.
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition: Xml.php:459
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
$filter
setBottomText(FormOptions $opts)
Send the text to be displayed after the options.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
Represents a filter group with multiple string options.
const DELETED_RESTRICTED
Definition: LogPage.php:37
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
static newFromContext(IContextSource $context, array $groups=[])
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
Definition: ChangesList.php:87
static check( $name, $checked=false, array $attribs=[])
Convenience function to produce a checkbox (input element with type=checkbox)
Definition: Html.php:688
const LIST_OR
Definition: Defines.php:42
registerFilterGroup(ChangesListFilterGroup $group)
Register a structured changes list filter group.
addFeedLinks( $params)
Adds RSS/atom links.
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:117
isStructuredFilterUiEnabled()
Check whether the structured filter UI is enabled.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
const NONE
Signifies that no options in the group are selected, meaning the group has no effect.
Special page which uses a ChangesList to show query results.
outputFeedLinks()
Output feed links.
requireLogin( $reasonMsg='exception-nologin-text', $titleMsg='exception-nologin')
If the user is not logged in, throws UserNotLoggedIn error.
getUser()
Shortcut to get the User executing this instance.
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
getConfig()
Shortcut to get main config object.
runMainQueryHook(&$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts)
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
getLanguage()
Shortcut to get user&#39;s language.
registerFilters()
PhanUndeclaredMethod
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new recentchanges object...
getChangedValues()
Return options modified as an array ( name => value )
const DB_REPLICA
Definition: defines.php:25
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:419
getRequest()
Get the WebRequest being used for this instance.
const DELETED_ACTION
Definition: LogPage.php:34
makeLegend()
Return the legend displayed within the fieldset.
cutoffselector( $options)
If the group is active, any unchecked filters will translate to hide parameters in the URL...
A special page that lists last changes made to the wiki, limited to user-defined list of titles...
setTopText(FormOptions $opts)
getPageTitle( $subpage=false)
Get a self-referential title object.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
const RC_LOG
Definition: Defines.php:124
execute( $subpage)
Main execution point.
static newFromRow( $row)
static namespaceSelector(array $params=[], array $selectAttribs=[])
Build a drop-down box for selecting a namespace.
Definition: Html.php:892