aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-21 13:12:15 +0200
committerArthurHoaro <arthur@hoa.ro>2020-10-21 15:06:47 +0200
commit0cf76ccb4736473a958d9fd36ed914e2d25d594a (patch)
tree0bb11821bc45ad2a7c2b965137a901ae5546455a /application
parentd8030c8155ee4c20573848b2444f6df0b65d1662 (diff)
downloadShaarli-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.php93
-rw-r--r--application/FileUtils.php56
-rw-r--r--application/front/controller/admin/ServerController.php87
-rw-r--r--application/front/controller/visitor/BookmarkListController.php28
-rw-r--r--application/front/controller/visitor/InstallController.php12
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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Admin;
6
7use Shaarli\ApplicationUtils;
8use Shaarli\FileUtils;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * Slim controller used to handle Server administration page, and actions.
14 */
15class 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 }