MediaWiki  master
WatchlistManager.php
Go to the documentation of this file.
1 <?php
2 
24 
37 use NamespaceInfo;
38 use Status;
39 use StatusValue;
40 use User;
44 
51 
55  public const OPTION_ENOTIF = 'isEnotifEnabled';
56 
58  private $isEnotifEnabled;
59 
61  private $hookRunner;
62 
64  private $readOnlyMode;
65 
67  private $revisionLookup;
68 
70  private $talkPageNotificationManager;
71 
73  private $watchedItemStore;
74 
76  private $userFactory;
77 
79  private $nsInfo;
80 
82  private $wikiPageFactory;
83 
100  private $notificationTimestampCache = [];
101 
113  public function __construct(
114  array $options,
115  HookContainer $hookContainer,
116  ReadOnlyMode $readOnlyMode,
117  RevisionLookup $revisionLookup,
118  TalkPageNotificationManager $talkPageNotificationManager,
119  WatchedItemStoreInterface $watchedItemStore,
120  UserFactory $userFactory,
121  NamespaceInfo $nsInfo,
122  WikiPageFactory $wikiPageFactory
123  ) {
124  $this->isEnotifEnabled = $options[ self::OPTION_ENOTIF ];
125  $this->hookRunner = new HookRunner( $hookContainer );
126  $this->readOnlyMode = $readOnlyMode;
127  $this->revisionLookup = $revisionLookup;
128  $this->talkPageNotificationManager = $talkPageNotificationManager;
129  $this->watchedItemStore = $watchedItemStore;
130  $this->userFactory = $userFactory;
131  $this->nsInfo = $nsInfo;
132  $this->wikiPageFactory = $wikiPageFactory;
133  }
134 
144  public function clearAllUserNotifications( $performer ) {
145  if ( $this->readOnlyMode->isReadOnly() ) {
146  // Cannot change anything in read only
147  return;
148  }
149 
150  if ( !$performer instanceof Authority ) {
151  $performer = $this->userFactory->newFromUserIdentity( $performer );
152  }
153 
154  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
155  // User isn't allowed to edit the watchlist
156  return;
157  }
158 
159  $user = $performer->getUser();
160 
161  if ( !$this->isEnotifEnabled ) {
162  $this->talkPageNotificationManager->removeUserHasNewMessages( $user );
163  return;
164  }
165 
166  if ( !$user->isRegistered() ) {
167  return;
168  }
169 
170  $this->watchedItemStore->resetAllNotificationTimestampsForUser( $user );
171 
172  // We also need to clear here the "you have new message" notification for the own
173  // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
174  }
175 
190  $performer,
191  $title,
192  int $oldid = 0,
193  RevisionRecord $oldRev = null
194  ) {
195  if ( $this->readOnlyMode->isReadOnly() ) {
196  // Cannot change anything in read only
197  return;
198  }
199 
200  if ( !$performer instanceof Authority ) {
201  $performer = $this->userFactory->newFromUserIdentity( $performer );
202  }
203 
204  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
205  // User isn't allowed to edit the watchlist
206  return;
207  }
208 
209  $userIdentity = $performer->getUser();
210  $userTalkPage = (
211  $title->getNamespace() === NS_USER_TALK &&
212  $title->getDBkey() === strtr( $userIdentity->getName(), ' ', '_' )
213  );
214 
215  if ( $userTalkPage ) {
216  if ( !$oldid ) {
217  $oldRev = null;
218  } elseif ( !$oldRev ) {
219  $oldRev = $this->revisionLookup->getRevisionById( $oldid );
220  }
221  $this->talkPageNotificationManager->clearForPageView( $userIdentity, $oldRev );
222  }
223 
224  if ( !$this->isEnotifEnabled ) {
225  return;
226  }
227 
228  if ( !$userIdentity->isRegistered() ) {
229  // Nothing else to do
230  return;
231  }
232 
233  // Only update the timestamp if the page is being watched.
234  // The query to find out if it is watched is cached both in memcached and per-invocation,
235  // and when it does have to be executed, it can be on a replica DB
236  // If this is the user's newtalk page, we always update the timestamp
237  $force = $userTalkPage ? 'force' : '';
238  $this->watchedItemStore->resetNotificationTimestamp( $userIdentity, $title, $force, $oldid );
239  }
240 
249  if ( !$user->isRegistered() ) {
250  return false;
251  }
252 
253  $cacheKey = 'u' . $user->getId() . '-' .
254  $title->getNamespace() . ':' . $title->getDBkey();
255 
256  // avoid isset here, as it'll return false for null entries
257  if ( array_key_exists( $cacheKey, $this->notificationTimestampCache ) ) {
258  return $this->notificationTimestampCache[ $cacheKey ];
259  }
260 
261  $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $title );
262  if ( $watchedItem ) {
263  $timestamp = $watchedItem->getNotificationTimestamp();
264  } else {
265  $timestamp = false;
266  }
267 
268  $this->notificationTimestampCache[ $cacheKey ] = $timestamp;
269  return $timestamp;
270  }
271 
277  public function isWatchable( PageReference $target ): bool {
278  if ( !$this->nsInfo->isWatchable( $target->getNamespace() ) ) {
279  return false;
280  }
281 
282  if ( $target instanceof PageIdentity && !$target->canExist() ) {
283  // Catch "improper" Title instances
284  return false;
285  }
286 
287  return true;
288  }
289 
297  public function isWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
298  if ( $this->isWatchable( $target ) ) {
299  return $this->watchedItemStore->isWatched( $userIdentity, $target );
300  }
301  return false;
302  }
303 
312  public function isWatched( Authority $performer, PageIdentity $target ): bool {
313  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
314  return $this->isWatchedIgnoringRights( $performer->getUser(), $target );
315  }
316  return false;
317  }
318 
326  public function isTempWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
327  if ( $this->isWatchable( $target ) ) {
328  return $this->watchedItemStore->isTempWatched( $userIdentity, $target );
329  }
330  return false;
331  }
332 
341  public function isTempWatched( Authority $performer, PageIdentity $target ): bool {
342  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
343  return $this->isTempWatchedIgnoringRights( $performer->getUser(), $target );
344  }
345  return false;
346  }
347 
357  public function addWatchIgnoringRights(
358  UserIdentity $userIdentity,
359  PageIdentity $target,
360  ?string $expiry = null
361  ): StatusValue {
362  if ( !$this->isWatchable( $target ) ) {
363  return StatusValue::newFatal( 'watchlistnotwatchable' );
364  }
365 
366  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
367  $title = $wikiPage->getTitle();
368 
369  // TODO: update hooks to take Authority
370  $status = Status::newFatal( 'hookaborted' );
371  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
372  if ( $this->hookRunner->onWatchArticle( $user, $wikiPage, $status, $expiry ) ) {
373  $status = StatusValue::newGood();
374  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ), $expiry );
375  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
376  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ), $expiry );
377  }
378  $this->hookRunner->onWatchArticleComplete( $user, $wikiPage );
379  }
380 
381  // eventually user_touched should be factored out of User and this should be replaced
382  $user->invalidateCache();
383 
384  return $status;
385  }
386 
397  public function addWatch(
398  Authority $performer,
399  PageIdentity $target,
400  ?string $expiry = null
401  ): StatusValue {
402  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
403  // TODO: this function should be moved out of User
404  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
405  }
406 
407  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
408  }
409 
417  public function removeWatchIgnoringRights(
418  UserIdentity $userIdentity,
419  PageIdentity $target
420  ): StatusValue {
421  if ( !$this->isWatchable( $target ) ) {
422  return StatusValue::newFatal( 'watchlistnotwatchable' );
423  }
424 
425  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
426  $title = $wikiPage->getTitle();
427 
428  // TODO: update hooks to take Authority
429  $status = Status::newFatal( 'hookaborted' );
430  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
431  if ( $this->hookRunner->onUnwatchArticle( $user, $wikiPage, $status ) ) {
432  $status = StatusValue::newGood();
433  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ) );
434  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
435  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ) );
436  }
437  $this->hookRunner->onUnwatchArticleComplete( $user, $wikiPage );
438  }
439 
440  // eventually user_touched should be factored out of User and this should be replaced
441  $user->invalidateCache();
442 
443  return $status;
444  }
445 
454  public function removeWatch(
455  Authority $performer,
456  PageIdentity $target
457  ): StatusValue {
458  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
459  // TODO: this function should be moved out of User
460  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
461  }
462 
463  return $this->removeWatchIgnoringRights( $performer->getUser(), $target );
464  }
465 
479  public function setWatch(
480  bool $watch,
481  Authority $performer,
482  PageIdentity $target,
483  string $expiry = null
484  ): StatusValue {
485  // User must be registered, and either changing the watch state or at least the expiry.
486  if ( !$performer->getUser()->isRegistered() ) {
487  return StatusValue::newGood();
488  }
489 
490  // Only call addWatchIgnoringRights() or removeWatch() if there's been a change in the watched status.
491  $oldWatchedItem = $this->watchedItemStore->getWatchedItem( $performer->getUser(), $target );
492  $changingWatchStatus = (bool)$oldWatchedItem !== $watch;
493  if ( $oldWatchedItem && $expiry !== null ) {
494  // If there's an old watched item, a non-null change to the expiry requires an UPDATE.
495  $oldWatchPeriod = $oldWatchedItem->getExpiry() ?? 'infinity';
496  $changingWatchStatus = $changingWatchStatus ||
497  $oldWatchPeriod !== ExpiryDef::normalizeExpiry( $expiry, TS_MW );
498  }
499 
500  if ( $changingWatchStatus ) {
501  // If the user doesn't have 'editmywatchlist', we still want to
502  // allow them to add but not remove items via edits and such.
503  if ( $watch ) {
504  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
505  } else {
506  return $this->removeWatch( $performer, $target );
507  }
508  }
509 
510  return StatusValue::newGood();
511  }
512 }
getUser()
const NS_USER_TALK
Definition: Defines.php:67
if(!defined('MW_SETUP_CALLBACK'))
Definition: WebStart.php:88
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:565
Service for creating WikiPage objects.
Page revision base class.
Creates User objects.
Definition: UserFactory.php:42
isTempWatchedIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Check if the article is temporarily watched by the user.
__construct(array $options, HookContainer $hookContainer, ReadOnlyMode $readOnlyMode, RevisionLookup $revisionLookup, TalkPageNotificationManager $talkPageNotificationManager, WatchedItemStoreInterface $watchedItemStore, UserFactory $userFactory, NamespaceInfo $nsInfo, WikiPageFactory $wikiPageFactory)
setWatch(bool $watch, Authority $performer, PageIdentity $target, string $expiry=null)
Watch or unwatch a page, calling watch/unwatch hooks as appropriate.
getTitleNotificationTimestamp(UserIdentity $user, $title)
Get the timestamp when this page was updated since the user last saw it.
clearTitleUserNotifications( $performer, $title, int $oldid=0, RevisionRecord $oldRev=null)
Clear the user's notification timestamp for the given title.
clearAllUserNotifications( $performer)
Resets all of the given user's page-change notification timestamps.
removeWatch(Authority $performer, PageIdentity $target)
Stop watching a page if the user has permission to edit their watchlist.
isWatched(Authority $performer, PageIdentity $target)
Check if the page is watched by the user and the user has permission to view their watchlist.
addWatch(Authority $performer, PageIdentity $target, ?string $expiry=null)
Watch a page if the user has permission to edit their watchlist.
isTempWatched(Authority $performer, PageIdentity $target)
Check if the page is temporarily watched by the user and the user has permission to view their watchl...
isWatchedIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Check if the page is watched by the user.
addWatchIgnoringRights(UserIdentity $userIdentity, PageIdentity $target, ?string $expiry=null)
Watch a page.
removeWatchIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Stop watching a page.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:46
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:46
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:71
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:3415
Type definition for expiry timestamps.
Definition: ExpiryDef.php:17
Determine whether a site is currently in read-only mode.
Interface for objects (potentially) representing an editable wiki page.
canExist()
Checks whether this PageIdentity represents a "proper" page, meaning that it could exist as an editab...
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
Service for looking up page revisions.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)