JsonSchemaValidator.jsm 7.55 KB
Newer Older
1
2
3
4
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

5
6
7
8
9
10
11
12
13
14
/* This file implements a not-quite standard JSON schema validator. It differs
 * from the spec in a few ways:
 *
 *  - the spec doesn't allow custom types to be defined, but this validator
 *    defines "URL", "URLorEmpty", "origin" etc.
 * - Strings are automatically converted to nsIURIs for the appropriate types.
 * - It doesn't support "pattern" when matching strings.
 * - The boolean type accepts (and casts) 0 and 1 as valid values.
 */

15
16
"use strict";

17
18
19
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
20

21
22
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);

23
XPCOMUtils.defineLazyGetter(this, "log", () => {
24
  let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
25
  return new ConsoleAPI({
26
    prefix: "JsonSchemaValidator.jsm",
27
28
29
30
31
32
    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
    // messages during development. See LOG_LEVELS in Console.jsm for details.
    maxLogLevel: "error",
  });
});

33
var EXPORTED_SYMBOLS = ["JsonSchemaValidator"];
34

35
var JsonSchemaValidator = {
36
37
  validateAndParseParameters(param, properties) {
    return validateAndParseParamRecursive(param, properties);
38
  },
39
40
41
};

function validateAndParseParamRecursive(param, properties) {
42
  if (properties.enum) {
43
44
45
46
47
48
49
    if (properties.enum.includes(param)) {
      return [true, param];
    }
    return [false, null];
  }

  log.debug(`checking @${param}@ for type ${properties.type}`);
50
51
52
53
54
55
56

  if (Array.isArray(properties.type)) {
    log.debug("type is an array");
    // For an array of types, the value is valid if it matches any of the listed
    // types. To check this, make versions of the object definition that include
    // only one type at a time, and check the value against each one.
    for (const type of properties.type) {
57
      let typeProperties = Object.assign({}, properties, { type });
58
59
60
61
62
63
64
65
66
67
      log.debug(`checking subtype ${type}`);
      let [valid, data] = validateAndParseParamRecursive(param, typeProperties);
      if (valid) {
        return [true, data];
      }
    }
    // None of the types matched
    return [false, null];
  }

68
69
70
71
72
73
  switch (properties.type) {
    case "boolean":
    case "number":
    case "integer":
    case "string":
    case "URL":
74
    case "URLorEmpty":
75
    case "origin":
76
    case "null":
77
78
79
80
81
82
83
84
      return validateAndParseSimpleParam(param, properties.type);

    case "array":
      if (!Array.isArray(param)) {
        log.error("Array expected but not received");
        return [false, null];
      }

85
86
87
88
89
90
      // strict defaults to true if not present
      let strict = true;
      if ("strict" in properties) {
        strict = properties.strict;
      }

91
92
      let parsedArray = [];
      for (let item of param) {
93
94
95
96
97
98
99
        log.debug(
          `in array, checking @${item}@ for type ${properties.items.type}`
        );
        let [valid, parsedValue] = validateAndParseParamRecursive(
          item,
          properties.items
        );
100
        if (!valid) {
101
102
103
104
          if (strict) {
            return [false, null];
          }
          continue;
105
106
107
108
109
110
111
112
        }

        parsedArray.push(parsedValue);
      }

      return [true, parsedArray];

    case "object": {
113
      if (typeof param != "object") {
114
115
116
117
118
        log.error("Object expected but not received");
        return [false, null];
      }

      let parsedObj = {};
119
120
121
122
123
124
125
      let patternProperties = [];
      if ("patternProperties" in properties) {
        for (let propName of Object.keys(properties.patternProperties || {})) {
          let pattern;
          try {
            pattern = new RegExp(propName);
          } catch (e) {
126
127
128
            throw new Error(
              `Internal error: Invalid property pattern ${propName}`
            );
129
130
131
132
133
134
135
          }
          patternProperties.push({
            pattern,
            schema: properties.patternProperties[propName],
          });
        }
      }
136

137
138
139
140
      if (properties.required) {
        for (let required of properties.required) {
          if (!(required in param)) {
            log.error(`Object is missing required property ${required}`);
141
142
143
            return [false, null];
          }
        }
144
      }
145

146
147
      for (let item of Object.keys(param)) {
        let schema;
148
149
150
151
        if (
          "properties" in properties &&
          properties.properties.hasOwnProperty(item)
        ) {
152
153
154
155
156
157
158
159
160
161
          schema = properties.properties[item];
        } else if (patternProperties.length) {
          for (let patternProperty of patternProperties) {
            if (patternProperty.pattern.test(item)) {
              schema = patternProperty.schema;
              break;
            }
          }
        }
        if (schema) {
162
163
164
165
          let [valid, parsedValue] = validateAndParseParamRecursive(
            param[item],
            schema
          );
166
167
168
169
          if (!valid) {
            return [false, null];
          }
          parsedObj[item] = parsedValue;
170
171
172
173
        }
      }
      return [true, parsedObj];
    }
174
175

    case "JSON":
176
      if (typeof param == "object") {
177
178
179
180
        return [true, param];
      }
      try {
        let json = JSON.parse(param);
181
        if (typeof json != "object") {
182
183
184
185
186
187
188
189
          log.error("JSON was not an object");
          return [false, null];
        }
        return [true, json];
      } catch (e) {
        log.error("JSON string couldn't be parsed");
        return [false, null];
      }
190
191
192
193
194
195
196
197
198
199
200
  }

  return [false, null];
}

