Feature: add a Server administration page

It contains mostly read only information about the current Shaarli instance,
PHP version, extensions, file and folder permissions, etc.
Also action buttons to clear the cache or sync thumbnails.

Part of the content of this page is also displayed on the install page,
to check server requirement before installing Shaarli config file.

Fixes #40
Fixes #185
This commit is contained in:
ArthurHoaro 2020-10-21 13:12:15 +02:00
parent d8030c8155
commit 0cf76ccb47
16 changed files with 1086 additions and 104 deletions

View file

@ -14,8 +14,9 @@ class ApplicationUtils
*/ */
public static $VERSION_FILE = 'shaarli_version.php'; public static $VERSION_FILE = 'shaarli_version.php';
private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli';
private static $GIT_BRANCHES = array('latest', 'stable'); public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
public static $GIT_BRANCHES = array('latest', 'stable');
private static $VERSION_START_TAG = '<?php /* '; private static $VERSION_START_TAG = '<?php /* ';
private static $VERSION_END_TAG = ' */ ?>'; private static $VERSION_END_TAG = ' */ ?>';
@ -125,7 +126,7 @@ class ApplicationUtils
// Late Static Binding allows overriding within tests // Late Static Binding allows overriding within tests
// See http://php.net/manual/en/language.oop5.late-static-bindings.php // See http://php.net/manual/en/language.oop5.late-static-bindings.php
$latestVersion = static::getVersion( $latestVersion = static::getVersion(
self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
); );
if (!$latestVersion) { if (!$latestVersion) {
@ -172,34 +173,44 @@ class ApplicationUtils
* Checks Shaarli has the proper access permissions to its resources * Checks Shaarli has the proper access permissions to its resources
* *
* @param ConfigManager $conf Configuration Manager instance. * @param ConfigManager $conf Configuration Manager instance.
* @param bool $minimalMode In minimal mode we only check permissions to be able to display a template.
* Currently we only need to be able to read the theme and write in raintpl cache.
* *
* @return array A list of the detected configuration issues * @return array A list of the detected configuration issues
*/ */
public static function checkResourcePermissions($conf) public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array
{ {
$errors = array(); $errors = [];
$rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
// Check script and template directories are readable // Check script and template directories are readable
foreach (array( foreach ([
'application', 'application',
'inc', 'inc',
'plugins', 'plugins',
$rainTplDir, $rainTplDir,
$rainTplDir . '/' . $conf->get('resource.theme'), $rainTplDir . '/' . $conf->get('resource.theme'),
) as $path) { ] as $path) {
if (!is_readable(realpath($path))) { if (!is_readable(realpath($path))) {
$errors[] = '"' . $path . '" ' . t('directory is not readable'); $errors[] = '"' . $path . '" ' . t('directory is not readable');
} }
} }
// Check cache and data directories are readable and writable // Check cache and data directories are readable and writable
foreach (array( if ($minimalMode) {
$folders = [
$conf->get('resource.raintpl_tmp'),
];
} else {
$folders = [
$conf->get('resource.thumbnails_cache'), $conf->get('resource.thumbnails_cache'),
$conf->get('resource.data_dir'), $conf->get('resource.data_dir'),
$conf->get('resource.page_cache'), $conf->get('resource.page_cache'),
$conf->get('resource.raintpl_tmp'), $conf->get('resource.raintpl_tmp'),
) as $path) { ];
}
foreach ($folders as $path) {
if (!is_readable(realpath($path))) { if (!is_readable(realpath($path))) {
$errors[] = '"' . $path . '" ' . t('directory is not readable'); $errors[] = '"' . $path . '" ' . t('directory is not readable');
} }
@ -208,6 +219,10 @@ class ApplicationUtils
} }
} }
if ($minimalMode) {
return $errors;
}
// Check configuration files are readable and writable // Check configuration files are readable and writable
foreach (array( foreach (array(
$conf->getConfigFileExt(), $conf->getConfigFileExt(),
@ -246,4 +261,54 @@ class ApplicationUtils
{ {
return hash_hmac('sha256', $currentVersion, $salt); return hash_hmac('sha256', $currentVersion, $salt);
} }
/**
* Get a list of PHP extensions used by Shaarli.
*
* @return array[] List of extension with following keys:
* - name: extension name
* - required: whether the extension is required to use Shaarli
* - desc: short description of extension usage in Shaarli
* - loaded: whether the extension is properly loaded or not
*/
public static function getPhpExtensionsRequirement(): array
{
$extensions = [
['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')],
['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')],
['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')],
['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')],
['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')],
['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')],
['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')],
['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')],
];
foreach ($extensions as &$extension) {
$extension['loaded'] = extension_loaded($extension['name']);
}
return $extensions;
}
/**
* Return the EOL date of given PHP version. If the version is unknown,
* we return today + 2 years.
*
* @param string $fullVersion PHP version, e.g. 7.4.7
*
* @return string Date format: YYYY-MM-DD
*/
public static function getPhpEol(string $fullVersion): string
{
preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches);
return [
'7.1' => '2019-12-01',
'7.2' => '2020-11-30',
'7.3' => '2021-12-06',
'7.4' => '2022-11-28',
'8.0' => '2023-12-01',
][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d');
}
} }

View file

@ -81,4 +81,60 @@ class FileUtils
) )
); );
} }
/**
* Recursively deletes a folder content, and deletes itself optionally.
* If an excluded file is found, folders won't be deleted.
*
* Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory.
*
* @param string $path
* @param bool $selfDelete Delete the provided folder if true, only its content if false.
* @param array $exclude
*/
public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool
{
$skipped = false;
if (!is_dir($path)) {
throw new IOException(t('Provided path is not a directory.'));
}
if (!static::isPathInShaarliFolder($path)) {
throw new IOException(t('Trying to delete a folder outside of Shaarli path.'));
}
foreach (new \DirectoryIterator($path) as $file) {
if($file->isDot()) {
continue;
}
if (in_array($file->getBasename(), $exclude, true)) {
$skipped = true;
continue;
}
if ($file->isFile()) {
unlink($file->getPathname());
} elseif($file->isDir()) {
$skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped;
}
}
if ($selfDelete && !$skipped) {
rmdir($path);
}
return $skipped;
}
/**
* Checks that the given path is inside Shaarli directory.
*/
public static function isPathInShaarliFolder(string $path): bool
{
$rootDirectory = dirname(dirname(__FILE__));
return strpos(realpath($path), $rootDirectory) !== false;
}
} }

