1/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2/* vim: set sts=2 sw=2 et tw=80: */ 3/* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6"use strict"; 7 8const global = this; 9 10const { AppConstants } = ChromeUtils.import( 11 "resource://gre/modules/AppConstants.jsm" 12); 13const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 14const { XPCOMUtils } = ChromeUtils.import( 15 "resource://gre/modules/XPCOMUtils.jsm" 16); 17 18XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]); 19 20const { ExtensionUtils } = ChromeUtils.import( 21 "resource://gre/modules/ExtensionUtils.jsm" 22); 23var { DefaultMap, DefaultWeakMap } = ExtensionUtils; 24 25ChromeUtils.defineModuleGetter( 26 this, 27 "ExtensionParent", 28 "resource://gre/modules/ExtensionParent.jsm" 29); 30ChromeUtils.defineModuleGetter( 31 this, 32 "NetUtil", 33 "resource://gre/modules/NetUtil.jsm" 34); 35ChromeUtils.defineModuleGetter( 36 this, 37 "ShortcutUtils", 38 "resource://gre/modules/ShortcutUtils.jsm" 39); 40XPCOMUtils.defineLazyServiceGetter( 41 this, 42 "contentPolicyService", 43 "@mozilla.org/addons/content-policy;1", 44 "nsIAddonContentPolicy" 45); 46 47XPCOMUtils.defineLazyGetter( 48 this, 49 "StartupCache", 50 () => ExtensionParent.StartupCache 51); 52 53XPCOMUtils.defineLazyPreferenceGetter( 54 this, 55 "treatWarningsAsErrors", 56 "extensions.webextensions.warnings-as-errors", 57 false 58); 59 60var EXPORTED_SYMBOLS = ["SchemaRoot", "Schemas"]; 61 62const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content"; 63const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged"; 64 65const MIN_MANIFEST_VERSION = 2; 66const MAX_MANIFEST_VERSION = 3; 67 68const { DEBUG } = AppConstants; 69 70const isParentProcess = 71 Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT; 72 73function readJSON(url) { 74 return new Promise((resolve, reject) => { 75 NetUtil.asyncFetch( 76 { uri: url, loadUsingSystemPrincipal: true }, 77 (inputStream, status) => { 78 if (!Components.isSuccessCode(status)) { 79 // Convert status code to a string 80 let e = Components.Exception("", status); 81 reject(new Error(`Error while loading '${url}' (${e.name})`)); 82 return; 83 } 84 try { 85 let text = NetUtil.readInputStreamToString( 86 inputStream, 87 inputStream.available() 88 ); 89 90 // Chrome JSON files include a license comment that we need to 91 // strip off for this to be valid JSON. As a hack, we just 92 // look for the first '[' character, which signals the start 93 // of the JSON content. 94 let index = text.indexOf("["); 95 text = text.slice(index); 96 97 resolve(JSON.parse(text)); 98 } catch (e) { 99 reject(e); 100 } 101 } 102 ); 103 }); 104} 105 106function stripDescriptions(json, stripThis = true) { 107 if (Array.isArray(json)) { 108 for (let i = 0; i < json.length; i++) { 109 if (typeof json[i] === "object" && json[i] !== null) { 110 json[i] = stripDescriptions(json[i]); 111 } 112 } 113 return json; 114 } 115 116 let result = {}; 117 118 // Objects are handled much more efficiently, both in terms of memory and 119 // CPU, if they have the same shape as other objects that serve the same 120 // purpose. So, normalize the order of properties to increase the chances 121 // that the majority of schema objects wind up in large shape groups. 122 for (let key of Object.keys(json).sort()) { 123 if (stripThis && key === "description" && typeof json[key] === "string") { 124 continue; 125 } 126 127 if (typeof json[key] === "object" && json[key] !== null) { 128 result[key] = stripDescriptions(json[key], key !== "properties"); 129 } else { 130 result[key] = json[key]; 131 } 132 } 133 134 return result; 135} 136 137function blobbify(json) { 138 // We don't actually use descriptions at runtime, and they make up about a 139 // third of the size of our structured clone data, so strip them before 140 // blobbifying. 141 json = stripDescriptions(json); 142 143 return new StructuredCloneHolder(json); 144} 145 146async function readJSONAndBlobbify(url) { 147 let json = await readJSON(url); 148 149 return blobbify(json); 150} 151 152/** 153 * Defines a lazy getter for the given property on the given object. Any 154 * security wrappers are waived on the object before the property is 155 * defined, and the getter and setter methods are wrapped for the target 156 * scope. 157 * 158 * The given getter function is guaranteed to be called only once, even 159 * if the target scope retrieves the wrapped getter from the property 160 * descriptor and calls it directly. 161 * 162 * @param {object} object 163 * The object on which to define the getter. 164 * @param {string|Symbol} prop 165 * The property name for which to define the getter. 166 * @param {function} getter 167 * The function to call in order to generate the final property 168 * value. 169 */ 170function exportLazyGetter(object, prop, getter) { 171 object = ChromeUtils.waiveXrays(object); 172 173 let redefine = value => { 174 if (value === undefined) { 175 delete object[prop]; 176 } else { 177 Object.defineProperty(object, prop, { 178 enumerable: true, 179 configurable: true, 180 writable: true, 181 value, 182 }); 183 } 184 185 getter = null; 186 187 return value; 188 }; 189 190 Object.defineProperty(object, prop, { 191 enumerable: true, 192 configurable: true, 193 194 get: Cu.exportFunction(function() { 195 return redefine(getter.call(this)); 196 }, object), 197 198 set: Cu.exportFunction(value => { 199 redefine(value); 200 }, object), 201 }); 202} 203 204/** 205 * Defines a lazily-instantiated property descriptor on the given 206 * object. Any security wrappers are waived on the object before the 207 * property is defined. 208 * 209 * The given getter function is guaranteed to be called only once, even 210 * if the target scope retrieves the wrapped getter from the property 211 * descriptor and calls it directly. 212 * 213 * @param {object} object 214 * The object on which to define the getter. 215 * @param {string|Symbol} prop 216 * The property name for which to define the getter. 217 * @param {function} getter 218 * The function to call in order to generate the final property 219 * descriptor object. This will be called, and the property 220 * descriptor installed on the object, the first time the 221 * property is written or read. The function may return 222 * undefined, which will cause the property to be deleted. 223 */ 224function exportLazyProperty(object, prop, getter) { 225 object = ChromeUtils.waiveXrays(object); 226 227 let redefine = obj => { 228 let desc = getter.call(obj); 229 getter = null; 230 231 delete object[prop]; 232 if (desc) { 233 let defaults = { 234 configurable: true, 235 enumerable: true, 236 }; 237 238 if (!desc.set && !desc.get) { 239 defaults.writable = true; 240 } 241 242 Object.defineProperty(object, prop, Object.assign(defaults, desc)); 243 } 244 }; 245 246 Object.defineProperty(object, prop, { 247 enumerable: true, 248 configurable: true, 249 250 get: Cu.exportFunction(function() { 251 redefine(this); 252 return object[prop]; 253 }, object), 254 255 set: Cu.exportFunction(function(value) { 256 redefine(this); 257 object[prop] = value; 258 }, object), 259 }); 260} 261 262const POSTPROCESSORS = { 263 convertImageDataToURL(imageData, context) { 264 let document = context.cloneScope.document; 265 let canvas = document.createElementNS( 266 "http://www.w3.org/1999/xhtml", 267 "canvas" 268 ); 269 canvas.width = imageData.width; 270 canvas.height = imageData.height; 271 canvas.getContext("2d").putImageData(imageData, 0, 0); 272 273 return canvas.toDataURL("image/png"); 274 }, 275 webRequestBlockingPermissionRequired(string, context) { 276 if (string === "blocking" && !context.hasPermission("webRequestBlocking")) { 277 throw new context.cloneScope.Error( 278 "Using webRequest.addListener with the " + 279 "blocking option requires the 'webRequestBlocking' permission." 280 ); 281 } 282 283 return string; 284 }, 285 requireBackgroundServiceWorkerEnabled(value, context) { 286 if (WebExtensionPolicy.backgroundServiceWorkerEnabled) { 287 return value; 288 } 289 290 // Add an error to the manifest validations and throw the 291 // same error. 292 const msg = "background.service_worker is currently disabled"; 293 context.logError(context.makeError(msg)); 294 throw new Error(msg); 295 }, 296 297 manifestVersionCheck(value, context) { 298 if ( 299 value == 2 || 300 (value == 3 && 301 Services.prefs.getBoolPref("extensions.manifestV3.enabled", false)) 302 ) { 303 return value; 304 } 305 const msg = `Unsupported manifest version: ${value}`; 306 context.logError(context.makeError(msg)); 307 throw new Error(msg); 308 }, 309}; 310 311// Parses a regular expression, with support for the Python extended 312// syntax that allows setting flags by including the string (?im) 313function parsePattern(pattern) { 314 let flags = ""; 315 let match = /^\(\?([im]*)\)(.*)/.exec(pattern); 316 if (match) { 317 [, flags, pattern] = match; 318 } 319 return new RegExp(pattern, flags); 320} 321 322function getValueBaseType(value) { 323 let type = typeof value; 324 switch (type) { 325 case "object": 326 if (value === null) { 327 return "null"; 328 } 329 if (Array.isArray(value)) { 330 return "array"; 331 } 332 break; 333 334 case "number": 335 if (value % 1 === 0) { 336 return "integer"; 337 } 338 } 339 return type; 340} 341 342// Methods of Context that are used by Schemas.normalize. These methods can be 343// overridden at the construction of Context. 344const CONTEXT_FOR_VALIDATION = ["checkLoadURL", "hasPermission", "logError"]; 345 346// Methods of Context that are used by Schemas.inject. 347// Callers of Schemas.inject should implement all of these methods. 348const CONTEXT_FOR_INJECTION = [ 349 ...CONTEXT_FOR_VALIDATION, 350 "getImplementation", 351 "isPermissionRevokable", 352 "shouldInject", 353]; 354 355// If the message is a function, call it and return the result. 356// Otherwise, assume it's a string. 357function forceString(msg) { 358 if (typeof msg === "function") { 359 return msg(); 360 } 361 return msg; 362} 363 364/** 365 * A context for schema validation and error reporting. This class is only used 366 * internally within Schemas. 367 */ 368class Context { 369 /** 370 * @param {object} params Provides the implementation of this class. 371 * @param {Array<string>} overridableMethods 372 */ 373 constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) { 374 this.params = params; 375 376 if (typeof params.manifestVersion !== "number") { 377 throw new Error( 378 `Unexpected params.manifestVersion value: ${params.manifestVersion}` 379 ); 380 } 381 382 this.path = []; 383 this.preprocessors = { 384 localize(value, context) { 385 return value; 386 }, 387 }; 388 this.postprocessors = POSTPROCESSORS; 389 this.isChromeCompat = false; 390 391 this.currentChoices = new Set(); 392 this.choicePathIndex = 0; 393 394 for (let method of overridableMethods) { 395 if (method in params) { 396 this[method] = params[method].bind(params); 397 } 398 } 399 400 let props = ["preprocessors", "isChromeCompat", "manifestVersion"]; 401 for (let prop of props) { 402 if (prop in params) { 403 if (prop in this && typeof this[prop] == "object") { 404 Object.assign(this[prop], params[prop]); 405 } else { 406 this[prop] = params[prop]; 407 } 408 } 409 } 410 } 411 412 get choicePath() { 413 let path = this.path.slice(this.choicePathIndex); 414 return path.join("."); 415 } 416 417 get cloneScope() { 418 return this.params.cloneScope || undefined; 419 } 420 421 get url() { 422 return this.params.url; 423 } 424 425 get principal() { 426 return ( 427 this.params.principal || 428 Services.scriptSecurityManager.createNullPrincipal({}) 429 ); 430 } 431 432 /** 433 * Checks whether `url` may be loaded by the extension in this context. 434 * 435 * @param {string} url The URL that the extension wished to load. 436 * @returns {boolean} Whether the context may load `url`. 437 */ 438 checkLoadURL(url) { 439 let ssm = Services.scriptSecurityManager; 440 try { 441 ssm.checkLoadURIWithPrincipal( 442 this.principal, 443 Services.io.newURI(url), 444 ssm.DISALLOW_INHERIT_PRINCIPAL 445 ); 446 } catch (e) { 447 return false; 448 } 449 return true; 450 } 451 452 /** 453 * Checks whether this context has the given permission. 454 * 455 * @param {string} permission 456 * The name of the permission to check. 457 * 458 * @returns {boolean} True if the context has the given permission. 459 */ 460 hasPermission(permission) { 461 return false; 462 } 463 464 /** 465 * Checks whether the given permission can be dynamically revoked or 466 * granted. 467 * 468 * @param {string} permission 469 * The name of the permission to check. 470 * 471 * @returns {boolean} True if the given permission is revokable. 472 */ 473 isPermissionRevokable(permission) { 474 return false; 475 } 476 477 /** 478 * Returns an error result object with the given message, for return 479 * by Type normalization functions. 480 * 481 * If the context has a `currentTarget` value, this is prepended to 482 * the message to indicate the location of the error. 483 * 484 * @param {string|function} errorMessage 485 * The error message which will be displayed when this is the 486 * only possible matching schema. If a function is passed, it 487 * will be evaluated when the error string is first needed, and 488 * must return a string. 489 * @param {string|function} choicesMessage 490 * The message describing the valid what constitutes a valid 491 * value for this schema, which will be displayed when multiple 492 * schema choices are available and none match. 493 * 494 * A caller may pass `null` to prevent a choice from being 495 * added, but this should *only* be done from code processing a 496 * choices type. 497 * @param {boolean} [warning = false] 498 * If true, make message prefixed `Warning`. If false, make message 499 * prefixed `Error` 500 * @returns {object} 501 */ 502 error(errorMessage, choicesMessage = undefined, warning = false) { 503 if (choicesMessage !== null) { 504 let { choicePath } = this; 505 if (choicePath) { 506 choicesMessage = `.${choicePath} must ${choicesMessage}`; 507 } 508 509 this.currentChoices.add(choicesMessage); 510 } 511 512 if (this.currentTarget) { 513 let { currentTarget } = this; 514 return { 515 error: () => 516 `${ 517 warning ? "Warning" : "Error" 518 } processing ${currentTarget}: ${forceString(errorMessage)}`, 519 }; 520 } 521 return { error: errorMessage }; 522 } 523 524 /** 525 * Creates an `Error` object belonging to the current unprivileged 526 * scope. If there is no unprivileged scope associated with this 527 * context, the message is returned as a string. 528 * 529 * If the context has a `currentTarget` value, this is prepended to 530 * the message, in the same way as for the `error` method. 531 * 532 * @param {string} message 533 * @param {object} [options] 534 * @param {boolean} [options.warning = false] 535 * @returns {Error} 536 */ 537 makeError(message, { warning = false } = {}) { 538 let error = forceString(this.error(message, null, warning).error); 539 if (this.cloneScope) { 540 return new this.cloneScope.Error(error); 541 } 542 return error; 543 } 544 545 /** 546 * Logs the given error to the console. May be overridden to enable 547 * custom logging. 548 * 549 * @param {Error|string} error 550 */ 551 logError(error) { 552 if (this.cloneScope) { 553 Cu.reportError( 554 // Error objects logged using Cu.reportError are not associated 555 // to the related innerWindowID. This results in a leaked docshell 556 // since consoleService cannot release the error object when the 557 // extension global is destroyed. 558 typeof error == "string" ? error : String(error), 559 // Report the error with the appropriate stack trace when the 560 // is related to an actual extension global (instead of being 561 // related to a manifest validation). 562 this.principal && ChromeUtils.getCallerLocation(this.principal) 563 ); 564 } else { 565 Cu.reportError(error); 566 } 567 } 568 569 /** 570 * Returns the name of the value currently being normalized. For a 571 * nested object, this is usually approximately equivalent to the 572 * JavaScript property accessor for that property. Given: 573 * 574 * { foo: { bar: [{ baz: x }] } } 575 * 576 * When processing the value for `x`, the currentTarget is 577 * 'foo.bar.0.baz' 578 */ 579 get currentTarget() { 580 return this.path.join("."); 581 } 582 583 /** 584 * Executes the given callback, and returns an array of choice strings 585 * passed to {@see #error} during its execution. 586 * 587 * @param {function} callback 588 * @returns {object} 589 * An object with a `result` property containing the return 590 * value of the callback, and a `choice` property containing 591 * an array of choices. 592 */ 593 withChoices(callback) { 594 let { currentChoices, choicePathIndex } = this; 595 596 let choices = new Set(); 597 this.currentChoices = choices; 598 this.choicePathIndex = this.path.length; 599 600 try { 601 let result = callback(); 602 603 return { result, choices }; 604 } finally { 605 this.currentChoices = currentChoices; 606 this.choicePathIndex = choicePathIndex; 607 608 if (choices.size == 1) { 609 for (let choice of choices) { 610 currentChoices.add(choice); 611 } 612 } else if (choices.size) { 613 this.error(null, () => { 614 let array = Array.from(choices, forceString); 615 let n = array.length - 1; 616 array[n] = `or ${array[n]}`; 617 618 return `must either [${array.join(", ")}]`; 619 }); 620 } 621 } 622 } 623 624 /** 625 * Appends the given component to the `currentTarget` path to indicate 626 * that it is being processed, calls the given callback function, and 627 * then restores the original path. 628 * 629 * This is used to identify the path of the property being processed 630 * when reporting type errors. 631 * 632 * @param {string} component 633 * @param {function} callback 634 * @returns {*} 635 */ 636 withPath(component, callback) { 637 this.path.push(component); 638 try { 639 return callback(); 640 } finally { 641 this.path.pop(); 642 } 643 } 644 645 matchManifestVersion(entry) { 646 let { manifestVersion } = this; 647 return ( 648 manifestVersion >= entry.min_manifest_version && 649 manifestVersion <= entry.max_manifest_version 650 ); 651 } 652} 653 654/** 655 * Represents a schema entry to be injected into an object. Handles the 656 * injection, revocation, and permissions of said entry. 657 * 658 * @param {InjectionContext} context 659 * The injection context for the entry. 660 * @param {Entry} entry 661 * The entry to inject. 662 * @param {object} parentObject 663 * The object into which to inject this entry. 664 * @param {string} name 665 * The property name at which to inject this entry. 666 * @param {Array<string>} path 667 * The full path from the root entry to this entry. 668 * @param {Entry} parentEntry 669 * The parent entry for the injected entry. 670 */ 671class InjectionEntry { 672 constructor(context, entry, parentObj, name, path, parentEntry) { 673 this.context = context; 674 this.entry = entry; 675 this.parentObj = parentObj; 676 this.name = name; 677 this.path = path; 678 this.parentEntry = parentEntry; 679 680 this.injected = null; 681 this.lazyInjected = null; 682 } 683 684 /** 685 * @property {Array<string>} allowedContexts 686 * The list of allowed contexts into which the entry may be 687 * injected. 688 */ 689 get allowedContexts() { 690 let { allowedContexts } = this.entry; 691 if (allowedContexts.length) { 692 return allowedContexts; 693 } 694 return this.parentEntry.defaultContexts; 695 } 696 697 /** 698 * @property {boolean} isRevokable 699 * Returns true if this entry may be dynamically injected or 700 * revoked based on its permissions. 701 */ 702 get isRevokable() { 703 return ( 704 this.entry.permissions && 705 this.entry.permissions.some(perm => 706 this.context.isPermissionRevokable(perm) 707 ) 708 ); 709 } 710 711 /** 712 * @property {boolean} hasPermission 713 * Returns true if the injection context currently has the 714 * appropriate permissions to access this entry. 715 */ 716 get hasPermission() { 717 return ( 718 !this.entry.permissions || 719 this.entry.permissions.some(perm => this.context.hasPermission(perm)) 720 ); 721 } 722 723 /** 724 * @property {boolean} shouldInject 725 * Returns true if this entry should be injected in the given 726 * context, without respect to permissions. 727 */ 728 get shouldInject() { 729 return ( 730 this.context.matchManifestVersion(this.entry) && 731 this.context.shouldInject( 732 this.path.join("."), 733 this.name, 734 this.allowedContexts 735 ) 736 ); 737 } 738 739 /** 740 * Revokes this entry, removing its property from its parent object, 741 * and invalidating its wrappers. 742 */ 743 revoke() { 744 if (this.lazyInjected) { 745 this.lazyInjected = false; 746 } else if (this.injected) { 747 if (this.injected.revoke) { 748 this.injected.revoke(); 749 } 750 751 try { 752 let unwrapped = ChromeUtils.waiveXrays(this.parentObj); 753 delete unwrapped[this.name]; 754 } catch (e) { 755 Cu.reportError(e); 756 } 757 758 let { value } = this.injected.descriptor; 759 if (value) { 760 this.context.revokeChildren(value); 761 } 762 763 this.injected = null; 764 } 765 } 766 767 /** 768 * Returns a property descriptor object for this entry, if it should 769 * be injected, or undefined if it should not. 770 * 771 * @returns {object?} 772 * A property descriptor object, or undefined if the property 773 * should be removed. 774 */ 775 getDescriptor() { 776 this.lazyInjected = false; 777 778 if (this.injected) { 779 let path = [...this.path, this.name]; 780 throw new Error( 781 `Attempting to re-inject already injected entry: ${path.join(".")}` 782 ); 783 } 784 785 if (!this.shouldInject) { 786 return; 787 } 788 789 if (this.isRevokable) { 790 this.context.pendingEntries.add(this); 791 } 792 793 if (!this.hasPermission) { 794 return; 795 } 796 797 this.injected = this.entry.getDescriptor(this.path, this.context); 798 if (!this.injected) { 799 return undefined; 800 } 801 802 return this.injected.descriptor; 803 } 804 805 /** 806 * Injects a lazy property descriptor into the parent object which 807 * checks permissions and eligibility for injection the first time it 808 * is accessed. 809 */ 810 lazyInject() { 811 if (this.lazyInjected || this.injected) { 812 let path = [...this.path, this.name]; 813 throw new Error( 814 `Attempting to re-lazy-inject already injected entry: ${path.join(".")}` 815 ); 816 } 817 818 this.lazyInjected = true; 819 exportLazyProperty(this.parentObj, this.name, () => { 820 if (this.lazyInjected) { 821 return this.getDescriptor(); 822 } 823 }); 824 } 825 826 /** 827 * Injects or revokes this entry if its current state does not match 828 * the context's current permissions. 829 */ 830 permissionsChanged() { 831 if (this.injected) { 832 this.maybeRevoke(); 833 } else { 834 this.maybeInject(); 835 } 836 } 837 838 maybeInject() { 839 if (!this.injected && !this.lazyInjected) { 840 this.lazyInject(); 841 } 842 } 843 844 maybeRevoke() { 845 if (this.injected && !this.hasPermission) { 846 this.revoke(); 847 } 848 } 849} 850 851/** 852 * Holds methods that run the actual implementation of the extension APIs. These 853 * methods are only called if the extension API invocation matches the signature 854 * as defined in the schema. Otherwise an error is reported to the context. 855 */ 856class InjectionContext extends Context { 857 constructor(params, schemaRoot) { 858 super(params, CONTEXT_FOR_INJECTION); 859 860 this.schemaRoot = schemaRoot; 861 862 this.pendingEntries = new Set(); 863 this.children = new DefaultWeakMap(() => new Map()); 864 865 this.injectedRoots = new Set(); 866 867 if (params.setPermissionsChangedCallback) { 868 params.setPermissionsChangedCallback(this.permissionsChanged.bind(this)); 869 } 870 } 871 872 /** 873 * Check whether the API should be injected. 874 * 875 * @abstract 876 * @param {string} namespace The namespace of the API. This may contain dots, 877 * e.g. in the case of "devtools.inspectedWindow". 878 * @param {string} [name] The name of the property in the namespace. 879 * `null` if we are checking whether the namespace should be injected. 880 * @param {Array<string>} allowedContexts A list of additional contexts in which 881 * this API should be available. May include any of: 882 * "main" - The main chrome browser process. 883 * "addon" - An addon process. 884 * "content" - A content process. 885 * @returns {boolean} Whether the API should be injected. 886 */ 887 shouldInject(namespace, name, allowedContexts) { 888 throw new Error("Not implemented"); 889 } 890 891 /** 892 * Generate the implementation for `namespace`.`name`. 893 * 894 * @abstract 895 * @param {string} namespace The full path to the namespace of the API, minus 896 * the name of the method or property. E.g. "storage.local". 897 * @param {string} name The name of the method, property or event. 898 * @returns {SchemaAPIInterface} The implementation of the API. 899 */ 900 getImplementation(namespace, name) { 901 throw new Error("Not implemented"); 902 } 903 904 /** 905 * Updates all injection entries which may need to be updated after a 906 * permission change, revoking or re-injecting them as necessary. 907 */ 908 permissionsChanged() { 909 for (let entry of this.pendingEntries) { 910 try { 911 entry.permissionsChanged(); 912 } catch (e) { 913 Cu.reportError(e); 914 } 915 } 916 } 917 918 /** 919 * Recursively revokes all child injection entries of the given 920 * object. 921 * 922 * @param {object} object 923 * The object for which to invoke children. 924 */ 925 revokeChildren(object) { 926 if (!this.children.has(object)) { 927 return; 928 } 929 930 let children = this.children.get(object); 931 for (let [name, entry] of children.entries()) { 932 try { 933 entry.revoke(); 934 } catch (e) { 935 Cu.reportError(e); 936 } 937 children.delete(name); 938 939 // When we revoke children for an object, we consider that object 940 // dead. If the entry is ever reified again, a new object is 941 // created, with new child entries. 942 this.pendingEntries.delete(entry); 943 } 944 this.children.delete(object); 945 } 946 947 _getInjectionEntry(entry, dest, name, path, parentEntry) { 948 let injection = new InjectionEntry( 949 this, 950 entry, 951 dest, 952 name, 953 path, 954 parentEntry 955 ); 956 957 this.children.get(dest).set(name, injection); 958 959 return injection; 960 } 961 962 /** 963 * Returns the property descriptor for the given entry. 964 * 965 * @param {Entry} entry 966 * The entry instance to return a descriptor for. 967 * @param {object} dest 968 * The object into which this entry is being injected. 969 * @param {string} name 970 * The property name on the destination object where the entry 971 * will be injected. 972 * @param {Array<string>} path 973 * The full path from the root injection object to this entry. 974 * @param {Entry} parentEntry 975 * The parent entry for this entry. 976 * 977 * @returns {object?} 978 * A property descriptor object, or null if the entry should 979 * not be injected. 980 */ 981 getDescriptor(entry, dest, name, path, parentEntry) { 982 let injection = this._getInjectionEntry( 983 entry, 984 dest, 985 name, 986 path, 987 parentEntry 988 ); 989 990 return injection.getDescriptor(); 991 } 992 993 /** 994 * Lazily injects the given entry into the given object. 995 * 996 * @param {Entry} entry 997 * The entry instance to lazily inject. 998 * @param {object} dest 999 * The object into which to inject this entry. 1000 * @param {string} name 1001 * The property name at which to inject the entry. 1002 * @param {Array<string>} path 1003 * The full path from the root injection object to this entry. 1004 * @param {Entry} parentEntry 1005 * The parent entry for this entry. 1006 */ 1007 injectInto(entry, dest, name, path, parentEntry) { 1008 let injection = this._getInjectionEntry( 1009 entry, 1010 dest, 1011 name, 1012 path, 1013 parentEntry 1014 ); 1015 1016 injection.lazyInject(); 1017 } 1018} 1019 1020/** 1021 * The methods in this singleton represent the "format" specifier for 1022 * JSON Schema string types. 1023 * 1024 * Each method either returns a normalized version of the original 1025 * value, or throws an error if the value is not valid for the given 1026 * format. 1027 */ 1028const FORMATS = { 1029 hostname(string, context) { 1030 let valid = true; 1031 1032 try { 1033 valid = new URL(`http://${string}`).host === string; 1034 } catch (e) { 1035 valid = false; 1036 } 1037 1038 if (!valid) { 1039 throw new Error(`Invalid hostname ${string}`); 1040 } 1041 1042 return string; 1043 }, 1044 1045 url(string, context) { 1046 let url = new URL(string).href; 1047 1048 if (!context.checkLoadURL(url)) { 1049 throw new Error(`Access denied for URL ${url}`); 1050 } 1051 return url; 1052 }, 1053 1054 origin(string, context) { 1055 let url; 1056 try { 1057 url = new URL(string); 1058 } catch (e) { 1059 throw new Error(`Invalid origin: ${string}`); 1060 } 1061 if (!/^https?:/.test(url.protocol)) { 1062 throw new Error(`Invalid origin must be http or https for URL ${string}`); 1063 } 1064 // url.origin is punycode so a direct check against string wont work. 1065 // url.href appends a slash even if not in the original string, we we 1066 // additionally check that string does not end in slash. 1067 if (string.endsWith("/") || url.href != new URL(url.origin).href) { 1068 throw new Error( 1069 `Invalid origin for URL ${string}, replace with origin ${url.origin}` 1070 ); 1071 } 1072 if (!context.checkLoadURL(url.origin)) { 1073 throw new Error(`Access denied for URL ${url}`); 1074 } 1075 return url.origin; 1076 }, 1077 1078 relativeUrl(string, context) { 1079 if (!context.url) { 1080 // If there's no context URL, return relative URLs unresolved, and 1081 // skip security checks for them. 1082 try { 1083 new URL(string); 1084 } catch (e) { 1085 return string; 1086 } 1087 } 1088 1089 let url = new URL(string, context.url).href; 1090 1091 if (!context.checkLoadURL(url)) { 1092 throw new Error(`Access denied for URL ${url}`); 1093 } 1094 return url; 1095 }, 1096 1097 strictRelativeUrl(string, context) { 1098 void FORMATS.unresolvedRelativeUrl(string, context); 1099 return FORMATS.relativeUrl(string, context); 1100 }, 1101 1102 unresolvedRelativeUrl(string, context) { 1103 if (!string.startsWith("//")) { 1104 try { 1105 new URL(string); 1106 } catch (e) { 1107 return string; 1108 } 1109 } 1110 1111 throw new SyntaxError( 1112 `String ${JSON.stringify(string)} must be a relative URL` 1113 ); 1114 }, 1115 1116 homepageUrl(string, context) { 1117 // Pipes are used for separating homepages, but we only allow extensions to 1118 // set a single homepage. Encoding any pipes makes it one URL. 1119 return FORMATS.relativeUrl( 1120 string.replace(new RegExp("\\|", "g"), "%7C"), 1121 context 1122 ); 1123 }, 1124 1125 imageDataOrStrictRelativeUrl(string, context) { 1126 // Do not accept a string which resolves as an absolute URL, or any 1127 // protocol-relative URL, except PNG or JPG data URLs 1128 if ( 1129 !string.startsWith("data:image/png;base64,") && 1130 !string.startsWith("data:image/jpeg;base64,") 1131 ) { 1132 try { 1133 return FORMATS.strictRelativeUrl(string, context); 1134 } catch (e) { 1135 throw new SyntaxError( 1136 `String ${JSON.stringify( 1137 string 1138 )} must be a relative or PNG or JPG data:image URL` 1139 ); 1140 } 1141 } 1142 return string; 1143 }, 1144 1145 contentSecurityPolicy(string, context) { 1146 // Manifest V3 extension_pages allows localhost. When sandbox is 1147 // implemented, or any other V3 or later directive, the flags 1148 // logic will need to be updated. 1149 let flags = 1150 context.manifestVersion < 3 1151 ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY 1152 : Ci.nsIAddonContentPolicy.CSP_ALLOW_LOCALHOST; 1153 let error = contentPolicyService.validateAddonCSP(string, flags); 1154 if (error != null) { 1155 // The CSP validation error is not reported as part of the "choices" error message, 1156 // we log the CSP validation error explicitly here to make it easier for the addon developers 1157 // to see and fix the extension CSP. 1158 context.logError(`Error processing ${context.currentTarget}: ${error}`); 1159 return null; 1160 } 1161 return string; 1162 }, 1163 1164 date(string, context) { 1165 // A valid ISO 8601 timestamp. 1166 const PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/; 1167 if (!PATTERN.test(string)) { 1168 throw new Error(`Invalid date string ${string}`); 1169 } 1170 // Our pattern just checks the format, we could still have invalid 1171 // values (e.g., month=99 or month=02 and day=31). Let the Date 1172 // constructor do the dirty work of validating. 1173 if (isNaN(new Date(string))) { 1174 throw new Error(`Invalid date string ${string}`); 1175 } 1176 return string; 1177 }, 1178 1179 manifestShortcutKey(string, context) { 1180 if (ShortcutUtils.validate(string) == ShortcutUtils.IS_VALID) { 1181 return string; 1182 } 1183 let errorMessage = 1184 `Value "${string}" must consist of ` + 1185 `either a combination of one or two modifiers, including ` + 1186 `a mandatory primary modifier and a key, separated by '+', ` + 1187 `or a media key. For details see: ` + 1188 `https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`; 1189 throw new Error(errorMessage); 1190 }, 1191 1192 manifestShortcutKeyOrEmpty(string, context) { 1193 return string === "" ? "" : FORMATS.manifestShortcutKey(string, context); 1194 }, 1195}; 1196 1197// Schema files contain namespaces, and each namespace contains types, 1198// properties, functions, and events. An Entry is a base class for 1199// types, properties, functions, and events. 1200class Entry { 1201 constructor(schema = {}) { 1202 /** 1203 * If set to any value which evaluates as true, this entry is 1204 * deprecated, and any access to it will result in a deprecation 1205 * warning being logged to the browser console. 1206 * 1207 * If the value is a string, it will be appended to the deprecation 1208 * message. If it contains the substring "${value}", it will be 1209 * replaced with a string representation of the value being 1210 * processed. 1211 * 1212 * If the value is any other truthy value, a generic deprecation 1213 * message will be emitted. 1214 */ 1215 this.deprecated = false; 1216 if ("deprecated" in schema) { 1217 this.deprecated = schema.deprecated; 1218 } 1219 1220 /** 1221 * @property {string} [preprocessor] 1222 * If set to a string value, and a preprocessor of the same is 1223 * defined in the validation context, it will be applied to this 1224 * value prior to any normalization. 1225 */ 1226 this.preprocessor = schema.preprocess || null; 1227 1228 /** 1229 * @property {string} [postprocessor] 1230 * If set to a string value, and a postprocessor of the same is 1231 * defined in the validation context, it will be applied to this 1232 * value after any normalization. 1233 */ 1234 this.postprocessor = schema.postprocess || null; 1235 1236 /** 1237 * @property {Array<string>} allowedContexts A list of allowed contexts 1238 * to consider before generating the API. 1239 * These are not parsed by the schema, but passed to `shouldInject`. 1240 */ 1241 this.allowedContexts = schema.allowedContexts || []; 1242 1243 this.min_manifest_version = 1244 schema.min_manifest_version ?? MIN_MANIFEST_VERSION; 1245 this.max_manifest_version = 1246 schema.max_manifest_version ?? MAX_MANIFEST_VERSION; 1247 } 1248 1249 /** 1250 * Preprocess the given value with the preprocessor declared in 1251 * `preprocessor`. 1252 * 1253 * @param {*} value 1254 * @param {Context} context 1255 * @returns {*} 1256 */ 1257 preprocess(value, context) { 1258 if (this.preprocessor) { 1259 return context.preprocessors[this.preprocessor](value, context); 1260 } 1261 return value; 1262 } 1263 1264 /** 1265 * Postprocess the given result with the postprocessor declared in 1266 * `postprocessor`. 1267 * 1268 * @param {object} result 1269 * @param {Context} context 1270 * @returns {object} 1271 */ 1272 postprocess(result, context) { 1273 if (result.error || !this.postprocessor) { 1274 return result; 1275 } 1276 1277 let value = context.postprocessors[this.postprocessor]( 1278 result.value, 1279 context 1280 ); 1281 return { value }; 1282 } 1283 1284 /** 1285 * Logs a deprecation warning for this entry, based on the value of 1286 * its `deprecated` property. 1287 * 1288 * @param {Context} context 1289 * @param {value} [value] 1290 */ 1291 logDeprecation(context, value = null) { 1292 let message = "This property is deprecated"; 1293 if (typeof this.deprecated == "string") { 1294 message = this.deprecated; 1295 if (message.includes("${value}")) { 1296 try { 1297 value = JSON.stringify(value); 1298 } catch (e) { 1299 value = String(value); 1300 } 1301 message = message.replace(/\$\{value\}/g, () => value); 1302 } 1303 } 1304 1305 this.logWarning(context, message); 1306 } 1307 1308 /** 1309 * @param {Context} context 1310 * @param {string} warningMessage 1311 */ 1312 logWarning(context, warningMessage) { 1313 let error = context.makeError(warningMessage, { warning: true }); 1314 context.logError(error); 1315 1316 if (treatWarningsAsErrors) { 1317 // This pref is false by default, and true by default in tests to 1318 // discourage the use of deprecated APIs in our unit tests. 1319 // If a warning is an expected part of a test, temporarily set the pref 1320 // to false, e.g. with the ExtensionTestUtils.failOnSchemaWarnings helper. 1321 Services.console.logStringMessage( 1322 "Treating warning as error because the preference " + 1323 "extensions.webextensions.warnings-as-errors is set to true" 1324 ); 1325 if (typeof error === "string") { 1326 error = new Error(error); 1327 } 1328 throw error; 1329 } 1330 } 1331 1332 /** 1333 * Checks whether the entry is deprecated and, if so, logs a 1334 * deprecation message. 1335 * 1336 * @param {Context} context 1337 * @param {value} [value] 1338 */ 1339 checkDeprecated(context, value = null) { 1340 if (this.deprecated) { 1341 this.logDeprecation(context, value); 1342 } 1343 } 1344 1345 /** 1346 * Returns an object containing property descriptor for use when 1347 * injecting this entry into an API object. 1348 * 1349 * @param {Array<string>} path The API path, e.g. `["storage", "local"]`. 1350 * @param {InjectionContext} context 1351 * 1352 * @returns {object?} 1353 * An object containing a `descriptor` property, specifying the 1354 * entry's property descriptor, and an optional `revoke` 1355 * method, to be called when the entry is being revoked. 1356 */ 1357 getDescriptor(path, context) { 1358 return undefined; 1359 } 1360} 1361 1362// Corresponds either to a type declared in the "types" section of the 1363// schema or else to any type object used throughout the schema. 1364class Type extends Entry { 1365 /** 1366 * @property {Array<string>} EXTRA_PROPERTIES 1367 * An array of extra properties which may be present for 1368 * schemas of this type. 1369 */ 1370 static get EXTRA_PROPERTIES() { 1371 return [ 1372 "description", 1373 "deprecated", 1374 "preprocess", 1375 "postprocess", 1376 "allowedContexts", 1377 "min_manifest_version", 1378 "max_manifest_version", 1379 ]; 1380 } 1381 1382 /** 1383 * Parses the given schema object and returns an instance of this 1384 * class which corresponds to its properties. 1385 * 1386 * @param {SchemaRoot} root 1387 * The root schema for this type. 1388 * @param {object} schema 1389 * A JSON schema object which corresponds to a definition of 1390 * this type. 1391 * @param {Array<string>} path 1392 * The path to this schema object from the root schema, 1393 * corresponding to the property names and array indices 1394 * traversed during parsing in order to arrive at this schema 1395 * object. 1396 * @param {Array<string>} [extraProperties] 1397 * An array of extra property names which are valid for this 1398 * schema in the current context. 1399 * @returns {Type} 1400 * An instance of this type which corresponds to the given 1401 * schema object. 1402 * @static 1403 */ 1404 static parseSchema(root, schema, path, extraProperties = []) { 1405 this.checkSchemaProperties(schema, path, extraProperties); 1406 1407 return new this(schema); 1408 } 1409 1410 /** 1411 * Checks that all of the properties present in the given schema 1412 * object are valid properties for this type, and throws if invalid. 1413 * 1414 * @param {object} schema 1415 * A JSON schema object. 1416 * @param {Array<string>} path 1417 * The path to this schema object from the root schema, 1418 * corresponding to the property names and array indices 1419 * traversed during parsing in order to arrive at this schema 1420 * object. 1421 * @param {Array<string>} [extra] 1422 * An array of extra property names which are valid for this 1423 * schema in the current context. 1424 * @throws {Error} 1425 * An error describing the first invalid property found in the 1426 * schema object. 1427 */ 1428 static checkSchemaProperties(schema, path, extra = []) { 1429 if (DEBUG) { 1430 let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]); 1431 1432 for (let prop of Object.keys(schema)) { 1433 if (!allowedSet.has(prop)) { 1434 throw new Error( 1435 `Internal error: Namespace ${path.join(".")} has ` + 1436 `invalid type property "${prop}" ` + 1437 `in type "${schema.id || JSON.stringify(schema)}"` 1438 ); 1439 } 1440 } 1441 } 1442 } 1443 1444 // Takes a value, checks that it has the correct type, and returns a 1445 // "normalized" version of the value. The normalized version will 1446 // include "nulls" in place of omitted optional properties. The 1447 // result of this function is either {error: "Some type error"} or 1448 // {value: <normalized-value>}. 1449 normalize(value, context) { 1450 return context.error("invalid type"); 1451 } 1452 1453 // Unlike normalize, this function does a shallow check to see if 1454 // |baseType| (one of the possible getValueBaseType results) is 1455 // valid for this type. It returns true or false. It's used to fill 1456 // in optional arguments to functions before actually type checking 1457 1458 checkBaseType(baseType) { 1459 return false; 1460 } 1461 1462 // Helper method that simply relies on checkBaseType to implement 1463 // normalize. Subclasses can choose to use it or not. 1464 normalizeBase(type, value, context) { 1465 if (this.checkBaseType(getValueBaseType(value))) { 1466 this.checkDeprecated(context, value); 1467 return { value: this.preprocess(value, context) }; 1468 } 1469 1470 let choice; 1471 if ("aeiou".includes(type[0])) { 1472 choice = `be an ${type} value`; 1473 } else { 1474 choice = `be a ${type} value`; 1475 } 1476 1477 return context.error( 1478 () => `Expected ${type} instead of ${JSON.stringify(value)}`, 1479 choice 1480 ); 1481 } 1482} 1483 1484// Type that allows any value. 1485class AnyType extends Type { 1486 normalize(value, context) { 1487 this.checkDeprecated(context, value); 1488 return this.postprocess({ value }, context); 1489 } 1490 1491 checkBaseType(baseType) { 1492 return true; 1493 } 1494} 1495 1496// An untagged union type. 1497class ChoiceType extends Type { 1498 static get EXTRA_PROPERTIES() { 1499 return ["choices", ...super.EXTRA_PROPERTIES]; 1500 } 1501 1502 static parseSchema(root, schema, path, extraProperties = []) { 1503 this.checkSchemaProperties(schema, path, extraProperties); 1504 1505 let choices = schema.choices.map(t => root.parseSchema(t, path)); 1506 return new this(schema, choices); 1507 } 1508 1509 constructor(schema, choices) { 1510 super(schema); 1511 this.choices = choices; 1512 } 1513 1514 extend(type) { 1515 this.choices.push(...type.choices); 1516 1517 return this; 1518 } 1519 1520 normalize(value, context) { 1521 this.checkDeprecated(context, value); 1522 1523 let error; 1524 let { choices, result } = context.withChoices(() => { 1525 for (let choice of this.choices) { 1526 // Ignore a possible choice if it is not supported by 1527 // the manifest version we are normalizing. 1528 if (!context.matchManifestVersion(choice)) { 1529 continue; 1530 } 1531 1532 let r = choice.normalize(value, context); 1533 if (!r.error) { 1534 return r; 1535 } 1536 1537 error = r; 1538 } 1539 }); 1540 1541 if (result) { 1542 return result; 1543 } 1544 if (choices.size <= 1) { 1545 return error; 1546 } 1547 1548 choices = Array.from(choices, forceString); 1549 let n = choices.length - 1; 1550 choices[n] = `or ${choices[n]}`; 1551 1552 let message; 1553 if (typeof value === "object") { 1554 message = () => `Value must either: ${choices.join(", ")}`; 1555 } else { 1556 message = () => 1557 `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`; 1558 } 1559 1560 return context.error(message, null); 1561 } 1562 1563 checkBaseType(baseType) { 1564 return this.choices.some(t => t.checkBaseType(baseType)); 1565 } 1566 1567 getDescriptor(path, context) { 1568 // In StringType.getDescriptor, unlike any other Type, a descriptor is returned if 1569 // it is an enumeration. Since we need versioned choices in some cases, here we 1570 // build a list of valid enumerations that will work for a given manifest version. 1571 if ( 1572 !this.choices.length || 1573 !this.choices.every(t => t.checkBaseType("string") && t.enumeration) 1574 ) { 1575 return; 1576 } 1577 1578 let obj = Cu.createObjectIn(context.cloneScope); 1579 let descriptor = { value: obj }; 1580 for (let choice of this.choices) { 1581 // Ignore a possible choice if it is not supported by 1582 // the manifest version we are normalizing. 1583 if (!context.matchManifestVersion(choice)) { 1584 continue; 1585 } 1586 let d = choice.getDescriptor(path, context); 1587 if (d) { 1588 Object.assign(obj, d.descriptor.value); 1589 } 1590 } 1591 1592 return { descriptor }; 1593 } 1594} 1595 1596// This is a reference to another type--essentially a typedef. 1597class RefType extends Type { 1598 static get EXTRA_PROPERTIES() { 1599 return ["$ref", ...super.EXTRA_PROPERTIES]; 1600 } 1601 1602 static parseSchema(root, schema, path, extraProperties = []) { 1603 this.checkSchemaProperties(schema, path, extraProperties); 1604 1605 let ref = schema.$ref; 1606 let ns = path.join("."); 1607 if (ref.includes(".")) { 1608 [, ns, ref] = /^(.*)\.(.*?)$/.exec(ref); 1609 } 1610 return new this(root, schema, ns, ref); 1611 } 1612 1613 // For a reference to a type named T declared in namespace NS, 1614 // namespaceName will be NS and reference will be T. 1615 constructor(root, schema, namespaceName, reference) { 1616 super(schema); 1617 this.root = root; 1618 this.namespaceName = namespaceName; 1619 this.reference = reference; 1620 } 1621 1622 get targetType() { 1623 let ns = this.root.getNamespace(this.namespaceName); 1624 let type = ns.get(this.reference); 1625 if (!type) { 1626 throw new Error(`Internal error: Type ${this.reference} not found`); 1627 } 1628 return type; 1629 } 1630 1631 normalize(value, context) { 1632 this.checkDeprecated(context, value); 1633 return this.targetType.normalize(value, context); 1634 } 1635 1636 checkBaseType(baseType) { 1637 return this.targetType.checkBaseType(baseType); 1638 } 1639} 1640 1641class StringType extends Type { 1642 static get EXTRA_PROPERTIES() { 1643 return [ 1644 "enum", 1645 "minLength", 1646 "maxLength", 1647 "pattern", 1648 "format", 1649 ...super.EXTRA_PROPERTIES, 1650 ]; 1651 } 1652 1653 static parseSchema(root, schema, path, extraProperties = []) { 1654 this.checkSchemaProperties(schema, path, extraProperties); 1655 1656 let enumeration = schema.enum || null; 1657 if (enumeration) { 1658 // The "enum" property is either a list of strings that are 1659 // valid values or else a list of {name, description} objects, 1660 // where the .name values are the valid values. 1661 enumeration = enumeration.map(e => { 1662 if (typeof e == "object") { 1663 return e.name; 1664 } 1665 return e; 1666 }); 1667 } 1668 1669 let pattern = null; 1670 if (schema.pattern) { 1671 try { 1672 pattern = parsePattern(schema.pattern); 1673 } catch (e) { 1674 throw new Error( 1675 `Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}` 1676 ); 1677 } 1678 } 1679 1680 let format = null; 1681 if (schema.format) { 1682 if (!(schema.format in FORMATS)) { 1683 throw new Error( 1684 `Internal error: Invalid string format ${schema.format}` 1685 ); 1686 } 1687 format = FORMATS[schema.format]; 1688 } 1689 return new this( 1690 schema, 1691 schema.id || undefined, 1692 enumeration, 1693 schema.minLength || 0, 1694 schema.maxLength || Infinity, 1695 pattern, 1696 format 1697 ); 1698 } 1699 1700 constructor( 1701 schema, 1702 name, 1703 enumeration, 1704 minLength, 1705 maxLength, 1706 pattern, 1707 format 1708 ) { 1709 super(schema); 1710 this.name = name; 1711 this.enumeration = enumeration; 1712 this.minLength = minLength; 1713 this.maxLength = maxLength; 1714 this.pattern = pattern; 1715 this.format = format; 1716 } 1717 1718 normalize(value, context) { 1719 let r = this.normalizeBase("string", value, context); 1720 if (r.error) { 1721 return r; 1722 } 1723 value = r.value; 1724 1725 if (this.enumeration) { 1726 if (this.enumeration.includes(value)) { 1727 return this.postprocess({ value }, context); 1728 } 1729 1730 let choices = this.enumeration.map(JSON.stringify).join(", "); 1731 1732 return context.error( 1733 () => `Invalid enumeration value ${JSON.stringify(value)}`, 1734 `be one of [${choices}]` 1735 ); 1736 } 1737 1738 if (value.length < this.minLength) { 1739 return context.error( 1740 () => 1741 `String ${JSON.stringify(value)} is too short (must be ${ 1742 this.minLength 1743 })`, 1744 `be longer than ${this.minLength}` 1745 ); 1746 } 1747 if (value.length > this.maxLength) { 1748 return context.error( 1749 () => 1750 `String ${JSON.stringify(value)} is too long (must be ${ 1751 this.maxLength 1752 })`, 1753 `be shorter than ${this.maxLength}` 1754 ); 1755 } 1756 1757 if (this.pattern && !this.pattern.test(value)) { 1758 return context.error( 1759 () => `String ${JSON.stringify(value)} must match ${this.pattern}`, 1760 `match the pattern ${this.pattern.toSource()}` 1761 ); 1762 } 1763 1764 if (this.format) { 1765 try { 1766 r.value = this.format(r.value, context); 1767 } catch (e) { 1768 return context.error( 1769 String(e), 1770 `match the format "${this.format.name}"` 1771 ); 1772 } 1773 } 1774 1775 return r; 1776 } 1777 1778 checkBaseType(baseType) { 1779 return baseType == "string"; 1780 } 1781 1782 getDescriptor(path, context) { 1783 if (this.enumeration) { 1784 let obj = Cu.createObjectIn(context.cloneScope); 1785 1786 for (let e of this.enumeration) { 1787 obj[e.toUpperCase()] = e; 1788 } 1789 1790 return { 1791 descriptor: { value: obj }, 1792 }; 1793 } 1794 } 1795} 1796 1797class NullType extends Type { 1798 normalize(value, context) { 1799 return this.normalizeBase("null", value, context); 1800 } 1801 1802 checkBaseType(baseType) { 1803 return baseType == "null"; 1804 } 1805} 1806 1807let FunctionEntry; 1808let Event; 1809let SubModuleType; 1810 1811class ObjectType extends Type { 1812 static get EXTRA_PROPERTIES() { 1813 return [ 1814 "properties", 1815 "patternProperties", 1816 "$import", 1817 ...super.EXTRA_PROPERTIES, 1818 ]; 1819 } 1820 1821 static parseSchema(root, schema, path, extraProperties = []) { 1822 if ("functions" in schema) { 1823 return SubModuleType.parseSchema(root, schema, path, extraProperties); 1824 } 1825 1826 if (DEBUG && !("$extend" in schema)) { 1827 // Only allow extending "properties" and "patternProperties". 1828 extraProperties = [ 1829 "additionalProperties", 1830 "isInstanceOf", 1831 ...extraProperties, 1832 ]; 1833 } 1834 this.checkSchemaProperties(schema, path, extraProperties); 1835 1836 let imported = null; 1837 if ("$import" in schema) { 1838 let importPath = schema.$import; 1839 let idx = importPath.indexOf("."); 1840 if (idx === -1) { 1841 imported = [path[0], importPath]; 1842 } else { 1843 imported = [importPath.slice(0, idx), importPath.slice(idx + 1)]; 1844 } 1845 } 1846 1847 let parseProperty = (schema, extraProps = []) => { 1848 return { 1849 type: root.parseSchema( 1850 schema, 1851 path, 1852 DEBUG && [ 1853 "unsupported", 1854 "onError", 1855 "permissions", 1856 "default", 1857 ...extraProps, 1858 ] 1859 ), 1860 optional: schema.optional || false, 1861 unsupported: schema.unsupported || false, 1862 onError: schema.onError || null, 1863 default: schema.default === undefined ? null : schema.default, 1864 }; 1865 }; 1866 1867 // Parse explicit "properties" object. 1868 let properties = Object.create(null); 1869 for (let propName of Object.keys(schema.properties || {})) { 1870 properties[propName] = parseProperty(schema.properties[propName], [ 1871 "optional", 1872 ]); 1873 } 1874 1875 // Parse regexp properties from "patternProperties" object. 1876 let patternProperties = []; 1877 for (let propName of Object.keys(schema.patternProperties || {})) { 1878 let pattern; 1879 try { 1880 pattern = parsePattern(propName); 1881 } catch (e) { 1882 throw new Error( 1883 `Internal error: Invalid property pattern ${JSON.stringify(propName)}` 1884 ); 1885 } 1886 1887 patternProperties.push({ 1888 pattern, 1889 type: parseProperty(schema.patternProperties[propName]), 1890 }); 1891 } 1892 1893 // Parse "additionalProperties" schema. 1894 let additionalProperties = null; 1895 if (schema.additionalProperties) { 1896 let type = schema.additionalProperties; 1897 if (type === true) { 1898 type = { type: "any" }; 1899 } 1900 1901 additionalProperties = root.parseSchema(type, path); 1902 } 1903 1904 return new this( 1905 schema, 1906 properties, 1907 additionalProperties, 1908 patternProperties, 1909 schema.isInstanceOf || null, 1910 imported 1911 ); 1912 } 1913 1914 constructor( 1915 schema, 1916 properties, 1917 additionalProperties, 1918 patternProperties, 1919 isInstanceOf, 1920 imported 1921 ) { 1922 super(schema); 1923 this.properties = properties; 1924 this.additionalProperties = additionalProperties; 1925 this.patternProperties = patternProperties; 1926 this.isInstanceOf = isInstanceOf; 1927 1928 if (imported) { 1929 let [ns, path] = imported; 1930 ns = Schemas.getNamespace(ns); 1931 let importedType = ns.get(path); 1932 if (!importedType) { 1933 throw new Error(`Internal error: imported type ${path} not found`); 1934 } 1935 1936 if (DEBUG && !(importedType instanceof ObjectType)) { 1937 throw new Error( 1938 `Internal error: cannot import non-object type ${path}` 1939 ); 1940 } 1941 1942 this.properties = Object.assign( 1943 {}, 1944 importedType.properties, 1945 this.properties 1946 ); 1947 this.patternProperties = [ 1948 ...importedType.patternProperties, 1949 ...this.patternProperties, 1950 ]; 1951 this.additionalProperties = 1952 importedType.additionalProperties || this.additionalProperties; 1953 } 1954 } 1955 1956 extend(type) { 1957 for (let key of Object.keys(type.properties)) { 1958 if (key in this.properties) { 1959 throw new Error( 1960 `InternalError: Attempt to extend an object with conflicting property "${key}"` 1961 ); 1962 } 1963 this.properties[key] = type.properties[key]; 1964 } 1965 1966 this.patternProperties.push(...type.patternProperties); 1967 1968 return this; 1969 } 1970 1971 checkBaseType(baseType) { 1972 return baseType == "object"; 1973 } 1974 1975 /** 1976 * Extracts the enumerable properties of the given object, including 1977 * function properties which would normally be omitted by X-ray 1978 * wrappers. 1979 * 1980 * @param {object} value 1981 * @param {Context} context 1982 * The current parse context. 1983 * @returns {object} 1984 * An object with an `error` or `value` property. 1985 */ 1986 extractProperties(value, context) { 1987 // |value| should be a JS Xray wrapping an object in the 1988 // extension compartment. This works well except when we need to 1989 // access callable properties on |value| since JS Xrays don't 1990 // support those. To work around the problem, we verify that 1991 // |value| is a plain JS object (i.e., not anything scary like a 1992 // Proxy). Then we copy the properties out of it into a normal 1993 // object using a waiver wrapper. 1994 1995 let klass = ChromeUtils.getClassName(value, true); 1996 if (klass != "Object") { 1997 throw context.error( 1998 `Expected a plain JavaScript object, got a ${klass}`, 1999 `be a plain JavaScript object` 2000 ); 2001 } 2002 2003 return ChromeUtils.shallowClone(value); 2004 } 2005 2006 checkProperty(context, prop, propType, result, properties, remainingProps) { 2007 let { type, optional, unsupported, onError } = propType; 2008 let error = null; 2009 2010 if (!context.matchManifestVersion(type)) { 2011 if (prop in properties) { 2012 error = context.error( 2013 `Property "${prop}" is unsupported in Manifest Version ${context.manifestVersion}`, 2014 `not contain an unsupported "${prop}" property` 2015 ); 2016 if (context.manifestVersion === 2) { 2017 // Existing MV2 extensions might have some of the new MV3 properties. 2018 // Since we've ignored them till now, we should just warn and bail. 2019 this.logWarning(context, forceString(error.error)); 2020 return; 2021 } 2022 } 2023 } else if (unsupported) { 2024 if (prop in properties) { 2025 error = context.error( 2026 `Property "${prop}" is unsupported by Firefox`, 2027 `not contain an unsupported "${prop}" property` 2028 ); 2029 } 2030 } else if (prop in properties) { 2031 if ( 2032 optional && 2033 (properties[prop] === null || properties[prop] === undefined) 2034 ) { 2035 result[prop] = propType.default; 2036 } else { 2037 let r = context.withPath(prop, () => 2038 type.normalize(properties[prop], context) 2039 ); 2040 if (r.error) { 2041 error = r; 2042 } else { 2043 result[prop] = r.value; 2044 properties[prop] = r.value; 2045 } 2046 } 2047 remainingProps.delete(prop); 2048 } else if (!optional) { 2049 error = context.error( 2050 `Property "${prop}" is required`, 2051 `contain the required "${prop}" property` 2052 ); 2053 } else if (optional !== "omit-key-if-missing") { 2054 result[prop] = propType.default; 2055 } 2056 2057 if (error) { 2058 if (onError == "warn") { 2059 this.logWarning(context, forceString(error.error)); 2060 } else if (onError != "ignore") { 2061 throw error; 2062 } 2063 2064 result[prop] = propType.default; 2065 } 2066 } 2067 2068 normalize(value, context) { 2069 try { 2070 let v = this.normalizeBase("object", value, context); 2071 if (v.error) { 2072 return v; 2073 } 2074 value = v.value; 2075 2076 if (this.isInstanceOf) { 2077 if (DEBUG) { 2078 if ( 2079 Object.keys(this.properties).length || 2080 this.patternProperties.length || 2081 !(this.additionalProperties instanceof AnyType) 2082 ) { 2083 throw new Error( 2084 "InternalError: isInstanceOf can only be used " + 2085 "with objects that are otherwise unrestricted" 2086 ); 2087 } 2088 } 2089 2090 if ( 2091 ChromeUtils.getClassName(value) !== this.isInstanceOf && 2092 (this.isInstanceOf !== "Element" || value.nodeType !== 1) 2093 ) { 2094 return context.error( 2095 `Object must be an instance of ${this.isInstanceOf}`, 2096 `be an instance of ${this.isInstanceOf}` 2097 ); 2098 } 2099 2100 // This is kind of a hack, but we can't normalize things that 2101 // aren't JSON, so we just return them. 2102 return this.postprocess({ value }, context); 2103 } 2104 2105 let properties = this.extractProperties(value, context); 2106 let remainingProps = new Set(Object.keys(properties)); 2107 2108 let result = {}; 2109 for (let prop of Object.keys(this.properties)) { 2110 this.checkProperty( 2111 context, 2112 prop, 2113 this.properties[prop], 2114 result, 2115 properties, 2116 remainingProps 2117 ); 2118 } 2119 2120 for (let prop of Object.keys(properties)) { 2121 for (let { pattern, type } of this.patternProperties) { 2122 if (pattern.test(prop)) { 2123 this.checkProperty( 2124 context, 2125 prop, 2126 type, 2127 result, 2128 properties, 2129 remainingProps 2130 ); 2131 } 2132 } 2133 } 2134 2135 if (this.additionalProperties) { 2136 for (let prop of remainingProps) { 2137 let r = context.withPath(prop, () => 2138 this.additionalProperties.normalize(properties[prop], context) 2139 ); 2140 if (r.error) { 2141 return r; 2142 } 2143 result[prop] = r.value; 2144 } 2145 } else if (remainingProps.size == 1) { 2146 return context.error( 2147 `Unexpected property "${[...remainingProps]}"`, 2148 `not contain an unexpected "${[...remainingProps]}" property` 2149 ); 2150 } else if (remainingProps.size) { 2151 let props = [...remainingProps].sort().join(", "); 2152 return context.error( 2153 `Unexpected properties: ${props}`, 2154 `not contain the unexpected properties [${props}]` 2155 ); 2156 } 2157 2158 return this.postprocess({ value: result }, context); 2159 } catch (e) { 2160 if (e.error) { 2161 return e; 2162 } 2163 throw e; 2164 } 2165 } 2166} 2167 2168// This type is just a placeholder to be referred to by 2169// SubModuleProperty. No value is ever expected to have this type. 2170SubModuleType = class SubModuleType extends Type { 2171 static get EXTRA_PROPERTIES() { 2172 return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES]; 2173 } 2174 2175 static parseSchema(root, schema, path, extraProperties = []) { 2176 this.checkSchemaProperties(schema, path, extraProperties); 2177 2178 // The path we pass in here is only used for error messages. 2179 path = [...path, schema.id]; 2180 let functions = schema.functions 2181 .filter(fun => !fun.unsupported) 2182 .map(fun => FunctionEntry.parseSchema(root, fun, path)); 2183 2184 let events = []; 2185 2186 if (schema.events) { 2187 events = schema.events 2188 .filter(event => !event.unsupported) 2189 .map(event => Event.parseSchema(root, event, path)); 2190 } 2191 2192 return new this(schema, functions, events); 2193 } 2194 2195 constructor(schema, functions, events) { 2196 // schema contains properties such as min/max_manifest_version needed 2197 // in the base class so that the Context class can version compare 2198 // any entries against the manifest version. 2199 super(schema); 2200 this.functions = functions; 2201 this.events = events; 2202 } 2203}; 2204 2205class NumberType extends Type { 2206 normalize(value, context) { 2207 let r = this.normalizeBase("number", value, context); 2208 if (r.error) { 2209 return r; 2210 } 2211 2212 if (isNaN(r.value) || !Number.isFinite(r.value)) { 2213 return context.error( 2214 "NaN and infinity are not valid", 2215 "be a finite number" 2216 ); 2217 } 2218 2219 return r; 2220 } 2221 2222 checkBaseType(baseType) { 2223 return baseType == "number" || baseType == "integer"; 2224 } 2225} 2226 2227class IntegerType extends Type { 2228 static get EXTRA_PROPERTIES() { 2229 return ["minimum", "maximum", ...super.EXTRA_PROPERTIES]; 2230 } 2231 2232 static parseSchema(root, schema, path, extraProperties = []) { 2233 this.checkSchemaProperties(schema, path, extraProperties); 2234 2235 let { minimum = -Infinity, maximum = Infinity } = schema; 2236 return new this(schema, minimum, maximum); 2237 } 2238 2239 constructor(schema, minimum, maximum) { 2240 super(schema); 2241 this.minimum = minimum; 2242 this.maximum = maximum; 2243 } 2244 2245 normalize(value, context) { 2246 let r = this.normalizeBase("integer", value, context); 2247 if (r.error) { 2248 return r; 2249 } 2250 value = r.value; 2251 2252 // Ensure it's between -2**31 and 2**31-1 2253 if (!Number.isSafeInteger(value)) { 2254 return context.error( 2255 "Integer is out of range", 2256 "be a valid 32 bit signed integer" 2257 ); 2258 } 2259 2260 if (value < this.minimum) { 2261 return context.error( 2262 `Integer ${value} is too small (must be at least ${this.minimum})`, 2263 `be at least ${this.minimum}` 2264 ); 2265 } 2266 if (value > this.maximum) { 2267 return context.error( 2268 `Integer ${value} is too big (must be at most ${this.maximum})`, 2269 `be no greater than ${this.maximum}` 2270 ); 2271 } 2272 2273 return this.postprocess(r, context); 2274 } 2275 2276 checkBaseType(baseType) { 2277 return baseType == "integer"; 2278 } 2279} 2280 2281class BooleanType extends Type { 2282 static get EXTRA_PROPERTIES() { 2283 return ["enum", ...super.EXTRA_PROPERTIES]; 2284 } 2285 2286 static parseSchema(root, schema, path, extraProperties = []) { 2287 this.checkSchemaProperties(schema, path, extraProperties); 2288 let enumeration = schema.enum || null; 2289 return new this(schema, enumeration); 2290 } 2291 2292 constructor(schema, enumeration) { 2293 super(schema); 2294 this.enumeration = enumeration; 2295 } 2296 2297 normalize(value, context) { 2298 if (!this.checkBaseType(getValueBaseType(value))) { 2299 return context.error( 2300 () => `Expected boolean instead of ${JSON.stringify(value)}`, 2301 `be a boolean` 2302 ); 2303 } 2304 value = this.preprocess(value, context); 2305 if (this.enumeration && !this.enumeration.includes(value)) { 2306 return context.error( 2307 () => `Invalid value ${JSON.stringify(value)}`, 2308 `be ${this.enumeration}` 2309 ); 2310 } 2311 this.checkDeprecated(context, value); 2312 return { value }; 2313 } 2314 2315 checkBaseType(baseType) { 2316 return baseType == "boolean"; 2317 } 2318} 2319 2320class ArrayType extends Type { 2321 static get EXTRA_PROPERTIES() { 2322 return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES]; 2323 } 2324 2325 static parseSchema(root, schema, path, extraProperties = []) { 2326 this.checkSchemaProperties(schema, path, extraProperties); 2327 2328 let items = root.parseSchema(schema.items, path, ["onError"]); 2329 2330 return new this( 2331 schema, 2332 items, 2333 schema.minItems || 0, 2334 schema.maxItems || Infinity 2335 ); 2336 } 2337 2338 constructor(schema, itemType, minItems, maxItems) { 2339 super(schema); 2340 this.itemType = itemType; 2341 this.minItems = minItems; 2342 this.maxItems = maxItems; 2343 this.onError = schema.items.onError || null; 2344 } 2345 2346 normalize(value, context) { 2347 let v = this.normalizeBase("array", value, context); 2348 if (v.error) { 2349 return v; 2350 } 2351 value = v.value; 2352 2353 let result = []; 2354 for (let [i, element] of value.entries()) { 2355 element = context.withPath(String(i), () => 2356 this.itemType.normalize(element, context) 2357 ); 2358 if (element.error) { 2359 if (this.onError == "warn") { 2360 this.logWarning(context, forceString(element.error)); 2361 } else if (this.onError != "ignore") { 2362 return element; 2363 } 2364 continue; 2365 } 2366 result.push(element.value); 2367 } 2368 2369 if (result.length < this.minItems) { 2370 return context.error( 2371 `Array requires at least ${this.minItems} items; you have ${result.length}`, 2372 `have at least ${this.minItems} items` 2373 ); 2374 } 2375 2376 if (result.length > this.maxItems) { 2377 return context.error( 2378 `Array requires at most ${this.maxItems} items; you have ${result.length}`, 2379 `have at most ${this.maxItems} items` 2380 ); 2381 } 2382 2383 return this.postprocess({ value: result }, context); 2384 } 2385 2386 checkBaseType(baseType) { 2387 return baseType == "array"; 2388 } 2389} 2390 2391class FunctionType extends Type { 2392 static get EXTRA_PROPERTIES() { 2393 return [ 2394 "parameters", 2395 "async", 2396 "returns", 2397 "requireUserInput", 2398 ...super.EXTRA_PROPERTIES, 2399 ]; 2400 } 2401 2402 static parseSchema(root, schema, path, extraProperties = []) { 2403 this.checkSchemaProperties(schema, path, extraProperties); 2404 2405 let isAsync = !!schema.async; 2406 let isExpectingCallback = typeof schema.async === "string"; 2407 let parameters = null; 2408 if ("parameters" in schema) { 2409 parameters = []; 2410 for (let param of schema.parameters) { 2411 // Callbacks default to optional for now, because of promise 2412 // handling. 2413 let isCallback = isAsync && param.name == schema.async; 2414 if (isCallback) { 2415 isExpectingCallback = false; 2416 } 2417 2418 parameters.push({ 2419 type: root.parseSchema(param, path, ["name", "optional", "default"]), 2420 name: param.name, 2421 optional: param.optional == null ? isCallback : param.optional, 2422 default: param.default == undefined ? null : param.default, 2423 }); 2424 } 2425 } 2426 let hasAsyncCallback = false; 2427 if (isAsync) { 2428 hasAsyncCallback = 2429 parameters && 2430 parameters.length && 2431 parameters[parameters.length - 1].name == schema.async; 2432 } 2433 2434 if (DEBUG) { 2435 if (isExpectingCallback) { 2436 throw new Error( 2437 `Internal error: Expected a callback parameter ` + 2438 `with name ${schema.async}` 2439 ); 2440 } 2441 2442 if (isAsync && schema.returns) { 2443 throw new Error( 2444 "Internal error: Async functions must not have return values." 2445 ); 2446 } 2447 if ( 2448 isAsync && 2449 schema.allowAmbiguousOptionalArguments && 2450 !hasAsyncCallback 2451 ) { 2452 throw new Error( 2453 "Internal error: Async functions with ambiguous " + 2454 "arguments must declare the callback as the last parameter" 2455 ); 2456 } 2457 } 2458 2459 return new this( 2460 schema, 2461 parameters, 2462 isAsync, 2463 hasAsyncCallback, 2464 !!schema.requireUserInput 2465 ); 2466 } 2467 2468 constructor(schema, parameters, isAsync, hasAsyncCallback, requireUserInput) { 2469 super(schema); 2470 this.parameters = parameters; 2471 this.isAsync = isAsync; 2472 this.hasAsyncCallback = hasAsyncCallback; 2473 this.requireUserInput = requireUserInput; 2474 } 2475 2476 normalize(value, context) { 2477 return this.normalizeBase("function", value, context); 2478 } 2479 2480 checkBaseType(baseType) { 2481 return baseType == "function"; 2482 } 2483} 2484 2485// Represents a "property" defined in a schema namespace with a 2486// particular value. Essentially this is a constant. 2487class ValueProperty extends Entry { 2488 constructor(schema, name, value) { 2489 super(schema); 2490 this.name = name; 2491 this.value = value; 2492 } 2493 2494 getDescriptor(path, context) { 2495 // Prevent injection if not a supported version. 2496 if (!context.matchManifestVersion(this)) { 2497 return; 2498 } 2499 2500 return { 2501 descriptor: { value: this.value }, 2502 }; 2503 } 2504} 2505 2506// Represents a "property" defined in a schema namespace that is not a 2507// constant. 2508class TypeProperty extends Entry { 2509 constructor(schema, path, name, type, writable, permissions) { 2510 super(schema); 2511 this.path = path; 2512 this.name = name; 2513 this.type = type; 2514 this.writable = writable; 2515 this.permissions = permissions; 2516 } 2517 2518 throwError(context, msg) { 2519 throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`); 2520 } 2521 2522 getDescriptor(path, context) { 2523 if (this.unsupported || !context.matchManifestVersion(this)) { 2524 return; 2525 } 2526 2527 let apiImpl = context.getImplementation(path.join("."), this.name); 2528 2529 let getStub = () => { 2530 this.checkDeprecated(context); 2531 return apiImpl.getProperty(); 2532 }; 2533 2534 let descriptor = { 2535 get: Cu.exportFunction(getStub, context.cloneScope), 2536 }; 2537 2538 if (this.writable) { 2539 let setStub = value => { 2540 let normalized = this.type.normalize(value, context); 2541 if (normalized.error) { 2542 this.throwError(context, forceString(normalized.error)); 2543 } 2544 2545 apiImpl.setProperty(normalized.value); 2546 }; 2547 2548 descriptor.set = Cu.exportFunction(setStub, context.cloneScope); 2549 } 2550 2551 return { 2552 descriptor, 2553 revoke() { 2554 apiImpl.revoke(); 2555 apiImpl = null; 2556 }, 2557 }; 2558 } 2559} 2560 2561class SubModuleProperty extends Entry { 2562 // A SubModuleProperty represents a tree of objects and properties 2563 // to expose to an extension. Currently we support only a limited 2564 // form of sub-module properties, where "$ref" points to a 2565 // SubModuleType containing a list of functions and "properties" is 2566 // a list of additional simple properties. 2567 // 2568 // name: Name of the property stuff is being added to. 2569 // namespaceName: Namespace in which the property lives. 2570 // reference: Name of the type defining the functions to add to the property. 2571 // properties: Additional properties to add to the module (unsupported). 2572 constructor(root, schema, path, name, reference, properties, permissions) { 2573 super(schema); 2574 this.root = root; 2575 this.name = name; 2576 this.path = path; 2577 this.namespaceName = path.join("."); 2578 this.reference = reference; 2579 this.properties = properties; 2580 this.permissions = permissions; 2581 } 2582 2583 getDescriptor(path, context) { 2584 let obj = Cu.createObjectIn(context.cloneScope); 2585 2586 let ns = this.root.getNamespace(this.namespaceName); 2587 let type = ns.get(this.reference); 2588 if (!type && this.reference.includes(".")) { 2589 let [namespaceName, ref] = this.reference.split("."); 2590 ns = this.root.getNamespace(namespaceName); 2591 type = ns.get(ref); 2592 } 2593 // Prevent injection if not a supported version. 2594 if (!context.matchManifestVersion(type)) { 2595 return; 2596 } 2597 2598 if (DEBUG) { 2599 if (!type || !(type instanceof SubModuleType)) { 2600 throw new Error( 2601 `Internal error: ${this.namespaceName}.${this.reference} ` + 2602 `is not a sub-module` 2603 ); 2604 } 2605 } 2606 let subpath = [...path, this.name]; 2607 2608 let functions = type.functions; 2609 for (let fun of functions) { 2610 context.injectInto(fun, obj, fun.name, subpath, ns); 2611 } 2612 2613 let events = type.events; 2614 for (let event of events) { 2615 context.injectInto(event, obj, event.name, subpath, ns); 2616 } 2617 2618 // TODO: Inject this.properties. 2619 2620 return { 2621 descriptor: { value: obj }, 2622 revoke() { 2623 let unwrapped = ChromeUtils.waiveXrays(obj); 2624 for (let fun of functions) { 2625 try { 2626 delete unwrapped[fun.name]; 2627 } catch (e) { 2628 Cu.reportError(e); 2629 } 2630 } 2631 }, 2632 }; 2633 } 2634} 2635 2636// This class is a base class for FunctionEntrys and Events. It takes 2637// care of validating parameter lists (i.e., handling of optional 2638// parameters and parameter type checking). 2639class CallEntry extends Entry { 2640 constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) { 2641 super(schema); 2642 this.path = path; 2643 this.name = name; 2644 this.parameters = parameters; 2645 this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments; 2646 } 2647 2648 throwError(context, msg) { 2649 throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`); 2650 } 2651 2652 checkParameters(args, context) { 2653 let fixedArgs = []; 2654 2655 // First we create a new array, fixedArgs, that is the same as 2656 // |args| but with default values in place of omitted optional parameters. 2657 let check = (parameterIndex, argIndex) => { 2658 if (parameterIndex == this.parameters.length) { 2659 if (argIndex == args.length) { 2660 return true; 2661 } 2662 return false; 2663 } 2664 2665 let parameter = this.parameters[parameterIndex]; 2666 if (parameter.optional) { 2667 // Try skipping it. 2668 fixedArgs[parameterIndex] = parameter.default; 2669 if (check(parameterIndex + 1, argIndex)) { 2670 return true; 2671 } 2672 } 2673 2674 if (argIndex == args.length) { 2675 return false; 2676 } 2677 2678 let arg = args[argIndex]; 2679 if (!parameter.type.checkBaseType(getValueBaseType(arg))) { 2680 // For Chrome compatibility, use the default value if null or undefined 2681 // is explicitly passed but is not a valid argument in this position. 2682 if (parameter.optional && (arg === null || arg === undefined)) { 2683 fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, global); 2684 } else { 2685 return false; 2686 } 2687 } else { 2688 fixedArgs[parameterIndex] = arg; 2689 } 2690 2691 return check(parameterIndex + 1, argIndex + 1); 2692 }; 2693 2694 if (this.allowAmbiguousOptionalArguments) { 2695 // When this option is set, it's up to the implementation to 2696 // parse arguments. 2697 // The last argument for asynchronous methods is either a function or null. 2698 // This is specifically done for runtime.sendMessage. 2699 if (this.hasAsyncCallback && typeof args[args.length - 1] != "function") { 2700 args.push(null); 2701 } 2702 return args; 2703 } 2704 let success = check(0, 0); 2705 if (!success) { 2706 this.throwError(context, "Incorrect argument types"); 2707 } 2708 2709 // Now we normalize (and fully type check) all non-omitted arguments. 2710 fixedArgs = fixedArgs.map((arg, parameterIndex) => { 2711 if (arg === null) { 2712 return null; 2713 } 2714 let parameter = this.parameters[parameterIndex]; 2715 let r = parameter.type.normalize(arg, context); 2716 if (r.error) { 2717 this.throwError( 2718 context, 2719 `Type error for parameter ${parameter.name} (${forceString(r.error)})` 2720 ); 2721 } 2722 return r.value; 2723 }); 2724 2725 return fixedArgs; 2726 } 2727} 2728 2729// Represents a "function" defined in a schema namespace. 2730FunctionEntry = class FunctionEntry extends CallEntry { 2731 static parseSchema(root, schema, path) { 2732 // When not in DEBUG mode, we just need to know *if* this returns. 2733 let returns = !!schema.returns; 2734 if (DEBUG && "returns" in schema) { 2735 returns = { 2736 type: root.parseSchema(schema.returns, path, ["optional", "name"]), 2737 optional: schema.returns.optional || false, 2738 name: "result", 2739 }; 2740 } 2741 2742 return new this( 2743 schema, 2744 path, 2745 schema.name, 2746 root.parseSchema(schema, path, [ 2747 "name", 2748 "unsupported", 2749 "returns", 2750 "permissions", 2751 "allowAmbiguousOptionalArguments", 2752 "allowCrossOriginArguments", 2753 ]), 2754 schema.unsupported || false, 2755 schema.allowAmbiguousOptionalArguments || false, 2756 schema.allowCrossOriginArguments || false, 2757 returns, 2758 schema.permissions || null 2759 ); 2760 } 2761 2762 constructor( 2763 schema, 2764 path, 2765 name, 2766 type, 2767 unsupported, 2768 allowAmbiguousOptionalArguments, 2769 allowCrossOriginArguments, 2770 returns, 2771 permissions 2772 ) { 2773 super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments); 2774 this.unsupported = unsupported; 2775 this.returns = returns; 2776 this.permissions = permissions; 2777 this.allowCrossOriginArguments = allowCrossOriginArguments; 2778 2779 this.isAsync = type.isAsync; 2780 this.hasAsyncCallback = type.hasAsyncCallback; 2781 this.requireUserInput = type.requireUserInput; 2782 } 2783 2784 checkValue({ type, optional, name }, value, context) { 2785 if (optional && value == null) { 2786 return; 2787 } 2788 if ( 2789 type.reference === "ExtensionPanel" || 2790 type.reference === "ExtensionSidebarPane" || 2791 type.reference === "Port" 2792 ) { 2793 // TODO: We currently treat objects with functions as SubModuleType, 2794 // which is just wrong, and a bigger yak. Skipping for now. 2795 return; 2796 } 2797 const { error } = type.normalize(value, context); 2798 if (error) { 2799 this.throwError( 2800 context, 2801 `Type error for ${name} value (${forceString(error)})` 2802 ); 2803 } 2804 } 2805 2806 checkCallback(args, context) { 2807 const callback = this.parameters[this.parameters.length - 1]; 2808 for (const [i, param] of callback.type.parameters.entries()) { 2809 this.checkValue(param, args[i], context); 2810 } 2811 } 2812 2813 getDescriptor(path, context) { 2814 let apiImpl = context.getImplementation(path.join("."), this.name); 2815 2816 let stub; 2817 if (this.isAsync) { 2818 stub = (...args) => { 2819 this.checkDeprecated(context); 2820 let actuals = this.checkParameters(args, context); 2821 let callback = null; 2822 if (this.hasAsyncCallback) { 2823 callback = actuals.pop(); 2824 } 2825 if (callback === null && context.isChromeCompat) { 2826 // We pass an empty stub function as a default callback for 2827 // the `chrome` API, so promise objects are not returned, 2828 // and lastError values are reported immediately. 2829 callback = () => {}; 2830 } 2831 if (DEBUG && this.hasAsyncCallback && callback) { 2832 let original = callback; 2833 callback = (...args) => { 2834 this.checkCallback(args, context); 2835 original(...args); 2836 }; 2837 } 2838 let result = apiImpl.callAsyncFunction( 2839 actuals, 2840 callback, 2841 this.requireUserInput 2842 ); 2843 if (DEBUG && this.hasAsyncCallback && !callback) { 2844 return result.then(result => { 2845 this.checkCallback([result], context); 2846 return result; 2847 }); 2848 } 2849 return result; 2850 }; 2851 } else if (!this.returns) { 2852 stub = (...args) => { 2853 this.checkDeprecated(context); 2854 let actuals = this.checkParameters(args, context); 2855 return apiImpl.callFunctionNoReturn(actuals); 2856 }; 2857 } else { 2858 stub = (...args) => { 2859 this.checkDeprecated(context); 2860 let actuals = this.checkParameters(args, context); 2861 let result = apiImpl.callFunction(actuals); 2862 if (DEBUG && this.returns) { 2863 this.checkValue(this.returns, result, context); 2864 } 2865 return result; 2866 }; 2867 } 2868 2869 return { 2870 descriptor: { 2871 value: Cu.exportFunction(stub, context.cloneScope, { 2872 allowCrossOriginArguments: this.allowCrossOriginArguments, 2873 }), 2874 }, 2875 revoke() { 2876 apiImpl.revoke(); 2877 apiImpl = null; 2878 }, 2879 }; 2880 } 2881}; 2882 2883// Represents an "event" defined in a schema namespace. 2884// 2885// TODO Bug 1369722: we should be able to remove the eslint-disable-line that follows 2886// once Bug 1369722 has been fixed. 2887// eslint-disable-next-line no-global-assign 2888Event = class Event extends CallEntry { 2889 static parseSchema(root, event, path) { 2890 let extraParameters = Array.from(event.extraParameters || [], param => ({ 2891 type: root.parseSchema(param, path, ["name", "optional", "default"]), 2892 name: param.name, 2893 optional: param.optional || false, 2894 default: param.default == undefined ? null : param.default, 2895 })); 2896 2897 let extraProperties = [ 2898 "name", 2899 "unsupported", 2900 "permissions", 2901 "extraParameters", 2902 // We ignore these properties for now. 2903 "returns", 2904 "filters", 2905 ]; 2906 2907 return new this( 2908 event, 2909 path, 2910 event.name, 2911 root.parseSchema(event, path, extraProperties), 2912 extraParameters, 2913 event.unsupported || false, 2914 event.permissions || null 2915 ); 2916 } 2917 2918 constructor( 2919 schema, 2920 path, 2921 name, 2922 type, 2923 extraParameters, 2924 unsupported, 2925 permissions 2926 ) { 2927 super(schema, path, name, extraParameters); 2928 this.type = type; 2929 this.unsupported = unsupported; 2930 this.permissions = permissions; 2931 } 2932 2933 checkListener(listener, context) { 2934 let r = this.type.normalize(listener, context); 2935 if (r.error) { 2936 this.throwError(context, "Invalid listener"); 2937 } 2938 return r.value; 2939 } 2940 2941 getDescriptor(path, context) { 2942 let apiImpl = context.getImplementation(path.join("."), this.name); 2943 2944 let addStub = (listener, ...args) => { 2945 listener = this.checkListener(listener, context); 2946 let actuals = this.checkParameters(args, context); 2947 apiImpl.addListener(listener, actuals); 2948 }; 2949 2950 let removeStub = listener => { 2951 listener = this.checkListener(listener, context); 2952 apiImpl.removeListener(listener); 2953 }; 2954 2955 let hasStub = listener => { 2956 listener = this.checkListener(listener, context); 2957 return apiImpl.hasListener(listener); 2958 }; 2959 2960 let obj = Cu.createObjectIn(context.cloneScope); 2961 2962 Cu.exportFunction(addStub, obj, { defineAs: "addListener" }); 2963 Cu.exportFunction(removeStub, obj, { defineAs: "removeListener" }); 2964 Cu.exportFunction(hasStub, obj, { defineAs: "hasListener" }); 2965 2966 return { 2967 descriptor: { value: obj }, 2968 revoke() { 2969 apiImpl.revoke(); 2970 apiImpl = null; 2971 2972 let unwrapped = ChromeUtils.waiveXrays(obj); 2973 delete unwrapped.addListener; 2974 delete unwrapped.removeListener; 2975 delete unwrapped.hasListener; 2976 }, 2977 }; 2978 } 2979}; 2980 2981const TYPES = Object.freeze( 2982 Object.assign(Object.create(null), { 2983 any: AnyType, 2984 array: ArrayType, 2985 boolean: BooleanType, 2986 function: FunctionType, 2987 integer: IntegerType, 2988 null: NullType, 2989 number: NumberType, 2990 object: ObjectType, 2991 string: StringType, 2992 }) 2993); 2994 2995const LOADERS = { 2996 events: "loadEvent", 2997 functions: "loadFunction", 2998 properties: "loadProperty", 2999 types: "loadType", 3000}; 3001 3002class Namespace extends Map { 3003 constructor(root, name, path) { 3004 super(); 3005 3006 this.root = root; 3007 3008 this._lazySchemas = []; 3009 this.initialized = false; 3010 3011 this.name = name; 3012 this.path = name ? [...path, name] : [...path]; 3013 3014 this.superNamespace = null; 3015 3016 this.min_manifest_version = MIN_MANIFEST_VERSION; 3017 this.max_manifest_version = MAX_MANIFEST_VERSION; 3018 3019 this.permissions = null; 3020 this.allowedContexts = []; 3021 this.defaultContexts = []; 3022 } 3023 3024 /** 3025 * Adds a JSON Schema object to the set of schemas that represent this 3026 * namespace. 3027 * 3028 * @param {object} schema 3029 * A JSON schema object which partially describes this 3030 * namespace. 3031 */ 3032 addSchema(schema) { 3033 this._lazySchemas.push(schema); 3034 3035 for (let prop of [ 3036 "permissions", 3037 "allowedContexts", 3038 "defaultContexts", 3039 "min_manifest_version", 3040 "max_manifest_version", 3041 ]) { 3042 if (schema[prop]) { 3043 this[prop] = schema[prop]; 3044 } 3045 } 3046 3047 if (schema.$import) { 3048 this.superNamespace = this.root.getNamespace(schema.$import); 3049 } 3050 } 3051 3052 /** 3053 * Initializes the keys of this namespace based on the schema objects 3054 * added via previous `addSchema` calls. 3055 */ 3056 init() { 3057 if (this.initialized) { 3058 return; 3059 } 3060 3061 if (this.superNamespace) { 3062 this._lazySchemas.unshift(...this.superNamespace._lazySchemas); 3063 } 3064 3065 for (let type of Object.keys(LOADERS)) { 3066 this[type] = new DefaultMap(() => []); 3067 } 3068 3069 for (let schema of this._lazySchemas) { 3070 for (let type of schema.types || []) { 3071 if (!type.unsupported) { 3072 this.types.get(type.$extend || type.id).push(type); 3073 } 3074 } 3075 3076 for (let [name, prop] of Object.entries(schema.properties || {})) { 3077 if (!prop.unsupported) { 3078 this.properties.get(name).push(prop); 3079 } 3080 } 3081 3082 for (let fun of schema.functions || []) { 3083 if (!fun.unsupported) { 3084 this.functions.get(fun.name).push(fun); 3085 } 3086 } 3087 3088 for (let event of schema.events || []) { 3089 if (!event.unsupported) { 3090 this.events.get(event.name).push(event); 3091 } 3092 } 3093 } 3094 3095 // For each type of top-level property in the schema object, iterate 3096 // over all properties of that type, and create a temporary key for 3097 // each property pointing to its type. Those temporary properties 3098 // are later used to instantiate an Entry object based on the actual 3099 // schema object. 3100 for (let type of Object.keys(LOADERS)) { 3101 for (let key of this[type].keys()) { 3102 this.set(key, type); 3103 } 3104 } 3105 3106 this.initialized = true; 3107 3108 if (DEBUG) { 3109 for (let key of this.keys()) { 3110 this.get(key); 3111 } 3112 } 3113 } 3114 3115 /** 3116 * Initializes the value of a given key, by parsing the schema object 3117 * associated with it and replacing its temporary value with an `Entry` 3118 * instance. 3119 * 3120 * @param {string} key 3121 * The name of the property to initialize. 3122 * @param {string} type 3123 * The type of property the key represents. Must have a 3124 * corresponding entry in the `LOADERS` object, pointing to the 3125 * initialization method for that type. 3126 * 3127 * @returns {Entry} 3128 */ 3129 initKey(key, type) { 3130 let loader = LOADERS[type]; 3131 3132 for (let schema of this[type].get(key)) { 3133 this.set(key, this[loader](key, schema)); 3134 } 3135 3136 return this.get(key); 3137 } 3138 3139 loadType(name, type) { 3140 if ("$extend" in type) { 3141 return this.extendType(type); 3142 } 3143 return this.root.parseSchema(type, this.path, ["id"]); 3144 } 3145 3146 extendType(type) { 3147 let targetType = this.get(type.$extend); 3148 3149 // Only allow extending object and choices types for now. 3150 if (targetType instanceof ObjectType) { 3151 type.type = "object"; 3152 } else if (DEBUG) { 3153 if (!targetType) { 3154 throw new Error( 3155 `Internal error: Attempt to extend a nonexistent type ${type.$extend}` 3156 ); 3157 } else if (!(targetType instanceof ChoiceType)) { 3158 throw new Error( 3159 `Internal error: Attempt to extend a non-extensible type ${type.$extend}` 3160 ); 3161 } 3162 } 3163 3164 let parsed = this.root.parseSchema(type, this.path, ["$extend"]); 3165 3166 if (DEBUG && parsed.constructor !== targetType.constructor) { 3167 throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`); 3168 } 3169 3170 targetType.extend(parsed); 3171 3172 return targetType; 3173 } 3174 3175 loadProperty(name, prop) { 3176 if ("$ref" in prop) { 3177 if (!prop.unsupported) { 3178 return new SubModuleProperty( 3179 this.root, 3180 prop, 3181 this.path, 3182 name, 3183 prop.$ref, 3184 prop.properties || {}, 3185 prop.permissions || null 3186 ); 3187 } 3188 } else if ("value" in prop) { 3189 return new ValueProperty(prop, name, prop.value); 3190 } else { 3191 // We ignore the "optional" attribute on properties since we 3192 // don't inject anything here anyway. 3193 let type = this.root.parseSchema( 3194 prop, 3195 [this.name], 3196 ["optional", "permissions", "writable"] 3197 ); 3198 return new TypeProperty( 3199 prop, 3200 this.path, 3201 name, 3202 type, 3203 prop.writable || false, 3204 prop.permissions || null 3205 ); 3206 } 3207 } 3208 3209 loadFunction(name, fun) { 3210 return FunctionEntry.parseSchema(this.root, fun, this.path); 3211 } 3212 3213 loadEvent(name, event) { 3214 return Event.parseSchema(this.root, event, this.path); 3215 } 3216 3217 /** 3218 * Injects the properties of this namespace into the given object. 3219 * 3220 * @param {object} dest 3221 * The object into which to inject the namespace properties. 3222 * @param {InjectionContext} context 3223 * The injection context with which to inject the properties. 3224 */ 3225 injectInto(dest, context) { 3226 for (let name of this.keys()) { 3227 // If the entry does not match the manifest version do not 3228 // inject the property. This prevents the item from being 3229 // enumerable in the namespace object. We cannot accomplish 3230 // this inside exportLazyProperty, it specifically injects 3231 // an enumerable object. 3232 let entry = this.get(name); 3233 if (!context.matchManifestVersion(entry)) { 3234 continue; 3235 } 3236 exportLazyProperty(dest, name, () => { 3237 let entry = this.get(name); 3238 3239 return context.getDescriptor(entry, dest, name, this.path, this); 3240 }); 3241 } 3242 } 3243 3244 getDescriptor(path, context) { 3245 let obj = Cu.createObjectIn(context.cloneScope); 3246 3247 let ns = context.schemaRoot.getNamespace(this.path.join(".")); 3248 ns.injectInto(obj, context); 3249 3250 // Only inject the namespace object if it isn't empty. 3251 if (Object.keys(obj).length) { 3252 return { 3253 descriptor: { value: obj }, 3254 }; 3255 } 3256 } 3257 3258 keys() { 3259 this.init(); 3260 return super.keys(); 3261 } 3262 3263 *entries() { 3264 for (let key of this.keys()) { 3265 yield [key, this.get(key)]; 3266 } 3267 } 3268 3269 get(key) { 3270 this.init(); 3271 let value = super.get(key); 3272 3273 // The initial values of lazily-initialized schema properties are 3274 // strings, pointing to the type of property, corresponding to one 3275 // of the entries in the `LOADERS` object. 3276 if (typeof value === "string") { 3277 value = this.initKey(key, value); 3278 } 3279 3280 return value; 3281 } 3282 3283 /** 3284 * Returns a Namespace object for the given namespace name. If a 3285 * namespace object with this name does not already exist, it is 3286 * created. If the name contains any '.' characters, namespaces are 3287 * recursively created, for each dot-separated component. 3288 * 3289 * @param {string} name 3290 * The name of the sub-namespace to retrieve. 3291 * @param {boolean} [create = true] 3292 * If true, create any intermediate namespaces which don't 3293 * exist. 3294 * 3295 * @returns {Namespace} 3296 */ 3297 getNamespace(name, create = true) { 3298 let subName; 3299 3300 let idx = name.indexOf("."); 3301 if (idx > 0) { 3302 subName = name.slice(idx + 1); 3303 name = name.slice(0, idx); 3304 } 3305 3306 let ns = super.get(name); 3307 if (!ns) { 3308 if (!create) { 3309 return null; 3310 } 3311 ns = new Namespace(this.root, name, this.path); 3312 this.set(name, ns); 3313 } 3314 3315 if (subName) { 3316 return ns.getNamespace(subName); 3317 } 3318 return ns; 3319 } 3320 3321 getOwnNamespace(name) { 3322 return this.getNamespace(name); 3323 } 3324 3325 has(key) { 3326 this.init(); 3327 return super.has(key); 3328 } 3329} 3330 3331/** 3332 * A namespace which combines the children of an arbitrary number of 3333 * sub-namespaces. 3334 */ 3335class Namespaces extends Namespace { 3336 constructor(root, name, path, namespaces) { 3337 super(root, name, path); 3338 3339 this.namespaces = namespaces; 3340 } 3341 3342 injectInto(obj, context) { 3343 for (let ns of this.namespaces) { 3344 ns.injectInto(obj, context); 3345 } 3346 } 3347} 3348 3349/** 3350 * A root schema which combines the contents of an arbitrary number of base 3351 * schema roots. 3352 */ 3353class SchemaRoots extends Namespaces { 3354 constructor(root, bases) { 3355 bases = bases.map(base => base.rootSchema || base); 3356 3357 super(null, "", [], bases); 3358 3359 this.root = root; 3360 this.bases = bases; 3361 this._namespaces = new Map(); 3362 } 3363 3364 _getNamespace(name, create) { 3365 let results = []; 3366 for (let root of this.bases) { 3367 let ns = root.getNamespace(name, create); 3368 if (ns) { 3369 results.push(ns); 3370 } 3371 } 3372 3373 if (results.length == 1) { 3374 return results[0]; 3375 } 3376 3377 if (results.length) { 3378 return new Namespaces(this.root, name, name.split("."), results); 3379 } 3380 return null; 3381 } 3382 3383 getNamespace(name, create) { 3384 let ns = this._namespaces.get(name); 3385 if (!ns) { 3386 ns = this._getNamespace(name, create); 3387 if (ns) { 3388 this._namespaces.set(name, ns); 3389 } 3390 } 3391 return ns; 3392 } 3393 3394 *getNamespaces(name) { 3395 for (let root of this.bases) { 3396 yield* root.getNamespaces(name); 3397 } 3398 } 3399} 3400 3401/** 3402 * A root schema namespace containing schema data which is isolated from data in 3403 * other schema roots. May extend a base namespace, in which case schemas in 3404 * this root may refer to types in a base, but not vice versa. 3405 * 3406 * @param {SchemaRoot|Array<SchemaRoot>|null} base 3407 * A base schema root (or roots) from which to derive, or null. 3408 * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON 3409 * A map of schema URLs and corresponding JSON blobs from which to 3410 * populate this root namespace. 3411 */ 3412class SchemaRoot extends Namespace { 3413 constructor(base, schemaJSON) { 3414 super(null, "", []); 3415 3416 if (Array.isArray(base)) { 3417 base = new SchemaRoots(this, base); 3418 } 3419 3420 this.root = this; 3421 this.base = base; 3422 this.schemaJSON = schemaJSON; 3423 } 3424 3425 *getNamespaces(path) { 3426 let name = path.join("."); 3427 3428 let ns = this.getNamespace(name, false); 3429 if (ns) { 3430 yield ns; 3431 } 3432 3433 if (this.base) { 3434 yield* this.base.getNamespaces(name); 3435 } 3436 } 3437 3438 /** 3439 * Returns the sub-namespace with the given name. If the given namespace 3440 * doesn't already exist, attempts to find it in the base SchemaRoot before 3441 * creating a new empty namespace. 3442 * 3443 * @param {string} name 3444 * The namespace to retrieve. 3445 * @param {boolean} [create = true] 3446 * If true, an empty namespace should be created if one does not 3447 * already exist. 3448 * @returns {Namespace|null} 3449 */ 3450 getNamespace(name, create = true) { 3451 let ns = super.getNamespace(name, false); 3452 if (ns) { 3453 return ns; 3454 } 3455 3456 ns = this.base && this.base.getNamespace(name, false); 3457 if (ns) { 3458 return ns; 3459 } 3460 return create && super.getNamespace(name, create); 3461 } 3462 3463 /** 3464 * Like getNamespace, but does not take the base SchemaRoot into account. 3465 * 3466 * @param {string} name 3467 * The namespace to retrieve. 3468 * @returns {Namespace} 3469 */ 3470 getOwnNamespace(name) { 3471 return super.getNamespace(name); 3472 } 3473 3474 parseSchema(schema, path, extraProperties = []) { 3475 let allowedProperties = DEBUG && new Set(extraProperties); 3476 3477 if ("choices" in schema) { 3478 return ChoiceType.parseSchema(this, schema, path, allowedProperties); 3479 } else if ("$ref" in schema) { 3480 return RefType.parseSchema(this, schema, path, allowedProperties); 3481 } 3482 3483 let type = TYPES[schema.type]; 3484 3485 if (DEBUG) { 3486 allowedProperties.add("type"); 3487 3488 if (!("type" in schema)) { 3489 throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`); 3490 } 3491 3492 if (!type) { 3493 throw new Error(`Unexpected type ${schema.type}`); 3494 } 3495 } 3496 3497 return type.parseSchema(this, schema, path, allowedProperties); 3498 } 3499 3500 parseSchemas() { 3501 for (let [key, schema] of this.schemaJSON.entries()) { 3502 try { 3503 if (typeof schema.deserialize === "function") { 3504 schema = schema.deserialize(global, isParentProcess); 3505 3506 // If we're in the parent process, we need to keep the 3507 // StructuredCloneHolder blob around in order to send to future child 3508 // processes. If we're in a child, we have no further use for it, so 3509 // just store the deserialized schema data in its place. 3510 if (!isParentProcess) { 3511 this.schemaJSON.set(key, schema); 3512 } 3513 } 3514 3515 this.loadSchema(schema); 3516 } catch (e) { 3517 Cu.reportError(e); 3518 } 3519 } 3520 } 3521 3522 loadSchema(json) { 3523 for (let namespace of json) { 3524 this.getOwnNamespace(namespace.namespace).addSchema(namespace); 3525 } 3526 } 3527 3528 /** 3529 * Checks whether a given object has the necessary permissions to 3530 * expose the given namespace. 3531 * 3532 * @param {string} namespace 3533 * The top-level namespace to check permissions for. 3534 * @param {object} wrapperFuncs 3535 * Wrapper functions for the given context. 3536 * @param {function} wrapperFuncs.hasPermission 3537 * A function which, when given a string argument, returns true 3538 * if the context has the given permission. 3539 * @returns {boolean} 3540 * True if the context has permission for the given namespace. 3541 */ 3542 checkPermissions(namespace, wrapperFuncs) { 3543 let ns = this.getNamespace(namespace); 3544 if (ns && ns.permissions) { 3545 return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm)); 3546 } 3547 return true; 3548 } 3549 3550 /** 3551 * Inject registered extension APIs into `dest`. 3552 * 3553 * @param {object} dest The root namespace for the APIs. 3554 * This object is usually exposed to extensions as "chrome" or "browser". 3555 * @param {object} wrapperFuncs An implementation of the InjectionContext 3556 * interface, which runs the actual functionality of the generated API. 3557 */ 3558 inject(dest, wrapperFuncs) { 3559 let context = new InjectionContext(wrapperFuncs, this); 3560 3561 this.injectInto(dest, context); 3562 } 3563 3564 injectInto(dest, context) { 3565 // For schema graphs where multiple schema roots have the same base, don't 3566 // inject it more than once. 3567 3568 if (!context.injectedRoots.has(this)) { 3569 context.injectedRoots.add(this); 3570 if (this.base) { 3571 this.base.injectInto(dest, context); 3572 } 3573 super.injectInto(dest, context); 3574 } 3575 } 3576 3577 /** 3578 * Normalize `obj` according to the loaded schema for `typeName`. 3579 * 3580 * @param {object} obj The object to normalize against the schema. 3581 * @param {string} typeName The name in the format namespace.propertyname 3582 * @param {object} context An implementation of Context. Any validation errors 3583 * are reported to the given context. 3584 * @returns {object} The normalized object. 3585 */ 3586 normalize(obj, typeName, context) { 3587 let [namespaceName, prop] = typeName.split("."); 3588 let ns = this.getNamespace(namespaceName); 3589 let type = ns.get(prop); 3590 3591 let result = type.normalize(obj, new Context(context)); 3592 if (result.error) { 3593 return { error: forceString(result.error) }; 3594 } 3595 return result; 3596 } 3597} 3598 3599this.Schemas = { 3600 initialized: false, 3601 3602 REVOKE: Symbol("@@revoke"), 3603 3604 // Maps a schema URL to the JSON contained in that schema file. This 3605 // is useful for sending the JSON across processes. 3606 schemaJSON: new Map(), 3607 3608 // A map of schema JSON which should be available in all content processes. 3609 contentSchemaJSON: new Map(), 3610 3611 // A map of schema JSON which should only be available to extension processes. 3612 privilegedSchemaJSON: new Map(), 3613 3614 _rootSchema: null, 3615 3616 // A weakmap for the validation Context class instances given an extension 3617 // context (keyed by the extensin context instance). 3618 // This is used instead of the InjectionContext for webIDL API validation 3619 // and normalization (see Schemas.checkParameters). 3620 paramsValidationContexts: new DefaultWeakMap( 3621 extContext => new Context(extContext) 3622 ), 3623 3624 get rootSchema() { 3625 if (!this.initialized) { 3626 this.init(); 3627 } 3628 if (!this._rootSchema) { 3629 this._rootSchema = new SchemaRoot(null, this.schemaJSON); 3630 this._rootSchema.parseSchemas(); 3631 } 3632 return this._rootSchema; 3633 }, 3634 3635 getNamespace(name) { 3636 return this.rootSchema.getNamespace(name); 3637 }, 3638 3639 init() { 3640 if (this.initialized) { 3641 return; 3642 } 3643 this.initialized = true; 3644 3645 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { 3646 let addSchemas = schemas => { 3647 for (let [key, value] of schemas.entries()) { 3648 this.schemaJSON.set(key, value); 3649 } 3650 }; 3651 3652 if (WebExtensionPolicy.isExtensionProcess || DEBUG) { 3653 addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS)); 3654 } 3655 3656 let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS); 3657 if (schemas) { 3658 addSchemas(schemas); 3659 } 3660 } 3661 }, 3662 3663 _loadCachedSchemasPromise: null, 3664 loadCachedSchemas() { 3665 if (!this._loadCachedSchemasPromise) { 3666 this._loadCachedSchemasPromise = StartupCache.schemas 3667 .getAll() 3668 .then(results => { 3669 return results; 3670 }); 3671 } 3672 3673 return this._loadCachedSchemasPromise; 3674 }, 3675 3676 addSchema(url, schema, content = false) { 3677 this.schemaJSON.set(url, schema); 3678 3679 if (content) { 3680 this.contentSchemaJSON.set(url, schema); 3681 } else { 3682 this.privilegedSchemaJSON.set(url, schema); 3683 } 3684 3685 if (this._rootSchema) { 3686 throw new Error("Schema loaded after root schema populated"); 3687 } 3688 }, 3689 3690 updateSharedSchemas() { 3691 let { sharedData } = Services.ppmm; 3692 3693 sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON); 3694 sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON); 3695 }, 3696 3697 fetch(url) { 3698 return readJSONAndBlobbify(url); 3699 }, 3700 3701 processSchema(json) { 3702 return blobbify(json); 3703 }, 3704 3705 async load(url, content = false) { 3706 if (!isParentProcess) { 3707 return; 3708 } 3709 3710 const startTime = Cu.now(); 3711 let schemaCache = await this.loadCachedSchemas(); 3712 const fromCache = schemaCache.has(url); 3713 3714 let blob = 3715 schemaCache.get(url) || 3716 (await StartupCache.schemas.get(url, readJSONAndBlobbify)); 3717 3718 if (!this.schemaJSON.has(url)) { 3719 this.addSchema(url, blob, content); 3720 } 3721 3722 ChromeUtils.addProfilerMarker( 3723 "ExtensionSchemas", 3724 { startTime }, 3725 `load ${url}, from cache: ${fromCache}` 3726 ); 3727 }, 3728 3729 /** 3730 * Checks whether a given object has the necessary permissions to 3731 * expose the given namespace. 3732 * 3733 * @param {string} namespace 3734 * The top-level namespace to check permissions for. 3735 * @param {object} wrapperFuncs 3736 * Wrapper functions for the given context. 3737 * @param {function} wrapperFuncs.hasPermission 3738 * A function which, when given a string argument, returns true 3739 * if the context has the given permission. 3740 * @returns {boolean} 3741 * True if the context has permission for the given namespace. 3742 */ 3743 checkPermissions(namespace, wrapperFuncs) { 3744 return this.rootSchema.checkPermissions(namespace, wrapperFuncs); 3745 }, 3746 3747 /** 3748 * Returns a sorted array of permission names for the given permission types. 3749 * 3750 * @param {Array} types An array of permission types, defaults to all permissions. 3751 * @returns {Array} sorted array of permission names 3752 */ 3753 getPermissionNames( 3754 types = [ 3755 "Permission", 3756 "OptionalPermission", 3757 "PermissionNoPrompt", 3758 "OptionalPermissionNoPrompt", 3759 ] 3760 ) { 3761 const ns = this.getNamespace("manifest"); 3762 let names = []; 3763 for (let typeName of types) { 3764 for (let choice of ns 3765 .get(typeName) 3766 .choices.filter(choice => choice.enumeration)) { 3767 names = names.concat(choice.enumeration); 3768 } 3769 } 3770 return names.sort(); 3771 }, 3772 3773 exportLazyGetter, 3774 3775 /** 3776 * Inject registered extension APIs into `dest`. 3777 * 3778 * @param {object} dest The root namespace for the APIs. 3779 * This object is usually exposed to extensions as "chrome" or "browser". 3780 * @param {object} wrapperFuncs An implementation of the InjectionContext 3781 * interface, which runs the actual functionality of the generated API. 3782 */ 3783 inject(dest, wrapperFuncs) { 3784 this.rootSchema.inject(dest, wrapperFuncs); 3785 }, 3786 3787 /** 3788 * Normalize `obj` according to the loaded schema for `typeName`. 3789 * 3790 * @param {object} obj The object to normalize against the schema. 3791 * @param {string} typeName The name in the format namespace.propertyname 3792 * @param {object} context An implementation of Context. Any validation errors 3793 * are reported to the given context. 3794 * @returns {object} The normalized object. 3795 */ 3796 normalize(obj, typeName, context) { 3797 return this.rootSchema.normalize(obj, typeName, context); 3798 }, 3799 3800 /** 3801 * Validate and normalize the arguments for an API request originated 3802 * from the webIDL API bindings. 3803 * 3804 * This provides for calls originating through WebIDL the parameters 3805 * validation and normalization guarantees that the ext-APINAMESPACE.js 3806 * scripts expects (what InjectionContext does for the regular bindings). 3807 * 3808 * @param {object} extContext 3809 * @param {string} apiNamespace 3810 * @param {string} apiName 3811 * @param {Array<any>} args 3812 * 3813 * @returns {Array<any>} Normalized arguments array. 3814 */ 3815 checkParameters(extContext, apiNamespace, apiName, args) { 3816 const apiSchema = this.getNamespace(apiNamespace)?.get(apiName); 3817 if (!apiSchema) { 3818 throw new Error(`API Schema not found for ${apiNamespace}.${apiName}`); 3819 } 3820 3821 return apiSchema.checkParameters( 3822 args, 3823 this.paramsValidationContexts.get(extContext) 3824 ); 3825 }, 3826}; 3827