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