Passed
Push — master ( b2964c...05352e )
by MusikAnimal
11:27
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 0
Metric Value
cc 8
eloc 30
nc 5
nop 2
dl 0
loc 60
rs 8.1954
c 0
b 0
f 0
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 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 14
        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 $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
            }
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 7
            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 7
        $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