1#!/usr/bin/env php
2<?php
3/**
4 * JSON schema validator
5 *
6 * @author Christian Weiske <christian.weiske@netresearch.de>
7 */
8
9/**
10 * Dead simple autoloader
11 *
12 * @param string $className Name of class to load
13 *
14 * @return void
15 */
16function __autoload($className)
17{
18    $className = ltrim($className, '\\');
19    $fileName  = '';
20    $namespace = '';
21    if ($lastNsPos = strrpos($className, '\\')) {
22        $namespace = substr($className, 0, $lastNsPos);
23        $className = substr($className, $lastNsPos + 1);
24        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
25    }
26    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
27    if (stream_resolve_include_path($fileName)) {
28        require_once $fileName;
29    }
30}
31
32/**
33 * Show the json parse error that happened last
34 *
35 * @return void
36 */
37function showJsonError()
38{
39    $constants = get_defined_constants(true);
40    $json_errors = array();
41    foreach ($constants['json'] as $name => $value) {
42        if (!strncmp($name, 'JSON_ERROR_', 11)) {
43            $json_errors[$value] = $name;
44        }
45    }
46
47    echo 'JSON parse error: ' . $json_errors[json_last_error()] . "\n";
48}
49
50function getUrlFromPath($path)
51{
52    if (parse_url($path, PHP_URL_SCHEME) !== null) {
53        //already an URL
54        return $path;
55    }
56    if ($path{0} == '/') {
57        //absolute path
58        return 'file://' . $path;
59    }
60
61    //relative path: make absolute
62    return 'file://' . getcwd() . '/' . $path;
63}
64
65/**
66 * Take a HTTP header value and split it up into parts.
67 *
68 * @return array Key "_value" contains the main value, all others
69 *               as given in the header value
70 */
71function parseHeaderValue($headerValue)
72{
73    if (strpos($headerValue, ';') === false) {
74        return array('_value' => $headerValue);
75    }
76
77    $parts = explode(';', $headerValue);
78    $arData = array('_value' => array_shift($parts));
79    foreach ($parts as $part) {
80        list($name, $value) = explode('=', $part);
81        $arData[$name] = trim($value, ' "\'');
82    }
83    return $arData;
84}
85
86
87// support running this tool from git checkout
88if (is_dir(__DIR__ . '/../src/JsonSchema')) {
89    set_include_path(__DIR__ . '/../src' . PATH_SEPARATOR . get_include_path());
90}
91
92$arOptions = array();
93$arArgs = array();
94array_shift($argv);//script itself
95foreach ($argv as $arg) {
96    if ($arg{0} == '-') {
97        $arOptions[$arg] = true;
98    } else {
99        $arArgs[] = $arg;
100    }
101}
102
103if (count($arArgs) == 0
104    || isset($arOptions['--help']) || isset($arOptions['-h'])
105) {
106    echo <<<HLP
107Validate schema
108Usage: validate-json data.json
109   or: validate-json data.json schema.json
110
111Options:
112      --dump-schema     Output full schema and exit
113      --dump-schema-url Output URL of schema
114   -h --help            Show this help
115
116HLP;
117    exit(1);
118}
119
120if (count($arArgs) == 1) {
121    $pathData   = $arArgs[0];
122    $pathSchema = null;
123} else {
124    $pathData   = $arArgs[0];
125    $pathSchema = getUrlFromPath($arArgs[1]);
126}
127
128$urlData = getUrlFromPath($pathData);
129
130$context = stream_context_create(
131    array(
132        'http' => array(
133            'header'        => array(
134                'Accept: */*',
135                'Connection: Close'
136            ),
137            'max_redirects' => 5
138        )
139    )
140);
141$dataString = file_get_contents($pathData, false, $context);
142if ($dataString == '') {
143    echo "Data file is not readable or empty.\n";
144    exit(3);
145}
146
147$data = json_decode($dataString);
148unset($dataString);
149if ($data === null) {
150    echo "Error loading JSON data file\n";
151    showJsonError();
152    exit(5);
153}
154
155if ($pathSchema === null) {
156    if (isset($http_response_header)) {
157        array_shift($http_response_header);//HTTP/1.0 line
158        foreach ($http_response_header as $headerLine) {
159            list($hName, $hValue) = explode(':', $headerLine, 2);
160            $hName = strtolower($hName);
161            if ($hName == 'link') {
162                //Link: <http://example.org/schema#>; rel="describedBy"
163                $hParts = parseHeaderValue($hValue);
164                if (isset($hParts['rel']) && $hParts['rel'] == 'describedBy') {
165                    $pathSchema = trim($hParts['_value'], ' <>');
166                }
167            } else if ($hName == 'content-type') {
168                //Content-Type: application/my-media-type+json;
169                //              profile=http://example.org/schema#
170                $hParts = parseHeaderValue($hValue);
171                if (isset($hParts['profile'])) {
172                    $pathSchema = $hParts['profile'];
173                }
174
175            }
176        }
177    }
178    if (is_object($data) && property_exists($data, '$schema')) {
179        $pathSchema = $data->{'$schema'};
180    }
181
182    //autodetect schema
183    if ($pathSchema === null) {
184        echo "JSON data must be an object and have a \$schema property.\n";
185        echo "You can pass the schema file on the command line as well.\n";
186        echo "Schema autodetection failed.\n";
187        exit(6);
188    }
189}
190if ($pathSchema{0} == '/') {
191    $pathSchema = 'file://' . $pathSchema;
192}
193
194$resolver = new JsonSchema\Uri\UriResolver();
195$retriever = new JsonSchema\Uri\UriRetriever();
196try {
197    $urlSchema = $resolver->resolve($pathSchema, $urlData);
198
199    if (isset($arOptions['--dump-schema-url'])) {
200        echo $urlSchema . "\n";
201        exit();
202    }
203} catch (Exception $e) {
204    echo "Error loading JSON schema file\n";
205    echo $urlSchema . "\n";
206    echo $e->getMessage() . "\n";
207    exit(2);
208}
209$refResolver = new JsonSchema\RefResolver($retriever, $resolver);
210$schema = $refResolver->resolve($urlSchema);
211
212if (isset($arOptions['--dump-schema'])) {
213    $options = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
214    echo json_encode($schema, $options) . "\n";
215    exit();
216}
217
218try {
219    $validator = new JsonSchema\Validator();
220    $validator->check($data, $schema);
221
222    if ($validator->isValid()) {
223        echo "OK. The supplied JSON validates against the schema.\n";
224    } else {
225        echo "JSON does not validate. Violations:\n";
226        foreach ($validator->getErrors() as $error) {
227            echo sprintf("[%s] %s\n", $error['property'], $error['message']);
228        }
229        exit(23);
230    }
231} catch (Exception $e) {
232    echo "JSON does not validate. Error:\n";
233    echo $e->getMessage() . "\n";
234    echo "Error code: " . $e->getCode() . "\n";
235    exit(24);
236}
237