Process configure page through Slim controller

This commit is contained in:
ArthurHoaro 2020-05-30 14:00:06 +02:00
parent 465033230d
commit 66063ed1a1
12 changed files with 427 additions and 105 deletions

View file

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use Shaarli\Languages;
use Shaarli\Render\ThemeUtils;
use Shaarli\Thumbnailer;
use Slim\Http\Request;
use Slim\Http\Response;
use Throwable;
/**
* Class PasswordController
*
* Slim controller used to handle Shaarli configuration page (display + save new config).
*/
class ConfigureController extends ShaarliAdminController
{
/**
* GET /configure - Displays the configuration page
*/
public function index(Request $request, Response $response): Response
{
$this->assignView('title', $this->container->conf->get('general.title', 'Shaarli'));
$this->assignView('theme', $this->container->conf->get('resource.theme'));
$this->assignView(
'theme_available',
ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl'))
);
$this->assignView('formatter_available', ['default', 'markdown']);
list($continents, $cities) = generateTimeZoneData(
timezone_identifiers_list(),
$this->container->conf->get('general.timezone')
);
$this->assignView('continents', $continents);
$this->assignView('cities', $cities);
$this->assignView('retrieve_description', $this->container->conf->get('general.retrieve_description', false));
$this->assignView('private_links_default', $this->container->conf->get('privacy.default_private_links', false));
$this->assignView(
'session_protection_disabled',
$this->container->conf->get('security.session_protection_disabled', false)
);
$this->assignView('enable_rss_permalinks', $this->container->conf->get('feed.rss_permalinks', false));
$this->assignView('enable_update_check', $this->container->conf->get('updates.check_updates', true));
$this->assignView('hide_public_links', $this->container->conf->get('privacy.hide_public_links', false));
$this->assignView('api_enabled', $this->container->conf->get('api.enabled', true));
$this->assignView('api_secret', $this->container->conf->get('api.secret'));
$this->assignView('languages', Languages::getAvailableLanguages());
$this->assignView('gd_enabled', extension_loaded('gd'));
$this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
$this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
return $response->write($this->render('configure'));
}
/**
* POST /configure - Update Shaarli's configuration
*/
public function save(Request $request, Response $response): Response
{
$this->checkToken($request);
$continent = $request->getParam('continent');
$city = $request->getParam('city');
$tz = 'UTC';
if (null !== $continent && null !== $city && isTimeZoneValid($continent, $city)) {
$tz = $continent . '/' . $city;
}
$this->container->conf->set('general.timezone', $tz);
$this->container->conf->set('general.title', escape($request->getParam('title')));
$this->container->conf->set('general.header_link', escape($request->getParam('titleLink')));
$this->container->conf->set('general.retrieve_description', !empty($request->getParam('retrieveDescription')));
$this->container->conf->set('resource.theme', escape($request->getParam('theme')));
$this->container->conf->set(
'security.session_protection_disabled',
!empty($request->getParam('disablesessionprotection'))
);
$this->container->conf->set(
'privacy.default_private_links',
!empty($request->getParam('privateLinkByDefault'))
);
$this->container->conf->set('feed.rss_permalinks', !empty($request->getParam('enableRssPermalinks')));
$this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
$this->container->conf->set('privacy.hide_public_links', !empty($request->getParam('hidePublicLinks')));
$this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
$this->container->conf->set('api.secret', escape($request->getParam('apiSecret')));
$this->container->conf->set('formatter', escape($request->getParam('formatter')));
if (!empty($request->getParam('language'))) {
$this->container->conf->set('translation.language', escape($request->getParam('language')));
}
$thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE;
if ($thumbnailsMode !== Thumbnailer::MODE_NONE
&& $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
) {
$this->saveWarningMessage(t(
'You have enabled or changed thumbnails mode. '
.'<a href="./?do=thumbs_update">Please synchronize them</a>.'
));
}
$this->container->conf->set('thumbnails.mode', $thumbnailsMode);
try {
$this->container->conf->write($this->container->loginManager->isLoggedIn());
$this->container->history->updateSettings();
$this->container->pageCacheManager->invalidateCaches();
} catch (Throwable $e) {
// TODO: translation + stacktrace
$this->saveErrorMessage('ERROR while writing config file after configuration update.');
}
$this->saveSuccessMessage(t('Configuration was saved.'));
return $response->withRedirect('./configure');
}
}

