diff --git a/CHANGELOG.md b/CHANGELOG.md index e9725a3d0..c62a2fc48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ 1. [](#bugfix) * Fixed `FlexUser` loosing ACL information * Fixed `FlexUser::find()` breaking when nothing is found + * Fixed `FlexObject::update()` removing fields which aren't allowed by ACL # v1.6.0-rc.3 ## 02/18/2019 diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index d3a7ff0ed..24720c783 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -121,15 +121,31 @@ class Blueprint extends BlueprintForm * * @param array $data * @param bool $missingValuesAsNull + * @param bool $keepEmptyValues * @return array */ - public function filter(array $data, bool $missingValuesAsNull = false) + public function filter(array $data, bool $missingValuesAsNull = false, bool $keepEmptyValues = false) { $this->initInternals(); - return $this->blueprintSchema->filter($data, $missingValuesAsNull); + return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues); } + + /** + * Flatten data by using blueprints. + * + * @param array $data + * @return array + */ + public function flattenData(array $data) + { + $this->initInternals(); + + return $this->blueprintSchema->flattenData($data); + } + + /** * Return blueprint data schema. * diff --git a/system/src/Grav/Common/Data/BlueprintSchema.php b/system/src/Grav/Common/Data/BlueprintSchema.php index 7d79455c2..667ef9454 100644 --- a/system/src/Grav/Common/Data/BlueprintSchema.php +++ b/system/src/Grav/Common/Data/BlueprintSchema.php @@ -79,11 +79,51 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface * * @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) + public function filter(array $data, $missingValuesAsNull = false, $keepEmptyValues = false) { - return $this->filterArray($data, $this->nested, $missingValuesAsNull); + return $this->filterArray($data, $this->nested, $missingValuesAsNull, $keepEmptyValues); + } + + /** + * Flatten data by using blueprints. + * + * @param array $data Data to be flattened. + * @return array + */ + public function flattenData(array $data) + { + return $this->flattenArray($data, $this->nested, ''); + } + + /** + * @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; } /** @@ -124,9 +164,10 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface * @param array $data * @param array $rules * @param bool $missingValuesAsNull + * @param bool $keepEmptyValues * @return array */ - protected function filterArray(array $data, array $rules, $missingValuesAsNull) + protected function filterArray(array $data, array $rules, $missingValuesAsNull, $keepEmptyValues) { $results = []; @@ -138,7 +179,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface $rule = \is_string($val) ? $this->items[$val] : null; if (empty($rule['validate']['ignore'])) { - $results[$key] = null; + continue; } } } @@ -152,26 +193,28 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface // Item has been defined in blueprints. if (!empty($rule['validate']['ignore'])) { // Skip any data in the ignored field. + unset($results[$key]); continue; } $field = Validation::filter($field, $rule); } elseif (\is_array($field) && \is_array($val)) { // Array has been defined in blueprints. - $field = $this->filterArray($field, $val, $missingValuesAsNull); + $field = $this->filterArray($field, $val, $missingValuesAsNull, $keepEmptyValues); + } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') { - $field = null; + // Skip any extra data. + continue; } - if (null !== $field && (!\is_array($field) || !empty($field))) { + if ($keepEmptyValues || (null !== $field && (!\is_array($field) || !empty($field)))) { $results[$key] = $field; } } - return $results; + return $results ?: null; } - /** * @param array $data * @param array $toggles diff --git a/system/src/Grav/Common/Data/Data.php b/system/src/Grav/Common/Data/Data.php index 6fd33a819..76f38762e 100644 --- a/system/src/Grav/Common/Data/Data.php +++ b/system/src/Grav/Common/Data/Data.php @@ -197,13 +197,15 @@ class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable } /** - * @param bool $missingValuesAsNull * @return $this - * Filter all items by using blueprints. */ - public function filter(bool $missingValuesAsNull = false) + public function filter() { - $this->items = $this->blueprints()->filter($this->items, $missingValuesAsNull); + $args = func_get_args(); + $missingValuesAsNull = (bool)(array_shift($args) ?: false); + $keepEmptyValues = (bool)(array_shift($args) ?: false); + + $this->items = $this->blueprints()->filter($this->items, $missingValuesAsNull, $keepEmptyValues); return $this; } diff --git a/system/src/Grav/Common/User/FlexUser/User.php b/system/src/Grav/Common/User/FlexUser/User.php index c2ef855e2..357b145e7 100644 --- a/system/src/Grav/Common/User/FlexUser/User.php +++ b/system/src/Grav/Common/User/FlexUser/User.php @@ -150,48 +150,6 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa return $this; } - /** - * @param array $data - * @param array $files - * @return $this - * @throws ValidationException - */ - public function update(array $data, array $files = []) - { - if ($data) { - // Filter updated data. - $this->filterElements($data); - - // Merge data to the existing object. - $elements = $this->getElements(); - - // Validate and filter the incoming data. - $blueprint = $this->getFlexDirectory()->getBlueprint(); - - // Merge existing object to the data. - $data = $blueprint->mergeData($elements, $data); - - // Validate and filter elements and throw an error if any issues were found. - $blueprint->validate($data + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()]); - $data = $blueprint->filter($data); - - // Make sure that we add missing (filtered by ACL) elements back. - $data = $blueprint->mergeData($elements, $data); - - // Store the changes - $this->_changes = Utils::arrayDiffMultidimensional($data, $this->getElements()); - - // Finally update the object. - $this->setElements($data); - } - - if ($files && method_exists($this, 'setUpdatedMedia')) { - $this->setUpdatedMedia($files); - } - - return $this; - } - /** * Get value from a page variable (used mostly for creating edit forms). * diff --git a/system/src/Grav/Framework/Flex/FlexForm.php b/system/src/Grav/Framework/Flex/FlexForm.php index 22e131be6..a5838b855 100644 --- a/system/src/Grav/Framework/Flex/FlexForm.php +++ b/system/src/Grav/Framework/Flex/FlexForm.php @@ -255,7 +255,7 @@ class FlexForm implements FlexFormInterface protected function filterData(\ArrayAccess $data): void { if ($data instanceof Data) { - $data->filter(true); + $data->filter(true, true); } } } diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php index 9558bdd76..c04ca55c3 100644 --- a/system/src/Grav/Framework/Flex/FlexObject.php +++ b/system/src/Grav/Framework/Flex/FlexObject.php @@ -117,27 +117,32 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface public function update(array $data, array $files = []) { if ($data) { - // Filter updated data. + $blueprint = $this->getBlueprint(); + + // Process updated data through the object filters. $this->filterElements($data); - // Merge data to the existing object. + // Get currently stored data. $elements = $this->getElements(); - // Validate and filter the incoming data. - $blueprint = $this->getFlexDirectory()->getBlueprint(); - - // Merge existing object to the data. - $data = $blueprint->mergeData($elements, $data); + // Merge existing object to the test data to be validated. + $test = $blueprint->mergeData($elements, $data); // Validate and filter elements and throw an error if any issues were found. - $blueprint->validate($data + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()]); - $data = $blueprint->filter($data); - - // Store the changes - $this->_changes = Utils::arrayDiffMultidimensional($data, $this->getElements()); + $blueprint->validate($test + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()]); + $data = $blueprint->filter($data, false, true); // Finally update the object. - $this->setElements($data); + foreach ($blueprint->flattenData($data) as $key => $value) { + if ($value === null) { + $this->unsetNestedProperty($key); + } else { + $this->setNestedProperty($key, $value); + } + } + + // Store the changes + $this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements); } if ($files && method_exists($this, 'setUpdatedMedia')) {