diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php
index a85792961c..a657c4e951 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 @@ public function hasAssets()
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 @@ public function hasMigrations()
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 45decbe948..2214183674 100644
--- a/src/Extension/ExtensionManager.php
+++ b/src/Extension/ExtensionManager.php
@@ -222,26 +222,16 @@ public function getAsset(Extension $extension, $path)
* 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 @@ public function migrate(Extension $extension, $up = true)
*/
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 70f300071f..62841faeab 100644
--- a/src/Install/Console/DefaultsDataProvider.php
+++ b/src/Install/Console/DefaultsDataProvider.php
@@ -14,13 +14,16 @@
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 37cd346554..e429d62d89 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 DataProviderInterface
- */
- protected $dataSource;
-
- /**
- * @var Application
+ * @var Installation
*/
- protected $application;
+ protected $installation;
/**
- * @var Filesystem
+ * @var Factory
*/
- protected $filesystem;
+ protected $validator;
/**
- * @var ConnectionInterface
- */
- protected $db;
-
- /**
- * @var Migrator
+ * @var DataProviderInterface
*/
- protected $migrator;
+ protected $dataSource;
/**
- * @param Application $application
- * @param Filesystem $filesystem
+ * @param Installation $installation
+ * @param Factory $validator
*/
- 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 @@ protected function fire()
{
$this->init();
- $prerequisites = $this->getPrerequisites();
+ $prerequisites = $this->installation->prerequisites();
$prerequisites->check();
$errors = $prerequisites->getErrors();
@@ -124,245 +100,82 @@ protected function fire()
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();
-
- $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'
+ $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',
+ ]
);
- $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');
+ if ($validation->fails()) {
+ throw new Exception(implode("\n",
+ call_user_func_array('array_merge',
+ $validation->getMessageBag()->toArray())));
+ }
- $settings->set('version', $this->application->version());
+ $admin = $this->dataSource->getAdminUser();
- foreach ($this->settings as $k => $v) {
- $settings->set($k, $v);
+ if (strlen($admin['password']) < 8) {
+ throw new Exception('Password must be at least 8 characters.');
}
- }
-
- protected function createAdminUser()
- {
- $admin = $this->adminUser;
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);
+ if (! filter_var($admin['email'], FILTER_VALIDATE_EMAIL)) {
+ throw new Exception('You must enter a valid email.');
+ }
- $extensions->enable($name);
+ if (! $admin['username'] || preg_match('/[^a-z0-9_-]/i',
+ $admin['username'])) {
+ throw new Exception('Username can only contain letters, numbers, underscores, and dashes.');
}
- }
- protected function publishAssets()
- {
- $this->filesystem->copyDirectory(
- $this->application->basePath().'/vendor/components/font-awesome/webfonts',
- $this->application->publicPath().'/assets/fonts'
+ $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()
- {
- 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));
+ private function runPipeline(Pipeline $pipeline)
+ {
+ $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 a84df20506..eb10ef5ba3 100644
--- a/src/Install/Console/UserDataProvider.php
+++ b/src/Install/Console/UserDataProvider.php
@@ -43,13 +43,16 @@ public function getDatabaseConfiguration()
}
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 e961109697..03b1e24490 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 @@ public function render(Request $request)
{
$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 aaa13e2775..835a6e312b 100644
--- a/src/Install/Controller/InstallController.php
+++ b/src/Install/Controller/InstallController.php
@@ -13,19 +13,18 @@
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 @@ public function handle(Request $request): ResponseInterface
{
$input = $request->getParsedBody();
- $data = new DefaultsDataProvider;
-
$host = array_get($input, 'mysqlHost');
$port = '3306';
@@ -60,45 +57,58 @@ public function handle(Request $request): ResponseInterface
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 4a0c8ae039..1ea4fa07bc 100644
--- a/src/Install/InstallServiceProvider.php
+++ b/src/Install/InstallServiceProvider.php
@@ -14,11 +14,6 @@
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 0000000000..0da25ca9f8
--- /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 3df6a1b865..287a513fe3 100644
--- a/src/Install/Installer.php
+++ b/src/Install/Installer.php
@@ -17,6 +17,8 @@
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 @@ public function getRequestHandler()
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 0000000000..31c8cb1c90
--- /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 0000000000..b65de49936
--- /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 0000000000..6b0de2658b
--- /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 0000000000..8fe1a3b7d0
--- /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 0000000000..bab1a160e0
--- /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 0000000000..1f8357b251
--- /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 0000000000..af51fa04d2
--- /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 0000000000..f3f045b0aa
--- /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 0000000000..7ec1d767ae
--- /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 0000000000..3bf2049ef3
--- /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 0000000000..f2c2a22f33
--- /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 0000000000..657e838d95
--- /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);
+ }
+ }
+}