Test Failed
Pull Request — master (#372)
by MusikAnimal
07:08
created

AutomatedEditsHelper::getConfig()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

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

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

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