Completed
Pull Request — master (#41)
by Matt
15:33
created

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