Passed
Push — master ( 48da0f...5f527e )
by MusikAnimal
04:29
created

AutoEditsRepository::getAutomatedCounts()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 43
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 20
nc 4
nop 5
dl 0
loc 43
rs 8.5806
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the AutoEditsRepository class.
4
 */
5
6
namespace Xtools;
7
8
use DateInterval;
9
use Symfony\Component\Config\Definition\Exception\Exception;
10
11
/**
12
 * AutoEditsRepository is responsible for retrieving data from the database
13
 * about the automated edits made by a user.
14
 * @codeCoverageIgnore
15
 */
16
class AutoEditsRepository extends UserRepository
17
{
18
    /**
19
     * Get the number of edits this user made using semi-automated tools.
20
     * @param Project $project
21
     * @param User $user
22
     * @param string|int $namespace Namespace ID or 'all'
23
     * @param string $start Start date in a format accepted by strtotime()
24
     * @param string $end End date in a format accepted by strtotime()
25
     * @return int Result of query, see below.
26
     */
27
    public function countAutomatedEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '')
28
    {
29
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autoeditcount');
30
        if ($this->cache->hasItem($cacheKey)) {
31
            return $this->cache->getItem($cacheKey)->get();
32
        }
33
        $this->stopwatch->start($cacheKey, 'XTools');
34
35
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
36
37
        // Get the combined regex and tags for the tools
38
        list($regex, $tags) = $this->getToolRegexAndTags($project->getDomain());
39
40
        list($pageJoin, $condNamespace) = $this->getPageAndNamespaceSql($project, $namespace);
41
42
        $revisionTable = $project->getTableName('revision');
43
        $tagTable = $project->getTableName('change_tag');
44
        $tagJoin = '';
45
46
        // Build SQL for detecting autoedits via regex and/or tags
47
        $condTools = [];
48
        if ($regex != '') {
49
            $condTools[] = "rev_comment REGEXP $regex";
50
        }
51
        if ($tags != '') {
52
            $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id" : '';
53
            $condTools[] = "ct_tag IN ($tags)";
54
        }
55
        $condTool = 'AND (' . implode(' OR ', $condTools) . ')';
56
57
        $sql = "SELECT COUNT(DISTINCT(rev_id))
58
                FROM $revisionTable
59
                $pageJoin
60
                $tagJoin
61
                WHERE rev_user_text = :username
62
                $condTool
63
                $condNamespace
64
                $condBegin
65
                $condEnd";
66
67
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
68
        $result = (int) $resultQuery->fetchColumn();
69
70
        // Cache for 10 minutes, and return.
71
        $this->setCache($cacheKey, $result);
72
        $this->stopwatch->stop($cacheKey);
73
74
        return $result;
75
    }
76
77
    /**
78
     * Get non-automated contributions for the given user.
79
     * @param Project $project
80
     * @param User $user
81
     * @param string|int $namespace Namespace ID or 'all'
82
     * @param string $start Start date in a format accepted by strtotime()
83
     * @param string $end End date in a format accepted by strtotime()
84
     * @param int $offset Used for pagination, offset results by N edits
85
     * @return string[] Result of query, with columns 'page_title',
86
     *   'page_namespace', 'rev_id', 'timestamp', 'minor',
87
     *   'length', 'length_change', 'comment'
88
     */
