Passed
Pull Request — master (#373)
by MusikAnimal
11:46
created

AutomatedEditsHelper::getTools()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 60
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 8.7368

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 30
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 60
rs 8.1954
ccs 24
cts 31
cp 0.7742
crap 8.7368

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
 * 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 DateInterval;
12
use Psr\Cache\CacheItemPoolInterface;
13
use Symfony\Component\DependencyInjection\ContainerInterface;
14
15
/**
16
 * Helper class for fetching semi-automated definitions.
17
 */
18
class AutomatedEditsHelper
19
{
20
    /** @var array The list of tools that are considered reverting. */
21
    protected $revertTools = [];
22
23
    /** @var array The list of tool names and their regexes/tags. */
24
    protected $tools = [];
25
26
    /** @var ContainerInterface */
27
    private $container;
28
29
    /** @var CacheItemPoolInterface */
30
    protected $cache;
31
32
    /**
33
     * AutomatedEditsHelper constructor.
34
     * @param ContainerInterface $container
35
     */
36 14
    public function __construct(ContainerInterface $container)
37
    {
38 14
        $this->container = $container;
39 14
        $this->cache = $container->get('cache.app');
40 14
    }
41
42
    /**
43
     * Get the tool that matched the given edit summary.
44
     * This only works for tools defined with regular expressions, not tags.
45
     * @param string $summary Edit summary
46
     * @param Project $project
47
     * @return string[]|false Tool entry including key for 'name', or false if nothing was found
48
     */
49 9
    public function getTool(string $summary, Project $project)
50
    {
51 9
        foreach ($this->getTools($project) as $tool => $values) {
52 9
            if (isset($values['regex']) && preg_match('/'.$values['regex'].'/', $summary)) {
53 9
                return array_merge([
54 9
                    'name' => $tool,
55 9
                ], $values);
56
            }
57
        }
58
59 7
        return false;
60
    }
61
62
    /**
63
     * Was the edit (semi-)automated, based on the edit summary?
64
     * This only works for tools defined with regular expressions, not tags.
65
     * @param string $summary Edit summary
66
     * @param Project $project
67
     * @return bool
68
     */
69 1
    public function isAutomated(string $summary, Project $project): bool
70
    {
71 1
        return (bool)$this->getTool($summary, $project);
72
    }
73
74
    /**
75
     * Fetch the config from https://meta.wikimedia.org/wiki/MediaWiki:XTools-AutoEdits.json
76
     * @param bool $useSandbox Use the sandbox version of the config, located at MediaWiki:XTools-AutoEdits.json/sandbox
77
     * @return array
78
     */
79 14
    public function getConfig(bool $useSandbox = false): array
80
    {
81 14
        $cacheKey = 'autoedits_config';
82 14
        if (!$useSandbox && $this->cache->hasItem($cacheKey)) {
83 13
            return $this->cache->getItem($cacheKey)->get();
84
        }
85
86 1
        $title = 'MediaWiki:XTools-AutoEdits.json' . ($useSandbox ? '/sandbox' : '');
87 1
        $ret = json_decode(file_get_contents(
88 1
            "https://meta.wikimedia.org/w/index.php?action=raw&ctype=application/json&title=$title"
89 1
        ), true);
90
91 1
        if (!$useSandbox) {
92 1
            $cacheItem = $this->cache
93 1
                ->getItem($cacheKey)
94 1
                ->set($ret)
95 1
                ->expiresAfter(new DateInterval('PT20M'));
96 1
            $this->cache->save($cacheItem);
97
        }
98
99 1
        return $ret;
100
    }
101
102
    /**
103
     * Get list of automated tools and their associated info for the given project.
104
     * This defaults to the 'default_project' if entries for the given project are not found.
105
     * @param Project $project
106
     * @param bool $useSandbox Whether to use the /sandbox version for testing (also bypasses caching).
107
     * @return array Each tool with the tool name as the key and 'link', 'regex' and/or 'tag' as the subarray keys.
108
     */
109 14
    public function getTools(Project $project, bool $useSandbox = false): array
110
    {
111 14
        $projectDomain = $project->getDomain();
112
113 14
        if (isset($this->tools[$projectDomain])) {
114 7
            return $this->tools[$projectDomain];
115
        }
116
117
        // Load the semi-automated edit types.
118 14
        $tools = $this->getConfig($useSandbox);
119
120 14
        if (isset($tools[$projectDomain])) {
121 14
            $localRules = $tools[$projectDomain];
122
        } else {
123
            $localRules = [];
124
        }
125
126 14
        $langRules = $tools[$project->getLang()] ?? [];
127
128
        // Per-wiki rules have priority, followed by language-specific and global.
129 14
        $globalWithLangRules = $this->mergeRules($tools['global'], $langRules);
130
131 14
        $this->tools[$projectDomain] = $this->mergeRules(
132 14
            $globalWithLangRules,
133 14
            $localRules
134
        );
135
136
        // Once last walk through for some tidying up and validation.
137 14
        $invalid = [];
138 14
        array_walk($this->tools[$projectDomain], function (&$data, $tool) use (&$invalid): void {
139
            // Populate the 'label' with the tool name, if a label doesn't already exist.
140 14
            $data['label'] = $data['label'] ?? $tool;
141
142
            // 'namespaces' should be an array of ints.
143 14
            $data['namespaces'] = $data['namespaces'] ?? [];
144 14
            if (isset($data['namespace'])) {
145
                $data['namespaces'][] = $data['namespace'];
146
                unset($data['namespace']);
147
            }
148
149
            // 'tags' should be an array of strings.
150 14
            $data['tags'] = $data['tags'] ?? [];
151 14
            if (isset($data['tag'])) {
152
                $data['tags'][] = $data['tag'];
153
                unset($data['tag']);
154
            }
155
156
            // If neither a tag or regex is given, it's invalid.
157 14
            if (empty($data['tags']) && empty($data['regex'])) {
158
                $invalid[] = $tool;
159
            }
160 14
        });
161
162 14
        uksort($this->tools[$projectDomain], 'strcasecmp');
163
164 14
        if ($invalid) {
165
            $this->tools[$projectDomain]['invalid'] = $invalid;
166
        }
167
168 14
        return $this->tools[$projectDomain];
169
    }
170
171
    /**
172
     * Merges the given rule sets, giving priority to the local set. Regex is concatenated, not overridden.
173
     * @param string[] $globalRules The global rule set.
174
     * @param string[] $localRules The rule set for the local wiki.
175
     * @return string[] Merged rules.
176
     */
177 14
    private function mergeRules(array $globalRules, array $localRules): array
178
    {
179
        // Initial set, including just the global rules.
180 14
        $tools = $globalRules;
181
182
        // Loop through local rules and override/merge as necessary.
183 14
        foreach ($localRules as $tool => $rules) {
184 14
            $newRules = $rules;
185
186 14
            if (isset($globalRules[$tool])) {
187
                // Order within array_merge is important, so that local rules get priority.
188 14
                $newRules = array_merge($globalRules[$tool], $rules);
0 ignored issues
show
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

188
                $newRules = array_merge($globalRules[$tool], /** @scrutinizer ignore-type */ $rules);
Loading history...
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

188
                $newRules = array_merge(/** @scrutinizer ignore-type */ $globalRules[$tool], $rules);
Loading history...
189
            }
190
191
            // Regex should be merged, not overridden.
192 14
            if (isset($rules['regex']) && isset($globalRules[$tool]['regex'])) {
193 1
                $newRules['regex'] = implode('|', [
194 1
                    $rules['regex'],
195 1
                    $globalRules[$tool]['regex'],
196
                ]);
197
            }
198
199 14
            $tools[$tool] = $newRules;
200
        }
201
202 14
        return $tools;
203
    }
204
205
    /**
206
     * Get only tools that are used to revert edits.
207
     * Revert detection happens only by testing against a regular expression, and not by checking tags.
208
     * @param Project $project
209
     * @return string[][] Each tool with the tool name as the key,
210
     *   and 'link' and 'regex' as the subarray keys.
211
     */
212 7
    public function getRevertTools(Project $project): array
213
    {
214 7
        $projectDomain = $project->getDomain();
215
216 7
        if (isset($this->revertTools[$projectDomain])) {
217 7
            return $this->revertTools[$projectDomain];
218
        }
219
220 7
        $revertEntries = array_filter(
221 7
            $this->getTools($project),
222 7
            function ($tool) {
223 7
                return isset($tool['revert']) && isset($tool['regex']);
224 7
            }
225
        );
226
227
        // If 'revert' is set to `true`, then use 'regex' as the regular expression,
228
        //  otherwise 'revert' is assumed to be the regex string.
229 7
        $this->revertTools[$projectDomain] = array_map(function ($revertTool) {
230
            return [
231 7
                'link' => $revertTool['link'],
232 7
                'regex' => true === $revertTool['revert'] ? $revertTool['regex'] : $revertTool['revert'],
233
            ];
234 7
        }, $revertEntries);
235
236 7
        return $this->revertTools[$projectDomain];
237
    }
238
239
    /**
240
     * Was the edit a revert, based on the edit summary?
241
     * This only works for tools defined with regular expressions, not tags.
242
     * @param string|null $summary Edit summary. Can be null for instance for suppressed edits.
243
     * @param Project $project
244
     * @return bool
245
     */
246 7
    public function isRevert(?string $summary, Project $project): bool
247
    {
248 7
        foreach (array_values($this->getRevertTools($project)) as $values) {
249 7
            if (preg_match('/'.$values['regex'].'/', (string)$summary)) {
250 7
                return true;
251
            }
252
        }
253
254 7
        return false;
255
    }
256
}
257