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