Completed
Push — master ( aeb527...c7e150 )
by Peter
02:22 queued 13s
created

Configuration::validateDatabases()   B

Complexity

Conditions 9
Paths 1

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 18
cts 18
cp 1
rs 8.0555
c 0
b 0
f 0
cc 9
nc 1
nop 1
crap 9
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * GpsLab component.
6
 *
7
 * @author    Peter Gribanov <[email protected]>
8
 * @copyright Copyright (c) 2017, Peter Gribanov
9
 * @license   http://opensource.org/licenses/MIT
10
 */
11
12
namespace GpsLab\Bundle\GeoIP2Bundle\DependencyInjection;
13
14
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
15
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
16
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
17
use Symfony\Component\Config\Definition\ConfigurationInterface;
18
19
class Configuration implements ConfigurationInterface
20
{
21
    private const URL = 'https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz';
22
23
    private const PATH = '%s/%s.mmdb';
24
25
    private const LICENSE_DIRTY_HACK = 'YOUR-LICENSE-KEY';
26
27
    private const DATABASE_EDITION_IDS = [
28
        'GeoLite2-ASN',
29
        'GeoLite2-City',
30
        'GeoLite2-Country',
31
        'GeoIP2-City',
32
        'GeoIP2-Country',
33
        'GeoIP2-Anonymous-IP',
34
        'GeoIP2-Domain',
35
        'GeoIP2-ISP',
36
    ];
37
38
    /**
39
     * @var string
40
     */
41
    private $cache_dir;
42
43
    /**
44
     * @param string|null $cache_dir
45
     */
46 96
    public function __construct(?string $cache_dir)
47
    {
48 96
        $this->cache_dir = $cache_dir ?: sys_get_temp_dir();
49 96
    }
50
51
    /**
52
     * @return TreeBuilder
53
     */
54 96
    public function getConfigTreeBuilder(): TreeBuilder
55
    {
56 96
        $tree_builder = $this->createTreeBuilder('gpslab_geoip');
57 96
        $root_node = $this->getRootNode($tree_builder, 'gpslab_geoip');
58
59 96
        $this->normalizeDefaultDatabase($root_node);
60 96
        $this->normalizeRootConfigurationToDefaultDatabase($root_node);
61 96
        $this->normalizeLicenseDirtyHack($root_node);
62 96
        $this->validateAvailableDefaultDatabase($root_node);
63 96
        $this->allowGlobalLicense($root_node);
64 96
        $this->allowGlobalLocales($root_node);
65 96
        $this->validateDatabases($root_node);
66
67 96
        $root_node->fixXmlConfig('locale');
68 96
        $locales = $root_node->children()->arrayNode('locales');
69 96
        $locales->prototype('scalar');
70 96
        $locales->treatNullLike([]);
71 96
        $locales->defaultValue(['en']);
72
73 96
        $root_node->children()->scalarNode('license');
74
75 96
        $default_database = $root_node->children()->scalarNode('default_database');
76 96
        $default_database->defaultValue('default');
77
78 96
        $root_node->fixXmlConfig('database');
79 96
        $root_node->append($this->getDatabaseNode());
80
81 96
        return $tree_builder;
82
    }
83
84
    /**
85
     * @return ArrayNodeDefinition
86
     */
87 96
    private function getDatabaseNode(): ArrayNodeDefinition
88
    {
89 96
        $tree_builder = $this->createTreeBuilder('databases');
90 96
        $root_node = $this->getRootNode($tree_builder, 'databases');
91 96
        $root_node->useAttributeAsKey('name');
92
93 96
        $database_node = $this->arrayPrototype($root_node);
94
95 96
        $this->normalizeUrl($database_node);
96 96
        $this->normalizePath($database_node);
97
98 96
        $url = $database_node->children()->scalarNode('url');
99 96
        $url->isRequired();
100
101 96
        $this->validateURL($url);
102
103 96
        $path = $database_node->children()->scalarNode('path');
104 96
        $path->isRequired();
105
106 96
        $database_node->fixXmlConfig('locale');
107 96
        $locales = $database_node->children()->arrayNode('locales');
108 96
        $locales->prototype('scalar');
109 96
        $locales->treatNullLike([]);
110 96
        $locales->defaultValue(['en']);
111
112 96
        $database_node->children()->scalarNode('license');
113
114 96
        $database_node->children()->enumNode('edition')->values(self::DATABASE_EDITION_IDS);
115
116 96
        return $root_node;
117
    }
118
119
    /**
120
     * @param string $name
121
     *
122
     * @return TreeBuilder
123
     */
124 96
    private function createTreeBuilder(string $name): TreeBuilder
125
    {
126
        // Symfony 4.2 +
127 96
        if (method_exists(TreeBuilder::class, '__construct')) {
128
            return new TreeBuilder($name);
129
        }
130
131
        // Symfony 4.1 and below
132 96
        return new TreeBuilder();
0 ignored issues
show
Bug introduced by
The call to TreeBuilder::__construct() misses a required argument $name.

This check looks for function calls that miss required arguments.

Loading history...
133
    }
134
135
    /**
136
     * @param TreeBuilder $tree_builder
137
     * @param string      $name
138
     *
139
     * @return ArrayNodeDefinition
140
     */
141 96
    private function getRootNode(TreeBuilder $tree_builder, string $name): ArrayNodeDefinition
142
    {
143 96
        if (method_exists($tree_builder, 'getRootNode')) {
144
            // Symfony 4.2 +
145
            $root = $tree_builder->getRootNode();
146
        } else {
147
            // Symfony 4.1 and below
148 96
            $root = $tree_builder->root($name);
0 ignored issues
show
Bug introduced by
The method root() does not seem to exist on object<Symfony\Component...on\Builder\TreeBuilder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
149
        }
150
151
        // @codeCoverageIgnoreStart
152
        if (!($root instanceof ArrayNodeDefinition)) { // should be always false
153
            throw new \RuntimeException(sprintf('The root node should be instance of %s, got %s instead.', ArrayNodeDefinition::class, get_class($root)));
154
        }
155
        // @codeCoverageIgnoreEnd
156
157 96
        return $root;
158
    }
159
160
    /**
161
     * @param ArrayNodeDefinition $root_node
162
     *
163
     * @return ArrayNodeDefinition
164
     */
165 96
    private function arrayPrototype(ArrayNodeDefinition $root_node): ArrayNodeDefinition
166
    {
167
        // Symfony 3.3 +
168 96
        if (method_exists($root_node, 'arrayPrototype')) {
169
            return $root_node->arrayPrototype();
170
        }
171
172
        // Symfony 3.2 and below
173 96
        $node = $root_node->prototype('array');
174
175
        // @codeCoverageIgnoreStart
176
        if (!($node instanceof ArrayNodeDefinition)) { // should be always false
177
            throw new \RuntimeException(sprintf('The "array" prototype should be instance of %s, got %s instead.', ArrayNodeDefinition::class, get_class($node)));
178
        }
179
        // @codeCoverageIgnoreEnd
180
181 96
        return $node;
182
    }
183
184
    /**
185
     * Normalize default_database from databases.
186
     *
187
     * @param NodeDefinition $root_node
188
     */
189 96
    private function normalizeDefaultDatabase(NodeDefinition $root_node): void
190
    {
191
        $root_node
192 96
            ->beforeNormalization()
193
            ->ifTrue(static function ($v): bool {
194
                return
195 91
                    is_array($v) &&
196 91
                    !array_key_exists('default_database', $v) &&
197 91
                    !empty($v['databases']) &&
198 91
                    is_array($v['databases']);
199 96
            })
200
            ->then(static function (array $v): array {
201 36
                $keys = array_keys($v['databases']);
202 36
                $v['default_database'] = reset($keys);
203
204 36
                return $v;
205 96
            });
206 96
    }
207
208
    /**
209
     * Normalize databases root configuration to default_database.
210
     *
211
     * @param NodeDefinition $root_node
212
     */
213 96
    private function normalizeRootConfigurationToDefaultDatabase(NodeDefinition $root_node): void
214
    {
215
        $root_node
216 96
            ->beforeNormalization()
217
            ->ifTrue(static function ($v): bool {
218 91
                return $v && is_array($v) && !array_key_exists('databases', $v) && !array_key_exists('database', $v);
219 96
            })
220
            ->then(static function (array $v): array {
221 36
                $database = $v;
222 36
                unset($database['default_database']);
223 36
                $default_database = isset($v['default_database']) ? (string) $v['default_database'] : 'default';
224
225
                return [
226 36
                    'default_database' => $default_database,
227
                    'databases' => [
228 36
                        $default_database => $database,
229
                    ],
230
                ];
231 96
            });
232 96
    }
233
234
    /**
235
     * Dirty hack for Symfony Flex.
236
     *
237
     * @see https://github.com/symfony/recipes-contrib/pull/837
238
     *
239
     * @param NodeDefinition $root_node
240
     */
241 96
    private function normalizeLicenseDirtyHack(NodeDefinition $root_node): void
242
    {
243
        $root_node
244 96
            ->beforeNormalization()
245
            ->ifTrue(static function ($v): bool {
246 91
                return $v && is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
247 96
            })
248
            ->then(static function (array $v): array {
249 85
                foreach ($v['databases'] as $name => $database) {
250 81
                    if (isset($database['license']) && $database['license'] === self::LICENSE_DIRTY_HACK) {
251 4
                        unset($v['databases'][$name]);
252 81
                        @trigger_error(sprintf('License for downloaded database "%s" is not specified.', $name), E_USER_WARNING);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
253
                    }
254
                }
255
256 85
                return $v;
257 96
            });
258 96
    }
259
260
    /**
261
     * Validate that the default_database exists in the list of databases.
262
     *
263
     * @param NodeDefinition $root_node
264
     */
265 96
    private function validateAvailableDefaultDatabase(NodeDefinition $root_node): void
266
    {
267
        $root_node
268 96
            ->validate()
269
            ->ifTrue(static function ($v): bool {
270
                return
271 80
                    is_array($v) &&
272 80
                    array_key_exists('default_database', $v) &&
273 80
                    !empty($v['databases']) &&
274 80
                    !array_key_exists($v['default_database'], $v['databases']);
275 96
            })
276
            ->then(static function (array $v): array {
277 4
                $databases = implode('", "', array_keys($v['databases']));
278
279 4
                throw new \InvalidArgumentException(sprintf('Undefined default database "%s". Available "%s" databases.', $v['default_database'], $databases));
280 96
            });
281 96
    }
282
283
    /**
284
     * Add a license option to the databases configuration if it does not exist.
285
     * Allow use a global license for all databases.
286
     *
287
     * @param NodeDefinition $root_node
288
     */
289 96
    private function allowGlobalLicense(NodeDefinition $root_node): void
290
    {
291
        $root_node
292 96
            ->beforeNormalization()
293
            ->ifTrue(static function ($v): bool {
294
                return
295 91
                    is_array($v) &&
296 91
                    array_key_exists('license', $v) &&
297 91
                    array_key_exists('databases', $v) &&
298 91
                    is_array($v['databases']);
299 96
            })
300
            ->then(static function (array $v): array {
301 20
                foreach ($v['databases'] as $name => $database) {
302 18
                    if (!array_key_exists('license', $database)) {
303 18
                        $v['databases'][$name]['license'] = $v['license'];
304
                    }
305
                }
306
307 20
                return $v;
308 96
            });
309 96
    }
310
311
    /**
312
     * Add a locales option to the databases configuration if it does not exist.
313
     * Allow use a global locales for all databases.
314
     *
315
     * @param NodeDefinition $root_node
316
     */
317 96
    private function allowGlobalLocales(NodeDefinition $root_node): void
318
    {
319
        $root_node
320 96
            ->beforeNormalization()
321
            ->ifTrue(static function ($v): bool {
322
                return
323 91
                    is_array($v) &&
324 91
                    array_key_exists('locales', $v) &&
325 91
                    array_key_exists('databases', $v) &&
326 91
                    is_array($v['databases']);
327 96
            })
328
            ->then(static function (array $v): array {
329 2
                foreach ($v['databases'] as $name => $database) {
330 2
                    if (!array_key_exists('locales', $database)) {
331 2
                        $v['databases'][$name]['locales'] = $v['locales'];
332
                    }
333
                }
334
335 2
                return $v;
336 96
            });
337 96
    }
338
339
    /**
340
     * Validate database options.
341
     *
342
     * @param NodeDefinition $root_node
343
     */
344 96
    private function validateDatabases(NodeDefinition $root_node): void
345
    {
346
        $root_node
347 96
            ->validate()
348
            ->ifTrue(static function ($v): bool {
349 76
                return is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
350 96
            })
351
            ->then(static function (array $v): array {
352 76
                foreach ($v['databases'] as $name => $database) {
353 59
                    if (empty($database['license'])) {
354 8
                        throw new \InvalidArgumentException(sprintf('License for downloaded database "%s" is not specified.', $name));
355
                    }
356
357 51
                    if (empty($database['edition'])) {
358 4
                        throw new \InvalidArgumentException(sprintf('Edition of downloaded database "%s" is not selected.', $name));
359
                    }
360
361 47
                    if (empty($database['url'])) {
362 4
                        throw new \InvalidArgumentException(sprintf('URL for download database "%s" is not specified.', $name));
363
                    }
364
365 43
                    if (empty($database['path'])) {
366 4
                        throw new \InvalidArgumentException(sprintf('The destination path to download database "%s" is not specified.', $name));
367
                    }
368
369 39
                    if (empty($database['locales'])) {
370 39
                        throw new \InvalidArgumentException(sprintf('The list of locales for database "%s" should not be empty.', $name));
371
                    }
372
                }
373
374 56
                return $v;
375 96
            });
376 96
    }
377
378
    /**
379
     * Normalize url option from license key and edition id.
380
     *
381
     * @param NodeDefinition $database_node
382
     */
383 96
    private function normalizeUrl(NodeDefinition $database_node): void
384
    {
385
        $database_node
386 96
            ->beforeNormalization()
387
            ->ifTrue(static function ($v): bool {
388
                return
389 77
                    is_array($v) &&
390 77
                    !array_key_exists('url', $v) &&
391 77
                    array_key_exists('license', $v) &&
392 77
                    array_key_exists('edition', $v);
393 96
            })
394
            ->then(static function (array $v): array {
395 45
                $v['url'] = sprintf(self::URL, urlencode($v['edition']), urlencode($v['license']));
396
397 45
                return $v;
398 96
            });
399 96
    }
400
401
    /**
402
     * Normalize path option from edition id.
403
     *
404
     * @param NodeDefinition $database_node
405
     */
406 96
    private function normalizePath(NodeDefinition $database_node): void
407
    {
408
        $database_node
409 96
            ->beforeNormalization()
410
            ->ifTrue(static function ($v): bool {
411 77
                return is_array($v) && !array_key_exists('path', $v) && array_key_exists('edition', $v);
412 96
            })
413
            ->then(function (array $v): array {
414 45
                $v['path'] = sprintf(self::PATH, $this->cache_dir, $v['edition']);
415
416 45
                return $v;
417 96
            });
418 96
    }
419
420
    /**
421
     * The url option must be a valid URL.
422
     *
423
     * @param NodeDefinition $url
424
     */
425 96
    private function validateURL(NodeDefinition $url): void
426
    {
427
        $url
428 96
            ->validate()
429
            ->ifTrue(static function ($v): bool {
430 71
                return is_string($v) && $v && !filter_var($v, FILTER_VALIDATE_URL);
431 96
            })
432
            ->then(static function (string $v): array {
433 2
                throw new \InvalidArgumentException(sprintf('URL "%s" must be valid.', $v));
434 96
            });
435 96
    }
436
}
437