1<?php
2
3/*
4 * This file is part of the FOSRestBundle package.
5 *
6 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace FOS\RestBundle\DependencyInjection;
13
14use Symfony\Component\Config\FileLocator;
15use Symfony\Component\DependencyInjection\Alias;
16use Symfony\Component\DependencyInjection\ChildDefinition;
17use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
18use Symfony\Component\DependencyInjection\ContainerBuilder;
19use Symfony\Component\DependencyInjection\DefinitionDecorator;
20use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
21use Symfony\Component\DependencyInjection\Reference;
22use Symfony\Component\Form\AbstractType;
23use Symfony\Component\Form\Extension\Core\Type\FormType;
24use Symfony\Component\HttpFoundation\Response;
25use Symfony\Component\HttpKernel\DependencyInjection\Extension;
26use Symfony\Component\HttpKernel\Kernel;
27
28class FOSRestExtension extends Extension
29{
30    /**
31     * {@inheritdoc}
32     */
33    public function getConfiguration(array $config, ContainerBuilder $container)
34    {
35        return new Configuration($container->getParameter('kernel.debug'));
36    }
37
38    /**
39     * Loads the services based on your application configuration.
40     *
41     * @param array            $configs
42     * @param ContainerBuilder $container
43     *
44     * @throws \InvalidArgumentException
45     * @throws \LogicException
46     */
47    public function load(array $configs, ContainerBuilder $container)
48    {
49        $configuration = new Configuration($container->getParameter('kernel.debug'));
50        $config = $this->processConfiguration($configuration, $configs);
51
52        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
53        $loader->load('view.xml');
54        $loader->load('routing.xml');
55        $loader->load('request.xml');
56        $loader->load('serializer.xml');
57
58        $container->getDefinition('fos_rest.routing.loader.controller')->replaceArgument(4, $config['routing_loader']['default_format']);
59        $container->getDefinition('fos_rest.routing.loader.yaml_collection')->replaceArgument(4, $config['routing_loader']['default_format']);
60        $container->getDefinition('fos_rest.routing.loader.xml_collection')->replaceArgument(4, $config['routing_loader']['default_format']);
61
62        $container->getDefinition('fos_rest.routing.loader.yaml_collection')->replaceArgument(2, $config['routing_loader']['include_format']);
63        $container->getDefinition('fos_rest.routing.loader.xml_collection')->replaceArgument(2, $config['routing_loader']['include_format']);
64        $container->getDefinition('fos_rest.routing.loader.reader.action')->replaceArgument(3, $config['routing_loader']['include_format']);
65        $container->getDefinition('fos_rest.routing.loader.reader.action')->replaceArgument(5, $config['routing_loader']['prefix_methods']);
66
67        foreach ($config['service'] as $key => $service) {
68            if ('validator' === $service && empty($config['body_converter']['validate'])) {
69                continue;
70            }
71
72            if (null !== $service) {
73                if ('view_handler' === $key) {
74                    $container->setAlias('fos_rest.'.$key, new Alias($service, true));
75                } else {
76                    $container->setAlias('fos_rest.'.$key, $service);
77                }
78            }
79        }
80
81        $this->loadForm($config, $loader, $container);
82        $this->loadException($config, $loader, $container);
83        $this->loadBodyConverter($config, $loader, $container);
84        $this->loadView($config, $loader, $container);
85
86        $this->loadBodyListener($config, $loader, $container);
87        $this->loadFormatListener($config, $loader, $container);
88        $this->loadVersioning($config, $loader, $container);
89        $this->loadParamFetcherListener($config, $loader, $container);
90        $this->loadAllowedMethodsListener($config, $loader, $container);
91        $this->loadAccessDeniedListener($config, $loader, $container);
92        $this->loadZoneMatcherListener($config, $loader, $container);
93
94        // Needs RequestBodyParamConverter and View Handler loaded.
95        $this->loadSerializer($config, $container);
96    }
97
98    private function loadForm(array $config, XmlFileLoader $loader, ContainerBuilder $container)
99    {
100        if (!empty($config['disable_csrf_role'])) {
101            $loader->load('forms.xml');
102
103            $definition = $container->getDefinition('fos_rest.form.extension.csrf_disable');
104            $definition->replaceArgument(1, $config['disable_csrf_role']);
105
106            // BC for Symfony < 2.8: the extended_type attribute is used on higher versions
107            if (!method_exists(AbstractType::class, 'getBlockPrefix')) {
108                $definition->addTag('form.type_extension', ['alias' => 'form']);
109            } else {
110                $definition->addTag('form.type_extension', ['extended_type' => FormType::class]);
111            }
112        }
113    }
114
115    private function loadAccessDeniedListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
116    {
117        if ($config['access_denied_listener']['enabled'] && !empty($config['access_denied_listener']['formats'])) {
118            $loader->load('access_denied_listener.xml');
119
120            $service = $container->getDefinition('fos_rest.access_denied_listener');
121
122            if (!empty($config['access_denied_listener']['service'])) {
123                $service->clearTag('kernel.event_subscriber');
124            }
125
126            $service->replaceArgument(0, $config['access_denied_listener']['formats']);
127            $service->replaceArgument(1, $config['unauthorized_challenge']);
128        }
129    }
130
131    private function loadAllowedMethodsListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
132    {
133        if ($config['allowed_methods_listener']['enabled']) {
134            if (!empty($config['allowed_methods_listener']['service'])) {
135                $service = $container->getDefinition('fos_rest.allowed_methods_listener');
136                $service->clearTag('kernel.event_listener');
137            }
138
139            $loader->load('allowed_methods_listener.xml');
140
141            $container->getDefinition('fos_rest.allowed_methods_loader')->replaceArgument(1, $config['cache_dir']);
142        }
143    }
144
145    private function loadBodyListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
146    {
147        if ($config['body_listener']['enabled']) {
148            $loader->load('body_listener.xml');
149
150            $service = $container->getDefinition('fos_rest.body_listener');
151
152            if (!empty($config['body_listener']['service'])) {
153                $service->clearTag('kernel.event_listener');
154            }
155
156            $service->replaceArgument(1, $config['body_listener']['throw_exception_on_unsupported_content_type']);
157            $service->addMethodCall('setDefaultFormat', array($config['body_listener']['default_format']));
158
159            $container->getDefinition('fos_rest.decoder_provider')->replaceArgument(1, $config['body_listener']['decoders']);
160
161            if (class_exists(ServiceLocatorTagPass::class)) {
162                $decoderServicesMap = array();
163
164                foreach ($config['body_listener']['decoders'] as $id) {
165                    $decoderServicesMap[$id] = new Reference($id);
166                }
167
168                $decodersServiceLocator = ServiceLocatorTagPass::register($container, $decoderServicesMap);
169                $container->getDefinition('fos_rest.decoder_provider')->replaceArgument(0, $decodersServiceLocator);
170            }
171
172            $arrayNormalizer = $config['body_listener']['array_normalizer'];
173
174            if (null !== $arrayNormalizer['service']) {
175                $bodyListener = $container->getDefinition('fos_rest.body_listener');
176                $bodyListener->addArgument(new Reference($arrayNormalizer['service']));
177                $bodyListener->addArgument($arrayNormalizer['forms']);
178            }
179        }
180    }
181
182    private function loadFormatListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
183    {
184        if ($config['format_listener']['enabled'] && !empty($config['format_listener']['rules'])) {
185            $loader->load('format_listener.xml');
186
187            if (!empty($config['format_listener']['service'])) {
188                $service = $container->getDefinition('fos_rest.format_listener');
189                $service->clearTag('kernel.event_listener');
190            }
191
192            $container->setParameter(
193                'fos_rest.format_listener.rules',
194                $config['format_listener']['rules']
195            );
196        }
197    }
198
199    private function loadVersioning(array $config, XmlFileLoader $loader, ContainerBuilder $container)
200    {
201        if (!empty($config['versioning']['enabled'])) {
202            $loader->load('versioning.xml');
203
204            $versionListener = $container->getDefinition('fos_rest.versioning.listener');
205            $versionListener->replaceArgument(1, $config['versioning']['default_version']);
206
207            $resolvers = [];
208            if ($config['versioning']['resolvers']['query']['enabled']) {
209                $resolvers['query'] = $container->getDefinition('fos_rest.versioning.query_parameter_resolver');
210                $resolvers['query']->replaceArgument(0, $config['versioning']['resolvers']['query']['parameter_name']);
211            }
212            if ($config['versioning']['resolvers']['custom_header']['enabled']) {
213                $resolvers['custom_header'] = $container->getDefinition('fos_rest.versioning.header_resolver');
214                $resolvers['custom_header']->replaceArgument(0, $config['versioning']['resolvers']['custom_header']['header_name']);
215            }
216            if ($config['versioning']['resolvers']['media_type']['enabled']) {
217                $resolvers['media_type'] = $container->getDefinition('fos_rest.versioning.media_type_resolver');
218                $resolvers['media_type']->replaceArgument(0, $config['versioning']['resolvers']['media_type']['regex']);
219            }
220
221            $chainResolver = $container->getDefinition('fos_rest.versioning.chain_resolver');
222            foreach ($config['versioning']['guessing_order'] as $resolver) {
223                if (isset($resolvers[$resolver])) {
224                    $chainResolver->addMethodCall('addResolver', [$resolvers[$resolver]]);
225                }
226            }
227        }
228    }
229
230    private function loadParamFetcherListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
231    {
232        if ($config['param_fetcher_listener']['enabled']) {
233            $loader->load('param_fetcher_listener.xml');
234
235            if (!empty($config['param_fetcher_listener']['service'])) {
236                $service = $container->getDefinition('fos_rest.param_fetcher_listener');
237                $service->clearTag('kernel.event_listener');
238            }
239
240            if ($config['param_fetcher_listener']['force']) {
241                $container->getDefinition('fos_rest.param_fetcher_listener')->replaceArgument(1, true);
242            }
243        }
244    }
245
246    private function loadBodyConverter(array $config, XmlFileLoader $loader, ContainerBuilder $container)
247    {
248        if (!$this->isConfigEnabled($container, $config['body_converter'])) {
249            return;
250        }
251
252        $loader->load('request_body_param_converter.xml');
253
254        if (!empty($config['body_converter']['validation_errors_argument'])) {
255            $container->getDefinition('fos_rest.converter.request_body')->replaceArgument(4, $config['body_converter']['validation_errors_argument']);
256        }
257    }
258
259    private function loadView(array $config, XmlFileLoader $loader, ContainerBuilder $container)
260    {
261        if (!empty($config['view']['jsonp_handler'])) {
262            $childDefinitionClass = class_exists(ChildDefinition::class) ? ChildDefinition::class : DefinitionDecorator::class;
263            $handler = new $childDefinitionClass($config['service']['view_handler']);
264            $handler->setPublic(true);
265
266            $jsonpHandler = new Reference('fos_rest.view_handler.jsonp');
267            $handler->addMethodCall('registerHandler', ['jsonp', [$jsonpHandler, 'createResponse']]);
268            $container->setDefinition('fos_rest.view_handler', $handler);
269
270            $container->getDefinition('fos_rest.view_handler.jsonp')->replaceArgument(0, $config['view']['jsonp_handler']['callback_param']);
271
272            if (empty($config['view']['mime_types']['jsonp'])) {
273                $config['view']['mime_types']['jsonp'] = $config['view']['jsonp_handler']['mime_type'];
274            }
275        }
276
277        if ($config['view']['mime_types']['enabled']) {
278            $loader->load('mime_type_listener.xml');
279
280            if (!empty($config['mime_type_listener']['service'])) {
281                $service = $container->getDefinition('fos_rest.mime_type_listener');
282                $service->clearTag('kernel.event_listener');
283            }
284
285            $container->getDefinition('fos_rest.mime_type_listener')->replaceArgument(0, $config['view']['mime_types']['formats']);
286        }
287
288        if ($config['view']['view_response_listener']['enabled']) {
289            $loader->load('view_response_listener.xml');
290            $service = $container->getDefinition('fos_rest.view_response_listener');
291
292            if (!empty($config['view_response_listener']['service'])) {
293                $service->clearTag('kernel.event_listener');
294            }
295
296            $service->replaceArgument(1, $config['view']['view_response_listener']['force']);
297        }
298
299        $formats = [];
300        foreach ($config['view']['formats'] as $format => $enabled) {
301            if ($enabled) {
302                $formats[$format] = false;
303            }
304        }
305        foreach ($config['view']['templating_formats'] as $format => $enabled) {
306            if ($enabled) {
307                $formats[$format] = true;
308            }
309        }
310
311        $container->getDefinition('fos_rest.routing.loader.yaml_collection')->replaceArgument(3, $formats);
312        $container->getDefinition('fos_rest.routing.loader.xml_collection')->replaceArgument(3, $formats);
313        $container->getDefinition('fos_rest.routing.loader.reader.action')->replaceArgument(4, $formats);
314
315        foreach ($config['view']['force_redirects'] as $format => $code) {
316            if (true === $code) {
317                $config['view']['force_redirects'][$format] = Response::HTTP_FOUND;
318            }
319        }
320
321        if (!is_numeric($config['view']['failed_validation'])) {
322            $config['view']['failed_validation'] = constant('\Symfony\Component\HttpFoundation\Response::'.$config['view']['failed_validation']);
323        }
324
325        $defaultViewHandler = $container->getDefinition('fos_rest.view_handler.default');
326        $defaultViewHandler->replaceArgument(4, $formats);
327        $defaultViewHandler->replaceArgument(5, $config['view']['failed_validation']);
328
329        if (!is_numeric($config['view']['empty_content'])) {
330            $config['view']['empty_content'] = constant('\Symfony\Component\HttpFoundation\Response::'.$config['view']['empty_content']);
331        }
332
333        $defaultViewHandler->replaceArgument(6, $config['view']['empty_content']);
334        $defaultViewHandler->replaceArgument(7, $config['view']['serialize_null']);
335        $defaultViewHandler->replaceArgument(8, $config['view']['force_redirects']);
336        $defaultViewHandler->replaceArgument(9, $config['view']['default_engine']);
337    }
338
339    private function loadException(array $config, XmlFileLoader $loader, ContainerBuilder $container)
340    {
341        if ($config['exception']['enabled']) {
342            $loader->load('exception_listener.xml');
343
344            if (!empty($config['exception']['service'])) {
345                $service = $container->getDefinition('fos_rest.exception_listener');
346                $service->clearTag('kernel.event_subscriber');
347            }
348
349            if (Kernel::VERSION_ID >= 40100) {
350                $controller = 'fos_rest.exception.controller::showAction';
351            } else {
352                $controller = 'fos_rest.exception.controller:showAction';
353            }
354
355            if ($config['exception']['exception_controller']) {
356                $controller = $config['exception']['exception_controller'];
357            } elseif (isset($container->getParameter('kernel.bundles')['TwigBundle'])) {
358                if (Kernel::VERSION_ID >= 40100) {
359                    $controller = 'fos_rest.exception.twig_controller::showAction';
360                } else {
361                    $controller = 'fos_rest.exception.twig_controller:showAction';
362                }
363            }
364
365            $container->getDefinition('fos_rest.exception_listener')->replaceArgument(0, $controller);
366
367            $container->getDefinition('fos_rest.exception.codes_map')
368                ->replaceArgument(0, $config['exception']['codes']);
369            $container->getDefinition('fos_rest.exception.messages_map')
370                ->replaceArgument(0, $config['exception']['messages']);
371
372            $container->getDefinition('fos_rest.exception.controller')
373                ->replaceArgument(2, $config['exception']['debug']);
374            $container->getDefinition('fos_rest.serializer.exception_normalizer.jms')
375                ->replaceArgument(1, $config['exception']['debug']);
376            $container->getDefinition('fos_rest.serializer.exception_normalizer.symfony')
377                ->replaceArgument(1, $config['exception']['debug']);
378        }
379    }
380
381    private function loadSerializer(array $config, ContainerBuilder $container)
382    {
383        $bodyConverter = $container->hasDefinition('fos_rest.converter.request_body') ? $container->getDefinition('fos_rest.converter.request_body') : null;
384        $viewHandler = $container->getDefinition('fos_rest.view_handler.default');
385        $options = array();
386
387        if (!empty($config['serializer']['version'])) {
388            if ($bodyConverter) {
389                $bodyConverter->replaceArgument(2, $config['serializer']['version']);
390            }
391            $options['exclusionStrategyVersion'] = $config['serializer']['version'];
392        }
393
394        if (!empty($config['serializer']['groups'])) {
395            if ($bodyConverter) {
396                $bodyConverter->replaceArgument(1, $config['serializer']['groups']);
397            }
398            $options['exclusionStrategyGroups'] = $config['serializer']['groups'];
399        }
400
401        $options['serializeNullStrategy'] = $config['serializer']['serialize_null'];
402        $viewHandler->addArgument($options);
403    }
404
405    private function loadZoneMatcherListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
406    {
407        if (!empty($config['zone'])) {
408            $loader->load('zone_matcher_listener.xml');
409            $zoneMatcherListener = $container->getDefinition('fos_rest.zone_matcher_listener');
410
411            foreach ($config['zone'] as $zone) {
412                $matcher = $this->createZoneRequestMatcher($container,
413                    $zone['path'],
414                    $zone['host'],
415                    $zone['methods'],
416                    $zone['ips']
417                );
418
419                $zoneMatcherListener->addMethodCall('addRequestMatcher', array($matcher));
420            }
421        }
422    }
423
424    private function createZoneRequestMatcher(ContainerBuilder $container, $path = null, $host = null, $methods = array(), $ip = null)
425    {
426        if ($methods) {
427            $methods = array_map('strtoupper', (array) $methods);
428        }
429
430        $serialized = serialize(array($path, $host, $methods, $ip));
431        $id = 'fos_rest.zone_request_matcher.'.md5($serialized).sha1($serialized);
432
433        // only add arguments that are necessary
434        $arguments = array($path, $host, $methods, $ip);
435        while (count($arguments) > 0 && !end($arguments)) {
436            array_pop($arguments);
437        }
438
439        $childDefinitionClass = class_exists(ChildDefinition::class) ? ChildDefinition::class : DefinitionDecorator::class;
440        $container
441            ->setDefinition($id, new $childDefinitionClass('fos_rest.zone_request_matcher'))
442            ->setArguments($arguments)
443        ;
444
445        return new Reference($id);
446    }
447}
448