MediaWiki REL1_39
WatchlistManager.php
Go to the documentation of this file.
1<?php
2
24
40use ReadOnlyMode;
41use Status;
42use StatusValue;
43use User;
46
53
57 public const CONSTRUCTOR_OPTIONS = [
61 ];
62
64 private $options;
65
67 private $hookRunner;
68
70 private $readOnlyMode;
71
73 private $revisionLookup;
74
76 private $talkPageNotificationManager;
77
79 private $watchedItemStore;
80
82 private $userFactory;
83
85 private $nsInfo;
86
88 private $wikiPageFactory;
89
106 private $notificationTimestampCache = [];
107
119 public function __construct(
120 ServiceOptions $options,
121 HookContainer $hookContainer,
122 ReadOnlyMode $readOnlyMode,
123 RevisionLookup $revisionLookup,
124 TalkPageNotificationManager $talkPageNotificationManager,
125 WatchedItemStoreInterface $watchedItemStore,
126 UserFactory $userFactory,
127 NamespaceInfo $nsInfo,
128 WikiPageFactory $wikiPageFactory
129 ) {
130 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
131 $this->options = $options;
132 $this->hookRunner = new HookRunner( $hookContainer );
133 $this->readOnlyMode = $readOnlyMode;
134 $this->revisionLookup = $revisionLookup;
135 $this->talkPageNotificationManager = $talkPageNotificationManager;
136 $this->watchedItemStore = $watchedItemStore;
137 $this->userFactory = $userFactory;
138 $this->nsInfo = $nsInfo;
139 $this->wikiPageFactory = $wikiPageFactory;
140 }
141
151 public function clearAllUserNotifications( $performer ) {
152 if ( $this->readOnlyMode->isReadOnly() ) {
153 // Cannot change anything in read only
154 return;
155 }
156
157 if ( !$performer instanceof Authority ) {
158 $performer = $this->userFactory->newFromUserIdentity( $performer );
159 }
160
161 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
162 // User isn't allowed to edit the watchlist
163 return;
164 }
165
166 $user = $performer->getUser();
167
168 if ( !$this->options->get( MainConfigNames::EnotifUserTalk ) &&
169 !$this->options->get( MainConfigNames::EnotifWatchlist ) &&
170 !$this->options->get( MainConfigNames::ShowUpdatedMarker )
171 ) {
172 $this->talkPageNotificationManager->removeUserHasNewMessages( $user );
173 return;
174 }
175
176 if ( !$user->isRegistered() ) {
177 return;
178 }
179
180 $this->watchedItemStore->resetAllNotificationTimestampsForUser( $user );
181
182 // We also need to clear here the "you have new message" notification for the own
183 // user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
184 }
185
200 $performer,
201 $title,
202 int $oldid = 0,
203 RevisionRecord $oldRev = null
204 ) {
205 if ( $this->readOnlyMode->isReadOnly() ) {
206 // Cannot change anything in read only
207 return;
208 }
209
210 if ( !$performer instanceof Authority ) {
211 $performer = $this->userFactory->newFromUserIdentity( $performer );
212 }
213
214 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
215 // User isn't allowed to edit the watchlist
216 return;
217 }
218
219 $userIdentity = $performer->getUser();
220 $userTalkPage = (
221 $title->getNamespace() === NS_USER_TALK &&
222 $title->getDBkey() === strtr( $userIdentity->getName(), ' ', '_' )
223 );
224
225 if ( $userTalkPage ) {
226 if ( !$oldid ) {
227 $oldRev = null;
228 } elseif ( !$oldRev ) {
229 $oldRev = $this->revisionLookup->getRevisionById( $oldid );
230 }
231 $this->talkPageNotificationManager->clearForPageView( $userIdentity, $oldRev );
232 }
233
234 if ( !$this->options->get( MainConfigNames::EnotifUserTalk ) &&
235 !$this->options->get( MainConfigNames::EnotifWatchlist ) &&
236 !$this->options->get( MainConfigNames::ShowUpdatedMarker )
237 ) {
238 return;
239 }
240
241 if ( !$userIdentity->isRegistered() ) {
242 // Nothing else to do
243 return;
244 }
245
246 // Only update the timestamp if the page is being watched.
247 // The query to find out if it is watched is cached both in memcached and per-invocation,
248 // and when it does have to be executed, it can be on a replica DB
249 // If this is the user's newtalk page, we always update the timestamp
250 $force = $userTalkPage ? 'force' : '';
251 $this->watchedItemStore->resetNotificationTimestamp( $userIdentity, $title, $force, $oldid );
252 }
253
262 if ( !$user->isRegistered() ) {
263 return false;
264 }
265
266 $cacheKey = 'u' . (string)$user->getId() . '-' .
267 (string)$title->getNamespace() . ':' . $title->getDBkey();
268
269 // avoid isset here, as it'll return false for null entries
270 if ( array_key_exists( $cacheKey, $this->notificationTimestampCache ) ) {
271 return $this->notificationTimestampCache[ $cacheKey ];
272 }
273
274 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $title );
275 if ( $watchedItem ) {
276 $timestamp = $watchedItem->getNotificationTimestamp();
277 } else {
278 $timestamp = false;
279 }
280
281 $this->notificationTimestampCache[ $cacheKey ] = $timestamp;
282 return $timestamp;
283 }
284
290 public function isWatchable( PageReference $target ): bool {
291 if ( !$this->nsInfo->isWatchable( $target->getNamespace() ) ) {
292 return false;
293 }
294
295 if ( $target instanceof PageIdentity && !$target->canExist() ) {
296 // Catch "improper" Title instances
297 return false;
298 }
299
300 return true;
301 }
302
310 public function isWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
311 if ( $this->isWatchable( $target ) ) {
312 return $this->watchedItemStore->isWatched( $userIdentity, $target );
313 }
314 return false;
315 }
316
325 public function isWatched( Authority $performer, PageIdentity $target ): bool {
326 if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
327 return $this->isWatchedIgnoringRights( $performer->getUser(), $target );
328 }
329 return false;
330 }
331
339 public function isTempWatchedIgnoringRights( UserIdentity $userIdentity, PageIdentity $target ): bool {
340 if ( $this->isWatchable( $target ) ) {
341 return $this->watchedItemStore->isTempWatched( $userIdentity, $target );
342 }
343 return false;
344 }
345
354 public function isTempWatched( Authority $performer, PageIdentity $target ): bool {
355 if ( $performer->isAllowed( 'viewmywatchlist' ) ) {
356 return $this->isTempWatchedIgnoringRights( $performer->getUser(), $target );
357 }
358 return false;
359 }
360
370 public function addWatchIgnoringRights(
371 UserIdentity $userIdentity,
372 PageIdentity $target,
373 ?string $expiry = null
374 ): StatusValue {
375 if ( !$this->isWatchable( $target ) ) {
376 return StatusValue::newFatal( 'watchlistnotwatchable' );
377 }
378
379 $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
380 $title = $wikiPage->getTitle();
381
382 // TODO: update hooks to take Authority
383 $status = Status::newFatal( 'hookaborted' );
384 $user = $this->userFactory->newFromUserIdentity( $userIdentity );
385 if ( $this->hookRunner->onWatchArticle( $user, $wikiPage, $status, $expiry ) ) {
386 $status = StatusValue::newGood();
387 $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ), $expiry );
388 if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
389 $this->watchedItemStore->addWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ), $expiry );
390 }
391 $this->hookRunner->onWatchArticleComplete( $user, $wikiPage );
392 }
393
394 // eventually user_touched should be factored out of User and this should be replaced
395 $user->invalidateCache();
396
397 return $status;
398 }
399
410 public function addWatch(
411 Authority $performer,
412 PageIdentity $target,
413 ?string $expiry = null
414 ): StatusValue {
415 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
416 // TODO: this function should be moved out of User
417 return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
418 }
419
420 return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
421 }
422
431 UserIdentity $userIdentity,
432 PageIdentity $target
433 ): StatusValue {
434 if ( !$this->isWatchable( $target ) ) {
435 return StatusValue::newFatal( 'watchlistnotwatchable' );
436 }
437
438 $wikiPage = $this->wikiPageFactory->newFromTitle( $target );
439 $title = $wikiPage->getTitle();
440
441 // TODO: update hooks to take Authority
442 $status = Status::newFatal( 'hookaborted' );
443 $user = $this->userFactory->newFromUserIdentity( $userIdentity );
444 if ( $this->hookRunner->onUnwatchArticle( $user, $wikiPage, $status ) ) {
445 $status = StatusValue::newGood();
446 $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getSubjectPage( $title ) );
447 if ( $this->nsInfo->canHaveTalkPage( $title ) ) {
448 $this->watchedItemStore->removeWatch( $userIdentity, $this->nsInfo->getTalkPage( $title ) );
449 }
450 $this->hookRunner->onUnwatchArticleComplete( $user, $wikiPage );
451 }
452
453 // eventually user_touched should be factored out of User and this should be replaced
454 $user->invalidateCache();
455
456 return $status;
457 }
458
467 public function removeWatch(
468 Authority $performer,
469 PageIdentity $target
470 ): StatusValue {
471 if ( !$performer->isAllowed( 'editmywatchlist' ) ) {
472 // TODO: this function should be moved out of User
473 return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
474 }
475
476 return $this->removeWatchIgnoringRights( $performer->getUser(), $target );
477 }
478
492 public function setWatch(
493 bool $watch,
494 Authority $performer,
495 PageIdentity $target,
496 string $expiry = null
497 ): StatusValue {
498 // User must be registered, and either changing the watch state or at least the expiry.
499 if ( !$performer->getUser()->isRegistered() ) {
500 return StatusValue::newGood();
501 }
502
503 // Only call addWatchIgnoringRights() or removeWatch() if there's been a change in the watched status.
504 $oldWatchedItem = $this->watchedItemStore->getWatchedItem( $performer->getUser(), $target );
505 $changingWatchStatus = (bool)$oldWatchedItem !== $watch;
506 if ( $oldWatchedItem && $expiry !== null ) {
507 // If there's an old watched item, a non-null change to the expiry requires an UPDATE.
508 $oldWatchPeriod = $oldWatchedItem->getExpiry() ?? 'infinity';
509 $changingWatchStatus = $changingWatchStatus ||
510 $oldWatchPeriod !== ExpiryDef::normalizeExpiry( $expiry, TS_MW );
511 }
512
513 if ( $changingWatchStatus ) {
514 // If the user doesn't have 'editmywatchlist', we still want to
515 // allow them to add but not remove items via edits and such.
516 if ( $watch ) {
517 return $this->addWatchIgnoringRights( $performer->getUser(), $target, $expiry );
518 } else {
519 return $this->removeWatch( $performer, $target );
520 }
521 }
522
523 return StatusValue::newGood();
524 }
525}
526
531class_alias( WatchlistManager::class, 'MediaWiki\User\WatchlistNotificationManager' );
getUser()
const NS_USER_TALK
Definition Defines.php:67
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
const EnotifWatchlist
Name constant for the EnotifWatchlist setting, for use with Config::get()
const EnotifUserTalk
Name constant for the EnotifUserTalk setting, for use with Config::get()
const ShowUpdatedMarker
Name constant for the ShowUpdatedMarker setting, for use with Config::get()
Service for creating WikiPage objects.
Page revision base class.
Creates User objects.
isTempWatchedIgnoringRights(UserIdentity $userIdentity, PageIdentity $target)
Check if the article is temporarily watched by the user.
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.
__construct(ServiceOptions $options, HookContainer $hookContainer, ReadOnlyMode $readOnlyMode, RevisionLookup $revisionLookup, TalkPageNotificationManager $talkPageNotificationManager, WatchedItemStoreInterface $watchedItemStore, UserFactory $userFactory, NamespaceInfo $nsInfo, WikiPageFactory $wikiPageFactory)
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...
A service class for fetching the wiki's current read-only mode.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static newGood( $value=null)
Factory function for good results.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
internal since 1.36
Definition User.php:70
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition User.php:3402
Type definition for expiry timestamps.
Definition ExpiryDef.php:17
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)