Completed
Push — master ( ee12fc...c0dc65 )
by Matt
03:03
created

Dereferencer::makeReferenceAbsolute()   A

Complexity

Conditions 3
Paths 3

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 12
Code Lines 6

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 2
b 0
f 0
nc 3
nop 4
dl 0
loc 12
ccs 4
cts 4
cp 1
crap 3
rs 9.4285
1
<?php
2
3
namespace League\JsonGuard;
4
5
use League\JsonGuard\Loaders\CurlWebLoader;
6
use League\JsonGuard\Loaders\FileGetContentsWebLoader;
7
use League\JsonGuard\Loaders\FileLoader;
8
9
/**
10
 * The Dereferencer resolves all external $refs and replaces
11
 * internal references with Reference objects.
12
 */
13
class Dereferencer
14
{
15
    /**
16
     * @var array
17
     */
18
    private $loaders;
19
20
    /**
21
     * Create a new Dereferencer.
22
     */
23 158
    public function __construct()
24
    {
25 158
        $this->registerFileLoader();
26 158
        $this->registerDefaultWebLoaders();
27 158
    }
28
29
    /**
30
     * Return the schema with all references resolved.
31
     *
32
     * @param string|object $schema Either a valid path like "http://json-schema.org/draft-03/schema#"
33
     *                              or the object resulting from a json_decode call.
34
     *
35
     * @return object
36
     */
37 158
    public function dereference($schema)
38
    {
39 158
        if (is_string($schema)) {
40 24
            $uri    = $schema;
41 24
            $schema = $this->loadExternalRef($uri);
42 24
            $schema = $this->resolveFragment($uri, $schema);
43
44 24
            return $this->crawl($schema, strip_fragment($uri));
45
        }
46
47 134
        return $this->crawl($schema);
48
    }
49
50
    /**
51
     * Register a Loader for the given prefix.
52
     *
53
     * @param Loader $loader
54
     * @param string $prefix
55
     */
56 122
    public function registerLoader(Loader $loader, $prefix)
57
    {
58 122
        $this->loaders[$prefix] = $loader;
59 122
    }
60
61
    /**
62
     * Get the loader for the given prefix.
63
     *
64
     * @param $prefix
65
     *
66
     * @return Loader
67
     * @throws \InvalidArgumentException
68
     */
69 38
    private function getLoader($prefix)
70
    {
71 38
        if (!array_key_exists($prefix, $this->loaders)) {
72
            throw new \InvalidArgumentException(sprintf('A loader is not registered for the prefix "%s"', $prefix));
73
        }
74
75 38
        return $this->loaders[$prefix];
76
    }
77
78
    /**
79
     * Register the default file loader.
80
     */
81 158
    private function registerFileLoader()
82
    {
83 158
        $this->loaders['file'] = new FileLoader();
84 158
    }
85
86
    /**
87
     * Register the default web loaders.  If the curl extension is loaded,
88
     * the CurlWebLoader will be used.  Otherwise the FileGetContentsWebLoader
89
     * will be used.  You can override this by registering your own loader
90
     * for the 'http' and 'https' protocols.
91
     */
92 158
    private function registerDefaultWebLoaders()
93
    {
94 158
        if (function_exists('curl_init')) {
95 158
            $this->loaders['https'] = new CurlWebLoader('https://');
96 158
            $this->loaders['http']  = new CurlWebLoader('http://');
97 158
        } else {
98
            $this->loaders['https'] = new FileGetContentsWebLoader('https://');
99
            $this->loaders['http']  = new FileGetContentsWebLoader('http://');
100
        }
101 158
    }
102
103
    /**
104
     * Crawl the schema and resolve any references.
105
     *
106
     * @param object      $schema
107
     * @param string|null $currentUri
108
     *
109
     * @return object
110
     */
111 158
    private function crawl($schema, $currentUri = null)
112
    {
113 158
        $references = $this->getReferences($schema);
114
115 158
        foreach ($references as $path => $ref) {
116
            // resolve
117 48
            if ($this->isExternalRef($ref)) {
118 20
                $resolved = new Reference(function () use ($schema, $path, $ref, $currentUri) {
119 18
                    return $this->resolveExternalReference($schema, $path, $ref, $currentUri);
120 42
                }, $ref);
121
            } else {
122
                $resolved = new Reference($schema, $ref);
123
            }
124 46
125
            // handle any fragments
126
            $resolved = $this->resolveFragment($ref, $resolved);
127 46
128 156
            // merge
129
            $this->mergeResolvedReference($schema, $resolved, $path);
130 156
        }
131
132
        return $schema;
133
    }
134
135
    /**
136
     * Resolve the external reference at the given path.
137
     *
138
     * @param  object      $schema     The JSON Schema
139
     * @param  string      $path       A JSON pointer to the $ref's location in the schema.
140
     * @param  string      $ref        The JSON reference
141
     * @param  string|null $currentUri The URI of the schema, or null if the schema was loaded from an object.
142 20
     *
143
     * @return object                  The schema with the reference resolved.
144 20
     */
145 20
    private function resolveExternalReference($schema, $path, $ref, $currentUri)
146
    {
147 18
        $ref      = $this->makeReferenceAbsolute($schema, $path, $ref, $currentUri);
148
        $resolved = $this->loadExternalRef($ref);
149
150
        return $this->crawl($resolved, strip_fragment($ref));
151
    }
152
153
    /**
154
     * Merge the resolved reference with the schema, at the given path.
155
     *
156
     * @param  object $schema   The schema to merge the resolved reference with
157
     * @param  object $resolved The resolved schema
158 46
     * @param  string $path     A JSON pointer to the path where the reference should be merged.
159
     *
160 46
     * @return void
161
     */
162 16
    private function mergeResolvedReference($schema, $resolved, $path)
163 8
    {
164 8
        if ($path === '') {
165 16
            // Immediately resolve any root references.
166 16
            while ($resolved instanceof Reference) {
167 44
                $resolved = $resolved->resolve();
168 44
            }
169 44
            $this->mergeRootRef($schema, $resolved);
170 44
        } else {
171
            $pointer = new Pointer($schema);
172 46
            if ($pointer->has($path)) {
173
                $pointer->set($path, $resolved);
174
            }
175
        }
176
    }
177
178
    /**
179
     * Check if the reference contains a fragment and resolve
180
     * the pointer.  Otherwise returns the original schema.
181
     *
182 48
     * @param  string $ref
183
     * @param  object $schema
184 48
     *
185 48
     * @return object
186 10
     */
187 10
    private function resolveFragment($ref, $schema)
188
    {
189
        $fragment = parse_url($ref, PHP_URL_FRAGMENT);
190 48
        if ($this->isExternalRef($ref) && is_string($fragment)) {
191
            if ($schema instanceof Reference) {
192
                $schema = $schema->resolve();
193
            }
194
            $pointer  = new Pointer($schema);
195
            return $pointer->get($fragment);
196
        }
197
198
        return $schema;
199
    }
200
201
    /**
202
     * Recursively get all of the references for the given schema.
203
     * Returns an associative array like [path => reference].
204
     * Example:
205
     *
206
     * ['/properties' => '#/definitions/b']
207 158
     *
208
     * The path does NOT include the $ref.
209 158
     *
210
     * @param object $schema The schema to resolve references for.
211 158
     * @param string $path   The current schema path.
212 40
     *
213
     * @return array
214
     */
215 158
    private function getReferences($schema, $path = '')
216 158
    {
217 158
        $refs = [];
218 48
219 48
        if (!is_array($schema) && !is_object($schema)) {
220 156
            return $refs;
221 90
        }
222 90
223 154
        foreach ($schema as $attribute => $parameter) {
224 62
            switch (true) {
225 58
                case $this->isRef($attribute, $parameter):
226 58
                    $refs[$path] = $parameter;
227 58
                    break;
228 58
                case is_object($parameter):
229 62
                    $refs = array_merge($refs, $this->getReferences($parameter, $this->pathPush($path, $attribute)));
230 62
                    break;
231
                case is_array($parameter):
232 158
                    foreach ($parameter as $k => $v) {
233
                        $refs = array_merge(
234 158
                            $refs,
235
                            $this->getReferences($v, $this->pathPush($this->pathPush($path, $attribute), $k))
236
                        );
237
                    }
238
                    break;
239
            }
240
        }
241
242
        return $refs;
243
    }
244
245 104
    /**
246
     * Push a segment onto the given path.
247 104
     *
248
     * @param string $path
249
     * @param string $segment
250
     *
251
     * @return string
252
     */
253
    private function pathPush($path, $segment)
254
    {
255
        return $path . '/' . escape_pointer($segment);
256 158
    }
257
258 158
    /**
259
     * @param string $attribute
260
     * @param mixed  $attributeValue
261
     *
262
     * @return bool
263
     */
264
    private function isRef($attribute, $attributeValue)
265
    {
266 50
        return $attribute === '$ref' && is_string($attributeValue);
267
    }
268 50
269
    /**
270
     * @param string $value
271
     *
272
     * @return bool
273
     */
274
    private function isInternalRef($value)
275
    {
276 50
        return is_string($value) && substr($value, 0, 1) === '#';
277
    }
278 50
279
    /**
280
     * @param string $value
281
     *
282
     * @return bool
283
     */
284
    private function isExternalRef($value)
285
    {
286
        return !$this->isInternalRef($value);
287
    }
288 40
289
    /**
290 40
     * Load an external ref and return the JSON object.
291 38
     *
292
     * @param string $reference
293 38
     *
294
     * @return object
295 38
     */
296
    private function loadExternalRef($reference)
297 38
    {
298
        $this->validateAbsolutePath($reference);
299
        list($prefix, $path) = explode('://', $reference, 2);
300
301
        $loader = $this->getLoader($prefix);
302
303
        $schema = $loader->load($path);
304
305
        return $schema;
306 16
    }
307
308 16
    /**
309 16
     * Merge a resolved reference into the root of the given schema.
310 16
     *
311 16
     * @param object $rootSchema
312 16
     * @param object $resolvedRef
313 16
     */
314
    private function mergeRootRef($rootSchema, $resolvedRef)
315
    {
316
        $ref = '$ref';
317
        unset($rootSchema->$ref);
318
        foreach (get_object_vars($resolvedRef) as $prop => $value) {
319
            $rootSchema->$prop = $value;
320 40
        }
321
    }
322 40
323 2
    /**
324 2
     * Validate an absolute path is valid.
325
     *
326 2
     * @param string $path
327
     */
328 2
    private function validateAbsolutePath($path)
329 2
    {
330
        if (!preg_match('#^.+\:\/\/.*#', $path)) {
331 38
            throw new \InvalidArgumentException(
332
                sprintf(
333
                    'Your path  "%s" is missing a valid prefix.  ' .
334
                    'The schema path should start with a prefix i.e. "file://".',
335
                    $path
336
                )
337
            );
338
        }
339
    }
340
341 20
    /**
342
     * Take a relative reference, and prepend the id of the schema and any
343 20
     * sub schemas to get the absolute url.
344
     *
345
     * @param object      $schema
346
     * @param string      $path
347
     * @param string      $ref
348
     * @param string|null $currentUri
349
     *
350
     * @return string
351
     */
352
    private function makeReferenceAbsolute($schema, $path, $ref, $currentUri = null)
353
    {
354
        // If the reference is absolute, we can just return it without walking the schema.
355
        if (!is_relative_ref($ref)) {
356
            return $ref;
357 20
        }
358
359 20
        $scope = $currentUri ?: '';
360 14
        $scope = $this->getResolvedResolutionScope($schema, $path, $scope);
361
362
        return resolve_uri($ref, $scope);
363 12
    }
364 12
365
    /**
366 12
     * Get the resolved resolution scope by walking the schema and resolving
367
     * every `id` against the msot immediate parent scope.
368
     *
369
     * @see  http://json-schema.org/latest/json-schema-core.html#anchor27
370
     *
371
     * @param  object $schema
372
     * @param  string $path
373
     * @param  string $scope
374
     *
375
     * @return string
376
     */
377
    private function getResolvedResolutionScope($schema, $path, $scope)
378 12
    {
379
        $pointer     = new Pointer($schema);
380 12
        $currentPath = '';
381
382
        foreach (explode('/', $path) as $segment) {
383
            if (!empty($segment)) {
384
                $currentPath .= '/' . $segment;
385
            }
386
            if ($pointer->has($currentPath . '/id')) {
387
                $id = $pointer->get($currentPath . '/id');
388
                if (is_string($id)) {
389
                    $scope = resolve_uri($id, $scope);
390
                }
391 12
            }
392
        }
393 12
394
        return $scope;
395
    }
396
}
397