aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-27 19:29:43 +0100
committerGitHub <noreply@github.com>2020-10-27 19:29:43 +0100
commite6215a2ad97182efcf88ef532ec6bd65ae35fd19 (patch)
tree33a1619091366c8a0cf0c772ba644a75ce61764f /application
parent034c1ce5269956ec43448f9fd2959e094c861004 (diff)
parent0cf76ccb4736473a958d9fd36ed914e2d25d594a (diff)
downloadShaarli-e6215a2ad97182efcf88ef532ec6bd65ae35fd19.tar.gz
Shaarli-e6215a2ad97182efcf88ef532ec6bd65ae35fd19.tar.zst
Shaarli-e6215a2ad97182efcf88ef532ec6bd65ae35fd19.zip
Merge pull request #1604 from ArthurHoaro/feature/server-admin-page
Feature: add a Server administration page
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 }