Add better maintenance handling

This commit is contained in:
Romain Neutron
2013-06-19 17:43:52 +02:00
parent 5604455695
commit ae89305575
13 changed files with 319 additions and 53 deletions

View File

@@ -76,6 +76,7 @@ use Alchemy\Phrasea\Controller\User\Preferences;
use Alchemy\Phrasea\Core\PhraseaExceptionHandler;
use Alchemy\Phrasea\Core\Event\Subscriber\LogoutSubscriber;
use Alchemy\Phrasea\Core\Event\Subscriber\PhraseaLocaleSubscriber;
use Alchemy\Phrasea\Core\Event\Subscriber\MaintenanceSubscriber;
use Alchemy\Phrasea\Core\Provider\AuthenticationManagerServiceProvider;
use Alchemy\Phrasea\Core\Provider\BrowserServiceProvider;
use Alchemy\Phrasea\Core\Provider\BorderManagerServiceProvider;
@@ -369,6 +370,7 @@ class Application extends SilexApplication
$dispatcher->addListener(KernelEvents::RESPONSE, array($app, 'disableCookiesIfRequired'), -256);
$dispatcher->addSubscriber(new LogoutSubscriber());
$dispatcher->addSubscriber(new PhraseaLocaleSubscriber($app));
$dispatcher->addSubscriber(new MaintenanceSubscriber($app));
return $dispatcher;
})

View File

