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