--- /dev/null
+<?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');
+ }
+}
$this->tpl->assign('conf', $this->conf);
}
+ /**
+ * Affect variable after controller processing.
+ * Used for alert messages.
+ */
protected function finalize(): void
{
// TODO: use the SessionManager
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
// -------- 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
$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')
--- /dev/null
+<?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]);
+ }
+}
use Shaarli\Container\ShaarliTestContainer;
use Shaarli\Front\Controller\Visitor\FrontControllerMockHelper;
+use Shaarli\History;
/**
* Trait FrontControllerMockHelper
{
$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;
+ })
+ ;
+ }
}
(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))
,
];
})
(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://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'),
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));
- }
}
// 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
;
}
+ 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.
*/
<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>
<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>
<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>
<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>