màj
This commit is contained in:
		@@ -47,7 +47,8 @@ form:
 | 
			
		||||
              label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
 | 
			
		||||
              placeholder: '-lah'
 | 
			
		||||
            .at:
 | 
			
		||||
              type: cron
 | 
			
		||||
              type: text
 | 
			
		||||
              wrapper_classes: cron-selector
 | 
			
		||||
              label: PLUGIN_ADMIN.SCHEDULER_RUNAT
 | 
			
		||||
              help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
 | 
			
		||||
              placeholder: '* * * * *'
 | 
			
		||||
 
 | 
			
		||||
@@ -184,9 +184,9 @@ config:
 | 
			
		||||
      # Fields to be searched
 | 
			
		||||
      fields:
 | 
			
		||||
        - key
 | 
			
		||||
        - slug
 | 
			
		||||
        - menu
 | 
			
		||||
        - title
 | 
			
		||||
        - name
 | 
			
		||||
 | 
			
		||||
blueprints:
 | 
			
		||||
  configure:
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,10 @@ types:
 | 
			
		||||
    type: image
 | 
			
		||||
    thumb: media/thumb-webp.png
 | 
			
		||||
    mime: image/webp
 | 
			
		||||
  avif:
 | 
			
		||||
    type: image
 | 
			
		||||
    thumb: media/thumb.png
 | 
			
		||||
    mime: image/avif
 | 
			
		||||
  gif:
 | 
			
		||||
    type: animated
 | 
			
		||||
    thumb: media/thumb-gif.png
 | 
			
		||||
@@ -91,7 +95,7 @@ types:
 | 
			
		||||
  aif:
 | 
			
		||||
    type: audio
 | 
			
		||||
    thumb: media/thumb-aif.png
 | 
			
		||||
    mime: audio/aif
 | 
			
		||||
    mime: audio/aiff
 | 
			
		||||
  txt:
 | 
			
		||||
    type: file
 | 
			
		||||
    thumb: media/thumb-txt.png
 | 
			
		||||
@@ -207,7 +211,7 @@ types:
 | 
			
		||||
  js:
 | 
			
		||||
    type: file
 | 
			
		||||
    thumb: media/thumb-js.png
 | 
			
		||||
    mime: application/javascript
 | 
			
		||||
    mime: text/javascript
 | 
			
		||||
  json:
 | 
			
		||||
    type: file
 | 
			
		||||
    thumb: media/thumb-json.png
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1986
									
								
								system/config/mime.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1986
									
								
								system/config/mime.yaml
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -96,7 +96,7 @@ cache:
 | 
			
		||||
  purge_at: '0 4 * * *'                          # How often to purge old file cache (using new scheduler)
 | 
			
		||||
  clear_at: '0 3 * * *'                           # How often to clear cache (using new scheduler)
 | 
			
		||||
  clear_job_type: 'standard'                     # Type to clear when processing the scheduled clear job `standard`|`all`
 | 
			
		||||
  clear_images_by_default: false                  # By default grav will include processed images in cache clear, this can be disabled
 | 
			
		||||
  clear_images_by_default: false                  # By default grav does not include processed images in cache clear, this can be enabled
 | 
			
		||||
  cli_compatibility: false                       # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
 | 
			
		||||
  lifetime: 604800                               # Lifetime of cached data in seconds (0 = infinite)
 | 
			
		||||
  gzip: false                                    # GZip compress the page output
 | 
			
		||||
@@ -131,7 +131,7 @@ assets:                                          # Configuration for Assets Mana
 | 
			
		||||
  enable_asset_timestamp: false                  # Enable asset timestamps
 | 
			
		||||
  enable_asset_sri: false                        # Enable asset SRI
 | 
			
		||||
  collections:
 | 
			
		||||
    jquery: system://assets/jquery/jquery-2.x.min.js
 | 
			
		||||
    jquery: system://assets/jquery/jquery-3.x.min.js
 | 
			
		||||
 | 
			
		||||
errors:
 | 
			
		||||
  display: 0                                     # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
 | 
			
		||||
// Some standard defines
 | 
			
		||||
define('GRAV', true);
 | 
			
		||||
define('GRAV_VERSION', '1.7.16');
 | 
			
		||||
define('GRAV_VERSION', '1.7.21');
 | 
			
		||||
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
 | 
			
		||||
define('GRAV_TESTING', false);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -110,7 +110,7 @@ class Assets extends PropertyObject
 | 
			
		||||
 | 
			
		||||
        /** @var UniformResourceLocator $locator */
 | 
			
		||||
        $locator = $grav['locator'];
 | 
			
		||||
        $this->assets_dir = $locator->findResource('asset://') . DS;
 | 
			
		||||
        $this->assets_dir = $locator->findResource('asset://');
 | 
			
		||||
        $this->assets_url = $locator->findResource('asset://', false);
 | 
			
		||||
 | 
			
		||||
        $this->config($asset_config);
 | 
			
		||||
