Bug 1419102 - Add param validation and parsing. r=Mossop draft
authorFelipe Gomes <felipc@gmail.com>
Fri, 19 Jan 2018 12:49:08 -0200
changeset 722687 8e36f77b1c8fd2cc3b4889ab978f45de6b7afde8
parent 722686 1793ca584c5cc13c843b227192c98d3ff34888a2
child 722688 6f86932d308f1fdf08982f1c611aefdca81db6d1
push id96199
push userfelipc@gmail.com
push dateFri, 19 Jan 2018 15:05:44 +0000
reviewersMossop
bugs1419102
milestone59.0a1
Bug 1419102 - Add param validation and parsing. r=Mossop MozReview-Commit-ID: qIkQwAoW8Y
browser/components/enterprisepolicies/EnterprisePolicies.js
browser/components/enterprisepolicies/Policies.jsm
browser/components/enterprisepolicies/PoliciesValidator.jsm
browser/components/enterprisepolicies/helpers/sample.json
browser/components/enterprisepolicies/moz.build
browser/components/enterprisepolicies/schemas/policies.json
browser/components/enterprisepolicies/tests/browser/browser.ini
browser/components/enterprisepolicies/tests/browser/browser_policies_validate_and_parse_API.js
--- a/browser/components/enterprisepolicies/EnterprisePolicies.js
+++ b/browser/components/enterprisepolicies/EnterprisePolicies.js
@@ -10,17 +10,17 @@ const Cu = Components.utils;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   Policies: "resource:///modules/policies/Policies.jsm",
-  PoliciesValidator: "resource:///modules/policies/Policies.jsm",
+  PoliciesValidator: "resource:///modules/policies/PoliciesValidator.jsm",
 });
 
 function LOG(s) {
   Services.console.logStringMessage("$ POLICIES $: " + s + "\n");
   dump("% POLICIES %: " + s + "\n");
 }
 
 // This is the file that will be searched for in the
