]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Merge pull request #1604 from ArthurHoaro/feature/server-admin-page
authorArthurHoaro <arthur@hoa.ro>
Tue, 27 Oct 2020 18:29:43 +0000 (19:29 +0100)
committerGitHub <noreply@github.com>
Tue, 27 Oct 2020 18:29:43 +0000 (19:29 +0100)
Feature: add a Server administration page

16 files changed:
application/ApplicationUtils.php
application/FileUtils.php
application/front/controller/admin/ServerController.php [new file with mode: 0644]
application/front/controller/visitor/BookmarkListController.php
application/front/controller/visitor/InstallController.php
assets/default/scss/shaarli.scss
inc/languages/fr/LC_MESSAGES/shaarli.po
index.php
tests/ApplicationUtilsTest.php
tests/FileUtilsTest.php
tests/front/controller/admin/ServerControllerTest.php [new file with mode: 0644]
tests/front/controller/visitor/InstallControllerTest.php
tpl/default/install.html
tpl/default/server.html [new file with mode: 0644]
tpl/default/server.requirements.html [new file with mode: 0644]
tpl/default/tools.html

index 3aa218295c634e3d0d02b3e5e9fa00ff534d7804..bd1c7cf3f42a06a427d817821c5c9a058f549aba 100644 (file)
@@ -14,8 +14,9 @@ class ApplicationUtils
      */
     public static $VERSION_FILE = 'shaarli_version.php';
 
-    private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
-    private static $GIT_BRANCHES = array('latest', 'stable');
+    public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli';
+    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_END_TAG = ' */ ?>';
 
@@ -125,7 +126,7 @@ class ApplicationUtils
         // Late Static Binding allows overriding within tests
         // See http://php.net/manual/en/language.oop5.late-static-bindings.php
         $latestVersion = static::getVersion(
-            self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE
+            self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
         );
 
         if (!$latestVersion) {
@@ -171,35 +172,45 @@ class ApplicationUtils
     /**
      * 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
      */
-    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'), '/');
 
         // Check script and template directories are readable
-        foreach (array(
+        foreach ([
                      'application',
                      'inc',
                      'plugins',
                      $rainTplDir,
                      $rainTplDir . '/' . $conf->get('resource.theme'),
-                 ) as $path) {
+                 ] as $path) {
             if (!is_readable(realpath($path))) {
                 $errors[] = '"' . $path . '" ' . t('directory is not readable');
             }
         }
 
         // Check cache and data directories are readable and writable
-        foreach (array(
-                     $conf->get('resource.thumbnails_cache'),
-                     $conf->get('resource.data_dir'),
-                     $conf->get('resource.page_cache'),
-                     $conf->get('resource.raintpl_tmp'),
-                 ) as $path) {
+        if ($minimalMode) {
+            $folders = [
+                $conf->get('resource.raintpl_tmp'),
+            ];
+        } else {
+            $folders = [
+                $conf->get('resource.thumbnails_cache'),
+                $conf->get('resource.data_dir'),
+                $conf->get('resource.page_cache'),
+                $conf->get('resource.raintpl_tmp'),
+            ];
+        }
+
+        foreach ($folders as $path) {
             if (!is_readable(realpath($path))) {
                 $errors[] = '"' . $path . '" ' . t('directory is not readable');
             }
@@ -208,6 +219,10 @@ class ApplicationUtils
             }
         }
 
+        if ($minimalMode) {
+            return $errors;
+        }
+
         // Check configuration files are readable and writable
         foreach (array(
                      $conf->getConfigFileExt(),
@@ -246,4 +261,54 @@ class ApplicationUtils
     {
         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');
+    }
 }
index 30560bfc3a929a272a7932c893a7212f5d598da1..3f940751ccdcc9f7b2f720ee7f04eedc40d83920 100644 (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;
+    }
 }
diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php
new file mode 100644 (file)
index 0000000..85654a4
--- /dev/null
@@ -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');
+    }
+}
index a8019ead33865e99365eba4c9239b1e9caac5a9f..5267c8f5bd14d77830800d4192232534084c8933 100644 (file)
@@ -169,16 +169,24 @@ class BookmarkListController extends ShaarliVisitorController
      */
     protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
     {
-        // Logged in, not async retrieval, thumbnails enabled, and thumbnail should be updated
-        if ($this->container->loginManager->isLoggedIn()
-            && true !== $this->container->conf->get('general.enable_async_metadata', true)
-            && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
-            && $bookmark->shouldUpdateThumbnail()
-        ) {
-            $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
-            $this->container->bookmarkService->set($bookmark, $writeDatastore);
-
-            return true;
+        if (false === $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)
+                && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
+            ) {
+                $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
+                $this->container->bookmarkService->set($bookmark, $writeDatastore);
+
+                return true;
+            }
         }
 
         return false;
