Wrap libxml_disable_entity_loader() calls in version constraint
[mediawiki.git] / includes / user / TalkPageNotificationManager.php
blobdaf63cbf8b52de46f7bb7f78e9b5aaac7f2c4b78
1 <?php
3 /**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
19 * @file
22 namespace MediaWiki\User;
24 use MediaWiki\Config\ServiceOptions;
25 use MediaWiki\Revision\RevisionLookup;
26 use MediaWiki\Revision\RevisionRecord;
27 use MWTimestamp;
28 use ReadOnlyMode;
29 use Wikimedia\Rdbms\ILoadBalancer;
31 /**
32 * Manages user talk page notifications
33 * @since 1.35
35 class TalkPageNotificationManager {
37 /**
38 * @internal For use by ServiceWiring
40 public const CONSTRUCTOR_OPTIONS = [
41 'DisableAnonTalk'
44 /** @var array */
45 private $userMessagesCache = [];
47 /** @var bool */
48 private $disableAnonTalk;
50 /** @var ILoadBalancer */
51 private $loadBalancer;
53 /** @var ReadOnlyMode */
54 private $readOnlyMode;
56 /** @var RevisionLookup */
57 private $revisionLookup;
59 /**
60 * @param ServiceOptions $serviceOptions
61 * @param ILoadBalancer $loadBalancer
62 * @param ReadOnlyMode $readOnlyMode
63 * @param RevisionLookup $revisionLookup
65 public function __construct(
66 ServiceOptions $serviceOptions,
67 ILoadBalancer $loadBalancer,
68 ReadOnlyMode $readOnlyMode,
69 RevisionLookup $revisionLookup
70 ) {
71 $serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
72 $this->disableAnonTalk = $serviceOptions->get( 'DisableAnonTalk' );
73 $this->loadBalancer = $loadBalancer;
74 $this->readOnlyMode = $readOnlyMode;
75 $this->revisionLookup = $revisionLookup;
78 /**
79 * Check if the user has new messages.
80 * @param UserIdentity $user
81 * @return bool whether the user has new messages
83 public function userHasNewMessages( UserIdentity $user ) : bool {
84 $userKey = $this->getCacheKey( $user );
86 // Load the newtalk status if it is unloaded
87 if ( !isset( $this->userMessagesCache[$userKey] ) ) {
88 if ( $this->isTalkDisabled( $user ) ) {
89 // Anon disabled by configuration.
90 $this->userMessagesCache[$userKey] = false;
91 } else {
92 $this->userMessagesCache[$userKey] = $this->dbCheckNewUserMessages( $user );
96 return (bool)$this->userMessagesCache[$userKey];
99 /**
100 * Update the talk page messages status.
102 * @param UserIdentity $user
103 * @param RevisionRecord|null $curRev New, as yet unseen revision of the user talk page.
104 * Null is acceptable in case the revision is not known. This will indicate that new messages
105 * exist, but will not affect the latest seen message timestamp
107 public function setUserHasNewMessages(
108 UserIdentity $user,
109 RevisionRecord $curRev = null
110 ) : void {
111 if ( $this->isTalkDisabled( $user ) ) {
112 return;
115 $userKey = $this->getCacheKey( $user );
116 $this->userMessagesCache[$userKey] = true;
117 $this->dbUpdateNewUserMessages( $user, $curRev );
121 * Remove the new messages status
122 * @param UserIdentity $user
124 public function removeUserHasNewMessages( UserIdentity $user ) : void {
125 if ( $this->isTalkDisabled( $user ) ) {
126 return;
129 $userKey = $this->getCacheKey( $user );
130 $this->userMessagesCache[$userKey] = false;
132 $this->dbDeleteNewUserMessages( $user );
136 * Returns the timestamp of the latest revision of the user talkpage
137 * that the user has already seen in TS_MW format.
138 * If the user has no new messages, returns null
140 * @param UserIdentity $user
141 * @return string|null
143 public function getLatestSeenMessageTimestamp( UserIdentity $user ) : ?string {
144 $userKey = $this->getCacheKey( $user );
145 // Don't use self::userHasNewMessages here to avoid an extra DB query
146 // in case the value is not cached already
147 if ( $this->isTalkDisabled( $user ) ||
148 isset( $this->userMessagesCache[$userKey] ) && !$this->userMessagesCache[$userKey]
150 return null;
153 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
154 list( $field, $id ) = $this->getQueryFieldAndId( $user );
155 // Get the "last viewed rev" timestamp from the oldest message notification
156 $timestamp = $dbr->selectField(
157 'user_newtalk',
158 'MIN(user_last_timestamp)',
159 [ $field => $id ],
160 __METHOD__
162 if ( $timestamp ) {
163 // TODO: Once passing 'null' as Revision setUserHasNewMessages is removed,
164 // null $timestamp would mean no new messages, so negatives can be cached too.
165 $this->userMessagesCache[$userKey] = true;
167 return $timestamp !== null ? MWTimestamp::convert( TS_MW, $timestamp ) : null;
171 * Remove the cached newtalk status for the given user
172 * @internal There should be no need to call this other than from User::clearInstanceCache
173 * @param UserIdentity $user
175 public function clearInstanceCache( UserIdentity $user ) : void {
176 $userKey = $this->getCacheKey( $user );
177 $this->userMessagesCache[$userKey] = null;
181 * Check whether the talk page is disabled for a user
182 * @param UserIdentity $user
183 * @return bool
185 private function isTalkDisabled( UserIdentity $user ) : bool {
186 return !$user->isRegistered() && $this->disableAnonTalk;
190 * Internal uncached check for new messages
191 * @param UserIdentity $user
192 * @return bool True if the user has new messages
194 private function dbCheckNewUserMessages( UserIdentity $user ) : bool {
195 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
196 list( $field, $id ) = $this->getQueryFieldAndId( $user );
197 $ok = $dbr->selectField(
198 'user_newtalk',
199 $field,
200 [ $field => $id ],
201 __METHOD__
203 return (bool)$ok;
207 * Add or update the new messages flag
208 * @param UserIdentity $user
209 * @param RevisionRecord|null $curRev New, as yet unseen revision of the
210 * user talk page. Ignored if null.
211 * @return bool True if successful, false otherwise
213 private function dbUpdateNewUserMessages(
214 UserIdentity $user,
215 RevisionRecord $curRev = null
216 ) : bool {
217 if ( $this->readOnlyMode->isReadOnly() ) {
218 return false;
221 if ( $curRev ) {
222 $prevRev = $this->revisionLookup->getPreviousRevision( $curRev );
223 $ts = $prevRev ? $prevRev->getTimestamp() : null;
224 } else {
225 $ts = null;
228 // Mark the user as having new messages since this revision
229 $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
230 list( $field, $id ) = $this->getQueryFieldAndId( $user );
231 $dbw->insert(
232 'user_newtalk',
234 $field => $id,
235 'user_last_timestamp' => $dbw->timestampOrNull( $ts )
237 __METHOD__,
238 [ 'IGNORE' ]
240 return (bool)$dbw->affectedRows();
244 * Clear the new messages flag for the given user
245 * @param UserIdentity $user
246 * @return bool True if successful, false otherwise
248 private function dbDeleteNewUserMessages( UserIdentity $user ) : bool {
249 if ( $this->readOnlyMode->isReadOnly() ) {
250 return false;
252 $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
253 list( $field, $id ) = $this->getQueryFieldAndId( $user );
254 $dbw->delete(
255 'user_newtalk',
256 [ $field => $id ],
257 __METHOD__
259 return (bool)$dbw->affectedRows();
263 * Get the field name and id for the user_newtalk table query
264 * @param UserIdentity $user
265 * @return array ( string $field, string|int $id )
267 private function getQueryFieldAndId( UserIdentity $user ) : array {
268 if ( $user->isRegistered() ) {
269 $field = 'user_id';
270 $id = $user->getId();
271 } else {
272 $field = 'user_ip';
273 $id = $user->getName();
275 return [ $field, $id ];
279 * Gets a unique key for various caches.
280 * @param UserIdentity $user
281 * @return string
283 private function getCacheKey( UserIdentity $user ) : string {
284 return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";