Issues (171)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/DependencyInjection/Configuration.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Http\HttplugBundle\DependencyInjection;
6
7
use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator;
8
use Http\Client\Common\Plugin\Cache\Listener\CacheListener;
9
use Http\Client\Common\Plugin\CachePlugin;
10
use Http\Client\Common\Plugin\Journal;
11
use Http\Client\Plugin\Vcr\NamingStrategy\NamingStrategyInterface;
12
use Http\Client\Plugin\Vcr\Recorder\PlayerInterface;
13
use Http\Client\Plugin\Vcr\Recorder\RecorderInterface;
14
use Http\Message\CookieJar;
15
use Http\Message\Formatter;
16
use Http\Message\StreamFactory;
17
use Psr\Cache\CacheItemPoolInterface;
18
use Psr\Log\LoggerInterface;
19
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
20
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
21
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
22
use Symfony\Component\Config\Definition\ConfigurationInterface;
23
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
24
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
25
26
/**
27
 * This class contains the configuration information for the bundle.
28
 *
29
 * This information is solely responsible for how the different configuration
30
 * sections are normalized, and merged.
31
 *
32
 * @author David Buchmann <[email protected]>
33
 * @author Tobias Nyholm <[email protected]>
34
 */
35
class Configuration implements ConfigurationInterface
36
{
37
    /**
38
     * Whether to use the debug mode.
39
     *
40
     * @see https://github.com/doctrine/DoctrineBundle/blob/v1.5.2/DependencyInjection/Configuration.php#L31-L41
41
     *
42
     * @var bool
43
     */
44
    private $debug;
45
46
    /**
47
     * @param bool $debug
48
     */
49 51
    public function __construct($debug)
50
    {
51 51
        $this->debug = (bool) $debug;
52 51
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 51
    public function getConfigTreeBuilder()
58
    {
59 51
        $treeBuilder = new TreeBuilder('httplug');
60
        // Keep compatibility with symfony/config < 4.2
61 51
        if (!method_exists($treeBuilder, 'getRootNode')) {
62
            $rootNode = $treeBuilder->root('httplug');
0 ignored issues
show
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...
63
        } else {
64 51
            $rootNode = $treeBuilder->getRootNode();
65
        }
66
67 51
        $this->configureClients($rootNode);
68 51
        $this->configureSharedPlugins($rootNode);
69
70
        $rootNode
71 51
            ->validate()
72
                ->ifTrue(function ($v) {
73 44
                    return !empty($v['classes']['client'])
74 41
                        || !empty($v['classes']['message_factory'])
75 41
                        || !empty($v['classes']['uri_factory'])
76 44
                        || !empty($v['classes']['stream_factory']);
77 51
                })
78
                ->then(function ($v) {
79 3
                    foreach ($v['classes'] as $key => $class) {
80 3
                        if (null !== $class && !class_exists($class)) {
81 1
                            throw new InvalidConfigurationException(sprintf(
82 1
                                'Class %s specified for httplug.classes.%s does not exist.',
83
                                $class,
84
                                $key
85
                            ));
86
                        }
87
                    }
88
89 2
                    return $v;
90 51
                })
91 51
            ->end()
92 51
            ->beforeNormalization()
93
                ->ifTrue(function ($v) {
94 51
                    return is_array($v) && array_key_exists('toolbar', $v) && is_array($v['toolbar']);
95 51
                })
96
                ->then(function ($v) {
97 4
                    if (array_key_exists('profiling', $v)) {
98 1
                        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".');
99
                    }
100
101 3
                    @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);
102
103 3
                    if (array_key_exists('enabled', $v['toolbar']) && 'auto' === $v['toolbar']['enabled']) {
104 1
                        @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);
105 1
                        $v['toolbar']['enabled'] = $this->debug;
106
                    }
107
108 3
                    $v['profiling'] = $v['toolbar'];
109
110 3
                    unset($v['toolbar']);
111
112 3
                    return $v;
113 51
                })
114 51
            ->end()
115 51
            ->fixXmlConfig('client')
116 51
            ->children()
117 51
                ->booleanNode('default_client_autowiring')
118 51
                    ->defaultTrue()
119 51
                    ->info('Set to false to not autowire HttpClient and HttpAsyncClient.')
120 51
                ->end()
121 51
                ->arrayNode('main_alias')
122 51
                    ->addDefaultsIfNotSet()
123 51
                    ->info('Configure which service the main alias point to.')
124 51
                    ->children()
125 51
                        ->scalarNode('client')->defaultValue('httplug.client.default')->end()
126 51
                        ->scalarNode('psr18_client')->defaultValue('httplug.psr18_client.default')->end()
127 51
                        ->scalarNode('message_factory')->defaultValue('httplug.message_factory.default')->end()
128 51
                        ->scalarNode('uri_factory')->defaultValue('httplug.uri_factory.default')->end()
129 51
                        ->scalarNode('stream_factory')->defaultValue('httplug.stream_factory.default')->end()
130 51
                        ->scalarNode('psr17_request_factory')->defaultValue('httplug.psr17_request_factory.default')->end()
131 51
                        ->scalarNode('psr17_response_factory')->defaultValue('httplug.psr17_response_factory.default')->end()
132 51
                        ->scalarNode('psr17_stream_factory')->defaultValue('httplug.psr17_stream_factory.default')->end()
133 51
                        ->scalarNode('psr17_uri_factory')->defaultValue('httplug.psr17_uri_factory.default')->end()
134 51
                        ->scalarNode('psr17_uploaded_file_factory')->defaultValue('httplug.psr17_uploaded_file_factory.default')->end()
135 51
                        ->scalarNode('psr17_server_request_factory')->defaultValue('httplug.psr17_server_request_factory.default')->end()
136 51
                    ->end()
137 51
                ->end()
138 51
                ->arrayNode('classes')
139 51
                    ->addDefaultsIfNotSet()
140 51
                    ->info('Overwrite a service class instead of using the discovery mechanism.')
141 51
                    ->children()
142 51
                        ->scalarNode('client')->defaultNull()->end()
143 51
                        ->scalarNode('psr18_client')->defaultNull()->end()
144 51
                        ->scalarNode('message_factory')->defaultNull()->end()
145 51
                        ->scalarNode('uri_factory')->defaultNull()->end()
146 51
                        ->scalarNode('stream_factory')->defaultNull()->end()
147 51
                        ->scalarNode('psr17_request_factory')->defaultNull()->end()
148 51
                        ->scalarNode('psr17_response_factory')->defaultNull()->end()
149 51
                        ->scalarNode('psr17_stream_factory')->defaultNull()->end()
150 51
                        ->scalarNode('psr17_uri_factory')->defaultNull()->end()
151 51
                        ->scalarNode('psr17_uploaded_file_factory')->defaultNull()->end()
152 51
                        ->scalarNode('psr17_server_request_factory')->defaultNull()->end()
153 51
                    ->end()
154 51
                ->end()
155 51
                ->arrayNode('profiling')
156 51
                    ->addDefaultsIfNotSet()
157 51
                    ->treatFalseLike(['enabled' => false])
158 51
                    ->treatTrueLike(['enabled' => true])
159 51
                    ->treatNullLike(['enabled' => $this->debug])
160 51
                    ->info('Extend the debug profiler with information about requests.')
161 51
                    ->children()
162 51
                        ->booleanNode('enabled')
163 51
                            ->info('Turn the toolbar on or off. Defaults to kernel debug mode.')
164 51
                            ->defaultValue($this->debug)
165 51
                        ->end()
166 51
                        ->scalarNode('formatter')->defaultNull()->end()
167 51
                        ->scalarNode('captured_body_length')
168 51
                            ->validate()
169
                                ->ifTrue(function ($v) {
170 4
                                    return null !== $v && !is_int($v);
171 51
                                })
172 51
                                ->thenInvalid('The child node "captured_body_length" at path "httplug.profiling" must be an integer or null ("%s" given).')
173 51
                            ->end()
174 51
                            ->defaultValue(0)
175 51
                            ->info('Limit long HTTP message bodies to x characters. If set to 0 we do not read the message body. If null the body will not be truncated. Only available with the default formatter (FullHttpMessageFormatter).')
176 51
                        ->end()
177 51
                    ->end()
178 51
                ->end()
179 51
                ->arrayNode('discovery')
180 51
                    ->addDefaultsIfNotSet()
181 51
                    ->info('Control what clients should be found by the discovery.')
182 51
                    ->children()
183 51
                        ->scalarNode('client')
184 51
                            ->defaultValue('auto')
185 51
                            ->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.')
186 51
                        ->end()
187 51
                        ->scalarNode('async_client')
188 51
                            ->defaultNull()
189 51
                            ->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.')
190 51
                        ->end()
191 51
                    ->end()
192 51
                ->end()
193 51
            ->end();
194
195 51
        return $treeBuilder;
196
    }
