]>
Commit | Line | Data |
---|---|---|
2e28269b | 1 | <?php |
53054b2b | 2 | |
c2cd15da | 3 | namespace Shaarli\Helper; |
9778a155 V |
4 | |
5 | use Exception; | |
8a6b7e96 A |
6 | use malkusch\lock\exception\LockAcquireException; |
7 | use malkusch\lock\mutex\FlockMutex; | |
9778a155 V |
8 | use Shaarli\Config\ConfigManager; |
9 | ||
2e28269b V |
10 | /** |
11 | * Shaarli (application) utilities | |
12 | */ | |
13 | class ApplicationUtils | |
14 | { | |
b786c883 A |
15 | /** |
16 | * @var string File containing the current version | |
17 | */ | |
18 | public static $VERSION_FILE = 'shaarli_version.php'; | |
19 | ||
0cf76ccb A |
20 | public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli'; |
21 | public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli'; | |
53054b2b | 22 | public static $GIT_BRANCHES = ['latest', 'stable']; |
4bf35ba5 V |
23 | private static $VERSION_START_TAG = '<?php /* '; |
24 | private static $VERSION_END_TAG = ' */ ?>'; | |
25 | ||
26 | /** | |
27 | * Gets the latest version code from the Git repository | |
28 | * | |
29 | * The code is read from the raw content of the version file on the Git server. | |
30 | * | |
7af9a418 A |
31 | * @param string $url URL to reach to get the latest version. |
32 | * @param int $timeout Timeout to check the URL (in seconds). | |
33 | * | |
4bf35ba5 V |
34 | * @return mixed the version code from the repository if available, else 'false' |
35 | */ | |
f211e417 | 36 | public static function getLatestGitVersionCode($url, $timeout = 2) |
4bf35ba5 | 37 | { |
1557cefb | 38 | list($headers, $data) = get_http_response($url, $timeout); |
4bf35ba5 V |
39 | |
40 | if (strpos($headers[0], '200 OK') === false) { | |
41 | error_log('Failed to retrieve ' . $url); | |
42 | return false; | |
43 | } | |
44 | ||
b786c883 A |
45 | return $data; |
46 | } | |
47 | ||
48 | /** | |
49 | * Retrieve the version from a remote URL or a file. | |
50 | * | |
51 | * @param string $remote URL or file to fetch. | |
52 | * @param int $timeout For URLs fetching. | |
53 | * | |
54 | * @return bool|string The version or false if it couldn't be retrieved. | |
55 | */ | |
56 | public static function getVersion($remote, $timeout = 2) | |
57 | { | |
58 | if (startsWith($remote, 'http')) { | |
59 | if (($data = static::getLatestGitVersionCode($remote, $timeout)) === false) { | |
60 | return false; | |
61 | } | |
62 | } else { | |
9778a155 | 63 | if (!is_file($remote)) { |
b786c883 A |
64 | return false; |
65 | } | |
66 | $data = file_get_contents($remote); | |
67 | } | |
68 | ||
4bf35ba5 | 69 | return str_replace( |
53054b2b A |
70 | [self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL], |
71 | ['', '', ''], | |
4bf35ba5 V |
72 | $data |
73 | ); | |
74 | } | |
75 | ||
76 | /** | |
77 | * Checks if a new Shaarli version has been published on the Git repository | |
78 | * | |
79 | * Updates checks are run periodically, according to the following criteria: | |
80 | * - the update checks are enabled (install, global config); | |
81 | * - the user is logged in (or this is an open instance); | |
82 | * - the last check is older than a given interval; | |
83 | * - the check is non-blocking if the HTTPS connection to Git fails; | |
84 | * - in case of failure, the update file's modification date is updated, | |
85 | * to avoid intempestive connection attempts. | |
86 | * | |
87 | * @param string $currentVersion the current version code | |
88 | * @param string $updateFile the file where to store the latest version code | |
89 | * @param int $checkInterval the minimum interval between update checks (in seconds | |
90 | * @param bool $enableCheck whether to check for new versions | |
91 | * @param bool $isLoggedIn whether the user is logged in | |
7af9a418 | 92 | * @param string $branch check update for the given branch |
4bf35ba5 | 93 | * |
4a7af975 V |
94 | * @throws Exception an invalid branch has been set for update checks |
95 | * | |
4bf35ba5 V |
96 | * @return mixed the new version code if available and greater, else 'false' |
97 | */ | |
f211e417 V |
98 | public static function checkUpdate( |
99 | $currentVersion, | |
100 | $updateFile, | |
101 | $checkInterval, | |
102 | $enableCheck, | |
103 | $isLoggedIn, | |
104 | $branch = 'stable' | |
105 | ) { | |
b897c81f A |
106 | // Do not check versions for visitors |
107 | // Do not check if the user doesn't want to | |
108 | // Do not check with dev version | |
9778a155 | 109 | if (!$isLoggedIn || empty($enableCheck) || $currentVersion === 'dev') { |
4bf35ba5 V |
110 | return false; |
111 | } | |
112 | ||
113 | if (is_file($updateFile) && (filemtime($updateFile) > time() - $checkInterval)) { | |
114 | // Shaarli has checked for updates recently - skip HTTP query | |
115 | $latestKnownVersion = file_get_contents($updateFile); | |
116 | ||
117 | if (version_compare($latestKnownVersion, $currentVersion) == 1) { | |
118 | return $latestKnownVersion; | |
119 | } | |
120 | return false; | |
121 | } | |
122 | ||
9778a155 | 123 | if (!in_array($branch, self::$GIT_BRANCHES)) { |
4407b45f V |
124 | throw new Exception( |
125 | 'Invalid branch selected for updates: "' . $branch . '"' | |
126 | ); | |
127 | } | |
128 | ||
4bf35ba5 V |
129 | // Late Static Binding allows overriding within tests |
130 | // See http://php.net/manual/en/language.oop5.late-static-bindings.php | |
b786c883 | 131 | $latestVersion = static::getVersion( |
0cf76ccb | 132 | self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE |
4bf35ba5 V |
133 | ); |
134 | ||
9778a155 | 135 | if (!$latestVersion) { |
4bf35ba5 V |
136 | // Only update the file's modification date |
137 | file_put_contents($updateFile, $currentVersion); | |
138 | return false; | |
139 | } | |
140 | ||
141 | // Update the file's content and modification date | |
142 | file_put_contents($updateFile, $latestVersion); | |
143 | ||
144 | if (version_compare($latestVersion, $currentVersion) == 1) { | |
145 | return $latestVersion; | |
146 | } | |
147 | ||
148 | return false; | |
149 | } | |
2e28269b | 150 | |
c9cf2715 V |
151 | /** |
152 | * Checks the PHP version to ensure Shaarli can run | |
153 | * | |
154 | * @param string $minVersion minimum PHP required version | |
155 | * @param string $curVersion current PHP version (use PHP_VERSION) | |
156 | * | |
def39d0d A |
157 | * @return bool true on success |
158 | * | |
c9cf2715 V |
159 | * @throws Exception the PHP version is not supported |
160 | */ | |
161 | public static function checkPHPVersion($minVersion, $curVersion) | |
162 | { | |
163 | if (version_compare($curVersion, $minVersion) < 0) { | |
12266213 | 164 | $msg = t( |
c9cf2715 | 165 | 'Your PHP version is obsolete!' |
9778a155 V |
166 | . ' Shaarli requires at least PHP %s, and thus cannot run.' |
167 | . ' Your PHP version has known security vulnerabilities and should be' | |
168 | . ' updated as soon as possible.' | |
c9cf2715 | 169 | ); |
12266213 | 170 | throw new Exception(sprintf($msg, $minVersion)); |
c9cf2715 | 171 | } |
def39d0d | 172 | return true; |
c9cf2715 V |
173 | } |
174 | ||
2e28269b V |
175 | /** |
176 | * Checks Shaarli has the proper access permissions to its resources | |
177 | * | |
0cf76ccb A |
178 | * @param ConfigManager $conf Configuration Manager instance. |
179 | * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template. | |
180 | * Currently we only need to be able to read the theme and write in raintpl cache. | |
278d9ee2 | 181 | * |
2e28269b V |
182 | * @return array A list of the detected configuration issues |
183 | */ | |
0cf76ccb | 184 | public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array |
2e28269b | 185 | { |
0cf76ccb | 186 | $errors = []; |
e4325b15 | 187 | $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); |
2e28269b V |
188 | |
189 | // Check script and template directories are readable | |
53054b2b A |
190 | foreach ( |
191 | [ | |
192 | 'application', | |
193 | 'inc', | |
194 | 'plugins', | |
195 | $rainTplDir, | |
196 | $rainTplDir . '/' . $conf->get('resource.theme'), | |
197 | ] as $path | |
198 | ) { | |
9778a155 V |
199 | if (!is_readable(realpath($path))) { |
200 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); | |
2e28269b V |
201 | } |
202 | } | |
203 | ||
7af9a418 | 204 | // Check cache and data directories are readable and writable |
0cf76ccb A |
205 | if ($minimalMode) { |
206 | $folders = [ | |
207 | $conf->get('resource.raintpl_tmp'), | |
208 | ]; | |
209 | } else { | |
210 | $folders = [ | |
53054b2b A |
211 | $conf->get('resource.thumbnails_cache'), |
212 | $conf->get('resource.data_dir'), | |
213 | $conf->get('resource.page_cache'), | |
214 | $conf->get('resource.raintpl_tmp'), | |
0cf76ccb A |
215 | ]; |
216 | } | |
217 | ||
218 | foreach ($folders as $path) { | |
9778a155 V |
219 | if (!is_readable(realpath($path))) { |
220 | $errors[] = '"' . $path . '" ' . t('directory is not readable'); | |
2e28269b | 221 | } |
9778a155 V |
222 | if (!is_writable(realpath($path))) { |
223 | $errors[] = '"' . $path . '" ' . t('directory is not writable'); | |
2e28269b V |
224 | } |
225 | } | |
226 | ||
0cf76ccb A |
227 | if ($minimalMode) { |
228 | return $errors; | |
229 | } | |
230 | ||
7af9a418 | 231 | // Check configuration files are readable and writable |
53054b2b A |
232 | foreach ( |
233 | [ | |
234 | $conf->getConfigFileExt(), | |
235 | $conf->get('resource.datastore'), | |
236 | $conf->get('resource.ban_file'), | |
237 | $conf->get('resource.log'), | |
238 | $conf->get('resource.update_check'), | |
239 | ] as $path | |
240 | ) { | |
9778a155 | 241 | if (!is_file(realpath($path))) { |
2e28269b V |
242 | # the file may not exist yet |
243 | continue; | |
244 | } | |
245 | ||
9778a155 V |
246 | if (!is_readable(realpath($path))) { |
247 | $errors[] = '"' . $path . '" ' . t('file is not readable'); | |
2e28269b | 248 | } |
9778a155 V |
249 | if (!is_writable(realpath($path))) { |
250 | $errors[] = '"' . $path . '" ' . t('file is not writable'); | |
2e28269b V |
251 | } |
252 | } | |
253 | ||
254 | return $errors; | |
255 | } | |
bfe4f536 | 256 | |
8a6b7e96 A |
257 | public static function checkDatastoreMutex(): array |
258 | { | |
259 | $mutex = new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2); | |
260 | try { | |
261 | $mutex->synchronized(function () { | |
262 | return true; | |
263 | }); | |
264 | } catch (LockAcquireException $e) { | |
265 | $errors[] = t('Lock can not be acquired on the datastore. You might encounter concurrent access issues.'); | |
266 | } | |
267 | ||
268 | return $errors ?? []; | |
269 | } | |
270 | ||
bfe4f536 A |
271 | /** |
272 | * Returns a salted hash representing the current Shaarli version. | |
273 | * | |
274 | * Useful for assets browser cache. | |
275 | * | |
276 | * @param string $currentVersion of Shaarli | |
277 | * @param string $salt User personal salt, also used for the authentication | |
278 | * | |
279 | * @return string version hash | |
280 | */ | |
281 | public static function getVersionHash($currentVersion, $salt) | |
282 | { | |
283 | return hash_hmac('sha256', $currentVersion, $salt); | |
284 | } | |
0cf76ccb A |
285 | |
286 | /** | |
287 | * Get a list of PHP extensions used by Shaarli. | |
288 | * | |
289 | * @return array[] List of extension with following keys: | |
290 | * - name: extension name | |
291 | * - required: whether the extension is required to use Shaarli | |
292 | * - desc: short description of extension usage in Shaarli | |
293 | * - loaded: whether the extension is properly loaded or not | |
294 | */ | |
295 | public static function getPhpExtensionsRequirement(): array | |
296 | { | |
297 | $extensions = [ | |
298 | ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')], | |
299 | ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')], | |
300 | ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')], | |
301 | ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')], | |
302 | ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->รจ->f)')], | |
303 | ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')], | |
304 | ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')], | |
305 | ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')], | |
306 | ]; | |
307 | ||
308 | foreach ($extensions as &$extension) { | |
309 | $extension['loaded'] = extension_loaded($extension['name']); | |
310 | } | |
311 | ||
312 | return $extensions; | |
313 | } | |
314 | ||
315 | /** | |
316 | * Return the EOL date of given PHP version. If the version is unknown, | |
317 | * we return today + 2 years. | |
318 | * | |
319 | * @param string $fullVersion PHP version, e.g. 7.4.7 | |
320 | * | |
321 | * @return string Date format: YYYY-MM-DD | |
322 | */ | |
323 | public static function getPhpEol(string $fullVersion): string | |
324 | { | |
325 | preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches); | |
326 | ||
327 | return [ | |
328 | '7.1' => '2019-12-01', | |
329 | '7.2' => '2020-11-30', | |
330 | '7.3' => '2021-12-06', | |
331 | '7.4' => '2022-11-28', | |
332 | '8.0' => '2023-12-01', | |
333 | ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d'); | |
334 | } | |
2e28269b | 335 | } |