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 @@
[](https://github.com/phpstan/phpstan)
[](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad)
[](https://chat.getgrav.org)
- [](https://travis-ci.org/getgrav/grav) [](#backers) [](#sponsors)
+ [](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [](#backers) [](#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'));