View file

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use Shaarli\ApplicationUtils;
use Shaarli\FileUtils;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Slim controller used to handle Server administration page, and actions.
*/
class ServerController extends ShaarliAdminController
{
/** @var string Cache type - main - by default pagecache/ and tmp/ */
protected const CACHE_MAIN = 'main';
/** @var string Cache type - thumbnails - by default cache/ */
protected const CACHE_THUMB = 'thumbnails';
/**
* GET /admin/server - Display page Server administration
*/
public function index(Request $request, Response $response): Response
{
$latestVersion = 'v' . ApplicationUtils::getVersion(
ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE
);
$currentVersion = ApplicationUtils::getVersion('./shaarli_version.php');
$currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion;
$phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
$this->assignView('php_version', PHP_VERSION);
$this->assignView('php_eol', format_date($phpEol, false));
$this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
$this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
$this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
$this->assignView('release_url', ApplicationUtils::$GITHUB_URL . '/releases/tag/' . $latestVersion);
$this->assignView('latest_version', $latestVersion);
$this->assignView('current_version', $currentVersion);
$this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode'));
$this->assignView('index_url', index_url($this->container->environment));
$this->assignView('client_ip', client_ip_id($this->container->environment));
$this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', []));
$this->assignView(
'pagetitle',
t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
);
return $response->write($this->render('server'));
}
/**
* GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails).
*/
public function clearCache(Request $request, Response $response): Response
{
$exclude = ['.htaccess'];
if ($request->getQueryParam('type') === static::CACHE_THUMB) {
$folders = [$this->container->conf->get('resource.thumbnails_cache')];
$this->saveWarningMessage(
t('Thumbnails cache has been cleared.') . ' ' .
'<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>'
);
} else {
$folders = [
$this->container->conf->get('resource.page_cache'),
$this->container->conf->get('resource.raintpl_tmp'),
];
$this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!'));
}
// Make sure that we don't delete root cache folder
$folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders))));
foreach ($folders as $folder) {
FileUtils::clearFolder($folder, false, $exclude);
}
return $this->redirect($response, '/admin/server');
}
}

View file

@ -169,17 +169,25 @@ class BookmarkListController extends ShaarliVisitorController
*/ */
protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
{ {
// Logged in, not async retrieval, thumbnails enabled, and thumbnail should be updated if (false === $this->container->loginManager->isLoggedIn()) {
if ($this->container->loginManager->isLoggedIn() return false;
}
// If thumbnail should be updated, we reset it to null
if ($bookmark->shouldUpdateThumbnail()) {
$bookmark->setThumbnail(null);
// Requires an update, not async retrieval, thumbnails enabled
if ($bookmark->shouldUpdateThumbnail()
&& true !== $this->container->conf->get('general.enable_async_metadata', true) && true !== $this->container->conf->get('general.enable_async_metadata', true)
&& $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
&& $bookmark->shouldUpdateThumbnail()
) { ) {
$bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
$this->container->bookmarkService->set($bookmark, $writeDatastore); $this->container->bookmarkService->set($bookmark, $writeDatastore);
return true; return true;
} }
}
return false; return false;
} }

View file

