Completed
Push — master ( 157c74...e24db7 )
by Matt
01:59
created

Dereferencer::getLoaders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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