Passed
Pull Request — master (#376)
by MusikAnimal
10:04 queued 21s
created

AutomatedEditsHelper::getTools()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 60
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 8.8126

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 23
cts 30
cp 0.7667
crap 8.8126
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
     */
37 14
    public function __construct(ContainerInterface $container)
38
    {
39 14
        $this->container = $container;
40 14
        $this->cache = $container->get('cache.app');
41 14
    }
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
     */
50 9
    public function getTool(string $summary, Project $project)
51
    {
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 9
                ], $values);
57
            }
58
        }
59
60 7
        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
     */
70 1
    public function isAutomated(string $summary, Project $project): bool
71
    {
72 1
        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
     */
80 14
    public function getConfig(bool $useSandbox = false): array
81
    {
82 14
        $cacheKey = 'autoedits_config';
83 14
        if (!$useSandbox && $this->cache->hasItem($cacheKey)) {
84 13
            return $this->cache->getItem($cacheKey)->get();
85
        }
86
87 1
        $session = $this->container->get('session');
88
        $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
            // Request via OAuth to get around server-side caching.
93
            /** @var Client $client */
94
            $client = $this->container->get('session')->get('oauth_client');
95
            $resp = $client->makeOAuthCall(
96
                $this->container->get('session')->get('oauth_access_token'),
97
                $uri
98
            );
99
        } else {
100 1
            $resp = file_get_contents($uri);
101
        }
102
103 1
        $ret = json_decode($resp, true);
104
105 1
        if (!$useSandbox) {
106 1
            $cacheItem = $this->cache
107 1
                ->getItem($cacheKey)
108 1
                ->set($ret)
109 1
                ->expiresAfter(new DateInterval('PT20M'));
110 1
            $this->cache->save($cacheItem);
111
        }
112
113 1
        return $ret;
114
    }
115
116
    /**
117
     * Get list of automated tools and their associated info for the given project.
118
     * This defaults to the 'default_project' if entries for the given project are not found.
119
     * @param Project $project
120
     * @param bool $useSandbox Whether to use the /sandbox version for testing (also bypasses caching).
121
     * @return array Each tool with the tool name as the key and 'link', 'regex' and/or 'tag' as the subarray keys.
122
     */
123 14
    public function getTools(Project $project, bool $useSandbox = false): array
124
    {
125 14
        $projectDomain = $project->getDomain();
126
127 14
        if (isset($this->tools[$projectDomain])) {
128 7
            return $this->tools[$projectDomain];
129
        }
130
131
        // Load the semi-automated edit types.
132 14
        $tools = $this->getConfig($useSandbox);
133
134 14
        if (isset($tools[$projectDomain])) {
135 14
            $localRules = $tools[$projectDomain];
136
        } else {
137
            $localRules = [];
138
        }
139
140 14
        $langRules = $tools[$project->getLang()] ?? [];
141
142
        // Per-wiki rules have priority, followed by language-specific and global.
143 14
        $globalWithLangRules = $this->mergeRules($tools['global'], $langRules);
144
145 14
        $this->tools[$projectDomain] = $this->mergeRules(
146 14
            $globalWithLangRules,
147 14
            $localRules
148
        );
149
150
        // Once last walk through for some tidying up and validation.
151 14
        $invalid = [];
152
        array_walk($this->tools[$projectDomain], function (&$data, $tool) use (&$invalid): void {
153
            // Populate the 'label' with the tool name, if a label doesn't already exist.
154 14
            $data['label'] = $data['label'] ?? $tool;
155
156
            // 'namespaces' should be an array of ints.
157 14
            $data['namespaces'] = $data['namespaces'] ?? [];
158 14
            if (isset($data['namespace'])) {
159
                $data['namespaces'][] = $data['namespace'];
160
                unset($data['namespace']);
161
            }
162
163
            // 'tags' should be an array of strings.
164 14
            $data['tags'] = $data['tags'] ?? [];
165 14
            if (isset($data['tag'])) {
166
                $data['tags'][] = $data['tag'];
167
                unset($data['tag']);
168
            }
169
170
            // If neither a tag or regex is given, it's invalid.
171 14
            if (empty($data['tags']) && empty($data['regex'])) {
172
                $invalid[] = $tool;
173
            }
174 14
        });
175
176 14
        uksort($this->tools[$projectDomain], 'strcasecmp');
177
178 14
        if ($invalid) {
179
            $this->tools[$projectDomain]['invalid'] = $invalid;
180
        }
181
182 14
        return $this->tools[$projectDomain];
183
    }
184
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
     * @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 14
        $tools = $globalRules;
195
196
        // Loop through local rules and override/merge as necessary.
197 14
        foreach ($localRules as $tool => $rules) {
198 14
            $newRules = $rules;
199
200 14
            if (isset($globalRules[$tool])) {
201
                // Order within array_merge is important, so that local rules get priority.
202 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 $arrays 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...
203
            }
204
205
            // Regex should be merged, not overridden.
206 14
            if (isset($rules['regex']) && isset($globalRules[$tool]['regex'])) {
207 1
                $newRules['regex'] = implode('|', [
208 1
                    $rules['regex'],
209 1
                    $globalRules[$tool]['regex'],
210
                ]);
211
            }
212
213 14
            $tools[$tool] = $newRules;
214
        }
215
216 14
        return $tools;
217
    }
218
219
    /**
220
     * Get only tools that are used to revert edits.
221
     * Revert detection happens only by testing against a regular expression, and not by checking tags.
222
     * @param Project $project
223
     * @return string[][] Each tool with the tool name as the key,
224
     *   and 'link' and 'regex' as the subarray keys.
225
     */
226 7
    public function getRevertTools(Project $project): array
227
    {
228 7
        $projectDomain = $project->getDomain();
229
230 7
        if (isset($this->revertTools[$projectDomain])) {
231 7
            return $this->revertTools[$projectDomain];
232
        }
233
234 7
        $revertEntries = array_filter(
235 7
            $this->getTools($project),
236
            function ($tool) {
237 7
                return isset($tool['revert']) && isset($tool['regex']);
238 7
            }
239
        );
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
        $this->revertTools[$projectDomain] = array_map(function ($revertTool) {
244
            return [
245 7
                'link' => $revertTool['link'],
246 7
                'regex' => true === $revertTool['revert'] ? $revertTool['regex'] : $revertTool['revert'],
247
            ];
248 7
        }, $revertEntries);
249
250 7
        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 7
    public function isRevert(?string $summary, Project $project): bool
261
    {
262 7
        foreach (array_values($this->getRevertTools($project)) as $values) {
263 7
            if (preg_match('/'.$values['regex'].'/', (string)$summary)) {
264 7
                return true;
265
            }
266
        }
267
268 7
        return false;
269
    }
270
}
271