biurad/flange

View on GitHub
src/Extensions/HttpGalaxyExtension.php

Summary

Maintainability
D
2 days
Test Coverage
<?php declare(strict_types=1);

/*
 * This file is part of Biurad opensource projects.
 *
 * @copyright 2019 Biurad Group (https://biurad.com/)
 * @license   https://opensource.org/licenses/BSD-3-Clause License
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Flange\Extensions;

use Biurad\Http\Factory\CookieFactory;
use Biurad\Http\Factory\Psr17Factory;
use Biurad\Http\Interfaces\CookieFactoryInterface;
use Biurad\Http\Middlewares\CacheControlMiddleware;
use Biurad\Http\Middlewares\CookiesMiddleware;
use Biurad\Http\Middlewares\HttpCorsMiddleware;
use Biurad\Http\Middlewares\HttpHeadersMiddleware;
use Biurad\Http\Middlewares\HttpPolicyMiddleware;
use Biurad\Http\Middlewares\SessionMiddleware;
use Flange\KernelInterface;
use Rade\DI\Container;
use Rade\DI\Extensions\AliasedInterface;
use Rade\DI\Extensions\ExtensionInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\SessionHandlerFactory;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\Security\Csrf\CsrfTokenManager;
use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage;
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;

use function Rade\DI\Loader\{reference, service, wrap};

/**
 * Biurad Http Galaxy Provider.
 *
 * @author Divine Niiquaye Ibok <divineibok@gmail.com>
 */
class HttpGalaxyExtension implements AliasedInterface, ConfigurationInterface, ExtensionInterface
{
    /**
     * {@inheritdoc}
     */
    public function getAlias(): string
    {
        return 'http_galaxy';
    }

    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder(__CLASS__);

