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