Test Failed
Push — dependency-injection ( 7565fa )
by MusikAnimal
07:05
created

TopEditsRepository::queryTopEditsPage()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 85
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 44
nc 4
nop 5
dl 0
loc 85
rs 9.216
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Repository;
6
7
use App\Model\Edit;
8
use App\Model\Page;
9
use App\Model\Project;
10
use App\Model\User;
11
use GuzzleHttp\Client;
12
use PDO;
13
use Psr\Cache\CacheItemPoolInterface;
14
use Psr\Log\LoggerInterface;
15
use Symfony\Component\DependencyInjection\ContainerInterface;
16
use Wikimedia\IPUtils;
17
18
/**
19
 * TopEditsRepository is responsible for retrieving data from the database
20
 * about the top-edited pages of a user. It doesn't do any post-processing
21
 * of that information.
22
 * @codeCoverageIgnore
23
 */
24
class TopEditsRepository extends UserRepository
25
{
26
    protected EditRepository $editRepo;
27
    protected UserRepository $userRepo;
28
29
    public function __construct(
30
        ContainerInterface $container,
31
        CacheItemPoolInterface $cache,
32
        Client $guzzle,
33
        LoggerInterface $logger,
34
        bool $isWMF,
35
        int $queryTimeout,
36
        EditRepository $editRepo,
37
        UserRepository $userRepo
38
    ) {
39
        $this->editRepo = $editRepo;
40
        $this->userRepo = $userRepo;
41
        parent::__construct($container, $cache, $guzzle, $logger, $isWMF, $queryTimeout);
0 ignored issues
show
Bug introduced by
The call to App\Repository\UserRepository::__construct() has too few arguments starting with projectRepo. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

41
        parent::/** @scrutinizer ignore-call */ 
42
                __construct($container, $cache, $guzzle, $logger, $isWMF, $queryTimeout);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
42
    }
43
44
    /**
45
     * Factory to instantiate a new Edit for the given revision.
46
     * @param Page $page
47
     * @param array $revision
48
     * @return Edit
49
     */
50
    public function getEdit(Page $page, array $revision): Edit
51
    {
52
        return new Edit($this->editRepo, $this->userRepo, $page, $revision);
53
    }
54
55
    /**
56
     * Get the top edits by a user in a single namespace.
57
     * @param Project $project
58
     * @param User $user
59
     * @param int $namespace Namespace ID.
60
     * @param int|false $start Start date as Unix timestamp.
61
     * @param int|false $end End date as Unix timestamp.
62
     * @param int $limit Number of edits to fetch.
63
     * @param int $pagination Which page of results to return.
64
     * @return string[] page_namespace, page_title, page_is_redirect,
65
     *   count (number of edits), assessment (page assessment).
66
     */
67
    public function getTopEditsNamespace(
68
        Project $project,
69
        User $user,
70
        int $namespace = 0,
71
        $start = false,
72
        $end = false,
73
        int $limit = 1000,
74
        int $pagination = 0
75
    ): array {
76
        // Set up cache.
77
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_ns');
78
        if ($this->cache->hasItem($cacheKey)) {
79
            return $this->cache->getItem($cacheKey)->get();
80
        }
81
82
        $revDateConditions = $this->getDateConditions($start, $end);
83
        $pageTable = $project->getTableName('page');
84
        $revisionTable = $project->getTableName('revision');
85
86
        $hasPageAssessments = $this->isWMF && $project->hasPageAssessments() && 0 === $namespace;
87
        $paTable = $project->getTableName('page_assessments');
88
        $paSelect = $hasPageAssessments
89
            ?  ", (
90
                    SELECT pa_class
91
                    FROM $paTable
92
                    WHERE pa_page_id = page_id
93
                    AND pa_class != 'Unknown'
94
                    LIMIT 1
95
                ) AS pa_class"
96
            : '';
97
98
        $ipcJoin = '';
99
        $whereClause = 'rev_actor = :actorId';
100
        $params = [];
101
        if ($user->isIpRange()) {
102
            $ipcTable = $project->getTableName('ip_changes');
103
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
104
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
105
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
106
        }
107
108
        $offset = $pagination * $limit;
109
        $sql = "SELECT page_namespace, page_title, page_is_redirect, COUNT(page_title) AS count
110
                    $paSelect
111
                FROM $pageTable
112
                JOIN $revisionTable ON page_id = rev_page
113
                $ipcJoin
114
                WHERE $whereClause
115
                AND page_namespace = :namespace
116
                $revDateConditions
117
                GROUP BY page_namespace, page_title
118
                ORDER BY count DESC
119
                LIMIT $limit
120
                OFFSET $offset";
121
122
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
123
        $result = $resultQuery->fetchAllAssociative();
124
125
        // Cache and return.
126
        return $this->setCache($cacheKey, $result);
127
    }
