Completed
Pull Request — master (#112)
by David
07:59
created

Configuration::addAuthenticationPluiginNode()   B

Complexity

Conditions 5
Paths 1

Size

Total Lines 44
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 5

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 44
rs 8.439
ccs 27
cts 27
cp 1
cc 5
eloc 38
nc 1
nop 0
crap 5
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\NodeBuilder;
8
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
9
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
10
use Symfony\Component\Config\Definition\ConfigurationInterface;
11
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
12
13
/**
14
 * This class contains the configuration information for the bundle.
15
 *
16
 * This information is solely responsible for how the different configuration
17
 * sections are normalized, and merged.
18
 *
19
 * @author David Buchmann <[email protected]>
20
 * @author Tobias Nyholm <[email protected]>
21
 */
22
class Configuration implements ConfigurationInterface
23
{
24
    /**
25
     * Whether to use the debug mode.
26
     *
27
     * @see https://github.com/doctrine/DoctrineBundle/blob/v1.5.2/DependencyInjection/Configuration.php#L31-L41
28
     *
29
     * @var bool
30
     */
31
    private $debug;
32
33
    /**
34 12
     * @param bool $debug
35
     */
36 12
    public function __construct($debug)
37 12
    {
38
        $this->debug = (bool) $debug;
39
    }
40
41
    /**
42 12
     * {@inheritdoc}
43
     */
44 12
    public function getConfigTreeBuilder()
45 12
    {
46
        $treeBuilder = new TreeBuilder();
47 12
        $rootNode = $treeBuilder->root('httplug');
48 12
49
        $this->configureClients($rootNode);
50
        $this->configurePlugins($rootNode);
51 12
52
        $rootNode
53 11
            ->validate()
54 11
                ->ifTrue(function ($v) {
55 8
                    return !empty($v['classes']['client'])
56 11
                        || !empty($v['classes']['message_factory'])
57 12
                        || !empty($v['classes']['uri_factory'])
58
                        || !empty($v['classes']['stream_factory']);
59 3
                })
60 3
                ->then(function ($v) {
61 1
                    foreach ($v['classes'] as $key => $class) {
62 1
                        if (null !== $class && !class_exists($class)) {
63 1
                            throw new InvalidConfigurationException(sprintf(
64
                                'Class %s specified for httplug.classes.%s does not exist.',
65 1
                                $class,
66
                                $key
67 2
                            ));
68
                        }
69 2
                    }
70 12
71 12
                    return $v;
72 12
                })
73 12
            ->end()
74 12
            ->beforeNormalization()
75 12
                ->ifTrue(function ($v) {
76 12
                    return is_array($v) && array_key_exists('toolbar', $v) && is_array($v['toolbar']);
77 12
                })
78 12
                ->then(function ($v) {
79 12
                    if (array_key_exists('profiling', $v)) {
80 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".');
81 12
                    }
82 12
83 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...
84 12
85 12
                    if (array_key_exists('enabled', $v['toolbar']) && 'auto' === $v['toolbar']['enabled']) {
86 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...
87 12
                        $v['toolbar']['enabled'] = $this->debug;
88 12
                    }
89 12
90 12
                    $v['profiling'] = $v['toolbar'];
91 12
92 12
                    unset($v['toolbar']);
93 12
94 12
                    return $v;
95 12
                })
96 12
            ->end()
97 12
            ->fixXmlConfig('client')
98 12
            ->children()
99 12
                ->arrayNode('main_alias')
100
                    ->addDefaultsIfNotSet()
101 1
                    ->info('Configure which service the main alias point to.')
102 12
                    ->children()
103 12
                        ->scalarNode('client')->defaultValue('httplug.client.default')->end()
104 12
                        ->scalarNode('message_factory')->defaultValue('httplug.message_factory.default')->end()
105 12
                        ->scalarNode('uri_factory')->defaultValue('httplug.uri_factory.default')->end()
106 12
                        ->scalarNode('stream_factory')->defaultValue('httplug.stream_factory.default')->end()
107 12
                    ->end()
108 12
                ->end()
109 12
                ->arrayNode('classes')
110 12
                    ->addDefaultsIfNotSet()
111 12
                    ->info('Overwrite a service class instead of using the discovery mechanism.')
112 12
                    ->children()
113 12
                        ->scalarNode('client')->defaultNull()->end()
114 12
                        ->scalarNode('message_factory')->defaultNull()->end()
115 12
                        ->scalarNode('uri_factory')->defaultNull()->end()
116 12
                        ->scalarNode('stream_factory')->defaultNull()->end()
117 12
                    ->end()
118 12
                ->end()
119 12
                ->arrayNode('profiling')
120 12
                    ->addDefaultsIfNotSet()
121 12
                    ->treatFalseLike(['enabled' => false])
122 12
                    ->treatTrueLike(['enabled' => true])
123 12
                    ->treatNullLike(['enabled' => $this->debug])
124 12
                    ->info('Extend the debug profiler with information about requests.')
125 12
                    ->children()
126 12
                        ->booleanNode('enabled')
127 12
                            ->info('Turn the toolbar on or off. Defaults to kernel debug mode.')
128 12
                            ->defaultValue($this->debug)
129
                        ->end()
130 12
                        ->scalarNode('formatter')->defaultNull()->end()
131
                        ->integerNode('captured_body_length')
132
                            ->defaultValue(0)
133 12
                            ->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).')
134
                        ->end()
135 12
                    ->end()
136 12
                ->end()
137 12
                ->arrayNode('discovery')
138
                    ->addDefaultsIfNotSet()
139 3
                    ->info('Control what clients should be found by the discovery.')
140
                    ->children()
141 3
                        ->scalarNode('client')
142
                            ->defaultValue('auto')
143
                            ->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.')
144
                        ->end()
145 12
                        ->scalarNode('async_client')
146 12
                            ->defaultNull()
147 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.')
148 12
                        ->end()
149 12
                    ->end()
150 12
                ->end()
151 12
            ->end();
152 12
153 12
        return $treeBuilder;
154 12
    }
155 12
156 12
    private function configureClients(ArrayNodeDefinition $root)
157 12
    {
158 12
        $definition = $root->children()
159 12
            ->arrayNode('clients')
160 12
                ->fixXmlConfig('plugin')
161 12
                ->validate()
162 12
                    ->ifTrue(function ($clients) {
163 12
                        foreach ($clients as $name => $config) {
164 12
                            // Make sure we only allow one of these to be true
165 12
                            return (bool) $config['flexible_client'] + (bool) $config['http_methods_client'] + (bool) $config['batch_client'] >= 2;
166 12
                        }
167 12
168 12
                        return false;
169 12
                    })
170 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()
171 12
                ->useAttributeAsKey('name')
172 12
                ->prototype('array')
173 12
                ->children()
174 12
                    ->scalarNode('factory')
175
                        ->isRequired()
176
                        ->cannotBeEmpty()
177
                        ->info('The service id of a factory to use when creating the adapter.')
178
                    ->end()
179 12
                    ->booleanNode('flexible_client')
180
                        ->defaultFalse()
181 12
                        ->info('Set to true to get the client wrapped in a FlexibleHttpClient which emulates async or sync behavior.')
182 12
                    ->end()
183 12
                    ->booleanNode('http_methods_client')
184 12
                        ->defaultFalse()
185 12
                        ->info('Set to true to get the client wrapped in a HttpMethodsClient which emulates provides functions for HTTP verbs.')
186
                    ->end()
187 12
                    ->booleanNode('batch_client')
188 12
                        ->defaultFalse()
189 12
                        ->info('Set to true to get the client wrapped in a BatchClient which allows you to send multiple request at the same time.')
190 12
                    ->end()
191 12
                    ->variableNode('config')->defaultValue([])->end()
192 12
            /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
193 12
                    ->append($this->createExtraPluginsNode())
194 12
195 12
                ->end()
196 12
            ->end()*/;
197 12
198 12
        $this->configureClientPluginsNode($definition);
199 12
    }
200 12
201 12
    /**
202 12
     * @param ArrayNodeDefinition $root
203 12
     */
204 12
    private function configurePlugins(ArrayNodeDefinition $root)
205 12
    {
206 12
        $pluginsNode = $root
207 12
            ->children()
208 12
                ->arrayNode('plugins')
209 12
                ->addDefaultsIfNotSet()
210
        ;
211 12
        $this->configureSharedPluginNodes($pluginsNode);
212 12
    }
213 12
214 12
    /**
215 12
     * Configure plugins node on client.
216 12
     */
217 12
    private function configureClientPluginsNode(NodeBuilder $node)
218 12
    {
219 12
        $plugins = $node
220 12
            ->arrayNode('plugins')
221
                ->info('A list of service ids and client specific plugin definitions. The order is important.')
222 12
                ->prototype('array')
223 12
        ;
224 12
        $plugins
225 12
            ->beforeNormalization()
226 12
                ->always(function ($plugin) {
227 12
                    if (is_string($plugin)) {
228 12
                        return [
229
                            'reference' => [
230 12
                                'enabled' => true,
231 12
                                'id' => $plugin,
232 12
                            ],
233 12
                        ];
234 12
                    }
235 12
236 12
                    return $plugin;
237 12
                })
238 12
            ->end()
239 12
240
            ->validate()
241 12
                ->always(function ($plugins) {
242 12
                    if (isset($plugins['authentication']) && !count($plugins['authentication'])) {
243 12
                        unset($plugins['authentication']);
244 12
                    }
245 12
                    foreach ($plugins as $name => $definition) {
246 12
                        if (!$definition['enabled']) {
247 12
                            unset($plugins[$name]);
248 12
                        }
249 12
                    }
250 12
251 12
                    return $plugins;
252 12
                })
253 12
            ->end()
254 12
        ;
255 12
        $this->configureSharedPluginNodes($plugins, true);
256
257 12
        $plugins
258 12
            ->children()
259 12
                ->arrayNode('reference')
260 12
                    ->canBeEnabled()
261 12
                    ->info('Reference to a plugin service')
262 12
                    ->children()
263 12
                        ->scalarNode('id')
264 12
                            ->info('Service id of a plugin')
265
                            ->isRequired()
266 12
                            ->cannotBeEmpty()
267 12
                        ->end()
268 12
                    ->end()
269 12
                ->end()
270 12
                ->arrayNode('add_host')
271 12
                    ->canBeEnabled()
272 12
                    ->addDefaultsIfNotSet()
273
                    ->info('Configure the AddHostPlugin for this client.')
274 12
                    ->children()
275 12
                        ->scalarNode('host')
276 12
                            ->info('Host name including protocol and optionally the port number, e.g. https://api.local:8000')
277 12
                            ->isRequired()
278 12
                            ->cannotBeEmpty()
279 12
                        ->end()
280 12
                        ->scalarNode('replace')
281 12
                            ->info('Whether to replace the host if request already specifies it')
282 12
                            ->defaultValue(false)
283 12
                        ->end()
284 12
                    ->end()
285
                ->end()
286 12
            ->end()
287 12
        ->end();
288 12
    }
289 12
290
    /**
291
     * @param ArrayNodeDefinition $pluginNode
292
     * @param bool                $disableAll Some shared plugins are enabled by default. On the client, all are disabled by default.
293
     */
294
    private function configureSharedPluginNodes(ArrayNodeDefinition $pluginNode, $disableAll = false)
295
    {
296 12
        $children = $pluginNode->children();
297
298 12
        $children->append($this->createAuthenticationPluginNode());
299 12
300
        $children->arrayNode('cache')
301 12
            ->canBeEnabled()
302 12
            ->addDefaultsIfNotSet()
303 12
                ->children()
304 12
                    ->scalarNode('cache_pool')
305 12
                        ->info('This must be a service id to a service implementing Psr\Cache\CacheItemPoolInterface')
306 2
                        ->isRequired()
307 2
                        ->cannotBeEmpty()
308 1
                    ->end()
309 1
                    ->scalarNode('stream_factory')
310 2
                        ->info('This must be a service id to a service implementing Http\Message\StreamFactory')
311 1
                        ->defaultValue('httplug.stream_factory')
312 1
                        ->cannotBeEmpty()
313 2
                    ->end()
314 2
                    ->arrayNode('config')
315 1
                        ->addDefaultsIfNotSet()
316 1
                        ->children()
317 1
                            ->scalarNode('default_ttl')->defaultNull()->end()
318 1
                            ->scalarNode('respect_cache_headers')->defaultTrue()->end()
319 1
                        ->end()
320
                    ->end()
321 1
                ->end()
322 12
            ->end();
323 12
        // End cache plugin
324 12
325 12
        $children->arrayNode('cookie')
326 12
            ->canBeEnabled()
327 12
                ->children()
328 12
                    ->scalarNode('cookie_jar')
329 12
                        ->info('This must be a service id to a service implementing Http\Message\CookieJar')
330 12
                        ->isRequired()
331 12
                        ->cannotBeEmpty()
332 12
                    ->end()
333 12
                ->end()
334 12
            ->end();
335 12
        // End cookie plugin
336 12
337
        $decoder = $children->arrayNode('decoder');
338 12
        if ($disableAll) {
339
            $decoder->canBeEnabled();
340
        } else {
341
            $decoder->canBeDisabled();
342
        }
343
        $decoder->addDefaultsIfNotSet()
344
            ->children()
345
                ->scalarNode('use_content_encoding')->defaultTrue()->end()
346
            ->end()
347
        ->end();
348
        // End decoder plugin
349
350 2
        $children->arrayNode('history')
351
            ->canBeEnabled()
352 2
                ->children()
353 2
                    ->scalarNode('journal')
354 2
                        ->info('This must be a service id to a service implementing Http\Client\Plugin\Journal')
355 2
                        ->isRequired()
356
                        ->cannotBeEmpty()
357 2
                    ->end()
358 1
                ->end()
359
            ->end();
360
        // End history plugin
361 1
362 1
        $logger = $children->arrayNode('logger');
363 1
        if ($disableAll) {
364 1
            $logger->canBeEnabled();
365 1
        } else {
366 1
            $logger->canBeDisabled();
367
        }
368
        $logger->addDefaultsIfNotSet()
369
            ->children()
370
                ->scalarNode('logger')
371
                    ->info('This must be a service id to a service implementing Psr\Log\LoggerInterface')
372
                    ->defaultValue('logger')
373
                    ->cannotBeEmpty()
374
                ->end()
375
                ->scalarNode('formatter')
376
                    ->info('This must be a service id to a service implementing Http\Message\Formatter')
377
                    ->defaultNull()
378
                ->end()
379
            ->end()
380
        ->end();
381
        // End logger plugin
382
383
        $redirect = $children->arrayNode('redirect');
384
        if ($disableAll) {
385
            $redirect->canBeEnabled();
386
        } else {
387
            $redirect->canBeDisabled();
388
        }
389
        $redirect->addDefaultsIfNotSet()
390
            ->children()
391
                ->scalarNode('preserve_header')->defaultTrue()->end()
392
                ->scalarNode('use_default_for_multiple')->defaultTrue()->end()
393
            ->end()
394
        ->end();
395
        // End redirect plugin
396
397
        $retry = $children->arrayNode('retry');
398
        if ($disableAll) {
399
            $retry->canBeEnabled();
400
        } else {
401
            $retry->canBeDisabled();
402
        }
403
        $retry->addDefaultsIfNotSet()
404
            ->children()
405
                ->scalarNode('retry')->defaultValue(1)->end() // TODO: should be called retries for consistency with the class
406
            ->end()
407
        ->end();
408
        // End retry plugin
409
410
        $stopwatch = $children->arrayNode('stopwatch');
411
        if ($disableAll) {
412
            $stopwatch->canBeEnabled();
413
        } else {
414
            $stopwatch->canBeDisabled();
415
        }
416
        $stopwatch->addDefaultsIfNotSet()
417
            ->children()
418
                ->scalarNode('stopwatch')
419
                    ->info('This must be a service id to a service extending Symfony\Component\Stopwatch\Stopwatch')
420
                    ->defaultValue('debug.stopwatch')
421
                    ->cannotBeEmpty()
422
                ->end()
423
            ->end()
424
        ->end();
425
        // End stopwatch plugin
426
    }
427
428
    /**
429
     * Create configuration for authentication plugin.
430
     *
431
     * @return NodeDefinition Definition for the authentication node in the plugins list.
432
     */
433
    private function createAuthenticationPluginNode()
434
    {
435
        $builder = new TreeBuilder();
436
        $node = $builder->root('authentication');
437
        $node
438
            ->useAttributeAsKey('name')
439
            ->prototype('array')
440
                ->validate()
441
                    ->always()
442
                    ->then(function ($config) {
443
                        switch ($config['type']) {
444
                            case 'basic':
445
                                $this->validateAuthenticationType(['username', 'password'], $config, 'basic');
446
                                break;
447
                            case 'bearer':
448
                                $this->validateAuthenticationType(['token'], $config, 'bearer');
449
                                break;
450
                            case 'service':
451
                                $this->validateAuthenticationType(['service'], $config, 'service');
452
                                break;
453
                            case 'wsse':
454
                                $this->validateAuthenticationType(['username', 'password'], $config, 'wsse');
455
                                break;
456
                        }
457
458
                        return $config;
459
                    })
460
                ->end()
461
                ->children()
462
                    ->enumNode('type')
463
                        ->values(['basic', 'bearer', 'wsse', 'service'])
464
                        ->isRequired()
465
                        ->cannotBeEmpty()
466
                    ->end()
467
                    ->scalarNode('username')->end()
468
                    ->scalarNode('password')->end()
469
                    ->scalarNode('token')->end()
470
                    ->scalarNode('service')->end()
471
                    ->end()
472
                ->end()
473
            ->end(); // End authentication plugin
474
475
        return $node;
476
    }
477
478
    /**
479
     * Validate that the configuration fragment has the specified keys and none other.
480
     *
481
     * @param array  $expected Fields that must exist
482
     * @param array  $actual   Actual configuration hashmap
483
     * @param string $authName Name of authentication method for error messages
484
     *
485
     * @throws InvalidConfigurationException If $actual does not have exactly the keys specified in $expected (plus 'type')
486
     */
487
    private function validateAuthenticationType(array $expected, array $actual, $authName)
488
    {
489
        unset($actual['type']);
490
        $actual = array_keys($actual);
491
        sort($actual);
492
        sort($expected);
493
494
        if ($expected === $actual) {
495
            return;
496
        }
497
498
        throw new InvalidConfigurationException(sprintf(
499
            'Authentication "%s" requires %s but got %s',
500
            $authName,
501
            implode(', ', $expected),
502
            implode(', ', $actual)
503
        ));
504
    }
505
}
506