feat: customizable session driver (#3610)

This commit is contained in:
Sami Mazouz 2022-09-14 18:10:30 +01:00 committed by GitHub
parent 84c31165e5
commit f6761843b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 340 additions and 9 deletions

View File

@ -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);
});
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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);
}
/**