mirror of
https://github.com/getgrav/grav.git
synced 2026-05-07 21:46:00 +02:00
Merge branch 'develop' of github.com:getgrav/grav into develop
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1196,7 +1196,6 @@ abstract class Utils
|
||||
if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) {
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <info>index</info> 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' => '<cyan>' . Utils::truncate($package->name, 20, false, ' ', '...') . '</cyan> ',
|
||||
'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 ? '<magenta>not installed</magenta>' : '<cyan>installed</cyan>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = '<cyan>enabled</cyan>';
|
||||
} elseif ($enabled === false) {
|
||||
$result = '<red>disabled</red>';
|
||||
}
|
||||
} 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]);
|
||||
}
|
||||
|
||||
@@ -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("<red>Some plugins failed to initialize: {$e->getMessage()}</red>");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Simple Page avec traduction
|
||||
---
|
||||
|
||||
Simple Page Content in English
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Simple Page avec traduction
|
||||
---
|
||||
|
||||
Page Simple FR
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Simple Page avec traduction
|
||||
---
|
||||
|
||||
Page Simple FR
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user