Test Failed
Pull Request — master (#374)
by MusikAnimal
07:12
created

AutomatedEditsHelper::getTools()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 60
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 8.4065

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
ccs 22
cts 27
cp 0.8148
crap 8.4065
rs 8.1954

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

202
                $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

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