aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/helper
diff options
context:
space:
mode:
Diffstat (limited to 'application/helper')
-rw-r--r--application/helper/ApplicationUtils.php314
-rw-r--r--application/helper/FileUtils.php140
2 files changed, 454 insertions, 0 deletions
diff --git a/application/helper/ApplicationUtils.php b/application/helper/ApplicationUtils.php
new file mode 100644
index 00000000..4b34e114
--- /dev/null
+++ b/application/helper/ApplicationUtils.php
@@ -0,0 +1,314 @@
1<?php
2namespace Shaarli\Helper;
3
4use Exception;
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Shaarli (application) utilities
9 */
10class ApplicationUtils
11{
12 /**
13 * @var string File containing the current version
14 */
15 public static $VERSION_FILE = 'shaarli_version.php';
16
17 public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli';
18 public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
19 public static $GIT_BRANCHES = array('latest', 'stable');
20 private static $VERSION_START_TAG = '<?php /* ';
21 private static $VERSION_END_TAG = ' */ ?>';
22
23 /**
24 * Gets the latest version code from the Git repository
25 *
26 * The code is read from the raw content of the version file on the Git server.
27 *
28 * @param string $url URL to reach to get the latest version.
29 * @param int $timeout Timeout to check the URL (in seconds).
30 *
31 * @return mixed the version code from the repository if available, else 'false'
32 */
33 public static function getLatestGitVersionCode($url, $timeout = 2)
34 {
35 list($headers, $data) = get_http_response($url, $timeout);
36
37 if (strpos($headers[0], '200 OK') === false) {
38 error_log('Failed to retrieve ' . $url);
39 return false;
40 }
41
42 return $data;
43 }
44
45 /**
46 * Retrieve the version from a remote URL or a file.
47 *
48 * @param string $remote URL or file to fetch.
49 * @param int $timeout For URLs fetching.
50 *
51 * @return bool|string The version or false if it couldn't be retrieved.
52 */
53 public static function getVersion($remote, $timeout = 2)
54 {
55 if (startsWith($remote, 'http')) {
56 if (($data = static::getLatestGitVersionCode($remote, $timeout)) === false) {
57 return false;
58 }
59 } else {
60 if (!is_file($remote)) {
61 return false;
62 }
63 $data = file_get_contents($remote);
64 }
65
66 return str_replace(
67 array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL),
68 array('', '', ''),
69 $data
70 );
71 }
72
73 /**
74 * Checks if a new Shaarli version has been published on the Git repository
75 *
76 * Updates checks are run periodically, according to the following criteria:
77 * - the update checks are enabled (install, global config);
78 * - the user is logged in (or this is an open instance);
79 * - the last check is older than a given interval;
80 * - the check is non-blocking if the HTTPS connection to Git fails;
81 * - in case of failure, the update file's modification date is updated,
82 * to avoid intempestive connection attempts.
83 *
84 * @param string $currentVersion the current version code
85 * @param string $updateFile the file where to store the latest version code
86 * @param int $checkInterval the minimum interval between update checks (in seconds
87 * @param bool $enableCheck whether to check for new versions
88 * @param bool $isLoggedIn whether the user is logged in
89 * @param string $branch check update for the given branch
90 *
91 * @throws Exception an invalid branch has been set for update checks
92 *
93 * @return mixed the new version code if available and greater, else 'false'
94 */
95 public static function checkUpdate(
96 $currentVersion,
97 $updateFile,
98 $checkInterval,
99 $enableCheck,
100 $isLoggedIn,
101 $branch = 'stable'
102 ) {
103 // Do not check versions for visitors
104 // Do not check if the user doesn't want to
105 // Do not check with dev version
106 if (!$isLoggedIn || empty($enableCheck) || $currentVersion === 'dev') {
107 return false;
108 }
109
110 if (is_file($updateFile) && (filemtime($updateFile) > time() - $checkInterval)) {
111 // Shaarli has checked for updates recently - skip HTTP query
112 $latestKnownVersion = file_get_contents($updateFile);
113
114 if (version_compare($latestKnownVersion, $currentVersion) == 1) {
115 return $latestKnownVersion;
116 }
117 return false;
118 }
119
120 if (!in_array($branch, self::$GIT_BRANCHES)) {
121 throw new Exception(
122 'Invalid branch selected for updates: "' . $branch . '"'
123 );
124 }
125
126 // Late Static Binding allows overriding within tests
127 // See http://php.net/manual/en/language.oop5.late-static-bindings.php
128 $latestVersion = static::getVersion(
129 self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
130 );
131
132 if (!$latestVersion) {
133 // Only update the file's modification date
134 file_put_contents($updateFile, $currentVersion);
135 return false;
136 }
137
138 // Update the file's content and modification date
139 file_put_contents($updateFile, $latestVersion);
140
141 if (version_compare($latestVersion, $currentVersion) == 1) {
142 return $latestVersion;
143 }
144
145 return false;
146 }
147
148 /**
149 * Checks the PHP version to ensure Shaarli can run
150 *
151 * @param string $minVersion minimum PHP required version
152 * @param string $curVersion current PHP version (use PHP_VERSION)
153 *
154 * @return bool true on success
155 *
156 * @throws Exception the PHP version is not supported
157 */
158 public static function checkPHPVersion($minVersion, $curVersion)
159 {
160 if (version_compare($curVersion, $minVersion) < 0) {
161 $msg = t(
162 'Your PHP version is obsolete!'
163 . ' Shaarli requires at least PHP %s, and thus cannot run.'
164 . ' Your PHP version has known security vulnerabilities and should be'
165 . ' updated as soon as possible.'
166 );
167 throw new Exception(sprintf($msg, $minVersion));
168 }
169 return true;
170 }
171
172 /**
173 * Checks Shaarli has the proper access permissions to its resources
174 *
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.
178 *
179 * @return array A list of the detected configuration issues
180 */
181 public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array
182 {
183 $errors = [];
184 $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
185
186 // Check script and template directories are readable
187 foreach ([
188 'application',
189 'inc',
190 'plugins',
191 $rainTplDir,
192 $rainTplDir . '/' . $conf->get('resource.theme'),
193 ] as $path) {
194 if (!is_readable(realpath($path))) {
195 $errors[] = '"' . $path . '" ' . t('directory is not readable');
196 }
197 }
198
199 // Check cache and data directories are readable and writable
200 if ($minimalMode) {
201 $folders = [
202 $conf->get('resource.raintpl_tmp'),
203 ];
204 } else {
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) {
214 if (!is_readable(realpath($path))) {
215 $errors[] = '"' . $path . '" ' . t('directory is not readable');
216 }
217 if (!is_writable(realpath($path))) {
218 $errors[] = '"' . $path . '" ' . t('directory is not writable');
219 }
220 }
221
222 if ($minimalMode) {
223 return $errors;
224 }
225
226 // Check configuration files are readable and writable
227 foreach (array(
228 $conf->getConfigFileExt(),
229 $conf->get('resource.datastore'),
230 $conf->get('resource.ban_file'),
231 $conf->get('resource.log'),
232 $conf->get('resource.update_check'),
233 ) as $path) {
234 if (!is_file(realpath($path))) {
235 # the file may not exist yet
236 continue;
237 }
238
239 if (!is_readable(realpath($path))) {
240 $errors[] = '"' . $path . '" ' . t('file is not readable');
241 }
242 if (!is_writable(realpath($path))) {
243 $errors[] = '"' . $path . '" ' . t('file is not writable');
244 }
245 }
246
247 return $errors;
248 }
249
250 /**
251 * Returns a salted hash representing the current Shaarli version.
252 *
253 * Useful for assets browser cache.
254 *
255 * @param string $currentVersion of Shaarli
256 * @param string $salt User personal salt, also used for the authentication
257 *
258 * @return string version hash
259 */
260 public static function getVersionHash($currentVersion, $salt)
261 {
262 return hash_hmac('sha256', $currentVersion, $salt);
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 }
314}
diff --git a/application/helper/FileUtils.php b/application/helper/FileUtils.php
new file mode 100644
index 00000000..2d50d850
--- /dev/null
+++ b/application/helper/FileUtils.php
@@ -0,0 +1,140 @@
1<?php
2
3namespace Shaarli\Helper;
4
5use Shaarli\Exceptions\IOException;
6
7/**
8 * Class FileUtils
9 *
10 * Utility class for file manipulation.
11 */
12class FileUtils
13{
14 /**
15 * @var string
16 */
17 protected static $phpPrefix = '<?php /* ';
18
19 /**
20 * @var string
21 */
22 protected static $phpSuffix = ' */ ?>';
23
24 /**
25 * Write data into a file (Shaarli database format).
26 * The data is stored in a PHP file, as a comment, in compressed base64 format.
27 *
28 * The file will be created if it doesn't exist.
29 *
30 * @param string $file File path.
31 * @param mixed $content Content to write.
32 *
33 * @return int|bool Number of bytes written or false if it fails.
34 *
35 * @throws IOException The destination file can't be written.
36 */
37 public static function writeFlatDB($file, $content)
38 {
39 if (is_file($file) && !is_writeable($file)) {
40 // The datastore exists but is not writeable
41 throw new IOException($file);
42 } elseif (!is_file($file) && !is_writeable(dirname($file))) {
43 // The datastore does not exist and its parent directory is not writeable
44 throw new IOException(dirname($file));
45 }
46
47 return file_put_contents(
48 $file,
49 self::$phpPrefix . base64_encode(gzdeflate(serialize($content))) . self::$phpSuffix
50 );
51 }
52
53 /**
54 * Read data from a file containing Shaarli database format content.
55 *
56 * If the file isn't readable or doesn't exist, default data will be returned.
57 *
58 * @param string $file File path.
59 * @param mixed $default The default value to return if the file isn't readable.
60 *
61 * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails.
62 */
63 public static function readFlatDB($file, $default = null)
64 {
65 // Note that gzinflate is faster than gzuncompress.
66 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
67 if (!is_readable($file)) {
68 return $default;
69 }
70
71 $data = file_get_contents($file);
72 if ($data == '') {
73 return $default;
74 }
75
76 return unserialize(
77 gzinflate(
78 base64_decode(
79 substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
80 )
81 )
82 );
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 }
140}