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