@ -53,6 +53,16 @@ class InstallController extends ShaarliVisitorController
$this->assignView('cities', $cities); $this->assignView('cities', $cities);
$this->assignView('languages', Languages::getAvailableLanguages()); $this->assignView('languages', Languages::getAvailableLanguages());
$phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
$this->assignView('php_version', PHP_VERSION);
$this->assignView('php_eol', format_date($phpEol, false));
$this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
$this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
$this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf));
$this->assignView('pagetitle', t('Install Shaarli'));
return $response->write($this->render('install')); return $response->write($this->render('install'));
} }
@ -150,7 +160,7 @@ class InstallController extends ShaarliVisitorController
protected function checkPermissions(): bool protected function checkPermissions(): bool
{ {
// Ensure Shaarli has proper access to its resources // Ensure Shaarli has proper access to its resources
$errors = ApplicationUtils::checkResourcePermissions($this->container->conf); $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true);
if (empty($errors)) { if (empty($errors)) {
return true; return true;
} }

View file

@ -1047,7 +1047,7 @@ body,
} }
table { table {
margin: auto; margin: 10px auto 25px auto;
width: 90%; width: 90%;
.order { .order {
@ -1696,6 +1696,60 @@ form {
} }
} }
// SERVER PAGE
.server-tables-page,
.server-tables {
.window-subtitle {
&::before {
display: block;
margin: 8px auto;
background: linear-gradient(to right, var(--background-color), $dark-grey, var(--background-color));
width: 50%;
height: 1px;
content: '';
}
}
.server-row {
p {
height: 25px;
padding: 0 10px;
}
}
.server-label {
text-align: right;
font-weight: bold;
}
i {
&.fa-color-green {
color: $main-green;
}
&.fa-color-orange {
color: $orange;
}
&.fa-color-red {
color: $red;
}
}
@media screen and (max-width: 64em) {
.server-label {
text-align: center;
}
.server-row {
p {
text-align: center;
}
}
}
}
// Print rules // Print rules
@media print { @media print {
.shaarli-menu { .shaarli-menu {

View file

@ -1,8 +1,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Shaarli\n" "Project-Id-Version: Shaarli\n"
"POT-Creation-Date: 2020-10-16 20:01+0200\n" "POT-Creation-Date: 2020-10-21 15:00+0200\n"
"PO-Revision-Date: 2020-10-16 20:02+0200\n" "PO-Revision-Date: 2020-10-21 15:06+0200\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Shaarli\n" "Language-Team: Shaarli\n"
"Language: fr_FR\n" "Language: fr_FR\n"
@ -20,7 +20,7 @@ msgstr ""
"X-Poedit-SearchPath-3: init.php\n" "X-Poedit-SearchPath-3: init.php\n"
"X-Poedit-SearchPath-4: plugins\n" "X-Poedit-SearchPath-4: plugins\n"
#: application/ApplicationUtils.php:161 #: application/ApplicationUtils.php:162
#, php-format #, php-format
msgid "" msgid ""
"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
@ -31,22 +31,62 @@ msgstr ""
"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " "peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
"connues et devrait être mise à jour au plus tôt." "connues et devrait être mise à jour au plus tôt."
#: application/ApplicationUtils.php:192 application/ApplicationUtils.php:204 #: application/ApplicationUtils.php:195 application/ApplicationUtils.php:215
msgid "directory is not readable" msgid "directory is not readable"
msgstr "le répertoire n'est pas accessible en lecture" msgstr "le répertoire n'est pas accessible en lecture"
#: application/ApplicationUtils.php:207 #: application/ApplicationUtils.php:218
msgid "directory is not writable" msgid "directory is not writable"
msgstr "le répertoire n'est pas accessible en écriture" msgstr "le répertoire n'est pas accessible en écriture"
#: application/ApplicationUtils.php:225 #: application/ApplicationUtils.php:240
msgid "file is not readable" msgid "file is not readable"
msgstr "le fichier n'est pas accessible en lecture" msgstr "le fichier n'est pas accessible en lecture"
#: application/ApplicationUtils.php:228 #: application/ApplicationUtils.php:243
msgid "file is not writable" msgid "file is not writable"
msgstr "le fichier n'est pas accessible en écriture" msgstr "le fichier n'est pas accessible en écriture"
#: application/ApplicationUtils.php:277
msgid "Configuration parsing"
msgstr "Chargement de la configuration"
#: application/ApplicationUtils.php:278
msgid "Slim Framework (routing, etc.)"
msgstr "Slim Framwork (routage, etc.)"
#: application/ApplicationUtils.php:279
msgid "Multibyte (Unicode) string support"
msgstr "Support des chaînes de caractère multibytes (Unicode)"
#: application/ApplicationUtils.php:280
msgid "Required to use thumbnails"
msgstr "Obligatoire pour utiliser les miniatures"
#: application/ApplicationUtils.php:281
msgid "Localized text sorting (e.g. e->è->f)"
msgstr "Tri des textes traduits (ex : e->è->f)"
#: application/ApplicationUtils.php:282
msgid "Better retrieval of bookmark metadata and thumbnail"
msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
#: application/ApplicationUtils.php:283
msgid "Use the translation system in gettext mode"
msgstr "Utiliser le système de traduction en mode gettext"
#: application/ApplicationUtils.php:284
msgid "Login using LDAP server"
msgstr "Authentification via un serveur LDAP"
#: application/FileUtils.php:100
msgid "Provided path is not a directory."
msgstr "Le chemin fourni n'est pas un dossier."
#: application/FileUtils.php:104
msgid "Trying to delete a folder outside of Shaarli path."
msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli."
#: application/History.php:179 #: application/History.php:179
msgid "History file isn't readable or writable" msgid "History file isn't readable or writable"
msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
@ -330,12 +370,13 @@ msgid "You have enabled or changed thumbnails mode."
msgstr "Vous avez activé ou changé le mode de miniatures." msgstr "Vous avez activé ou changé le mode de miniatures."
#: application/front/controller/admin/ConfigureController.php:103 #: application/front/controller/admin/ConfigureController.php:103
#: application/front/controller/admin/ServerController.php:68
#: application/legacy/LegacyUpdater.php:538 #: application/legacy/LegacyUpdater.php:538
msgid "Please synchronize them." msgid "Please synchronize them."
msgstr "Merci de les synchroniser." msgstr "Merci de les synchroniser."
#: application/front/controller/admin/ConfigureController.php:113 #: application/front/controller/admin/ConfigureController.php:113
#: application/front/controller/visitor/InstallController.php:136 #: application/front/controller/visitor/InstallController.php:146
msgid "Error while writing config file after configuration update." msgid "Error while writing config file after configuration update."
msgstr "" msgstr ""
"Une erreur s'est produite lors de la sauvegarde du fichier de configuration." "Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
@ -377,33 +418,33 @@ msgstr ""
msgid "Shaare a new link" msgid "Shaare a new link"
msgstr "Partager un nouveau lien" msgstr "Partager un nouveau lien"
#: application/front/controller/admin/ManageShaareController.php:78 #: application/front/controller/admin/ManageShaareController.php:64
msgid "Note: " msgid "Note: "
msgstr "Note : " msgstr "Note : "
#: application/front/controller/admin/ManageShaareController.php:109 #: application/front/controller/admin/ManageShaareController.php:95
#: application/front/controller/admin/ManageShaareController.php:206 #: application/front/controller/admin/ManageShaareController.php:193
#: application/front/controller/admin/ManageShaareController.php:275 #: application/front/controller/admin/ManageShaareController.php:262
#: application/front/controller/admin/ManageShaareController.php:315 #: application/front/controller/admin/ManageShaareController.php:302
#, php-format #, php-format
msgid "Bookmark with identifier %s could not be found." msgid "Bookmark with identifier %s could not be found."
msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé." msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
#: application/front/controller/admin/ManageShaareController.php:194 #: application/front/controller/admin/ManageShaareController.php:181
#: application/front/controller/admin/ManageShaareController.php:252 #: application/front/controller/admin/ManageShaareController.php:239
msgid "Invalid bookmark ID provided." msgid "Invalid bookmark ID provided."
msgstr "ID du lien non valide." msgstr "ID du lien non valide."
#: application/front/controller/admin/ManageShaareController.php:260 #: application/front/controller/admin/ManageShaareController.php:247
msgid "Invalid visibility provided." msgid "Invalid visibility provided."
msgstr "Visibilité du lien non valide." msgstr "Visibilité du lien non valide."
#: application/front/controller/admin/ManageShaareController.php:363 #: application/front/controller/admin/ManageShaareController.php:352
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
msgid "Edit" msgid "Edit"
msgstr "Modifier" msgstr "Modifier"
#: application/front/controller/admin/ManageShaareController.php:366 #: application/front/controller/admin/ManageShaareController.php:355
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
msgid "Shaare" msgid "Shaare"
@ -411,7 +452,7 @@ msgstr "Shaare"
#: application/front/controller/admin/ManageTagController.php:29 #: application/front/controller/admin/ManageTagController.php:29
#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
msgid "Manage tags" msgid "Manage tags"
msgstr "Gérer les tags" msgstr "Gérer les tags"
@ -435,7 +476,7 @@ msgstr[1] "Le tag a été renommé dans %d liens."
#: application/front/controller/admin/PasswordController.php:28 #: application/front/controller/admin/PasswordController.php:28
#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
msgid "Change password" msgid "Change password"
msgstr "Modifier le mot de passe" msgstr "Modifier le mot de passe"
@ -467,6 +508,20 @@ msgstr ""
"Une erreur s'est produite lors de la sauvegarde de la configuration des " "Une erreur s'est produite lors de la sauvegarde de la configuration des "
"plugins : " "plugins : "
#: application/front/controller/admin/ServerController.php:50
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
msgid "Server administration"
msgstr "Administration serveur"
#: application/front/controller/admin/ServerController.php:67
msgid "Thumbnails cache has been cleared."
msgstr "Le cache des miniatures a été vidé."
#: application/front/controller/admin/ServerController.php:76
msgid "Shaarli's cache folder has been cleared!"
msgstr "Le dossier de cache de Shaarli a été vidé !"
#: application/front/controller/admin/ThumbnailsController.php:37 #: application/front/controller/admin/ThumbnailsController.php:37
#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
msgid "Thumbnails update" msgid "Thumbnails update"
@ -502,9 +557,14 @@ msgstr "Une erreur inattendue s'est produite."
#: application/front/controller/visitor/ErrorNotFoundController.php:25 #: application/front/controller/visitor/ErrorNotFoundController.php:25
msgid "Requested page could not be found." msgid "Requested page could not be found."
msgstr "" msgstr "La page demandée n'a pas pu être trouvée."
#: application/front/controller/visitor/InstallController.php:73 #: application/front/controller/visitor/InstallController.php:64
#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
msgid "Install Shaarli"
msgstr "Installation de Shaarli"
#: application/front/controller/visitor/InstallController.php:83
#, php-format #, php-format
msgid "" msgid ""
"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " "<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@ -523,14 +583,14 @@ msgstr ""
"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " "des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" "adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
#: application/front/controller/visitor/InstallController.php:144 #: application/front/controller/visitor/InstallController.php:154
msgid "" msgid ""
"Shaarli is now configured. Please login and start shaaring your bookmarks!" "Shaarli is now configured. Please login and start shaaring your bookmarks!"
msgstr "" msgstr ""
"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " "Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
"shaare vos liens !" "shaare vos liens !"
#: application/front/controller/visitor/InstallController.php:158 #: application/front/controller/visitor/InstallController.php:168
msgid "Insufficient permissions:" msgid "Insufficient permissions:"
msgstr "Permissions insuffisantes :" msgstr "Permissions insuffisantes :"
@ -1016,25 +1076,28 @@ msgstr ""
"miniatures." "miniatures."
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 #: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
msgid "Synchronize thumbnails" msgid "Synchronize thumbnails"
msgstr "Synchroniser les miniatures" msgstr "Synchroniser les miniatures"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
msgid "All" msgid "All"
msgstr "Tous" msgstr "Tous"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
msgid "Only common media hosts" msgid "Only common media hosts"
msgstr "Seulement les hébergeurs de média connus" msgstr "Seulement les hébergeurs de média connus"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
msgid "None" msgid "None"
msgstr "Aucune" msgstr "Aucune"
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
msgid "Save" msgid "Save"
@ -1060,27 +1123,27 @@ msgstr "Tous les liens d'un jour sur une page."
msgid "Next day" msgid "Next day"
msgstr "Jour suivant" msgstr "Jour suivant"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
msgid "Edit Shaare" msgid "Edit Shaare"
msgstr "Modifier le Shaare" msgstr "Modifier le Shaare"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
msgid "New Shaare" msgid "New Shaare"
msgstr "Nouveau Shaare" msgstr "Nouveau Shaare"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
msgid "Created:" msgid "Created:"
msgstr "Création :" msgstr "Création :"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
msgid "URL" msgid "URL"
msgstr "URL" msgstr "URL"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
msgid "Title" msgid "Title"
msgstr "Titre" msgstr "Titre"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@ -1088,33 +1151,33 @@ msgstr "Titre"
msgid "Description" msgid "Description"
msgstr "Description" msgstr "Description"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
msgid "Tags" msgid "Tags"
msgstr "Tags" msgstr "Tags"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
msgid "Private" msgid "Private"
msgstr "Privé" msgstr "Privé"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
msgid "Description will be rendered with" msgid "Description will be rendered with"
msgstr "La description sera générée avec" msgstr "La description sera générée avec"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
msgid "Markdown syntax documentation" msgid "Markdown syntax documentation"
msgstr "Documentation sur la syntaxe Markdown" msgstr "Documentation sur la syntaxe Markdown"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
msgid "Markdown syntax" msgid "Markdown syntax"
msgstr "la syntaxe Markdown" msgstr "la syntaxe Markdown"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
msgid "Apply Changes" msgid "Apply Changes"
msgstr "Appliquer les changements" msgstr "Appliquer les changements"
#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93 #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
@ -1179,10 +1242,6 @@ msgstr "Les doublons s'appuient sur les URL"
msgid "Add default tags" msgid "Add default tags"
msgstr "Ajouter des tags par défaut" msgstr "Ajouter des tags par défaut"
#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
msgid "Install Shaarli"
msgstr "Installation de Shaarli"
#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
msgid "It looks like it's the first time you run Shaarli. Please configure it." msgid "It looks like it's the first time you run Shaarli. Please configure it."
msgstr "" msgstr ""
@ -1215,6 +1274,10 @@ msgstr "Mes liens"
msgid "Install" msgid "Install"
msgstr "Installer" msgstr "Installer"
#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190
msgid "Server requirements"
msgstr "Pré-requis serveur"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
msgid "shaare" msgid "shaare"
@ -1511,6 +1574,100 @@ msgstr "Configuration des extensions"
msgid "No parameter available." msgid "No parameter available."
msgstr "Aucun paramètre disponible." msgstr "Aucun paramètre disponible."
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
msgid "General"
msgstr "Général"
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
msgid "Index URL"
msgstr "URL de l'index"
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
msgid "Base path"
msgstr "Chemin de base"
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
msgid "Client IP"
msgstr "IP du client"
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
msgid "Trusted reverse proxies"
msgstr "Reverse proxies de confiance"
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
msgid "N/A"
msgstr "N/A"
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
msgid "Visit releases page on Github"
msgstr "Visiter la page des releases sur Github"
#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
msgid "Synchronize all link thumbnails"
msgstr "Synchroniser toutes les miniatures"
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2
msgid "Permissions"
msgstr "Permissions"
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8
msgid "There are permissions that need to be fixed."
msgstr "Il y a des permissions qui doivent être corrigées."
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23
msgid "All read/write permissions are properly set."
msgstr "Toutes les permissions de lecture/écriture sont définies correctement."
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32
msgid "Running PHP"
msgstr "Fonctionnant avec PHP"
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36
msgid "End of life: "
msgstr "Fin de vie : "
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48
msgid "Extension"
msgstr "Extension"
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49
msgid "Usage"
msgstr "Utilisation"
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50
msgid "Status"
msgstr "Statut"
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66
msgid "Loaded"
msgstr "Chargé"
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
msgid "Required"
msgstr "Obligatoire"
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
msgid "Optional"
msgstr "Optionnel"
#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70
msgid "Not loaded"
msgstr "Non chargé"
#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
msgid "tags" msgid "tags"
@ -1561,15 +1718,19 @@ msgstr "Configurer Shaarli"
msgid "Enable, disable and configure plugins" msgid "Enable, disable and configure plugins"
msgstr "Activer, désactiver et configurer les extensions" msgstr "Activer, désactiver et configurer les extensions"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27
msgid "Check instance's server configuration"
msgstr "Vérifier la configuration serveur de l'instance"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
msgid "Change your password" msgid "Change your password"
msgstr "Modifier le mot de passe" msgstr "Modifier le mot de passe"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
msgid "Rename or delete a tag in all links" msgid "Rename or delete a tag in all links"
msgstr "Renommer ou supprimer un tag dans tous les liens" msgstr "Renommer ou supprimer un tag dans tous les liens"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
msgid "" msgid ""
"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
"delicious...)" "delicious...)"
@ -1577,11 +1738,11 @@ msgstr ""
"Importer des marques pages au format Netscape HTML (comme exportés depuis " "Importer des marques pages au format Netscape HTML (comme exportés depuis "
"Firefox, Chrome, Opera, delicious...)" "Firefox, Chrome, Opera, delicious...)"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
msgid "Import links" msgid "Import links"
msgstr "Importer des liens" msgstr "Importer des liens"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
msgid "" msgid ""
"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
"Opera, delicious...)" "Opera, delicious...)"
@ -1589,15 +1750,11 @@ msgstr ""
"Exporter les marques pages au format Netscape HTML (comme exportés depuis " "Exporter les marques pages au format Netscape HTML (comme exportés depuis "
"Firefox, Chrome, Opera, delicious...)" "Firefox, Chrome, Opera, delicious...)"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54
msgid "Export database" msgid "Export database"
msgstr "Exporter les données" msgstr "Exporter les données"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
msgid "Synchronize all link thumbnails"
msgstr "Synchroniser toutes les miniatures"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
msgid "" msgid ""
"Drag one of these button to your bookmarks toolbar or right-click it and " "Drag one of these button to your bookmarks toolbar or right-click it and "
"\"Bookmark This Link\"" "\"Bookmark This Link\""
@ -1605,13 +1762,13 @@ msgstr ""
"Glisser un de ces boutons dans votre barre de favoris ou cliquer droit " "Glisser un de ces boutons dans votre barre de favoris ou cliquer droit "
"dessus et « Ajouter aux favoris »" "dessus et « Ajouter aux favoris »"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
msgid "then click on the bookmarklet in any page you want to share." msgid "then click on the bookmarklet in any page you want to share."
msgstr "" msgstr ""
"puis cliquer sur le marque-page depuis un site que vous souhaitez partager." "puis cliquer sur le marque-page depuis un site que vous souhaitez partager."
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
msgid "" msgid ""
"Drag this link to your bookmarks toolbar or right-click it and Bookmark This " "Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
"Link" "Link"
@ -1619,40 +1776,40 @@ msgstr ""
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »" "Ajouter aux favoris »"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
msgid "then click ✚Shaare link button in any page you want to share" msgid "then click ✚Shaare link button in any page you want to share"
msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
msgid "The selected text is too long, it will be truncated." msgid "The selected text is too long, it will be truncated."
msgstr "Le texte sélectionné est trop long, il sera tronqué." msgstr "Le texte sélectionné est trop long, il sera tronqué."
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
msgid "Shaare link" msgid "Shaare link"
msgstr "Shaare" msgstr "Shaare"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
msgid "" msgid ""
"Then click ✚Add Note button anytime to start composing a private Note (text " "Then click ✚Add Note button anytime to start composing a private Note (text "
"post) to your Shaarli" "post) to your Shaarli"
msgstr "" msgstr ""
"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" "Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
msgid "Add Note" msgid "Add Note"
msgstr "Ajouter une Note" msgstr "Ajouter une Note"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
msgid "3rd party" msgid "3rd party"
msgstr "Applications tierces" msgstr "Applications tierces"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140
msgid "plugin" msgid "plugin"
msgstr "extension" msgstr "extension"
#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 #: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
msgid "" msgid ""
"Drag this link to your bookmarks toolbar, or right-click it and choose " "Drag this link to your bookmarks toolbar, or right-click it and choose "
"Bookmark This Link" "Bookmark This Link"
@ -1660,9 +1817,6 @@ msgstr ""
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »" "Ajouter aux favoris »"
#~ msgid "Provided data is invalid"
#~ msgstr "Les informations fournies ne sont pas valides"
#~ msgid "Rename" #~ msgid "Rename"
#~ msgstr "Renommer" #~ msgstr "Renommer"

View file

@ -128,6 +128,8 @@ $app->group('/admin', function () {
$this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index'); $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
$this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save'); $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
$this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken'); $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
$this->get('/server', '\Shaarli\Front\Controller\Admin\ServerController:index');
$this->get('/clear-cache', '\Shaarli\Front\Controller\Admin\ServerController:clearCache');
$this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index'); $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
$this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle'); $this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle');
$this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');

View file

@ -339,6 +339,35 @@ class ApplicationUtilsTest extends \Shaarli\TestCase
); );
} }
/**
* Checks resource permissions in minimal mode.
*/
public function testCheckCurrentResourcePermissionsErrorsMinimalMode(): void
{
$conf = new ConfigManager('');
$conf->set('resource.thumbnails_cache', 'null/cache');
$conf->set('resource.config', 'null/data/config.php');
$conf->set('resource.data_dir', 'null/data');
$conf->set('resource.datastore', 'null/data/store.php');
$conf->set('resource.ban_file', 'null/data/ipbans.php');
$conf->set('resource.log', 'null/data/log.txt');
$conf->set('resource.page_cache', 'null/pagecache');
$conf->set('resource.raintpl_tmp', 'null/tmp');
$conf->set('resource.raintpl_tpl', 'null/tpl');
$conf->set('resource.raintpl_theme', 'null/tpl/default');
$conf->set('resource.update_check', 'null/data/lastupdatecheck.txt');
static::assertSame(
[
'"null/tpl" directory is not readable',
'"null/tpl/default" directory is not readable',
'"null/tmp" directory is not readable',
'"null/tmp" directory is not writable'
],
ApplicationUtils::checkResourcePermissions($conf, true)
);
}
/** /**
* Check update with 'dev' as curent version (master branch). * Check update with 'dev' as curent version (master branch).
* It should always return false. * It should always return false.
@ -349,4 +378,37 @@ class ApplicationUtilsTest extends \Shaarli\TestCase
ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true) ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true)
); );
} }
/**
* Basic test of getPhpExtensionsRequirement()
*/
public function testGetPhpExtensionsRequirementSimple(): void
{
static::assertCount(8, ApplicationUtils::getPhpExtensionsRequirement());
static::assertSame([
'name' => 'json',
'required' => true,
'desc' => 'Configuration parsing',
'loaded' => true,
], ApplicationUtils::getPhpExtensionsRequirement()[0]);
}
/**
* Test getPhpEol with a known version: 7.4 -> 2022
*/
public function testGetKnownPhpEol(): void
{
static::assertSame('2022-11-28', ApplicationUtils::getPhpEol('7.4.7'));
}
/**
* Test getPhpEol with an unknown version: 7.4 -> 2022
*/
public function testGetUnknownPhpEol(): void
{
static::assertSame(
(((int) (new \DateTime())->format('Y')) + 2) . (new \DateTime())->format('-m-d'),
ApplicationUtils::getPhpEol('7.51.34')
);
}
} }

