Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
JsonTreeRef
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 14
2756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 attachSchema
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getTitle
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 renamePropname
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getType
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getFullIndex
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getDataPath
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getDataPathAsString
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getDataPathTitles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getMappingChildRef
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 getSequenceChildRef
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 validate
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
210
 validateObjectChildren
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
 validateArrayChildren
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * JSON Schema Validation Library
4 *
5 * Copyright (c) 2005-2012, Rob Lanphier
6 * All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions are
10 * met:
11 *
12 *     * Redistributions of source code must retain the above copyright
13 *       notice, this list of conditions and the following disclaimer.
14 *
15 *     * Redistributions in binary form must reproduce the above
16 *       copyright notice, this list of conditions and the following
17 *       disclaimer in the documentation and/or other materials provided
18 *       with the distribution.
19 *
20 *     * Neither my name nor the names of my contributors may be used to
21 *       endorse or promote products derived from this software without
22 *       specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 *
36 * @author Rob Lanphier <robla@wikimedia.org>
37 * @copyright © 2011-2012 Rob Lanphier
38 * @license http://jsonwidget.org/LICENSE BSD-3-Clause
39 */
40
41namespace MediaWiki\Extension\EventLogging\Libs\JsonSchemaValidation;
42
43/**
44 * Structure for representing a data tree, where each node (ref) is aware of its
45 * context and associated schema.
46 */
47class JsonTreeRef {
48
49    /** @var mixed|null */
50    public $node;
51
52    /** @var JsonTreeRef|null */
53    public $parent;
54
55    /** @var int|null */
56    public $nodeindex;
57
58    /** @var string|null */
59    public $nodename;
60
61    /** @var TreeRef|null */
62    public $schemaref;
63
64    /** @var string */
65    public $fullindex;
66
67    /** @var array */
68    public $datapath;
69
70    /** @var JsonSchemaIndex */
71    public $schemaindex;
72
73    /**
74     * @param mixed|null $node
75     * @param JsonTreeRef|null $parent
76     * @param int|null $nodeindex
77     * @param string|null $nodename
78     * @param TreeRef|null $schemaref
79     */
80    public function __construct(
81        $node,
82        $parent = null,
83        $nodeindex = null,
84        $nodename = null,
85        $schemaref = null
86    ) {
87        $this->node = $node;
88        $this->parent = $parent;
89        $this->nodeindex = $nodeindex;
90        $this->nodename = $nodename;
91        $this->schemaref = $schemaref;
92        $this->fullindex = $this->getFullIndex();
93        $this->datapath = [];
94        if ( $schemaref !== null ) {
95            $this->attachSchema();
96        }
97    }
98
99    /**
100     * Associate the relevant node of the JSON schema to this node in the JSON
101     * @param null|array $schema
102     */
103    public function attachSchema( $schema = null ) {
104        if ( $schema !== null ) {
105            $this->schemaindex = new JsonSchemaIndex( $schema );
106            $this->nodename = $schema['title'] ?? 'Root node';
107            $this->schemaref = $this->schemaindex->newRef( $schema, null, null, $this->nodename );
108        } elseif ( $this->parent !== null ) {
109            $this->schemaindex = $this->parent->schemaindex;
110        }
111    }
112
113    /**
114     * Return the title for this ref, typically defined in the schema as the
115     * user-friendly string for this node.
116     * @return string
117     */
118    public function getTitle() {
119        if ( isset( $this->nodename ) ) {
120            return $this->nodename;
121        }
122        if ( isset( $this->node['title'] ) ) {
123            return $this->node['title'];
124        }
125
126        return (string)$this->nodeindex;
127    }
128
129    /**
130     * Rename a user key.  Useful for interactive editing/modification, but not
131     * so helpful for static interpretation.
132     * @param int $newindex
133     */
134    public function renamePropname( $newindex ) {
135        $oldindex = $this->nodeindex;
136        $this->parent->node[$newindex] = $this->node;
137        $this->nodeindex = $newindex;
138        $this->nodename = (string)$newindex;
139        $this->fullindex = $this->getFullIndex();
140        unset( $this->parent->node[$oldindex] );
141    }
142
143    /**
144     * Return the type of this node as specified in the schema.  If "any",
145     * infer it from the data.
146     * @return mixed
147     */
148    public function getType() {
149        if ( array_key_exists( 'type', $this->schemaref->node ) ) {
150            $nodetype = $this->schemaref->node['type'];
151        } else {
152            $nodetype = 'any';
153        }
154
155        if ( $nodetype === 'any' ) {
156            if ( $this->node === null ) {
157                return null;
158            }
159            return JsonUtil::getType( $this->node );
160        }
161
162        return $nodetype;
163    }
164
165    /**
166     * Return a unique identifier that may be used to find a node.  This
167     * is only as robust as stringToId is (i.e. not that robust), but is
168     * good enough for many cases.
169     * @return string
170     */
171    public function getFullIndex() {
172        if ( $this->parent === null ) {
173            return 'json_root';
174        }
175
176        return $this->parent->getFullIndex() . '.' . JsonUtil::stringToId( $this->nodeindex );
177    }
178
179    /**
180     * Get a path to the element in the array.  if $foo['a'][1] would load the
181     * node, then the return value of this would be array('a',1)
182     * @return array
183     */
184    public function getDataPath() {
185        if ( !is_object( $this->parent ) ) {
186            return [];
187        }
188        $retval = $this->parent->getDataPath();
189        $retval[] = $this->nodeindex;
190        return $retval;
191    }
192
193    /**
194     * Return path in something that looks like an array path.  For example,
195     * for this data: [{'0a':1,'0b':{'0ba':2,'0bb':3}},{'1a':4}]
196     * the leaf node with a value of 4 would have a data path of '[1]["1a"]',
197     * while the leaf node with a value of 2 would have a data path of
198     * '[0]["0b"]["oba"]'
199     * @return string
200     */
201    public function getDataPathAsString() {
202        $retval = '';
203        foreach ( $this->getDataPath() as $item ) {
204            $retval .= '[' . json_encode( $item ) . ']';
205        }
206        return $retval;
207    }
208
209    /**
210     * Return data path in user-friendly terms.  This will use the same
211     * terminology as used in the user interface (1-indexed arrays)
212     * @return string
213     */
214    public function getDataPathTitles() {
215        if ( !is_object( $this->parent ) ) {
216            return $this->getTitle();
217        }
218
219        return $this->parent->getDataPathTitles() . ' -> '
220            . $this->getTitle();
221    }
222
223    /**
224     * Return the child ref for $this ref associated with a given $key
225     * @param int|string $key
226     * @return JsonTreeRef
227     * @throws JsonSchemaException
228     */
229    public function getMappingChildRef( $key ) {
230        $snode = $this->schemaref->node;
231        $schemadata = [];
232        $nodename = $key;
233        if ( array_key_exists( 'properties', $snode ) &&
234            array_key_exists( $key, $snode['properties'] ) ) {
235            $schemadata = $snode['properties'][$key];
236            $nodename = $schemadata['title'] ?? $key;
237        } elseif ( array_key_exists( 'additionalProperties', $snode ) ) {
238            // additionalProperties can *either* be a boolean or can be
239            // defined as a schema (an object)
240            if ( gettype( $snode['additionalProperties'] ) === 'boolean' ) {
241                if ( !$snode['additionalProperties'] ) {
242                    throw new JsonSchemaException( 'jsonschema-invalidkey',
243                        (string)$key, $this->getDataPathTitles() );
244                }
245            } else {
246                $schemadata = $snode['additionalProperties'];
247                $nodename = $key;
248            }
249        }
250        // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
251        $value = $this->node[$key];
252        $schemai = $this->schemaindex->newRef( $schemadata, $this->schemaref, $key, (string)$key );
253
254        return new JsonTreeRef( $value, $this, $key, $nodename, $schemai );
255    }
256
257    /**
258     * Return the child ref for $this ref associated with a given index $i
259     * @param int $i
260     * @return JsonTreeRef
261     */
262    public function getSequenceChildRef( $i ) {
263        // TODO: make this conform to draft-03 by also allowing single object
264        if ( array_key_exists( 'items', $this->schemaref->node ) ) {
265            $schemanode = $this->schemaref->node['items'];
266        } else {
267            $schemanode = [];
268        }
269        $itemname = $schemanode['title'] ?? "Item";
270        $nodename = $itemname . " #" . ( (string)( $i + 1 ) );
271        $schemai = $this->schemaindex->newRef( $schemanode, $this->schemaref, 0, (string)$i );
272
273        // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
274        return new JsonTreeRef( $this->node[$i], $this, $i, $nodename, $schemai );
275    }
276
277    /**
278     * Validate the JSON node in this ref against the attached schema ref.
279     * Return true on success, and throw a JsonSchemaException on failure.
280     * @return bool
281     */
282    public function validate() {
283        if ( array_key_exists( 'enum', $this->schemaref->node ) &&
284            !in_array( $this->node, $this->schemaref->node['enum'] ) ) {
285            $e = new JsonSchemaException( 'jsonschema-invalid-notinenum',
286                JsonUtil::encodeForMsg( $this->node ), $this->getDataPathTitles() );
287            $e->subtype = 'validate-fail';
288            throw $e;
289        }
290        $datatype = JsonUtil::getType( $this->node );
291        $schematype = $this->getType();
292        if ( $datatype === 'array' && $schematype === 'object' ) {
293            // PHP datatypes are kinda loose, so we'll fudge
294            $datatype = 'object';
295        }
296        if ( $datatype === 'number' && $schematype === 'integer' &&
297            $this->node == (int)$this->node ) {
298            // Alright, it'll work as an int
299            $datatype = 'integer';
300        }
301        if ( $datatype != $schematype ) {
302            if ( $datatype === null && !is_object( $this->parent ) ) {
303                $e = new JsonSchemaException( 'jsonschema-invalidempty' );
304                $e->subtype = 'validate-fail-null';
305                throw $e;
306            }
307            $datatype = $datatype ?: 'null';
308            $e = new JsonSchemaException( 'jsonschema-invalidnode',
309                $schematype, $datatype, $this->getDataPathTitles() );
310            $e->subtype = 'validate-fail';
311            throw $e;
312        }
313        switch ( $schematype ) {
314            case 'object':
315                $this->validateObjectChildren();
316                break;
317            case 'array':
318                $this->validateArrayChildren();
319                break;
320        }
321        return true;
322    }
323
324    private function validateObjectChildren() {
325        if ( array_key_exists( 'properties', $this->schemaref->node ) ) {
326            foreach ( $this->schemaref->node['properties'] as $skey => $svalue ) {
327                $keyRequired = array_key_exists( 'required', $svalue ) ? $svalue['required'] : false;
328                if ( $keyRequired && !array_key_exists( $skey, $this->node ) ) {
329                    $e = new JsonSchemaException( 'jsonschema-invalid-missingfield', $skey );
330                    $e->subtype = 'validate-fail-missingfield';
331                    throw $e;
332                }
333            }
334        }
335
336        foreach ( $this->node as $key => $value ) {
337            $jsoni = $this->getMappingChildRef( $key );
338            $jsoni->validate();
339        }
340        return true;
341    }
342
343    private function validateArrayChildren() {
344        $length = count( $this->node );
345        for ( $i = 0; $i < $length; $i++ ) {
346            $jsoni = $this->getSequenceChildRef( $i );
347            $jsoni->validate();
348        }
349    }
350}