diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-10-21 13:12:15 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2020-10-21 15:06:47 +0200 |
commit | 0cf76ccb4736473a958d9fd36ed914e2d25d594a (patch) | |
tree | 0bb11821bc45ad2a7c2b965137a901ae5546455a /application | |
parent | d8030c8155ee4c20573848b2444f6df0b65d1662 (diff) | |
download | Shaarli-0cf76ccb4736473a958d9fd36ed914e2d25d594a.tar.gz Shaarli-0cf76ccb4736473a958d9fd36ed914e2d25d594a.tar.zst Shaarli-0cf76ccb4736473a958d9fd36ed914e2d25d594a.zip |
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
Diffstat (limited to 'application')
-rw-r--r-- | application/ApplicationUtils.php | 93 | ||||
-rw-r--r-- | application/FileUtils.php | 56 | ||||
-rw-r--r-- | application/front/controller/admin/ServerController.php | 87 | ||||
-rw-r--r-- | application/front/controller/visitor/BookmarkListController.php | 28 | ||||
-rw-r--r-- | application/front/controller/visitor/InstallController.php | 12 |
5 files changed, 251 insertions, 25 deletions
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 | |||
14 | */ | 14 | */ |
15 | public static $VERSION_FILE = 'shaarli_version.php'; | 15 | public static $VERSION_FILE = 'shaarli_version.php'; |
16 | 16 | ||
17 | private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; | 17 | public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli'; |
18 | private static $GIT_BRANCHES = array('latest', 'stable'); | 18 | public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; |
19 | public static $GIT_BRANCHES = array('latest', 'stable'); | ||
19 | private static $VERSION_START_TAG = '<?php /* '; | 20 | private static $VERSION_START_TAG = '<?php /* '; |
20 | private static $VERSION_END_TAG = ' */ ?>'; | 21 | private static $VERSION_END_TAG = ' */ ?>'; |
21 | 22 | ||
@@ -125,7 +126,7 @@ class ApplicationUtils | |||
125 | // Late Static Binding allows overriding within tests | 126 | // Late Static Binding allows overriding within tests |
126 | // See http://php.net/manual/en/language.oop5.late-static-bindings.php | 127 | // See http://php.net/manual/en/language.oop5.late-static-bindings.php |
127 | $latestVersion = static::getVersion( | 128 | $latestVersion = static::getVersion( |
128 | self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE | 129 | self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE |
129 | ); | 130 | ); |
130 | 131 | ||
131 | if (!$latestVersion) { | 132 | if (!$latestVersion) { |
@@ -171,35 +172,45 @@ class ApplicationUtils | |||
171 | /** | 172 | /** |
172 | * Checks Shaarli has the proper access permissions to its resources | 173 | * Checks Shaarli has the proper access permissions to its resources |
173 | * | 174 | * |
174 | * @param ConfigManager $conf Configuration Manager instance. | 175 | * @param ConfigManager $conf Configuration Manager instance. |
176 | * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template. | ||
177 | * Currently we only need to be able to read the theme and write in raintpl cache. | ||
175 | * | 178 | * |
176 | * @return array A list of the detected configuration issues | 179 | * @return array A list of the detected configuration issues |
177 | */ | 180 | */ |
178 | public static function checkResourcePermissions($conf) | 181 | public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array |
179 | { | 182 | { |
180 | $errors = array(); | 183 | $errors = []; |
181 | $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); | 184 | $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); |
182 | 185 | ||
183 | // Check script and template directories are readable | 186 | // Check script and template directories are readable |
184 | foreach (array( | 187 | foreach ([ |
185 | 'application', | 188 | 'application', |
186 | 'inc', | 189 | 'inc', |
187 | 'plugins', | 190 | 'plugins', |
188 | $rainTplDir, | 191 | $rainTplDir, |
189 | $rainTplDir . '/' . $conf->get('resource.theme'), | 192 | $rainTplDir . '/' . $conf->get('resource.theme'), |
190 | ) as $path) { | 193 | ] as $path) { |
191 | if (!is_readable(realpath($path))) { | 194 | if (!is_readable(realpath($path))) { |
192 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); | 195 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); |
193 | } | 196 | } |
194 | } | 197 | } |
195 | 198 | ||
196 | // Check cache and data directories are readable and writable | 199 | // Check cache and data directories are readable and writable |
197 | foreach (array( | 200 | if ($minimalMode) { |
198 | $conf->get('resource.thumbnails_cache'), | 201 | $folders = [ |
199 | $conf->get('resource.data_dir'), | 202 | $conf->get('resource.raintpl_tmp'), |
200 | $conf->get('resource.page_cache'), | 203 | ]; |
201 | $conf->get('resource.raintpl_tmp'), | 204 | } else { |
202 | ) as $path) { | 205 | $folders = [ |
206 | $conf->get('resource.thumbnails_cache'), | ||
207 | $conf->get('resource.data_dir'), | ||
208 | $conf->get('resource.page_cache'), | ||
209 | $conf->get('resource.raintpl_tmp'), | ||
210 | ]; | ||
211 | } | ||
212 | |||
213 | foreach ($folders as $path) { | ||
203 | if (!is_readable(realpath($path))) { | 214 | if (!is_readable(realpath($path))) { |
204 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); | 215 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); |
205 | } | 216 | } |
@@ -208,6 +219,10 @@ class ApplicationUtils | |||
208 | } | 219 | } |
209 | } | 220 | } |
210 | 221 | ||
222 | if ($minimalMode) { | ||
223 | return $errors; | ||
224 | } | ||
225 | |||
211 | // Check configuration files are readable and writable | 226 | // Check configuration files are readable and writable |
212 | foreach (array( | 227 | foreach (array( |
213 | $conf->getConfigFileExt(), | 228 | $conf->getConfigFileExt(), |
@@ -246,4 +261,54 @@ class ApplicationUtils | |||
246 | { | 261 | { |
247 | return hash_hmac('sha256', $currentVersion, $salt); | 262 | return hash_hmac('sha256', $currentVersion, $salt); |
248 | } | 263 | } |
264 | |||
265 | /** | ||
266 | * Get a list of PHP extensions used by Shaarli. | ||
267 | * | ||
268 | * @return array[] List of extension with following keys: | ||
269 | * - name: extension name | ||
270 | * - required: whether the extension is required to use Shaarli | ||
271 | * - desc: short description of extension usage in Shaarli | ||
272 | * - loaded: whether the extension is properly loaded or not | ||
273 | */ | ||
274 | public static function getPhpExtensionsRequirement(): array | ||
275 | { | ||
276 | $extensions = [ | ||
277 | ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')], | ||
278 | ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')], | ||
279 | ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')], | ||
280 | ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')], | ||
281 | ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->รจ->f)')], | ||
282 | ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')], | ||
283 | ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')], | ||
284 | ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')], | ||
285 | ]; | ||
286 | |||
287 | foreach ($extensions as &$extension) { | ||
288 | $extension['loaded'] = extension_loaded($extension['name']); | ||
289 | } | ||
290 | |||
291 | return $extensions; | ||
292 | } | ||
293 | |||
294 | /** | ||
295 | * Return the EOL date of given PHP version. If the version is unknown, | ||
296 | * we return today + 2 years. | ||
297 | * | ||
298 | * @param string $fullVersion PHP version, e.g. 7.4.7 | ||
299 | * | ||
300 | * @return string Date format: YYYY-MM-DD | ||
301 | */ | ||
302 | public static function getPhpEol(string $fullVersion): string | ||
303 | { | ||
304 | preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches); | ||
305 | |||
306 | return [ | ||
307 | '7.1' => '2019-12-01', | ||
308 | '7.2' => '2020-11-30', | ||
309 | '7.3' => '2021-12-06', | ||
310 | '7.4' => '2022-11-28', | ||
311 | '8.0' => '2023-12-01', | ||
312 | ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d'); | ||
313 | } | ||
249 | } | 314 | } |
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 | |||
81 | ) | 81 | ) |
82 | ); | 82 | ); |
83 | } | 83 | } |
84 | |||
85 | /** | ||
86 | * Recursively deletes a folder content, and deletes itself optionally. | ||
87 | * If an excluded file is found, folders won't be deleted. | ||
88 | * | ||
89 | * Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory. | ||
90 | * | ||
91 | * @param string $path | ||
92 | * @param bool $selfDelete Delete the provided folder if true, only its content if false. | ||
93 | * @param array $exclude | ||
94 | */ | ||
95 | public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool | ||
96 | { | ||
97 | $skipped = false; | ||
98 | |||
99 | if (!is_dir($path)) { | ||
100 | throw new IOException(t('Provided path is not a directory.')); | ||
101 | } | ||
102 | |||
103 | if (!static::isPathInShaarliFolder($path)) { | ||
104 | throw new IOException(t('Trying to delete a folder outside of Shaarli path.')); | ||
105 | } | ||
106 | |||
107 | foreach (new \DirectoryIterator($path) as $file) { | ||
108 | if($file->isDot()) { | ||
109 | continue; | ||
110 | } | ||
111 | |||
112 | if (in_array($file->getBasename(), $exclude, true)) { | ||
113 | $skipped = true; | ||
114 | continue; | ||
115 | } | ||
116 | |||
117 | if ($file->isFile()) { | ||
118 | unlink($file->getPathname()); | ||
119 | } elseif($file->isDir()) { | ||
120 | $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped; | ||
121 | } | ||
122 | } | ||
123 | |||
124 | if ($selfDelete && !$skipped) { | ||
125 | rmdir($path); | ||
126 | } | ||
127 | |||
128 | return $skipped; | ||
129 | } | ||
130 | |||
131 | /** | ||
132 | * Checks that the given path is inside Shaarli directory. | ||
133 | */ | ||
134 | public static function isPathInShaarliFolder(string $path): bool | ||
135 | { | ||
136 | $rootDirectory = dirname(dirname(__FILE__)); | ||
137 | |||
138 | return strpos(realpath($path), $rootDirectory) !== false; | ||
139 | } | ||
84 | } | 140 | } |
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Admin; | ||
6 | |||
7 | use Shaarli\ApplicationUtils; | ||
8 | use Shaarli\FileUtils; | ||
9 | use Slim\Http\Request; | ||
10 | use Slim\Http\Response; | ||
11 | |||
12 | /** | ||
13 | * Slim controller used to handle Server administration page, and actions. | ||
14 | */ | ||
15 | class ServerController extends ShaarliAdminController | ||
16 | { | ||
17 | /** @var string Cache type - main - by default pagecache/ and tmp/ */ | ||
18 | protected const CACHE_MAIN = 'main'; | ||
19 | |||
20 | /** @var string Cache type - thumbnails - by default cache/ */ | ||
21 | protected const CACHE_THUMB = 'thumbnails'; | ||
22 | |||
23 | /** | ||
24 | * GET /admin/server - Display page Server administration | ||
25 | */ | ||
26 | public function index(Request $request, Response $response): Response | ||
27 | { | ||
28 | $latestVersion = 'v' . ApplicationUtils::getVersion( | ||
29 | ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE | ||
30 | ); | ||
31 | $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php'); | ||
32 | $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion; | ||
33 | $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); | ||
34 | |||
35 | $this->assignView('php_version', PHP_VERSION); | ||
36 | $this->assignView('php_eol', format_date($phpEol, false)); | ||
37 | $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); | ||
38 | $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); | ||
39 | $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); | ||
40 | $this->assignView('release_url', ApplicationUtils::$GITHUB_URL . '/releases/tag/' . $latestVersion); | ||
41 | $this->assignView('latest_version', $latestVersion); | ||
42 | $this->assignView('current_version', $currentVersion); | ||
43 | $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode')); | ||
44 | $this->assignView('index_url', index_url($this->container->environment)); | ||
45 | $this->assignView('client_ip', client_ip_id($this->container->environment)); | ||
46 | $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', [])); | ||
47 | |||
48 | $this->assignView( | ||
49 | 'pagetitle', | ||
50 | t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') | ||
51 | ); | ||
52 | |||
53 | return $response->write($this->render('server')); | ||
54 | } | ||
55 | |||
56 | /** | ||
57 | * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails). | ||
58 | */ | ||
59 | public function clearCache(Request $request, Response $response): Response | ||
60 | { | ||
61 | $exclude = ['.htaccess']; | ||
62 | |||
63 | if ($request->getQueryParam('type') === static::CACHE_THUMB) { | ||
64 | $folders = [$this->container->conf->get('resource.thumbnails_cache')]; | ||
65 | |||
66 | $this->saveWarningMessage( | ||
67 | t('Thumbnails cache has been cleared.') . ' ' . | ||
68 | '<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>' | ||
69 | ); | ||
70 | } else { | ||
71 | $folders = [ | ||
72 | $this->container->conf->get('resource.page_cache'), | ||
73 | $this->container->conf->get('resource.raintpl_tmp'), | ||
74 | ]; | ||
75 | |||
76 | $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!')); | ||
77 | } | ||
78 | |||
79 | // Make sure that we don't delete root cache folder | ||
80 | $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders)))); | ||
81 | foreach ($folders as $folder) { | ||
82 | FileUtils::clearFolder($folder, false, $exclude); | ||
83 | } | ||
84 | |||
85 | return $this->redirect($response, '/admin/server'); | ||
86 | } | ||
87 | } | ||
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 | |||
169 | */ | 169 | */ |
170 | protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool | 170 | protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool |
171 | { | 171 | { |
172 | // Logged in, not async retrieval, thumbnails enabled, and thumbnail should be updated | 172 | if (false === $this->container->loginManager->isLoggedIn()) { |
173 | if ($this->container->loginManager->isLoggedIn() | 173 | return false; |
174 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | 174 | } |
175 | && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | 175 | |
176 | && $bookmark->shouldUpdateThumbnail() | 176 | // If thumbnail should be updated, we reset it to null |
177 | ) { | 177 | if ($bookmark->shouldUpdateThumbnail()) { |
178 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | 178 | $bookmark->setThumbnail(null); |
179 | $this->container->bookmarkService->set($bookmark, $writeDatastore); | 179 | |
180 | 180 | // Requires an update, not async retrieval, thumbnails enabled | |
181 | return true; | 181 | if ($bookmark->shouldUpdateThumbnail() |
182 | && true !== $this->container->conf->get('general.enable_async_metadata', true) | ||
183 | && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE | ||
184 | ) { | ||
185 | $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); | ||
186 | $this->container->bookmarkService->set($bookmark, $writeDatastore); | ||
187 | |||
188 | return true; | ||
189 | } | ||
182 | } | 190 | } |
183 | 191 | ||
184 | return false; | 192 | 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 | |||
53 | $this->assignView('cities', $cities); | 53 | $this->assignView('cities', $cities); |
54 | $this->assignView('languages', Languages::getAvailableLanguages()); | 54 | $this->assignView('languages', Languages::getAvailableLanguages()); |
55 | 55 | ||
56 | $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); | ||
57 | |||
58 | $this->assignView('php_version', PHP_VERSION); | ||
59 | $this->assignView('php_eol', format_date($phpEol, false)); | ||
60 | $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); | ||
61 | $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); | ||
62 | $this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); | ||
63 | |||
64 | $this->assignView('pagetitle', t('Install Shaarli')); | ||
65 | |||
56 | return $response->write($this->render('install')); | 66 | return $response->write($this->render('install')); |
57 | } | 67 | } |
58 | 68 | ||
@@ -150,7 +160,7 @@ class InstallController extends ShaarliVisitorController | |||
150 | protected function checkPermissions(): bool | 160 | protected function checkPermissions(): bool |
151 | { | 161 | { |
152 | // Ensure Shaarli has proper access to its resources | 162 | // Ensure Shaarli has proper access to its resources |
153 | $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); | 163 | $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true); |
154 | if (empty($errors)) { | 164 | if (empty($errors)) { |
155 | return true; | 165 | return true; |
156 | } | 166 | } |