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(); |
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); |
|
|
|
|
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); |
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
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.