128
129
    /**
130
     * Count the number of edits in the given namespace.
131
     * @param Project $project
132
     * @param User $user
133
     * @param int|string $namespace
134
     * @param int|false $start Start date as Unix timestamp.
135
     * @param int|false $end End date as Unix timestamp.
136
     * @return mixed
137
     */
138
    public function countEditsNamespace(Project $project, User $user, $namespace, $start = false, $end = false)
139
    {
140
        // Set up cache.
141
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_count_ns');
142
        if ($this->cache->hasItem($cacheKey)) {
143
            return $this->cache->getItem($cacheKey)->get();
144
        }
145
146
        $revDateConditions = $this->getDateConditions($start, $end);
147
        $pageTable = $project->getTableName('page');
148
        $revisionTable = $project->getTableName('revision');
149
150
        $ipcJoin = '';
151
        $whereClause = 'rev_actor = :actorId';
152
        $params = [];
153
        if ($user->isIpRange()) {
154
            $ipcTable = $project->getTableName('ip_changes');
155
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
156
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
157
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
158
        }
159
160
        $sql = "SELECT COUNT(DISTINCT page_id)
161
                FROM $pageTable
162
                JOIN $revisionTable ON page_id = rev_page
163
                $ipcJoin
164
                WHERE $whereClause
165
                AND page_namespace = :namespace
166
                $revDateConditions";
167
168
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
169
170
        // Cache and return.
171
        return $this->setCache($cacheKey, $resultQuery->fetchOne());
172
    }
173
174
    /**
175
     * Get the top edits by a user across all namespaces.
176
     * @param Project $project
177
     * @param User $user
178
     * @param int|false $start Start date as Unix timestamp.
179
     * @param int|false $end End date as Unix timestamp.
180
     * @param int $limit Number of edits to fetch.
181
     * @return string[] page_namespace, page_title, page_is_redirect,
182
     *   count (number of edits), assessment (page assessment).
183
     */
184
    public function getTopEditsAllNamespaces(
185
        Project $project,
186
        User $user,
187
        $start = false,
188
        $end = false,
189
        int $limit = 10
190
    ): array {
191
        // Set up cache.
192
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_all');
193
        if ($this->cache->hasItem($cacheKey)) {
194
            return $this->cache->getItem($cacheKey)->get();
195
        }
196
197
        $revDateConditions = $this->getDateConditions($start, $end);
198
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
199
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
200
        $hasPageAssessments = $this->isWMF && $project->hasPageAssessments();
201
        $pageAssessmentsTable = $this->getTableName($project->getDatabaseName(), 'page_assessments');
202
        $paSelect = $hasPageAssessments
203
            ?  ", (
204
                    SELECT pa_class
205
                    FROM $pageAssessmentsTable
206
                    WHERE pa_page_id = e.page_id
207
                    LIMIT 1
208
                ) AS pa_class"
209
            : ', NULL as pa_class';
210
211
        $ipcJoin = '';
212
        $whereClause = 'rev_actor = :actorId';
213
        $params = [];
214
        if ($user->isIpRange()) {
215
            $ipcTable = $project->getTableName('ip_changes');
216
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
217
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
218
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
219
        }
220
221
        $sql = "SELECT c.page_namespace, e.page_title, c.page_is_redirect, c.count $paSelect
222
                FROM
223
                (
224
                    SELECT b.page_namespace, b.page_is_redirect, b.rev_page, b.count
225
                        ,@rn := if(@ns = b.page_namespace, @rn + 1, 1) AS row_number
226
                        ,@ns := b.page_namespace AS dummy
227
                    FROM
228
                    (
229
                        SELECT page_namespace, page_is_redirect, rev_page, count(rev_page) AS count
230
                        FROM $revisionTable
231
                        $ipcJoin
232
                        JOIN $pageTable ON page_id = rev_page
233
                        WHERE $whereClause
234
                        $revDateConditions
235
                        GROUP BY page_namespace, rev_page
236
                    ) AS b
237
                    JOIN (SELECT @ns := NULL, @rn := 0) AS vars
238
                    ORDER BY b.page_namespace ASC, b.count DESC
239
                ) AS c
240
                JOIN $pageTable e ON e.page_id = c.rev_page
241
                WHERE c.row_number <= $limit";
242
        $resultQuery = $this->executeQuery($sql, $project, $user, 'all', $params);
243
        $result = $resultQuery->fetchAllAssociative();
244
245
        // Cache and return.
246
        return $this->setCache($cacheKey, $result);
247
    }
248
249
    /**
250
     * Get the top edits by a user to a single page.
251
     * @param Page $page
252
     * @param User $user
253
     * @param int|false $start Start date as Unix timestamp.
254
     * @param int|false $end End date as Unix timestamp.
255
     * @return string[][] Each row with keys 'id', 'timestamp', 'minor', 'length',
256
     *   'length_change', 'reverted', 'user_id', 'username', 'comment', 'parent_comment'
257
     */
258
    public function getTopEditsPage(Page $page, User $user, $start = false, $end = false): array
