Implement proper update process

If the version in the settings table mismatches the code version, then we return a 503 error for all requests coming through index.php and api.php, while admin.php serves up a form prompting for the database password which will run outstanding migrations.
This commit is contained in:
Toby Zerner 2015-10-19 15:09:54 +10:30
parent ddfedcb4dd
commit 1242fa79af
14 changed files with 290 additions and 55 deletions

13
error/503.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>503 Service Unavailable</h1>
<p>This forum is down for maintenance.</p>
</body>
</html>

View File

@ -15,7 +15,6 @@ use Flarum\Foundation\Application;
use Flarum\Http\AbstractServer;
use Zend\Stratigility\MiddlewarePipe;
use Flarum\Http\Middleware\HandleErrors;
use Franzl\Middleware\Whoops\Middleware as WhoopsMiddleware;
class Server extends AbstractServer
{
@ -27,18 +26,21 @@ class Server extends AbstractServer
$pipe = new MiddlewarePipe;
if ($app->isInstalled()) {
$app->register('Flarum\Admin\AdminServiceProvider');
$adminPath = parse_url($app->url('admin'), PHP_URL_PATH);
$routes = $app->make('flarum.admin.routes');
$errorDir = __DIR__ . '/../../error';
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie'));
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
$pipe->pipe($adminPath, $app->make('Flarum\Admin\Middleware\RequireAdministrateAbility'));
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\DispatchRoute', compact('routes')));
$pipe->pipe(new HandleErrors(__DIR__.'/../../error', $app->inDebugMode()));
if ($app->isUpToDate()) {
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie'));
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
$pipe->pipe($adminPath, $app->make('Flarum\Admin\Middleware\RequireAdministrateAbility'));
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.admin.routes')]));
$pipe->pipe($adminPath, new HandleErrors($errorDir, $app->inDebugMode()));
} else {
$app->register('Flarum\Update\UpdateServiceProvider');
$pipe->pipe($adminPath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.update.routes')]));
$pipe->pipe($adminPath, new HandleErrors($errorDir, true));
}
}
return $pipe;

View File

@ -13,6 +13,7 @@ namespace Flarum\Api;
use Flarum\Foundation\Application;
use Flarum\Http\AbstractServer;
use Tobscure\JsonApi\Document;
use Zend\Stratigility\MiddlewarePipe;
class Server extends AbstractServer
@ -24,19 +25,27 @@ class Server extends AbstractServer
{
$pipe = new MiddlewarePipe;
if ($app->isInstalled()) {
$app->register('Flarum\Api\ApiServiceProvider');
$routes = $app->make('flarum.api.routes');
$apiPath = parse_url($app->url('api'), PHP_URL_PATH);
$apiPath = parse_url($app->url('api'), PHP_URL_PATH);
if ($app->isInstalled() && $app->isUpToDate()) {
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie'));
$pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\AuthenticateWithHeader'));
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
$pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\FakeHttpMethods'));
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\DispatchRoute', compact('routes')));
$pipe->pipe($apiPath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.api.routes')]));
$pipe->pipe($apiPath, $app->make('Flarum\Api\Middleware\HandleErrors'));
} else {
$pipe->pipe($apiPath, function () {
$document = new Document;
$document->setErrors([
[
'code' => 503,
'title' => 'Service Unavailable'
]
]);
return new JsonApiResponse($document, 503);
});
}
return $pipe;

View File

@ -32,12 +32,9 @@ class Server extends AbstractServer
$console = new Application('Flarum', $app::VERSION);
if (! $app->isInstalled()) {
$app->register('Flarum\Install\InstallServiceProvider');
$console->add($app->make('Flarum\Install\Console\InstallCommand'));
}
$app->register('Flarum\Install\InstallServiceProvider');
$console->add($app->make('Flarum\Install\Console\InstallCommand'));
$console->add($app->make('Flarum\Console\Command\UpgradeCommand'));
$console->add($app->make('Flarum\Console\Command\GenerateExtensionCommand'));
$console->add($app->make('Flarum\Console\Command\GenerateMigrationCommand'));

View File

@ -13,6 +13,7 @@ namespace Flarum\Forum;
use Flarum\Foundation\Application;
use Flarum\Http\AbstractServer;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Stratigility\MiddlewarePipe;
use Flarum\Http\Middleware\HandleErrors;
@ -25,26 +26,25 @@ class Server extends AbstractServer
{
$pipe = new MiddlewarePipe;
$installed = $app->isInstalled();
$basePath = parse_url($app->url(), PHP_URL_PATH);
$errorDir = __DIR__.'/../../error';
if ($installed) {
$app->register('Flarum\Forum\ForumServiceProvider');
$routes = $app->make('flarum.forum.routes');
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie'));
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
} else {
if (! $app->isInstalled()) {
$app->register('Flarum\Install\InstallServiceProvider');
$routes = $app->make('flarum.install.routes');
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.install.routes')]));
$pipe->pipe($basePath, new HandleErrors($errorDir, true));
} elseif ($app->isUpToDate()) {
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\AuthenticateWithCookie'));
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.forum.routes')]));
$pipe->pipe($basePath, new HandleErrors($errorDir, $app->inDebugMode()));
} else {
$pipe->pipe($basePath, function () use ($errorDir) {
return new HtmlResponse(file_get_contents($errorDir.'/503.html', 503));
});
}
$pipe->pipe($basePath, $app->make('Flarum\Http\Middleware\DispatchRoute', compact('routes')));
$pipe->pipe(new HandleErrors(__DIR__.'/../../error', $app->inDebugMode() || ! $installed));
return $pipe;
}
}

View File

@ -44,16 +44,15 @@ class IndexController extends AbstractHtmlController
*/
public function render(Request $request, array $routeParams = [])
{
$view = $this->view->make('flarum.install::app');
$view = $this->view->make('flarum.install::app')->with('title', 'Install Flarum');
$this->prerequisite->check();
$errors = $this->prerequisite->getErrors();
if (count($errors)) {
$view->content = $this->view->make('flarum.install::errors');
$view->content->errors = $errors;
$view->with('content', $this->view->make('flarum.install::errors')->with('errors', $errors));
} else {
$view->content = $this->view->make('flarum.install::install');
$view->with('content', $this->view->make('flarum.install::install'));
}
return $view;

View File

@ -12,8 +12,7 @@ namespace Flarum\Install\Controller;
use Flarum\Http\Controller\ControllerInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Diactoros\Response\EmptyResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response;
use Flarum\Install\Console\InstallCommand;
use Flarum\Install\Console\DefaultsDataProvider;
@ -85,9 +84,7 @@ class InstallController implements ControllerInterface
try {
$this->command->run($input, $output);
} catch (Exception $e) {
return new JsonResponse([
'error' => $e->getMessage()
], 500);
return new HtmlResponse($e->getMessage(), 500);
}
$token = $this->bus->dispatch(

View File

@ -8,15 +8,16 @@
* file that was distributed with this source code.
*/
namespace Flarum\Console\Command;
namespace Flarum\Update\Console;
use Flarum\Console\Command\AbstractCommand;
use Illuminate\Contracts\Container\Container;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
class UpgradeCommand extends AbstractCommand
class MigrateCommand extends AbstractCommand
{
/**
* @var Container
@ -39,8 +40,8 @@ class UpgradeCommand extends AbstractCommand
protected function configure()
{
$this
->setName('upgrade')
->setDescription("Run Flarum's upgrade script");
->setName('migrate')
->setDescription("Run outstanding migrations.");
}
/**
@ -48,7 +49,7 @@ class UpgradeCommand extends AbstractCommand
*/
protected function fire()
{
$this->info('Upgrading Flarum...');
$this->info('Migrating Flarum...');
$this->upgrade();
@ -66,7 +67,7 @@ class UpgradeCommand extends AbstractCommand
$migrator->run(base_path('core/migrations'));
foreach ($migrator->getNotes() as $note) {
// $this->info($note);
$this->info($note);
}
$extensions = $this->container->make('Flarum\Extension\ExtensionManager');
@ -78,13 +79,15 @@ class UpgradeCommand extends AbstractCommand
continue;
}
// $this->info('Upgrading extension: '.$extension->name);
$this->info('Migrating extension: '.$name);
$extensions->migrate($name);
foreach ($migrator->getNotes() as $note) {
// $this->info($note);
$this->info($note);
}
}
$this->container->make('Flarum\Settings\SettingsRepositoryInterface')->set('version', $this->container->version());
}
}

View File

@ -0,0 +1,45 @@
<?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\Update\Controller;
use Flarum\Http\Controller\AbstractHtmlController;
use Psr\Http\Message\ServerRequestInterface as Request;
use Illuminate\Contracts\View\Factory;
class IndexController extends AbstractHtmlController
{
/**
* @var Factory
*/
protected $view;
/**
* @param Factory $view
*/
public function __construct(Factory $view)
{
$this->view = $view;
}
/**
* @param Request $request
* @param array $routeParams
* @return \Psr\Http\Message\ResponseInterface
*/
public function render(Request $request, array $routeParams = [])
{
$view = $this->view->make('flarum.update::app')->with('title', 'Update Flarum');
$view->with('content', $this->view->make('flarum.update::update'));
return $view;
}
}

View File

@ -0,0 +1,68 @@
<?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\Update\Controller;
use Exception;
use Flarum\Foundation\Application;
use Flarum\Http\Controller\ControllerInterface;
use Flarum\Update\Console\MigrateCommand;
use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface as Request;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\StreamOutput;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\HtmlResponse;
class UpdateController implements ControllerInterface
{
protected $command;
/**
* @var Application
*/
protected $app;
/**
* @param MigrateCommand $command
* @param Application $app
*/
public function __construct(MigrateCommand $command, Application $app)
{
$this->command = $command;
$this->app = $app;
}
/**
* @param Request $request
* @param array $routeParams
* @return \Psr\Http\Message\ResponseInterface
*/
public function handle(Request $request, array $routeParams = [])
{
$input = $request->getParsedBody();
if (array_get($input, 'databasePassword') !== $this->app->config('database.password')) {
return new HtmlResponse('Incorrect database password.', 500);
}
$body = fopen('php://temp', 'wb+');
$input = new StringInput('');
$output = new StreamOutput($body);
try {
$this->command->run($input, $output);
} catch (Exception $e) {
return new HtmlResponse($e->getMessage(), 500);
}
return new Response($body, 200);
}
}

View File

@ -0,0 +1,57 @@
<?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\Update;
use Flarum\Http\GenerateRouteHandlerTrait;
use Flarum\Http\RouteCollection;
use Flarum\Foundation\AbstractServiceProvider;
use Psr\Http\Message\ServerRequestInterface;
class UpdateServiceProvider extends AbstractServiceProvider
{
use GenerateRouteHandlerTrait;
/**
* {@inheritdoc}
*/
public function register()
{
$this->app->singleton('flarum.update.routes', function () {
return $this->getRoutes();
});
$this->loadViewsFrom(__DIR__.'/../../views/install', 'flarum.update');
}
/**
* @return RouteCollection
*/
protected function getRoutes()
{
$routes = new RouteCollection;
$toController = $this->getHandlerGenerator($this->app);
$routes->get(
'/',
'index',
$toController('Flarum\Update\Controller\IndexController')
);
$routes->post(
'/',
'update',
$toController('Flarum\Update\Controller\UpdateController')
);
return $routes;
}
}

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Install Flarum</title>
<title><?php echo $title; ?></title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
<style>

View File

@ -83,7 +83,7 @@ $(function() {
window.location.reload();
})
.fail(function(data) {
$('#error').show().text('Something went wrong:\n\n' + data.responseJSON.error);
$('#error').show().text('Something went wrong:\n\n' + data.responseText);
$button.prop('disabled', false).text('Install Flarum');
});

45
views/install/update.php Normal file
View File

@ -0,0 +1,45 @@
<h2>Update Flarum</h2>
<p>Enter your database password to update Flarum. Before you proceed, you should <strong>back up your database</strong>. If you have any trouble, get help on the <a href="http://flarum.org/docs/updating" target="_blank">Flarum website</a>.</p>
<form method="post">
<div id="error" style="display:none"></div>
<div class="FormGroup">
<div class="FormField">
<label>Database Password</label>
<input type="password" name="databasePassword">
</div>
</div>
<div class="FormButtons">
<button type="submit">Update Flarum</button>
</div>
</form>
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script>
$(function() {
$('form :input:first').select();
$('form').on('submit', function(e) {
e.preventDefault();
var $button = $(this).find('button')
.text('Please Wait...')
.prop('disabled', true);
$.post('', $(this).serialize())
.done(function() {
window.location.reload();
})
.fail(function(data) {
$('#error').show().text('Something went wrong:\n\n' + data.responseText);
$button.prop('disabled', false).text('Update Flarum');
});
return false;
});
});
</script>