Merge pull request #1617 from flarum/fl/installer-cleanup

Split up the installer logic
This commit is contained in:
Franz Liedke 2019-02-01 17:43:59 +01:00 committed by GitHub
commit 6484dc4982
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1480 additions and 753 deletions

View File

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

View File

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

View File

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

View File

@ -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.
*

View File

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

58
src/Install/AdminUser.php Normal file
View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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.');
}
}
}

View File

@ -11,15 +11,9 @@
namespace Flarum\Install\Console;
use Flarum\Install\Installation;
interface DataProviderInterface
{
public function getDatabaseConfiguration();
public function getBaseUrl();
public function getAdminUser();
public function getSettings();
public function isDebugMode(): bool;
public function configure(Installation $installation): Installation;
}

View File

@ -1,111 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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' => '',
'prefix' => '',
'port' => '3306',
];
protected $debug = false;
protected $baseUrl = 'http://flarum.local';
protected $adminUser = [
'username' => 'admin',
'password' => 'password',
'password_confirmation' => 'password',
'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',
];
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;
}
}

View File

@ -12,12 +12,14 @@
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;
class FileDataProvider implements DataProviderInterface
{
protected $default;
protected $debug = false;
protected $baseUrl = null;
protected $databaseConfiguration = [];
@ -26,9 +28,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');
@ -55,28 +54,35 @@ class FileDataProvider implements DataProviderInterface
}
}
public function getDatabaseConfiguration()
public function configure(Installation $installation): Installation
{
return $this->databaseConfiguration + $this->default->getDatabaseConfiguration();
return $installation
->debugMode($this->debug)
->baseUrl($this->baseUrl ?? 'http://flarum.local')
->databaseConfig($this->getDatabaseConfiguration())
->adminUser($this->getAdminUser())
->settings($this->settings);
}
public function getBaseUrl()
private function getDatabaseConfiguration(): DatabaseConfig
{
return (! is_null($this->baseUrl)) ? $this->baseUrl : $this->default->getBaseUrl();
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 getAdminUser()
private function getAdminUser(): AdminUser
{
return $this->adminUser + $this->default->getAdminUser();
}
public function getSettings()
{
return $this->settings + $this->default->getSettings();
}
public function isDebugMode(): bool
{
return $this->debug;
return new AdminUser(
$this->adminUser['username'] ?? 'admin',
$this->adminUser['password'] ?? 'password',
$this->adminUser['email'] ?? 'admin@example.com'
);
}
}

View File

