Completed
Push — master ( abda03...0c8019 )
by Matt
03:06
created

Dereferencer::getInitialResolutionScope()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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