diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-10-27 19:29:43 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-27 19:29:43 +0100 |
commit | e6215a2ad97182efcf88ef532ec6bd65ae35fd19 (patch) | |
tree | 33a1619091366c8a0cf0c772ba644a75ce61764f /application | |
parent | 034c1ce5269956ec43448f9fd2959e094c861004 (diff) | |
parent | 0cf76ccb4736473a958d9fd36ed914e2d25d594a (diff) | |
download | Shaarli-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.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 | } |