123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- <?php
- /**
- * @package Grav\Common\Data
- *
- * @copyright Copyright (c) 2015 - 2022 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'] ?? '';
- if (!str_starts_with($type, '_') && !str_contains($key, '*')) {
- $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;
- }
- }
- }
|