From 790d5beee5e283178716bc8f9901c758d9e5b6a0 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Wed, 24 Oct 2018 22:33:45 +0200 Subject: [PATCH 01/23] Split up the installer logic This is probably the most complicated way I could find to fix #1587. Jokes aside, this was done with a few goals in mind: - Reduce coupling between the installer and the rest of Flarum's "Application", which we are building during installation. - Move the installer logic to several smaller classes, which can then be used by the web frontend and the console task, instead of the former hacking its way into the latter to be "DRY". - Separate installer infrastructure (the "pipeline", with the ability to revert steps upon failure) from the actual steps being taken. The problem was conceptual, and would certainly re-occur in a similar fashion if we wouldn't tackle it at its roots. It is fixed now, because we no longer use the ExtensionManager for enabling extensions, but instead duplicate some of its logic. That is fine because we don't want to do everything it does, e.g. omit extenders' lifecycle hooks (which depend on the Application instance being complete). > for each desired change, make the change easy (warning: this may be > hard), then make the easy change - Kent Beck, https://twitter.com/kentbeck/status/250733358307500032 Fixes #1587. --- src/Extension/Extension.php | 38 ++ src/Extension/ExtensionManager.php | 18 +- src/Install/Console/DefaultsDataProvider.php | 17 +- src/Install/Console/InstallCommand.php | 337 ++++-------------- src/Install/Console/UserDataProvider.php | 17 +- src/Install/Controller/IndexController.php | 17 +- src/Install/Controller/InstallController.php | 92 ++--- src/Install/InstallServiceProvider.php | 36 +- src/Install/Installation.php | 178 +++++++++ src/Install/Installer.php | 7 +- src/Install/Pipeline.php | 108 ++++++ src/Install/ReversibleStep.php | 17 + src/Install/Step.php | 33 ++ src/Install/StepFailed.php | 18 + src/Install/Steps/BuildConfig.php | 75 ++++ src/Install/Steps/ConnectToDatabase.php | 57 +++ src/Install/Steps/CreateAdminUser.php | 63 ++++ src/Install/Steps/EnableBundledExtensions.php | 112 ++++++ src/Install/Steps/PublishAssets.php | 47 +++ src/Install/Steps/RunMigrations.php | 60 ++++ src/Install/Steps/StoreConfig.php | 46 +++ src/Install/Steps/WriteSettings.php | 52 +++ 22 files changed, 1077 insertions(+), 368 deletions(-) create mode 100644 src/Install/Installation.php create mode 100644 src/Install/Pipeline.php create mode 100644 src/Install/ReversibleStep.php create mode 100644 src/Install/Step.php create mode 100644 src/Install/StepFailed.php create mode 100644 src/Install/Steps/BuildConfig.php create mode 100644 src/Install/Steps/ConnectToDatabase.php create mode 100644 src/Install/Steps/CreateAdminUser.php create mode 100644 src/Install/Steps/EnableBundledExtensions.php create mode 100644 src/Install/Steps/PublishAssets.php create mode 100644 src/Install/Steps/RunMigrations.php create mode 100644 src/Install/Steps/StoreConfig.php create mode 100644 src/Install/Steps/WriteSettings.php diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index a85792961..a657c4e95 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -11,12 +11,18 @@ namespace Flarum\Extension; +use Flarum\Database\Migrator; use Flarum\Extend\Compat; use Flarum\Extend\LifecycleInterface; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use League\Flysystem\Adapter\Local; +use League\Flysystem\Filesystem; +use League\Flysystem\FilesystemInterface; +use League\Flysystem\MountManager; +use League\Flysystem\Plugin\ListFiles; /** * @property string $name @@ -307,6 +313,25 @@ class Extension implements Arrayable return realpath($this->path.'/assets/') !== false; } + public function copyAssetsTo(FilesystemInterface $target) + { + if (! $this->hasAssets()) { + return; + } + + $mount = new MountManager([ + 'source' => $source = new Filesystem(new Local($this->getPath().'/assets')), + 'target' => $target, + ]); + + $source->addPlugin(new ListFiles); + $assetFiles = $source->listFiles('/', true); + + foreach ($assetFiles as $file) { + $mount->copy("source://$file[path]", "target://extensions/$this->id/$file[path]"); + } + } + /** * Tests whether the extension has migrations. * @@ -317,6 +342,19 @@ class Extension implements Arrayable return realpath($this->path.'/migrations/') !== false; } + public function migrate(Migrator $migrator, $direction = 'up') + { + if (! $this->hasMigrations()) { + return; + } + + if ($direction == 'up') { + return $migrator->run($this->getPath().'/migrations', $this); + } else { + return $migrator->reset($this->getPath().'/migrations', $this); + } + } + /** * Generates an array result for the object. * diff --git a/src/Extension/ExtensionManager.php b/src/Extension/ExtensionManager.php index 45decbe94..221418367 100644 --- a/src/Extension/ExtensionManager.php +++ b/src/Extension/ExtensionManager.php @@ -222,26 +222,16 @@ class ExtensionManager * Runs the database migrations for the extension. * * @param Extension $extension - * @param bool|true $up + * @param string $direction * @return void */ - public function migrate(Extension $extension, $up = true) + public function migrate(Extension $extension, $direction = 'up') { - if (! $extension->hasMigrations()) { - return; - } - - $migrationDir = $extension->getPath().'/migrations'; - $this->app->bind('Illuminate\Database\Schema\Builder', function ($container) { return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder(); }); - if ($up) { - $this->migrator->run($migrationDir, $extension); - } else { - $this->migrator->reset($migrationDir, $extension); - } + $extension->migrate($this->migrator, $direction); } /** @@ -252,7 +242,7 @@ class ExtensionManager */ public function migrateDown(Extension $extension) { - return $this->migrate($extension, false); + return $this->migrate($extension, 'down'); } /** diff --git a/src/Install/Console/DefaultsDataProvider.php b/src/Install/Console/DefaultsDataProvider.php index 70f300071..62841faea 100644 --- a/src/Install/Console/DefaultsDataProvider.php +++ b/src/Install/Console/DefaultsDataProvider.php @@ -14,13 +14,16 @@ namespace Flarum\Install\Console; class DefaultsDataProvider implements DataProviderInterface { protected $databaseConfiguration = [ - 'driver' => 'mysql', - 'host' => 'localhost', - 'database' => 'flarum', - 'username' => 'root', - 'password' => '', - 'prefix' => '', - 'port' => '3306', + 'driver' => 'mysql', + 'host' => 'localhost', + 'database' => 'flarum', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'port' => '3306', + 'strict' => false, ]; protected $debug = false; diff --git a/src/Install/Console/InstallCommand.php b/src/Install/Console/InstallCommand.php index 37cd34655..e429d62d8 100644 --- a/src/Install/Console/InstallCommand.php +++ b/src/Install/Console/InstallCommand.php @@ -11,65 +11,41 @@ namespace Flarum\Install\Console; -use Carbon\Carbon; use Exception; use Flarum\Console\AbstractCommand; -use Flarum\Database\DatabaseMigrationRepository; -use Flarum\Database\Migrator; -use Flarum\Extension\ExtensionManager; -use Flarum\Foundation\Application as FlarumApplication; -use Flarum\Foundation\Site; -use Flarum\Group\Group; -use Flarum\Install\Prerequisite\PrerequisiteInterface; -use Flarum\Settings\DatabaseSettingsRepository; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Contracts\Foundation\Application; -use Illuminate\Contracts\Translation\Translator; -use Illuminate\Database\ConnectionInterface; -use Illuminate\Database\Connectors\ConnectionFactory; -use Illuminate\Filesystem\Filesystem; -use Illuminate\Hashing\BcryptHasher; -use Illuminate\Validation\Factory; -use PDO; +use Flarum\Install\Installation; +use Flarum\Install\Pipeline; +use Flarum\Install\Step; +use Illuminate\Contracts\Validation\Factory; use Symfony\Component\Console\Input\InputOption; class InstallCommand extends AbstractCommand { + /** + * @var Installation + */ + protected $installation; + + /** + * @var Factory + */ + protected $validator; + /** * @var DataProviderInterface */ protected $dataSource; /** - * @var Application + * @param Installation $installation + * @param Factory $validator */ - protected $application; - - /** - * @var Filesystem - */ - protected $filesystem; - - /** - * @var ConnectionInterface - */ - protected $db; - - /** - * @var Migrator - */ - protected $migrator; - - /** - * @param Application $application - * @param Filesystem $filesystem - */ - public function __construct(Application $application, Filesystem $filesystem) + public function __construct(Installation $installation, Factory $validator) { - $this->application = $application; + $this->installation = $installation; + $this->validator = $validator; parent::__construct(); - $this->filesystem = $filesystem; } protected function configure() @@ -104,7 +80,7 @@ class InstallCommand extends AbstractCommand { $this->init(); - $prerequisites = $this->getPrerequisites(); + $prerequisites = $this->installation->prerequisites(); $prerequisites->check(); $errors = $prerequisites->getErrors(); @@ -124,245 +100,82 @@ class InstallCommand extends AbstractCommand protected function init() { - if ($this->dataSource === null) { - if ($this->input->getOption('defaults')) { - $this->dataSource = new DefaultsDataProvider(); - } elseif ($this->input->getOption('file')) { - $this->dataSource = new FileDataProvider($this->input); - } else { - $this->dataSource = new UserDataProvider($this->input, $this->output, $this->getHelperSet()->get('question')); - } + if ($this->input->getOption('defaults')) { + $this->dataSource = new DefaultsDataProvider(); + } elseif ($this->input->getOption('file')) { + $this->dataSource = new FileDataProvider($this->input); + } else { + $this->dataSource = new UserDataProvider($this->input, $this->output, $this->getHelperSet()->get('question')); } } - public function setDataSource(DataProviderInterface $dataSource) - { - $this->dataSource = $dataSource; - } - protected function install() { - try { - $this->dbConfig = $this->dataSource->getDatabaseConfiguration(); + $dbConfig = $this->dataSource->getDatabaseConfiguration(); - $validation = $this->getValidator()->make( - $this->dbConfig, - [ - 'driver' => 'required|in:mysql', - 'host' => 'required', - 'database' => 'required|string', - 'username' => 'required|string', - 'prefix' => 'nullable|alpha_dash|max:10', - 'port' => 'nullable|integer|min:1|max:65535', - ] - ); - - if ($validation->fails()) { - throw new Exception(implode("\n", call_user_func_array('array_merge', $validation->getMessageBag()->toArray()))); - } - - $this->baseUrl = $this->dataSource->getBaseUrl(); - $this->settings = $this->dataSource->getSettings(); - $this->adminUser = $admin = $this->dataSource->getAdminUser(); - - if (strlen($admin['password']) < 8) { - throw new Exception('Password must be at least 8 characters.'); - } - - if ($admin['password'] !== $admin['password_confirmation']) { - throw new Exception('The password did not match its confirmation.'); - } - - if (! filter_var($admin['email'], FILTER_VALIDATE_EMAIL)) { - throw new Exception('You must enter a valid email.'); - } - - if (! $admin['username'] || preg_match('/[^a-z0-9_-]/i', $admin['username'])) { - throw new Exception('Username can only contain letters, numbers, underscores, and dashes.'); - } - - $this->storeConfiguration($this->dataSource->isDebugMode()); - - $this->runMigrations(); - - $this->writeSettings(); - - $this->createAdminUser(); - - $this->publishAssets(); - - // Now that the installation of core is complete, boot up a new - // application instance before enabling extensions so that all of - // the application services are available. - Site::fromPaths([ - 'base' => $this->application->basePath(), - 'public' => $this->application->publicPath(), - 'storage' => $this->application->storagePath(), - ])->bootApp(); - - $this->application = FlarumApplication::getInstance(); - - $this->enableBundledExtensions(); - } catch (Exception $e) { - @unlink($this->getConfigFile()); - - throw $e; - } - } - - protected function storeConfiguration(bool $debugMode) - { - $dbConfig = $this->dbConfig; - - $config = [ - 'debug' => $debugMode, - 'database' => $laravelDbConfig = [ - 'driver' => $dbConfig['driver'], - 'host' => $dbConfig['host'], - 'database' => $dbConfig['database'], - 'username' => $dbConfig['username'], - 'password' => $dbConfig['password'], - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => $dbConfig['prefix'], - 'port' => $dbConfig['port'], - 'strict' => false - ], - 'url' => $this->baseUrl, - 'paths' => [ - 'api' => 'api', - 'admin' => 'admin', - ], - ]; - - $this->info('Testing config'); - - $factory = new ConnectionFactory($this->application); - - $laravelDbConfig['engine'] = 'InnoDB'; - - $this->db = $factory->make($laravelDbConfig); - $version = $this->db->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); - - if (version_compare($version, '5.5.0', '<')) { - throw new Exception('MySQL version too low. You need at least MySQL 5.5.'); - } - - $repository = new DatabaseMigrationRepository( - $this->db, 'migrations' + $validation = $this->validator->make( + $dbConfig, + [ + 'driver' => 'required|in:mysql', + 'host' => 'required', + 'database' => 'required|string', + 'username' => 'required|string', + 'prefix' => 'nullable|alpha_dash|max:10', + 'port' => 'nullable|integer|min:1|max:65535', + ] ); - $files = $this->application->make('files'); - $this->migrator = new Migrator($repository, $this->db, $files); - - $this->info('Writing config'); - - file_put_contents( - $this->getConfigFile(), - 'migrator->setOutput($this->output); - $this->migrator->getRepository()->createRepository(); - $this->migrator->run(__DIR__.'/../../../migrations'); - } - - protected function writeSettings() - { - $settings = new DatabaseSettingsRepository($this->db); - - $this->info('Writing default settings'); - - $settings->set('version', $this->application->version()); - - foreach ($this->settings as $k => $v) { - $settings->set($k, $v); + if ($validation->fails()) { + throw new Exception(implode("\n", + call_user_func_array('array_merge', + $validation->getMessageBag()->toArray()))); } - } - protected function createAdminUser() - { - $admin = $this->adminUser; + $admin = $this->dataSource->getAdminUser(); + + if (strlen($admin['password']) < 8) { + throw new Exception('Password must be at least 8 characters.'); + } if ($admin['password'] !== $admin['password_confirmation']) { throw new Exception('The password did not match its confirmation.'); } - $this->info('Creating admin user '.$admin['username']); - - $uid = $this->db->table('users')->insertGetId([ - 'username' => $admin['username'], - 'email' => $admin['email'], - 'password' => (new BcryptHasher)->make($admin['password']), - 'joined_at' => Carbon::now(), - 'is_email_confirmed' => 1, - ]); - - $this->db->table('group_user')->insert([ - 'user_id' => $uid, - 'group_id' => Group::ADMINISTRATOR_ID, - ]); - } - - protected function enableBundledExtensions() - { - $extensions = new ExtensionManager( - new DatabaseSettingsRepository($this->db), - $this->application, - $this->migrator, - $this->application->make(Dispatcher::class), - $this->application->make('files') - ); - - $disabled = [ - 'flarum-akismet', - 'flarum-auth-facebook', - 'flarum-auth-github', - 'flarum-auth-twitter', - 'flarum-pusher', - ]; - - foreach ($extensions->getExtensions() as $name => $extension) { - if (in_array($name, $disabled)) { - continue; - } - - $this->info('Enabling extension: '.$name); - - $extensions->enable($name); + if (! filter_var($admin['email'], FILTER_VALIDATE_EMAIL)) { + throw new Exception('You must enter a valid email.'); } - } - protected function publishAssets() - { - $this->filesystem->copyDirectory( - $this->application->basePath().'/vendor/components/font-awesome/webfonts', - $this->application->publicPath().'/assets/fonts' + if (! $admin['username'] || preg_match('/[^a-z0-9_-]/i', + $admin['username'])) { + throw new Exception('Username can only contain letters, numbers, underscores, and dashes.'); + } + + $this->runPipeline( + $this->installation + ->configPath($this->input->getOption('config')) + ->debugMode($this->dataSource->isDebugMode()) + ->baseUrl($this->dataSource->getBaseUrl()) + ->databaseConfig($dbConfig) + ->adminUser($admin) + ->settings($this->dataSource->getSettings()) + ->build() ); } - protected function getConfigFile() + private function runPipeline(Pipeline $pipeline) { - return $this->input->getOption('config') ?: base_path('config.php'); - } - - /** - * @return \Flarum\Install\Prerequisite\PrerequisiteInterface - */ - protected function getPrerequisites() - { - return $this->application->make(PrerequisiteInterface::class); - } - - /** - * @return \Illuminate\Contracts\Validation\Factory - */ - protected function getValidator() - { - return new Factory($this->application->make(Translator::class)); + $pipeline + ->on('start', function (Step $step) { + $this->output->write($step->getMessage().'...'); + })->on('end', function () { + $this->output->write("done\n"); + })->on('fail', function () { + $this->output->write("failed\n"); + $this->output->writeln('Rolling back...'); + })->on('rollback', function (Step $step) { + $this->output->writeln($step->getMessage().' (rollback)'); + }) + ->run(); } protected function showErrors($errors) diff --git a/src/Install/Console/UserDataProvider.php b/src/Install/Console/UserDataProvider.php index a84df2050..eb10ef5ba 100644 --- a/src/Install/Console/UserDataProvider.php +++ b/src/Install/Console/UserDataProvider.php @@ -43,13 +43,16 @@ class UserDataProvider implements DataProviderInterface } return [ - 'driver' => 'mysql', - 'host' => $host, - 'port' => $port, - 'database' => $this->ask('Database name:'), - 'username' => $this->ask('Database user:'), - 'password' => $this->secret('Database password:'), - 'prefix' => $this->ask('Prefix:'), + 'driver' => 'mysql', + 'host' => $host, + 'port' => $port, + 'database' => $this->ask('Database name:'), + 'username' => $this->ask('Database user:'), + 'password' => $this->secret('Database password:'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => $this->ask('Prefix:'), + 'strict' => false, ]; } diff --git a/src/Install/Controller/IndexController.php b/src/Install/Controller/IndexController.php index e96110969..03b1e2449 100644 --- a/src/Install/Controller/IndexController.php +++ b/src/Install/Controller/IndexController.php @@ -12,7 +12,7 @@ namespace Flarum\Install\Controller; use Flarum\Http\Controller\AbstractHtmlController; -use Flarum\Install\Prerequisite\PrerequisiteInterface; +use Flarum\Install\Installation; use Illuminate\Contracts\View\Factory; use Psr\Http\Message\ServerRequestInterface as Request; @@ -24,18 +24,18 @@ class IndexController extends AbstractHtmlController protected $view; /** - * @var \Flarum\Install\Prerequisite\PrerequisiteInterface + * @var Installation */ - protected $prerequisite; + protected $installation; /** * @param Factory $view - * @param PrerequisiteInterface $prerequisite + * @param Installation $installation */ - public function __construct(Factory $view, PrerequisiteInterface $prerequisite) + public function __construct(Factory $view, Installation $installation) { $this->view = $view; - $this->prerequisite = $prerequisite; + $this->installation = $installation; } /** @@ -46,8 +46,9 @@ class IndexController extends AbstractHtmlController { $view = $this->view->make('flarum.install::app')->with('title', 'Install Flarum'); - $this->prerequisite->check(); - $errors = $this->prerequisite->getErrors(); + $prerequisites = $this->installation->prerequisites(); + $prerequisites->check(); + $errors = $prerequisites->getErrors(); if (count($errors)) { $view->with('content', $this->view->make('flarum.install::errors')->with('errors', $errors)); diff --git a/src/Install/Controller/InstallController.php b/src/Install/Controller/InstallController.php index aaa13e277..835a6e312 100644 --- a/src/Install/Controller/InstallController.php +++ b/src/Install/Controller/InstallController.php @@ -13,19 +13,18 @@ namespace Flarum\Install\Controller; use Exception; use Flarum\Http\SessionAuthenticator; -use Flarum\Install\Console\DefaultsDataProvider; -use Flarum\Install\Console\InstallCommand; +use Flarum\Install\Installation; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface; -use Symfony\Component\Console\Input\StringInput; -use Symfony\Component\Console\Output\StreamOutput; use Zend\Diactoros\Response; -use Zend\Diactoros\Response\HtmlResponse; class InstallController implements RequestHandlerInterface { - protected $command; + /** + * @var Installation + */ + protected $installation; /** * @var SessionAuthenticator @@ -34,12 +33,12 @@ class InstallController implements RequestHandlerInterface /** * InstallController constructor. - * @param InstallCommand $command + * @param Installation $installation * @param SessionAuthenticator $authenticator */ - public function __construct(InstallCommand $command, SessionAuthenticator $authenticator) + public function __construct(Installation $installation, SessionAuthenticator $authenticator) { - $this->command = $command; + $this->installation = $installation; $this->authenticator = $authenticator; } @@ -51,8 +50,6 @@ class InstallController implements RequestHandlerInterface { $input = $request->getParsedBody(); - $data = new DefaultsDataProvider; - $host = array_get($input, 'mysqlHost'); $port = '3306'; @@ -60,45 +57,58 @@ class InstallController implements RequestHandlerInterface list($host, $port) = explode(':', $host, 2); } - $data->setDatabaseConfiguration([ - 'driver' => 'mysql', - 'host' => $host, - 'database' => array_get($input, 'mysqlDatabase'), - 'username' => array_get($input, 'mysqlUsername'), - 'password' => array_get($input, 'mysqlPassword'), - 'prefix' => array_get($input, 'tablePrefix'), - 'port' => $port, - ]); - - $data->setAdminUser([ - 'username' => array_get($input, 'adminUsername'), - 'password' => array_get($input, 'adminPassword'), - 'password_confirmation' => array_get($input, 'adminPasswordConfirmation'), - 'email' => array_get($input, 'adminEmail'), - ]); - $baseUrl = rtrim((string) $request->getUri(), '/'); - $data->setBaseUrl($baseUrl); - $data->setSetting('forum_title', array_get($input, 'forumTitle')); - $data->setSetting('mail_from', 'noreply@'.preg_replace('/^www\./i', '', parse_url($baseUrl, PHP_URL_HOST))); - $data->setSetting('welcome_title', 'Welcome to '.array_get($input, 'forumTitle')); - - $body = fopen('php://temp', 'wb+'); - $input = new StringInput(''); - $output = new StreamOutput($body); - - $this->command->setDataSource($data); + $pipeline = $this->installation + ->baseUrl($baseUrl) + ->databaseConfig([ + 'driver' => 'mysql', + 'host' => $host, + 'port' => $port, + 'database' => array_get($input, 'mysqlDatabase'), + 'username' => array_get($input, 'mysqlUsername'), + 'password' => array_get($input, 'mysqlPassword'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => array_get($input, 'tablePrefix'), + 'strict' => false, + ]) + ->adminUser([ + 'username' => array_get($input, 'adminUsername'), + 'password' => array_get($input, 'adminPassword'), + 'password_confirmation' => array_get($input, 'adminPasswordConfirmation'), + 'email' => array_get($input, 'adminEmail'), + ]) + ->settings([ + 'allow_post_editing' => 'reply', + 'allow_renaming' => '10', + 'allow_sign_up' => '1', + 'custom_less' => '', + 'default_locale' => 'en', + 'default_route' => '/all', + 'extensions_enabled' => '[]', + 'forum_title' => array_get($input, 'forumTitle'), + 'forum_description' => '', + 'mail_driver' => 'mail', + 'mail_from' => 'noreply@'.preg_replace('/^www\./i', '', parse_url($baseUrl, PHP_URL_HOST)), + 'theme_colored_header' => '0', + 'theme_dark_mode' => '0', + 'theme_primary_color' => '#4D698E', + 'theme_secondary_color' => '#4D698E', + 'welcome_message' => 'This is beta software and you should not use it in production.', + 'welcome_title' => 'Welcome to '.array_get($input, 'forumTitle'), + ]) + ->build(); try { - $this->command->run($input, $output); + $pipeline->run(); } catch (Exception $e) { - return new HtmlResponse($e->getMessage(), 500); + return new Response\HtmlResponse($e->getMessage(), 500); } $session = $request->getAttribute('session'); $this->authenticator->logIn($session, 1); - return new Response($body); + return new Response\EmptyResponse; } } diff --git a/src/Install/InstallServiceProvider.php b/src/Install/InstallServiceProvider.php index 4a0c8ae03..1ea4fa07b 100644 --- a/src/Install/InstallServiceProvider.php +++ b/src/Install/InstallServiceProvider.php @@ -14,11 +14,6 @@ namespace Flarum\Install; use Flarum\Foundation\AbstractServiceProvider; use Flarum\Http\RouteCollection; use Flarum\Http\RouteHandlerFactory; -use Flarum\Install\Prerequisite\Composite; -use Flarum\Install\Prerequisite\PhpExtensions; -use Flarum\Install\Prerequisite\PhpVersion; -use Flarum\Install\Prerequisite\PrerequisiteInterface; -use Flarum\Install\Prerequisite\WritablePaths; class InstallServiceProvider extends AbstractServiceProvider { @@ -27,32 +22,17 @@ class InstallServiceProvider extends AbstractServiceProvider */ public function register() { - $this->app->bind( - PrerequisiteInterface::class, - function () { - return new Composite( - new PhpVersion('7.1.0'), - new PhpExtensions([ - 'dom', - 'gd', - 'json', - 'mbstring', - 'openssl', - 'pdo_mysql', - 'tokenizer', - ]), - new WritablePaths([ - base_path(), - public_path('assets'), - storage_path(), - ]) - ); - } - ); - $this->app->singleton('flarum.install.routes', function () { return new RouteCollection; }); + + $this->app->singleton(Installation::class, function () { + return new Installation( + $this->app->basePath(), + $this->app->publicPath(), + $this->app->storagePath() + ); + }); } /** diff --git a/src/Install/Installation.php b/src/Install/Installation.php new file mode 100644 index 000000000..0da25ca9f --- /dev/null +++ b/src/Install/Installation.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install; + +use Flarum\Install\Prerequisite\Composite; +use Flarum\Install\Prerequisite\PhpExtensions; +use Flarum\Install\Prerequisite\PhpVersion; +use Flarum\Install\Prerequisite\PrerequisiteInterface; +use Flarum\Install\Prerequisite\WritablePaths; +use Flarum\Install\Steps\BuildConfig; +use Flarum\Install\Steps\ConnectToDatabase; +use Flarum\Install\Steps\CreateAdminUser; +use Flarum\Install\Steps\EnableBundledExtensions; +use Flarum\Install\Steps\PublishAssets; +use Flarum\Install\Steps\RunMigrations; +use Flarum\Install\Steps\StoreConfig; +use Flarum\Install\Steps\WriteSettings; + +class Installation +{ + private $basePath; + private $publicPath; + private $storagePath; + + private $configPath; + private $debug = false; + private $dbConfig = []; + private $baseUrl; + private $defaultSettings = []; + private $adminUser = []; + + public function __construct($basePath, $publicPath, $storagePath) + { + $this->basePath = $basePath; + $this->publicPath = $publicPath; + $this->storagePath = $storagePath; + } + + public function configPath($path) + { + $this->configPath = $path; + + return $this; + } + + public function debugMode($flag) + { + $this->debug = $flag; + + return $this; + } + + public function databaseConfig(array $dbConfig) + { + $this->dbConfig = $dbConfig; + + return $this; + } + + public function baseUrl($baseUrl) + { + $this->baseUrl = $baseUrl; + + return $this; + } + + public function settings($settings) + { + $this->defaultSettings = $settings; + + return $this; + } + + public function adminUser($admin) + { + $this->adminUser = $admin; + + return $this; + } + + public function prerequisites(): PrerequisiteInterface + { + return new Composite( + new PhpVersion('7.1.0'), + new PhpExtensions([ + 'dom', + 'gd', + 'json', + 'mbstring', + 'openssl', + 'pdo_mysql', + 'tokenizer', + ]), + new WritablePaths([ + $this->basePath, + $this->getAssetPath(), + $this->storagePath, + ]) + ); + } + + public function build(): Pipeline + { + $pipeline = new Pipeline; + + // A new array to persist some objects between steps. + // It's an instance variable so that access in closures is easier. :) + $this->tmp = []; + + $pipeline->pipe(function () { + return new BuildConfig( + $this->debug, $this->dbConfig, $this->baseUrl, + function ($config) { + $this->tmp['config'] = $config; + } + ); + }); + + $pipeline->pipe(function () { + return new ConnectToDatabase( + $this->dbConfig, + function ($connection) { + $this->tmp['db'] = $connection; + } + ); + }); + + $pipeline->pipe(function () { + return new StoreConfig($this->tmp['config'], $this->getConfigPath()); + }); + + $pipeline->pipe(function () { + return new RunMigrations($this->tmp['db'], $this->getMigrationPath()); + }); + + $pipeline->pipe(function () { + return new WriteSettings($this->tmp['db'], $this->defaultSettings); + }); + + $pipeline->pipe(function () { + return new CreateAdminUser($this->tmp['db'], $this->adminUser); + }); + + $pipeline->pipe(function () { + return new PublishAssets($this->basePath, $this->getAssetPath()); + }); + + $pipeline->pipe(function () { + return new EnableBundledExtensions($this->tmp['db'], $this->basePath, $this->getAssetPath()); + }); + + return $pipeline; + } + + private function getConfigPath() + { + return $this->basePath.'/'.($this->configPath ?? 'config.php'); + } + + private function getAssetPath() + { + return "$this->publicPath/assets"; + } + + private function getMigrationPath() + { + return __DIR__.'/../../migrations'; + } +} diff --git a/src/Install/Installer.php b/src/Install/Installer.php index 3df6a1b86..287a513fe 100644 --- a/src/Install/Installer.php +++ b/src/Install/Installer.php @@ -17,6 +17,8 @@ use Flarum\Http\Middleware\HandleErrorsWithWhoops; use Flarum\Http\Middleware\StartSession; use Flarum\Install\Console\InstallCommand; use Illuminate\Contracts\Container\Container; +use Illuminate\Contracts\Translation\Translator; +use Illuminate\Validation\Factory; use Zend\Stratigility\MiddlewarePipe; class Installer implements AppInterface @@ -52,7 +54,10 @@ class Installer implements AppInterface public function getConsoleCommands() { return [ - $this->container->make(InstallCommand::class), + new InstallCommand( + $this->container->make(Installation::class), + new Factory($this->container->make(Translator::class)) + ), ]; } } diff --git a/src/Install/Pipeline.php b/src/Install/Pipeline.php new file mode 100644 index 000000000..31c8cb1c9 --- /dev/null +++ b/src/Install/Pipeline.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install; + +use Exception; +use SplStack; + +class Pipeline +{ + /** + * @var callable[] + */ + private $steps; + + /** + * @var callable[] + */ + private $callbacks; + + /** + * @var SplStack + */ + private $successfulSteps; + + public function __construct(array $steps = []) + { + $this->steps = $steps; + } + + public function pipe(callable $factory) + { + $this->steps[] = $factory; + + return $this; + } + + public function on($event, callable $callback) + { + $this->callbacks[$event] = $callback; + + return $this; + } + + public function run() + { + $this->successfulSteps = new SplStack; + + try { + foreach ($this->steps as $factory) { + $this->runStep($factory); + } + } catch (StepFailed $failure) { + $this->revertReversibleSteps(); + + throw $failure; + } + } + + /** + * @param callable $factory + * @throws StepFailed + */ + private function runStep(callable $factory) + { + /** @var Step $step */ + $step = $factory(); + + $this->fireCallbacks('start', $step); + + try { + $step->run(); + $this->successfulSteps->push($step); + + $this->fireCallbacks('end', $step); + } catch (Exception $e) { + $this->fireCallbacks('fail', $step); + + throw new StepFailed('Step failed', 0, $e); + } + } + + private function revertReversibleSteps() + { + foreach ($this->successfulSteps as $step) { + if ($step instanceof ReversibleStep) { + $this->fireCallbacks('rollback', $step); + + $step->revert(); + } + } + } + + private function fireCallbacks($event, Step $step) + { + if (isset($this->callbacks[$event])) { + ($this->callbacks[$event])($step); + } + } +} diff --git a/src/Install/ReversibleStep.php b/src/Install/ReversibleStep.php new file mode 100644 index 000000000..b65de4993 --- /dev/null +++ b/src/Install/ReversibleStep.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install; + +interface ReversibleStep +{ + public function revert(); +} diff --git a/src/Install/Step.php b/src/Install/Step.php new file mode 100644 index 000000000..6b0de2658 --- /dev/null +++ b/src/Install/Step.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install; + +interface Step +{ + /** + * A one-line status message summarizing what's happening in this step. + * + * @return string + */ + public function getMessage(); + + /** + * Do the work that constitutes this step. + * + * This method should raise a `StepFailed` exception whenever something goes + * wrong that should result in the entire installation being reverted. + * + * @return void + * @throws StepFailed + */ + public function run(); +} diff --git a/src/Install/StepFailed.php b/src/Install/StepFailed.php new file mode 100644 index 000000000..8fe1a3b7d --- /dev/null +++ b/src/Install/StepFailed.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install; + +use Exception; + +class StepFailed extends Exception +{ +} diff --git a/src/Install/Steps/BuildConfig.php b/src/Install/Steps/BuildConfig.php new file mode 100644 index 000000000..bab1a160e --- /dev/null +++ b/src/Install/Steps/BuildConfig.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install\Steps; + +use Flarum\Install\Step; + +class BuildConfig implements Step +{ + private $debugMode; + + private $dbConfig; + + private $baseUrl; + + private $store; + + public function __construct($debugMode, $dbConfig, $baseUrl, callable $store) + { + $this->debugMode = $debugMode; + $this->dbConfig = $dbConfig; + $this->baseUrl = $baseUrl; + + $this->store = $store; + } + + public function getMessage() + { + return 'Building config array'; + } + + public function run() + { + $config = [ + 'debug' => $this->debugMode, + 'database' => $this->getDatabaseConfig(), + 'url' => $this->baseUrl, + 'paths' => $this->getPathsConfig(), + ]; + + ($this->store)($config); + } + + private function getDatabaseConfig() + { + return [ + 'driver' => $this->dbConfig['driver'], + 'host' => $this->dbConfig['host'], + 'database' => $this->dbConfig['database'], + 'username' => $this->dbConfig['username'], + 'password' => $this->dbConfig['password'], + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => $this->dbConfig['prefix'], + 'port' => $this->dbConfig['port'], + 'strict' => false, + ]; + } + + private function getPathsConfig() + { + return [ + 'api' => 'api', + 'admin' => 'admin', + ]; + } +} diff --git a/src/Install/Steps/ConnectToDatabase.php b/src/Install/Steps/ConnectToDatabase.php new file mode 100644 index 000000000..1f8357b25 --- /dev/null +++ b/src/Install/Steps/ConnectToDatabase.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install\Steps; + +use Flarum\Install\Step; +use Illuminate\Database\Connectors\MySqlConnector; +use Illuminate\Database\MySqlConnection; +use PDO; +use RangeException; + +class ConnectToDatabase implements Step +{ + private $dbConfig; + private $store; + + public function __construct($dbConfig, callable $store) + { + $this->dbConfig = $dbConfig; + $this->dbConfig['engine'] = 'InnoDB'; + + $this->store = $store; + } + + public function getMessage() + { + return 'Connecting to database'; + } + + public function run() + { + $pdo = (new MySqlConnector)->connect($this->dbConfig); + + $version = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); + + if (version_compare($version, '5.5.0', '<')) { + throw new RangeException('MySQL version too low. You need at least MySQL 5.5.'); + } + + ($this->store)( + new MySqlConnection( + $pdo, + $this->dbConfig['database'], + $this->dbConfig['prefix'], + $this->dbConfig + ) + ); + } +} diff --git a/src/Install/Steps/CreateAdminUser.php b/src/Install/Steps/CreateAdminUser.php new file mode 100644 index 000000000..af51fa04d --- /dev/null +++ b/src/Install/Steps/CreateAdminUser.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install\Steps; + +use Carbon\Carbon; +use Flarum\Group\Group; +use Flarum\Install\Step; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Hashing\BcryptHasher; +use UnexpectedValueException; + +class CreateAdminUser implements Step +{ + /** + * @var ConnectionInterface + */ + private $database; + + /** + * @var array + */ + private $admin; + + public function __construct(ConnectionInterface $database, array $admin) + { + $this->database = $database; + $this->admin = $admin; + } + + public function getMessage() + { + return 'Creating admin user '.$this->admin['username']; + } + + public function run() + { + if ($this->admin['password'] !== $this->admin['password_confirmation']) { + throw new UnexpectedValueException('The password did not match its confirmation.'); + } + + $uid = $this->database->table('users')->insertGetId([ + 'username' => $this->admin['username'], + 'email' => $this->admin['email'], + 'password' => (new BcryptHasher)->make($this->admin['password']), + 'joined_at' => Carbon::now(), + 'is_email_confirmed' => 1, + ]); + + $this->database->table('group_user')->insert([ + 'user_id' => $uid, + 'group_id' => Group::ADMINISTRATOR_ID, + ]); + } +} diff --git a/src/Install/Steps/EnableBundledExtensions.php b/src/Install/Steps/EnableBundledExtensions.php new file mode 100644 index 000000000..f3f045b0a --- /dev/null +++ b/src/Install/Steps/EnableBundledExtensions.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install\Steps; + +use Flarum\Database\DatabaseMigrationRepository; +use Flarum\Database\Migrator; +use Flarum\Extension\Extension; +use Flarum\Install\Step; +use Flarum\Settings\DatabaseSettingsRepository; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Support\Arr; +use League\Flysystem\Adapter\Local; +use League\Flysystem\Filesystem; + +class EnableBundledExtensions implements Step +{ + /** + * @var ConnectionInterface + */ + private $database; + + /** + * @var string + */ + private $basePath; + + /** + * @var string + */ + private $assetPath; + + public function __construct(ConnectionInterface $database, $basePath, $assetPath) + { + $this->database = $database; + $this->basePath = $basePath; + $this->assetPath = $assetPath; + } + + public function getMessage() + { + return 'Enabling bundled extensions'; + } + + public function run() + { + $extensions = $this->loadExtensions(); + + foreach ($extensions as $extension) { + $extension->migrate($this->getMigrator()); + $extension->copyAssetsTo( + new Filesystem(new Local($this->assetPath)) + ); + } + + (new DatabaseSettingsRepository($this->database))->set( + 'extensions_enabled', + $extensions->keys()->toJson() + ); + } + + const DISABLED_EXTENSIONS = [ + 'flarum-akismet', + 'flarum-auth-facebook', + 'flarum-auth-github', + 'flarum-auth-twitter', + 'flarum-pusher', + ]; + + /** + * @return \Illuminate\Support\Collection + */ + private function loadExtensions() + { + $json = file_get_contents("$this->basePath/vendor/composer/installed.json"); + + return collect(json_decode($json, true)) + ->filter(function ($package) { + return Arr::get($package, 'type') == 'flarum-extension'; + })->filter(function ($package) { + return ! empty(Arr::get($package, 'name')); + })->map(function ($package) { + $extension = new Extension($this->basePath.'/vendor/'.Arr::get($package, 'name'), $package); + $extension->setVersion(Arr::get($package, 'version')); + + return $extension; + })->filter(function (Extension $extension) { + return ! in_array($extension->getId(), self::DISABLED_EXTENSIONS); + })->sortBy(function (Extension $extension) { + return $extension->composerJsonAttribute('extra.flarum-extension.title'); + })->mapWithKeys(function (Extension $extension) { + return [$extension->getId() => $extension]; + }); + } + + private function getMigrator() + { + return $this->migrator = $this->migrator ?? new Migrator( + new DatabaseMigrationRepository($this->database, 'migrations'), + $this->database, + new \Illuminate\Filesystem\Filesystem + ); + } +} diff --git a/src/Install/Steps/PublishAssets.php b/src/Install/Steps/PublishAssets.php new file mode 100644 index 000000000..7ec1d767a --- /dev/null +++ b/src/Install/Steps/PublishAssets.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install\Steps; + +use Flarum\Install\Step; +use Illuminate\Filesystem\Filesystem; + +class PublishAssets implements Step +{ + /** + * @var string + */ + private $basePath; + + /** + * @var string + */ + private $assetPath; + + public function __construct($basePath, $assetPath) + { + $this->basePath = $basePath; + $this->assetPath = $assetPath; + } + + public function getMessage() + { + return 'Publishing all assets'; + } + + public function run() + { + (new Filesystem)->copyDirectory( + "$this->basePath/vendor/components/font-awesome/webfonts", + "$this->assetPath/fonts" + ); + } +} diff --git a/src/Install/Steps/RunMigrations.php b/src/Install/Steps/RunMigrations.php new file mode 100644 index 000000000..3bf2049ef --- /dev/null +++ b/src/Install/Steps/RunMigrations.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install\Steps; + +use Flarum\Database\DatabaseMigrationRepository; +use Flarum\Database\Migrator; +use Flarum\Install\Step; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Filesystem\Filesystem; + +class RunMigrations implements Step +{ + /** + * @var ConnectionInterface + */ + private $database; + + /** + * @var string + */ + private $path; + + public function __construct(ConnectionInterface $database, $path) + { + $this->database = $database; + $this->path = $path; + } + + public function getMessage() + { + return 'Running migrations'; + } + + public function run() + { + $migrator = $this->getMigrator(); + + $migrator->getRepository()->createRepository(); + $migrator->run($this->path); + } + + private function getMigrator() + { + $repository = new DatabaseMigrationRepository( + $this->database, 'migrations' + ); + $files = new Filesystem; + + return new Migrator($repository, $this->database, $files); + } +} diff --git a/src/Install/Steps/StoreConfig.php b/src/Install/Steps/StoreConfig.php new file mode 100644 index 000000000..f2c2a22f3 --- /dev/null +++ b/src/Install/Steps/StoreConfig.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install\Steps; + +use Flarum\Install\ReversibleStep; +use Flarum\Install\Step; + +class StoreConfig implements Step, ReversibleStep +{ + private $config; + + private $configFile; + + public function __construct(array $config, $configFile) + { + $this->config = $config; + $this->configFile = $configFile; + } + + public function getMessage() + { + return 'Writing config file'; + } + + public function run() + { + file_put_contents( + $this->configFile, + 'config, true).';' + ); + } + + public function revert() + { + @unlink($this->configFile); + } +} diff --git a/src/Install/Steps/WriteSettings.php b/src/Install/Steps/WriteSettings.php new file mode 100644 index 000000000..657e838d9 --- /dev/null +++ b/src/Install/Steps/WriteSettings.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install\Steps; + +use Flarum\Foundation\Application; +use Flarum\Install\Step; +use Flarum\Settings\DatabaseSettingsRepository; +use Illuminate\Database\ConnectionInterface; + +class WriteSettings implements Step +{ + /** + * @var ConnectionInterface + */ + private $database; + + /** + * @var array + */ + private $defaults; + + public function __construct(ConnectionInterface $database, array $defaults) + { + $this->database = $database; + $this->defaults = $defaults; + } + + public function getMessage() + { + return 'Writing default settings'; + } + + public function run() + { + $repo = new DatabaseSettingsRepository($this->database); + + $repo->set('version', Application::VERSION); + + foreach ($this->defaults as $key => $value) { + $repo->set($key, $value); + } + } +} From de6001f4cf46b06a43848a999d586530f7cff627 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Wed, 24 Oct 2018 23:16:29 +0200 Subject: [PATCH 02/23] Fix the test setup and installer tests We are still testing the installation logic, but not testing the actual CLI task. I would love to do that, but IMO we first need to find a way to do this fully from the outside, by invoking and talking to the installer through the shell. Because acceptance tests are easier to do when fully decoupled from the application. (After all, they are intended to save us from breaking things when changing code; and we cannot prove that when we change the tests at the same time.) It might be easier to start with acceptance tests for the web installer, though. --- .../DefaultInstallationCommandTest.php | 59 ------------ tests/Install/DefaultInstallationTest.php | 91 +++++++++++++++++++ tests/Test/Concerns/CreatesForum.php | 60 +++++------- 3 files changed, 114 insertions(+), 96 deletions(-) delete mode 100644 tests/Install/DefaultInstallationCommandTest.php create mode 100644 tests/Install/DefaultInstallationTest.php diff --git a/tests/Install/DefaultInstallationCommandTest.php b/tests/Install/DefaultInstallationCommandTest.php deleted file mode 100644 index 0d2fee829..000000000 --- a/tests/Install/DefaultInstallationCommandTest.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Tests\Install; - -use Flarum\Install\Console\InstallCommand; -use Flarum\Tests\Test\TestCase; -use Illuminate\Database\Connectors\ConnectionFactory; -use Symfony\Component\Console\Input\StringInput; -use Symfony\Component\Console\Output\StreamOutput; - -class DefaultInstallationCommandTest extends TestCase -{ - protected $isInstalled = false; - - /** - * @test - */ - public function allows_forum_installation() - { - if (file_exists(base_path('config.php'))) { - unlink(base_path('config.php')); - } - - /** @var InstallCommand $command */ - $command = app(InstallCommand::class); - $command->setDataSource($this->configuration); - - $body = fopen('php://temp', 'wb+'); - $input = new StringInput(''); - $output = new StreamOutput($body); - - $command->run($input, $output); - - $this->assertFileExists(base_path('config.php')); - - $admin = $this->configuration->getAdminUser(); - - $this->assertEquals( - $this->getDatabase()->table('users')->find(1)->username, - $admin['username'] - ); - } - - private function getDatabase() - { - $factory = new ConnectionFactory(app()); - - return $factory->make($this->configuration->getDatabaseConfiguration()); - } -} diff --git a/tests/Install/DefaultInstallationTest.php b/tests/Install/DefaultInstallationTest.php new file mode 100644 index 000000000..b7507ea7b --- /dev/null +++ b/tests/Install/DefaultInstallationTest.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Tests\Install; + +use Flarum\Install\Installation; +use Flarum\Tests\Test\TestCase; +use Illuminate\Database\Connectors\ConnectionFactory; + +class DefaultInstallationTest extends TestCase +{ + protected $isInstalled = false; + + /** + * @test + */ + public function allows_forum_installation() + { + if (file_exists(base_path('config.php'))) { + unlink(base_path('config.php')); + } + + /** @var Installation $installation */ + $installation = app(Installation::class); + + $installation + ->debugMode(true) + ->baseUrl('http://flarum.local') + ->databaseConfig($this->getDatabaseConfiguration()) + ->adminUser($this->getAdmin()) + ->settings($this->getSettings()) + ->build()->run(); + + $this->assertFileExists(base_path('config.php')); + + $admin = $this->getAdmin(); + + $this->assertEquals( + $this->getDatabase()->table('users')->find(1)->username, + $admin['username'] + ); + } + + private function getDatabase() + { + $factory = new ConnectionFactory(app()); + + return $factory->make($this->getDatabaseConfiguration()); + } + + private function getAdmin() + { + return [ + 'username' => 'admin', + 'password' => 'password', + 'password_confirmation' => 'password', + 'email' => 'admin@example.com', + ]; + } + + private function getSettings() + { + return [ + 'allow_post_editing' => 'reply', + 'allow_renaming' => '10', + 'allow_sign_up' => '1', + 'custom_less' => '', + 'default_locale' => 'en', + 'default_route' => '/all', + 'extensions_enabled' => '[]', + 'forum_title' => 'Development Forum', + 'forum_description' => '', + 'mail_driver' => 'log', + 'mail_from' => 'noreply@flarum.dev', + 'theme_colored_header' => '0', + 'theme_dark_mode' => '0', + 'theme_primary_color' => '#4D698E', + 'theme_secondary_color' => '#4D698E', + 'welcome_message' => 'This is beta software and you should not use it in production.', + 'welcome_title' => 'Welcome to Development Forum', + ]; + } +} diff --git a/tests/Test/Concerns/CreatesForum.php b/tests/Test/Concerns/CreatesForum.php index a16b2269d..9046aac0c 100644 --- a/tests/Test/Concerns/CreatesForum.php +++ b/tests/Test/Concerns/CreatesForum.php @@ -19,9 +19,10 @@ use Flarum\Foundation\SiteInterface; use Flarum\Foundation\UninstalledSite; use Flarum\Http\Server; use Flarum\Install\Console\DataProviderInterface; -use Flarum\Install\Console\DefaultsDataProvider; use Illuminate\Database\ConnectionInterface; -use Illuminate\Database\Connectors\ConnectionFactory; +use Illuminate\Database\Connectors\MySqlConnector; +use Illuminate\Database\MySqlConnection; +use Illuminate\Filesystem\Filesystem; trait CreatesForum { @@ -72,26 +73,24 @@ trait CreatesForum $this->app = $this->site->bootApp(); } - protected function collectsConfiguration() + protected function getDatabaseConfiguration() { - $this->configuration = new DefaultsDataProvider(); - - $this->configuration->setDebugMode(); - $this->configuration->setSetting('mail_driver', 'log'); - - $database = $this->configuration->getDatabaseConfiguration(); - $database['host'] = env('DB_HOST', $database['host']); - $database['database'] = env('DB_DATABASE', $database['database']); - $database['username'] = env('DB_USERNAME', $database['username']); - $database['password'] = env('DB_PASSWORD', $database['password']); - $database['prefix'] = env('DB_PREFIX', $database['prefix']); - $this->configuration->setDatabaseConfiguration($database); + return [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', 'localhost'), + 'database' => env('DB_DATABASE', 'flarum'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => env('DB_PREFIX', ''), + 'port' => '3306', + 'strict' => false, + ]; } protected function refreshApplication() { - $this->collectsConfiguration(); - $this->seedsDatabase(); $this->createsSite(); @@ -108,23 +107,10 @@ trait CreatesForum protected function getFlarumConfig() { - $dbConfig = $this->configuration->getDatabaseConfiguration(); - return [ - 'debug' => $this->configuration->isDebugMode(), - 'database' => [ - 'driver' => $dbConfig['driver'], - 'host' => $dbConfig['host'], - 'database' => $dbConfig['database'], - 'username' => $dbConfig['username'], - 'password' => $dbConfig['password'], - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => $dbConfig['prefix'], - 'port' => $dbConfig['port'], - 'strict' => false - ], - 'url' => $this->configuration->getBaseUrl(), + 'debug' => true, + 'database' => $this->getDatabaseConfiguration(), + 'url' => 'http://flarum.local', 'paths' => [ 'api' => 'api', 'admin' => 'admin', @@ -138,13 +124,13 @@ trait CreatesForum return; } - $app = app(\Illuminate\Contracts\Foundation\Application::class); + $dbConfig = $this->getDatabaseConfiguration(); - $factory = new ConnectionFactory($app); - $db = $factory->make($this->configuration->getDatabaseConfiguration()); + $pdo = (new MySqlConnector)->connect($dbConfig); + $db = new MySqlConnection($pdo, $dbConfig['database'], $dbConfig['prefix'], $dbConfig); $repository = new DatabaseMigrationRepository($db, 'migrations'); - $migrator = new Migrator($repository, $db, app('files')); + $migrator = new Migrator($repository, $db, new Filesystem); if (! $migrator->getRepository()->repositoryExists()) { $migrator->getRepository()->createRepository(); From 89e018a4f09df825153dc3c734ef7dc36e92349b Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Tue, 13 Nov 2018 22:23:52 +0100 Subject: [PATCH 03/23] Simplify PrerequisiteInterface I went with a return type of Collection, because it is easier to call methods such as isEmpty() directly on those objects. --- src/Install/Console/InstallCommand.php | 25 +++++++------ src/Install/Controller/IndexController.php | 10 +++--- .../Prerequisite/AbstractPrerequisite.php | 24 ------------- src/Install/Prerequisite/Composite.php | 17 ++++----- src/Install/Prerequisite/PhpExtensions.php | 17 +++++---- src/Install/Prerequisite/PhpVersion.php | 12 ++++--- .../Prerequisite/PrerequisiteInterface.php | 15 ++++++-- src/Install/Prerequisite/WritablePaths.php | 35 ++++++++++++++----- views/install/app.php | 12 +++---- views/install/errors.php | 14 -------- views/install/problems.php | 14 ++++++++ 11 files changed, 98 insertions(+), 97 deletions(-) delete mode 100644 src/Install/Prerequisite/AbstractPrerequisite.php delete mode 100644 views/install/errors.php create mode 100644 views/install/problems.php diff --git a/src/Install/Console/InstallCommand.php b/src/Install/Console/InstallCommand.php index e429d62d8..246dbc0ce 100644 --- a/src/Install/Console/InstallCommand.php +++ b/src/Install/Console/InstallCommand.php @@ -80,21 +80,16 @@ class InstallCommand extends AbstractCommand { $this->init(); - $prerequisites = $this->installation->prerequisites(); - $prerequisites->check(); - $errors = $prerequisites->getErrors(); + $problems = $this->installation->prerequisites()->problems(); - if (empty($errors)) { + if ($problems->isEmpty()) { $this->info('Installing Flarum...'); $this->install(); $this->info('DONE.'); } else { - $this->output->writeln( - 'Please fix the following errors before we can continue with the installation.' - ); - $this->showErrors($errors); + $this->showProblems($problems); } } @@ -178,13 +173,17 @@ class InstallCommand extends AbstractCommand ->run(); } - protected function showErrors($errors) + protected function showProblems($problems) { - foreach ($errors as $error) { - $this->info($error['message']); + $this->output->writeln( + 'Please fix the following problems before we can continue with the installation.' + ); - if (isset($error['detail'])) { - $this->output->writeln(''.$error['detail'].''); + foreach ($problems as $problem) { + $this->info($problem['message']); + + if (isset($problem['detail'])) { + $this->output->writeln(''.$problem['detail'].''); } } } diff --git a/src/Install/Controller/IndexController.php b/src/Install/Controller/IndexController.php index 03b1e2449..3d66789d2 100644 --- a/src/Install/Controller/IndexController.php +++ b/src/Install/Controller/IndexController.php @@ -46,14 +46,12 @@ class IndexController extends AbstractHtmlController { $view = $this->view->make('flarum.install::app')->with('title', 'Install Flarum'); - $prerequisites = $this->installation->prerequisites(); - $prerequisites->check(); - $errors = $prerequisites->getErrors(); + $problems = $this->installation->prerequisites()->problems(); - if (count($errors)) { - $view->with('content', $this->view->make('flarum.install::errors')->with('errors', $errors)); - } else { + if ($problems->isEmpty()) { $view->with('content', $this->view->make('flarum.install::install')); + } else { + $view->with('content', $this->view->make('flarum.install::problems')->with('problems', $problems)); } return $view; diff --git a/src/Install/Prerequisite/AbstractPrerequisite.php b/src/Install/Prerequisite/AbstractPrerequisite.php deleted file mode 100644 index 06c6210b1..000000000 --- a/src/Install/Prerequisite/AbstractPrerequisite.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Install\Prerequisite; - -abstract class AbstractPrerequisite implements PrerequisiteInterface -{ - protected $errors = []; - - abstract public function check(); - - public function getErrors() - { - return $this->errors; - } -} diff --git a/src/Install/Prerequisite/Composite.php b/src/Install/Prerequisite/Composite.php index ed0fb77e4..ed3a36e07 100644 --- a/src/Install/Prerequisite/Composite.php +++ b/src/Install/Prerequisite/Composite.php @@ -11,6 +11,8 @@ namespace Flarum\Install\Prerequisite; +use Illuminate\Support\Collection; + class Composite implements PrerequisiteInterface { /** @@ -25,21 +27,14 @@ class Composite implements PrerequisiteInterface } } - public function check() + public function problems(): Collection { return array_reduce( $this->prerequisites, - function ($previous, PrerequisiteInterface $prerequisite) { - return $prerequisite->check() && $previous; + function (Collection $errors, PrerequisiteInterface $condition) { + return $errors->concat($condition->problems()); }, - true + collect() ); } - - public function getErrors() - { - return collect($this->prerequisites)->map(function (PrerequisiteInterface $prerequisite) { - return $prerequisite->getErrors(); - })->reduce('array_merge', []); - } } diff --git a/src/Install/Prerequisite/PhpExtensions.php b/src/Install/Prerequisite/PhpExtensions.php index 434eff927..bac5f0d47 100644 --- a/src/Install/Prerequisite/PhpExtensions.php +++ b/src/Install/Prerequisite/PhpExtensions.php @@ -11,7 +11,9 @@ namespace Flarum\Install\Prerequisite; -class PhpExtensions extends AbstractPrerequisite +use Illuminate\Support\Collection; + +class PhpExtensions implements PrerequisiteInterface { protected $extensions; @@ -20,14 +22,15 @@ class PhpExtensions extends AbstractPrerequisite $this->extensions = $extensions; } - public function check() + public function problems(): Collection { - foreach ($this->extensions as $extension) { - if (! extension_loaded($extension)) { - $this->errors[] = [ + return collect($this->extensions) + ->reject(function ($extension) { + return extension_loaded($extension); + })->map(function ($extension) { + return [ 'message' => "The PHP extension '$extension' is required.", ]; - } - } + }); } } diff --git a/src/Install/Prerequisite/PhpVersion.php b/src/Install/Prerequisite/PhpVersion.php index b2960d372..59d21266f 100644 --- a/src/Install/Prerequisite/PhpVersion.php +++ b/src/Install/Prerequisite/PhpVersion.php @@ -11,7 +11,9 @@ namespace Flarum\Install\Prerequisite; -class PhpVersion extends AbstractPrerequisite +use Illuminate\Support\Collection; + +class PhpVersion implements PrerequisiteInterface { protected $minVersion; @@ -20,13 +22,15 @@ class PhpVersion extends AbstractPrerequisite $this->minVersion = $minVersion; } - public function check() + public function problems(): Collection { if (version_compare(PHP_VERSION, $this->minVersion, '<')) { - $this->errors[] = [ + return collect()->push([ 'message' => "PHP $this->minVersion is required.", 'detail' => 'You are running version '.PHP_VERSION.'. Talk to your hosting provider about upgrading to the latest PHP version.', - ]; + ]); } + + return collect(); } } diff --git a/src/Install/Prerequisite/PrerequisiteInterface.php b/src/Install/Prerequisite/PrerequisiteInterface.php index 115e9f808..9b7907883 100644 --- a/src/Install/Prerequisite/PrerequisiteInterface.php +++ b/src/Install/Prerequisite/PrerequisiteInterface.php @@ -11,9 +11,18 @@ namespace Flarum\Install\Prerequisite; +use Illuminate\Support\Collection; + interface PrerequisiteInterface { - public function check(); - - public function getErrors(); + /** + * Verify that this prerequisite is fulfilled. + * + * If everything is okay, this method should return an empty Collection + * instance. When problems are detected, it should return a Collection of + * arrays, each having at least a "message" and optionally a "detail" key. + * + * @return Collection + */ + public function problems(): Collection; } diff --git a/src/Install/Prerequisite/WritablePaths.php b/src/Install/Prerequisite/WritablePaths.php index 1f356ee6a..99beb2a3d 100644 --- a/src/Install/Prerequisite/WritablePaths.php +++ b/src/Install/Prerequisite/WritablePaths.php @@ -11,7 +11,9 @@ namespace Flarum\Install\Prerequisite; -class WritablePaths extends AbstractPrerequisite +use Illuminate\Support\Collection; + +class WritablePaths implements PrerequisiteInterface { protected $paths; @@ -20,21 +22,36 @@ class WritablePaths extends AbstractPrerequisite $this->paths = $paths; } - public function check() + public function problems(): Collection { - foreach ($this->paths as $path) { - if (! file_exists($path)) { - $this->errors[] = [ + return $this->getMissingPaths() + ->concat($this->getNonWritablePaths()); + } + + private function getMissingPaths(): Collection + { + return collect($this->paths) + ->reject(function ($path) { + return file_exists($path); + })->map(function ($path) { + return [ 'message' => 'The '.$this->getAbsolutePath($path).' directory doesn\'t exist', 'detail' => 'This directory is necessary for the installation. Please create the folder.', ]; - } elseif (! is_writable($path)) { - $this->errors[] = [ + }); + } + + private function getNonWritablePaths(): Collection + { + return collect($this->paths) + ->filter(function ($path) { + return file_exists($path) && ! is_writable($path); + })->map(function ($path) { + return [ 'message' => 'The '.$this->getAbsolutePath($path).' directory is not writable.', 'detail' => 'Please chmod this directory'.($path !== public_path() ? ' and its contents' : '').' to 0775.' ]; - } - } + }); } private function getAbsolutePath($path) diff --git a/views/install/app.php b/views/install/app.php index 2e8601022..3e4f572de 100644 --- a/views/install/app.php +++ b/views/install/app.php @@ -129,30 +129,30 @@ animation-name: fadeIn; } - .Errors { + .Problems { margin-top: 50px; } - .Errors .Error:first-child { + .Problems .Problem:first-child { border-top-left-radius: 4px; border-top-right-radius: 4px; } - .Errors .Error:last-child { + .Problems .Problem:last-child { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } - .Error { + .Problem { background: #EDF2F7; margin: 0 0 1px; padding: 20px 25px; text-align: left; } - .Error-message { + .Problem-message { font-size: 16px; color: #3C5675; font-weight: normal; margin: 0; } - .Error-detail { + .Problem-detail { font-size: 13px; margin: 5px 0 0; } diff --git a/views/install/errors.php b/views/install/errors.php deleted file mode 100644 index bd73aeb43..000000000 --- a/views/install/errors.php +++ /dev/null @@ -1,14 +0,0 @@ -