@ -11,65 +11,32 @@
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 Symfony\Component\Console\Input\InputOption;
class InstallCommand extends AbstractCommand
{
/**
* @var Installation
*/
protected $installation;
/**
* @var DataProviderInterface
*/
protected $dataSource;
/**
* @var Application
* @param Installation $installation
*/
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)
{
$this->application = $application;
$this->installation = $installation;
parent::__construct();
$this->filesystem = $filesystem;
}
protected function configure()
@ -77,12 +44,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',
@ -104,274 +65,64 @@ class InstallCommand extends AbstractCommand
{
$this->init();
$prerequisites = $this->getPrerequisites();
$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(
'<error>Please fix the following errors before we can continue with the installation.</error>'
);
$this->showErrors($errors);
$this->showProblems($problems);
}
}
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('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();
$pipeline = $this->dataSource->configure(
$this->installation->configPath($this->input->getOption('config'))
)->build();
$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;
}
$this->runPipeline($pipeline);
}
protected function storeConfiguration(bool $debugMode)
private function runPipeline(Pipeline $pipeline)
{
$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'
);
$files = $this->application->make('files');
$this->migrator = new Migrator($repository, $this->db, $files);
$this->info('Writing config');
file_put_contents(
$this->getConfigFile(),
'<?php return '.var_export($config, true).';'
);
$pipeline
->on('start', function (Step $step) {
$this->output->write($step->getMessage().'...');
})->on('end', function () {
$this->output->write("<info>done</info>\n");
})->on('fail', function () {
$this->output->write("<error>failed</error>\n");
$this->output->writeln('Rolling back...');
})->on('rollback', function (Step $step) {
$this->output->writeln($step->getMessage().' (rollback)');
})
->run();
}
protected function runMigrations()
protected function showProblems($problems)
{
$this->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);
}
}
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')
$this->output->writeln(
'<error>Please fix the following problems before we can continue with the installation.</error>'
);
$disabled = [
'flarum-akismet',
'flarum-auth-facebook',
'flarum-auth-github',
'flarum-auth-twitter',
'flarum-pusher',
];
foreach ($problems as $problem) {
$this->info($problem['message']);
foreach ($extensions->getExtensions() as $name => $extension) {
if (in_array($name, $disabled)) {
continue;
}
$this->info('Enabling extension: '.$name);
$extensions->enable($name);
}
}
protected function publishAssets()
{
$this->filesystem->copyDirectory(
$this->application->basePath().'/vendor/components/font-awesome/webfonts',
$this->application->publicPath().'/assets/fonts'
);
}
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));
}
protected function showErrors($errors)
{
foreach ($errors as $error) {
$this->info($error['message']);
if (isset($error['detail'])) {
$this->output->writeln('<comment>'.$error['detail'].'</comment>');
if (isset($problem['detail'])) {
$this->output->writeln('<comment>'.$problem['detail'].'</comment>');
}
}
}

View File

@ -11,6 +11,9 @@
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;
@ -33,75 +36,91 @@ class UserDataProvider implements DataProviderInterface
$this->questionHelper = $questionHelper;
}
public function getDatabaseConfiguration()
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';
$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:'),
'prefix' => $this->ask('Prefix:'),
];
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()
private function getBaseUrl()
{
return $this->baseUrl = rtrim($this->ask('Base URL:'), '/');
}
public function getAdminUser()
private function getAdminUser(): AdminUser
{
return [
'username' => $this->ask('Admin username:'),
'password' => $this->secret('Admin password:'),
'password_confirmation' => $this->secret('Admin password (confirmation):'),
'email' => $this->ask('Admin email address:'),
];
return new AdminUser(
$this->ask('Admin username:'),
$this->askForAdminPassword(),
$this->ask('Admin email address:')
);
}
public function getSettings()
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;
}
}
private function getSettings()
{
$title = $this->ask('Forum title:');
$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,
];
}
protected function ask($question, $default = null)
private function ask($question, $default = null)
{
$question = new Question("<question>$question</question> ", $default);
return $this->questionHelper->ask($this->input, $this->output, $question);
}
protected function secret($question)
private function secret($question)
{
$question = new Question("<question>$question</question> ");
@ -110,8 +129,9 @@ class UserDataProvider implements DataProviderInterface
return $this->questionHelper->ask($this->input, $this->output, $question);
}
public function isDebugMode(): bool
private function validationError($message)
{
return false;
$this->output->writeln("<error>$message</error>");
$this->output->writeln('Please try again.');
}
}

View File

@ -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,13 +46,12 @@ class IndexController extends AbstractHtmlController
{
$view = $this->view->make('flarum.install::app')->with('title', 'Install Flarum');
$this->prerequisite->check();
$errors = $this->prerequisite->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;

View File

@ -11,21 +11,23 @@
namespace Flarum\Install\Controller;
use Exception;
use Flarum\Http\SessionAuthenticator;
use Flarum\Install\Console\DefaultsDataProvider;
use Flarum\Install\Console\InstallCommand;
use Flarum\Install\AdminUser;
use Flarum\Install\DatabaseConfig;
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;
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 +36,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;
}
@ -50,55 +52,78 @@ class InstallController implements RequestHandlerInterface
public function handle(Request $request): ResponseInterface
{
$input = $request->getParsedBody();
$data = new DefaultsDataProvider;
$host = array_get($input, 'mysqlHost');
$port = '3306';
if (str_contains($host, ':')) {
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);
try {
$this->command->run($input, $output);
} catch (Exception $e) {
return new HtmlResponse($e->getMessage(), 500);
$pipeline = $this->installation
->baseUrl($baseUrl)
->databaseConfig($this->makeDatabaseConfig($input))
->adminUser($this->makeAdminUser($input))
->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();
} catch (StepFailed $e) {
return new Response\HtmlResponse($e->getPrevious()->getMessage(), 500);
}
$session = $request->getAttribute('session');
$this->authenticator->logIn($session, 1);
return new Response($body);
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
* @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');
if ($password !== $confirmation) {
throw new ValidationFailed('The admin password did not match its confirmation.');
}
return $password;
}
}

View File

@ -0,0 +1,100 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Install;
use Illuminate\Contracts\Support\Arrayable;
class DatabaseConfig implements Arrayable
{
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 toArray()
{
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.');
}
}
}
}

View File

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

View File