89
    public function getNonAutomatedEdits(
90
        Project $project,
91
        User $user,
92
        $namespace = 'all',
93
        $start = '',
94
        $end = '',
95
        $offset = 0
96
    ) {
97
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_nonautoedits');
98
        if ($this->cache->hasItem($cacheKey)) {
99
            return $this->cache->getItem($cacheKey)->get();
100
        }
101
        $this->stopwatch->start($cacheKey, 'XTools');
102
103
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
104
105
        // Get the combined regex and tags for the tools
106
        list($regex, $tags) = $this->getToolRegexAndTags($project->getDomain());
107
108
        $pageTable = $project->getTableName('page');
109
        $revisionTable = $project->getTableName('revision');
110
        $tagTable = $project->getTableName('change_tag');
111
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
112
        $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON (ct_rev_id = revs.rev_id)" : '';
113
        $condTag = $tags != '' ? "AND (ct_tag NOT IN ($tags) OR ct_tag IS NULL)" : '';
114
        $sql = "SELECT
115
                    page_title,
116
                    page_namespace,
117
                    revs.rev_id AS rev_id,
118
                    revs.rev_timestamp AS timestamp,
119
                    revs.rev_minor_edit AS minor,
120
                    revs.rev_len AS length,
121
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
122
                    revs.rev_comment AS comment
123
                FROM $pageTable
124
                JOIN $revisionTable AS revs ON (page_id = revs.rev_page)
125
                LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
126
                $tagJoin
127
                WHERE revs.rev_user_text = :username
128
                AND revs.rev_timestamp > 0
129
                AND revs.rev_comment NOT RLIKE $regex
130
                $condTag
131
                $condBegin
132
                $condEnd
133
                $condNamespace
134
                ORDER BY revs.rev_timestamp DESC
135
                LIMIT 50
136
                OFFSET $offset";
137
138
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
139
        $result = $resultQuery->fetchAll();
140
141
        // Cache for 10 minutes, and return.
142
        $this->setCache($cacheKey, $result);
143
        $this->stopwatch->stop($cacheKey);
144
145
        return $result;
146
    }
147
148
    /**
149
     * Get counts of known automated tools used by the given user.
150
     * @param Project $project
151
     * @param User $user
152
     * @param string|int $namespace Namespace ID or 'all'.
153
     * @param string $start Start date in a format accepted by strtotime()
154
     * @param string $end End date in a format accepted by strtotime()
155
     * @return string[] Each tool that they used along with the count and link:
156
     *                  [
157
     *                      'Twinkle' => [
158
     *                          'count' => 50,
159
     *                          'link' => 'Wikipedia:Twinkle',
160
     *                      ],
161
     *                  ]
162
     */
163
    public function getAutomatedCounts(
164
        Project $project,
165
        User $user,
166
        $namespace = 'all',
167
        $start = '',
168
        $end = ''
169
    ) {
170
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autotoolcounts');
171
        if ($this->cache->hasItem($cacheKey)) {
172
            return $this->cache->getItem($cacheKey)->get();
173
        }
174
        $this->stopwatch->start($cacheKey, 'XTools');
175
176
        $sql = $this->getAutomatedCountsSql($project, $namespace, $start, $end);
177
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
178
179
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
180
        $tools = $automatedEditsHelper->getTools($project->getDomain());
181
182
        // handling results
183
        $results = [];
184
185
        while ($row = $resultQuery->fetch()) {
186
            // Only track tools that they've used at least once
187
            $tool = $row['toolname'];
188
            if ($row['count'] > 0) {
189
                $results[$tool] = [
190
                    'link' => $tools[$tool]['link'],
191
                    'count' => $row['count'],
192
                ];
193
            }
194
        }
195
196
        // Sort the array by count
197
        uasort($results, function ($a, $b) {
198
            return $b['count'] - $a['count'];
199
        });
200
201
        // Cache for 10 minutes, and return.
202
        $this->setCache($cacheKey, $results);
203
        $this->stopwatch->stop($cacheKey);
204
205
        return $results;
206
    }
207
208
    /**
209
     * Get SQL for getting counts of known automated tools used by the given user.
210
     * @see self::getAutomatedCounts()
211
     * @param Project $project
212
     * @param string|int $namespace Namespace ID or 'all'.
213
     * @param string $start Start date in a format accepted by strtotime()
214
     * @param string $end End date in a format accepted by strtotime()
215
     * @return string The SQL.
216
     */