197
198 51
    private function configureClients(ArrayNodeDefinition $root)
199
    {
200 51
        $root->children()
201 51
            ->arrayNode('clients')
202 51
                ->useAttributeAsKey('name')
203 51
                ->prototype('array')
204 51
                ->fixXmlConfig('plugin')
205 51
                ->validate()
206
                    ->ifTrue(function ($config) {
207
                        // Make sure we only allow one of these to be true
208 27
                        return (bool) $config['flexible_client'] + (bool) $config['http_methods_client'] + (bool) $config['batch_client'] >= 2;
209 51
                    })
210 51
                    ->thenInvalid('A http client can\'t be decorated with several of FlexibleHttpClient, HttpMethodsClient and BatchClient. Only one of the following options can be true. ("flexible_client", "http_methods_client", "batch_client")')
211 51
                ->end()
212 51
                ->validate()
213
                    ->ifTrue(function ($config) {
214 27
                        return 'httplug.factory.auto' === $config['factory'] && !empty($config['config']);
215 51
                    })
216 51
                    ->thenInvalid('If you want to use the "config" key you must also specify a valid "factory".')
217 51
                ->end()
218 51
                ->validate()
219
                    ->ifTrue(function ($config) {
220 27
                        return !empty($config['service']) && ('httplug.factory.auto' !== $config['factory'] || !empty($config['config']));
221 51
                    })
222 51
                    ->thenInvalid('If you want to use the "service" key you cannot specify "factory" or "config".')
223 51
                ->end()
224 51
                ->children()
225 51
                    ->scalarNode('factory')
226 51
                        ->defaultValue('httplug.factory.auto')
227 51
                        ->cannotBeEmpty()
228 51
                        ->info('The service id of a factory to use when creating the adapter.')
229 51
                    ->end()
230 51
                    ->scalarNode('service')
231 51
                        ->defaultNull()
232 51
                        ->info('The service id of the client to use.')
233 51
                    ->end()
234 51
                    ->booleanNode('public')
235 51
                        ->defaultNull()
236 51
                        ->info('Set to true if you really cannot use dependency injection and need to make the client service public.')
237 51
                    ->end()
238 51
                    ->booleanNode('flexible_client')
239 51
                        ->defaultFalse()
240 51
                        ->info('Set to true to get the client wrapped in a FlexibleHttpClient which emulates async or sync behavior.')
241 51
                    ->end()
242 51
                    ->booleanNode('http_methods_client')
243 51
                        ->defaultFalse()
244 51
                        ->info('Set to true to get the client wrapped in a HttpMethodsClient which emulates provides functions for HTTP verbs.')
245 51
                    ->end()
246 51
                    ->booleanNode('batch_client')
247 51
                        ->defaultFalse()
248 51
                        ->info('Set to true to get the client wrapped in a BatchClient which allows you to send multiple request at the same time.')
249 51
                    ->end()
250 51
                    ->variableNode('config')->defaultValue([])->end()
251 51
                    ->append($this->createClientPluginNode())
252 51
                ->end()
253 51
            ->end()
254 51
        ->end();
255 51
    }