function validateAndParseSimpleParam(param, type) {
  let valid = false;
  let parsedParam = param;

  switch (type) {
    case "boolean":
201
      if (typeof param == "boolean") {
202
        valid = true;
203
      } else if (typeof param == "number" && (param == 0 || param == 1)) {
204
205
206
207
208
        valid = true;
        parsedParam = !!param;
      }
      break;

209
210
    case "number":
    case "string":
211
      valid = typeof param == type;
212
213
214
215
      break;

    // integer is an alias to "number" that some JSON schema tools use
    case "integer":
216
      valid = typeof param == "number";
217
218
      break;

219
220
221
222
    case "null":
      valid = param === null;
      break;

223
    case "origin":
224
      if (typeof param != "string") {
225
226
227
228
        break;
      }

      try {
229
        parsedParam = new URL(param);
230

231
232
233
234
235
        if (parsedParam.protocol == "file:") {
          // Treat the entire file URL as an origin.
          // Note this is stricter than the current Firefox policy,
          // but consistent with Chrome.
          // See https://bugzilla.mozilla.org/show_bug.cgi?id=803143
236
          valid = true;
237
238
239
240
        } else {
          let pathQueryRef = parsedParam.pathname + parsedParam.hash;
          // Make sure that "origin" types won't accept full URLs.
          if (pathQueryRef != "/" && pathQueryRef != "") {
241
242
243
            log.error(
              `Ignoring parameter "${param}" - origin was expected but received full URL.`
            );
244
245
246
247
            valid = false;
          } else {
            valid = true;
          }
248
249
250
251
252
253
254
        }
      } catch (ex) {
        valid = false;
      }
      break;

    case "URL":
255
    case "URLorEmpty":
256
      if (typeof param != "string") {
257
258
259
        break;
      }

260
261
262
263
264
      if (type == "URLorEmpty" && param === "") {
        valid = true;
        break;
      }

265
      try {
266
        parsedParam = new URL(param);
267
268
        valid = true;
      } catch (ex) {
269
270
271
272
273
        if (!param.startsWith("http")) {
          log.error(
            `Ignoring parameter "${param}" - scheme (http or https) must be specified.`
          );
        }
274
275
276
277
278
279
280
        valid = false;
      }
      break;
  }

  return [valid, parsedParam];
}