|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* This file contains only the UserRepository class. |
|
4
|
|
|
*/ |
|
5
|
|
|
|
|
6
|
|
|
namespace Xtools; |
|
7
|
|
|
|
|
8
|
|
|
use Exception; |
|
9
|
|
|
use DateInterval; |
|
10
|
|
|
use Mediawiki\Api\SimpleRequest; |
|
11
|
|
|
use Symfony\Component\DependencyInjection\Container; |
|
12
|
|
|
use Symfony\Component\HttpFoundation\Session\Session; |
|
13
|
|
|
|
|
14
|
|
|
/** |
|
15
|
|
|
* This class provides data for the User class. |
|
16
|
|
|
* @codeCoverageIgnore |
|
17
|
|
|
*/ |
|
18
|
|
|
class UserRepository extends Repository |
|
19
|
|
|
{ |
|
20
|
|
|
|
|
21
|
|
|
/** |
|
22
|
|
|
* Convenience method to get a new User object. |
|
23
|
|
|
* @param string $username The username. |
|
24
|
|
|
* @param Container $container The DI container. |
|
25
|
|
|
* @return User |
|
26
|
|
|
*/ |
|
27
|
|
|
public static function getUser($username, Container $container) |
|
28
|
|
|
{ |
|
29
|
|
|
$user = new User($username); |
|
30
|
|
|
$userRepo = new UserRepository(); |
|
31
|
|
|
$userRepo->setContainer($container); |
|
32
|
|
|
$user->setRepository($userRepo); |
|
33
|
|
|
return $user; |
|
34
|
|
|
} |
|
35
|
|
|
|
|
36
|
|
|
/** |
|
37
|
|
|
* Get the user's ID. |
|
38
|
|
|
* @param string $databaseName The database to query. |
|
39
|
|
|
* @param string $username The username to find. |
|
40
|
|
|
* @return int |
|
41
|
|
|
*/ |
|
42
|
|
View Code Duplication |
public function getId($databaseName, $username) |
|
|
|
|
|
|
43
|
|
|
{ |
|
44
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_id'); |
|
45
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
|
46
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
|
47
|
|
|
} |
|
48
|
|
|
|
|
49
|
|
|
$userTable = $this->getTableName($databaseName, 'user'); |
|
50
|
|
|
$sql = "SELECT user_id FROM $userTable WHERE user_name = :username LIMIT 1"; |
|
51
|
|
|
$resultQuery = $this->getProjectsConnection()->prepare($sql); |
|
52
|
|
|
$resultQuery->bindParam("username", $username); |
|
53
|
|
|
$resultQuery->execute(); |
|
54
|
|
|
$userId = (int)$resultQuery->fetchColumn(); |
|
55
|
|
|
|
|
56
|
|
|
// Cache for 10 minutes. |
|
57
|
|
|
$cacheItem = $this->cache |
|
58
|
|
|
->getItem($cacheKey) |
|
59
|
|
|
->set($userId) |
|
60
|
|
|
->expiresAfter(new DateInterval('PT10M')); |
|
61
|
|
|
$this->cache->save($cacheItem); |
|
62
|
|
|
return $userId; |
|
63
|
|
|
} |
|
64
|
|
|
|
|
65
|
|
|
/** |
|
66
|
|
|
* Get the user's registration date. |
|
67
|
|
|
* @param string $databaseName The database to query. |
|
68
|
|
|
* @param string $username The username to find. |
|
69
|
|
|
* @return string|null As returned by the database. |
|
70
|
|
|
*/ |
|
71
|
|
View Code Duplication |
public function getRegistrationDate($databaseName, $username) |
|
|
|
|
|
|
72
|
|
|
{ |
|
73
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_registration'); |
|
74
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
|
75
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
$userTable = $this->getTableName($databaseName, 'user'); |
|
79
|
|
|
$sql = "SELECT user_registration FROM $userTable WHERE user_name = :username LIMIT 1"; |
|
80
|
|
|
$resultQuery = $this->getProjectsConnection()->prepare($sql); |
|
81
|
|
|
$resultQuery->bindParam('username', $username); |
|
82
|
|
|
$resultQuery->execute(); |
|
83
|
|
|
$registrationDate = $resultQuery->fetchColumn(); |
|
|
|
|
|
|
84
|
|
|
|
|
85
|
|
|
// Cache for 10 minutes. |
|
86
|
|
|
$cacheItem = $this->cache |
|
87
|
|
|
->getItem($cacheKey) |
|
88
|
|
|
->set($registrationDate) |
|
89
|
|
|
->expiresAfter(new DateInterval('PT10M')); |
|
90
|
|
|
$this->cache->save($cacheItem); |
|
91
|
|
|
return $registrationDate; |
|
92
|
|
|
} |
|
93
|
|
|
|
|
94
|
|
|
/** |
|
95
|
|
|
* Get the user's (system) edit count. |
|
96
|
|
|
* @param string $databaseName The database to query. |
|
97
|
|
|
* @param string $username The username to find. |
|
98
|
|
|
* @return int|null As returned by the database. |
|
99
|
|
|
*/ |
|
100
|
|
View Code Duplication |
public function getEditCount($databaseName, $username) |
|
|
|
|
|
|
101
|
|
|
{ |
|
102
|
|
|
$userTable = $this->getTableName($databaseName, 'user'); |
|
103
|
|
|
$sql = "SELECT user_editcount FROM $userTable WHERE user_name = :username LIMIT 1"; |
|
104
|
|
|
$resultQuery = $this->getProjectsConnection()->prepare($sql); |
|
105
|
|
|
$resultQuery->bindParam('username', $username); |
|
106
|
|
|
$resultQuery->execute(); |
|
107
|
|
|
return $resultQuery->fetchColumn(); |
|
108
|
|
|
} |
|
109
|
|
|
|
|
110
|
|
|
/** |
|
111
|
|
|
* Get group names of the given user. |
|
112
|
|
|
* @param Project $project The project. |
|
113
|
|
|
* @param string $username The username. |
|
114
|
|
|
* @return string[] |
|
115
|
|
|
*/ |
|
116
|
|
|
public function getGroups(Project $project, $username) |
|
117
|
|
|
{ |
|
118
|
|
|
// Use md5 to ensure the key does not contain reserved characters. |
|
119
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_groups'); |
|
120
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
|
121
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
$this->stopwatch->start($cacheKey, 'XTools'); |
|
125
|
|
|
$api = $this->getMediawikiApi($project); |
|
126
|
|
|
$params = [ "list"=>"users", "ususers"=>$username, "usprop"=>"groups" ]; |
|
127
|
|
|
$query = new SimpleRequest('query', $params); |
|
128
|
|
|
$result = []; |
|
129
|
|
|
$res = $api->getRequest($query); |
|
130
|
|
|
if (isset($res["batchcomplete"]) && isset($res["query"]["users"][0]["groups"])) { |
|
131
|
|
|
$result = $res["query"]["users"][0]["groups"]; |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
// Cache for 10 minutes, and return. |
|
135
|
|
|
$cacheItem = $this->cache->getItem($cacheKey) |
|
136
|
|
|
->set($result) |
|
137
|
|
|
->expiresAfter(new DateInterval('PT10M')); |
|
138
|
|
|
$this->cache->save($cacheItem); |
|
139
|
|
|
$this->stopwatch->stop($cacheKey); |
|
140
|
|
|
|
|
141
|
|
|
return $result; |
|
142
|
|
|
} |
|
143
|
|
|
|
|
144
|
|
|
/** |
|
145
|
|
|
* Get a user's global group membership (starting at XTools' default project if none is |
|
146
|
|
|
* provided). This requires the CentralAuth extension to be installed. |
|
147
|
|
|
* @link https://www.mediawiki.org/wiki/Extension:CentralAuth |
|
148
|
|
|
* @param string $username The username. |
|
149
|
|
|
* @param Project $project The project to query. |
|
150
|
|
|
* @return string[] |
|
151
|
|
|
*/ |
|
152
|
|
|
public function getGlobalGroups($username, Project $project = null) |
|
153
|
|
|
{ |
|
154
|
|
|
// Get the default project if not provided. |
|
155
|
|
|
if (!$project instanceof Project) { |
|
156
|
|
|
$project = ProjectRepository::getDefaultProject($this->container); |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
// Create the API query. |
|
160
|
|
|
$api = $this->getMediawikiApi($project); |
|
161
|
|
|
$params = [ |
|
162
|
|
|
'meta' => 'globaluserinfo', |
|
163
|
|
|
'guiuser' => $username, |
|
164
|
|
|
'guiprop' => 'groups' |
|
165
|
|
|
]; |
|
166
|
|
|
$query = new SimpleRequest('query', $params); |
|
167
|
|
|
|
|
168
|
|
|
// Get the result. |
|
169
|
|
|
$res = $api->getRequest($query); |
|
170
|
|
|
$result = []; |
|
171
|
|
|
if (isset($res['batchcomplete']) && isset($res['query']['globaluserinfo']['groups'])) { |
|
172
|
|
|
$result = $res['query']['globaluserinfo']['groups']; |
|
173
|
|
|
} |
|
174
|
|
|
return $result; |
|
175
|
|
|
} |
|
176
|
|
|
|
|
177
|
|
|
/** |
|
178
|
|
|
* Search the ipblocks table to see if the user is currently blocked |
|
179
|
|
|
* and return the expiry if they are. |
|
180
|
|
|
* @param $databaseName The database to query. |
|
181
|
|
|
* @param $userid The ID of the user to search for. |
|
182
|
|
|
* @return bool|string Expiry of active block or false |
|
183
|
|
|
*/ |
|
184
|
|
View Code Duplication |
public function getBlockExpiry($databaseName, $userid) |
|
|
|
|
|
|
185
|
|
|
{ |
|
186
|
|
|
$ipblocksTable = $this->getTableName($databaseName, 'ipblocks'); |
|
187
|
|
|
$sql = "SELECT ipb_expiry |
|
188
|
|
|
FROM $ipblocksTable |
|
189
|
|
|
WHERE ipb_user = :userid |
|
190
|
|
|
LIMIT 1"; |
|
191
|
|
|
$resultQuery = $this->getProjectsConnection()->prepare($sql); |
|
192
|
|
|
$resultQuery->bindParam('userid', $userid); |
|
193
|
|
|
$resultQuery->execute(); |
|
194
|
|
|
return $resultQuery->fetchColumn(); |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
/** |
|
198
|
|
|
* Get pages created by a user |
|
199
|
|
|
* @param Project $project |
|
200
|
|
|
* @param User $user |
|
201
|
|
|
* @param string|int $namespace Namespace ID or 'all' |
|
202
|
|
|
* @param string $redirects One of 'noredirects', 'onlyredirects' or blank for both |
|
203
|
|
|
* @return string[] Result of query, see below. Includes live and deleted pages. |
|
204
|
|
|
*/ |
|
205
|
|
|
public function getPagesCreated(Project $project, User $user, $namespace, $redirects) |
|
206
|
|
|
{ |
|
207
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_pages_created'); |
|
208
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
|
209
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
|
210
|
|
|
} |
|
211
|
|
|
$this->stopwatch->start($cacheKey, 'XTools'); |
|
212
|
|
|
|
|
213
|
|
|
$pageTable = $project->getTableName('page'); |
|
214
|
|
|
$pageAssessmentsTable = $project->getTableName('page_assessments'); |
|
215
|
|
|
$revisionTable = $project->getTableName('revision'); |
|
216
|
|
|
$archiveTable = $project->getTableName('archive'); |
|
217
|
|
|
$logTable = $project->getTableName('logging', 'logindex'); |
|
218
|
|
|
|
|
219
|
|
|
$username = $user->getUsername(); |
|
220
|
|
|
$userId = $user->getId($project); |
|
221
|
|
|
|
|
222
|
|
|
$namespaceConditionArc = ''; |
|
223
|
|
|
$namespaceConditionRev = ''; |
|
224
|
|
|
|
|
225
|
|
|
if ($namespace != 'all') { |
|
226
|
|
|
$namespaceConditionRev = " AND page_namespace = '".intval($namespace)."' "; |
|
227
|
|
|
$namespaceConditionArc = " AND ar_namespace = '".intval($namespace)."' "; |
|
228
|
|
|
} |
|
229
|
|
|
|
|
230
|
|
|
$redirectCondition = ''; |
|
231
|
|
|
|
|
232
|
|
|
if ($redirects == 'onlyredirects') { |
|
233
|
|
|
$redirectCondition = " AND page_is_redirect = '1' "; |
|
234
|
|
|
} elseif ($redirects == 'noredirects') { |
|
235
|
|
|
$redirectCondition = " AND page_is_redirect = '0' "; |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
if ($userId == 0) { // IP Editor or undefined username. |
|
239
|
|
|
$whereRev = " rev_user_text = '$username' AND rev_user = '0' "; |
|
240
|
|
|
$whereArc = " ar_user_text = '$username' AND ar_user = '0' "; |
|
241
|
|
|
} else { |
|
242
|
|
|
$whereRev = " rev_user = '$userId' AND rev_timestamp > 1 "; |
|
243
|
|
|
$whereArc = " ar_user = '$userId' AND ar_timestamp > 1 "; |
|
244
|
|
|
} |
|
245
|
|
|
|
|
246
|
|
|
$hasPageAssessments = $this->isLabs() && $project->hasPageAssessments(); |
|
247
|
|
|
$paSelects = $hasPageAssessments ? ', pa_class, pa_importance, pa_page_revision' : ''; |
|
248
|
|
|
$paSelectsArchive = $hasPageAssessments ? |
|
249
|
|
|
', NULL AS pa_class, NULL AS pa_page_id, NULL AS pa_page_revision' |
|
250
|
|
|
: ''; |
|
251
|
|
|
$paJoin = $hasPageAssessments ? "LEFT JOIN $pageAssessmentsTable ON rev_page = pa_page_id" : ''; |
|
252
|
|
|
|
|
253
|
|
|
$sql = " |
|
254
|
|
|
( |
|
255
|
|
|
SELECT DISTINCT page_namespace AS namespace, 'rev' AS type, page_title AS page_title, |
|
256
|
|
|
page_len, page_is_redirect, rev_timestamp AS rev_timestamp, |
|
257
|
|
|
rev_user, rev_user_text AS username, rev_len, rev_id $paSelects |
|
258
|
|
|
FROM $pageTable |
|
259
|
|
|
JOIN $revisionTable ON page_id = rev_page |
|
260
|
|
|
$paJoin |
|
261
|
|
|
WHERE $whereRev AND rev_parent_id = '0' $namespaceConditionRev $redirectCondition" . |
|
262
|
|
|
($hasPageAssessments ? 'GROUP BY rev_page' : '') . " |
|
263
|
|
|
) |
|
264
|
|
|
|
|
265
|
|
|
UNION |
|
266
|
|
|
|
|
267
|
|
|
( |
|
268
|
|
|
SELECT ar_namespace AS namespace, 'arc' AS type, ar_title AS page_title, |
|
269
|
|
|
0 AS page_len, '0' AS page_is_redirect, MIN(ar_timestamp) AS rev_timestamp, |
|
270
|
|
|
ar_user AS rev_user, ar_user_text AS username, ar_len AS rev_len, |
|
271
|
|
|
ar_rev_id AS rev_id $paSelectsArchive |
|
272
|
|
|
FROM $archiveTable |
|
273
|
|
|
LEFT JOIN $logTable ON log_namespace = ar_namespace AND log_title = ar_title |
|
274
|
|
|
AND log_user = ar_user AND (log_action = 'move' OR log_action = 'move_redir') |
|
275
|
|
|
AND log_type = 'move' |
|
276
|
|
|
WHERE $whereArc AND ar_parent_id = '0' $namespaceConditionArc AND log_action IS NULL |
|
277
|
|
|
GROUP BY ar_namespace, ar_title |
|
278
|
|
|
)"; |
|
279
|
|
|
|
|
280
|
|
|
$resultQuery = $this->getProjectsConnection()->prepare($sql); |
|
281
|
|
|
$resultQuery->execute(); |
|
282
|
|
|
$result = $resultQuery->fetchAll(); |
|
283
|
|
|
|
|
284
|
|
|
// Cache for 10 minutes, and return. |
|
285
|
|
|
$cacheItem = $this->cache->getItem($cacheKey) |
|
286
|
|
|
->set($result) |
|
287
|
|
|
->expiresAfter(new DateInterval('PT10M')); |
|
288
|
|
|
$this->cache->save($cacheItem); |
|
289
|
|
|
$this->stopwatch->stop($cacheKey); |
|
290
|
|
|
|
|
291
|
|
|
return $result; |
|
292
|
|
|
} |
|
293
|
|
|
|
|
294
|
|
|
/** |
|
295
|
|
|
* Get edit count within given timeframe and namespace. |
|
296
|
|
|
* @param Project $project |
|
297
|
|
|
* @param User $user |
|
298
|
|
|
* @param int|string $namespace Namespace ID or 'all' for all namespaces |
|
299
|
|
|
* @param string $start Start date in a format accepted by strtotime() |
|
300
|
|
|
* @param string $end End date in a format accepted by strtotime() |
|
301
|
|
|
*/ |
|
302
|
|
|
public function countEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '') |
|
303
|
|
|
{ |
|
304
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_editcount'); |
|
305
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
|
306
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
|
307
|
|
|
} |
|
308
|
|
|
|
|
309
|
|
|
$condBegin = ''; |
|
310
|
|
|
$condEnd = ''; |
|
311
|
|
|
|
|
312
|
|
|
if (!empty($start)) { |
|
313
|
|
|
// For the query |
|
314
|
|
|
$start = date('Ymd000000', strtotime($start)); |
|
315
|
|
|
$condBegin = 'AND rev_timestamp >= :start '; |
|
316
|
|
|
} |
|
317
|
|
|
if (!empty($end)) { |
|
318
|
|
|
// For the query |
|
319
|
|
|
$end = date('Ymd235959', strtotime($end)); |
|
320
|
|
|
$condEnd = 'AND rev_timestamp <= :end '; |
|
321
|
|
|
} |
|
322
|
|
|
|
|
323
|
|
|
$pageTable = $project->getTableName('page'); |
|
324
|
|
|
$revisionTable = $project->getTableName('revision'); |
|
325
|
|
|
$condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace'; |
|
326
|
|
|
$pageJoin = $namespace === 'all' ? '' : "JOIN $pageTable ON rev_page = page_id"; |
|
327
|
|
|
|
|
328
|
|
|
$sql = "SELECT COUNT(rev_id) |
|
329
|
|
|
FROM $revisionTable |
|
330
|
|
|
$pageJoin |
|
331
|
|
|
WHERE rev_user_text = :username |
|
332
|
|
|
$condNamespace |
|
333
|
|
|
$condBegin |
|
334
|
|
|
$condEnd"; |
|
335
|
|
|
|
|
336
|
|
|
$username = $user->getUsername(); |
|
337
|
|
|
$conn = $this->getProjectsConnection(); |
|
338
|
|
|
$resultQuery = $conn->prepare($sql); |
|
339
|
|
|
$resultQuery->bindParam('username', $username); |
|
340
|
|
|
if (!empty($start)) { |
|
341
|
|
|
$resultQuery->bindParam('start', $start); |
|
342
|
|
|
} |
|
343
|
|
|
if (!empty($end)) { |
|
344
|
|
|
$resultQuery->bindParam('end', $end); |
|
345
|
|
|
} |
|
346
|
|
|
if ($namespace !== 'all') { |
|
347
|
|
|
$resultQuery->bindParam('namespace', $namespace); |
|
348
|
|
|
} |
|
349
|
|
|
$resultQuery->execute(); |
|
350
|
|
|
$result = $resultQuery->fetchColumn(); |
|
351
|
|
|
|
|
352
|
|
|
// Cache for 10 minutes, and return. |
|
353
|
|
|
$cacheItem = $this->cache->getItem($cacheKey) |
|
354
|
|
|
->set($result) |
|
355
|
|
|
->expiresAfter(new DateInterval('PT10M')); |
|
356
|
|
|
$this->cache->save($cacheItem); |
|
357
|
|
|
|
|
358
|
|
|
return $result; |
|
359
|
|
|
} |
|
360
|
|
|
|
|
361
|
|
|
/** |
|
362
|
|
|
* Get the number of edits this user made using semi-automated tools. |
|
363
|
|
|
* @param Project $project |
|
364
|
|
|
* @param User $user |
|
365
|
|
|
* @param string|int $namespace Namespace ID or 'all' |
|
366
|
|
|
* @param string $start Start date in a format accepted by strtotime() |
|
367
|
|
|
* @param string $end End date in a format accepted by strtotime() |
|
368
|
|
|
* @return int Result of query, see below. |
|
369
|
|
|
*/ |
|
370
|
|
|
public function countAutomatedEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '') |
|
371
|
|
|
{ |
|
372
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_autoeditcount'); |
|
373
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
|
374
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
|
375
|
|
|
} |
|
376
|
|
|
|
|
377
|
|
|
$condBegin = ''; |
|
378
|
|
|
$condEnd = ''; |
|
379
|
|
|
|
|
380
|
|
|
if (!empty($start)) { |
|
381
|
|
|
$cacheKey .= '.' . $start; |
|
382
|
|
|
|
|
383
|
|
|
// For the query |
|
384
|
|
|
$start = date('Ymd000000', strtotime($start)); |
|
385
|
|
|
$condBegin = 'AND rev_timestamp >= :start '; |
|
386
|
|
|
} |
|
387
|
|
|
if (!empty($end)) { |
|
388
|
|
|
$cacheKey .= '.' . $end; |
|
389
|
|
|
|
|
390
|
|
|
// For the query |
|
391
|
|
|
$end = date('Ymd235959', strtotime($end)); |
|
392
|
|
|
$condEnd = 'AND rev_timestamp <= :end '; |
|
393
|
|
|
} |
|
394
|
|
|
|
|
395
|
|
|
$this->stopwatch->start($cacheKey, 'XTools'); |
|
396
|
|
|
|
|
397
|
|
|
// Get the combined regex and tags for the tools |
|
398
|
|
|
$conn = $this->getProjectsConnection(); |
|
399
|
|
|
list($regex, $tags) = $this->getToolRegexAndTags($project->getDomain(), $conn); |
|
|
|
|
|
|
400
|
|
|
|
|
401
|
|
|
$pageTable = $project->getTableName('page'); |
|
402
|
|
|
$revisionTable = $project->getTableName('revision'); |
|
403
|
|
|
$tagTable = $project->getTableName('change_tag'); |
|
404
|
|
|
$condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace'; |
|
405
|
|
|
$pageJoin = $namespace === 'all' ? '' : "JOIN $pageTable ON page_id = rev_page"; |
|
406
|
|
|
$tagJoin = ''; |
|
407
|
|
|
|
|
408
|
|
|
// Build SQL for detecting autoedits via regex and/or tags |
|
409
|
|
|
$condTools = []; |
|
410
|
|
|
if ($regex != '') { |
|
411
|
|
|
$condTools[] = "rev_comment REGEXP $regex"; |
|
412
|
|
|
} |
|
413
|
|
|
if ($tags != '') { |
|
414
|
|
|
$tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id" : ''; |
|
415
|
|
|
$condTools[] = "ct_tag IN ($tags)"; |
|
416
|
|
|
} |
|
417
|
|
|
$condTool = 'AND (' . implode(' OR ', $condTools) . ')'; |
|
418
|
|
|
|
|
419
|
|
|
$sql = "SELECT COUNT(DISTINCT(rev_id)) |
|
420
|
|
|
FROM $revisionTable |
|
421
|
|
|
$pageJoin |
|
422
|
|
|
$tagJoin |
|
423
|
|
|
WHERE rev_user_text = :username |
|
424
|
|
|
$condTool |
|
425
|
|
|
$condNamespace |
|
426
|
|
|
$condBegin |
|
427
|
|
|
$condEnd"; |
|
428
|
|
|
|
|
429
|
|
|
$username = $user->getUsername(); |
|
430
|
|
|
$resultQuery = $conn->prepare($sql); |
|
431
|
|
|
$resultQuery->bindParam('username', $username); |
|
432
|
|
|
if (!empty($start)) { |
|
433
|
|
|
$resultQuery->bindParam('start', $start); |
|
434
|
|
|
} |
|
435
|
|
|
if (!empty($end)) { |
|
436
|
|
|
$resultQuery->bindParam('end', $end); |
|
437
|
|
|
} |
|
438
|
|
|
if ($namespace !== 'all') { |
|
439
|
|
|
$resultQuery->bindParam('namespace', $namespace); |
|
440
|
|
|
} |
|
441
|
|
|
$resultQuery->execute(); |
|
442
|
|
|
$result = (int) $resultQuery->fetchColumn(); |
|
443
|
|
|
|
|
444
|
|
|
// Cache for 10 minutes, and return. |
|
445
|
|
|
$cacheItem = $this->cache->getItem($cacheKey) |
|
446
|
|
|
->set($result) |
|
447
|
|
|
->expiresAfter(new DateInterval('PT10M')); |
|
448
|
|
|
$this->cache->save($cacheItem); |
|
449
|
|
|
$this->stopwatch->stop($cacheKey); |
|
450
|
|
|
|
|
451
|
|
|
return $result; |
|
452
|
|
|
} |
|
453
|
|
|
|
|
454
|
|
|
/** |
|
455
|
|
|
* Get non-automated contributions for the given user. |
|
456
|
|
|
* @param Project $project |
|
457
|
|
|
* @param User $user |
|
458
|
|
|
* @param string|int $namespace Namespace ID or 'all' |
|
459
|
|
|
* @param string $start Start date in a format accepted by strtotime() |
|
460
|
|
|
* @param string $end End date in a format accepted by strtotime() |
|
461
|
|
|
* @param int $offset Used for pagination, offset results by N edits |
|
462
|
|
|
* @return string[] Result of query, with columns 'page_title', |
|
463
|
|
|
* 'page_namespace', 'rev_id', 'timestamp', 'minor', |
|
464
|
|
|
* 'length', 'length_change', 'comment' |
|
465
|
|
|
*/ |
|
466
|
|
|
public function getNonAutomatedEdits( |
|
467
|
|
|
Project $project, |
|
468
|
|
|
User $user, |
|
469
|
|
|
$namespace = 'all', |
|
470
|
|
|
$start = '', |
|
471
|
|
|
$end = '', |
|
472
|
|
|
$offset = 0 |
|
473
|
|
|
) { |
|
474
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_nonautoedits'); |
|
475
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
|
476
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
|
477
|
|
|
} |
|
478
|
|
|
$this->stopwatch->start($cacheKey, 'XTools'); |
|
479
|
|
|
|
|
480
|
|
|
$condBegin = ''; |
|
481
|
|
|
$condEnd = ''; |
|
482
|
|
|
|
|
483
|
|
|
if (!empty($start)) { |
|
484
|
|
|
// For the query |
|
485
|
|
|
$start = date('Ymd000000', strtotime($start)); |
|
486
|
|
|
$condBegin = 'AND revs.rev_timestamp >= :start '; |
|
487
|
|
|
} |
|
488
|
|
|
if (!empty($end)) { |
|
489
|
|
|
// For the query |
|
490
|
|
|
$end = date('Ymd235959', strtotime($end)); |
|
491
|
|
|
$condEnd = 'AND revs.rev_timestamp <= :end '; |
|
492
|
|
|
} |
|
493
|
|
|
|
|
494
|
|
|
// Get the combined regex and tags for the tools |
|
495
|
|
|
$conn = $this->getProjectsConnection(); |
|
496
|
|
|
list($regex, $tags) = $this->getToolRegexAndTags($project->getDomain(), $conn); |
|
|
|
|
|
|
497
|
|
|
|
|
498
|
|
|
$pageTable = $project->getTableName('page'); |
|
499
|
|
|
$revisionTable = $project->getTableName('revision'); |
|
500
|
|
|
$tagTable = $project->getTableName('change_tag'); |
|
501
|
|
|
$condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace'; |
|
502
|
|
|
$tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON (ct_rev_id = revs.rev_id)" : ''; |
|
503
|
|
|
$condTag = $tags != '' ? "AND (ct_tag NOT IN ($tags) OR ct_tag IS NULL)" : ''; |
|
504
|
|
|
$sql = "SELECT |
|
505
|
|
|
page_title, |
|
506
|
|
|
page_namespace, |
|
507
|
|
|
revs.rev_id AS rev_id, |
|
508
|
|
|
revs.rev_timestamp AS timestamp, |
|
509
|
|
|
revs.rev_minor_edit AS minor, |
|
510
|
|
|
revs.rev_len AS length, |
|
511
|
|
|
(CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change, |
|
512
|
|
|
revs.rev_comment AS comment |
|
513
|
|
|
FROM $pageTable |
|
514
|
|
|
JOIN $revisionTable AS revs ON (page_id = revs.rev_page) |
|
515
|
|
|
LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id) |
|
516
|
|
|
$tagJoin |
|
517
|
|
|
WHERE revs.rev_user_text = :username |
|
518
|
|
|
AND revs.rev_timestamp > 0 |
|
519
|
|
|
AND revs.rev_comment NOT RLIKE $regex |
|
520
|
|
|
$condTag |
|
521
|
|
|
$condBegin |
|
522
|
|
|
$condEnd |
|
523
|
|
|
$condNamespace |
|
524
|
|
|
ORDER BY revs.rev_timestamp DESC |
|
525
|
|
|
LIMIT 50 |
|
526
|
|
|
OFFSET $offset"; |
|
527
|
|
|
|
|
528
|
|
|
$username = $user->getUsername(); |
|
529
|
|
|
$resultQuery = $conn->prepare($sql); |
|
530
|
|
|
$resultQuery->bindParam('username', $username); |
|
531
|
|
|
if (!empty($start)) { |
|
532
|
|
|
$resultQuery->bindParam('start', $start); |
|
533
|
|
|
} |
|
534
|
|
|
if (!empty($end)) { |
|
535
|
|
|
$resultQuery->bindParam('end', $end); |
|
536
|
|
|
} |
|
537
|
|
|
if ($namespace !== 'all') { |
|
538
|
|
|
$resultQuery->bindParam('namespace', $namespace); |
|
539
|
|
|
} |
|
540
|
|
|
$resultQuery->execute(); |
|
541
|
|
|
$result = $resultQuery->fetchAll(); |
|
542
|
|
|
|
|
543
|
|
|
// Cache for 10 minutes, and return. |
|
544
|
|
|
$cacheItem = $this->cache->getItem($cacheKey) |
|
545
|
|
|
->set($result) |
|
546
|
|
|
->expiresAfter(new DateInterval('PT10M')); |
|
547
|
|
|
$this->cache->save($cacheItem); |
|
548
|
|
|
$this->stopwatch->stop($cacheKey); |
|
549
|
|
|
|
|
550
|
|
|
return $result; |
|
551
|
|
|
} |
|
552
|
|
|
|
|
553
|
|
|
/** |
|
554
|
|
|
* Get non-automated contributions for the given user. |
|
555
|
|
|
* @param Project $project |
|
556
|
|
|
* @param User $user |
|
557
|
|
|
* @param string|int $namespace Namespace ID or 'all'. |
|
558
|
|
|
* @param string $start Start date in a format accepted by strtotime() |
|
559
|
|
|
* @param string $end End date in a format accepted by strtotime() |
|
560
|
|
|
* @return string[] Each tool that they used along with the count and link: |
|
561
|
|
|
* [ |
|
562
|
|
|
* 'Twinkle' => [ |
|
563
|
|
|
* 'count' => 50, |
|
564
|
|
|
* 'link' => 'Wikipedia:Twinkle', |
|
565
|
|
|
* ], |
|
566
|
|
|
* ] |
|
567
|
|
|
*/ |
|
568
|
|
|
public function getAutomatedCounts( |
|
569
|
|
|
Project $project, |
|
570
|
|
|
User $user, |
|
571
|
|
|
$namespace = 'all', |
|
572
|
|
|
$start = '', |
|
573
|
|
|
$end = '' |
|
574
|
|
|
) { |
|
575
|
|
|
$cacheKey = $this->getCacheKey(func_get_args(), 'user_autotoolcounts'); |
|
576
|
|
|
if ($this->cache->hasItem($cacheKey)) { |
|
577
|
|
|
return $this->cache->getItem($cacheKey)->get(); |
|
578
|
|
|
} |
|
579
|
|
|
$this->stopwatch->start($cacheKey, 'XTools'); |
|
580
|
|
|
|
|
581
|
|
|
$condBegin = ''; |
|
582
|
|
|
$condEnd = ''; |
|
583
|
|
|
|
|
584
|
|
|
if (!empty($start)) { |
|
585
|
|
|
// For the query |
|
586
|
|
|
$start = date('Ymd000000', strtotime($start)); |
|
587
|
|
|
$condBegin = 'AND rev_timestamp >= :start '; |
|
588
|
|
|
} |
|
589
|
|
|
if (!empty($end)) { |
|
590
|
|
|
// For the query |
|
591
|
|
|
$end = date('Ymd235959', strtotime($end)); |
|
592
|
|
|
$condEnd = 'AND rev_timestamp <= :end '; |
|
593
|
|
|
} |
|
594
|
|
|
|
|
595
|
|
|
$conn = $this->getProjectsConnection(); |
|
596
|
|
|
|
|
597
|
|
|
// Load the semi-automated edit types. |
|
598
|
|
|
$automatedEditsHelper = $this->container->get('app.automated_edits_helper'); |
|
599
|
|
|
$tools = $automatedEditsHelper->getTools($project->getDomain()); |
|
600
|
|
|
|
|
601
|
|
|
// Create a collection of queries that we're going to run. |
|
602
|
|
|
$queries = []; |
|
603
|
|
|
|
|
604
|
|
|
$revisionTable = $project->getRepository()->getTableName($project->getDatabaseName(), 'revision'); |
|
605
|
|
|
$pageTable = $project->getRepository()->getTableName($project->getDatabaseName(), 'page'); |
|
606
|
|
|
$tagTable = $project->getRepository()->getTableName($project->getDatabaseName(), 'change_tag'); |
|
607
|
|
|
|
|
608
|
|
|
$pageJoin = $namespace !== 'all' ? "LEFT JOIN $pageTable ON rev_page = page_id" : null; |
|
609
|
|
|
$condNamespace = $namespace !== 'all' ? "AND page_namespace = :namespace" : null; |
|
610
|
|
|
|
|
611
|
|
|
foreach ($tools as $toolname => $values) { |
|
612
|
|
|
$tagJoin = ''; |
|
613
|
|
|
$condTool = ''; |
|
614
|
|
|
$toolname = $conn->quote($toolname, \PDO::PARAM_STR); |
|
615
|
|
|
|
|
616
|
|
|
if (isset($values['regex'])) { |
|
617
|
|
|
$regex = $conn->quote($values['regex'], \PDO::PARAM_STR); |
|
618
|
|
|
$condTool = "rev_comment REGEXP $regex"; |
|
619
|
|
|
} |
|
620
|
|
|
if (isset($values['tag'])) { |
|
621
|
|
|
$tagJoin = "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id"; |
|
622
|
|
|
$tag = $conn->quote($values['tag'], \PDO::PARAM_STR); |
|
623
|
|
|
|
|
624
|
|
|
// Append to regex clause if already present. |
|
625
|
|
|
// Tags are more reliable but may not be present for edits made with |
|
626
|
|
|
// older versions of the tool, before it started adding tags. |
|
627
|
|
|
if ($condTool === '') { |
|
628
|
|
|
$condTool = "ct_tag = $tag"; |
|
629
|
|
|
} else { |
|
630
|
|
|
$condTool = '(' . $condTool . " OR ct_tag = $tag)"; |
|
631
|
|
|
} |
|
632
|
|
|
} |
|
633
|
|
|
|
|
634
|
|
|
// Developer error, no regex or tag provided for this tool. |
|
635
|
|
|
if ($condTool === '') { |
|
636
|
|
|
throw new Exception("No regex or tag found for the tool $toolname. " . |
|
637
|
|
|
"Please verify this entry in semi_automated.yml"); |
|
638
|
|
|
} |
|
639
|
|
|
|
|
640
|
|
|
$queries[] .= " |
|
641
|
|
|
SELECT $toolname AS toolname, COUNT(rev_id) AS count |
|
642
|
|
|
FROM $revisionTable |
|
643
|
|
|
$pageJoin |
|
644
|
|
|
$tagJoin |
|
645
|
|
|
WHERE rev_user_text = :username |
|
646
|
|
|
AND $condTool |
|
647
|
|
|
$condNamespace |
|
648
|
|
|
$condBegin |
|
649
|
|
|
$condEnd"; |
|
650
|
|
|
} |
|
651
|
|
|
|
|
652
|
|
|
// Create a big query and execute. |
|
653
|
|
|
$sql = implode(' UNION ', $queries); |
|
654
|
|
|
|
|
655
|
|
|
$resultQuery = $conn->prepare($sql); |
|
656
|
|
|
|
|
657
|
|
|
$username = $user->getUsername(); // use normalized user name |
|
658
|
|
|
$resultQuery->bindParam('username', $username); |
|
659
|
|
|
if (!empty($start)) { |
|
660
|
|
|
$startParam = date('Ymd000000', strtotime($start)); |
|
661
|
|
|
$resultQuery->bindParam('start', $startParam); |
|
662
|
|
|
} |
|
663
|
|
|
if (!empty($end)) { |
|
664
|
|
|
$endParam = date('Ymd235959', strtotime($end)); |
|
665
|
|
|
$resultQuery->bindParam('end', $endParam); |
|
666
|
|
|
} |
|
667
|
|
|
if ($namespace !== 'all') { |
|
668
|
|
|
$resultQuery->bindParam('namespace', $namespace); |
|
669
|
|
|
} |
|
670
|
|
|
|
|
671
|
|
|
$resultQuery->execute(); |
|
672
|
|
|
|
|
673
|
|
|
// handling results |
|
674
|
|
|
$results = []; |
|
675
|
|
|
|
|
676
|
|
|
while ($row = $resultQuery->fetch()) { |
|
677
|
|
|
// Only track tools that they've used at least once |
|
678
|
|
|
$tool = $row['toolname']; |
|
679
|
|
|
if ($row['count'] > 0) { |
|
680
|
|
|
$results[$tool] = [ |
|
681
|
|
|
'link' => $tools[$tool]['link'], |
|
682
|
|
|
'count' => $row['count'], |
|
683
|
|
|
]; |
|
684
|
|
|
} |
|
685
|
|
|
} |
|
686
|
|
|
|
|
687
|
|
|
// Sort the array by count |
|
688
|
|
|
uasort($results, function ($a, $b) { |
|
689
|
|
|
return $b['count'] - $a['count']; |
|
690
|
|
|
}); |
|
691
|
|
|
|
|
692
|
|
|
// Cache for 10 minutes, and return. |
|
693
|
|
|
$cacheItem = $this->cache->getItem($cacheKey) |
|
694
|
|
|
->set($results) |
|
695
|
|
|
->expiresAfter(new DateInterval('PT10M')); |
|
696
|
|
|
$this->cache->save($cacheItem); |
|
697
|
|
|
$this->stopwatch->stop($cacheKey); |
|
698
|
|
|
|
|
699
|
|
|
return $results; |
|
700
|
|
|
} |
|
701
|
|
|
|
|
702
|
|
|
/** |
|
703
|
|
|
* Get information about the currently-logged in user. |
|
704
|
|
|
* @return array |
|
705
|
|
|
*/ |
|
706
|
|
|
public function getXtoolsUserInfo() |
|
707
|
|
|
{ |
|
708
|
|
|
/** @var Session $session */ |
|
709
|
|
|
$session = $this->container->get('session'); |
|
710
|
|
|
return $session->get('logged_in_user'); |
|
711
|
|
|
} |
|
712
|
|
|
|
|
713
|
|
|
/** |
|
714
|
|
|
* Maximum number of edits to process, based on configuration. |
|
715
|
|
|
* @return int |
|
716
|
|
|
*/ |
|
717
|
|
|
public function maxEdits() |
|
718
|
|
|
{ |
|
719
|
|
|
return $this->container->getParameter('app.max_user_edits'); |
|
720
|
|
|
} |
|
721
|
|
|
|
|
722
|
|
|
/** |
|
723
|
|
|
* Get the combined regex and tags for all semi-automated tools, |
|
724
|
|
|
* ready to be used in a query. |
|
725
|
|
|
* @param string $projectDomain Such as en.wikipedia.org |
|
726
|
|
|
* @param $conn Doctrine\DBAL\Connection Used for proper escaping |
|
727
|
|
|
* @return string[] In the format: |
|
728
|
|
|
* ['combined|regex', 'combined,tags'] |
|
729
|
|
|
*/ |
|
730
|
|
|
private function getToolRegexAndTags($projectDomain, $conn) |
|
731
|
|
|
{ |
|
732
|
|
|
$automatedEditsHelper = $this->container->get('app.automated_edits_helper'); |
|
733
|
|
|
$tools = $automatedEditsHelper->getTools($projectDomain); |
|
734
|
|
|
$regexes = []; |
|
735
|
|
|
$tags = []; |
|
736
|
|
|
foreach ($tools as $tool => $values) { |
|
737
|
|
|
if (isset($values['regex'])) { |
|
738
|
|
|
$regexes[] = $values['regex']; |
|
739
|
|
|
} |
|
740
|
|
|
if (isset($values['tag'])) { |
|
741
|
|
|
$tags[] = $conn->quote($values['tag'], \PDO::PARAM_STR); |
|
742
|
|
|
} |
|
743
|
|
|
} |
|
744
|
|
|
|
|
745
|
|
|
return [ |
|
746
|
|
|
$conn->quote(implode('|', $regexes), \PDO::PARAM_STR), |
|
747
|
|
|
implode(',', $tags), |
|
748
|
|
|
]; |
|
749
|
|
|
} |
|
750
|
|
|
} |
|
751
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.