aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/helper/ApplicationUtils.php
blob: 212dd8e2dc7578aa6da6a3f8d403fff319ae3f42 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
<?php

namespace Shaarli\Helper;

use Exception;
use Shaarli\Config\ConfigManager;

/**
 * Shaarli (application) utilities
 */
class ApplicationUtils
{
    /**
     * @var string File containing the current version
     */
    public static $VERSION_FILE = 'shaarli_version.php';

    public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli';
    public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
    public static $GIT_BRANCHES = ['latest', 'stable'];
    private static $VERSION_START_TAG = '<?php /* ';
    private static $VERSION_END_TAG = ' */ ?>';

    /**
     * Gets the latest version code from the Git repository
     *
     * The code is read from the raw content of the version file on the Git server.
     *
     * @param string $url     URL to reach to get the latest version.
     * @param int    $timeout Timeout to check the URL (in seconds).
     *
     * @return mixed the version code from the repository if available, else 'false'
     */
    public static function getLatestGitVersionCode($url, $timeout = 2)
    {
        list($headers, $data) = get_http_response($url, $timeout);

        if (strpos($headers[0], '200 OK') === false) {
            error_log('Failed to retrieve ' . $url);
            return false;
        }

        return $data;
    }

    /**
     * Retrieve the version from a remote URL or a file.
     *
     * @param string $remote  URL or file to fetch.
     * @param int    $timeout For URLs fetching.
     *
     * @return bool|string The version or false if it couldn't be retrieved.
     */
    public static function getVersion($remote, $timeout = 2)
    {
        if (startsWith($remote, 'http')) {
            if (($data = static::getLatestGitVersionCode($remote, $timeout)) === false) {
                return false;
            }
        } else {
            if (!is_file($remote)) {
                return false;
            }
            $data = file_get_contents($remote);
        }

        return str_replace(
            [self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL],
            ['', '', ''],
            $data
        );
    }

    /**
     * Checks if a new Shaarli version has been published on the Git repository
     *
     * Updates checks are run periodically, according to the following criteria:
     * - the update checks are enabled (install, global config);
     * - the user is logged in (or this is an open instance);
     * - the last check is older than a given interval;
     * - the check is non-blocking if the HTTPS connection to Git fails;
     * - in case of failure, the update file's modification date is updated,
     *   to avoid intempestive connection attempts.
     *
     * @param string $currentVersion the current version code
     * @param string $updateFile     the file where to store the latest version code
     * @param int    $checkInterval  the minimum interval between update checks (in seconds
     * @param bool   $enableCheck    whether to check for new versions
     * @param bool   $isLoggedIn     whether the user is logged in
     * @param string $branch         check update for the given branch
     *
     * @throws Exception an invalid branch has been set for update checks
     *
     * @return mixed the new version code if available and greater, else 'false'
     */
    public static function checkUpdate(
        $currentVersion,
        $updateFile,
        $checkInterval,
        $enableCheck,
        $isLoggedIn,
        $branch = 'stable'
    ) {
        // Do not check versions for visitors
        // Do not check if the user doesn't want to
        // Do not check with dev version
        if (!$isLoggedIn || empty($enableCheck) || $currentVersion === 'dev') {
            return false;
        }

        if (is_file($updateFile) && (filemtime($updateFile) > time() - $checkInterval)) {
            // Shaarli has checked for updates recently - skip HTTP query
            $latestKnownVersion = file_get_contents($updateFile);

            if (version_compare($latestKnownVersion, $currentVersion) == 1) {
                return $latestKnownVersion;
            }
            return false;
        }

        if (!in_array($branch, self::$GIT_BRANCHES)) {
            throw new Exception(
                'Invalid branch selected for updates: "' . $branch . '"'
            );
        }

        // Late Static Binding allows overriding within tests
        // See http://php.net/manual/en/language.oop5.late-static-bindings.php
        $latestVersion = static::getVersion(
            self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
        );

        if (!$latestVersion) {
            // Only update the file's modification date
            file_put_contents($updateFile, $currentVersion);
            return false;
        }

        // Update the file's content and modification date
        file_put_contents($updateFile, $latestVersion);

        if (version_compare($latestVersion, $currentVersion) == 1) {
            return $latestVersion;
        }

        return false;
    }

