Passed
Push — ip-ranges ( 157734...88dad8 )
by MusikAnimal
06:12 queued 01:19
created

TopEditsRepository::getTopEditsPage()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 1
b 0
f 0
nc 3
nop 4
dl 0
loc 18
rs 10
1
<?php
2
/**
3
 * This file contains only the TopEditsRepository class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Repository;
9
10
use AppBundle\Model\Page;
11
use AppBundle\Model\Project;
12
use AppBundle\Model\User;
13
use PDO;
14
use Symfony\Component\DependencyInjection\ContainerInterface;
15
use Wikimedia\IPUtils;
16
17
/**
18
 * TopEditsRepository is responsible for retrieving data from the database
19
 * about the top-edited pages of a user. It doesn't do any post-processing
20
 * of that information.
21
 * @codeCoverageIgnore
22
 */
23
class TopEditsRepository extends UserRepository
24
{
25
    /**
26
     * Expose the container to the TopEdits class.
27
     * @return ContainerInterface
28
     */
29
    public function getContainer(): ContainerInterface
30
    {
31
        return $this->container;
32
    }
33
34
    /**
35
     * Get the top edits by a user in a single namespace.
36
     * @param Project $project
37
     * @param User $user
38
     * @param int $namespace Namespace ID.
39
     * @param int|false $start Start date as Unix timestamp.
40
     * @param int|false $end End date as Unix timestamp.
41
     * @param int $limit Number of edits to fetch.
42
     * @param int $pagination Which page of results to return.
43
     * @return string[] page_namespace, page_title, page_is_redirect,
44
     *   count (number of edits), assessment (page assessment).
45
     */
46
    public function getTopEditsNamespace(
47
        Project $project,
48
        User $user,
49
        int $namespace = 0,
50
        $start = false,
51
        $end = false,
52
        int $limit = 1000,
53
        int $pagination = 0
54
    ): array {
55
        // Set up cache.
56
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_ns');
57
        if ($this->cache->hasItem($cacheKey)) {
58
            return $this->cache->getItem($cacheKey)->get();
59
        }
60
61
        $revDateConditions = $this->getDateConditions($start, $end);
62
        $pageTable = $project->getTableName('page');
63
        $revisionTable = $project->getTableName('revision');
64
65
        $hasPageAssessments = $this->isLabs() && $project->hasPageAssessments() && 0 === $namespace;
66
        $paTable = $project->getTableName('page_assessments');
67
        $paSelect = $hasPageAssessments
68
            ?  ", (
69
                    SELECT pa_class
70
                    FROM $paTable
71
                    WHERE pa_page_id = page_id
72
                    AND pa_class != 'Unknown'
73
                    LIMIT 1
74
                ) AS pa_class"
75
            : '';
76
77
        $ipcJoin = '';
78
        $whereClause = 'rev_actor = :actorId';
79
        $params = [];
80
        if ($user->isIpRange()) {
81
            $ipcTable = $project->getTableName('ip_changes');
82
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
83
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
84
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
85
        }
86
87
        $offset = $pagination * $limit;
88
        $sql = "SELECT page_namespace, page_title, page_is_redirect, COUNT(page_title) AS count
89
                    $paSelect
90
                FROM $pageTable
91
                JOIN $revisionTable ON page_id = rev_page
92
                $ipcJoin
93
                WHERE $whereClause
94
                AND page_namespace = :namespace
95
                $revDateConditions
96
                GROUP BY page_namespace, page_title
97
                ORDER BY count DESC
98
                LIMIT $limit
99
                OFFSET $offset";
100
101
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
102
        $result = $resultQuery->fetchAll();
103
104
        // Cache and return.
105
        return $this->setCache($cacheKey, $result);
106
    }
107
108
    /**
109
     * Count the number of edits in the given namespace.
110
     * @param Project $project
111
     * @param User $user
112
     * @param int|string $namespace
113
     * @param int|false $start Start date as Unix timestamp.
114
     * @param int|false $end End date as Unix timestamp.
115
     * @return mixed
116
     */
117
    public function countEditsNamespace(Project $project, User $user, $namespace, $start = false, $end = false)