259
    {
260
        // Set up cache.
261
        $cacheKey = $this->getCacheKey(func_get_args(), 'topedits_page');
262
        if ($this->cache->hasItem($cacheKey)) {
263
            return $this->cache->getItem($cacheKey)->get();
264
        }
265
266
        $results = $this->queryTopEditsPage($page, $user, $start, $end, true);
267
268
        // Now we need to get the most recent revision, since the childrevs stuff excludes it.
269
        $lastRev = $this->queryTopEditsPage($page, $user, $start, $end, false);
270
        if (empty($results) || $lastRev[0]['id'] !== $results[0]['id']) {
271
            $results = array_merge($lastRev, $results);
272
        }
273
274
        // Cache and return.
275
        return $this->setCache($cacheKey, $results);
276
    }
277
278
    /**
279
     * The actual query to get the top edits by the user to the page.
280
     * Because of the way the main query works, we aren't given the most recent revision,
281
     * so we have to call this twice, once with $childRevs set to true and once with false.
282
     * @param Page $page
283
     * @param User $user
284
     * @param int|false $start Start date as Unix timestamp.
285
     * @param int|false $end End date as Unix timestamp.
286
     * @param boolean $childRevs Whether to include child revisions.
287
     * @return array Each row with keys 'id', 'timestamp', 'minor', 'length',
288
     *   'length_change', 'reverted', 'user_id', 'username', 'comment', 'parent_comment'
289
     */
290
    private function queryTopEditsPage(
291
        Page $page,
292
        User $user,
293
        $start = false,
294
        $end = false,
295
        bool $childRevs = false
296
    ): array {
297
        $project = $page->getProject();
298
        $revDateConditions = $this->getDateConditions($start, $end, false, 'revs.');
299
        $revTable = $project->getTableName('revision');
300
        $commentTable = $project->getTableName('comment');
301
        $tagTable = $project->getTableName('change_tag');
302
        $tagDefTable = $project->getTableName('change_tag_def');
303
304
        if ($childRevs) {
305
            $childSelect = ", (
306
                    CASE WHEN
307
                        childrevs.rev_sha1 = parentrevs.rev_sha1
308
                        OR (
309
                            SELECT 1
310
                            FROM $tagTable
311
                            WHERE ct_rev_id = revs.rev_id
312
                            AND ct_tag_id = (
313
                                SELECT ctd_id
314
                                FROM $tagDefTable
315
                                WHERE ctd_name = 'mw-reverted'
316
                            )
317
                        )
318
                    THEN 1
319
                    ELSE 0
320
                    END
321
                ) AS `reverted`,
322
                childcomments.comment_text AS `parent_comment`";
323
            $childJoin = "LEFT JOIN $revTable AS childrevs ON (revs.rev_id = childrevs.rev_parent_id)
324
                LEFT OUTER JOIN $commentTable AS childcomments
325
                ON (childrevs.rev_comment_id = childcomments.comment_id)";
326
            $childWhere = 'AND childrevs.rev_page = :pageid';
327
            $childLimit = '';
328
        } else {
329
            $childSelect = ', "" AS parent_comment, 0 AS reverted';
330
            $childJoin = '';
331
            $childWhere = '';
332
            $childLimit = 'LIMIT 1';
333
        }
334
335
        $userId = $this->getProjectsConnection($project)->quote($user->getId($page->getProject()), PDO::PARAM_STR);
336
        $username = $this->getProjectsConnection($project)->quote($user->getUsername(), PDO::PARAM_STR);
337
338
        // IP range handling.
339
        $ipcJoin = '';
340
        $whereClause = 'revs.rev_actor = :actorId';
341
        $params = ['pageid' => $page->getId()];
342
        if ($user->isIpRange()) {
343
            $ipcTable = $project->getTableName('ip_changes');
344
            $ipcJoin = "JOIN $ipcTable ON revs.rev_id = ipc_rev_id";
345
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
346
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
347
        }
348
349
        $sql = "SELECT * FROM (
350
                    SELECT
351
                        revs.rev_id AS id,
352
                        revs.rev_timestamp AS timestamp,
353
                        revs.rev_minor_edit AS minor,
354
                        revs.rev_len AS length,
355
                        (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
356
                        $userId AS user_id,
357
                        $username AS username,
358
                        comments.comment_text AS `comment`
359
                        $childSelect
360
                    FROM $revTable AS revs
361
                    LEFT JOIN $revTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
362
                    $ipcJoin
363
                    LEFT OUTER JOIN $commentTable AS comments ON (revs.rev_comment_id = comments.comment_id)
364
                    $childJoin
365
                    WHERE $whereClause
366
                    $revDateConditions
367
                    AND revs.rev_page = :pageid
368
                    $childWhere
369
                ) a
370
                ORDER BY timestamp DESC
371
                $childLimit";
372
373
        $resultQuery = $this->executeQuery($sql, $project, $user, null, $params);
374
        return $resultQuery->fetchAllAssociative();
375
    }
376
}
377