MediaWiki  1.28.3
SpecialWatchlist.php
Go to the documentation of this file.
1 <?php
25 
33  public function __construct( $page = 'Watchlist', $restriction = 'viewmywatchlist' ) {
34  parent::__construct( $page, $restriction );
35  }
36 
37  public function doesWrites() {
38  return true;
39  }
40 
46  function execute( $subpage ) {
47  // Anons don't get a watchlist
48  $this->requireLogin( 'watchlistanontext' );
49 
50  $output = $this->getOutput();
51  $request = $this->getRequest();
52  $this->addHelpLink( 'Help:Watching pages' );
54  'mediawiki.special.changeslist.visitedstatus',
55  ] );
56 
57  $mode = SpecialEditWatchlist::getMode( $request, $subpage );
58  if ( $mode !== false ) {
59  if ( $mode === SpecialEditWatchlist::EDIT_RAW ) {
60  $title = SpecialPage::getTitleFor( 'EditWatchlist', 'raw' );
61  } elseif ( $mode === SpecialEditWatchlist::EDIT_CLEAR ) {
62  $title = SpecialPage::getTitleFor( 'EditWatchlist', 'clear' );
63  } else {
64  $title = SpecialPage::getTitleFor( 'EditWatchlist' );
65  }
66 
67  $output->redirect( $title->getLocalURL() );
68 
69  return;
70  }
71 
72  $this->checkPermissions();
73 
74  $user = $this->getUser();
75  $opts = $this->getOptions();
76 
77  $config = $this->getConfig();
78  if ( ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) )
79  && $request->getVal( 'reset' )
80  && $request->wasPosted()
81  && $user->matchEditToken( $request->getVal( 'token' ) )
82  ) {
83  $user->clearAllNotifications();
84  $output->redirect( $this->getPageTitle()->getFullURL( $opts->getChangedValues() ) );
85 
86  return;
87  }
88 
89  parent::execute( $subpage );
90  }
91 
98  public function getSubpagesForPrefixSearch() {
99  return [
100  'clear',
101  'edit',
102  'raw',
103  ];
104  }
105 
111  public function getDefaultOptions() {
112  $opts = parent::getDefaultOptions();
113  $user = $this->getUser();
114 
115  $opts->add( 'days', $user->getOption( 'watchlistdays' ), FormOptions::FLOAT );
116  $opts->add( 'extended', $user->getBoolOption( 'extendwatchlist' ) );
117  if ( $this->getRequest()->getVal( 'action' ) == 'submit' ) {
118  // The user has submitted the form, so we dont need the default values
119  return $opts;
120  }
121 
122  $opts->add( 'hideminor', $user->getBoolOption( 'watchlisthideminor' ) );
123  $opts->add( 'hidebots', $user->getBoolOption( 'watchlisthidebots' ) );
124  $opts->add( 'hideanons', $user->getBoolOption( 'watchlisthideanons' ) );
125  $opts->add( 'hideliu', $user->getBoolOption( 'watchlisthideliu' ) );
126  $opts->add( 'hidepatrolled', $user->getBoolOption( 'watchlisthidepatrolled' ) );
127  $opts->add( 'hidemyself', $user->getBoolOption( 'watchlisthideown' ) );
128  $opts->add( 'hidecategorization', $user->getBoolOption( 'watchlisthidecategorization' ) );
129 
130  return $opts;
131  }
132 
138  protected function getCustomFilters() {
139  if ( $this->customFilters === null ) {
140  $this->customFilters = parent::getCustomFilters();
141  Hooks::run( 'SpecialWatchlistFilters', [ $this, &$this->customFilters ], '1.23' );
142  }
143 
144  return $this->customFilters;
145  }
146 
156  protected function fetchOptionsFromRequest( $opts ) {
157  static $compatibilityMap = [
158  'hideMinor' => 'hideminor',
159  'hideBots' => 'hidebots',
160  'hideAnons' => 'hideanons',
161  'hideLiu' => 'hideliu',
162  'hidePatrolled' => 'hidepatrolled',
163  'hideOwn' => 'hidemyself',
164  ];
165 
166  $params = $this->getRequest()->getValues();
167  foreach ( $compatibilityMap as $from => $to ) {
168  if ( isset( $params[$from] ) ) {
169  $params[$to] = $params[$from];
170  unset( $params[$from] );
171  }
172  }
173 
174  // Not the prettiest way to achieve this… FormOptions internally depends on data sanitization
175  // methods defined on WebRequest and removing this dependency would cause some code duplication.
176  $request = new DerivativeRequest( $this->getRequest(), $params );
177  $opts->fetchValuesFromRequest( $request );
178 
179  return $opts;
180  }
181 
188  public function buildMainQueryConds( FormOptions $opts ) {
189  $dbr = $this->getDB();
190  $conds = parent::buildMainQueryConds( $opts );
191 
192  // Calculate cutoff
193  if ( $opts['days'] > 0 ) {
194  $conds[] = 'rc_timestamp > ' .
195  $dbr->addQuotes( $dbr->timestamp( time() - intval( $opts['days'] * 86400 ) ) );
196  }
197 
198  return $conds;
199  }
200 
208  public function doMainQuery( $conds, $opts ) {
209  $dbr = $this->getDB();
210  $user = $this->getUser();
211 
212  # Toggle watchlist content (all recent edits or just the latest)
213  if ( $opts['extended'] ) {
214  $limitWatchlist = $user->getIntOption( 'wllimit' );
215  $usePage = false;
216  } else {
217  # Top log Ids for a page are not stored
218  $nonRevisionTypes = [ RC_LOG ];
219  Hooks::run( 'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
220  if ( $nonRevisionTypes ) {
221  $conds[] = $dbr->makeList(
222  [
223  'rc_this_oldid=page_latest',
224  'rc_type' => $nonRevisionTypes,
225  ],
226  LIST_OR
227  );
228  }
229  $limitWatchlist = 0;
230  $usePage = true;
231  }
232 
233  $tables = [ 'recentchanges', 'watchlist' ];
234  $fields = RecentChange::selectFields();
235  $query_options = [ 'ORDER BY' => 'rc_timestamp DESC' ];
236  $join_conds = [
237  'watchlist' => [
238  'INNER JOIN',
239  [
240  'wl_user' => $user->getId(),
241  'wl_namespace=rc_namespace',
242  'wl_title=rc_title'
243  ],
244  ],
245  ];
246 
247  if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
248  $fields[] = 'wl_notificationtimestamp';
249  }
250  if ( $limitWatchlist ) {
251  $query_options['LIMIT'] = $limitWatchlist;
252  }
253 
254  $rollbacker = $user->isAllowed( 'rollback' );
255  if ( $usePage || $rollbacker ) {
256  $tables[] = 'page';
257  $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
258  if ( $rollbacker ) {
259  $fields[] = 'page_latest';
260  }
261  }
262 
263  // Log entries with DELETED_ACTION must not show up unless the user has
264  // the necessary rights.
265  if ( !$user->isAllowed( 'deletedhistory' ) ) {
266  $bitmask = LogPage::DELETED_ACTION;
267  } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
269  } else {
270  $bitmask = 0;
271  }
272  if ( $bitmask ) {
273  $conds[] = $dbr->makeList( [
274  'rc_type != ' . RC_LOG,
275  $dbr->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
276  ], LIST_OR );
277  }
278 
280  $tables,
281  $fields,
282  $conds,
283  $join_conds,
284  $query_options,
285  ''
286  );
287 
288  $this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts );
289 
290  return $dbr->select(
291  $tables,
292  $fields,
293  $conds,
294  __METHOD__,
295  $query_options,
296  $join_conds
297  );
298  }
299 
300  protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options,
301  &$join_conds, $opts
302  ) {
303  return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts )
304  && Hooks::run(
305  'SpecialWatchlistQuery',
306  [ &$conds, &$tables, &$join_conds, &$fields, $opts ],
307  '1.23'
308  );
309  }
310 
316  protected function getDB() {
317  return wfGetDB( DB_REPLICA, 'watchlist' );
318  }
319 
323  public function outputFeedLinks() {
324  $user = $this->getUser();
325  $wlToken = $user->getTokenFromOption( 'watchlisttoken' );
326  if ( $wlToken ) {
327  $this->addFeedLinks( [
328  'action' => 'feedwatchlist',
329  'allrev' => 1,
330  'wlowner' => $user->getName(),
331  'wltoken' => $wlToken,
332  ] );
333  }
334  }
335 
342  public function outputChangesList( $rows, $opts ) {
343  $dbr = $this->getDB();
344  $user = $this->getUser();
345  $output = $this->getOutput();
346 
347  # Show a message about replica DB lag, if applicable
348  $lag = wfGetLB()->safeGetLag( $dbr );
349  if ( $lag > 0 ) {
350  $output->showLagWarning( $lag );
351  }
352 
353  # If no rows to display, show message before try to render the list
354  if ( $rows->numRows() == 0 ) {
355  $output->wrapWikiMsg(
356  "<div class='mw-changeslist-empty'>\n$1\n</div>", 'recentchanges-noresult'
357  );
358  return;
359  }
360 
361  $dbr->dataSeek( $rows, 0 );
362 
363  $list = ChangesList::newFromContext( $this->getContext() );
364  $list->setWatchlistDivs();
365  $list->initChangesListRows( $rows );
366  $dbr->dataSeek( $rows, 0 );
367 
368  if ( $this->getConfig()->get( 'RCShowWatchingUsers' )
369  && $user->getOption( 'shownumberswatching' )
370  ) {
371  $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
372  }
373 
374  $s = $list->beginRecentChangesList();
375  $userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
376  $counter = 1;
377  foreach ( $rows as $obj ) {
378  # Make RC entry
379  $rc = RecentChange::newFromRow( $obj );
380 
381  # Skip CatWatch entries for hidden cats based on user preference
382  if (
383  $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
384  !$userShowHiddenCats &&
385  $rc->getParam( 'hidden-cat' )
386  ) {
387  continue;
388  }
389 
390  $rc->counter = $counter++;
391 
392  if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
393  $updated = $obj->wl_notificationtimestamp;
394  } else {
395  $updated = false;
396  }
397 
398  if ( isset( $watchedItemStore ) ) {
399  $rcTitleValue = new TitleValue( (int)$obj->rc_namespace, $obj->rc_title );
400  $rc->numberofWatchingusers = $watchedItemStore->countWatchers( $rcTitleValue );
401  } else {
402  $rc->numberofWatchingusers = 0;
403  }
404 
405  $changeLine = $list->recentChangesLine( $rc, $updated, $counter );
406  if ( $changeLine !== false ) {
407  $s .= $changeLine;
408  }
409  }
410  $s .= $list->endRecentChangesList();
411 
412  $output->addHTML( $s );
413  }
414 
421  public function doHeader( $opts, $numRows ) {
422  $user = $this->getUser();
423  $out = $this->getOutput();
424 
425  // if the user wishes, that the watchlist is reloaded, whenever a filter changes,
426  // add the module for that
427  if ( $user->getBoolOption( 'watchlistreloadautomatically' ) ) {
428  $out->addModules( [ 'mediawiki.special.watchlist' ] );
429  }
430 
431  $out->addSubtitle(
432  $this->msg( 'watchlistfor2', $user->getName() )
434  $this->getLanguage(),
435  $this->getLinkRenderer()
436  ) )
437  );
438 
439  $this->setTopText( $opts );
440 
441  $lang = $this->getLanguage();
442  if ( $opts['days'] > 0 ) {
443  $days = $opts['days'];
444  } else {
445  $days = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 );
446  }
448  $wlInfo = $this->msg( 'wlnote' )->numParams( $numRows, round( $days * 24 ) )->params(
449  $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user )
450  )->parse() . "<br />\n";
451 
452  $nondefaults = $opts->getChangedValues();
453  $cutofflinks = $this->msg( 'wlshowtime' ) . ' ' . $this->cutoffselector( $opts );
454 
455  # Spit out some control panel links
456  $filters = [
457  'hideminor' => 'wlshowhideminor',
458  'hidebots' => 'wlshowhidebots',
459  'hideanons' => 'wlshowhideanons',
460  'hideliu' => 'wlshowhideliu',
461  'hidemyself' => 'wlshowhidemine',
462  'hidepatrolled' => 'wlshowhidepatr'
463  ];
464 
465  if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) {
466  $filters['hidecategorization'] = 'wlshowhidecategorization';
467  }
468 
469  foreach ( $this->getCustomFilters() as $key => $params ) {
470  $filters[$key] = $params['msg'];
471  }
472  // Disable some if needed
473  if ( !$user->useRCPatrol() ) {
474  unset( $filters['hidepatrolled'] );
475  }
476 
477  $links = [];
478  foreach ( $filters as $name => $msg ) {
479  $links[] = $this->showHideCheck( $nondefaults, $msg, $name, $opts[$name] );
480  }
481 
482  $hiddenFields = $nondefaults;
483  $hiddenFields['action'] = 'submit';
484  unset( $hiddenFields['namespace'] );
485  unset( $hiddenFields['invert'] );
486  unset( $hiddenFields['associated'] );
487  unset( $hiddenFields['days'] );
488  foreach ( $filters as $key => $value ) {
489  unset( $hiddenFields[$key] );
490  }
491 
492  # Create output
493  $form = '';
494 
495  # Namespace filter and put the whole form together.
496  $form .= $wlInfo;
497  $form .= $cutofflinks;
498  $form .= $this->msg( 'watchlist-hide' ) .
499  $this->msg( 'colon-separator' )->escaped() .
500  implode( ' ', $links );
501  $form .= "\n<br />\n";
502  $form .= Html::namespaceSelector(
503  [
504  'selected' => $opts['namespace'],
505  'all' => '',
506  'label' => $this->msg( 'namespace' )->text()
507  ], [
508  'name' => 'namespace',
509  'id' => 'namespace',
510  'class' => 'namespaceselector',
511  ]
512  ) . "\n";
513  $form .= '<span class="mw-input-with-label">' . Xml::checkLabel(
514  $this->msg( 'invert' )->text(),
515  'invert',
516  'nsinvert',
517  $opts['invert'],
518  [ 'title' => $this->msg( 'tooltip-invert' )->text() ]
519  ) . "</span>\n";
520  $form .= '<span class="mw-input-with-label">' . Xml::checkLabel(
521  $this->msg( 'namespace_association' )->text(),
522  'associated',
523  'nsassociated',
524  $opts['associated'],
525  [ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
526  ) . "</span>\n";
527  $form .= Xml::submitButton( $this->msg( 'watchlist-submit' )->text() ) . "\n";
528  foreach ( $hiddenFields as $key => $value ) {
529  $form .= Html::hidden( $key, $value ) . "\n";
530  }
531  $form .= Xml::closeElement( 'fieldset' ) . "\n";
532  $form .= Xml::closeElement( 'form' ) . "\n";
533  $this->getOutput()->addHTML( $form );
534 
535  $this->setBottomText( $opts );
536  }
537 
538  function cutoffselector( $options ) {
539  // Cast everything to strings immediately, so that we know all of the values have the same
540  // precision, and can be compared with '==='. 2/24 has a few more decimal places than its
541  // default string representation, for example, and would confuse comparisons.
542 
543  // Misleadingly, the 'days' option supports hours too.
544  $days = array_map( 'strval', [ 1/24, 2/24, 6/24, 12/24, 1, 3, 7 ] );
545 
546  $userWatchlistOption = (string)$this->getUser()->getOption( 'watchlistdays' );
547  // add the user preference, if it isn't available already
548  if ( !in_array( $userWatchlistOption, $days ) && $userWatchlistOption !== '0' ) {
549  $days[] = $userWatchlistOption;
550  }
551 
552  $maxDays = (string)( $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
553  // add the maximum possible value, if it isn't available already
554  if ( !in_array( $maxDays, $days ) ) {
555  $days[] = $maxDays;
556  }
557 
558  $selected = (string)$options['days'];
559  if ( $selected <= 0 ) {
560  $selected = $maxDays;
561  }
562 
563  // add the currently selected value, if it isn't available already
564  if ( !in_array( $selected, $days ) ) {
565  $days[] = $selected;
566  }
567 
568  $select = new XmlSelect( 'days', 'days', $selected );
569 
570  asort( $days );
571  foreach ( $days as $value ) {
572  if ( $value < 1 ) {
573  $name = $this->msg( 'hours' )->numParams( $value * 24 )->text();
574  } else {
575  $name = $this->msg( 'days' )->numParams( $value )->text();
576  }
577  $select->addOption( $name, $value );
578  }
579 
580  return $select->getHTML() . "\n<br />\n";
581  }
582 
583  function setTopText( FormOptions $opts ) {
584  $nondefaults = $opts->getChangedValues();
585  $form = "";
586  $user = $this->getUser();
587 
588  $numItems = $this->countItems();
589  $showUpdatedMarker = $this->getConfig()->get( 'ShowUpdatedMarker' );
590 
591  // Show watchlist header
592  $form .= "<p>";
593  if ( $numItems == 0 ) {
594  $form .= $this->msg( 'nowatchlist' )->parse() . "\n";
595  } else {
596  $form .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n";
597  if ( $this->getConfig()->get( 'EnotifWatchlist' )
598  && $user->getOption( 'enotifwatchlistpages' )
599  ) {
600  $form .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
601  }
602  if ( $showUpdatedMarker ) {
603  $form .= $this->msg( 'wlheader-showupdated' )->parse() . "\n";
604  }
605  }
606  $form .= "</p>";
607 
608  if ( $numItems > 0 && $showUpdatedMarker ) {
609  $form .= Xml::openElement( 'form', [ 'method' => 'post',
610  'action' => $this->getPageTitle()->getLocalURL(),
611  'id' => 'mw-watchlist-resetbutton' ] ) . "\n" .
612  Xml::submitButton( $this->msg( 'enotif_reset' )->text(), [ 'name' => 'dummy' ] ) . "\n" .
613  Html::hidden( 'token', $user->getEditToken() ) . "\n" .
614  Html::hidden( 'reset', 'all' ) . "\n";
615  foreach ( $nondefaults as $key => $value ) {
616  $form .= Html::hidden( $key, $value ) . "\n";
617  }
618  $form .= Xml::closeElement( 'form' ) . "\n";
619  }
620 
621  $form .= Xml::openElement( 'form', [
622  'method' => 'get',
623  'action' => wfScript(),
624  'id' => 'mw-watchlist-form'
625  ] );
626  $form .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
627  $form .= Xml::fieldset(
628  $this->msg( 'watchlist-options' )->text(),
629  false,
630  [ 'id' => 'mw-watchlist-options' ]
631  );
632 
633  $form .= $this->makeLegend();
634 
635  $this->getOutput()->addHTML( $form );
636  }
637 
638  protected function showHideCheck( $options, $message, $name, $value ) {
639  $options[$name] = 1 - (int)$value;
640 
641  return '<span class="mw-input-with-label">' . Xml::checkLabel(
642  $this->msg( $message, '' )->text(),
643  $name,
644  $name,
645  (int)$value
646  ) . '</span>';
647  }
648 
656  protected function countItems() {
657  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
658  $count = $store->countWatchedItems( $this->getUser() );
659  return floor( $count / 2 );
660  }
661 }
static newFromContext(IContextSource $context)
Fetch an appropriate changes list class for the specified context Some users might want to use an enh...
Definition: ChangesList.php:74
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:35
const RC_CATEGORIZE
Definition: Defines.php:140
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
const FLOAT
Float type, maps guessType() to WebRequest::getFloat()
Definition: FormOptions.php:48
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:806
getCustomFilters()
Get custom show/hide filters.
getContext()
Gets the context this SpecialPage is executed in.
wfScript($script= 'index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
showHideCheck($options, $message, $name, $value)
$batch execute()
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
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:82
const EDIT_CLEAR
Editing modes.
outputChangesList($rows, $opts)
Build and output the actual changes list.
if(!isset($args[0])) $lang
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
static hidden($name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:758
Class for generating HTML