diff --git a/CHANGELOG.md b/CHANGELOG.md index b22141a1b..9b1430ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,20 @@ * Updated bundled `composer.phar` binary to latest version `2.0.9` * Improved session fixation handling in PHP 7.4+ (cannot fix it in PHP 7.3 due to PHP bug) * Added optional password/database attributes for redis in `system.yaml` + * Added ability to filter enabled or disabled with bin/gpm index [#3187](https://github.com/getgrav/grav/pull/3187) + * Added `$grav->getVersion()` or `grav.version` in twig to get the current Grav version [#3142](https://github.com/getgrav/grav/issues/3142) 1. [](#bugfix) * Fixed issue with `content-security-policy` not being properly supported with `http-equiv` + support single quotes * Fixed CLI progressbar in `backup` and `security` commands to use styled output [#3198](https://github.com/getgrav/grav/issues/3198) * Fixed page save failing because of uploaded images [#3191](https://github.com/getgrav/grav/issues/3191) * Fixed `Flex Pages` using only default language in frontend [#106](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/106) + * Fixed empty `route()` and `raw_route()` when getting translated pages [#3184](https://github.com/getgrav/grav/pull/3184) + * Fixed error on `bin/gpm plugin uninstall` [#3207](https://github.com/getgrav/grav/issues/3207) + * Fixed broken min/max validation for field `type: int` + * Fixed lowering uppercase characters in usernames when saving from frontend [#2565](https://github.com/getgrav/grav/pull/2565) + * Fixed save error when editing accounts that have been created with capital letters in their username [#3211](https://github.com/getgrav/grav/issues/3211) + * Fixed renaming flex objects key when using file storage + * Fixed wrong values in Admin pages list [#3214](https://github.com/getgrav/grav/issues/3214) # v1.7.5 ## 02/01/2021 diff --git a/README.md b/README.md index 962cb36d1..562349f91 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad/mini.png)](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad) [![Discord](https://img.shields.io/discord/501836936584101899.svg?logo=discord&colorB=728ADA&label=Discord%20Chat)](https://chat.getgrav.org) - [![Build Status](https://travis-ci.org/getgrav/grav.svg?branch=develop)](https://travis-ci.org/getgrav/grav) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors) + [![PHP Tests](https://github.com/getgrav/grav/workflows/PHP%20Tests/badge.svg?branch=develop)](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors) Grav is a **Fast**, **Simple**, and **Flexible**, file-based Web-platform. There is **Zero** installation required. Just extract the ZIP archive, and you are already up and running. It follows similar principles to other flat-file CMS platforms, but has a different design philosophy than most. Grav comes with a powerful **Package Management System** to allow for simple installation and upgrading of plugins and themes, as well as simple updating of Grav itself. diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 9fa9f0194..2fb45a136 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -741,6 +741,16 @@ form: size: small label: PLUGIN_ADMIN.REDIS_PASSWORD + cache.redis.database: + type: text + size: medium + label: PLUGIN_ADMIN.REDIS_DATABASE + help: PLUGIN_ADMIN.REDIS_DATABASE_HELP + placeholder: "0" + validate: + type: number + min: 0 + flex_caching: type: section title: PLUGIN_ADMIN.FLEX_CACHING diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index 17d5ee641..3e0ba32f1 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -1116,6 +1116,21 @@ class Validation return ctype_xdigit($value); } + /** + * Custom input: int + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeInt($value, array $params, array $field) + { + $params['step'] = max(1, (int)($params['step'] ?? 0)); + + return self::typeNumber($value, $params, $field); + } + /** * @param mixed $value * @param mixed $params diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php index f5da89203..fbf0df4c1 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php @@ -448,6 +448,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface 'has-children' => $child_count > 0 ]; } else { + $lang = $child->findTranslation($language) ?? 'n/a'; + /** @var PageObject $child */ + $child = $child->getTranslation($language) ?? $child; + // TODO: all these features are independent from each other, we cannot just have one icon/color to catch all. // TODO: maybe icon by home/modular/page/folder (or even from blueprints) and color by visibility etc.. if ($child->home()) { @@ -467,9 +471,6 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface $child->visible() ? 'visible' : 'non-visible', $child->routable() ? 'routable' : 'non-routable' ]; - $lang = $child->findTranslation($language) ?? 'n/a'; - /** @var PageObject $child */ - $child = $child->getTranslation($language) ?? $child; $extras = [ 'template' => $child->template(), 'lang' => $lang ?: null, diff --git a/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php b/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php index 4ac561e62..f565c9f17 100644 --- a/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php +++ b/system/src/Grav/Common/Flex/Types/Users/Storage/UserFileStorage.php @@ -19,22 +19,6 @@ use Grav\Framework\Flex\Storage\FileStorage; */ class UserFileStorage extends FileStorage { - /** @var bool */ - public $caseSensitive; - - /** - * @param string $key - * @return string - */ - public function normalizeKey(string $key): string - { - if ($this->caseSensitive === true) { - return $key; - } - - return mb_strtolower($key); - } - /** * {@inheritdoc} * @see FlexStorageInterface::getMediaPath() @@ -60,15 +44,4 @@ class UserFileStorage extends FileStorage $row['access'] = $access; } } - - /** - * @param array $options - * @return void - */ - protected function initOptions(array $options): void - { - parent::initOptions($options); - - $this->caseSensitive = $options['case_sensitive'] ?? false; - } } diff --git a/system/src/Grav/Common/Flex/Types/Users/Storage/UserFolderStorage.php b/system/src/Grav/Common/Flex/Types/Users/Storage/UserFolderStorage.php index d1b36a745..7d9924e0d 100644 --- a/system/src/Grav/Common/Flex/Types/Users/Storage/UserFolderStorage.php +++ b/system/src/Grav/Common/Flex/Types/Users/Storage/UserFolderStorage.php @@ -19,22 +19,6 @@ use Grav\Framework\Flex\Storage\FolderStorage; */ class UserFolderStorage extends FolderStorage { - /** @var bool */ - public $caseSensitive; - - /** - * @param string $key - * @return string - */ - public function normalizeKey(string $key): string - { - if ($this->caseSensitive === true) { - return $key; - } - - return mb_strtolower($key); - } - /** * Prepares the row for saving and returns the storage key for the record. * @@ -50,15 +34,4 @@ class UserFolderStorage extends FolderStorage $row['access'] = $access; } } - - /** - * @param array $options - * @return void - */ - protected function initOptions(array $options): void - { - parent::initOptions($options); - - $this->caseSensitive = $options['case_sensitive'] ?? false; - } } diff --git a/system/src/Grav/Common/Flex/Types/Users/UserIndex.php b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php index 9e44236e4..4735c933e 100644 --- a/system/src/Grav/Common/Flex/Types/Users/UserIndex.php +++ b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php @@ -146,11 +146,7 @@ class UserIndex extends FlexIndex */ protected static function filterUsername(string $key, FlexStorageInterface $storage): string { - if (method_exists($storage, 'normalizeKey')) { - return $storage->normalizeKey($key); - } - - return mb_strtolower($key); + return $storage->normalizeKey($key); } /** diff --git a/system/src/Grav/Common/Flex/Types/Users/UserObject.php b/system/src/Grav/Common/Flex/Types/Users/UserObject.php index 2abefce32..1d803bd6a 100644 --- a/system/src/Grav/Common/Flex/Types/Users/UserObject.php +++ b/system/src/Grav/Common/Flex/Types/Users/UserObject.php @@ -120,11 +120,22 @@ class UserObject extends FlexObject implements UserInterface, Countable // User can only be authenticated via login. unset($elements['authenticated'], $elements['authorized']); - parent::__construct($elements, $key, $directory, $validate); + // Define username if it's not set. + if (!isset($elements['username'])) { + $storageKey = $elements['__META']['storage_key'] ?? null; + if (null !== $storageKey && $key === $directory->getStorage()->normalizeKey($storageKey)) { + $elements['username'] = $storageKey; + } else { + $elements['username'] = $key; + } + } - // Define username and state if they aren't set. - $this->defProperty('username', $key); - $this->defProperty('state', 'enabled'); + // Define state if it isn't set. + if (!isset($elements['state'])) { + $elements['state'] = 'enabled'; + } + + parent::__construct($elements, $key, $directory, $validate); } /** @@ -535,7 +546,7 @@ class UserObject extends FlexObject implements UserInterface, Countable } /** - * Save user without the username + * Save user * * @return static */ diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php index 3c3249a05..1b15e5de8 100644 --- a/system/src/Grav/Common/GPM/GPM.php +++ b/system/src/Grav/Common/GPM/GPM.php @@ -139,13 +139,27 @@ class GPM extends Iterator return $this->installed['plugins']; } + + /** + * Returns the plugin's enabled state + * + * @param string $slug + * @return bool True if the Plugin is Enabled. False if manually set to enable:false. Null otherwise. + */ + public function isPluginEnabled($slug): bool + { + $grav = Grav::instance(); + + return ($grav['config']['plugins'][$slug]['enabled'] ?? false) === true; + } + /** * Checks if a Plugin is installed * * @param string $slug The slug of the Plugin * @return bool True if the Plugin has been installed. False otherwise */ - public function isPluginInstalled($slug) + public function isPluginInstalled($slug): bool { return isset($this->installed['plugins'][$slug]); } @@ -182,13 +196,28 @@ class GPM extends Iterator return $this->installed['themes']; } + /** + * Checks if a Theme is enabled + * + * @param string $slug The slug of the Theme + * @return bool True if the Theme has been set to the default theme. False if installed, but not enabled. Null otherwise. + */ + public function isThemeEnabled($slug): bool + { + $grav = Grav::instance(); + + $current_theme = $grav['config']['system']['pages']['theme'] ?? null; + + return $current_theme === $slug; + } + /** * Checks if a Theme is installed * * @param string $slug The slug of the Theme * @return bool True if the Theme has been installed. False otherwise */ - public function isThemeInstalled($slug) + public function isThemeInstalled($slug): bool { return isset($this->installed['themes'][$slug]); } @@ -1023,7 +1052,6 @@ class GPM extends Iterator //Factor in the package dependencies too $dependencies = $this->calculateMergedDependenciesOfPackage($dependencyName, $dependencies); - } elseif ($dependencyVersion !== '*') { // Dependency already added by another package // If this package requires a version higher than the currently stored one, store this requirement instead @@ -1059,7 +1087,7 @@ class GPM extends Iterator $dependencies[$dependencyName] = $dependencyVersion; } } else { - $compatible = $this->checkNextSignificantReleasesAreCompatible($currently_stored_version_number,$current_package_version_number); + $compatible = $this->checkNextSignificantReleasesAreCompatible($currently_stored_version_number, $current_package_version_number); if (!$compatible) { throw new RuntimeException("Dependency {$dependencyName} is required in two incompatible versions", 2); } diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index db8316a9e..048f7d40b 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -162,6 +162,19 @@ class Grav extends Container return self::$instance; } + /** + * Get Grav version. + * + * @return string + */ + public function getVersion(): string + { + return GRAV_VERSION; + } + + /** + * @return bool + */ public function isSetup(): bool { return isset($this->initialized['setup']); diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index d749c75b3..27f1b4d9b 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -271,7 +271,8 @@ class Page implements PageInterface if ($exists) { $aPage = new Page(); $aPage->init(new SplFileInfo($path), $languageExtension); - + $aPage->route($this->route()); + $aPage->rawRoute($this->rawRoute()); $route = $aPage->header()->routes['default'] ?? $aPage->rawRoute(); if (!$route) { $route = $aPage->route(); diff --git a/system/src/Grav/Common/User/DataUser/User.php b/system/src/Grav/Common/User/DataUser/User.php index 6448361d3..d81381777 100644 --- a/system/src/Grav/Common/User/DataUser/User.php +++ b/system/src/Grav/Common/User/DataUser/User.php @@ -110,7 +110,7 @@ class User extends Data implements UserInterface } /** - * Save user without the username + * Save user * * @return void */ @@ -138,7 +138,10 @@ class User extends Data implements UserInterface } $data = $this->items; - unset($data['username'], $data['authenticated'], $data['authorized']); + if ($username === $data['username']) { + unset($data['username']); + } + unset($data['authenticated'], $data['authorized']); $file->save($data); diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 592484a12..cfb96bc33 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -1196,7 +1196,6 @@ abstract class Utils if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) { return $parts[0]; } - return false; } diff --git a/system/src/Grav/Console/Gpm/IndexCommand.php b/system/src/Grav/Console/Gpm/IndexCommand.php index 569b5f7a4..97a71bdf8 100644 --- a/system/src/Grav/Console/Gpm/IndexCommand.php +++ b/system/src/Grav/Console/Gpm/IndexCommand.php @@ -90,6 +90,18 @@ class IndexCommand extends GpmCommand InputOption::VALUE_NONE, 'Reverses the order of the output.' ) + ->addOption( + 'enabled', + 'e', + InputOption::VALUE_NONE, + 'Filters the results to only enabled Themes and Plugins.' + ) + ->addOption( + 'disabled', + 'd', + InputOption::VALUE_NONE, + 'Filters the results to only disabled Themes and Plugins.' + ) ->setDescription('Lists the plugins and themes available for installation') ->setHelp('The index command lists the plugins and themes available for installation') ; @@ -129,7 +141,7 @@ class IndexCommand extends GpmCommand if (!empty($packages)) { $io->section('Packages table'); $table = new Table($io); - $table->setHeaders(['Count', 'Name', 'Slug', 'Version', 'Installed']); + $table->setHeaders(['Count', 'Name', 'Slug', 'Version', 'Installed', 'Enabled']); $index = 0; foreach ($packages as $slug => $package) { @@ -138,7 +150,8 @@ class IndexCommand extends GpmCommand 'Name' => '' . Utils::truncate($package->name, 20, false, ' ', '...') . ' ', 'Slug' => $slug, 'Version'=> $this->version($package), - 'Installed' => $this->installed($package) + 'Installed' => $this->installed($package), + 'Enabled' => $this->enabled($package), ]; $table->addRow($row); @@ -195,6 +208,32 @@ class IndexCommand extends GpmCommand return !$installed ? 'not installed' : 'installed'; } + /** + * @param Package $package + * @return string + */ + private function enabled(Package $package): string + { + $package = $list[$package->slug] ?? $package; + $type = ucfirst(preg_replace('/s$/', '', $package->package_type)); + $method = 'is' . $type . 'Installed'; + $installed = $this->gpm->{$method}($package->slug); + + if ($installed) { + $method = 'is' . $type . 'Enabled'; + $enabled = $this->gpm->{$method}($package->slug); + if ($enabled === true) { + $result = 'enabled'; + } elseif ($enabled === false) { + $result = 'disabled'; + } + } else { + $result = ''; + } + + return $result; + } + /** * @param Packages $data * @return Packages @@ -210,10 +249,12 @@ class IndexCommand extends GpmCommand } $filter = [ + $this->options['desc'], + $this->options['disabled'], + $this->options['enabled'], $this->options['filter'], $this->options['installed-only'], $this->options['updates-only'], - $this->options['desc'] ]; if (count(array_filter($filter))) { @@ -227,7 +268,7 @@ class IndexCommand extends GpmCommand } // Filtering updatables only - if ($filter && $this->options['installed-only']) { + if ($filter && ($this->options['installed-only'] || $this->options['enabled'] || $this->options['disabled'])) { $method = ucfirst(preg_replace('/s$/', '', $package->package_type)); $function = 'is' . $method . 'Installed'; $filter = $this->gpm->{$function}($package->slug); @@ -240,6 +281,29 @@ class IndexCommand extends GpmCommand $filter = $this->gpm->{$function}($package->slug); } + // Filtering enabled only + if ($filter && $this->options['enabled']) { + $method = ucfirst(preg_replace('/s$/', '', $package->package_type)); + + // Check if packaged is enabled. + $function = 'is' . $method . 'Enabled'; + $filter = $this->gpm->{$function}($package->slug); + } + + // Filtering disabled only + if ($filter && $this->options['disabled']) { + $method = ucfirst(preg_replace('/s$/', '', $package->package_type)); + + // Check if package is disabled. + $function = 'is' . $method . 'Enabled'; + $enabled_filter = $this->gpm->{$function}($package->slug); + + // Apply filtering results. + if (!( $enabled_filter === false)) { + $filter = false; + } + } + if (!$filter) { unset($data[$type][$slug]); } diff --git a/system/src/Grav/Console/Gpm/UninstallCommand.php b/system/src/Grav/Console/Gpm/UninstallCommand.php index 3470f67dd..ff917191e 100644 --- a/system/src/Grav/Console/Gpm/UninstallCommand.php +++ b/system/src/Grav/Console/Gpm/UninstallCommand.php @@ -11,12 +11,14 @@ namespace Grav\Console\Gpm; use Grav\Common\GPM\GPM; use Grav\Common\GPM\Installer; -use Grav\Common\GPM\Local\Package; +use Grav\Common\GPM\Local; +use Grav\Common\GPM\Remote; use Grav\Common\Grav; use Grav\Console\GpmCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Question\ConfirmationQuestion; +use Throwable; use function count; use function in_array; use function is_array; @@ -113,7 +115,7 @@ class UninstallCommand extends GpmCommand // Plugins need to be initialized in order to make clear-cache to work. try { $this->initializePlugins(); - } catch (\Throwable $e) { + } catch (Throwable $e) { $io->writeln("Some plugins failed to initialize: {$e->getMessage()}"); } @@ -148,11 +150,11 @@ class UninstallCommand extends GpmCommand /** * @param string $slug - * @param Package $package + * @param Local\Package|Remote\Package $package * @param bool $is_dependency * @return bool */ - private function uninstallPackage($slug, Package $package, $is_dependency = false): bool + private function uninstallPackage($slug, $package, $is_dependency = false): bool { $io = $this->getIO(); @@ -255,10 +257,10 @@ class UninstallCommand extends GpmCommand /** * @param string $slug - * @param Package $package + * @param Local\Package|Remote\Package $package * @return bool */ - private function checkDestination(string $slug, Package $package): bool + private function checkDestination(string $slug, $package): bool { $io = $this->getIO(); @@ -297,10 +299,10 @@ class UninstallCommand extends GpmCommand * Check if package exists * * @param string $slug - * @param Package $package + * @param Local\Package|Remote\Package $package * @return int */ - private function packageExists(string $slug, Package $package): int + private function packageExists(string $slug, $package): int { $path = Grav::instance()['locator']->findResource($package->package_type . '://' . $slug); Installer::isValidDestination($path); diff --git a/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php index 8f20a45e3..97384abce 100644 --- a/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php +++ b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php @@ -20,7 +20,6 @@ use Grav\Framework\File\Formatter\MarkdownFormatter; use Grav\Framework\File\Formatter\YamlFormatter; use Grav\Framework\File\Interfaces\FileFormatterInterface; use Grav\Framework\Flex\Interfaces\FlexStorageInterface; -use RocketTheme\Toolbox\File\File; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RuntimeException; use function is_array; @@ -37,6 +36,8 @@ abstract class AbstractFilesystemStorage implements FlexStorageInterface protected $keyField = 'storage_key'; /** @var int */ protected $keyLen = 32; + /** @var bool */ + protected $caseSensitive = true; /** * @return bool @@ -98,7 +99,7 @@ abstract class AbstractFilesystemStorage implements FlexStorageInterface public function extractKeysFromRow(array $row): array { return [ - 'key' => $row[$this->keyField] ?? '' + 'key' => $this->normalizeKey($row[$this->keyField] ?? '') ]; } @@ -201,6 +202,19 @@ abstract class AbstractFilesystemStorage implements FlexStorageInterface return substr(hash('sha256', random_bytes($this->keyLen)), 0, $this->keyLen); } + /** + * @param string $key + * @return string + */ + public function normalizeKey(string $key): string + { + if ($this->caseSensitive === true) { + return $key; + } + + return mb_strtolower($key); + } + /** * Checks if a key is valid. * diff --git a/system/src/Grav/Framework/Flex/Storage/FileStorage.php b/system/src/Grav/Framework/Flex/Storage/FileStorage.php index cecaa5319..2dc0757a9 100644 --- a/system/src/Grav/Framework/Flex/Storage/FileStorage.php +++ b/system/src/Grav/Framework/Flex/Storage/FileStorage.php @@ -13,6 +13,7 @@ namespace Grav\Framework\Flex\Storage; use FilesystemIterator; use Grav\Framework\Flex\Interfaces\FlexStorageInterface; +use RuntimeException; use SplFileInfo; /** @@ -50,6 +51,73 @@ class FileStorage extends FolderStorage return $key ? "{$path}/{$key}" : $path; } + /** + * @param string $src + * @param string $dst + * @return bool + */ + public function copyRow(string $src, string $dst): bool + { + if ($this->hasKey($dst)) { + throw new RuntimeException("Cannot copy object: key '{$dst}' is already taken"); + } + + if (!$this->hasKey($src)) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::renameRow() + */ + public function renameRow(string $src, string $dst): bool + { + if (!$this->hasKey($src)) { + return false; + } + + // Remove old file. + $path = $this->getPathFromKey($src); + $file = $this->getFile($path); + $file->delete(); + + return true; + } + + /** + * @param string $src + * @param string $dst + * @return bool + */ + protected function copyFolder(string $src, string $dst): bool + { + // Nothing to copy. + return true; + } + + /** + * @param string $src + * @param string $dst + * @return bool + */ + protected function moveFolder(string $src, string $dst): bool + { + // Nothing to move. + return true; + } + + /** + * @param string $key + * @return bool + */ + protected function canDeleteFolder(string $key): bool + { + return false; + } + /** * {@inheritdoc} */ diff --git a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php index 514eeea80..acdab7816 100644 --- a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php +++ b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php @@ -242,7 +242,6 @@ class FolderStorage extends AbstractFilesystemStorage return $this->copyFolder($srcPath, $dstPath); } - /** * {@inheritdoc} * @see FlexStorageInterface::renameRow() @@ -360,7 +359,12 @@ class FolderStorage extends AbstractFilesystemStorage */ protected function prepareRow(array &$row): void { - unset($row[$this->keyField]); + if (array_key_exists($this->keyField, $row)) { + $key = $row[$this->keyField]; + if ($key === $this->normalizeKey($key)) { + unset($row[$this->keyField]); + } + } } /** @@ -401,6 +405,8 @@ class FolderStorage extends AbstractFilesystemStorage $key = $this->getNewKey(); } + $key = $this->normalizeKey($key); + // Check if the row already exists and if the key has been changed. $oldKey = $row['__META']['storage_key'] ?? null; if (is_string($oldKey) && $oldKey !== $key) { @@ -679,6 +685,7 @@ class FolderStorage extends AbstractFilesystemStorage $this->indexed = (bool)($options['indexed'] ?? false); $this->keyField = $options['key'] ?? 'storage_key'; $this->keyLen = (int)($options['key_len'] ?? 32); + $this->caseSensitive = (bool)($options['case_sensitive'] ?? true); $variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s']; $pattern = Utils::simpleTemplate($pattern, $variables); diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php index 59be24ede..618781de6 100644 --- a/tests/_bootstrap.php +++ b/tests/_bootstrap.php @@ -12,6 +12,10 @@ $grav = function () { $grav = Grav::instance(); $grav['config']->init(); + // This must be set first before the other init + $grav['config']->set('system.languages.supported', ['en', 'fr', 'vi']); + $grav['config']->set('system.languages.default_lang', 'en'); + foreach (array_keys($grav['setup']->getStreams()) as $stream) { @stream_wrapper_unregister($stream); } diff --git a/tests/fake/simple-site/user/pages/04.page-translated/default.en.md b/tests/fake/simple-site/user/pages/04.page-translated/default.en.md new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fake/simple-site/user/pages/04.page-translated/default.fr.md b/tests/fake/simple-site/user/pages/04.page-translated/default.fr.md new file mode 100644 index 000000000..97156cc5e --- /dev/null +++ b/tests/fake/simple-site/user/pages/04.page-translated/default.fr.md @@ -0,0 +1,5 @@ +--- +title: Simple Page avec traduction +--- + +Simple Page Content in English \ No newline at end of file diff --git a/tests/fake/simple-site/user/pages/05.translatedlong/part2/default.en.md b/tests/fake/simple-site/user/pages/05.translatedlong/part2/default.en.md new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fake/simple-site/user/pages/05.translatedlong/part2/default.fr.md b/tests/fake/simple-site/user/pages/05.translatedlong/part2/default.fr.md new file mode 100644 index 000000000..715706234 --- /dev/null +++ b/tests/fake/simple-site/user/pages/05.translatedlong/part2/default.fr.md @@ -0,0 +1,5 @@ +--- +title: Simple Page avec traduction +--- + +Page Simple FR \ No newline at end of file diff --git a/tests/fake/single-page-translated/user/pages/01.simple-page/default.en.md b/tests/fake/single-page-translated/user/pages/01.simple-page/default.en.md new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fake/single-page-translated/user/pages/01.simple-page/default.fr.md b/tests/fake/single-page-translated/user/pages/01.simple-page/default.fr.md new file mode 100644 index 000000000..715706234 --- /dev/null +++ b/tests/fake/single-page-translated/user/pages/01.simple-page/default.fr.md @@ -0,0 +1,5 @@ +--- +title: Simple Page avec traduction +--- + +Page Simple FR \ No newline at end of file diff --git a/tests/unit/Grav/Common/Page/PagesTest.php b/tests/unit/Grav/Common/Page/PagesTest.php index 6301a1b3d..edff75bc8 100644 --- a/tests/unit/Grav/Common/Page/PagesTest.php +++ b/tests/unit/Grav/Common/Page/PagesTest.php @@ -236,6 +236,31 @@ class PagesTest extends \Codeception\TestCase\Test self::assertSame('—-▸ Blog', $list['/blog']); } + public function testTranslatedLanguages(): void + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + $folder = $locator->findResource('tests://'); + + $page = $this->pages->get($folder . '/fake/simple-site/user/pages/04.page-translated'); + $this->assertInstanceOf(PageInterface::class, $page); + $translatedLanguages = $page->translatedLanguages(); + $this->assertIsArray($translatedLanguages); + $this->assertSame(["en" => "/page-translated", "fr" => "/page-translated"], $translatedLanguages); + } + + public function testLongPathTranslatedLanguages(): void + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + $folder = $locator->findResource('tests://'); + $page = $this->pages->get($folder . '/fake/simple-site/user/pages/05.translatedlong/part2'); + $this->assertInstanceOf(PageInterface::class, $page); + $translatedLanguages = $page->translatedLanguages(); + $this->assertIsArray($translatedLanguages); + $this->assertSame(["en" => "/translatedlong/part2", "fr" => "/translatedlong/part2"], $translatedLanguages); + } + public function testGetTypes(): void { } diff --git a/tests/unit/Grav/Common/UtilsTest.php b/tests/unit/Grav/Common/UtilsTest.php index d396a2c3b..4329a454e 100644 --- a/tests/unit/Grav/Common/UtilsTest.php +++ b/tests/unit/Grav/Common/UtilsTest.php @@ -299,7 +299,9 @@ class UtilsTest extends \Codeception\TestCase\Test $oneLanguageNotEnabled = reset($languagesNotEnabled); if (count($languagesEnabled)) { - self::assertTrue(Utils::pathPrefixedByLangCode('/' . $languagesEnabled[0] . '/test')); + $languageCodePathPrefix = Utils::pathPrefixedByLangCode('/' . $languagesEnabled[0] . '/test'); + $this->assertIsString($languageCodePathPrefix); + $this->assertTrue(in_array($languageCodePathPrefix, $languagesEnabled)); } self::assertFalse(Utils::pathPrefixedByLangCode('/' . $oneLanguageNotEnabled . '/test'));