View file

@ -143,6 +143,10 @@ private function initialize()
$this->tpl->assign('conf', $this->conf);
}
/**
* Affect variable after controller processing.
* Used for alert messages.
*/
protected function finalize(): void
{
// TODO: use the SessionManager

View file

@ -35,7 +35,7 @@ http://<replace_domain>/?nonope
http://<replace_domain>/?do=addlink
http://<replace_domain>/?do=changepasswd
http://<replace_domain>/?do=changetag
http://<replace_domain>/?do=configure
http://<replace_domain>/configure
http://<replace_domain>/tools
http://<replace_domain>/daily
http://<replace_domain>/?post

View file

@ -513,88 +513,8 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
// -------- User wants to change configuration
if ($targetPage == Router::$PAGE_CONFIGURE) {
if (!empty($_POST['title'])) {
if (!$sessionManager->checkToken($_POST['token'])) {
die(t('Wrong token.')); // Go away!
}
$tz = 'UTC';
if (!empty($_POST['continent']) && !empty($_POST['city'])
&& isTimeZoneValid($_POST['continent'], $_POST['city'])
) {
$tz = $_POST['continent'] . '/' . $_POST['city'];
}
$conf->set('general.timezone', $tz);
$conf->set('general.title', escape($_POST['title']));
$conf->set('general.header_link', escape($_POST['titleLink']));
$conf->set('general.retrieve_description', !empty($_POST['retrieveDescription']));
$conf->set('resource.theme', escape($_POST['theme']));
$conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
$conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
$conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
$conf->set('updates.check_updates', !empty($_POST['updateCheck']));
$conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
$conf->set('api.enabled', !empty($_POST['enableApi']));
$conf->set('api.secret', escape($_POST['apiSecret']));
$conf->set('formatter', escape($_POST['formatter']));
if (! empty($_POST['language'])) {
$conf->set('translation.language', escape($_POST['language']));
}
$thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
if ($thumbnailsMode !== Thumbnailer::MODE_NONE
&& $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
) {
$_SESSION['warnings'][] = t(
'You have enabled or changed thumbnails mode. '
.'<a href="./?do=thumbs_update">Please synchronize them</a>.'
);
}
$conf->set('thumbnails.mode', $thumbnailsMode);
try {
$conf->write($loginManager->isLoggedIn());
$history->updateSettings();
$pageCacheManager->invalidateCaches();
} catch (Exception $e) {
error_log(
'ERROR while writing config file after configuration update.' . PHP_EOL .
$e->getMessage()
);
// TODO: do not handle exceptions/errors in JS.
echo '<script>alert("'. $e->getMessage() .'");document.location=\'./?do=configure\';</script>';
exit;
}
echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'./?do=configure\';</script>';
exit;
} else {
// Show the configuration form.
$PAGE->assign('title', $conf->get('general.title'));
$PAGE->assign('theme', $conf->get('resource.theme'));
$PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
$PAGE->assign('formatter_available', ['default', 'markdown']);
list($continents, $cities) = generateTimeZoneData(
timezone_identifiers_list(),
$conf->get('general.timezone')
);
$PAGE->assign('continents', $continents);
$PAGE->assign('cities', $cities);
$PAGE->assign('retrieve_description', $conf->get('general.retrieve_description'));
$PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
$PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
$PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
$PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true));
$PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
$PAGE->assign('api_enabled', $conf->get('api.enabled', true));
$PAGE->assign('api_secret', $conf->get('api.secret'));
$PAGE->assign('languages', Languages::getAvailableLanguages());
$PAGE->assign('gd_enabled', extension_loaded('gd'));
$PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
$PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
$PAGE->renderPage('configure');
exit;
}
header('Location: ./configure');
exit;
}
// -------- User wants to rename a tag or delete it
@ -1458,6 +1378,8 @@ function install($conf, $sessionManager, $loginManager)
$this->get('/tools', '\Shaarli\Front\Controller\Admin\ToolsController:index')->setName('tools');
$this->get('/password', '\Shaarli\Front\Controller\Admin\PasswordController:index')->setName('password');
$this->post('/password', '\Shaarli\Front\Controller\Admin\PasswordController:change')->setName('changePassword');
$this->get('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:index')->setName('configure');
$this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save')->setName('saveConfigure');
$this
->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage')

