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