Completed
Pull Request — master (#112)
by David
20:07 queued 10:11
created

Configuration::createExtraPluginsNode()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 41
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 41
ccs 37
cts 37
cp 1
rs 8.5806
cc 4
eloc 33
nc 1
nop 0
crap 4
1
<?php
2
3
namespace Http\HttplugBundle\DependencyInjection;
4
5
use Symfony\Component\Config\Definition\ArrayNode;
6
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
7
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
8
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
9
use Symfony\Component\Config\Definition\ConfigurationInterface;
10
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
11
12
/**
13
 * This class contains the configuration information for the bundle.
14
 *
15
 * This information is solely responsible for how the different configuration
16
 * sections are normalized, and merged.
17
 *
18
 * @author David Buchmann <[email protected]>
19
 * @author Tobias Nyholm <[email protected]>
20
 */
21
class Configuration implements ConfigurationInterface
22
{
23
    /**
24
     * Whether to use the debug mode.
25
     *
26
     * @see https://github.com/doctrine/DoctrineBundle/blob/v1.5.2/DependencyInjection/Configuration.php#L31-L41
27
     *
28
     * @var bool
29
     */
30
    private $debug;
31
32
    /**
33
     * @param bool $debug
34 12
     */
35
    public function __construct($debug)
36 12
    {
37 12
        $this->debug = (bool) $debug;
38
    }
39
40
    /**
41
     * {@inheritdoc}
42 12
     */
43
    public function getConfigTreeBuilder()
44 12
    {
45 12
        $treeBuilder = new TreeBuilder();
46
        $rootNode = $treeBuilder->root('httplug');
47 12
48 12
        $this->configureClients($rootNode);
49
        $this->configurePlugins($rootNode);
50
51 12
        $rootNode
52
            ->validate()
53 11
                ->ifTrue(function ($v) {
54 11
                    return !empty($v['classes']['client'])
55 8
                        || !empty($v['classes']['message_factory'])
56 11
                        || !empty($v['classes']['uri_factory'])
57 12
                        || !empty($v['classes']['stream_factory']);
58
                })
59 3
                ->then(function ($v) {
60 3
                    foreach ($v['classes'] as $key => $class) {
61 1
                        if (null !== $class && !class_exists($class)) {
62 1
                            throw new InvalidConfigurationException(sprintf(
63 1
                                'Class %s specified for httplug.classes.%s does not exist.',
64
                                $class,
65 1
                                $key
66
                            ));
67 2
                        }
68
                    }
69 2
70 12
                    return $v;
71 12
                })
72 12
            ->end()
73 12
            ->beforeNormalization()
74 12
                ->ifTrue(function ($v) {
75 12
                    return is_array($v) && array_key_exists('toolbar', $v) && is_array($v['toolbar']);
76 12
                })
77 12
                ->then(function ($v) {
78 12
                    if (array_key_exists('profiling', $v)) {
79 12
                        throw new InvalidConfigurationException('Can\'t configure both "toolbar" and "profiling" section. The "toolbar" config is deprecated as of version 1.3.0, please only use "profiling".');
80 12
                    }
81 12
82 12
                    @trigger_error('"httplug.toolbar" config is deprecated since version 1.3 and will be removed in 2.0. Use "httplug.profiling" instead.', E_USER_DEPRECATED);
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...
83 12
84 12
                    if (array_key_exists('enabled', $v['toolbar']) && 'auto' === $v['toolbar']['enabled']) {
85 12
                        @trigger_error('"auto" value in "httplug.toolbar" config is deprecated since version 1.3 and will be removed in 2.0. Use a boolean value instead.', E_USER_DEPRECATED);
1 ignored issue
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...
86 12
                        $v['toolbar']['enabled'] = $this->debug;
87 12
                    }
88 12
89 12
                    $v['profiling'] = $v['toolbar'];
90 12
91 12
                    unset($v['toolbar']);
92 12
93 12
                    return $v;
94 12
                })
95 12
            ->end()
96 12
            ->fixXmlConfig('client')
97 12
            ->children()
98 12
                ->arrayNode('main_alias')
99 12
                    ->addDefaultsIfNotSet()
100
                    ->info('Configure which service the main alias point to.')
101 1
                    ->children()
102 12
                        ->scalarNode('client')->defaultValue('httplug.client.default')->end()
103 12
                        ->scalarNode('message_factory')->defaultValue('httplug.message_factory.default')->end()
104 12
                        ->scalarNode('uri_factory')->defaultValue('httplug.uri_factory.default')->end()
105 12
                        ->scalarNode('stream_factory')->defaultValue('httplug.stream_factory.default')->end()
106 12
                    ->end()
107 12
                ->end()
108 12
                ->arrayNode('classes')
109 12
                    ->addDefaultsIfNotSet()
110 12
                    ->info('Overwrite a service class instead of using the discovery mechanism.')
111 12
                    ->children()
112 12
                        ->scalarNode('client')->defaultNull()->end()
113 12
                        ->scalarNode('message_factory')->defaultNull()->end()
114 12
                        ->scalarNode('uri_factory')->defaultNull()->end()
115 12
                        ->scalarNode('stream_factory')->defaultNull()->end()
116 12
                    ->end()
117 12
                ->end()
118 12
                ->arrayNode('profiling')
119 12
                    ->addDefaultsIfNotSet()
120 12
                    ->treatFalseLike(['enabled' => false])
121 12
                    ->treatTrueLike(['enabled' => true])
122 12
                    ->treatNullLike(['enabled' => $this->debug])
123 12
                    ->info('Extend the debug profiler with information about requests.')
124 12
                    ->children()
125 12
                        ->booleanNode('enabled')
126 12
                            ->info('Turn the toolbar on or off. Defaults to kernel debug mode.')
127 12
                            ->defaultValue($this->debug)
128 12
                        ->end()
129
                        ->scalarNode('formatter')->defaultNull()->end()
130 12
                        ->integerNode('captured_body_length')
131
                            ->defaultValue(0)
132
                            ->info('Limit long HTTP message bodies to x characters. If set to 0 we do not read the message body. Only available with the default formatter (FullHttpMessageFormatter).')
133 12
                        ->end()
134
                    ->end()
135 12
                ->end()
136 12
                ->arrayNode('discovery')
137 12
                    ->addDefaultsIfNotSet()
138
                    ->info('Control what clients should be found by the discovery.')
139 3
                    ->children()
140
                        ->scalarNode('client')
141 3
                            ->defaultValue('auto')
142
                            ->info('Set to "auto" to see auto discovered client in the web profiler. If provided a service id for a client then this client will be found by auto discovery.')
143
                        ->end()
144
                        ->scalarNode('async_client')
145 12
                            ->defaultNull()
146 12
                            ->info('Set to "auto" to see auto discovered client in the web profiler. If provided a service id for a client then this client will be found by auto discovery.')
147 12
                        ->end()
148 12
                    ->end()
149 12
                ->end()
150 12
            ->end();
151 12
152 12
        return $treeBuilder;
153 12
    }
154 12
155 12
    private function configureClients(ArrayNodeDefinition $root)
156 12
    {
157 12
        $root->children()
158 12
            ->arrayNode('clients')
159 12
                ->validate()
160 12
                    ->ifTrue(function ($clients) {
161 12
                        foreach ($clients as $name => $config) {
162 12
                            // Make sure we only allow one of these to be true
163 12
                            return (bool) $config['flexible_client'] + (bool) $config['http_methods_client'] + (bool) $config['batch_client'] >= 2;
164 12
                        }
165 12
166 12
                        return false;
167 12
                    })
168 12
                    ->thenInvalid('A http client can\'t be decorated with both FlexibleHttpClient and HttpMethodsClient. Only one of the following options can be true. ("flexible_client", "http_methods_client")')->end()
169 12
                ->useAttributeAsKey('name')
170 12
                ->prototype('array')
171 12
                ->children()
172 12
                    ->scalarNode('factory')
173 12
                        ->isRequired()
174 12
                        ->cannotBeEmpty()
175
                        ->info('The service id of a factory to use when creating the adapter.')
176
                    ->end()
177
                    ->booleanNode('flexible_client')
178
                        ->defaultFalse()
179 12
                        ->info('Set to true to get the client wrapped in a FlexibleHttpClient which emulates async or sync behavior.')
180
                    ->end()
181 12
                    ->booleanNode('http_methods_client')
182 12
                        ->defaultFalse()
183 12
                        ->info('Set to true to get the client wrapped in a HttpMethodsClient which emulates provides functions for HTTP verbs.')
184 12
                    ->end()
185 12
                    ->booleanNode('batch_client')
186
                        ->defaultFalse()
187 12
                        ->info('Set to true to get the client wrapped in a BatchClient which allows you to send multiple request at the same time.')
188 12
                    ->end()
189 12
                    ->arrayNode('plugins')
190 12
                        ->info('A list of service ids of plugins. The order is important.')
191 12
                        ->prototype('scalar')->end()
192 12
                    ->end()
193 12
                    ->variableNode('config')->defaultValue([])->end()
194 12
                    ->append($this->createExtraPluginsNode())
195 12
                ->end()
196 12
            ->end();
197 12
    }
198 12
199 12
    /**
200 12
     * @param ArrayNodeDefinition $root
201 12
     */
202 12
    private function configurePlugins(ArrayNodeDefinition $root)
203 12
    {
204 12
        $pluginsNode = $root
205 12
            ->children()
206 12
                ->arrayNode('plugins')
207 12
                ->addDefaultsIfNotSet()
208 12
        ;
209 12
        $this->configureSharedPluginNodes($pluginsNode);
210
    }
211 12
212 12
    /**
213 12
     * Create configuration for the extra_plugins node inside the client.
214 12
     *
215 12
     * @return NodeDefinition Definition of the extra_plugins node in the client.
216 12
     */
217 12
    private function createExtraPluginsNode()
218 12
    {
219 12
        $builder = new TreeBuilder();
220 12
        $node = $builder->root('extra_plugins');
221
        $node->validate()
222 12
            ->always(function ($plugins) {
223 12
                if (!count($plugins['authentication'])) {
224 12
                    unset($plugins['authentication']);
225 12
                }
226 12
                foreach ($plugins as $name => $definition) {
227 12
                    if (!$definition['enabled']) {
228 12
                        unset($plugins[$name]);
229
                    }
230 12
                }
231 12
                return $plugins;
232 12
            })
233 12
        ;
234 12
        $this->configureSharedPluginNodes($node, true);
235 12
        $node
236 12
            ->children()
237 12
                ->arrayNode('add_host')
238 12
                    ->canBeEnabled()
239 12
                    ->addDefaultsIfNotSet()
240
                    ->info('Configure the AddHostPlugin for this client.')
241 12
                    ->children()
242 12
                        ->scalarNode('host')
243 12
                            ->info('Host name including protocol and optionally the port number, e.g. https://api.local:8000')
244 12
                            ->isRequired()
245 12
                            ->cannotBeEmpty()
246 12
                        ->end()
247 12
                        ->scalarNode('replace')
248 12
                            ->info('Whether to replace the host if request already specifies it')
249 12
                            ->defaultValue(false)
250 12
                        ->end()
251 12
                    ->end()
252 12
                ->end()
253 12
            ->end()
254 12
        ->end();
255 12
256
        return $node;
257 12
    }
258 12
259 12
    /**
260 12
     * @param ArrayNodeDefinition $pluginNode
261 12
     * @param bool                $disableAll Some shared plugins are enabled by default. On the client, all are disabled by default.
262 12
     */
263 12
    private function configureSharedPluginNodes(ArrayNodeDefinition $pluginNode, $disableAll = false)
264 12
    {
265
        $children = $pluginNode->children();
266 12
267 12
        $children->append($this->createAuthenticationPluginNode());
268 12
269 12
        $children->arrayNode('cache')
270 12
            ->canBeEnabled()
271 12
            ->addDefaultsIfNotSet()
272 12
                ->children()
273
                    ->scalarNode('cache_pool')
274 12
                        ->info('This must be a service id to a service implementing Psr\Cache\CacheItemPoolInterface')
275 12
                        ->isRequired()
276 12
                        ->cannotBeEmpty()
277 12
                    ->end()
278 12
                    ->scalarNode('stream_factory')
279 12
                        ->info('This must be a service id to a service implementing Http\Message\StreamFactory')
280 12
                        ->defaultValue('httplug.stream_factory')
281 12
                        ->cannotBeEmpty()
282 12
                    ->end()
283 12
                    ->arrayNode('config')
284 12
                        ->addDefaultsIfNotSet()
285
                        ->children()
286 12
                            ->scalarNode('default_ttl')->defaultNull()->end()
287 12
                            ->scalarNode('respect_cache_headers')->defaultTrue()->end()
288 12
                        ->end()
289 12
                    ->end()
290
                ->end()
291
            ->end();
292
        // End cache plugin
293
294
        $children->arrayNode('cookie')
295
            ->canBeEnabled()
296 12
                ->children()
297
                    ->scalarNode('cookie_jar')
298 12
                        ->info('This must be a service id to a service implementing Http\Message\CookieJar')
299 12
                        ->isRequired()
300
                        ->cannotBeEmpty()
301 12
                    ->end()
302 12
                ->end()
303 12
            ->end();
304 12
        // End cookie plugin
305 12
306 2
        $decoder = $children->arrayNode('decoder');
307 2
        if ($disableAll) {
308 1
            $decoder->canBeEnabled();
309 1
        } else {
310 2
            $decoder->canBeDisabled();
311 1
        }
312 1
        $decoder->addDefaultsIfNotSet()
313 2
            ->children()
314 2
                ->scalarNode('use_content_encoding')->defaultTrue()->end()
315 1
            ->end()
316 1
        ->end();
317 1
        // End decoder plugin
318 1
319 1
        $children->arrayNode('history')
320
            ->canBeEnabled()
321 1
                ->children()
322 12
                    ->scalarNode('journal')
323 12
                        ->info('This must be a service id to a service implementing Http\Client\Plugin\Journal')
324 12
                        ->isRequired()
325 12
                        ->cannotBeEmpty()
326 12
                    ->end()
327 12
                ->end()
328 12
            ->end();
329 12
        // End history plugin
330 12
331 12
        $logger = $children->arrayNode('logger');
332 12
        if ($disableAll) {
333 12
            $logger->canBeEnabled();
334 12
        } else {
335 12
            $logger->canBeDisabled();
336 12
        }
337
        $logger->addDefaultsIfNotSet()
338 12
            ->children()
339
                ->scalarNode('logger')
340
                    ->info('This must be a service id to a service implementing Psr\Log\LoggerInterface')
341
                    ->defaultValue('logger')
342
                    ->cannotBeEmpty()
343
                ->end()
344
                ->scalarNode('formatter')
345
                    ->info('This must be a service id to a service implementing Http\Message\Formatter')
346
                    ->defaultNull()
347
                ->end()
348
            ->end()
349
        ->end();
350 2
        // End logger plugin
351
352 2
        $redirect = $children->arrayNode('redirect');
353 2
        if ($disableAll) {
354 2
            $redirect->canBeEnabled();
355 2
        } else {
356
            $redirect->canBeDisabled();
357 2
        }
358 1
        $redirect->addDefaultsIfNotSet()
359
            ->children()
360
                ->scalarNode('preserve_header')->defaultTrue()->end()
361 1
                ->scalarNode('use_default_for_multiple')->defaultTrue()->end()
362 1
            ->end()
363 1
        ->end();
364 1
        // End redirect plugin
365 1
366 1
        $retry = $children->arrayNode('retry');
367
        if ($disableAll) {
368
            $retry->canBeEnabled();
369
        } else {
370
            $retry->canBeDisabled();
371
        }
372
        $retry->addDefaultsIfNotSet()
373
            ->children()
374
                ->scalarNode('retry')->defaultValue(1)->end() // TODO: should be called retries for consistency with the class
375
            ->end()
376
        ->end();
377
        // End retry plugin
378
379
        $stopwatch = $children->arrayNode('stopwatch');
380
        if ($disableAll) {
381
            $stopwatch->canBeEnabled();
382
        } else {
383
            $stopwatch->canBeDisabled();
384
        }
385
        $stopwatch->addDefaultsIfNotSet()
386
            ->children()
387
                ->scalarNode('stopwatch')
388
                    ->info('This must be a service id to a service extending Symfony\Component\Stopwatch\Stopwatch')
389
                    ->defaultValue('debug.stopwatch')
390
                    ->cannotBeEmpty()
391
                ->end()
392
            ->end()
393
        ->end();
394
        // End stopwatch plugin
395
    }
396
397
    /**
398
     * Create configuration for authentication plugin.
399
     *
400
     * @return NodeDefinition Definition for the authentication node in the plugins list.
401
     */
402
    private function createAuthenticationPluginNode()
403
    {
404
        $builder = new TreeBuilder();
405
        $node = $builder->root('authentication');
406
        $node
407
            ->useAttributeAsKey('name')
408
            ->prototype('array')
409
                ->validate()
410
                    ->always()
411
                    ->then(function ($config) {
412
                        switch ($config['type']) {
413
                            case 'basic':
414
                                $this->validateAuthenticationType(['username', 'password'], $config, 'basic');
415
                                break;
416
                            case 'bearer':
417
                                $this->validateAuthenticationType(['token'], $config, 'bearer');
418
                                break;
419
                            case 'service':
420
                                $this->validateAuthenticationType(['service'], $config, 'service');
421
                                break;
422
                            case 'wsse':
423
                                $this->validateAuthenticationType(['username', 'password'], $config, 'wsse');
424
                                break;
425
                        }
426
427
                        return $config;
428
                    })
429
                ->end()
430
                ->children()
431
                    ->enumNode('type')
432
                        ->values(['basic', 'bearer', 'wsse', 'service'])
433
                        ->isRequired()
434
                        ->cannotBeEmpty()
435
                    ->end()
436
                    ->scalarNode('username')->end()
437
                    ->scalarNode('password')->end()
438
                    ->scalarNode('token')->end()
439
                    ->scalarNode('service')->end()
440
                    ->end()
441
                ->end()
442
            ->end(); // End authentication plugin
443
444
        return $node;
445
    }
446
447
    /**
448
     * Validate that the configuration fragment has the specified keys and none other.
449
     *
450
     * @param array  $expected Fields that must exist
451
     * @param array  $actual   Actual configuration hashmap
452
     * @param string $authName Name of authentication method for error messages
453
     *
454
     * @throws InvalidConfigurationException If $actual does not have exactly the keys specified in $expected (plus 'type')
455
     */
456
    private function validateAuthenticationType(array $expected, array $actual, $authName)
457
    {
458
        unset($actual['type']);
459
        $actual = array_keys($actual);
460
        sort($actual);
461
        sort($expected);
462
463
        if ($expected === $actual) {
464
            return;
465
        }
466
467
        throw new InvalidConfigurationException(sprintf(
468
            'Authentication "%s" requires %s but got %s',
469
            $authName,
470
            implode(', ', $expected),
471
            implode(', ', $actual)
472
        ));
473
    }
474
}
475