diff --git a/CHANGELOG.md b/CHANGELOG.md index a619b6395..789fcd4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Added `Grav\Framework\Collection\AbstractIndexCollection` class * Added `Grav\Framework\Object\ObjectIndex` class * Added `Grav\Framework\Flex` classes + * Added proper support for hiding form fields in blueprints by using dynamic property like `security@: admin.foobar` to any field 1. [](#improved) * Doctrine filecache is now namespaced with prefix to support purging diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index 740b339d5..b48581b6f 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -10,6 +10,7 @@ namespace Grav\Common\Data; use Grav\Common\File\CompiledYamlFile; use Grav\Common\Grav; +use Grav\Common\User\User; use RocketTheme\Toolbox\Blueprints\BlueprintForm; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; @@ -252,4 +253,45 @@ class Blueprint extends BlueprintForm $field[$property] = $config; } } + + /** + * @param array $field + * @param string $property + * @param array $call + */ + protected function dynamicSecurity(array &$field, $property, array &$call) + { + if ($property) { + return; + } + + $grav = Grav::instance(); + $actions = (array)$call['params']; + + /** @var User $user */ + if (isset($grav['user'])) { + $user = Grav::instance()['user'] ?? null; + foreach ($actions as $action) { + if (!$user->authorize($action)) { + $this->addPropertyRecursive($field, 'validate', ['ignore' => true]); + return; + } + } + } + } + + protected function addPropertyRecursive(array &$field, $property, $value) + { + if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) { + $field[$property] = array_merge_recursive($field[$property], $value); + } else { + $field[$property] = $value; + } + + if (!empty($field['fields'])) { + foreach ($field['fields'] as $key => &$child) { + $this->addPropertyRecursive($child, $property, $value); + } + } + } } diff --git a/system/src/Grav/Common/Data/BlueprintSchema.php b/system/src/Grav/Common/Data/BlueprintSchema.php index 410309fe7..67a8021de 100644 --- a/system/src/Grav/Common/Data/BlueprintSchema.php +++ b/system/src/Grav/Common/Data/BlueprintSchema.php @@ -49,20 +49,20 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface /** * Filter data by using blueprints. * - * @param array $data + * @param array $data Incoming data, for example from a form. + * @param bool $missingValuesAsNull Include missing values as nulls. * @return array */ - public function filter(array $data) + public function filter(array $data, $missingValuesAsNull = false) { - return $this->filterArray($data, $this->nested); + return $this->filterArray($data, $this->nested, $missingValuesAsNull); } /** * @param array $data * @param array $rules - * @returns array + * @return array * @throws \RuntimeException - * @internal */ protected function validateArray(array $data, array $rules) { @@ -74,6 +74,11 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface if ($rule) { // Item has been defined in blueprints. + if (!empty($rule['validate']['ignore'])) { + // Skip validation in the ignored field. + continue; + } + $messages += Validation::validate($field, $rule); } elseif (\is_array($field) && \is_array($val)) { // Array has been defined in blueprints. @@ -90,22 +95,42 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface /** * @param array $data * @param array $rules + * @param bool $missingValuesAsNull * @return array - * @internal */ - protected function filterArray(array $data, array $rules) + protected function filterArray(array $data, array $rules, $missingValuesAsNull) { $results = []; + + if ($missingValuesAsNull) { + // First pass is to fill up all the fields with null. This is done to lock the ordering of the fields. + foreach ($rules as $key => $rule) { + if ($key && !isset($results[$key])) { + $val = $rules[$key] ?? $rules['*'] ?? null; + $rule = \is_string($val) ? $this->items[$val] : null; + + if (empty($rule['validate']['ignore'])) { + $results[$key] = null; + } + } + } + } + foreach ($data as $key => $field) { $val = $rules[$key] ?? $rules['*'] ?? null; $rule = \is_string($val) ? $this->items[$val] : null; if ($rule) { // Item has been defined in blueprints. + if (!empty($rule['validate']['ignore'])) { + // Skip any data in the ignored field. + continue; + } + $field = Validation::filter($field, $rule); } elseif (\is_array($field) && \is_array($val)) { // Array has been defined in blueprints. - $field = $this->filterArray($field, $val); + $field = $this->filterArray($field, $val, $missingValuesAsNull); } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') { $field = null; } @@ -133,6 +158,13 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface } $field = $this->items[$field]; + + // Skip ignored field, it will not be required. + if (!empty($field['validate']['ignore'])) { + continue; + } + + // Check if required. if (isset($field['validate']['required']) && $field['validate']['required'] === true) { diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php index dd6db49a8..944e8cb72 100644 --- a/system/src/Grav/Framework/Flex/FlexObject.php +++ b/system/src/Grav/Framework/Flex/FlexObject.php @@ -45,12 +45,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface /** @var FlexDirectory */ private $_flexDirectory; - /** @var string */ - private $storageKey; - /** @var int */ - private $timestamp = 0; /** @var FlexForm[] */ - private $forms = []; + private $_forms = []; + /** @var string */ + private $_storageKey; + /** @var int */ + private $_timestamp = 0; /** * @return array @@ -116,18 +116,23 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function update(array $data, $isFullUpdate = false) { + // Validate and filter the incoming data. $blueprint = $this->getFlexDirectory()->getBlueprint(); + $blueprint->validate($data + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()]); + $data = $blueprint->filter($data); if (!$isFullUpdate) { + // Partial update: merge data to the existing object. $elements = $this->getElements(); $data = $blueprint->mergeData($elements, $data); } - $blueprint->validate($data + ['storage_key' => $this->getStorageKey()]); - $data = $blueprint->filter($data); - + // Filter object data. $this->filterElements($data); - $this->setElements($data); + + if ($data) { + $this->setElements($data); + } return $this; } @@ -165,11 +170,11 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function getForm($name = 'default') { - if (!isset($this->forms[$name])) { - $this->forms[$name] = new FlexForm($name, $this); + if (!isset($this->_forms[$name])) { + $this->_forms[$name] = new FlexForm($name, $this); } - return $this->forms[$name]; + return $this->_forms[$name]; } /** @@ -212,7 +217,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function getStorageKey() { - return $this->storageKey; + return $this->_storageKey; } /** @@ -221,7 +226,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function setStorageKey($key = null) { - $this->storageKey = $key; + $this->_storageKey = $key; return $this; } @@ -231,7 +236,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function getTimestamp() : int { - return $this->timestamp; + return $this->_timestamp; } /** @@ -240,7 +245,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function setTimestamp($timestamp = null) { - $this->timestamp = $timestamp ?? time(); + $this->_timestamp = $timestamp ?? time(); return $this; } @@ -485,8 +490,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface throw new \InvalidArgumentException("Cannot unserialize '{$type}': Not found"); } $this->_flexDirectory = $directory; - $this->storageKey = $serialized['storage_key']; - $this->timestamp = $serialized['storage_timestamp']; + $this->_storageKey = $serialized['storage_key']; + $this->_timestamp = $serialized['storage_timestamp']; $this->setKey($serialized['key']); $this->setElements($serialized['elements']); @@ -571,10 +576,10 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface protected function filterElements(array &$elements) { if (!empty($elements['storage_key'])) { - $this->storageKey = trim($elements['storage_key']); + $this->_storageKey = trim($elements['storage_key']); } if (!empty($elements['storage_timestamp'])) { - $this->timestamp = (int)$elements['storage_timestamp']; + $this->_timestamp = (int)$elements['storage_timestamp']; } unset ($elements['storage_key'], $elements['storage_timestamp']); diff --git a/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php index 2966f2bc8..728f8f188 100644 --- a/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php +++ b/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php @@ -19,7 +19,7 @@ use Grav\Common\User\User; */ trait FlexAuthorizeTrait { - private $authorize = '%s.flex-object.%s'; + private $_authorize = '%s.flex-object.%s'; public function authorize(string $action, string $scope = null) : bool { @@ -34,11 +34,11 @@ trait FlexAuthorizeTrait $action = $this->exists() ? 'update' : 'create'; } - return $user->authorize(sprintf($this->authorize, $scope, $action)) || $user->authorize('admin.super'); + return $user->authorize(sprintf($this->_authorize, $scope, $action)) || $user->authorize('admin.super'); } protected function setAuthorizeRule(string $authorize) : void { - $this->authorize = $authorize; + $this->_authorize = $authorize; } }