From c2cd15dac2bfaebe6d32f7649fbdedc07400fa08 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 16 Oct 2020 13:34:59 +0200 Subject: Move utils classes to Shaarli\Helper namespace and folder --- application/ApplicationUtils.php | 314 ---------------- application/FileUtils.php | 140 ------- application/History.php | 1 + .../front/controller/visitor/InstallController.php | 2 +- application/helper/ApplicationUtils.php | 314 ++++++++++++++++ application/helper/FileUtils.php | 140 +++++++ application/legacy/LegacyLinkDB.php | 2 +- application/legacy/LegacyUpdater.php | 2 +- application/render/PageBuilder.php | 2 +- application/security/BanManager.php | 2 +- composer.json | 1 + init.php | 2 +- tests/ApplicationUtilsTest.php | 414 -------------------- tests/FileUtilsTest.php | 196 ---------- tests/helper/ApplicationUtilsTest.php | 415 +++++++++++++++++++++ tests/helper/FileUtilsTest.php | 196 ++++++++++ tests/security/BanManagerTest.php | 2 +- tests/utils/FakeApplicationUtils.php | 2 + tests/utils/ReferenceHistory.php | 2 +- 19 files changed, 1077 insertions(+), 1072 deletions(-) delete mode 100644 application/ApplicationUtils.php delete mode 100644 application/FileUtils.php create mode 100644 application/helper/ApplicationUtils.php create mode 100644 application/helper/FileUtils.php delete mode 100644 tests/ApplicationUtilsTest.php delete mode 100644 tests/FileUtilsTest.php create mode 100644 tests/helper/ApplicationUtilsTest.php create mode 100644 tests/helper/FileUtilsTest.php diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php deleted file mode 100644 index bd1c7cf3..00000000 --- a/application/ApplicationUtils.php +++ /dev/null @@ -1,314 +0,0 @@ -'; - - /** - * 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( - array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), - array('', '', ''), - $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 (array( - $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'); - } -} diff --git a/application/FileUtils.php b/application/FileUtils.php deleted file mode 100644 index 3f940751..00000000 --- a/application/FileUtils.php +++ /dev/null @@ -1,140 +0,0 @@ -'; - - /** - * Write data into a file (Shaarli database format). - * The data is stored in a PHP file, as a comment, in compressed base64 format. - * - * The file will be created if it doesn't exist. - * - * @param string $file File path. - * @param mixed $content Content to write. - * - * @return int|bool Number of bytes written or false if it fails. - * - * @throws IOException The destination file can't be written. - */ - public static function writeFlatDB($file, $content) - { - if (is_file($file) && !is_writeable($file)) { - // The datastore exists but is not writeable - throw new IOException($file); - } elseif (!is_file($file) && !is_writeable(dirname($file))) { - // The datastore does not exist and its parent directory is not writeable - throw new IOException(dirname($file)); - } - - return file_put_contents( - $file, - self::$phpPrefix . base64_encode(gzdeflate(serialize($content))) . self::$phpSuffix - ); - } - - /** - * Read data from a file containing Shaarli database format content. - * - * If the file isn't readable or doesn't exist, default data will be returned. - * - * @param string $file File path. - * @param mixed $default The default value to return if the file isn't readable. - * - * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails. - */ - public static function readFlatDB($file, $default = null) - { - // Note that gzinflate is faster than gzuncompress. - // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 - if (!is_readable($file)) { - return $default; - } - - $data = file_get_contents($file); - if ($data == '') { - return $default; - } - - return unserialize( - gzinflate( - base64_decode( - substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) - ) - ) - ); - } - - /** - * Recursively deletes a folder content, and deletes itself optionally. - * If an excluded file is found, folders won't be deleted. - * - * Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory. - * - * @param string $path - * @param bool $selfDelete Delete the provided folder if true, only its content if false. - * @param array $exclude - */ - public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool - { - $skipped = false; - - if (!is_dir($path)) { - throw new IOException(t('Provided path is not a directory.')); - } - - if (!static::isPathInShaarliFolder($path)) { - throw new IOException(t('Trying to delete a folder outside of Shaarli path.')); - } - - foreach (new \DirectoryIterator($path) as $file) { - if($file->isDot()) { - continue; - } - - if (in_array($file->getBasename(), $exclude, true)) { - $skipped = true; - continue; - } - - if ($file->isFile()) { - unlink($file->getPathname()); - } elseif($file->isDir()) { - $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped; - } - } - - if ($selfDelete && !$skipped) { - rmdir($path); - } - - return $skipped; - } - - /** - * Checks that the given path is inside Shaarli directory. - */ - public static function isPathInShaarliFolder(string $path): bool - { - $rootDirectory = dirname(dirname(__FILE__)); - - return strpos(realpath($path), $rootDirectory) !== false; - } -} diff --git a/application/History.php b/application/History.php index 4fd2f294..bd5c1bf7 100644 --- a/application/History.php +++ b/application/History.php @@ -4,6 +4,7 @@ namespace Shaarli; use DateTime; use Exception; use Shaarli\Bookmark\Bookmark; +use Shaarli\Helper\FileUtils; /** * Class History diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 564a5777..22329294 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; -use Shaarli\ApplicationUtils; use Shaarli\Container\ShaarliContainer; use Shaarli\Front\Exception\AlreadyInstalledException; use Shaarli\Front\Exception\ResourcePermissionException; +use Shaarli\Helper\ApplicationUtils; use Shaarli\Languages; use Shaarli\Security\SessionManager; use Slim\Http\Request; 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 @@ +'; + + /** + * 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( + array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), + array('', '', ''), + $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 (array( + $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'); + } +} 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 @@ +'; + + /** + * Write data into a file (Shaarli database format). + * The data is stored in a PHP file, as a comment, in compressed base64 format. + * + * The file will be created if it doesn't exist. + * + * @param string $file File path. + * @param mixed $content Content to write. + * + * @return int|bool Number of bytes written or false if it fails. + * + * @throws IOException The destination file can't be written. + */ + public static function writeFlatDB($file, $content) + { + if (is_file($file) && !is_writeable($file)) { + // The datastore exists but is not writeable + throw new IOException($file); + } elseif (!is_file($file) && !is_writeable(dirname($file))) { + // The datastore does not exist and its parent directory is not writeable + throw new IOException(dirname($file)); + } + + return file_put_contents( + $file, + self::$phpPrefix . base64_encode(gzdeflate(serialize($content))) . self::$phpSuffix + ); + } + + /** + * Read data from a file containing Shaarli database format content. + * + * If the file isn't readable or doesn't exist, default data will be returned. + * + * @param string $file File path. + * @param mixed $default The default value to return if the file isn't readable. + * + * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails. + */ + public static function readFlatDB($file, $default = null) + { + // Note that gzinflate is faster than gzuncompress. + // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 + if (!is_readable($file)) { + return $default; + } + + $data = file_get_contents($file); + if ($data == '') { + return $default; + } + + return unserialize( + gzinflate( + base64_decode( + substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) + ) + ) + ); + } + + /** + * Recursively deletes a folder content, and deletes itself optionally. + * If an excluded file is found, folders won't be deleted. + * + * Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory. + * + * @param string $path + * @param bool $selfDelete Delete the provided folder if true, only its content if false. + * @param array $exclude + */ + public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool + { + $skipped = false; + + if (!is_dir($path)) { + throw new IOException(t('Provided path is not a directory.')); + } + + if (!static::isPathInShaarliFolder($path)) { + throw new IOException(t('Trying to delete a folder outside of Shaarli path.')); + } + + foreach (new \DirectoryIterator($path) as $file) { + if($file->isDot()) { + continue; + } + + if (in_array($file->getBasename(), $exclude, true)) { + $skipped = true; + continue; + } + + if ($file->isFile()) { + unlink($file->getPathname()); + } elseif($file->isDir()) { + $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped; + } + } + + if ($selfDelete && !$skipped) { + rmdir($path); + } + + return $skipped; + } + + /** + * Checks that the given path is inside Shaarli directory. + */ + public static function isPathInShaarliFolder(string $path): bool + { + $rootDirectory = dirname(dirname(__FILE__)); + + return strpos(realpath($path), $rootDirectory) !== false; + } +} diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php index 7bf76fd4..5c02a21b 100644 --- a/application/legacy/LegacyLinkDB.php +++ b/application/legacy/LegacyLinkDB.php @@ -8,7 +8,7 @@ use DateTime; use Iterator; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Exceptions\IOException; -use Shaarli\FileUtils; +use Shaarli\Helper\FileUtils; use Shaarli\Render\PageCacheManager; /** diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php index 0ab3a55b..fe1a286f 100644 --- a/application/legacy/LegacyUpdater.php +++ b/application/legacy/LegacyUpdater.php @@ -7,7 +7,6 @@ use RainTPL; use ReflectionClass; use ReflectionException; use ReflectionMethod; -use Shaarli\ApplicationUtils; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\BookmarkArray; use Shaarli\Bookmark\BookmarkFilter; @@ -17,6 +16,7 @@ use Shaarli\Config\ConfigJson; use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigPhp; use Shaarli\Exceptions\IOException; +use Shaarli\Helper\ApplicationUtils; use Shaarli\Thumbnailer; use Shaarli\Updater\Exception\UpdaterException; diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 512bb79e..25e0e284 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -5,9 +5,9 @@ namespace Shaarli\Render; use Exception; use Psr\Log\LoggerInterface; use RainTPL; -use Shaarli\ApplicationUtils; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; +use Shaarli\Helper\ApplicationUtils; use Shaarli\Security\SessionManager; use Shaarli\Thumbnailer; diff --git a/application/security/BanManager.php b/application/security/BanManager.php index f72c8b7b..288cbde0 100644 --- a/application/security/BanManager.php +++ b/application/security/BanManager.php @@ -4,7 +4,7 @@ namespace Shaarli\Security; use Psr\Log\LoggerInterface; -use Shaarli\FileUtils; +use Shaarli\Helper\FileUtils; /** * Class BanManager diff --git a/composer.json b/composer.json index 64f0025e..94492586 100644 --- a/composer.json +++ b/composer.json @@ -59,6 +59,7 @@ "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin", "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor", "Shaarli\\Front\\Exception\\": "application/front/exceptions", + "Shaarli\\Helper\\": "application/helper", "Shaarli\\Http\\": "application/http", "Shaarli\\Legacy\\": "application/legacy", "Shaarli\\Netscape\\": "application/netscape", diff --git a/init.php b/init.php index ab0e4ea7..d8462712 100644 --- a/init.php +++ b/init.php @@ -2,7 +2,7 @@ require_once __DIR__ . '/vendor/autoload.php'; -use Shaarli\ApplicationUtils; +use Shaarli\Helper\ApplicationUtils; use Shaarli\Security\SessionManager; // Set 'UTC' as the default timezone if it is not defined in php.ini diff --git a/tests/ApplicationUtilsTest.php b/tests/ApplicationUtilsTest.php deleted file mode 100644 index ac46cbf1..00000000 --- a/tests/ApplicationUtilsTest.php +++ /dev/null @@ -1,414 +0,0 @@ -assertEquals( - '0.5.4', - ApplicationUtils::getVersion( - 'https://raw.githubusercontent.com/shaarli/Shaarli/' - .'v0.5.4/shaarli_version.php', - $testTimeout - ) - ); - $this->assertRegExp( - self::$versionPattern, - ApplicationUtils::getVersion( - 'https://raw.githubusercontent.com/shaarli/Shaarli/' - .'latest/shaarli_version.php', - $testTimeout - ) - ); - } - - /** - * Attempt to retrieve the latest version from an invalid File - */ - public function testGetVersionCodeFromFile() - { - file_put_contents('sandbox/version.php', ''. PHP_EOL); - $this->assertEquals( - '1.2.3', - ApplicationUtils::getVersion('sandbox/version.php', 1) - ); - } - - /** - * Attempt to retrieve the latest version from an invalid File - */ - public function testGetVersionCodeInvalidFile() - { - $oldlog = ini_get('error_log'); - ini_set('error_log', '/dev/null'); - $this->assertFalse( - ApplicationUtils::getVersion('idontexist', 1) - ); - ini_set('error_log', $oldlog); - } - - /** - * Test update checks - the user is logged off - */ - public function testCheckUpdateLoggedOff() - { - $this->assertFalse( - ApplicationUtils::checkUpdate(self::$testVersion, 'null', 0, false, false) - ); - } - - /** - * Test update checks - the user has disabled updates - */ - public function testCheckUpdateUserDisabled() - { - $this->assertFalse( - ApplicationUtils::checkUpdate(self::$testVersion, 'null', 0, false, true) - ); - } - - /** - * A newer version is available - */ - public function testCheckUpdateNewVersionAvailable() - { - $newVersion = '1.8.3'; - FakeApplicationUtils::$VERSION_CODE = $newVersion; - - $version = FakeApplicationUtils::checkUpdate( - self::$testVersion, - self::$testUpdateFile, - 100, - true, - true - ); - - $this->assertEquals($newVersion, $version); - } - - /** - * No available information about versions - */ - public function testCheckUpdateNewVersionUnavailable() - { - $version = FakeApplicationUtils::checkUpdate( - self::$testVersion, - self::$testUpdateFile, - 100, - true, - true - ); - - $this->assertFalse($version); - } - - /** - * Test update checks - invalid Git branch - */ - public function testCheckUpdateInvalidGitBranch() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessageRegExp('/Invalid branch selected for updates/'); - - ApplicationUtils::checkUpdate('', 'null', 0, true, true, 'unstable'); - } - - /** - * Shaarli is up-to-date - */ - public function testCheckUpdateNewVersionUpToDate() - { - FakeApplicationUtils::$VERSION_CODE = self::$testVersion; - - $version = FakeApplicationUtils::checkUpdate( - self::$testVersion, - self::$testUpdateFile, - 100, - true, - true - ); - - $this->assertFalse($version); - } - - /** - * Time-traveller's Shaarli - */ - public function testCheckUpdateNewVersionMaartiMcFly() - { - FakeApplicationUtils::$VERSION_CODE = '0.4.1'; - - $version = FakeApplicationUtils::checkUpdate( - self::$testVersion, - self::$testUpdateFile, - 100, - true, - true - ); - - $this->assertFalse($version); - } - - /** - * The version has been checked recently and Shaarli is up-to-date - */ - public function testCheckUpdateNewVersionTwiceUpToDate() - { - FakeApplicationUtils::$VERSION_CODE = self::$testVersion; - - // Create the update file - $version = FakeApplicationUtils::checkUpdate( - self::$testVersion, - self::$testUpdateFile, - 100, - true, - true - ); - - $this->assertFalse($version); - - // Reuse the update file - $version = FakeApplicationUtils::checkUpdate( - self::$testVersion, - self::$testUpdateFile, - 100, - true, - true - ); - - $this->assertFalse($version); - } - - /** - * The version has been checked recently and Shaarli is outdated - */ - public function testCheckUpdateNewVersionTwiceOutdated() - { - $newVersion = '1.8.3'; - FakeApplicationUtils::$VERSION_CODE = $newVersion; - - // Create the update file - $version = FakeApplicationUtils::checkUpdate( - self::$testVersion, - self::$testUpdateFile, - 100, - true, - true - ); - $this->assertEquals($newVersion, $version); - - // Reuse the update file - $version = FakeApplicationUtils::checkUpdate( - self::$testVersion, - self::$testUpdateFile, - 100, - true, - true - ); - $this->assertEquals($newVersion, $version); - } - - /** - * Check supported PHP versions - */ - public function testCheckSupportedPHPVersion() - { - $minVersion = '5.3'; - $this->assertTrue(ApplicationUtils::checkPHPVersion($minVersion, '5.4.32')); - $this->assertTrue(ApplicationUtils::checkPHPVersion($minVersion, '5.5')); - $this->assertTrue(ApplicationUtils::checkPHPVersion($minVersion, '5.6.10')); - } - - /** - * Check a unsupported PHP version - */ - public function testCheckSupportedPHPVersion51() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessageRegExp('/Your PHP version is obsolete/'); - - $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.1.0')); - } - - /** - * Check another unsupported PHP version - */ - public function testCheckSupportedPHPVersion52() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessageRegExp('/Your PHP version is obsolete/'); - - $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.2')); - } - - /** - * Checks resource permissions for the current Shaarli installation - */ - public function testCheckCurrentResourcePermissions() - { - $conf = new ConfigManager(''); - $conf->set('resource.thumbnails_cache', 'cache'); - $conf->set('resource.config', 'data/config.php'); - $conf->set('resource.data_dir', 'data'); - $conf->set('resource.datastore', 'data/datastore.php'); - $conf->set('resource.ban_file', 'data/ipbans.php'); - $conf->set('resource.log', 'data/log.txt'); - $conf->set('resource.page_cache', 'pagecache'); - $conf->set('resource.raintpl_tmp', 'tmp'); - $conf->set('resource.raintpl_tpl', 'tpl'); - $conf->set('resource.theme', 'default'); - $conf->set('resource.update_check', 'data/lastupdatecheck.txt'); - - $this->assertEquals( - array(), - ApplicationUtils::checkResourcePermissions($conf) - ); - } - - /** - * Checks resource permissions for a non-existent Shaarli installation - */ - public function testCheckCurrentResourcePermissionsErrors() - { - $conf = new ConfigManager(''); - $conf->set('resource.thumbnails_cache', 'null/cache'); - $conf->set('resource.config', 'null/data/config.php'); - $conf->set('resource.data_dir', 'null/data'); - $conf->set('resource.datastore', 'null/data/store.php'); - $conf->set('resource.ban_file', 'null/data/ipbans.php'); - $conf->set('resource.log', 'null/data/log.txt'); - $conf->set('resource.page_cache', 'null/pagecache'); - $conf->set('resource.raintpl_tmp', 'null/tmp'); - $conf->set('resource.raintpl_tpl', 'null/tpl'); - $conf->set('resource.raintpl_theme', 'null/tpl/default'); - $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt'); - $this->assertEquals( - array( - '"null/tpl" directory is not readable', - '"null/tpl/default" directory is not readable', - '"null/cache" directory is not readable', - '"null/cache" directory is not writable', - '"null/data" directory is not readable', - '"null/data" directory is not writable', - '"null/pagecache" directory is not readable', - '"null/pagecache" directory is not writable', - '"null/tmp" directory is not readable', - '"null/tmp" directory is not writable' - ), - ApplicationUtils::checkResourcePermissions($conf) - ); - } - - /** - * Checks resource permissions in minimal mode. - */ - public function testCheckCurrentResourcePermissionsErrorsMinimalMode(): void - { - $conf = new ConfigManager(''); - $conf->set('resource.thumbnails_cache', 'null/cache'); - $conf->set('resource.config', 'null/data/config.php'); - $conf->set('resource.data_dir', 'null/data'); - $conf->set('resource.datastore', 'null/data/store.php'); - $conf->set('resource.ban_file', 'null/data/ipbans.php'); - $conf->set('resource.log', 'null/data/log.txt'); - $conf->set('resource.page_cache', 'null/pagecache'); - $conf->set('resource.raintpl_tmp', 'null/tmp'); - $conf->set('resource.raintpl_tpl', 'null/tpl'); - $conf->set('resource.raintpl_theme', 'null/tpl/default'); - $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt'); - - static::assertSame( - [ - '"null/tpl" directory is not readable', - '"null/tpl/default" directory is not readable', - '"null/tmp" directory is not readable', - '"null/tmp" directory is not writable' - ], - ApplicationUtils::checkResourcePermissions($conf, true) - ); - } - - /** - * Check update with 'dev' as curent version (master branch). - * It should always return false. - */ - public function testCheckUpdateDev() - { - $this->assertFalse( - ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true) - ); - } - - /** - * Basic test of getPhpExtensionsRequirement() - */ - public function testGetPhpExtensionsRequirementSimple(): void - { - static::assertCount(8, ApplicationUtils::getPhpExtensionsRequirement()); - static::assertSame([ - 'name' => 'json', - 'required' => true, - 'desc' => 'Configuration parsing', - 'loaded' => true, - ], ApplicationUtils::getPhpExtensionsRequirement()[0]); - } - - /** - * Test getPhpEol with a known version: 7.4 -> 2022 - */ - public function testGetKnownPhpEol(): void - { - static::assertSame('2022-11-28', ApplicationUtils::getPhpEol('7.4.7')); - } - - /** - * Test getPhpEol with an unknown version: 7.4 -> 2022 - */ - public function testGetUnknownPhpEol(): void - { - static::assertSame( - (((int) (new \DateTime())->format('Y')) + 2) . (new \DateTime())->format('-m-d'), - ApplicationUtils::getPhpEol('7.51.34') - ); - } -} diff --git a/tests/FileUtilsTest.php b/tests/FileUtilsTest.php deleted file mode 100644 index 3384504a..00000000 --- a/tests/FileUtilsTest.php +++ /dev/null @@ -1,196 +0,0 @@ -assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); - $this->assertTrue(startsWith(file_get_contents(self::$file), 'assertEquals($data, FileUtils::readFlatDB(self::$file)); - - $data = 0; - $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); - $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); - - $data = null; - $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); - $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); - - $data = false; - $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); - $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); - } - - /** - * File not writable: raise an exception. - */ - public function testWriteWithoutPermission() - { - $this->expectException(\Shaarli\Exceptions\IOException::class); - $this->expectExceptionMessage('Error accessing "sandbox/flat.db"'); - - touch(self::$file); - chmod(self::$file, 0440); - FileUtils::writeFlatDB(self::$file, null); - } - - /** - * Folder non existent: raise an exception. - */ - public function testWriteFolderDoesNotExist() - { - $this->expectException(\Shaarli\Exceptions\IOException::class); - $this->expectExceptionMessage('Error accessing "nopefolder"'); - - FileUtils::writeFlatDB('nopefolder/file', null); - } - - /** - * Folder non writable: raise an exception. - */ - public function testWriteFolderPermission() - { - $this->expectException(\Shaarli\Exceptions\IOException::class); - $this->expectExceptionMessage('Error accessing "sandbox"'); - - chmod(dirname(self::$file), 0555); - try { - FileUtils::writeFlatDB(self::$file, null); - } catch (Exception $e) { - chmod(dirname(self::$file), 0755); - throw $e; - } - } - - /** - * Read non existent file, use default parameter. - */ - public function testReadNotExistentFile() - { - $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); - $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); - } - - /** - * Read non readable file, use default parameter. - */ - public function testReadNotReadable() - { - touch(self::$file); - chmod(self::$file, 0220); - $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); - $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); - } - - /** - * Test clearFolder with self delete and excluded files - */ - public function testClearFolderSelfDeleteWithExclusion(): void - { - FileUtils::clearFolder('sandbox', true, ['file2']); - - static::assertFileExists('sandbox/folder1/file2'); - static::assertFileExists('sandbox/folder1'); - static::assertFileExists('sandbox/file2'); - static::assertFileExists('sandbox'); - - static::assertFileNotExists('sandbox/folder1/file1'); - static::assertFileNotExists('sandbox/file1'); - static::assertFileNotExists('sandbox/folder3'); - } - - /** - * Test clearFolder with self delete and excluded files - */ - public function testClearFolderSelfDeleteWithoutExclusion(): void - { - FileUtils::clearFolder('sandbox', true); - - static::assertFileNotExists('sandbox'); - } - - /** - * Test clearFolder with self delete and excluded files - */ - public function testClearFolderNoSelfDeleteWithoutExclusion(): void - { - FileUtils::clearFolder('sandbox', false); - - static::assertFileExists('sandbox'); - - // 2 because '.' and '..' - static::assertCount(2, new \DirectoryIterator('sandbox')); - } - - /** - * Test clearFolder on a file instead of a folder - */ - public function testClearFolderOnANonDirectory(): void - { - $this->expectException(IOException::class); - $this->expectExceptionMessage('Provided path is not a directory.'); - - FileUtils::clearFolder('sandbox/file1', false); - } - - /** - * Test clearFolder on a file instead of a folder - */ - public function testClearFolderOutsideOfShaarliDirectory(): void - { - $this->expectException(IOException::class); - $this->expectExceptionMessage('Trying to delete a folder outside of Shaarli path.'); - - - FileUtils::clearFolder('/tmp/shaarli-to-delete', true); - } -} diff --git a/tests/helper/ApplicationUtilsTest.php b/tests/helper/ApplicationUtilsTest.php new file mode 100644 index 00000000..654857b9 --- /dev/null +++ b/tests/helper/ApplicationUtilsTest.php @@ -0,0 +1,415 @@ +assertEquals( + '0.5.4', + ApplicationUtils::getVersion( + 'https://raw.githubusercontent.com/shaarli/Shaarli/' + .'v0.5.4/shaarli_version.php', + $testTimeout + ) + ); + $this->assertRegExp( + self::$versionPattern, + ApplicationUtils::getVersion( + 'https://raw.githubusercontent.com/shaarli/Shaarli/' + .'latest/shaarli_version.php', + $testTimeout + ) + ); + } + + /** + * Attempt to retrieve the latest version from an invalid File + */ + public function testGetVersionCodeFromFile() + { + file_put_contents('sandbox/version.php', ''. PHP_EOL); + $this->assertEquals( + '1.2.3', + ApplicationUtils::getVersion('sandbox/version.php', 1) + ); + } + + /** + * Attempt to retrieve the latest version from an invalid File + */ + public function testGetVersionCodeInvalidFile() + { + $oldlog = ini_get('error_log'); + ini_set('error_log', '/dev/null'); + $this->assertFalse( + ApplicationUtils::getVersion('idontexist', 1) + ); + ini_set('error_log', $oldlog); + } + + /** + * Test update checks - the user is logged off + */ + public function testCheckUpdateLoggedOff() + { + $this->assertFalse( + ApplicationUtils::checkUpdate(self::$testVersion, 'null', 0, false, false) + ); + } + + /** + * Test update checks - the user has disabled updates + */ + public function testCheckUpdateUserDisabled() + { + $this->assertFalse( + ApplicationUtils::checkUpdate(self::$testVersion, 'null', 0, false, true) + ); + } + + /** + * A newer version is available + */ + public function testCheckUpdateNewVersionAvailable() + { + $newVersion = '1.8.3'; + FakeApplicationUtils::$VERSION_CODE = $newVersion; + + $version = FakeApplicationUtils::checkUpdate( + self::$testVersion, + self::$testUpdateFile, + 100, + true, + true + ); + + $this->assertEquals($newVersion, $version); + } + + /** + * No available information about versions + */ + public function testCheckUpdateNewVersionUnavailable() + { + $version = FakeApplicationUtils::checkUpdate( + self::$testVersion, + self::$testUpdateFile, + 100, + true, + true + ); + + $this->assertFalse($version); + } + + /** + * Test update checks - invalid Git branch + */ + public function testCheckUpdateInvalidGitBranch() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessageRegExp('/Invalid branch selected for updates/'); + + ApplicationUtils::checkUpdate('', 'null', 0, true, true, 'unstable'); + } + + /** + * Shaarli is up-to-date + */ + public function testCheckUpdateNewVersionUpToDate() + { + FakeApplicationUtils::$VERSION_CODE = self::$testVersion; + + $version = FakeApplicationUtils::checkUpdate( + self::$testVersion, + self::$testUpdateFile, + 100, + true, + true + ); + + $this->assertFalse($version); + } + + /** + * Time-traveller's Shaarli + */ + public function testCheckUpdateNewVersionMaartiMcFly() + { + FakeApplicationUtils::$VERSION_CODE = '0.4.1'; + + $version = FakeApplicationUtils::checkUpdate( + self::$testVersion, + self::$testUpdateFile, + 100, + true, + true + ); + + $this->assertFalse($version); + } + + /** + * The version has been checked recently and Shaarli is up-to-date + */ + public function testCheckUpdateNewVersionTwiceUpToDate() + { + FakeApplicationUtils::$VERSION_CODE = self::$testVersion; + + // Create the update file + $version = FakeApplicationUtils::checkUpdate( + self::$testVersion, + self::$testUpdateFile, + 100, + true, + true + ); + + $this->assertFalse($version); + + // Reuse the update file + $version = FakeApplicationUtils::checkUpdate( + self::$testVersion, + self::$testUpdateFile, + 100, + true, + true + ); + + $this->assertFalse($version); + } + + /** + * The version has been checked recently and Shaarli is outdated + */ + public function testCheckUpdateNewVersionTwiceOutdated() + { + $newVersion = '1.8.3'; + FakeApplicationUtils::$VERSION_CODE = $newVersion; + + // Create the update file + $version = FakeApplicationUtils::checkUpdate( + self::$testVersion, + self::$testUpdateFile, + 100, + true, + true + ); + $this->assertEquals($newVersion, $version); + + // Reuse the update file + $version = FakeApplicationUtils::checkUpdate( + self::$testVersion, + self::$testUpdateFile, + 100, + true, + true + ); + $this->assertEquals($newVersion, $version); + } + + /** + * Check supported PHP versions + */ + public function testCheckSupportedPHPVersion() + { + $minVersion = '5.3'; + $this->assertTrue(ApplicationUtils::checkPHPVersion($minVersion, '5.4.32')); + $this->assertTrue(ApplicationUtils::checkPHPVersion($minVersion, '5.5')); + $this->assertTrue(ApplicationUtils::checkPHPVersion($minVersion, '5.6.10')); + } + + /** + * Check a unsupported PHP version + */ + public function testCheckSupportedPHPVersion51() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessageRegExp('/Your PHP version is obsolete/'); + + $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.1.0')); + } + + /** + * Check another unsupported PHP version + */ + public function testCheckSupportedPHPVersion52() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessageRegExp('/Your PHP version is obsolete/'); + + $this->assertTrue(ApplicationUtils::checkPHPVersion('5.3', '5.2')); + } + + /** + * Checks resource permissions for the current Shaarli installation + */ + public function testCheckCurrentResourcePermissions() + { + $conf = new ConfigManager(''); + $conf->set('resource.thumbnails_cache', 'cache'); + $conf->set('resource.config', 'data/config.php'); + $conf->set('resource.data_dir', 'data'); + $conf->set('resource.datastore', 'data/datastore.php'); + $conf->set('resource.ban_file', 'data/ipbans.php'); + $conf->set('resource.log', 'data/log.txt'); + $conf->set('resource.page_cache', 'pagecache'); + $conf->set('resource.raintpl_tmp', 'tmp'); + $conf->set('resource.raintpl_tpl', 'tpl'); + $conf->set('resource.theme', 'default'); + $conf->set('resource.update_check', 'data/lastupdatecheck.txt'); + + $this->assertEquals( + array(), + ApplicationUtils::checkResourcePermissions($conf) + ); + } + + /** + * Checks resource permissions for a non-existent Shaarli installation + */ + public function testCheckCurrentResourcePermissionsErrors() + { + $conf = new ConfigManager(''); + $conf->set('resource.thumbnails_cache', 'null/cache'); + $conf->set('resource.config', 'null/data/config.php'); + $conf->set('resource.data_dir', 'null/data'); + $conf->set('resource.datastore', 'null/data/store.php'); + $conf->set('resource.ban_file', 'null/data/ipbans.php'); + $conf->set('resource.log', 'null/data/log.txt'); + $conf->set('resource.page_cache', 'null/pagecache'); + $conf->set('resource.raintpl_tmp', 'null/tmp'); + $conf->set('resource.raintpl_tpl', 'null/tpl'); + $conf->set('resource.raintpl_theme', 'null/tpl/default'); + $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt'); + $this->assertEquals( + array( + '"null/tpl" directory is not readable', + '"null/tpl/default" directory is not readable', + '"null/cache" directory is not readable', + '"null/cache" directory is not writable', + '"null/data" directory is not readable', + '"null/data" directory is not writable', + '"null/pagecache" directory is not readable', + '"null/pagecache" directory is not writable', + '"null/tmp" directory is not readable', + '"null/tmp" directory is not writable' + ), + ApplicationUtils::checkResourcePermissions($conf) + ); + } + + /** + * Checks resource permissions in minimal mode. + */ + public function testCheckCurrentResourcePermissionsErrorsMinimalMode(): void + { + $conf = new ConfigManager(''); + $conf->set('resource.thumbnails_cache', 'null/cache'); + $conf->set('resource.config', 'null/data/config.php'); + $conf->set('resource.data_dir', 'null/data'); + $conf->set('resource.datastore', 'null/data/store.php'); + $conf->set('resource.ban_file', 'null/data/ipbans.php'); + $conf->set('resource.log', 'null/data/log.txt'); + $conf->set('resource.page_cache', 'null/pagecache'); + $conf->set('resource.raintpl_tmp', 'null/tmp'); + $conf->set('resource.raintpl_tpl', 'null/tpl'); + $conf->set('resource.raintpl_theme', 'null/tpl/default'); + $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt'); + + static::assertSame( + [ + '"null/tpl" directory is not readable', + '"null/tpl/default" directory is not readable', + '"null/tmp" directory is not readable', + '"null/tmp" directory is not writable' + ], + ApplicationUtils::checkResourcePermissions($conf, true) + ); + } + + /** + * Check update with 'dev' as curent version (master branch). + * It should always return false. + */ + public function testCheckUpdateDev() + { + $this->assertFalse( + ApplicationUtils::checkUpdate('dev', self::$testUpdateFile, 100, true, true) + ); + } + + /** + * Basic test of getPhpExtensionsRequirement() + */ + public function testGetPhpExtensionsRequirementSimple(): void + { + static::assertCount(8, ApplicationUtils::getPhpExtensionsRequirement()); + static::assertSame([ + 'name' => 'json', + 'required' => true, + 'desc' => 'Configuration parsing', + 'loaded' => true, + ], ApplicationUtils::getPhpExtensionsRequirement()[0]); + } + + /** + * Test getPhpEol with a known version: 7.4 -> 2022 + */ + public function testGetKnownPhpEol(): void + { + static::assertSame('2022-11-28', ApplicationUtils::getPhpEol('7.4.7')); + } + + /** + * Test getPhpEol with an unknown version: 7.4 -> 2022 + */ + public function testGetUnknownPhpEol(): void + { + static::assertSame( + (((int) (new \DateTime())->format('Y')) + 2) . (new \DateTime())->format('-m-d'), + ApplicationUtils::getPhpEol('7.51.34') + ); + } +} diff --git a/tests/helper/FileUtilsTest.php b/tests/helper/FileUtilsTest.php new file mode 100644 index 00000000..948e46d1 --- /dev/null +++ b/tests/helper/FileUtilsTest.php @@ -0,0 +1,196 @@ +assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); + $this->assertTrue(startsWith(file_get_contents(self::$file), 'assertEquals($data, FileUtils::readFlatDB(self::$file)); + + $data = 0; + $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); + $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); + + $data = null; + $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); + $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); + + $data = false; + $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); + $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); + } + + /** + * File not writable: raise an exception. + */ + public function testWriteWithoutPermission() + { + $this->expectException(\Shaarli\Exceptions\IOException::class); + $this->expectExceptionMessage('Error accessing "sandbox/flat.db"'); + + touch(self::$file); + chmod(self::$file, 0440); + FileUtils::writeFlatDB(self::$file, null); + } + + /** + * Folder non existent: raise an exception. + */ + public function testWriteFolderDoesNotExist() + { + $this->expectException(\Shaarli\Exceptions\IOException::class); + $this->expectExceptionMessage('Error accessing "nopefolder"'); + + FileUtils::writeFlatDB('nopefolder/file', null); + } + + /** + * Folder non writable: raise an exception. + */ + public function testWriteFolderPermission() + { + $this->expectException(\Shaarli\Exceptions\IOException::class); + $this->expectExceptionMessage('Error accessing "sandbox"'); + + chmod(dirname(self::$file), 0555); + try { + FileUtils::writeFlatDB(self::$file, null); + } catch (Exception $e) { + chmod(dirname(self::$file), 0755); + throw $e; + } + } + + /** + * Read non existent file, use default parameter. + */ + public function testReadNotExistentFile() + { + $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); + $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); + } + + /** + * Read non readable file, use default parameter. + */ + public function testReadNotReadable() + { + touch(self::$file); + chmod(self::$file, 0220); + $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); + $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); + } + + /** + * Test clearFolder with self delete and excluded files + */ + public function testClearFolderSelfDeleteWithExclusion(): void + { + FileUtils::clearFolder('sandbox', true, ['file2']); + + static::assertFileExists('sandbox/folder1/file2'); + static::assertFileExists('sandbox/folder1'); + static::assertFileExists('sandbox/file2'); + static::assertFileExists('sandbox'); + + static::assertFileNotExists('sandbox/folder1/file1'); + static::assertFileNotExists('sandbox/file1'); + static::assertFileNotExists('sandbox/folder3'); + } + + /** + * Test clearFolder with self delete and excluded files + */ + public function testClearFolderSelfDeleteWithoutExclusion(): void + { + FileUtils::clearFolder('sandbox', true); + + static::assertFileNotExists('sandbox'); + } + + /** + * Test clearFolder with self delete and excluded files + */ + public function testClearFolderNoSelfDeleteWithoutExclusion(): void + { + FileUtils::clearFolder('sandbox', false); + + static::assertFileExists('sandbox'); + + // 2 because '.' and '..' + static::assertCount(2, new \DirectoryIterator('sandbox')); + } + + /** + * Test clearFolder on a file instead of a folder + */ + public function testClearFolderOnANonDirectory(): void + { + $this->expectException(IOException::class); + $this->expectExceptionMessage('Provided path is not a directory.'); + + FileUtils::clearFolder('sandbox/file1', false); + } + + /** + * Test clearFolder on a file instead of a folder + */ + public function testClearFolderOutsideOfShaarliDirectory(): void + { + $this->expectException(IOException::class); + $this->expectExceptionMessage('Trying to delete a folder outside of Shaarli path.'); + + + FileUtils::clearFolder('/tmp/shaarli-to-delete', true); + } +} diff --git a/tests/security/BanManagerTest.php b/tests/security/BanManagerTest.php index 22aa8666..29d2791b 100644 --- a/tests/security/BanManagerTest.php +++ b/tests/security/BanManagerTest.php @@ -4,7 +4,7 @@ namespace Shaarli\Security; use Psr\Log\LoggerInterface; -use Shaarli\FileUtils; +use Shaarli\Helper\FileUtils; use Shaarli\TestCase; /** diff --git a/tests/utils/FakeApplicationUtils.php b/tests/utils/FakeApplicationUtils.php index de83d598..d5289ede 100644 --- a/tests/utils/FakeApplicationUtils.php +++ b/tests/utils/FakeApplicationUtils.php @@ -2,6 +2,8 @@ namespace Shaarli; +use Shaarli\Helper\ApplicationUtils; + /** * Fake ApplicationUtils class to avoid HTTP requests */ diff --git a/tests/utils/ReferenceHistory.php b/tests/utils/ReferenceHistory.php index 516c9f51..aed5d2cf 100644 --- a/tests/utils/ReferenceHistory.php +++ b/tests/utils/ReferenceHistory.php @@ -1,6 +1,6 @@