@@ -57,13 +57,6 @@ class Login implements ControllerProviderInterface
if ($request->getPathInfo() == $app->path('homepage')) {
return;
}
if ($app['phraseanet.registry']->get('GV_maintenance')) {
$app->addFlash('warning', _('login::erreur: maintenance en cours, merci de nous excuser pour la gene occasionee'));
return $app->redirect($app->path('homepage', array(
'redirect' => ltrim($request->get('redirect'), '/'),
)));
}
});
// Displays the homepage
@@ -697,9 +690,7 @@ class Login implements ControllerProviderInterface
$feeds = $public_feeds->get_feeds();
array_unshift($feeds, $public_feeds->get_aggregate());
$form = $app->form(new PhraseaAuthenticationForm(), null, array(
'disabled' => $app['phraseanet.registry']->get('GV_maintenance')
));
$form = $app->form(new PhraseaAuthenticationForm());
return $app['twig']->render('login/index.html.twig', array(
'module_name' => _('Accueil'),

View File

@@ -112,7 +112,7 @@ class Session implements ControllerProviderInterface
}
if (in_array($app['session']->get('phraseanet.message'), array('1', null))) {
if ($app['phraseanet.registry']->get('GV_maintenance')) {
if ($app['phraseanet.configuration']['main']['maintenance']) {
$ret['message'] .= _('The application is going down for maintenance, please logout.');
}

View File

@@ -15,9 +15,13 @@ use Alchemy\Phrasea\Application;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class ApiExceptionHandlerSubscriber implements EventSubscriberInterface
{
@@ -44,6 +48,8 @@ class ApiExceptionHandlerSubscriber implements EventSubscriberInterface
$code = \API_V1_result::ERROR_METHODNOTALLOWED;
} elseif ($e instanceof MethodNotAllowedHttpException) {
$code = \API_V1_result::ERROR_METHODNOTALLOWED;
} elseif ($e instanceof BadRequestHttpException) {
$code = \API_V1_result::ERROR_BAD_REQUEST;
} elseif ($e instanceof \API_V1_exception_badrequest) {
$code = \API_V1_result::ERROR_BAD_REQUEST;
} elseif ($e instanceof \API_V1_exception_forbidden) {
@@ -52,13 +58,23 @@ class ApiExceptionHandlerSubscriber implements EventSubscriberInterface
$code = \API_V1_result::ERROR_UNAUTHORIZED;
} elseif ($e instanceof \API_V1_exception_internalservererror) {
$code = \API_V1_result::ERROR_INTERNALSERVERERROR;
} elseif ($e instanceof AccessDeniedHttpException) {
$code = \API_V1_result::ERROR_FORBIDDEN;
} elseif ($e instanceof UnauthorizedHttpException) {
$code = \API_V1_result::ERROR_UNAUTHORIZED;
} elseif ($e instanceof NotFoundHttpException) {
$code = \API_V1_result::ERROR_NOTFOUND;
} elseif ($e instanceof HttpExceptionInterface) {
if (503 === $e->getStatusCode()) {
$code = \API_V1_result::ERROR_MAINTENANCE;
} else {
$code = \API_V1_result::ERROR_INTERNALSERVERERROR;
}
} else {
$code = \API_V1_result::ERROR_INTERNALSERVERERROR;
}
if ($e instanceof HttpException) {
if ($e instanceof HttpExceptionInterface) {
$headers = $e->getHeaders();
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Core\Event\Subscriber;
use Silex\Application;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class MaintenanceSubscriber implements EventSubscriberInterface
{
private $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array('checkForMaintenance', 0),
);
}
public function checkForMaintenance(GetResponseEvent $event)
{
if ($this->app['phraseanet.configuration']['main']['maintenance']) {
$this->app->abort(503, 'Service Temporarily Unavailable', array('Retry-After' => 3600));
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2013 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
*
* @package APIv1
* @license http://opensource.org/licenses/gpl-3.0 GPLv3
* @link www.phraseanet.com
*/
class API_V1_exception_maintenance extends API_V1_exception_abstract
{
protected static $details = 'Server is offline for maintenance, try again soon.';
}

View File

@@ -89,6 +89,7 @@ class API_V1_result
const ERROR_UNAUTHORIZED = 'Unauthorized';
const ERROR_FORBIDDEN = 'Forbidden';
const ERROR_NOTFOUND = 'Not Found';
const ERROR_MAINTENANCE = 'Service Temporarily Unavailable';
const ERROR_METHODNOTALLOWED = 'Method Not Allowed';
const ERROR_INTERNALSERVERERROR = 'Internal Server Error';
@@ -295,6 +296,11 @@ class API_V1_result
$this->error_type = $const;
$this->error_message = API_V1_exception_internalservererror::get_details();
break;
case self::ERROR_MAINTENANCE:
$this->http_code = 503;
$this->error_type = $const;
$this->error_message = API_V1_exception_maintenance::get_details();
break;
case OAUTH2_ERROR_INVALID_REQUEST:
$this->error_type = $const;
break;

View File

@@ -50,7 +50,6 @@ class registry implements registryInterface
if ($app['phraseanet.configuration-tester']->isInstalled()) {
$this->cache->save('GV_ServerName', $app['phraseanet.configuration']['main']['servername']);
$this->cache->save('GV_debug', $app['debug']);
$this->cache->save('GV_maintenance', $app['phraseanet.configuration']['main']['maintenance']);
$config = $app['phraseanet.configuration']->getConfig();
@@ -91,7 +90,7 @@ class registry implements registryInterface
}
foreach ($rs as $row) {
if (in_array($row['key'], array('GV_ServerName', 'GV_sit', 'GV_debug', 'GV_maintenance'))) {
if (in_array($row['key'], array('GV_ServerName', 'GV_sit', 'GV_debug'))) {
continue;
}

View File

@@ -20,7 +20,7 @@
<div class="control-group">
<label class="control-label">Maintenance : </label>
<div class="controls">
<input type="checkbox" readonly="readonly" disabled="disabled" {{ app['phraseanet.registry'].get('GV_maintenance') == true ? "checked='checked'" : '' }} />
<input type="checkbox" readonly="readonly" disabled="disabled"/>
</div>
</div>
<div class="control-group">

View File

@@ -302,6 +302,32 @@ class ApplicationTest extends \PhraseanetPHPUnitAbstract
$this->assertEquals('cat.turbocat.com', $app['url_generator']->getContext()->getHost());
}
public function testMaintenanceModeTriggers503s()
{
$app = new Application('test');
$app['phraseanet.configuration.config-path'] = __DIR__ . '/Core/Event/Subscriber/Fixtures/configuration-maintenance.yml';
$app['phraseanet.configuration.config-compiled-path'] = __DIR__ . '/Core/Event/Subscriber/Fixtures/configuration-maintenance.php';
if (is_file($app['phraseanet.configuration.config-compiled-path'])) {
unlink($app['phraseanet.configuration.config-compiled-path']);
}
$app->get('/', function(Application $app, Request $request) {
return 'Hello';
});
$client = new Client($app);
$client->request('GET', '/');
$this->assertEquals(503, $client->getResponse()->getStatusCode());
$this->assertNotEquals('Hello', $client->getResponse()->getContent());
if (is_file($app['phraseanet.configuration.config-compiled-path'])) {
unlink($app['phraseanet.configuration.config-compiled-path']);
}
}
private function getAppThatReturnLocale()
{
$app = new Application('test');

View File

@@ -1287,44 +1287,6 @@ class LoginTest extends \PhraseanetWebTestCaseAuthenticatedAbstract
self::$DI['user']->set_mail_locked(false);
}
/**
* @covers \Alchemy\Phrasea\Controller\Root\Login::authenticate
*/
public function testAuthenticateUnavailable()
{
self::$DI['app']['authentication']->closeAccount();
$password = \random::generatePassword();
self::$DI['app']['phraseanet.registry']->set('GV_maintenance', true , \registry::TYPE_BOOLEAN);
self::$DI['client'] = new Client(self::$DI['app'], array());
self::$DI['client']->request('POST', '/login/authenticate/', array(
'login' => self::$DI['user']->get_login(),
'password' => $password,
'_token' => 'token'
));
self::$DI['app']['phraseanet.registry']->set('GV_maintenance', false, \registry::TYPE_BOOLEAN);
$this->assertTrue(self::$DI['client']->getResponse()->isRedirect());
$this->assertFlashMessagePopulated(self::$DI['app'], 'warning', 1);
$this->assertFalse(self::$DI['app']['authentication']->isAuthenticated());
}
/**
* @covers \Alchemy\Phrasea\Controller\Root\Login::authenticate
*/
public function testMaintenanceOnLoginDoesNotRedirect()
{
self::$DI['app']['authentication']->closeAccount();
self::$DI['app']['phraseanet.registry']->set('GV_maintenance', true , \registry::TYPE_BOOLEAN);
self::$DI['client'] = new Client(self::$DI['app'], array());
self::$DI['client']->request('GET', '/login/');
self::$DI['app']['phraseanet.registry']->set('GV_maintenance', false, \registry::TYPE_BOOLEAN);
$this->assertFalse(self::$DI['client']->getResponse()->isRedirect());
}
public function testAuthenticateWithProvider()
{
$provider = $this->getMock('Alchemy\Phrasea\Authentication\Provider\ProviderInterface');

View File

@@ -0,0 +1,141 @@
main:
servername: 'http://local.phrasea/'
maintenance: true
database:
host: sql-host
port: '3306'
user: sql-user
password: sql-password
dbname: ab_phraseanet
driver: pdo_mysql
charset: UTF8
database-test:
driver: pdo_sqlite
path: /tmp/db.sqlite
charset: UTF8
api-timers: true
cache:
type: MemcacheCache
options:
host: localhost
port: 11211
opcodecache:
type: ArrayCache
options: { }
search-engine:
type: Alchemy\Phrasea\SearchEngine\Phrasea\PhraseaEngine
options: { }
task-manager:
options: ''
trusted-proxies: { }
debugger:
allowed-ips: { }
binaries: { }
border-manager:
enabled: true
checkers:
-
type: Checker\Sha256
enabled: true
-
type: Checker\UUID
enabled: true
-
type: Checker\Colorspace
enabled: false
options:
colorspaces:
- cmyk
- grayscale
- rgb
-
type: Checker\Dimension
enabled: false
options:
width: 80
height: 160
-
type: Checker\Extension
enabled: false
options:
extensions:
- jpg
- jpeg
- bmp
- tif
- gif
- png
- pdf
- doc
- odt
- mpg
- mpeg
- mov
- avi
- xls
- flv
- mp3
- mp2
-
type: Checker\Filename
enabled: false
options:
sensitive: true
-
type: Checker\MediaType
enabled: false
options:
mediatypes:
- Audio
- Document
- Flash
- Image
- Video
authentication:
auto-create:
enabled: false
templates: { }
captcha:
enabled: true
trials-before-failure: 9
providers:
facebook:
enabled: false
options:
app-id: ''
secret: ''
twitter:
enabled: false
options:
consumer-key: ''
consumer-secret: ''
google-plus:
enabled: false
options:
client-id: ''
client-secret: ''
github:
enabled: false
options:
client-id: ''
client-secret: ''
viadeo:
enabled: false
options:
client-id: ''
client-secret: ''
linkedin:
enabled: false
options:
client-id: ''
client-secret: ''
registration-fields:
-
name: company
required: true
-
name: firstname
required: true
-
name: geonameid
required: true

View File

@@ -0,0 +1,61 @@
<?php
namespace Alchemy\Tests\Phrasea\Core\Event\Subscriber;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Core\Event\Subscriber\MaintenanceSubscriber;
use Symfony\Component\HttpKernel\Client;
use Symfony\Component\HttpKernel\Exception\HttpException;
class MaintenanceSubscriberTest extends \PHPUnit_Framework_TestCase
{
public function tearDown()
{
if (is_file(__DIR__ . '/Fixtures/configuration-maintenance.php')) {
unlink(__DIR__ . '/Fixtures/configuration-maintenance.php');
}
}
public function testCheckNegative()
{
$app = new Application();
unset($app['exception_handler']);
$app['dispatcher']->addSubscriber(new MaintenanceSubscriber($app));
$app->get('/', function () {
return 'Hello';
});
$client = new Client($app);
$client->request('GET', '/');
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$this->assertEquals('Hello', $client->getResponse()->getContent());
}
public function testCheckPositive()
{
$app = new Application();
$app['phraseanet.configuration.config-path'] = __DIR__ . '/Fixtures/configuration-maintenance.yml';
$app['phraseanet.configuration.config-compiled-path'] = __DIR__ . '/Fixtures/configuration-maintenance.php';
if (is_file($app['phraseanet.configuration.config-compiled-path'])) {
unlink($app['phraseanet.configuration.config-compiled-path']);
}
unset($app['exception_handler']);
$app['dispatcher']->addSubscriber(new MaintenanceSubscriber($app));
$app->get('/', function () {
return 'Hello';
});
$client = new Client($app);
try {
$client->request('GET', '/');
$this->fail('An exception should have been raised');
} catch (HttpException $e) {
$this->assertEquals(503, $e->getStatusCode());
$this->assertEquals(array('Retry-After' => 3600), $e->getHeaders());
}
}
}