Test Failed
Pull Request — main (#442)
by MusikAnimal
07:57 queued 03:53
created

AutoEditsRepository::getAutomatedEdits()   B

Complexity

Conditions 8
Paths 33

Size

Total Lines 81
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 42
nc 33
nop 8
dl 0
loc 81
rs 8.0035
c 0
b 0
f 0

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Repository;
6
7
use App\Model\Project;
8
use App\Model\User;
9
use PDO;
10
use Wikimedia\IPUtils;
11
12
/**
13
 * AutoEditsRepository is responsible for retrieving data from the database
14
 * about the automated edits made by a user.
15
 * @codeCoverageIgnore
16
 */
17
class AutoEditsRepository extends UserRepository
18
{
19
    /** @var array List of automated tools, used for fetching the tool list and filtering it. */
20
    private array $aeTools;
21
22
    /** @var bool Whether to use the /sandbox version of the config, bypassing caching. */
23
    private bool $useSandbox = false;
24
25
    /** @var array Process cache for tags/IDs. */
26
    private array $tags;
27
28
    /**
29
     * @param bool $useSandbox
30
     * @return AutoEditsRepository
31
     */
32
    public function setUseSandbox(bool $useSandbox): AutoEditsRepository
33
    {
34
        $this->useSandbox = $useSandbox;
35
        return $this;
36
    }
37
38
    /**
39
     * Method to give the repository access to the AutomatedEditsHelper and fetch the list of semi-automated tools.
40
     * @param Project $project
41
     * @param int|string $namespace Namespace ID or 'all'.
42
     * @return array
43
     */
44
    public function getTools(Project $project, $namespace = 'all'): array
45
    {
46
        if (!isset($this->aeTools)) {
47
            $this->aeTools = $this->container
48
                ->get('app.automated_edits_helper')
49
                ->getTools($project, $this->useSandbox);
50
        }
51
52
        if ('all' !== $namespace) {
53
            // Limit by namespace.
54
            return array_filter($this->aeTools, function (array $tool) use ($namespace) {
55
                return empty($tool['namespaces']) ||
56
                    in_array((int)$namespace, $tool['namespaces']) ||
57
                    (
58
                        1 === $namespace % 2 &&
59
                        isset($tool['talk_namespaces'])
60
                    );
61
            });
62
        }
63
64
        return $this->aeTools;
65
    }
66
67
    /**
68
     * Get tools that were misconfigured, also removing them from $this->aeTools.
69
     * @param Project $project
70
     * @return string[] Labels for the invalid tools.
71
     */
72
    public function getInvalidTools(Project $project): array
73
    {
74
        $tools = $this->getTools($project);
75
        $invalidTools = $tools['invalid'] ?? [];
76
        unset($this->aeTools['invalid']);
77
        return $invalidTools;
78
    }
79
80
    /**
81
     * Overrides Repository::setCache(), and will not call the parent (which sets the cache) if using the sandbox.
82
     * @inheritDoc
83
     */
84
    public function setCache(string $cacheKey, $value, $duration = 'PT20M')
85
    {
86
        if ($this->useSandbox) {
87
            return $value;
88
        }
89
90
        return parent::setCache($cacheKey, $value, $duration);
91
    }
92
93
    /**
94
     * Get the number of edits this user made using semi-automated tools.
95
     * @param Project $project
96
     * @param User $user
97
     * @param string|int $namespace Namespace ID or 'all'
98
     * @param int|false $start Start date as Unix timestamp.
99
     * @param int|false $end End date as Unix timestamp.
100
     * @return int Result of query, see below.
101
     */
102
    public function countAutomatedEdits(
103
        Project $project,
104
        User $user,
105
        $namespace = 'all',
106
        $start = false,
107
        $end = false
108
    ): int {
109
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autoeditcount');
110
        if (!$this->useSandbox && $this->cache->hasItem($cacheKey)) {
111
            return $this->cache->getItem($cacheKey)->get();
112
        }
113
114
        $revDateConditions = $this->getDateConditions($start, $end);
115
116
        // Get the combined regex and tags for the tools
117
        [$regex, $tagIds] = $this->getToolRegexAndTags($project, false, null, $namespace);
118
119
        [$pageJoin, $condNamespace] = $this->getPageAndNamespaceSql($project, $namespace);
120
121
        $revisionTable = $project->getTableName('revision');
122
        $ipcTable = $project->getTableName('ip_changes');
123
        $commentTable = $project->getTableName('comment', 'revision');
124
        $tagTable = $project->getTableName('change_tag');
125
        $commentJoin = '';
126
        $tagJoin = '';
127
128
        $params = [];
129
130
        // IP range handling.
131
        $ipcJoin = '';
132
        $whereClause = 'rev_actor = :actorId';
133
        if ($user->isIpRange()) {
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
        // Build SQL for detecting AutoEdits via regex and/or tags.
140
        $condTools = [];
141
        if ('' != $regex) {
142
            $commentJoin = "LEFT OUTER JOIN $commentTable ON rev_comment_id = comment_id";
143
            $condTools[] = "comment_text REGEXP :tools";
144
            $params['tools'] = $regex;
145
        }
146
        if ('' != $tagIds) {
147
            $tagJoin = "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id";
148
            $condTools[] = "ct_tag_id IN ($tagIds)";
149
        }
150
        $condTool = 'AND (' . implode(' OR ', $condTools) . ')';
151
152
        $sql = "SELECT COUNT(DISTINCT(rev_id))
153
                FROM $revisionTable
154
                $ipcJoin
155
                $pageJoin
156
                $commentJoin
157
                $tagJoin
158
                WHERE $whereClause
159
                $condNamespace
160
                $condTool
161
                $revDateConditions";
162
163
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
164
        $result = (int)$resultQuery->fetchOne();
165
166
        // Cache and return.
167
        return $this->setCache($cacheKey, $result);
168
    }
169
170
    /**
171
     * Get non-automated contributions for the given user.
172
     * @param Project $project
173
     * @param User $user
174
     * @param string|int $namespace Namespace ID or 'all'.
175
     * @param int|false $start Start date as Unix timestamp.
176
     * @param int|false $end End date as Unix timestamp.
177
     * @param int|false $offset Unix timestamp. Used for pagination.
178
     * @param int $limit Number of results to return.
179
     * @return string[] Result of query, with columns 'page_title', 'page_namespace', 'rev_id', 'timestamp', 'minor',
180
     *   'length', 'length_change', 'comment'.
181
     */
182
    public function getNonAutomatedEdits(
183
        Project $project,
184
        User $user,
185
        $namespace = 'all',
186
        $start = false,
187
        $end = false,
188
        $offset = false,
189
        int $limit = 50
190
    ): array {
191
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_nonautoedits');
192
        if (!$this->useSandbox && $this->cache->hasItem($cacheKey)) {
193
            return $this->cache->getItem($cacheKey)->get();
194
        }
195
196
        $revDateConditions = $this->getDateConditions($start, $end, $offset, 'revs.');
197
198
        // Get the combined regex and tags for the tools
199
        [$regex, $tagIds] = $this->getToolRegexAndTags($project, false, null, $namespace);
200
201
        $pageTable = $project->getTableName('page');
202
        $revisionTable = $project->getTableName('revision');
203
        $ipcTable = $project->getTableName('ip_changes');
204
        $commentTable = $project->getTableName('comment', 'revision');
205
        $tagTable = $project->getTableName('change_tag');
206
207
        // IP range handling.
208
        $ipcJoin = '';
209
        $whereClause = 'revs.rev_actor = :actorId';
210
        $params = ['tools' => $regex];
211
        if ($user->isIpRange()) {
212
            $ipcJoin = "JOIN $ipcTable ON revs.rev_id = ipc_rev_id";
213
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
214
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
215
        }
216
217
        $condNamespace = 'all' === $namespace ? '' : 'AND page_namespace = :namespace';
218
        $condTag = '' != $tagIds ? "AND NOT EXISTS (SELECT 1 FROM $tagTable
219
            WHERE ct_rev_id = revs.rev_id AND ct_tag_id IN ($tagIds))" : '';
220
221
        $sql = "SELECT
222
                    page_title,
223
                    page_namespace,
224
                    revs.rev_id AS rev_id,
225
                    revs.rev_timestamp AS timestamp,
226
                    revs.rev_minor_edit AS minor,
227
                    revs.rev_len AS length,
228
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
229
                    comment_text AS comment
230
                FROM $pageTable
231
                JOIN $revisionTable AS revs ON (page_id = revs.rev_page)
232
                $ipcJoin
233
                LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
234
                LEFT OUTER JOIN $commentTable ON (revs.rev_comment_id = comment_id)
235
                WHERE $whereClause
236
                AND revs.rev_timestamp > 0
237
                AND comment_text NOT RLIKE :tools
238
                $condTag
239
                $revDateConditions
240
                $condNamespace
241
                GROUP BY revs.rev_id
242
                ORDER BY revs.rev_timestamp DESC
243
                LIMIT $limit";
244
245
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
246
        $result = $resultQuery->fetchAllAssociative();
247
248
        // Cache and return.
249
        return $this->setCache($cacheKey, $result);
250
    }
251
252
    /**
253
     * Get (semi-)automated contributions for the given user, and optionally for a given tool.
254
     * @param Project $project
255
     * @param User $user
256
     * @param string|int $namespace Namespace ID or 'all'.
257
     * @param int|false $start Start date as Unix timestamp.
258
     * @param int|false $end End date as Unix timestamp.
259
     * @param string|null $tool Only get edits made with this tool. Must match the keys in the AutoEdits config.
260
     * @param int|false $offset Unix timestamp. Used for pagination.
261
     * @param int $limit Number of results to return.
262
     * @return string[] Result of query, with columns 'page_title', 'page_namespace', 'rev_id', 'timestamp', 'minor',
263
     *   'length', 'length_change', 'comment'.
264
     */
265
    public function getAutomatedEdits(
266
        Project $project,
267
        User $user,
268
        $namespace = 'all',
269
        $start = false,
270
        $end = false,
271
        ?string $tool = null,
272
        $offset = false,
273
        int $limit = 50
274
    ): array {
275
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autoedits');
276
        if (!$this->useSandbox && $this->cache->hasItem($cacheKey)) {
277
            return $this->cache->getItem($cacheKey)->get();
278
        }
279
280
        $revDateConditions = $this->getDateConditions($start, $end, $offset, 'revs.');
281
282
        // In this case there is a slight performance improvement we can make if we're not given a start date.
283
        if ('' === $revDateConditions) {
284
            $revDateConditions = 'AND revs.rev_timestamp > 0';
285
        }
286
287
        // Get the combined regex and tags for the tools
288
        [$regex, $tagIds] = $this->getToolRegexAndTags($project, false, $tool);
289
290
        $pageTable = $project->getTableName('page');
291
        $revisionTable = $project->getTableName('revision');
292
        $ipcTable = $project->getTableName('ip_changes');
293
        $commentTable = $project->getTableName('comment', 'revision');
294
        $tagTable = $project->getTableName('change_tag');
295
        $condNamespace = 'all' === $namespace ? '' : 'AND page_namespace = :namespace';
296
        $tagJoin = '';
297
        $condsTool = [];
298
299
        if ('' != $regex) {
300
            $condsTool[] = 'comment_text RLIKE :tools';
301
        }
302
303
        if ('' != $tagIds) {
304
            $tagJoin = "LEFT OUTER JOIN $tagTable ON (ct_rev_id = revs.rev_id)";
305
            $condsTool[] = "ct_tag_id IN ($tagIds)";
306
        }
307
308
        // IP range handling.
309
        $ipcJoin = '';
310
        $whereClause = 'revs.rev_actor = :actorId';
311
        $params = ['tools' => $regex];
312
        if ($user->isIpRange()) {
313
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
314
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
315
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
316
        }
317
318
        $sql = "SELECT
319
                    page_title,
320
                    page_namespace,
321
                    revs.rev_id AS rev_id,
322
                    revs.rev_timestamp AS timestamp,
323
                    revs.rev_minor_edit AS minor,
324
                    revs.rev_len AS length,
325
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
326
                    comment_text AS comment
327
                FROM $pageTable
328
                JOIN $revisionTable AS revs ON (page_id = revs.rev_page)
329
                $ipcJoin
330
                LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
331
                LEFT OUTER JOIN $commentTable ON (revs.rev_comment_id = comment_id)
332
                $tagJoin
333
                WHERE $whereClause
334
                $revDateConditions
335
                $condNamespace
336
                AND (".implode(' OR ', $condsTool).")
337
                GROUP BY revs.rev_id
338
                ORDER BY revs.rev_timestamp DESC
339
                LIMIT $limit";
340
341
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
342
        $result = $resultQuery->fetchAllAssociative();
343
344
        // Cache and return.
345
        return $this->setCache($cacheKey, $result);
346
    }
347
348
    /**
349
     * Get counts of known automated tools used by the given user.
350
     * @param Project $project
351
     * @param User $user
352
     * @param string|int $namespace Namespace ID or 'all'.
353
     * @param int|false $start Start date as Unix timestamp.
354
     * @param int|false $end End date as Unix timestamp.
355
     * @return string[] Each tool that they used along with the count and link:
356
     *                  [
357
     *                      'Twinkle' => [
358
     *                          'count' => 50,
359
     *                          'link' => 'Wikipedia:Twinkle',
360
     *                      ],
361
     *                  ]
362
     */
363
    public function getToolCounts(Project $project, User $user, $namespace = 'all', $start = false, $end = false): array
364
    {
365
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autotoolcounts');
366
        if (!$this->useSandbox && $this->cache->hasItem($cacheKey)) {
367
            return $this->cache->getItem($cacheKey)->get();
368
        }
369
370
        $sql = $this->getAutomatedCountsSql($project, $user, $namespace, $start, $end);
371
        $params = [];
372
        if ($user->isIpRange()) {
373
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
374
        }
375
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
376
377
        $tools = $this->getTools($project, $namespace);
378
379
        // handling results
380
        $results = [];
381
382
        while ($row = $resultQuery->fetchAssociative()) {
383
            // Only track tools that they've used at least once
384
            $tool = $row['toolname'];
385
            if ($row['count'] > 0) {
386
                $results[$tool] = [
387
                    'link' => $tools[$tool]['link'],
388
                    'label' => $tools[$tool]['label'] ?? $tool,
389
                    'count' => $row['count'],
390
                ];
391
            }
392
        }
393
394
        // Sort the array by count
395
        uasort($results, function ($a, $b) {
396
            return $b['count'] - $a['count'];
397
        });
398
399
        // Cache and return.
400
        return $this->setCache($cacheKey, $results);
401
    }
402
403
    /**
404
     * Get SQL for getting counts of known automated tools used by the user.
405
     * @see self::getAutomatedCounts()
406
     * @param Project $project
407
     * @param User $user
408
     * @param string|int $namespace Namespace ID or 'all'.
409
     * @param int|false $start Start date as Unix timestamp.
410
     * @param int|false $end End date as Unix timestamp.
411
     * @return string The SQL.
412
     */
413
    private function getAutomatedCountsSql(
414
        Project $project,
415
        User $user,
416
        $namespace,
417
        $start = false,
418
        $end = false
419
    ): string {
420
        $revDateConditions = $this->getDateConditions($start, $end);
421
422
        // Load the semi-automated edit types.
423
        $tools = $this->getTools($project, $namespace);
424
425
        // Create a collection of queries that we're going to run.
426
        $queries = [];
427
428
        $revisionTable = $project->getTableName('revision');
429
        $ipcTable = $project->getTableName('ip_changes');
430
        [$pageJoin, $condNamespace] = $this->getPageAndNamespaceSql($project, $namespace);
431
        $conn = $this->getProjectsConnection($project);
432
433
        // IP range handling.
434
        $ipcJoin = '';
435
        $whereClause = 'rev_actor = :actorId';
436
        if ($user->isIpRange()) {
437
            $ipcJoin = "JOIN $ipcTable ON rev_id = ipc_rev_id";
438
            $whereClause = 'ipc_hex BETWEEN :startIp AND :endIp';
439
        }
440
441
        foreach ($tools as $toolName => $values) {
442
            [$condTool, $commentJoin, $tagJoin] = $this->getInnerAutomatedCountsSql($project, $toolName, $values);
443
444
            $toolName = $conn->quote($toolName, PDO::PARAM_STR);
445
446
            // No regex or tag provided for this tool. This can happen for tag-only tools that are in the global
447
            // configuration, but no local tag exists on the said project.
448
            if ('' === $condTool) {
449
                continue;
450
            }
451
452
            $queries[] .= "
453
                SELECT $toolName AS toolname, COUNT(DISTINCT(rev_id)) AS count
454
                FROM $revisionTable
455
                $ipcJoin
456
                $pageJoin
457
                $commentJoin
458
                $tagJoin
459
                WHERE $whereClause
460
                AND $condTool
461
                $condNamespace
462
                $revDateConditions";
463
        }
464
465
        // Combine to one big query.
466
        return implode(' UNION ', $queries);
467
    }
468
469
    /**
470
     * Get some of the inner SQL for self::getAutomatedCountsSql().
471
     * @param Project $project
472
     * @param string $toolName
473
     * @param string[] $values Values as defined in the AutoEdits config.
474
     * @return string[] [Equality clause, JOIN clause]
475
     */
476
    private function getInnerAutomatedCountsSql(Project $project, string $toolName, array $values): array
477
    {
478
        $conn = $this->getProjectsConnection($project);
479
        $commentJoin = '';
480
        $tagJoin = '';
481
        $condTool = '';
482
483
        if (isset($values['regex'])) {
484
            $commentTable = $project->getTableName('comment', 'revision');
485
            $commentJoin = "LEFT OUTER JOIN $commentTable ON rev_comment_id = comment_id";
486
            $regex = $conn->quote($values['regex'], PDO::PARAM_STR);
487
            $condTool = "comment_text REGEXP $regex";
488
        }
489
        if (isset($values['tags'])) {
490
            $tagIds = $this->getTagIdsFromNames($project, $values['tags']);
0 ignored issues
show
Bug introduced by
$values['tags'] of type string is incompatible with the type array expected by parameter $tagNames of App\Repository\AutoEdits...y::getTagIdsFromNames(). ( Ignorable by Annotation )

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

490
            $tagIds = $this->getTagIdsFromNames($project, /** @scrutinizer ignore-type */ $values['tags']);
Loading history...
491
492
            if ($tagIds) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tagIds of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
493
                $tagTable = $project->getTableName('change_tag');
494
                $tagJoin = "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id";
495
                $tagClause = $this->getTagsExclusionsSql($project, $toolName, $tagIds);
496
497
                // Use tags in addition to the regex clause, if already present.
498
                // Tags are more reliable but may not be present for edits made with
499
                // older versions of the tool, before it started adding tags.
500
                if ('' === $condTool) {
501
                    $condTool = $tagClause;
502
                } else {
503
                    $condTool = "($condTool OR $tagClause)";
504
                }
505
            }
506
        }
507
508
        return [$condTool, $commentJoin, $tagJoin];
509
    }
510
511
    /**
512
     * Get the combined regex and tags for all semi-automated tools, or the given tool, ready to be used in a query.
513
     * @param Project $project
514
     * @param bool $nonAutoEdits Set to true to exclude tools with the 'contribs' flag.
515
     * @param string|null $tool
516
     * @param int|string|null $namespace Tools only used in given namespace ID, or 'all' for all namespaces.
517
     * @return array In the format: ['combined|regex', '1,2,3'] where the second element is a
518
     *   comma-separated list of the tag IDs, ready to be used in SQL.
519
     */
520
    private function getToolRegexAndTags(
521
        Project $project,
522
        bool $nonAutoEdits = false,
523
        ?string $tool = null,
524
        $namespace = null
525
    ): array {
526
        $tools = $this->getTools($project);
527
        $regexes = [];
528
        $tagIds = [];
529
530
        if ('' != $tool) {
531
            $tools = [$tools[$tool]];
532
        }
533
534
        foreach (array_values($tools) as $values) {
535
            if ($nonAutoEdits && isset($values['contribs'])) {
536
                continue;
537
            }
538
539
            if (is_numeric($namespace) &&
540
                !empty($values['namespaces']) &&
541
                !in_array((int)$namespace, $values['namespaces'])
542
            ) {
543
                continue;
544
            }
545
546
            if (isset($values['regex'])) {
547
                $regexes[] = $values['regex'];
548
            }
549
            if (isset($values['tags'])) {
550
                $tagIds = array_merge($tagIds, $this->getTagIdsFromNames($project, $values['tags']));
551
            }
552
        }
553
554
        return [
555
            implode('|', $regexes),
556
            implode(',', $tagIds),
557
        ];
558
    }
559
560
    /**
561
     * Get the IDs of tags for given Project, which are used in the IN clauses of other queries above.
562
     * This join decomposition is actually faster than JOIN'ing on change_tag_def all in one query.
563
     * @param Project $project
564
     * @return int[] Keys are the tag name, values are the IDs.
565
     */
566
    public function getTags(Project $project): array
567
    {
568
        // Use process cache; ensures we don't needlessly re-query for tag IDs
569
        // during the same request when using the ?usesandbox=1 option.
570
        if (isset($this->tags)) {
571
            return $this->tags;
572
        }
573
574
        $cacheKey = $this->getCacheKey(func_get_args(), 'ae_tag_ids');
575
        if (!$this->useSandbox && $this->cache->hasItem($cacheKey)) {
576
            return $this->cache->getItem($cacheKey)->get();
577
        }
578
579
        $conn = $this->getProjectsConnection($project);
580
581
        // Get all tag values.
582
        $tags = [];
583
        foreach (array_values($this->getTools($project)) as $values) {
584
            if (isset($values['tags'])) {
585
                $tags = array_merge(
586
                    $tags,
587
                    array_map(function ($tag) use ($conn) {
588
                        return $conn->quote($tag, PDO::PARAM_STR);
589
                    }, $values['tags'])
590
                );
591
            }
592
        }
593
594
        $tags = implode(',', $tags);
595
        $tagDefTable = $project->getTableName('change_tag_def');
596
        $sql = "SELECT ctd_name, ctd_id FROM $tagDefTable
597
                WHERE ctd_name IN ($tags)";
598
        $this->tags = $this->executeProjectsQuery($project, $sql)->fetchAllKeyValue();
599
600
        // Cache and return.
601
        return $this->setCache($cacheKey, $this->tags);
602
    }
603
604
    /**
605
     * Generate the WHERE clause to query for the given tags, filtering out exclusions ('tag_excludes' option).
606
     * For instance, Huggle edits are also tagged as Rollback, but when viewing
607
     * Rollback edits we don't want to show Huggle edits.
608
     * @param Project $project
609
     * @param string $tool
610
     * @param array $tagIds
611
     * @return string
612
     */
613
    private function getTagsExclusionsSql(Project $project, string $tool, array $tagIds): string
614
    {
615
        $tagsList = implode(',', $tagIds);
616
        $tagExcludes = $this->getTools($project)[$tool]['tag_excludes'] ?? [];
617
        $excludesSql = '';
618
619
        if ($tagExcludes && 1 === count($tagIds)) {
620
            // Get tag IDs, filtering out those for which no ID exists (meaning there is no local tag for that tool).
621
            $excludesList = implode(',', array_filter(array_map(function ($tagName) use ($project) {
622
                return $this->getTags($project)[$tagName] ?? null;
623
            }, $tagExcludes)));
624
625
            if (strlen($excludesList)) {
626
                $excludesSql = "AND ct_tag_id NOT IN ($excludesList)";
627
            }
628
        }
629
630
        return "ct_tag_id IN ($tagsList) $excludesSql";
631
    }
632
633
    /**
634
     * Get IDs for tags given the names.
635
     * @param Project $project
636
     * @param array $tagNames
637
     * @return array
638
     */
639
    private function getTagIdsFromNames(Project $project, array $tagNames): array
640
    {
641
        $allTagIds = $this->getTags($project);
642
        $tagIds = [];
643
644
        foreach ($tagNames as $tag) {
645
            if (isset($allTagIds[$tag])) {
646
                $tagIds[] = $allTagIds[$tag];
647
            }
648
        }
649
650
        return $tagIds;
651
    }
652
}
653