430 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			430 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
/**
 | 
						|
 * @package    Grav\Common\Data
 | 
						|
 *
 | 
						|
 * @copyright  Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
 | 
						|
 * @license    MIT License; see LICENSE file for details.
 | 
						|
 */
 | 
						|
 | 
						|
namespace Grav\Common\Data;
 | 
						|
 | 
						|
use Grav\Common\Config\Config;
 | 
						|
use Grav\Common\Grav;
 | 
						|
use RocketTheme\Toolbox\ArrayTraits\Export;
 | 
						|
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
 | 
						|
use RocketTheme\Toolbox\Blueprints\BlueprintSchema as BlueprintSchemaBase;
 | 
						|
use RuntimeException;
 | 
						|
use function is_array;
 | 
						|
use function is_string;
 | 
						|
 | 
						|
/**
 | 
						|
 * Class BlueprintSchema
 | 
						|
 * @package Grav\Common\Data
 | 
						|
 */
 | 
						|
class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
 | 
						|
{
 | 
						|
    use Export;
 | 
						|
 | 
						|
    /** @var array */
 | 
						|
    protected $filter = ['validation' => true, 'xss_check' => true];
 | 
						|
 | 
						|
    /** @var array */
 | 
						|
    protected $ignoreFormKeys = [
 | 
						|
        'title' => true,
 | 
						|
        'help' => true,
 | 
						|
        'placeholder' => true,
 | 
						|
        'placeholder_key' => true,
 | 
						|
        'placeholder_value' => true,
 | 
						|
        'fields' => true
 | 
						|
    ];
 | 
						|
 | 
						|
    /**
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function getTypes()
 | 
						|
    {
 | 
						|
        return $this->types;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param string $name
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function getType($name)
 | 
						|
    {
 | 
						|
        return $this->types[$name] ?? [];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param string $name
 | 
						|
     * @return array|null
 | 
						|
     */
 | 
						|
    public function getNestedRules(string $name)
 | 
						|
    {
 | 
						|
        return $this->getNested($name);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Validate data against blueprints.
 | 
						|
     *
 | 
						|
     * @param  array $data
 | 
						|
     * @param  array $options
 | 
						|
     * @return void
 | 
						|
     * @throws RuntimeException
 | 
						|
     */
 | 
						|
    public function validate(array $data, array $options = [])
 | 
						|
    {
 | 
						|
        try {
 | 
						|
            $validation = $this->items['']['form']['validation'] ?? 'loose';
 | 
						|
            $messages = $this->validateArray($data, $this->nested, $validation === 'strict', $options['xss_check'] ?? true);
 | 
						|
        } catch (RuntimeException $e) {
 | 
						|
            throw (new ValidationException($e->getMessage(), $e->getCode(), $e))->setMessages();
 | 
						|
        }
 | 
						|
 | 
						|
        if (!empty($messages)) {
 | 
						|
            throw (new ValidationException('', 400))->setMessages($messages);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $data
 | 
						|
     * @param array $toggles
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function processForm(array $data, array $toggles = [])
 | 
						|
    {
 | 
						|
        return $this->processFormRecursive($data, $toggles, $this->nested) ?? [];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Filter data by using blueprints.
 | 
						|
     *
 | 
						|
     * @param  array $data                  Incoming data, for example from a form.
 | 
						|
     * @param  bool  $missingValuesAsNull   Include missing values as nulls.
 | 
						|
     * @param bool   $keepEmptyValues       Include empty values.
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function filter(array $data, $missingValuesAsNull = false, $keepEmptyValues = false)
 | 
						|
    {
 | 
						|
        $this->buildIgnoreNested($this->nested);
 | 
						|
 | 
						|
        return $this->filterArray($data, $this->nested, '', $missingValuesAsNull, $keepEmptyValues) ?? [];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Flatten data by using blueprints.
 | 
						|
     *
 | 
						|
     * @param array $data       Data to be flattened.
 | 
						|
     * @param bool $includeAll  True if undefined properties should also be included.
 | 
						|
     * @param string $name      Property which will be flattened, useful for flattening repeating data.
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function flattenData(array $data, bool $includeAll = false, string $name = '')
 | 
						|
    {
 | 
						|
        $prefix = $name !== '' ? $name . '.' : '';
 | 
						|
 | 
						|
        $list = [];
 | 
						|
        if ($includeAll) {
 | 
						|
            $items = $name !== '' ? $this->getProperty($name)['fields'] ?? [] : $this->items;
 | 
						|
            foreach ($items as $key => $rules) {
 | 
						|
                $type = $rules['type'] ?? '';
 | 
						|
                $ignore = (bool) array_filter((array)($rules['validate']['ignore'] ?? [])) ?? false;
 | 
						|
                if (!str_starts_with($type, '_') && !str_contains($key, '*') && $ignore !== true) {
 | 
						|
                    $list[$prefix . $key] = null;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        $nested = $this->getNestedRules($name);
 | 
						|
 | 
						|
        return array_replace($list, $this->flattenArray($data, $nested, $prefix));
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $data
 | 
						|
     * @param array $rules
 | 
						|
     * @param string $prefix
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    protected function flattenArray(array $data, array $rules, string $prefix)
 | 
						|
    {
 | 
						|
        $array = [];
 | 
						|
 | 
						|
        foreach ($data as $key => $field) {
 | 
						|
            $val = $rules[$key] ?? $rules['*'] ?? null;
 | 
						|
            $rule = is_string($val) ? $this->items[$val] : null;
 | 
						|
 | 
						|
            if ($rule || isset($val['*'])) {
 | 
						|
                // Item has been defined in blueprints.
 | 
						|
                $array[$prefix.$key] = $field;
 | 
						|
            } elseif (is_array($field) && is_array($val)) {
 | 
						|
                // Array has been defined in blueprints.
 | 
						|
                $array += $this->flattenArray($field, $val, $prefix . $key . '.');
 | 
						|
            } else {
 | 
						|
                // Undefined/extra item.
 | 
						|
                $array[$prefix.$key] = $field;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $array;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $data
 | 
						|
     * @param array $rules
 | 
						|
     * @param bool $strict
 | 
						|
     * @param bool $xss
 | 
						|
     * @return array
 | 
						|
     * @throws RuntimeException
 | 
						|
     */
 | 
						|
    protected function validateArray(array $data, array $rules, bool $strict, bool $xss = true)
 | 
						|
    {
 | 
						|
        $messages = $this->checkRequired($data, $rules);
 | 
						|
 | 
						|
        foreach ($data as $key => $child) {
 | 
						|
            $val = $rules[$key] ?? $rules['*'] ?? null;
 | 
						|
            $rule = is_string($val) ? $this->items[$val] : null;
 | 
						|
            $checkXss = $xss;
 | 
						|
 | 
						|
            if ($rule) {
 | 
						|
                // Item has been defined in blueprints.
 | 
						|
                if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
 | 
						|
                    // Skip validation in the ignored field.
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                $messages += Validation::validate($child, $rule);
 | 
						|
 | 
						|
            } elseif (is_array($child) && is_array($val)) {
 | 
						|
                // Array has been defined in blueprints.
 | 
						|
                $messages += $this->validateArray($child, $val, $strict);
 | 
						|
                $checkXss = false;
 | 
						|
 | 
						|
            } elseif ($strict) {
 | 
						|
                // Undefined/extra item in strict mode.
 | 
						|
                /** @var Config $config */
 | 
						|
                $config = Grav::instance()['config'];
 | 
						|
                if (!$config->get('system.strict_mode.blueprint_strict_compat', true)) {
 | 
						|
                    throw new RuntimeException(sprintf('%s is not defined in blueprints', $key), 400);
 | 
						|
                }
 | 
						|
 | 
						|
                user_error(sprintf('Having extra key %s in your data is deprecated with blueprint having \'validation: strict\'', $key), E_USER_DEPRECATED);
 | 
						|
            }
 | 
						|
 | 
						|
            if ($checkXss) {
 | 
						|
                $messages += Validation::checkSafety($child, $rule ?: ['name' => $key]);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $messages;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $data
 | 
						|
     * @param array $rules
 | 
						|
     * @param string $parent
 | 
						|
     * @param bool  $missingValuesAsNull
 | 
						|
     * @param bool $keepEmptyValues
 | 
						|
     * @return array|null
 | 
						|
     */
 | 
						|
    protected function filterArray(array $data, array $rules, string $parent, bool $missingValuesAsNull, bool $keepEmptyValues)
 | 
						|
    {
 | 
						|
        $results = [];
 | 
						|
 | 
						|
        foreach ($data as $key => $field) {
 | 
						|
            $val = $rules[$key] ?? $rules['*'] ?? null;
 | 
						|
            $rule = is_string($val) ? $this->items[$val] : $this->items[$parent . $key] ?? null;
 | 
						|
 | 
						|
            if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
 | 
						|
                // Skip any data in the ignored field.
 | 
						|
                unset($results[$key]);
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            if (null === $field) {
 | 
						|
                if ($missingValuesAsNull) {
 | 
						|
                    $results[$key] = null;
 | 
						|
                } else {
 | 
						|
                    unset($results[$key]);
 | 
						|
                }
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            $isParent = isset($val['*']);
 | 
						|
            $type = $rule['type'] ?? null;
 | 
						|
 | 
						|
            if (!$isParent && $type && $type !== '_parent') {
 | 
						|
                $field = Validation::filter($field, $rule);
 | 
						|
            } elseif (is_array($field) && is_array($val)) {
 | 
						|
                // Array has been defined in blueprints.
 | 
						|
                $k = $isParent ? '*' : $key;
 | 
						|
                $field = $this->filterArray($field, $val, $parent . $k . '.', $missingValuesAsNull, $keepEmptyValues);
 | 
						|
 | 
						|
                if (null === $field) {
 | 
						|
                    // Nested parent has no values.
 | 
						|
                    unset($results[$key]);
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
            } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
 | 
						|
                // Skip any extra data.
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            if ($keepEmptyValues || (null !== $field && (!is_array($field) || !empty($field)))) {
 | 
						|
                $results[$key] = $field;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $results ?: null;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $nested
 | 
						|
     * @param string $parent
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    protected function buildIgnoreNested(array $nested, $parent = '')
 | 
						|
    {
 | 
						|
        $ignore = true;
 | 
						|
        foreach ($nested as $key => $val) {
 | 
						|
            $key = $parent . $key;
 | 
						|
            if (is_array($val)) {
 | 
						|
                $ignore = $this->buildIgnoreNested($val, $key . '.') && $ignore; // Keep the order!
 | 
						|
            } else {
 | 
						|
                $child = $this->items[$key] ?? null;
 | 
						|
                $ignore = $ignore && (!$child || !empty($child['disabled']) || !empty($child['validate']['ignore']));
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if ($ignore) {
 | 
						|
            $key = trim($parent, '.');
 | 
						|
            $this->items[$key]['validate']['ignore'] = true;
 | 
						|
        }
 | 
						|
 | 
						|
        return $ignore;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array|null $data
 | 
						|
     * @param array $toggles
 | 
						|
     * @param array $nested
 | 
						|
     * @return array|null
 | 
						|
     */
 | 
						|
    protected function processFormRecursive(?array $data, array $toggles, array $nested)
 | 
						|
    {
 | 
						|
        foreach ($nested as $key => $value) {
 | 
						|
            if ($key === '') {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
            if ($key === '*') {
 | 
						|
                // TODO: Add support to collections.
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
            if (is_array($value)) {
 | 
						|
                // Special toggle handling for all the nested data.
 | 
						|
                $toggle = $toggles[$key] ?? [];
 | 
						|
                if (!is_array($toggle)) {
 | 
						|
                    if (!$toggle) {
 | 
						|
                        $data[$key] = null;
 | 
						|
 | 
						|
                        continue;
 | 
						|
                    }
 | 
						|
 | 
						|
                    $toggle = [];
 | 
						|
                }
 | 
						|
                // Recursively fetch the items.
 | 
						|
                $childData = $data[$key] ?? null;
 | 
						|
                if (null !== $childData && !is_array($childData)) {
 | 
						|
                    throw new \RuntimeException(sprintf("Bad form data for field collection '%s': %s used instead of an array", $key, gettype($childData)));
 | 
						|
                }
 | 
						|
                $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value);
 | 
						|
            } else {
 | 
						|
                $field = $this->get($value);
 | 
						|
                // Do not add the field if:
 | 
						|
                if (
 | 
						|
                    // Not an input field
 | 
						|
                    !$field
 | 
						|
                    // Field has been disabled
 | 
						|
                    || !empty($field['disabled'])
 | 
						|
                    // Field validation is set to be ignored
 | 
						|
                    || !empty($field['validate']['ignore'])
 | 
						|
                    // Field is overridable and the toggle is turned off
 | 
						|
                    || (!empty($field['overridable']) && empty($toggles[$key]))
 | 
						|
                ) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
                if (!isset($data[$key])) {
 | 
						|
                    $data[$key] = null;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $data;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $data
 | 
						|
     * @param array $fields
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    protected function checkRequired(array $data, array $fields)
 | 
						|
    {
 | 
						|
        $messages = [];
 | 
						|
 | 
						|
        foreach ($fields as $name => $field) {
 | 
						|
            if (!is_string($field)) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            $field = $this->items[$field];
 | 
						|
 | 
						|
            // Skip ignored field, it will not be required.
 | 
						|
            if (!empty($field['disabled']) || !empty($field['validate']['ignore'])) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            // Skip overridable fields without value.
 | 
						|
            // TODO: We need better overridable support, which is not just ignoring required values but also looking if defaults are good.
 | 
						|
            if (!empty($field['overridable']) && !isset($data[$name])) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            // Check if required.
 | 
						|
            if (isset($field['validate']['required'])
 | 
						|
                && $field['validate']['required'] === true) {
 | 
						|
                if (isset($data[$name])) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
                if ($field['type'] === 'file' && isset($data['data']['name'][$name])) { //handle case of file input fields required
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                $value = $field['label'] ?? $field['name'];
 | 
						|
                $language = Grav::instance()['language'];
 | 
						|
                $message  = sprintf($language->translate('GRAV.FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value));
 | 
						|
                $messages[$field['name']][] = $message;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $messages;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $field
 | 
						|
     * @param string $property
 | 
						|
     * @param array $call
 | 
						|
     * @return void
 | 
						|
     */
 | 
						|
    protected function dynamicConfig(array &$field, $property, array &$call)
 | 
						|
    {
 | 
						|
        $value = $call['params'];
 | 
						|
 | 
						|
        $default = $field[$property] ?? null;
 | 
						|
        $config = Grav::instance()['config']->get($value, $default);
 | 
						|
 | 
						|
        if (null !== $config) {
 | 
						|
            $field[$property] = $config;
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |