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
|
|
|
use JsonSchema\Rfc3339; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* @author List of contributors <https://github.com/libgraviton/graviton/graphs/contributors> |
14
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GNU Public License |
15
|
|
|
* @link http://swisscom.ch |
16
|
|
|
*/ |
17
|
|
|
class RecordOriginConstraint |
18
|
|
|
{ |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @var string |
22
|
|
|
*/ |
23
|
|
|
private $recordOriginField; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var array |
27
|
|
|
*/ |
28
|
|
|
private $recordOriginBlacklist; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var array |
32
|
|
|
*/ |
33
|
|
|
private $exceptionFieldMap; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var array |
37
|
|
|
*/ |
38
|
|
|
private $changedObjectPaths; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* RecordOriginConstraint constructor. |
42
|
|
|
* |
43
|
|
|
* @param ConstraintUtils $utils Utils |
44
|
|
|
* @param string $recordOriginField name of the recordOrigin field |
45
|
|
|
* @param array $recordOriginBlacklist list of recordOrigin values that cannot be modified |
46
|
|
|
* @param array $exceptionFieldMap field map from compiler pass with excluded fields |
47
|
|
|
*/ |
48
|
|
|
public function __construct( |
49
|
|
|
ConstraintUtils $utils, |
50
|
|
|
$recordOriginField, |
51
|
|
|
array $recordOriginBlacklist, |
52
|
|
|
array $exceptionFieldMap |
53
|
|
|
) { |
54
|
|
|
$this->utils = $utils; |
55
|
|
|
$this->recordOriginField = $recordOriginField; |
56
|
|
|
$this->recordOriginBlacklist = $recordOriginBlacklist; |
57
|
|
|
$this->exceptionFieldMap = $exceptionFieldMap; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Checks the recordOrigin rules and sets error in event if needed |
62
|
|
|
* |
63
|
|
|
* @param ConstraintEventSchema $event event class |
64
|
|
|
* |
65
|
|
|
* @return void |
66
|
|
|
*/ |
67
|
|
|
public function checkRecordOrigin(ConstraintEventSchema $event) |
68
|
|
|
{ |
69
|
|
|
$currentRecord = $this->utils->getCurrentEntity(); |
70
|
|
|
$data = $event->getElement(); |
71
|
|
|
|
72
|
|
|
// if no recordorigin set on saved record; we let it through |
73
|
|
|
if (is_null($currentRecord) || !isset($currentRecord->{$this->recordOriginField})) { |
74
|
|
|
// we have no current record.. but make sure user doesn't want to send the banned recordOrigin |
75
|
|
|
if (isset($data->{$this->recordOriginField}) && |
76
|
|
|
!is_null($data->{$this->recordOriginField}) && |
77
|
|
|
in_array($data->{$this->recordOriginField}, $this->recordOriginBlacklist) |
78
|
|
|
) { |
79
|
|
|
$event->addError( |
80
|
|
|
sprintf( |
81
|
|
|
'Creating documents with the %s field having a value of %s is not permitted.', |
82
|
|
|
$this->recordOriginField, |
83
|
|
|
implode(', ', $this->recordOriginBlacklist) |
84
|
|
|
), |
85
|
|
|
$this->recordOriginField |
86
|
|
|
); |
87
|
|
|
return; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
return; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
$recordOrigin = $currentRecord->{$this->recordOriginField}; |
94
|
|
|
|
95
|
|
|
// not in the blacklist? can also go through.. |
96
|
|
|
if (!in_array($recordOrigin, $this->recordOriginBlacklist)) { |
97
|
|
|
return; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
// ok, user is trying to modify an object with blacklist recordorigin.. let's check fields |
101
|
|
|
$schema = $event->getSchema(); |
102
|
|
|
$isAllowed = true; |
103
|
|
|
|
104
|
|
|
if (!isset($schema->{'x-documentClass'})) { |
105
|
|
|
// this should never happen but we need to check. if schema has no information to *check* our rules, we |
106
|
|
|
// MUST deny it in that case.. |
107
|
|
|
$event->addError( |
108
|
|
|
'Internal error, not enough schema information to validate recordOrigin rules.', |
109
|
|
|
$this->recordOriginField |
110
|
|
|
); |
111
|
|
|
return; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
$documentClass = $schema->{'x-documentClass'}; |
115
|
|
|
|
116
|
|
|
if (!isset($this->exceptionFieldMap[$documentClass])) { |
117
|
|
|
// if he wants to edit on blacklist, but we have no exceptions, also deny.. |
118
|
|
|
$isAllowed = false; |
119
|
|
|
} else { |
120
|
|
|
// so to check our exceptions, we remove it from both documents (the stored and the clients) and compare |
121
|
|
|
$exceptions = $this->exceptionFieldMap[$documentClass]; |
122
|
|
|
|
123
|
|
|
$accessor = PropertyAccess::createPropertyAccessorBuilder() |
124
|
|
|
->enableMagicCall() |
125
|
|
|
->getPropertyAccessor(); |
126
|
|
|
|
127
|
|
|
$storedObject = clone $currentRecord; |
128
|
|
|
$userObject = clone $data; |
129
|
|
|
|
130
|
|
|
// convert all datetimes to UTC so we compare eggs with eggs |
131
|
|
|
$userObject = $this->convertDatetimeToUTC($userObject, $schema, new \DateTimeZone('UTC')); |
132
|
|
|
|
133
|
|
|
foreach ($exceptions as $fieldName) { |
134
|
|
View Code Duplication |
if ($accessor->isWritable($storedObject, $fieldName)) { |
|
|
|
|
135
|
|
|
$accessor->setValue($storedObject, $fieldName, null); |
136
|
|
|
} else { |
137
|
|
|
$this->addProperties($fieldName, $storedObject); |
138
|
|
|
} |
139
|
|
View Code Duplication |
if ($accessor->isWritable($userObject, $fieldName)) { |
|
|
|
|
140
|
|
|
$accessor->setValue($userObject, $fieldName, null); |
141
|
|
|
} else { |
142
|
|
|
$this->addProperties($fieldName, $userObject); |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
// so now all unimportant fields were set to null on both - they should match if rest is untouched ;-) |
147
|
|
|
if ($userObject != $storedObject) { |
148
|
|
|
$isAllowed = false; |
149
|
|
|
$this->getChangedObjectPaths($userObject, $storedObject); |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
if (!$isAllowed) { |
154
|
|
|
$error = sprintf( |
155
|
|
|
'Prohibited modification attempt on record with %s of %s.', |
156
|
|
|
$this->recordOriginField, |
157
|
|
|
implode(', ', $this->recordOriginBlacklist) |
158
|
|
|
); |
159
|
|
|
// if there are recordCoreExceptions we can be more explicit |
160
|
|
|
if (isset($this->exceptionFieldMap[$documentClass]) && !empty($this->changedObjectPaths)) { |
161
|
|
|
$error.= sprintf( |
162
|
|
|
' You tried to change (%s), but you can only change (%s) by recordOriginException.', |
163
|
|
|
implode(', ', $this->changedObjectPaths), |
164
|
|
|
implode(', ', $this->exceptionFieldMap[$documentClass]) |
165
|
|
|
); |
166
|
|
|
} |
167
|
|
|
$event->addError($error, $this->recordOriginField); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
return; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Recursive convert date time to UTC |
175
|
|
|
* @param object $object Form data to be verified |
176
|
|
|
* @param object $schema Entity schema |
177
|
|
|
* @param \DateTimeZone $timezone to be converted to |
178
|
|
|
* @return object |
179
|
|
|
*/ |
180
|
|
|
private function convertDatetimeToUTC($object, $schema, \DateTimeZone $timezone) |
181
|
|
|
{ |
182
|
|
|
foreach ($schema->properties as $field => $property) { |
183
|
|
|
if (isset($property->format) && $property->format == 'date-time' && isset($object->{$field})) { |
184
|
|
|
$dateTime = Rfc3339::createFromString($object->{$field}); |
185
|
|
|
$dateTime->setTimezone($timezone); |
186
|
|
|
$object->{$field} = $dateTime->format(\DateTime::ISO8601); |
187
|
|
|
} elseif (isset($property->properties) && isset($object->{$field})) { |
188
|
|
|
$object->{$field} = $this->convertDatetimeToUTC($object->{$field}, $property, $timezone); |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
return $object; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* recursive helperfunction that walks through two arrays/objects of the same structure, |
196
|
|
|
* compares the values and writes the paths containining differences into the $this->changedObjectPaths |
197
|
|
|
* |
198
|
|
|
* @param mixed $object the first of the datastructures to compare |
199
|
|
|
* @param mixed $compareObject the second of the datastructures to compare |
200
|
|
|
* @param array $path the array holding the current path |
201
|
|
|
* @return array returning the child nodes in an array |
202
|
|
|
*/ |
203
|
|
|
private function getChangedObjectPaths($object, $compareObject, $path = []) |
204
|
|
|
{ |
205
|
|
|
$return = []; |
206
|
|
|
foreach ($object as $fieldName => $value) { |
207
|
|
|
if (is_object($compareObject)) { |
208
|
|
|
$compareValue = $compareObject->$fieldName; |
209
|
|
|
} else { |
210
|
|
|
$compareValue = $compareObject[$fieldName]; |
211
|
|
|
} |
212
|
|
|
$path[]=$fieldName; |
213
|
|
|
if (is_object($value) || is_array($value)) { |
214
|
|
|
$return[$fieldName] = $this->getChangedObjectPaths($value, $compareValue, $path); |
215
|
|
|
} else { |
216
|
|
|
if ($value!=$compareValue) { |
217
|
|
|
$this->changedObjectPaths[] = implode('.', $path); |
218
|
|
|
} |
219
|
|
|
} |
220
|
|
|
array_pop($path); |
221
|
|
|
} |
222
|
|
|
return $return; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* if the user provides properties that are in the exception list but not on the currently saved |
227
|
|
|
* object, we try here to synthetically add them to our representation. and yes, this won't support |
228
|
|
|
* exclusions in an array structure for the moment, but that is also not needed for now. |
229
|
|
|
* |
230
|
|
|
* @param string $expression the expression |
231
|
|
|
* @param object $obj the object |
232
|
|
|
* |
233
|
|
|
* @return object the modified object |
234
|
|
|
*/ |
235
|
|
|
private function addProperties($expression, $obj) |
236
|
|
|
{ |
237
|
|
|
$val = &$obj; |
238
|
|
|
$parts = explode('.', $expression); |
239
|
|
|
$numParts = count($parts); |
240
|
|
|
|
241
|
|
|
if ($numParts == 1) { |
242
|
|
|
$val->{$parts[0]} = null; |
243
|
|
|
} else { |
244
|
|
|
$iteration = 1; |
245
|
|
|
foreach ($parts as $part) { |
246
|
|
|
if ($iteration < $numParts) { |
247
|
|
|
if (!isset($val->{$part}) || !is_object($val->{$part})) { |
248
|
|
|
$val->{$part} = new \stdClass(); |
249
|
|
|
} |
250
|
|
|
$val = &$val->{$part}; |
251
|
|
|
} else { |
252
|
|
|
$val->{$part} = null; |
253
|
|
|
} |
254
|
|
|
$iteration++; |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
return $val; |
259
|
|
|
} |
260
|
|
|
} |
261
|
|
|
|
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.