View file

@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use PHPUnit\Framework\TestCase;
use Shaarli\Config\ConfigManager;
use Shaarli\Front\Exception\WrongTokenException;
use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
use Slim\Http\Request;
use Slim\Http\Response;
class ConfigureControllerTest extends TestCase
{
use FrontAdminControllerMockHelper;
/** @var ConfigureController */
protected $controller;
public function setUp(): void
{
$this->createContainer();
$this->controller = new ConfigureController($this->container);
}
/**
* Test displaying configure page - it should display all config variables
*/
public function testIndex(): void
{
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$request = $this->createMock(Request::class);
$response = new Response();
$this->container->conf = $this->createMock(ConfigManager::class);
$this->container->conf->method('get')->willReturnCallback(function (string $key) {
return $key;
});
$result = $this->controller->index($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('configure', (string) $result->getBody());
static::assertSame('Configure - general.title', $assignedVariables['pagetitle']);
static::assertSame('general.title', $assignedVariables['title']);
static::assertSame('resource.theme', $assignedVariables['theme']);
static::assertEmpty($assignedVariables['theme_available']);
static::assertSame(['default', 'markdown'], $assignedVariables['formatter_available']);
static::assertNotEmpty($assignedVariables['continents']);
static::assertNotEmpty($assignedVariables['cities']);
static::assertSame('general.retrieve_description', $assignedVariables['retrieve_description']);
static::assertSame('privacy.default_private_links', $assignedVariables['private_links_default']);
static::assertSame('security.session_protection_disabled', $assignedVariables['session_protection_disabled']);
static::assertSame('feed.rss_permalinks', $assignedVariables['enable_rss_permalinks']);
static::assertSame('updates.check_updates', $assignedVariables['enable_update_check']);
static::assertSame('privacy.hide_public_links', $assignedVariables['hide_public_links']);
static::assertSame('api.enabled', $assignedVariables['api_enabled']);
static::assertSame('api.secret', $assignedVariables['api_secret']);
static::assertCount(4, $assignedVariables['languages']);
static::assertArrayHasKey('gd_enabled', $assignedVariables);
static::assertSame('thumbnails.mode', $assignedVariables['thumbnails_mode']);
}
/**
* Test posting a new config - make sure that everything is saved properly, without errors.
*/
public function testSaveNewConfig(): void
{
$session = [];
$this->assignSessionVars($session);
$parameters = [
'token' => 'token',
'continent' => 'Europe',
'city' => 'Moscow',
'title' => 'Shaarli',
'titleLink' => './',
'retrieveDescription' => 'on',
'theme' => 'vintage',
'disablesessionprotection' => null,
'privateLinkByDefault' => true,
'enableRssPermalinks' => true,
'updateCheck' => false,
'hidePublicLinks' => 'on',
'enableApi' => 'on',
'apiSecret' => 'abcdef',
'formatter' => 'markdown',
'language' => 'fr',
'enableThumbnails' => Thumbnailer::MODE_NONE,
];
$parametersConfigMapping = [
'general.timezone' => $parameters['continent'] . '/' . $parameters['city'],
'general.title' => $parameters['title'],
'general.header_link' => $parameters['titleLink'],
'general.retrieve_description' => !!$parameters['retrieveDescription'],
'resource.theme' => $parameters['theme'],
'security.session_protection_disabled' => !!$parameters['disablesessionprotection'],
'privacy.default_private_links' => !!$parameters['privateLinkByDefault'],
'feed.rss_permalinks' => !!$parameters['enableRssPermalinks'],
'updates.check_updates' => !!$parameters['updateCheck'],
'privacy.hide_public_links' => !!$parameters['hidePublicLinks'],
'api.enabled' => !!$parameters['enableApi'],
'api.secret' => $parameters['apiSecret'],
'formatter' => $parameters['formatter'],
'translation.language' => $parameters['language'],
'thumbnails.mode' => $parameters['enableThumbnails'],
];
$request = $this->createMock(Request::class);
$request
->expects(static::atLeastOnce())
->method('getParam')->willReturnCallback(function (string $key) use ($parameters) {
if (false === array_key_exists($key, $parameters)) {
static::fail('unknown key: ' . $key);
}
return $parameters[$key];
}
);
$response = new Response();
$this->container->conf = $this->createMock(ConfigManager::class);
$this->container->conf
->expects(static::atLeastOnce())
->method('set')
->willReturnCallback(function (string $key, $value) use ($parametersConfigMapping): void {
if (false === array_key_exists($key, $parametersConfigMapping)) {
static::fail('unknown key: ' . $key);
}
static::assertSame($parametersConfigMapping[$key], $value);
}
);
$result = $this->controller->save($request, $response);
static::assertSame(302, $result->getStatusCode());
static::assertSame(['./configure'], $result->getHeader('Location'));
static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
}
/**
* Test posting a new config - wrong token.
*/
public function testSaveNewConfigWrongToken(): void
{
$this->container->sessionManager = $this->createMock(SessionManager::class);
$this->container->sessionManager->method('checkToken')->willReturn(false);
$this->container->conf->expects(static::never())->method('set');
$this->container->conf->expects(static::never())->method('write');
$request = $this->createMock(Request::class);
$response = new Response();
$this->expectException(WrongTokenException::class);
$this->controller->save($request, $response);
}
/**
* Test posting a new config - thumbnail activation.
*/
public function testSaveNewConfigThumbnailsActivation(): void
{
$session = [];
$this->assignSessionVars($session);
$request = $this->createMock(Request::class);
$request
->expects(static::atLeastOnce())
->method('getParam')->willReturnCallback(function (string $key) {
if ('enableThumbnails' === $key) {
return Thumbnailer::MODE_ALL;
}
return $key;
})
;
$response = new Response();
$result = $this->controller->save($request, $response);
static::assertSame(302, $result->getStatusCode());
static::assertSame(['./configure'], $result->getHeader('Location'));
static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
static::assertArrayHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
static::assertStringContainsString(
'You have enabled or changed thumbnails mode',
$session[SessionManager::KEY_WARNING_MESSAGES][0]
);
static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
}
/**
* Test posting a new config - thumbnail activation.
*/
public function testSaveNewConfigThumbnailsAlreadyActive(): void
{
$session = [];
$this->assignSessionVars($session);
$request = $this->createMock(Request::class);
$request
->expects(static::atLeastOnce())
->method('getParam')->willReturnCallback(function (string $key) {
if ('enableThumbnails' === $key) {
return Thumbnailer::MODE_ALL;
}
return $key;
})
;
$response = new Response();
$this->container->conf = $this->createMock(ConfigManager::class);
$this->container->conf
->expects(static::atLeastOnce())
->method('get')
->willReturnCallback(function (string $key): string {
if ('thumbnails.mode' === $key) {
return Thumbnailer::MODE_ALL;
}
return $key;
})
;
$result = $this->controller->save($request, $response);
static::assertSame(302, $result->getStatusCode());
static::assertSame(['./configure'], $result->getHeader('Location'));
static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
static::assertSame(['Configuration was saved.'], $session[SessionManager::KEY_SUCCESS_MESSAGES]);
}
}

View file

@ -6,6 +6,7 @@
use Shaarli\Container\ShaarliTestContainer;
use Shaarli\Front\Controller\Visitor\FrontControllerMockHelper;
use Shaarli\History;
/**
* Trait FrontControllerMockHelper
@ -27,7 +28,29 @@ protected function createContainer(): void
{
$this->parentCreateContainer();
$this->container->history = $this->createMock(History::class);
$this->container->loginManager->method('isLoggedIn')->willReturn(true);
$this->container->sessionManager->method('checkToken')->willReturn(true);
}
/**
* Pass a reference of an array which will be populated by `sessionManager->setSessionParameter`
* calls during execution.
*
* @param mixed $variables Array reference to populate.
*/
protected function assignSessionVars(array &$variables): void
{
$this->container->sessionManager
->expects(static::atLeastOnce())
->method('setSessionParameter')
->willReturnCallback(function ($key, $value) use (&$variables) {
$variables[$key] = $value;
return $this->container->sessionManager;
})
;
}
}