    /**
     * Checks the PHP version to ensure Shaarli can run
     *
     * @param string $minVersion minimum PHP required version
     * @param string $curVersion current PHP version (use PHP_VERSION)
     *
     * @return bool true on success
     *
     * @throws Exception the PHP version is not supported
     */
    public static function checkPHPVersion($minVersion, $curVersion)
    {
        if (version_compare($curVersion, $minVersion) < 0) {
            $msg = t(
                'Your PHP version is obsolete!'
                . ' Shaarli requires at least PHP %s, and thus cannot run.'
                . ' Your PHP version has known security vulnerabilities and should be'
                . ' updated as soon as possible.'
            );
            throw new Exception(sprintf($msg, $minVersion));
        }
        return true;
    }

    /**
     * Checks Shaarli has the proper access permissions to its resources
     *
     * @param ConfigManager $conf        Configuration Manager instance.
     * @param bool          $minimalMode In minimal mode we only check permissions to be able to display a template.
     *                                   Currently we only need to be able to read the theme and write in raintpl cache.
     *
     * @return array A list of the detected configuration issues
     */
    public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array
    {
        $errors = [];
        $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');

        // Check script and template directories are readable
        foreach (
            [
            'application',
            'inc',
            'plugins',
            $rainTplDir,
            $rainTplDir . '/' . $conf->get('resource.theme'),
            ] as $path
        ) {
            if (!is_readable(realpath($path))) {
                $errors[] = '"' . $path . '" ' . t('directory is not readable');
            }
        }

        // Check cache and data directories are readable and writable
        if ($minimalMode) {
            $folders = [
                $conf->get('resource.raintpl_tmp'),
            ];
        } else {
            $folders = [
            $conf->get('resource.thumbnails_cache'),
            $conf->get('resource.data_dir'),
            $conf->get('resource.page_cache'),
            $conf->get('resource.raintpl_tmp'),
            ];
        }

        foreach ($folders as $path) {
            if (!is_readable(realpath($path))) {
                $errors[] = '"' . $path . '" ' . t('directory is not readable');
            }
            if (!is_writable(realpath($path))) {
                $errors[] = '"' . $path . '" ' . t('directory is not writable');
            }
        }

        if ($minimalMode) {
            return $errors;
        }

        // Check configuration files are readable and writable
        foreach (
            [
                 $conf->getConfigFileExt(),
                 $conf->get('resource.datastore'),
                 $conf->get('resource.ban_file'),
                 $conf->get('resource.log'),
                 $conf->get('resource.update_check'),
             ] as $path
        ) {
            if (!is_file(realpath($path))) {
                # the file may not exist yet
                continue;
            }

            if (!is_readable(realpath($path))) {
                $errors[] = '"' . $path . '" ' . t('file is not readable');
            }
            if (!is_writable(realpath($path))) {
                $errors[] = '"' . $path . '" ' . t('file is not writable');
            }
        }

        return $errors;
    }

    /**
     * Returns a salted hash representing the current Shaarli version.
     *
     * Useful for assets browser cache.
     *
     * @param string $currentVersion of Shaarli
     * @param string $salt           User personal salt, also used for the authentication
     *
     * @return string version hash
     */
    public static function getVersionHash($currentVersion, $salt)
    {
        return hash_hmac('sha256', $currentVersion, $salt);
    }

    /**
     * Get a list of PHP extensions used by Shaarli.
     *
     * @return array[] List of extension with following keys:
     *                   - name: extension name
     *                   - required: whether the extension is required to use Shaarli
     *                   - desc: short description of extension usage in Shaarli
     *                   - loaded: whether the extension is properly loaded or not
     */
    public static function getPhpExtensionsRequirement(): array
    {
        $extensions = [
            ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')],
            ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')],
            ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')],
            ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')],
            ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')],
            ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')],
            ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')],
            ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')],
        ];

        foreach ($extensions as &$extension) {
            $extension['loaded'] = extension_loaded($extension['name']);
        }

        return $extensions;
    }

    /**
     * Return the EOL date of given PHP version. If the version is unknown,
     * we return today + 2 years.
     *
     * @param string $fullVersion PHP version, e.g. 7.4.7
     *
     * @return string Date format: YYYY-MM-DD
     */
    public static function getPhpEol(string $fullVersion): string
    {
        preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches);

        return [
            '7.1' => '2019-12-01',
            '7.2' => '2020-11-30',
            '7.3' => '2021-12-06',
            '7.4' => '2022-11-28',
            '8.0' => '2023-12-01',
        ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d');
    }
}