@ -0,0 +1,164 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Install;
class Installation
{
private $basePath;
private $publicPath;
private $storagePath;
private $configPath;
private $debug = false;
private $baseUrl;
private $customSettings = [];
/** @var DatabaseConfig */
private $dbConfig;
/** @var AdminUser */
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;
$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(DatabaseConfig $dbConfig)
{
$this->dbConfig = $dbConfig;
return $this;
}
public function baseUrl($baseUrl)
{
$this->baseUrl = $baseUrl;
return $this;
}
public function settings($settings)
{
$this->customSettings = $settings;
return $this;
}
public function adminUser(AdminUser $admin)
{
$this->adminUser = $admin;
return $this;
}
public function prerequisites(): Prerequisite\PrerequisiteInterface
{
return new Prerequisite\Composite(
new Prerequisite\PhpVersion('7.1.0'),
new Prerequisite\PhpExtensions([
'dom',
'gd',
'json',
'mbstring',
'openssl',
'pdo_mysql',
'tokenizer',
]),
new Prerequisite\WritablePaths([
$this->basePath,
$this->getAssetPath(),
$this->storagePath,
])
);
}
public function build(): Pipeline
{
$pipeline = new Pipeline;
$pipeline->pipe(function () {
return new Steps\ConnectToDatabase(
$this->dbConfig,
function ($connection) {
$this->db = $connection;
}
);
});
$pipeline->pipe(function () {
return new Steps\StoreConfig(
$this->debug, $this->dbConfig, $this->baseUrl, $this->getConfigPath()
);
});
$pipeline->pipe(function () {
return new Steps\RunMigrations($this->db, $this->getMigrationPath());
});
$pipeline->pipe(function () {
return new Steps\WriteSettings($this->db, $this->customSettings);
});
$pipeline->pipe(function () {
return new Steps\CreateAdminUser($this->db, $this->adminUser);
});
$pipeline->pipe(function () {
return new Steps\PublishAssets($this->basePath, $this->getAssetPath());
});
$pipeline->pipe(function () {
return new Steps\EnableBundledExtensions($this->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';
}
}

View File

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

108
src/Install/Pipeline.php Normal file
View File

@ -0,0 +1,108 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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);
}
}
}

View File

@ -1,24 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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;
}
}

View File

@ -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
new Collection
);
}
public function getErrors()
{
return collect($this->prerequisites)->map(function (PrerequisiteInterface $prerequisite) {
return $prerequisite->getErrors();
})->reduce('array_merge', []);
}
}

View File

@ -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 (new Collection($this->extensions))
->reject(function ($extension) {
return extension_loaded($extension);
})->map(function ($extension) {
return [
'message' => "The PHP extension '$extension' is required.",
];
}
}
});
}
}

View File

@ -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,17 @@ class PhpVersion extends AbstractPrerequisite
$this->minVersion = $minVersion;
}
public function check()
public function problems(): Collection
{
$collection = new Collection;
if (version_compare(PHP_VERSION, $this->minVersion, '<')) {
$this->errors[] = [
$collection->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.',
]);
}
return $collection;
}
}

View File

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

View File

