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