        $treeBuilder->getRootNode()
            ->info('HTTP Galaxy configuration')
            ->fixXmlConfig('policy')
            ->fixXmlConfig('header')
            ->children()
                ->scalarNode('psr17_factory')->end()
                ->arrayNode('caching')
                    ->canBeEnabled()
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->integerNode('cache_lifetime')->defaultValue(86400 * 30)->end()
                        ->integerNode('default_ttl')->end()
                        ->enumNode('hash_algo')->values(\hash_algos())->end()
                        ->arrayNode('methods')
                            ->defaultValue(['GET', 'HEAD'])
                            ->prototype('scalar')->end()
                        ->end()
                        ->arrayNode('respect_response_cache_directives')
                            ->performNoDeepMerging()
                            ->defaultValue(['no-cache', 'private', 'max-age', 'no-store'])
                            ->prototype('scalar')->end()
                        ->end()
                        ->scalarNode('cache_key_generator')->end()
                        ->arrayNode('cache_listeners')
                            ->defaultValue([])
                            ->prototype('scalar')->end()
                        ->end()
                        ->arrayNode('blacklisted_paths')
                            ->defaultValue([])
                            ->prototype('scalar')->end()
                        ->end()
                    ->end()
                ->end()
                ->arrayNode('csrf_protection')
                    ->addDefaultsIfNotSet()
                    ->beforeNormalization()
                        ->ifTrue(fn ($v) => \is_bool($v))
                        ->then(fn ($v) => ['enabled' => $v])
                    ->end()
                    ->children()
                        ->booleanNode('enabled')->defaultFalse()->end()
                    ->end()
                ->end()
                ->arrayNode('policies')
                    ->canBeEnabled()
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->arrayNode('content_security_policy')
                            ->defaultValue([])
                            ->prototype('scalar')->end()
                        ->end()
                        ->arrayNode('csp_report_only')
                            ->defaultValue([])
                            ->prototype('scalar')->end()
                        ->end()
                        ->arrayNode('feature_policy')
                            ->defaultValue([])
                            ->prototype('scalar')->end()
                        ->end()
                        ->arrayNode('referrer_policy')
                            ->defaultValue([])
                            ->prototype('scalar')->end()
                        ->end()
                        ->scalarNode('frame_policy')
                            ->beforeNormalization()
                                ->ifTrue(fn ($v) => false === $v)
                                ->then(fn ($v) => 'DENY')
                            ->end()
                            ->defaultValue('SAMEORIGIN')
                        ->end()
                        ->booleanNode('expose_csp_nonce')->defaultValue(true)->end()
                    ->end()
                ->end()
                ->arrayNode('headers')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->append(Config\HttpCorsSection::getConfigNode())
                        ->arrayNode('request')
                            ->normalizeKeys(false)
                            ->useAttributeAsKey('name')
                            ->prototype('scalar')->end()
                        ->end()
                        ->arrayNode('response')
                            ->normalizeKeys(false)
                            ->useAttributeAsKey('name')
                            ->prototype('scalar')->end()
                        ->end()
                    ->end()
                ->end()
                ->arrayNode('cookie')
                    ->info('cookie configuration')
                    ->addDefaultsIfNotSet()
                    ->beforeNormalization()
                        ->ifTrue(fn ($v) => \is_bool($v))->then(fn ($v) => ['enabled' => $v])
                        ->ifString()->then(fn ($v) => ['prefix_name' => $v, 'enabled' => true])
                    ->end()
                    ->canBeEnabled()
                    ->children()
                        ->scalarNode('prefix_name')->defaultValue('rade_')->end()
                        ->scalarNode('encrypter')->defaultValue(null)->end()
                        ->arrayNode('excludes_encryption')
                            ->prototype('scalar')->defaultValue([])->end()
                        ->end()
                        ->arrayNode('cookies')
                            ->arrayPrototype()
                                ->addDefaultsIfNotSet()
                                ->children()
                                    ->scalarNode('name')->isRequired()->end()
                                    ->scalarNode('value')->defaultNull()->end()
                                    ->variableNode('expire')
                                        ->beforeNormalization()
                                            ->always()
                                            ->then(fn ($v) => wrap('Nette\Utils\DateTime::from', [$v]))
                                        ->end()
                                        ->defaultValue(0)
                                    ->end()
                                    ->scalarNode('path')->defaultValue('/')->end()
                                    ->scalarNode('domain')->defaultNull()->end()
                                    ->booleanNode('secure')->defaultNull()->end()
                                    ->booleanNode('httpOnly')->defaultTrue()->end()
                                    ->booleanNode('raw')->defaultFalse()->end()
                                    ->enumNode('sameSite')
                                        ->values([null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_NONE, Cookie::SAMESITE_STRICT])
                                        ->defaultValue(Cookie::SAMESITE_LAX)
                                    ->end()
                                ->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
                ->arrayNode('session')
                    ->info('session configuration')
                    ->addDefaultsIfNotSet()
                    ->beforeNormalization()
                        ->ifTrue(fn ($v) => \is_bool($v))
                        ->then(fn ($v) => ['enabled' => $v])
                    ->end()
                    ->canBeEnabled()
                    ->children()
                        ->scalarNode('storage_id')->defaultValue('session.storage.native')->end()
                        ->scalarNode('handler_id')->defaultValue('session.handler.native_file')->end()
                        ->scalarNode('name')
                            ->validate()
                                ->ifTrue(function ($v): bool {
                                    \parse_str($v, $parsed);

                                    return \implode('&', \array_keys($parsed)) !== (string) $v;
                                })
                                ->thenInvalid('Session name %s contains illegal character(s)')
                            ->end()
                        ->end()
                        ->scalarNode('cookie_lifetime')->defaultValue(\ini_get('session.gc_maxlifetime'))->end()
                        ->scalarNode('cookie_path')->end()
                        ->scalarNode('cookie_domain')->end()
                        ->enumNode('cookie_secure')->values([true, false])->end()
                        ->booleanNode('cookie_httponly')->defaultTrue()->end()
                        ->enumNode('cookie_samesite')
                            ->values([null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_NONE, Cookie::SAMESITE_STRICT])
                            ->defaultValue(Cookie::SAMESITE_LAX)
                        ->end()
                        ->booleanNode('use_cookies')->end()
                        ->scalarNode('gc_divisor')->end()
                        ->scalarNode('gc_probability')->defaultValue(1)->end()
                        ->scalarNode('gc_maxlifetime')->end()
                        ->scalarNode('save_path')->defaultValue('%project.var_dir%/sessions')->end()
                        ->scalarNode('meta_storage_key')->defaultValue('_rade_meta')->end()
                        ->integerNode('metadata_update_threshold')
                            ->defaultValue(0)
                            ->info('seconds to wait between 2 session metadata updates')
                        ->end()
                        ->integerNode('sid_length')
                            ->min(22)
                            ->max(256)
                        ->end()
                        ->integerNode('sid_bits_per_character')
                            ->min(4)
                            ->max(6)
                        ->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }

    /**
     * {@inheritdoc}
     */
    public function register(Container $container, array $configs = []): void
    {
        $definitions = [
            'http.middleware.headers' => service(HttpHeadersMiddleware::class, [\array_diff_key($configs['headers'], ['cors' => []])])->public(false),
        ];

        if (!$container->has('psr17.factory')) {
            $definitions['psr17.factory'] = service($configs['psr17_factory'] ?? Psr17Factory::class)->typed();
        }

        if (!$container->has('request_stack')) {
            $container->autowire('request_stack', service(RequestStack::class));
        }

        if ($configs['cookie']['enabled']) {
            unset($configs['cookie']['enabled']);
            $definitions += [
                'http.cookie' => $cookie = service(CookieFactory::class)->typed(CookieFactory::class, CookieFactoryInterface::class),
                'http.middleware.cookie' => $cookieMiddleware = service(CookiesMiddleware::class)->public(false),
            ];
            $cookies = [];

            foreach ($configs['cookie']['cookies'] as $cookieData) {
                $cookies[] = wrap(Cookie::class, $cookieData);
            }

            if (!empty($cookies)) {
                $cookie->bind('addCookie', [$cookies]);
            }

            if (!empty($excludeCookies = $configs['cookie']['excludes_encryption'])) {
                $cookieMiddleware->bind('excludeEncodingFor', $excludeCookies);
            }
        }

        if ($configs['policies']['enabled']) {
            unset($configs['policies']['enabled']);
            $definitions['http.middleware.policies'] = service(HttpPolicyMiddleware::class, [$configs['policies']])->public(false);
        }

        if ($configs['headers']['cors']['enabled']) {
            unset($configs['headers']['cors']['enabled']);
            $definitions['http.middleware.cors'] = service(HttpCorsMiddleware::class, [$configs['headers']['cors']])->public(false);
        }

        if ($configs['caching']['enabled']) {
            unset($configs['caching']['enabled']);
            $definitions['http.middleware.cache'] = service(CacheControlMiddleware::class, [3 => $configs['caching']])->public(false);
        }

        if (($session = $configs['session'])['enabled']) {
            unset($session['enabled']);

            if (!\extension_loaded('session')) {
                throw new \LogicException('Session support cannot be enabled as the session extension is not installed. See https://php.net/session.installation for instructions.');
            }

            $inConsole = $container instanceof KernelInterface && $container->isRunningInConsole();
            $metaBag = wrap(MetadataBag::class, [$session['meta_storage_key'], $session['metadata_update_threshold']]);
            $sessionArgs = \array_diff_key($session, ['storage_id' => null, 'handler_id' => null, 'meta_storage_key' => null, 'metadata_update_threshold' => null]);
            $container->multiple([
                'session.storage.native' => ($inConsole ? service(MockArraySessionStorage::class, [1 => $metaBag]) : service(NativeSessionStorage::class, [
                    $sessionArgs,
                    reference('session.handler'),
                    $metaBag,
                ]))->typed(),
                'session.handler.native_file' => ($inConsole ? service(NullSessionHandler::class) : service(NativeFileSessionHandler::class, [$session['save_path']]))->typed(),
                'http.middleware.session' => service(SessionMiddleware::class, [wrap(reference('http.session'), $sessionArgs, true)])->public(false),
                'http.session' => service(Session::class, [reference($session['storage_id'])])->typed(),
            ]);

            if ($container->has($session['handler_id'])) {
                $container->alias('session.handler', $session['handler_id']);
            } else {
                $definitions['session.handler'] = service([wrap(SessionHandlerFactory::class), 'createHandler'], [$session['handler_id']])->typed(AbstractSessionHandler::class);
            }
        }

        if ($configs['csrf_protection']['enabled']) {
            unset($configs['csrf_protection']['enabled']);

            if (!\class_exists(\Symfony\Component\Security\Csrf\CsrfToken::class)) {
                throw new \LogicException('CSRF support cannot be enabled as the Security CSRF component is not installed. Try running "composer require symfony/security-csrf".');
            }

            $definitions += [
                'http.csrf.token_storage' => ($container->has('http.session') ? service(SessionTokenStorage::class, [reference('request_stack')]) : service(NativeSessionTokenStorage::class))->typed(),
                'http.csrf.token_manager' => service(CsrfTokenManager::class, [1 => reference('http.csrf.token_storage'), 2 => reference('?request_stack')])->typed(),
            ];
        }

        $container->multiple($definitions);
    }
}