@ -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 (new Collection($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 (new Collection($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)

View File

@ -0,0 +1,17 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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();
}

33
src/Install/Step.php Normal file
View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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();
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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
{
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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\DatabaseConfig;
use Flarum\Install\Step;
use Illuminate\Database\Connectors\MySqlConnector;
use Illuminate\Database\MySqlConnection;
use RangeException;
class ConnectToDatabase implements Step
{
private $dbConfig;
private $store;
public function __construct(DatabaseConfig $dbConfig, callable $store)
{
$this->dbConfig = $dbConfig;
$this->store = $store;
}
public function getMessage()
{
return 'Connecting to database';
}
public function run()
{
$config = $this->dbConfig->toArray();
$pdo = (new MySqlConnector)->connect($config);
$version = $pdo->query('SELECT VERSION()')->fetchColumn();
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)(
new MySqlConnection(
$pdo,
$config['database'],
$config['prefix'],
$config
)
);
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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\Group\Group;
use Flarum\Install\AdminUser;
use Flarum\Install\Step;
use Illuminate\Database\ConnectionInterface;
class CreateAdminUser implements Step
{
/**
* @var ConnectionInterface
*/
private $database;
/**
* @var AdminUser
*/
private $admin;
public function __construct(ConnectionInterface $database, AdminUser $admin)
{
$this->database = $database;
$this->admin = $admin;
}
public function getMessage()
{
return 'Creating admin user '.$this->admin->getUsername();
}
public function run()
{
$uid = $this->database->table('users')->insertGetId(
$this->admin->getAttributes()
);
$this->database->table('group_user')->insert([
'user_id' => $uid,
'group_id' => Group::ADMINISTRATOR_ID,
]);
}
}

View File

@ -0,0 +1,122 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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 Illuminate\Support\Collection;
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 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',
];
/**
* @return \Illuminate\Support\Collection
*/
private function loadExtensions()
{
$json = file_get_contents("$this->basePath/vendor/composer/installed.json");
return (new Collection(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::EXTENSION_WHITELIST);
})->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
);
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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;
use Illuminate\Filesystem\Filesystem;
class PublishAssets implements Step, ReversibleStep
{
/**
* @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->targetPath()
);
}
public function revert()
{
(new Filesystem)->deleteDirectory($this->targetPath());
}
private function targetPath()
{
return "$this->assetPath/fonts";
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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);
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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\DatabaseConfig;
use Flarum\Install\ReversibleStep;
use Flarum\Install\Step;
class StoreConfig implements Step, ReversibleStep
{
private $debugMode;
private $dbConfig;
private $baseUrl;
private $configFile;
public function __construct($debugMode, DatabaseConfig $dbConfig, $baseUrl, $configFile)
{
$this->debugMode = $debugMode;
$this->dbConfig = $dbConfig;
$this->baseUrl = $baseUrl;
$this->configFile = $configFile;
}
public function getMessage()
{
return 'Writing config file';
}
public function run()
{
file_put_contents(
$this->configFile,
'<?php return '.var_export($this->buildConfig(), true).';'
);
}
public function revert()
{
@unlink($this->configFile);
}
private function buildConfig()
{
return [
'debug' => $this->debugMode,
'database' => $this->dbConfig->toArray(),
'url' => $this->baseUrl,
'paths' => $this->getPathsConfig(),
];
}
private function getPathsConfig()
{
return [
'api' => 'api',
'admin' => 'admin',
];
}
}

View File

@ -0,0 +1,80 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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 $custom;
public function __construct(ConnectionInterface $database, array $custom)
{
$this->database = $database;
$this->custom = $custom;
}
public function getMessage()
{
return 'Writing default settings';
}
public function run()
{
$repo = new DatabaseSettingsRepository($this->database);
$repo->set('version', Application::VERSION);
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',
];
}
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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
{
}

View File

@ -1,59 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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());
}
}

View File

@ -0,0 +1,75 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* 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\AdminUser;
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'));
$this->assertEquals(
$this->getDatabase()->table('users')->find(1)->username,
'admin'
);
}
private function getDatabase()
{
$factory = new ConnectionFactory(app());
return $factory->make($this->getDatabaseConfiguration()->toArray());
}
private function getAdmin(): AdminUser
{
return new AdminUser(
'admin',
'password',
'admin@example.com'
);
}
private function getSettings()
{
return [
'forum_title' => 'Development Forum',
'mail_driver' => 'log',
'welcome_title' => 'Welcome to Development Forum',
];
}
}

View File

@ -19,9 +19,11 @@ use Flarum\Foundation\SiteInterface;
use Flarum\Foundation\UninstalledSite;
use Flarum\Http\Server;
use Flarum\Install\Console\DataProviderInterface;
use Flarum\Install\Console\DefaultsDataProvider;
use Flarum\Install\DatabaseConfig;
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 +74,21 @@ trait CreatesForum
$this->app = $this->site->bootApp();
}
protected function collectsConfiguration()
protected function getDatabaseConfiguration(): DatabaseConfig
{
$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 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()
{
$this->collectsConfiguration();
$this->seedsDatabase();
$this->createsSite();
@ -108,23 +105,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()->toArray(),
'url' => 'http://flarum.local',
'paths' => [
'api' => 'api',
'admin' => 'admin',
@ -138,13 +122,13 @@ trait CreatesForum
return;
}
$app = app(\Illuminate\Contracts\Foundation\Application::class);
$dbConfig = $this->getDatabaseConfiguration()->toArray();
$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();

View File

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

View File

@ -1,14 +0,0 @@
<h2>Hold Up!</h2>
<p>These errors must be resolved before you can continue the installation. If you're having trouble, get help on the <a href="https://flarum.org/docs/install.html" target="_blank">Flarum website</a>.</p>
<div class="Errors">
<?php foreach ($errors as $error): ?>
<div class="Error">
<h3 class="Error-message"><?php echo $error['message']; ?></h3>
<?php if (! empty($error['detail'])): ?>
<p class="Error-detail"><?php echo $error['detail']; ?></p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>

View File

@ -0,0 +1,14 @@
<h2>Hold Up!</h2>
<p>These problems must be resolved before you can continue the installation. If you're having trouble, get help on the <a href="https://flarum.org/docs/install.html" target="_blank">Flarum website</a>.</p>
<div class="Problems">
<?php foreach ($problems as $problem): ?>
<div class="Problem">
<h3 class="Problem-message"><?php echo $problem['message']; ?></h3>
<?php if (! empty($problem['detail'])): ?>
<p class="Problem-detail"><?php echo $problem['detail']; ?></p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>