256
257 51
    private function configureSharedPlugins(ArrayNodeDefinition $root)
258
    {
259
        $pluginsNode = $root
260 51
            ->children()
261 51
                ->arrayNode('plugins')
262 51
                ->info('Global plugin configuration. Plugins need to be explicitly added to clients.')
263 51
                ->addDefaultsIfNotSet()
264
            // don't call end to get the plugins node
265
        ;
266 51
        $this->addSharedPluginNodes($pluginsNode);
267 51
    }
268
269
    /**
270
     * Createplugins node of a client.
271
     *
272
     * @return ArrayNodeDefinition The plugin node
273
     */
274 51
    private function createClientPluginNode()
275
    {
276 51
        $treeBuilder = new TreeBuilder('plugins');
277
        // Keep compatibility with symfony/config < 4.2
278 51
        if (!method_exists($treeBuilder, 'getRootNode')) {
279
            $node = $treeBuilder->root('plugins');
0 ignored issues
show
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...
280
        } else {
281 51
            $node = $treeBuilder->getRootNode();
282
        }
283
284
        /** @var ArrayNodeDefinition $pluginList */
285
        $pluginList = $node
286 51
            ->info('A list of plugin service ids and client specific plugin definitions. The order is important.')
287 51
            ->prototype('array')
288
        ;
289
        $pluginList
290
            // support having just a service id in the list
291 51
            ->beforeNormalization()
292
                ->always(function ($plugin) {
293 18
                    if (is_string($plugin)) {
294
                        return [
295
                            'reference' => [
296 10
                                'enabled' => true,
297 10
                                'id' => $plugin,
298
                            ],
299
                        ];
300
                    }
301
302 15
                    return $plugin;
303 51
                })
304 51
            ->end()
305
306 51
            ->validate()
307
                ->always(function ($plugins) {
308 16
                    foreach ($plugins as $name => $definition) {
309 16
                        if ('authentication' === $name) {
310 16
                            if (!count($definition)) {
311 16
                                unset($plugins['authentication']);
312
                            }
313 16
                        } elseif (!$definition['enabled']) {
314 16
                            unset($plugins[$name]);
315
                        }
316
                    }
317
318 16
                    return $plugins;
319 51
                })
320 51
            ->end()
321
        ;
322 51
        $this->addSharedPluginNodes($pluginList, true);
323
324
        $pluginList
325 51
            ->children()
326 51
                ->arrayNode('reference')
327 51
                    ->canBeEnabled()
328 51
                    ->info('Reference to a plugin service')
329 51
                    ->children()
330 51
                        ->scalarNode('id')
331 51
                            ->info('Service id of a plugin')
332 51
                            ->isRequired()
333 51
                            ->cannotBeEmpty()
334 51
                        ->end()
335 51
                    ->end()
336 51
                ->end()
337 51
                ->arrayNode('add_host')
338 51
                    ->canBeEnabled()
339 51
                    ->addDefaultsIfNotSet()
340 51
                    ->info('Set scheme, host and port in the request URI.')
341 51
                    ->children()
342 51
                        ->scalarNode('host')
343 51
                            ->info('Host name including protocol and optionally the port number, e.g. https://api.local:8000')
344 51
                            ->isRequired()
345 51
                            ->cannotBeEmpty()
346 51
                        ->end()
347 51
                        ->scalarNode('replace')
348 51
                            ->info('Whether to replace the host if request already specifies one')
349 51
                            ->defaultValue(false)
350 51
                        ->end()
351 51
                    ->end()
352 51
                ->end()
353 51
                ->arrayNode('add_path')
354 51
                    ->canBeEnabled()
355 51
                    ->addDefaultsIfNotSet()
356 51
                    ->info('Add a base path to the request.')
357 51
                    ->children()
358 51
                        ->scalarNode('path')
359 51
                            ->info('Path to be added, e.g. /api/v1')
360 51
                            ->isRequired()
361 51
                            ->cannotBeEmpty()
362 51
                        ->end()
363 51
                    ->end()
364 51
                ->end()
365 51
                ->arrayNode('base_uri')
366 51
                    ->canBeEnabled()
367 51
                    ->addDefaultsIfNotSet()
368 51
                    ->info('Set a base URI to the request.')
369 51
                    ->children()
370 51
                        ->scalarNode('uri')
371 51
                            ->info('Base Uri including protocol, optionally the port number and prepend path, e.g. https://api.local:8000/api')
372 51
                            ->isRequired()
373 51
                            ->cannotBeEmpty()
374 51
                        ->end()
375 51
                        ->scalarNode('replace')
376 51
                            ->info('Whether to replace the host if request already specifies one')
377 51
                            ->defaultValue(false)
378 51
                        ->end()
379 51
                    ->end()
380 51
                ->end()
381 51
                ->arrayNode('content_type')
382 51
                    ->canBeEnabled()
383 51
                    ->info('Detect the content type of a request body and set the Content-Type header if it is not already set.')
384 51
                    ->children()
385 51
                        ->booleanNode('skip_detection')
386 51
                            ->info('Whether to skip detection when request body is larger than size_limit')
387 51
                            ->defaultFalse()
388 51
                        ->end()
389 51
                        ->scalarNode('size_limit')
390 51
                            ->info('Skip content type detection if request body is larger than size_limit bytes')
391 51
                        ->end()
392 51
                    ->end()
393 51
                ->end()
394 51
                ->arrayNode('header_append')
395 51
                    ->canBeEnabled()
396 51
                    ->info('Append headers to the request. If the header already exists the value will be appended to the current value.')
397 51
                    ->fixXmlConfig('header')
398 51
                    ->children()
399 51
                        ->arrayNode('headers')
400 51
                            ->info('Keys are the header names, values the header values')
401 51
                            ->normalizeKeys(false)
402 51
                            ->useAttributeAsKey('name')
403 51
                            ->prototype('scalar')->end()
404 51
                        ->end()
405 51
                    ->end()
406 51
                ->end()
407 51
                ->arrayNode('header_defaults')
408 51
                    ->canBeEnabled()
409 51
                    ->info('Set header to default value if it does not exist.')
410 51
                    ->fixXmlConfig('header')
411 51
                    ->children()
412 51
                        ->arrayNode('headers')
413 51
                            ->info('Keys are the header names, values the header values')
414 51
                            ->normalizeKeys(false)
415 51
                            ->useAttributeAsKey('name')
416 51
                            ->prototype('scalar')->end()
417 51
                        ->end()
418 51
                    ->end()
419 51
                ->end()
420 51
                ->arrayNode('header_set')
421 51
                    ->canBeEnabled()
422 51
                    ->info('Set headers to requests. If the header does not exist it wil be set, if the header already exists it will be replaced.')
423 51
                    ->fixXmlConfig('header')
424 51
                    ->children()
425 51
                        ->arrayNode('headers')
426 51
                            ->info('Keys are the header names, values the header values')
427 51
                            ->normalizeKeys(false)
428 51
                            ->useAttributeAsKey('name')
429 51
                            ->prototype('scalar')->end()
430 51
                        ->end()
431 51
                    ->end()
432 51
                ->end()
433 51
                ->arrayNode('header_remove')
434 51
                    ->canBeEnabled()
435 51
                    ->info('Remove headers from requests.')
436 51
                    ->fixXmlConfig('header')
437 51
                    ->children()
438 51
                        ->arrayNode('headers')
439 51
                            ->info('List of header names to remove')
440 51
                            ->prototype('scalar')->end()
441 51
                        ->end()
442 51
                    ->end()
443 51
                ->end()
444 51
                ->arrayNode('query_defaults')
445 51
                    ->canBeEnabled()
446 51
                    ->info('Sets query parameters to default value if they are not present in the request.')
447 51
                    ->fixXmlConfig('parameter')
448 51
                    ->children()
449 51
                        ->arrayNode('parameters')
450 51
                            ->info('List of query parameters. Names and values must not be url encoded as the plugin will encode them.')
451 51
                            ->normalizeKeys(false)
452 51
                            ->useAttributeAsKey('name')
453 51
                            ->prototype('scalar')->end()
454 51
                        ->end()
455 51
                    ->end()
456 51
                ->end()
457 51
                ->arrayNode('vcr')
458 51
                    ->canBeEnabled()
459 51
                    ->addDefaultsIfNotSet()
460 51
                    ->info('Record response to be replayed during tests or development cycle.')
461 51
                    ->validate()
462
                        ->ifTrue(function ($config) {
463 5
                            return 'filesystem' === $config['recorder'] && empty($config['fixtures_directory']);
464 51
                        })
465 51
                        ->thenInvalid('If you want to use the "filesystem" recorder you must also specify a "fixtures_directory".')
466 51
                    ->end()
467 51
                    ->children()
468 51
                        ->enumNode('mode')
469 51
                        ->info('What should be the behavior of the plugin?')
470 51
                        ->values(['record', 'replay', 'replay_or_record'])
471 51
                        ->isRequired()
472 51
                        ->cannotBeEmpty()
473 51
                    ->end()
474 51
                    ->scalarNode('recorder')
475 51
                        ->info(sprintf('Which recorder to use. Can be "in_memory", "filesystem" or the ID of your service implementing %s and %s. When using filesystem, specify "fixtures_directory" as well.', RecorderInterface::class, PlayerInterface::class))
476 51
                        ->defaultValue('filesystem')
477 51
                        ->cannotBeEmpty()
478 51
                    ->end()
479 51
                    ->scalarNode('naming_strategy')
480 51
                        ->info(sprintf('Which naming strategy to use. Add the ID of your service implementing %s to override the default one.', NamingStrategyInterface::class))
481 51
                        ->defaultValue('default')
482 51
                        ->cannotBeEmpty()
483 51
                    ->end()
484 51
                    ->arrayNode('naming_strategy_options')
485 51
                        ->info('See http://docs.php-http.org/en/latest/plugins/vcr.html#the-naming-strategy for more details')
486 51
                        ->children()
487 51
                            ->arrayNode('hash_headers')
488 51
                                ->info('List of header(s) that make the request unique (Ex: ‘Authorization’)')
489 51
                                ->prototype('scalar')->end()
490 51
                            ->end()
491 51
                            ->arrayNode('hash_body_methods')
492 51
                                ->info('for which request methods the body makes requests distinct.')
493 51
                                ->prototype('scalar')->end()
494 51
                            ->end()
495 51
                        ->end()
496 51
                    ->end() // End naming_strategy_options
497 51
                    ->scalarNode('fixtures_directory')
498 51
                        ->info('Where the responses will be stored and replay from when using the filesystem recorder. Should be accessible to your VCS.')
499 51
                    ->end()
500 51
                ->end()
501 51
            ->end()
502 51
        ->end();
503
504 51
        return $node;
505
    }
