Completed
Pull Request — master (#41)
by Matt
06:04
created

Dereferencer::makeReferenceAbsolute()   C

Complexity

Conditions 7
Paths 17

Size

Total Lines 37
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7.0052

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 18
c 2
b 0
f 0
nc 17
nop 4
dl 0
loc 37
ccs 20
cts 21
cp 0.9524
crap 7.0052
rs 6.7272
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 152
    public function __construct()
24
    {
25 152
        $this->registerFileLoader();
26 152
        $this->registerDefaultWebLoaders();
27 152
    }
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 152
    public function dereference($schema)
38
    {
39 152
        $uri = is_string($schema) ? $schema : null;
40
41 152
        if ($uri) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uri of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
42 20
            $schema = $this->loadExternalRef($uri);
43 20
        }
44
45 152
        return $this->crawl($schema, $uri);
0 ignored issues
show
Bug introduced by
It seems like $schema defined by parameter $schema on line 37 can also be of type string; however, League\JsonGuard\Dereferencer::crawl() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
46
    }
47
48
    /**
49
     * Register a Loader for the given prefix.
50
     *
51
     * @param Loader $loader
52
     * @param string $prefix
53
     */
54 120
    public function registerLoader(Loader $loader, $prefix)
55
    {
56 120
        $this->loaders[$prefix] = $loader;
57 120
    }
58
59
    /**
60
     * Get the loader for the given prefix.
61
     *
62
     * @param $prefix
63
     *
64
     * @return Loader
65
     * @throws \InvalidArgumentException
66
     */
67 32
    private function getLoader($prefix)
68
    {
69 32
        if (!array_key_exists($prefix, $this->loaders)) {
70
            throw new \InvalidArgumentException(sprintf('A loader is not registered for the prefix "%s"', $prefix));
71
        }
72
73 32
        return $this->loaders[$prefix];
74
    }
75
76
    /**
77
     * Register the default file loader.
78
     */
79 152
    private function registerFileLoader()
80
    {
81 152
        $this->loaders['file'] = new FileLoader();
82 152
    }
83
84
    /**
85
     * Register the default web loaders.  If the curl extension is loaded,
86
     * the CurlWebLoader will be used.  Otherwise the FileGetContentsWebLoader
87
     * will be used.  You can override this by registering your own loader
88
     * for the 'http' and 'https' protocols.
89
     */
90 152
    private function registerDefaultWebLoaders()
91
    {
92 152
        if (function_exists('curl_init')) {
93 152
            $this->loaders['https'] = new CurlWebLoader('https://');
94 152
            $this->loaders['http']  = new CurlWebLoader('http://');
95 152
        } else {
96
            $this->loaders['https'] = new FileGetContentsWebLoader('https://');
97
            $this->loaders['http']  = new FileGetContentsWebLoader('http://');
98
        }
99 152
    }
100
101
    /**
102
     * Crawl the schema and resolve any references.
103
     *
104
     * @param object      $schema
105
     * @param string|null $currentUri
106
     *
107
     * @return object
108
     */
109 152
    private function crawl($schema, $currentUri = null)
110
    {
111 152
        $references = $this->getReferences($schema);
112
113 152
        foreach ($references as $path => $ref) {
114
            // resolve
115 42
            if ($this->isExternalRef($ref)) {
116 16
                $ref      = $this->makeReferenceAbsolute($schema, $path, $ref, $currentUri);
117 16
                $resolved = $this->loadExternalRef($ref);
118 14
                $resolved = $this->crawl($resolved);
119 14
            } else {
120 38
                $resolved = new Reference($schema, $ref);
121
            }
122
123
            // handle any fragments
124 40
            $fragment = parse_url($ref, PHP_URL_FRAGMENT);
125 40
            if ($this->isExternalRef($ref) && is_string($fragment)) {
126 4
                $pointer  = new Pointer($resolved);
127 4
                $resolved = $pointer->get($fragment);
128 4
            }
129
130
            // Immediately resolve any root references.
131 40
            if ($path === '') {
132 12
                while ($resolved instanceof Reference) {
133 8
                    $resolved = $resolved->resolve();
134 8
                }
135 12
            }
136
137
            // merge
138 40
            if ($path === '') {
139 12
                $this->mergeRootRef($schema, $resolved);
140 12
            } else {
141 40
                $pointer = new Pointer($schema);
142 40
                if ($pointer->has($path)) {
143 40
                    $pointer->set($path, $resolved);
144 40
                }
145
            }
146 150
        }
147
148 150
        return $schema;
149
    }
150
151
    /**
152
     * Recursively get all of the references for the given schema.
153
     * Returns an associative array like [path => reference].
154
     * Example:
155
     *
156
     * ['/properties' => '#/definitions/b']
157
     *
158
     * The path does NOT include the $ref.
159
     *
160
     * @param object $schema The schema to resolve references for.
161
     * @param string $path   The current schema path.
162
     *
163
     * @return array
164
     */
165 152
    private function getReferences($schema, $path = '')
