Completed
Push — master ( a0be20...c3e525 )
by Matt
02:18
created

Dereferencer::getLoader()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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