217
    private function getAutomatedCountsSql(Project $project, $namespace, $start, $end)
218
    {
219
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
220
221
        // Load the semi-automated edit types.
222
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
223
        $tools = $automatedEditsHelper->getTools($project->getDomain());
224
225
        // Create a collection of queries that we're going to run.
226
        $queries = [];
227
228
        $revisionTable = $project->getTableName('revision');
229
        $tagTable = $project->getTableName('change_tag');
230
231
        list($pageJoin, $condNamespace) = $this->getPageAndNamespaceSql($project, $namespace);
232
233
        $conn = $this->getProjectsConnection();
234
235
        foreach ($tools as $toolname => $values) {
236
            list($condTool, $tagJoin) = $this->getInnerAutomatedCountsSql($tagTable, $values);
237
238
            $toolname = $conn->quote($toolname, \PDO::PARAM_STR);
239
240
            // Developer error, no regex or tag provided for this tool.
241
            if ($condTool === '') {
242
                throw new Exception("No regex or tag found for the tool $toolname. " .
243
                    "Please verify this entry in semi_automated.yml");
244
            }
245
246
            $queries[] .= "
247
                SELECT $toolname AS toolname, COUNT(rev_id) AS count
248
                FROM $revisionTable
249
                $pageJoin
250
                $tagJoin
251
                WHERE rev_user_text = :username
252
                AND $condTool
253
                $condNamespace
254
                $condBegin
255
                $condEnd";
256
        }
257
258
        // Combine to one big query.
259
        return implode(' UNION ', $queries);
260
    }
261
262
    /**
263
     * Get some of the inner SQL for self::getAutomatedCountsSql().
264
     * @param  string $tagTable Name of the `change_tag` table.
265
     * @param  string[] $values Values as defined in semi_automated.yml
266
     * @return string[] [Equality clause, JOIN clause]
267
     */
268
    private function getInnerAutomatedCountsSql($tagTable, $values)
269
    {
270
        $conn = $this->getProjectsConnection();
271
        $tagJoin = '';
272
        $condTool = '';
273
274
        if (isset($values['regex'])) {
275
            $regex = $conn->quote($values['regex'], \PDO::PARAM_STR);
276
            $condTool = "rev_comment REGEXP $regex";
277
        }
278
        if (isset($values['tag'])) {
279
            $tagJoin = "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id";
280
            $tag = $conn->quote($values['tag'], \PDO::PARAM_STR);
281
282
            // Append to regex clause if already present.
283
            // Tags are more reliable but may not be present for edits made with
284
            //   older versions of the tool, before it started adding tags.
285
            if ($condTool === '') {
286
                $condTool = "ct_tag = $tag";
287
            } else {
288
                $condTool = '(' . $condTool . " OR ct_tag = $tag)";
289
            }
290
        }
291
292
        return [$condTool, $tagJoin];
293
    }
294
295
    /**
296
     * Get the combined regex and tags for all semi-automated tools,
297
     *   ready to be used in a query.
298
     * @param string $projectDomain Such as en.wikipedia.org
299
     * @return string[] In the format:
300
     *    ['combined|regex', 'combined,tags']
301
     */
302
    private function getToolRegexAndTags($projectDomain)
303
    {
304
        $conn = $this->getProjectsConnection();
305
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
306
        $tools = $automatedEditsHelper->getTools($projectDomain);
307
        $regexes = [];
308
        $tags = [];
309
310
        foreach ($tools as $tool => $values) {
311
            if (isset($values['regex'])) {
312
                $regexes[] = $values['regex'];
313
            }
314
            if (isset($values['tag'])) {
315
                $tags[] = $conn->quote($values['tag'], \PDO::PARAM_STR);
316
            }
317
        }
318
319
        return [
320
            $conn->quote(implode('|', $regexes), \PDO::PARAM_STR),
321
            implode(',', $tags),
322
        ];
323
    }
324
}
325