@@ -107,33 +107,35 @@ EnterprisePoliciesManager.prototype = {
       let policySchema = schema.properties[policyName];
       let policyParameters = json.policies[policyName];
 
       if (!policySchema) {
         LOG("This policy is not defined in the schema.");
         continue;
       }
 
-      if (!PoliciesValidator.validateAndParseParameters(policyName,
-                                                        policyParameters,
-                                                        policySchema)) {
+      let [parametersAreValid, parsedParameters] =
+        PoliciesValidator.validateAndParseParameters(policyParameters,
+                                                     policySchema);
+
+      if (!parametersAreValid) {
         LOG(`Invalid parameters specified for ${policyName}.`);
         continue;
       }
 
       let policyImpl = Policies[policyName];
 
       for (let timing of Object.keys(this._callbacks)) {
         let policyCallback = policyImpl["on" + timing];
         if (policyCallback) {
           this._schedulePolicyCallback(
             timing,
             policyCallback.bind(null,
                                 this, /* the EnterprisePoliciesManager */
-                                policyParameters));
+                                parsedParameters));
         }
       }
     }
   },
 
   _callbacks: {
     ProfileAfterChange: [],
     BeforeUIStartup: [],
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -7,35 +7,42 @@
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
-this.EXPORTED_SYMBOLS = ["Policies", "PoliciesValidator"];
+function LOG(s) {
+  Services.console.logStringMessage("$ POLICIES CHILD $: " + s + "\n");
+  dump("% POLICIES.JSM %: " + s + "\n");
+}
 
-this.PoliciesValidator = {
-  validateAndParseParameters() {
-    return true;
-  }
-}
+this.EXPORTED_SYMBOLS = ["Policies"];
 
 this.Policies = {
   "block_about_config": {
     onBeforeUIStartup(manager, param) {
       if (param == true) {
+        LOG("BLOCKING ABOUT:CONFIG");
         manager.disallowFeature("about:config", true);
       }
     }
   },
 
   "block_devtools": {
     onProfileAfterChange(manager, param) {
       if (param == true) {
         manager.disallowFeature("devtools");
       }
     }
   },
 
-  "bookmarks_on_menu": {},
+  "bookmarks_on_menu": {
+    onProfileAfterChange(manager, param) {
+      LOG("Bookmarks to add: ");
+      for (let bookmark of param) {
+        LOG("  -> " + bookmark.spec);
+      }
+    }
+  },
 };
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/PoliciesValidator.jsm
@@ -0,0 +1,140 @@
+/* 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/. */
+
+"use strict";
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function LOG(s) {
+  Services.console.logStringMessage("$ POLICIES CHILD $: " + s + "\n");
+  dump("% POLICIES.JSM %: " + s + "\n");
+}
+
+this.EXPORTED_SYMBOLS = ["PoliciesValidator"];
+
+this.PoliciesValidator = {
+  validateAndParseParameters(param, properties) {
+    return validateAndParseParamRecursive(param, properties);
+  }
+};
+
+function validateAndParseParamRecursive(param, properties) {
+  if (properties.enum) {
+    if (properties.enum.includes(param)) {
+      return [true, param];
+    }
+    return [false, null];
+  }
+
+  LOG(`checking @${param}@ for type ${properties.type}`);
+  switch (properties.type) {
+    case "boolean":
+    case "number":
+    case "integer":
+    case "string":
+    case "URL":
+    case "origin":
+      return validateAndParseSimpleParam(param, properties.type);
+
+    case "array":
+      if (!Array.isArray(param)) {
+        LOG("bail early");
+        return [false, null];
+      }
+
+      let parsedArray = [];
+      for (let item of param) {
+        LOG(`in array, checking @${item}@ for type ${properties.items.type}`);
+        let [valid, parsedValue] = validateAndParseParamRecursive(item, properties.items);
+        if (!valid) {
+          return [false, null];
+        }
+
+        parsedArray.push(parsedValue);
+      }
+
+      return [true, parsedArray];
+
+    case "object": {
+      if (typeof(param) != "object") {
+        return [false, null];
+      }
+
+      let parsedObj = {};
+      for (let property of Object.keys(properties.properties)) {
+        LOG(`in object, for property ${property} checking @${param[property]}@ for type ${properties.properties[property].type}`);
+        let [valid, parsedValue] = validateAndParseParamRecursive(param[property], properties.properties[property]);
+        if (!valid) {
+          return [false, null];
+        }
+
+        parsedObj[property] = parsedValue;
+      }
+
+      return [true, parsedObj];
+    }
+  }
+
+  return [false, null];
+}
+
+function validateAndParseSimpleParam(param, type) {
+  let valid = false;
+  let parsedParam = param;
+
+  switch (type) {
+    case "boolean":
+    case "number":
+    case "string":
+      valid = (typeof(param) == type);
+      break;
+
+    // integer is an alias to "number" that some JSON schema tools use
+    case "integer":
+      valid = (typeof(param) == "number");
+      break;
+
+    case "origin":
+      if (typeof(param) != "string") {
+        break;
+      }
+
+      try {
+        parsedParam = Services.io.newURI(param);
+
+        let pathQueryRef = parsedParam.pathQueryRef;
+        // Make sure that "host" types won't accept full URLs that include
+        // something besides the host.
+        if (pathQueryRef != "/" && pathQueryRef != "") {
+          valid = false;
+        } else {
+          valid = true;
+        }
+      } catch (ex) {
+        valid = false;
+      }
+      break;
+
+    case "URL":
+      if (typeof(param) != "string") {
+        break;
+      }
+
+      try {
+        parsedParam = Services.io.newURI(param);
+        valid = true;
+      } catch (ex) {
+        valid = false;
+      }
+      break;
+  }
+
+  return [valid, parsedParam];
+}
--- a/browser/components/enterprisepolicies/helpers/sample.json
+++ b/browser/components/enterprisepolicies/helpers/sample.json
@@ -1,11 +1,16 @@
 {
   "policies": {
     "block_about_config": true,
     "block_devtools": true,
     "bookmarks_on_menu": [
       "https://www.mozilla.org/firefox/new/",
       "https://www.example.com",
       "https://www.example.org"
-    ]
+    ],
+    "url_policy": "example.com",
+    "bookmarks_test_policy": {
+      "title": "Bookmark title",
+      "url": "http://www.example.com/bookmark"
+    }
   }
 }