506
507
    /**
508
     * Add the definitions for shared plugin configurations.
509
     *
510
     * @param ArrayNodeDefinition $pluginNode the node to add to
511
     * @param bool                $disableAll Some shared plugins are enabled by default. On the client, all are disabled by default.
512
     */
513 51
    private function addSharedPluginNodes(ArrayNodeDefinition $pluginNode, $disableAll = false)
514
    {
515 51
        $children = $pluginNode->children();
516
517 51
        $children->append($this->createAuthenticationPluginNode());
518 51
        $children->append($this->createCachePluginNode());
519
520
        $children
521 51
            ->arrayNode('cookie')
522 51
                ->canBeEnabled()
523 51
                ->children()
524 51
                    ->scalarNode('cookie_jar')
525 51
                        ->info('This must be a service id to a service implementing '.CookieJar::class)
526 51
                        ->isRequired()
527 51
                        ->cannotBeEmpty()
528 51
                    ->end()
529 51
                ->end()
530 51
            ->end();
531
        // End cookie plugin
532
533
        $children
534 51
            ->arrayNode('history')
535 51
                ->canBeEnabled()
536 51
                ->children()
537 51
                    ->scalarNode('journal')
538 51
                        ->info('This must be a service id to a service implementing '.Journal::class)
539 51
                        ->isRequired()
540 51
                        ->cannotBeEmpty()
541 51
                    ->end()
542 51
                ->end()
543 51
            ->end();
544
        // End history plugin
545
546 51
        $decoder = $children->arrayNode('decoder');
547 51
        $disableAll ? $decoder->canBeEnabled() : $decoder->canBeDisabled();
548 51
        $decoder->addDefaultsIfNotSet()
549 51
            ->children()
550 51
                ->scalarNode('use_content_encoding')->defaultTrue()->end()
551 51
            ->end()
552 51
        ->end();
553
        // End decoder plugin
554
555 51
        $logger = $children->arrayNode('logger');
556 51
        $disableAll ? $logger->canBeEnabled() : $logger->canBeDisabled();
557 51
        $logger->addDefaultsIfNotSet()
558 51
            ->children()
559 51
                ->scalarNode('logger')
560 51
                    ->info('This must be a service id to a service implementing '.LoggerInterface::class)
561 51
                    ->defaultValue('logger')
562 51
                    ->cannotBeEmpty()
563 51
                ->end()
564 51
                ->scalarNode('formatter')
565 51
                    ->info('This must be a service id to a service implementing '.Formatter::class)
566 51
                    ->defaultNull()
567 51
                ->end()
568 51
            ->end()
569 51
        ->end();
570
        // End logger plugin
571
572 51
        $redirect = $children->arrayNode('redirect');
573 51
        $disableAll ? $redirect->canBeEnabled() : $redirect->canBeDisabled();
574 51
        $redirect->addDefaultsIfNotSet()
575 51
            ->children()
576 51
                ->scalarNode('preserve_header')->defaultTrue()->end()
577 51
                ->scalarNode('use_default_for_multiple')->defaultTrue()->end()
578 51
            ->end()
579 51
        ->end();
580
        // End redirect plugin
581
582 51
        $retry = $children->arrayNode('retry');
583 51
        $disableAll ? $retry->canBeEnabled() : $retry->canBeDisabled();
584 51
        $retry->addDefaultsIfNotSet()
585 51
            ->children()
586 51
                ->scalarNode('retry')->defaultValue(1)->end() // TODO: should be called retries for consistency with the class
587 51
            ->end()
588 51
        ->end();
589
        // End retry plugin
590
591 51
        $stopwatch = $children->arrayNode('stopwatch');
592 51
        $disableAll ? $stopwatch->canBeEnabled() : $stopwatch->canBeDisabled();
593 51
        $stopwatch->addDefaultsIfNotSet()
594 51
            ->children()
595 51
                ->scalarNode('stopwatch')
596 51
                    ->info('This must be a service id to a service extending Symfony\Component\Stopwatch\Stopwatch')
597 51
                    ->defaultValue('debug.stopwatch')
598 51
                    ->cannotBeEmpty()
599 51
                ->end()
600 51
            ->end()
601 51
        ->end();
602
        // End stopwatch plugin
603 51
    }
