MediaWiki  master
WatchlistManager.php
Go to the documentation of this file.
1 <?php
2 
24 
25 use DeferredUpdates;
38 use NamespaceInfo;
39 use ReadOnlyMode;
40 use Status;
41 use StatusValue;
42 use TitleValue;
43 use User;
46 
53 
57  public const CONSTRUCTOR_OPTIONS = [
58  'UseEnotif',
59  'ShowUpdatedMarker',
60  ];
61 
63  private $options;
64 
66  private $hookRunner;
67 
69  private $readOnlyMode;
70 
72  private $revisionLookup;
73 
76 
79 
81  private $userFactory;
82 
84  private $nsInfo;
85 
88 
106 
118  public function __construct(
120  HookContainer $hookContainer,
128  ) {
129  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
130  $this->options = $options;
131  $this->hookRunner = new HookRunner( $hookContainer );
132  $this->readOnlyMode = $readOnlyMode;
133  $this->revisionLookup = $revisionLookup;
134  $this->talkPageNotificationManager = $talkPageNotificationManager;
135  $this->watchedItemStore = $watchedItemStore;
136  $this->userFactory = $userFactory;
137  $this->nsInfo = $nsInfo;
138  $this->wikiPageFactory = $wikiPageFactory;
139  }
140 
150  public function clearAllUserNotifications( $performer ) {
151  if ( $this->readOnlyMode->isReadOnly() ) {
152  // Cannot change anything in read only
153  return;
154  }
155 
156  if ( !$performer instanceof Authority ) {
157  $performer = $this->userFactory->newFromUserIdentity( $performer );
158  }
159 
160  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
161  // User isn't allowed to edit the watchlist
162  return;
163  }
164 
165  $user = $performer->getUser();
166 
167  if ( !$this->options->get( 'UseEnotif' ) &&
168  !$this->options->get( 'ShowUpdatedMarker' )
169  ) {
170  $this->talkPageNotificationManager->removeUserHasNewMessages( $user );
171  return;
172  }
173 
174  $userId = $user->getId();
175  if ( !$userId ) {
176  return;
177  }
178 
179  $this->watchedItemStore->resetAllNotificationTimestampsForUser( $user );
180 
181  // We also need to clear here the "you have new message" notification for the own
182  // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
183  }
184 
197  $performer,
198  $title,
199  int $oldid = 0
200  ) {
201  if ( $this->readOnlyMode->isReadOnly() ) {
202  // Cannot change anything in read only
203  return;
204  }
205 
206  if ( !$performer instanceof Authority ) {
207  $performer = $this->userFactory->newFromUserIdentity( $performer );
208  }
209 
210  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
211  // User isn't allowed to edit the watchlist
212  return;
213  }
214 
215  $userIdentity = $performer->getUser();
216  $userTalkPage = (
217  $title->getNamespace() === NS_USER_TALK &&
218  $title->getDBkey() === strtr( $userIdentity->getName(), ' ', '_' )
219  );
220 
221  if ( $userTalkPage ) {
222  // If we're working on user's talk page, we should update the talk page message indicator
223  if ( !$this->hookRunner->onUserClearNewTalkNotification(
224  $userIdentity,
225  $oldid
226  ) ) {
227  return;
228  }
229 
230  // Try to update the DB post-send and only if needed...
233  DeferredUpdates::addCallableUpdate( static function () use (
234  $userIdentity,
235  $oldid,
238  ) {
239  if ( !$talkPageNotificationManager->userHasNewMessages( $userIdentity ) ) {
240  // no notifications to clear
241  return;
242  }
243  // Delete the last notifications (they stack up)
245 
246  // If there is a new, unseen, revision, use its timestamp
247  if ( !$oldid ) {
248  return;
249  }
250 
251  $oldRev = $revisionLookup->getRevisionById(
252  $oldid,
253  RevisionLookup::READ_LATEST
254  );
255  if ( !$oldRev ) {
256  return;
257  }
258 
259  $newRev = $revisionLookup->getNextRevision( $oldRev );
260  if ( $newRev ) {
262  $userIdentity,
263  $newRev
264  );
265  }
266  } );
267  }
268 
269  if ( !$this->options->get( 'UseEnotif' ) &&
270  !$this->options->get( 'ShowUpdatedMarker' )
271  ) {
272  return;
273  }
274 
275  if ( !$userIdentity->isRegistered() ) {
276  // Nothing else to do
277  return;
278  }
279 
280  // Only update the timestamp if the page is being watched.
281  // The query to find out if it is watched is cached both in memcached and per-invocation,
282  // and when it does have to be executed, it can be on a replica DB
283  // If this is the user's newtalk page, we always update the timestamp
284  $force = $userTalkPage ? 'force' : '';
285  $this->watchedItemStore->resetNotificationTimestamp( $userIdentity, $title, $force, $oldid );
286  }
287 
296  $userId = $user->getId();
297 
298  if ( !$userId ) {
299  return false;
300  }
301 
302  $cacheKey = 'u' . (string)$userId . '-' .
303  (string)$title->getNamespace() . ':' . $title->getDBkey();
304 
305  // avoid isset here, as it'll return false for null entries
306  if ( array_key_exists( $cacheKey, $this->notificationTimestampCache ) ) {
307  return $this->notificationTimestampCache[ $cacheKey ];
308  }
309 
310  $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $title );
311  if ( $watchedItem ) {
312  $timestamp = $watchedItem->getNotificationTimestamp();
313  } else {
314  $timestamp = false;
315  }
316 
317  $this->notificationTimestampCache[ $cacheKey ] = $timestamp;
318  return $timestamp;
319  }
320 
326  public function isWatchable( PageReference $target ): bool {
327  if ( !$this->nsInfo->isWatchable( $target->getNamespace() ) ) {
328  return false;
329  }
330 
331  if ( $target instanceof PageIdentity && !$target->canExist() ) {
332  // Catch "improper" Title instances
333  return false;
334  }
335 
336  return true;
337  }
338 
346  public function isWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
347  if ( $this->isWatchable( $target ) ) {
348  return $this->watchedItemStore->isWatched( $userIdentity, $target );
349  }
350  return false;
351  }
352 
361  public function isWatched( Authority $performer, PageIdentity $target ): bool {
362  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
363  return $this->isWatchedIgnoringRights( $performer->getUser(), $target );
364  }
365  return false;
366  }
367 
375  public function isTempWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
376  if ( $this->isWatchable( $target ) ) {
377  return $this->watchedItemStore->isTempWatched( $userIdentity, $target );
378  }
379  return false;
380  }
381 
390  public function isTempWatched( Authority $performer, PageIdentity $target ): bool {
391  if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
392  return $this->isTempWatchedIgnoringRights( $performer->getUser(), $target );
393  }
394  return false;
395  }
396 
406  public function addWatchIgnoringRights(
407  UserIdentity $userIdentity,
408  PageIdentity $target,
409  ?string $expiry = null
410  ): StatusValue {
411  if ( !$this->isWatchable( $target ) ) {
412  return StatusValue::newFatal( 'watchlistnotwatchable' );
413  }
414 
415  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
416  $title = $wikiPage->getTitle();
417 
418  // TODO: update hooks to take Authority
419  $status = Status::newFatal( 'hookaborted' );
420  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
421  if ( $this->hookRunner->onWatchArticle( $user, $wikiPage, $status, $expiry ) ) {
422  $status = StatusValue::newGood();
423  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ), $expiry );
424  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
425  $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ), $expiry );
426  }
427  $this->hookRunner->onWatchArticleComplete( $user, $wikiPage );
428  }
429 
430  // eventually user_touched should be factored out of User and this should be replaced
431  $user->invalidateCache();
432 
433  return $status;
434  }
435 
446  public function addWatch(
447  Authority $performer,
448  PageIdentity $target,
449  ?string $expiry = null
450  ): StatusValue {
451  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
452  // TODO: this function should be moved out of User
453  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
454  }
455 
456  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
457  }
458 
466  public function removeWatchIgnoringRights(
467  UserIdentity $userIdentity,
468  PageIdentity $target
469  ): StatusValue {
470  if ( !$this->isWatchable( $target ) ) {
471  return StatusValue::newFatal( 'watchlistnotwatchable' );
472  }
473 
474  $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
475  $title = $wikiPage->getTitle();
476 
477  // TODO: update hooks to take Authority
478  $status = Status::newFatal( 'hookaborted' );
479  $user = $this->userFactory->newFromUserIdentity( $userIdentity );
480  if ( $this->hookRunner->onUnwatchArticle( $user, $wikiPage, $status ) ) {
481  $status = StatusValue::newGood();
482  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ) );
483  if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
484  $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ) );
485  }
486  $this->hookRunner->onUnwatchArticleComplete( $user, $wikiPage );
487  }
488 
489  // eventually user_touched should be factored out of User and this should be replaced
490  $user->invalidateCache();
491 
492  return $status;
493  }
494 
503  public function removeWatch(
504  Authority $performer,
505  PageIdentity $target
506  ): StatusValue {
507  if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
508  // TODO: this function should be moved out of User
509  return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
510  }
511 
512  return $this->removeWatchIgnoringRights( $performer->getUser(), $target );
513  }
514 
528  public function setWatch(
529  bool $watch,
530  Authority $performer,
531  PageIdentity $target,
532  string $expiry = null
533  ): StatusValue {
534  // User must be registered, and either changing the watch state or at least the expiry.
535  if ( !$performer->getUser()->isRegistered() ) {
536  return StatusValue::newGood();
537  }
538 
539  // Only call addWatchhIgnoringRights() or removeWatch() if there's been a change in the watched status.
540  $link = TitleValue::newFromPage( $target );
541  $oldWatchedItem = $this->watchedItemStore->getWatchedItem( $performer->getUser(), $link );
542  $changingWatchStatus = (bool)$oldWatchedItem !== $watch;
543  if ( $oldWatchedItem && $expiry !== null ) {
544  // If there's an old watched item, a non-null change to the expiry requires an UPDATE.
545  $oldWatchPeriod = $oldWatchedItem->getExpiry() ?? 'infinity';
546  $changingWatchStatus = $changingWatchStatus ||
547  $oldWatchPeriod !== ExpiryDef::normalizeExpiry( $expiry, TS_MW );
548  }
549 
550  if ( $changingWatchStatus ) {
551  // If the user doesn't have 'editmywatchlist', we still want to
552  // allow them to add but not remove items via edits and such.
553  if ( $watch ) {
554  return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
555  } else {
556  return $this->removeWatch( $performer, $target );
557  }
558  }
559 
560  return StatusValue::newGood();
561  }
562 }
563 
568 class_alias( WatchlistManager::class, 'MediaWiki\User\WatchlistNotificationManager' );
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
MediaWiki\Watchlist\WatchlistManager\addWatchIgnoringRights
addWatchIgnoringRights(UserIdentity $userIdentity, PageIdentity $target, ?string $expiry=null)
Watch a page.
Definition: WatchlistManager.php:406
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:43
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
MediaWiki\Watchlist\WatchlistManager\$hookRunner
HookRunner $hookRunner
Definition: WatchlistManager.php:66
MediaWiki\Watchlist\WatchlistManager\setWatch
setWatch(bool $watch, Authority $performer, PageIdentity $target, string $expiry=null)
Watch or unwatch a page, calling watch/unwatch hooks as appropriate.
Definition: WatchlistManager.php:528
User\newFatalPermissionDeniedStatus
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:4206
MediaWiki\Watchlist\WatchlistManager\removeWatchIgnoringRights
removeWatchIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Stop watching a page.
Definition: WatchlistManager.php:466
MediaWiki\Watchlist\WatchlistManager\$watchedItemStore
WatchedItemStoreInterface $watchedItemStore
Definition: WatchlistManager.php:78
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:85
MediaWiki\Watchlist\WatchlistManager\$talkPageNotificationManager
TalkPageNotificationManager $talkPageNotificationManager
Definition: WatchlistManager.php:75
MediaWiki\Watchlist\WatchlistManager\isWatchable
isWatchable(PageReference $target)
Definition: WatchlistManager.php:326
ReadOnlyMode
A service class for fetching the wiki's current read-only mode.
Definition: ReadOnlyMode.php:11
MediaWiki\User\TalkPageNotificationManager\removeUserHasNewMessages
removeUserHasNewMessages(UserIdentity $user)
Remove the new messages status.
Definition: TalkPageNotificationManager.php:124
MediaWiki\Watchlist\WatchlistManager\$userFactory
UserFactory $userFactory
Definition: WatchlistManager.php:81
MediaWiki\Watchlist\WatchlistManager\clearAllUserNotifications
clearAllUserNotifications( $performer)
Resets all of the given user's page-change notification timestamps.
Definition: WatchlistManager.php:150
MediaWiki\User\UserIdentity\getId
getId( $wikiId=self::LOCAL)
MediaWiki\Permissions\Authority\getUser
getUser()
Returns the performer of the actions associated with this authority.
Page\PageReference
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Definition: PageReference.php:49
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
MediaWiki\Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
MediaWiki\User\TalkPageNotificationManager
Manages user talk page notifications.
Definition: TalkPageNotificationManager.php:35
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
Wikimedia\ParamValidator\TypeDef\ExpiryDef
Type definition for expiry timestamps.
Definition: ExpiryDef.php:17
MediaWiki\Watchlist\WatchlistManager\isTempWatchedIgnoringRights
isTempWatchedIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Check if the article is temporarily watched by the user.
Definition: WatchlistManager.php:375
MediaWiki\User\TalkPageNotificationManager\setUserHasNewMessages
setUserHasNewMessages(UserIdentity $user, RevisionRecord $curRev=null)
Update the talk page messages status.
Definition: TalkPageNotificationManager.php:107
TitleValue\newFromPage
static newFromPage(PageReference $page)
Constructs a TitleValue from a local PageReference.
Definition: TitleValue.php:119
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:27
MediaWiki\Watchlist\WatchlistManager
WatchlistManager service.
Definition: WatchlistManager.php:52
Page\PageReference\getNamespace
getNamespace()
Returns the page's namespace number.
MediaWiki\Watchlist\WatchlistManager\isTempWatched
isTempWatched(Authority $performer, PageIdentity $target)
Check if the page is temporarily watched by the user and the user has permission to view their watchl...
Definition: WatchlistManager.php:390
MediaWiki\Watchlist\WatchlistManager\$readOnlyMode
ReadOnlyMode $readOnlyMode
Definition: WatchlistManager.php:69
MediaWiki\Revision\RevisionLookup\getNextRevision
getNextRevision(RevisionRecord $rev, $flags=0)
Get next revision for this title.
MediaWiki\Watchlist\WatchlistManager\getTitleNotificationTimestamp
getTitleNotificationTimestamp(UserIdentity $user, $title)
Get the timestamp when this page was updated since the user last saw it.
Definition: WatchlistManager.php:295
DeferredUpdates
Class for managing the deferral of updates within the scope of a PHP script invocation.
Definition: DeferredUpdates.php:82
Page\WikiPageFactory
Definition: WikiPageFactory.php:20
$title
$title
Definition: testCompression.php:38
MediaWiki\Watchlist\WatchlistManager\clearTitleUserNotifications
clearTitleUserNotifications( $performer, $title, int $oldid=0)
Clear the user's notification timestamp for the given title.
Definition: WatchlistManager.php:196
Wikimedia\ParamValidator\TypeDef\ExpiryDef\normalizeExpiry
static normalizeExpiry(?string $expiry=null, ?int $style=null)
Normalize a user-inputted expiry in ConvertibleTimestamp.
Definition: ExpiryDef.php:92
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
MediaWiki\Watchlist
Definition: WatchlistManager.php:23
MediaWiki\Watchlist\WatchlistManager\isWatchedIgnoringRights
isWatchedIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Check if the page is watched by the user.
Definition: WatchlistManager.php:346
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
MediaWiki\User\TalkPageNotificationManager\userHasNewMessages
userHasNewMessages(UserIdentity $user)
Check if the user has new messages.
Definition: TalkPageNotificationManager.php:83
MediaWiki\Watchlist\WatchlistManager\CONSTRUCTOR_OPTIONS
const CONSTRUCTOR_OPTIONS
Definition: WatchlistManager.php:57
MediaWiki\Permissions\Authority\isAllowed
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
MediaWiki\Watchlist\WatchlistManager\isWatched
isWatched(Authority $performer, PageIdentity $target)
Check if the page is watched by the user and the user has permission to view their watchlist.
Definition: WatchlistManager.php:361
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
MediaWiki\Watchlist\WatchlistManager\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: WatchlistManager.php:87
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
MediaWiki\Watchlist\WatchlistManager\$notificationTimestampCache
array $notificationTimestampCache
Cache for getTitleNotificationTimestamp.
Definition: WatchlistManager.php:105
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:35
MediaWiki\Watchlist\WatchlistManager\$nsInfo
NamespaceInfo $nsInfo
Definition: WatchlistManager.php:84
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:556
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
WatchedItemStoreInterface
Definition: WatchedItemStoreInterface.php:31
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
Definition: DeferredUpdates.php:145
MediaWiki\Watchlist\WatchlistManager\__construct
__construct(ServiceOptions $options, HookContainer $hookContainer, ReadOnlyMode $readOnlyMode, RevisionLookup $revisionLookup, TalkPageNotificationManager $talkPageNotificationManager, WatchedItemStoreInterface $watchedItemStore, UserFactory $userFactory, NamespaceInfo $nsInfo, WikiPageFactory $wikiPageFactory)
Definition: WatchlistManager.php:118
MediaWiki\Watchlist\WatchlistManager\addWatch
addWatch(Authority $performer, PageIdentity $target, ?string $expiry=null)
Watch a page if the user has permission to edit their watchlist.
Definition: WatchlistManager.php:446
MediaWiki\User\UserFactory
Creates User objects.
Definition: UserFactory.php:41
MediaWiki\Watchlist\WatchlistManager\removeWatch
removeWatch(Authority $performer, PageIdentity $target)
Stop watching a page if the user has permission to edit their watchlist.
Definition: WatchlistManager.php:503
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:71
MediaWiki\Watchlist\WatchlistManager\$options
ServiceOptions $options
Definition: WatchlistManager.php:63
MediaWiki\Revision\RevisionLookup\getRevisionById
getRevisionById( $id, $flags=0, PageIdentity $page=null)
Load a page revision from a given revision ID number.
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:40
MediaWiki\Watchlist\WatchlistManager\$revisionLookup
RevisionLookup $revisionLookup
Definition: WatchlistManager.php:72