View file

@ -57,20 +57,20 @@ public function testValidIndexControllerInvokeDefault(): void
(new Bookmark())
->setId(1)
->setUrl('http://url.tld')
->setTitle(static::generateContent(50))
->setDescription(static::generateContent(500))
->setTitle(static::generateString(50))
->setDescription(static::generateString(500))
,
(new Bookmark())
->setId(2)
->setUrl('http://url2.tld')
->setTitle(static::generateContent(50))
->setDescription(static::generateContent(500))
->setTitle(static::generateString(50))
->setDescription(static::generateString(500))
,
(new Bookmark())
->setId(3)
->setUrl('http://url3.tld')
->setTitle(static::generateContent(50))
->setDescription(static::generateContent(500))
->setTitle(static::generateString(50))
->setDescription(static::generateString(500))
,
];
})
@ -194,8 +194,8 @@ public function testValidIndexControllerInvokeNoFutureOrPast(): void
(new Bookmark())
->setId(1)
->setUrl('http://url.tld')
->setTitle(static::generateContent(50))
->setDescription(static::generateContent(500))
->setTitle(static::generateString(50))
->setDescription(static::generateString(500))
,
];
})
@ -267,8 +267,8 @@ public function testValidIndexControllerInvokeHeightAdjustment(): void
(new Bookmark())
->setId(2)
->setUrl('http://url.tld')
->setTitle(static::generateContent(50))
->setDescription(static::generateContent(5000))
->setTitle(static::generateString(50))
->setDescription(static::generateString(5000))
,
(new Bookmark())->setId(3)->setUrl('http://url.tld')->setTitle('title'),
(new Bookmark())->setId(4)->setUrl('http://url.tld')->setTitle('title'),
@ -473,11 +473,4 @@ public function testValidRssControllerInvokeNoBookmark(): void
static::assertFalse($assignedVariables['hide_timestamps']);
static::assertCount(0, $assignedVariables['days']);
}
protected static function generateContent(int $length): string
{
// bin2hex(random_bytes) generates string twice as long as given parameter
$length = (int) ceil($length / 2);
return bin2hex(random_bytes($length));
}
}