604
605
    /**
606
     * Create configuration for authentication plugin.
607
     *
608
     * @return NodeDefinition definition for the authentication node in the plugins list
609
     */
610 51
    private function createAuthenticationPluginNode()
611
    {
612 51
        $treeBuilder = new TreeBuilder('authentication');
613
        // Keep compatibility with symfony/config < 4.2
614 51
        if (!method_exists($treeBuilder, 'getRootNode')) {
615
            $node = $treeBuilder->root('authentication');
0 ignored issues
show
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...
616
        } else {
617 51
            $node = $treeBuilder->getRootNode();
618
        }
619
620
        $node
621 51
            ->useAttributeAsKey('name')
622 51
            ->prototype('array')
623 51
                ->validate()
624 51
                    ->always()
625
                    ->then(function ($config) {
626 8
                        switch ($config['type']) {
627 8
                            case 'basic':
628 7
                                $this->validateAuthenticationType(['username', 'password'], $config, 'basic');
629
630 7
                                break;
631 2
                            case 'bearer':
632 1
                                $this->validateAuthenticationType(['token'], $config, 'bearer');
633
634 1
                                break;
635 2
                            case 'service':
636 2
                                $this->validateAuthenticationType(['service'], $config, 'service');
637
638 1
                                break;
639 1
                            case 'wsse':
640 1
                                $this->validateAuthenticationType(['username', 'password'], $config, 'wsse');
641
642 1
                                break;
643
                            case 'query_param':
644
                                $this->validateAuthenticationType(['params'], $config, 'query_param');
645
646
                                break;
647
                        }
648
649 7
                        return $config;
650 51
                    })
651 51
                ->end()
652 51
                ->children()
653 51
                    ->enumNode('type')
654 51
                        ->values(['basic', 'bearer', 'wsse', 'service', 'query_param'])
655 51
                        ->isRequired()
656 51
                        ->cannotBeEmpty()
657 51
                    ->end()
658 51
                    ->scalarNode('username')->end()
659 51
                    ->scalarNode('password')->end()
660 51
                    ->scalarNode('token')->end()
661 51
                    ->scalarNode('service')->end()
662 51
                    ->arrayNode('params')->prototype('scalar')->end()
663 51
                    ->end()
664 51
                ->end()
665 51
            ->end(); // End authentication plugin
666
667 51
        return $node;
668
    }
