diff --git a/administrator/includes/app.php b/administrator/includes/app.php index ee549bb6d3d13..ea56b1e27f8af 100644 --- a/administrator/includes/app.php +++ b/administrator/includes/app.php @@ -36,8 +36,25 @@ // Set profiler start time and memory usage and mark afterLoad in the profiler. JDEBUG ? JProfiler::getInstance('Application')->setStart($startTime, $startMem)->mark('afterLoad') : null; +// Boot the DI container +$container = \Joomla\CMS\Factory::getContainer(); + +/* + * Alias the session service keys to the web session service as that is the primary session backend for this application + * + * In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects + * is supported. This includes aliases for aliased class names, and the keys for alised class names should be considered + * deprecated to be removed when the class name alias is removed as well. + */ +$container->alias('session.web', 'session.web.administrator') + ->alias('session', 'session.web.administrator') + ->alias('JSession', 'session.web.administrator') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.administrator'); + // Instantiate the application. -$app = \Joomla\CMS\Factory::getContainer()->get(\Joomla\CMS\Application\AdministratorApplication::class); +$app = $container->get(\Joomla\CMS\Application\AdministratorApplication::class); // Set the application as global app \Joomla\CMS\Factory::$application = $app; diff --git a/cli/joomla.php b/cli/joomla.php index ec81556afc8cf..dc2f105942aeb 100644 --- a/cli/joomla.php +++ b/cli/joomla.php @@ -34,6 +34,22 @@ // Get the framework. require_once JPATH_BASE . '/includes/framework.php'; +// Boot the DI container +$container = \Joomla\CMS\Factory::getContainer(); + +/* + * Alias the session service keys to the CLI session service as that is the primary session backend for this application + * + * In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects + * is supported. This includes aliases for aliased class names, and the keys for alised class names should be considered + * deprecated to be removed when the class name alias is removed as well. + */ +$container->alias('session', 'session.cli') + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + $app = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Console\Application::class); \Joomla\CMS\Factory::$application = $app; $app->execute(); diff --git a/composer.json b/composer.json index c219c5b78636e..7b8a765c2a376 100644 --- a/composer.json +++ b/composer.json @@ -79,6 +79,7 @@ "symfony/console": "3.4.*", "symfony/debug": "3.4.*", "symfony/ldap": "3.4.*", + "symfony/options-resolver": "3.4.*", "symfony/web-link": "3.4.*", "symfony/yaml": "3.4.*", "wamania/php-stemmer": "^1.2" diff --git a/composer.lock b/composer.lock index f3564176ef0ce..3347bc4f7542e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "81c9ca521a0712b07e07143b469e835a", + "content-hash": "5749677e2afdf181fc07ea6b394cbe40", "packages": [ { "name": "composer/ca-bundle", @@ -3675,12 +3675,12 @@ "source": { "type": "git", "url": "https://github.com/joomla-projects/joomla-browser.git", - "reference": "9020a33c3b14aadb86057eb25b61305e63a15e86" + "reference": "13c12ee493a4f5bad482dd601e86db52b2918714" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla-projects/joomla-browser/zipball/9020a33c3b14aadb86057eb25b61305e63a15e86", - "reference": "9020a33c3b14aadb86057eb25b61305e63a15e86", + "url": "https://api.github.com/repos/joomla-projects/joomla-browser/zipball/13c12ee493a4f5bad482dd601e86db52b2918714", + "reference": "13c12ee493a4f5bad482dd601e86db52b2918714", "shasum": "" }, "require": { @@ -3718,7 +3718,7 @@ "acceptance testing", "joomla" ], - "time": "2018-05-15T13:46:14+00:00" + "time": "2018-08-06T11:31:54+00:00" }, { "name": "joomla-projects/robo-joomla", @@ -3861,12 +3861,12 @@ "source": { "type": "git", "url": "https://github.com/joomla/test-system.git", - "reference": "472c69aad1db7fbad67b07311ced5a21336366e1" + "reference": "0548840f05f4fcb0671f0ef1ff5acd978565ebb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla/test-system/zipball/472c69aad1db7fbad67b07311ced5a21336366e1", - "reference": "472c69aad1db7fbad67b07311ced5a21336366e1", + "url": "https://api.github.com/repos/joomla/test-system/zipball/0548840f05f4fcb0671f0ef1ff5acd978565ebb1", + "reference": "0548840f05f4fcb0671f0ef1ff5acd978565ebb1", "shasum": "" }, "require": { @@ -3887,7 +3887,7 @@ "cms", "joomla" ], - "time": "2018-05-15T13:20:55+00:00" + "time": "2018-07-28T21:13:03+00:00" }, { "name": "joomla/test-unit", diff --git a/includes/app.php b/includes/app.php index 56863041a55c4..d6933a504ec49 100644 --- a/includes/app.php +++ b/includes/app.php @@ -36,8 +36,25 @@ // Set profiler start time and memory usage and mark afterLoad in the profiler. JDEBUG ? JProfiler::getInstance('Application')->setStart($startTime, $startMem)->mark('afterLoad') : null; -// Get the application. -$app = \Joomla\CMS\Factory::getContainer()->get(\Joomla\CMS\Application\SiteApplication::class); +// Boot the DI container +$container = \Joomla\CMS\Factory::getContainer(); + +/* + * Alias the session service keys to the web session service as that is the primary session backend for this application + * + * In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects + * is supported. This includes aliases for aliased class names, and the keys for alised class names should be considered + * deprecated to be removed when the class name alias is removed as well. + */ +$container->alias('session.web', 'session.web.site') + ->alias('session', 'session.web.site') + ->alias('JSession', 'session.web.site') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.site'); + +// Instantiate the application. +$app = $container->get(\Joomla\CMS\Application\SiteApplication::class); // Set the application as global app \Joomla\CMS\Factory::$application = $app; diff --git a/installation/includes/app.php b/installation/includes/app.php index 3803de03eed9d..9f6846c7ea644 100644 --- a/installation/includes/app.php +++ b/installation/includes/app.php @@ -47,5 +47,19 @@ $container = \Joomla\CMS\Factory::getContainer(); $container->registerServiceProvider(new \Joomla\CMS\Installation\Service\Provider\Application); +/* + * Alias the session service keys to the web session service as that is the primary session backend for this application + * + * In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects + * is supported. This includes aliases for aliased class names, and the keys for alised class names should be considered + * deprecated to be removed when the class name alias is removed as well. + */ +$container->alias('session.web', 'session.web.installation') + ->alias('session', 'session.web.installation') + ->alias('JSession', 'session.web.installation') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.installation') + ->alias(\Joomla\Session\Session::class, 'session.web.installation') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.installation'); + // Instantiate and execute the application $container->get(\Joomla\CMS\Installation\Application\InstallationApplication::class)->execute(); diff --git a/libraries/src/Console/SessionGcCommand.php b/libraries/src/Console/SessionGcCommand.php index 6041d9665c5d8..82153cfe9b90f 100644 --- a/libraries/src/Console/SessionGcCommand.php +++ b/libraries/src/Console/SessionGcCommand.php @@ -11,36 +11,19 @@ defined('JPATH_PLATFORM') or die; use Joomla\Console\AbstractCommand; +use Joomla\DI\ContainerAwareInterface; +use Joomla\DI\ContainerAwareTrait; use Joomla\Session\SessionInterface; +use Symfony\Component\Console\Input\InputOption; /** * Console command for performing session garbage collection * * @since 4.0.0 */ -class SessionGcCommand extends AbstractCommand +class SessionGcCommand extends AbstractCommand implements ContainerAwareInterface { - /** - * The session object. - * - * @var SessionInterface - * @since 4.0.0 - */ - private $session; - - /** - * Instantiate the command. - * - * @param SessionInterface $session The session object. - * - * @since 4.0.0 - */ - public function __construct(SessionInterface $session) - { - $this->session = $session; - - parent::__construct(); - } + use ContainerAwareTrait; /** * Execute the command. @@ -55,7 +38,14 @@ public function execute(): int $symfonyStyle->title('Running Session Garbage Collection'); - if ($this->session->gc() === false) + $session = $this->getSessionService($this->getApplication()->getConsoleInput()->getOption('application')); + + $gcResult = $session->gc(); + + // Destroy the session started for this process + $session->destroy(); + + if ($gcResult === false) { $symfonyStyle->error('Garbage collection was not completed. Either the operation failed or is not supported on your platform.'); @@ -78,12 +68,42 @@ protected function initialise() { $this->setName('session:gc'); $this->setDescription('Performs session garbage collection'); + $this->addOption('application', 'app', InputOption::VALUE_OPTIONAL, 'The application to perform garbage collection for.', 'site'); $this->setHelp( <<%command.name% command runs PHP's garbage collection operation for session data php %command.full_name% + +This command defaults to performing garbage collection for the frontend (site) application. To run garbage collection +for another application, you can specify it with the --application option. + +php %command.full_name% --application=[APPLICATION] EOF ); } + + /** + * Get the session service for the requested application. + * + * @param string $application The application session service to retrieve + * + * @return SessionInterface + * + * @since 4.0.0 + */ + private function getSessionService(string $application): SessionInterface + { + if (!$this->getContainer()->has("session.web.$application")) + { + throw new \InvalidArgumentException( + sprintf( + 'The `%s` application is not a valid option.', + $application + ) + ); + } + + return $this->getContainer()->get("session.web.$application"); + } } diff --git a/libraries/src/Service/Provider/Console.php b/libraries/src/Service/Provider/Console.php index 42ab38ad3ed56..696a8d58e5a44 100644 --- a/libraries/src/Service/Provider/Console.php +++ b/libraries/src/Service/Provider/Console.php @@ -38,7 +38,14 @@ public function register(Container $container) SessionGcCommand::class, function (Container $container) { - return new SessionGcCommand($container->get('session')); + /* + * The command will need the same session handler that web apps use to run correctly, + * since this is based on an option we need to inject the container + */ + $command = new SessionGcCommand; + $command->setContainer($container); + + return $command; }, true ); diff --git a/libraries/src/Service/Provider/Session.php b/libraries/src/Service/Provider/Session.php index 2d73080f48c5c..c2e3d9ec97204 100644 --- a/libraries/src/Service/Provider/Session.php +++ b/libraries/src/Service/Provider/Session.php @@ -11,21 +11,25 @@ defined('JPATH_PLATFORM') or die; -use InvalidArgumentException; +use Joomla\CMS\Application\AdministratorApplication; use Joomla\CMS\Application\ApplicationHelper; +use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Application\ConsoleApplication; +use Joomla\CMS\Application\SiteApplication; use Joomla\CMS\Factory; +use Joomla\CMS\Installation\Application\InstallationApplication; +use Joomla\CMS\Session\SessionFactory; use Joomla\CMS\Session\Storage\JoomlaStorage; -use Joomla\Database\DatabaseDriver; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; -use Joomla\Session\Handler; +use Joomla\Event\DispatcherInterface; +use Joomla\Registry\Registry; use Joomla\Session\SessionEvents; +use Joomla\Session\SessionInterface; use Joomla\Session\Storage\RuntimeStorage; +use Joomla\Session\StorageInterface; use Joomla\Session\Validator\AddressValidator; use Joomla\Session\Validator\ForwardedValidator; -use Memcached; -use Redis; -use RuntimeException; /** * Service provider for the application's session dependency @@ -45,174 +49,177 @@ class Session implements ServiceProviderInterface */ public function register(Container $container) { - $container->alias('session', 'Joomla\Session\SessionInterface') - ->alias('JSession', 'Joomla\Session\SessionInterface') - ->alias('Joomla\Session\Session', 'Joomla\Session\SessionInterface') - ->share( - 'Joomla\Session\SessionInterface', - function (Container $container) + $container->share( + 'session.web.administrator', + function (Container $container) + { + /** @var Registry $config */ + $config = $container->get('config'); + $app = Factory::getApplication(); + + // Generate a session name. + $name = ApplicationHelper::getHash($config->get('session_name', AdministratorApplication::class)); + + // Calculate the session lifetime. + $lifetime = $config->get('lifetime') ? $config->get('lifetime') * 60 : 900; + + // Initialize the options for the Session object. + $options = [ + 'name' => $name, + 'expire' => $lifetime, + ]; + + if ($config->get('force_ssl') >= 1) { - $config = $container->get('config'); - $app = Factory::getApplication(); - - // Generate a session name. - $name = ApplicationHelper::getHash($config->get('session_name', get_class($app))); - - // Calculate the session lifetime. - $lifetime = (($config->get('lifetime')) ? $config->get('lifetime') * 60 : 900); - - // Initialize the options for the Session object. - $options = array( - 'name' => $name, - 'expire' => $lifetime - ); - - if ($app->isClient('site') && $config->get('force_ssl') == 2) - { - $options['force_ssl'] = true; - } - - if ($app->isClient('administrator') && $config->get('force_ssl') >= 1) - { - $options['force_ssl'] = true; - } - - // Set up the storage handler - $handlerType = $config->get('session_handler', 'filesystem'); - - switch ($handlerType) - { - case 'apcu': - if (!Handler\ApcuHandler::isSupported()) - { - throw new RuntimeException('APCu is not supported on this system.'); - } - - $handler = new Handler\ApcuHandler; - - break; - - case 'database': - $handler = new Handler\DatabaseHandler($container->get(DatabaseDriver::class)); - - break; - - case 'filesystem': - case 'none': - // Try to use a custom configured path, fall back to the path in the PHP runtime configuration - $path = $config->get('session_filesystem_path', ini_get('session.save_path')); - - // If we still have no path, as a last resort fall back to the system's temporary directory - if (empty($path)) - { - $path = sys_get_temp_dir(); - } - - $handler = new Handler\FilesystemHandler($path); - - break; - - case 'memcached': - if (!Handler\MemcachedHandler::isSupported()) - { - throw new RuntimeException('Memcached is not supported on this system.'); - } - - $host = $config->get('session_memcached_server_host', 'localhost'); - $port = $config->get('session_memcached_server_port', 11211); - - $memcached = new Memcached($config->get('session_memcached_server_id', 'joomla_cms')); - $memcached->addServer($host, $port); - - $handler = new Handler\MemcachedHandler($memcached, ['ttl' => $lifetime]); - - ini_set('session.save_path', "$host:$port"); - ini_set('session.save_handler', 'memcached'); - - break; - - case 'redis': - if (!Handler\RedisHandler::isSupported()) - { - throw new RuntimeException('Redis is not supported on this system.'); - } - - $redis = new Redis; - $host = $config->get('session_redis_server_host', '127.0.0.1'); - - // Use default port if connecting over a socket whatever the config value - $port = $host[0] === '/' ? $config->get('session_redis_server_port', 6379) : 6379; - - if ($config->get('session_redis_persist', true)) - { - $redis->pconnect( - $host, - $port - ); - } - else - { - $redis->connect( - $host, - $port - ); - } - - if (!empty($config->get('session_redis_server_auth', ''))) - { - $redis->auth($config->get('session_redis_server_auth', null)); - } - - $db = (int) $config->get('session_redis_server_db', 0); - - if ($db !== 0) - { - $redis->select($db); - } - - $handler = new Handler\RedisHandler($redis, ['ttl' => $lifetime]); - - break; - - case 'wincache': - if (!Handler\WincacheHandler::isSupported()) - { - throw new RuntimeException('Wincache is not supported on this system.'); - } - - $handler = new Handler\WincacheHandler; - - break; + $options['force_ssl'] = true; + } + + return $this->buildSession( + new JoomlaStorage($app->input, $container->get('session.factory')->createSessionHandler($options)), + $app, + $container->get(DispatcherInterface::class), + $options + ); + }, + true + ); + + $container->share( + 'session.web.installation', + function (Container $container) + { + /** @var Registry $config */ + $config = $container->get('config'); + $app = Factory::getApplication(); + + // Generate a session name. + $name = ApplicationHelper::getHash($config->get('session_name', InstallationApplication::class)); + + // Calculate the session lifetime. + $lifetime = $config->get('lifetime') ? $config->get('lifetime') * 60 : 900; + + // Initialize the options for the Session object. + $options = [ + 'name' => $name, + 'expire' => $lifetime, + ]; + + return $this->buildSession( + new JoomlaStorage($app->input, $container->get('session.factory')->createSessionHandler($options)), + $app, + $container->get(DispatcherInterface::class), + $options + ); + }, + true + ); + + $container->share( + 'session.web.site', + function (Container $container) + { + /** @var Registry $config */ + $config = $container->get('config'); + $app = Factory::getApplication(); + + // Generate a session name. + $name = ApplicationHelper::getHash($config->get('session_name', SiteApplication::class)); + + // Calculate the session lifetime. + $lifetime = $config->get('lifetime') ? $config->get('lifetime') * 60 : 900; + + // Initialize the options for the Session object. + $options = [ + 'name' => $name, + 'expire' => $lifetime, + ]; + + if ($config->get('force_ssl') == 2) + { + $options['force_ssl'] = true; + } + + return $this->buildSession( + new JoomlaStorage($app->input, $container->get('session.factory')->createSessionHandler($options)), + $app, + $container->get(DispatcherInterface::class), + $options + ); + }, + true + ); + + $container->share( + 'session.cli', + function (Container $container) + { + /** @var Registry $config */ + $config = $container->get('config'); + $app = Factory::getApplication(); + + // Generate a session name. + $name = ApplicationHelper::getHash($config->get('session_name', ConsoleApplication::class)); + + // Calculate the session lifetime. + $lifetime = $config->get('lifetime') ? $config->get('lifetime') * 60 : 900; + + // Initialize the options for the Session object. + $options = [ + 'name' => $name, + 'expire' => $lifetime, + ]; + + // Unlike the web apps, we will only toggle the force SSL setting based on it being enabled and not based on client + if ($config->get('force_ssl') >= 1) + { + $options['force_ssl'] = true; + } - default: - throw new InvalidArgumentException(sprintf('The "%s" session handler is not recognised.', $handlerType)); - } + return $this->buildSession(new RuntimeStorage, $app, $container->get(DispatcherInterface::class), $options); + }, + true + ); - $input = $app->input; + $container->alias(SessionFactory::class, 'session.factory') + ->share( + 'session.factory', + function (Container $container) + { + $factory = new SessionFactory; + $factory->setContainer($container); - if ($app->isClient('cli')) - { - $storage = new RuntimeStorage; - } - else - { - $storage = new JoomlaStorage($input, $handler); - } + return $factory; + }, + true + ); + } - $dispatcher = $container->get('Joomla\Event\DispatcherInterface'); + /** + * Build the root session service + * + * @param StorageInterface $storage The session storage engine. + * @param CMSApplicationInterface $app The application instance. + * @param DispatcherInterface $dispatcher The event dispatcher. + * @param array $options The configured session options. + * + * @return SessionInterface + * + * @since 4.0 + */ + private function buildSession(StorageInterface $storage, CMSApplicationInterface $app, DispatcherInterface $dispatcher, + array $options): SessionInterface + { + $input = $app->input; - if (method_exists($app, 'afterSessionStart')) - { - $dispatcher->addListener(SessionEvents::START, array($app, 'afterSessionStart')); - } + if (method_exists($app, 'afterSessionStart')) + { + $dispatcher->addListener(SessionEvents::START, [$app, 'afterSessionStart']); + } - $session = new \Joomla\CMS\Session\Session($storage, $dispatcher, $options); - $session->addValidator(new AddressValidator($input, $session)); - $session->addValidator(new ForwardedValidator($input, $session)); + $session = new \Joomla\CMS\Session\Session($storage, $dispatcher, $options); + $session->addValidator(new AddressValidator($input, $session)); + $session->addValidator(new ForwardedValidator($input, $session)); - return $session; - }, - true - ); + return $session; } } diff --git a/libraries/src/Session/SessionFactory.php b/libraries/src/Session/SessionFactory.php new file mode 100644 index 0000000000000..bdbcea7b9647e --- /dev/null +++ b/libraries/src/Session/SessionFactory.php @@ -0,0 +1,176 @@ +configureSessionHandlerOptions($resolver); + + $options = $resolver->resolve($options); + + /** @var Registry $config */ + $config = $this->getContainer()->get('config'); + + $handlerType = $config->get('session_handler', 'filesystem'); + + switch ($handlerType) + { + case 'apcu': + if (!Handler\ApcuHandler::isSupported()) + { + throw new RuntimeException('APCu is not supported on this system.'); + } + + return new Handler\ApcuHandler; + + case 'database': + return new Handler\DatabaseHandler($this->getContainer()->get(DatabaseInterface::class)); + + case 'filesystem': + case 'none': + // Try to use a custom configured path, fall back to the path in the PHP runtime configuration + $path = $config->get('session_filesystem_path', ini_get('session.save_path')); + + // If we still have no path, as a last resort fall back to the system's temporary directory + if (empty($path)) + { + $path = sys_get_temp_dir(); + } + + return new Handler\FilesystemHandler($path); + + case 'memcached': + if (!Handler\MemcachedHandler::isSupported()) + { + throw new RuntimeException('Memcached is not supported on this system.'); + } + + $host = $config->get('session_memcached_server_host', 'localhost'); + $port = $config->get('session_memcached_server_port', 11211); + + $memcached = new Memcached($config->get('session_memcached_server_id', 'joomla_cms')); + $memcached->addServer($host, $port); + + ini_set('session.save_path', "$host:$port"); + ini_set('session.save_handler', 'memcached'); + + return new Handler\MemcachedHandler($memcached, ['ttl' => $options['expire']]); + + case 'redis': + if (!Handler\RedisHandler::isSupported()) + { + throw new RuntimeException('Redis is not supported on this system.'); + } + + $redis = new Redis; + $host = $config->get('session_redis_server_host', '127.0.0.1'); + + // Use default port if connecting over a socket whatever the config value + $port = $host[0] === '/' ? $config->get('session_redis_server_port', 6379) : 6379; + + if ($config->get('session_redis_persist', true)) + { + $redis->pconnect( + $host, + $port + ); + } + else + { + $redis->connect( + $host, + $port + ); + } + + if (!empty($config->get('session_redis_server_auth', ''))) + { + $redis->auth($config->get('session_redis_server_auth', null)); + } + + $db = (int) $config->get('session_redis_server_db', 0); + + if ($db !== 0) + { + $redis->select($db); + } + + return new Handler\RedisHandler($redis, ['ttl' => $options['expire']]); + + case 'wincache': + if (!Handler\WincacheHandler::isSupported()) + { + throw new RuntimeException('Wincache is not supported on this system.'); + } + + return new Handler\WincacheHandler; + + default: + throw new InvalidArgumentException(sprintf('The "%s" session handler is not recognised.', $handlerType)); + } + } + + /** + * Resolve the options for the session handler. + * + * @param OptionsResolver $resolver The options resolver. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected function configureSessionHandlerOptions(OptionsResolver $resolver) + { + $resolver->setDefaults( + [ + 'force_ssl' => false, + ] + ); + + $resolver->setRequired(['name', 'expire']); + + $resolver->setAllowedTypes('name', ['string']); + $resolver->setAllowedTypes('expire', ['int']); + $resolver->setAllowedTypes('force_ssl', ['bool']); + } +}