1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types = 1); |
4
|
|
|
|
5
|
|
|
namespace App\Repository; |
6
|
|
|
|
7
|
|
|
use App\Model\Project; |
8
|
|
|
use App\Model\User; |
9
|
|
|
use Doctrine\DBAL\Driver\ResultStatement; |
10
|
|
|
use GuzzleHttp\Client; |
11
|
|
|
use Psr\Cache\CacheItemPoolInterface; |
12
|
|
|
use Psr\Container\ContainerInterface; |
13
|
|
|
use Psr\Log\LoggerInterface; |
14
|
|
|
use Symfony\Component\HttpFoundation\Session\Session; |
15
|
|
|
use Wikimedia\IPUtils; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* This class provides data for the User class. |
19
|
|
|
* @codeCoverageIgnore |
20
|
|
|
*/ |
21
|
|
|
class UserRepository extends Repository |
22
|
|
|
{ |
23
|
|
|
protected ProjectRepository $projectRepo; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @param ContainerInterface $container |
27
|
|
|
* @param CacheItemPoolInterface $cache |
28
|
|
|
* @param Client $guzzle |
29
|
|
|
* @param LoggerInterface $logger |
30
|
|
|
* @param bool $isWMF |
31
|
|
|
* @param int $queryTimeout |
32
|
|
|
* @param ProjectRepository $projectRepo |
33
|
|
|
*/ |
34
|
|
|
public function __construct( |
35
|
|
|
ContainerInterface $container, |
36
|
|
|
CacheItemPoolInterface $cache, |
37
|
|
|
Client $guzzle, |
38
|
|
|
LoggerInterface $logger, |
39
|
|
|
bool $isWMF, |
40
|
|
|
int $queryTimeout, |
41
|
|
|
ProjectRepository $projectRepo |
42
|
|
|
) { |
43
|
|
|
$this->projectRepo = $projectRepo; |
44
|
|
|
parent::__construct($container, $cache, $guzzle, $logger, $isWMF, $queryTimeout); |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Get the user's ID and registration date. |
49
|
|
|
* @param string $databaseName The database to query. |
50
|
|
|
* @param string $username The username to find. |
51
|
|
|
* @return array|false With keys 'userId' and regDate'. false if user not found. |
52
|
|
|
*/ |
53
|
|
|
public function getIdAndRegistration(string $databaseName, string $username) |
54
|
|
|
{ |
55
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_id_reg'); |
56
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
57
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
$userTable = $this->getTableName($databaseName, 'user'); |
61
|
|
|
$sql = "SELECT user_id AS userId, user_registration AS regDate |
62
|
|
|
FROM $userTable |
63
|
|
|
WHERE user_name = :username |
64
|
|
|
LIMIT 1"; |
65
|
|
|
$resultQuery = $this->executeProjectsQuery($databaseName, $sql, ['username' => $username]); |
66
|
|
|
|
67
|
|
|
// Cache and return. |
68
|
|
|
return $this->setCache($cacheKey, $resultQuery->fetchAssociative()); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Get the user's actor ID. |
73
|
|
|
* @param string $databaseName |
74
|
|
|
* @param string $username |
75
|
|
|
* @return int|null |
76
|
|
|
*/ |
77
|
|
|
public function getActorId(string $databaseName, string $username): ?int |
78
|
|
|
{ |
79
|
|
|
if (IPUtils::isValidRange($username)) { |
80
|
|
|
return null; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_actor_id'); |
84
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
85
|
|
|
return (int)$this->cache->getItem($cacheKey)->get(); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
$actorTable = $this->getTableName($databaseName, 'actor'); |
89
|
|
|
|
90
|
|
|
$sql = "SELECT actor_id |
91
|
|
|
FROM $actorTable |
92
|
|
|
WHERE actor_name = :username |
93
|
|
|
LIMIT 1"; |
94
|
|
|
$resultQuery = $this->executeProjectsQuery($databaseName, $sql, ['username' => $username]); |
95
|
|
|
|
96
|
|
|
// Cache and return. |
97
|
|
|
return (int)$this->setCache($cacheKey, $resultQuery->fetchOne()); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Get the user's (system) edit count. |
102
|
|
|
* @param string $databaseName The database to query. |
103
|
|
|
* @param string $username The username to find. |
104
|
|
|
* @return int As returned by the database. |
105
|
|
|
*/ |
106
|
|
|
public function getEditCount(string $databaseName, string $username): int |
107
|
|
|
{ |
108
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_edit_count'); |
109
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
110
|
|
|
return (int)$this->cache->getItem($cacheKey)->get(); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
$userTable = $this->getTableName($databaseName, 'user'); |
114
|
|
|
$sql = "SELECT user_editcount FROM $userTable WHERE user_name = :username LIMIT 1"; |
115
|
|
|
$resultQuery = $this->executeProjectsQuery($databaseName, $sql, ['username' => $username]); |
116
|
|
|
|
117
|
|
|
return (int)$this->setCache($cacheKey, $resultQuery->fetchColumn()); |
|
|
|
|
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Search the ipblocks table to see if the user is currently blocked and return the expiry if they are. |
122
|
|
|
* @param string $databaseName The database to query. |
123
|
|
|
* @param string $username The username of the user to search for. |
124
|
|
|
* @return bool|string Expiry of active block or false |
125
|
|
|
*/ |
126
|
|
|
public function getBlockExpiry(string $databaseName, string $username) |
127
|
|
|
{ |
128
|
|
|
$ipblocksTable = $this->getTableName($databaseName, 'ipblocks', 'ipindex'); |
129
|
|
|
$sql = "SELECT ipb_expiry |
130
|
|
|
FROM $ipblocksTable |
131
|
|
|
WHERE ipb_address = :username |
132
|
|
|
LIMIT 1"; |
133
|
|
|
$resultQuery = $this->executeProjectsQuery($databaseName, $sql, ['username' => $username]); |
134
|
|
|
return $resultQuery->fetchOne(); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Get edit count within given timeframe and namespace. |
139
|
|
|
* @param Project $project |
140
|
|
|
* @param User $user |
141
|
|
|
* @param int|string $namespace Namespace ID or 'all' for all namespaces |
142
|
|
|
* @param int|false $start Start date as Unix timestamp. |
143
|
|
|
* @param int|false $end End date as Unix timestamp. |
144
|
|
|
* @return int |
145
|
|
|
*/ |
146
|
|
|
public function countEdits(Project $project, User $user, $namespace = 'all', $start = false, $end = false): int |
147
|
|
|
{ |
148
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_editcount'); |
149
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
150
|
|
|
return (int)$this->cache->getItem($cacheKey)->get(); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
$revDateConditions = $this->getDateConditions($start, $end); |
154
|
|
|
[$pageJoin, $condNamespace] = $this->getPageAndNamespaceSql($project, $namespace); |
155
|
|
|
$revisionTable = $project->getTableName('revision'); |
156
|
|
|
$params = []; |
157
|
|
|
|
158
|
|
|
if ($user->isAnon()) { |
159
|
|
|
[$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername()); |
160
|
|
|
$ipcTable = $project->getTableName('ip_changes'); |
161
|
|
|
$sql = "SELECT COUNT(ipc_rev_id) |
162
|
|
|
FROM $ipcTable |
163
|
|
|
JOIN $revisionTable ON ipc_rev_id = rev_id |
164
|
|
|
$pageJoin |
165
|
|
|
WHERE ipc_hex BETWEEN :startIp AND :endIp |
166
|
|
|
$condNamespace |
167
|
|
|
$revDateConditions"; |
168
|
|
|
} else { |
169
|
|
|
$sql = "SELECT COUNT(rev_id) |
170
|
|
|
FROM $revisionTable |
171
|
|
|
$pageJoin |
172
|
|
|
WHERE rev_actor = :actorId |
173
|
|
|
$condNamespace |
174
|
|
|
$revDateConditions"; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
$resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params); |
178
|
|
|
$result = (int)$resultQuery->fetchOne(); |
179
|
|
|
|
180
|
|
|
// Cache and return. |
181
|
|
|
return $this->setCache($cacheKey, $result); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Get information about the currently-logged in user. |
186
|
|
|
* @return array|object|null null if not logged in. |
187
|
|
|
*/ |
188
|
|
|
public function getXtoolsUserInfo() |
189
|
|
|
{ |
190
|
|
|
/** @var Session $session */ |
191
|
|
|
$session = $this->container->get('session'); |
192
|
|
|
return $session->get('logged_in_user'); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Maximum number of edits to process, based on configuration. |
197
|
|
|
* @return int |
198
|
|
|
*/ |
199
|
|
|
public function maxEdits(): int |
200
|
|
|
{ |
201
|
|
|
return (int)$this->container->getParameter('app.max_user_edits'); |
|
|
|
|
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Get SQL clauses for joining on `page` and restricting to a namespace. |
206
|
|
|
* @param Project $project |
207
|
|
|
* @param int|string $namespace Namespace ID or 'all' for all namespaces. |
208
|
|
|
* @return array [page join clause, page namespace clause] |
209
|
|
|
*/ |
210
|
|
|
protected function getPageAndNamespaceSql(Project $project, $namespace): array |
211
|
|
|
{ |
212
|
|
|
if ('all' === $namespace) { |
213
|
|
|
return [null, null]; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
$pageTable = $project->getTableName('page'); |
217
|
|
|
$pageJoin = "LEFT JOIN $pageTable ON rev_page = page_id"; |
218
|
|
|
$condNamespace = 'AND page_namespace = :namespace'; |
219
|
|
|
|
220
|
|
|
return [$pageJoin, $condNamespace]; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Get SQL fragments for filtering by user. |
225
|
|
|
* Used in self::getPagesCreatedInnerSql(). |
226
|
|
|
* @param bool $dateFiltering Whether the query you're working with has date filtering. |
227
|
|
|
* If false, a clause to check timestamp > 1 is added to force use of the timestamp index. |
228
|
|
|
* @return string[] Keys 'whereRev' and 'whereArc'. |
229
|
|
|
*/ |
230
|
|
|
public function getUserConditions(bool $dateFiltering = false): array |
231
|
|
|
{ |
232
|
|
|
return [ |
233
|
|
|
'whereRev' => " rev_actor = :actorId ".($dateFiltering ? '' : "AND rev_timestamp > 1 "), |
234
|
|
|
'whereArc' => " ar_actor = :actorId ".($dateFiltering ? '' : "AND ar_timestamp > 1 "), |
235
|
|
|
]; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Prepare the given SQL, bind the given parameters, and execute the Doctrine Statement. |
240
|
|
|
* @param string $sql |
241
|
|
|
* @param Project $project |
242
|
|
|
* @param User $user |
243
|
|
|
* @param int|string|null $namespace Namespace ID, or 'all'/null for all namespaces. |
244
|
|
|
* @param array $extraParams Will get merged in the params array used for binding values. |
245
|
|
|
* @return ResultStatement |
246
|
|
|
*/ |
247
|
|
|
protected function executeQuery( |
248
|
|
|
string $sql, |
249
|
|
|
Project $project, |
250
|
|
|
User $user, |
251
|
|
|
$namespace = 'all', |
252
|
|
|
array $extraParams = [] |
253
|
|
|
): ResultStatement { |
254
|
|
|
$params = ['actorId' => $user->getActorId($project)]; |
255
|
|
|
|
256
|
|
|
if ('all' !== $namespace) { |
257
|
|
|
$params['namespace'] = $namespace; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
return $this->executeProjectsQuery($project, $sql, array_merge($params, $extraParams)); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Get a user's local user rights on the given Project. |
265
|
|
|
* @param Project $project |
266
|
|
|
* @param User $user |
267
|
|
|
* @return string[] |
268
|
|
|
*/ |
269
|
|
|
public function getUserRights(Project $project, User $user): array |
270
|
|
|
{ |
271
|
|
|
if ($user->isAnon()) { |
272
|
|
|
return []; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_rights'); |
276
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
277
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
$userGroupsTable = $project->getTableName('user_groups'); |
281
|
|
|
$userTable = $project->getTableName('user'); |
282
|
|
|
|
283
|
|
|
$sql = "SELECT ug_group |
284
|
|
|
FROM $userGroupsTable |
285
|
|
|
JOIN $userTable ON user_id = ug_user |
286
|
|
|
WHERE user_name = :username |
287
|
|
|
AND (ug_expiry IS NULL OR ug_expiry > CURRENT_TIMESTAMP)"; |
288
|
|
|
|
289
|
|
|
$ret = $this->executeProjectsQuery($project, $sql, [ |
290
|
|
|
'username' => $user->getUsername(), |
291
|
|
|
])->fetchFirstColumn(); |
292
|
|
|
|
293
|
|
|
// Cache and return. |
294
|
|
|
return $this->setCache($cacheKey, $ret); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Get a user's global group membership (starting at XTools' default project if none is |
299
|
|
|
* provided). This requires the CentralAuth extension to be installed. |
300
|
|
|
* @link https://www.mediawiki.org/wiki/Extension:CentralAuth |
301
|
|
|
* @param string $username The username. |
302
|
|
|
* @param Project|null $project The project to query. |
303
|
|
|
* @return string[] |
304
|
|
|
*/ |
305
|
|
|
public function getGlobalUserRights(string $username, ?Project $project = null): array |
306
|
|
|
{ |
307
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_global_groups'); |
308
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
309
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
// Get the default project if not provided. |
313
|
|
|
if (!$project instanceof Project) { |
314
|
|
|
$project = $this->projectRepo->getDefaultProject(); |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
$params = [ |
318
|
|
|
'meta' => 'globaluserinfo', |
319
|
|
|
'guiuser' => $username, |
320
|
|
|
'guiprop' => 'groups', |
321
|
|
|
]; |
322
|
|
|
|
323
|
|
|
$res = $this->executeApiRequest($project, $params); |
324
|
|
|
$result = []; |
325
|
|
|
if (isset($res['batchcomplete']) && isset($res['query']['globaluserinfo']['groups'])) { |
326
|
|
|
$result = $res['query']['globaluserinfo']['groups']; |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
// Cache and return. |
330
|
|
|
return $this->setCache($cacheKey, $result); |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.