669
670
    /**
671
     * Validate that the configuration fragment has the specified keys and none other.
672
     *
673
     * @param array  $expected Fields that must exist
674
     * @param array  $actual   Actual configuration hashmap
675
     * @param string $authName Name of authentication method for error messages
676
     *
677
     * @throws InvalidConfigurationException If $actual does not have exactly the keys specified in $expected (plus 'type')
678
     */
679 8
    private function validateAuthenticationType(array $expected, array $actual, $authName)
680
    {
681 8
        unset($actual['type']);
682
        // Empty array is always provided, even if the config is not filled.
683 8
        if (empty($actual['params'])) {
684 8
            unset($actual['params']);
685
        }
686 8
        $actual = array_keys($actual);
687 8
        sort($actual);
688 8
        sort($expected);
689
690 8
        if ($expected === $actual) {
691 7
            return;
692
        }
693
694 1
        throw new InvalidConfigurationException(sprintf(
695 1
            'Authentication "%s" requires %s but got %s',
696
            $authName,
697 1
            implode(', ', $expected),
698 1
            implode(', ', $actual)
699
        ));
700
    }
701
702
    /**
703
     * Create configuration for cache plugin.
704
     *
705
     * @return NodeDefinition definition for the cache node in the plugins list
706
     */
707 51
    private function createCachePluginNode()