--- a/browser/components/enterprisepolicies/moz.build
+++ b/browser/components/enterprisepolicies/moz.build
@@ -25,11 +25,12 @@ XPIDL_MODULE = 'enterprisepolicies'
 EXTRA_COMPONENTS += [
     'EnterprisePolicies.js',
     'EnterprisePolicies.manifest',
     'EnterprisePoliciesContent.js',
 ]
 
 EXTRA_JS_MODULES.policies += [
     'Policies.jsm',
+    'PoliciesValidator.jsm',
 ]
 
 FINAL_LIBRARY = 'browsercomps'
--- a/browser/components/enterprisepolicies/schemas/policies.json
+++ b/browser/components/enterprisepolicies/schemas/policies.json
@@ -1,13 +1,13 @@
 {
   "$schema": "http://json-schema.org/draft-04/schema#",
   "type": "object",
   "properties": {
-    "block_about_config": {
+     "block_about_config": {
       "description": "Blocks access to the about:config page.",
       "first_available": "59.0",
 
       "type": "boolean",
       "enum": [true]
     },
 
     "block_devtools": {
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -3,8 +3,9 @@ prefs =
   browser.policies.enabled=true
 support-files =
   head.js
   config_simple_policies.json
   config_broken_json.json
 
 [browser_policies_broken_json.js]
 [browser_policies_simple_policies.js]
+[browser_policies_validate_and_parse_API.js]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_validate_and_parse_API.js
@@ -0,0 +1,231 @@
+/* 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/. */
+
+"use strict";
+
+/* This file will test the parameters parsing and validation directly through
+   the PoliciesValidator API.
+ */
+
+const { PoliciesValidator } = Cu.import("resource:///modules/policies/PoliciesValidator.jsm", {});
+
+add_task(async function test_boolean_values() {
+  let schema = {
+    type: "boolean"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(true, schema);
+  ok(valid && parsed === true, "Parsed boolean value correctly");
+
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(false, schema);
+  ok(valid && parsed === false, "Parsed boolean value correctly");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("0", schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters("true", schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(undefined, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value");
+});
+
+add_task(async function test_number_values() {
+  let schema = {
+    type: "number"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(1, schema);
+  ok(valid && parsed === 1, "Parsed number value correctly");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("1", schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(true, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value");
+});
+
+add_task(async function test_integer_values() {
+  // Integer is an alias for number
+  let schema = {
+    type: "integer"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(1, schema);
+  ok(valid && parsed == 1, "Parsed integer value correctly");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("1", schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(true, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value");
+});
+
+add_task(async function test_string_values() {
+  let schema = {
+    type: "string"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters("foobar", schema);
+  ok(valid && parsed == "foobar", "Parsed string value correctly");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters(1, schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(true, schema)[0], "No type coercion");
+  ok(!PoliciesValidator.validateAndParseParameters(undefined, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+  ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value");
+});
+
+add_task(async function test_URL_values() {
+  let schema = {
+    type: "URL"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters("https://www.example.com/foo#bar", schema);
+  ok(valid, "URL is valid");
+  ok(parsed instanceof Ci.nsIURI, "parsed is a nsIURI");
+  is(parsed.prePath, "https://www.example.com", "prePath is correct");
+  is(parsed.pathQueryRef, "/foo#bar", "pathQueryRef is correct");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("www.example.com", schema)[0], "Scheme is required for URL");
+  ok(!PoliciesValidator.validateAndParseParameters("https://:!$%", schema)[0], "Invalid URL");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+});
+
+add_task(async function test_origin_values() {
+  // Origin is a URL that doesn't contain a path/query string (i.e., it's only scheme + host + port)
+  let schema = {
+    type: "origin"
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters("https://www.example.com", schema);
+  ok(valid, "Origin is valid");
+  ok(parsed instanceof Ci.nsIURI, "parsed is a nsIURI");
+  is(parsed.prePath, "https://www.example.com", "prePath is correct");
+  is(parsed.pathQueryRef, "/", "pathQueryRef is corect");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters("https://www.example.com/foobar", schema)[0], "Origin cannot contain a path part");
+  ok(!PoliciesValidator.validateAndParseParameters("https://:!$%", schema)[0], "Invalid origin");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value");
+});
+
+add_task(async function test_array_values() {
+  // The types inside an array object must all be the same
+  let schema = {
+    type: "array",
+    items: {
+      type: "number"
+    }
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters([1, 2, 3], schema);
+  ok(valid, "Array is valid");
+  ok(Array.isArray(parsed), "parsed is an array");
+  is(parsed.length, 3, "array is correct");
+
+  // An empty array is also valid
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters([], schema);
+  ok(valid, "Array is valid");
+  ok(Array.isArray(parsed), "parsed is an array");
+  is(parsed.length, 0, "array is correct");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters([1, true, 3], schema)[0], "Mixed types");
+  ok(!PoliciesValidator.validateAndParseParameters(2, schema)[0], "Type is correct but not in an array");
+  ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Object is not an array");
+});
+
+add_task(async function test_object_values() {
+  let schema = {
+    type: "object",
+    properties: {
+      url: {
+        type: "URL"
+      },
+      title: {
+        type: "string"
+      }
+    }
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(
+    {
+      url: "https://www.example.com/foo#bar",
+      title: "Foo",
+      alias: "Bar"
+    },
+    schema);
+
+  ok(valid, "Object is valid");
+  ok(typeof(parsed) == "object", "parsed in an object");
+  ok(parsed.url instanceof Ci.nsIURI, "types inside the object are also parsed");
+  is(parsed.url.spec, "https://www.example.com/foo#bar", "URL was correctly parsed");
+  is(parsed.title, "Foo", "title was correctly parsed");
+  is(parsed.alias, undefined, "property not described in the schema is not present in the parsed object");
+
+  // Invalid values:
+  ok(!PoliciesValidator.validateAndParseParameters(
+    {
+      url: "https://www.example.com/foo#bar",
+      title: 3,
+    },
+    schema)[0], "Mismatched type for title");
+
+  ok(!PoliciesValidator.validateAndParseParameters(
+    {
+      url: "www.example.com",
+      title: 3,
+    },
+    schema)[0], "Invalid URL inside the object");
+});
+
+add_task(async function test_array_of_objects() {
+  // This schema is used, for example, for bookmarks
+  let schema = {
+    type: "array",
+    items: {
+      type: "object",
+      properties: {
+        url: {
+          type: "URL",
+        },
+        title: {
+          type: "string"
+        }
+      }
+    }
+  };
+
+  let valid, parsed;
+  [valid, parsed] = PoliciesValidator.validateAndParseParameters(
+    [{
+      url: "https://www.example.com/bookmark1",
+      title: "Foo",
+    },
+    {
+      url: "https://www.example.com/bookmark2",
+      title: "Bar",
+    }],
+    schema);
+
+  ok(valid, "Array is valid");
+  is(parsed.length, 2, "Correct number of items");
+
+  ok(typeof(parsed[0]) == "object" && typeof(parsed[1]) == "object", "Correct objects inside array");
+
+  is(parsed[0].url.spec, "https://www.example.com/bookmark1", "Correct URL for bookmark 1");
+  is(parsed[1].url.spec, "https://www.example.com/bookmark2", "Correct URL for bookmark 2");
+
+  is(parsed[0].title, "Foo", "Correct title for bookmark 1");
+  is(parsed[1].title, "Bar", "Correct title for bookmark 2");
+});