Hold Up!

- -

These errors must be resolved before you can continue the installation. If you're having trouble, get help on the Flarum website.

- -
- -
-

- -

- -
- -
diff --git a/views/install/problems.php b/views/install/problems.php new file mode 100644 index 000000000..36b8d6252 --- /dev/null +++ b/views/install/problems.php @@ -0,0 +1,14 @@ +

Hold Up!

+ +

These problems must be resolved before you can continue the installation. If you're having trouble, get help on the Flarum website.

+ +
+ +
+

+ +

+ +
+ +
From e0a508a7650a874cb34e0668d61801ca8a58d371 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Tue, 13 Nov 2018 22:27:11 +0100 Subject: [PATCH 04/23] Catch pipeline's own exception --- src/Install/Controller/InstallController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Install/Controller/InstallController.php b/src/Install/Controller/InstallController.php index 835a6e312..d86db5caa 100644 --- a/src/Install/Controller/InstallController.php +++ b/src/Install/Controller/InstallController.php @@ -11,9 +11,9 @@ namespace Flarum\Install\Controller; -use Exception; use Flarum\Http\SessionAuthenticator; use Flarum\Install\Installation; +use Flarum\Install\StepFailed; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface; @@ -102,8 +102,8 @@ class InstallController implements RequestHandlerInterface try { $pipeline->run(); - } catch (Exception $e) { - return new Response\HtmlResponse($e->getMessage(), 500); + } catch (StepFailed $e) { + return new Response\HtmlResponse($e->getPrevious()->getMessage(), 500); } $session = $request->getAttribute('session'); From f5a21584c2707e746d3c8db89780b5a656ef1f45 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Tue, 22 Jan 2019 21:34:27 +0100 Subject: [PATCH 05/23] Collapse namespace imports --- src/Install/Installation.php | 40 ++++++++++++------------------------ 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/Install/Installation.php b/src/Install/Installation.php index 0da25ca9f..e6dba3414 100644 --- a/src/Install/Installation.php +++ b/src/Install/Installation.php @@ -11,20 +11,6 @@ namespace Flarum\Install; -use Flarum\Install\Prerequisite\Composite; -use Flarum\Install\Prerequisite\PhpExtensions; -use Flarum\Install\Prerequisite\PhpVersion; -use Flarum\Install\Prerequisite\PrerequisiteInterface; -use Flarum\Install\Prerequisite\WritablePaths; -use Flarum\Install\Steps\BuildConfig; -use Flarum\Install\Steps\ConnectToDatabase; -use Flarum\Install\Steps\CreateAdminUser; -use Flarum\Install\Steps\EnableBundledExtensions; -use Flarum\Install\Steps\PublishAssets; -use Flarum\Install\Steps\RunMigrations; -use Flarum\Install\Steps\StoreConfig; -use Flarum\Install\Steps\WriteSettings; - class Installation { private $basePath; @@ -87,11 +73,11 @@ class Installation return $this; } - public function prerequisites(): PrerequisiteInterface + public function prerequisites(): Prerequisite\PrerequisiteInterface { - return new Composite( - new PhpVersion('7.1.0'), - new PhpExtensions([ + return new Prerequisite\Composite( + new Prerequisite\PhpVersion('7.1.0'), + new Prerequisite\PhpExtensions([ 'dom', 'gd', 'json', @@ -100,7 +86,7 @@ class Installation 'pdo_mysql', 'tokenizer', ]), - new WritablePaths([ + new Prerequisite\WritablePaths([ $this->basePath, $this->getAssetPath(), $this->storagePath, @@ -117,7 +103,7 @@ class Installation $this->tmp = []; $pipeline->pipe(function () { - return new BuildConfig( + return new Steps\BuildConfig( $this->debug, $this->dbConfig, $this->baseUrl, function ($config) { $this->tmp['config'] = $config; @@ -126,7 +112,7 @@ class Installation }); $pipeline->pipe(function () { - return new ConnectToDatabase( + return new Steps\ConnectToDatabase( $this->dbConfig, function ($connection) { $this->tmp['db'] = $connection; @@ -135,27 +121,27 @@ class Installation }); $pipeline->pipe(function () { - return new StoreConfig($this->tmp['config'], $this->getConfigPath()); + return new Steps\StoreConfig($this->tmp['config'], $this->getConfigPath()); }); $pipeline->pipe(function () { - return new RunMigrations($this->tmp['db'], $this->getMigrationPath()); + return new Steps\RunMigrations($this->tmp['db'], $this->getMigrationPath()); }); $pipeline->pipe(function () { - return new WriteSettings($this->tmp['db'], $this->defaultSettings); + return new Steps\WriteSettings($this->tmp['db'], $this->defaultSettings); }); $pipeline->pipe(function () { - return new CreateAdminUser($this->tmp['db'], $this->adminUser); + return new Steps\CreateAdminUser($this->tmp['db'], $this->adminUser); }); $pipeline->pipe(function () { - return new PublishAssets($this->basePath, $this->getAssetPath()); + return new Steps\PublishAssets($this->basePath, $this->getAssetPath()); }); $pipeline->pipe(function () { - return new EnableBundledExtensions($this->tmp['db'], $this->basePath, $this->getAssetPath()); + return new Steps\EnableBundledExtensions($this->tmp['db'], $this->basePath, $this->getAssetPath()); }); return $pipeline; From bc9e8f68f1eb8d4093a3eab8e64617703d91bbe8 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Tue, 22 Jan 2019 21:49:42 +0100 Subject: [PATCH 06/23] Move default settings to install step The various installation "frontends" (such as GUI and console) can now provide custom overrides, if they want to. --- src/Install/Console/DefaultsDataProvider.php | 20 +---------- src/Install/Console/FileDataProvider.php | 2 +- src/Install/Console/UserDataProvider.php | 14 -------- src/Install/Controller/InstallController.php | 14 -------- src/Install/Installation.php | 6 ++-- src/Install/Steps/WriteSettings.php | 36 +++++++++++++++++--- tests/Install/DefaultInstallationTest.php | 14 -------- 7 files changed, 37 insertions(+), 69 deletions(-) diff --git a/src/Install/Console/DefaultsDataProvider.php b/src/Install/Console/DefaultsDataProvider.php index 62841faea..119e657a4 100644 --- a/src/Install/Console/DefaultsDataProvider.php +++ b/src/Install/Console/DefaultsDataProvider.php @@ -37,25 +37,7 @@ class DefaultsDataProvider implements DataProviderInterface 'email' => 'admin@example.com', ]; - protected $settings = [ - 'allow_post_editing' => 'reply', - 'allow_renaming' => '10', - 'allow_sign_up' => '1', - 'custom_less' => '', - 'default_locale' => 'en', - 'default_route' => '/all', - 'extensions_enabled' => '[]', - 'forum_title' => 'Development Forum', - 'forum_description' => '', - 'mail_driver' => 'mail', - 'mail_from' => 'noreply@flarum.dev', - 'theme_colored_header' => '0', - 'theme_dark_mode' => '0', - 'theme_primary_color' => '#4D698E', - 'theme_secondary_color' => '#4D698E', - 'welcome_message' => 'This is beta software and you should not use it in production.', - 'welcome_title' => 'Welcome to Development Forum', - ]; + protected $settings = []; public function getDatabaseConfiguration() { diff --git a/src/Install/Console/FileDataProvider.php b/src/Install/Console/FileDataProvider.php index 0eb8886f0..7e4a00d0a 100644 --- a/src/Install/Console/FileDataProvider.php +++ b/src/Install/Console/FileDataProvider.php @@ -72,7 +72,7 @@ class FileDataProvider implements DataProviderInterface public function getSettings() { - return $this->settings + $this->default->getSettings(); + return $this->settings; } public function isDebugMode(): bool diff --git a/src/Install/Console/UserDataProvider.php b/src/Install/Console/UserDataProvider.php index eb10ef5ba..2de2a2677 100644 --- a/src/Install/Console/UserDataProvider.php +++ b/src/Install/Console/UserDataProvider.php @@ -77,22 +77,8 @@ class UserDataProvider implements DataProviderInterface $baseUrl = $this->baseUrl ?: 'http://localhost'; return [ - 'allow_post_editing' => 'reply', - 'allow_renaming' => '10', - 'allow_sign_up' => '1', - 'custom_less' => '', - 'default_locale' => 'en', - 'default_route' => '/all', - 'extensions_enabled' => '[]', 'forum_title' => $title, - 'forum_description' => '', - 'mail_driver' => 'mail', 'mail_from' => 'noreply@'.preg_replace('/^www\./i', '', parse_url($baseUrl, PHP_URL_HOST)), - 'theme_colored_header' => '0', - 'theme_dark_mode' => '0', - 'theme_primary_color' => '#4D698E', - 'theme_secondary_color' => '#4D698E', - 'welcome_message' => 'This is beta software and you should not use it in production.', 'welcome_title' => 'Welcome to '.$title, ]; } diff --git a/src/Install/Controller/InstallController.php b/src/Install/Controller/InstallController.php index d86db5caa..8dd46e9a3 100644 --- a/src/Install/Controller/InstallController.php +++ b/src/Install/Controller/InstallController.php @@ -80,22 +80,8 @@ class InstallController implements RequestHandlerInterface 'email' => array_get($input, 'adminEmail'), ]) ->settings([ - 'allow_post_editing' => 'reply', - 'allow_renaming' => '10', - 'allow_sign_up' => '1', - 'custom_less' => '', - 'default_locale' => 'en', - 'default_route' => '/all', - 'extensions_enabled' => '[]', 'forum_title' => array_get($input, 'forumTitle'), - 'forum_description' => '', - 'mail_driver' => 'mail', 'mail_from' => 'noreply@'.preg_replace('/^www\./i', '', parse_url($baseUrl, PHP_URL_HOST)), - 'theme_colored_header' => '0', - 'theme_dark_mode' => '0', - 'theme_primary_color' => '#4D698E', - 'theme_secondary_color' => '#4D698E', - 'welcome_message' => 'This is beta software and you should not use it in production.', 'welcome_title' => 'Welcome to '.array_get($input, 'forumTitle'), ]) ->build(); diff --git a/src/Install/Installation.php b/src/Install/Installation.php index e6dba3414..bc703db7c 100644 --- a/src/Install/Installation.php +++ b/src/Install/Installation.php @@ -21,7 +21,7 @@ class Installation private $debug = false; private $dbConfig = []; private $baseUrl; - private $defaultSettings = []; + private $customSettings = []; private $adminUser = []; public function __construct($basePath, $publicPath, $storagePath) @@ -61,7 +61,7 @@ class Installation public function settings($settings) { - $this->defaultSettings = $settings; + $this->customSettings = $settings; return $this; } @@ -129,7 +129,7 @@ class Installation }); $pipeline->pipe(function () { - return new Steps\WriteSettings($this->tmp['db'], $this->defaultSettings); + return new Steps\WriteSettings($this->tmp['db'], $this->customSettings); }); $pipeline->pipe(function () { diff --git a/src/Install/Steps/WriteSettings.php b/src/Install/Steps/WriteSettings.php index 657e838d9..366fc3696 100644 --- a/src/Install/Steps/WriteSettings.php +++ b/src/Install/Steps/WriteSettings.php @@ -26,12 +26,12 @@ class WriteSettings implements Step /** * @var array */ - private $defaults; + private $custom; - public function __construct(ConnectionInterface $database, array $defaults) + public function __construct(ConnectionInterface $database, array $custom) { $this->database = $database; - $this->defaults = $defaults; + $this->custom = $custom; } public function getMessage() @@ -45,8 +45,36 @@ class WriteSettings implements Step $repo->set('version', Application::VERSION); - foreach ($this->defaults as $key => $value) { + foreach ($this->getSettings() as $key => $value) { $repo->set($key, $value); } } + + private function getSettings() + { + return $this->custom + $this->getDefaults(); + } + + private function getDefaults() + { + return [ + 'allow_post_editing' => 'reply', + 'allow_renaming' => '10', + 'allow_sign_up' => '1', + 'custom_less' => '', + 'default_locale' => 'en', + 'default_route' => '/all', + 'extensions_enabled' => '[]', + 'forum_title' => 'A new Flarum forum', + 'forum_description' => '', + 'mail_driver' => 'mail', + 'mail_from' => 'noreply@localhost', + 'theme_colored_header' => '0', + 'theme_dark_mode' => '0', + 'theme_primary_color' => '#4D698E', + 'theme_secondary_color' => '#4D698E', + 'welcome_message' => 'This is beta software and you should not use it in production.', + 'welcome_title' => 'Welcome to Flarum', + ]; + } } diff --git a/tests/Install/DefaultInstallationTest.php b/tests/Install/DefaultInstallationTest.php index b7507ea7b..02eaf4d2a 100644 --- a/tests/Install/DefaultInstallationTest.php +++ b/tests/Install/DefaultInstallationTest.php @@ -69,22 +69,8 @@ class DefaultInstallationTest extends TestCase private function getSettings() { return [ - 'allow_post_editing' => 'reply', - 'allow_renaming' => '10', - 'allow_sign_up' => '1', - 'custom_less' => '', - 'default_locale' => 'en', - 'default_route' => '/all', - 'extensions_enabled' => '[]', 'forum_title' => 'Development Forum', - 'forum_description' => '', 'mail_driver' => 'log', - 'mail_from' => 'noreply@flarum.dev', - 'theme_colored_header' => '0', - 'theme_dark_mode' => '0', - 'theme_primary_color' => '#4D698E', - 'theme_secondary_color' => '#4D698E', - 'welcome_message' => 'This is beta software and you should not use it in production.', 'welcome_title' => 'Welcome to Development Forum', ]; } From 4585f03ee356c92942fbc2ae8c683c651b473954 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Tue, 22 Jan 2019 21:56:39 +0100 Subject: [PATCH 07/23] Switch to a whitelist for enabling extensions --- src/Install/Steps/EnableBundledExtensions.php | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Install/Steps/EnableBundledExtensions.php b/src/Install/Steps/EnableBundledExtensions.php index f3f045b0a..235da2eb6 100644 --- a/src/Install/Steps/EnableBundledExtensions.php +++ b/src/Install/Steps/EnableBundledExtensions.php @@ -67,12 +67,21 @@ class EnableBundledExtensions implements Step ); } - const DISABLED_EXTENSIONS = [ - 'flarum-akismet', - 'flarum-auth-facebook', - 'flarum-auth-github', - 'flarum-auth-twitter', - 'flarum-pusher', + const EXTENSION_WHITELIST = [ + 'flarum-approval', + 'flarum-bbcode', + 'flarum-emoji', + 'flarum-lang-english', + 'flarum-flags', + 'flarum-likes', + 'flarum-lock', + 'flarum-markdown', + 'flarum-mentions', + 'flarum-statistics', + 'flarum-sticky', + 'flarum-subscriptions', + 'flarum-suspend', + 'flarum-tags', ]; /** @@ -93,7 +102,7 @@ class EnableBundledExtensions implements Step return $extension; })->filter(function (Extension $extension) { - return ! in_array($extension->getId(), self::DISABLED_EXTENSIONS); + return in_array($extension->getId(), self::EXTENSION_WHITELIST); })->sortBy(function (Extension $extension) { return $extension->composerJsonAttribute('extra.flarum-extension.title'); })->mapWithKeys(function (Extension $extension) { From 44c91099cd77138bb5fc29f14fb1e81a9781272d Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Tue, 22 Jan 2019 22:03:47 +0100 Subject: [PATCH 08/23] Get rid of DefaultsDataProvider Since we do not provide a development VM anymore, it does not make sense to have "default" credentials etc. To reproduce something similar, I'd suggest using a YAML or JSON file together with the `--file` option. --- src/Install/Console/DefaultsDataProvider.php | 96 -------------------- src/Install/Console/FileDataProvider.php | 26 ++++-- src/Install/Console/InstallCommand.php | 10 +- 3 files changed, 20 insertions(+), 112 deletions(-) delete mode 100644 src/Install/Console/DefaultsDataProvider.php diff --git a/src/Install/Console/DefaultsDataProvider.php b/src/Install/Console/DefaultsDataProvider.php deleted file mode 100644 index 119e657a4..000000000 --- a/src/Install/Console/DefaultsDataProvider.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Install\Console; - -class DefaultsDataProvider implements DataProviderInterface -{ - protected $databaseConfiguration = [ - 'driver' => 'mysql', - 'host' => 'localhost', - 'database' => 'flarum', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'port' => '3306', - 'strict' => false, - ]; - - protected $debug = false; - - protected $baseUrl = 'http://flarum.local'; - - protected $adminUser = [ - 'username' => 'admin', - 'password' => 'password', - 'password_confirmation' => 'password', - 'email' => 'admin@example.com', - ]; - - protected $settings = []; - - public function getDatabaseConfiguration() - { - return $this->databaseConfiguration; - } - - public function setDatabaseConfiguration(array $databaseConfiguration) - { - $this->databaseConfiguration = $databaseConfiguration; - } - - public function getBaseUrl() - { - return $this->baseUrl; - } - - public function setBaseUrl($baseUrl) - { - $this->baseUrl = $baseUrl; - } - - public function getAdminUser() - { - return $this->adminUser; - } - - public function setAdminUser(array $adminUser) - { - $this->adminUser = $adminUser; - } - - public function getSettings() - { - return $this->settings; - } - - public function setSettings(array $settings) - { - $this->settings = $settings; - } - - public function setSetting($key, $value) - { - $this->settings[$key] = $value; - } - - public function isDebugMode(): bool - { - return $this->debug; - } - - public function setDebugMode(bool $debug = true) - { - $this->debug = $debug; - } -} diff --git a/src/Install/Console/FileDataProvider.php b/src/Install/Console/FileDataProvider.php index 7e4a00d0a..09975b861 100644 --- a/src/Install/Console/FileDataProvider.php +++ b/src/Install/Console/FileDataProvider.php @@ -17,7 +17,6 @@ use Symfony\Component\Yaml\Yaml; class FileDataProvider implements DataProviderInterface { - protected $default; protected $debug = false; protected $baseUrl = null; protected $databaseConfiguration = []; @@ -26,9 +25,6 @@ class FileDataProvider implements DataProviderInterface public function __construct(InputInterface $input) { - // Get default configuration - $this->default = new DefaultsDataProvider(); - // Get configuration file path $configurationFile = $input->getOption('file'); @@ -57,17 +53,33 @@ class FileDataProvider implements DataProviderInterface public function getDatabaseConfiguration() { - return $this->databaseConfiguration + $this->default->getDatabaseConfiguration(); + return $this->databaseConfiguration + [ + 'driver' => 'mysql', + 'host' => 'localhost', + 'database' => 'flarum', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'port' => '3306', + 'strict' => false, + ]; } public function getBaseUrl() { - return (! is_null($this->baseUrl)) ? $this->baseUrl : $this->default->getBaseUrl(); + return $this->baseUrl ?? 'http://flarum.local'; } public function getAdminUser() { - return $this->adminUser + $this->default->getAdminUser(); + return $this->adminUser + [ + 'username' => 'admin', + 'password' => 'password', + 'password_confirmation' => 'password', + 'email' => 'admin@example.com', + ]; } public function getSettings() diff --git a/src/Install/Console/InstallCommand.php b/src/Install/Console/InstallCommand.php index 246dbc0ce..7213e5ef2 100644 --- a/src/Install/Console/InstallCommand.php +++ b/src/Install/Console/InstallCommand.php @@ -53,12 +53,6 @@ class InstallCommand extends AbstractCommand $this ->setName('install') ->setDescription("Run Flarum's installation migration and seeds") - ->addOption( - 'defaults', - 'd', - InputOption::VALUE_NONE, - 'Create default settings and user' - ) ->addOption( 'file', 'f', @@ -95,9 +89,7 @@ class InstallCommand extends AbstractCommand protected function init() { - if ($this->input->getOption('defaults')) { - $this->dataSource = new DefaultsDataProvider(); - } elseif ($this->input->getOption('file')) { + if ($this->input->getOption('file')) { $this->dataSource = new FileDataProvider($this->input); } else { $this->dataSource = new UserDataProvider($this->input, $this->output, $this->getHelperSet()->get('question')); From 78ba3bd854e7ba3ef156e45420e9d4b5625fb172 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Tue, 22 Jan 2019 22:13:40 +0100 Subject: [PATCH 09/23] Combine building and storing config in one step --- src/Install/Installation.php | 13 ++---- src/Install/Steps/BuildConfig.php | 75 ------------------------------- src/Install/Steps/StoreConfig.php | 49 ++++++++++++++++++-- 3 files changed, 48 insertions(+), 89 deletions(-) delete mode 100644 src/Install/Steps/BuildConfig.php diff --git a/src/Install/Installation.php b/src/Install/Installation.php index bc703db7c..e18347143 100644 --- a/src/Install/Installation.php +++ b/src/Install/Installation.php @@ -102,15 +102,6 @@ class Installation // It's an instance variable so that access in closures is easier. :) $this->tmp = []; - $pipeline->pipe(function () { - return new Steps\BuildConfig( - $this->debug, $this->dbConfig, $this->baseUrl, - function ($config) { - $this->tmp['config'] = $config; - } - ); - }); - $pipeline->pipe(function () { return new Steps\ConnectToDatabase( $this->dbConfig, @@ -121,7 +112,9 @@ class Installation }); $pipeline->pipe(function () { - return new Steps\StoreConfig($this->tmp['config'], $this->getConfigPath()); + return new Steps\StoreConfig( + $this->debug, $this->dbConfig, $this->baseUrl, $this->getConfigPath() + ); }); $pipeline->pipe(function () { diff --git a/src/Install/Steps/BuildConfig.php b/src/Install/Steps/BuildConfig.php deleted file mode 100644 index bab1a160e..000000000 --- a/src/Install/Steps/BuildConfig.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Install\Steps; - -use Flarum\Install\Step; - -class BuildConfig implements Step -{ - private $debugMode; - - private $dbConfig; - - private $baseUrl; - - private $store; - - public function __construct($debugMode, $dbConfig, $baseUrl, callable $store) - { - $this->debugMode = $debugMode; - $this->dbConfig = $dbConfig; - $this->baseUrl = $baseUrl; - - $this->store = $store; - } - - public function getMessage() - { - return 'Building config array'; - } - - public function run() - { - $config = [ - 'debug' => $this->debugMode, - 'database' => $this->getDatabaseConfig(), - 'url' => $this->baseUrl, - 'paths' => $this->getPathsConfig(), - ]; - - ($this->store)($config); - } - - private function getDatabaseConfig() - { - return [ - 'driver' => $this->dbConfig['driver'], - 'host' => $this->dbConfig['host'], - 'database' => $this->dbConfig['database'], - 'username' => $this->dbConfig['username'], - 'password' => $this->dbConfig['password'], - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => $this->dbConfig['prefix'], - 'port' => $this->dbConfig['port'], - 'strict' => false, - ]; - } - - private function getPathsConfig() - { - return [ - 'api' => 'api', - 'admin' => 'admin', - ]; - } -} diff --git a/src/Install/Steps/StoreConfig.php b/src/Install/Steps/StoreConfig.php index f2c2a22f3..fca897ff4 100644 --- a/src/Install/Steps/StoreConfig.php +++ b/src/Install/Steps/StoreConfig.php @@ -16,13 +16,20 @@ use Flarum\Install\Step; class StoreConfig implements Step, ReversibleStep { - private $config; + private $debugMode; + + private $dbConfig; + + private $baseUrl; private $configFile; - public function __construct(array $config, $configFile) + public function __construct($debugMode, $dbConfig, $baseUrl, $configFile) { - $this->config = $config; + $this->debugMode = $debugMode; + $this->dbConfig = $dbConfig; + $this->baseUrl = $baseUrl; + $this->configFile = $configFile; } @@ -35,7 +42,7 @@ class StoreConfig implements Step, ReversibleStep { file_put_contents( $this->configFile, - 'config, true).';' + 'buildConfig(), true).';' ); } @@ -43,4 +50,38 @@ class StoreConfig implements Step, ReversibleStep { @unlink($this->configFile); } + + private function buildConfig() + { + return [ + 'debug' => $this->debugMode, + 'database' => $this->getDatabaseConfig(), + 'url' => $this->baseUrl, + 'paths' => $this->getPathsConfig(), + ]; + } + + private function getDatabaseConfig() + { + return [ + 'driver' => $this->dbConfig['driver'], + 'host' => $this->dbConfig['host'], + 'database' => $this->dbConfig['database'], + 'username' => $this->dbConfig['username'], + 'password' => $this->dbConfig['password'], + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => $this->dbConfig['prefix'], + 'port' => $this->dbConfig['port'], + 'strict' => false, + ]; + } + + private function getPathsConfig() + { + return [ + 'api' => 'api', + 'admin' => 'admin', + ]; + } } From 0879829dc4b3c56fd967e203a95f9193125c468e Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Tue, 22 Jan 2019 22:16:59 +0100 Subject: [PATCH 10/23] Use dedicated temporary variable instead of array --- src/Install/Installation.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Install/Installation.php b/src/Install/Installation.php index e18347143..4ebfa0c1d 100644 --- a/src/Install/Installation.php +++ b/src/Install/Installation.php @@ -24,6 +24,13 @@ class Installation private $customSettings = []; private $adminUser = []; + // A few instance variables to persist objects between steps. + // Could also be local variables in build(), but this way + // access in closures is easier. :) + + /** @var \Illuminate\Database\ConnectionInterface */ + private $db; + public function __construct($basePath, $publicPath, $storagePath) { $this->basePath = $basePath; @@ -98,15 +105,11 @@ class Installation { $pipeline = new Pipeline; - // A new array to persist some objects between steps. - // It's an instance variable so that access in closures is easier. :) - $this->tmp = []; - $pipeline->pipe(function () { return new Steps\ConnectToDatabase( $this->dbConfig, function ($connection) { - $this->tmp['db'] = $connection; + $this->db = $connection; } ); }); @@ -118,15 +121,15 @@ class Installation }); $pipeline->pipe(function () { - return new Steps\RunMigrations($this->tmp['db'], $this->getMigrationPath()); + return new Steps\RunMigrations($this->db, $this->getMigrationPath()); }); $pipeline->pipe(function () { - return new Steps\WriteSettings($this->tmp['db'], $this->customSettings); + return new Steps\WriteSettings($this->db, $this->customSettings); }); $pipeline->pipe(function () { - return new Steps\CreateAdminUser($this->tmp['db'], $this->adminUser); + return new Steps\CreateAdminUser($this->db, $this->adminUser); }); $pipeline->pipe(function () { @@ -134,7 +137,7 @@ class Installation }); $pipeline->pipe(function () { - return new Steps\EnableBundledExtensions($this->tmp['db'], $this->basePath, $this->getAssetPath()); + return new Steps\EnableBundledExtensions($this->db, $this->basePath, $this->getAssetPath()); }); return $pipeline; From 0a59b7164e8b3c2a436af2e65378f2684538ed9c Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Sat, 26 Jan 2019 00:51:28 +0100 Subject: [PATCH 11/23] Move password confirmation validation to frontends Since this is not strictly speaking a domain invariant, but rather specific to the user interface where passwords are not displayed, and should therefore be entered twice to prevent mistakes going unnoticed, this stuff should be checked in the frontend, not in the install steps. Next step: Ensure that all domain-specific validation is done in the installer's domain layer. This will ensure these validations cannot be forgotten, and keep the frontends DRY. --- src/Install/Console/FileDataProvider.php | 1 - src/Install/Console/InstallCommand.php | 8 --- src/Install/Console/UserDataProvider.php | 34 ++++++++-- src/Install/Controller/InstallController.php | 68 ++++++++++++-------- src/Install/Steps/CreateAdminUser.php | 5 -- src/Install/ValidationFailed.php | 18 ++++++ 6 files changed, 90 insertions(+), 44 deletions(-) create mode 100644 src/Install/ValidationFailed.php diff --git a/src/Install/Console/FileDataProvider.php b/src/Install/Console/FileDataProvider.php index 09975b861..bd39e7deb 100644 --- a/src/Install/Console/FileDataProvider.php +++ b/src/Install/Console/FileDataProvider.php @@ -77,7 +77,6 @@ class FileDataProvider implements DataProviderInterface return $this->adminUser + [ 'username' => 'admin', 'password' => 'password', - 'password_confirmation' => 'password', 'email' => 'admin@example.com', ]; } diff --git a/src/Install/Console/InstallCommand.php b/src/Install/Console/InstallCommand.php index 7213e5ef2..1138ba657 100644 --- a/src/Install/Console/InstallCommand.php +++ b/src/Install/Console/InstallCommand.php @@ -120,14 +120,6 @@ class InstallCommand extends AbstractCommand $admin = $this->dataSource->getAdminUser(); - if (strlen($admin['password']) < 8) { - throw new Exception('Password must be at least 8 characters.'); - } - - if ($admin['password'] !== $admin['password_confirmation']) { - throw new Exception('The password did not match its confirmation.'); - } - if (! filter_var($admin['email'], FILTER_VALIDATE_EMAIL)) { throw new Exception('You must enter a valid email.'); } diff --git a/src/Install/Console/UserDataProvider.php b/src/Install/Console/UserDataProvider.php index 2de2a2677..6641f4265 100644 --- a/src/Install/Console/UserDataProvider.php +++ b/src/Install/Console/UserDataProvider.php @@ -64,13 +64,33 @@ class UserDataProvider implements DataProviderInterface public function getAdminUser() { return [ - 'username' => $this->ask('Admin username:'), - 'password' => $this->secret('Admin password:'), - 'password_confirmation' => $this->secret('Admin password (confirmation):'), - 'email' => $this->ask('Admin email address:'), + 'username' => $this->ask('Admin username:'), + 'password' => $this->askForAdminPassword(), + 'email' => $this->ask('Admin email address:'), ]; } + private function askForAdminPassword() + { + while (true) { + $password = $this->secret('Admin password:'); + + if (strlen($password) < 8) { + $this->validationError('Password must be at least 8 characters.'); + continue; + } + + $confirmation = $this->secret('Admin password (confirmation):'); + + if ($password !== $confirmation) { + $this->validationError('The password did not match its confirmation.'); + continue; + } + + return $password; + } + } + public function getSettings() { $title = $this->ask('Forum title:'); @@ -99,6 +119,12 @@ class UserDataProvider implements DataProviderInterface return $this->questionHelper->ask($this->input, $this->output, $question); } + protected function validationError($message) + { + $this->output->writeln("$message"); + $this->output->writeln('Please try again.'); + } + public function isDebugMode(): bool { return false; diff --git a/src/Install/Controller/InstallController.php b/src/Install/Controller/InstallController.php index 8dd46e9a3..e3c0e6ba4 100644 --- a/src/Install/Controller/InstallController.php +++ b/src/Install/Controller/InstallController.php @@ -14,6 +14,7 @@ namespace Flarum\Install\Controller; use Flarum\Http\SessionAuthenticator; use Flarum\Install\Installation; use Flarum\Install\StepFailed; +use Flarum\Install\ValidationFailed; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface; @@ -59,32 +60,35 @@ class InstallController implements RequestHandlerInterface $baseUrl = rtrim((string) $request->getUri(), '/'); - $pipeline = $this->installation - ->baseUrl($baseUrl) - ->databaseConfig([ - 'driver' => 'mysql', - 'host' => $host, - 'port' => $port, - 'database' => array_get($input, 'mysqlDatabase'), - 'username' => array_get($input, 'mysqlUsername'), - 'password' => array_get($input, 'mysqlPassword'), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => array_get($input, 'tablePrefix'), - 'strict' => false, - ]) - ->adminUser([ - 'username' => array_get($input, 'adminUsername'), - 'password' => array_get($input, 'adminPassword'), - 'password_confirmation' => array_get($input, 'adminPasswordConfirmation'), - 'email' => array_get($input, 'adminEmail'), - ]) - ->settings([ - 'forum_title' => array_get($input, 'forumTitle'), - 'mail_from' => 'noreply@'.preg_replace('/^www\./i', '', parse_url($baseUrl, PHP_URL_HOST)), - 'welcome_title' => 'Welcome to '.array_get($input, 'forumTitle'), - ]) - ->build(); + try { + $pipeline = $this->installation + ->baseUrl($baseUrl) + ->databaseConfig([ + 'driver' => 'mysql', + 'host' => $host, + 'port' => $port, + 'database' => array_get($input, 'mysqlDatabase'), + 'username' => array_get($input, 'mysqlUsername'), + 'password' => array_get($input, 'mysqlPassword'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => array_get($input, 'tablePrefix'), + 'strict' => false, + ]) + ->adminUser([ + 'username' => array_get($input, 'adminUsername'), + 'password' => $this->getConfirmedAdminPassword($input), + 'email' => array_get($input, 'adminEmail'), + ]) + ->settings([ + 'forum_title' => array_get($input, 'forumTitle'), + 'mail_from' => 'noreply@'.preg_replace('/^www\./i', '', parse_url($baseUrl, PHP_URL_HOST)), + 'welcome_title' => 'Welcome to '.array_get($input, 'forumTitle'), + ]) + ->build(); + } catch (ValidationFailed $e) { + return new Response\HtmlResponse($e->getMessage(), 500); + } try { $pipeline->run(); @@ -97,4 +101,16 @@ class InstallController implements RequestHandlerInterface return new Response\EmptyResponse; } + + private function getConfirmedAdminPassword(array $input) + { + $password = array_get($input, 'adminPassword'); + $confirmation = array_get($input, 'adminPasswordConfirmation'); + + if ($password !== $confirmation) { + throw new ValidationFailed('The admin password did not match its confirmation.'); + } + + return $password; + } } diff --git a/src/Install/Steps/CreateAdminUser.php b/src/Install/Steps/CreateAdminUser.php index af51fa04d..faed9c6c3 100644 --- a/src/Install/Steps/CreateAdminUser.php +++ b/src/Install/Steps/CreateAdminUser.php @@ -16,7 +16,6 @@ use Flarum\Group\Group; use Flarum\Install\Step; use Illuminate\Database\ConnectionInterface; use Illuminate\Hashing\BcryptHasher; -use UnexpectedValueException; class CreateAdminUser implements Step { @@ -43,10 +42,6 @@ class CreateAdminUser implements Step public function run() { - if ($this->admin['password'] !== $this->admin['password_confirmation']) { - throw new UnexpectedValueException('The password did not match its confirmation.'); - } - $uid = $this->database->table('users')->insertGetId([ 'username' => $this->admin['username'], 'email' => $this->admin['email'], diff --git a/src/Install/ValidationFailed.php b/src/Install/ValidationFailed.php new file mode 100644 index 000000000..d79206637 --- /dev/null +++ b/src/Install/ValidationFailed.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install; + +use Exception; + +class ValidationFailed extends Exception +{ +} From 8b68ff6232f42888515b22bf9b5d14885ca90694 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Sat, 26 Jan 2019 01:13:38 +0100 Subject: [PATCH 12/23] Extract AdminUser class that enforces invariants --- src/Install/AdminUser.php | 58 +++++++++++++++++++ src/Install/Console/DataProviderInterface.php | 4 +- src/Install/Console/FileDataProvider.php | 13 +++-- src/Install/Console/InstallCommand.php | 13 +---- src/Install/Console/UserDataProvider.php | 13 +++-- src/Install/Controller/InstallController.php | 23 ++++++-- src/Install/Installation.php | 6 +- src/Install/Steps/CreateAdminUser.php | 19 +++--- 8 files changed, 104 insertions(+), 45 deletions(-) create mode 100644 src/Install/AdminUser.php diff --git a/src/Install/AdminUser.php b/src/Install/AdminUser.php new file mode 100644 index 000000000..943191516 --- /dev/null +++ b/src/Install/AdminUser.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install; + +use Carbon\Carbon; +use Illuminate\Hashing\BcryptHasher; + +class AdminUser +{ + private $username; + private $password; + private $email; + + public function __construct($username, $password, $email) + { + $this->username = $username; + $this->password = $password; + $this->email = $email; + + $this->validate(); + } + + public function getUsername() + { + return $this->username; + } + + public function getAttributes(): array + { + return [ + 'username' => $this->username, + 'email' => $this->email, + 'password' => (new BcryptHasher)->make($this->password), + 'joined_at' => Carbon::now(), + 'is_email_confirmed' => 1, + ]; + } + + private function validate() + { + if (! filter_var($this->email, FILTER_VALIDATE_EMAIL)) { + throw new ValidationFailed('You must enter a valid email.'); + } + + if (! $this->username || preg_match('/[^a-z0-9_-]/i', $this->username)) { + throw new ValidationFailed('Username can only contain letters, numbers, underscores, and dashes.'); + } + } +} diff --git a/src/Install/Console/DataProviderInterface.php b/src/Install/Console/DataProviderInterface.php index 9976598e2..4913e2a28 100644 --- a/src/Install/Console/DataProviderInterface.php +++ b/src/Install/Console/DataProviderInterface.php @@ -11,13 +11,15 @@ namespace Flarum\Install\Console; +use Flarum\Install\AdminUser; + interface DataProviderInterface { public function getDatabaseConfiguration(); public function getBaseUrl(); - public function getAdminUser(); + public function getAdminUser(): AdminUser; public function getSettings(); diff --git a/src/Install/Console/FileDataProvider.php b/src/Install/Console/FileDataProvider.php index bd39e7deb..ec304c593 100644 --- a/src/Install/Console/FileDataProvider.php +++ b/src/Install/Console/FileDataProvider.php @@ -12,6 +12,7 @@ namespace Flarum\Install\Console; use Exception; +use Flarum\Install\AdminUser; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Yaml\Yaml; @@ -72,13 +73,13 @@ class FileDataProvider implements DataProviderInterface return $this->baseUrl ?? 'http://flarum.local'; } - public function getAdminUser() + public function getAdminUser(): AdminUser { - return $this->adminUser + [ - 'username' => 'admin', - 'password' => 'password', - 'email' => 'admin@example.com', - ]; + return new AdminUser( + $this->adminUser['username'] ?? 'admin', + $this->adminUser['password'] ?? 'password', + $this->adminUser['email'] ?? 'admin@example.com' + ); } public function getSettings() diff --git a/src/Install/Console/InstallCommand.php b/src/Install/Console/InstallCommand.php index 1138ba657..07995a87a 100644 --- a/src/Install/Console/InstallCommand.php +++ b/src/Install/Console/InstallCommand.php @@ -118,24 +118,13 @@ class InstallCommand extends AbstractCommand $validation->getMessageBag()->toArray()))); } - $admin = $this->dataSource->getAdminUser(); - - if (! filter_var($admin['email'], FILTER_VALIDATE_EMAIL)) { - throw new Exception('You must enter a valid email.'); - } - - if (! $admin['username'] || preg_match('/[^a-z0-9_-]/i', - $admin['username'])) { - throw new Exception('Username can only contain letters, numbers, underscores, and dashes.'); - } - $this->runPipeline( $this->installation ->configPath($this->input->getOption('config')) ->debugMode($this->dataSource->isDebugMode()) ->baseUrl($this->dataSource->getBaseUrl()) ->databaseConfig($dbConfig) - ->adminUser($admin) + ->adminUser($this->dataSource->getAdminUser()) ->settings($this->dataSource->getSettings()) ->build() ); diff --git a/src/Install/Console/UserDataProvider.php b/src/Install/Console/UserDataProvider.php index 6641f4265..d125470d6 100644 --- a/src/Install/Console/UserDataProvider.php +++ b/src/Install/Console/UserDataProvider.php @@ -11,6 +11,7 @@ namespace Flarum\Install\Console; +use Flarum\Install\AdminUser; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -61,13 +62,13 @@ class UserDataProvider implements DataProviderInterface return $this->baseUrl = rtrim($this->ask('Base URL:'), '/'); } - public function getAdminUser() + public function getAdminUser(): AdminUser { - return [ - 'username' => $this->ask('Admin username:'), - 'password' => $this->askForAdminPassword(), - 'email' => $this->ask('Admin email address:'), - ]; + return new AdminUser( + $this->ask('Admin username:'), + $this->askForAdminPassword(), + $this->ask('Admin email address:') + ); } private function askForAdminPassword() diff --git a/src/Install/Controller/InstallController.php b/src/Install/Controller/InstallController.php index e3c0e6ba4..7d4acaa9d 100644 --- a/src/Install/Controller/InstallController.php +++ b/src/Install/Controller/InstallController.php @@ -12,6 +12,7 @@ namespace Flarum\Install\Controller; use Flarum\Http\SessionAuthenticator; +use Flarum\Install\AdminUser; use Flarum\Install\Installation; use Flarum\Install\StepFailed; use Flarum\Install\ValidationFailed; @@ -75,11 +76,7 @@ class InstallController implements RequestHandlerInterface 'prefix' => array_get($input, 'tablePrefix'), 'strict' => false, ]) - ->adminUser([ - 'username' => array_get($input, 'adminUsername'), - 'password' => $this->getConfirmedAdminPassword($input), - 'email' => array_get($input, 'adminEmail'), - ]) + ->adminUser($this->makeAdminUser($input)) ->settings([ 'forum_title' => array_get($input, 'forumTitle'), 'mail_from' => 'noreply@'.preg_replace('/^www\./i', '', parse_url($baseUrl, PHP_URL_HOST)), @@ -102,7 +99,21 @@ class InstallController implements RequestHandlerInterface return new Response\EmptyResponse; } - private function getConfirmedAdminPassword(array $input) + /** + * @param array $input + * @return AdminUser + * @throws ValidationFailed + */ + private function makeAdminUser(array $input): AdminUser + { + return new AdminUser( + array_get($input, 'adminUsername'), + $this->getConfirmedAdminPassword($input), + array_get($input, 'adminEmail') + ); + } + + private function getConfirmedAdminPassword(array $input): string { $password = array_get($input, 'adminPassword'); $confirmation = array_get($input, 'adminPasswordConfirmation'); diff --git a/src/Install/Installation.php b/src/Install/Installation.php index 4ebfa0c1d..744a76cfa 100644 --- a/src/Install/Installation.php +++ b/src/Install/Installation.php @@ -22,7 +22,9 @@ class Installation private $dbConfig = []; private $baseUrl; private $customSettings = []; - private $adminUser = []; + + /** @var AdminUser */ + private $adminUser; // A few instance variables to persist objects between steps. // Could also be local variables in build(), but this way @@ -73,7 +75,7 @@ class Installation return $this; } - public function adminUser($admin) + public function adminUser(AdminUser $admin) { $this->adminUser = $admin; diff --git a/src/Install/Steps/CreateAdminUser.php b/src/Install/Steps/CreateAdminUser.php index faed9c6c3..b058c27cf 100644 --- a/src/Install/Steps/CreateAdminUser.php +++ b/src/Install/Steps/CreateAdminUser.php @@ -11,11 +11,10 @@ namespace Flarum\Install\Steps; -use Carbon\Carbon; use Flarum\Group\Group; +use Flarum\Install\AdminUser; use Flarum\Install\Step; use Illuminate\Database\ConnectionInterface; -use Illuminate\Hashing\BcryptHasher; class CreateAdminUser implements Step { @@ -25,11 +24,11 @@ class CreateAdminUser implements Step private $database; /** - * @var array + * @var AdminUser */ private $admin; - public function __construct(ConnectionInterface $database, array $admin) + public function __construct(ConnectionInterface $database, AdminUser $admin) { $this->database = $database; $this->admin = $admin; @@ -37,18 +36,14 @@ class CreateAdminUser implements Step public function getMessage() { - return 'Creating admin user '.$this->admin['username']; + return 'Creating admin user '.$this->admin->getUsername(); } public function run() { - $uid = $this->database->table('users')->insertGetId([ - 'username' => $this->admin['username'], - 'email' => $this->admin['email'], - 'password' => (new BcryptHasher)->make($this->admin['password']), - 'joined_at' => Carbon::now(), - 'is_email_confirmed' => 1, - ]); + $uid = $this->database->table('users')->insertGetId( + $this->admin->getAttributes() + ); $this->database->table('group_user')->insert([ 'user_id' => $uid, From 484c6d2edbd5a8d31fedd699aa8fcb41a6a65f78 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Sat, 26 Jan 2019 22:30:58 +0100 Subject: [PATCH 13/23] Extract DatabaseConfig class with validation --- src/Install/Console/DataProviderInterface.php | 3 +- src/Install/Console/FileDataProvider.php | 24 +++-- src/Install/Console/InstallCommand.php | 33 +------ src/Install/Console/UserDataProvider.php | 26 +++-- src/Install/Controller/InstallController.php | 42 ++++---- src/Install/DatabaseConfig.php | 98 +++++++++++++++++++ src/Install/Installation.php | 6 +- src/Install/Steps/ConnectToDatabase.php | 14 +-- src/Install/Steps/StoreConfig.php | 21 +--- 9 files changed, 161 insertions(+), 106 deletions(-) create mode 100644 src/Install/DatabaseConfig.php diff --git a/src/Install/Console/DataProviderInterface.php b/src/Install/Console/DataProviderInterface.php index 4913e2a28..0c4175a93 100644 --- a/src/Install/Console/DataProviderInterface.php +++ b/src/Install/Console/DataProviderInterface.php @@ -12,10 +12,11 @@ namespace Flarum\Install\Console; use Flarum\Install\AdminUser; +use Flarum\Install\DatabaseConfig; interface DataProviderInterface { - public function getDatabaseConfiguration(); + public function getDatabaseConfiguration(): DatabaseConfig; public function getBaseUrl(); diff --git a/src/Install/Console/FileDataProvider.php b/src/Install/Console/FileDataProvider.php index ec304c593..5ce2e1e26 100644 --- a/src/Install/Console/FileDataProvider.php +++ b/src/Install/Console/FileDataProvider.php @@ -13,6 +13,7 @@ namespace Flarum\Install\Console; use Exception; use Flarum\Install\AdminUser; +use Flarum\Install\DatabaseConfig; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Yaml\Yaml; @@ -52,20 +53,17 @@ class FileDataProvider implements DataProviderInterface } } - public function getDatabaseConfiguration() + public function getDatabaseConfiguration(): DatabaseConfig { - return $this->databaseConfiguration + [ - 'driver' => 'mysql', - 'host' => 'localhost', - 'database' => 'flarum', - 'username' => 'root', - 'password' => '', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'port' => '3306', - 'strict' => false, - ]; + return new DatabaseConfig( + $this->databaseConfiguration['driver'] ?? 'mysql', + $this->databaseConfiguration['host'] ?? 'localhost', + $this->databaseConfiguration['port'] ?? 3306, + $this->databaseConfiguration['database'] ?? 'flarum', + $this->databaseConfiguration['username'] ?? 'root', + $this->databaseConfiguration['password'] ?? '', + $this->databaseConfiguration['prefix'] ?? '' + ); } public function getBaseUrl() diff --git a/src/Install/Console/InstallCommand.php b/src/Install/Console/InstallCommand.php index 07995a87a..b476fb45d 100644 --- a/src/Install/Console/InstallCommand.php +++ b/src/Install/Console/InstallCommand.php @@ -11,12 +11,10 @@ namespace Flarum\Install\Console; -use Exception; use Flarum\Console\AbstractCommand; use Flarum\Install\Installation; use Flarum\Install\Pipeline; use Flarum\Install\Step; -use Illuminate\Contracts\Validation\Factory; use Symfony\Component\Console\Input\InputOption; class InstallCommand extends AbstractCommand @@ -26,11 +24,6 @@ class InstallCommand extends AbstractCommand */ protected $installation; - /** - * @var Factory - */ - protected $validator; - /** * @var DataProviderInterface */ @@ -38,12 +31,10 @@ class InstallCommand extends AbstractCommand /** * @param Installation $installation - * @param Factory $validator */ - public function __construct(Installation $installation, Factory $validator) + public function __construct(Installation $installation) { $this->installation = $installation; - $this->validator = $validator; parent::__construct(); } @@ -98,32 +89,12 @@ class InstallCommand extends AbstractCommand protected function install() { - $dbConfig = $this->dataSource->getDatabaseConfiguration(); - - $validation = $this->validator->make( - $dbConfig, - [ - 'driver' => 'required|in:mysql', - 'host' => 'required', - 'database' => 'required|string', - 'username' => 'required|string', - 'prefix' => 'nullable|alpha_dash|max:10', - 'port' => 'nullable|integer|min:1|max:65535', - ] - ); - - if ($validation->fails()) { - throw new Exception(implode("\n", - call_user_func_array('array_merge', - $validation->getMessageBag()->toArray()))); - } - $this->runPipeline( $this->installation ->configPath($this->input->getOption('config')) ->debugMode($this->dataSource->isDebugMode()) ->baseUrl($this->dataSource->getBaseUrl()) - ->databaseConfig($dbConfig) + ->databaseConfig($this->dataSource->getDatabaseConfiguration()) ->adminUser($this->dataSource->getAdminUser()) ->settings($this->dataSource->getSettings()) ->build() diff --git a/src/Install/Console/UserDataProvider.php b/src/Install/Console/UserDataProvider.php index d125470d6..ef8bd1df0 100644 --- a/src/Install/Console/UserDataProvider.php +++ b/src/Install/Console/UserDataProvider.php @@ -12,6 +12,7 @@ namespace Flarum\Install\Console; use Flarum\Install\AdminUser; +use Flarum\Install\DatabaseConfig; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -34,27 +35,24 @@ class UserDataProvider implements DataProviderInterface $this->questionHelper = $questionHelper; } - public function getDatabaseConfiguration() + public function getDatabaseConfiguration(): DatabaseConfig { $host = $this->ask('Database host:'); - $port = '3306'; + $port = 3306; if (str_contains($host, ':')) { list($host, $port) = explode(':', $host, 2); } - return [ - 'driver' => 'mysql', - 'host' => $host, - 'port' => $port, - 'database' => $this->ask('Database name:'), - 'username' => $this->ask('Database user:'), - 'password' => $this->secret('Database password:'), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => $this->ask('Prefix:'), - 'strict' => false, - ]; + return new DatabaseConfig( + 'mysql', + $host, + intval($port), + $this->ask('Database name:'), + $this->ask('Database user:'), + $this->secret('Database password:'), + $this->ask('Prefix:') + ); } public function getBaseUrl() diff --git a/src/Install/Controller/InstallController.php b/src/Install/Controller/InstallController.php index 7d4acaa9d..cbefc3849 100644 --- a/src/Install/Controller/InstallController.php +++ b/src/Install/Controller/InstallController.php @@ -13,6 +13,7 @@ namespace Flarum\Install\Controller; use Flarum\Http\SessionAuthenticator; use Flarum\Install\AdminUser; +use Flarum\Install\DatabaseConfig; use Flarum\Install\Installation; use Flarum\Install\StepFailed; use Flarum\Install\ValidationFailed; @@ -51,31 +52,12 @@ class InstallController implements RequestHandlerInterface public function handle(Request $request): ResponseInterface { $input = $request->getParsedBody(); - - $host = array_get($input, 'mysqlHost'); - $port = '3306'; - - if (str_contains($host, ':')) { - list($host, $port) = explode(':', $host, 2); - } - $baseUrl = rtrim((string) $request->getUri(), '/'); try { $pipeline = $this->installation ->baseUrl($baseUrl) - ->databaseConfig([ - 'driver' => 'mysql', - 'host' => $host, - 'port' => $port, - 'database' => array_get($input, 'mysqlDatabase'), - 'username' => array_get($input, 'mysqlUsername'), - 'password' => array_get($input, 'mysqlPassword'), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => array_get($input, 'tablePrefix'), - 'strict' => false, - ]) + ->databaseConfig($this->makeDatabaseConfig($input)) ->adminUser($this->makeAdminUser($input)) ->settings([ 'forum_title' => array_get($input, 'forumTitle'), @@ -99,6 +81,26 @@ class InstallController implements RequestHandlerInterface return new Response\EmptyResponse; } + private function makeDatabaseConfig(array $input): DatabaseConfig + { + $host = array_get($input, 'mysqlHost'); + $port = 3306; + + if (str_contains($host, ':')) { + list($host, $port) = explode(':', $host, 2); + } + + return new DatabaseConfig( + 'mysql', + $host, + intval($port), + array_get($input, 'mysqlDatabase'), + array_get($input, 'mysqlUsername'), + array_get($input, 'mysqlPassword'), + array_get($input, 'tablePrefix') + ); + } + /** * @param array $input * @return AdminUser diff --git a/src/Install/DatabaseConfig.php b/src/Install/DatabaseConfig.php new file mode 100644 index 000000000..b4e666429 --- /dev/null +++ b/src/Install/DatabaseConfig.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Install; + +class DatabaseConfig +{ + private $driver; + private $host; + private $port; + private $database; + private $username; + private $password; + private $prefix; + + public function __construct($driver, $host, $port, $database, $username, $password, $prefix) + { + $this->driver = $driver; + $this->host = $host; + $this->port = $port; + $this->database = $database; + $this->username = $username; + $this->password = $password; + $this->prefix = $prefix; + + $this->validate(); + } + + public function getConfig(): array + { + return [ + 'driver' => $this->driver, + 'host' => $this->host, + 'port' => $this->port, + 'database' => $this->database, + 'username' => $this->username, + 'password' => $this->password, + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => $this->prefix, + 'strict' => false, + 'engine' => 'InnoDB', + ]; + } + + private function validate() + { + if (empty($this->driver)) { + throw new ValidationFailed('Please specify a database driver.'); + } + + if ($this->driver !== 'mysql') { + throw new ValidationFailed('Currently, only MySQL/MariaDB is supported.'); + } + + if (empty($this->host)) { + throw new ValidationFailed('Please specify the hostname of your database server.'); + } + + if (! is_int($this->port) || $this->port < 1 || $this->port > 65535) { + throw new ValidationFailed('Please provide a valid port number between 1 and 65535.'); + } + + if (empty($this->database)) { + throw new ValidationFailed('Please specify the database name.'); + } + + if (! is_string($this->database)) { + throw new ValidationFailed('The database name must be a non-empty string.'); + } + + if (empty($this->username)) { + throw new ValidationFailed('Please specify the username for accessing the database.'); + } + + if (! is_string($this->database)) { + throw new ValidationFailed('The username must be a non-empty string.'); + } + + if (! empty($this->prefix)) { + if (! preg_match('/^[\pL\pM\pN_-]+$/u', $this->prefix)) { + throw new ValidationFailed('The prefix may only contain characters, dashes and underscores.'); + } + + if (strlen($this->prefix) > 10) { + throw new ValidationFailed('The prefix should be no longer than 10 characters.'); + } + } + } +} diff --git a/src/Install/Installation.php b/src/Install/Installation.php index 744a76cfa..074d5d135 100644 --- a/src/Install/Installation.php +++ b/src/Install/Installation.php @@ -19,10 +19,12 @@ class Installation private $configPath; private $debug = false; - private $dbConfig = []; private $baseUrl; private $customSettings = []; + /** @var DatabaseConfig */ + private $dbConfig; + /** @var AdminUser */ private $adminUser; @@ -54,7 +56,7 @@ class Installation return $this; } - public function databaseConfig(array $dbConfig) + public function databaseConfig(DatabaseConfig $dbConfig) { $this->dbConfig = $dbConfig; diff --git a/src/Install/Steps/ConnectToDatabase.php b/src/Install/Steps/ConnectToDatabase.php index 1f8357b25..027c378ca 100644 --- a/src/Install/Steps/ConnectToDatabase.php +++ b/src/Install/Steps/ConnectToDatabase.php @@ -11,6 +11,7 @@ namespace Flarum\Install\Steps; +use Flarum\Install\DatabaseConfig; use Flarum\Install\Step; use Illuminate\Database\Connectors\MySqlConnector; use Illuminate\Database\MySqlConnection; @@ -22,11 +23,9 @@ class ConnectToDatabase implements Step private $dbConfig; private $store; - public function __construct($dbConfig, callable $store) + public function __construct(DatabaseConfig $dbConfig, callable $store) { $this->dbConfig = $dbConfig; - $this->dbConfig['engine'] = 'InnoDB'; - $this->store = $store; } @@ -37,7 +36,8 @@ class ConnectToDatabase implements Step public function run() { - $pdo = (new MySqlConnector)->connect($this->dbConfig); + $config = $this->dbConfig->getConfig(); + $pdo = (new MySqlConnector)->connect($config); $version = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); @@ -48,9 +48,9 @@ class ConnectToDatabase implements Step ($this->store)( new MySqlConnection( $pdo, - $this->dbConfig['database'], - $this->dbConfig['prefix'], - $this->dbConfig + $config['database'], + $config['prefix'], + $config ) ); } diff --git a/src/Install/Steps/StoreConfig.php b/src/Install/Steps/StoreConfig.php index fca897ff4..62d4df77e 100644 --- a/src/Install/Steps/StoreConfig.php +++ b/src/Install/Steps/StoreConfig.php @@ -11,6 +11,7 @@ namespace Flarum\Install\Steps; +use Flarum\Install\DatabaseConfig; use Flarum\Install\ReversibleStep; use Flarum\Install\Step; @@ -24,7 +25,7 @@ class StoreConfig implements Step, ReversibleStep private $configFile; - public function __construct($debugMode, $dbConfig, $baseUrl, $configFile) + public function __construct($debugMode, DatabaseConfig $dbConfig, $baseUrl, $configFile) { $this->debugMode = $debugMode; $this->dbConfig = $dbConfig; @@ -55,28 +56,12 @@ class StoreConfig implements Step, ReversibleStep { return [ 'debug' => $this->debugMode, - 'database' => $this->getDatabaseConfig(), + 'database' => $this->dbConfig->getConfig(), 'url' => $this->baseUrl, 'paths' => $this->getPathsConfig(), ]; } - private function getDatabaseConfig() - { - return [ - 'driver' => $this->dbConfig['driver'], - 'host' => $this->dbConfig['host'], - 'database' => $this->dbConfig['database'], - 'username' => $this->dbConfig['username'], - 'password' => $this->dbConfig['password'], - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => $this->dbConfig['prefix'], - 'port' => $this->dbConfig['port'], - 'strict' => false, - ]; - } - private function getPathsConfig() { return [ From f4fb1ab272b200f0256f1cf21e44652ba7209bfa Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Sat, 26 Jan 2019 23:04:29 +0100 Subject: [PATCH 14/23] Simplify DataProviderInterface Instead of passing all these objects / arrays from one object to the next, simply pass an Installation instance around for configuration. --- src/Install/Console/DataProviderInterface.php | 13 ++------ src/Install/Console/FileDataProvider.php | 30 ++++++++----------- src/Install/Console/InstallCommand.php | 15 ++++------ src/Install/Console/UserDataProvider.php | 30 +++++++++++-------- 4 files changed, 38 insertions(+), 50 deletions(-) diff --git a/src/Install/Console/DataProviderInterface.php b/src/Install/Console/DataProviderInterface.php index 0c4175a93..94816bd7b 100644 --- a/src/Install/Console/DataProviderInterface.php +++ b/src/Install/Console/DataProviderInterface.php @@ -11,18 +11,9 @@ namespace Flarum\Install\Console; -use Flarum\Install\AdminUser; -use Flarum\Install\DatabaseConfig; +use Flarum\Install\Installation; interface DataProviderInterface { - public function getDatabaseConfiguration(): DatabaseConfig; - - public function getBaseUrl(); - - public function getAdminUser(): AdminUser; - - public function getSettings(); - - public function isDebugMode(): bool; + public function configure(Installation $installation): Installation; } diff --git a/src/Install/Console/FileDataProvider.php b/src/Install/Console/FileDataProvider.php index 5ce2e1e26..07626bfa4 100644 --- a/src/Install/Console/FileDataProvider.php +++ b/src/Install/Console/FileDataProvider.php @@ -14,6 +14,7 @@ namespace Flarum\Install\Console; use Exception; use Flarum\Install\AdminUser; use Flarum\Install\DatabaseConfig; +use Flarum\Install\Installation; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Yaml\Yaml; @@ -53,7 +54,17 @@ class FileDataProvider implements DataProviderInterface } } - public function getDatabaseConfiguration(): DatabaseConfig + public function configure(Installation $installation): Installation + { + return $installation + ->debugMode($this->debug) + ->baseUrl($this->baseUrl ?? 'http://flarum.local') + ->databaseConfig($this->getDatabaseConfiguration()) + ->adminUser($this->getAdminUser()) + ->settings($this->settings); + } + + private function getDatabaseConfiguration(): DatabaseConfig { return new DatabaseConfig( $this->databaseConfiguration['driver'] ?? 'mysql', @@ -66,12 +77,7 @@ class FileDataProvider implements DataProviderInterface ); } - public function getBaseUrl() - { - return $this->baseUrl ?? 'http://flarum.local'; - } - - public function getAdminUser(): AdminUser + private function getAdminUser(): AdminUser { return new AdminUser( $this->adminUser['username'] ?? 'admin', @@ -79,14 +85,4 @@ class FileDataProvider implements DataProviderInterface $this->adminUser['email'] ?? 'admin@example.com' ); } - - public function getSettings() - { - return $this->settings; - } - - public function isDebugMode(): bool - { - return $this->debug; - } } diff --git a/src/Install/Console/InstallCommand.php b/src/Install/Console/InstallCommand.php index b476fb45d..6356cf620 100644 --- a/src/Install/Console/InstallCommand.php +++ b/src/Install/Console/InstallCommand.php @@ -89,16 +89,11 @@ class InstallCommand extends AbstractCommand protected function install() { - $this->runPipeline( - $this->installation - ->configPath($this->input->getOption('config')) - ->debugMode($this->dataSource->isDebugMode()) - ->baseUrl($this->dataSource->getBaseUrl()) - ->databaseConfig($this->dataSource->getDatabaseConfiguration()) - ->adminUser($this->dataSource->getAdminUser()) - ->settings($this->dataSource->getSettings()) - ->build() - ); + $pipeline = $this->dataSource->configure( + $this->installation->configPath($this->input->getOption('config')) + )->build(); + + $this->runPipeline($pipeline); } private function runPipeline(Pipeline $pipeline) diff --git a/src/Install/Console/UserDataProvider.php b/src/Install/Console/UserDataProvider.php index ef8bd1df0..d1c4caa20 100644 --- a/src/Install/Console/UserDataProvider.php +++ b/src/Install/Console/UserDataProvider.php @@ -13,6 +13,7 @@ namespace Flarum\Install\Console; use Flarum\Install\AdminUser; use Flarum\Install\DatabaseConfig; +use Flarum\Install\Installation; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -35,7 +36,17 @@ class UserDataProvider implements DataProviderInterface $this->questionHelper = $questionHelper; } - public function getDatabaseConfiguration(): DatabaseConfig + public function configure(Installation $installation): Installation + { + return $installation + ->debugMode(false) + ->baseUrl($this->getBaseUrl()) + ->databaseConfig($this->getDatabaseConfiguration()) + ->adminUser($this->getAdminUser()) + ->settings($this->getSettings()); + } + + private function getDatabaseConfiguration(): DatabaseConfig { $host = $this->ask('Database host:'); $port = 3306; @@ -55,12 +66,12 @@ class UserDataProvider implements DataProviderInterface ); } - public function getBaseUrl() + private function getBaseUrl() { return $this->baseUrl = rtrim($this->ask('Base URL:'), '/'); } - public function getAdminUser(): AdminUser + private function getAdminUser(): AdminUser { return new AdminUser( $this->ask('Admin username:'), @@ -90,7 +101,7 @@ class UserDataProvider implements DataProviderInterface } } - public function getSettings() + private function getSettings() { $title = $this->ask('Forum title:'); $baseUrl = $this->baseUrl ?: 'http://localhost'; @@ -102,14 +113,14 @@ class UserDataProvider implements DataProviderInterface ]; } - protected function ask($question, $default = null) + private function ask($question, $default = null) { $question = new Question("$question ", $default); return $this->questionHelper->ask($this->input, $this->output, $question); } - protected function secret($question) + private function secret($question) { $question = new Question("$question "); @@ -118,14 +129,9 @@ class UserDataProvider implements DataProviderInterface return $this->questionHelper->ask($this->input, $this->output, $question); } - protected function validationError($message) + private function validationError($message) { $this->output->writeln("$message"); $this->output->writeln('Please try again.'); } - - public function isDebugMode(): bool - { - return false; - } } From 7ff9a90204923293adc520d3c02dc984845d4f9f Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Sat, 26 Jan 2019 23:41:16 +0100 Subject: [PATCH 15/23] Check MariaDB version, update MySQL constraint See flarum/docs#43. --- src/Install/Steps/ConnectToDatabase.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Install/Steps/ConnectToDatabase.php b/src/Install/Steps/ConnectToDatabase.php index 027c378ca..1e48899c0 100644 --- a/src/Install/Steps/ConnectToDatabase.php +++ b/src/Install/Steps/ConnectToDatabase.php @@ -15,7 +15,6 @@ use Flarum\Install\DatabaseConfig; use Flarum\Install\Step; use Illuminate\Database\Connectors\MySqlConnector; use Illuminate\Database\MySqlConnection; -use PDO; use RangeException; class ConnectToDatabase implements Step @@ -39,10 +38,16 @@ class ConnectToDatabase implements Step $config = $this->dbConfig->getConfig(); $pdo = (new MySqlConnector)->connect($config); - $version = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); + $version = $pdo->query('SELECT VERSION()')->fetchColumn(); - if (version_compare($version, '5.5.0', '<')) { - throw new RangeException('MySQL version too low. You need at least MySQL 5.5.'); + if (str_contains($version, 'MariaDB')) { + if (version_compare($version, '10.0.5', '<')) { + throw new RangeException('MariaDB version too low. You need at least MariaDB 10.0.5'); + } + } else { + if (version_compare($version, '5.6.0', '<')) { + throw new RangeException('MySQL version too low. You need at least MySQL 5.6.'); + } } ($this->store)( From 8ae85bc49fecd5b22659a0b4c7a77e2dc300ab75 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Thu, 31 Jan 2019 22:00:41 +0100 Subject: [PATCH 16/23] Remove obsolete dropForeign() migration Forgotten in commit 5a04635e7aa3be34ead71488e2693faf275ea6e7. --- migrations/2018_01_18_135100_change_posts_add_foreign_keys.php | 1 - 1 file changed, 1 deletion(-) diff --git a/migrations/2018_01_18_135100_change_posts_add_foreign_keys.php b/migrations/2018_01_18_135100_change_posts_add_foreign_keys.php index 00ef23223..ffdaad0df 100644 --- a/migrations/2018_01_18_135100_change_posts_add_foreign_keys.php +++ b/migrations/2018_01_18_135100_change_posts_add_foreign_keys.php @@ -41,7 +41,6 @@ return [ 'down' => function (Builder $schema) { $schema->table('posts', function (Blueprint $table) { $table->dropForeign(['user_id']); - $table->dropForeign(['discussion_id']); $table->dropForeign(['edited_user_id']); $table->dropForeign(['hidden_user_id']); }); From 8e4f02d994276a6a8140f638e91da5d6f3b1e039 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Thu, 31 Jan 2019 22:01:05 +0100 Subject: [PATCH 17/23] Fix table name in migration --- .../2018_09_22_004100_change_registration_tokens_columns.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/2018_09_22_004100_change_registration_tokens_columns.php b/migrations/2018_09_22_004100_change_registration_tokens_columns.php index 526899c84..6fc797fea 100644 --- a/migrations/2018_09_22_004100_change_registration_tokens_columns.php +++ b/migrations/2018_09_22_004100_change_registration_tokens_columns.php @@ -24,7 +24,7 @@ return [ }, 'down' => function (Builder $schema) { - $schema->table('auth_tokens', function (Blueprint $table) { + $schema->table('registration_tokens', function (Blueprint $table) { $table->dropColumn('provider', 'identifier', 'user_attributes'); $table->string('payload', 150)->change(); From 8ad326941fec7e67f9693a740ea202dd8d45bb83 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Thu, 31 Jan 2019 22:42:35 +0100 Subject: [PATCH 18/23] Migrator: Fix resetting core migrations --- src/Database/Migrator.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Database/Migrator.php b/src/Database/Migrator.php index 980abe2f6..ac61af7d7 100644 --- a/src/Database/Migrator.php +++ b/src/Database/Migrator.php @@ -146,7 +146,9 @@ class Migrator */ public function reset($path, Extension $extension = null) { - $migrations = array_reverse($this->repository->getRan($extension->getId())); + $migrations = array_reverse($this->repository->getRan( + $extension ? $extension->getId() : null + )); $count = count($migrations); From ed9591c16fb2ea7a4be3387b805d855a53e0a7d5 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Thu, 31 Jan 2019 22:43:07 +0100 Subject: [PATCH 19/23] Installer: Support reverting asset publication --- src/Install/Steps/PublishAssets.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Install/Steps/PublishAssets.php b/src/Install/Steps/PublishAssets.php index 7ec1d767a..b8f20868b 100644 --- a/src/Install/Steps/PublishAssets.php +++ b/src/Install/Steps/PublishAssets.php @@ -11,10 +11,11 @@ namespace Flarum\Install\Steps; +use Flarum\Install\ReversibleStep; use Flarum\Install\Step; use Illuminate\Filesystem\Filesystem; -class PublishAssets implements Step +class PublishAssets implements Step, ReversibleStep { /** * @var string @@ -41,7 +42,17 @@ class PublishAssets implements Step { (new Filesystem)->copyDirectory( "$this->basePath/vendor/components/font-awesome/webfonts", - "$this->assetPath/fonts" + $this->targetPath() ); } + + public function revert() + { + (new Filesystem)->deleteDirectory($this->targetPath()); + } + + private function targetPath() + { + return "$this->assetPath/fonts"; + } } From af185fd3d131053db10e854c80c8cb04113151c4 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Fri, 1 Feb 2019 10:33:21 +0100 Subject: [PATCH 20/23] Fix tests --- tests/Install/DefaultInstallationTest.php | 20 ++++++++-------- tests/Test/Concerns/CreatesForum.php | 28 +++++++++++------------ 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/tests/Install/DefaultInstallationTest.php b/tests/Install/DefaultInstallationTest.php index 02eaf4d2a..7533a9343 100644 --- a/tests/Install/DefaultInstallationTest.php +++ b/tests/Install/DefaultInstallationTest.php @@ -11,6 +11,7 @@ namespace Flarum\Tests\Install; +use Flarum\Install\AdminUser; use Flarum\Install\Installation; use Flarum\Tests\Test\TestCase; use Illuminate\Database\Connectors\ConnectionFactory; @@ -41,11 +42,9 @@ class DefaultInstallationTest extends TestCase $this->assertFileExists(base_path('config.php')); - $admin = $this->getAdmin(); - $this->assertEquals( $this->getDatabase()->table('users')->find(1)->username, - $admin['username'] + 'admin' ); } @@ -53,17 +52,16 @@ class DefaultInstallationTest extends TestCase { $factory = new ConnectionFactory(app()); - return $factory->make($this->getDatabaseConfiguration()); + return $factory->make($this->getDatabaseConfiguration()->getConfig()); } - private function getAdmin() + private function getAdmin(): AdminUser { - return [ - 'username' => 'admin', - 'password' => 'password', - 'password_confirmation' => 'password', - 'email' => 'admin@example.com', - ]; + return new AdminUser( + 'admin', + 'password', + 'admin@example.com' + ); } private function getSettings() diff --git a/tests/Test/Concerns/CreatesForum.php b/tests/Test/Concerns/CreatesForum.php index 9046aac0c..6727510c8 100644 --- a/tests/Test/Concerns/CreatesForum.php +++ b/tests/Test/Concerns/CreatesForum.php @@ -19,6 +19,7 @@ use Flarum\Foundation\SiteInterface; use Flarum\Foundation\UninstalledSite; use Flarum\Http\Server; use Flarum\Install\Console\DataProviderInterface; +use Flarum\Install\DatabaseConfig; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\Connectors\MySqlConnector; use Illuminate\Database\MySqlConnection; @@ -73,20 +74,17 @@ trait CreatesForum $this->app = $this->site->bootApp(); } - protected function getDatabaseConfiguration() + protected function getDatabaseConfiguration(): DatabaseConfig { - return [ - 'driver' => 'mysql', - 'host' => env('DB_HOST', 'localhost'), - 'database' => env('DB_DATABASE', 'flarum'), - 'username' => env('DB_USERNAME', 'root'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => env('DB_PREFIX', ''), - 'port' => '3306', - 'strict' => false, - ]; + return new DatabaseConfig( + 'mysql', + env('DB_HOST', 'localhost'), + 3306, + env('DB_DATABASE', 'flarum'), + env('DB_USERNAME', 'root'), + env('DB_PASSWORD', ''), + env('DB_PREFIX', '') + ); } protected function refreshApplication() @@ -109,7 +107,7 @@ trait CreatesForum { return [ 'debug' => true, - 'database' => $this->getDatabaseConfiguration(), + 'database' => $this->getDatabaseConfiguration()->getConfig(), 'url' => 'http://flarum.local', 'paths' => [ 'api' => 'api', @@ -124,7 +122,7 @@ trait CreatesForum return; } - $dbConfig = $this->getDatabaseConfiguration(); + $dbConfig = $this->getDatabaseConfiguration()->getConfig(); $pdo = (new MySqlConnector)->connect($dbConfig); $db = new MySqlConnection($pdo, $dbConfig['database'], $dbConfig['prefix'], $dbConfig); From 968152b740cb00f886b53e2357ce67dffe1158ef Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Fri, 1 Feb 2019 13:00:07 +0100 Subject: [PATCH 21/23] DatabaseConfig: Implement Arrayable contract --- src/Install/DatabaseConfig.php | 6 ++++-- src/Install/Steps/ConnectToDatabase.php | 2 +- src/Install/Steps/StoreConfig.php | 2 +- tests/Install/DefaultInstallationTest.php | 2 +- tests/Test/Concerns/CreatesForum.php | 4 ++-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Install/DatabaseConfig.php b/src/Install/DatabaseConfig.php index b4e666429..60ed3ea5d 100644 --- a/src/Install/DatabaseConfig.php +++ b/src/Install/DatabaseConfig.php @@ -11,7 +11,9 @@ namespace Flarum\Install; -class DatabaseConfig +use Illuminate\Contracts\Support\Arrayable; + +class DatabaseConfig implements Arrayable { private $driver; private $host; @@ -34,7 +36,7 @@ class DatabaseConfig $this->validate(); } - public function getConfig(): array + public function toArray() { return [ 'driver' => $this->driver, diff --git a/src/Install/Steps/ConnectToDatabase.php b/src/Install/Steps/ConnectToDatabase.php index 1e48899c0..51696731f 100644 --- a/src/Install/Steps/ConnectToDatabase.php +++ b/src/Install/Steps/ConnectToDatabase.php @@ -35,7 +35,7 @@ class ConnectToDatabase implements Step public function run() { - $config = $this->dbConfig->getConfig(); + $config = $this->dbConfig->toArray(); $pdo = (new MySqlConnector)->connect($config); $version = $pdo->query('SELECT VERSION()')->fetchColumn(); diff --git a/src/Install/Steps/StoreConfig.php b/src/Install/Steps/StoreConfig.php index 62d4df77e..757f3e376 100644 --- a/src/Install/Steps/StoreConfig.php +++ b/src/Install/Steps/StoreConfig.php @@ -56,7 +56,7 @@ class StoreConfig implements Step, ReversibleStep { return [ 'debug' => $this->debugMode, - 'database' => $this->dbConfig->getConfig(), + 'database' => $this->dbConfig->toArray(), 'url' => $this->baseUrl, 'paths' => $this->getPathsConfig(), ]; diff --git a/tests/Install/DefaultInstallationTest.php b/tests/Install/DefaultInstallationTest.php index 7533a9343..27eac7a42 100644 --- a/tests/Install/DefaultInstallationTest.php +++ b/tests/Install/DefaultInstallationTest.php @@ -52,7 +52,7 @@ class DefaultInstallationTest extends TestCase { $factory = new ConnectionFactory(app()); - return $factory->make($this->getDatabaseConfiguration()->getConfig()); + return $factory->make($this->getDatabaseConfiguration()->toArray()); } private function getAdmin(): AdminUser diff --git a/tests/Test/Concerns/CreatesForum.php b/tests/Test/Concerns/CreatesForum.php index 6727510c8..643afe5b1 100644 --- a/tests/Test/Concerns/CreatesForum.php +++ b/tests/Test/Concerns/CreatesForum.php @@ -107,7 +107,7 @@ trait CreatesForum { return [ 'debug' => true, - 'database' => $this->getDatabaseConfiguration()->getConfig(), + 'database' => $this->getDatabaseConfiguration()->toArray(), 'url' => 'http://flarum.local', 'paths' => [ 'api' => 'api', @@ -122,7 +122,7 @@ trait CreatesForum return; } - $dbConfig = $this->getDatabaseConfiguration()->getConfig(); + $dbConfig = $this->getDatabaseConfiguration()->toArray(); $pdo = (new MySqlConnector)->connect($dbConfig); $db = new MySqlConnection($pdo, $dbConfig['database'], $dbConfig['prefix'], $dbConfig); From 4d1411e2a81b39bdbb7b1d3144db2ac8b079856d Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Fri, 1 Feb 2019 13:00:25 +0100 Subject: [PATCH 22/23] Improve problem description for wrong PHP version --- src/Install/Prerequisite/PhpVersion.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Install/Prerequisite/PhpVersion.php b/src/Install/Prerequisite/PhpVersion.php index 59d21266f..fca994735 100644 --- a/src/Install/Prerequisite/PhpVersion.php +++ b/src/Install/Prerequisite/PhpVersion.php @@ -27,7 +27,7 @@ class PhpVersion implements PrerequisiteInterface if (version_compare(PHP_VERSION, $this->minVersion, '<')) { return collect()->push([ 'message' => "PHP $this->minVersion is required.", - 'detail' => 'You are running version '.PHP_VERSION.'. Talk to your hosting provider about upgrading to the latest PHP version.', + 'detail' => 'You are running version '.PHP_VERSION.'. You might want to talk to your system administrator about upgrading to the latest PHP version.', ]); } From 1a9f1f7a3d97814db10bd80cd5eaefc0c2713e21 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Fri, 1 Feb 2019 14:12:29 +0100 Subject: [PATCH 23/23] Use Collection class rather than collect() helper --- src/Install/Prerequisite/Composite.php | 2 +- src/Install/Prerequisite/PhpExtensions.php | 2 +- src/Install/Prerequisite/PhpVersion.php | 6 ++++-- src/Install/Prerequisite/WritablePaths.php | 4 ++-- src/Install/Steps/EnableBundledExtensions.php | 3 ++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Install/Prerequisite/Composite.php b/src/Install/Prerequisite/Composite.php index ed3a36e07..dcf380f1c 100644 --- a/src/Install/Prerequisite/Composite.php +++ b/src/Install/Prerequisite/Composite.php @@ -34,7 +34,7 @@ class Composite implements PrerequisiteInterface function (Collection $errors, PrerequisiteInterface $condition) { return $errors->concat($condition->problems()); }, - collect() + new Collection ); } } diff --git a/src/Install/Prerequisite/PhpExtensions.php b/src/Install/Prerequisite/PhpExtensions.php index bac5f0d47..89ed4ac7b 100644 --- a/src/Install/Prerequisite/PhpExtensions.php +++ b/src/Install/Prerequisite/PhpExtensions.php @@ -24,7 +24,7 @@ class PhpExtensions implements PrerequisiteInterface public function problems(): Collection { - return collect($this->extensions) + return (new Collection($this->extensions)) ->reject(function ($extension) { return extension_loaded($extension); })->map(function ($extension) { diff --git a/src/Install/Prerequisite/PhpVersion.php b/src/Install/Prerequisite/PhpVersion.php index fca994735..3257b27c9 100644 --- a/src/Install/Prerequisite/PhpVersion.php +++ b/src/Install/Prerequisite/PhpVersion.php @@ -24,13 +24,15 @@ class PhpVersion implements PrerequisiteInterface public function problems(): Collection { + $collection = new Collection; + if (version_compare(PHP_VERSION, $this->minVersion, '<')) { - return collect()->push([ + $collection->push([ 'message' => "PHP $this->minVersion is required.", 'detail' => 'You are running version '.PHP_VERSION.'. You might want to talk to your system administrator about upgrading to the latest PHP version.', ]); } - return collect(); + return $collection; } } diff --git a/src/Install/Prerequisite/WritablePaths.php b/src/Install/Prerequisite/WritablePaths.php index 99beb2a3d..0d5e8e7c5 100644 --- a/src/Install/Prerequisite/WritablePaths.php +++ b/src/Install/Prerequisite/WritablePaths.php @@ -30,7 +30,7 @@ class WritablePaths implements PrerequisiteInterface private function getMissingPaths(): Collection { - return collect($this->paths) + return (new Collection($this->paths)) ->reject(function ($path) { return file_exists($path); })->map(function ($path) { @@ -43,7 +43,7 @@ class WritablePaths implements PrerequisiteInterface private function getNonWritablePaths(): Collection { - return collect($this->paths) + return (new Collection($this->paths)) ->filter(function ($path) { return file_exists($path) && ! is_writable($path); })->map(function ($path) { diff --git a/src/Install/Steps/EnableBundledExtensions.php b/src/Install/Steps/EnableBundledExtensions.php index 235da2eb6..b887339e5 100644 --- a/src/Install/Steps/EnableBundledExtensions.php +++ b/src/Install/Steps/EnableBundledExtensions.php @@ -18,6 +18,7 @@ use Flarum\Install\Step; use Flarum\Settings\DatabaseSettingsRepository; use Illuminate\Database\ConnectionInterface; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use League\Flysystem\Adapter\Local; use League\Flysystem\Filesystem; @@ -91,7 +92,7 @@ class EnableBundledExtensions implements Step { $json = file_get_contents("$this->basePath/vendor/composer/installed.json"); - return collect(json_decode($json, true)) + return (new Collection(json_decode($json, true))) ->filter(function ($package) { return Arr::get($package, 'type') == 'flarum-extension'; })->filter(function ($package) {