118
    {
119
        // Set up cache.
120
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_count_ns');
121
        if ($this->cache->hasItem($cacheKey)) {
122
            return $this->cache->getItem($cacheKey)->get();
123
        }
124
125
        $revDateConditions = $this->getDateConditions($start, $end);
126
        $pageTable = $project->getTableName('page');
127
        $revisionTable = $project->getTableName('revision');
128
129
        $ipcJoin = '';
130
        $whereClause = 'rev_actor = :actorId';
131
        $params = [];
132
        if ($user->isIpRange()) {
133
            $ipcTable = $project->getTableName('ip_changes');
134
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
135
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
136
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
137
        }
138
139
        $sql = "SELECT COUNT(DISTINCT page_id) AS count
140
                FROM $pageTable
141
                JOIN $revisionTable ON page_id = rev_page
142
                $ipcJoin
143
                WHERE $whereClause
144
                AND page_namespace = :namespace
145
                $revDateConditions";
146
147
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
148
149
        // Cache and return.
150
        return $this->setCache($cacheKey, $resultQuery->fetch()['count']);
151
    }
152
153
    /**
154
     * Get the top edits by a user across all namespaces.
155
     * @param Project $project
156
     * @param User $user
157
     * @param int|false $start Start date as Unix timestamp.
158
     * @param int|false $end End date as Unix timestamp.
159
     * @param int $limit Number of edits to fetch.
160
     * @return string[] page_namespace, page_title, page_is_redirect,
161
     *   count (number of edits), assessment (page assessment).
162
     */
163
    public function getTopEditsAllNamespaces(
164
        Project $project,
165
        User $user,
166
        $start = false,
167
        $end = false,
168
        int $limit = 10
169
    ): array {
170
        // Set up cache.
171
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_all');
172
        if ($this->cache->hasItem($cacheKey)) {
173
            return $this->cache->getItem($cacheKey)->get();
174
        }
175
176
        $revDateConditions = $this->getDateConditions($start, $end);
177
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
178
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
179
        $hasPageAssessments = $this->isLabs() && $project->hasPageAssessments();
180
        $pageAssessmentsTable = $this->getTableName($project->getDatabaseName(), 'page_assessments');
181
        $paSelect = $hasPageAssessments
182
            ?  ", (
183
                    SELECT pa_class
184
                    FROM $pageAssessmentsTable
185
                    WHERE pa_page_id = e.page_id
186
                    LIMIT 1
187
                ) AS pa_class"
188
            : ', NULL as pa_class';
189
190
        $ipcJoin = '';
191
        $whereClause = 'rev_actor = :actorId';
192
        $params = [];
193
        if ($user->isIpRange()) {
194
            $ipcTable = $project->getTableName('ip_changes');
195
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
196
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
197
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
198
        }
199
200
        $sql = "SELECT c.page_namespace, e.page_title, c.page_is_redirect, c.count $paSelect
201
                FROM
202
                (
203
                    SELECT b.page_namespace, b.page_is_redirect, b.rev_page, b.count
204
                        ,@rn := if(@ns = b.page_namespace, @rn + 1, 1) AS row_number
205
                        ,@ns := b.page_namespace AS dummy
206
                    FROM
207
                    (
208
                        SELECT page_namespace, page_is_redirect, rev_page, count(rev_page) AS count
209
                        FROM $revisionTable
210
                        $ipcJoin
211
                        JOIN $pageTable ON page_id = rev_page
212
                        WHERE $whereClause
213
                        $revDateConditions
214
                        GROUP BY page_namespace, rev_page
215
                    ) AS b
216
                    JOIN (SELECT @ns := NULL, @rn := 0) AS vars
217
                    ORDER BY b.page_namespace ASC, b.count DESC
218
                ) AS c
219
                JOIN $pageTable e ON e.page_id = c.rev_page
220
                WHERE c.row_number <= $limit";
221
        $resultQuery = $this->executeQuery($sql, $project, $user, 'all', $params);
222
        $result = $resultQuery->fetchAll();
223
224
        // Cache and return.
225
        return $this->setCache($cacheKey, $result);
226
    }
227
228
    /**
229
     * Get the top edits by a user to a single page.
230
     * @param Page $page
231
     * @param User $user
232
     * @param int|false $start Start date as Unix timestamp.
233
     * @param int|false $end End date as Unix timestamp.
234
     * @return string[] Each row with keys 'id', 'timestamp', 'minor', 'length',
235
     *   'length_change', 'reverted', 'user_id', 'username', 'comment', 'parent_comment'
236
     */
237
    public function getTopEditsPage(Page $page, User $user, $start = false, $end = false): array