View file

@ -42,7 +42,7 @@ protected function createContainer(): void
// Config
$this->container->conf = $this->createMock(ConfigManager::class);
$this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
return $default;
return $default === null ? $parameter : $default;
});
// PageBuilder
@ -101,6 +101,14 @@ protected function assignTemplateVars(array &$variables): void
;
}
protected static function generateString(int $length): string
{
// bin2hex(random_bytes) generates string twice as long as given parameter
$length = (int) ceil($length / 2);
return bin2hex(random_bytes($length));
}
/**
* Force to be used in PHPUnit context.
*/

View file

@ -35,7 +35,7 @@ <h2 class="window-title">{'Configure'|t}</h2>
<div class="form-label">
<label for="titleLink">
<span class="label-name">{'Home link'|t}</span><br>
<span class="label-desc">{'Default value'|t}: ?</span>
<span class="label-desc">{'Default value'|t}: ./</span>
</label>
</div>
</div>

View file

@ -11,7 +11,7 @@
<div class="pure-u-lg-1-3 pure-u-22-24 page-form page-form-light">
<h2 class="window-title">{'Settings'|t}</h2>
<div class="tools-item">
<a href="./?do=configure" title="{'Change Shaarli settings: title, timezone, etc.'|t}">
<a href="./configure" title="{'Change Shaarli settings: title, timezone, etc.'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Configure your Shaarli'|t}</span>
</a>
</div>

View file

@ -16,7 +16,7 @@
<tr>
<td><b>Home link:</b></td>
<td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label
for="titleLink">(default value is: ?)</label></td>
for="titleLink">(default value is: ./)</label></td>
</tr>
<tr>

View file

@ -5,7 +5,7 @@
<div id="pageheader">
{include="page.header"}
<div id="toolsdiv">
<a href="./?do=configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
<a href="./configure"><b>Configure your Shaarli</b><span>: Change Title, timezone...</span></a>
<br><br>
<a href="./?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
<br><br>