708
    {
709 51
        $builder = new TreeBuilder('config');
710
        // Keep compatibility with symfony/config < 4.2
711 51
        if (!method_exists($builder, 'getRootNode')) {
712
            $config = $builder->root('config');
0 ignored issues
show
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...
713
        } else {
714 51
            $config = $builder->getRootNode();
715
        }
716
717
        $config
718 51
            ->fixXmlConfig('method')
719 51
            ->fixXmlConfig('respect_response_cache_directive')
720 51
            ->fixXmlConfig('cache_listener')
721 51
            ->addDefaultsIfNotSet()
722 51
            ->validate()
723
                ->ifTrue(function ($config) {
724
                    // Cannot set both respect_cache_headers and respect_response_cache_directives
725 6
                    return isset($config['respect_cache_headers'], $config['respect_response_cache_directives']);
726 51
                })
727 51
                ->thenInvalid('You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives" simultaniously. Use "respect_response_cache_directives" instead.')
728 51
            ->end()
729 51
            ->children()
730 51
                ->scalarNode('cache_key_generator')
731 51
                    ->info('This must be a service id to a service implementing '.CacheKeyGenerator::class)
732 51
                ->end()
733 51
                ->integerNode('cache_lifetime')
734 51
                    ->info('The minimum time we should store a cache item')
735 51
                ->end()
736 51
                ->integerNode('default_ttl')
737 51
                    ->info('The default max age of a Response')
738 51
                ->end()
739 51
                ->arrayNode('blacklisted_paths')
740 51
                    ->info('An array of regular expression patterns for paths not to be cached. Defaults to an empty array.')
741 51
                    ->defaultValue([])
742 51
                    ->beforeNormalization()
743 51
                        ->castToArray()
744 51
                    ->end()
745 51
                    ->prototype('scalar')
746 51
                        ->validate()
747
                            ->ifTrue(function ($v) {
748 1
                                return false === @preg_match($v, '');
749 51
                            })
750 51
                            ->thenInvalid('Invalid regular expression for a blacklisted path: %s')
751 51
                        ->end()
752 51
                    ->end()
753 51
                ->end()
754 51
                ->enumNode('hash_algo')
755 51
                    ->info('Hashing algorithm to use')
756 51
                    ->values(hash_algos())
757 51
                    ->cannotBeEmpty()
758 51
                ->end()
759 51
                ->arrayNode('methods')
760 51
                    ->info('Which request methods to cache')
761 51
                    ->defaultValue(['GET', 'HEAD'])
762 51
                    ->prototype('scalar')
763 51
                        ->validate()
764
                            ->ifTrue(function ($v) {
765
                                /* RFC7230 sections 3.1.1 and 3.2.6 except limited to uppercase characters. */
766 1
                                return preg_match('/[^A-Z0-9!#$%&\'*+\-.^_`|~]+/', $v);
767 51
                            })
768 51
                            ->thenInvalid('Invalid method: %s')
769 51
                        ->end()
770 51
                    ->end()
771 51
                ->end()
772 51
                ->arrayNode('cache_listeners')
773 51
                    ->info('A list of service ids to act on the response based on the results of the cache check. Must implement '.CacheListener::class.'. Defaults to an empty array.')
774 51
                    ->beforeNormalization()->castToArray()->end()
775 51
                    ->prototype('scalar')
776 51
                    ->end()
777 51
                ->end()
778 51
                ->scalarNode('respect_cache_headers')
779 51
                    ->info('Whether we should care about cache headers or not [DEPRECATED]')
780 51
                    ->beforeNormalization()
781
                        ->always(function ($v) {
782 3
                            @trigger_error('The option "respect_cache_headers" is deprecated since version 1.3 and will be removed in 2.0. Use "respect_response_cache_directives" instead.', E_USER_DEPRECATED);
783
784 3
                            return $v;
785 51
                        })
786 51
                    ->end()
787 51
                    ->validate()
788 51
                        ->ifNotInArray([null, true, false])
789 51
                        ->thenInvalid('Value for "respect_cache_headers" must be null or boolean')
790 51
                    ->end()
791 51
                ->end()
792 51
                ->variableNode('respect_response_cache_directives')
793 51
                    ->info('A list of cache directives to respect when caching responses')
794 51
                    ->validate()
795
                        ->always(function ($v) {
796 2
                            if (is_null($v) || is_array($v)) {
797 2
                                return $v;
798
                            }
799
800
                            throw new InvalidTypeException();
801 51
                        })
802 51
                    ->end()
803 51
                ->end()
804 51
            ->end()
805
        ;
806
807 51
        $treeBuilder = new TreeBuilder('cache');
808
        // Keep compatibility with symfony/config < 4.2
809 51
        if (!method_exists($treeBuilder, 'getRootNode')) {
810
            $cache = $treeBuilder->root('cache');
0 ignored issues
show
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...
811
        } else {
812 51
            $cache = $treeBuilder->getRootNode();
813
        }
814
815
        $cache
816 51
            ->canBeEnabled()
817 51
            ->info('Configure HTTP caching, requires the php-http/cache-plugin package')
818 51
            ->addDefaultsIfNotSet()
819 51
            ->validate()
820
                ->ifTrue(function ($v) {
821 6
                    return !empty($v['enabled']) && !class_exists(CachePlugin::class);
822 51
                })
823 51
                ->thenInvalid('To use the cache plugin, you need to require php-http/cache-plugin in your project')
824 51
            ->end()
825 51
            ->children()
826 51
                ->scalarNode('cache_pool')
827 51
                    ->info('This must be a service id to a service implementing '.CacheItemPoolInterface::class)
828 51
                    ->isRequired()
829 51
                    ->cannotBeEmpty()
830 51
                ->end()
831 51
                ->scalarNode('stream_factory')
832 51
                    ->info('This must be a service id to a service implementing '.StreamFactory::class)
833 51
                    ->defaultValue('httplug.stream_factory')
834 51
                    ->cannotBeEmpty()
835 51
                ->end()
836 51
            ->end()
837 51
            ->append($config)
838
        ;
839
840 51
        return $cache;
841
    }
842
}
843