From f6761843b2ba739ab9d9afa263f6247854847d42 Mon Sep 17 00:00:00 2001 From: Sami Mazouz <sychocouldy@gmail.com> Date: Wed, 14 Sep 2022 18:10:30 +0100 Subject: [PATCH] feat: customizable session driver (#3610) --- framework/core/src/Extend/Session.php | 42 +++++++ .../src/Foundation/Console/InfoCommand.php | 70 ++++++++++- .../core/src/User/SessionDriverInterface.php | 27 ++++ framework/core/src/User/SessionManager.php | 46 +++++++ .../core/src/User/SessionServiceProvider.php | 46 +++++-- .../integration/extenders/SessionTest.php | 116 ++++++++++++++++++ .../testing/src/integration/TestCase.php | 2 +- 7 files changed, 340 insertions(+), 9 deletions(-) create mode 100644 framework/core/src/Extend/Session.php create mode 100644 framework/core/src/User/SessionDriverInterface.php create mode 100644 framework/core/src/User/SessionManager.php create mode 100644 framework/core/tests/integration/extenders/SessionTest.php diff --git a/framework/core/src/Extend/Session.php b/framework/core/src/Extend/Session.php new file mode 100644 index 000000000..83d4af8a1 --- /dev/null +++ b/framework/core/src/Extend/Session.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of Flarum. + * + * For detailed copyright and license information, please view the + * LICENSE file that was distributed with this source code. + */ + +namespace Flarum\Extend; + +use Flarum\Extension\Extension; +use Illuminate\Contracts\Container\Container; + +class Session implements ExtenderInterface +{ + private $drivers = []; + + /** + * Register a new session driver. + * + * A driver can currently be selected by setting `session.driver` in `config.php`. + * + * @param string $name: The name of the driver. + * @param string $driverClass: The ::class attribute of the driver. + * Driver must implement `\Flarum\User\SessionDriverInterface`. + * @return self + */ + public function driver(string $name, string $driverClass): self + { + $this->drivers[$name] = $driverClass; + + return $this; + } + + public function extend(Container $container, Extension $extension = null) + { + $container->extend('flarum.session.drivers', function ($drivers) { + return array_merge($drivers, $this->drivers); + }); + } +} diff --git a/framework/core/src/Foundation/Console/InfoCommand.php b/framework/core/src/Foundation/Console/InfoCommand.php index 383710bd5..f7bd3e80b 100644 --- a/framework/core/src/Foundation/Console/InfoCommand.php +++ b/framework/core/src/Foundation/Console/InfoCommand.php @@ -14,10 +14,14 @@ use Flarum\Extension\ExtensionManager; use Flarum\Foundation\Application; use Flarum\Foundation\Config; use Flarum\Settings\SettingsRepositoryInterface; +use Flarum\User\SessionManager; use Illuminate\Contracts\Queue\Queue; use Illuminate\Database\ConnectionInterface; +use Illuminate\Support\Arr; use Illuminate\Support\Str; +use InvalidArgumentException; use PDO; +use SessionHandlerInterface; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableStyle; @@ -48,18 +52,32 @@ class InfoCommand extends AbstractCommand */ private $queue; + /** + * @var SessionManager + */ + private $session; + + /** + * @var SessionHandlerInterface + */ + private $sessionHandler; + public function __construct( ExtensionManager $extensions, Config $config, SettingsRepositoryInterface $settings, ConnectionInterface $db, - Queue $queue + Queue $queue, + SessionManager $session, + SessionHandlerInterface $sessionHandler ) { $this->extensions = $extensions; $this->config = $config; $this->settings = $settings; $this->db = $db; $this->queue = $queue; + $this->session = $session; + $this->sessionHandler = $sessionHandler; parent::__construct(); } @@ -93,6 +111,7 @@ class InfoCommand extends AbstractCommand $this->output->writeln('<info>Base URL:</info> '.$this->config->url()); $this->output->writeln('<info>Installation path:</info> '.getcwd()); $this->output->writeln('<info>Queue driver:</info> '.$this->identifyQueueDriver()); + $this->output->writeln('<info>Session driver:</info> '.$this->identifySessionDriver()); $this->output->writeln('<info>Mail driver:</info> '.$this->settings->get('mail_driver', 'unknown')); $this->output->writeln('<info>Debug mode:</info> '.($this->config->inDebugMode() ? '<error>ON</error>' : 'off')); @@ -169,4 +188,53 @@ class InfoCommand extends AbstractCommand { return $this->db->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); } + + /** + * Reports on the session driver in use based on three scenarios: + * 1. If the configured session driver is valid and in use, it will be returned. + * 2. If the configured session driver is invalid, fallback to the default one and mention it. + * 3. If the actual used driver (i.e `session.handler`) is different from the current one (configured or default), mention it. + */ + private function identifySessionDriver(): string + { + /* + * Get the configured driver and fallback to the default one. + */ + $defaultDriver = $this->session->getDefaultDriver(); + $configuredDriver = Arr::get($this->config, 'session.driver', $defaultDriver); + $driver = $configuredDriver; + + try { + // Try to get the configured driver instance. + // Driver instances are created on demand. + $this->session->driver($configuredDriver); + } catch (InvalidArgumentException $e) { + // An exception is thrown if the configured driver is not a valid driver. + // So we fallback to the default driver. + $driver = $defaultDriver; + } + + /* + * Get actual driver name from its class name. + * And compare that to the current configured driver. + */ + // Get class name + $handlerName = get_class($this->sessionHandler); + // Drop the namespace + $handlerName = Str::afterLast($handlerName, '\\'); + // Lowercase the class name + $handlerName = strtolower($handlerName); + // Drop everything like sessionhandler FileSessionHandler, DatabaseSessionHandler ..etc + $handlerName = str_replace('sessionhandler', '', $handlerName); + + if ($driver !== $handlerName) { + return "$handlerName <comment>(Code override. Configured to <options=bold,underscore>$configuredDriver</>)</comment>"; + } + + if ($driver !== $configuredDriver) { + return "$driver <comment>(Fallback default driver. Configured to invalid driver <options=bold,underscore>$configuredDriver</>)</comment>"; + } + + return $driver; + } } diff --git a/framework/core/src/User/SessionDriverInterface.php b/framework/core/src/User/SessionDriverInterface.php new file mode 100644 index 000000000..7a0b284f7 --- /dev/null +++ b/framework/core/src/User/SessionDriverInterface.php @@ -0,0 +1,27 @@ +<?php + +/* + * This file is part of Flarum. + * + * For detailed copyright and license information, please view the + * LICENSE file that was distributed with this source code. + */ + +namespace Flarum\User; + +use Flarum\Foundation\Config; +use Flarum\Settings\SettingsRepositoryInterface; +use SessionHandlerInterface; + +interface SessionDriverInterface +{ + /** + * Build a session handler to handle sessions. + * Settings and configuration can either be pulled from the Flarum settings repository + * or the config.php file. + * + * @param SettingsRepositoryInterface $settings: An instance of the Flarum settings repository. + * @param Config $config: An instance of the wrapper class around `config.php`. + */ + public function build(SettingsRepositoryInterface $settings, Config $config): SessionHandlerInterface; +} diff --git a/framework/core/src/User/SessionManager.php b/framework/core/src/User/SessionManager.php new file mode 100644 index 000000000..f550cdc11 --- /dev/null +++ b/framework/core/src/User/SessionManager.php @@ -0,0 +1,46 @@ +<?php + +/* + * This file is part of Flarum. + * + * For detailed copyright and license information, please view the + * LICENSE file that was distributed with this source code. + */ + +namespace Flarum\User; + +use Flarum\Foundation\Config; +use Illuminate\Session\SessionManager as IlluminateSessionManager; +use Illuminate\Support\Arr; +use InvalidArgumentException; +use Psr\Log\LoggerInterface; +use SessionHandlerInterface; + +class SessionManager extends IlluminateSessionManager +{ + /** + * Returns the configured session handler. + * Picks up the driver from `config.php` using the `session.driver` item. + * Falls back to the default driver if the configured one is not available, + * and logs a critical error in that case. + */ + public function handler(): SessionHandlerInterface + { + $config = $this->container->make(Config::class); + $driverName = Arr::get($config, 'session.driver'); + + try { + $driverInstance = parent::driver($driverName); + } catch (InvalidArgumentException $e) { + $defaultDriverName = $this->getDefaultDriver(); + $driverInstance = parent::driver($defaultDriverName); + + // But we will log a critical error to the webmaster. + $this->container->make(LoggerInterface::class)->critical( + "The configured session driver [$driverName] is not available. Falling back to default [$defaultDriverName]. Please check your configuration." + ); + } + + return $driverInstance->getHandler(); + } +} diff --git a/framework/core/src/User/SessionServiceProvider.php b/framework/core/src/User/SessionServiceProvider.php index 2eb070547..e0f8023a2 100644 --- a/framework/core/src/User/SessionServiceProvider.php +++ b/framework/core/src/User/SessionServiceProvider.php @@ -10,7 +10,9 @@ namespace Flarum\User; use Flarum\Foundation\AbstractServiceProvider; -use Illuminate\Session\FileSessionHandler; +use Flarum\Foundation\Config; +use Flarum\Settings\SettingsRepositoryInterface; +use Illuminate\Contracts\Container\Container; use SessionHandlerInterface; class SessionServiceProvider extends AbstractServiceProvider @@ -20,12 +22,42 @@ class SessionServiceProvider extends AbstractServiceProvider */ public function register() { - $this->container->singleton('session.handler', function ($container) { - return new FileSessionHandler( - $container['files'], - $container['config']['session.files'], - $container['config']['session.lifetime'] - ); + $this->container->singleton('flarum.session.drivers', function () { + return []; + }); + + $this->container->singleton('session', function (Container $container) { + $manager = new SessionManager($container); + $drivers = $container->make('flarum.session.drivers'); + $settings = $container->make(SettingsRepositoryInterface::class); + $config = $container->make(Config::class); + + /** + * Default to the file driver already defined by Laravel. + * + * @see \Illuminate\Session\SessionManager::createFileDriver() + */ + $manager->setDefaultDriver('file'); + + foreach ($drivers as $driver => $className) { + /** @var SessionDriverInterface $driverInstance */ + $driverInstance = $container->make($className); + + $manager->extend($driver, function () use ($settings, $config, $driverInstance) { + return $driverInstance->build($settings, $config); + }); + } + + return $manager; + }); + + $this->container->alias('session', SessionManager::class); + + $this->container->singleton('session.handler', function (Container $container): SessionHandlerInterface { + /** @var SessionManager $manager */ + $manager = $container->make('session'); + + return $manager->handler(); }); $this->container->alias('session.handler', SessionHandlerInterface::class); diff --git a/framework/core/tests/integration/extenders/SessionTest.php b/framework/core/tests/integration/extenders/SessionTest.php new file mode 100644 index 000000000..82efc5456 --- /dev/null +++ b/framework/core/tests/integration/extenders/SessionTest.php @@ -0,0 +1,116 @@ +<?php + +/* + * This file is part of Flarum. + * + * For detailed copyright and license information, please view the + * LICENSE file that was distributed with this source code. + */ + +namespace Flarum\Tests\integration\extenders; + +use Flarum\Extend; +use Flarum\Foundation\Config; +use Flarum\Settings\SettingsRepositoryInterface; +use Flarum\Testing\integration\RetrievesAuthorizedUsers; +use Flarum\Testing\integration\TestCase; +use Flarum\User\SessionDriverInterface; +use Illuminate\Session\FileSessionHandler; +use Illuminate\Session\NullSessionHandler; +use InvalidArgumentException; +use SessionHandlerInterface; + +class SessionTest extends TestCase +{ + use RetrievesAuthorizedUsers; + + /** + * @test + */ + public function default_driver_exists_by_default() + { + $this->expectNotToPerformAssertions(); + $this->app()->getContainer()->make('session.handler'); + } + + /** + * @test + */ + public function custom_driver_doesnt_exist_by_default() + { + $this->expectException(InvalidArgumentException::class); + $this->app()->getContainer()->make('session')->driver('flarum-acme'); + } + + /** + * @test + */ + public function custom_driver_exists_if_added() + { + $this->extend((new Extend\Session())->driver('flarum-acme', AcmeSessionDriver::class)); + + $driver = $this->app()->getContainer()->make('session')->driver('flarum-acme'); + + $this->assertEquals(NullSessionHandler::class, get_class($driver->getHandler())); + } + + /** + * @test + */ + public function custom_driver_overrides_laravel_defined_drivers_if_added() + { + $this->extend((new Extend\Session())->driver('redis', AcmeSessionDriver::class)); + + $driver = $this->app()->getContainer()->make('session')->driver('redis'); + + $this->assertEquals(NullSessionHandler::class, get_class($driver->getHandler())); + } + + /** + * @test + */ + public function uses_default_driver_if_driver_from_config_file_not_configured() + { + $this->config('session.driver', null); + + $handler = $this->app()->getContainer()->make('session.handler'); + + $this->assertEquals(FileSessionHandler::class, get_class($handler)); + } + + /** + * @test + */ + public function uses_default_driver_if_configured_driver_from_config_file_unavailable() + { + $this->config('session.driver', 'nevergonnagiveyouup'); + + $handler = $this->app()->getContainer()->make('session.handler'); + + $this->assertEquals(FileSessionHandler::class, get_class($handler)); + } + + /** + * @test + */ + public function uses_custom_driver_from_config_file_if_configured_and_available() + { + $this->extend( + (new Extend\Session)->driver('flarum-acme', AcmeSessionDriver::class) + ); + + $this->config('session.driver', 'flarum-acme'); + + $handler = $this->app()->getContainer()->make('session.handler'); + + $this->assertEquals(NullSessionHandler::class, get_class($handler)); + } +} + +class AcmeSessionDriver implements SessionDriverInterface +{ + public function build(SettingsRepositoryInterface $settings, Config $config): SessionHandlerInterface + { + return new NullSessionHandler(); + } +} diff --git a/php-packages/testing/src/integration/TestCase.php b/php-packages/testing/src/integration/TestCase.php index 79a7a7f25..4efcec34c 100644 --- a/php-packages/testing/src/integration/TestCase.php +++ b/php-packages/testing/src/integration/TestCase.php @@ -148,7 +148,7 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase */ protected function config(string $key, $value) { - $this->config[$key] = $value; + Arr::set($this->config, $key, $value); } /**