Merge branch 'develop' of github.com:getgrav/grav into develop

This commit is contained in:
Andy Miller
2021-02-08 11:45:26 -07:00
28 changed files with 326 additions and 93 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
/**

View File

@@ -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
*/

View File

@@ -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);
}

View File

@@ -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']);

View File

@@ -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();

View File

@@ -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);

View File

@@ -1196,7 +1196,6 @@ abstract class Utils
if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) {
return $parts[0];
}
return false;
}

View File

@@ -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]);
}

View File

@@ -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);

View File

@@ -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.
*

View File

@@ -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}
*/

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -0,0 +1,5 @@
---
title: Simple Page avec traduction
---
Simple Page Content in English

View File

@@ -0,0 +1,5 @@
---
title: Simple Page avec traduction
---
Page Simple FR

View File

@@ -0,0 +1,5 @@
---
title: Simple Page avec traduction
---
Page Simple FR

View File

@@ -236,6 +236,31 @@ class PagesTest extends \Codeception\TestCase\Test
self::assertSame('&mdash;-&rtrif; 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
{
}

View File

@@ -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'));