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 @@ +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('Base URL: '.$this->config->url()); $this->output->writeln('Installation path: '.getcwd()); $this->output->writeln('Queue driver: '.$this->identifyQueueDriver()); + $this->output->writeln('Session driver: '.$this->identifySessionDriver()); $this->output->writeln('Mail driver: '.$this->settings->get('mail_driver', 'unknown')); $this->output->writeln('Debug mode: '.($this->config->inDebugMode() ? 'ON' : '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 (Code override. Configured to $configuredDriver)"; + } + + if ($driver !== $configuredDriver) { + return "$driver (Fallback default driver. Configured to invalid driver $configuredDriver)"; + } + + 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 @@ +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 @@ +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); } /**