View file

@ -3,25 +3,48 @@
namespace Shaarli; namespace Shaarli;
use Exception; use Exception;
use Shaarli\Exceptions\IOException;
/** /**
* Class FileUtilsTest * Class FileUtilsTest
* *
* Test file utility class. * Test file utility class.
*/ */
class FileUtilsTest extends \Shaarli\TestCase class FileUtilsTest extends TestCase
{ {
/** /**
* @var string Test file path. * @var string Test file path.
*/ */
protected static $file = 'sandbox/flat.db'; protected static $file = 'sandbox/flat.db';
protected function setUp(): void
{
@mkdir('sandbox');
mkdir('sandbox/folder2');
touch('sandbox/file1');
touch('sandbox/file2');
mkdir('sandbox/folder1');
touch('sandbox/folder1/file1');
touch('sandbox/folder1/file2');
mkdir('sandbox/folder3');
mkdir('/tmp/shaarli-to-delete');
}
/** /**
* Delete test file after every test. * Delete test file after every test.
*/ */
protected function tearDown(): void protected function tearDown(): void
{ {
@unlink(self::$file); @unlink(self::$file);
@unlink('sandbox/folder1/file1');
@unlink('sandbox/folder1/file2');
@rmdir('sandbox/folder1');
@unlink('sandbox/file1');
@unlink('sandbox/file2');
@rmdir('sandbox/folder2');
@rmdir('sandbox/folder3');
@rmdir('/tmp/shaarli-to-delete');
} }
/** /**
@ -107,4 +130,67 @@ class FileUtilsTest extends \Shaarli\TestCase
$this->assertEquals(null, FileUtils::readFlatDB(self::$file)); $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
$this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
} }
/**
* Test clearFolder with self delete and excluded files
*/
public function testClearFolderSelfDeleteWithExclusion(): void
{
FileUtils::clearFolder('sandbox', true, ['file2']);
static::assertFileExists('sandbox/folder1/file2');
static::assertFileExists('sandbox/folder1');
static::assertFileExists('sandbox/file2');
static::assertFileExists('sandbox');
static::assertFileNotExists('sandbox/folder1/file1');
static::assertFileNotExists('sandbox/file1');
static::assertFileNotExists('sandbox/folder3');
}
/**
* Test clearFolder with self delete and excluded files
*/
public function testClearFolderSelfDeleteWithoutExclusion(): void
{
FileUtils::clearFolder('sandbox', true);
static::assertFileNotExists('sandbox');
}
/**
* Test clearFolder with self delete and excluded files
*/
public function testClearFolderNoSelfDeleteWithoutExclusion(): void
{
FileUtils::clearFolder('sandbox', false);
static::assertFileExists('sandbox');
// 2 because '.' and '..'
static::assertCount(2, new \DirectoryIterator('sandbox'));
}
/**
* Test clearFolder on a file instead of a folder
*/
public function testClearFolderOnANonDirectory(): void
{
$this->expectException(IOException::class);
$this->expectExceptionMessage('Provided path is not a directory.');
FileUtils::clearFolder('sandbox/file1', false);
}
/**
* Test clearFolder on a file instead of a folder
*/
public function testClearFolderOutsideOfShaarliDirectory(): void
{
$this->expectException(IOException::class);
$this->expectExceptionMessage('Trying to delete a folder outside of Shaarli path.');
FileUtils::clearFolder('/tmp/shaarli-to-delete', true);
}
} }

