@kirigami/php-prepros 1.9.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +502 -21
- package/README.md +1988 -1817
- package/index.d.ts +62 -26
- package/index.js +1 -1
- package/package.json +59 -59
- package/src/imagebatch.php +68 -67
- package/src/libraries/aliases.inc.php +879 -879
- package/src/libraries/curl.class.php +7 -7
- package/src/libraries/ld.class.php +803 -804
- package/src/libraries/md-legacy.class.php +1257 -0
- package/src/libraries/md.class.php +51 -1249
- package/src/libraries/meta.class.php +496 -506
- package/src/libraries/{normalizer.class.php → normalizer-legacy.class.php} +11 -1
- package/src/libraries/prepros.class.php +85 -8
- package/src/libraries/schema-legacy.class.php +549 -0
- package/src/libraries/schema.class.php +99 -472
- package/src/libraries/yaml-legacy.class.php +598 -0
- package/src/libraries/yaml.class.php +41 -406
- package/src/prepros.js +152 -48
- package/src/prepros.php +4 -5
- package/src/runenv.php +2 -2
- package/src/utils.inc.php +12 -2
|
@@ -3,39 +3,44 @@
|
|
|
3
3
|
declare(strict_types=1);
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* SCHEMA — JSON Schema validator backed by the native `jsonk` extension
|
|
7
|
+
* (statically built into @kirigami/php-wasm — see php-kirigami/php-jsonk),
|
|
8
|
+
* with an Ajv-like API.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* jsonk targets draft 2020-12 and takes JSON strings, so this class encodes
|
|
11
|
+
* the PHP schema and data first. The encoding step keeps the schemas written
|
|
12
|
+
* for the previous pure-PHP validator working:
|
|
13
|
+
* - an empty PHP array in a schema position (`'properties' => []`, a `[]`
|
|
14
|
+
* sub-schema) becomes `{}` instead of JSON `[]`;
|
|
15
|
+
* - draft-07 tuple `items` (a list of schemas) becomes `prefixItems`, and
|
|
16
|
+
* `additionalItems` becomes `items`;
|
|
17
|
+
* - `format: url` becomes `format: uri`.
|
|
10
18
|
*
|
|
11
|
-
*
|
|
19
|
+
* Errors keep the previous `"path: message"` string format, with paths such
|
|
20
|
+
* as `(root)`, `name` or `tags[1]`.
|
|
12
21
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 'required' => ['name', 'age'],
|
|
16
|
-
* 'properties' => [
|
|
17
|
-
* 'name' => ['type' => 'string', 'minLength' => 1],
|
|
18
|
-
* 'age' => ['type' => 'integer', 'minimum' => 0],
|
|
19
|
-
* 'tags' => ['type' => 'array', 'items' => ['type' => 'string']],
|
|
20
|
-
* ],
|
|
21
|
-
* 'additionalProperties' => false,
|
|
22
|
-
* ];
|
|
22
|
+
* Kept the old implementation as SCHEMA_LEGACY (schema-legacy.class.php) —
|
|
23
|
+
* untouched, still autoloadable — as a rollback path.
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* if (!$validator->isValid(
|
|
27
|
-
* foreach ($validator->getErrors() as $error) {
|
|
28
|
-
* echo $error, PHP_EOL;
|
|
29
|
-
* }
|
|
30
|
-
* }
|
|
25
|
+
* Usage:
|
|
26
|
+
* $validator = new SCHEMA(['type' => 'object', 'required' => ['name']]);
|
|
27
|
+
* if (!$validator->isValid($data)) print_r($validator->getErrors());
|
|
31
28
|
*/
|
|
32
29
|
final class SCHEMA
|
|
33
30
|
{
|
|
34
|
-
/**
|
|
35
|
-
private
|
|
31
|
+
/** Keywords whose value is a single sub-schema. */
|
|
32
|
+
private const SUBSCHEMA = [
|
|
33
|
+
'additionalItems', 'additionalProperties', 'contains', 'else', 'if', 'items',
|
|
34
|
+
'not', 'propertyNames', 'then', 'unevaluatedItems', 'unevaluatedProperties',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** Keywords whose value is a list of sub-schemas. */
|
|
38
|
+
private const SUBSCHEMA_LIST = ['allOf', 'anyOf', 'oneOf', 'prefixItems'];
|
|
39
|
+
|
|
40
|
+
/** Keywords whose value maps names to sub-schemas. */
|
|
41
|
+
private const SUBSCHEMA_MAP = ['$defs', 'definitions', 'dependentSchemas', 'patternProperties', 'properties'];
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
private array $refRoot;
|
|
43
|
+
private string $schemaJson;
|
|
39
44
|
|
|
40
45
|
/** @var string[] */
|
|
41
46
|
private array $errors = [];
|
|
@@ -45,506 +50,128 @@ final class SCHEMA
|
|
|
45
50
|
*/
|
|
46
51
|
public function __construct(array $schema)
|
|
47
52
|
{
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
if (!function_exists('jsonk_validate')) {
|
|
54
|
+
throw new RuntimeException('SCHEMA requires the jsonk extension; use SCHEMA_LEGACY without it.');
|
|
55
|
+
}
|
|
56
|
+
$this->schemaJson = json_encode(self::normalize($schema), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
/**
|
|
53
60
|
* Validates data. Returns true/false, like Ajv's `validate()`.
|
|
54
61
|
* Errors are available via getErrors().
|
|
55
|
-
*
|
|
56
|
-
* @param mixed $data
|
|
57
62
|
*/
|
|
58
63
|
public function isValid(mixed $data): bool
|
|
59
64
|
{
|
|
60
65
|
$this->errors = [];
|
|
61
|
-
$this->validateAgainst($data, $this->rootSchema, '');
|
|
62
|
-
|
|
63
|
-
return count($this->errors) === 0;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Alias closer to Ajv's semantics (validate($data)).
|
|
68
|
-
*/
|
|
69
|
-
public function validate(mixed $data): bool
|
|
70
|
-
{
|
|
71
|
-
return $this->isValid($data);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @return string[] List of error messages (path: message).
|
|
76
|
-
*/
|
|
77
|
-
public function getErrors(): array
|
|
78
|
-
{
|
|
79
|
-
return $this->errors;
|
|
80
|
-
}
|
|
81
66
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* @param mixed $data
|
|
90
|
-
* @param array<string,mixed> $schema
|
|
91
|
-
*/
|
|
92
|
-
private function validateAgainst(mixed $data, array $schema, string $path): void
|
|
93
|
-
{
|
|
94
|
-
// Local $ref (#/definitions/xxx or #/$defs/xxx)
|
|
95
|
-
if (isset($schema['$ref'])) {
|
|
96
|
-
$resolved = $this->resolveRef((string) $schema['$ref']);
|
|
97
|
-
if ($resolved === null) {
|
|
98
|
-
$this->addError($path, "reference not found: {$schema['$ref']}");
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
$this->validateAgainst($data, $resolved, $path);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (isset($schema['const'])) {
|
|
106
|
-
if (!$this->looseEquals($data, $schema['const'])) {
|
|
107
|
-
$this->addError($path, 'does not match the expected const value');
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (isset($schema['enum']) && is_array($schema['enum'])) {
|
|
112
|
-
$found = false;
|
|
113
|
-
foreach ($schema['enum'] as $candidate) {
|
|
114
|
-
if ($this->looseEquals($data, $candidate)) {
|
|
115
|
-
$found = true;
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (!$found) {
|
|
120
|
-
$this->addError($path, 'is not one of the allowed enum values');
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (isset($schema['type'])) {
|
|
125
|
-
$this->validateType($data, $schema['type'], $path);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
$this->validateCombinators($data, $schema, $path);
|
|
129
|
-
|
|
130
|
-
$type = $this->detectType($data);
|
|
131
|
-
|
|
132
|
-
switch ($type) {
|
|
133
|
-
case 'string':
|
|
134
|
-
$this->validateString($data, $schema, $path);
|
|
135
|
-
break;
|
|
136
|
-
case 'integer':
|
|
137
|
-
case 'number':
|
|
138
|
-
$this->validateNumber($data, $schema, $path);
|
|
139
|
-
break;
|
|
140
|
-
case 'array':
|
|
141
|
-
$this->validateArray($data, $schema, $path);
|
|
142
|
-
break;
|
|
143
|
-
case 'object':
|
|
144
|
-
$this->validateObject($data, $schema, $path);
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* @param array<string,mixed> $schema
|
|
151
|
-
*/
|
|
152
|
-
private function validateCombinators(mixed $data, array $schema, string $path): void
|
|
153
|
-
{
|
|
154
|
-
if (isset($schema['allOf']) && is_array($schema['allOf'])) {
|
|
155
|
-
foreach ($schema['allOf'] as $sub) {
|
|
156
|
-
$this->validateAgainst($data, $sub, $path);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (isset($schema['anyOf']) && is_array($schema['anyOf'])) {
|
|
161
|
-
$ok = false;
|
|
162
|
-
foreach ($schema['anyOf'] as $sub) {
|
|
163
|
-
if ($this->isSubSchemaValid($data, $sub)) {
|
|
164
|
-
$ok = true;
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (!$ok) {
|
|
169
|
-
$this->addError($path, 'does not satisfy any of the anyOf schemas');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (isset($schema['oneOf']) && is_array($schema['oneOf'])) {
|
|
174
|
-
$matches = 0;
|
|
175
|
-
foreach ($schema['oneOf'] as $sub) {
|
|
176
|
-
if ($this->isSubSchemaValid($data, $sub)) {
|
|
177
|
-
$matches++;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if ($matches !== 1) {
|
|
181
|
-
$this->addError($path, "must satisfy exactly one oneOf schema ({$matches} match(es))");
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (isset($schema['not'])) {
|
|
186
|
-
if ($this->isSubSchemaValid($data, $schema['not'])) {
|
|
187
|
-
$this->addError($path, 'must not satisfy the "not" schema');
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* @param array<string,mixed> $schema
|
|
194
|
-
*/
|
|
195
|
-
private function isSubSchemaValid(mixed $data, array $schema): bool
|
|
196
|
-
{
|
|
197
|
-
$sub = new self($schema);
|
|
198
|
-
// $ref pointers inside the sub-schema must keep resolving against
|
|
199
|
-
// the original root document, not against the sub-schema itself.
|
|
200
|
-
$sub->refRoot = $this->refRoot;
|
|
201
|
-
return $sub->isValid($data);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private function validateType(mixed $data, mixed $expected, string $path): void
|
|
205
|
-
{
|
|
206
|
-
$expectedTypes = is_array($expected) ? $expected : [$expected];
|
|
207
|
-
$actual = $this->detectType($data);
|
|
208
|
-
|
|
209
|
-
foreach ($expectedTypes as $t) {
|
|
210
|
-
if ($t === 'integer' && $actual === 'number' && is_float($data) && floor($data) === $data) {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
if ($t === $actual) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if ($t === 'number' && $actual === 'integer') {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
$expectedStr = implode('|', $expectedTypes);
|
|
222
|
-
$this->addError($path, "invalid type: expected {$expectedStr}, got {$actual}");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private function detectType(mixed $data): string
|
|
226
|
-
{
|
|
227
|
-
return match (true) {
|
|
228
|
-
$data === null => 'null',
|
|
229
|
-
is_bool($data) => 'boolean',
|
|
230
|
-
is_int($data) => 'integer',
|
|
231
|
-
is_float($data) => (floor($data) === $data ? 'integer' : 'number'),
|
|
232
|
-
is_string($data) => 'string',
|
|
233
|
-
// A JSON array always decodes to a PHP list array, whether or not
|
|
234
|
-
// json_decode() was called with associative=true.
|
|
235
|
-
is_array($data) && $this->isList($data) => 'array',
|
|
236
|
-
// A JSON object decodes to either an associative array
|
|
237
|
-
// (json_decode(..., true)) or a stdClass (json_decode(..., false)).
|
|
238
|
-
is_array($data) || is_object($data) => 'object',
|
|
239
|
-
default => 'unknown',
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* True for plain PHP list arrays (sequential integer keys from 0).
|
|
245
|
-
* Objects (stdClass or associative arrays) are never lists.
|
|
246
|
-
*/
|
|
247
|
-
private function isList(mixed $arr): bool
|
|
248
|
-
{
|
|
249
|
-
if (!is_array($arr)) {
|
|
67
|
+
try {
|
|
68
|
+
$json = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
|
69
|
+
} catch (JsonException $e) {
|
|
70
|
+
$this->errors[] = '(root): value cannot be encoded as JSON: ' . $e->getMessage();
|
|
250
71
|
return false;
|
|
251
72
|
}
|
|
252
|
-
|
|
73
|
+
|
|
74
|
+
if (jsonk_validate($json, $this->schemaJson)) {
|
|
253
75
|
return true;
|
|
254
76
|
}
|
|
255
|
-
return array_keys($arr) === range(0, count($arr) - 1);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* True when $data behaves like a JSON object: either a stdClass
|
|
260
|
-
* (json_decode(..., false)) or a non-list associative array
|
|
261
|
-
* (json_decode(..., true)).
|
|
262
|
-
*/
|
|
263
|
-
private function isObjectLike(mixed $data): bool
|
|
264
|
-
{
|
|
265
|
-
return is_object($data) || (is_array($data) && !$this->isList($data));
|
|
266
|
-
}
|
|
267
77
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return array_keys(get_object_vars($data));
|
|
275
|
-
}
|
|
276
|
-
if (is_array($data)) {
|
|
277
|
-
return array_map('strval', array_keys($data));
|
|
78
|
+
$violations = jsonk_get_last_errors();
|
|
79
|
+
if (!$violations) {
|
|
80
|
+
// Schema-level failure (unresolvable $ref, invalid keyword value…):
|
|
81
|
+
// jsonk reports it through its last error message only.
|
|
82
|
+
$this->errors[] = '(root): ' . jsonk_last_error_msg();
|
|
83
|
+
return false;
|
|
278
84
|
}
|
|
279
|
-
return [];
|
|
280
|
-
}
|
|
281
85
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return property_exists($data, $key);
|
|
286
|
-
}
|
|
287
|
-
if (is_array($data)) {
|
|
288
|
-
return array_key_exists($key, $data);
|
|
86
|
+
$decoded = json_decode($json);
|
|
87
|
+
foreach ($violations as $violation) {
|
|
88
|
+
$this->errors[] = self::displayPath($violation['path'], $decoded) . ': ' . $violation['message'];
|
|
289
89
|
}
|
|
290
90
|
return false;
|
|
291
91
|
}
|
|
292
92
|
|
|
293
|
-
private function objectGet(mixed $data, string $key): mixed
|
|
294
|
-
{
|
|
295
|
-
if (is_object($data)) {
|
|
296
|
-
return $data->{$key};
|
|
297
|
-
}
|
|
298
|
-
return $data[$key];
|
|
299
|
-
}
|
|
300
|
-
|
|
301
93
|
/**
|
|
302
|
-
*
|
|
94
|
+
* Alias closer to Ajv's semantics (validate($data)).
|
|
303
95
|
*/
|
|
304
|
-
|
|
305
|
-
{
|
|
306
|
-
if (!is_string($data)) {
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
$len = function_exists('mb_strlen') ? mb_strlen($data) : strlen($data);
|
|
311
|
-
|
|
312
|
-
if (isset($schema['minLength']) && $len < (int) $schema['minLength']) {
|
|
313
|
-
$this->addError($path, "minimum length {$schema['minLength']} not met ({$len})");
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (isset($schema['maxLength']) && $len > (int) $schema['maxLength']) {
|
|
317
|
-
$this->addError($path, "maximum length {$schema['maxLength']} exceeded ({$len})");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (isset($schema['pattern'])) {
|
|
321
|
-
$pattern = '/' . str_replace('/', '\/', (string) $schema['pattern']) . '/u';
|
|
322
|
-
if (@preg_match($pattern, $data) !== 1) {
|
|
323
|
-
$this->addError($path, "does not match pattern {$schema['pattern']}");
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (isset($schema['format'])) {
|
|
328
|
-
$this->validateFormat($data, (string) $schema['format'], $path);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private function validateFormat(string $data, string $format, string $path): void
|
|
96
|
+
public function validate(mixed $data): bool
|
|
333
97
|
{
|
|
334
|
-
$
|
|
335
|
-
'email' => (bool) filter_var($data, FILTER_VALIDATE_EMAIL),
|
|
336
|
-
'uri', 'url' => (bool) filter_var($data, FILTER_VALIDATE_URL),
|
|
337
|
-
'date' => (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $data)
|
|
338
|
-
&& checkdate((int) substr($data, 5, 2), (int) substr($data, 8, 2), (int) substr($data, 0, 4)),
|
|
339
|
-
'date-time' => (bool) preg_match(
|
|
340
|
-
'/^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/',
|
|
341
|
-
$data
|
|
342
|
-
),
|
|
343
|
-
'ipv4' => (bool) filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4),
|
|
344
|
-
'ipv6' => (bool) filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6),
|
|
345
|
-
'uuid' => (bool) preg_match(
|
|
346
|
-
'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i',
|
|
347
|
-
$data
|
|
348
|
-
),
|
|
349
|
-
default => true, // Unknown format: does not block, same as Ajv in non-strict mode.
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
if (!$ok) {
|
|
353
|
-
$this->addError($path, "invalid format, expected '{$format}'");
|
|
354
|
-
}
|
|
98
|
+
return $this->isValid($data);
|
|
355
99
|
}
|
|
356
100
|
|
|
357
101
|
/**
|
|
358
|
-
* @
|
|
102
|
+
* @return string[] List of error messages (path: message).
|
|
359
103
|
*/
|
|
360
|
-
|
|
104
|
+
public function getErrors(): array
|
|
361
105
|
{
|
|
362
|
-
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (isset($schema['minimum']) && $data < $schema['minimum']) {
|
|
367
|
-
$this->addError($path, "must be >= {$schema['minimum']}");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (isset($schema['maximum']) && $data > $schema['maximum']) {
|
|
371
|
-
$this->addError($path, "must be <= {$schema['maximum']}");
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (isset($schema['exclusiveMinimum']) && $data <= $schema['exclusiveMinimum']) {
|
|
375
|
-
$this->addError($path, "must be > {$schema['exclusiveMinimum']}");
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (isset($schema['exclusiveMaximum']) && $data >= $schema['exclusiveMaximum']) {
|
|
379
|
-
$this->addError($path, "must be < {$schema['exclusiveMaximum']}");
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (isset($schema['multipleOf'])) {
|
|
383
|
-
$factor = (float) $schema['multipleOf'];
|
|
384
|
-
if ($factor > 0) {
|
|
385
|
-
$div = $data / $factor;
|
|
386
|
-
if (abs($div - round($div)) > 1e-9) {
|
|
387
|
-
$this->addError($path, "must be a multiple of {$schema['multipleOf']}");
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
106
|
+
return $this->errors;
|
|
391
107
|
}
|
|
392
108
|
|
|
393
109
|
/**
|
|
394
|
-
*
|
|
110
|
+
* Prepares a schema node for JSON encoding (see the class comment).
|
|
395
111
|
*/
|
|
396
|
-
private function
|
|
112
|
+
private static function normalize(mixed $schema): mixed
|
|
397
113
|
{
|
|
398
|
-
if (
|
|
399
|
-
|
|
114
|
+
if (is_object($schema)) {
|
|
115
|
+
$schema = get_object_vars($schema);
|
|
400
116
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (isset($schema['minItems']) && $count < (int) $schema['minItems']) {
|
|
405
|
-
$this->addError($path, "must contain at least {$schema['minItems']} item(s)");
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (isset($schema['maxItems']) && $count > (int) $schema['maxItems']) {
|
|
409
|
-
$this->addError($path, "must contain at most {$schema['maxItems']} item(s)");
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (($schema['uniqueItems'] ?? false) === true) {
|
|
413
|
-
$seen = [];
|
|
414
|
-
foreach ($data as $item) {
|
|
415
|
-
$key = json_encode($item, JSON_THROW_ON_ERROR);
|
|
416
|
-
if (isset($seen[$key])) {
|
|
417
|
-
$this->addError($path, 'items must be unique');
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
|
-
$seen[$key] = true;
|
|
421
|
-
}
|
|
117
|
+
if (!is_array($schema)) {
|
|
118
|
+
return $schema; // Boolean schema, or an invalid value jsonk reports.
|
|
422
119
|
}
|
|
423
120
|
|
|
424
|
-
if (isset($schema['items'])) {
|
|
425
|
-
$
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (isset($items[$i])) {
|
|
431
|
-
$this->validateAgainst($item, $items[$i], "{$path}[{$i}]");
|
|
432
|
-
} elseif (isset($schema['additionalItems']) && $schema['additionalItems'] === false) {
|
|
433
|
-
$this->addError("{$path}[{$i}]", 'additional item not allowed');
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
} else {
|
|
437
|
-
// Standard mode: a single schema applied to every item.
|
|
438
|
-
foreach ($data as $i => $item) {
|
|
439
|
-
$this->validateAgainst($item, $items, "{$path}[{$i}]");
|
|
440
|
-
}
|
|
121
|
+
if (isset($schema['items']) && is_array($schema['items']) && $schema['items'] !== [] && array_is_list($schema['items'])) {
|
|
122
|
+
$schema['prefixItems'] = $schema['items'];
|
|
123
|
+
unset($schema['items']);
|
|
124
|
+
if (array_key_exists('additionalItems', $schema)) {
|
|
125
|
+
$schema['items'] = $schema['additionalItems'];
|
|
126
|
+
unset($schema['additionalItems']);
|
|
441
127
|
}
|
|
442
128
|
}
|
|
443
|
-
}
|
|
444
129
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
*/
|
|
448
|
-
private function validateObject(mixed $data, array $schema, string $path): void
|
|
449
|
-
{
|
|
450
|
-
// Accepts a JSON object regardless of how it was decoded:
|
|
451
|
-
// associative array (json_decode(..., true)) or stdClass (json_decode(..., false)).
|
|
452
|
-
if (!$this->isObjectLike($data)) {
|
|
453
|
-
return;
|
|
130
|
+
if (($schema['format'] ?? null) === 'url') {
|
|
131
|
+
$schema['format'] = 'uri';
|
|
454
132
|
}
|
|
455
133
|
|
|
456
|
-
$
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
134
|
+
foreach ($schema as $keyword => $value) {
|
|
135
|
+
if (in_array($keyword, self::SUBSCHEMA, true)) {
|
|
136
|
+
$schema[$keyword] = self::normalize($value);
|
|
137
|
+
} elseif (in_array($keyword, self::SUBSCHEMA_LIST, true) && is_array($value)) {
|
|
138
|
+
$schema[$keyword] = array_map(self::normalize(...), array_values($value));
|
|
139
|
+
} elseif (in_array($keyword, self::SUBSCHEMA_MAP, true) && (is_array($value) || is_object($value))) {
|
|
140
|
+
$schema[$keyword] = (object) array_map(self::normalize(...), is_object($value) ? get_object_vars($value) : $value);
|
|
141
|
+
} elseif ($keyword === 'dependentRequired' && is_array($value)) {
|
|
142
|
+
$schema[$keyword] = (object) $value;
|
|
464
143
|
}
|
|
465
144
|
}
|
|
466
145
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
$additional = $schema['additionalProperties'] ?? true;
|
|
470
|
-
|
|
471
|
-
foreach ($keys as $key) {
|
|
472
|
-
$value = $this->objectGet($data, $key);
|
|
473
|
-
$childPath = $path === '' ? $key : "{$path}.{$key}";
|
|
474
|
-
$matched = false;
|
|
475
|
-
|
|
476
|
-
if (isset($properties[$key])) {
|
|
477
|
-
$matched = true;
|
|
478
|
-
$this->validateAgainst($value, $properties[$key], $childPath);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
foreach ($patternProperties as $pattern => $subSchema) {
|
|
482
|
-
$regex = '/' . str_replace('/', '\/', (string) $pattern) . '/u';
|
|
483
|
-
if (@preg_match($regex, $key) === 1) {
|
|
484
|
-
$matched = true;
|
|
485
|
-
$this->validateAgainst($value, $subSchema, $childPath);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (!$matched) {
|
|
490
|
-
if ($additional === false) {
|
|
491
|
-
$this->addError($childPath, 'additional property not allowed');
|
|
492
|
-
} elseif (is_array($additional)) {
|
|
493
|
-
$this->validateAgainst($value, $additional, $childPath);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
$count = count($keys);
|
|
499
|
-
|
|
500
|
-
if (isset($schema['minProperties']) && $count < (int) $schema['minProperties']) {
|
|
501
|
-
$this->addError($path, "must have at least {$schema['minProperties']} propert(y/ies)");
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (isset($schema['maxProperties']) && $count > (int) $schema['maxProperties']) {
|
|
505
|
-
$this->addError($path, "must have at most {$schema['maxProperties']} propert(y/ies)");
|
|
506
|
-
}
|
|
146
|
+
// An object, even when empty or keyed 0..n.
|
|
147
|
+
return (object) $schema;
|
|
507
148
|
}
|
|
508
149
|
|
|
509
150
|
/**
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
151
|
+
* Converts a jsonk JSON Pointer ("/tags/1") to the previous validator's
|
|
152
|
+
* path format ("tags[1]"), using the data to tell array indexes from
|
|
153
|
+
* object keys.
|
|
513
154
|
*/
|
|
514
|
-
private function
|
|
155
|
+
private static function displayPath(string $pointer, mixed $data): string
|
|
515
156
|
{
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
157
|
+
$path = '';
|
|
158
|
+
$node = $data;
|
|
519
159
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
foreach ($segments as $segment) {
|
|
524
|
-
$segment = str_replace(['~1', '~0'], ['/', '~'], $segment);
|
|
525
|
-
if (!is_array($current) || !array_key_exists($segment, $current)) {
|
|
526
|
-
return null;
|
|
160
|
+
foreach (explode('/', ltrim($pointer, '/')) as $segment) {
|
|
161
|
+
if ($segment === '') {
|
|
162
|
+
continue;
|
|
527
163
|
}
|
|
528
|
-
$
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return is_array($current) ? $current : null;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
private function looseEquals(mixed $a, mixed $b): bool
|
|
535
|
-
{
|
|
536
|
-
$aStructured = is_array($a) || is_object($a);
|
|
537
|
-
$bStructured = is_array($b) || is_object($b);
|
|
164
|
+
$segment = str_replace(['~1', '~0'], ['/', '~'], $segment);
|
|
538
165
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return false;
|
|
166
|
+
if (is_array($node)) {
|
|
167
|
+
$path .= "[{$segment}]";
|
|
168
|
+
$node = $node[(int) $segment] ?? null;
|
|
169
|
+
} else {
|
|
170
|
+
$path .= $path === '' ? $segment : ".{$segment}";
|
|
171
|
+
$node = is_object($node) ? ($node->{$segment} ?? null) : null;
|
|
546
172
|
}
|
|
547
173
|
}
|
|
548
|
-
|
|
174
|
+
|
|
175
|
+
return $path === '' ? '(root)' : $path;
|
|
549
176
|
}
|
|
550
|
-
}
|
|
177
|
+
}
|