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 @@ Date: Fri, 16 Oct 2020 11:50:53 +0200 Subject: Feature: add weekly and monthly view/RSS feed for daily page - Heavy refactoring of DailyController - Add a banner like in tag cloud to display monthly and weekly links - Translations: t() now supports variables with optional first letter uppercase Fixes #160 --- application/Utils.php | 33 +- application/bookmark/BookmarkFileService.php | 38 +- application/bookmark/BookmarkServiceInterface.php | 27 +- .../front/controller/visitor/DailyController.php | 105 +++--- application/helper/DailyPageHelper.php | 208 +++++++++++ inc/languages/fr/LC_MESSAGES/shaarli.po | 256 +++++++------ tests/bookmark/BookmarkFileServiceTest.php | 124 +++++-- .../controller/visitor/DailyControllerTest.php | 412 ++++++++++++++++----- tests/helper/DailyPageHelperTest.php | 262 +++++++++++++ tpl/default/daily.html | 32 +- tpl/default/dailyrss.html | 11 +- 11 files changed, 1190 insertions(+), 318 deletions(-) create mode 100644 application/helper/DailyPageHelper.php create mode 100644 tests/helper/DailyPageHelperTest.php diff --git a/application/Utils.php b/application/Utils.php index bc1c9f5d..db046893 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -326,6 +326,23 @@ function format_date($date, $time = true, $intl = true) return $formatter->format($date); } +/** + * Format the date month according to the locale. + * + * @param DateTimeInterface $date to format. + * + * @return bool|string Formatted date, or false if the input is invalid. + */ +function format_month(DateTimeInterface $date) +{ + if (! $date instanceof DateTimeInterface) { + return false; + } + + return strftime('%B', $date->getTimestamp()); +} + + /** * Check if the input is an integer, no matter its real type. * @@ -454,16 +471,20 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) * Wrapper function for translation which match the API * of gettext()/_() and ngettext(). * - * @param string $text Text to translate. - * @param string $nText The plural message ID. - * @param int $nb The number of items for plural forms. - * @param string $domain The domain where the translation is stored (default: shaarli). + * @param string $text Text to translate. + * @param string $nText The plural message ID. + * @param int $nb The number of items for plural forms. + * @param string $domain The domain where the translation is stored (default: shaarli). + * @param array $variables Associative array of variables to replace in translated text. + * @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables. * * @return string Text translated. */ -function t($text, $nText = '', $nb = 1, $domain = 'shaarli') +function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false) { - return dn__($domain, $text, $nText, $nb); + $postFunction = $fixCase ? 'ucfirst' : function ($input) { return $input; }; + + return $postFunction(dn__($domain, $text, $nText, $nb, $variables)); } /** diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 14b3d620..0df2f47f 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -343,26 +343,42 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function days(): array - { - $bookmarkDays = []; - foreach ($this->search() as $bookmark) { - $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; + public function findByDate( + \DateTimeInterface $from, + \DateTimeInterface $to, + ?\DateTimeInterface &$previous, + ?\DateTimeInterface &$next + ): array { + $out = []; + $previous = null; + $next = null; + + foreach ($this->search([], null, false, false, true) as $bookmark) { + if ($to < $bookmark->getCreated()) { + $next = $bookmark->getCreated(); + } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { + $out[] = $bookmark; + } else { + if ($previous !== null) { + break; + } + $previous = $bookmark->getCreated(); + } } - $bookmarkDays = array_keys($bookmarkDays); - sort($bookmarkDays); - return array_map('strval', $bookmarkDays); + return $out; } /** * @inheritDoc */ - public function filterDay(string $request) + public function getLatest(): ?Bookmark { - $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; + foreach ($this->search([], null, false, false, true) as $bookmark) { + return $bookmark; + } - return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); + return null; } /** diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 9fa61533..08cdbb4e 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -156,22 +156,29 @@ interface BookmarkServiceInterface public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; /** - * Returns the list of days containing articles (oldest first) + * Return a list of bookmark matching provided period of time. + * It also update directly previous and next date outside of given period found in the datastore. * - * @return array containing days (in format YYYYMMDD). + * @param \DateTimeInterface $from Starting date. + * @param \DateTimeInterface $to Ending date. + * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from. + * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to. + * + * @return array List of bookmarks matching provided period of time. */ - public function days(): array; + public function findByDate( + \DateTimeInterface $from, + \DateTimeInterface $to, + ?\DateTimeInterface &$previous, + ?\DateTimeInterface &$next + ): array; /** - * Returns the list of articles for a given day. - * - * @param string $request day to filter. Format: YYYYMMDD. + * Returns the latest bookmark by creation date. * - * @return Bookmark[] list of shaare found. - * - * @throws BookmarkNotFoundException + * @return Bookmark|null Found Bookmark or null if the datastore is empty. */ - public function filterDay(string $request); + public function getLatest(): ?Bookmark; /** * Creates the default database after a fresh install. diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 07617cf1..728bc2d8 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; use DateTime; -use DateTimeImmutable; use Shaarli\Bookmark\Bookmark; +use Shaarli\Helper\DailyPageHelper; use Shaarli\Render\TemplatePage; use Slim\Http\Request; use Slim\Http\Response; @@ -26,32 +26,20 @@ class DailyController extends ShaarliVisitorController */ public function index(Request $request, Response $response): Response { - $day = $request->getQueryParam('day') ?? date('Ymd'); - - $availableDates = $this->container->bookmarkService->days(); - $nbAvailableDates = count($availableDates); - $index = array_search($day, $availableDates); - - if ($index === false) { - // no bookmarks for day, but at least one day with bookmarks - $day = $availableDates[$nbAvailableDates - 1] ?? $day; - $previousDay = $availableDates[$nbAvailableDates - 2] ?? ''; - } else { - $previousDay = $availableDates[$index - 1] ?? ''; - $nextDay = $availableDates[$index + 1] ?? ''; - } - - if ($day === date('Ymd')) { - $this->assignView('dayDesc', t('Today')); - } elseif ($day === date('Ymd', strtotime('-1 days'))) { - $this->assignView('dayDesc', t('Yesterday')); - } - - try { - $linksToDisplay = $this->container->bookmarkService->filterDay($day); - } catch (\Exception $exc) { - $linksToDisplay = []; - } + $type = DailyPageHelper::extractRequestedType($request); + $format = DailyPageHelper::getFormatByType($type); + $latestBookmark = $this->container->bookmarkService->getLatest(); + $dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark); + $start = DailyPageHelper::getStartDateTimeByType($type, $dateTime); + $end = DailyPageHelper::getEndDateTimeByType($type, $dateTime); + $dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime); + + $linksToDisplay = $this->container->bookmarkService->findByDate( + $start, + $end, + $previousDay, + $nextDay + ); $formatter = $this->container->formatterFactory->getFormatter(); $formatter->addContextData('base_path', $this->container->basePath); @@ -63,13 +51,15 @@ class DailyController extends ShaarliVisitorController $linksToDisplay[$key]['description'] = $bookmark->getDescription(); } - $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); $data = [ 'linksToDisplay' => $linksToDisplay, - 'day' => $dayDate->getTimestamp(), - 'dayDate' => $dayDate, - 'previousday' => $previousDay ?? '', - 'nextday' => $nextDay ?? '', + 'dayDate' => $start, + 'day' => $start->getTimestamp(), + 'previousday' => $previousDay ? $previousDay->format($format) : '', + 'nextday' => $nextDay ? $nextDay->format($format) : '', + 'dayDesc' => $dailyDesc, + 'type' => $type, + 'localizedType' => $this->translateType($type), ]; // Hooks are called before column construction so that plugins don't have to deal with columns. @@ -82,7 +72,7 @@ class DailyController extends ShaarliVisitorController $mainTitle = $this->container->conf->get('general.title', 'Shaarli'); $this->assignView( 'pagetitle', - t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle + $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle ); return $response->write($this->render(TemplatePage::DAILY)); @@ -106,11 +96,14 @@ class DailyController extends ShaarliVisitorController } $days = []; + $type = DailyPageHelper::extractRequestedType($request); + $format = DailyPageHelper::getFormatByType($type); + $length = DailyPageHelper::getRssLengthByType($type); foreach ($this->container->bookmarkService->search() as $bookmark) { - $day = $bookmark->getCreated()->format('Ymd'); + $day = $bookmark->getCreated()->format($format); // Stop iterating after DAILY_RSS_NB_DAYS entries - if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) { + if (count($days) === $length && !isset($days[$day])) { break; } @@ -127,12 +120,19 @@ class DailyController extends ShaarliVisitorController /** @var Bookmark[] $bookmarks */ foreach ($days as $day => $bookmarks) { - $dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000'); + $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day); + $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime); + + // We only want the RSS entry to be published when the period is over. + if (new DateTime() < $endDateTime) { + continue; + } + $dataPerDay[$day] = [ - 'date' => $dayDatetime, - 'date_rss' => $dayDatetime->format(DateTime::RSS), - 'date_human' => format_date($dayDatetime, false, true), - 'absolute_url' => $indexUrl . 'daily?day=' . $day, + 'date' => $endDateTime, + 'date_rss' => $endDateTime->format(DateTime::RSS), + 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime), + 'absolute_url' => $indexUrl . 'daily?'. $type .'=' . $day, 'links' => [], ]; @@ -141,16 +141,20 @@ class DailyController extends ShaarliVisitorController // Make permalink URL absolute if ($bookmark->isNote()) { - $dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl(); + $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl(); } } } - $this->assignView('title', $this->container->conf->get('general.title', 'Shaarli')); - $this->assignView('index_url', $indexUrl); - $this->assignView('page_url', $pageUrl); - $this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false)); - $this->assignView('days', $dataPerDay); + $this->assignAllView([ + 'title' => $this->container->conf->get('general.title', 'Shaarli'), + 'index_url' => $indexUrl, + 'page_url' => $pageUrl, + 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false), + 'days' => $dataPerDay, + 'type' => $type, + 'localizedType' => $this->translateType($type), + ]); $rssContent = $this->render(TemplatePage::DAILY_RSS); @@ -189,4 +193,13 @@ class DailyController extends ShaarliVisitorController return $columns; } + + protected function translateType($type): string + { + return [ + t('day') => t('Daily'), + t('week') => t('Weekly'), + t('month') => t('Monthly'), + ][t($type)] ?? t('Daily'); + } } diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php new file mode 100644 index 00000000..5fabc907 --- /dev/null +++ b/application/helper/DailyPageHelper.php @@ -0,0 +1,208 @@ +getQueryParam(static::MONTH) !== null) { + return static::MONTH; + } elseif ($request->getQueryParam(static::WEEK) !== null) { + return static::WEEK; + } + + return static::DAY; + } + + /** + * Extracts a DateTimeImmutable from provided HTTP request. + * If no parameter is provided, we rely on the creation date of the latest provided created bookmark. + * If the datastore is empty or no bookmark is provided, we use the current date. + * + * @param string $type month/week/day + * @param string|null $requestedDate Input string extracted from the request + * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date) + * + * @return \DateTimeImmutable from input or latest bookmark. + * + * @throws \Exception Type not supported. + */ + public static function extractRequestedDateTime( + string $type, + ?string $requestedDate, + Bookmark $latestBookmark = null + ): \DateTimeImmutable { + $format = static::getFormatByType($type); + if (empty($requestedDate)) { + return $latestBookmark instanceof Bookmark + ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM)) + : new \DateTimeImmutable() + ; + } + + // W is not supported by createFromFormat... + if ($type === static::WEEK) { + return (new \DateTimeImmutable()) + ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2)) + ; + } + + return \DateTimeImmutable::createFromFormat($format, $requestedDate); + } + + /** + * Get the DateTime format used by provided type + * Examples: + * - day: 20201016 () + * - week: 202041 () + * - month: 202010 () + * + * @param string $type month/week/day + * + * @return string DateTime compatible format + * + * @see https://www.php.net/manual/en/datetime.format.php + * + * @throws \Exception Type not supported. + */ + public static function getFormatByType(string $type): string + { + switch ($type) { + case static::MONTH: + return 'Ym'; + case static::WEEK: + return 'YW'; + case static::DAY: + return 'Ymd'; + default: + throw new \Exception('Unsupported daily format type'); + } + } + + /** + * Get the first DateTime of the time period depending on given datetime and type. + * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax + * and we don't want to alter original datetime. + * + * @param string $type month/week/day + * @param \DateTimeImmutable $requested DateTime extracted from request input + * (should come from extractRequestedDateTime) + * + * @return \DateTimeInterface First DateTime of the time period + * + * @throws \Exception Type not supported. + */ + public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface + { + switch ($type) { + case static::MONTH: + return $requested->modify('first day of this month midnight'); + case static::WEEK: + return $requested->modify('Monday this week midnight'); + case static::DAY: + return $requested->modify('Today midnight'); + default: + throw new \Exception('Unsupported daily format type'); + } + } + + /** + * Get the last DateTime of the time period depending on given datetime and type. + * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax + * and we don't want to alter original datetime. + * + * @param string $type month/week/day + * @param \DateTimeImmutable $requested DateTime extracted from request input + * (should come from extractRequestedDateTime) + * + * @return \DateTimeInterface Last DateTime of the time period + * + * @throws \Exception Type not supported. + */ + public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface + { + switch ($type) { + case static::MONTH: + return $requested->modify('last day of this month 23:59:59'); + case static::WEEK: + return $requested->modify('Sunday this week 23:59:59'); + case static::DAY: + return $requested->modify('Today 23:59:59'); + default: + throw new \Exception('Unsupported daily format type'); + } + } + + /** + * Get localized description of the time period depending on given datetime and type. + * Example: for a month period, it returns `October, 2020`. + * + * @param string $type month/week/day + * @param \DateTimeImmutable $requested DateTime extracted from request input + * (should come from extractRequestedDateTime) + * + * @return string Localized time period description + * + * @throws \Exception Type not supported. + */ + public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string + { + switch ($type) { + case static::MONTH: + return $requested->format('F') . ', ' . $requested->format('Y'); + case static::WEEK: + $requested = $requested->modify('Monday this week'); + return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')'; + case static::DAY: + $out = ''; + if ($requested->format('Ymd') === date('Ymd')) { + $out = t('Today') . ' - '; + } elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) { + $out = t('Yesterday') . ' - '; + } + return $out . format_date($requested, false); + default: + throw new \Exception('Unsupported daily format type'); + } + } + + /** + * Get the number of items to display in the RSS feed depending on the given type. + * + * @param string $type month/week/day + * + * @return int number of elements + * + * @throws \Exception Type not supported. + */ + public static function getRssLengthByType(string $type): int + { + switch ($type) { + case static::MONTH: + return 12; // 1 year + case static::WEEK: + return 26; // ~6 months + case static::DAY: + return 30; // ~1 month + default: + throw new \Exception('Unsupported daily format type'); + } + } +} diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index 3f14d22c..6d4ff0bd 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2020-10-27 19:32+0100\n" -"PO-Revision-Date: 2020-10-27 19:32+0100\n" +"POT-Creation-Date: 2020-10-27 19:44+0100\n" +"PO-Revision-Date: 2020-10-27 19:44+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" @@ -20,78 +20,11 @@ msgstr "" "X-Poedit-SearchPath-3: init.php\n" "X-Poedit-SearchPath-4: plugins\n" -#: application/ApplicationUtils.php:162 -#, php-format -msgid "" -"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." -msgstr "" -"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne " -"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " -"connues et devrait être mise à jour au plus tôt." - -#: application/ApplicationUtils.php:195 application/ApplicationUtils.php:215 -msgid "directory is not readable" -msgstr "le répertoire n'est pas accessible en lecture" - -#: application/ApplicationUtils.php:218 -msgid "directory is not writable" -msgstr "le répertoire n'est pas accessible en écriture" - -#: application/ApplicationUtils.php:240 -msgid "file is not readable" -msgstr "le fichier n'est pas accessible en lecture" - -#: application/ApplicationUtils.php:243 -msgid "file is not writable" -msgstr "le fichier n'est pas accessible en écriture" - -#: application/ApplicationUtils.php:277 -msgid "Configuration parsing" -msgstr "Chargement de la configuration" - -#: application/ApplicationUtils.php:278 -msgid "Slim Framework (routing, etc.)" -msgstr "Slim Framwork (routage, etc.)" - -#: application/ApplicationUtils.php:279 -msgid "Multibyte (Unicode) string support" -msgstr "Support des chaînes de caractère multibytes (Unicode)" - -#: application/ApplicationUtils.php:280 -msgid "Required to use thumbnails" -msgstr "Obligatoire pour utiliser les miniatures" - -#: application/ApplicationUtils.php:281 -msgid "Localized text sorting (e.g. e->è->f)" -msgstr "Tri des textes traduits (ex : e->è->f)" - -#: application/ApplicationUtils.php:282 -msgid "Better retrieval of bookmark metadata and thumbnail" -msgstr "Meilleure récupération des meta-données des marque-pages et minatures" - -#: application/ApplicationUtils.php:283 -msgid "Use the translation system in gettext mode" -msgstr "Utiliser le système de traduction en mode gettext" - -#: application/ApplicationUtils.php:284 -msgid "Login using LDAP server" -msgstr "Authentification via un serveur LDAP" - -#: application/FileUtils.php:100 -msgid "Provided path is not a directory." -msgstr "Le chemin fourni n'est pas un dossier." - -#: application/FileUtils.php:104 -msgid "Trying to delete a folder outside of Shaarli path." -msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli." - -#: application/History.php:179 +#: application/History.php:180 msgid "History file isn't readable or writable" msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" -#: application/History.php:190 +#: application/History.php:191 msgid "Could not parse history file" msgstr "Format incorrect pour le fichier d'historique" @@ -123,27 +56,27 @@ msgstr "" "l'extension php-gd doit être chargée pour utiliser les miniatures. Les " "miniatures sont désormais désactivées. Rechargez la page." -#: application/Utils.php:385 +#: application/Utils.php:402 msgid "Setting not set" msgstr "Paramètre non défini" -#: application/Utils.php:392 +#: application/Utils.php:409 msgid "Unlimited" msgstr "Illimité" -#: application/Utils.php:395 +#: application/Utils.php:412 msgid "B" msgstr "o" -#: application/Utils.php:395 +#: application/Utils.php:412 msgid "kiB" msgstr "ko" -#: application/Utils.php:395 +#: application/Utils.php:412 msgid "MiB" msgstr "Mo" -#: application/Utils.php:395 +#: application/Utils.php:412 msgid "GiB" msgstr "Go" @@ -156,7 +89,7 @@ msgstr "Vous n'êtes pas autorisé à modifier les données" #: application/bookmark/BookmarkFileService.php:208 msgid "This bookmarks already exists" -msgstr "Ce marque-page existe déjà." +msgstr "Ce marque-page existe déjà" #: application/bookmark/BookmarkInitializer.php:39 msgid "(private bookmark with thumbnail demo)" @@ -354,7 +287,8 @@ msgid "Direct link" msgstr "Liens directs" #: application/feed/FeedBuilder.php:181 -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 +#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 msgid "Permalink" msgstr "Permalien" @@ -537,20 +471,36 @@ msgstr "Outils" msgid "Search: " msgstr "Recherche : " -#: application/front/controller/visitor/DailyController.php:45 -msgid "Today" -msgstr "Aujourd'hui" - -#: application/front/controller/visitor/DailyController.php:47 -msgid "Yesterday" -msgstr "Hier" +#: application/front/controller/visitor/DailyController.php:200 +msgid "day" +msgstr "jour" -#: application/front/controller/visitor/DailyController.php:85 +#: application/front/controller/visitor/DailyController.php:200 +#: application/front/controller/visitor/DailyController.php:203 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 msgid "Daily" msgstr "Quotidien" +#: application/front/controller/visitor/DailyController.php:201 +msgid "week" +msgstr "semaine" + +#: application/front/controller/visitor/DailyController.php:201 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Weekly" +msgstr "Hebdomadaire" + +#: application/front/controller/visitor/DailyController.php:202 +msgid "month" +msgstr "mois" + +#: application/front/controller/visitor/DailyController.php:202 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "Monthly" +msgstr "Mensuel" + #: application/front/controller/visitor/ErrorController.php:33 msgid "An unexpected error occurred." msgstr "Une erreur inattendue s'est produite." @@ -616,7 +566,7 @@ msgstr "Mur d'images" #: application/front/controller/visitor/TagCloudController.php:88 msgid "Tag " -msgstr "Tag" +msgstr "Tag " #: application/front/exceptions/AlreadyInstalledException.php:11 msgid "Shaarli has already been installed. Login to edit the configuration." @@ -644,6 +594,86 @@ msgstr "" msgid "Wrong token." msgstr "Jeton invalide." +#: application/helper/ApplicationUtils.php:162 +#, php-format +msgid "" +"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." +msgstr "" +"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne " +"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " +"connues et devrait être mise à jour au plus tôt." + +#: application/helper/ApplicationUtils.php:195 +#: application/helper/ApplicationUtils.php:215 +msgid "directory is not readable" +msgstr "le répertoire n'est pas accessible en lecture" + +#: application/helper/ApplicationUtils.php:218 +msgid "directory is not writable" +msgstr "le répertoire n'est pas accessible en écriture" + +#: application/helper/ApplicationUtils.php:240 +msgid "file is not readable" +msgstr "le fichier n'est pas accessible en lecture" + +#: application/helper/ApplicationUtils.php:243 +msgid "file is not writable" +msgstr "le fichier n'est pas accessible en écriture" + +#: application/helper/ApplicationUtils.php:277 +msgid "Configuration parsing" +msgstr "Chargement de la configuration" + +#: application/helper/ApplicationUtils.php:278 +msgid "Slim Framework (routing, etc.)" +msgstr "Slim Framwork (routage, etc.)" + +#: application/helper/ApplicationUtils.php:279 +msgid "Multibyte (Unicode) string support" +msgstr "Support des chaînes de caractère multibytes (Unicode)" + +#: application/helper/ApplicationUtils.php:280 +msgid "Required to use thumbnails" +msgstr "Obligatoire pour utiliser les miniatures" + +#: application/helper/ApplicationUtils.php:281 +msgid "Localized text sorting (e.g. e->è->f)" +msgstr "Tri des textes traduits (ex : e->è->f)" + +#: application/helper/ApplicationUtils.php:282 +msgid "Better retrieval of bookmark metadata and thumbnail" +msgstr "Meilleure récupération des meta-données des marque-pages et minatures" + +#: application/helper/ApplicationUtils.php:283 +msgid "Use the translation system in gettext mode" +msgstr "Utiliser le système de traduction en mode gettext" + +#: application/helper/ApplicationUtils.php:284 +msgid "Login using LDAP server" +msgstr "Authentification via un serveur LDAP" + +#: application/helper/DailyPageHelper.php:172 +msgid "Week" +msgstr "Semaine" + +#: application/helper/DailyPageHelper.php:176 +msgid "Today" +msgstr "Aujourd'hui" + +#: application/helper/DailyPageHelper.php:178 +msgid "Yesterday" +msgstr "Hier" + +#: application/helper/FileUtils.php:100 +msgid "Provided path is not a directory." +msgstr "Le chemin fourni n'est pas un dossier." + +#: application/helper/FileUtils.php:104 +msgid "Trying to delete a folder outside of Shaarli path." +msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli." + #: application/legacy/LegacyLinkDB.php:131 msgid "You are not authorized to add a link." msgstr "Vous n'êtes pas autorisé à ajouter un lien." @@ -1103,25 +1133,30 @@ msgstr "Aucune" msgid "Save" msgstr "Enregistrer" -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -msgid "The Daily Shaarli" -msgstr "Le Quotidien Shaarli" - -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 -msgid "1 RSS entry per day" -msgstr "1 entrée RSS par jour" - -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 -msgid "Previous day" -msgstr "Jour précédent" - -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 -msgid "All links of one day in a single page." -msgstr "Tous les liens d'un jour sur une page." - -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 -msgid "Next day" -msgstr "Jour suivant" +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +msgid "1 RSS entry per :type" +msgid_plural "" +msgstr[0] "1 entrée RSS par :type" +msgstr[1] "" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +msgid "Previous :type" +msgid_plural "" +msgstr[0] ":type précédent" +msgstr[1] "Jour précédent" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 +msgid "All links of one :type in a single page." +msgid_plural "" +msgstr[0] "Tous les liens d'un :type sur une page." +msgstr[1] "Tous les liens d'un jour sur une page." + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +msgid "Next :type" +msgid_plural "" +msgstr[0] ":type suivant" +msgstr[1] "" #: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 msgid "Edit Shaare" @@ -1821,8 +1856,11 @@ msgstr "" "Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " "Ajouter aux favoris »" -#~ msgid "Rename" -#~ msgstr "Renommer" +#~ msgid "Display:" +#~ msgstr "Afficher :" + +#~ msgid "The Daily Shaarli" +#~ msgstr "Le Quotidien Shaarli" #, fuzzy #~| msgid "Selection" diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php index 47970117..8e0ff8dd 100644 --- a/tests/bookmark/BookmarkFileServiceTest.php +++ b/tests/bookmark/BookmarkFileServiceTest.php @@ -685,22 +685,6 @@ class BookmarkFileServiceTest extends TestCase $this->assertEquals(0, $linkDB->count()); } - /** - * List the days for which bookmarks have been posted - */ - public function testDays() - { - $this->assertSame( - ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'], - $this->publicLinkDB->days() - ); - - $this->assertSame( - ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'], - $this->privateLinkDB->days() - ); - } - /** * The URL corresponds to an existing entry in the DB */ @@ -1074,33 +1058,105 @@ class BookmarkFileServiceTest extends TestCase } /** - * Test filterDay while logged in + * Test find by dates in the middle of the datastore (sorted by dates) with a single bookmark as a result. */ - public function testFilterDayLoggedIn(): void + public function testFilterByDateMidTimePeriodSingleBookmark(): void { - $bookmarks = $this->privateLinkDB->filterDay('20121206'); - $expectedIds = [4, 9, 1, 0]; + $bookmarks = $this->privateLinkDB->findByDate( + DateTime::createFromFormat('Ymd_His', '20121206_150000'), + DateTime::createFromFormat('Ymd_His', '20121206_160000'), + $before, + $after + ); - static::assertCount(4, $bookmarks); - foreach ($bookmarks as $bookmark) { - $i = ($i ?? -1) + 1; - static::assertSame($expectedIds[$i], $bookmark->getId()); - } + static::assertCount(1, $bookmarks); + + static::assertSame(9, $bookmarks[0]->getId()); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_172539'), $after); } /** - * Test filterDay while logged out + * Test find by dates in the middle of the datastore (sorted by dates) with a multiple bookmarks as a result. */ - public function testFilterDayLoggedOut(): void + public function testFilterByDateMidTimePeriodMultipleBookmarks(): void { - $bookmarks = $this->publicLinkDB->filterDay('20121206'); - $expectedIds = [4, 9, 1]; + $bookmarks = $this->privateLinkDB->findByDate( + DateTime::createFromFormat('Ymd_His', '20121206_150000'), + DateTime::createFromFormat('Ymd_His', '20121206_180000'), + $before, + $after + ); - static::assertCount(3, $bookmarks); - foreach ($bookmarks as $bookmark) { - $i = ($i ?? -1) + 1; - static::assertSame($expectedIds[$i], $bookmark->getId()); - } + static::assertCount(2, $bookmarks); + + static::assertSame(1, $bookmarks[0]->getId()); + static::assertSame(9, $bookmarks[1]->getId()); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_142300'), $before); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20121206_182539'), $after); + } + + /** + * Test find by dates at the end of the datastore (sorted by dates). + */ + public function testFilterByDateLastTimePeriod(): void + { + $after = new DateTime(); + $bookmarks = $this->privateLinkDB->findByDate( + DateTime::createFromFormat('Ymd_His', '20150310_114640'), + DateTime::createFromFormat('Ymd_His', '20450101_010101'), + $before, + $after + ); + + static::assertCount(1, $bookmarks); + + static::assertSame(41, $bookmarks[0]->getId()); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20150310_114633'), $before); + static::assertNull($after); + } + + /** + * Test find by dates at the beginning of the datastore (sorted by dates). + */ + public function testFilterByDateFirstTimePeriod(): void + { + $before = new DateTime(); + $bookmarks = $this->privateLinkDB->findByDate( + DateTime::createFromFormat('Ymd_His', '20000101_101010'), + DateTime::createFromFormat('Ymd_His', '20100309_110000'), + $before, + $after + ); + + static::assertCount(1, $bookmarks); + + static::assertSame(11, $bookmarks[0]->getId()); + static::assertNull($before); + static::assertEquals(DateTime::createFromFormat('Ymd_His', '20100310_101010'), $after); + } + + /** + * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead. + */ + public function testGetLatestWithSticky(): void + { + $bookmark = $this->publicLinkDB->getLatest(); + + static::assertSame(41, $bookmark->getId()); + } + + /** + * Test getLatest with a sticky bookmark: it should be ignored and return the latest by creation date instead. + */ + public function testGetLatestEmptyDatastore(): void + { + unlink($this->conf->get('resource.datastore')); + $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); + + $bookmark = $this->publicLinkDB->getLatest(); + + static::assertNull($bookmark); } /** diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php index fc78bc13..758e7219 100644 --- a/tests/front/controller/visitor/DailyControllerTest.php +++ b/tests/front/controller/visitor/DailyControllerTest.php @@ -28,52 +28,49 @@ class DailyControllerTest extends TestCase public function testValidIndexControllerInvokeDefault(): void { $currentDay = new \DateTimeImmutable('2020-05-13'); + $previousDate = new \DateTime('2 days ago 00:00:00'); + $nextDate = new \DateTime('today 00:00:00'); $request = $this->createMock(Request::class); - $request->method('getQueryParam')->willReturn($currentDay->format('Ymd')); + $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { + return $key === 'day' ? $currentDay->format('Ymd') : null; + }); $response = new Response(); // Save RainTPL assigned variables $assignedVariables = []; $this->assignTemplateVars($assignedVariables); - // Links dataset: 2 links with thumbnails - $this->container->bookmarkService - ->expects(static::once()) - ->method('days') - ->willReturnCallback(function () use ($currentDay): array { - return [ - '20200510', - $currentDay->format('Ymd'), - '20200516', - ]; - }) - ; $this->container->bookmarkService ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { - return [ - (new Bookmark()) - ->setId(1) - ->setUrl('http://url.tld') - ->setTitle(static::generateString(50)) - ->setDescription(static::generateString(500)) - , - (new Bookmark()) - ->setId(2) - ->setUrl('http://url2.tld') - ->setTitle(static::generateString(50)) - ->setDescription(static::generateString(500)) - , - (new Bookmark()) - ->setId(3) - ->setUrl('http://url3.tld') - ->setTitle(static::generateString(50)) - ->setDescription(static::generateString(500)) - , - ]; - }) + ->method('findByDate') + ->willReturnCallback( + function ($from, $to, &$previous, &$next) use ($currentDay, $previousDate, $nextDate): array { + $previous = $previousDate; + $next = $nextDate; + + return [ + (new Bookmark()) + ->setId(1) + ->setUrl('http://url.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + (new Bookmark()) + ->setId(2) + ->setUrl('http://url2.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + (new Bookmark()) + ->setId(3) + ->setUrl('http://url3.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + ]; + } + ) ; // Make sure that PluginManager hook is triggered @@ -81,20 +78,22 @@ class DailyControllerTest extends TestCase ->expects(static::atLeastOnce()) ->method('executeHooks') ->withConsecutive(['render_daily']) - ->willReturnCallback(function (string $hook, array $data, array $param) use ($currentDay): array { - if ('render_daily' === $hook) { - static::assertArrayHasKey('linksToDisplay', $data); - static::assertCount(3, $data['linksToDisplay']); - static::assertSame(1, $data['linksToDisplay'][0]['id']); - static::assertSame($currentDay->getTimestamp(), $data['day']); - static::assertSame('20200510', $data['previousday']); - static::assertSame('20200516', $data['nextday']); - - static::assertArrayHasKey('loggedin', $param); + ->willReturnCallback( + function (string $hook, array $data, array $param) use ($currentDay, $previousDate, $nextDate): array { + if ('render_daily' === $hook) { + static::assertArrayHasKey('linksToDisplay', $data); + static::assertCount(3, $data['linksToDisplay']); + static::assertSame(1, $data['linksToDisplay'][0]['id']); + static::assertSame($currentDay->getTimestamp(), $data['day']); + static::assertSame($previousDate->format('Ymd'), $data['previousday']); + static::assertSame($nextDate->format('Ymd'), $data['nextday']); + + static::assertArrayHasKey('loggedin', $param); + } + + return $data; } - - return $data; - }) + ) ; $result = $this->controller->index($request, $response); @@ -107,6 +106,11 @@ class DailyControllerTest extends TestCase ); static::assertEquals($currentDay, $assignedVariables['dayDate']); static::assertEquals($currentDay->getTimestamp(), $assignedVariables['day']); + static::assertSame($previousDate->format('Ymd'), $assignedVariables['previousday']); + static::assertSame($nextDate->format('Ymd'), $assignedVariables['nextday']); + static::assertSame('day', $assignedVariables['type']); + static::assertSame('May 13, 2020', $assignedVariables['dayDesc']); + static::assertSame('Daily', $assignedVariables['localizedType']); static::assertCount(3, $assignedVariables['linksToDisplay']); $link = $assignedVariables['linksToDisplay'][0]; @@ -171,26 +175,19 @@ class DailyControllerTest extends TestCase $currentDay = new \DateTimeImmutable('2020-05-13'); $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { + return $key === 'day' ? $currentDay->format('Ymd') : null; + }); $response = new Response(); // Save RainTPL assigned variables $assignedVariables = []; $this->assignTemplateVars($assignedVariables); - // Links dataset: 2 links with thumbnails $this->container->bookmarkService ->expects(static::once()) - ->method('days') + ->method('findByDate') ->willReturnCallback(function () use ($currentDay): array { - return [ - $currentDay->format($currentDay->format('Ymd')), - ]; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { return [ (new Bookmark()) ->setId(1) @@ -250,20 +247,10 @@ class DailyControllerTest extends TestCase $assignedVariables = []; $this->assignTemplateVars($assignedVariables); - // Links dataset: 2 links with thumbnails $this->container->bookmarkService ->expects(static::once()) - ->method('days') + ->method('findByDate') ->willReturnCallback(function () use ($currentDay): array { - return [ - $currentDay->format($currentDay->format('Ymd')), - ]; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') - ->willReturnCallback(function (): array { return [ (new Bookmark())->setId(1)->setUrl('http://url.tld')->setTitle('title'), (new Bookmark()) @@ -320,14 +307,7 @@ class DailyControllerTest extends TestCase // Links dataset: 2 links with thumbnails $this->container->bookmarkService ->expects(static::once()) - ->method('days') - ->willReturnCallback(function (): array { - return []; - }) - ; - $this->container->bookmarkService - ->expects(static::once()) - ->method('filterDay') + ->method('findByDate') ->willReturnCallback(function (): array { return []; }) @@ -347,7 +327,7 @@ class DailyControllerTest extends TestCase static::assertSame(200, $result->getStatusCode()); static::assertSame('daily', (string) $result->getBody()); static::assertCount(0, $assignedVariables['linksToDisplay']); - static::assertSame('Today', $assignedVariables['dayDesc']); + static::assertSame('Today - ' . (new \DateTime())->format('F d, Y'), $assignedVariables['dayDesc']); static::assertEquals((new \DateTime())->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); static::assertEquals((new \DateTime())->setTime(0, 0), $assignedVariables['dayDate']); } @@ -361,6 +341,7 @@ class DailyControllerTest extends TestCase new \DateTimeImmutable('2020-05-17'), new \DateTimeImmutable('2020-05-15'), new \DateTimeImmutable('2020-05-13'), + new \DateTimeImmutable('+1 month'), ]; $request = $this->createMock(Request::class); @@ -371,6 +352,7 @@ class DailyControllerTest extends TestCase (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), + (new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'), ]); $this->container->pageCacheManager @@ -397,13 +379,14 @@ class DailyControllerTest extends TestCase static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']); static::assertFalse($assignedVariables['hide_timestamps']); - static::assertCount(2, $assignedVariables['days']); + static::assertCount(3, $assignedVariables['days']); $day = $assignedVariables['days'][$dates[0]->format('Ymd')]; + $date = $dates[0]->setTime(23, 59, 59); - static::assertEquals($dates[0], $day['date']); - static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']); - static::assertSame(format_date($dates[0], false), $day['date_human']); + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame(format_date($date, false), $day['date_human']); static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']); static::assertCount(1, $day['links']); static::assertSame(1, $day['links'][0]['id']); @@ -411,10 +394,11 @@ class DailyControllerTest extends TestCase static::assertEquals($dates[0], $day['links'][0]['created']); $day = $assignedVariables['days'][$dates[1]->format('Ymd')]; + $date = $dates[1]->setTime(23, 59, 59); - static::assertEquals($dates[1], $day['date']); - static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']); - static::assertSame(format_date($dates[1], false), $day['date_human']); + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame(format_date($date, false), $day['date_human']); static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']); static::assertCount(2, $day['links']); @@ -424,6 +408,18 @@ class DailyControllerTest extends TestCase static::assertSame(3, $day['links'][1]['id']); static::assertSame('http://domain.tld/3', $day['links'][1]['url']); static::assertEquals($dates[1], $day['links'][1]['created']); + + $day = $assignedVariables['days'][$dates[2]->format('Ymd')]; + $date = $dates[2]->setTime(23, 59, 59); + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame(format_date($date, false), $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?day='. $dates[2]->format('Ymd'), $day['absolute_url']); + static::assertCount(1, $day['links']); + static::assertSame(4, $day['links'][0]['id']); + static::assertSame('http://domain.tld/4', $day['links'][0]['url']); + static::assertEquals($dates[2], $day['links'][0]['created']); } /** @@ -475,4 +471,246 @@ class DailyControllerTest extends TestCase static::assertFalse($assignedVariables['hide_timestamps']); static::assertCount(0, $assignedVariables['days']); } + + /** + * Test simple display index with week parameter + */ + public function testSimpleIndexWeekly(): void + { + $currentDay = new \DateTimeImmutable('2020-05-13'); + $expectedDay = new \DateTimeImmutable('2020-05-11'); + + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { + return $key === 'week' ? $currentDay->format('YW') : null; + }); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('findByDate') + ->willReturnCallback( + function (): array { + return [ + (new Bookmark()) + ->setId(1) + ->setUrl('http://url.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + (new Bookmark()) + ->setId(2) + ->setUrl('http://url2.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + ]; + } + ) + ; + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('daily', (string) $result->getBody()); + static::assertSame( + 'Weekly - Week 20 (May 11, 2020) - Shaarli', + $assignedVariables['pagetitle'] + ); + + static::assertCount(2, $assignedVariables['linksToDisplay']); + static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']); + static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); + static::assertSame('', $assignedVariables['previousday']); + static::assertSame('', $assignedVariables['nextday']); + static::assertSame('Week 20 (May 11, 2020)', $assignedVariables['dayDesc']); + static::assertSame('week', $assignedVariables['type']); + static::assertSame('Weekly', $assignedVariables['localizedType']); + } + + /** + * Test simple display index with month parameter + */ + public function testSimpleIndexMonthly(): void + { + $currentDay = new \DateTimeImmutable('2020-05-13'); + $expectedDay = new \DateTimeImmutable('2020-05-01'); + + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key) use ($currentDay): ?string { + return $key === 'month' ? $currentDay->format('Ym') : null; + }); + $response = new Response(); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $this->container->bookmarkService + ->expects(static::once()) + ->method('findByDate') + ->willReturnCallback( + function (): array { + return [ + (new Bookmark()) + ->setId(1) + ->setUrl('http://url.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + (new Bookmark()) + ->setId(2) + ->setUrl('http://url2.tld') + ->setTitle(static::generateString(50)) + ->setDescription(static::generateString(500)) + , + ]; + } + ) + ; + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('daily', (string) $result->getBody()); + static::assertSame( + 'Monthly - May, 2020 - Shaarli', + $assignedVariables['pagetitle'] + ); + + static::assertCount(2, $assignedVariables['linksToDisplay']); + static::assertEquals($expectedDay->setTime(0, 0), $assignedVariables['dayDate']); + static::assertSame($expectedDay->setTime(0, 0)->getTimestamp(), $assignedVariables['day']); + static::assertSame('', $assignedVariables['previousday']); + static::assertSame('', $assignedVariables['nextday']); + static::assertSame('May, 2020', $assignedVariables['dayDesc']); + static::assertSame('month', $assignedVariables['type']); + static::assertSame('Monthly', $assignedVariables['localizedType']); + } + + /** + * Test simple display RSS with week parameter + */ + public function testSimpleRssWeekly(): void + { + $dates = [ + new \DateTimeImmutable('2020-05-19'), + new \DateTimeImmutable('2020-05-13'), + ]; + $expectedDates = [ + new \DateTimeImmutable('2020-05-24 23:59:59'), + new \DateTimeImmutable('2020-05-17 23:59:59'), + ]; + + $this->container->environment['QUERY_STRING'] = 'week'; + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string { + return $key === 'week' ? '' : null; + }); + $response = new Response(); + + $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ + (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), + (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), + (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), + ]); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->rss($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); + static::assertSame('dailyrss', (string) $result->getBody()); + static::assertSame('Shaarli', $assignedVariables['title']); + static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); + static::assertSame('http://shaarli/subfolder/daily-rss?week', $assignedVariables['page_url']); + static::assertFalse($assignedVariables['hide_timestamps']); + static::assertCount(2, $assignedVariables['days']); + + $day = $assignedVariables['days'][$dates[0]->format('YW')]; + $date = $expectedDates[0]; + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame('Week 21 (May 18, 2020)', $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?week='. $dates[0]->format('YW'), $day['absolute_url']); + static::assertCount(1, $day['links']); + + $day = $assignedVariables['days'][$dates[1]->format('YW')]; + $date = $expectedDates[1]; + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame('Week 20 (May 11, 2020)', $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?week='. $dates[1]->format('YW'), $day['absolute_url']); + static::assertCount(2, $day['links']); + } + + /** + * Test simple display RSS with month parameter + */ + public function testSimpleRssMonthly(): void + { + $dates = [ + new \DateTimeImmutable('2020-05-19'), + new \DateTimeImmutable('2020-04-13'), + ]; + $expectedDates = [ + new \DateTimeImmutable('2020-05-31 23:59:59'), + new \DateTimeImmutable('2020-04-30 23:59:59'), + ]; + + $this->container->environment['QUERY_STRING'] = 'month'; + $request = $this->createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function (string $key): ?string { + return $key === 'month' ? '' : null; + }); + $response = new Response(); + + $this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ + (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), + (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), + (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), + ]); + + // Save RainTPL assigned variables + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $result = $this->controller->rss($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]); + static::assertSame('dailyrss', (string) $result->getBody()); + static::assertSame('Shaarli', $assignedVariables['title']); + static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']); + static::assertSame('http://shaarli/subfolder/daily-rss?month', $assignedVariables['page_url']); + static::assertFalse($assignedVariables['hide_timestamps']); + static::assertCount(2, $assignedVariables['days']); + + $day = $assignedVariables['days'][$dates[0]->format('Ym')]; + $date = $expectedDates[0]; + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame('May, 2020', $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?month='. $dates[0]->format('Ym'), $day['absolute_url']); + static::assertCount(1, $day['links']); + + $day = $assignedVariables['days'][$dates[1]->format('Ym')]; + $date = $expectedDates[1]; + + static::assertEquals($date, $day['date']); + static::assertSame($date->format(\DateTime::RSS), $day['date_rss']); + static::assertSame('April, 2020', $day['date_human']); + static::assertSame('http://shaarli/subfolder/daily?month='. $dates[1]->format('Ym'), $day['absolute_url']); + static::assertCount(2, $day['links']); + } } diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php new file mode 100644 index 00000000..e0378491 --- /dev/null +++ b/tests/helper/DailyPageHelperTest.php @@ -0,0 +1,262 @@ +createMock(Request::class); + $request->method('getQueryParam')->willReturnCallback(function ($key) use ($queryParams): ?string { + return $queryParams[$key] ?? null; + }); + + $type = DailyPageHelper::extractRequestedType($request); + + static::assertSame($type, $expectedType); + } + + /** + * @dataProvider getRequestedDateTimes + */ + public function testExtractRequestedDateTime( + string $type, + string $input, + ?Bookmark $bookmark, + \DateTimeInterface $expectedDateTime, + string $compareFormat = 'Ymd' + ): void { + $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark); + + static::assertSame($dateTime->format($compareFormat), $expectedDateTime->format($compareFormat)); + } + + public function testExtractRequestedDateTimeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::extractRequestedDateTime('nope', null, null); + } + + /** + * @dataProvider getFormatsByType + */ + public function testGetFormatByType(string $type, string $expectedFormat): void + { + $format = DailyPageHelper::getFormatByType($type); + + static::assertSame($expectedFormat, $format); + } + + public function testGetFormatByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getFormatByType('nope'); + } + + /** + * @dataProvider getStartDatesByType + */ + public function testGetStartDatesByType( + string $type, + \DateTimeImmutable $dateTime, + \DateTimeInterface $expectedDateTime + ): void { + $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime); + + static::assertEquals($expectedDateTime, $startDateTime); + } + + public function testGetStartDatesByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable()); + } + + /** + * @dataProvider getEndDatesByType + */ + public function testGetEndDatesByType( + string $type, + \DateTimeImmutable $dateTime, + \DateTimeInterface $expectedDateTime + ): void { + $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime); + + static::assertEquals($expectedDateTime, $endDateTime); + } + + public function testGetEndDatesByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable()); + } + + /** + * @dataProvider getDescriptionsByType + */ + public function testGeDescriptionsByType( + string $type, + \DateTimeImmutable $dateTime, + string $expectedDescription + ): void { + $description = DailyPageHelper::getDescriptionByType($type, $dateTime); + + static::assertEquals($expectedDescription, $description); + } + + public function getDescriptionByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable()); + } + + /** + * @dataProvider getRssLengthsByType + */ + public function testGeRssLengthsByType(string $type): void { + $length = DailyPageHelper::getRssLengthByType($type); + + static::assertIsInt($length); + } + + public function testGeRssLengthsByTypeExceptionUnknownType(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Unsupported daily format type'); + + DailyPageHelper::getRssLengthByType('nope'); + } + + /** + * Data provider for testExtractRequestedType() test method. + */ + public function getRequestedTypes(): array + { + return [ + [['month' => null], DailyPageHelper::DAY], + [['month' => ''], DailyPageHelper::MONTH], + [['month' => 'content'], DailyPageHelper::MONTH], + [['week' => null], DailyPageHelper::DAY], + [['week' => ''], DailyPageHelper::WEEK], + [['week' => 'content'], DailyPageHelper::WEEK], + [['day' => null], DailyPageHelper::DAY], + [['day' => ''], DailyPageHelper::DAY], + [['day' => 'content'], DailyPageHelper::DAY], + ]; + } + + /** + * Data provider for testExtractRequestedDateTime() test method. + */ + public function getRequestedDateTimes(): array + { + return [ + [DailyPageHelper::DAY, '20201013', null, new \DateTime('2020-10-13')], + [ + DailyPageHelper::DAY, + '', + (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), + $date, + ], + [DailyPageHelper::DAY, '', null, new \DateTime()], + [DailyPageHelper::WEEK, '202030', null, new \DateTime('2020-07-20')], + [ + DailyPageHelper::WEEK, + '', + (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), + new \DateTime('2020-10-13'), + ], + [DailyPageHelper::WEEK, '', null, new \DateTime(), 'Ym'], + [DailyPageHelper::MONTH, '202008', null, new \DateTime('2020-08-01'), 'Ym'], + [ + DailyPageHelper::MONTH, + '', + (new Bookmark())->setCreated($date = new \DateTime('2020-10-13 12:05:31')), + new \DateTime('2020-10-13'), + 'Ym' + ], + [DailyPageHelper::MONTH, '', null, new \DateTime(), 'Ym'], + ]; + } + + /** + * Data provider for testGetFormatByType() test method. + */ + public function getFormatsByType(): array + { + return [ + [DailyPageHelper::DAY, 'Ymd'], + [DailyPageHelper::WEEK, 'YW'], + [DailyPageHelper::MONTH, 'Ym'], + ]; + } + + /** + * Data provider for testGetStartDatesByType() test method. + */ + public function getStartDatesByType(): array + { + return [ + [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')], + [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')], + [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')], + ]; + } + + /** + * Data provider for testGetEndDatesByType() test method. + */ + public function getEndDatesByType(): array + { + return [ + [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')], + [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')], + [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')], + ]; + } + + /** + * Data provider for testGetDescriptionsByType() test method. + */ + public function getDescriptionsByType(): array + { + return [ + [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F d, Y')], + [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F d, Y')], + [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'], + [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'], + [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'], + ]; + } + + /** + * Data provider for testGetDescriptionsByType() test method. + */ + public function getRssLengthsByType(): array + { + return [ + [DailyPageHelper::DAY], + [DailyPageHelper::WEEK], + [DailyPageHelper::MONTH], + ]; + } +} diff --git a/tpl/default/daily.html b/tpl/default/daily.html index 3749bffb..5e038c39 100644 --- a/tpl/default/daily.html +++ b/tpl/default/daily.html @@ -6,12 +6,25 @@ {include="page.header"} + + +

