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

 Conflicts:
	CHANGELOG.md
	composer.lock
This commit is contained in:
Matias Griese
2022-03-31 11:26:37 +03:00
13 changed files with 187 additions and 48 deletions

View File

@@ -13,9 +13,18 @@
* Removed `system.umask_fix` setting for security reasons
* Support phpstan level 6 in Framework classes
# v1.7.32
# v1.7.33
## mm/dd/2022
1. [](#improved)
* When saving yaml and markdown, create also a cached version of the file and recompile it in opcache
2. [](#bugfix)
* Fixed missing changes in yaml & markdown files if saved multiple times during the same second because of a caching issue
* Fixed XSS check not detecting onX events without quotes
# v1.7.32
## 03/28/2022
1. [](#new)
* Added `|replace_last(search, replace)` filter
* Added `parseurl` Twig function to expose PHP's `parse_url` function
@@ -27,8 +36,9 @@
- `text`, `url`, `hidden`, `commalist`: 2048
- `text` (multiline), `textarea`: 65536
3. [](#bugfix)
* Fixed issue with `system.cache.gzip: true` resulted in admin "Fetch Failed" for PHP 8.0+
* Fixed issue with `system.cache.gzip: true` resulted in "Fetch Failed" for PHP 8.0.17 and PHP 8.1.4 [PHP issue #8218](https://github.com/php/php-src/issues/8218)
* Fix for multi-lang issues with Security Report
* Fixed page search not working with selected language [#3316](https://github.com/getgrav/grav/issues/3316)
# v1.7.31
## 03/14/2022

View File

@@ -9,7 +9,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.7.31');
define('GRAV_VERSION', '1.7.32');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false);

View File

@@ -43,4 +43,25 @@ class SystemFacade extends \Whoops\Util\SystemFacade
$handler();
}
}
/**
* @param int $httpCode
*
* @return int
*/
public function setHttpResponseCode($httpCode)
{
if (!headers_sent()) {
// Ensure that no 'location' header is present as otherwise this
// will override the HTTP code being set here, and mask the
// expected error page.
header_remove('location');
// Work around PHP bug #8218 (8.0.17 & 8.1.4).
header_remove('Content-Encoding');
}
return http_response_code($httpCode);
}
}

View File

@@ -10,6 +10,8 @@
namespace Grav\Common\File;
use Exception;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Utils;
use RocketTheme\Toolbox\File\PhpFile;
use RuntimeException;
@@ -32,9 +34,10 @@ trait CompiledFile
public function content($var = null)
{
try {
$filename = $this->filename;
// If nothing has been loaded, attempt to get pre-compiled version of the file first.
if ($var === null && $this->raw === null && $this->content === null) {
$key = md5($this->filename);
$key = md5($filename);
$file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
$modified = $this->modified();
@@ -48,39 +51,49 @@ trait CompiledFile
$class = get_class($this);
$size = filesize($filename);
$cache = $file->exists() ? $file->content() : null;
// Load real file if cache isn't up to date (or is invalid).
if (!isset($cache['@class'])
|| $cache['@class'] !== $class
|| $cache['modified'] !== $modified
|| $cache['filename'] !== $this->filename
|| ($cache['size'] ?? null) !== $size
|| $cache['filename'] !== $filename
) {
// Attempt to lock the file for writing.
try {
$file->lock(false);
$locked = $file->lock(false);
} catch (Exception $e) {
// Another process has locked the file; we will check this in a bit.
$locked = false;
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning');
}
// Decode RAW file into compiled array.
$data = (array)$this->decode($this->raw());
$cache = [
'@class' => $class,
'filename' => $this->filename,
'filename' => $filename,
'modified' => $modified,
'size' => $size,
'data' => $data
];
// If compiled file wasn't already locked by another process, save it.
if ($file->locked() !== false) {
if ($locked) {
$file->save($cache);
$file->unlock();
// Compile cached file into bytecode cache
if (function_exists('opcache_invalidate')) {
if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
$lockName = $file->filename();
// Silence error if function exists, but is restricted.
@opcache_invalidate($file->filename(), true);
@opcache_invalidate($lockName, true);
@opcache_compile_file($lockName);
}
}
}
@@ -89,12 +102,64 @@ trait CompiledFile
$this->content = $cache['data'];
}
} catch (Exception $e) {
throw new RuntimeException(sprintf('Failed to read %s: %s', Utils::basename($this->filename), $e->getMessage()), 500, $e);
throw new RuntimeException(sprintf('Failed to read %s: %s', Utils::basename($filename), $e->getMessage()), 500, $e);
}
return parent::content($var);
}
/**
* Save file.
*
* @param mixed $data Optional data to be saved, usually array.
* @return void
* @throws RuntimeException
*/
public function save($data = null)
{
// Make sure that the cache file is always up to date!
$key = md5($this->filename);
$file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php");
try {
$locked = $file->lock();
} catch (Exception $e) {
$locked = false;
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('%s(): Cannot obtain a lock for compiling cache file for %s: %s', __METHOD__, $this->filename, $e->getMessage()), 'warning');
}
parent::save($data);
if ($locked) {
$modified = $this->modified();
$filename = $this->filename;
$class = get_class($this);
$size = filesize($filename);
// Decode data into compiled array.
$cache = [
'@class' => $class,
'filename' => $filename,
'modified' => $modified,
'size' => $size,
'data' => $data
];
$file->save($cache);
$file->unlock();
// Compile cached file into bytecode cache
if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
$lockName = $file->filename();
// Silence error if function exists, but is restricted.
@opcache_invalidate($lockName, true);
@opcache_compile_file($lockName);
}
}
}
/**
* Serialize file.
*

View File

@@ -454,7 +454,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
continue;
}
// Get the main key without template and langauge.
// Get the main key without template and language.
[$main_key,] = explode('|', $entry['storage_key'] . '|', 2);
// Update storage key and language.
@@ -527,10 +527,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
$language = $options['lang'];
$status = 'error';
$msg = null;
$response = [];
$children = null;
$sub_route = null;
$extra = null;
// Handle leaf_route
@@ -610,7 +607,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
$children = $page->children();
/** @var PageIndex $children */
$children = $children->getIndex();
$selectedChildren = $children->filterBy($filters, true);
$selectedChildren = $children->filterBy($filters + ['language' => $language], true);
/** @var Header $header */
$header = $page->header();

View File

@@ -242,6 +242,7 @@ class PageObject extends FlexPageObject
{
/** @var PageCollection $siblings */
$siblings = $variables['siblings'];
/** @var PageObject $sibling */
foreach ($siblings as $sibling) {
$sibling->save(false);
}
@@ -585,38 +586,46 @@ class PageObject extends FlexPageObject
*/
public function filterBy(array $filters, bool $recursive = false): bool
{
$language = $filters['language'] ?? null;
if (null !== $language) {
/** @var PageObject $test */
$test = $this->getTranslation($language) ?? $this;
} else {
$test = $this;
}
foreach ($filters as $key => $value) {
switch ($key) {
case 'search':
$matches = $this->search((string)$value) > 0.0;
$matches = $test->search((string)$value) > 0.0;
break;
case 'page_type':
$types = $value ? explode(',', $value) : [];
$matches = in_array($this->template(), $types, true);
$matches = in_array($test->template(), $types, true);
break;
case 'extension':
$matches = Utils::contains((string)$value, $this->extension());
$matches = Utils::contains((string)$value, $test->extension());
break;
case 'routable':
$matches = $this->isRoutable() === (bool)$value;
$matches = $test->isRoutable() === (bool)$value;
break;
case 'published':
$matches = $this->isPublished() === (bool)$value;
$matches = $test->isPublished() === (bool)$value;
break;
case 'visible':
$matches = $this->isVisible() === (bool)$value;
$matches = $test->isVisible() === (bool)$value;
break;
case 'module':
$matches = $this->isModule() === (bool)$value;
$matches = $test->isModule() === (bool)$value;
break;
case 'page':
$matches = $this->isPage() === (bool)$value;
$matches = $test->isPage() === (bool)$value;
break;
case 'folder':
$matches = $this->isPage() === !$value;
$matches = $test->isPage() === !$value;
break;
case 'translated':
$matches = $this->hasTranslation() === (bool)$value;
$matches = $test->hasTranslation() === (bool)$value;
break;
default:
$matches = true;

View File

@@ -350,14 +350,12 @@ class Grav extends Container
*/
public function cleanOutputBuffers(): void
{
/** @var Config $config */
$config = $this['config'];
$gzip_enabled = (int) $config->get('system.cache.gzip');
// Make sure nothing extra gets written to the response.
while (ob_get_level() > 2 + $gzip_enabled) {
while (ob_get_level()) {
ob_end_clean();
}
// Work around PHP bug #8218 (8.0.17 & 8.1.4).
header_remove('Content-Encoding');
}
/**

View File

@@ -219,7 +219,8 @@ class Security
$string = html_entity_decode($string, ENT_NOQUOTES | ENT_HTML5, 'UTF-8');
// Strip whitespace characters
$string = preg_replace('!\s!u', '', $string);
$string = preg_replace('!\s!u', ' ', $string);
$stripped = preg_replace('!\s!u', '', $string);
// Set the patterns we'll test against
$patterns = [
@@ -242,7 +243,7 @@ class Security
// Iterate over rules and return label if fail
foreach ($patterns as $name => $regex) {
if (!empty($enabled_rules[$name])) {
if (preg_match($regex, $string) || preg_match($regex, $orig)) {
if (preg_match($regex, $string) || preg_match($regex, $stripped) || preg_match($regex, $orig)) {
return $name;
}
}

View File

@@ -550,9 +550,9 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
}
/**
* @param $str
* @param $search
* @param $replace
* @param string|mixed $str
* @param string $search
* @param string $replace
* @return string|mixed
*/
public function replaceLastFilter($str, $search, $replace)

View File

@@ -147,6 +147,10 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
*/
public function search(string $search, $properties = null, array $options = null)
{
$directory = $this->getFlexDirectory();
$properties = $directory->getSearchProperties($properties);
$options = $directory->getSearchOptions($options);
$matching = $this->call('search', [$search, $properties, $options]);
$matching = array_filter($matching);

View File

@@ -171,6 +171,44 @@ class FlexDirectory implements FlexDirectoryInterface
return null === $name ? $this->config : $this->config->get($name, $default);
}
/**
* @param string|string[]|null
* @return array
*/
public function getSearchProperties($properties = null): array
{
if (null !== $properties) {
return (array)$properties;
}
$properties = $this->getConfig('data.search.fields');
if (!$properties) {
$fields = $this->getConfig('admin.views.list.fields') ?? $this->getConfig('admin.list.fields', []);
foreach ($fields as $property => $value) {
if (!empty($value['link'])) {
$properties[] = $property;
}
}
}
return $properties;
}
/**
* @param array|null $options
* @return array
*/
public function getSearchOptions(array $options = null): array
{
if (empty($options['merge'])) {
return $options ?? (array)$this->getConfig('data.search.options');
}
unset($options['merge']);
return $options + (array)$this->getConfig('data.search.options');
}
/**
* @param string|null $name
* @param array $options

View File

@@ -162,6 +162,10 @@ class FlexIndex extends ObjectIndex implements FlexIndexInterface
*/
public function search(string $search, $properties = null, array $options = null)
{
$directory = $this->getFlexDirectory();
$properties = $directory->getSearchProperties($properties);
$options = $directory->getSearchOptions($options);
return $this->__call('search', [$search, $properties, $options]);
}

View File

@@ -287,17 +287,9 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function search(string $search, $properties = null, array $options = null): float
{
$properties = (array)($properties ?? $this->getFlexDirectory()->getConfig('data.search.fields'));
if (!$properties) {
$fields = $this->getFlexDirectory()->getConfig('admin.views.list.fields') ?? $this->getFlexDirectory()->getConfig('admin.list.fields', []);
foreach ($fields as $property => $value) {
if (!empty($value['link'])) {
$properties[] = $property;
}
}
}
$options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
$directory = $this->getFlexDirectory();
$properties = $directory->getSearchProperties($properties);
$options = $directory->getSearchOptions($options);
$weight = 0;
foreach ($properties as $property) {