Empty when the data fits. */ public static function validate($data, array $schema): array { $errors = []; self::check($data, $schema, $schema, '', $errors); return $errors; } private static function check($data, array $schema, array $root, string $path, array &$errors): void { if (isset($schema['$ref'])) { $schema = self::resolve($schema['$ref'], $root); } if (isset($schema['type']) && ! self::hasType($data, (array) $schema['type'])) { /* translators: 1: where in the file, 2: the type that is expected */ $errors[] = self::error($path, sprintf(__('%1$s must be of type %2$s.', 'admin-options-pages'), self::name($path), implode('/', (array) $schema['type']))); return; } if (array_key_exists('const', $schema) && $data !== $schema['const']) { /* translators: %s: where in the file */ $errors[] = self::error($path, sprintf(__('%s has a value that is not supported.', 'admin-options-pages'), self::name($path))); } if (isset($schema['enum']) && ! in_array($data, $schema['enum'], true)) { /* translators: %s: where in the file */ $errors[] = self::error($path, sprintf(__('%s has a value that is not allowed.', 'admin-options-pages'), self::name($path))); } if (isset($schema['anyOf'])) { $fits = false; foreach ($schema['anyOf'] as $option) { $inner = []; self::check($data, $option, $root, $path, $inner); $fits = $fits || $inner === []; } if (! $fits) { /* translators: %s: where in the file */ $errors[] = self::error($path, sprintf(__('%s does not have what it needs.', 'admin-options-pages'), self::name($path))); } } if (is_string($data)) { self::checkString($data, $schema, $path, $errors); } if (is_array($data)) { self::checkContainer($data, $schema, $root, $path, $errors); } } private static function checkString(string $data, array $schema, string $path, array &$errors): void { $length = function_exists('mb_strlen') ? mb_strlen($data, 'UTF-8') : strlen($data); if (isset($schema['maxLength']) && $length > $schema['maxLength']) { /* translators: 1: where in the file, 2: a number */ $errors[] = self::error($path, sprintf(__('%1$s is longer than %2$d characters.', 'admin-options-pages'), self::name($path), $schema['maxLength'])); } if (isset($schema['minLength']) && $length < $schema['minLength']) { /* translators: %s: where in the file */ $errors[] = self::error($path, sprintf(__('%s is empty.', 'admin-options-pages'), self::name($path))); } // A pattern of the schema is a JSON Schema pattern without delimiters; ~ does not occur in ours. if (isset($schema['pattern']) && ! preg_match('~' . $schema['pattern'] . '~u', $data)) { /* translators: %s: where in the file */ $errors[] = self::error($path, sprintf(__('%s has characters that are not allowed.', 'admin-options-pages'), self::name($path))); } } private static function checkContainer(array $data, array $schema, array $root, string $path, array &$errors): void { $isList = self::isList($data); if (isset($schema['maxItems']) && $isList && count($data) > $schema['maxItems']) { /* translators: 1: where in the file, 2: a number */ $errors[] = self::error($path, sprintf(__('%1$s has more than %2$d items.', 'admin-options-pages'), self::name($path), $schema['maxItems'])); } if (isset($schema['minItems']) && count($data) < $schema['minItems']) { /* translators: %s: where in the file */ $errors[] = self::error($path, sprintf(__('%s has no items.', 'admin-options-pages'), self::name($path))); } if (isset($schema['maxProperties']) && count($data) > $schema['maxProperties']) { /* translators: 1: where in the file, 2: a number */ $errors[] = self::error($path, sprintf(__('%1$s has more than %2$d entries.', 'admin-options-pages'), self::name($path), $schema['maxProperties'])); } foreach ($schema['required'] ?? [] as $key) { if (! array_key_exists($key, $data)) { /* translators: 1: where in the file, 2: the name of what is missing */ $errors[] = self::error($path, sprintf(__('%1$s misses "%2$s".', 'admin-options-pages'), self::name($path), $key)); } } foreach ($schema['properties'] ?? [] as $key => $sub) { if (array_key_exists($key, $data)) { self::check($data[$key], $sub, $root, $path . '/' . $key, $errors); } } if (isset($schema['items']) && $isList) { foreach ($data as $index => $item) { self::check($item, $schema['items'], $root, $path . '/' . $index, $errors); } } } private static function hasType($data, array $types): bool { foreach ($types as $type) { $fits = [ 'string' => is_string($data), 'integer' => is_int($data), 'number' => is_int($data) || is_float($data), 'boolean' => is_bool($data), 'null' => $data === null, // An empty JSON object and an empty JSON array both decode to []. 'array' => is_array($data) && ($data === [] || self::isList($data)), 'object' => is_array($data) && ($data === [] || ! self::isList($data)), ][$type] ?? false; if ($fits) { return true; } } return false; } private static function isList(array $data): bool { return $data === [] || array_keys($data) === range(0, count($data) - 1); } private static function resolve(string $ref, array $root): array { $node = $root; foreach (explode('/', ltrim($ref, '#/')) as $part) { $node = $node[$part] ?? []; } return (array) $node; } private static function name(string $path): string { return $path === '' ? __('The file', 'admin-options-pages') : ltrim($path, '/'); } private static function error(string $path, string $message): array { return ['path' => $path, 'message' => $message]; } }