index 7cb3277794fbe8c9b2929766b68cc9bcfad21c5b..564a577740795b5a23ec826152d93611035a77e9 100644 (file)
@@ -53,6 +53,16 @@ class InstallController extends ShaarliVisitorController
         $this->assignView('cities', $cities);
         $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'));
     }
 
@@ -150,7 +160,7 @@ class InstallController extends ShaarliVisitorController
     protected function checkPermissions(): bool
     {
         // Ensure Shaarli has proper access to its resources
-        $errors = ApplicationUtils::checkResourcePermissions($this->container->conf);
+        $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true);
         if (empty($errors)) {
             return true;
         }
index 286ac83b32b487a3d51837b54dd81b1282d6d76f..7dc61903aef9e0d2cac90a86a56ffe158f96b8f6 100644 (file)
@@ -1047,7 +1047,7 @@ body,
   }
 
   table {
-    margin: auto;
+    margin: 10px auto 25px auto;
     width: 90%;
 
     .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
 @media print {
   .shaarli-menu {
index f7baedfb4c8cfac01728e8ed357eed95f9028253..db6bfa3eac660f02670abb370ee210e35586f951 100644 (file)
@@ -1,8 +1,8 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: Shaarli\n"
-"POT-Creation-Date: 2020-10-16 20:01+0200\n"
-"PO-Revision-Date: 2020-10-16 20:02+0200\n"
+"POT-Creation-Date: 2020-10-21 15:00+0200\n"
+"PO-Revision-Date: 2020-10-21 15:06+0200\n"
 "Last-Translator: \n"
 "Language-Team: Shaarli\n"
 "Language: fr_FR\n"
@@ -20,7 +20,7 @@ msgstr ""
 "X-Poedit-SearchPath-3: init.php\n"
 "X-Poedit-SearchPath-4: plugins\n"
 
-#: application/ApplicationUtils.php:161
+#: application/ApplicationUtils.php:162
 #, php-format
 msgid ""
 "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 "
 "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"
 msgstr "le rรฉpertoire n'est pas accessible en lecture"
 
-#: application/ApplicationUtils.php:207
+#: application/ApplicationUtils.php:218
 msgid "directory is not writable"
 msgstr "le rรฉpertoire n'est pas accessible en รฉcriture"
 
-#: application/ApplicationUtils.php:225
+#: application/ApplicationUtils.php:240
 msgid "file is not readable"
 msgstr "le fichier n'est pas accessible en lecture"
 
-#: application/ApplicationUtils.php:228
+#: application/ApplicationUtils.php:243
 msgid "file is not writable"
 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
 msgid "History file isn't readable or writable"
 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."
 
 #: application/front/controller/admin/ConfigureController.php:103
+#: application/front/controller/admin/ServerController.php:68
 #: application/legacy/LegacyUpdater.php:538
 msgid "Please synchronize them."
 msgstr "Merci de les synchroniser."
 
 #: 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."
 msgstr ""
 "Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
@@ -377,33 +418,33 @@ msgstr ""
 msgid "Shaare a new link"
 msgstr "Partager un nouveau lien"
 
-#: application/front/controller/admin/ManageShaareController.php:78
+#: application/front/controller/admin/ManageShaareController.php:64
 msgid "Note: "
 msgstr "Note : "
 
-#: application/front/controller/admin/ManageShaareController.php:109
-#: application/front/controller/admin/ManageShaareController.php:206
-#: application/front/controller/admin/ManageShaareController.php:275
-#: application/front/controller/admin/ManageShaareController.php:315
+#: application/front/controller/admin/ManageShaareController.php:95
+#: application/front/controller/admin/ManageShaareController.php:193
+#: application/front/controller/admin/ManageShaareController.php:262
+#: application/front/controller/admin/ManageShaareController.php:302
 #, php-format
 msgid "Bookmark with identifier %s could not be found."
 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:252
+#: application/front/controller/admin/ManageShaareController.php:181
+#: application/front/controller/admin/ManageShaareController.php:239
 msgid "Invalid bookmark ID provided."
 msgstr "ID du lien non valide."
 
-#: application/front/controller/admin/ManageShaareController.php:260
+#: application/front/controller/admin/ManageShaareController.php:247
 msgid "Invalid visibility provided."
 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
 msgid "Edit"
 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.cedf684561d925457130839629000a81.rtpl.php:28
 msgid "Shaare"
@@ -411,7 +452,7 @@ msgstr "Shaare"
 
 #: application/front/controller/admin/ManageTagController.php:29
 #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
 msgid "Manage 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
 #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
 msgid "Change password"
 msgstr "Modifier le mot de passe"
 
@@ -467,6 +508,20 @@ msgstr ""
 "Une erreur s'est produite lors de la sauvegarde de la configuration des "
 "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
 #: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
 msgid "Thumbnails update"
@@ -502,9 +557,14 @@ msgstr "Une erreur inattendue s'est produite."
 
 #: application/front/controller/visitor/ErrorNotFoundController.php:25
 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
 msgid ""
 "<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 "
 "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 ""
 "Shaarli is now configured. Please login and start shaaring your bookmarks!"
 msgstr ""
 "Shaarli est maintenant configurรฉ. Vous pouvez vous connecter et commencez ร  "
 "shaare vos liens !"
 
-#: application/front/controller/visitor/InstallController.php:158
+#: application/front/controller/visitor/InstallController.php:168
 msgid "Insufficient permissions:"
 msgstr "Permissions insuffisantes :"
 
@@ -1016,25 +1076,28 @@ msgstr ""
 "miniatures."
 
 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
 msgid "Synchronize thumbnails"
 msgstr "Synchroniser les miniatures"
 
 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
 #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
 msgid "All"
 msgstr "Tous"
 
 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
 msgid "Only common media hosts"
 msgstr "Seulement les hรฉbergeurs de mรฉdia connus"
 
 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
 msgid "None"
 msgstr "Aucune"
 
 #: 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:199
 msgid "Save"
@@ -1060,27 +1123,27 @@ msgstr "Tous les liens d'un jour sur une page."
 msgid "Next day"
 msgstr "Jour suivant"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
 msgid "Edit Shaare"
 msgstr "Modifier le Shaare"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
 msgid "New Shaare"
 msgstr "Nouveau Shaare"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
 msgid "Created:"
 msgstr "Crรฉation :"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
 msgid "URL"
 msgstr "URL"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
 msgid "Title"
 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:75
 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@@ -1088,33 +1151,33 @@ msgstr "Titre"
 msgid "Description"
 msgstr "Description"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
 msgid "Tags"
 msgstr "Tags"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
 #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
 msgid "Private"
 msgstr "Privรฉ"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
 msgid "Description will be rendered with"
 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"
 msgstr "Documentation sur la syntaxe Markdown"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
 msgid "Markdown syntax"
 msgstr "la syntaxe Markdown"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
 msgid "Apply Changes"
 msgstr "Appliquer les changements"
 
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:93
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.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"
 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
 msgid "It looks like it's the first time you run Shaarli. Please configure it."
 msgstr ""
@@ -1215,6 +1274,10 @@ msgstr "Mes liens"
 msgid "Install"
 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:79
 msgid "shaare"
@@ -1511,6 +1574,100 @@ msgstr "Configuration des extensions"
 msgid "No parameter available."
 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.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
 msgid "tags"
@@ -1561,15 +1718,19 @@ msgstr "Configurer Shaarli"
 msgid "Enable, disable and configure plugins"
 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"
 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"
 msgstr "Renommer ou supprimer un tag dans tous les liens"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
 msgid ""
 "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
 "delicious...)"
@@ -1577,11 +1738,11 @@ msgstr ""
 "Importer des marques pages au format Netscape HTML (comme exportรฉs depuis "
 "Firefox, Chrome, Opera, delicious...)"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
 msgid "Import links"
 msgstr "Importer des liens"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
 msgid ""
 "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
 "Opera, delicious...)"
@@ -1589,15 +1750,11 @@ msgstr ""
 "Exporter les marques pages au format Netscape HTML (comme exportรฉs depuis "
 "Firefox, Chrome, Opera, delicious...)"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54
 msgid "Export database"
 msgstr "Exporter les donnรฉes"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55
-msgid "Synchronize all link thumbnails"
-msgstr "Synchroniser toutes les miniatures"
-
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
 msgid ""
 "Drag one of these button to your bookmarks toolbar or right-click it and "
 "\"Bookmark This Link\""
@@ -1605,13 +1762,13 @@ msgstr ""
 "Glisser un de ces boutons dans votre barre de favoris ou cliquer droit "
 "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."
 msgstr ""
 "puis cliquer sur le marque-page depuis un site que vous souhaitez partager."
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
 msgid ""
 "Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
 "Link"
@@ -1619,40 +1776,40 @@ msgstr ""
 "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et ยซ "
 "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"
 msgstr "puis cliquer sur โœšShaare depuis un site que vous souhaitez partager"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
 msgid "The selected text is too long, it will be truncated."
 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"
 msgstr "Shaare"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
 msgid ""
 "Then click โœšAdd Note button anytime to start composing a private Note (text "
 "post) to your Shaarli"
 msgstr ""
 "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"
 msgstr "Ajouter une Note"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
 msgid "3rd party"
 msgstr "Applications tierces"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140
 msgid "plugin"
 msgstr "extension"
 
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
 msgid ""
 "Drag this link to your bookmarks toolbar, or right-click it and choose "
 "Bookmark This Link"
@@ -1660,9 +1817,6 @@ msgstr ""
 "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et ยซ "
 "Ajouter aux favoris ยป"
 
-#~ msgid "Provided data is invalid"
-#~ msgstr "Les informations fournies ne sont pas valides"
-
 #~ msgid "Rename"
 #~ msgstr "Renommer"
 
index 1b10ee41c8299f196b510bde155d6c09e8bd045b..a46e32c9ec42215595426b96ff6302fbb0d334c1 100644 (file)
--- a/index.php
+++ b/index.php
@@ -143,6 +143,8 @@ $app->group('/admin', function () {
     $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
     $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
     $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('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle');
     $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
index a232b351f4cfdae5c63c4d31553f055500a4111b..ac46cbf129df09479f039d44f3eeee3784b299be 100644 (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).
      * It should always return false.
@@ -349,4 +378,37 @@ class ApplicationUtilsTest extends \Shaarli\TestCase
             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')
+        );
+    }
 }
index 9163bdf1face0b250ad801b13de5cd96385507a5..3384504a70c1d5daaff727c8655ce6a317786a36 100644 (file)
@@ -3,25 +3,48 @@
 namespace Shaarli;
 
 use Exception;
+use Shaarli\Exceptions\IOException;
 
 /**
  * Class FileUtilsTest
  *
  * Test file utility class.
  */
-class FileUtilsTest extends \Shaarli\TestCase
+class FileUtilsTest extends TestCase
 {
     /**
      * @var string Test file path.
      */
     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.
      */
     protected function tearDown(): void
     {
         @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(['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);
+    }
 }
diff --git a/tests/front/controller/admin/ServerControllerTest.php b/tests/front/controller/admin/ServerControllerTest.php
new file mode 100644 (file)
index 0000000..355cce7
--- /dev/null
@@ -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');
+    }
+}
index 345ad544b85a582ccfab88b9f1a9ad4e74375843..2105ed770cd48b908c4f366b9ad5cc7129ff2b39 100644 (file)
@@ -79,6 +79,15 @@ class InstallControllerTest extends TestCase
         static::assertIsArray($assignedVariables['languages']);
         static::assertSame('Automatic', $assignedVariables['languages']['auto']);
         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']);
     }
 
     /**
index a506a2eb2543b76f7dbe5ee760de737938a0bce5..4f98d49dff9f066d7ff50b8a73633b31ff2613a7 100644 (file)
   </div>
 </div>
 </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"}
 </body>
 </html>
diff --git a/tpl/default/server.html b/tpl/default/server.html
new file mode 100644 (file)
index 0000000..de1c8b5
--- /dev/null
@@ -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>
diff --git a/tpl/default/server.requirements.html b/tpl/default/server.requirements.html
new file mode 100644 (file)
index 0000000..85def9b
--- /dev/null
@@ -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>
index 2cb08e387b468e8f2b39942a46ae0699abc98088..2df73598173ae522306ae1007ba5188824dcfbd8 100644 (file)
         <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Plugin administration'|t}</span>
       </a>
     </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"}
       <div class="tools-item">
         <a href="{$base_path}/admin/password" title="{'Change your password'|t}">
       </a>
     </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"}
       <div class="tools-item">
         {$value}