From 0cf76ccb4736473a958d9fd36ed914e2d25d594a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 21 Oct 2020 13:12:15 +0200 Subject: 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 --- application/ApplicationUtils.php | 93 ++++++++++++++++++---- application/FileUtils.php | 56 +++++++++++++ .../front/controller/admin/ServerController.php | 87 ++++++++++++++++++++ .../controller/visitor/BookmarkListController.php | 28 ++++--- .../front/controller/visitor/InstallController.php | 12 ++- 5 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 application/front/controller/admin/ServerController.php (limited to 'application') diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 3aa21829..bd1c7cf3 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php @@ -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 = ''; @@ -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'); + } } diff --git a/application/FileUtils.php b/application/FileUtils.php index 30560bfc..3f940751 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php @@ -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 index 00000000..85654a43 --- /dev/null +++ b/application/front/controller/admin/ServerController.php @@ -0,0 +1,87 @@ +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.') . ' ' . + '' . t('Please synchronize them.') .'' + ); + } 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'); + } +} diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index a8019ead..5267c8f5 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -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; diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 7cb32777..564a5777 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -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; } -- cgit v1.2.3