238
    {
239
        // Set up cache.
240
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_page');
241
        if ($this->cache->hasItem($cacheKey)) {
242
            return $this->cache->getItem($cacheKey)->get();
243
        }
244
245
        $results = $this->queryTopEditsPage($page, $user, $start, $end, true);
246
247
        // Now we need to get the most recent revision, since the childrevs stuff excludes it.
248
        $lastRev = $this->queryTopEditsPage($page, $user, $start, $end, false);
249
        if (empty($results) || $lastRev[0]['id'] !== $results[0]['id']) {
250
            $results = array_merge($lastRev, $results);
251
        }
252
253
        // Cache and return.
254
        return $this->setCache($cacheKey, $results);
255
    }
256
257
    /**
258
     * The actual query to get the top edits by the user to the page.
259
     * Because of the way the main query works, we aren't given the most recent revision,
260
     * so we have to call this twice, once with $childRevs set to true and once with false.
261
     * @param Page $page
262
     * @param User $user
263
     * @param int|false $start Start date as Unix timestamp.
264
     * @param int|false $end End date as Unix timestamp.
265
     * @param boolean $childRevs Whether to include child revisions.
266
     * @return array Each row with keys 'id', 'timestamp', 'minor', 'length',
267
     *   'length_change', 'reverted', 'user_id', 'username', 'comment', 'parent_comment'
268
     */
269
    private function queryTopEditsPage(
270
        Page $page,
271
        User $user,
272
        $start = false,
273
        $end = false,
274
        bool $childRevs = false
275
    ): array {
276
        $project = $page->getProject();
277
        $revDateConditions = $this->getDateConditions($start, $end, false, 'revs.');
278
        $revTable = $this->getTableName($project->getDatabaseName(), 'revision');
279
        $commentTable = $this->getTableName($project->getDatabaseName(), 'comment');
280
281
        if ($childRevs) {
282
            $childSelect = ', (CASE WHEN childrevs.rev_sha1 = parentrevs.rev_sha1 THEN 1 ELSE 0 END) AS reverted,
283
                childcomments.comment_text AS parent_comment';
284
            $childJoin = "LEFT JOIN $revTable AS childrevs ON (revs.rev_id = childrevs.rev_parent_id)
285
                LEFT OUTER JOIN $commentTable AS childcomments
286
                ON (childrevs.rev_comment_id = childcomments.comment_id)";
287
            $childWhere = 'AND childrevs.rev_page = :pageid';
288
            $childLimit = '';
289
        } else {
290
            $childSelect = ', "" AS parent_comment, 0 AS reverted';
291
            $childJoin = '';
292
            $childWhere = '';
293
            $childLimit = 'LIMIT 1';
294
        }
295
296
        $userId = $this->getProjectsConnection($project)->quote($user->getId($page->getProject()), PDO::PARAM_STR);
297
        $username = $this->getProjectsConnection($project)->quote($user->getUsername(), PDO::PARAM_STR);
298
299
        // IP range handling.
300
        $ipcJoin = '';
301
        $whereClause = 'revs.rev_actor = :actorId';
302
        $params = ['pageid' => $page->getId()];
303
        if ($user->isIpRange()) {
304
            $ipcTable = $project->getTableName('ip_changes');
305
            $ipcJoin = "JOIN $ipcTable ON revs.rev_id = ipc_rev_id";
306
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
307
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
308
        }
309
310
        $sql = "SELECT * FROM (
311
                    SELECT
312
                        revs.rev_id AS id,
313
                        revs.rev_timestamp AS timestamp,
314
                        revs.rev_minor_edit AS minor,
315
                        revs.rev_len AS length,
316
                        (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
317
                        $userId AS user_id,
318
                        $username AS username,
319
                        comments.comment_text AS `comment`
320
                        $childSelect
321
                    FROM $revTable AS revs
322
                    LEFT JOIN $revTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
323
                    $ipcJoin
324
                    LEFT OUTER JOIN $commentTable AS comments ON (revs.rev_comment_id = comments.comment_id)
325
                    $childJoin
326
                    WHERE $whereClause
327
                    $revDateConditions
328
                    AND revs.rev_page = :pageid
329
                    $childWhere
330
                ) a
331
                ORDER BY timestamp DESC
332
                $childLimit";
333
334
        $resultQuery = $this->executeQuery($sql, $project, $user, null, $params);
335
        return $resultQuery->fetchAll();
336
    }
337
}
338