Passed
Push — master ( 6f4298...fb4a6d )
by MusikAnimal
12:29
created

AutomatedEditsHelper::mergeRules()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 26
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 5
nop 2
dl 0
loc 26
ccs 12
cts 12
cp 1
crap 5
rs 9.6111
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the AutomatedEditsHelper class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Helper;
9
10
use AppBundle\Model\Project;
11
use Symfony\Component\DependencyInjection\ContainerInterface;
12
13
/**
14
 * Helper class for fetching semi-automated definitions.
15
 */
16
class AutomatedEditsHelper
17
{
18
    /** @var array The list of tools that are considered reverting. */
19
    protected $revertTools = [];
20
21
    /** @var array The list of tool names and their regexes/tags. */
22
    protected $tools = [];
23
24
    /** @var ContainerInterface The service container. */
25
    private $container;
26
27
    /**
28
     * AutomatedEditsHelper constructor.
29
     * @param ContainerInterface $container
30
     */
31 14
    public function __construct(ContainerInterface $container)
32
    {
33 14
        $this->container = $container;
34 14
    }
35
36
    /**
37
     * Get the tool that matched the given edit summary.
38
     * This only works for tools defined with regular expressions, not tags.
39
     * @param string $summary Edit summary
40
     * @param Project $project
41
     * @return string[]|false Tool entry including key for 'name', or false if nothing was found
42
     */
43 9
    public function getTool(string $summary, Project $project)
44
    {
45 9
        foreach ($this->getTools($project) as $tool => $values) {
46 9
            if (isset($values['regex']) && preg_match('/'.$values['regex'].'/', $summary)) {
47 9
                return array_merge([
48 9
                    'name' => $tool,
49 9
                ], $values);
50
            }
51
        }
52
53 7
        return false;
54
    }
55
56
    /**
57
     * Was the edit (semi-)automated, based on the edit summary?
58
     * This only works for tools defined with regular expressions, not tags.
59
     * @param string $summary Edit summary
60
     * @param Project $project
61
     * @return bool
62
     */
63 1
    public function isAutomated(string $summary, Project $project): bool
64
    {
65 1
        return (bool)$this->getTool($summary, $project);
66
    }
67
68
    /**
69
     * Get list of automated tools and their associated info for the given project.
70
     * This defaults to the 'default_project' if entries for the given project are not found.
71
     * @param Project $project
72
     * @return array Each tool with the tool name as the key and 'link', 'regex' and/or 'tag' as the subarray keys.
73
     */
74 14
    public function getTools(Project $project): array
75
    {
76 14
        $projectDomain = $project->getDomain();
77
78 14
        if (isset($this->tools[$projectDomain])) {
79 7
            return $this->tools[$projectDomain];
80
        }
81
82
        // Load the semi-automated edit types.
83 14
        $tools = $this->container->getParameter('automated_tools');
84
85 14
        if (isset($tools[$projectDomain])) {
86 14
            $localRules = $tools[$projectDomain];
87
        } else {
88
            $localRules = [];
89
        }
90
91 14
        $langRules = $tools[$project->getLang()] ?? [];
92
93
        // Per-wiki rules have priority, followed by language-specific and global.
94 14
        $globalWithLangRules = $this->mergeRules($tools['global'], $langRules);
95
96 14
        $this->tools[$projectDomain] = $this->mergeRules(
97 14
            $globalWithLangRules,
98 14
            $localRules
99
        );
100
101
        // Finally, populate the 'label' with the tool name, if a label doesn't already exist.
102 14
        array_walk($this->tools[$projectDomain], function (&$data, $tool): void {
103 14
            $data['label'] = $data['label'] ?? $tool;
104
105
            // 'namespaces' should be an array of ints.
106 14
            $data['namespaces'] = $data['namespaces'] ?? [];
107 14
            if (isset($data['namespace'])) {
108
                $data['namespaces'][] = $data['namespace'];
109
                unset($data['namespace']);
110
            }
111
112
            // 'tags' should be an array of strings.
113 14
            $data['tags'] = $data['tags'] ?? [];
114 14
            if (isset($data['tag'])) {
115
                $data['tags'][] = $data['tag'];
116
                unset($data['tag']);
117
            }
118 14
        });
119
120 14
        uksort($this->tools[$projectDomain], 'strcasecmp');
121
122 14
        return $this->tools[$projectDomain];
123
    }
124
125
    /**
126
     * Merges the given rule sets, giving priority to the local set. Regex is concatenated, not overridden.
127
     * @param string[] $globalRules The global rule set.
128
     * @param string[] $localRules The rule set for the local wiki.
129
     * @return string[] Merged rules.
130
     */
131 14
    private function mergeRules(array $globalRules, array $localRules): array
132
    {
133
        // Initial set, including just the global rules.
134 14
        $tools = $globalRules;
135
136
        // Loop through local rules and override/merge as necessary.
137 14
        foreach ($localRules as $tool => $rules) {
138 14
            $newRules = $rules;
139
140 14
            if (isset($globalRules[$tool])) {
141
                // Order within array_merge is important, so that local rules get priority.
142 14
                $newRules = array_merge($globalRules[$tool], $rules);
0 ignored issues
show
Bug introduced by
$globalRules[$tool] of type string is incompatible with the type array expected by parameter $array1 of array_merge(). ( Ignorable by Annotation )

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

142
                $newRules = array_merge(/** @scrutinizer ignore-type */ $globalRules[$tool], $rules);
Loading history...
Bug introduced by
$rules of type string is incompatible with the type array|null expected by parameter $array2 of array_merge(). ( Ignorable by Annotation )

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

142
                $newRules = array_merge($globalRules[$tool], /** @scrutinizer ignore-type */ $rules);
Loading history...
143
            }
144
145
            // Regex should be merged, not overridden.
146 14
            if (isset($rules['regex']) && isset($globalRules[$tool]['regex'])) {
147 1
                $newRules['regex'] = implode('|', [
148 1
                    $rules['regex'],
149 1
                    $globalRules[$tool]['regex'],
150
                ]);
151
            }
152
153 14
            $tools[$tool] = $newRules;
154
        }
155
156 14
        return $tools;
157
    }
158
159
    /**
160
     * Get only tools that are used to revert edits.
161
     * Revert detection happens only by testing against a regular expression, and not by checking tags.
162
     * @param Project $project
163
     * @return string[][] Each tool with the tool name as the key,
164
     *   and 'link' and 'regex' as the subarray keys.
165
     */
166 7
    public function getRevertTools(Project $project): array
167
    {
168 7
        $projectDomain = $project->getDomain();
169
170 7
        if (isset($this->revertTools[$projectDomain])) {
171 7
            return $this->revertTools[$projectDomain];
172
        }
173
174 7
        $revertEntries = array_filter(
175 7
            $this->getTools($project),
176 7
            function ($tool) {
177 7
                return isset($tool['revert']) && isset($tool['regex']);
178 7
            }
179
        );
180
181
        // If 'revert' is set to `true`, then use 'regex' as the regular expression,
182
        //  otherwise 'revert' is assumed to be the regex string.
183 7
        $this->revertTools[$projectDomain] = array_map(function ($revertTool) {
184
            return [
185 7
                'link' => $revertTool['link'],
186 7
                'regex' => true === $revertTool['revert'] ? $revertTool['regex'] : $revertTool['revert'],
187
            ];
188 7
        }, $revertEntries);
189
190 7
        return $this->revertTools[$projectDomain];
191
    }
192
193
    /**
194
     * Was the edit a revert, based on the edit summary?
195
     * This only works for tools defined with regular expressions, not tags.
196
     * @param string|null $summary Edit summary. Can be null for instance for suppressed edits.
197
     * @param Project $project
198
     * @return bool
199
     */
200 7
    public function isRevert(?string $summary, Project $project): bool
201
    {
202 7
        foreach (array_values($this->getRevertTools($project)) as $values) {
203 7
            if (preg_match('/'.$values['regex'].'/', (string)$summary)) {
204 7
                return true;
205
            }
206
        }
207
208 7
        return false;
209
    }
210
}
211