Completed
Push — master ( 7053c8...c25338 )
by
unknown
02:34
created

Dereferencer::loadExternalRef()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

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