View file

@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Admin;
use Shaarli\Config\ConfigManager;
use Shaarli\Security\SessionManager;
use Shaarli\TestCase;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Test Server administration controller.
*/
class ServerControllerTest extends TestCase
{
use FrontAdminControllerMockHelper;
/** @var ServerController */
protected $controller;
public function setUp(): void
{
$this->createContainer();
$this->controller = new ServerController($this->container);
// initialize dummy cache
@mkdir('sandbox/');
foreach (['pagecache', 'tmp', 'cache'] as $folder) {
@mkdir('sandbox/' . $folder);
@touch('sandbox/' . $folder . '/.htaccess');
@touch('sandbox/' . $folder . '/1');
@touch('sandbox/' . $folder . '/2');
}
}
public function tearDown(): void
{
foreach (['pagecache', 'tmp', 'cache'] as $folder) {
@unlink('sandbox/' . $folder . '/.htaccess');
@unlink('sandbox/' . $folder . '/1');
@unlink('sandbox/' . $folder . '/2');
@rmdir('sandbox/' . $folder);
}
}
/**
* Test default display of server administration page.
*/
public function testIndex(): void
{
$request = $this->createMock(Request::class);
$response = new Response();
// Save RainTPL assigned variables
$assignedVariables = [];
$this->assignTemplateVars($assignedVariables);
$result = $this->controller->index($request, $response);
static::assertSame(200, $result->getStatusCode());
static::assertSame('server', (string) $result->getBody());
static::assertSame(PHP_VERSION, $assignedVariables['php_version']);
static::assertArrayHasKey('php_has_reached_eol', $assignedVariables);
static::assertArrayHasKey('php_eol', $assignedVariables);
static::assertArrayHasKey('php_extensions', $assignedVariables);
static::assertArrayHasKey('permissions', $assignedVariables);
static::assertEmpty($assignedVariables['permissions']);
static::assertRegExp(
'#https://github\.com/shaarli/Shaarli/releases/tag/v\d+\.\d+\.\d+#',
$assignedVariables['release_url']
);
static::assertRegExp('#v\d+\.\d+\.\d+#', $assignedVariables['latest_version']);
static::assertRegExp('#(v\d+\.\d+\.\d+|dev)#', $assignedVariables['current_version']);
static::assertArrayHasKey('index_url', $assignedVariables);
static::assertArrayHasKey('client_ip', $assignedVariables);
static::assertArrayHasKey('trusted_proxies', $assignedVariables);
static::assertSame('Server administration - Shaarli', $assignedVariables['pagetitle']);
}
/**
* Test clearing the main cache
*/
public function testClearMainCache(): void
{
$this->container->conf = $this->createMock(ConfigManager::class);
$this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
if ($key === 'resource.page_cache') {
return 'sandbox/pagecache';
} elseif ($key === 'resource.raintpl_tmp') {
return 'sandbox/tmp';
} elseif ($key === 'resource.thumbnails_cache') {
return 'sandbox/cache';
} else {
return $default;
}
});
$this->container->sessionManager
->expects(static::once())
->method('setSessionParameter')
->with(SessionManager::KEY_SUCCESS_MESSAGES, ['Shaarli\'s cache folder has been cleared!'])
;
$request = $this->createMock(Request::class);
$request->method('getQueryParam')->with('type')->willReturn('main');
$response = new Response();
$result = $this->controller->clearCache($request, $response);
static::assertSame(302, $result->getStatusCode());
static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location'));
static::assertFileNotExists('sandbox/pagecache/1');
static::assertFileNotExists('sandbox/pagecache/2');
static::assertFileNotExists('sandbox/tmp/1');
static::assertFileNotExists('sandbox/tmp/2');
static::assertFileExists('sandbox/pagecache/.htaccess');
static::assertFileExists('sandbox/tmp/.htaccess');
static::assertFileExists('sandbox/cache');
static::assertFileExists('sandbox/cache/.htaccess');
static::assertFileExists('sandbox/cache/1');
static::assertFileExists('sandbox/cache/2');
}
/**
* Test clearing thumbnails cache
*/
public function testClearThumbnailsCache(): void
{
$this->container->conf = $this->createMock(ConfigManager::class);
$this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
if ($key === 'resource.page_cache') {
return 'sandbox/pagecache';
} elseif ($key === 'resource.raintpl_tmp') {
return 'sandbox/tmp';
} elseif ($key === 'resource.thumbnails_cache') {
return 'sandbox/cache';
} else {
return $default;
}
});
$this->container->sessionManager
->expects(static::once())
->method('setSessionParameter')
->willReturnCallback(function (string $key, array $value): SessionManager {
static::assertSame(SessionManager::KEY_WARNING_MESSAGES, $key);
static::assertCount(1, $value);
static::assertStringStartsWith('Thumbnails cache has been cleared.', $value[0]);
return $this->container->sessionManager;
});
;
$request = $this->createMock(Request::class);
$request->method('getQueryParam')->with('type')->willReturn('thumbnails');
$response = new Response();
$result = $this->controller->clearCache($request, $response);
static::assertSame(302, $result->getStatusCode());
static::assertSame('/subfolder/admin/server', (string) $result->getHeaderLine('Location'));
static::assertFileNotExists('sandbox/cache/1');
static::assertFileNotExists('sandbox/cache/2');
static::assertFileExists('sandbox/cache/.htaccess');
static::assertFileExists('sandbox/pagecache');
static::assertFileExists('sandbox/pagecache/.htaccess');
static::assertFileExists('sandbox/pagecache/1');
static::assertFileExists('sandbox/pagecache/2');
static::assertFileExists('sandbox/tmp');
static::assertFileExists('sandbox/tmp/.htaccess');
static::assertFileExists('sandbox/tmp/1');
static::assertFileExists('sandbox/tmp/2');
}
}