@@ -164,10 +164,19 @@ class Assets extends PropertyObject
 | 
			
		||||
 | 
			
		||||
        // More than one asset
 | 
			
		||||
        if (is_array($asset)) {
 | 
			
		||||
            foreach ($asset as $a) {
 | 
			
		||||
                array_shift($args);
 | 
			
		||||
                $args = array_merge([$a], $args);
 | 
			
		||||
                call_user_func_array([$this, 'add'], $args);
 | 
			
		||||
            foreach ($asset as $index => $location) {
 | 
			
		||||
                $params = array_slice($args, 1);
 | 
			
		||||
                if (is_array($location)) {
 | 
			
		||||
                    $params = array_shift($params);
 | 
			
		||||
                    if (is_numeric($params)) {
 | 
			
		||||
                        $params = [ 'priority' => $params ];
 | 
			
		||||
                    }
 | 
			
		||||
                    $params = [array_replace_recursive([], $location, $params)];
 | 
			
		||||
                    $location = $index;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $params = array_merge([$location], $params);
 | 
			
		||||
                call_user_func_array([$this, 'add'], $params);
 | 
			
		||||
            }
 | 
			
		||||
        } elseif (isset($this->collections[$asset])) {
 | 
			
		||||
            array_shift($args);
 | 
			
		||||
@@ -201,8 +210,13 @@ class Assets extends PropertyObject
 | 
			
		||||
    protected function addType($collection, $type, $asset, $options)
 | 
			
		||||
    {
 | 
			
		||||
        if (is_array($asset)) {
 | 
			
		||||
            foreach ($asset as $a) {
 | 
			
		||||
                $this->addType($collection, $type, $a, $options);
 | 
			
		||||
            foreach ($asset as $index => $location) {
 | 
			
		||||
                $assetOptions = $options;
 | 
			
		||||
                if (is_array($location)) {
 | 
			
		||||
                    $assetOptions = array_replace_recursive([], $options, $location);
 | 
			
		||||
                    $location = $index;
 | 
			
		||||
                }
 | 
			
		||||
                $this->addType($collection, $type, $location, $assetOptions);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $this;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,9 @@
 | 
			
		||||
 | 
			
		||||
namespace Grav\Common\Assets;
 | 
			
		||||
 | 
			
		||||
use Grav\Common\Assets\BaseAsset;
 | 
			
		||||
use Grav\Common\Assets\Traits\AssetUtilsTrait;
 | 
			
		||||
use Grav\Common\Config\Config;
 | 
			
		||||
use Grav\Common\Filesystem\Folder;
 | 
			
		||||
use Grav\Common\Grav;
 | 
			
		||||
use Grav\Common\Uri;
 | 
			
		||||
use Grav\Common\Utils;
 | 
			
		||||
@@ -88,7 +88,14 @@ class Pipeline extends PropertyObject
 | 
			
		||||
        $uri = Grav::instance()['uri'];
 | 
			
		||||
 | 
			
		||||
        $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
 | 
			
		||||
        $this->assets_dir = $locator->findResource('asset://') . DS;
 | 
			
		||||
        $this->assets_dir = $locator->findResource('asset://');
 | 
			
		||||
        if (!$this->assets_dir) {
 | 
			
		||||
            // Attempt to create assets folder if it doesn't exist yet.
 | 
			
		||||
            $this->assets_dir = $locator->findResource('asset://', true, true);
 | 
			
		||||
            Folder::mkdir($this->assets_dir);
 | 
			
		||||
            $locator->clearCache();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->assets_url = $locator->findResource('asset://', false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -119,10 +126,9 @@ class Pipeline extends PropertyObject
 | 
			
		||||
        $file = $uid . '.css';
 | 
			
		||||
        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
 | 
			
		||||
 | 
			
		||||
        $buffer = null;
 | 
			
		||||
 | 
			
		||||
        if (file_exists($this->assets_dir . $file)) {
 | 
			
		||||
            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
 | 
			
		||||
        $filepath = "{$this->assets_dir}/{$file}";
 | 
			
		||||
        if (file_exists($filepath)) {
 | 
			
		||||
            $buffer = file_get_contents($filepath) . "\n";
 | 
			
		||||
        } else {
 | 
			
		||||
            //if nothing found get out of here!
 | 
			
		||||
            if (empty($assets)) {
 | 
			
		||||
@@ -141,7 +147,7 @@ class Pipeline extends PropertyObject
 | 
			
		||||
 | 
			
		||||
            // Write file
 | 
			
		||||
            if (trim($buffer) !== '') {
 | 
			
		||||
                file_put_contents($this->assets_dir . $file, $buffer);
 | 
			
		||||
                file_put_contents($filepath, $buffer);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -182,10 +188,9 @@ class Pipeline extends PropertyObject
 | 
			
		||||
        $file = $uid . '.js';
 | 
			
		||||
        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
 | 
			
		||||
 | 
			
		||||
        $buffer = null;
 | 
			
		||||
 | 
			
		||||
        if (file_exists($this->assets_dir . $file)) {
 | 
			
		||||
            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
 | 
			
		||||
        $filepath = "{$this->assets_dir}/{$file}";
 | 
			
		||||
        if (file_exists($filepath)) {
 | 
			
		||||
            $buffer = file_get_contents($filepath) . "\n";
 | 
			
		||||
        } else {
 | 
			
		||||
            //if nothing found get out of here!
 | 
			
		||||
            if (empty($assets)) {
 | 
			
		||||
@@ -204,7 +209,7 @@ class Pipeline extends PropertyObject
 | 
			
		||||
 | 
			
		||||
            // Write file
 | 
			
		||||
            if (trim($buffer) !== '') {
 | 
			
		||||
                file_put_contents($this->assets_dir . $file, $buffer);
 | 
			
		||||
                file_put_contents($filepath, $buffer);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -156,6 +156,10 @@ trait AssetUtilsTrait
 | 
			
		||||
        $no_key = ['loading'];
 | 
			
		||||
 | 
			
		||||
        foreach ($this->attributes as $key => $value) {
 | 
			
		||||
            if ($value === null) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (is_numeric($key)) {
 | 
			
		||||
                $key = $value;
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,9 @@ class Setup extends Data
 | 
			
		||||
     */
 | 
			
		||||
    public static $environment;
 | 
			
		||||
 | 
			
		||||
    /** @var string */
 | 
			
		||||
    public static $securityFile = 'config://security.yaml';
 | 
			
		||||
 | 
			
		||||
    /** @var array */
 | 
			
		||||
    protected $streams = [
 | 
			
		||||
        'user' => [
 | 
			
		||||
@@ -390,12 +393,19 @@ class Setup extends Data
 | 
			
		||||
 | 
			
		||||
            if (!$locator->findResource('environment://config', true)) {
 | 
			
		||||
                // If environment does not have its own directory, remove it from the lookup.
 | 
			
		||||
                $this->set('streams.schemes.environment.prefixes', ['config' => []]);
 | 
			
		||||
                $prefixes = $this->get('streams.schemes.environment.prefixes');
 | 
			
		||||
                $prefixes['config'] = [];
 | 
			
		||||
 | 
			
		||||
                $this->set('streams.schemes.environment.prefixes', $prefixes);
 | 
			
		||||
                $this->initializeLocator($locator);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Create security.yaml if it doesn't exist.
 | 
			
		||||
            $filename = $locator->findResource('config://security.yaml', true, true);
 | 
			
		||||
            // Create security.yaml salt if it doesn't exist into existing configuration environment if possible.
 | 
			
		||||
            $securityFile = basename(static::$securityFile);
 | 
			
		||||
            $securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile));
 | 
			
		||||
            $securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true);
 | 
			
		||||
            $filename = "{$securityFolder}/{$securityFile}";
 | 
			
		||||
 | 
			
		||||
            $security_file = CompiledYamlFile::instance($filename);
 | 
			
		||||
            $security_content = (array)$security_file->content();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -519,17 +519,32 @@ class Validation
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isset($params['min']) && $value < $params['min']) {
 | 
			
		||||
            return false;
 | 
			
		||||
        $value = (float)$value;
 | 
			
		||||
 | 
			
		||||
        $min = 0;
 | 
			
		||||
        if (isset($params['min'])) {
 | 
			
		||||
            $min = (float)$params['min'];
 | 
			
		||||
            if ($value < $min) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isset($params['max']) && $value > $params['max']) {
 | 
			
		||||
            return false;
 | 
			
		||||
        if (isset($params['max'])) {
 | 
			
		||||
            $max = (float)$params['max'];
 | 
			
		||||
            if ($value > $max) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $min = $params['min'] ?? 0;
 | 
			
		||||
        if (isset($params['step'])) {
 | 
			
		||||
            $step = (float)$params['step'];
 | 
			
		||||
            // Count of how many steps we are above/below the minimum value.
 | 
			
		||||
            $pos = ($value - $min) / $step;
 | 
			
		||||
 | 
			
		||||
        return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
 | 
			
		||||
            return is_int(static::filterNumber($pos, $params, $field));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ namespace Grav\Common\Flex;
 | 
			
		||||
 | 
			
		||||
use Grav\Common\Flex\Traits\FlexGravTrait;
 | 
			
		||||
use Grav\Common\Flex\Traits\FlexObjectTrait;
 | 
			
		||||
use Grav\Common\Media\Interfaces\MediaInterface;
 | 
			
		||||
use Grav\Framework\Flex\Traits\FlexMediaTrait;
 | 
			
		||||
use function is_array;
 | 
			
		||||
 | 
			
		||||
@@ -21,7 +22,7 @@ use function is_array;
 | 
			
		||||
 *
 | 
			
		||||
 * @package Grav\Common\Flex
 | 
			
		||||
 */
 | 
			
		||||
abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
 | 
			
		||||
abstract class FlexObject extends \Grav\Framework\Flex\FlexObject implements MediaInterface
 | 
			
		||||
{
 | 
			
		||||
    use FlexGravTrait;
 | 
			
		||||
    use FlexObjectTrait;
 | 
			
		||||
 
 | 
			
		||||
@@ -192,6 +192,14 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
 | 
			
		||||
        throw new RuntimeException(__METHOD__ . '(): Not Implemented');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set current page.
 | 
			
		||||
     */
 | 
			
		||||
    public function setCurrent(string $path): void
 | 
			
		||||
    {
 | 
			
		||||
        throw new RuntimeException(__METHOD__ . '(): Not Implemented');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return previous item.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -674,12 +674,12 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
 | 
			
		||||
                    $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
 | 
			
		||||
                    $route = $child->getRoute();
 | 
			
		||||
                    $payload = [
 | 
			
		||||
                        'item-key' => basename($child->rawRoute() ?? $child->getKey()),
 | 
			
		||||
                        'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())),
 | 
			
		||||
                        'icon' => $icon,
 | 
			
		||||
                        'title' => htmlspecialchars($child->menu()),
 | 
			
		||||
                        'route' => [
 | 
			
		||||
                            'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '',
 | 
			
		||||
                            'raw' => $child->rawRoute(),
 | 
			
		||||
                            'display' => htmlspecialchars(($route ? ($route->toString(false) ?: '/') : null) ?? ''),
 | 
			
		||||
                            'raw' => htmlspecialchars($child->rawRoute()),
 | 
			
		||||
                        ],
 | 
			
		||||
                        'modified' => $this->jsDate($child->modified()),
 | 
			
		||||
                        'child_count' => $child_count ?: null,
 | 
			
		||||
 
 | 
			
		||||
@@ -262,6 +262,24 @@ class PageObject extends FlexPageObject
 | 
			
		||||
        $this->getFlexDirectory()->reloadIndex();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param UserInterface|null $user
 | 
			
		||||
     */
 | 
			
		||||
    public function check(UserInterface $user = null): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::check($user);
 | 
			
		||||
 | 
			
		||||
        if ($user && $this->isMoved()) {
 | 
			
		||||
            $parentKey = $this->getProperty('parent_key');
 | 
			
		||||
 | 
			
		||||
            /** @var PageObject|null $parent */
 | 
			
		||||
            $parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
 | 
			
		||||
            if (!$parent || !$parent->isAuthorized('create', null, $user)) {
 | 
			
		||||
                throw new \RuntimeException('Forbidden', 403);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array|bool $reorder
 | 
			
		||||
     * @return FlexObject|FlexObjectInterface
 | 
			
		||||
@@ -357,6 +375,19 @@ class PageObject extends FlexPageObject
 | 
			
		||||
        return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    protected function isMoved(): bool
 | 
			
		||||
    {
 | 
			
		||||
        $storageKey = $this->getMasterKey();
 | 
			
		||||
        $filesystem = Filesystem::getInstance(false);
 | 
			
		||||
        $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
 | 
			
		||||
        $newParentKey = $this->getProperty('parent_key');
 | 
			
		||||
 | 
			
		||||
        return $this->exists() && $oldParentKey !== $newParentKey;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array $ordering
 | 
			
		||||
     * @return PageCollection|null
 | 
			
		||||
@@ -364,10 +395,7 @@ class PageObject extends FlexPageObject
 | 
			
		||||
    protected function reorderSiblings(array $ordering)
 | 
			
		||||
    {
 | 
			
		||||
        $storageKey = $this->getMasterKey();
 | 
			
		||||
        $filesystem = Filesystem::getInstance(false);
 | 
			
		||||
        $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
 | 
			
		||||
        $newParentKey = $this->getProperty('parent_key');
 | 
			
		||||
        $isMoved = $this->exists() && $oldParentKey !== $newParentKey;
 | 
			
		||||
        $isMoved = $this->isMoved();
 | 
			
		||||
        $order = !$isMoved ? $this->order() : false;
 | 
			
		||||
        if ($order !== false) {
 | 
			
		||||
            $order = (int)$order;
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,14 @@ class UserGroupObject extends FlexObject implements UserGroupInterface
 | 
			
		||||
        ] + parent::getCachedMethods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->getProperty('readableName');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks user authorization to the action.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Grav\Common\Flex\Types\Users;
 | 
			
		||||
 | 
			
		||||
use Closure;
 | 
			
		||||
use Countable;
 | 
			
		||||
use Grav\Common\Config\Config;
 | 
			
		||||
use Grav\Common\Data\Blueprint;
 | 
			
		||||
@@ -31,6 +32,7 @@ use Grav\Common\User\Interfaces\UserInterface;
 | 
			
		||||
use Grav\Common\User\Traits\UserTrait;
 | 
			
		||||
use Grav\Framework\File\Formatter\JsonFormatter;
 | 
			
		||||
use Grav\Framework\File\Formatter\YamlFormatter;
 | 
			
		||||
use Grav\Framework\Filesystem\Filesystem;
 | 
			
		||||
use Grav\Framework\Flex\Flex;
 | 
			
		||||
use Grav\Framework\Flex\FlexDirectory;
 | 
			
		||||
use Grav\Framework\Flex\Storage\FileStorage;
 | 
			
		||||
@@ -75,6 +77,9 @@ class UserObject extends FlexObject implements UserInterface, Countable
 | 
			
		||||
    use UserTrait;
 | 
			
		||||
    use UserObjectLegacyTrait;
 | 
			
		||||
 | 
			
		||||
    /** @var Closure|null */
 | 
			
		||||
    static public $authorizeCallable;
 | 
			
		||||
 | 
			
		||||
    /** @var array|null */
 | 
			
		||||
    protected $_uploads_original;
 | 
			
		||||
    /** @var FileInterface|null */
 | 
			
		||||
@@ -259,6 +264,15 @@ class UserObject extends FlexObject implements UserInterface, Countable
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $authorizeCallable = static::$authorizeCallable;
 | 
			
		||||
        if ($authorizeCallable instanceof Closure) {
 | 
			
		||||
            $authorizeCallable->bindTo($this);
 | 
			
		||||
            $authorized = $authorizeCallable($action, $scope);
 | 
			
		||||
            if (is_bool($authorized)) {
 | 
			
		||||
                return $authorized;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check user access.
 | 
			
		||||
        $access = $this->getAccess();
 | 
			
		||||
        $authorized = $access->authorize($action, $scope);
 | 
			
		||||
@@ -292,6 +306,14 @@ class UserObject extends FlexObject implements UserInterface, Countable
 | 
			
		||||
        return $value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return UserGroupIndex
 | 
			
		||||
     */
 | 
			
		||||
    public function getRoles(): UserGroupIndex
 | 
			
		||||
    {
 | 
			
		||||
        return $this->getGroups();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert object into an array.
 | 
			
		||||
     *
 | 
			
		||||
@@ -689,6 +711,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array $files
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    protected function setUpdatedMedia(array $files): void
 | 
			
		||||
    {
 | 
			
		||||
@@ -700,9 +723,12 @@ class UserObject extends FlexObject implements UserInterface, Countable
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $filesystem = Filesystem::getInstance(false);
 | 
			
		||||
 | 
			
		||||
        $list = [];
 | 
			
		||||
        $list_original = [];
 | 
			
		||||
        foreach ($files as $field => $group) {
 | 
			
		||||
            // Ignore files without a field.
 | 
			
		||||
            if ($field === '') {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
@@ -724,7 +750,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if ($file) {
 | 
			
		||||
                    // Check file upload against media limits.
 | 
			
		||||
                    // Check file upload against media limits (except for max size).
 | 
			
		||||
                    $filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -748,15 +774,19 @@ class UserObject extends FlexObject implements UserInterface, Countable
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Calculate path without the retina scaling factor.
 | 
			
		||||
                $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath));
 | 
			
		||||
 | 
			
		||||
                $list[$filename] = [$file, $settings];
 | 
			
		||||
 | 
			
		||||
                $path = str_replace('.', "\n", $field);
 | 
			
		||||
                if (null !== $data) {
 | 
			
		||||
                    $data['name'] = $filename;
 | 
			
		||||
                    $data['path'] = $filepath;
 | 
			
		||||
 | 
			
		||||
                    $this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
 | 
			
		||||
                    $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
 | 
			
		||||
                } else {
 | 
			
		||||
                    $this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
 | 
			
		||||
                    $this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,11 @@ class GPM extends Iterator
 | 
			
		||||
    /** @var Remote\Packages|null Remote available Packages */
 | 
			
		||||
    private $repository;
 | 
			
		||||
    /** @var Remote\GravCore|null Remove Grav Packages */
 | 
			
		||||
    public $grav;
 | 
			
		||||
    private $grav;
 | 
			
		||||
    /** @var bool */
 | 
			
		||||
    private $refresh;
 | 
			
		||||
    /** @var callable|null */
 | 
			
		||||
    private $callback;
 | 
			
		||||
 | 
			
		||||
    /** @var array Internal cache */
 | 
			
		||||
    protected $cache;
 | 
			
		||||
@@ -55,13 +59,45 @@ class GPM extends Iterator
 | 
			
		||||
    public function __construct($refresh = false, $callback = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        Folder::create(CACHE_DIR . '/gpm');
 | 
			
		||||
 | 
			
		||||
        $this->cache = [];
 | 
			
		||||
        $this->installed = new Local\Packages();
 | 
			
		||||
        try {
 | 
			
		||||
            $this->repository = new Remote\Packages($refresh, $callback);
 | 
			
		||||
            $this->grav = new Remote\GravCore($refresh, $callback);
 | 
			
		||||
        } catch (Exception $e) {
 | 
			
		||||
        $this->refresh = $refresh;
 | 
			
		||||
        $this->callback = $callback;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Magic getter method
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $offset Asset name value
 | 
			
		||||
     * @return mixed Asset value
 | 
			
		||||
     */
 | 
			
		||||
    public function __get($offset)
 | 
			
		||||
    {
 | 
			
		||||
        switch ($offset) {
 | 
			
		||||
            case 'grav':
 | 
			
		||||
                return $this->getGrav();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return parent::__get($offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Magic method to determine if the attribute is set
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $offset Asset name value
 | 
			
		||||
     * @return bool True if the value is set
 | 
			
		||||
     */
 | 
			
		||||
    public function __isset($offset)
 | 
			
		||||
    {
 | 
			
		||||
        switch ($offset) {
 | 
			
		||||
            case 'grav':
 | 
			
		||||
                return $this->getGrav() !== null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return parent::__isset($offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -266,11 +302,12 @@ class GPM extends Iterator
 | 
			
		||||
    {
 | 
			
		||||
        $items = [];
 | 
			
		||||
 | 
			
		||||
        if (null === $this->repository) {
 | 
			
		||||
        $repository = $this->getRepository();
 | 
			
		||||
        if (null === $repository) {
 | 
			
		||||
            return $items;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $repository = $this->repository['plugins'];
 | 
			
		||||
        $plugins = $repository['plugins'];
 | 
			
		||||
 | 
			
		||||
        // local cache to speed things up
 | 
			
		||||
        if (isset($this->cache[__METHOD__])) {
 | 
			
		||||
@@ -278,18 +315,18 @@ class GPM extends Iterator
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($this->installed['plugins'] as $slug => $plugin) {
 | 
			
		||||
            if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
 | 
			
		||||
            if (!isset($plugins[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $local_version = $plugin->version ?? 'Unknown';
 | 
			
		||||
            $remote_version = $repository[$slug]->version;
 | 
			
		||||
            $remote_version = $plugins[$slug]->version;
 | 
			
		||||
 | 
			
		||||
            if (version_compare($local_version, $remote_version) < 0) {
 | 
			
		||||
                $repository[$slug]->available = $remote_version;
 | 
			
		||||
                $repository[$slug]->version = $local_version;
 | 
			
		||||
                $repository[$slug]->type = $repository[$slug]->release_type;
 | 
			
		||||
                $items[$slug] = $repository[$slug];
 | 
			
		||||
                $plugins[$slug]->available = $remote_version;
 | 
			
		||||
                $plugins[$slug]->version = $local_version;
 | 
			
		||||
                $plugins[$slug]->type = $plugins[$slug]->release_type;
 | 
			
		||||
                $items[$slug] = $plugins[$slug];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -306,19 +343,20 @@ class GPM extends Iterator
 | 
			
		||||
     */
 | 
			
		||||
    public function getLatestVersionOfPackage($package_name)
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $this->repository) {
 | 
			
		||||
        $repository = $this->getRepository();
 | 
			
		||||
        if (null === $repository) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $repository = $this->repository['plugins'];
 | 
			
		||||
        if (isset($repository[$package_name])) {
 | 
			
		||||
            return $repository[$package_name]->available ?: $repository[$package_name]->version;
 | 
			
		||||
        $plugins = $repository['plugins'];
 | 
			
		||||
        if (isset($plugins[$package_name])) {
 | 
			
		||||
            return $plugins[$package_name]->available ?: $plugins[$package_name]->version;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Not a plugin, it's a theme?
 | 
			
		||||
        $repository = $this->repository['themes'];
 | 
			
		||||
        if (isset($repository[$package_name])) {
 | 
			
		||||
            return $repository[$package_name]->available ?: $repository[$package_name]->version;
 | 
			
		||||
        $themes = $repository['themes'];
 | 
			
		||||
        if (isset($themes[$package_name])) {
 | 
			
		||||
            return $themes[$package_name]->available ?: $themes[$package_name]->version;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
@@ -356,11 +394,12 @@ class GPM extends Iterator
 | 
			
		||||
    {
 | 
			
		||||
        $items = [];
 | 
			
		||||
 | 
			
		||||
        if (null === $this->repository) {
 | 
			
		||||
        $repository = $this->getRepository();
 | 
			
		||||
        if (null === $repository) {
 | 
			
		||||
            return $items;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $repository = $this->repository['themes'];
 | 
			
		||||
        $themes = $repository['themes'];
 | 
			
		||||
 | 
			
		||||
        // local cache to speed things up
 | 
			
		||||
        if (isset($this->cache[__METHOD__])) {
 | 
			
		||||
@@ -368,18 +407,18 @@ class GPM extends Iterator
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($this->installed['themes'] as $slug => $plugin) {
 | 
			
		||||
            if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
 | 
			
		||||
            if (!isset($themes[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $local_version = $plugin->version ?? 'Unknown';
 | 
			
		||||
            $remote_version = $repository[$slug]->version;
 | 
			
		||||
            $remote_version = $themes[$slug]->version;
 | 
			
		||||
 | 
			
		||||
            if (version_compare($local_version, $remote_version) < 0) {
 | 
			
		||||
                $repository[$slug]->available = $remote_version;
 | 
			
		||||
                $repository[$slug]->version = $local_version;
 | 
			
		||||
                $repository[$slug]->type = $repository[$slug]->release_type;
 | 
			
		||||
                $items[$slug] = $repository[$slug];
 | 
			
		||||
                $themes[$slug]->available = $remote_version;
 | 
			
		||||
                $themes[$slug]->version = $local_version;
 | 
			
		||||
                $themes[$slug]->type = $themes[$slug]->release_type;
 | 
			
		||||
                $items[$slug] = $themes[$slug];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -407,19 +446,20 @@ class GPM extends Iterator
 | 
			
		||||
     */
 | 
			
		||||
    public function getReleaseType($package_name)
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $this->repository) {
 | 
			
		||||
        $repository = $this->getRepository();
 | 
			
		||||
        if (null === $repository) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $repository = $this->repository['plugins'];
 | 
			
		||||
        if (isset($repository[$package_name])) {
 | 
			
		||||
            return $repository[$package_name]->release_type;
 | 
			
		||||
        $plugins = $repository['plugins'];
 | 
			
		||||
        if (isset($plugins[$package_name])) {
 | 
			
		||||
            return $plugins[$package_name]->release_type;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Not a plugin, it's a theme?
 | 
			
		||||
        $repository = $this->repository['themes'];
 | 
			
		||||
        if (isset($repository[$package_name])) {
 | 
			
		||||
            return $repository[$package_name]->release_type;
 | 
			
		||||
        $themes = $repository['themes'];
 | 
			
		||||
        if (isset($themes[$package_name])) {
 | 
			
		||||
            return $themes[$package_name]->release_type;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
@@ -470,7 +510,7 @@ class GPM extends Iterator
 | 
			
		||||
     */
 | 
			
		||||
    public function getRepositoryPlugins()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository['plugins'] ?? null;
 | 
			
		||||
        return $this->getRepository()['plugins'] ?? null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -493,7 +533,7 @@ class GPM extends Iterator
 | 
			
		||||
     */
 | 
			
		||||
    public function getRepositoryThemes()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository['themes'] ?? null;
 | 
			
		||||
        return $this->getRepository()['themes'] ?? null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -504,9 +544,31 @@ class GPM extends Iterator
 | 
			
		||||
     */
 | 
			
		||||
    public function getRepository()
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $this->repository) {
 | 
			
		||||
            try {
 | 
			
		||||
                $this->repository = new Remote\Packages($this->refresh, $this->callback);
 | 
			
		||||
            } catch (Exception $e) {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->repository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns Grav version available in the repository
 | 
			
		||||
     *
 | 
			
		||||
     * @return Remote\GravCore|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getGrav()
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $this->grav) {
 | 
			
		||||
            try {
 | 
			
		||||
                $this->grav = new Remote\GravCore($this->refresh, $this->callback);
 | 
			
		||||
            } catch (Exception $e) {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->grav;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Searches for a Package in the repository
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
 | 
			
		||||
namespace Grav\Common;
 | 
			
		||||
 | 
			
		||||
use Composer\Autoload\ClassLoader;
 | 
			
		||||
use Grav\Common\Config\Config;
 | 
			
		||||
use Grav\Common\Config\Setup;
 | 
			
		||||
use Grav\Common\Helpers\Exif;
 | 
			
		||||
@@ -152,6 +153,13 @@ class Grav extends Container
 | 
			
		||||
    {
 | 
			
		||||
        if (null === self::$instance) {
 | 
			
		||||
            self::$instance = static::load($values);
 | 
			
		||||
 | 
			
		||||
            /** @var ClassLoader|null $loader */
 | 
			
		||||
            $loader = self::$instance['loader'] ?? null;
 | 
			
		||||
            if ($loader) {
 | 
			
		||||
                // Load fix for Deferred Twig Extension
 | 
			
		||||
                $loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true);
 | 
			
		||||
            }
 | 
			
		||||
        } elseif ($values) {
 | 
			
		||||
            $instance = self::$instance;
 | 
			
		||||
            foreach ($values as $key => $value) {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,11 +20,13 @@ use Grav\Common\Security;
 | 
			
		||||
use Grav\Common\Utils;
 | 
			
		||||
use Grav\Framework\Filesystem\Filesystem;
 | 
			
		||||
use Grav\Framework\Form\FormFlashFile;
 | 
			
		||||
use Grav\Framework\Mime\MimeTypes;
 | 
			
		||||
use Psr\Http\Message\UploadedFileInterface;
 | 
			
		||||
use RocketTheme\Toolbox\File\YamlFile;
 | 
			
		||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 | 
			
		||||
use RuntimeException;
 | 
			
		||||
use function dirname;
 | 
			
		||||
use function in_array;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implements media upload and delete functionality.
 | 
			
		||||
@@ -179,16 +181,20 @@ trait MediaUploadTrait
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $grav = Grav::instance();
 | 
			
		||||
        /** @var MimeTypes $mimeChecker */
 | 
			
		||||
        $mimeChecker = $grav['mime'];
 | 
			
		||||
 | 
			
		||||
        // Handle Accepted file types. Accept can only be mime types (image/png | image/*) or file extensions (.pdf | .jpg)
 | 
			
		||||
        $accepted = false;
 | 
			
		||||
        $errors = [];
 | 
			
		||||
        // Do not trust mime type sent by the browser.
 | 
			
		||||
        $mime = Utils::getMimeByFilename($filename);
 | 
			
		||||
        $mimeTest = $metadata['mime'] ?? $mime;
 | 
			
		||||
        if ($mime !== $mimeTest) {
 | 
			
		||||
        $mime = $metadata['mime'] ?? $mimeChecker->getMimeType($extension);
 | 
			
		||||
        $validExtensions = $mimeChecker->getExtensions($mime);
 | 
			
		||||
        if (!in_array($extension, $validExtensions, true)) {
 | 
			
		||||
            throw new RuntimeException('The mime type does not match to file extension', 400);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $accepted = false;
 | 
			
		||||
        $errors = [];
 | 
			
		||||
        foreach ((array)$settings['accept'] as $type) {
 | 
			
		||||
            // Force acceptance of any file when star notation
 | 
			
		||||
            if ($type === '*') {
 | 
			
		||||
@@ -418,6 +424,17 @@ trait MediaUploadTrait
 | 
			
		||||
        $uploadedFile->moveTo($filepath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get upload settings.
 | 
			
		||||
     *
 | 
			
		||||
     * @param array|null $settings Form field specific settings (override).
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    public function getUploadSettings(?array $settings = null): array
 | 
			
		||||
    {
 | 
			
		||||
        return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Internal logic to copy file.
 | 
			
		||||
     *
 | 
			
		||||
@@ -604,17 +621,6 @@ trait MediaUploadTrait
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get upload settings.
 | 
			
		||||
     *
 | 
			
		||||
     * @param array|null $settings Form field specific settings (override).
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    protected function getUploadSettings(?array $settings = null): array
 | 
			
		||||
    {
 | 
			
		||||
        return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $filename
 | 
			
		||||
     * @param string $path
 | 
			
		||||
 
 | 
			
		||||
@@ -145,6 +145,18 @@ class Collection extends Iterator implements PageCollectionInterface
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set current page.
 | 
			
		||||
     */
 | 
			
		||||
    public function setCurrent(string $path): void
 | 
			
		||||
    {
 | 
			
		||||
        reset($this->items);
 | 
			
		||||
 | 
			
		||||
        while (($key = key($this->items)) !== null && $key !== $path) {
 | 
			
		||||
            next($this->items);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns current page.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -102,12 +102,13 @@ class Media extends AbstractMedia
 | 
			
		||||
 | 
			
		||||
        foreach ($iterator as $file => $info) {
 | 
			
		||||
            // Ignore folders and Markdown files.
 | 
			
		||||
            if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) {
 | 
			
		||||
            $filename = $info->getFilename();
 | 
			
		||||
            if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Find out what type we're dealing with
 | 
			
		||||
            [$basename, $ext, $type, $extra] = $this->getFileParts($info->getFilename());
 | 
			
		||||
            [$basename, $ext, $type, $extra] = $this->getFileParts($filename);
 | 
			
		||||
 | 
			
		||||
            if (!in_array(strtolower($ext), $media_types, true)) {
 | 
			
		||||
                continue;
 | 
			
		||||
 
 | 
			
		||||
@@ -105,12 +105,12 @@ class InitializeProcessor extends ProcessorBase
 | 
			
		||||
        // TODO: remove in 2.0.
 | 
			
		||||
        $this->container['accounts'];
 | 
			
		||||
 | 
			
		||||
        // Initialize session.
 | 
			
		||||
        $this->initializeSession($config);
 | 
			
		||||
 | 
			
		||||
        // Initialize URI (uses session, see issue #3269).
 | 
			
		||||
        $this->initializeUri($config);
 | 
			
		||||
 | 
			
		||||
        // Initialize session.
 | 
			
		||||
        $this->initializeSession($config);
 | 
			
		||||
 | 
			
		||||
        // Grav may return redirect response right away.
 | 
			
		||||
        $redirectCode = (int)$config->get('system.pages.redirect_trailing_slash', 1);
 | 
			
		||||
        if ($redirectCode) {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,8 @@
 | 
			
		||||
namespace Grav\Common\Processors;
 | 
			
		||||
 | 
			
		||||
use Grav\Common\Page\Interfaces\PageInterface;
 | 
			
		||||
use Grav\Framework\RequestHandler\Exception\RequestException;
 | 
			
		||||
use Grav\Plugin\Form\Forms;
 | 
			
		||||
use RocketTheme\Toolbox\Event\Event;
 | 
			
		||||
use Psr\Http\Message\ResponseInterface;
 | 
			
		||||
use Psr\Http\Message\ServerRequestInterface;
 | 
			
		||||
@@ -47,8 +49,17 @@ class PagesProcessor extends ProcessorBase
 | 
			
		||||
        $page = $this->container['page'];
 | 
			
		||||
 | 
			
		||||
        if (!$page->routable()) {
 | 
			
		||||
            $exception = new RequestException($request, 'Page Not Found', 404);
 | 
			
		||||
            $route = $this->container['route'];
 | 
			
		||||
            // If no page found, fire event
 | 
			
		||||
            $event = new Event(['page' => $page]);
 | 
			
		||||
            $event = new Event([
 | 
			
		||||
                'page' => $page,
 | 
			
		||||
                'code' => $exception->getCode(),
 | 
			
		||||
                'message' => $exception->getMessage(),
 | 
			
		||||
                'exception' => $exception,
 | 
			
		||||
                'route' => $route,
 | 
			
		||||
                'request' => $request
 | 
			
		||||
            ]);
 | 
			
		||||
            $event->page = null;
 | 
			
		||||
            $event = $this->container->fireEvent('onPageNotFound', $event);
 | 
			
		||||
 | 
			
		||||
@@ -65,12 +76,18 @@ class PagesProcessor extends ProcessorBase
 | 
			
		||||
 | 
			
		||||
            $task = $this->container['task'];
 | 
			
		||||
            $action = $this->container['action'];
 | 
			
		||||
 | 
			
		||||
            /** @var Forms $forms */
 | 
			
		||||
            $forms = $this->container['forms'] ?? null;
 | 
			
		||||
            $form = $forms ? $forms->getActiveForm() : null;
 | 
			
		||||
 | 
			
		||||
            $options = ['page' => $page, 'form' => $form, 'request' => $request];
 | 
			
		||||
            if ($task) {
 | 
			
		||||
                $event = new Event(['task' => $task, 'page' => $page]);
 | 
			
		||||
                $event = new Event(['task' => $task] + $options);
 | 
			
		||||
                $this->container->fireEvent('onPageTask', $event);
 | 
			
		||||
                $this->container->fireEvent('onPageTask.' . $task, $event);
 | 
			
		||||
            } elseif ($action) {
 | 
			
		||||
                $event = new Event(['action' => $action, 'page' => $page]);
 | 
			
		||||
                $event = new Event(['action' => $action] + $options);
 | 
			
		||||
                $this->container->fireEvent('onPageAction', $event);
 | 
			
		||||
                $this->container->fireEvent('onPageAction.' . $action, $event);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -390,7 +390,9 @@ class Job
 | 
			
		||||
        if (count($this->outputTo) > 0) {
 | 
			
		||||
            foreach ($this->outputTo as $file) {
 | 
			
		||||
                $output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX;
 | 
			
		||||
                file_put_contents($file, $this->output, $output_mode);
 | 
			
		||||
                $timestamp = (new DateTime('now'))->format('c');
 | 
			
		||||
                $output = $timestamp . "\n" . str_pad('', strlen($timestamp), '>') . "\n" . $this->output;
 | 
			
		||||
                file_put_contents($file, $output, $output_mode);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ namespace Grav\Common;
 | 
			
		||||
use enshrined\svgSanitize\Sanitizer;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Grav\Common\Config\Config;
 | 
			
		||||
use Grav\Common\Filesystem\Folder;
 | 
			
		||||
use Grav\Common\Page\Pages;
 | 
			
		||||
use function chr;
 | 
			
		||||
use function count;
 | 
			
		||||
@@ -56,9 +57,16 @@ class Security
 | 
			
		||||
            $original_svg = file_get_contents($file);
 | 
			
		||||
            $clean_svg = $sanitizer->sanitize($original_svg);
 | 
			
		||||
 | 
			
		||||
            // TODO: what to do with bad SVG files which return false?
 | 
			
		||||
            if ($clean_svg !== false && $clean_svg !== $original_svg) {
 | 
			
		||||
            // Quarantine bad SVG files and throw exception
 | 
			
		||||
            if ($clean_svg !== false ) {
 | 
			
		||||
                file_put_contents($file, $clean_svg);
 | 
			
		||||
            } else {
 | 
			
		||||
                $quarantine_file = basename($file);
 | 
			
		||||
                $quarantine_dir = 'log://quarantine';
 | 
			
		||||
                Folder::mkdir($quarantine_dir);
 | 
			
		||||
                file_put_contents("$quarantine_dir/$quarantine_file", $original_svg);
 | 
			
		||||
                unlink($file);
 | 
			
		||||
                throw new Exception('SVG could not be sanitized, it has been moved to the logs/quarantine folder');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ use Grav\Common\Config\Config;
 | 
			
		||||
use Grav\Common\Config\ConfigFileFinder;
 | 
			
		||||
use Grav\Common\Config\Setup;
 | 
			
		||||
use Grav\Common\Language\Language;
 | 
			
		||||
use Grav\Framework\Mime\MimeTypes;
 | 
			
		||||
use Pimple\Container;
 | 
			
		||||
use Pimple\ServiceProviderInterface;
 | 
			
		||||
use RocketTheme\Toolbox\File\YamlFile;
 | 
			
		||||
@@ -56,6 +57,19 @@ class ConfigServiceProvider implements ServiceProviderInterface
 | 
			
		||||
            return $config;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $container['mime'] = function ($c) {
 | 
			
		||||
            /** @var Config $config */
 | 
			
		||||
            $config = $c['config'];
 | 
			
		||||
            $mimes = $config->get('mime.types', []);
 | 
			
		||||
            foreach ($config->get('media.types', []) as $ext => $media) {
 | 
			
		||||
                if (!empty($media['mime'])) {
 | 
			
		||||
                    $mimes[$ext] = array_unique(array_merge([$media['mime']], $mimes[$ext] ?? []));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return MimeTypes::createFromMimes($mimes);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $container['languages'] = function ($c) {
 | 
			
		||||
            return static::languages($c);
 | 
			
		||||
        };
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ namespace Grav\Common\Service;
 | 
			
		||||
use Grav\Common\Grav;
 | 
			
		||||
use Pimple\Container;
 | 
			
		||||
use Pimple\ServiceProviderInterface;
 | 
			
		||||
use Psr\Http\Message\ServerRequestInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class TaskServiceProvider
 | 
			
		||||
@@ -26,7 +27,11 @@ class TaskServiceProvider implements ServiceProviderInterface
 | 
			
		||||
    public function register(Container $container)
 | 
			
		||||
    {
 | 
			
		||||
        $container['task'] = function (Grav $c) {
 | 
			
		||||
            $task = $_POST['task'] ?? $c['uri']->param('task');
 | 
			
		||||
            /** @var ServerRequestInterface $request */
 | 
			
		||||
            $request = $c['request'];
 | 
			
		||||
            $body = $request->getParsedBody();
 | 
			
		||||
 | 
			
		||||
            $task = $body['task'] ?? $c['uri']->param('task');
 | 
			
		||||
            if (null !== $task) {
 | 
			
		||||
                $task = filter_var($task, FILTER_SANITIZE_STRING);
 | 
			
		||||
            }
 | 
			
		||||
@@ -35,7 +40,11 @@ class TaskServiceProvider implements ServiceProviderInterface
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $container['action'] = function (Grav $c) {
 | 
			
		||||
            $action = $_POST['action'] ?? $c['uri']->param('action');
 | 
			
		||||
            /** @var ServerRequestInterface $request */
 | 
			
		||||
            $request = $c['request'];
 | 
			
		||||
            $body = $request->getParsedBody();
 | 
			
		||||
 | 
			
		||||
            $action = $body['action'] ?? $c['uri']->param('action');
 | 
			
		||||
            if (null !== $action) {
 | 
			
		||||
                $action = filter_var($action, FILTER_SANITIZE_STRING);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ namespace Grav\Common;
 | 
			
		||||
use Grav\Common\Form\FormFlash;
 | 
			
		||||
use Grav\Events\SessionStartEvent;
 | 
			
		||||
use Grav\Plugin\Form\Forms;
 | 
			
		||||
use JsonException;
 | 
			
		||||
use function is_string;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -148,10 +149,11 @@ class Session extends \Grav\Framework\Session\Session
 | 
			
		||||
     * @param mixed $object
 | 
			
		||||
     * @param int $time
 | 
			
		||||
     * @return $this
 | 
			
		||||
     * @throws JsonException
 | 
			
		||||
     */
 | 
			
		||||
    public function setFlashCookieObject($name, $object, $time = 60)
 | 
			
		||||
    {
 | 
			
		||||
        setcookie($name, json_encode($object), time() + $time, '/');
 | 
			
		||||
        setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time));
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
@@ -161,13 +163,15 @@ class Session extends \Grav\Framework\Session\Session
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $name
 | 
			
		||||
     * @return mixed|null
 | 
			
		||||
     * @throws JsonException
 | 
			
		||||
     */
 | 
			
		||||
    public function getFlashCookieObject($name)
 | 
			
		||||
    {
 | 
			
		||||
        if (isset($_COOKIE[$name])) {
 | 
			
		||||
            $object = json_decode($_COOKIE[$name], false);
 | 
			
		||||
            setcookie($name, '', time() - 3600, '/');
 | 
			
		||||
            return $object;
 | 
			
		||||
            $cookie = $_COOKIE[$name];
 | 
			
		||||
            setcookie($name, '', $this->getCookieOptions(-42000));
 | 
			
		||||
 | 
			
		||||
            return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								system/src/Grav/Common/Twig/Exception/TwigException.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								system/src/Grav/Common/Twig/Exception/TwigException.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @package    Grav\Common\Twig\Exception
 | 
			
		||||
 *
 | 
			
		||||
 * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
 | 
			
		||||
 * @license    MIT License; see LICENSE file for details.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Grav\Common\Twig\Exception;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TwigException gets thrown when you use {% throw code message %} in twig.
 | 
			
		||||
 *
 | 
			
		||||
 * This allows Grav to catch 401, 403 and 404 exceptions and display proper error page.
 | 
			
		||||
 */
 | 
			
		||||
class TwigException extends \RuntimeException
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
@@ -155,6 +155,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
 | 
			
		||||
            new TwigFilter('bool', [$this, 'boolFilter']),
 | 
			
		||||
            new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
 | 
			
		||||
            new TwigFilter('array', [$this, 'arrayFilter']),
 | 
			
		||||
            new TwigFilter('yaml', [$this, 'yamlFilter']),
 | 
			
		||||
 | 
			
		||||
            // Object Types
 | 
			
		||||
            new TwigFilter('get_type', [$this, 'getTypeFunc']),
 | 
			
		||||
@@ -807,6 +808,17 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
 | 
			
		||||
        return (array)$input;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array|object $value
 | 
			
		||||
     * @param int|null $inline
 | 
			
		||||
     * @param int|null $indent
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function yamlFilter($value, $inline = null, $indent = null): string
 | 
			
		||||
    {
 | 
			
		||||
        return Yaml::dump($value, $inline, $indent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param Environment $twig
 | 
			
		||||
     * @return string
 | 
			
		||||
@@ -1499,7 +1511,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //Look for existing class
 | 
			
		||||
            $svg = preg_replace_callback('/^<svg[^>]*(class=\")([^"]*)(\")[^>]*>/', function($matches) use ($classes, &$matched) {
 | 
			
		||||
            $svg = preg_replace_callback('/^<svg[^>]*(class=\"([^"]*)\")[^>]*>/', function($matches) use ($classes, &$matched) {
 | 
			
		||||
                if (isset($matches[2])) {
 | 
			
		||||
                    $new_classes = $matches[2] . $classes;
 | 
			
		||||
                    $matched = true;
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ class TwigNodeThrow extends Node
 | 
			
		||||
        $compiler->addDebugInfo($this);
 | 
			
		||||
 | 
			
		||||
        $compiler
 | 
			
		||||
            ->write('throw new \RuntimeException(')
 | 
			
		||||
            ->write('throw new \Grav\Common\Twig\Exception\TwigException(')
 | 
			
		||||
            ->subcompile($this->getNode('message'))
 | 
			
		||||
            ->write(', ')
 | 
			
		||||
            ->write($this->getAttribute('code') ?: 500)
 | 
			
		||||
 
 | 
			
		||||
@@ -49,16 +49,15 @@ class TwigNodeTryCatch extends Node
 | 
			
		||||
 | 
			
		||||
        $compiler
 | 
			
		||||
            ->indent()
 | 
			
		||||
            ->subcompile($this->getNode('try'));
 | 
			
		||||
            ->subcompile($this->getNode('try'))
 | 
			
		||||
            ->outdent()
 | 
			
		||||
            ->write('} catch (\Exception $e) {' . "\n")
 | 
			
		||||
            ->indent()
 | 
			
		||||
            ->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
 | 
			
		||||
            ->write('$context[\'e\'] = $e;' . "\n");
 | 
			
		||||
 | 
			
		||||
        if ($this->hasNode('catch')) {
 | 
			
		||||
            $compiler
 | 
			
		||||
                ->outdent()
 | 
			
		||||
                ->write('} catch (\Exception $e) {' . "\n")
 | 
			
		||||
                ->indent()
 | 
			
		||||
                ->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
 | 
			
		||||
                ->write('$context[\'e\'] = $e;' . "\n")
 | 
			
		||||
                ->subcompile($this->getNode('catch'));
 | 
			
		||||
            $compiler->subcompile($this->getNode('catch'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $compiler
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ use Grav\Common\Language\Language;
 | 
			
		||||
use Grav\Common\Language\LanguageCodes;
 | 
			
		||||
use Grav\Common\Page\Interfaces\PageInterface;
 | 
			
		||||
use Grav\Common\Page\Pages;
 | 
			
		||||
use Grav\Common\Twig\Exception\TwigException;
 | 
			
		||||
use Grav\Common\Twig\Extension\FilesystemExtension;
 | 
			
		||||
use Grav\Common\Twig\Extension\GravExtension;
 | 
			
		||||
use Grav\Common\Utils;
 | 
			
		||||
@@ -26,6 +27,7 @@ use RuntimeException;
 | 
			
		||||
use Twig\Cache\FilesystemCache;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
use Twig\Error\LoaderError;
 | 
			
		||||
use Twig\Error\RuntimeError;
 | 
			
		||||
use Twig\Extension\CoreExtension;
 | 
			
		||||
use Twig\Extension\DebugExtension;
 | 
			
		||||
use Twig\Extension\StringLoaderExtension;
 | 
			
		||||
@@ -404,38 +406,63 @@ class Twig
 | 
			
		||||
     */
 | 
			
		||||
    public function processSite($format = null, array $vars = [])
 | 
			
		||||
    {
 | 
			
		||||
        // set the page now its been processed
 | 
			
		||||
        $this->grav->fireEvent('onTwigSiteVariables');
 | 
			
		||||
        /** @var Pages $pages */
 | 
			
		||||
        $pages = $this->grav['pages'];
 | 
			
		||||
        /** @var PageInterface $page */
 | 
			
		||||
        $page = $this->grav['page'];
 | 
			
		||||
        $content = $page->content();
 | 
			
		||||
 | 
			
		||||
        $twig_vars = $this->twig_vars;
 | 
			
		||||
 | 
			
		||||
        $twig_vars['theme'] = $this->grav['config']->get('theme');
 | 
			
		||||
        $twig_vars['pages'] = $pages->root();
 | 
			
		||||
        $twig_vars['page'] = $page;
 | 
			
		||||
        $twig_vars['header'] = $page->header();
 | 
			
		||||
        $twig_vars['media'] = $page->media();
 | 
			
		||||
        $twig_vars['content'] = $content;
 | 
			
		||||
 | 
			
		||||
        // determine if params are set, if so disable twig cache
 | 
			
		||||
        $params = $this->grav['uri']->params(null, true);
 | 
			
		||||
        if (!empty($params)) {
 | 
			
		||||
            $this->twig->setCache(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get Twig template layout
 | 
			
		||||
        $template = $this->getPageTwigTemplate($page, $format);
 | 
			
		||||
        $page->templateFormat($format);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $grav = $this->grav;
 | 
			
		||||
 | 
			
		||||
            // set the page now its been processed
 | 
			
		||||
            $grav->fireEvent('onTwigSiteVariables');
 | 
			
		||||
 | 
			
		||||
            /** @var Pages $pages */
 | 
			
		||||
            $pages = $grav['pages'];
 | 
			
		||||
 | 
			
		||||
            /** @var PageInterface $page */
 | 
			
		||||
            $page = $grav['page'];
 | 
			
		||||
 | 
			
		||||
            $twig_vars = $this->twig_vars;
 | 
			
		||||
            $twig_vars['theme'] = $grav['config']->get('theme');
 | 
			
		||||
            $twig_vars['pages'] = $pages->root();
 | 
			
		||||
            $twig_vars['page'] = $page;
 | 
			
		||||
            $twig_vars['header'] = $page->header();
 | 
			
		||||
            $twig_vars['media'] = $page->media();
 | 
			
		||||
            $twig_vars['content'] = $page->content();
 | 
			
		||||
 | 
			
		||||
            // determine if params are set, if so disable twig cache
 | 
			
		||||
            $params = $grav['uri']->params(null, true);
 | 
			
		||||
            if (!empty($params)) {
 | 
			
		||||
                $this->twig->setCache(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get Twig template layout
 | 
			
		||||
            $template = $this->getPageTwigTemplate($page, $format);
 | 
			
		||||
            $page->templateFormat($format);
 | 
			
		||||
 | 
			
		||||
            $output = $this->twig->render($template, $vars + $twig_vars);
 | 
			
		||||
        } catch (LoaderError $e) {
 | 
			
		||||
            $error_msg = $e->getMessage();
 | 
			
		||||
            throw new RuntimeException($error_msg, 400, $e);
 | 
			
		||||
            throw new RuntimeException($e->getMessage(), 400, $e);
 | 
			
		||||
        } catch (RuntimeError $e) {
 | 
			
		||||
            $prev = $e->getPrevious();
 | 
			
		||||
            if ($prev instanceof TwigException) {
 | 
			
		||||
                $code = $prev->getCode() ?: 500;
 | 
			
		||||
                // Fire onPageNotFound event.
 | 
			
		||||
                $event = new Event([
 | 
			
		||||
                    'page' => $page,
 | 
			
		||||
                    'code' => $code,
 | 
			
		||||
                    'message' => $prev->getMessage(),
 | 
			
		||||
                    'exception' => $prev,
 | 
			
		||||
                    'route' => $grav['route'],
 | 
			
		||||
                    'request' => $grav['request']
 | 
			
		||||
                ]);
 | 
			
		||||
                $event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event);
 | 
			
		||||
                $newPage = $event['page'];
 | 
			
		||||
                if ($newPage && $newPage !== $page) {
 | 
			
		||||
                    unset($grav['page']);
 | 
			
		||||
                    $grav['page'] = $newPage;
 | 
			
		||||
 | 
			
		||||
                    return $this->processSite($newPage->templateFormat(), $vars);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            throw $e;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $output;
 | 
			
		||||
 
 | 
			
		||||
@@ -675,10 +675,15 @@ class Uri
 | 
			
		||||
     */
 | 
			
		||||
    public static function ip()
 | 
			
		||||
    {
 | 
			
		||||
        $ip = 'UNKNOWN';
 | 
			
		||||
 | 
			
		||||
        if (getenv('HTTP_CLIENT_IP')) {
 | 
			
		||||
            $ip = getenv('HTTP_CLIENT_IP');
 | 
			
		||||
        } elseif (getenv('HTTP_CF_CONNECTING_IP')) {
 | 
			
		||||
            $ip = getenv('HTTP_CF_CONNECTING_IP');
 | 
			
		||||
        } elseif (getenv('HTTP_X_FORWARDED_FOR') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
 | 
			
		||||
            $ip = getenv('HTTP_X_FORWARDED_FOR');
 | 
			
		||||
            $ips = array_map('trim', explode(',', getenv('HTTP_X_FORWARDED_FOR')));
 | 
			
		||||
            $ip = array_shift($ips);
 | 
			
		||||
        } elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
 | 
			
		||||
            $ip = getenv('HTTP_X_FORWARDED');
 | 
			
		||||
        } elseif (getenv('HTTP_FORWARDED_FOR')) {
 | 
			
		||||
@@ -687,8 +692,6 @@ class Uri
 | 
			
		||||
            $ip = getenv('HTTP_FORWARDED');
 | 
			
		||||
        } elseif (getenv('REMOTE_ADDR')) {
 | 
			
		||||
            $ip = getenv('REMOTE_ADDR');
 | 
			
		||||
        } else {
 | 
			
		||||
            $ip = 'UNKNOWN';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $ip;
 | 
			
		||||
@@ -1258,7 +1261,7 @@ class Uri
 | 
			
		||||
            $this->port = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($this->hasStandardPort()) {
 | 
			
		||||
        if ($this->port === 0 || $this->hasStandardPort()) {
 | 
			
		||||
            $this->port = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -1311,11 +1314,13 @@ class Uri
 | 
			
		||||
        if ($parts === false) {
 | 
			
		||||
            throw new RuntimeException('Malformed URL: ' . $url);
 | 
			
		||||
        }
 | 
			
		||||
        $port = (int)($parts['port'] ?? 0);
 | 
			
		||||
 | 
			
		||||
        $this->scheme = $parts['scheme'] ?? null;
 | 
			
		||||
        $this->user = $parts['user'] ?? null;
 | 
			
		||||
        $this->password = $parts['pass'] ?? null;
 | 
			
		||||
        $this->host = $parts['host'] ?? null;
 | 
			
		||||
        $this->port = isset($parts['port']) ? (int)$parts['port'] : null;
 | 
			
		||||
        $this->port = $port ?: null;
 | 
			
		||||
        $this->path = $parts['path'] ?? '';
 | 
			
		||||
        $this->query = $parts['query'] ?? '';
 | 
			
		||||
        $this->fragment = $parts['fragment'] ?? null;
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ use function is_callable;
 | 
			
		||||
 * @package Grav\Framework\Flex
 | 
			
		||||
 * @template T
 | 
			
		||||
 */
 | 
			
		||||
class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
 | 
			
		||||
class FlexDirectory implements FlexDirectoryInterface
 | 
			
		||||
{
 | 
			
		||||
    use FlexAuthorizeTrait;
 | 
			
		||||
 | 
			
		||||
@@ -235,7 +235,17 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
 | 
			
		||||
 | 
			
		||||
        /** @var UniformResourceLocator $locator */
 | 
			
		||||
        $locator = $grav['locator'];
 | 
			
		||||
        $filename = $locator->findResource($this->getDirectoryConfigUri($name), true);
 | 
			
		||||
        $uri = $this->getDirectoryConfigUri($name);
 | 
			
		||||
 | 
			
		||||
        // If configuration is found in main configuration, use it.
 | 
			
		||||
        if (str_starts_with($uri, 'config://')) {
 | 
			
		||||
            $path = str_replace('/', '.', substr($uri, 9, -5));
 | 
			
		||||
 | 
			
		||||
            return (array)$grav['config']->get($path);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Load the configuration file.
 | 
			
		||||
        $filename = $locator->findResource($uri, true);
 | 
			
		||||
        if ($filename === false) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
@@ -821,20 +831,46 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
 | 
			
		||||
     * @param array $call
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    protected function dynamicFlexField(array &$field, $property, array $call)
 | 
			
		||||
    protected function dynamicFlexField(array &$field, $property, array $call): void
 | 
			
		||||
    {
 | 
			
		||||
        $params = (array)$call['params'];
 | 
			
		||||
        $object = $call['object'] ?? null;
 | 
			
		||||
        $method = array_shift($params);
 | 
			
		||||
        $not = false;
 | 
			
		||||
        if (str_starts_with($method, '!')) {
 | 
			
		||||
            $method = substr($method, 1);
 | 
			
		||||
            $not = true;
 | 
			
		||||
        } elseif (str_starts_with($method, 'not ')) {
 | 
			
		||||
            $method = substr($method, 4);
 | 
			
		||||
            $not = true;
 | 
			
		||||
        }
 | 
			
		||||
        $method = trim($method);
 | 
			
		||||
 | 
			
		||||
        if ($object && method_exists($object, $method)) {
 | 
			
		||||
            $value = $object->{$method}(...$params);
 | 
			
		||||
            if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
 | 
			
		||||
                $field[$property] = array_merge_recursive($field[$property], $value);
 | 
			
		||||
                $value = $this->mergeArrays($field[$property], $value);
 | 
			
		||||
            }
 | 
			
		||||
            $field[$property] = $not ? !$value : $value;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array $array1
 | 
			
		||||
     * @param array $array2
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    protected function mergeArrays(array $array1, array $array2): array
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($array2 as $key => $value) {
 | 
			
		||||
            if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
 | 
			
		||||
                $array1[$key] = $this->mergeArrays($array1[$key], $value);
 | 
			
		||||
            } else {
 | 
			
		||||
                $field[$property] = $value;
 | 
			
		||||
                $array1[$key] = $value;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $array1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -318,11 +318,11 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $field
 | 
			
		||||
     * @param string $filename
 | 
			
		||||
     * @param string|null $field
 | 
			
		||||
     * @param string|null $filename
 | 
			
		||||
     * @return Route|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getFileDeleteAjaxRoute($field, $filename): ?Route
 | 
			
		||||
    public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
@@ -453,7 +453,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
 | 
			
		||||
    protected function doSerialize(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->doTraitSerialize() + [
 | 
			
		||||
                'form' => $this->form,
 | 
			
		||||
                'directory' => $this->directory,
 | 
			
		||||
                'flexName' => $this->flexName
 | 
			
		||||
            ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -465,7 +467,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
 | 
			
		||||
    {
 | 
			
		||||
        $this->doTraitUnserialize($data);
 | 
			
		||||
 | 
			
		||||
        $this->form = $data['form'];
 | 
			
		||||
        $this->directory = $data['directory'];
 | 
			
		||||
        $this->flexName = $data['flexName'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,14 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
 | 
			
		||||
    {
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
        $this->setObject($object);
 | 
			
		||||
        $this->setName($object->getFlexType(), $name);
 | 
			
		||||
 | 
			
		||||
        if (isset($options['form']['name'])) {
 | 
			
		||||
            // Use custom form name.
 | 
			
		||||
            $this->flexName = $options['form']['name'];
 | 
			
		||||
        } else {
 | 
			
		||||
            // Use standard form name.
 | 
			
		||||
            $this->setName($object->getFlexType(), $name);
 | 
			
		||||
        }
 | 
			
		||||
        $this->setId($this->getName());
 | 
			
		||||
 | 
			
		||||
        $uniqueId = $options['unique_id'] ?? null;
 | 
			
		||||
@@ -371,22 +378,28 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
 | 
			
		||||
    {
 | 
			
		||||
        $object = $this->getObject();
 | 
			
		||||
        if (!method_exists($object, 'route')) {
 | 
			
		||||
            return null;
 | 
			
		||||
            /** @var Route $route */
 | 
			
		||||
            $route = Grav::instance()['route'];
 | 
			
		||||
 | 
			
		||||
            return $route->withExtension('json')->withGravParam('task', 'media.upload');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $object->route('/edit.json/task:media.upload');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $field
 | 
			
		||||
     * @param string $filename
 | 
			
		||||
     * @param string|null $field
 | 
			
		||||
     * @param string|null $filename
 | 
			
		||||
     * @return Route|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getFileDeleteAjaxRoute($field, $filename): ?Route
 | 
			
		||||
    public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
 | 
			
		||||
    {
 | 
			
		||||
        $object = $this->getObject();
 | 
			
		||||
        if (!method_exists($object, 'route')) {
 | 
			
		||||
            return null;
 | 
			
		||||
            /** @var Route $route */
 | 
			
		||||
            $route = Grav::instance()['route'];
 | 
			
		||||
 | 
			
		||||
            return $route->withExtension('json')->withGravParam('task', 'media.delete');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $object->route('/edit.json/task:media.delete');
 | 
			
		||||
@@ -536,7 +549,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
 | 
			
		||||
    protected function doSerialize(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->doTraitSerialize() + [
 | 
			
		||||
                'items' => $this->items,
 | 
			
		||||
                'form' => $this->form,
 | 
			
		||||
                'object' => $this->object,
 | 
			
		||||
                'flexName' => $this->flexName,
 | 
			
		||||
                'submitMethod' => $this->submitMethod,
 | 
			
		||||
            ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -548,7 +565,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
 | 
			
		||||
    {
 | 
			
		||||
        $this->doTraitUnserialize($data);
 | 
			
		||||
 | 
			
		||||
        $this->object = $data['object'];
 | 
			
		||||
        $this->items = $data['items'] ?? null;
 | 
			
		||||
        $this->form = $data['form'] ?? null;
 | 
			
		||||
        $this->object = $data['object'] ?? null;
 | 
			
		||||
        $this->flexName = $data['flexName'] ?? null;
 | 
			
		||||
        $this->submitMethod = $data['submitMethod'] ?? null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,7 @@ use function is_array;
 | 
			
		||||
use function is_object;
 | 
			
		||||
use function is_scalar;
 | 
			
		||||
use function is_string;
 | 
			
		||||
use function json_encode;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class FlexObject
 | 
			
		||||
@@ -70,6 +71,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 | 
			
		||||
    /** @var array */
 | 
			
		||||
    private $_meta;
 | 
			
		||||
    /** @var array */
 | 
			
		||||
    protected $_original;
 | 
			
		||||
    /** @var array */
 | 
			
		||||
    protected $_changes;
 | 
			
		||||
    /** @var string */
 | 
			
		||||
    protected $storage_key;
 | 
			
		||||
@@ -369,7 +372,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function searchProperty(string $property, string $search, array $options = null): float
 | 
			
		||||
    {
 | 
			
		||||
        $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
 | 
			
		||||
        $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
 | 
			
		||||
        $value = $this->getProperty($property);
 | 
			
		||||
 | 
			
		||||
        return $this->searchValue($property, $value, $search, $options);
 | 
			
		||||
@@ -383,7 +386,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function searchNestedProperty(string $property, string $search, array $options = null): float
 | 
			
		||||
    {
 | 
			
		||||
        $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
 | 
			
		||||
        $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
 | 
			
		||||
        if ($property === 'key') {
 | 
			
		||||
            $value = $this->getKey();
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -440,6 +443,16 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get original data before update
 | 
			
		||||
     *
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    public function getOriginalData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->_original ?? [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get any changes based on data sent to update
 | 
			
		||||
     *
 | 
			
		||||
@@ -653,7 +666,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Store the changes
 | 
			
		||||
            $this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements);
 | 
			
		||||
            $this->_original = $this->getElements();
 | 
			
		||||
            $this->_changes = Utils::arrayDiffMultidimensional($this->_original, $elements);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($files && method_exists($this, 'setUpdatedMedia')) {
 | 
			
		||||
@@ -691,6 +705,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 | 
			
		||||
        return $this->create($key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param UserInterface|null $user
 | 
			
		||||
     */
 | 
			
		||||
    public function check(UserInterface $user = null): void
 | 
			
		||||
    {
 | 
			
		||||
        // If user has been provided, check if the user has permissions to save this object.
 | 
			
		||||
        if ($user && !$this->isAuthorized('save', null, $user)) {
 | 
			
		||||
            throw new \RuntimeException('Forbidden', 403);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritdoc}
 | 
			
		||||
     * @see FlexObjectInterface::save()
 | 
			
		||||
@@ -809,11 +834,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function getForm(string $name = '', array $options = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (!isset($this->_forms[$name])) {
 | 
			
		||||
            $this->_forms[$name] = $this->createFormObject($name, $options);
 | 
			
		||||
        $hash = $name . '-' . md5(json_encode($options, JSON_THROW_ON_ERROR));
 | 
			
		||||
        if (!isset($this->_forms[$hash])) {
 | 
			
		||||
            $this->_forms[$hash] = $this->createFormObject($name, $options);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->_forms[$name];
 | 
			
		||||
        return $this->_forms[$hash];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -1063,6 +1089,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
 | 
			
		||||
        return $action;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Method to reset blueprints if the type changes.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     * @since 1.7.18
 | 
			
		||||
     */
 | 
			
		||||
    protected function resetBlueprints(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->_blueprint = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // DEPRECATED METHODS
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ use Grav\Framework\Cache\CacheInterface;
 | 
			
		||||
 * Interface FlexDirectoryInterface
 | 
			
		||||
 * @package Grav\Framework\Flex\Interfaces
 | 
			
		||||
 */
 | 
			
		||||
interface FlexDirectoryInterface
 | 
			
		||||
interface FlexDirectoryInterface extends FlexAuthorizeInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @return bool
 | 
			
		||||
 
 | 
			
		||||
@@ -38,8 +38,8 @@ interface FlexFormInterface extends Serializable, FormInterface
 | 
			
		||||
    /**
 | 
			
		||||
     * Get route for deleting files by AJAX.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $field     Field where the file is associated into.
 | 
			
		||||
     * @param string $filename  Filename for the file.
 | 
			
		||||
     * @param string|null $field     Field where the file is associated into.
 | 
			
		||||
     * @param string|null $filename  Filename for the file.
 | 
			
		||||
     * @return Route|null       Returns Route object or null if file uploads are not enabled.
 | 
			
		||||
     */
 | 
			
		||||
    public function getFileDeleteAjaxRoute($field, $filename);
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,8 @@ class FolderStorage extends AbstractFilesystemStorage
 | 
			
		||||
    protected $dataFolder;
 | 
			
		||||
    /** @var string Pattern to access an object. */
 | 
			
		||||
    protected $dataPattern = '{FOLDER}/{KEY}/{FILE}{EXT}';
 | 
			
		||||
    /** @var string[] */
 | 
			
		||||
    protected $variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s'];
 | 
			
		||||
    /** @var string Filename for the object. */
 | 
			
		||||
    protected $dataFile;
 | 
			
		||||
    /** @var string File extension for the object. */
 | 
			
		||||
@@ -380,6 +382,12 @@ class FolderStorage extends AbstractFilesystemStorage
 | 
			
		||||
            if (isset($data[0])) {
 | 
			
		||||
                throw new RuntimeException('Broken object file');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add key field to the object.
 | 
			
		||||
            $keyField = $this->keyField;
 | 
			
		||||
            if ($keyField !== 'storage_key' && !isset($data[$keyField])) {
 | 
			
		||||
                $data[$keyField] = $key;
 | 
			
		||||
            }
 | 
			
		||||
        } catch (RuntimeException $e) {
 | 
			
		||||
            $data = ['__ERROR' => $e->getMessage()];
 | 
			
		||||
        } finally {
 | 
			
		||||
@@ -692,9 +700,7 @@ class FolderStorage extends AbstractFilesystemStorage
 | 
			
		||||
        $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);
 | 
			
		||||
 | 
			
		||||
        $pattern = Utils::simpleTemplate($pattern, $this->variables);
 | 
			
		||||
        if (!$pattern) {
 | 
			
		||||
            throw new RuntimeException('Bad storage folder pattern');
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -455,7 +455,7 @@ class SimpleStorage extends AbstractFilesystemStorage
 | 
			
		||||
        $content = (array) $file->content();
 | 
			
		||||
        if ($this->prefix) {
 | 
			
		||||
            $data = new Data($content);
 | 
			
		||||
            $content = $data->get($this->prefix);
 | 
			
		||||
            $content = $data->get($this->prefix, []);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $file->free();
 | 
			
		||||
 
 | 
			
		||||
@@ -120,7 +120,7 @@ trait FlexMediaTrait
 | 
			
		||||
        // Load settings for the field.
 | 
			
		||||
        $schema = $this->getBlueprint()->schema();
 | 
			
		||||
        $settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null;
 | 
			
		||||
        if (!isset($settings) || !is_array($settings)) {
 | 
			
		||||
        if (!is_array($settings)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -120,7 +120,7 @@ class FormFlash implements FormFlashInterface
 | 
			
		||||
    protected function loadStoredForm(): ?array
 | 
			
		||||
    {
 | 
			
		||||
        $file = $this->getTmpIndex();
 | 
			
		||||
        $exists = $file->exists();
 | 
			
		||||
        $exists = $file && $file->exists();
 | 
			
		||||
 | 
			
		||||
        $data = null;
 | 
			
		||||
        if ($exists) {
 | 
			
		||||
@@ -246,8 +246,10 @@ class FormFlash implements FormFlashInterface
 | 
			
		||||
        if ($force || $this->data || $this->files) {
 | 
			
		||||
            // Only save if there is data or files to be saved.
 | 
			
		||||
            $file = $this->getTmpIndex();
 | 
			
		||||
            $file->save($this->jsonSerialize());
 | 
			
		||||
            $this->exists = true;
 | 
			
		||||
            if ($file) {
 | 
			
		||||
                $file->save($this->jsonSerialize());
 | 
			
		||||
                $this->exists = true;
 | 
			
		||||
            }
 | 
			
		||||
        } elseif ($this->exists) {
 | 
			
		||||
            // Delete empty form flash if it exists (it carries no information).
 | 
			
		||||
            return $this->delete();
 | 
			
		||||
@@ -476,12 +478,14 @@ class FormFlash implements FormFlashInterface
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return YamlFile
 | 
			
		||||
     * @return ?YamlFile
 | 
			
		||||
     */
 | 
			
		||||
    protected function getTmpIndex(): YamlFile
 | 
			
		||||
    protected function getTmpIndex(): ?YamlFile
 | 
			
		||||
    {
 | 
			
		||||
        $tmpDir = $this->getTmpDir();
 | 
			
		||||
 | 
			
		||||
        // Do not use CompiledYamlFile as the file can change multiple times per second.
 | 
			
		||||
        return YamlFile::instance($this->getTmpDir() . '/index.yaml');
 | 
			
		||||
        return $tmpDir ? YamlFile::instance($tmpDir . '/index.yaml') : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -503,7 +507,9 @@ class FormFlash implements FormFlashInterface
 | 
			
		||||
    {
 | 
			
		||||
        // Make sure that index file cache gets always cleared.
 | 
			
		||||
        $file = $this->getTmpIndex();
 | 
			
		||||
        $file->free();
 | 
			
		||||
        if ($file) {
 | 
			
		||||
            $file->free();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $tmpDir = $this->getTmpDir();
 | 
			
		||||
        if ($tmpDir && file_exists($tmpDir)) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										107
									
								
								system/src/Grav/Framework/Mime/MimeTypes.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								system/src/Grav/Framework/Mime/MimeTypes.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
<?php declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @package    Grav\Framework\Mime
 | 
			
		||||
 *
 | 
			
		||||
 * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
 | 
			
		||||
 * @license    MIT License; see LICENSE file for details.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Grav\Framework\Mime;
 | 
			
		||||
 | 
			
		||||
use function in_array;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class to handle mime-types.
 | 
			
		||||
 */
 | 
			
		||||
class MimeTypes
 | 
			
		||||
{
 | 
			
		||||
    /** @var array */
 | 
			
		||||
    protected $extensions;
 | 
			
		||||
    /** @var array */
 | 
			
		||||
    protected $mimes;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new mime types instance with the given mappings.
 | 
			
		||||
     *
 | 
			
		||||
     * @param array $mimes An associative array containing ['ext' => ['mime/type', 'mime/type2']]
 | 
			
		||||
     */
 | 
			
		||||
    public static function createFromMimes(array $mimes): self
 | 
			
		||||
    {
 | 
			
		||||
        $extensions = [];
 | 
			
		||||
        foreach ($mimes as $ext => $list) {
 | 
			
		||||
            foreach ($list as $mime) {
 | 
			
		||||
                $list = $extensions[$mime] ?? [];
 | 
			
		||||
                if (!in_array($ext, $list, true)) {
 | 
			
		||||
                    $list[] = $ext;
 | 
			
		||||
                    $extensions[$mime] = $list;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new static($extensions, $mimes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $extension
 | 
			
		||||
     * @return string|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getMimeType(string $extension): ?string
 | 
			
		||||
    {
 | 
			
		||||
        $extension = $this->cleanInput($extension);
 | 
			
		||||
 | 
			
		||||
        return $this->mimes[$extension][0] ?? null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $mime
 | 
			
		||||
     * @return string|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getExtension(string $mime): ?string
 | 
			
		||||
    {
 | 
			
		||||
        $mime = $this->cleanInput($mime);
 | 
			
		||||
 | 
			
		||||
        return $this->extensions[$mime][0] ?? null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $extension
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    public function getMimeTypes(string $extension): array
 | 
			
		||||
    {
 | 
			
		||||
        $extension = $this->cleanInput($extension);
 | 
			
		||||
 | 
			
		||||
        return $this->mimes[$extension] ?? [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $mime
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    public function getExtensions(string $mime): array
 | 
			
		||||
    {
 | 
			
		||||
        $mime = $this->cleanInput($mime);
 | 
			
		||||
 | 
			
		||||
        return $this->extensions[$mime] ?? [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $input
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    protected function cleanInput(string $input): string
 | 
			
		||||
    {
 | 
			
		||||
        return strtolower(trim($input));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array $extensions
 | 
			
		||||
     * @param array $mimes
 | 
			
		||||
     */
 | 
			
		||||
    protected function __construct(array $extensions, array $mimes)
 | 
			
		||||
    {
 | 
			
		||||
        $this->extensions = $extensions;
 | 
			
		||||
        $this->mimes = $mimes;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,7 +9,6 @@
 | 
			
		||||
 | 
			
		||||
namespace Grav\Framework\Object;
 | 
			
		||||
 | 
			
		||||
use Doctrine\Common\Collections\Collection;
 | 
			
		||||
use Doctrine\Common\Collections\Criteria;
 | 
			
		||||
use Grav\Framework\Collection\ArrayCollection;
 | 
			
		||||
use Grav\Framework\Object\Access\NestedPropertyCollectionTrait;
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,9 @@ class UploadedFile implements UploadedFileInterface
 | 
			
		||||
{
 | 
			
		||||
    use UploadedFileDecoratorTrait;
 | 
			
		||||
 | 
			
		||||
    /** @var array */
 | 
			
		||||
    private $meta = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param StreamInterface|string|resource $streamOrFile
 | 
			
		||||
     * @param int                             $size
 | 
			
		||||
@@ -34,4 +37,34 @@ class UploadedFile implements UploadedFileInterface
 | 
			
		||||
    {
 | 
			
		||||
        $this->uploadedFile = new \Nyholm\Psr7\UploadedFile($streamOrFile, $size, $errorStatus, $clientFilename, $clientMediaType);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array $meta
 | 
			
		||||
     * @return $this
 | 
			
		||||
     */
 | 
			
		||||
    public function setMeta(array $meta)
 | 
			
		||||
    {
 | 
			
		||||
        $this->meta = $meta;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array $meta
 | 
			
		||||
     * @return $this
 | 
			
		||||
     */
 | 
			
		||||
    public function addMeta(array $meta)
 | 
			
		||||
    {
 | 
			
		||||
        $this->meta = array_merge($this->meta, $meta);
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    public function getMeta(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->meta;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -338,23 +338,12 @@ class Session implements SessionInterface
 | 
			
		||||
    {
 | 
			
		||||
        $name = $this->getName();
 | 
			
		||||
        if (null !== $name) {
 | 
			
		||||
            $params = session_get_cookie_params();
 | 
			
		||||
 | 
			
		||||
            $cookie_options = array (
 | 
			
		||||
                'expires'  => time() - 42000,
 | 
			
		||||
                'path'     => $params['path'],
 | 
			
		||||
                'domain'   => $params['domain'],
 | 
			
		||||
                'secure'   => $params['secure'],
 | 
			
		||||
                'httponly' => $params['httponly'],
 | 
			
		||||
                'samesite' => $params['samesite']
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            $this->removeCookie();
 | 
			
		||||
 | 
			
		||||
            setcookie(
 | 
			
		||||
                session_name(),
 | 
			
		||||
                '',
 | 
			
		||||
                $cookie_options
 | 
			
		||||
                $this->getCookieOptions(-42000)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -463,27 +452,36 @@ class Session implements SessionInterface
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return void
 | 
			
		||||
     * Store something in cookie temporarily.
 | 
			
		||||
     *
 | 
			
		||||
     * @param int|null $lifetime
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    protected function setCookie(): void
 | 
			
		||||
    public function getCookieOptions(int $lifetime = null): array
 | 
			
		||||
    {
 | 
			
		||||
        $params = session_get_cookie_params();
 | 
			
		||||
 | 
			
		||||
        $cookie_options = array (
 | 
			
		||||
            'expires'  => time() + $params['lifetime'],
 | 
			
		||||
        return [
 | 
			
		||||
            'expires'  => time() + ($lifetime ?? $params['lifetime']),
 | 
			
		||||
            'path'     => $params['path'],
 | 
			
		||||
            'domain'   => $params['domain'],
 | 
			
		||||
            'secure'   => $params['secure'],
 | 
			
		||||
            'httponly' => $params['httponly'],
 | 
			
		||||
            'samesite' => $params['samesite']
 | 
			
		||||
        );
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    protected function setCookie(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->removeCookie();
 | 
			
		||||
 | 
			
		||||
        setcookie(
 | 
			
		||||
            session_name(),
 | 
			
		||||
            session_id(),
 | 
			
		||||
            $cookie_options
 | 
			
		||||
            $this->getCookieOptions()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,70 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
// Fix too many ob_get_clean() calls when exception is thrown inside the template.
 | 
			
		||||
 | 
			
		||||
namespace Phive\Twig\Extensions\Deferred;
 | 
			
		||||
 | 
			
		||||
class DeferredExtension extends \Twig_Extension
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    private $blocks = array();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritdoc}
 | 
			
		||||
     */
 | 
			
		||||
    public function getTokenParsers()
 | 
			
		||||
    {
 | 
			
		||||
        return array(new DeferredTokenParser());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritdoc}
 | 
			
		||||
     */
 | 
			
		||||
    public function getNodeVisitors()
 | 
			
		||||
    {
 | 
			
		||||
        return array(new DeferredNodeVisitor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritdoc}
 | 
			
		||||
     */
 | 
			
		||||
    public function getName()
 | 
			
		||||
    {
 | 
			
		||||
        return 'deferred';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function defer(\Twig_Template $template, $blockName)
 | 
			
		||||
    {
 | 
			
		||||
        ob_start();
 | 
			
		||||
        $templateName = $template->getTemplateName();
 | 
			
		||||
        $this->blocks[$templateName][] = [ob_get_level(), $blockName];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function resolve(\Twig_Template $template, array $context, array $blocks)
 | 
			
		||||
    {
 | 
			
		||||
        $templateName = $template->getTemplateName();
 | 
			
		||||
        if (empty($this->blocks[$templateName])) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        while ($block = array_pop($this->blocks[$templateName])) {
 | 
			
		||||
            [$level, $blockName] = $block;
 | 
			
		||||
            if (ob_get_level() !== $level) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $buffer = ob_get_clean();
 | 
			
		||||
 | 
			
		||||
            $blocks[$blockName] = array($template, 'block_'.$blockName.'_deferred');
 | 
			
		||||
            $template->displayBlock($blockName, $context, $blocks);
 | 
			
		||||
 | 
			
		||||
            echo $buffer;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($parent = $template->getParent($context)) {
 | 
			
		||||
            $this->resolve($parent, $context, $blocks);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user