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