Completed
Push — feature/EVO-7166-no-cascading-... ( 84752e...92993c )
by Narcotic
73:27 queued 68:05
created

RecordOriginConstraint::getChangedObjectPaths()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 21
ccs 0
cts 21
cp 0
rs 8.7624
cc 6
eloc 15
nc 7
nop 3
crap 42
1
<?php
2
/**
3
 * Schema constraint that validates the rules of recordOrigin (and possible exceptions)
4
 */
5
6
namespace Graviton\SchemaBundle\Constraint;
7
8
use Graviton\JsonSchemaBundle\Validator\Constraint\Event\ConstraintEventSchema;
9
use Symfony\Component\PropertyAccess\PropertyAccess;
10
11
/**
12
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
13
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
14
 * @link     http://swisscom.ch
15
 */
16
class RecordOriginConstraint
17
{
18
19
    /**
20
     * @var string
21
     */
22
    private $recordOriginField;
23
24
    /**
25
     * @var array
26
     */
27
    private $recordOriginBlacklist;
28
29
    /**
30
     * @var array
31
     */
32
    private $exceptionFieldMap;
33
34
    /**
35
     * @var array
36
     */
37
    private $changedObjectPaths;
38
39
    /**
40
     * RecordOriginConstraint constructor.
41
     *
42
     * @param ConstraintUtils $utils                 Utils
43
     * @param string          $recordOriginField     name of the recordOrigin field
44
     * @param array           $recordOriginBlacklist list of recordOrigin values that cannot be modified
45
     * @param array           $exceptionFieldMap     field map from compiler pass with excluded fields
46
     */
47
    public function __construct(
48
        ConstraintUtils $utils,
49
        $recordOriginField,
50
        array $recordOriginBlacklist,
51
        array $exceptionFieldMap
52
    ) {
53
        $this->utils = $utils;
54
        $this->recordOriginField = $recordOriginField;
55
        $this->recordOriginBlacklist = $recordOriginBlacklist;
56
        $this->exceptionFieldMap = $exceptionFieldMap;
57
    }
58
59
    /**
60
     * Checks the recordOrigin rules and sets error in event if needed
61
     *
62
     * @param ConstraintEventSchema $event event class
63
     *
64
     * @return void
65
     */
66
    public function checkRecordOrigin(ConstraintEventSchema $event)
67
    {
68
        $currentRecord = $this->utils->getCurrentEntity();
69
        $data = $event->getElement();
70
71
        // if no recordorigin set on saved record; we let it through
72
        if (is_null($currentRecord) || !isset($currentRecord->{$this->recordOriginField})) {
73
            // we have no current record.. but make sure user doesn't want to send the banned recordOrigin
74
            if (isset($data->{$this->recordOriginField}) &&
75
                !is_null($data->{$this->recordOriginField}) &&
76
                in_array($data->{$this->recordOriginField}, $this->recordOriginBlacklist)
77
            ) {
78
                $event->addError(
79
                    sprintf(
80
                        'Creating documents with the %s field having a value of %s is not permitted.',
81
                        $this->recordOriginField,
82
                        implode(', ', $this->recordOriginBlacklist)
83
                    ),
84
                    $this->recordOriginField
85
                );
86
                return;
87
            }
88
89
            return;
90
        }
91
92
        $recordOrigin = $currentRecord->{$this->recordOriginField};
93
94
        // not in the blacklist? can also go through..
95
        if (!in_array($recordOrigin, $this->recordOriginBlacklist)) {
96
            return;
97
        }
98
99
        // ok, user is trying to modify an object with blacklist recordorigin.. let's check fields
100
        $schema = $event->getSchema();
101
        $isAllowed = true;
102
103
        if (!isset($schema->{'x-documentClass'})) {
104
            // this should never happen but we need to check. if schema has no information to *check* our rules, we
105
            // MUST deny it in that case..
106
            $event->addError(
107
                'Internal error, not enough schema information to validate recordOrigin rules.',
108
                $this->recordOriginField
109
            );
110
            return;
111
        }
112
113
        $documentClass = $schema->{'x-documentClass'};
114
115
        if (!isset($this->exceptionFieldMap[$documentClass])) {
116
            // if he wants to edit on blacklist, but we have no exceptions, also deny..
117
            $isAllowed = false;
118
        } else {
119
            // so to check our exceptions, we remove it from both documents (the stored and the clients) and compare
120
            $exceptions = $this->exceptionFieldMap[$documentClass];
121
122
            $accessor = PropertyAccess::createPropertyAccessorBuilder()
123
                ->enableMagicCall()
124
                ->getPropertyAccessor();
125
126
            $storedObject = clone $currentRecord;
127
            $userObject = clone $data;
128
129
            foreach ($exceptions as $fieldName) {
130 View Code Duplication
                if ($accessor->isWritable($storedObject, $fieldName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
131
                    $accessor->setValue($storedObject, $fieldName, null);
132
                } else {
133
                    $this->addProperties($fieldName, $storedObject);
134
                }
135 View Code Duplication
                if ($accessor->isWritable($userObject, $fieldName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
136
                    $accessor->setValue($userObject, $fieldName, null);
137
                } else {
138
                    $this->addProperties($fieldName, $userObject);
139
                }
140
            }
141
142
            // so now all unimportant fields were set to null on both - they should match if rest is untouched ;-)
143
            if ($userObject != $storedObject) {
144
                $isAllowed = false;
145
                $this->getChangedObjectPaths($userObject, $storedObject);
146
            }
147
        }
148
149
        if (!$isAllowed) {
150
            $error = sprintf(
151
                'Prohibited modification attempt on record with %s of %s.',
152
                $this->recordOriginField,
153
                implode(', ', $this->recordOriginBlacklist)
154
            );
155
            // if there are recordCoreExceptions we can be more explicit
156
            if (isset($this->exceptionFieldMap[$documentClass]) && !empty($this->changedObjectPaths)) {
157
                $error.= sprintf(
158
                    ' You tried to change (%s), but you can only change (%s) by recordOriginException.',
159
                    implode(', ', $this->changedObjectPaths),
160
                    implode(', ', $this->exceptionFieldMap[$documentClass])
161
                );
162
            }
163
            $event->addError($error, $this->recordOriginField);
164
        }
165
166
        return;
167
    }
168
169
    /**
170
     * recursive helperfunction that walks through two arrays/objects of the same structure,
171
     * compares the values and writes the paths containining differences into the $this->changedObjectPaths
172
     *
173
     * @param   mixed $object        the first of the datastructures to compare
174
     * @param   mixed $compareObject the second of the datastructures to compare
175
     * @param   array $path          the array holding the current path
176
     * @return  array                returning the child nodes in an array
177
     */
178
    private function getChangedObjectPaths($object, $compareObject, $path = [])
179
    {
180
        $return = [];
181
        foreach ($object as $fieldName => $value) {
182
            if (is_object($compareObject)) {
183
                $compareValue = $compareObject->$fieldName;
184
            } else {
185
                $compareValue = $compareObject[$fieldName];
186
            }
187
            $path[]=$fieldName;
188
            if (is_object($value) || is_array($value)) {
189
                $return[$fieldName] = $this->getChangedObjectPaths($value, $compareValue, $path);
190
            } else {
191
                if ($value!=$compareValue) {
192
                    $this->changedObjectPaths[] = implode('.', $path);
193
                }
194
            }
195
            array_pop($path);
196
        }
197
        return $return;
198
    }
199
200
    /**
201
     * if the user provides properties that are in the exception list but not on the currently saved
202
     * object, we try here to synthetically add them to our representation. and yes, this won't support
203
     * exclusions in an array structure for the moment, but that is also not needed for now.
204
     *
205
     * @param string $expression the expression
206
     * @param object $obj        the object
207
     *
208
     * @return object the modified object
209
     */
210
    private function addProperties($expression, $obj)
211
    {
212
        $val = &$obj;
213
        $parts = explode('.', $expression);
214
        $numParts = count($parts);
215
216
        if ($numParts == 1) {
217
            $val->{$parts[0]} = null;
218
        } else {
219
            $iteration = 1;
220
            foreach ($parts as $part) {
221
                if ($iteration < $numParts) {
222
                    if (!isset($val->{$part}) || !is_object($val->{$part})) {
223
                        $val->{$part} = new \stdClass();
224
                    }
225
                    $val = &$val->{$part};
226
                } else {
227
                    $val->{$part} = null;
228
                }
229
                $iteration++;
230
            }
231
        }
232
233
        return $val;
234
    }
235
}
236