MediaWiki master
UserEditTracker.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\User;
4
5use InvalidArgumentException;
6use LogicException;
15use Wikimedia\Timestamp\ConvertibleTimestamp;
16use Wikimedia\Timestamp\TimestampFormat as TS;
17
26
27 private const FIRST_EDIT = 1;
28 private const LATEST_EDIT = 2;
29
30 private ActorNormalization $actorNormalization;
31 private IConnectionProvider $dbProvider;
32 private JobQueueGroup $jobQueueGroup;
33
42 private $userEditCountCache = [];
43
49 public function __construct(
50 ActorNormalization $actorNormalization,
51 IConnectionProvider $dbProvider,
52 JobQueueGroup $jobQueueGroup
53 ) {
54 $this->actorNormalization = $actorNormalization;
55 $this->dbProvider = $dbProvider;
56 $this->jobQueueGroup = $jobQueueGroup;
57 }
58
65 public function getUserEditCount( UserIdentity $user ): ?int {
66 if ( !$user->isRegistered() ) {
67 return null;
68 }
69
70 $cacheKey = $this->getCacheKey( $user );
71 if ( isset( $this->userEditCountCache[ $cacheKey ] ) ) {
72 return $this->userEditCountCache[ $cacheKey ];
73 }
74
75 $wikiId = $user->getWikiId();
76 $userId = $user->getId( $wikiId );
77 $count = $this->dbProvider->getReplicaDatabase( $wikiId )->newSelectQueryBuilder()
78 ->select( 'user_editcount' )
79 ->from( 'user' )
80 ->where( [ 'user_id' => $userId ] )
81 ->caller( __METHOD__ )->fetchField();
82
83 if ( $count === null ) {
84 // it has not been initialized. do so.
85 $count = $this->initializeUserEditCount( $user );
86 }
87
88 $this->userEditCountCache[ $cacheKey ] = $count;
89 return $count;
90 }
91
107 public function preloadUserEditCountCache( array $users ): void {
108 $userIds = [];
109
110 foreach ( $users as $user ) {
111 if (
112 $user->isRegistered() &&
113 $user->getWikiId() === UserIdentity::LOCAL
114 ) {
115 $userIds[] = $user->getId();
116 }
117 }
118
119 $userIds = array_unique( $userIds );
120
121 $dbr = $this->dbProvider->getReplicaDatabase();
122
123 foreach ( array_chunk( $userIds, 500 ) as $batch ) {
124 $rows = $dbr->newSelectQueryBuilder()
125 ->select( [ 'user_id', 'user_editcount' ] )
126 ->from( 'user' )
127 ->where( [ 'user_id' => $batch ] )
128 ->caller( __METHOD__ )
129 ->fetchResultSet();
130
131 foreach ( $rows as $row ) {
132 if ( $row->user_editcount !== null ) {
133 $key = $this->getCacheKeyByUserId( (int)$row->user_id );
134
135 $this->userEditCountCache[$key] = (int)$row->user_editcount;
136 }
137 }
138 }
139 }
140
146 public function initializeUserEditCount( UserIdentity $user ): int {
147 if ( $user->getWikiId() !== UserIdentity::LOCAL ) {
148 // Don't record edits on remote wikis
149 throw new LogicException( __METHOD__ . ' only supports local users' );
150 }
151
152 $dbr = $this->dbProvider->getReplicaDatabase();
153 $count = (int)$dbr->newSelectQueryBuilder()
154 ->select( 'COUNT(*)' )
155 ->from( 'revision' )
156 ->where( [ 'rev_actor' => $this->actorNormalization->findActorId( $user, $dbr ) ] )
157 ->caller( __METHOD__ )
158 ->fetchField();
159
160 // Defer updating the edit count via a job (T259719)
161 $this->jobQueueGroup->push( new UserEditCountInitJob( [
162 'userId' => $user->getId(),
163 'editCount' => $count,
164 ] ) );
165
166 return $count;
167 }
168
175 public function incrementUserEditCount( UserIdentity $user ) {
176 if ( !$user->isRegistered() ) {
177 // Can't store editcount without user row (i.e. unregistered)
178 return;
179 }
180
181 DeferredUpdates::addUpdate(
182 new UserEditCountUpdate( $user, 1 ),
183 DeferredUpdates::POSTSEND
184 );
185 }
186
195 public function getFirstEditTimestamp( UserIdentity $user, int $flags = IDBAccessObject::READ_NORMAL ) {
196 return $this->getUserEditTimestamp( $user, self::FIRST_EDIT, $flags );
197 }
198
207 public function getLatestEditTimestamp( UserIdentity $user, int $flags = IDBAccessObject::READ_NORMAL ) {
208 return $this->getUserEditTimestamp( $user, self::LATEST_EDIT, $flags );
209 }
210
219 private function getUserEditTimestamp( UserIdentity $user, int $type, int $flags = IDBAccessObject::READ_NORMAL ) {
220 if ( !$user->isRegistered() ) {
221 return false;
222 }
223 if ( $flags & IDBAccessObject::READ_LATEST ) {
224 $db = $this->dbProvider->getPrimaryDatabase( $user->getWikiId() );
225 } else {
226 $db = $this->dbProvider->getReplicaDatabase( $user->getWikiId() );
227 }
228
229 $sortOrder = ( $type === self::FIRST_EDIT ) ? SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC;
230 $time = $db->newSelectQueryBuilder()
231 ->select( 'rev_timestamp' )
232 ->from( 'revision' )
233 ->where( [ 'rev_actor' => $this->actorNormalization->findActorId( $user, $db ) ] )
234 ->orderBy( 'rev_timestamp', $sortOrder )
235 ->caller( __METHOD__ )
236 ->fetchField();
237
238 if ( !$time ) {
239 return false; // no edits
240 }
241
242 return ConvertibleTimestamp::convert( TS::MW, $time );
243 }
244
249 public function clearUserEditCache( UserIdentity $user ) {
250 if ( !$user->isRegistered() ) {
251 return;
252 }
253
254 $cacheKey = $this->getCacheKey( $user );
255 unset( $this->userEditCountCache[ $cacheKey ] );
256 }
257
264 public function setCachedUserEditCount( UserIdentity $user, int $editCount ) {
265 if ( !$user->isRegistered() ) {
266 throw new InvalidArgumentException( __METHOD__ . ' with an anonymous user' );
267 }
268
269 $cacheKey = $this->getCacheKey( $user );
270 $this->userEditCountCache[ $cacheKey ] = $editCount;
271 }
272
280 private function getCacheKey( UserIdentity $user ): string {
281 if ( !$user->isRegistered() ) {
282 throw new InvalidArgumentException( 'Cannot prepare cache key for an anonymous user' );
283 }
284
285 $wikiId = $user->getWikiId();
286
287 return $this->getCacheKeyByUserId( $user->getId( $wikiId ), $wikiId );
288 }
289
299 private function getCacheKeyByUserId(
300 int $userId,
301 string|bool $wikiId = WikiAwareEntity::LOCAL
302 ): string {
303 $isRemoteWiki = ( $wikiId !== UserIdentity::LOCAL ) && !WikiMap::isCurrentWikiId( $wikiId );
304 if ( $isRemoteWiki ) {
305 return $wikiId . ':u' . $userId;
306 }
307 return 'u' . $userId;
308 }
309}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
Defer callable updates to run later in the PHP process.
Handles increment the edit count for a given set of users.
Handle enqueueing of background jobs.
Job that initializes an user's edit count.
Track info about user edit counts and timings.
getLatestEditTimestamp(UserIdentity $user, int $flags=IDBAccessObject::READ_NORMAL)
Get the user's latest edit timestamp.
preloadUserEditCountCache(array $users)
Preloads the internal edit count cache for the given users.
getUserEditCount(UserIdentity $user)
Get a user's edit count from the user_editcount field, falling back to initialize.
getFirstEditTimestamp(UserIdentity $user, int $flags=IDBAccessObject::READ_NORMAL)
Get the user's first edit timestamp.
incrementUserEditCount(UserIdentity $user)
Schedule a job to increase a user's edit count.
initializeUserEditCount(UserIdentity $user)
clearUserEditCache(UserIdentity $user)
__construct(ActorNormalization $actorNormalization, IConnectionProvider $dbProvider, JobQueueGroup $jobQueueGroup)
setCachedUserEditCount(UserIdentity $user, int $editCount)
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
Build SELECT queries with a fluent interface.
Marker interface for entities aware of the wiki they belong to.
getWikiId()
Get the ID of the wiki this page belongs to.
const LOCAL
Wiki ID value to use with instances that are defined relative to the local wiki.
Service for dealing with the actor table.
Interface for objects representing user identity.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
getId( $wikiId=self::LOCAL)
Provide primary and replica IDatabase connections.
Interface for database access objects.