- {'The Daily Shaarli'|t} - + {$localizedType} Shaarli + t($type)])"}" + > + +

@@ -25,19 +38,19 @@
- {'All links of one day in a single page.'|t} + {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"}

- {if="!empty($dayDesc)"} - {$dayDesc} - - {/if} - {function="format_date($dayDate, false)"} + {$dayDesc}

diff --git a/tpl/default/dailyrss.html b/tpl/default/dailyrss.html index d40d9496..871a3ba7 100644 --- a/tpl/default/dailyrss.html +++ b/tpl/default/dailyrss.html @@ -1,9 +1,9 @@ - Daily - {$title} + {$localizedType} - {$title} {$index_url} - Daily shaared bookmarks + {function="t('All links of one :type in a single page.', '', 1, 'shaarli', [':type' => t($type)])"} {$language} {$index_url} Shaarli @@ -18,12 +18,15 @@ {loop="$value.links"}

{$value.title}

- {if="!$hide_timestamps"}{$value.created|format_date} - {/if}{if="$value.tags"}{$value.tags}{/if}
+ {if="!$hide_timestamps"}{$value.created|format_date} — {/if} + {'Permalink'|t} + {if="$value.tags"} — {$value.tags}{/if} +
{$value.url}

{if="$value.thumbnail"}thumbnail{/if}
{if="$value.description"}{$value.description}{/if} -


+

{/loop} ]]> -- cgit v1.2.3 From 54afb1d6f65f727b20b66582bb63a42c421eea4d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 27 Oct 2020 19:55:29 +0100 Subject: Fix rebase issue --- application/front/controller/admin/ServerController.php | 4 ++-- application/helper/FileUtils.php | 2 +- tests/helper/FileUtilsTest.php | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index 85654a43..bfc99422 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; -use Shaarli\ApplicationUtils; -use Shaarli\FileUtils; +use Shaarli\Helper\ApplicationUtils; +use Shaarli\Helper\FileUtils; use Slim\Http\Request; use Slim\Http\Response; diff --git a/application/helper/FileUtils.php b/application/helper/FileUtils.php index 2d50d850..2eac0793 100644 --- a/application/helper/FileUtils.php +++ b/application/helper/FileUtils.php @@ -133,7 +133,7 @@ class FileUtils */ public static function isPathInShaarliFolder(string $path): bool { - $rootDirectory = dirname(dirname(__FILE__)); + $rootDirectory = dirname(dirname(dirname(__FILE__))); return strpos(realpath($path), $rootDirectory) !== false; } diff --git a/tests/helper/FileUtilsTest.php b/tests/helper/FileUtilsTest.php index 948e46d1..8035f79c 100644 --- a/tests/helper/FileUtilsTest.php +++ b/tests/helper/FileUtilsTest.php @@ -4,6 +4,7 @@ namespace Shaarli\Helper; use Exception; use Shaarli\Exceptions\IOException; +use Shaarli\TestCase; /** * Class FileUtilsTest -- cgit v1.2.3