View file

@ -79,6 +79,15 @@ class InstallControllerTest extends TestCase
static::assertIsArray($assignedVariables['languages']); static::assertIsArray($assignedVariables['languages']);
static::assertSame('Automatic', $assignedVariables['languages']['auto']); static::assertSame('Automatic', $assignedVariables['languages']['auto']);
static::assertSame('French', $assignedVariables['languages']['fr']); static::assertSame('French', $assignedVariables['languages']['fr']);
static::assertSame(PHP_VERSION, $assignedVariables['php_version']);
static::assertArrayHasKey('php_has_reached_eol', $assignedVariables);
static::assertArrayHasKey('php_eol', $assignedVariables);
static::assertArrayHasKey('php_extensions', $assignedVariables);
static::assertArrayHasKey('permissions', $assignedVariables);
static::assertEmpty($assignedVariables['permissions']);
static::assertSame('Install Shaarli', $assignedVariables['pagetitle']);
} }
/** /**

View file

@ -163,6 +163,16 @@
</div> </div>
</div> </div>
</form> </form>
<div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">
<h2 class="window-title">{'Server requirements'|t}</h2>
{include="server.requirements"}
</div>
</div>
{include="page.footer"} {include="page.footer"}
</body> </body>
</html> </html>

129
tpl/default/server.html Normal file
View file

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
<head>
{include="includes"}
</head>
<body>
{include="page.header"}
<div class="pure-g">
<div class="pure-u-lg-1-4 pure-u-1-24"></div>
<div class="pure-u-lg-1-2 pure-u-22-24 page-form server-tables-page">
<h2 class="window-title">{'Server administration'|t}</h2>
<h3 class="window-subtitle">{'General'|t}</h3>
<div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>{'Index URL'|t}</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
<p><a href="{$index_url}" title="{$pagetitle}">{$index_url}</a></p>
</div>
</div>
<div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>{'Base path'|t}</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
<p>{$base_path}</p>
</div>
</div>
<div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>{'Client IP'|t}</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
<p>{$client_ip}</p>
</div>
</div>
<div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>{'Trusted reverse proxies'|t}</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
{if="count($trusted_proxies) > 0"}
<p>
{loop="$trusted_proxies"}
{$value}<br>
{/loop}
</p>
{else}
<p>{'N/A'|t}</p>
{/if}
</div>
</div>
{include="server.requirements"}
<h3 class="window-subtitle">Version</h3>
<div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>Current version</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
<p>{$current_version}</p>
</div>
</div>
<div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>Latest release</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
<p>
<a href="{$release_url}" title="{'Visit releases page on Github'|t}">
{$latest_version}
</a>
</p>
</div>
</div>
<h3 class="window-subtitle">Thumbnails</h3>
<div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>Thumbnails status</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
<p>
{if="$thumbnails_mode==='all'"}
{'All'|t}
{elseif="$thumbnails_mode==='common'"}
{'Only common media hosts'|t}
{else}
{'None'|t}
{/if}
</p>
</div>
</div>
{if="$thumbnails_mode!=='none'"}
<div class="center tools-item">
<a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
</a>
</div>
{/if}
<h3 class="window-subtitle">Cache</h3>
<div class="center tools-item">
<a href="{$base_path}/admin/clear-cache?type=main">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear main cache</span>
</a>
</div>
<div class="center tools-item">
<a href="{$base_path}/admin/clear-cache?type=thumbnails">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear thumbnails cache</span>
</a>
</div>
</div>
</div>
{include="page.footer"}
</body>
</html>

View file

@ -0,0 +1,68 @@
<div class="server-tables">
<h3 class="window-subtitle">{'Permissions'|t}</h3>
{if="count($permissions) > 0"}
<p class="center">
<i class="fa fa-close fa-color-red" aria-hidden="true"></i>
{'There are permissions that need to be fixed.'|t}
</p>
<p>
{loop="$permissions"}
<div class="center">{$value}</div>
{/loop}
</p>
{else}
<p class="center">
<i class="fa fa-check fa-color-green" aria-hidden="true"></i>
{'All read/write permissions are properly set.'|t}
</p>
{/if}
<h3 class="window-subtitle">PHP</h3>
<p class="center">
<strong>{'Running PHP'|t} {$php_version}</strong>
{if="$php_has_reached_eol"}
<i class="fa fa-circle fa-color-orange" aria-label="hidden"></i><br>
{'End of life: '|t} {$php_eol}
{else}
<i class="fa fa-circle fa-color-green" aria-label="hidden"></i><br>
{/if}
</p>
<table class="center">
<thead>
<tr>
<th>{'Extension'|t}</th>
<th>{'Usage'|t}</th>
<th>{'Status'|t}</th>
<th>{'Loaded'|t}</th>
</tr>
</thead>
<tbody>
{loop="$php_extensions"}
<tr>
<td>{$value.name}</td>
<td>{$value.desc}</td>
<td>{$value.required ? t('Required') : t('Optional')}</td>
<td>
{if="$value.loaded"}
{$classLoaded="fa-color-green"}
{$strLoaded=t('Loaded')}
{else}
{$strLoaded=t('Not loaded')}
{if="$value.required"}
{$classLoaded="fa-color-red"}
{else}
{$classLoaded="fa-color-orange"}
{/if}
{/if}
<i class="fa fa-circle {$classLoaded}" aria-label="{$strLoaded}" title="{$strLoaded}"></i>
</td>
</tr>
{/loop}
</tbody>
</table>
</div>

View file

@ -20,6 +20,12 @@
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span> <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span>
</a> </a>
</div> </div>
<div class="tools-item">
<a href="{$base_path}/admin/server"
title="{'Check instance\'s server configuration'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Server administration'|t}</span>
</a>
</div>
{if="!$openshaarli"} {if="!$openshaarli"}
<div class="tools-item"> <div class="tools-item">
<a href="{$base_path}/admin/password" title="{'Change your password'|t}"> <a href="{$base_path}/admin/password" title="{'Change your password'|t}">
@ -45,14 +51,6 @@
</a> </a>
</div> </div>
{if="$thumbnails_enabled"}
<div class="tools-item">
<a href="{$base_path}/admin/thumbnails" title="{'Synchronize all link thumbnails'|t}">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Synchronize thumbnails'|t}</span>
</a>
</div>
{/if}
{loop="$tools_plugin"} {loop="$tools_plugin"}
<div class="tools-item"> <div class="tools-item">
{$value} {$value}