MediaWiki  master
WatchlistManager.php
Go to the documentation of this file.
1 <?php
2 
24 
40 use StatusValue;
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  $user = $performer->getUser();
155 
156  // NOTE: Has to be before `editmywatchlist` user right check, to ensure
157  // anonymous / temporary users have their talk page notifications cleared (T345031).
158  if ( !$this->isEnotifEnabled ) {
159  $this->talkPageNotificationManager->removeUserHasNewMessages( $user );
160  return;
161  }
162 
163  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
164  // User isn't allowed to edit the watchlist
165  return;
166  }
167 
168  if ( !$user->isRegistered() ) {
169  return;
170  }
171 
172  $this->watchedItemStore->resetAllNotificationTimestampsForUser( $user );
173 
174  // We also need to clear here the "you have new message" notification for the own
175  // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
176  }
177 
192  $performer,
193  $title,
194  int $oldid = 0,
195  RevisionRecord $oldRev = null
196  ) {
197  if ( $this->readOnlyMode->isReadOnly() ) {
198  // Cannot change anything in read only
199  return;
200  }
201 
202  if ( !$performer instanceof Authority ) {
203  $performer = $this->userFactory->newFromUserIdentity( $performer );
204  }
205 
206  $userIdentity = $performer->getUser();
207  $userTalkPage = (
208  $title->getNamespace() === NS_USER_TALK &&
209  $title->getDBkey() === strtr( $userIdentity->getName(), ' ', '_' )
210  );
211 
212  if ( $userTalkPage ) {
213  if ( !$oldid ) {
214  $oldRev = null;
215  } elseif ( !$oldRev ) {
216  $oldRev = $this->revisionLookup->getRevisionById( $oldid );
217  }
218  // NOTE: Has to be called before isAllowed() check, to ensure users with no watchlist
219  // access (by default, temporary and anonymous users) can clear their talk page
220  // notification (T345031).
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  // NOTE: Has to be checked after the TalkPageNotificationManager::clearForPageView call, to
234  // ensure users with no watchlist access (by default, temporary and anonymous users) can
235  // clear their talk page notification (T345031).
236  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
237  // User isn't allowed to edit the watchlist
238  return;
239  }
240 
241  // Only update the timestamp if the page is being watched.
242  // The query to find out if it is watched is cached both in memcached and per-invocation,
243  // and when it does have to be executed, it can be on a replica DB
244  // If this is the user's newtalk page, we always update the timestamp
245  $force = $userTalkPage ? 'force' : '';
246  $this->watchedItemStore->resetNotificationTimestamp( $userIdentity, $title, $force, $oldid );
247  }
248 
256  public function getTitleNotificationTimestamp( UserIdentity $user, $title ) {
257  if ( !$user->isRegistered() ) {
258  return false;
259  }
260 
261  $cacheKey = 'u' . $user->getId() . '-' .
262  $title->getNamespace() . ':' . $title->getDBkey();
263 
264  // avoid isset here, as it'll return false for null entries
265  if ( array_key_exists( $cacheKey, $this->notificationTimestampCache ) ) {
266  return $this->notificationTimestampCache[ $cacheKey ];
267  }
268 
269  $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $title );
270  if ( $watchedItem ) {
271  $timestamp = $watchedItem->getNotificationTimestamp();
272  } else {
273  $timestamp = false;
274  }
275 
276  $this->notificationTimestampCache[ $cacheKey ] = $timestamp;
277  return $timestamp;
278  }
279 
285  public function isWatchable( PageReference $target ): bool {
286  if ( !$this->nsInfo->isWatchable( $target->getNamespace() ) ) {
287  return false;
288  }
289 
290  if ( $target instanceof PageIdentity && !$target->canExist() ) {
291  // Catch "improper" Title instances
292  return false;
293  }
294 
295  return true;
296  }
297 
305  public function isWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
306  if ( $this->isWatchable( $target ) ) {
307  return $this->watchedItemStore->isWatched( $userIdentity, $target );
308  }
309  return false;
310  }
311 
320  public function isWatched( Authority $performer, PageIdentity $target ): bool {
321  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
322  return $this->isWatchedIgnoringRights( $performer->getUser(), $target );
323  }
324  return false;
325  }
326 
334  public function isTempWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
335  if ( $this->isWatchable( $target ) ) {
336  return $this->watchedItemStore->isTempWatched( $userIdentity, $target );
337  }
338  return false;
339  }
340 
349  public function isTempWatched( Authority $performer, PageIdentity $target ): bool {
350  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
351  return $this->isTempWatchedIgnoringRights( $performer->getUser(), $target );
352  }
353  return false;
354  }
355 
365  public function addWatchIgnoringRights(
366  UserIdentity $userIdentity,
367  PageIdentity $target,
368  ?string $expiry = null
369  ): StatusValue {
370  if ( !$this->isWatchable( $target ) ) {
371  return StatusValue::newFatal( 'watchlistnotwatchable' );
372  }
373 
374  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
375  $title = $wikiPage->getTitle();
376 
377  // TODO: update hooks to take Authority
378  $status = Status::newFatal( 'hookaborted' );
379  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
380  if ( $this->hookRunner->onWatchArticle( $user, $wikiPage, $status, $expiry ) ) {
381  $status = StatusValue::newGood();
382  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ), $expiry );
383  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
384  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ), $expiry );
385  }
386  $this->hookRunner->onWatchArticleComplete( $user, $wikiPage );
387  }
388 
389  // eventually user_touched should be factored out of User and this should be replaced
390  $user->invalidateCache();
391 
392  return $status;
393  }
394 
405  public function addWatch(
406  Authority $performer,
407  PageIdentity $target,
408  ?string $expiry = null
409  ): StatusValue {
410  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
411  // TODO: this function should be moved out of User
412  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
413  }
414 
415  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
416  }
417 
425  public function removeWatchIgnoringRights(
426  UserIdentity $userIdentity,
427  PageIdentity $target
428  ): StatusValue {
429  if ( !$this->isWatchable( $target ) ) {
430  return StatusValue::newFatal( 'watchlistnotwatchable' );
431  }
432 
433  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
434  $title = $wikiPage->getTitle();
435 
436  // TODO: update hooks to take Authority
437  $status = Status::newFatal( 'hookaborted' );
438  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
439  if ( $this->hookRunner->onUnwatchArticle( $user, $wikiPage, $status ) ) {
440  $status = StatusValue::newGood();
441  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ) );
442  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
443  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ) );
444  }
445  $this->hookRunner->onUnwatchArticleComplete( $user, $wikiPage );
446  }
447 
448  // eventually user_touched should be factored out of User and this should be replaced
449  $user->invalidateCache();
450 
451  return $status;
452  }
453 
462  public function removeWatch(
463  Authority $performer,
464  PageIdentity $target
465  ): StatusValue {
466  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
467  // TODO: this function should be moved out of User
468  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
469  }
470 
471  return $this->removeWatchIgnoringRights( $performer->getUser(), $target );
472  }
473 
487  public function setWatch(
488  bool $watch,
489  Authority $performer,
490  PageIdentity $target,
491  string $expiry = null
492  ): StatusValue {
493  // User must be registered, and either changing the watch state or at least the expiry.
494  if ( !$performer->getUser()->isRegistered() ) {
495  return StatusValue::newGood();
496  }
497 
498  // Only call addWatchIgnoringRights() or removeWatch() if there's been a change in the watched status.
499  $oldWatchedItem = $this->watchedItemStore->getWatchedItem( $performer->getUser(), $target );
500  $changingWatchStatus = (bool)$oldWatchedItem !== $watch;
501  if ( $oldWatchedItem && $expiry !== null ) {
502  // If there's an old watched item, a non-null change to the expiry requires an UPDATE.
503  $oldWatchPeriod = $oldWatchedItem->getExpiry() ?? 'infinity';
504  $changingWatchStatus = $changingWatchStatus ||
505  $oldWatchPeriod !== ExpiryDef::normalizeExpiry( $expiry, TS_MW );
506  }
507 
508  if ( $changingWatchStatus ) {
509  // If the user doesn't have 'editmywatchlist', we still want to
510  // allow them to add but not remove items via edits and such.
511  if ( $watch ) {
512  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
513  } else {
514  return $this->removeWatch( $performer, $target );
515  }
516  }
517 
518  return StatusValue::newGood();
519  }
520 }
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:568
Service for creating WikiPage objects.
Page revision base class.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Creates User objects.
Definition: UserFactory.php:41
internal since 1.36
Definition: User.php:98
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.
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
Type definition for expiry timestamps.
Definition: ExpiryDef.php:17
Determine whether a site is currently in read-only mode.
Represents the target of a wiki link.
Definition: LinkTarget.php:30
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.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
getId( $wikiId=self::LOCAL)