166
    {
167 152
        $refs = [];
168
169 152
        if (!is_array($schema) && !is_object($schema)) {
170 40
            if ($this->isRef($path, $schema)) {
171
                $refs[$path] = $schema;
172
            }
173
174 40
            return $refs;
175
        }
176
177 152
        foreach ($schema as $attribute => $parameter) {
178 152
            if ($this->isRef($attribute, $parameter)) {
179 42
                $refs[$path] = $parameter;
180 42
            }
181 152
            if (is_object($parameter)) {
182 86
                $refs = array_merge($refs, $this->getReferences($parameter, $this->pathPush($path, $attribute)));
183 86
            }
184 152
            if (is_array($parameter)) {
185 62
                foreach ($parameter as $k => $v) {
186 58
                    $refs = array_merge(
187 58
                        $refs,
188 58
                        $this->getReferences($v, $this->pathPush($this->pathPush($path, $attribute), $k))
189 58
                    );
190 62
                }
191 62
            }
192 152
        }
193
194 152
        return $refs;
195
    }
196
197
    /**
198
     * Push a segment onto the given path.
199
     *
200
     * @param string $path
201
     * @param string $segment
202
     *
203
     * @return string
204
     */
205 100
    private function pathPush($path, $segment)
206
    {
207 100
        return $path . '/' . escape_pointer($segment);
208
    }
209
210
    /**
211
     * @param string $attribute
212
     * @param mixed $attributeValue
213
     *
214
     * @return bool
215
     */
216 152
    private function isRef($attribute, $attributeValue)
217
    {
218 152
        return $attribute === '$ref' && is_string($attributeValue);
219
    }
220
221
    /**
222
     * @param string $parameter
223
     *
224
     * @return bool
225
     */
226 42
    private function isInternalRef($parameter)
227
    {
228 42
        return is_string($parameter) && substr($parameter, 0, 1) === '#';
229
    }
230
231
    /**
232
     * @param string $parameter
233
     *
234
     * @return bool
235
     */
236 42
    private function isExternalRef($parameter)
237
    {
238 42
        return !$this->isInternalRef($parameter);
239
    }
240
241
    /**
242
     * Load an external ref and return the JSON object.
243
     *
244
     * @param string $reference
245
     *
246
     * @return object
247
     */
248 34
    private function loadExternalRef($reference)
249
    {
250 34
        $this->validateAbsolutePath($reference);
251 32
        list($prefix, $path) = explode('://', $reference, 2);
252
253 32
        $loader = $this->getLoader($prefix);
254
255 32
        $schema = $loader->load($path);
256
257 32
        return $schema;
258
    }
259
260
    /**
261
     * Merge a resolved reference into the root of the given schema.
262
     *
263
     * @param object $rootSchema
264
     * @param object $resolvedRef
265
     */
266 12
    private function mergeRootRef($rootSchema, $resolvedRef)
267
    {
268 12
        $ref = '$ref';
269 12
        unset($rootSchema->$ref);
270 12
        foreach (get_object_vars($resolvedRef) as $prop => $value) {
271 12
            $rootSchema->$prop = $value;
272 12
        }
273 12
    }
274
275
    /**
276
     * Validate an absolute path is valid.
277
     *
278
     * @param string $path
279
     */
280 34
    private function validateAbsolutePath($path)
281
    {
282 34
        if (!preg_match('#^.+\:\/\/.*#', $path)) {
283 2
            throw new \InvalidArgumentException(
284
                'Your path is missing a valid prefix.  The schema path should start with a prefix i.e. "file://".'
285 2
            );
286
        }
287 32
    }
288
289
    /**
290
     * Determine if a reference is relative.
291
     * A reference is relative if it does not being with a prefix.
292
     *
293
     * @param string $ref
294
     *
295
     * @return bool
296
     */
297 16
    private function isRelativeRef($ref)
298
    {
299 16
        return !preg_match('#^.+\:\/\/.*#', $ref);
300
    }
301
302
    /**
303
     * Take a relative reference, and prepend the id of the schema and any
304
     * sub schemas to get the absolute url.
305
     *
306
     * @param object      $schema
307
     * @param string      $path
308
     * @param string      $ref
309
     * @param string|null $currentUri
310
     *
311
     * @return string
312
     */
313 16
    private function makeReferenceAbsolute($schema, $path, $ref, $currentUri = null)
314
    {
315 16
        if (!$this->isRelativeRef($ref)) {
316 12
            return $ref;
317
        }
318
319
        // The initial resolution scope of a schema is the URI of the schema itself,
320
        // if any, or the empty URI if the schema was not loaded from a URI.
321 8
        $scope = $currentUri ? str_replace(basename($currentUri), '', $currentUri) : '';
322
323 8
        $pointer = new Pointer($schema);
324
325
        // When an id is encountered, an implementation MUST resolve this id against the most
326
        // immediate parent scope.  The resolved URI will be the new resolution scope
327
        // for this subschema and all its children, until another id is encountered.
328 8
        if ($pointer->has('/id')) {
329 4
            $scope = $pointer->get('/id');
330 4
        }
331
332 8
        $currentPath = '';
333 8
        foreach (array_slice(explode('/', $path), 1) as $segment) {
334 6
            $currentPath .= '/' . $segment;
335 6
            if ($pointer->has($currentPath . '/id')) {
336 4
                $id = $pointer->get($currentPath . '/id');
337
                // If the ID is a relative reference, append it to the current scope.
338
                // Otherwise we completely replace the scope.
339 4
                if ($this->isRelativeRef($id)) {
340 4
                    $scope .= $id;
341 4
                } else {
342
                    $scope = $id;
343
                }
344 4
            }
345 8
        }
346 8
        $ref = $scope . $ref;
347
348 8
        return $ref;
349
    }
350
}
351