author | Mike Cooper <mcooper@mozilla.com> |
Thu, 10 Aug 2017 10:22:32 -0700 | |
changeset 647786 | 84d95cf021e3a4acf10bb20210f06cd7533d64c0 |
parent 647785 | 1d38626ba9686d119e489db538ce84f8f9854217 |
child 726633 | bb1bb29a99cc216adfd685836f2b295b76503ec6 |
push id | 74539 |
push user | bmo:mkelly@mozilla.com |
push date | Wed, 16 Aug 2017 22:39:32 +0000 |
reviewers | Gijs |
bugs | 1388823 |
milestone | 57.0a1 |
--- a/browser/base/content/test/static/browser_all_files_referenced.js +++ b/browser/base/content/test/static/browser_all_files_referenced.js @@ -113,18 +113,19 @@ var whitelist = [ {file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/intl.properties", platforms: ["linux", "macosx"]}, {file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/platformKeys.properties", platforms: ["linux", "macosx"]}, // browser/extensions/pdfjs/content/web/viewer.js#7450 {file: "resource://pdf.js/web/debugger.js"}, - // Needed by Normandy - {file: "resource://gre/modules/IndexedDB.jsm"}, + // These are used in content processes. They are actually referenced. + {file: "resource://shield-recipe-client-content/shield-content-frame.js"}, + {file: "resource://shield-recipe-client-content/shield-content-process.js"}, // New L10n API that is not yet used in production {file: "resource://gre/modules/Localization.jsm"}, // Starting from here, files in the whitelist are bugs that need fixing. // Bug 1339420 {file: "chrome://branding/content/icon128.png"}, // Bug 1339424 (wontfix?)
--- a/browser/extensions/shield-recipe-client/bootstrap.js +++ b/browser/extensions/shield-recipe-client/bootstrap.js @@ -1,50 +1,97 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {utils: Cu} = Components; +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ShieldRecipeClient", "resource://shield-recipe-client/lib/ShieldRecipeClient.jsm"); +const DEFAULT_PREFS = { + "extensions.shield-recipe-client.api_url": "https://normandy.cdn.mozilla.net/api/v1", + "extensions.shield-recipe-client.dev_mode": false, + "extensions.shield-recipe-client.enabled": true, + "extensions.shield-recipe-client.startup_delay_seconds": 300, + "extensions.shield-recipe-client.logging.level": Log.Level.Warn, + "extensions.shield-recipe-client.user_id": "", + "extensions.shield-recipe-client.run_interval_seconds": 86400, // 24 hours + "extensions.shield-recipe-client.first_run": true, + "extensions.shield-recipe-client.shieldLearnMoreUrl": ( + "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/shield" + ), + "app.shield.optoutstudies.enabled": AppConstants.MOZ_DATA_REPORTING, +}; + this.install = function() {}; -this.startup = async function() { - await ShieldRecipeClient.startup(); +this.startup = function() { + // Initialize preference defaults before anything else happens. + const prefBranch = Services.prefs.getDefaultBranch(""); + for (const [name, value] of Object.entries(DEFAULT_PREFS)) { + switch (typeof value) { + case "string": + prefBranch.setCharPref(name, value); + break; + case "number": + prefBranch.setIntPref(name, value); + break; + case "boolean": + prefBranch.setBoolPref(name, value); + break; + default: + throw new Error(`Invalid default preference type ${typeof value}`); + } + } + + ShieldRecipeClient.startup(); }; this.shutdown = function(data, reason) { ShieldRecipeClient.shutdown(reason); // Unload add-on modules. We don't do this in ShieldRecipeClient so that // modules are not unloaded accidentally during tests. const log = LogManager.getLogger("bootstrap"); - const modules = [ + let modules = [ "lib/ActionSandboxManager.jsm", + "lib/Addons.jsm", + "lib/AddonStudies.jsm", "lib/CleanupManager.jsm", "lib/ClientEnvironment.jsm", "lib/FilterExpressions.jsm", "lib/EventEmitter.jsm", "lib/Heartbeat.jsm", "lib/LogManager.jsm", "lib/NormandyApi.jsm", "lib/NormandyDriver.jsm", "lib/PreferenceExperiments.jsm", "lib/RecipeRunner.jsm", "lib/Sampling.jsm", "lib/SandboxManager.jsm", + "lib/ShieldPreferences.jsm", "lib/ShieldRecipeClient.jsm", "lib/Storage.jsm", + "lib/Uptake.jsm", "lib/Utils.jsm", - ]; + ].map(m => `resource://shield-recipe-client/${m}`); + modules = modules.concat([ + "AboutPages.jsm", + ].map(m => `resource://shield-recipe-client-content/${m}`)); + modules = modules.concat([ + "mozjexl.js", + ].map(m => `resource://shield-recipe-client-vendor/${m}`)); + for (const module of modules) { log.debug(`Unloading ${module}`); - Cu.unload(`resource://shield-recipe-client/${module}`); + Cu.unload(module); } }; this.uninstall = function() {};
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/content/AboutPages.jsm @@ -0,0 +1,218 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { interfaces: Ci, results: Cr, manager: Cm, utils: Cu } = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter( + this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm", +); +XPCOMUtils.defineLazyModuleGetter( + this, "AddonStudies", "resource://shield-recipe-client/lib/AddonStudies.jsm", +); + +this.EXPORTED_SYMBOLS = ["AboutPages"]; + +const SHIELD_LEARN_MORE_URL_PREF = "extensions.shield-recipe-client.shieldLearnMoreUrl"; + +// Due to bug 1051238 frame scripts are cached forever, so we can't update them +// as a restartless add-on. The Math.random() is the work around for this. +const PROCESS_SCRIPT = ( + `resource://shield-recipe-client-content/shield-content-process.js?${Math.random()}` +); +const FRAME_SCRIPT = ( + `resource://shield-recipe-client-content/shield-content-frame.js?${Math.random()}` +); + +/** + * Class for managing an about: page that Shield provides. Adapted from + * browser/extensions/pocket/content/AboutPocket.jsm. + * + * @implements nsIFactory + * @implements nsIAboutModule + */ +class AboutPage { + constructor({chromeUrl, aboutHost, classId, description, uriFlags}) { + this.chromeUrl = chromeUrl; + this.aboutHost = aboutHost; + this.classId = Components.ID(classId); + this.description = description; + this.uriFlags = uriFlags; + } + + getURIFlags() { + return this.uriFlags; + } + + newChannel(uri, loadInfo) { + const newURI = Services.io.newURI(this.chromeUrl); + const channel = Services.io.newChannelFromURIWithLoadInfo(newURI, loadInfo); + channel.originalURI = uri; + + if (this.uriFlags & Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT) { + const principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {}); + channel.owner = principal; + } + return channel; + } + + createInstance(outer, iid) { + if (outer !== null) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return this.QueryInterface(iid); + } + + /** + * Register this about: page with XPCOM. This must be called once in each + * process (parent and content) to correctly initialize the page. + */ + register() { + Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory( + this.classId, + this.description, + `@mozilla.org/network/protocol/about;1?what=${this.aboutHost}`, + this, + ); + } + + /** + * Unregister this about: page with XPCOM. This must be called before the + * add-on is cleaned up if the page has been registered. + */ + unregister() { + Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory(this.classId, this); + } +} +AboutPage.prototype.QueryInterface = XPCOMUtils.generateQI([Ci.nsIAboutModule]); + +/** + * The module exported by this file. + */ +this.AboutPages = { + async init() { + // Load scripts in content processes and tabs + Services.ppmm.loadProcessScript(PROCESS_SCRIPT, true); + Services.mm.loadFrameScript(FRAME_SCRIPT, true); + + // Register about: pages and their listeners + this.aboutStudies.register(); + this.aboutStudies.registerParentListeners(); + + CleanupManager.addCleanupHandler(() => { + // Stop loading processs scripts and notify existing scripts to clean up. + Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT); + Services.ppmm.broadcastAsyncMessage("Shield:ShuttingDown"); + Services.mm.removeDelayedFrameScript(FRAME_SCRIPT); + Services.mm.broadcastAsyncMessage("Shield:ShuttingDown"); + + // Clean up about pages + this.aboutStudies.unregisterParentListeners(); + this.aboutStudies.unregister(); + }); + }, +}; + +/** + * about:studies page for displaying in-progress and past Shield studies. + * @type {AboutPage} + * @implements {nsIMessageListener} + */ +XPCOMUtils.defineLazyGetter(this.AboutPages, "aboutStudies", () => { + const aboutStudies = new AboutPage({ + chromeUrl: "resource://shield-recipe-client-content/about-studies/about-studies.html", + aboutHost: "studies", + classId: "{6ab96943-a163-482c-9622-4faedc0e827f}", + description: "Shield Study Listing", + uriFlags: ( + Ci.nsIAboutModule.ALLOW_SCRIPT + | Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT + | Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD + ), + }); + + // Extra methods for about:study-specific behavior. + Object.assign(aboutStudies, { + /** + * Register listeners for messages from the content processes. + */ + registerParentListeners() { + Services.mm.addMessageListener("Shield:GetStudyList", this); + Services.mm.addMessageListener("Shield:RemoveStudy", this); + Services.mm.addMessageListener("Shield:OpenOldDataPreferences", this); + }, + + /** + * Unregister listeners for messages from the content process. + */ + unregisterParentListeners() { + Services.mm.removeMessageListener("Shield:GetStudyList", this); + Services.mm.removeMessageListener("Shield:RemoveStudy", this); + Services.mm.removeMessageListener("Shield:OpenOldDataPreferences", this); + }, + + /** + * Dispatch messages from the content process to the appropriate handler. + * @param {Object} message + * See the nsIMessageListener documentation for details about this object. + */ + receiveMessage(message) { + switch (message.name) { + case "Shield:GetStudyList": + this.sendStudyList(message.target); + break; + case "Shield:RemoveStudy": + this.removeStudy(message.data); + break; + case "Shield:OpenOldDataPreferences": + this.openOldDataPreferences(); + break; + } + }, + + /** + * Fetch a list of studies from storage and send it to the process that + * requested them. + * @param {<browser>} target + * XUL <browser> element for the tab containing the about:studies page + * that requested a study list. + */ + async sendStudyList(target) { + try { + target.messageManager.sendAsyncMessage("Shield:ReceiveStudyList", { + studies: await AddonStudies.getAll(), + }); + } catch (err) { + // The child process might be gone, so no need to throw here. + Cu.reportError(err); + } + }, + + /** + * Disable an active study and remove its add-on. + * @param {String} studyName + */ + async removeStudy(recipeId) { + await AddonStudies.stop(recipeId); + + // Update any open tabs with the new study list now that it has changed. + Services.mm.broadcastAsyncMessage("Shield:ReceiveStudyList", { + studies: await AddonStudies.getAll(), + }); + }, + + openOldDataPreferences() { + const browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + browserWindow.openAdvancedPreferences("dataChoicesTab", {origin: "aboutStudies"}); + }, + + getShieldLearnMoreHref() { + return Services.urlFormatter.formatURLPref(SHIELD_LEARN_MORE_URL_PREF); + }, + }); + + return aboutStudies; +});
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/content/about-studies/about-studies.css @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +:root { + --icon-background-color-1: #0A84FF; + --icon-background-color-2: #008EA4; + --icon-background-color-3: #ED00B5; + --icon-background-color-4: #058B00; + --icon-background-color-5: #A47F00; + --icon-background-color-6: #FF0039; + --icon-background-disabled-color: #737373; + --body-text-disabled-color: #737373; + --info-box-background-color: #D7D7DB; + --info-box-border-color: #98979C; + --study-status-active-color: #058B00; + --study-status-disabled-color: #737373; +} + +html, +body, +#app { + height: 100%; + width: 100%; +} + +button > .button-box { + padding-left: 10px; + padding-right: 10px; +} + +.about-studies-container { + display: flex; + flex-direction: row; + font-size: 1.25rem; + min-height: 100%; + width: 100%; +} + +#categories { + flex: 0 0; + margin: 0; + min-width: 200px; + padding: 40px 0 0; +} + +#categories .category { + align-items: center; + display: flex; + flex-direction: row; +} + +.main-content { + flex: 1; +} + +.info-box { + margin-bottom: 10px; + text-align: center; +} + +.info-box-content { + align-items: center; + background: var(--info-box-background-color); + border: 1px solid var(--info-box-border-color); + display: inline-flex; + padding: 10px 15px; +} + +.info-box-content > * { + margin-right: 10px; +} + +.info-box-content > *:last-child { + margin-right: 0; +} + +.study-list { + list-style-type: none; + margin: 0; + padding: 0; +} + +.study { + align-items: center; + border-bottom: 1px solid var(--in-content-border-color); + display: flex; + flex-direction: row; + padding: 10px; +} + +.study.disabled { + color: var(--body-text-disabled-color); +} + +.study .study-status { + color: var(--study-status-active-color); + font-weight: bold; +} + +.study.disabled .study-status { + color: var(--study-status-disabled-color); +} + +.study:last-child { + border-bottom: none; +} + +.study > * { + margin-right: 15px; +} + +.study > *:last-child { + margin-right: 0; +} + +.study-icon { + color: #FFF; + flex: 0 0 40px; + font-size: 26px; + height: 40px; + line-height: 40px; + text-align: center; + text-transform: capitalize; +} + +.study:nth-child(6n+0) .study-icon { + background: var(--icon-background-color-1); +} + +.study:nth-child(6n+1) .study-icon { + background: var(--icon-background-color-2); +} + +.study:nth-child(6n+2) .study-icon { + background: var(--icon-background-color-3); +} + +.study:nth-child(6n+3) .study-icon { + background: var(--icon-background-color-4); +} + +.study:nth-child(6n+4) .study-icon { + background: var(--icon-background-color-5); +} + +.study:nth-child(6n+5) .study-icon { + background: var(--icon-background-color-6); +} + +.study.disabled .study-icon { + background: var(--icon-background-disabled-color); +} + +.study-details { + flex: 1; + overflow: hidden; +} + +.study-name { + font-weight: bold; + margin-bottom: 0.3em; +} + +.study-description { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + +.study-description > * { + margin-right: 5px; +} + +.study-description > *:last-child { + margin-right: 0; +} + +.study-actions { + flex: 0 0; +}
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/content/about-studies/about-studies.html @@ -0,0 +1,23 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>about:studies</title> + <link rel="stylesheet" href="chrome://global/skin/global.css"> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <link rel="stylesheet" href="resource://shield-recipe-client-content/about-studies/about-studies.css"> + </head> + <body> + <div id="app"></div> + <script src="resource://shield-recipe-client-vendor/React.js"></script> + <script src="resource://shield-recipe-client-vendor/ReactDOM.js"></script> + <script src="resource://shield-recipe-client-vendor/PropTypes.js"></script> + <script src="resource://shield-recipe-client-vendor/classnames.js"></script> + <script src="resource://shield-recipe-client-content/about-studies/common.js"></script> + <script src="resource://shield-recipe-client-content/about-studies/shield-studies.js"></script> + <script src="resource://shield-recipe-client-content/about-studies/about-studies.js"></script> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/content/about-studies/about-studies.js @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; +/* global classnames PropTypes r React ReactDOM ShieldStudies */ + +/** + * Mapping of pages displayed on the sidebar. Keys are the value used in the + * URL hash to identify the current page. + * + * Pages will appear in the sidebar in the order they are defined here. If the + * URL doesn't contain a hash, the first page will be displayed in the content area. + */ +const PAGES = new Map([ + ["shieldStudies", { + name: "Shield Studies", + component: ShieldStudies, + icon: "resource://shield-recipe-client-content/about-studies/img/shield-logo.png", + }], +]); + +/** + * Handle basic layout and routing within about:studies. + */ +class AboutStudies extends React.Component { + constructor(props) { + super(props); + + let hash = new URL(window.location).hash.slice(1); + if (!PAGES.has(hash)) { + hash = "shieldStudies"; + } + + this.state = { + currentPageId: hash, + }; + + this.handleEvent = this.handleEvent.bind(this); + } + + componentDidMount() { + window.addEventListener("hashchange", this); + } + + componentWillUnmount() { + window.removeEventListener("hashchange", this); + } + + handleEvent(event) { + const newHash = new URL(event.newURL).hash.slice(1); + if (PAGES.has(newHash)) { + this.setState({currentPageId: newHash}); + } + } + + render() { + const currentPageId = this.state.currentPageId; + const pageEntries = Array.from(PAGES.entries()); + const currentPage = PAGES.get(currentPageId); + + return ( + r("div", {className: "about-studies-container"}, + r(Sidebar, {}, + pageEntries.map(([id, page]) => ( + r(SidebarItem, { + key: id, + pageId: id, + selected: id === currentPageId, + page, + }) + )), + ), + r(Content, {}, + currentPage && r(currentPage.component) + ), + ) + ); + } +} + +class Sidebar extends React.Component { + render() { + return r("ul", {id: "categories"}, this.props.children); + } +} +Sidebar.propTypes = { + children: PropTypes.node, +}; + +class SidebarItem extends React.Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + window.location = `#${this.props.pageId}`; + } + + render() { + const { page, selected } = this.props; + return ( + r("li", { + className: classnames("category", {selected}), + onClick: this.handleClick, + }, + page.icon && r("img", {className: "category-icon", src: page.icon}), + r("span", {className: "category-name"}, page.name), + ) + ); + } +} +SidebarItem.propTypes = { + pageId: PropTypes.string.isRequired, + page: PropTypes.shape({ + icon: PropTypes.string, + name: PropTypes.string.isRequired, + }).isRequired, + selected: PropTypes.bool, +}; + +class Content extends React.Component { + render() { + return ( + r("div", {className: "main-content"}, + r("div", {className: "content-box"}, + this.props.children, + ), + ) + ); + } +} +Content.propTypes = { + children: PropTypes.node, +}; + +ReactDOM.render( + r(AboutStudies), + document.getElementById("app"), +);
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/content/about-studies/common.js @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; +/* eslint-disable no-unused-vars */ +/* global PropTypes React */ + +/** + * Shorthand for creating elements (to avoid using a JSX preprocessor) + */ +const r = React.createElement; + +/** + * Information box used at the top of listings. + */ +window.InfoBox = class InfoBox extends React.Component { + render() { + return ( + r("div", {className: "info-box"}, + r("div", {className: "info-box-content"}, + this.props.children, + ), + ) + ); + } +}; +window.InfoBox.propTypes = { + children: PropTypes.node, +}; + +/** + * Button using in-product styling. + */ +window.FxButton = class FxButton extends React.Component { + render() { + return ( + r("button", Object.assign({}, this.props, {children: undefined}), + r("div", {className: "button-box"}, + this.props.children, + ), + ) + ); + } +}; +window.FxButton.propTypes = { + children: PropTypes.node, +}; + +/** + * Wrapper class for a value that is provided by the frame script. + * + * Emits a "GetRemoteValue:{name}" page event on load to fetch the initial + * value, and listens for "ReceiveRemoveValue:{name}" page callbacks to receive + * the value when it updates. + * + * @example + * const myRemoteValue = new RemoteValue("MyValue", 5); + * class MyComponent extends React.Component { + * constructor(props) { + * super(props); + * this.state = { + * myValue: null, + * }; + * } + * + * componentWillMount() { + * myRemoteValue.subscribe(this); + * } + * + * componentWillUnmount() { + * myRemoteValue.unsubscribe(this); + * } + * + * receiveRemoteValue(name, value) { + * this.setState({myValue: value}); + * } + * + * render() { + * return r("div", {}, this.state.myValue); + * } + * } + */ +class RemoteValue { + constructor(name, defaultValue = null) { + this.name = name; + this.handlers = []; + this.value = defaultValue; + + document.addEventListener(`ReceiveRemoteValue:${this.name}`, this); + sendPageEvent(`GetRemoteValue:${this.name}`); + } + + /** + * Subscribe to this value as it updates. Handlers are called with the current + * value immediately after subscribing. + * @param {Object} handler + * Object with a receiveRemoteValue(name, value) method that is called with + * the name and value of this RemoteValue when it is updated. + */ + subscribe(handler) { + this.handlers.push(handler); + handler.receiveRemoteValue(this.name, this.value); + } + + /** + * Remove a previously-registered handler. + * @param {Object} handler + */ + unsubscribe(handler) { + this.handlers = this.handlers.filter(h => h !== handler); + } + + handleEvent(event) { + this.value = event.detail; + for (const handler of this.handlers) { + handler.receiveRemoteValue(this.name, this.value); + } + } +} + +/** + * Collection of RemoteValue instances used within the page. + */ +const remoteValues = { + StudyList: new RemoteValue("StudyList", []), + ShieldLearnMoreHref: new RemoteValue("ShieldLearnMoreHref", ""), +}; + +/** + * Dispatches a page event to the privileged frame script for this tab. + * @param {String} action + * @param {Object} data + */ +function sendPageEvent(action, data) { + const event = new CustomEvent("ShieldPageEvent", {bubbles: true, detail: {action, data}}); + document.dispatchEvent(event); +}
new file mode 100644 index 0000000000000000000000000000000000000000..e8539268cedca719b8f694b5ba7845cd27c1974c GIT binary patch literal 5426 zc$@(;70v32P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU`)Ja4^RCwB?T4|6R<#~Skn){xe-C6A+ z?a89m#btw7aY(Wg8B*{Ar((NIs_ZzG*o3$$m4u{<q+GblDZ`HhjE%wN3p)-Kmk}5c z{sCC#(BYMYCD6Ge?P`zNYj$_;>7G9FzF*HQS}k!Ar$wXQ+3By}<9VO=dB2wAI1czY zb?Ow_+uL#U=n({iA<WFoAel^}zrPPZ`q7VY!;Lp$d1(nFBfId6=YN5=);0_d4&dOy zgV?p}Htp44y}D`ZmaRQwnX#dX@$qZY>GZ(ZSY}v}WqVUk&-=9Ag}%PtbDK8xoE;q< zz4WQOJ~i>i>#v*p_U*&TlP57dH;2!C_H#IJ;1whiNxc2`Tln%<zJiOHi<q9CMpsuC z^7%Y=@7^tJyOHeSL&a4GJLK_rpeQo@em^aifT}3)d3|u;_$MYN`fbY^$YjO_Xu-jW ziOU1?b3gAdmCNmC&zu$am~|6#v%f<$8smHL?uiqf{y^Z5PMvxWM~)mtEE>gg|MlEV ztyaD8)?06%pUq}3gd^b#nTweV^!$w9=c~!Gf|iyRWM{HaR7KcC)?IBbq-F*<3LNbt zM~=X<EZ_3-N<SGmkV>Wc-hS)NVXx0OP%c-7mU6kI&*wv>T1Bl^Log6Py<P`@M#~9< zn`_dAcqFX{S(e4;&9hu8LDe)Ai$##Zl=UDAbWH(Upf%4@KrZarv*-QA#l^8uC^Qz2 z$1;kd=zI3;#kq6maMMjUt#u%q&0>6f{9~`b_S%2Y^$wrk@1vVx=sKdY7;2R&Jy3;2 zhVaoeNRp&ggJrs=hmae#_?shU+ZH)!q3M%hn)I3bgGJX)V=a@eEz^Q&)#>{tW!wvX zet-hhl4?PrP^@fdZy&wq-h2Oc^XAR(c64;Cb>QgHqg$T-?srdBDY7v#m_OGvH*HQM znl>9ue^ga&7%P(l5T6@{fvRpmr-xT6HI&4uQ|dJYd>;5bD*Rp#JQM`F&2^d`p#}Ia zHkuu?$YDNg+ZHDWqE2h`_0q~R-LL5nJ@ijos20Z7)WCrQ2foaUwWZVI%WR-WRfVzq zxhzYlQ<SQD4Hbe;nGCE{b(E_HO696Bwp=#I_&R0FqSJ;|mk|mF;PFY&ynxRy5!6(1 zdOb2ZrBS3ca!i9t>w7f9x=dMyOpe$hLzEv-6ogIbwlwl9E8b_Hef9xz;HzsKSX#;r zgu`JGX<`nF3VB?}qiUEamMU&U%QY1Ey2yrFsFqEb20_S_MP|GKG9qBZ7dH{_vEWPS zWTc7uN&uC-j9N)SX@wxATOxCkS|M24<e&^MJLgr%8C9H|DUE{XC8sonopK=;CsN7I z%v{s7d({D|a^1FUf$5;%i_;e;@W$cy$T*-@lu@sdp*Ba79|m~CHd3v`SP_Cr%z%$h zE#_W(f^}%2IuxG;uN;J3^}r~pbSg0NUYG?hY6T6(iU(%N1HBA#P(j|XL>)*<4Vs4_ zOu>*92b(vgkf2Ofbw>o)>-8Gz93a|q*Z~fWO3<lM<V(dmVsvAq&x9wc!y7jcOp&4S zDj8mfKStm8>QKBkMUo&!2Ee8zO%;SRpzNa`{ct1`{)B}<ivdS230gFD8zuz>x*xT? zM9yjCm><;~IabtAFM6RD$c`EnHQB`wMw3V-$zJCG|Bi9b8K4570#+a!J_OP=T>Ix2 zq4>#Z#UglVf>~^W4NQ%2F2T}OP`n9tCKO2#u&dS$#QhqVBOFU2c98`W*_1_DQt7*v z_?_$_3T~xfw6l@qm_;e4@0vv)F7EGzKJO96GkpjOwwnxhl>=n9BMI!gOe4F*16ig2 zc?^mc-DG-Uz!C<qqqewK=tf5v#0J!f27R6&r96&8Z4sK|C#My`E*w3+Hf>WpL2^{8 zA%cvf@zY^BeC>y<k#nj+P;x}bxh9n>=-L#;-9S$BLQz#)uuUTi4rhYzriGn4al4}k z10}&ovN$EVan1M0j_BC41iQZ&c?6H&brks=WlAw9VycLcpk87bx8x<U9{)m;WcP$5 zjFlY9mZ^wmL;3(BvY=dt8()2$0|CF^YBnYTqefo13rHe86mNov;C+fb|8cKP@jJf9 zuytY%U~|U}NO`*9tp#C9HHx_5u0elHyY8l&SFzpo9AZ2dQV^LE*HQ!Vj@vUB9Wn)o zqaTh&jCBrBG_6(R#edSO_Gm_pe^wN&;Us05t|6k&g|9)k45T9gbVrAw27tCmw`g!| zIKxiBjsB>zCL)f;bu-GE;^EdR*5_w7l@IrTrUc>p$O&hi1JniUa>GkSa$(UddvUYm ztcpFiU>p%eh7KCnRSNEgXdnD?gz5%V#2cvTHSKGz;fNmN=ED)ucLU{q?l|smZ1=*2 zo~*3O7ygizG1oajVoYaGh4)0G&6{`;PIYquW*Rn#ofJhUxwXLXs?ygsx8J;q*_Kc{ zWq^r55wLJXhgVkN!q=vNrJH#!Zn;qlwvZG8U#r37;IS|a1cSA84rrQYG*i`d#6^uP zPDy0Nb;`YVLDei~{(<BUZtB~MvGW;ZE{@^a?i;|6=T6$BI(H4J0TZQJ&f@dtdSXpC z&0wT8)R?%39f(B2m30otid+?J(x55kUQzW8<>(fjTl}2rW>)xb+hV6pkP^Uf`WC!# z;uVZ9okdT}Fm-TR538g>nX0?+u^kcdHS0-k5p!lm&#**^2|KQIjL=bC=Riv`nP&mT z)lS(EN>gB)nu<(3EeS?~Xk>0(P~l2+Z3{|)L6W&W(t()Vg}2A{Bj=n)YhV++aG8oD zOe+b)xIi29;}&rv!p&<zr*M12DjPP+)->xpDK4*bpu2nH0uLxno>vLCoIaa6Ek~H0 zaSgXu<+bIwkmC=^IxyI_jidqayP4OadzPpMIuVd*LL+u!gAMxIuymlwa1DZ3<-FCx zrHpV^O}52tWQ{MAuq<JCcz9`@12op<NF<4c7&Ts+1w3BTH&yDcHKs2|V3k|F<hYNt z#I<M!)OCR!U3a2HOV3h*^JW&IfFD66?s7Xpgz?K+*6pgek88<=q9ji45pGk1fF8>f zRpq&ZrHHH3XgcHJYi^wb)6-MgI1MuV02Xzu)4inI>s4~1Ob}8U1?ad%$4Pdt6?cxN zhi>iIiL;Z(P^M=smS)8a&=TmR8QH?j$|Aln@~?Pg-}A^Vt-w)S?bD3E)F^Hy2bqjE zD<tl8g0qJ|b7c*)SX)~gCN588*J+`VkzG?Szw%0hg&gvfRH9zO%1jPZm#b)PrD<!$ z?WmC5Of%~w174C3x_CfuWIKL5{zK8kW){YYn}N>w0M0EOC4CPJwSNp-+jpVcvmM8$ zeuvf|_n*}^;6jsrCMAveX$zB?IcypV)7<T&AW-IrlWC6DKl^8&S?>VNz4NwZ<%xl! zbt)9*`A_a0L4ICE&+t5wgNvl$X+l#}!5SXLi@Cx$j!nLY&R7b&uf2!LE`*cQ2N91G zjFx7^x}AwZEN6i0`Z~}Vb?1=3+WR&9=gF_5HJ%p5YFd_{N@}4b22E&w6U#;`wmGd( zJS#Mpm#H2Vp+cz!bEnUo&J7F>u61B^bhJc+{CK%i+0Hb|I|L7X^{;W=b=Tp4JMskh zzklv-{NugfL0@t^CdcM5n)x--w05qzf<k>!WPpY^43R!k>wf&~<!A7jyS{|oH{9cf zoHJm#CpTq=I%iY;W2!ltF<yA#c|7sNlL9(?&_s6e{nMxG_uO;uS_f{w{dSshD`%&s zrnd7IM@M@akAM3qymvB-RKJbZ8yAHt<>(uAUF_5R+ZTU^r@#0P#wRY}{>OLYa_J1L zcn)zI-sejO^0hg1#Cu7K10Ns!1gx?L-~8p@;HAtH@Ri$KQOHH?NFOv%*=4B3lSvwG zJZMjQ(cRTS@S&_MFXQ^_uOEN#!3WpR1Lx15Bb?RG^Tw3BxuwyNUc%_n!_a0V?ASU_ z!w0bgu^?->DPrPZo%<oau=8v9!T#^yXQ$u5uDvo^BtML*+lc0h<7kO&LzupQeb1f9 z96N(5@$95?iYWM$C{C7TO_pS~_E6y)1l4n+11J-ted3NmM1x7uXTqT{L+JfOhu#72 z53O+^9F8EJZa*@1VN67#T&|*bQx`h=dr&Hv@R81s^cQHR)f!#8ir-xNIa;b)ap2TX zaq|dKFHLZD+Y!cB$<a#-XVBBS3th1=T9qFB;@r33kb!Y61v}6Xk&Ziah$6NfF{&=l zBoJ`guwhU_DiIUWw;LjIWB0~)+1B-Pn&^e_^)A7u!aHqsDy&#Igtl~qxZOeiQi!OO z#yLq7%n|nmFu9Py-@Wu{csJ#c@P(;M+QJyOqY^bQT_h^@;JVH|uxcJ0zjP4c7(L5U zg~7~ryq-HMesEoy3c?`RMS>o&r^Er(smVs8(LyX9Kfw}xodaCB(MV)E5{(=;bVH~? z-W}`OK+#g_C|-)9UZMxfE<8-iH*per`aB56Wa!nVW^#4AM{ymfm<5dV-b++Gg9{7C zh@rfK&z+`Jx1H53Pe~=!R3PY1hoSl@fDzv+z4C@zB9S;j=iG-nz}QcvQs~~e@jy*% zDNq9=?(OYrM>4HpDdR_RJPbKdb(w=P%L8~_rH+Z!Iai1@3I_E+Of6<$PzC?V&iirv z^dZbw=MeNqP&X}czmU*;v*_m3%zRZ8FGo<E@gbH_&=L(1d|V+|E|;-m$BtL%#Kw&q z*E`VJ*-6c+6~mi{UkZ=}U>FglqO-jf{e5YgZ*?r4Os&oZYq~PC2pKMOcNSevf?t!7 zqi$R_^6-Ww96SFGSlOvG>A2IP*raUqVY*YLS%X+Ee=a33-_{aDIOrG7a>RL;JRXnl zzkK;JR#sNlJK*jZm(kbPcXD80@R$50(O~F7+pq0KC}m^rcoZw=qfo<D(HR}b-FX(0 z+u^OpI;mHCRjn3lB>^LS9vIJ^M35SsPGg(Xz`xBI6tbCWD_BAPLL9mCAw(%oovksk zDa_3)Pqu8`y7k3v+qRuA6bfs4(Fa;+W*QTd6PTNyLw|q&x0`z}#bOx)n>w*$TQ`<- z70e#o2+JS^<S|7T<;Zgb64KgnmlX4_qoP7ktr$gQXO=Kq&L9@|i9#208jT{Rj40H3 zc{6MNa3^YY13g`FB#F5=6I?H>UwV3bo)H0R^ow;4<Q5mPl*{2_CPUR8d9}N%>u8lA z#5Wkag^`=KqMZtM>Rk`AzwU$@GQ@6#P{WNy*%8+vXI0-URkApC>M(M(DFl5%Q9sS! zi+hO_I2f~UcOo~TVMDiy-i{X3YVJ_QZ*I11-TK@6?z``$NF;)e&JJ;|bAY8ZpFGU) zp8U|YL*HPJS)G>1krWA>Pu_7oY+n%*ucfj4egbN&Oa{pUE<$F*tp(o2W?aXpl3zUe z6rTF+gNU^H1;&L8=l23OT<k{*$enD#?6DSj<2r_W+lj$c=v0GD2FyOYMn)dxCdO}x zBGdySAJzw`iHWnleFG`cFu(WOKQwghi#f^)7wyt=0Xv2UaK~-epin8{(*9nQCxZwi z4Y3i$HNen8W7|rhYmK_{Vu?gu_blS6Ff82>6P@F@`rn@*dd?&<^*X^)*0E(Uh1Nt= zY}GS_Ir_KVw(I#@Zn^otFI+g!k{?r3lj2<Wim6hmta(qydT3%|V&jwF`qq1tg%E3> z08y<<Hvat8gE)R@9_fuf^xrpzQ11$AOGGiI>~5Vn&6$hYC?K@H)4;968wAAn-iVI; z`4&vP(u;h-z~;?iTtC!9ddv_REK;%3GKI$;du&T@Z|`JtTD%f5Ydi53ClAq8;$7m~ zi92`i{_5hwg4+#s9ZVc|-+2qR+?c@Z<r>cZbO7^5((r{XVyBvjKMkY=J$HhWcq5lZ z8r=O<HB^O0{b}xS2Qn}82;+PEd>HQO0Pk1vD=03G1**wU|LLb6WaAk|&EjpI>oUOU zx%%)@)Oqgx$+JKC>6z1KKg(}#7?=G1fH$;Wc<mh=AI&15gt6heB9b>wBivmSkRt|# zWP0KE1S#Tm8ggtg<8m|1ooL6xxey$!hM{X>80u~lj<S5={=)&=vv=?J9(dq^FR^_b zxT_D66>A;f7iw1@>?yzMBpItd`skyhOG``JTarnU;t0*Al5FG9iT81M^a57qWW-xE zB(BLL+`EiGx(rWDhffKjp7*1i4UzFN8g9a<lr6;D91QoT(L++DSgKOwEit^)GmuOs z-g@}qf7wOR$Nc<!V@Gc7!FH{2VD;7L)$d^&xSN@oPCWheGlyoTrw8Nlgu64%Lw3-I z>A3|QK5+(PnMIVB`TeqnNZLfGqYR(NkIHNqg?UXdRWwP8vNMVvqRNopCos<q(<BE9 zg*-YoY&fy=)}1?VzWJ6Kzuac{eAp4^vF}O;ICa18V0z7Fv#}?hc;e03Y<5eMBH-?n z*_4d{WrcKpb^&7(vzVODQPb0*S7d}EBnhH2jjSqKlOeRHVn{?nsMcu4)(ycn2I-yo zg?WBYee{v9fBlX`JW*xXd|2eKwC5u`AT{0_<cS&HeDgP$o16Rb=;*P}^S#_$#QmgK z!l9tpepxOQF}JXS`K3H%D1byXK;lj&Y3!rH#1wF`E%${7O9MXdy8Esthc|D2;MQA5 z1Or~(aj!m_N<qa(J;(u8guLj!eV@8Nm1=$Y(4j-$&n@NxtYrDUSAnz?Gl-AIs{V}~ z=<804`)V}Z@b5}heHE%Tig2-5BJI_?LK^4uTefU@iK&vG!49y-`Y6Xocc768&J-6K ziL2+*?dcbf9zOEVm&V8cfj;wa5qf<-5!p($=Du%kyoGPl9~;PcPo-KaJ9pml$i991 zzFDbO?a9lNVvPNLu=#&Ga7CvRX(w1!`v(RdP9&56PV@6$W@obZ6TF7FDY681_gEE_ zN+m&sEKx}9o*En&_yJ8;-`~)&A<wU$@+<kq4z%=tHsud<KyvqHIUD?sH7N4A;o;$L z&`j|Sg2DFbsp)+Siwn0dEG&Lp(^M;+ZaYG}eUN1Ot6rb?4F5BpsmUoJ9+S*yl7v6> c_#XiV0CwRC6nI;bQ~&?~07*qoM6N<$g4ErHNdN!<
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/content/about-studies/shield-studies.js @@ -0,0 +1,148 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; +/* global classnames FxButton InfoBox PropTypes r React remoteValues sendPageEvent */ + +window.ShieldStudies = class ShieldStudies extends React.Component { + constructor(props) { + super(props); + this.state = { + learnMoreHref: null, + }; + } + + componentDidMount() { + remoteValues.ShieldLearnMoreHref.subscribe(this); + } + + componentWillUnmount() { + remoteValues.ShieldLearnMoreHref.unsubscribe(this); + } + + receiveRemoteValue(name, value) { + this.setState({ + learnMoreHref: value, + }); + } + + render() { + return ( + r("div", {}, + r(InfoBox, {}, + r("span", {}, "What's this? Firefox may install and run studies from time to time."), + r("a", {id: "shield-studies-learn-more", href: this.state.learnMoreHref}, "Learn more"), + r(UpdatePreferencesButton, {}, "Update Preferences"), + ), + r(StudyList), + ) + ); + } +}; + +class UpdatePreferencesButton extends React.Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + sendPageEvent("NavigateToDataPreferences"); + } + + render() { + return r( + FxButton, + Object.assign({ + id: "shield-studies-update-preferences", + onClick: this.handleClick, + }, this.props), + ); + } +} + +class StudyList extends React.Component { + constructor(props) { + super(props); + this.state = { + studies: [], + }; + } + + componentDidMount() { + remoteValues.StudyList.subscribe(this); + } + + componentWillUnmount() { + remoteValues.StudyList.unsubscribe(this); + } + + receiveRemoteValue(name, value) { + const studies = value.slice(); + + // Sort by active status, then by start date descending. + studies.sort((a, b) => { + if (a.active !== b.active) { + return a.active ? -1 : 1; + } + return b.studyStartDate - a.studyStartDate; + }); + + this.setState({studies}); + } + + render() { + return ( + r("ul", {className: "study-list"}, + this.state.studies.map(study => ( + r(StudyListItem, {key: study.name, study}) + )) + ) + ); + } +} + +class StudyListItem extends React.Component { + constructor(props) { + super(props); + this.handleClickRemove = this.handleClickRemove.bind(this); + } + + handleClickRemove() { + sendPageEvent("RemoveStudy", this.props.study.recipeId); + } + + render() { + const study = this.props.study; + return ( + r("li", { + className: classnames("study", {disabled: !study.active}), + "data-study-name": study.name, + }, + r("div", {className: "study-icon"}, + study.name.slice(0, 1) + ), + r("div", {className: "study-details"}, + r("div", {className: "study-name"}, study.name), + r("div", {className: "study-description", title: study.description}, + r("span", {className: "study-status"}, study.active ? "Active" : "Complete"), + r("span", {}, "\u2022"), // • + r("span", {}, study.description), + ), + ), + r("div", {className: "study-actions"}, + study.active && + r(FxButton, {className: "remove-button", onClick: this.handleClickRemove}, "Remove"), + ), + ) + ); + } +} +StudyListItem.propTypes = { + study: PropTypes.shape({ + recipeId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + active: PropTypes.boolean, + description: PropTypes.string.isRequired, + }).isRequired, +};
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/content/shield-content-frame.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * Listen for DOM events bubbling up from the about:studies page, and perform + * privileged actions in response to them. If we need to do anything that the + * content process can't handle (such as reading IndexedDB), we send a message + * to the parent process and handle it there. + * + * This file is loaded as a frame script. It will be loaded once per tab that + * is opened. + */ + +/* global content addMessageListener removeMessageListener sendAsyncMessage */ + +const { utils: Cu } = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const frameGlobal = {}; +XPCOMUtils.defineLazyModuleGetter( + frameGlobal, "AboutPages", "resource://shield-recipe-client-content/AboutPages.jsm", +); + +const USE_OLD_PREF_ORGANIZATION_PREF = "browser.preferences.useOldOrganization"; + +/** + * Handles incoming events from the parent process and about:studies. + * @implements nsIMessageListener + * @implements EventListener + */ +class ShieldFrameListener { + handleEvent(event) { + // Abort if the current page isn't about:studies. + if (!this.ensureTrustedOrigin()) { + return; + } + + // We waited until after we received an event to register message listeners + // in order to save resources for tabs that don't ever load about:studies. + addMessageListener("Shield:ShuttingDown", this); + addMessageListener("Shield:ReceiveStudyList", this); + + switch (event.detail.action) { + // Actions that require the parent process + case "GetRemoteValue:StudyList": + sendAsyncMessage("Shield:GetStudyList"); + break; + case "RemoveStudy": + sendAsyncMessage("Shield:RemoveStudy", event.detail.data); + break; + // Actions that can be performed in the content process + case "GetRemoteValue:ShieldLearnMoreHref": + this.triggerPageCallback( + "ReceiveRemoteValue:ShieldLearnMoreHref", + frameGlobal.AboutPages.aboutStudies.getShieldLearnMoreHref() + ); + break; + case "NavigateToDataPreferences": + this.navigateToDataPreferences(); + break; + } + } + + /** + * Check that the current webpage's origin is about:studies. + * @return {Boolean} + */ + ensureTrustedOrigin() { + return content.document.documentURI.startsWith("about:studies"); + } + + /** + * Handle messages from the parent process. + * @param {Object} message + * See the nsIMessageListener docs. + */ + receiveMessage(message) { + switch (message.name) { + case "Shield:ReceiveStudyList": + this.triggerPageCallback("ReceiveRemoteValue:StudyList", message.data.studies); + break; + case "Shield:ShuttingDown": + this.onShutdown(); + break; + } + } + + /** + * Trigger an event to communicate with the unprivileged about: page. + * @param {String} type + * @param {Object} detail + */ + triggerPageCallback(type, detail) { + // Do not communicate with untrusted pages. + if (!this.ensureTrustedOrigin()) { + return; + } + + // Clone details and use the event class from the unprivileged context. + const event = new content.document.defaultView.CustomEvent(type, { + bubbles: true, + detail: Cu.cloneInto(detail, content.document.defaultView), + }); + content.document.dispatchEvent(event); + } + + onShutdown() { + removeMessageListener("Shield:SendStudyList", this); + removeMessageListener("Shield:ShuttingDown", this); + removeEventListener("Shield", this); + } + + navigateToDataPreferences() { + if (Services.prefs.getBoolPref(USE_OLD_PREF_ORGANIZATION_PREF)) { + sendAsyncMessage("Shield:OpenOldDataPreferences"); + } else { + content.location = "about:preferences#privacy-reports"; + } + } +} + +addEventListener("ShieldPageEvent", new ShieldFrameListener(), false, true);
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/content/shield-content-process.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * Registers about: pages provided by Shield, and listens for a shutdown event + * from the add-on before un-registering them. + * + * This file is loaded as a process script. It is executed once for each + * process, including the parent one. + */ + +const { utils: Cu } = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://shield-recipe-client-content/AboutPages.jsm"); + +class ShieldChildListener { + onStartup() { + Services.cpmm.addMessageListener("Shield:ShuttingDown", this, true); + AboutPages.aboutStudies.register(); + } + + onShutdown() { + AboutPages.aboutStudies.unregister(); + Services.cpmm.removeMessageListener("Shield:ShuttingDown", this); + + // Unload AboutPages.jsm in case the add-on is reinstalled and we need to + // load a new version of it. + Cu.unload("resource://shield-recipe-client-content/AboutPages.jsm"); + } + + receiveMessage(message) { + switch (message.name) { + case "Shield:ShuttingDown": + this.onShutdown(); + break; + } + } +} + +// Only register in content processes; the parent process handles registration +// separately. +if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { + const listener = new ShieldChildListener(); + listener.onStartup(); +}
--- a/browser/extensions/shield-recipe-client/install.rdf.in +++ b/browser/extensions/shield-recipe-client/install.rdf.in @@ -3,17 +3,17 @@ #filter substitution <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#"> <Description about="urn:mozilla:install-manifest"> <em:id>shield-recipe-client@mozilla.org</em:id> <em:type>2</em:type> <em:bootstrap>true</em:bootstrap> <em:unpack>false</em:unpack> - <em:version>55.1</em:version> + <em:version>65</em:version> <em:name>Shield Recipe Client</em:name> <em:description>Client to download and run recipes for SHIELD, Heartbeat, etc.</em:description> <em:multiprocessCompatible>true</em:multiprocessCompatible> <em:targetApplication> <Description> <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
--- a/browser/extensions/shield-recipe-client/jar.mn +++ b/browser/extensions/shield-recipe-client/jar.mn @@ -1,9 +1,13 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. [features/shield-recipe-client@mozilla.org] chrome.jar: -% resource shield-recipe-client %content/ - content/lib/ (./lib/*) - content/node_modules/jexl/ (./node_modules/jexl/*) - content/skin/ (skin/*) +% resource shield-recipe-client % + lib/ (./lib/*) + data/ (./data/*) + skin/ (skin/*) +% resource shield-recipe-client-content %content/ + content/ (./content/*) +% resource shield-recipe-client-vendor %vendor/ + vendor/ (./vendor/*)
--- a/browser/extensions/shield-recipe-client/lib/ActionSandboxManager.jsm +++ b/browser/extensions/shield-recipe-client/lib/ActionSandboxManager.jsm @@ -67,18 +67,14 @@ this.ActionSandboxManager = class extend async runAsyncCallback(callbackName, ...args) { const callbackWasRegistered = this.evalInSandbox(`asyncCallbacks.has("${callbackName}")`); if (!callbackWasRegistered) { log.debug(`Script did not register a callback with the name "${callbackName}"`); return undefined; } this.cloneIntoGlobal("callbackArgs", args); - try { - const result = await this.evalInSandbox(` - asyncCallbacks.get("${callbackName}")(sandboxedDriver, ...callbackArgs); - `); - return Cu.cloneInto(result, {}); - } catch (err) { - throw new Error(Cu.cloneInto(err.message, {})); - } + const result = await this.evalInSandbox(` + asyncCallbacks.get("${callbackName}")(sandboxedDriver, ...callbackArgs); + `); + return Cu.cloneInto(result, {}); } };
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/lib/AddonStudies.jsm @@ -0,0 +1,324 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * @typedef {Object} Study + * @property {Number} recipeId + * ID of the recipe that created the study. Used as the primary key of the + * study. + * @property {string} name + * Name of the study + * @property {string} description + * Description of the study and its intent. + * @property {boolean} active + * Is the study still running? + * @property {string} addonId + * Add-on ID for this particular study. + * @property {string} addonUrl + * URL that the study add-on was installed from. + * @property {string} addonVersion + * Study add-on version number + * @property {string} studyStartDate + * Date when the study was started. + * @property {Date} studyEndDate + * Date when the study was ended. + */ + +const {utils: Cu, interfaces: Ci} = Components; +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB", "resource://gre/modules/IndexedDB.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Addons", "resource://shield-recipe-client/lib/Addons.jsm"); +XPCOMUtils.defineLazyModuleGetter( + this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm" +); +XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.jsm"); + +Cu.importGlobalProperties(["fetch"]); /* globals fetch */ + +this.EXPORTED_SYMBOLS = ["AddonStudies"]; + +const DB_NAME = "shield"; +const STORE_NAME = "addon-studies"; +const DB_OPTIONS = { + version: 1, + storage: "persistent", +}; +const STUDY_ENDED_TOPIC = "shield-study-ended"; +const log = LogManager.getLogger("addon-studies"); + +/** + * Create a new connection to the database. + */ +function openDatabase() { + return IndexedDB.open(DB_NAME, DB_OPTIONS, db => { + db.createObjectStore(STORE_NAME, { + keyPath: "recipeId", + }); + }); +} + +/** + * Cache the database connection so that it is shared among multiple operations. + */ +let databasePromise; +async function getDatabase() { + if (!databasePromise) { + databasePromise = openDatabase(); + } + return databasePromise; +} + +/** + * Get a transaction for interacting with the study store. + * + * NOTE: Methods on the store returned by this function MUST be called + * synchronously, otherwise the transaction with the store will expire. + * This is why the helper takes a database as an argument; if we fetched the + * database in the helper directly, the helper would be async and the + * transaction would expire before methods on the store were called. + */ +function getStore(db) { + return db.objectStore(STORE_NAME, "readwrite"); +} + +/** + * Mark a study object as having ended. Modifies the study in-place. + * @param {IDBDatabase} db + * @param {Study} study + */ +async function markAsEnded(db, study) { + study.active = false; + study.studyEndDate = new Date(); + await getStore(db).put(study); + Services.obs.notifyObservers(study, STUDY_ENDED_TOPIC); +} + +this.AddonStudies = { + /** + * Test wrapper that temporarily replaces the stored studies with the given + * ones. The original stored studies are restored upon completion. + * + * This is defined here instead of in test code since it needs to access the + * getDatabase, which we don't expose to avoid outside modules relying on the + * type of storage used for studies. + * + * @param {Array} [studies=[]] + */ + withStudies(studies = []) { + return function wrapper(testFunction) { + return async function wrappedTestFunction(...args) { + const oldStudies = await AddonStudies.getAll(); + let db = await getDatabase(); + await AddonStudies.clear(); + for (const study of studies) { + await getStore(db).add(study); + } + + try { + await testFunction(...args, studies); + } finally { + db = await getDatabase(); // Re-acquire in case the test closed the connection. + await AddonStudies.clear(); + for (const study of oldStudies) { + await getStore(db).add(study); + } + + await AddonStudies.close(); + } + }; + }; + }, + + async init() { + // If an active study's add-on has been removed since we last ran, stop the + // study. + const activeStudies = (await this.getAll()).filter(study => study.active); + const db = await getDatabase(); + for (const study of activeStudies) { + const addon = await AddonManager.getAddonByID(study.addonId); + if (!addon) { + await markAsEnded(db, study); + } + } + await this.close(); + + // Listen for add-on uninstalls so we can stop the corresponding studies. + AddonManager.addAddonListener(this); + CleanupManager.addCleanupHandler(() => { + AddonManager.removeAddonListener(this); + }); + }, + + /** + * If a study add-on is uninstalled, mark the study as having ended. + * @param {Addon} addon + */ + async onUninstalled(addon) { + const activeStudies = (await this.getAll()).filter(study => study.active); + const matchingStudy = activeStudies.find(study => study.addonId === addon.id); + if (matchingStudy) { + // Use a dedicated DB connection instead of the shared one so that we can + // close it without fear of affecting other users of the shared connection. + const db = await openDatabase(); + await markAsEnded(db, matchingStudy); + await db.close(); + } + }, + + /** + * Remove all stored studies. + */ + async clear() { + const db = await getDatabase(); + await getStore(db).clear(); + }, + + /** + * Close the current database connection if it is open. + */ + async close() { + if (databasePromise) { + const promise = databasePromise; + databasePromise = null; + const db = await promise; + await db.close(); + } + }, + + /** + * Test whether there is a study in storage for the given recipe ID. + * @param {Number} recipeId + * @returns {Boolean} + */ + async has(recipeId) { + const db = await getDatabase(); + const study = await getStore(db).get(recipeId); + return !!study; + }, + + /** + * Fetch a study from storage. + * @param {Number} recipeId + * @return {Study} + */ + async get(recipeId) { + const db = await getDatabase(); + return getStore(db).get(recipeId); + }, + + /** + * Fetch all studies in storage. + * @return {Array<Study>} + */ + async getAll() { + const db = await getDatabase(); + return getStore(db).getAll(); + }, + + /** + * Start a new study. Installs an add-on and stores the study info. + * @param {Object} options + * @param {Number} options.recipeId + * @param {String} options.name + * @param {String} options.description + * @param {String} options.addonUrl + * @throws + * If any of the required options aren't given. + * If a study for the given recipeID already exists in storage. + * If add-on installation fails. + */ + async start({recipeId, name, description, addonUrl}) { + if (!recipeId || !name || !description || !addonUrl) { + throw new Error("Required arguments (recipeId, name, description, addonUrl) missing."); + } + + const db = await getDatabase(); + if (await getStore(db).get(recipeId)) { + throw new Error(`A study for recipe ${recipeId} already exists.`); + } + + const addonFile = await this.downloadAddonToTemporaryFile(addonUrl); + const install = await AddonManager.getInstallForFile(addonFile); + const study = { + recipeId, + name, + description, + addonId: install.addon.id, + addonVersion: install.addon.version, + addonUrl, + active: true, + studyStartDate: new Date(), + }; + + try { + await getStore(db).add(study); + await Addons.applyInstall(install, false); + return study; + } catch (err) { + await getStore(db).delete(recipeId); + throw err; + } finally { + Services.obs.notifyObservers(addonFile, "flush-cache-entry"); + await OS.File.remove(addonFile.path); + } + }, + + /** + * Download a remote add-on and store it in a temporary nsIFile. + * @param {String} addonUrl + * @returns {nsIFile} + */ + async downloadAddonToTemporaryFile(addonUrl) { + const response = await fetch(addonUrl); + if (!response.ok) { + throw new Error(`Download for ${addonUrl} failed: ${response.status} ${response.statusText}`); + } + + // Create temporary file to store add-on. + const path = OS.Path.join(OS.Constants.Path.tmpDir, "study.xpi"); + const {file, path: uniquePath} = await OS.File.openUnique(path); + + // Write the add-on to the file + try { + const xpiArrayBufferView = new Uint8Array(await response.arrayBuffer()); + await file.write(xpiArrayBufferView); + } finally { + await file.close(); + } + + return new FileUtils.File(uniquePath); + }, + + /** + * Stop an active study, uninstalling the associated add-on. + * @param {Number} recipeId + * @throws + * If no study is found with the given recipeId. + * If the study is already inactive. + */ + async stop(recipeId) { + const db = await getDatabase(); + const study = await getStore(db).get(recipeId); + if (!study) { + throw new Error(`No study found for recipe ${recipeId}`); + } + if (!study.active) { + throw new Error(`Cannot stop study for recipe ${recipeId}; it is already inactive.`); + } + + await markAsEnded(db, study); + + try { + await Addons.uninstall(study.addonId); + } catch (err) { + log.warn(`Could not uninstall addon ${study.addonId} for recipe ${study.recipeId}:`, err); + } + }, +};
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/lib/Addons.jsm @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Extension", "resource://gre/modules/Extension.jsm"); +XPCOMUtils.defineLazyModuleGetter( + this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm" +); + +this.EXPORTED_SYMBOLS = ["Addons"]; + +/** + * SafeAddons store info about an add-on. They are single-depth + * objects to simplify cloning, and have no methods so they are safe + * to pass to sandboxes and filter expressions. + * + * @typedef {Object} SafeAddon + * @property {string} id + * Add-on id, such as "shield-recipe-client@mozilla.com" or "{4ea51ac2-adf2-4af8-a69d-17b48c558a12}" + * @property {Date} installDate + * @property {boolean} isActive + * @property {string} name + * @property {string} type + * "extension", "theme", etc. + * @property {string} version + */ + +this.Addons = { + /** + * Get information about an installed add-on by ID. + * + * @param {string} addonId + * @returns {SafeAddon?} Add-on with given ID, or null if not found. + * @throws If addonId is not specified or not a string. + */ + async get(addonId) { + const addon = await AddonManager.getAddonByID(addonId); + if (!addon) { + return null; + } + return this.serializeForSandbox(addon); + }, + + /** + * Get information about all installed add-ons. + * @async + * @returns {Array<SafeAddon>} + */ + async getAll(addonId) { + const addons = await AddonManager.getAllAddons(); + return addons.map(this.serializeForSandbox.bind(this)); + }, + + /** + * Installs an add-on + * + * @param {string} addonUrl + * Url to download the .xpi for the add-on from. + * @param {object} options + * @param {boolean} options.update=false + * If true, will update an existing installed add-on with the same ID. + * @async + * @returns {string} + * Add-on ID that was installed + * @throws {string} + * If the add-on can not be installed, or overwriting is disabled and an + * add-on with a matching ID is already installed. + */ + async install(addonUrl, options) { + const installObj = await AddonManager.getInstallForURL(addonUrl, null, "application/x-xpinstall"); + return this.applyInstall(installObj, options); + }, + + async applyInstall(addonInstall, {update = false} = {}) { + const result = new Promise((resolve, reject) => addonInstall.addListener({ + onInstallStarted(cbInstall) { + if (cbInstall.existingAddon && !update) { + reject(new Error(` + Cannot install add-on ${cbInstall.addon.id}; an existing add-on + with the same ID exists and updating is disabled. + `)); + return false; + } + return true; + }, + onInstallEnded(cbInstall, addon) { + resolve(addon.id); + }, + onInstallFailed(cbInstall) { + reject(new Error(`AddonInstall error code: [${cbInstall.error}]`)); + }, + onDownloadFailed(cbInstall) { + reject(new Error(`Download failed: [${cbInstall.sourceURI.spec}]`)); + }, + })); + addonInstall.install(); + return result; + }, + + /** + * Uninstalls an add-on by ID. + * @param addonId {string} Add-on ID to uninstall. + * @async + * @throws If no add-on with `addonId` is installed. + */ + async uninstall(addonId) { + const addon = await AddonManager.getAddonByID(addonId); + if (addon === null) { + throw new Error(`No addon with ID [${addonId}] found.`); + } + addon.uninstall(); + return null; + }, + + /** + * Make a safe serialization of an add-on + * @param addon {Object} An add-on object as returned from AddonManager. + */ + serializeForSandbox(addon) { + return { + id: addon.id, + installDate: new Date(addon.installDate), + isActive: addon.isActive, + name: addon.name, + type: addon.type, + version: addon.version, + }; + }, +};
--- a/browser/extensions/shield-recipe-client/lib/ClientEnvironment.jsm +++ b/browser/extensions/shield-recipe-client/lib/ClientEnvironment.jsm @@ -11,19 +11,20 @@ Cu.import("resource://gre/modules/XPCOMU XPCOMUtils.defineLazyModuleGetter(this, "ShellService", "resource:///modules/ShellService.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetryArchive", "resource://gre/modules/TelemetryArchive.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NormandyApi", "resource://shield-recipe-client/lib/NormandyApi.jsm"); XPCOMUtils.defineLazyModuleGetter( this, "PreferenceExperiments", - "resource://shield-recipe-client/lib/PreferenceExperiments.jsm", + "resource://shield-recipe-client/lib/PreferenceExperiments.jsm" ); XPCOMUtils.defineLazyModuleGetter(this, "Utils", "resource://shield-recipe-client/lib/Utils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Addons", "resource://shield-recipe-client/lib/Addons.jsm"); const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); this.EXPORTED_SYMBOLS = ["ClientEnvironment"]; // Cached API request for client attributes that are determined by the Normandy // service. let _classifyRequest = null; @@ -31,17 +32,17 @@ let _classifyRequest = null; this.ClientEnvironment = { /** * Fetches information about the client that is calculated on the server, * like geolocation and the current time. * * The server request is made lazily and is cached for the entire browser * session. */ - getClientClassification() { + async getClientClassification() { if (!_classifyRequest) { _classifyRequest = NormandyApi.classifyClient(); } return _classifyRequest; }, clearClassifyCache() { _classifyRequest = null; @@ -192,15 +193,20 @@ this.ClientEnvironment = { } else { names.active.push(experiment.name); } } return names; }); + XPCOMUtils.defineLazyGetter(environment, "addons", async () => { + const addons = await Addons.getAll(); + return Utils.keyBy(addons, "id"); + }); + XPCOMUtils.defineLazyGetter(environment, "isFirstRun", () => { return Preferences.get("extensions.shield-recipe-client.first_run"); }); return environment; }, };
--- a/browser/extensions/shield-recipe-client/lib/FilterExpressions.jsm +++ b/browser/extensions/shield-recipe-client/lib/FilterExpressions.jsm @@ -4,40 +4,62 @@ "use strict"; const {utils: Cu} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://shield-recipe-client/lib/Sampling.jsm"); Cu.import("resource://shield-recipe-client/lib/PreferenceFilters.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "mozjexl", "resource://shield-recipe-client-vendor/mozjexl.js"); + this.EXPORTED_SYMBOLS = ["FilterExpressions"]; -XPCOMUtils.defineLazyGetter(this, "nodeRequire", () => { - const {Loader, Require} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}); - const loader = new Loader({ - paths: { - "": "resource://shield-recipe-client/node_modules/", - }, - }); - return new Require(loader, {}); -}); - XPCOMUtils.defineLazyGetter(this, "jexl", () => { - const {Jexl} = nodeRequire("jexl/lib/Jexl.js"); - const jexl = new Jexl(); + const jexl = new mozjexl.Jexl(); jexl.addTransforms({ date: dateString => new Date(dateString), stableSample: Sampling.stableSample, bucketSample: Sampling.bucketSample, preferenceValue: PreferenceFilters.preferenceValue, preferenceIsUserSet: PreferenceFilters.preferenceIsUserSet, preferenceExists: PreferenceFilters.preferenceExists, + keys, }); + jexl.addBinaryOp("intersect", 40, operatorIntersect); return jexl; }); this.FilterExpressions = { eval(expr, context = {}) { const onelineExpr = expr.replace(/[\t\n\r]/g, " "); return jexl.eval(onelineExpr, context); }, }; + +/** + * Return an array of the given object's own keys (specifically, its enumerable + * properties), or undefined if the argument isn't an object. + * @param {Object} obj + * @return {Array[String]|undefined} + */ +function keys(obj) { + if (typeof obj !== "object" || obj === null) { + return undefined; + } + + return Object.keys(obj); +} + +/** + * Find all the values that are present in both lists. Returns undefined if + * the arguments are not both Arrays. + * @param {Array} listA + * @param {Array} listB + * @return {Array|undefined} + */ +function operatorIntersect(listA, listB) { + if (!Array.isArray(listA) || !Array.isArray(listB)) { + return undefined; + } + + return listA.filter(item => listB.includes(item)); +}
--- a/browser/extensions/shield-recipe-client/lib/NormandyApi.jsm +++ b/browser/extensions/shield-recipe-client/lib/NormandyApi.jsm @@ -1,29 +1,34 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {utils: Cu, classes: Cc, interfaces: Ci} = Components; Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/CanonicalJSON.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://shield-recipe-client/lib/LogManager.jsm"); -Cu.import("resource://shield-recipe-client/lib/Utils.jsm"); + +XPCOMUtils.defineLazyModuleGetter( + this, "CanonicalJSON", "resource://gre/modules/CanonicalJSON.jsm"); + Cu.importGlobalProperties(["fetch", "URL"]); /* globals fetch, URL */ this.EXPORTED_SYMBOLS = ["NormandyApi"]; const log = LogManager.getLogger("normandy-api"); const prefs = Services.prefs.getBranch("extensions.shield-recipe-client."); let indexPromise = null; this.NormandyApi = { + InvalidSignatureError: class InvalidSignatureError extends Error {}, + clearIndexCache() { indexPromise = null; }, apiCall(method, endpoint, data = {}) { const url = new URL(endpoint); method = method.toLowerCase(); @@ -58,95 +63,157 @@ this.NormandyApi = { } else if (url.startsWith("/")) { return server + url; } throw new Error("Can't use relative urls"); }, async getApiUrl(name) { if (!indexPromise) { - let apiBase = new URL(prefs.getCharPref("api_url")); + const apiBase = new URL(prefs.getCharPref("api_url")); if (!apiBase.pathname.endsWith("/")) { apiBase.pathname += "/"; } indexPromise = this.get(apiBase.toString()).then(res => res.json()); } const index = await indexPromise; if (!(name in index)) { throw new Error(`API endpoint with name "${name}" not found.`); } const url = index[name]; return this.absolutify(url); }, - async fetchRecipes(filters = {enabled: true}) { - const signedRecipesUrl = await this.getApiUrl("recipe-signed"); - const recipesResponse = await this.get(signedRecipesUrl, filters); - const rawText = await recipesResponse.text(); - const recipesWithSigs = JSON.parse(rawText); + async fetchSignedObjects(type, filters) { + const signedObjectsUrl = await this.getApiUrl(`${type}-signed`); + const objectsResponse = await this.get(signedObjectsUrl, filters); + const rawText = await objectsResponse.text(); + const objectsWithSigs = JSON.parse(rawText); + + const verifiedObjects = []; - const verifiedRecipes = []; + for (const objectWithSig of objectsWithSigs) { + const {signature, x5u} = objectWithSig.signature; + const object = objectWithSig[type]; - for (const {recipe, signature: {signature, x5u}} of recipesWithSigs) { - const serialized = CanonicalJSON.stringify(recipe); + const serialized = CanonicalJSON.stringify(object); + // Check that the rawtext (the object and the signature) + // includes the CanonicalJSON version of the object. This isn't + // strictly needed, but it is a great benefit for debugging + // signature problems. if (!rawText.includes(serialized)) { log.debug(rawText, serialized); - throw new Error("Canonical recipe serialization does not match!"); + throw new NormandyApi.InvalidSignatureError( + `Canonical ${type} serialization does not match!`); } - const certChainResponse = await fetch(this.absolutify(x5u)); + const certChainResponse = await this.get(this.absolutify(x5u)); const certChain = await certChainResponse.text(); const builtSignature = `p384ecdsa=${signature}`; const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"] .createInstance(Ci.nsIContentSignatureVerifier); - const valid = verifier.verifyContentSignature( - serialized, - builtSignature, - certChain, - "normandy.content-signature.mozilla.org" - ); + let valid; + try { + valid = verifier.verifyContentSignature( + serialized, + builtSignature, + certChain, + "normandy.content-signature.mozilla.org" + ); + } catch (err) { + throw new NormandyApi.InvalidSignatureError(`${type} signature validation failed: ${err}`); + } + if (!valid) { - throw new Error("Recipe signature is not valid"); + throw new NormandyApi.InvalidSignatureError(`${type} signature is not valid`); } - verifiedRecipes.push(recipe); + + verifiedObjects.push(object); } log.debug( - `Fetched ${verifiedRecipes.length} recipes from the server:`, - verifiedRecipes.map(r => r.name).join(", ") + `Fetched ${verifiedObjects.length} ${type} from the server:`, + verifiedObjects.map(r => r.name).join(", ") ); - return verifiedRecipes; + return verifiedObjects; }, /** * Fetch metadata about this client determined by the server. * @return {object} Metadata specified by the server */ async classifyClient() { const classifyClientUrl = await this.getApiUrl("classify-client"); const response = await this.get(classifyClientUrl); const clientData = await response.json(); clientData.request_time = new Date(clientData.request_time); return clientData; }, /** * Fetch an array of available actions from the server. + * @param filters + * @param filters.enabled {boolean} If true, only returns enabled + * recipes. Default true. * @resolves {Array} */ - async fetchActions() { - const actionApiUrl = await this.getApiUrl("action-list"); - const res = await this.get(actionApiUrl); - return res.json(); + async fetchRecipes(filters = {enabled: true}) { + return this.fetchSignedObjects("recipe", filters); + }, + + /** + * Fetch an array of available actions from the server. + * @resolves {Array} + */ + async fetchActions(filters = {}) { + return this.fetchSignedObjects("action", filters); }, async fetchImplementation(action) { - const response = await fetch(action.implementation_url); - if (response.ok) { - return response.text(); + const implementationUrl = new URL(this.absolutify(action.implementation_url)); + + // fetch implementation + const response = await fetch(implementationUrl); + if (!response.ok) { + throw new Error( + `Failed to fetch action implementation for ${action.name}: ${response.status}` + ); + } + const responseText = await response.text(); + + // Try to verify integrity of the implementation text. If the + // integrity value doesn't match the content or uses an unknown + // algorithm, fail. + + // Get the last non-empty portion of the url path, and split it + // into two to get the aglorithm and hash. + const parts = implementationUrl.pathname.split("/"); + const lastNonEmpty = parts.filter(p => p !== "").slice(-1)[0]; + const [algorithm, ...hashParts] = lastNonEmpty.split("-"); + const expectedHash = hashParts.join("-"); + + if (algorithm !== "sha384") { + throw new Error( + `Failed to fetch action implemenation for ${action.name}: ` + + `Unexpected integrity algorithm, expected "sha384", got ${algorithm}` + ); } - throw new Error(`Failed to fetch action implementation for ${action.name}: ${response.status}`); + // verify integrity hash + const hasher = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA384); + const dataToHash = new TextEncoder().encode(responseText); + hasher.update(dataToHash, dataToHash.length); + const useBase64 = true; + const hash = hasher.finish(useBase64).replace(/\+/g, "-").replace(/\//g, "_"); + if (hash !== expectedHash) { + throw new Error( + `Failed to fetch action implementation for ${action.name}: ` + + `Integrity hash does not match content. Expected ${expectedHash} got ${hash}.` + ); + } + + return responseText; }, };
--- a/browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm +++ b/browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm @@ -1,29 +1,34 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource:///modules/ShellService.jsm"); Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/Timer.jsm"); +Cu.import("resource://shield-recipe-client/lib/Addons.jsm"); Cu.import("resource://shield-recipe-client/lib/LogManager.jsm"); Cu.import("resource://shield-recipe-client/lib/Storage.jsm"); Cu.import("resource://shield-recipe-client/lib/Heartbeat.jsm"); Cu.import("resource://shield-recipe-client/lib/FilterExpressions.jsm"); Cu.import("resource://shield-recipe-client/lib/ClientEnvironment.jsm"); Cu.import("resource://shield-recipe-client/lib/PreferenceExperiments.jsm"); Cu.import("resource://shield-recipe-client/lib/Sampling.jsm"); +XPCOMUtils.defineLazyModuleGetter( + this, "AddonStudies", "resource://shield-recipe-client/lib/AddonStudies.jsm"); + const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); this.EXPORTED_SYMBOLS = ["NormandyDriver"]; const log = LogManager.getLogger("normandy-driver"); const actionLog = LogManager.getLogger("normandy-driver.actions"); this.NormandyDriver = function(sandboxManager) { @@ -151,22 +156,74 @@ this.NormandyDriver = function(sandboxMa return Cu.cloneInto(token, sandbox); }, clearTimeout(token) { clearTimeout(token); sandboxManager.removeHold(`setTimeout-${token}`); }, + addons: { + get: sandboxManager.wrapAsync(Addons.get.bind(Addons), {cloneInto: true}), + install: sandboxManager.wrapAsync(Addons.install.bind(Addons)), + uninstall: sandboxManager.wrapAsync(Addons.uninstall.bind(Addons)), + }, + // Sampling ratioSample: sandboxManager.wrapAsync(Sampling.ratioSample), // Preference Experiment API preferenceExperiments: { start: sandboxManager.wrapAsync(PreferenceExperiments.start, {cloneArguments: true}), markLastSeen: sandboxManager.wrapAsync(PreferenceExperiments.markLastSeen), stop: sandboxManager.wrapAsync(PreferenceExperiments.stop), get: sandboxManager.wrapAsync(PreferenceExperiments.get, {cloneInto: true}), getAllActive: sandboxManager.wrapAsync(PreferenceExperiments.getAllActive, {cloneInto: true}), has: sandboxManager.wrapAsync(PreferenceExperiments.has), }, + + // Study storage API + studies: { + start: sandboxManager.wrapAsync( + AddonStudies.start.bind(AddonStudies), + {cloneArguments: true, cloneInto: true} + ), + stop: sandboxManager.wrapAsync(AddonStudies.stop.bind(AddonStudies)), + get: sandboxManager.wrapAsync(AddonStudies.get.bind(AddonStudies), {cloneInto: true}), + getAll: sandboxManager.wrapAsync(AddonStudies.getAll.bind(AddonStudies), {cloneInto: true}), + has: sandboxManager.wrapAsync(AddonStudies.has.bind(AddonStudies)), + }, + + // Preference read-only API + preferences: { + getBool: wrapPrefGetter(Services.prefs.getBoolPref), + getInt: wrapPrefGetter(Services.prefs.getIntPref), + getChar: wrapPrefGetter(Services.prefs.getCharPref), + has(name) { + return Services.prefs.getPrefType(name) !== Services.prefs.PREF_INVALID; + }, + }, }; }; + +/** + * Wrap a getter form nsIPrefBranch for use in the sandbox. + * + * We don't want to export the getters directly in case they add parameters that + * aren't safe for the sandbox without us noticing; wrapping helps prevent + * passing unknown parameters. + * + * @param {Function} getter + * Function on an nsIPrefBranch that fetches a preference value. + * @return {Function} + */ +function wrapPrefGetter(getter) { + return (value, defaultValue = undefined) => { + // Passing undefined as the defaultValue disables throwing exceptions when + // the pref is missing or the type doesn't match, so we need to specifically + // exclude it if we don't want default value behavior. + const args = [value]; + if (defaultValue !== undefined) { + args.push(defaultValue); + } + return getter.apply(null, args); + }; +}
--- a/browser/extensions/shield-recipe-client/lib/PreferenceExperiments.jsm +++ b/browser/extensions/shield-recipe-client/lib/PreferenceExperiments.jsm @@ -144,17 +144,17 @@ this.PreferenceExperiments = { async init() { for (const experiment of await this.getAllActive()) { // Set experiment default preferences, since they don't persist between restarts if (experiment.preferenceBranchType === "default") { setPref(DefaultPreferences, experiment.preferenceName, experiment.preferenceType, experiment.preferenceValue); } // Check that the current value of the preference is still what we set it to - if (getPref(UserPreferences, experiment.preferenceName, experiment.preferenceType) !== experiment.preferenceValue) { + if (getPref(UserPreferences, experiment.preferenceName, experiment.preferenceType, undefined) !== experiment.preferenceValue) { // if not, stop the experiment, and skip the remaining steps log.info(`Stopping experiment "${experiment.name}" because its value changed`); await this.stop(experiment.name, false); continue; } // Notify Telemetry of experiments we're running, since they don't persist between restarts TelemetryEnvironment.setExperimentActive(experiment.name, experiment.branch); @@ -237,17 +237,17 @@ this.PreferenceExperiments = { const experiment = { name, branch, expired: false, lastSeen: new Date().toJSON(), preferenceName, preferenceValue, preferenceType, - previousPreferenceValue: getPref(preferences, preferenceName, preferenceType), + previousPreferenceValue: getPref(preferences, preferenceName, preferenceType, undefined), preferenceBranchType, }; const prevPrefType = Services.prefs.getPrefType(preferenceName); const givenPrefType = PREFERENCE_TYPE_MAP[preferenceType]; if (!preferenceType || !givenPrefType) { throw new Error(`Invalid preferenceType provided (given "${preferenceType}")`); @@ -284,17 +284,17 @@ this.PreferenceExperiments = { throw new Error( `An observer for the preference experiment ${experimentName} is already active.` ); } const observerInfo = { preferenceName, observer() { - let newValue = getPref(UserPreferences, preferenceName, preferenceType); + let newValue = getPref(UserPreferences, preferenceName, preferenceType, undefined); if (newValue !== preferenceValue) { PreferenceExperiments.stop(experimentName, false) .catch(Cu.reportError); } }, }; experimentObservers.set(experimentName, observerInfo); Services.prefs.addObserver(preferenceName, observerInfo.observer);
--- a/browser/extensions/shield-recipe-client/lib/RecipeRunner.jsm +++ b/browser/extensions/shield-recipe-client/lib/RecipeRunner.jsm @@ -24,16 +24,20 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "SandboxManager", "resource://shield-recipe-client/lib/SandboxManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ClientEnvironment", "resource://shield-recipe-client/lib/ClientEnvironment.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ActionSandboxManager", "resource://shield-recipe-client/lib/ActionSandboxManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AddonStudies", + "resource://shield-recipe-client/lib/AddonStudies.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Uptake", + "resource://shield-recipe-client/lib/Uptake.jsm"); Cu.importGlobalProperties(["fetch"]); this.EXPORTED_SYMBOLS = ["RecipeRunner"]; const log = LogManager.getLogger("recipe-runner"); const prefs = Services.prefs.getBranch("extensions.shield-recipe-client."); const TIMER_NAME = "recipe-client-addon-run"; @@ -122,106 +126,141 @@ this.RecipeRunner = { const runInterval = prefs.getIntPref(RUN_INTERVAL_PREF); timerManager.registerTimer(TIMER_NAME, () => this.run(), runInterval); }, async run() { this.clearCaches(); // Unless lazy classification is enabled, prep the classify cache. if (!Preferences.get("extensions.shield-recipe-client.experiments.lazy_classify", false)) { - await ClientEnvironment.getClientClassification(); + try { + await ClientEnvironment.getClientClassification(); + } catch (err) { + // Try to go on without this data; the filter expressions will + // gracefully fail without this info if they need it. + } + } + + // Fetch recipes before execution in case we fail and exit early. + let recipes; + try { + recipes = await NormandyApi.fetchRecipes({enabled: true}); + } catch (e) { + const apiUrl = prefs.getCharPref("api_url"); + log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`); + + let status = Uptake.RUNNER_SERVER_ERROR; + if (/NetworkError/.test(e)) { + status = Uptake.RUNNER_NETWORK_ERROR; + } else if (e instanceof NormandyApi.InvalidSignatureError) { + status = Uptake.RUNNER_INVALID_SIGNATURE; + } + Uptake.reportRunner(status); + return; } const actionSandboxManagers = await this.loadActionSandboxManagers(); Object.values(actionSandboxManagers).forEach(manager => manager.addHold("recipeRunner")); // Run pre-execution hooks. If a hook fails, we don't run recipes with that // action to avoid inconsistencies. for (const [actionName, manager] of Object.entries(actionSandboxManagers)) { try { await manager.runAsyncCallback("preExecution"); manager.disabled = false; } catch (err) { log.error(`Could not run pre-execution hook for ${actionName}:`, err.message); manager.disabled = true; + Uptake.reportAction(actionName, Uptake.ACTION_PRE_EXECUTION_ERROR); } } - // Fetch recipes from the API - let recipes; - try { - recipes = await NormandyApi.fetchRecipes({enabled: true}); - } catch (e) { - const apiUrl = prefs.getCharPref("api_url"); - log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`); - return; - } - // Evaluate recipe filters const recipesToRun = []; for (const recipe of recipes) { if (await this.checkFilter(recipe)) { recipesToRun.push(recipe); } } // Execute recipes, if we have any. if (recipesToRun.length === 0) { log.debug("No recipes to execute"); } else { for (const recipe of recipesToRun) { const manager = actionSandboxManagers[recipe.action]; + let status; if (!manager) { log.error( `Could not execute recipe ${recipe.name}:`, `Action ${recipe.action} is either missing or invalid.` ); + status = Uptake.RECIPE_INVALID_ACTION; } else if (manager.disabled) { log.warn( `Skipping recipe ${recipe.name} because ${recipe.action} failed during pre-execution.` ); + status = Uptake.RECIPE_ACTION_DISABLED; } else { try { log.info(`Executing recipe "${recipe.name}" (action=${recipe.action})`); await manager.runAsyncCallback("action", recipe); + status = Uptake.RECIPE_SUCCESS; } catch (e) { - log.error(`Could not execute recipe ${recipe.name}:`, e); + log.error(`Could not execute recipe ${recipe.name}:`); + Cu.reportError(e); + status = Uptake.RECIPE_EXECUTION_ERROR; } } + + Uptake.reportRecipe(recipe.id, status); } } // Run post-execution hooks for (const [actionName, manager] of Object.entries(actionSandboxManagers)) { // Skip if pre-execution failed. if (manager.disabled) { log.info(`Skipping post-execution hook for ${actionName} due to earlier failure.`); continue; } try { await manager.runAsyncCallback("postExecution"); + Uptake.reportAction(actionName, Uptake.ACTION_SUCCESS); } catch (err) { log.info(`Could not run post-execution hook for ${actionName}:`, err.message); + Uptake.reportAction(actionName, Uptake.ACTION_POST_EXECUTION_ERROR); } } // Nuke sandboxes Object.values(actionSandboxManagers).forEach(manager => manager.removeHold("recipeRunner")); + + // Close storage connections + await AddonStudies.close(); + + Uptake.reportRunner(Uptake.RUNNER_SUCCESS); }, async loadActionSandboxManagers() { const actions = await NormandyApi.fetchActions(); const actionSandboxManagers = {}; for (const action of actions) { try { const implementation = await NormandyApi.fetchImplementation(action); actionSandboxManagers[action.name] = new ActionSandboxManager(implementation); } catch (err) { log.warn(`Could not fetch implementation for ${action.name}:`, err); + + let status = Uptake.ACTION_SERVER_ERROR; + if (/NetworkError/.test(err)) { + status = Uptake.ACTION_NETWORK_ERROR; + } + Uptake.reportAction(action.name, status); } } return actionSandboxManagers; }, getFilterContext(recipe) { return { normandy: Object.assign(ClientEnvironment.getEnvironment(), {
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/lib/ShieldPreferences.jsm @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {utils: Cu} = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter( + this, "AppConstants", "resource://gre/modules/AppConstants.jsm" +); +XPCOMUtils.defineLazyModuleGetter( + this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm" +); + +this.EXPORTED_SYMBOLS = ["ShieldPreferences"]; + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; // from modules/libpref/nsIPrefBranch.idl +const FHR_UPLOAD_ENABLED_PREF = "datareporting.healthreport.uploadEnabled"; +const OPT_OUT_STUDIES_ENABLED_PREF = "app.shield.optoutstudies.enabled"; + +/** + * Handles Shield-specific preferences, including their UI. + */ +this.ShieldPreferences = { + init() { + // If the FHR pref was disabled since our last run, disable opt-out as well. + if (!Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED_PREF)) { + Services.prefs.setBoolPref(OPT_OUT_STUDIES_ENABLED_PREF, false); + } + + // Watch for changes to the FHR pref + Services.prefs.addObserver(FHR_UPLOAD_ENABLED_PREF, this); + CleanupManager.addCleanupHandler(() => { + Services.prefs.removeObserver(FHR_UPLOAD_ENABLED_PREF, this); + }); + + // Disabled outside of en-* locales temporarily (bug 1377192). + // Disabled when MOZ_DATA_REPORTING is false since the FHR UI is also hidden + // when data reporting is false. + if (AppConstants.MOZ_DATA_REPORTING && Services.locale.getAppLocaleAsLangTag().startsWith("en")) { + Services.obs.addObserver(this, "advanced-pane-loaded"); + CleanupManager.addCleanupHandler(() => { + Services.obs.removeObserver(this, "advanced-pane-loaded"); + }); + } + }, + + observe(subject, topic, data) { + switch (topic) { + // Add the opt-out-study checkbox to the Privacy preferences when it is shown. + case "advanced-pane-loaded": + if (!Services.prefs.getBoolPref("browser.preferences.useOldOrganization", false)) { + this.injectOptOutStudyCheckbox(subject.document); + } + break; + // If the FHR pref changes, set the opt-out-study pref to the value it is changing to. + case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID: + if (data === FHR_UPLOAD_ENABLED_PREF) { + const fhrUploadEnabled = Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED_PREF); + Services.prefs.setBoolPref(OPT_OUT_STUDIES_ENABLED_PREF, fhrUploadEnabled); + } + break; + } + }, + + /** + * Injects the opt-out-study preference checkbox into about:preferences and + * handles events coming from the UI for it. + */ + injectOptOutStudyCheckbox(doc) { + const container = doc.createElementNS(XUL_NS, "vbox"); + container.classList.add("indent"); + + const hContainer = doc.createElementNS(XUL_NS, "hbox"); + hContainer.setAttribute("align", "center"); + container.appendChild(hContainer); + + const checkbox = doc.createElementNS(XUL_NS, "checkbox"); + checkbox.setAttribute("id", "optOutStudiesEnabled"); + checkbox.setAttribute("label", "Allow Firefox to install and run studies"); + checkbox.setAttribute("preference", OPT_OUT_STUDIES_ENABLED_PREF); + checkbox.setAttribute("disabled", !Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED_PREF)); + hContainer.appendChild(checkbox); + + const viewStudies = doc.createElementNS(XUL_NS, "label"); + viewStudies.setAttribute("id", "viewShieldStudies"); + viewStudies.setAttribute("href", "about:studies"); + viewStudies.setAttribute("useoriginprincipal", true); + viewStudies.textContent = "View Firefox Studies"; + viewStudies.classList.add("learnMore", "text-link"); + hContainer.appendChild(viewStudies); + + // <prefrence> elements for prefs that we need to monitor while the page is open. + const optOutPref = doc.createElementNS(XUL_NS, "preference"); + optOutPref.setAttribute("id", OPT_OUT_STUDIES_ENABLED_PREF); + optOutPref.setAttribute("name", OPT_OUT_STUDIES_ENABLED_PREF); + optOutPref.setAttribute("type", "bool"); + + // Weirdly, FHR doesn't have a <preference> element on the page, so we create it. + const fhrPref = doc.createElementNS(XUL_NS, "preference"); + fhrPref.setAttribute("id", FHR_UPLOAD_ENABLED_PREF); + fhrPref.setAttribute("name", FHR_UPLOAD_ENABLED_PREF); + fhrPref.setAttribute("type", "bool"); + fhrPref.addEventListener("change", function(event) { + // Avoid reference to the document directly, to avoid leaks. + const eventTargetCheckbox = event.target.ownerDocument.getElementById("optOutStudiesEnabled"); + eventTargetCheckbox.disabled = !Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED_PREF); + }); + + // Actually inject the elements we've created. + const parent = doc.getElementById("submitHealthReportBox").closest("vbox"); + parent.appendChild(container); + + const preferences = doc.getElementById("privacyPreferences"); + preferences.appendChild(optOutPref); + preferences.appendChild(fhrPref); + }, +};
--- a/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm +++ b/browser/extensions/shield-recipe-client/lib/ShieldRecipeClient.jsm @@ -11,96 +11,82 @@ Cu.import("resource://gre/modules/XPCOMU XPCOMUtils.defineLazyModuleGetter(this, "LogManager", "resource://shield-recipe-client/lib/LogManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "RecipeRunner", "resource://shield-recipe-client/lib/RecipeRunner.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "CleanupManager", "resource://shield-recipe-client/lib/CleanupManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PreferenceExperiments", "resource://shield-recipe-client/lib/PreferenceExperiments.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AboutPages", + "resource://shield-recipe-client-content/AboutPages.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ShieldPreferences", + "resource://shield-recipe-client/lib/ShieldPreferences.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AddonStudies", + "resource://shield-recipe-client/lib/AddonStudies.jsm"); this.EXPORTED_SYMBOLS = ["ShieldRecipeClient"]; const {PREF_STRING, PREF_BOOL, PREF_INT} = Ci.nsIPrefBranch; const REASONS = { APP_STARTUP: 1, // The application is starting up. APP_SHUTDOWN: 2, // The application is shutting down. ADDON_ENABLE: 3, // The add-on is being enabled. ADDON_DISABLE: 4, // The add-on is being disabled. (Also sent during uninstallation) ADDON_INSTALL: 5, // The add-on is being installed. ADDON_UNINSTALL: 6, // The add-on is being uninstalled. ADDON_UPGRADE: 7, // The add-on is being upgraded. ADDON_DOWNGRADE: 8, // The add-on is being downgraded. }; -const PREF_BRANCH = "extensions.shield-recipe-client."; -const DEFAULT_PREFS = { - api_url: ["https://normandy.cdn.mozilla.net/api/v1", PREF_STRING], - dev_mode: [false, PREF_BOOL], - enabled: [true, PREF_BOOL], - startup_delay_seconds: [300, PREF_INT], - "logging.level": [Log.Level.Warn, PREF_INT], - user_id: ["", PREF_STRING], - run_interval_seconds: [86400, PREF_INT], // 24 hours - first_run: [true, PREF_BOOL], -}; const PREF_DEV_MODE = "extensions.shield-recipe-client.dev_mode"; -const PREF_LOGGING_LEVEL = PREF_BRANCH + "logging.level"; +const PREF_LOGGING_LEVEL = "extensions.shield-recipe-client.logging.level"; let log = null; /** * Handles startup and shutdown of the entire add-on. Bootsrap.js defers to this * module for most tasks so that we can more easily test startup and shutdown * (bootstrap.js is difficult to import in tests). */ this.ShieldRecipeClient = { async startup() { - ShieldRecipeClient.setDefaultPrefs(); - // Setup logging and listen for changes to logging prefs LogManager.configure(Services.prefs.getIntPref(PREF_LOGGING_LEVEL)); Services.prefs.addObserver(PREF_LOGGING_LEVEL, LogManager.configure); CleanupManager.addCleanupHandler( () => Services.prefs.removeObserver(PREF_LOGGING_LEVEL, LogManager.configure), ); log = LogManager.getLogger("bootstrap"); + try { + await AboutPages.init(); + } catch (err) { + log.error("Failed to initialize about pages:", err); + } + + try { + await AddonStudies.init(); + } catch (err) { + log.error("Failed to initialize addon studies:", err); + } + // Initialize experiments first to avoid a race between initializing prefs // and recipes rolling back pref changes when experiments end. try { await PreferenceExperiments.init(); } catch (err) { log.error("Failed to initialize preference experiments:", err); } + try { + ShieldPreferences.init(); + } catch (err) { + log.error("Failed to initialize preferences UI:", err); + } + await RecipeRunner.init(); }, shutdown(reason) { CleanupManager.cleanup(); }, - - setDefaultPrefs() { - for (const [key, [val, type]] of Object.entries(DEFAULT_PREFS)) { - const fullKey = PREF_BRANCH + key; - // If someone beat us to setting a default, don't overwrite it. - if (!Services.prefs.prefHasUserValue(fullKey)) { - switch (type) { - case PREF_BOOL: - Services.prefs.setBoolPref(fullKey, val); - break; - - case PREF_INT: - Services.prefs.setIntPref(fullKey, val); - break; - - case PREF_STRING: - Services.prefs.setStringPref(fullKey, val); - break; - - default: - throw new TypeError(`Unexpected type (${type}) for preference ${fullKey}.`) - } - } - } - }, };
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/lib/Uptake.jsm @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {utils: Cu} = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter( + this, "UptakeTelemetry", "resource://services-common/uptake-telemetry.js"); + +this.EXPORTED_SYMBOLS = ["Uptake"]; + +const SOURCE_PREFIX = "shield-recipe-client"; + +this.Uptake = { + // Action uptake + ACTION_NETWORK_ERROR: UptakeTelemetry.STATUS.NETWORK_ERROR, + ACTION_PRE_EXECUTION_ERROR: UptakeTelemetry.STATUS.CUSTOM_1_ERROR, + ACTION_POST_EXECUTION_ERROR: UptakeTelemetry.STATUS.CUSTOM_2_ERROR, + ACTION_SERVER_ERROR: UptakeTelemetry.STATUS.SERVER_ERROR, + ACTION_SUCCESS: UptakeTelemetry.STATUS.SUCCESS, + + // Per-recipe uptake + RECIPE_ACTION_DISABLED: UptakeTelemetry.STATUS.CUSTOM_1_ERROR, + RECIPE_EXECUTION_ERROR: UptakeTelemetry.STATUS.APPLY_ERROR, + RECIPE_INVALID_ACTION: UptakeTelemetry.STATUS.DOWNLOAD_ERROR, + RECIPE_SUCCESS: UptakeTelemetry.STATUS.SUCCESS, + + // Uptake for the runner as a whole + RUNNER_INVALID_SIGNATURE: UptakeTelemetry.STATUS.SIGNATURE_ERROR, + RUNNER_NETWORK_ERROR: UptakeTelemetry.STATUS.NETWORK_ERROR, + RUNNER_SERVER_ERROR: UptakeTelemetry.STATUS.SERVER_ERROR, + RUNNER_SUCCESS: UptakeTelemetry.STATUS.SUCCESS, + + reportRunner(status) { + UptakeTelemetry.report(`${SOURCE_PREFIX}/runner`, status); + }, + + reportRecipe(recipeId, status) { + UptakeTelemetry.report(`${SOURCE_PREFIX}/recipe/${recipeId}`, status); + }, + + reportAction(actionName, status) { + UptakeTelemetry.report(`${SOURCE_PREFIX}/action/${actionName}`, status); + }, +};
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/node_modules/jexl/LICENSE.txt +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2015 TechnologyAdvice - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/Jexl.js +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Jexl - * Copyright (c) 2015 TechnologyAdvice - */ - -var Evaluator = require('./evaluator/Evaluator'), - Lexer = require('./Lexer'), - Parser = require('./parser/Parser'), - defaultGrammar = require('./grammar').elements; - -/** - * Jexl is the Javascript Expression Language, capable of parsing and - * evaluating basic to complex expression strings, combined with advanced - * xpath-like drilldown into native Javascript objects. - * @constructor - */ -function Jexl() { - this._customGrammar = null; - this._lexer = null; - this._transforms = {}; -} - -/** - * Adds a binary operator to Jexl at the specified precedence. The higher the - * precedence, the earlier the operator is applied in the order of operations. - * For example, * has a higher precedence than +, because multiplication comes - * before division. - * - * Please see grammar.js for a listing of all default operators and their - * precedence values in order to choose the appropriate precedence for the - * new operator. - * @param {string} operator The operator string to be added - * @param {number} precedence The operator's precedence - * @param {function} fn A function to run to calculate the result. The function - * will be called with two arguments: left and right, denoting the values - * on either side of the operator. It should return either the resulting - * value, or a Promise that resolves with the resulting value. - */ -Jexl.prototype.addBinaryOp = function(operator, precedence, fn) { - this._addGrammarElement(operator, { - type: 'binaryOp', - precedence: precedence, - eval: fn - }); -}; - -/** - * Adds a unary operator to Jexl. Unary operators are currently only supported - * on the left side of the value on which it will operate. - * @param {string} operator The operator string to be added - * @param {function} fn A function to run to calculate the result. The function - * will be called with one argument: the literal value to the right of the - * operator. It should return either the resulting value, or a Promise - * that resolves with the resulting value. - */ -Jexl.prototype.addUnaryOp = function(operator, fn) { - this._addGrammarElement(operator, { - type: 'unaryOp', - weight: Infinity, - eval: fn - }); -}; - -/** - * Adds or replaces a transform function in this Jexl instance. - * @param {string} name The name of the transform function, as it will be used - * within Jexl expressions - * @param {function} fn The function to be executed when this transform is - * invoked. It will be provided with two arguments: - * - {*} value: The value to be transformed - * - {{}} args: The arguments for this transform - * - {function} cb: A callback function to be called with an error - * if the transform fails, or a null first argument and the - * transformed value as the second argument on success. - */ -Jexl.prototype.addTransform = function(name, fn) { - this._transforms[name] = fn; -}; - -/** - * Syntactic sugar for calling {@link #addTransform} repeatedly. This function - * accepts a map of one or more transform names to their transform function. - * @param {{}} map A map of transform names to transform functions - */ -Jexl.prototype.addTransforms = function(map) { - for (var key in map) { - if (map.hasOwnProperty(key)) - this._transforms[key] = map[key]; - } -}; - -/** - * Retrieves a previously set transform function. - * @param {string} name The name of the transform function - * @returns {function} The transform function - */ -Jexl.prototype.getTransform = function(name) { - return this._transforms[name]; -}; - -/** - * Evaluates a Jexl string within an optional context. - * @param {string} expression The Jexl expression to be evaluated - * @param {Object} [context] A mapping of variables to values, which will be - * made accessible to the Jexl expression when evaluating it - * @param {function} [cb] An optional callback function to be executed when - * evaluation is complete. It will be supplied with two arguments: - * - {Error|null} err: Present if an error occurred - * - {*} result: The result of the evaluation - * @returns {Promise<*>} resolves with the result of the evaluation. Note that - * if a callback is supplied, the returned promise will already have - * a '.catch' attached to it in order to pass the error to the callback. - */ -Jexl.prototype.eval = function(expression, context, cb) { - if (typeof context === 'function') { - cb = context; - context = {}; - } - else if (!context) - context = {}; - var valPromise = this._eval(expression, context); - if (cb) { - // setTimeout is used for the callback to break out of the Promise's - // try/catch in case the callback throws. - var called = false; - return valPromise.then(function(val) { - called = true; - setTimeout(cb.bind(null, null, val), 0); - }).catch(function(err) { - if (!called) - setTimeout(cb.bind(null, err), 0); - }); - } - return valPromise; -}; - -/** - * Removes a binary or unary operator from the Jexl grammar. - * @param {string} operator The operator string to be removed - */ -Jexl.prototype.removeOp = function(operator) { - var grammar = this._getCustomGrammar(); - if (grammar[operator] && (grammar[operator].type == 'binaryOp' || - grammar[operator].type == 'unaryOp')) { - delete grammar[operator]; - this._lexer = null; - } -}; - -/** - * Adds an element to the grammar map used by this Jexl instance, cloning - * the default grammar first if necessary. - * @param {string} str The key string to be added - * @param {{type: <string>}} obj A map of configuration options for this - * grammar element - * @private - */ -Jexl.prototype._addGrammarElement = function(str, obj) { - var grammar = this._getCustomGrammar(); - grammar[str] = obj; - this._lexer = null; -}; - -/** - * Evaluates a Jexl string in the given context. - * @param {string} exp The Jexl expression to be evaluated - * @param {Object} [context] A mapping of variables to values, which will be - * made accessible to the Jexl expression when evaluating it - * @returns {Promise<*>} resolves with the result of the evaluation. - * @private - */ -Jexl.prototype._eval = function(exp, context) { - var self = this, - grammar = this._getGrammar(), - parser = new Parser(grammar), - evaluator = new Evaluator(grammar, this._transforms, context); - return Promise.resolve().then(function() { - parser.addTokens(self._getLexer().tokenize(exp)); - return evaluator.eval(parser.complete()); - }); -}; - -/** - * Gets the custom grammar object, creating it first if necessary. New custom - * grammars are created by executing a shallow clone of the default grammar - * map. The returned map is available to be changed. - * @returns {{}} a customizable grammar map. - * @private - */ -Jexl.prototype._getCustomGrammar = function() { - if (!this._customGrammar) { - this._customGrammar = {}; - for (var key in defaultGrammar) { - if (defaultGrammar.hasOwnProperty(key)) - this._customGrammar[key] = defaultGrammar[key]; - } - } - return this._customGrammar; -}; - -/** - * Gets the grammar map currently being used by Jexl; either the default map, - * or a locally customized version. The returned map should never be changed - * in any way. - * @returns {{}} the grammar map currently in use. - * @private - */ -Jexl.prototype._getGrammar = function() { - return this._customGrammar || defaultGrammar; -}; - -/** - * Gets a Lexer instance as a singleton in reference to this Jexl instance. - * @returns {Lexer} an instance of Lexer, initialized with a grammar - * appropriate to this Jexl instance. - * @private - */ -Jexl.prototype._getLexer = function() { - if (!this._lexer) - this._lexer = new Lexer(this._getGrammar()); - return this._lexer; -}; - -module.exports = new Jexl(); -module.exports.Jexl = Jexl;
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/Lexer.js +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Jexl - * Copyright (c) 2015 TechnologyAdvice - */ - -var numericRegex = /^-?(?:(?:[0-9]*\.[0-9]+)|[0-9]+)$/, - identRegex = /^[a-zA-Z_\$][a-zA-Z0-9_\$]*$/, - escEscRegex = /\\\\/, - preOpRegexElems = [ - // Strings - "'(?:(?:\\\\')?[^'])*'", - '"(?:(?:\\\\")?[^"])*"', - // Whitespace - '\\s+', - // Booleans - '\\btrue\\b', - '\\bfalse\\b' - ], - postOpRegexElems = [ - // Identifiers - '\\b[a-zA-Z_\\$][a-zA-Z0-9_\\$]*\\b', - // Numerics (without negative symbol) - '(?:(?:[0-9]*\\.[0-9]+)|[0-9]+)' - ], - minusNegatesAfter = ['binaryOp', 'unaryOp', 'openParen', 'openBracket', - 'question', 'colon']; - -/** - * Lexer is a collection of stateless, statically-accessed functions for the - * lexical parsing of a Jexl string. Its responsibility is to identify the - * "parts of speech" of a Jexl expression, and tokenize and label each, but - * to do only the most minimal syntax checking; the only errors the Lexer - * should be concerned with are if it's unable to identify the utility of - * any of its tokens. Errors stemming from these tokens not being in a - * sensible configuration should be left for the Parser to handle. - * @type {{}} - */ -function Lexer(grammar) { - this._grammar = grammar; -} - -/** - * Splits a Jexl expression string into an array of expression elements. - * @param {string} str A Jexl expression string - * @returns {Array<string>} An array of substrings defining the functional - * elements of the expression. - */ -Lexer.prototype.getElements = function(str) { - var regex = this._getSplitRegex(); - return str.split(regex).filter(function(elem) { - // Remove empty strings - return elem; - }); -}; - -/** - * Converts an array of expression elements into an array of tokens. Note that - * the resulting array may not equal the element array in length, as any - * elements that consist only of whitespace get appended to the previous - * token's "raw" property. For the structure of a token object, please see - * {@link Lexer#tokenize}. - * @param {Array<string>} elements An array of Jexl expression elements to be - * converted to tokens - * @returns {Array<{type, value, raw}>} an array of token objects. - */ -Lexer.prototype.getTokens = function(elements) { - var tokens = [], - negate = false; - for (var i = 0; i < elements.length; i++) { - if (this._isWhitespace(elements[i])) { - if (tokens.length) - tokens[tokens.length - 1].raw += elements[i]; - } - else if (elements[i] === '-' && this._isNegative(tokens)) - negate = true; - else { - if (negate) { - elements[i] = '-' + elements[i]; - negate = false; - } - tokens.push(this._createToken(elements[i])); - } - } - // Catch a - at the end of the string. Let the parser handle that issue. - if (negate) - tokens.push(this._createToken('-')); - return tokens; -}; - -/** - * Converts a Jexl string into an array of tokens. Each token is an object - * in the following format: - * - * { - * type: <string>, - * [name]: <string>, - * value: <boolean|number|string>, - * raw: <string> - * } - * - * Type is one of the following: - * - * literal, identifier, binaryOp, unaryOp - * - * OR, if the token is a control character its type is the name of the element - * defined in the Grammar. - * - * Name appears only if the token is a control string found in - * {@link grammar#elements}, and is set to the name of the element. - * - * Value is the value of the token in the correct type (boolean or numeric as - * appropriate). Raw is the string representation of this value taken directly - * from the expression string, including any trailing spaces. - * @param {string} str The Jexl string to be tokenized - * @returns {Array<{type, value, raw}>} an array of token objects. - * @throws {Error} if the provided string contains an invalid token. - */ -Lexer.prototype.tokenize = function(str) { - var elements = this.getElements(str); - return this.getTokens(elements); -}; - -/** - * Creates a new token object from an element of a Jexl string. See - * {@link Lexer#tokenize} for a description of the token object. - * @param {string} element The element from which a token should be made - * @returns {{value: number|boolean|string, [name]: string, type: string, - * raw: string}} a token object describing the provided element. - * @throws {Error} if the provided string is not a valid expression element. - * @private - */ -Lexer.prototype._createToken = function(element) { - var token = { - type: 'literal', - value: element, - raw: element - }; - if (element[0] == '"' || element[0] == "'") - token.value = this._unquote(element); - else if (element.match(numericRegex)) - token.value = parseFloat(element); - else if (element === 'true' || element === 'false') - token.value = element === 'true'; - else if (this._grammar[element]) - token.type = this._grammar[element].type; - else if (element.match(identRegex)) - token.type = 'identifier'; - else - throw new Error("Invalid expression token: " + element); - return token; -}; - -/** - * Escapes a string so that it can be treated as a string literal within a - * regular expression. - * @param {string} str The string to be escaped - * @returns {string} the RegExp-escaped string. - * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions - * @private - */ -Lexer.prototype._escapeRegExp = function(str) { - str = str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - if (str.match(identRegex)) - str = '\\b' + str + '\\b'; - return str; -}; - -/** - * Gets a RegEx object appropriate for splitting a Jexl string into its core - * elements. - * @returns {RegExp} An element-splitting RegExp object - * @private - */ -Lexer.prototype._getSplitRegex = function() { - if (!this._splitRegex) { - var elemArray = Object.keys(this._grammar); - // Sort by most characters to least, then regex escape each - elemArray = elemArray.sort(function(a ,b) { - return b.length - a.length; - }).map(function(elem) { - return this._escapeRegExp(elem); - }, this); - this._splitRegex = new RegExp('(' + [ - preOpRegexElems.join('|'), - elemArray.join('|'), - postOpRegexElems.join('|') - ].join('|') + ')'); - } - return this._splitRegex; -}; - -/** - * Determines whether the addition of a '-' token should be interpreted as a - * negative symbol for an upcoming number, given an array of tokens already - * processed. - * @param {Array<Object>} tokens An array of tokens already processed - * @returns {boolean} true if adding a '-' should be considered a negative - * symbol; false otherwise - * @private - */ -Lexer.prototype._isNegative = function(tokens) { - if (!tokens.length) - return true; - return minusNegatesAfter.some(function(type) { - return type === tokens[tokens.length - 1].type; - }); -}; - -/** - * A utility function to determine if a string consists of only space - * characters. - * @param {string} str A string to be tested - * @returns {boolean} true if the string is empty or consists of only spaces; - * false otherwise. - * @private - */ -Lexer.prototype._isWhitespace = function(str) { - for (var i = 0; i < str.length; i++) { - if (str[i] != ' ') - return false; - } - return true; -}; - -/** - * Removes the beginning and trailing quotes from a string, unescapes any - * escaped quotes on its interior, and unescapes any escaped escape characters. - * Note that this function is not defensive; it assumes that the provided - * string is not empty, and that its first and last characters are actually - * quotes. - * @param {string} str A string whose first and last characters are quotes - * @returns {string} a string with the surrounding quotes stripped and escapes - * properly processed. - * @private - */ -Lexer.prototype._unquote = function(str) { - var quote = str[0], - escQuoteRegex = new RegExp('\\\\' + quote, 'g'); - return str.substr(1, str.length - 2) - .replace(escQuoteRegex, quote) - .replace(escEscRegex, '\\'); -}; - -module.exports = Lexer;
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/Evaluator.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Jexl - * Copyright (c) 2015 TechnologyAdvice - */ - -var handlers = require('./handlers'); - -/** - * The Evaluator takes a Jexl expression tree as generated by the - * {@link Parser} and calculates its value within a given context. The - * collection of transforms, context, and a relative context to be used as the - * root for relative identifiers, are all specific to an Evaluator instance. - * When any of these things change, a new instance is required. However, a - * single instance can be used to simultaneously evaluate many different - * expressions, and does not have to be reinstantiated for each. - * @param {{}} grammar A grammar map against which to evaluate the expression - * tree - * @param {{}} [transforms] A map of transform names to transform functions. A - * transform function takes two arguments: - * - {*} val: A value to be transformed - * - {{}} args: A map of argument keys to their evaluated values, as - * specified in the expression string - * The transform function should return either the transformed value, or - * a Promises/A+ Promise object that resolves with the value and rejects - * or throws only when an unrecoverable error occurs. Transforms should - * generally return undefined when they don't make sense to be used on the - * given value type, rather than throw/reject. An error is only - * appropriate when the transform would normally return a value, but - * cannot due to some other failure. - * @param {{}} [context] A map of variable keys to their values. This will be - * accessed to resolve the value of each non-relative identifier. Any - * Promise values will be passed to the expression as their resolved - * value. - * @param {{}|Array<{}|Array>} [relativeContext] A map or array to be accessed - * to resolve the value of a relative identifier. - * @constructor - */ -var Evaluator = function(grammar, transforms, context, relativeContext) { - this._grammar = grammar; - this._transforms = transforms || {}; - this._context = context || {}; - this._relContext = relativeContext || this._context; -}; - -/** - * Evaluates an expression tree within the configured context. - * @param {{}} ast An expression tree object - * @returns {Promise<*>} resolves with the resulting value of the expression. - */ -Evaluator.prototype.eval = function(ast) { - var self = this; - return Promise.resolve().then(function() { - return handlers[ast.type].call(self, ast); - }); -}; - -/** - * Simultaneously evaluates each expression within an array, and delivers the - * response as an array with the resulting values at the same indexes as their - * originating expressions. - * @param {Array<string>} arr An array of expression strings to be evaluated - * @returns {Promise<Array<{}>>} resolves with the result array - */ -Evaluator.prototype.evalArray = function(arr) { - return Promise.all(arr.map(function(elem) { - return this.eval(elem); - }, this)); -}; - -/** - * Simultaneously evaluates each expression within a map, and delivers the - * response as a map with the same keys, but with the evaluated result for each - * as their value. - * @param {{}} map A map of expression names to expression trees to be - * evaluated - * @returns {Promise<{}>} resolves with the result map. - */ -Evaluator.prototype.evalMap = function(map) { - var keys = Object.keys(map), - result = {}; - var asts = keys.map(function(key) { - return this.eval(map[key]); - }, this); - return Promise.all(asts).then(function(vals) { - vals.forEach(function(val, idx) { - result[keys[idx]] = val; - }); - return result; - }); -}; - -/** - * Applies a filter expression with relative identifier elements to a subject. - * The intent is for the subject to be an array of subjects that will be - * individually used as the relative context against the provided expression - * tree. Only the elements whose expressions result in a truthy value will be - * included in the resulting array. - * - * If the subject is not an array of values, it will be converted to a single- - * element array before running the filter. - * @param {*} subject The value to be filtered; usually an array. If this value is - * not an array, it will be converted to an array with this value as the - * only element. - * @param {{}} expr The expression tree to run against each subject. If the - * tree evaluates to a truthy result, then the value will be included in - * the returned array; otherwise, it will be eliminated. - * @returns {Promise<Array>} resolves with an array of values that passed the - * expression filter. - * @private - */ -Evaluator.prototype._filterRelative = function(subject, expr) { - var promises = []; - if (!Array.isArray(subject)) - subject = [subject]; - subject.forEach(function(elem) { - var evalInst = new Evaluator(this._grammar, this._transforms, - this._context, elem); - promises.push(evalInst.eval(expr)); - }, this); - return Promise.all(promises).then(function(values) { - var results = []; - values.forEach(function(value, idx) { - if (value) - results.push(subject[idx]); - }); - return results; - }); -}; - -/** - * Applies a static filter expression to a subject value. If the filter - * expression evaluates to boolean true, the subject is returned; if false, - * undefined. - * - * For any other resulting value of the expression, this function will attempt - * to respond with the property at that name or index of the subject. - * @param {*} subject The value to be filtered. Usually an Array (for which - * the expression would generally resolve to a numeric index) or an - * Object (for which the expression would generally resolve to a string - * indicating a property name) - * @param {{}} expr The expression tree to run against the subject - * @returns {Promise<*>} resolves with the value of the drill-down. - * @private - */ -Evaluator.prototype._filterStatic = function(subject, expr) { - return this.eval(expr).then(function(res) { - if (typeof res === 'boolean') - return res ? subject : undefined; - return subject[res]; - }); -}; - -module.exports = Evaluator;
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/handlers.js +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Jexl - * Copyright (c) 2015 TechnologyAdvice - */ - -/** - * Evaluates an ArrayLiteral by returning its value, with each element - * independently run through the evaluator. - * @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an - * ObjectLiteral as the top node - * @returns {Promise.<[]>} resolves to a map contained evaluated values. - * @private - */ -exports.ArrayLiteral = function(ast) { - return this.evalArray(ast.value); -}; - -/** - * Evaluates a BinaryExpression node by running the Grammar's evaluator for - * the given operator. - * @param {{type: 'BinaryExpression', operator: <string>, left: {}, - * right: {}}} ast An expression tree with a BinaryExpression as the top - * node - * @returns {Promise<*>} resolves with the value of the BinaryExpression. - * @private - */ -exports.BinaryExpression = function(ast) { - var self = this; - return Promise.all([ - this.eval(ast.left), - this.eval(ast.right) - ]).then(function(arr) { - return self._grammar[ast.operator].eval(arr[0], arr[1]); - }); -}; - -/** - * Evaluates a ConditionalExpression node by first evaluating its test branch, - * and resolving with the consequent branch if the test is truthy, or the - * alternate branch if it is not. If there is no consequent branch, the test - * result will be used instead. - * @param {{type: 'ConditionalExpression', test: {}, consequent: {}, - * alternate: {}}} ast An expression tree with a ConditionalExpression as - * the top node - * @private - */ -exports.ConditionalExpression = function(ast) { - var self = this; - return this.eval(ast.test).then(function(res) { - if (res) { - if (ast.consequent) - return self.eval(ast.consequent); - return res; - } - return self.eval(ast.alternate); - }); -}; - -/** - * Evaluates a FilterExpression by applying it to the subject value. - * @param {{type: 'FilterExpression', relative: <boolean>, expr: {}, - * subject: {}}} ast An expression tree with a FilterExpression as the top - * node - * @returns {Promise<*>} resolves with the value of the FilterExpression. - * @private - */ -exports.FilterExpression = function(ast) { - var self = this; - return this.eval(ast.subject).then(function(subject) { - if (ast.relative) - return self._filterRelative(subject, ast.expr); - return self._filterStatic(subject, ast.expr); - }); -}; - -/** - * Evaluates an Identifier by either stemming from the evaluated 'from' - * expression tree or accessing the context provided when this Evaluator was - * constructed. - * @param {{type: 'Identifier', value: <string>, [from]: {}}} ast An expression - * tree with an Identifier as the top node - * @returns {Promise<*>|*} either the identifier's value, or a Promise that - * will resolve with the identifier's value. - * @private - */ -exports.Identifier = function(ast) { - if (ast.from) { - return this.eval(ast.from).then(function(context) { - if (context === undefined) - return undefined; - if (Array.isArray(context)) - context = context[0]; - return context[ast.value]; - }); - } - else { - return ast.relative ? this._relContext[ast.value] : - this._context[ast.value]; - } -}; - -/** - * Evaluates a Literal by returning its value property. - * @param {{type: 'Literal', value: <string|number|boolean>}} ast An expression - * tree with a Literal as its only node - * @returns {string|number|boolean} The value of the Literal node - * @private - */ -exports.Literal = function(ast) { - return ast.value; -}; - -/** - * Evaluates an ObjectLiteral by returning its value, with each key - * independently run through the evaluator. - * @param {{type: 'ObjectLiteral', value: <{}>}} ast An expression tree with an - * ObjectLiteral as the top node - * @returns {Promise<{}>} resolves to a map contained evaluated values. - * @private - */ -exports.ObjectLiteral = function(ast) { - return this.evalMap(ast.value); -}; - -/** - * Evaluates a Transform node by applying a function from the transforms map - * to the subject value. - * @param {{type: 'Transform', name: <string>, subject: {}}} ast An - * expression tree with a Transform as the top node - * @returns {Promise<*>|*} the value of the transformation, or a Promise that - * will resolve with the transformed value. - * @private - */ -exports.Transform = function(ast) { - var transform = this._transforms[ast.name]; - if (!transform) - throw new Error("Transform '" + ast.name + "' is not defined."); - return Promise.all([ - this.eval(ast.subject), - this.evalArray(ast.args || []) - ]).then(function(arr) { - return transform.apply(null, [arr[0]].concat(arr[1])); - }); -}; - -/** - * Evaluates a Unary expression by passing the right side through the - * operator's eval function. - * @param {{type: 'UnaryExpression', operator: <string>, right: {}}} ast An - * expression tree with a UnaryExpression as the top node - * @returns {Promise<*>} resolves with the value of the UnaryExpression. - * @constructor - */ -exports.UnaryExpression = function(ast) { - var self = this; - return this.eval(ast.right).then(function(right) { - return self._grammar[ast.operator].eval(right); - }); -};
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/grammar.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Jexl - * Copyright (c) 2015 TechnologyAdvice - */ - -/** - * A map of all expression elements to their properties. Note that changes - * here may require changes in the Lexer or Parser. - * @type {{}} - */ -exports.elements = { - '.': {type: 'dot'}, - '[': {type: 'openBracket'}, - ']': {type: 'closeBracket'}, - '|': {type: 'pipe'}, - '{': {type: 'openCurl'}, - '}': {type: 'closeCurl'}, - ':': {type: 'colon'}, - ',': {type: 'comma'}, - '(': {type: 'openParen'}, - ')': {type: 'closeParen'}, - '?': {type: 'question'}, - '+': {type: 'binaryOp', precedence: 30, - eval: function(left, right) { return left + right; }}, - '-': {type: 'binaryOp', precedence: 30, - eval: function(left, right) { return left - right; }}, - '*': {type: 'binaryOp', precedence: 40, - eval: function(left, right) { return left * right; }}, - '/': {type: 'binaryOp', precedence: 40, - eval: function(left, right) { return left / right; }}, - '//': {type: 'binaryOp', precedence: 40, - eval: function(left, right) { return Math.floor(left / right); }}, - '%': {type: 'binaryOp', precedence: 50, - eval: function(left, right) { return left % right; }}, - '^': {type: 'binaryOp', precedence: 50, - eval: function(left, right) { return Math.pow(left, right); }}, - '==': {type: 'binaryOp', precedence: 20, - eval: function(left, right) { return left == right; }}, - '!=': {type: 'binaryOp', precedence: 20, - eval: function(left, right) { return left != right; }}, - '>': {type: 'binaryOp', precedence: 20, - eval: function(left, right) { return left > right; }}, - '>=': {type: 'binaryOp', precedence: 20, - eval: function(left, right) { return left >= right; }}, - '<': {type: 'binaryOp', precedence: 20, - eval: function(left, right) { return left < right; }}, - '<=': {type: 'binaryOp', precedence: 20, - eval: function(left, right) { return left <= right; }}, - '&&': {type: 'binaryOp', precedence: 10, - eval: function(left, right) { return left && right; }}, - '||': {type: 'binaryOp', precedence: 10, - eval: function(left, right) { return left || right; }}, - 'in': {type: 'binaryOp', precedence: 20, - eval: function(left, right) { - if (typeof right === 'string') - return right.indexOf(left) !== -1; - if (Array.isArray(right)) { - return right.some(function(elem) { - return elem == left; - }); - } - return false; - }}, - '!': {type: 'unaryOp', precedence: Infinity, - eval: function(right) { return !right; }} -};
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/Parser.js +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Jexl - * Copyright (c) 2015 TechnologyAdvice - */ - -var handlers = require('./handlers'), - states = require('./states').states; - -/** - * The Parser is a state machine that converts tokens from the {@link Lexer} - * into an Abstract Syntax Tree (AST), capable of being evaluated in any - * context by the {@link Evaluator}. The Parser expects that all tokens - * provided to it are legal and typed properly according to the grammar, but - * accepts that the tokens may still be in an invalid order or in some other - * unparsable configuration that requires it to throw an Error. - * @param {{}} grammar The grammar map to use to parse Jexl strings - * @param {string} [prefix] A string prefix to prepend to the expression string - * for error messaging purposes. This is useful for when a new Parser is - * instantiated to parse an subexpression, as the parent Parser's - * expression string thus far can be passed for a more user-friendly - * error message. - * @param {{}} [stopMap] A mapping of token types to any truthy value. When the - * token type is encountered, the parser will return the mapped value - * instead of boolean false. - * @constructor - */ -function Parser(grammar, prefix, stopMap) { - this._grammar = grammar; - this._state = 'expectOperand'; - this._tree = null; - this._exprStr = prefix || ''; - this._relative = false; - this._stopMap = stopMap || {}; -} - -/** - * Processes a new token into the AST and manages the transitions of the state - * machine. - * @param {{type: <string>}} token A token object, as provided by the - * {@link Lexer#tokenize} function. - * @throws {Error} if a token is added when the Parser has been marked as - * complete by {@link #complete}, or if an unexpected token type is added. - * @returns {boolean|*} the stopState value if this parser encountered a token - * in the stopState mapb; false if tokens can continue. - */ -Parser.prototype.addToken = function(token) { - if (this._state == 'complete') - throw new Error('Cannot add a new token to a completed Parser'); - var state = states[this._state], - startExpr = this._exprStr; - this._exprStr += token.raw; - if (state.subHandler) { - if (!this._subParser) - this._startSubExpression(startExpr); - var stopState = this._subParser.addToken(token); - if (stopState) { - this._endSubExpression(); - if (this._parentStop) - return stopState; - this._state = stopState; - } - } - else if (state.tokenTypes[token.type]) { - var typeOpts = state.tokenTypes[token.type], - handleFunc = handlers[token.type]; - if (typeOpts.handler) - handleFunc = typeOpts.handler; - if (handleFunc) - handleFunc.call(this, token); - if (typeOpts.toState) - this._state = typeOpts.toState; - } - else if (this._stopMap[token.type]) - return this._stopMap[token.type]; - else { - throw new Error('Token ' + token.raw + ' (' + token.type + - ') unexpected in expression: ' + this._exprStr); - } - return false; -}; - -/** - * Processes an array of tokens iteratively through the {@link #addToken} - * function. - * @param {Array<{type: <string>}>} tokens An array of tokens, as provided by - * the {@link Lexer#tokenize} function. - */ -Parser.prototype.addTokens = function(tokens) { - tokens.forEach(this.addToken, this); -}; - -/** - * Marks this Parser instance as completed and retrieves the full AST. - * @returns {{}|null} a full expression tree, ready for evaluation by the - * {@link Evaluator#eval} function, or null if no tokens were passed to - * the parser before complete was called - * @throws {Error} if the parser is not in a state where it's legal to end - * the expression, indicating that the expression is incomplete - */ -Parser.prototype.complete = function() { - if (this._cursor && !states[this._state].completable) - throw new Error('Unexpected end of expression: ' + this._exprStr); - if (this._subParser) - this._endSubExpression(); - this._state = 'complete'; - return this._cursor ? this._tree : null; -}; - -/** - * Indicates whether the expression tree contains a relative path identifier. - * @returns {boolean} true if a relative identifier exists; false otherwise. - */ -Parser.prototype.isRelative = function() { - return this._relative; -}; - -/** - * Ends a subexpression by completing the subParser and passing its result - * to the subHandler configured in the current state. - * @private - */ -Parser.prototype._endSubExpression = function() { - states[this._state].subHandler.call(this, this._subParser.complete()); - this._subParser = null; -}; - -/** - * Places a new tree node at the current position of the cursor (to the 'right' - * property) and then advances the cursor to the new node. This function also - * handles setting the parent of the new node. - * @param {{type: <string>}} node A node to be added to the AST - * @private - */ -Parser.prototype._placeAtCursor = function(node) { - if (!this._cursor) - this._tree = node; - else { - this._cursor.right = node; - this._setParent(node, this._cursor); - } - this._cursor = node; -}; - -/** - * Places a tree node before the current position of the cursor, replacing - * the node that the cursor currently points to. This should only be called in - * cases where the cursor is known to exist, and the provided node already - * contains a pointer to what's at the cursor currently. - * @param {{type: <string>}} node A node to be added to the AST - * @private - */ -Parser.prototype._placeBeforeCursor = function(node) { - this._cursor = this._cursor._parent; - this._placeAtCursor(node); -}; - -/** - * Sets the parent of a node by creating a non-enumerable _parent property - * that points to the supplied parent argument. - * @param {{type: <string>}} node A node of the AST on which to set a new - * parent - * @param {{type: <string>}} parent An existing node of the AST to serve as the - * parent of the new node - * @private - */ -Parser.prototype._setParent = function(node, parent) { - Object.defineProperty(node, '_parent', { - value: parent, - writable: true - }); -}; - -/** - * Prepares the Parser to accept a subexpression by (re)instantiating the - * subParser. - * @param {string} [exprStr] The expression string to prefix to the new Parser - * @private - */ -Parser.prototype._startSubExpression = function(exprStr) { - var endStates = states[this._state].endStates; - if (!endStates) { - this._parentStop = true; - endStates = this._stopMap; - } - this._subParser = new Parser(this._grammar, exprStr, endStates); -}; - -module.exports = Parser;
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/handlers.js +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Jexl - * Copyright (c) 2015 TechnologyAdvice - */ - -/** - * Handles a subexpression that's used to define a transform argument's value. - * @param {{type: <string>}} ast The subexpression tree - */ -exports.argVal = function(ast) { - this._cursor.args.push(ast); -}; - -/** - * Handles new array literals by adding them as a new node in the AST, - * initialized with an empty array. - */ -exports.arrayStart = function() { - this._placeAtCursor({ - type: 'ArrayLiteral', - value: [] - }); -}; - -/** - * Handles a subexpression representing an element of an array literal. - * @param {{type: <string>}} ast The subexpression tree - */ -exports.arrayVal = function(ast) { - if (ast) - this._cursor.value.push(ast); -}; - -/** - * Handles tokens of type 'binaryOp', indicating an operation that has two - * inputs: a left side and a right side. - * @param {{type: <string>}} token A token object - */ -exports.binaryOp = function(token) { - var precedence = this._grammar[token.value].precedence || 0, - parent = this._cursor._parent; - while (parent && parent.operator && - this._grammar[parent.operator].precedence >= precedence) { - this._cursor = parent; - parent = parent._parent; - } - var node = { - type: 'BinaryExpression', - operator: token.value, - left: this._cursor - }; - this._setParent(this._cursor, node); - this._cursor = parent; - this._placeAtCursor(node); -}; - -/** - * Handles successive nodes in an identifier chain. More specifically, it - * sets values that determine how the following identifier gets placed in the - * AST. - */ -exports.dot = function() { - this._nextIdentEncapsulate = this._cursor && - (this._cursor.type != 'BinaryExpression' || - (this._cursor.type == 'BinaryExpression' && this._cursor.right)) && - this._cursor.type != 'UnaryExpression'; - this._nextIdentRelative = !this._cursor || - (this._cursor && !this._nextIdentEncapsulate); - if (this._nextIdentRelative) - this._relative = true; -}; - -/** - * Handles a subexpression used for filtering an array returned by an - * identifier chain. - * @param {{type: <string>}} ast The subexpression tree - */ -exports.filter = function(ast) { - this._placeBeforeCursor({ - type: 'FilterExpression', - expr: ast, - relative: this._subParser.isRelative(), - subject: this._cursor - }); -}; - -/** - * Handles identifier tokens by adding them as a new node in the AST. - * @param {{type: <string>}} token A token object - */ -exports.identifier = function(token) { - var node = { - type: 'Identifier', - value: token.value - }; - if (this._nextIdentEncapsulate) { - node.from = this._cursor; - this._placeBeforeCursor(node); - this._nextIdentEncapsulate = false; - } - else { - if (this._nextIdentRelative) - node.relative = true; - this._placeAtCursor(node); - } -}; - -/** - * Handles literal values, such as strings, booleans, and numerics, by adding - * them as a new node in the AST. - * @param {{type: <string>}} token A token object - */ -exports.literal = function(token) { - this._placeAtCursor({ - type: 'Literal', - value: token.value - }); -}; - -/** - * Queues a new object literal key to be written once a value is collected. - * @param {{type: <string>}} token A token object - */ -exports.objKey = function(token) { - this._curObjKey = token.value; -}; - -/** - * Handles new object literals by adding them as a new node in the AST, - * initialized with an empty object. - */ -exports.objStart = function() { - this._placeAtCursor({ - type: 'ObjectLiteral', - value: {} - }); -}; - -/** - * Handles an object value by adding its AST to the queued key on the object - * literal node currently at the cursor. - * @param {{type: <string>}} ast The subexpression tree - */ -exports.objVal = function(ast) { - this._cursor.value[this._curObjKey] = ast; -}; - -/** - * Handles traditional subexpressions, delineated with the groupStart and - * groupEnd elements. - * @param {{type: <string>}} ast The subexpression tree - */ -exports.subExpression = function(ast) { - this._placeAtCursor(ast); -}; - -/** - * Handles a completed alternate subexpression of a ternary operator. - * @param {{type: <string>}} ast The subexpression tree - */ -exports.ternaryEnd = function(ast) { - this._cursor.alternate = ast; -}; - -/** - * Handles a completed consequent subexpression of a ternary operator. - * @param {{type: <string>}} ast The subexpression tree - */ -exports.ternaryMid = function(ast) { - this._cursor.consequent = ast; -}; - -/** - * Handles the start of a new ternary expression by encapsulating the entire - * AST in a ConditionalExpression node, and using the existing tree as the - * test element. - */ -exports.ternaryStart = function() { - this._tree = { - type: 'ConditionalExpression', - test: this._tree - }; - this._cursor = this._tree; -}; - -/** - * Handles identifier tokens when used to indicate the name of a transform to - * be applied. - * @param {{type: <string>}} token A token object - */ -exports.transform = function(token) { - this._placeBeforeCursor({ - type: 'Transform', - name: token.value, - args: [], - subject: this._cursor - }); -}; - -/** - * Handles token of type 'unaryOp', indicating that the operation has only - * one input: a right side. - * @param {{type: <string>}} token A token object - */ -exports.unaryOp = function(token) { - this._placeAtCursor({ - type: 'UnaryExpression', - operator: token.value - }); -};
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/states.js +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Jexl - * Copyright (c) 2015 TechnologyAdvice - */ - -var h = require('./handlers'); - -/** - * A mapping of all states in the finite state machine to a set of instructions - * for handling or transitioning into other states. Each state can be handled - * in one of two schemes: a tokenType map, or a subHandler. - * - * Standard expression elements are handled through the tokenType object. This - * is an object map of all legal token types to encounter in this state (and - * any unexpected token types will generate a thrown error) to an options - * object that defines how they're handled. The available options are: - * - * {string} toState: The name of the state to which to transition - * immediately after handling this token - * {string} handler: The handler function to call when this token type is - * encountered in this state. If omitted, the default handler - * matching the token's "type" property will be called. If the handler - * function does not exist, no call will be made and no error will be - * generated. This is useful for tokens whose sole purpose is to - * transition to other states. - * - * States that consume a subexpression should define a subHandler, the - * function to be called with an expression tree argument when the - * subexpression is complete. Completeness is determined through the - * endStates object, which maps tokens on which an expression should end to the - * state to which to transition once the subHandler function has been called. - * - * Additionally, any state in which it is legal to mark the AST as completed - * should have a 'completable' property set to boolean true. Attempting to - * call {@link Parser#complete} in any state without this property will result - * in a thrown Error. - * - * @type {{}} - */ -exports.states = { - expectOperand: { - tokenTypes: { - literal: {toState: 'expectBinOp'}, - identifier: {toState: 'identifier'}, - unaryOp: {}, - openParen: {toState: 'subExpression'}, - openCurl: {toState: 'expectObjKey', handler: h.objStart}, - dot: {toState: 'traverse'}, - openBracket: {toState: 'arrayVal', handler: h.arrayStart} - } - }, - expectBinOp: { - tokenTypes: { - binaryOp: {toState: 'expectOperand'}, - pipe: {toState: 'expectTransform'}, - dot: {toState: 'traverse'}, - question: {toState: 'ternaryMid', handler: h.ternaryStart} - }, - completable: true - }, - expectTransform: { - tokenTypes: { - identifier: {toState: 'postTransform', handler: h.transform} - } - }, - expectObjKey: { - tokenTypes: { - identifier: {toState: 'expectKeyValSep', handler: h.objKey}, - closeCurl: {toState: 'expectBinOp'} - } - }, - expectKeyValSep: { - tokenTypes: { - colon: {toState: 'objVal'} - } - }, - postTransform: { - tokenTypes: { - openParen: {toState: 'argVal'}, - binaryOp: {toState: 'expectOperand'}, - dot: {toState: 'traverse'}, - openBracket: {toState: 'filter'}, - pipe: {toState: 'expectTransform'} - }, - completable: true - }, - postTransformArgs: { - tokenTypes: { - binaryOp: {toState: 'expectOperand'}, - dot: {toState: 'traverse'}, - openBracket: {toState: 'filter'}, - pipe: {toState: 'expectTransform'} - }, - completable: true - }, - identifier: { - tokenTypes: { - binaryOp: {toState: 'expectOperand'}, - dot: {toState: 'traverse'}, - openBracket: {toState: 'filter'}, - pipe: {toState: 'expectTransform'}, - question: {toState: 'ternaryMid', handler: h.ternaryStart} - }, - completable: true - }, - traverse: { - tokenTypes: { - 'identifier': {toState: 'identifier'} - } - }, - filter: { - subHandler: h.filter, - endStates: { - closeBracket: 'identifier' - } - }, - subExpression: { - subHandler: h.subExpression, - endStates: { - closeParen: 'expectBinOp' - } - }, - argVal: { - subHandler: h.argVal, - endStates: { - comma: 'argVal', - closeParen: 'postTransformArgs' - } - }, - objVal: { - subHandler: h.objVal, - endStates: { - comma: 'expectObjKey', - closeCurl: 'expectBinOp' - } - }, - arrayVal: { - subHandler: h.arrayVal, - endStates: { - comma: 'arrayVal', - closeBracket: 'expectBinOp' - } - }, - ternaryMid: { - subHandler: h.ternaryMid, - endStates: { - colon: 'ternaryEnd' - } - }, - ternaryEnd: { - subHandler: h.ternaryEnd, - completable: true - } -};
--- a/browser/extensions/shield-recipe-client/skin/osx/Heartbeat.css +++ b/browser/extensions/shield-recipe-client/skin/osx/Heartbeat.css @@ -4,8 +4,21 @@ /* Notification overrides for Heartbeat UI */ notification.heartbeat { background-image: linear-gradient(-179deg, #FBFBFB 0%, #EBEBEB 100%) !important; border-bottom: 1px solid #C1C1C1 !important; height: 40px; } + +/* In themes/osx/global/notification.css the close icon is inverted because notifications + on OSX are usually dark. Heartbeat is light, so override that behaviour. */ + +notification.heartbeat[type="info"] .close-icon:not(:hover) { + -moz-image-region: rect(0, 16px, 16px, 0) !important; +} + +@media (min-resolution: 2dppx) { + notification.heartbeat[type="info"] .close-icon:not(:hover) { + -moz-image-region: rect(0, 32px, 32px, 0) !important; + } +}
--- a/browser/extensions/shield-recipe-client/skin/shared/Heartbeat.css +++ b/browser/extensions/shield-recipe-client/skin/shared/Heartbeat.css @@ -3,17 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* Notification overrides for Heartbeat UI */ notification.heartbeat { background-color: #F1F1F1 !important; border-bottom: 1px solid #C1C1C1 !important; height: 40px; - color: #333 !important; } @keyframes pulse-onshow { 0% { opacity: 0; transform: scale(1); } @@ -45,16 +44,17 @@ notification.heartbeat { } 100% { transform: scale(1); } } .messageText.heartbeat { + color: #333 !important; margin-inline-end: 12px !important; /* The !important is required to override OSX default style. */ margin-inline-start: 0; text-shadow: none; } .messageImage.heartbeat { height: 24px !important; margin-inline-end: 8px !important;
--- a/browser/extensions/shield-recipe-client/test/browser/.eslintrc.js +++ b/browser/extensions/shield-recipe-client/test/browser/.eslintrc.js @@ -3,15 +3,9 @@ module.exports = { extends: [ "plugin:mozilla/browser-test" ], plugins: [ "mozilla" ], - - globals: { - // Bug 1366720 - SimpleTest isn't being exported correctly, so list - // it here for now. - "SimpleTest": false - } };
--- a/browser/extensions/shield-recipe-client/test/browser/browser.ini +++ b/browser/extensions/shield-recipe-client/test/browser/browser.ini @@ -1,14 +1,22 @@ [DEFAULT] +support-files = + action_server.sjs + fixtures/normandy.xpi head = head.js +[browser_ActionSandboxManager.js] +[browser_Addons.js] +[browser_AddonStudies.js] [browser_NormandyDriver.js] [browser_FilterExpressions.js] [browser_EventEmitter.js] [browser_Storage.js] [browser_Heartbeat.js] [browser_RecipeRunner.js] -support-files = - action_server.sjs [browser_LogManager.js] [browser_ClientEnvironment.js] [browser_ShieldRecipeClient.js] [browser_PreferenceExperiments.js] +[browser_about_studies.js] +[browser_about_preferences.js] +# Skip this test when FHR/Telemetry aren't available. +skip-if = !healthreport || !telemetry
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/browser/browser_ActionSandboxManager.js @@ -0,0 +1,167 @@ +"use strict"; + +Cu.import("resource://shield-recipe-client/lib/ActionSandboxManager.jsm", this); +Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this); + +async function withManager(script, testFunction) { + const manager = new ActionSandboxManager(script); + manager.addHold("testing"); + await testFunction(manager); + manager.removeHold("testing"); +} + +add_task(async function testMissingCallbackName() { + await withManager("1 + 1", async manager => { + is( + await manager.runAsyncCallback("missingCallback"), + undefined, + "runAsyncCallback returns undefined when given a missing callback name", + ); + }); +}); + +add_task(async function testCallback() { + const script = ` + registerAsyncCallback("testCallback", async function(normandy) { + return 5; + }); + `; + + await withManager(script, async manager => { + const result = await manager.runAsyncCallback("testCallback"); + is(result, 5, "runAsyncCallback executes the named callback inside the sandbox"); + }); +}); + +add_task(async function testArguments() { + const script = ` + registerAsyncCallback("testCallback", async function(normandy, a, b) { + return a + b; + }); + `; + + await withManager(script, async manager => { + const result = await manager.runAsyncCallback("testCallback", 4, 6); + is(result, 10, "runAsyncCallback passes arguments to the callback"); + }); +}); + +add_task(async function testCloning() { + const script = ` + registerAsyncCallback("testCallback", async function(normandy, obj) { + return {foo: "bar", baz: obj.baz}; + }); + `; + + await withManager(script, async manager => { + const result = await manager.runAsyncCallback("testCallback", {baz: "biff"}); + + Assert.deepEqual( + result, + {foo: "bar", baz: "biff"}, + ( + "runAsyncCallback clones arguments into the sandbox and return values into the " + + "context it was called from" + ), + ); + }); +}); + +add_task(async function testError() { + const script = ` + registerAsyncCallback("testCallback", async function(normandy) { + throw new Error("WHY") + }); + `; + + await withManager(script, async manager => { + try { + await manager.runAsyncCallback("testCallback"); + ok(false, "runAsnycCallbackFromScript throws errors when raised by the sandbox"); + } catch (err) { + is(err.message, "WHY", "runAsnycCallbackFromScript throws errors when raised by the sandbox"); + } + }); +}); + +add_task(async function testDriver() { + // The value returned by runAsyncCallback is cloned without the cloneFunctions + // option, so we can't inspect the driver itself since its methods will not be + // present. Instead, we inspect the properties on it available to the sandbox. + const script = ` + registerAsyncCallback("testCallback", async function(normandy) { + return Object.keys(normandy); + }); + `; + + await withManager(script, async manager => { + const sandboxDriverKeys = await manager.runAsyncCallback("testCallback"); + const referenceDriver = new NormandyDriver(manager); + for (const prop of Object.keys(referenceDriver)) { + ok(sandboxDriverKeys.includes(prop), `runAsyncCallback's driver has the "${prop}" property.`); + } + }); +}); + +add_task(async function testGlobalObject() { + // Test that window is an alias for the global object, and that it + // has some expected functions available on it. + const script = ` + window.setOnWindow = "set"; + this.setOnGlobal = "set"; + + registerAsyncCallback("testCallback", async function(normandy) { + return { + setOnWindow: setOnWindow, + setOnGlobal: window.setOnGlobal, + setTimeoutExists: setTimeout !== undefined, + clearTimeoutExists: clearTimeout !== undefined, + }; + }); + `; + + await withManager(script, async manager => { + const result = await manager.runAsyncCallback("testCallback"); + Assert.deepEqual(result, { + setOnWindow: "set", + setOnGlobal: "set", + setTimeoutExists: true, + clearTimeoutExists: true, + }, "sandbox.window is the global object and has expected functions."); + }); +}); + +add_task(async function testRegisterActionShim() { + const recipe = { + foo: "bar", + }; + const script = ` + class TestAction { + constructor(driver, recipe) { + this.driver = driver; + this.recipe = recipe; + } + + execute() { + return new Promise(resolve => { + resolve({ + foo: this.recipe.foo, + isDriver: "log" in this.driver, + }); + }); + } + } + + registerAction('test-action', TestAction); + `; + + await withManager(script, async manager => { + const result = await manager.runAsyncCallback("action", recipe); + is(result.foo, "bar", "registerAction registers an async callback for actions"); + is( + result.isDriver, + true, + "registerAction passes the driver to the action class constructor", + ); + }); +});
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/browser/browser_AddonStudies.js @@ -0,0 +1,325 @@ +"use strict"; + +Cu.import("resource://gre/modules/IndexedDB.jsm", this); +Cu.import("resource://testing-common/TestUtils.jsm", this); +Cu.import("resource://testing-common/AddonTestUtils.jsm", this); +Cu.import("resource://shield-recipe-client/lib/Addons.jsm", this); +Cu.import("resource://shield-recipe-client/lib/AddonStudies.jsm", this); + +// Initialize test utils +AddonTestUtils.initMochitest(this); + +decorate_task( + AddonStudies.withStudies(), + async function testGetMissing() { + is( + await AddonStudies.get("does-not-exist"), + null, + "get returns null when the requested study does not exist" + ); + } +); + +decorate_task( + AddonStudies.withStudies([ + studyFactory({name: "test-study"}), + ]), + async function testGet([study]) { + const storedStudy = await AddonStudies.get(study.recipeId); + Assert.deepEqual(study, storedStudy, "get retrieved a study from storage."); + } +); + +decorate_task( + AddonStudies.withStudies([ + studyFactory(), + studyFactory(), + ]), + async function testGetAll(studies) { + const storedStudies = await AddonStudies.getAll(); + Assert.deepEqual( + new Set(storedStudies), + new Set(studies), + "getAll returns every stored study.", + ); + } +); + +decorate_task( + AddonStudies.withStudies([ + studyFactory({name: "test-study"}), + ]), + async function testHas([study]) { + let hasStudy = await AddonStudies.has(study.recipeId); + ok(hasStudy, "has returns true for a study that exists in storage."); + + hasStudy = await AddonStudies.has("does-not-exist"); + ok(!hasStudy, "has returns false for a study that doesn't exist in storage."); + } +); + +decorate_task( + AddonStudies.withStudies(), + async function testCloseDatabase() { + await AddonStudies.close(); + const openSpy = sinon.spy(IndexedDB, "open"); + sinon.assert.notCalled(openSpy); + + // Using studies at all should open the database, but only once. + await AddonStudies.has("foo"); + await AddonStudies.get("foo"); + sinon.assert.calledOnce(openSpy); + + // close can be called multiple times + await AddonStudies.close(); + await AddonStudies.close(); + + // After being closed, new operations cause the database to be opened again + await AddonStudies.has("test-study"); + sinon.assert.calledTwice(openSpy); + + openSpy.restore(); + } +); + +decorate_task( + AddonStudies.withStudies([ + studyFactory({name: "test-study1"}), + studyFactory({name: "test-study2"}), + ]), + async function testClear([study1, study2]) { + const hasAll = ( + (await AddonStudies.has(study1.recipeId)) && + (await AddonStudies.has(study2.recipeId)) + ); + ok(hasAll, "Before calling clear, both studies are in storage."); + + await AddonStudies.clear(); + const hasAny = ( + (await AddonStudies.has(study1.recipeId)) || + (await AddonStudies.has(study2.recipeId)) + ); + ok(!hasAny, "After calling clear, all studies are removed from storage."); + } +); + +let _startArgsFactoryId = 0; +function startArgsFactory(args) { + return Object.assign({ + recipeId: _startArgsFactoryId++, + name: "Test", + description: "Test", + addonUrl: "http://test/addon.xpi", + }, args); +} + +add_task(async function testStartRequiredArguments() { + const requiredArguments = startArgsFactory(); + for (const key in requiredArguments) { + const args = Object.assign({}, requiredArguments); + delete args[key]; + Assert.rejects( + AddonStudies.start(args), + /Required arguments/, + `start rejects when missing required argument ${key}.` + ); + } +}); + +decorate_task( + AddonStudies.withStudies([ + studyFactory(), + ]), + async function testStartExisting([study]) { + Assert.rejects( + AddonStudies.start(startArgsFactory({recipeId: study.recipeId})), + /already exists/, + "start rejects when a study exists with the given recipeId already." + ); + } +); + +decorate_task( + withStub(Addons, "applyInstall"), + withWebExtension(), + async function testStartAddonCleanup(applyInstallStub, [addonId, addonFile]) { + applyInstallStub.rejects(new Error("Fake failure")); + + const addonUrl = Services.io.newFileURI(addonFile).spec; + await Assert.rejects( + AddonStudies.start(startArgsFactory({addonUrl})), + /Fake failure/, + "start rejects when the Addons.applyInstall function rejects" + ); + + const addon = await Addons.get(addonId); + ok(!addon, "If something fails during start after the add-on is installed, it is uninstalled."); + } +); + +const testOverwriteId = "testStartAddonNoOverwrite@example.com"; +decorate_task( + withInstalledWebExtension({version: "1.0", id: testOverwriteId}), + withWebExtension({version: "2.0", id: testOverwriteId}), + async function testStartAddonNoOverwrite([installedId, installedFile], [id, addonFile]) { + const addonUrl = Services.io.newFileURI(addonFile).spec; + await Assert.rejects( + AddonStudies.start(startArgsFactory({addonUrl})), + /updating is disabled/, + "start rejects when the study add-on is already installed" + ); + + await Addons.uninstall(testOverwriteId); + } +); + +decorate_task( + withWebExtension({version: "2.0"}), + async function testStart([addonId, addonFile]) { + const startupPromise = AddonTestUtils.promiseWebExtensionStartup(addonId); + const addonUrl = Services.io.newFileURI(addonFile).spec; + + let addon = await Addons.get(addonId); + is(addon, null, "Before start is called, the add-on is not installed."); + + const args = startArgsFactory({ + name: "Test Study", + description: "Test Desc", + addonUrl, + }); + await AddonStudies.start(args); + await startupPromise; + + addon = await Addons.get(addonId); + ok(addon, "After start is called, the add-on is installed."); + + const study = await AddonStudies.get(args.recipeId); + Assert.deepEqual( + study, + { + recipeId: args.recipeId, + name: args.name, + description: args.description, + addonId, + addonVersion: "2.0", + addonUrl, + active: true, + studyStartDate: study.studyStartDate, + }, + "start saves study data to storage", + ); + ok(study.studyStartDate, "start assigns a value to the study start date."); + + await Addons.uninstall(addonId); + } +); + +decorate_task( + AddonStudies.withStudies(), + async function testStopNoStudy() { + await Assert.rejects( + AddonStudies.stop("does-not-exist"), + /No study found/, + "stop rejects when no study exists for the given recipe." + ); + } +); + +decorate_task( + AddonStudies.withStudies([ + studyFactory({active: false}), + ]), + async function testStopInactiveStudy([study]) { + await Assert.rejects( + AddonStudies.stop(study.recipeId), + /already inactive/, + "stop rejects when the requested study is already inactive." + ); + } +); + +const testStopId = "testStop@example.com"; +decorate_task( + AddonStudies.withStudies([ + studyFactory({active: true, addonId: testStopId, studyEndDate: null}), + ]), + withInstalledWebExtension({id: testStopId}), + async function testStop([study], [addonId, addonFile]) { + await AddonStudies.stop(study.recipeId); + const newStudy = await AddonStudies.get(study.recipeId); + ok(!newStudy.active, "stop marks the study as inactive."); + ok(newStudy.studyEndDate, "stop saves the study end date."); + + const addon = await Addons.get(addonId); + is(addon, null, "stop uninstalls the study add-on."); + } +); + +decorate_task( + AddonStudies.withStudies([ + studyFactory({active: true, addonId: "testStopWarn@example.com", studyEndDate: null}), + ]), + async function testStopWarn([study]) { + const addon = await Addons.get("testStopWarn@example.com"); + is(addon, null, "Before start is called, the add-on is not installed."); + + // If the add-on is not installed, log a warning to the console, but do not + // throw. + await new Promise(resolve => { + SimpleTest.waitForExplicitFinish(); + SimpleTest.monitorConsole(resolve, [{message: /Could not uninstall addon/}]); + AddonStudies.stop(study.recipeId).then(() => SimpleTest.endMonitorConsole()); + }); + } +); + +decorate_task( + AddonStudies.withStudies([ + studyFactory({active: true, addonId: "does.not.exist@example.com", studyEndDate: null}), + studyFactory({active: true, addonId: "installed@example.com"}), + studyFactory({active: false, addonId: "already.gone@example.com", studyEndDate: new Date(2012, 1)}), + ]), + withInstalledWebExtension({id: "installed@example.com"}), + async function testInit([activeStudy, activeInstalledStudy, inactiveStudy]) { + await AddonStudies.init(); + + const newActiveStudy = await AddonStudies.get(activeStudy.recipeId); + ok(!newActiveStudy.active, "init marks studies as inactive if their add-on is not installed."); + ok( + newActiveStudy.studyEndDate, + "init sets the study end date if a study's add-on is not installed." + ); + + const newInactiveStudy = await AddonStudies.get(inactiveStudy.recipeId); + is( + newInactiveStudy.studyEndDate.getFullYear(), + 2012, + "init does not modify inactive studies." + ); + + const newActiveInstalledStudy = await AddonStudies.get(activeInstalledStudy.recipeId); + Assert.deepEqual( + activeInstalledStudy, + newActiveInstalledStudy, + "init does not modify studies whose add-on is still installed." + ); + } +); + +decorate_task( + AddonStudies.withStudies([ + studyFactory({active: true, addonId: "installed@example.com", studyEndDate: null}), + ]), + withInstalledWebExtension({id: "installed@example.com"}), + async function testInit([study], [id, addonFile]) { + await Addons.uninstall(id); + await TestUtils.topicObserved("shield-study-ended"); + + const newStudy = await AddonStudies.get(study.recipeId); + ok(!newStudy.active, "Studies are marked as inactive when their add-on is uninstalled."); + ok( + newStudy.studyEndDate, + "The study end date is set when the add-on for the study is uninstalled." + ); + } +);
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/browser/browser_Addons.js @@ -0,0 +1,34 @@ +"use strict"; + +Cu.import("resource://testing-common/AddonTestUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://shield-recipe-client/lib/Addons.jsm", this); + +// Initialize test utils +AddonTestUtils.initMochitest(this); + +const testInstallId = "testInstallUpdate@example.com"; +decorate_task( + withInstalledWebExtension({version: "1.0", id: testInstallId}), + withWebExtension({version: "2.0", id: testInstallId}), + async function testInstallUpdate([id1, addonFile1], [id2, addonFile2]) { + // Fail to install the 2.0 add-on without updating enabled + const newAddonUrl = Services.io.newFileURI(addonFile2).spec; + await Assert.rejects( + Addons.install(newAddonUrl, {update: false}), + /updating is disabled/, + "install rejects when the study add-on is already installed and updating is disabled" + ); + + // Install the new add-on with updating enabled + const startupPromise = AddonTestUtils.promiseWebExtensionStartup(testInstallId); + await Addons.install(newAddonUrl, {update: true}); + + const addon = await startupPromise; + is( + addon.version, + "2.0", + "install can successfully update an already-installed addon when updating is enabled." + ); + } +);
--- a/browser/extensions/shield-recipe-client/test/browser/browser_ClientEnvironment.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_ClientEnvironment.js @@ -1,14 +1,18 @@ "use strict"; +Cu.import("resource://gre/modules/Services.jsm", this); Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/AddonManager.jsm", this); +Cu.import("resource://testing-common/AddonTestUtils.jsm", this); Cu.import("resource://shield-recipe-client/lib/ClientEnvironment.jsm", this); Cu.import("resource://shield-recipe-client/lib/PreferenceExperiments.jsm", this); + add_task(async function testTelemetry() { // setup await TelemetryController.submitExternalPing("testfoo", {foo: 1}); await TelemetryController.submitExternalPing("testbar", {bar: 2}); const environment = ClientEnvironment.getEnvironment(); // Test it can access telemetry const telemetry = await environment.telemetry; @@ -105,20 +109,33 @@ add_task(async function testExperiments( experiments.expired, ["expired"], "experiments.expired returns all expired experiment names", ); getAll.restore(); }); -add_task(async function isFirstRun() { - let environment = ClientEnvironment.getEnvironment(); +add_task(withDriver(Assert, async function testAddonsInContext(driver) { + // Create before install so that the listener is added before startup completes. + const startupPromise = AddonTestUtils.promiseWebExtensionStartup("normandydriver@example.com"); + const addonId = await driver.addons.install(TEST_XPI_URL); + await startupPromise; - // isFirstRun is initially set to true - ok(environment.isFirstRun, "isFirstRun has a default value"); + const environment = ClientEnvironment.getEnvironment(); + const addons = await environment.addons; + Assert.deepEqual(addons[addonId], { + id: [addonId], + name: "normandy_fixture", + version: "1.0", + installDate: addons[addonId].installDate, + isActive: true, + type: "extension", + }, "addons should be available in context"); - // isFirstRun is read from a preference + await driver.addons.uninstall(addonId); +})); + +add_task(async function isFirstRun() { await SpecialPowers.pushPrefEnv({set: [["extensions.shield-recipe-client.first_run", true]]}); - environment = ClientEnvironment.getEnvironment(); + const environment = ClientEnvironment.getEnvironment(); ok(environment.isFirstRun, "isFirstRun is read from preferences"); }); -
--- a/browser/extensions/shield-recipe-client/test/browser/browser_EventEmitter.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_EventEmitter.js @@ -21,107 +21,113 @@ function listenerB(x = 1) { evidence.log += "b"; } function listenerC(x = 1) { evidence.c += x; evidence.log += "c"; } -add_task(withSandboxManager(Assert, async function(sandboxManager) { - const eventEmitter = new EventEmitter(sandboxManager); +decorate_task( + withSandboxManager(Assert), + async function(sandboxManager) { + const eventEmitter = new EventEmitter(sandboxManager); - // Fire an unrelated event, to make sure nothing goes wrong - eventEmitter.on("nothing"); + // Fire an unrelated event, to make sure nothing goes wrong + eventEmitter.on("nothing"); - // bind listeners - eventEmitter.on("event", listenerA); - eventEmitter.on("event", listenerB); - eventEmitter.once("event", listenerC); + // bind listeners + eventEmitter.on("event", listenerA); + eventEmitter.on("event", listenerB); + eventEmitter.once("event", listenerC); - // one event for all listeners - eventEmitter.emit("event"); - // another event for a and b, since c should have turned off already - eventEmitter.emit("event", 10); + // one event for all listeners + eventEmitter.emit("event"); + // another event for a and b, since c should have turned off already + eventEmitter.emit("event", 10); - // make sure events haven't actually fired yet, just queued - Assert.deepEqual(evidence, { - a: 0, - b: 0, - c: 0, - log: "", - }, "events are fired async"); + // make sure events haven't actually fired yet, just queued + Assert.deepEqual(evidence, { + a: 0, + b: 0, + c: 0, + log: "", + }, "events are fired async"); - // Spin the event loop to run events, so we can safely "off" - await Promise.resolve(); + // Spin the event loop to run events, so we can safely "off" + await Promise.resolve(); - // Check intermediate event results - Assert.deepEqual(evidence, { - a: 11, - b: 11, - c: 1, - log: "abcab", - }, "intermediate events are fired"); + // Check intermediate event results + Assert.deepEqual(evidence, { + a: 11, + b: 11, + c: 1, + log: "abcab", + }, "intermediate events are fired"); - // one more event for a - eventEmitter.off("event", listenerB); - eventEmitter.emit("event", 100); + // one more event for a + eventEmitter.off("event", listenerB); + eventEmitter.emit("event", 100); - // And another unrelated event - eventEmitter.on("nothing"); + // And another unrelated event + eventEmitter.on("nothing"); - // Spin the event loop to run events - await Promise.resolve(); + // Spin the event loop to run events + await Promise.resolve(); - Assert.deepEqual(evidence, { - a: 111, - b: 11, - c: 1, - log: "abcaba", // events are in order - }, "events fired as expected"); + Assert.deepEqual(evidence, { + a: 111, + b: 11, + c: 1, + log: "abcaba", // events are in order + }, "events fired as expected"); - // Test that mutating the data passed to the event doesn't actually - // mutate it for other events. - let handlerRunCount = 0; - const mutationHandler = data => { - handlerRunCount++; - data.count++; - is(data.count, 1, "Event data is not mutated between handlers."); - }; - eventEmitter.on("mutationTest", mutationHandler); - eventEmitter.on("mutationTest", mutationHandler); + // Test that mutating the data passed to the event doesn't actually + // mutate it for other events. + let handlerRunCount = 0; + const mutationHandler = data => { + handlerRunCount++; + data.count++; + is(data.count, 1, "Event data is not mutated between handlers."); + }; + eventEmitter.on("mutationTest", mutationHandler); + eventEmitter.on("mutationTest", mutationHandler); - const data = {count: 0}; - eventEmitter.emit("mutationTest", data); - await Promise.resolve(); + const data = {count: 0}; + eventEmitter.emit("mutationTest", data); + await Promise.resolve(); - is(handlerRunCount, 2, "Mutation handler was executed twice."); - is(data.count, 0, "Event data cannot be mutated by handlers."); -})); + is(handlerRunCount, 2, "Mutation handler was executed twice."); + is(data.count, 0, "Event data cannot be mutated by handlers."); + } +); -add_task(withSandboxManager(Assert, async function sandboxedEmitter(sandboxManager) { - const eventEmitter = new EventEmitter(sandboxManager); +decorate_task( + withSandboxManager(Assert), + async function sandboxedEmitter(sandboxManager) { + const eventEmitter = new EventEmitter(sandboxManager); - // Event handlers inside the sandbox should be run in response to - // events triggered outside the sandbox. - sandboxManager.addGlobal("emitter", eventEmitter.createSandboxedEmitter()); - sandboxManager.evalInSandbox(` - this.eventCounts = {on: 0, once: 0}; - emitter.on("event", value => { - this.eventCounts.on += value; - }); - emitter.once("eventOnce", value => { - this.eventCounts.once += value; - }); - `); + // Event handlers inside the sandbox should be run in response to + // events triggered outside the sandbox. + sandboxManager.addGlobal("emitter", eventEmitter.createSandboxedEmitter()); + sandboxManager.evalInSandbox(` + this.eventCounts = {on: 0, once: 0}; + emitter.on("event", value => { + this.eventCounts.on += value; + }); + emitter.once("eventOnce", value => { + this.eventCounts.once += value; + }); + `); - eventEmitter.emit("event", 5); - eventEmitter.emit("event", 10); - eventEmitter.emit("eventOnce", 5); - eventEmitter.emit("eventOnce", 10); - await Promise.resolve(); + eventEmitter.emit("event", 5); + eventEmitter.emit("event", 10); + eventEmitter.emit("eventOnce", 5); + eventEmitter.emit("eventOnce", 10); + await Promise.resolve(); - const eventCounts = sandboxManager.evalInSandbox("this.eventCounts"); - Assert.deepEqual(eventCounts, { - on: 15, - once: 5, - }, "Events emitted outside a sandbox trigger handlers within a sandbox."); -})); + const eventCounts = sandboxManager.evalInSandbox("this.eventCounts"); + Assert.deepEqual(eventCounts, { + on: 15, + once: 5, + }, "Events emitted outside a sandbox trigger handlers within a sandbox."); + } +);
--- a/browser/extensions/shield-recipe-client/test/browser/browser_FilterExpressions.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_FilterExpressions.js @@ -86,8 +86,103 @@ add_task(async function() { ok(val, "preferenceIsUserSet expression determines if user's preference has been set"); // Compare if the preference has _any_ value, whether it's user-set or default, val = await FilterExpressions.eval('"normandy.test.nonexistant"|preferenceExists == true'); ok(!val, "preferenceExists expression determines if preference exists at all"); val = await FilterExpressions.eval('"normandy.test.value"|preferenceExists == true'); ok(val, "preferenceExists expression fails existence check appropriately"); }); + +// keys tests +add_task(async function testKeys() { + let val; + + // Test an object defined in JEXL + val = await FilterExpressions.eval("{foo: 1, bar: 2}|keys"); + Assert.deepEqual( + new Set(val), + new Set(["foo", "bar"]), + "keys returns the keys from an object in JEXL", + ); + + // Test an object in the context + let context = {ctxObject: {baz: "string", biff: NaN}}; + val = await FilterExpressions.eval("ctxObject|keys", context); + + Assert.deepEqual( + new Set(val), + new Set(["baz", "biff"]), + "keys returns the keys from an object in the context", + ); + + // Test that values from the prototype are not included + context = {ctxObject: Object.create({fooProto: 7})}; + context.ctxObject.baz = 8; + context.ctxObject.biff = 5; + is( + await FilterExpressions.eval("ctxObject.fooProto", context), + 7, + "Prototype properties are accessible via property access", + ); + val = await FilterExpressions.eval("ctxObject|keys", context); + Assert.deepEqual( + new Set(val), + new Set(["baz", "biff"]), + "keys does not return properties from the object's prototype chain", + ); + + // Return undefined for non-objects + is( + await FilterExpressions.eval("ctxObject|keys", {ctxObject: 45}), + undefined, + "keys returns undefined for numbers", + ); + is( + await FilterExpressions.eval("ctxObject|keys", {ctxObject: null}), + undefined, + "keys returns undefined for null", + ); +}); + +// intersect tests +add_task(async function testIntersect() { + let val; + + val = await FilterExpressions.eval("[1, 2, 3] intersect [4, 2, 6, 7, 3]"); + Assert.deepEqual( + new Set(val), + new Set([2, 3]), + "intersect finds the common elements between two lists in JEXL", + ); + + const context = {left: [5, 7], right: [4, 5, 3]}; + val = await FilterExpressions.eval("left intersect right", context); + Assert.deepEqual( + new Set(val), + new Set([5]), + "intersect finds the common elements between two lists in the context", + ); + + val = await FilterExpressions.eval("['string', 2] intersect [4, 'string', 'other', 3]"); + Assert.deepEqual( + new Set(val), + new Set(["string"]), + "intersect can compare strings", + ); + + // Return undefined when intersecting things that aren't lists. + is( + await FilterExpressions.eval("5 intersect 7"), + undefined, + "intersect returns undefined for numbers", + ); + is( + await FilterExpressions.eval("val intersect other", {val: null, other: null}), + undefined, + "intersect returns undefined for null", + ); + is( + await FilterExpressions.eval("5 intersect [1, 2, 5]"), + undefined, + "intersect returns undefined if only one operand is a list", + ); +});
--- a/browser/extensions/shield-recipe-client/test/browser/browser_NormandyDriver.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_NormandyDriver.js @@ -1,22 +1,68 @@ "use strict"; +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://testing-common/AddonTestUtils.jsm", this); +Cu.import("resource://shield-recipe-client/lib/AddonStudies.jsm", this); Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this); add_task(withDriver(Assert, async function uuids(driver) { // Test that it is a UUID const uuid1 = driver.uuid(); ok(UUID_REGEX.test(uuid1), "valid uuid format"); // Test that UUIDs are different each time const uuid2 = driver.uuid(); isnot(uuid1, uuid2, "uuids are unique"); })); +add_task(withDriver(Assert, async function installXpi(driver) { + // Test that we can install an XPI from any URL + // Create before install so that the listener is added before startup completes. + const startupPromise = AddonTestUtils.promiseWebExtensionStartup("normandydriver@example.com"); + + var addonId = await driver.addons.install(TEST_XPI_URL); + is(addonId, "normandydriver@example.com", "Expected test addon was installed"); + isnot(addonId, null, "Addon install was successful"); + + // Wait until the add-on is fully started up to uninstall it. + await startupPromise; + + const uninstallMsg = await driver.addons.uninstall(addonId); + is(uninstallMsg, null, `Uninstall returned an unexpected message [${uninstallMsg}]`); +})); + +add_task(withDriver(Assert, async function uninstallInvalidAddonId(driver) { + const invalidAddonId = "not_a_valid_xpi_id@foo.bar"; + try { + await driver.addons.uninstall(invalidAddonId); + ok(false, `Uninstalling an invalid XPI should fail. addons.uninstall resolved successfully though.`); + } catch (e) { + ok(true, `This is the expected failure`); + } +})); + + +add_task(withDriver(Assert, async function installXpiBadURL(driver) { + let xpiUrl; + if (AppConstants.platform === "win") { + xpiUrl = "file:///C:/invalid_xpi.xpi"; + } else { + xpiUrl = "file:///tmp/invalid_xpi.xpi"; + } + + try { + await driver.addons.install(xpiUrl); + ok(false, "Installation succeeded on an XPI that doesn't exist"); + } catch (reason) { + ok(true, `Installation was rejected: [${reason}]`); + } +})); + add_task(withDriver(Assert, async function userId(driver) { // Test that userId is a UUID ok(UUID_REGEX.test(driver.userId), "userId is a uuid"); })); add_task(withDriver(Assert, async function syncDeviceCounts(driver) { let client = await driver.client(); is(client.syncMobileDevices, 0, "syncMobileDevices defaults to zero"); @@ -40,43 +86,232 @@ add_task(withDriver(Assert, async functi let client = await driver.client(); is(client.distribution, "default", "distribution has a default value"); await SpecialPowers.pushPrefEnv({set: [["distribution.id", "funnelcake"]]}); client = await driver.client(); is(client.distribution, "funnelcake", "distribution is read from preferences"); })); -add_task(withSandboxManager(Assert, async function testCreateStorage(sandboxManager) { - const driver = new NormandyDriver(sandboxManager); - sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true}); +decorate_task( + withSandboxManager(Assert), + async function testCreateStorage(sandboxManager) { + const driver = new NormandyDriver(sandboxManager); + sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true}); + + // Assertion helpers + sandboxManager.addGlobal("is", is); + sandboxManager.addGlobal("deepEqual", (...args) => Assert.deepEqual(...args)); + + await sandboxManager.evalInSandbox(` + (async function sandboxTest() { + const store = driver.createStorage("testprefix"); + const otherStore = driver.createStorage("othertestprefix"); + await store.clear(); + await otherStore.clear(); + + await store.setItem("willremove", 7); + await otherStore.setItem("willremove", 4); + is(await store.getItem("willremove"), 7, "createStorage stores sandbox values"); + is( + await otherStore.getItem("willremove"), + 4, + "values are not shared between createStorage stores", + ); + + const deepValue = {"foo": ["bar", "baz"]}; + await store.setItem("deepValue", deepValue); + deepEqual(await store.getItem("deepValue"), deepValue, "createStorage clones stored values"); + + await store.removeItem("willremove"); + is(await store.getItem("willremove"), null, "createStorage removes items"); + + is('prefix' in store, false, "createStorage doesn't expose non-whitelist attributes"); + })(); + `); + } +); + +add_task(withDriver(Assert, async function getAddon(driver, sandboxManager) { + const ADDON_ID = "normandydriver@example.com"; + let addon = await driver.addons.get(ADDON_ID); + Assert.equal(addon, null, "Add-on is not yet installed"); + + await driver.addons.install(TEST_XPI_URL); + addon = await driver.addons.get(ADDON_ID); + + Assert.notEqual(addon, null, "Add-on object was returned"); + ok(addon.installDate instanceof sandboxManager.sandbox.Date, "installDate should be a Date object"); - // Assertion helpers - sandboxManager.addGlobal("is", is); - sandboxManager.addGlobal("deepEqual", (...args) => Assert.deepEqual(...args)); + Assert.deepEqual(addon, { + id: "normandydriver@example.com", + name: "normandy_fixture", + version: "1.0", + installDate: addon.installDate, + isActive: true, + type: "extension", + }, "Add-on is installed"); + + await driver.addons.uninstall(ADDON_ID); + addon = await driver.addons.get(ADDON_ID); + + Assert.equal(addon, null, "Add-on has been uninstalled"); +})); + +decorate_task( + withSandboxManager(Assert), + async function testAddonsGetWorksInSandbox(sandboxManager) { + const driver = new NormandyDriver(sandboxManager); + sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true}); + + // Assertion helpers + sandboxManager.addGlobal("is", is); + sandboxManager.addGlobal("deepEqual", (...args) => Assert.deepEqual(...args)); + + const ADDON_ID = "normandydriver@example.com"; + + await driver.addons.install(TEST_XPI_URL); - await sandboxManager.evalInSandbox(` - (async function sandboxTest() { - const store = driver.createStorage("testprefix"); - const otherStore = driver.createStorage("othertestprefix"); - await store.clear(); - await otherStore.clear(); + await sandboxManager.evalInSandbox(` + (async function sandboxTest() { + const addon = await driver.addons.get("${ADDON_ID}"); + + deepEqual(addon, { + id: "${ADDON_ID}", + name: "normandy_fixture", + version: "1.0", + installDate: addon.installDate, + isActive: true, + type: "extension", + }, "Add-on is accesible in the driver"); + })(); + `); + + await driver.addons.uninstall(ADDON_ID); + } +); + +decorate_task( + withSandboxManager(Assert), + withWebExtension({id: "driver-addon-studies@example.com"}), + async function testAddonStudies(sandboxManager, [addonId, addonFile]) { + const addonUrl = Services.io.newFileURI(addonFile).spec; + const driver = new NormandyDriver(sandboxManager); + sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true}); + + // Assertion helpers + sandboxManager.addGlobal("is", is); + sandboxManager.addGlobal("ok", ok); - await store.setItem("willremove", 7); - await otherStore.setItem("willremove", 4); - is(await store.getItem("willremove"), 7, "createStorage stores sandbox values"); - is( - await otherStore.getItem("willremove"), - 4, - "values are not shared between createStorage stores", - ); + await sandboxManager.evalInSandbox(` + (async function sandboxTest() { + const recipeId = 5; + let hasStudy = await driver.studies.has(recipeId); + ok(!hasStudy, "studies.has returns false if the study hasn't been started yet."); + + await driver.studies.start({ + recipeId, + name: "fake", + description: "fake", + addonUrl: "${addonUrl}", + }); + hasStudy = await driver.studies.has(recipeId); + ok(hasStudy, "studies.has returns true after the study has been started."); + + let study = await driver.studies.get(recipeId); + is( + study.addonId, + "driver-addon-studies@example.com", + "studies.get fetches studies from within a sandbox." + ); + ok(study.active, "Studies are marked as active after being started by the driver."); + + await driver.studies.stop(recipeId); + study = await driver.studies.get(recipeId); + ok(!study.active, "Studies are marked as inactive after being stopped by the driver."); + })(); + `); + } +); + +decorate_task( + withPrefEnv({ + set: [ + ["test.char", "a string"], + ["test.int", 5], + ["test.bool", true], + ], + }), + withSandboxManager(Assert, async function testPreferences(sandboxManager) { + const driver = new NormandyDriver(sandboxManager); + sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true}); + + // Assertion helpers + sandboxManager.addGlobal("is", is); + sandboxManager.addGlobal("ok", ok); + sandboxManager.addGlobal("assertThrows", Assert.throws.bind(Assert)); - const deepValue = {"foo": ["bar", "baz"]}; - await store.setItem("deepValue", deepValue); - deepEqual(await store.getItem("deepValue"), deepValue, "createStorage clones stored values"); - - await store.removeItem("willremove"); - is(await store.getItem("willremove"), null, "createStorage removes items"); - - is('prefix' in store, false, "createStorage doesn't expose non-whitelist attributes"); - })(); - `); -})); + await sandboxManager.evalInSandbox(` + (async function sandboxTest() { + ok( + driver.preferences.getBool("test.bool"), + "preferences.getBool can retrieve boolean preferences." + ); + is( + driver.preferences.getInt("test.int"), + 5, + "preferences.getInt can retrieve integer preferences." + ); + is( + driver.preferences.getChar("test.char"), + "a string", + "preferences.getChar can retrieve string preferences." + ); + assertThrows( + () => driver.preferences.getChar("test.int"), + "preferences.getChar throws when retreiving a non-string preference." + ); + assertThrows( + () => driver.preferences.getInt("test.bool"), + "preferences.getInt throws when retreiving a non-integer preference." + ); + assertThrows( + () => driver.preferences.getBool("test.char"), + "preferences.getBool throws when retreiving a non-boolean preference." + ); + assertThrows( + () => driver.preferences.getChar("test.does.not.exist"), + "preferences.getChar throws when retreiving a non-existant preference." + ); + assertThrows( + () => driver.preferences.getInt("test.does.not.exist"), + "preferences.getInt throws when retreiving a non-existant preference." + ); + assertThrows( + () => driver.preferences.getBool("test.does.not.exist"), + "preferences.getBool throws when retreiving a non-existant preference." + ); + ok( + driver.preferences.getBool("test.does.not.exist", true), + "preferences.getBool returns a default value if the preference doesn't exist." + ); + is( + driver.preferences.getInt("test.does.not.exist", 7), + 7, + "preferences.getInt returns a default value if the preference doesn't exist." + ); + is( + driver.preferences.getChar("test.does.not.exist", "default"), + "default", + "preferences.getChar returns a default value if the preference doesn't exist." + ); + ok( + driver.preferences.has("test.char"), + "preferences.has returns true if the given preference exists." + ); + ok( + !driver.preferences.has("test.does.not.exist"), + "preferences.has returns false if the given preference does not exist." + ); + })(); + `); + }) +);
--- a/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_PreferenceExperiments.js @@ -370,16 +370,19 @@ add_task(withMockExperiments(withMockPre stopObserver.restore(); PreferenceExperiments.stopAllObservers(); }))); // stop should also support user pref experiments add_task(withMockExperiments(withMockPreferences(async function(experiments, mockPreferences) { const stopObserver = sinon.stub(PreferenceExperiments, "stopObserver"); + const hasObserver = sinon.stub(PreferenceExperiments, "hasObserver"); + hasObserver.returns(true); + mockPreferences.set("fake.preference", "experimentvalue", "user"); experiments.test = experimentFactory({ name: "test", expired: false, preferenceName: "fake.preference", preferenceValue: "experimentvalue", preferenceType: "string", previousPreferenceValue: "oldvalue", @@ -392,16 +395,17 @@ add_task(withMockExperiments(withMockPre is(experiments.test.expired, true, "stop marked the experiment as expired"); is( Preferences.get("fake.preference"), "oldvalue", "stop reverted the preference to its previous value", ); stopObserver.restore(); + hasObserver.restore(); }))); // stop should not call stopObserver if there is no observer registered. add_task(withMockExperiments(withMockPreferences(async function(experiments) { const stopObserver = sinon.spy(PreferenceExperiments, "stopObserver"); experiments.test = experimentFactory({name: "test", expired: false}); await PreferenceExperiments.stop("test");
--- a/browser/extensions/shield-recipe-client/test/browser/browser_RecipeRunner.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_RecipeRunner.js @@ -1,15 +1,18 @@ "use strict"; +Cu.import("resource://testing-common/TestUtils.jsm", this); Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm", this); Cu.import("resource://shield-recipe-client/lib/ClientEnvironment.jsm", this); Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm", this); Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm", this); Cu.import("resource://shield-recipe-client/lib/ActionSandboxManager.jsm", this); +Cu.import("resource://shield-recipe-client/lib/AddonStudies.jsm", this); +Cu.import("resource://shield-recipe-client/lib/Uptake.jsm", this); add_task(async function getFilterContext() { const recipe = {id: 17, arguments: {foo: "bar"}, unrelated: false}; const context = RecipeRunner.getFilterContext(recipe); // Test for expected properties in the filter expression context. const expectedNormandyKeys = [ "channel", @@ -64,17 +67,17 @@ add_task(async function checkFilter() { }); add_task(withMockNormandyApi(async function testClientClassificationCache() { const getStub = sinon.stub(ClientEnvironment, "getClientClassification") .returns(Promise.resolve(false)); await SpecialPowers.pushPrefEnv({set: [ ["extensions.shield-recipe-client.api_url", - "https://example.com/selfsupport-dummy"], + "https://example.com/selfsupport-dummy"], ]}); // When the experiment pref is false, eagerly call getClientClassification. await SpecialPowers.pushPrefEnv({set: [ ["extensions.shield-recipe-client.experiments.lazy_classify", false], ]}); ok(!getStub.called, "getClientClassification hasn't been called"); await RecipeRunner.run(); @@ -93,37 +96,46 @@ add_task(withMockNormandyApi(async funct })); /** * Mocks RecipeRunner.loadActionSandboxManagers for testing run. */ async function withMockActionSandboxManagers(actions, testFunction) { const managers = {}; for (const action of actions) { - managers[action.name] = new ActionSandboxManager(""); + const manager = new ActionSandboxManager(""); + manager.addHold("testing"); + managers[action.name] = manager; sinon.stub(managers[action.name], "runAsyncCallback"); } - const loadActionSandboxManagers = sinon.stub( - RecipeRunner, - "loadActionSandboxManagers", - async () => managers, - ); + const loadActionSandboxManagers = sinon.stub(RecipeRunner, "loadActionSandboxManagers") + .resolves(managers); await testFunction(managers); loadActionSandboxManagers.restore(); + + for (const manager of Object.values(managers)) { + manager.removeHold("testing"); + await manager.isNuked(); + } } add_task(withMockNormandyApi(async function testRun(mockApi) { + const closeSpy = sinon.spy(AddonStudies, "close"); + const reportRunner = sinon.stub(Uptake, "reportRunner"); + const reportAction = sinon.stub(Uptake, "reportAction"); + const reportRecipe = sinon.stub(Uptake, "reportRecipe"); + const matchAction = {name: "matchAction"}; const noMatchAction = {name: "noMatchAction"}; mockApi.actions = [matchAction, noMatchAction]; - const matchRecipe = {action: "matchAction", filter_expression: "true"}; - const noMatchRecipe = {action: "noMatchAction", filter_expression: "false"}; - const missingRecipe = {action: "missingAction", filter_expression: "true"}; + const matchRecipe = {id: "match", action: "matchAction", filter_expression: "true"}; + const noMatchRecipe = {id: "noMatch", action: "noMatchAction", filter_expression: "false"}; + const missingRecipe = {id: "missing", action: "missingAction", filter_expression: "true"}; mockApi.recipes = [matchRecipe, noMatchRecipe, missingRecipe]; await withMockActionSandboxManagers(mockApi.actions, async managers => { const matchManager = managers.matchAction; const noMatchManager = managers.noMatchAction; await RecipeRunner.run(); @@ -134,28 +146,109 @@ add_task(withMockNormandyApi(async funct // noMatch should be called for preExecution and postExecution, and skipped // for action since the filter expression does not match. sinon.assert.calledWith(noMatchManager.runAsyncCallback, "preExecution"); sinon.assert.neverCalledWith(noMatchManager.runAsyncCallback, "action", noMatchRecipe); sinon.assert.calledWith(noMatchManager.runAsyncCallback, "postExecution"); // missing is never called at all due to no matching action/manager. - await matchManager.isNuked(); - await noMatchManager.isNuked(); + + // Test uptake reporting + sinon.assert.calledWith(reportRunner, Uptake.RUNNER_SUCCESS); + sinon.assert.calledWith(reportAction, "matchAction", Uptake.ACTION_SUCCESS); + sinon.assert.calledWith(reportAction, "noMatchAction", Uptake.ACTION_SUCCESS); + sinon.assert.calledWith(reportRecipe, "match", Uptake.RECIPE_SUCCESS); + sinon.assert.neverCalledWith(reportRecipe, "noMatch", Uptake.RECIPE_SUCCESS); + sinon.assert.calledWith(reportRecipe, "missing", Uptake.RECIPE_INVALID_ACTION); }); + + // Ensure storage is closed after the run. + sinon.assert.calledOnce(closeSpy); + + closeSpy.restore(); + reportRunner.restore(); + reportAction.restore(); + reportRecipe.restore(); +})); + +add_task(withMockNormandyApi(async function testRunRecipeError(mockApi) { + const reportRecipe = sinon.stub(Uptake, "reportRecipe"); + + const action = {name: "action"}; + mockApi.actions = [action]; + + const recipe = {id: "recipe", action: "action", filter_expression: "true"}; + mockApi.recipes = [recipe]; + + await withMockActionSandboxManagers(mockApi.actions, async managers => { + const manager = managers.action; + manager.runAsyncCallback.callsFake(async callbackName => { + if (callbackName === "action") { + throw new Error("Action execution failure"); + } + }); + + await RecipeRunner.run(); + + // Uptake should report that the recipe threw an exception + sinon.assert.calledWith(reportRecipe, "recipe", Uptake.RECIPE_EXECUTION_ERROR); + }); + + reportRecipe.restore(); +})); + +add_task(withMockNormandyApi(async function testRunFetchFail(mockApi) { + const closeSpy = sinon.spy(AddonStudies, "close"); + const reportRunner = sinon.stub(Uptake, "reportRunner"); + + const action = {name: "action"}; + mockApi.actions = [action]; + mockApi.fetchRecipes.rejects(new Error("Signature not valid")); + + await withMockActionSandboxManagers(mockApi.actions, async managers => { + const manager = managers.action; + await RecipeRunner.run(); + + // If the recipe fetch failed, do not run anything. + sinon.assert.notCalled(manager.runAsyncCallback); + sinon.assert.calledWith(reportRunner, Uptake.RUNNER_SERVER_ERROR); + + // Test that network errors report a specific uptake error + reportRunner.reset(); + mockApi.fetchRecipes.rejects(new Error("NetworkError: The system was down")); + await RecipeRunner.run(); + sinon.assert.calledWith(reportRunner, Uptake.RUNNER_NETWORK_ERROR); + + // Test that signature issues report a specific uptake error + reportRunner.reset(); + mockApi.fetchRecipes.rejects(new NormandyApi.InvalidSignatureError("Signature fail")); + await RecipeRunner.run(); + sinon.assert.calledWith(reportRunner, Uptake.RUNNER_INVALID_SIGNATURE); + }); + + // If the recipe fetch failed, we don't need to call close since nothing + // opened a connection in the first place. + sinon.assert.notCalled(closeSpy); + + closeSpy.restore(); + reportRunner.restore(); })); add_task(withMockNormandyApi(async function testRunPreExecutionFailure(mockApi) { + const closeSpy = sinon.spy(AddonStudies, "close"); + const reportAction = sinon.stub(Uptake, "reportAction"); + const reportRecipe = sinon.stub(Uptake, "reportRecipe"); + const passAction = {name: "passAction"}; const failAction = {name: "failAction"}; mockApi.actions = [passAction, failAction]; - const passRecipe = {action: "passAction", filter_expression: "true"}; - const failRecipe = {action: "failAction", filter_expression: "true"}; + const passRecipe = {id: "pass", action: "passAction", filter_expression: "true"}; + const failRecipe = {id: "fail", action: "failAction", filter_expression: "true"}; mockApi.recipes = [passRecipe, failRecipe]; await withMockActionSandboxManagers(mockApi.actions, async managers => { const passManager = managers.passAction; const failManager = managers.failAction; failManager.runAsyncCallback.returns(Promise.reject(new Error("oh no"))); await RecipeRunner.run(); @@ -165,19 +258,57 @@ add_task(withMockNormandyApi(async funct sinon.assert.calledWith(passManager.runAsyncCallback, "action", passRecipe); sinon.assert.calledWith(passManager.runAsyncCallback, "postExecution"); // fail should only be called for preExecution, since it fails during that sinon.assert.calledWith(failManager.runAsyncCallback, "preExecution"); sinon.assert.neverCalledWith(failManager.runAsyncCallback, "action", failRecipe); sinon.assert.neverCalledWith(failManager.runAsyncCallback, "postExecution"); - await passManager.isNuked(); - await failManager.isNuked(); + sinon.assert.calledWith(reportAction, "passAction", Uptake.ACTION_SUCCESS); + sinon.assert.calledWith(reportAction, "failAction", Uptake.ACTION_PRE_EXECUTION_ERROR); + sinon.assert.calledWith(reportRecipe, "fail", Uptake.RECIPE_ACTION_DISABLED); }); + + // Ensure storage is closed after the run, despite the failures. + sinon.assert.calledOnce(closeSpy); + closeSpy.restore(); + reportAction.restore(); + reportRecipe.restore(); +})); + +add_task(withMockNormandyApi(async function testRunPostExecutionFailure(mockApi) { + const reportAction = sinon.stub(Uptake, "reportAction"); + + const failAction = {name: "failAction"}; + mockApi.actions = [failAction]; + + const failRecipe = {action: "failAction", filter_expression: "true"}; + mockApi.recipes = [failRecipe]; + + await withMockActionSandboxManagers(mockApi.actions, async managers => { + const failManager = managers.failAction; + failManager.runAsyncCallback.callsFake(async callbackName => { + if (callbackName === "postExecution") { + throw new Error("postExecution failure"); + } + }); + + await RecipeRunner.run(); + + // fail should be called for every stage + sinon.assert.calledWith(failManager.runAsyncCallback, "preExecution"); + sinon.assert.calledWith(failManager.runAsyncCallback, "action", failRecipe); + sinon.assert.calledWith(failManager.runAsyncCallback, "postExecution"); + + // Uptake should report a post-execution error + sinon.assert.calledWith(reportAction, "failAction", Uptake.ACTION_POST_EXECUTION_ERROR); + }); + + reportAction.restore(); })); add_task(withMockNormandyApi(async function testLoadActionSandboxManagers(mockApi) { mockApi.actions = [ {name: "normalAction"}, {name: "missingImpl"}, ]; mockApi.implementations.normalAction = "window.scriptRan = true"; @@ -188,47 +319,70 @@ add_task(withMockNormandyApi(async funct const normalManager = managers.normalAction; ok( await normalManager.evalInSandbox("window.scriptRan"), "Implementations are run in the sandbox", ); })); -add_task(async function testStartup() { - const runStub = sinon.stub(RecipeRunner, "run"); - const addCleanupHandlerStub = sinon.stub(CleanupManager, "addCleanupHandler"); - const updateRunIntervalStub = sinon.stub(RecipeRunner, "updateRunInterval"); - - // in dev mode - await SpecialPowers.pushPrefEnv({ +decorate_task( + withPrefEnv({ set: [ ["extensions.shield-recipe-client.dev_mode", true], ["extensions.shield-recipe-client.first_run", false], ], - }); - - RecipeRunner.init(); - ok(runStub.called, "RecipeRunner.run is called immediately when in dev mode"); - ok(addCleanupHandlerStub.called, "A cleanup function is registered when in dev mode"); - ok(updateRunIntervalStub.called, "A timer is registered when in dev mode"); + }), + withStub(RecipeRunner, "run"), + withStub(CleanupManager, "addCleanupHandler"), + withStub(RecipeRunner, "updateRunInterval"), + async function testInitDevMode(runStub, addCleanupHandlerStub, updateRunIntervalStub) { + RecipeRunner.init(); + ok(runStub.called, "RecipeRunner.run is called immediately when in dev mode"); + ok(addCleanupHandlerStub.called, "A cleanup function is registered when in dev mode"); + ok(updateRunIntervalStub.called, "A timer is registered when in dev mode"); + } +); - runStub.reset(); - addCleanupHandlerStub.reset(); - updateRunIntervalStub.reset(); - - // not in dev mode - await SpecialPowers.pushPrefEnv({ +decorate_task( + withPrefEnv({ set: [ ["extensions.shield-recipe-client.dev_mode", false], ["extensions.shield-recipe-client.first_run", false], ], - }); + }), + withStub(RecipeRunner, "run"), + withStub(CleanupManager, "addCleanupHandler"), + withStub(RecipeRunner, "updateRunInterval"), + async function testInit(runStub, addCleanupHandlerStub, updateRunIntervalStub) { + RecipeRunner.init(); + ok(!runStub.called, "RecipeRunner.run is not called immediately when not in dev mode"); + ok(addCleanupHandlerStub.called, "A cleanup function is registered when not in dev mode"); + ok(updateRunIntervalStub.called, "A timer is registered when not in dev mode"); + } +); - RecipeRunner.init(); - ok(!runStub.called, "RecipeRunner.run is not called immediately when not in dev mode"); - ok(addCleanupHandlerStub.called, "A cleanup function is registered when not in dev mode"); - ok(updateRunIntervalStub.called, "A timer is registered when not in dev mode"); +decorate_task( + withPrefEnv({ + set: [ + ["extensions.shield-recipe-client.dev_mode", false], + ["extensions.shield-recipe-client.first_run", true], + ], + }), + withStub(RecipeRunner, "run"), + withStub(RecipeRunner, "registerTimer"), + withStub(CleanupManager, "addCleanupHandler"), + withStub(RecipeRunner, "updateRunInterval"), + async function testInitFirstRun(runStub, registerTimerStub) { + RecipeRunner.init(); + ok(!runStub.called, "RecipeRunner.run is not called immediately"); + ok(!registerTimerStub.called, "RecipeRunner.registerTimer is not called immediately"); - runStub.restore(); - addCleanupHandlerStub.restore(); - updateRunIntervalStub.restore(); -}); + Services.obs.notifyObservers(null, "sessionstore-windows-restored"); + await TestUtils.topicObserved("shield-init-complete"); + ok(runStub.called, "RecipeRunner.run is called after the UI is available"); + ok(registerTimerStub.called, "RecipeRunner.registerTimer is called after the UI is available"); + ok( + !Services.prefs.getBoolPref("extensions.shield-recipe-client.first_run"), + "On first run, the first run pref is set to false after the UI is available" + ); + } +);
--- a/browser/extensions/shield-recipe-client/test/browser/browser_ShieldRecipeClient.js +++ b/browser/extensions/shield-recipe-client/test/browser/browser_ShieldRecipeClient.js @@ -1,30 +1,67 @@ "use strict"; Cu.import("resource://shield-recipe-client/lib/ShieldRecipeClient.jsm", this); Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm", this); Cu.import("resource://shield-recipe-client/lib/PreferenceExperiments.jsm", this); +Cu.import("resource://shield-recipe-client-content/AboutPages.jsm", this); +Cu.import("resource://shield-recipe-client/lib/AddonStudies.jsm", this); -add_task(async function testStartup() { - sinon.stub(RecipeRunner, "init"); - sinon.stub(PreferenceExperiments, "init"); +function withStubInits(testFunction) { + return decorate( + withStub(AboutPages, "init"), + withStub(AddonStudies, "init"), + withStub(PreferenceExperiments, "init"), + withStub(RecipeRunner, "init"), + testFunction + ); +} - await ShieldRecipeClient.startup(); - ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); - ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); +decorate_task( + withStubInits, + async function testStartup() { + await ShieldRecipeClient.startup(); + ok(AboutPages.init.called, "startup calls AboutPages.init"); + ok(AddonStudies.init.called, "startup calls AddonStudies.init"); + ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); + ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); + } +); - PreferenceExperiments.init.restore(); - RecipeRunner.init.restore(); -}); +decorate_task( + withStubInits, + async function testStartupPrefInitFail() { + PreferenceExperiments.init.returns(Promise.reject(new Error("oh no"))); -add_task(async function testStartupPrefInitFail() { - sinon.stub(RecipeRunner, "init"); - sinon.stub(PreferenceExperiments, "init").returns(Promise.reject(new Error("oh no"))); + await ShieldRecipeClient.startup(); + ok(AboutPages.init.called, "startup calls AboutPages.init"); + ok(AddonStudies.init.called, "startup calls AddonStudies.init"); + ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); + ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); + } +); + +decorate_task( + withStubInits, + async function testStartupAboutPagesInitFail() { + AboutPages.init.returns(Promise.reject(new Error("oh no"))); - await ShieldRecipeClient.startup(); - ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); - // Even if PreferenceExperiments.init fails, RecipeRunner.init should be called. - ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); + await ShieldRecipeClient.startup(); + ok(AboutPages.init.called, "startup calls AboutPages.init"); + ok(AddonStudies.init.called, "startup calls AddonStudies.init"); + ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); + ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); + } +); - PreferenceExperiments.init.restore(); - RecipeRunner.init.restore(); -}); +decorate_task( + withStubInits, + async function testStartupAddonStudiesInitFail() { + AddonStudies.init.returns(Promise.reject(new Error("oh no"))); + + await ShieldRecipeClient.startup(); + ok(AboutPages.init.called, "startup calls AboutPages.init"); + ok(AddonStudies.init.called, "startup calls AddonStudies.init"); + ok(PreferenceExperiments.init.called, "startup calls PreferenceExperiments.init"); + ok(RecipeRunner.init.called, "startup calls RecipeRunner.init"); + } +);
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/browser/browser_about_preferences.js @@ -0,0 +1,177 @@ +"use strict"; + +Cu.import("resource://gre/modules/Services.jsm", this); + +const OPT_OUT_PREF = "app.shield.optoutstudies.enabled"; +const FHR_PREF = "datareporting.healthreport.uploadEnabled"; + +function withPrivacyPrefs(testFunc) { + return async (...args) => ( + BrowserTestUtils.withNewTab("about:preferences#privacy", async browser => ( + testFunc(...args, browser) + )) + ); +} + +decorate_task( + withPrefEnv({ + set: [[OPT_OUT_PREF, true]], + }), + withPrivacyPrefs, + async function testCheckedOnLoad(browser) { + const checkbox = browser.contentDocument.getElementById("optOutStudiesEnabled"); + ok(checkbox.checked, "Opt-out checkbox is checked on load when the pref is true"); + } +); + +decorate_task( + withPrefEnv({ + set: [[OPT_OUT_PREF, false]], + }), + withPrivacyPrefs, + async function testUncheckedOnLoad(browser) { + const checkbox = browser.contentDocument.getElementById("optOutStudiesEnabled"); + ok(!checkbox.checked, "Opt-out checkbox is unchecked on load when the pref is false"); + } +); + +decorate_task( + withPrefEnv({ + set: [[FHR_PREF, true]], + }), + withPrivacyPrefs, + async function testEnabledOnLoad(browser) { + const checkbox = browser.contentDocument.getElementById("optOutStudiesEnabled"); + ok(!checkbox.disabled, "Opt-out checkbox is enabled on load when the FHR pref is true"); + } +); + +decorate_task( + withPrefEnv({ + set: [[FHR_PREF, false]], + }), + withPrivacyPrefs, + async function testDisabledOnLoad(browser) { + const checkbox = browser.contentDocument.getElementById("optOutStudiesEnabled"); + ok(checkbox.disabled, "Opt-out checkbox is disabled on load when the FHR pref is false"); + } +); + +decorate_task( + withPrefEnv({ + set: [ + [FHR_PREF, true], + [OPT_OUT_PREF, true], + ], + }), + withPrivacyPrefs, + async function testCheckboxes(browser) { + const optOutCheckbox = browser.contentDocument.getElementById("optOutStudiesEnabled"); + const fhrCheckbox = browser.contentDocument.getElementById("submitHealthReportBox"); + + optOutCheckbox.click(); + ok( + !Services.prefs.getBoolPref(OPT_OUT_PREF), + "Unchecking the opt-out checkbox sets the pref to false." + ); + optOutCheckbox.click(); + ok( + Services.prefs.getBoolPref(OPT_OUT_PREF), + "Checking the opt-out checkbox sets the pref to true." + ); + + fhrCheckbox.click(); + ok( + !Services.prefs.getBoolPref(OPT_OUT_PREF), + "Unchecking the FHR checkbox sets the opt-out pref to false." + ); + ok( + optOutCheckbox.disabled, + "Unchecking the FHR checkbox disables the opt-out checkbox." + ); + ok( + !optOutCheckbox.checked, + "Unchecking the FHR checkbox unchecks the opt-out checkbox." + ); + + fhrCheckbox.click(); + ok( + Services.prefs.getBoolPref(OPT_OUT_PREF), + "Checking the FHR checkbox sets the opt-out pref to true." + ); + ok( + !optOutCheckbox.disabled, + "Checking the FHR checkbox enables the opt-out checkbox." + ); + ok( + optOutCheckbox.checked, + "Checking the FHR checkbox checks the opt-out checkbox." + ); + } +); + +decorate_task( + withPrefEnv({ + set: [ + [FHR_PREF, true], + [OPT_OUT_PREF, true], + ], + }), + withPrivacyPrefs, + async function testPrefWatchers(browser) { + const optOutCheckbox = browser.contentDocument.getElementById("optOutStudiesEnabled"); + + Services.prefs.setBoolPref(OPT_OUT_PREF, false); + ok( + !optOutCheckbox.checked, + "Disabling the opt-out pref unchecks the opt-out checkbox." + ); + Services.prefs.setBoolPref(OPT_OUT_PREF, true); + ok( + optOutCheckbox.checked, + "Enabling the opt-out pref checks the opt-out checkbox." + ); + + Services.prefs.setBoolPref(FHR_PREF, false); + ok( + !Services.prefs.getBoolPref(OPT_OUT_PREF), + "Disabling the FHR pref sets the opt-out pref to false." + ); + ok( + optOutCheckbox.disabled, + "Disabling the FHR pref disables the opt-out checkbox." + ); + ok( + !optOutCheckbox.checked, + "Disabling the FHR pref unchecks the opt-out checkbox." + ); + + Services.prefs.setBoolPref(FHR_PREF, true); + ok( + Services.prefs.getBoolPref(OPT_OUT_PREF), + "Enabling the FHR pref sets the opt-out pref to true." + ); + ok( + !optOutCheckbox.disabled, + "Enabling the FHR pref enables the opt-out checkbox." + ); + ok( + optOutCheckbox.checked, + "Enabling the FHR pref checks the opt-out checkbox." + ); + } +); + +decorate_task( + withPrivacyPrefs, + async function testViewStudiesLink(browser) { + browser.contentDocument.getElementById("viewShieldStudies").click(); + await BrowserTestUtils.waitForLocationChange(gBrowser); + + is( + browser.currentURI.spec, + "about:studies", + "Clicking the view studies link opens about:studies." + ); + } +);
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/browser/browser_about_studies.js @@ -0,0 +1,184 @@ +"use strict"; + +Cu.import("resource://shield-recipe-client/lib/AddonStudies.jsm", this); +Cu.import("resource://shield-recipe-client-content/AboutPages.jsm", this); + +function withAboutStudies(testFunc) { + return async (...args) => ( + BrowserTestUtils.withNewTab("about:studies", async browser => ( + testFunc(...args, browser) + )) + ); +} + +decorate_task( + withAboutStudies, + async function testAboutStudiesWorks(browser) { + ok(browser.contentDocument.getElementById("app"), "App element was found"); + } +); + +decorate_task( + withPrefEnv({ + set: [["extensions.shield-recipe-client.shieldLearnMoreUrl", "http://test/%OS%/"]], + }), + withAboutStudies, + async function testLearnMore(browser) { + ContentTask.spawn(browser, null, () => { + content.document.getElementById("shield-studies-learn-more").click(); + }); + await BrowserTestUtils.waitForLocationChange(gBrowser); + + const location = browser.currentURI.spec; + is( + location, + AboutPages.aboutStudies.getShieldLearnMoreHref(), + "Clicking Learn More opens the correct page on SUMO.", + ); + ok(!location.includes("%OS%"), "The Learn More URL is formatted."); + } +); + +decorate_task( + withPrefEnv({ + set: [["browser.preferences.useOldOrganization", false]], + }), + withAboutStudies, + async function testUpdatePreferencesNewOrganization(browser) { + ContentTask.spawn(browser, null, () => { + content.document.getElementById("shield-studies-update-preferences").click(); + }); + await BrowserTestUtils.waitForLocationChange(gBrowser); + + is( + browser.currentURI.spec, + "about:preferences#privacy-reports", + "Clicking Update Preferences opens the privacy section of the new about:prefernces.", + ); + } +); + +decorate_task( + withPrefEnv({ + set: [["browser.preferences.useOldOrganization", true]], + }), + withAboutStudies, + async function testUpdatePreferencesOldOrganization(browser) { + // We have to use gBrowser instead of browser in most spots since we're + // dealing with a new tab outside of the about:studies tab. + const tab = await BrowserTestUtils.switchTab(gBrowser, () => { + ContentTask.spawn(browser, null, () => { + content.document.getElementById("shield-studies-update-preferences").click(); + }); + }); + + if (gBrowser.contentDocument.readyState !== "complete") { + await BrowserTestUtils.waitForEvent(gBrowser.contentWindow, "load"); + } + + const location = gBrowser.contentWindow.location.href; + is( + location, + "about:preferences#advanced", + "Clicking Update Preferences opens the advanced section of the old about:prefernces.", + ); + + const dataChoicesTab = gBrowser.contentDocument.getElementById("dataChoicesTab"); + ok( + dataChoicesTab.selected, + "Click Update preferences selects the Data Choices tab in the old about:preferences." + ); + + await BrowserTestUtils.removeTab(tab); + } +); + +decorate_task( + AddonStudies.withStudies([ + // Sort order should be study3, study1, study2 (order by enabled, then most recent). + studyFactory({ + name: "A Fake Study", + active: true, + description: "A fake description", + studyStartDate: new Date(2017), + }), + studyFactory({ + name: "B Fake Study", + active: false, + description: "A fake description", + studyStartDate: new Date(2019), + }), + studyFactory({ + name: "C Fake Study", + active: true, + description: "A fake description", + studyStartDate: new Date(2018), + }), + ]), + withAboutStudies, + async function testStudyListing([study1, study2, study3], browser) { + await ContentTask.spawn(browser, [study1, study2, study3], async ([cStudy1, cStudy2, cStudy3]) => { + const doc = content.document; + + function getStudyRow(docElem, studyName) { + return docElem.querySelector(`.study[data-study-name="${studyName}"]`); + } + + await ContentTaskUtils.waitForCondition(() => doc.querySelectorAll(".study-list .study").length); + const studyRows = doc.querySelectorAll(".study-list .study"); + + const names = Array.from(studyRows).map(row => row.querySelector(".study-name").textContent); + Assert.deepEqual( + names, + [cStudy3.name, cStudy1.name, cStudy2.name], + "Studies are sorted first by enabled status, and then by descending start date." + ); + + const study1Row = getStudyRow(doc, cStudy1.name); + ok( + study1Row.querySelector(".study-description").textContent.includes(cStudy1.description), + "Study descriptions are shown in about:studies." + ); + is( + study1Row.querySelector(".study-status").textContent, + "Active", + "Active studies show an 'Active' indicator." + ); + ok( + study1Row.querySelector(".remove-button"), + "Active studies show a remove button" + ); + is( + study1Row.querySelector(".study-icon").textContent.toLowerCase(), + "a", + "Study icons use the first letter of the study name." + ); + + const study2Row = getStudyRow(doc, cStudy2.name); + is( + study2Row.querySelector(".study-status").textContent, + "Complete", + "Inactive studies are marked as complete." + ); + ok( + !study2Row.querySelector(".remove-button"), + "Inactive studies do not show a remove button" + ); + + study1Row.querySelector(".remove-button").click(); + await ContentTaskUtils.waitForCondition(() => ( + getStudyRow(doc, cStudy1.name).matches(".disabled") + )); + ok( + getStudyRow(doc, cStudy1.name).matches(".disabled"), + "Clicking the remove button updates the UI to show that the study has been disabled." + ); + }); + + const updatedStudy1 = await AddonStudies.get(study1.recipeId); + ok( + !updatedStudy1.active, + "Clicking the remove button marks the study as inactive in storage." + ); + } +);
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/browser/fixtures/addon-fixture/manifest.json @@ -0,0 +1,11 @@ +{ + "manifest_version": 2, + "name": "normandy_fixture", + "version": "1.0", + "description": "Dummy test fixture that's a webextension", + "applications": { + "gecko": { + "id": "normandydriver@example.com" + } + } +}
new file mode 100644 index 0000000000000000000000000000000000000000..71a6f8fe7cd90427ca710599e7125f986e8dd24d GIT binary patch literal 4230 zc$|$_Wl$7s*Ir-=rC})%M7qI6knWBpBwguNmXL-eq@`85q>)xax_c>!U0guAS)@x) zKKy3h@15tF=b8D=J?F=D=00bxb6@wLOGg6-2m$~A1OVgwE_K=~oAsYW0Duu00Pv@( zrKB&%rv_0LfVucNIyqVLzjC+yZsaSPw0}<|=5w4UhNvgEH_b&*6)ca!aA_2EeY3(t z*CeZ1FLJR#&lDZxbkA7P2nvKAvwjLDSs{C|8nT&iJkvDhnSIlK<)3oy`(ZcW$7VAY zcO-7C;{&aPFa39|0Gm2ub+;8fT448QfTo}yod|((Vh)ML<vI|N*9`@52e8P7C#}hL zG6~Y+iOYeJT{sm%{;!R_Lb{&?vA#Bzw<P#fgbSf1o5vj?2qS0viu|4(HJR_@uh;rU zx(Wm)A++$S<RvKWsECZ!)G1y7shCHa;wOcd3!#%Su{BJ-S54xHuA+bzdVn0}^m^`& z`&kKP|7%Va50ApcafI*n2)oLmS3ei_QBdk2a3xvtN(gB})OTDlqJ8t>@LrITn2scX zT-q=ToC{o9!<rAorJ|QM^z{W)AJ72hT?vAZ?e|7Hc7DyY9AnS71Y>o*P&{P2A~PNO zXkEn`bpHx66?g*l+8Q_vQhLPP`7F}aK^8MW)r30)av^0#A7&Lals_1PhJs{I4m>!1 zaVFfQzG*vV@)IYZ8i6z%f1uOE4c$^Nr?!Q)Mm<#tZ9LoaZ7!5}L$VI%cX%(;&?jfe zhbD=0#zvHEIN8Se_&BA4IpiC7*{Z?jL-*fL+nA~vqbp#vwF?<SwPmY>yFQN}ZmB<i zS*fXLN^iU><+m}#TrfuNLwmd%oFKOlpp=s#!a2rvJhC6j5+X2x6sw6!e&Eu;y+S$t zM9ukwD-}zcclt*mL5uWNNoH>K9=3ER=BZ*$E;Zg4TyOG<l+J&~S~FxHnZVjx@Ld;& zI4P;K$zykagg3&K{6;r<*}!kqV~PC+rFpi*pD^Pyb`C4?#k2EE&-t&mY)Q0?#T_Af z1EIs@ouO`nn_OtA+&9<0+IdecXs(_sv@a5x$OO?Bb%)97nZ_Wce<z*z&+$B+qs$aw z_9zw^eNU^`yD#jsFrxT+fZFP*&uGAURM(w*2%*xFymLf-h{3d&N__;KzJO2=N*W7k zYkeXYe~D^BQqpS2DH>3HV|s#?aViTA2+kPi@SDr*7NILps3m>?Z+MJBUb!yKO|3n3 zBrWKKQuJQI>7g{JN$E^Q8xfHy0Wa8oB|Pz^yt+fKqEn$Dl&}8Tmhd#+8c6w-C(a&Q z%{Jh%C^^Bx{kH}R2|~89T4%$f{JeAJ+6Lf}orrLWpwp6>CtM8tI}8PMU)C!{qDiss z1e9sP7pHsv%K=mS2HjLZ`A#x2hOGp27)F|?36}i|$LmHN^KiM&{UYx;a%l(;RrA3N zKc@@7gWg7JvZR@F*5zoHY3%*LxmZ}KldS~!{OeS%9xUu7xRqR)-UqT+a^RpyLyk5Z zW8bn{lq<_~%NmpHE<m_?6H<VWkrxWn&0+CjRH<&+NU{XI`4ZTG_;=@W5l3jMn|w|g ztHQ=rXRmnZkI?s=Rc>mM*CBM!bZQ)3Q7dB=Ga7W72H|gtobgBr31f1GjGcEG<QWVN zJL@+#+UFmut|y(!+D{HHcYX&-)d#aYQtAr(>Q_;|0Bd7mWitCh$!;RDzPm2_tzgx^ zHO5cCj|cSy#Pzmd9^@~hYX`{Rr`sR>MEhYZ@DqoZYoQ<_eq(iNxF<O#GyCCJsZXRO zoW}TL-o@_TMkfA35MIi+UvxD?D72Rwpp!rQ=#^f@VbVd&eEcZ!)^ga0^VV+v2O`CW z{&@R0QmHX9yQm~6_`A!^^<7ufE<E7_TmO`sI#phU3<VdK8=a^V%+XGv(AVXfe%pFI zGx{cC=!1JmxuR_@N>GI+K_36i$%{SC%wIieFB_nliOxtKZB>u_gnbfh(ub11?s+4q zV*YGTJ<`&xv;h2#)qD+{U(C0%U2sn(O(0Ew3z&mY1bycLQ9hN1u4bM3-D4vx3LWlm zuPEr2;faqRW&|d_#xo(}9UXC0x!qp?+$Ve56p^l9Uk3%T9WdaLURF~yat2?kEwwIy zqQ^;WtpizJ<>Rm8AFg2GX4SM}o0;hbruum2t;j6jjD}ol6D>@a9_+LW&H#m)e`f?M zbj2!#Eur<d`-YE4$I|`G6(n|j=5{HwQ6P=PQ0sbcmnORJlCbCdGTD|MEovK=!9GNA zR!ak+mp^$9GiIp-3AIcRxuww%Sj+;ck=nbIOS9{+kcE(V=7=R?UzOB8MGRVdgZN^g zZxgt~$2<ol;uCtS4Q5Qv%A5b48*$em!93c}14o~c+uy%`CCdFSdf7N?m|^cHoqd$m zmwk;(8w}159=Vbe;Cx3=ZW;}*MxNDLA<L!u`AMKjz}3d&v*C}@+n%9y%D>K~+bnB! zOvcZ&eVQS3$*dd<NkGN6v|qHW=u3B8@dN(BjE9m3nQxLm7>|dS3SwMcv<3{yHw!=E zSM?|?Dvmw5Lwb;@%*?CQ^Mvs}d|4=47;0bA8mzWLVwKc|uPy{XRL&(tB>kvT_9H!= zlJr;Q!lQ$D)wn-YbCO&6?qcV>9oS5i5LnQRW_agy82oe_wp-@UbLyA+IWwZzECTU+ z6d!`$;YJ3}XN*x#EqH!VQ~#7#?^v?ff5nc4uwV@N8p|-rPCt{pa8j1LNSp(;AF8y= zW3Kk#jAnm!yj-hSG1JJXqfDUG(b4EsNiZ~4fIVRN{h`E50|ArCfd%$?G)>lT6iIzG zrbh2XcG+lWsAwS4T?~YsD&x#UB40nF)3XxW$U4#EUMWs12(&BBQo-HB|Hvc>SoBR& z1tu{D2g}X@=J>4^FGJ+L`)TnGepn5e65*#=#)1XOq@-|wN7mDcPC90s4cea=yt{1V zj^-p7Kdoq}vcaS$RGBJ#X5u3v)&Z93G>!Yyqt7mrAc;6<2FYeBF(8RSv(hnQ!eN;k znPMO}0g$sZ4Kj5iTj!}o2IB7HSzcLPEsgH`ef8LiA<7uv#am4K-N1w1GwWZ;#L^^m z_b#KJ5)r_(KT3!P`cAu6BX_AE4HHteV&2R-lk7_6+b2zIyvHG9z<#uvdjPlS_K#c9 zUV^nl<Qq}h(wEG6wwA4BfdIvp*5^gj*GlV5#hzIzq0D)F8**uBIcqwWECe17hKIuA zh=t%I8H$9Zc{nnU-5t3$-?jsGnBN=gWt%lRU-}uK`j(h*m0jjxa_W6D_24A86s{VF zgUZ8vPZ88!@uJUqG_3N4$A&B=DH^nT!(S^&n!>SaAwTM}`k*+cWw2t`xkiZ+p}kai z&r?TQ>WALo{XE`QM9K?DrQcH>R=OKc!m-yf7G4)sIp0%Lz7Bx*EsZ!HuDq$6zp69L z#!tT3g?}AQd&*ieX-T-<v<();(DMuwG{y?>&(`$ZseN`3LztT>ESwwtvfiI0p74&g zHY$kv+Ss-A)W&|=G{xRY@*S5^a_i$g*roU=&-U|`&`()?SO-dDac(1+xRtt)3JG$1 zR=U(n9n$dXDn=gO!BncdJ3~Olg{^r%g+aaqtvYiNx*)6EiHYdaCSB%?5u1Q&xqA1C z=Z<k)-lAc}$;9dCj&@->wMEa)eK>XVkWuz~!6T1fNVrI<(;$MVUvJHFcPM533<WU4 zI+{5B5mV4EkG2BrX;q3>5Fq0A2}^xr(}ypt@SDv|eVC+?jop)kB4Gvc@%s+6F$|w^ z!cMO{c;9#QeG@oMai>_iNhS!nG9AI{?%+KUJ(Ecy3I1&xzT(x0L;ge~V)hy5xQIF; zXpOmJ4dioqlz_x73fYcbOtx=~gHC?t?iA=PpB4YE#<7@Q*XobCj;hHuRG*n=eC}Ql zxhn{t2B2i5$+k=dWSQRaZt_|;JAZXENK*sNy4X01!ciRrTHb_#liz3(y2@y6r?aiR zG*GM<;b@41+<5$H%aaGQ+<^<9T!tgI)_Mxc;V%;qa;M|s$21vR#wqjLj+uP#Mk8iC zXYTAkR3KjW>^z+A?%#9=S=rXB=sMryXAR(zOcmNidv233!rMaC2H@is{oGNV9PbO) zUk&k*po<qnOIK}cX?aAS^R_)2sxgH$01^A;eHZQJ1?+3dL(=}rwarCrQo0-4Y-C5z zmZ#0!plu0d^d|OYBX;oujRGsSk6{w0ZiXcRDDJX+`5Chu$&W=bciW1VACDs*hW=oN z7B^JeVPk$-tv}h|(N!yrvc85<J(|N?8Dpy~bu(ILt0O|rPq?Ld$gEZYbFzFkJ82Dh z&%_O0FuCfEJ6}TUIq$VEfIVLfVIEgWrR5y<Jd51E1EE&wHgj~Q7oPAcs&QnJK#Oq= z3U=44XwACmvbzaYgBvU+H8`v*1PgJ7owF@8KIjU!F^|SUW`f>QYX!M|?yWi-nPT~# zvfmbOLUm|1(?J0}0RP-8M-_i-cdBl;z$3(2&e~}*)7)KpA;#l5MKm?RYc6k`_7W2% z<m9ZM$*G2-{*ABme_qMKxKTafwMcyWExNu<VgM}wX6fu`XY204|H9qH*+3r`fK!*t zX5#5X2*3iKVgUgE+iDQsLeecrq;wt_o*a&A&^g%K8KnQvtt;P`(@mqi>;|IRGaSmc zxNE)G6r$g)s==r1j7Rh~i%Lb*$!J(T)DeUVuPZ7kE_!<voPS|1kJ<eWMba8DjrWP2 z_iJ`;<ACm8u=`TVb>j$#f@k4HOOsdeN$}-Ihwg^8?n<8CL3$}_H+Ivn^(rXiCof1| zuE$<f2q0%uckbx?#RtmIucn6HqEx`&ec*qVgxMWyjSOpQ@bOO&Yw&T8jf~go2~G<w ztsH5LXw~ZthO6(`Qf+R!HQ5x0g2XopH)(8BY}TluTo=u0+(3WPd#|wQn#p{5!fWKK z!7>H+?L%XupZ`*|-u(LM@$iWU6w05d-dH9ZTBBZ~7_>L-x>^>zl$1|4#ndx0vX&^T zU=HQy2mF<EeO!#oi(BM-o0R`#Uft~o$27+^c)9r0k$)k5<oO8cIi#t5yUgjIt#H5< zAYP)o%-I1_M0LdVZSEmjdbQX*`O(-gSW}49ed0&SG{9~Aa4~z8WT0}R1bukPx<7qO za!ZnrpP%CFD>tlLYFOAHoPT%L|A}BMz`ywHKNA>n{{6rGi9gMSuX%|9|G(e&cT#_s p`M;zHNd8&$zfbu4oBwx08_7Q^pre6{_vZre_R!w)GA8|l{s)EOuulL0
--- a/browser/extensions/shield-recipe-client/test/browser/head.js +++ b/browser/extensions/shield-recipe-client/test/browser/head.js @@ -1,12 +1,14 @@ const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/Preferences.jsm", this); +Cu.import("resource://testing-common/AddonTestUtils.jsm", this); +Cu.import("resource://shield-recipe-client/lib/Addons.jsm", this); Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this); Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this); Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm", this); Cu.import("resource://shield-recipe-client/lib/Utils.jsm", this); // Load mocking/stubbing library, sinon // docs: http://sinonjs.org/docs/ const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader); @@ -21,56 +23,129 @@ sinon.assert.fail = function(message) { registerCleanupFunction(async function() { // Cleanup window or the test runner will throw an error delete window.sinon; }); this.UUID_REGEX = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/; -this.withSandboxManager = function(Assert, testFunction) { - return async function inner() { - const sandboxManager = new SandboxManager(); - sandboxManager.addHold("test running"); +this.TEST_XPI_URL = (function() { + const dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append("fixtures"); + dir.append("normandy.xpi"); + return Services.io.newFileURI(dir).spec; +})(); + +this.withWebExtension = function(manifestOverrides = {}) { + return function wrapper(testFunction) { + return async function wrappedTestFunction(...args) { + const random = Math.random().toString(36).replace(/0./, "").substr(-3); + let id = `normandydriver_${random}@example.com`; + if ("id" in manifestOverrides) { + id = manifestOverrides.id; + delete manifestOverrides.id; + } + + const manifest = Object.assign({ + manifest_version: 2, + name: "normandy_fixture", + version: "1.0", + description: "Dummy test fixture that's a webextension", + applications: { + gecko: { id }, + }, + }, manifestOverrides); + + const addonFile = AddonTestUtils.createTempWebExtensionFile({manifest}); + + // Workaround: Add-on files are cached by URL, and + // createTempWebExtensionFile re-uses filenames if the previous file has + // been deleted. So we need to flush the cache to avoid it. + Services.obs.notifyObservers(addonFile, "flush-cache-entry"); - await testFunction(sandboxManager); + try { + await testFunction(...args, [id, addonFile]); + } finally { + AddonTestUtils.cleanupTempXPIs(); + } + }; + }; +}; - sandboxManager.removeHold("test running"); - await sandboxManager.isNuked() - .then(() => Assert.ok(true, "sandbox is nuked")) - .catch(e => Assert.ok(false, "sandbox is nuked", e)); +this.withInstalledWebExtension = function(manifestOverrides = {}) { + return function wrapper(testFunction) { + return decorate( + withWebExtension(manifestOverrides), + async function wrappedTestFunction(...args) { + const [id, file] = args[args.length - 1]; + const startupPromise = AddonTestUtils.promiseWebExtensionStartup(id); + const url = Services.io.newFileURI(file).spec; + await Addons.install(url); + await startupPromise; + try { + await testFunction(...args); + } finally { + if (await Addons.get(id)) { + await Addons.uninstall(id); + } + } + } + ); + }; +}; + +this.withSandboxManager = function(Assert) { + return function wrapper(testFunction) { + return async function wrappedTestFunction(...args) { + const sandboxManager = new SandboxManager(); + sandboxManager.addHold("test running"); + + await testFunction(...args, sandboxManager); + + sandboxManager.removeHold("test running"); + await sandboxManager.isNuked() + .then(() => Assert.ok(true, "sandbox is nuked")) + .catch(e => Assert.ok(false, "sandbox is nuked", e)); + }; }; }; this.withDriver = function(Assert, testFunction) { - return withSandboxManager(Assert, async function inner(sandboxManager) { + return withSandboxManager(Assert)(async function inner(...args) { + const sandboxManager = args[args.length - 1]; const driver = new NormandyDriver(sandboxManager); - await testFunction(driver); + await testFunction(driver, ...args); }); }; this.withMockNormandyApi = function(testFunction) { return async function inner(...args) { const mockApi = {actions: [], recipes: [], implementations: {}}; - sinon.stub(NormandyApi, "fetchActions", async () => mockApi.actions); - sinon.stub(NormandyApi, "fetchRecipes", async () => mockApi.recipes); - sinon.stub(NormandyApi, "fetchImplementation", async action => { - const impl = mockApi.implementations[action.name]; - if (!impl) { - throw new Error("Missing"); + // Use callsFake instead of resolves so that the current values in mockApi are used. + mockApi.fetchActions = sinon.stub(NormandyApi, "fetchActions").callsFake(async () => mockApi.actions); + mockApi.fetchRecipes = sinon.stub(NormandyApi, "fetchRecipes").callsFake(async () => mockApi.recipes); + mockApi.fetchImplementation = sinon.stub(NormandyApi, "fetchImplementation").callsFake( + async action => { + const impl = mockApi.implementations[action.name]; + if (!impl) { + throw new Error("Missing"); + } + return impl; } - return impl; - }); + ); - await testFunction(mockApi, ...args); - - NormandyApi.fetchActions.restore(); - NormandyApi.fetchRecipes.restore(); - NormandyApi.fetchImplementation.restore(); + try { + await testFunction(mockApi, ...args); + } finally { + mockApi.fetchActions.restore(); + mockApi.fetchRecipes.restore(); + mockApi.fetchImplementation.restore(); + } }; }; const preferenceBranches = { user: Preferences, default: new Preferences({defaultBranch: true}), }; @@ -113,8 +188,91 @@ class MockPreferences { preferenceBranch.set(name, oldValue); } else { preferenceBranch.reset(name); } } } } } + +this.withPrefEnv = function(inPrefs) { + return function wrapper(testFunc) { + return async function inner(...args) { + await SpecialPowers.pushPrefEnv(inPrefs); + try { + await testFunc(...args); + } finally { + await SpecialPowers.popPrefEnv(); + } + }; + }; +}; + +/** + * Combine a list of functions right to left. The rightmost function is passed + * to the preceeding function as the argument; the result of this is passed to + * the next function until all are exhausted. For example, this: + * + * decorate(func1, func2, func3); + * + * is equivalent to this: + * + * func1(func2(func3)); + */ +this.decorate = function(...args) { + const funcs = Array.from(args); + let decorated = funcs.pop(); + funcs.reverse(); + for (const func of funcs) { + decorated = func(decorated); + } + return decorated; +}; + +/** + * Wrapper around add_task for declaring tests that use several with-style + * wrappers. The last argument should be your test function; all other arguments + * should be functions that accept a single test function argument. + * + * The arguments are combined using decorate and passed to add_task as a single + * test function. + * + * @param {[Function]} args + * @example + * decorate_task( + * withMockPreferences, + * withMockNormandyApi, + * async function myTest(mockPreferences, mockApi) { + * // Do a test + * } + * ); + */ +this.decorate_task = function(...args) { + return add_task(decorate(...args)); +}; + +let _studyFactoryId = 0; +this.studyFactory = function(attrs) { + return Object.assign({ + recipeId: _studyFactoryId++, + name: "Test study", + description: "fake", + active: true, + addonId: "fake@example.com", + addonUrl: "http://test/addon.xpi", + addonVersion: "1.0.0", + studyStartDate: new Date(), + }, attrs); +}; + +this.withStub = function(...stubArgs) { + return function wrapper(testFunction) { + return async function wrappedTestFunction(...args) { + const stub = sinon.stub(...stubArgs); + try { + await testFunction(...args, stub); + } finally { + stub.restore(); + } + }; + }; +};
--- a/browser/extensions/shield-recipe-client/test/unit/head_xpc.js +++ b/browser/extensions/shield-recipe-client/test/unit/head_xpc.js @@ -18,24 +18,25 @@ if (!extensionDir.exists()) { extensionDir = extensionDir.parent; extensionDir.append(EXTENSION_ID + ".xpi"); } Components.manager.addBootstrappedManifestLocation(extensionDir); // ================================================ // Load mocking/stubbing library, sinon // docs: http://sinonjs.org/releases/v2.3.2/ +/* exported sinon */ Cu.import("resource://gre/modules/Timer.jsm"); const {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}); const loader = new Loader.Loader({ paths: { "": "resource://testing-common/", }, globals: { setTimeout, setInterval, clearTimeout, clearInterval, }, }); const require = Loader.Require(loader, {id: ""}); -const sinon = require("sinon-2.3.2"); +this.sinon = require("sinon-2.3.2"); // ================================================
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/invalid_recipe_signature_api/api/v1/index.json @@ -0,0 +1,4 @@ +{ + "recipe-signed": "/api/v1/recipe/signed/", + "classify-client": "/api/v1/classify_client/" +}
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/invalid_recipe_signature_api/api/v1/recipe/signed/index.json @@ -0,0 +1,1 @@ +[{"recipe":{"action":"console-log","arguments":{"message":"this signature does not match this recipe"},"channels":[],"countries":[],"enabled":true,"extra_filter_expression":"true || true","filter_expression":"true || true","id":1,"last_updated":"2017-02-17T18:29:09.839239Z","locales":[],"name":"system-addon-test","revision_id":"b2cb8a26e132182d7d02cf50695d2c7f06cf3b954ff2ff63bca49d724ee91950"},"signature":{"public_key":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p","signature":"p4g3eurmPsJK5UcGT97BRyKstpwZ_2mNJkDGpd6QXlkXfvgwprjeyb5yeIEkKUXqc6krWid4obB_OP9-CwOi9tvKY1pV8p98CT5BhF0IVgpF3b7KBW1a0BVdg5owoG5W","timestamp":"2017-02-17T18:29:09.847614Z","x5u":"/normandy.content-signature.mozilla.org-20210705.dev.chain"}}]
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/invalid_recipe_signature_api/normandy.content-signature.mozilla.org-20210705.dev.chain @@ -0,0 +1,123 @@ +-----BEGIN CERTIFICATE----- +MIIGRTCCBC2gAwIBAgIEAQAABTANBgkqhkiG9w0BAQwFADBrMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHQWxsaXpvbTEXMBUGA1UECxMOQ2xvdWQgU2VydmljZXMxMTAv +BgNVBAMTKERldnppbGxhIFNpZ25pbmcgU2VydmljZXMgSW50ZXJtZWRpYXRlIDEw +HhcNMTYwNzA2MjE1NzE1WhcNMjEwNzA1MjE1NzE1WjCBrzELMAkGA1UEBhMCVVMx +EzARBgNVBAgTCkNhbGlmb3JuaWExHDAaBgNVBAoTE01vemlsbGEgQ29ycG9yYXRp +b24xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMS8wLQYDVQQDEyZub3JtYW5keS5j +b250ZW50LXNpZ25hdHVyZS5tb3ppbGxhLm9yZzEjMCEGCSqGSIb3DQEJARYUc2Vj +dXJpdHlAbW96aWxsYS5vcmcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARUQqIIAiTB +GDVUWw/wk5h1IXpreq+BtE+gQr15O4tusHpCLGjOxwpHiJYnxk45fpE8JGAV19UO +hmqMUEU0k31C1EGTSZW0ducSvHrh3a8wXShZ6dxLWHItbbCGA6A7PumjggJYMIIC +VDAdBgNVHQ4EFgQUVfksSjlZ0i1TBiS1vcoObaMeXn0wge8GA1UdIwSB5zCB5IAU +/YboUIXAovChEpudDBuodHKbjUuhgcWkgcIwgb8xCzAJBgNVBAYTAlVTMQswCQYD +VQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVu +dCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNp +Z25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290 +Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIEAQAABDAMBgNVHRMBAf8EAjAA +MA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzBEBgNVHR8E +PTA7MDmgN6A1hjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3Mu +bmV0L2NhL2NybC5wZW0wQgYJYIZIAYb4QgEEBDUWM2h0dHBzOi8vY29udGVudC1z +aWduYXR1cmUuZGV2Lm1vemF3cy5uZXQvY2EvY3JsLnBlbTBOBggrBgEFBQcBAQRC +MEAwPgYIKwYBBQUHMAKGMmh0dHBzOi8vY29udGVudC1zaWduYXR1cmUuZGV2Lm1v +emF3cy5uZXQvY2EvY2EucGVtMDEGA1UdEQQqMCiCJm5vcm1hbmR5LmNvbnRlbnQt +c2lnbmF0dXJlLm1vemlsbGEub3JnMA0GCSqGSIb3DQEBDAUAA4ICAQCwb+8JTAB7 +ZfQmFqPUIV2cQQv696AaDPQCtA9YS4zmUfcLMvfZVAbK397zFr0RMDdLiTUQDoeq +rBEmPXhJRPiv6JAK4n7Jf6Y6XfXcNxx+q3garR09Vm/0CnEq/iV+ZAtPkoKIO9kr +Nkzecd894yQCF4hIuPQ5qtMySeqJmH3Dp13eq4T0Oew1Bu32rNHuBJh2xYBkWdun +aAw/YX0I5EqZBP/XA6gbiA160tTK+hnpnlMtw/ljkvfhHbWpICD4aSiTL8L3vABQ +j7bqjMKR5xDkuGWshZfcmonpvQhGTye/RZ1vz5IzA3VOJt1mz5bdZlitpaOm/Yv0 +x6aODz8GP/PiRWFQ5CW8Uf/7pGc5rSyvnfZV2ix8EzFlo8cUtuN1fjrPFPOFOLvG +iiB6S9nlXiKBGYIDdd8V8iC5xJpzjiAWJQigwSNzuc2K30+iPo3w0zazkwe5V8jW +gj6gItYxh5xwVQTPHD0EOd9HvV1ou42+rH5Y+ISFUm25zz02UtUHEK0BKtL0lmdt +DwVq5jcHn6bx2/iwUtlKvPXtfM/6JjTJlkLZLtS7U5/pwcS0owo9zAL0qg3bdm16 ++v/olmPqQFLUHmamJTzv3rojj5X/uVdx1HMM3wBjV9tRYoYaZw9RIInRmM8Z1pHv +JJ+CIZgCyd5vgp57BKiodRZcgHoCH+BkOQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHijCCBXKgAwIBAgIEAQAABDANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQK +Ex1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNv +bnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2Vj +K2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjIx +NDkyNloXDTIxMDcwNTIxNDkyNlowazELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0Fs +bGl6b20xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMTEwLwYDVQQDEyhEZXZ6aWxs +YSBTaWduaW5nIFNlcnZpY2VzIEludGVybWVkaWF0ZSAxMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAypIfUURYgWzGw8G/1Pz9zW+Tsjirx2owThiv2gys +wJiWL/9/2gzKOrYDEqlDUudfA/BjVRtT9+NbYgnhaCkNfADOAacWS83aMhedAqhP +bVd5YhGQdpijI7f1AVTSb0ehrU2nhOZHvHX5Tk2fbRx3ryefIazNTLFGpiMBbsNv +tSI/+fjW8s0MhKNqlLnk6a9mZKo0mEy7HjGYV8nzsgI17rKLx/s2HG4TFG0+JQzc +UGlum3Tg58ritDzWdyKIkmKNZ48oLBX99Qc8j8B1UyiLv6TZmjVX0I+Ds7eSWHZk +0axLEpTyf2r7fHvN4iDNCPajw+ZpuuBfbs80Ha8b8MHvnpf9fbwiirodNQOVpY4c +t5E3Us3eYwBKdqDEbECWxCKGAS2/iVVUCNKHsg0sSxgqcwxrxyrddQRUQ0EM38DZ +F/vHt+vTdHt07kezbjJe0Kklel59uSpghA0iL4vxzbTns1fuwYOgVrNGs3acTkiB +GhFOxRXUPGtpdYmv+AaR9WlWJQY1GIEoVrinPVH7bcCwyh1CcUbHL+oAFTcmc6kZ +7azNg21tWILIRL7R0IZYQm0tF5TTwCsjVC7FuHaBtkxtVrrZqeKjvVXQ8TK5VoI0 +BUQ6BKHGeTtm+0HBpheYBDy3wkOsEGbGHLEM6cMeiz6PyCXF8wXli8Qb/TjN3LHZ +e30CAwEAAaOCAd8wggHbMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGG +MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBT9huhQhcCi8KESm50M +G6h0cpuNSzCB7AYDVR0jBIHkMIHhgBSDx8s0qJaMyQCehKcuzgzVNRA75qGBxaSB +wjCBvzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFp +biBWaWV3MSYwJAYDVQQKEx1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEm +MCQGA1UEAxMdZGV2LmNvbnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG +9w0BCQEWLGNsb3Vkc2VjK2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEu +Y29tggEBMEIGCWCGSAGG+EIBBAQ1FjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJl +LmRldi5tb3phd3MubmV0L2NhL2NybC5wZW0wTgYIKwYBBQUHAQEEQjBAMD4GCCsG +AQUFBzAChjJodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3MubmV0 +L2NhL2NhLnBlbTANBgkqhkiG9w0BAQwFAAOCAgEAbum0z0ccqI1Wp49VtsGmUPHA +gjPPy2Xa5NePmqY87WrGdhjN3xbLVb3hx8T2N6pqDjMY2rEynXKEOek3oJkQ3C6J +8AFP6Y93gaAlNz6EA0J0mqdW5TMI8YEYsu2ma+dQQ8wm9f/5b+/Y8qwfhztP06W5 +H6IG04/RvgUwYMnSR4QvT309fu5UmCRUDzsO53ZmQCfmN94u3NxHc4S6n0Q6AKAM +kh5Ld9SQnlqqqDykzn7hYDi8nTLWc7IYqkGfNMilDEKbAl4CjnSfyEvpdFAJ9sPR +UL+kaWFSMvaqIPNpxS5OpoPZjmxEc9HHnCHxtfDHWdXTJILjijPrCdMaxOCHfIqV +5roOJggI4RZ0YM68IL1MfN4IEVOrHhKjDHtd1gcmy2KU4jfj9LQq9YTnyvZ2z1yS +lS310HG3or1K8Nnu5Utfe7T6ppX8bLRMkS1/w0p7DKxHaf4D/GJcCtM9lcSt9JpW +6ACKFikjWR4ZxczYKgApc0wcjd7XBuO5777xtOeyEUDHdDft3jiXA91dYM5UAzc3 +69z/3zmaELzo0gWcrjLXh7fU9AvbU4EUF6rwzxbPGF78jJcGK+oBf8uWUCkBykDt +VsAEZI1u4EDg8e/C1nFqaH9gNMArAgquYIB9rve+hdprIMnva0S147pflWopBWcb +jwzgpfquuYnnxe0CNBA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH3DCCBcSgAwIBAgIBATANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQKEx1D +b250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNvbnRl +bnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2VjK2Rl +dnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjE4MTUy +MloXDTI2MDcwNDE4MTUyMlowgb8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEW +MBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVudCBTaWduYXR1 +cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNpZ25hdHVyZS5y +b290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290Y29udGVudHNp +Z25hdHVyZUBtb3ppbGxhLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAJcPcXhD8MzF6OTn5qZ0L7lX1+PEgLKhI9g1HxxDYDVup4Zm0kZhPGmFSlml +6eVO99OvvHdAlHhQGCIG7h+w1cp66mWjfpcvtQH23uRoKZfiW3jy1jUWrvdXolxR +t1taZosjzo+9OP8TvG6LpJj7AvqUiYD4wYnQJtt0jNRN4d6MUfQwiavSS5uTBuxd +ZJ4TsPvEI+Iv4A4PSobSzxkg79LTMvsGtDLQv7nN5hMs9T18EL5GnIKoqnSQCU0d +n2CN7S3QiQ+cbORWsSYqCTj1gUbFh6X3duEB/ypLcyWFbqeJmPHikjY8q8pLjZSB +IYiTJYLyvYlKdM5QleC/xuBNnMPCftrwwLHjWE4Dd7C9t7k0R5xyOetuiHLCwOcQ +tuckp7RgFKoviMNv3gdkzwVapOklcsaRkRscv6OMTKJNsdJVIDLrPF1wMABhbEQB +64BL0uL4lkFtpXXbJzQ6mgUNQveJkfUWOoB+cA/6GtI4J0aQfvQgloCYI6jxNn/e +Nvk5OV9KFOhXS2dnDft3wHU46sg5yXOuds1u6UrOoATBNFlkS95m4zIX1Svu091+ +CKTiLK85+ZiFtAlU2bPr3Bk3GhL3Z586ae6a4QUEx6SPQVXc18ezB4qxKqFc+avI +ylccYMRhVP+ruADxtUM5Vy6x3U8BwBK5RLdecRx2FEBDImY1AgMBAAGjggHfMIIB +2zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAWBgNVHSUBAf8EDDAK +BggrBgEFBQcDAzAdBgNVHQ4EFgQUg8fLNKiWjMkAnoSnLs4M1TUQO+YwgewGA1Ud +IwSB5DCB4YAUg8fLNKiWjMkAnoSnLs4M1TUQO+ahgcWkgcIwgb8xCzAJBgNVBAYT +AlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UE +ChMdQ29udGVudCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5j +b250ZW50LXNpZ25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNl +YytkZXZyb290Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIBATBCBglghkgB +hvhCAQQENRYzaHR0cHM6Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5l +dC9jYS9jcmwucGVtME4GCCsGAQUFBwEBBEIwQDA+BggrBgEFBQcwAoYyaHR0cHM6 +Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5ldC9jYS9jYS5wZW0wDQYJ +KoZIhvcNAQEMBQADggIBAAAQ+fotZE79FfZ8Lz7eiTUzlwHXCdSE2XD3nMROu6n6 +uLTBPrf2C+k+U1FjKVvL5/WCUj6hIzP2X6Sb8+o0XHX9mKN0yoMORTEYJOnazYPK +KSUUFnE4vGgQkr6k/31gGRMTICdnf3VOOAlUCQ5bOmGIuWi401E3sbd85U+TJe0A +nHlU+XjtfzlqcBvQivdbA0s+GEG55uRPvn952aTBEMHfn+2JqKeLShl4AtUAfu+h +6md3Z2HrEC7B3GK8ekWPu0G/ZuWTuFvOimZ+5C8IPRJXcIR/siPQl1x6dpTCew6t +lPVcVuvg6SQwzvxetkNrGUe2Wb2s9+PK2PUvfOS8ee25SNmfG3XK9qJpqGUhzSBX +T8QQgyxd0Su5G7Wze7aaHZd/fqIm/G8YFR0HiC2xni/lnDTXFDPCe+HCnSk0bH6U +wpr6I1yK8oZ2IdnNVfuABGMmGOhvSQ8r7//ea9WKhCsGNQawpVWVioY7hpyNAJ0O +Vn4xqG5f6allz8lgpwAQ+AeEEClHca6hh6mj9KhD1Of1CC2Vx52GHNh/jMYEc3/g +zLKniencBqn3Y2XH2daITGJddcleN09+a1NaTkT3hgr7LumxM8EVssPkC+z9j4Vf +Gbste+8S5QCMhh00g5vR9QF8EaFqdxCdSxrsA4GmpCa5UQl8jtCnpp2DLKXuOh72 +-----END CERTIFICATE-----
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/console-log/implementation/sha384-RGx3rydrSq53UfmW9kFcK0mQYra67XIvZvr4MhmAe--ljiiMQOtgM7Cmca48um3v @@ -0,0 +1,1 @@ +(function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={exports:{},id:d,loaded:!1};return a[d].call(e.exports,e,e.exports,b),e.loaded=!0,e.exports}var c={};return b.m=a,b.c=c,b.p="",b(0)})([function(a,b,c){"use strict";Object.defineProperty(b,"__esModule",{value:!0});var d=c(1);class e extends d.Action{async execute(){this.normandy.log(this.recipe.arguments.message,"info")}}b.default=e,(0,d.registerAction)("console-log",e)},function(a,b){(function(c){"use strict";Object.defineProperty(b,"__esModule",{value:!0});b.Action=class{constructor(g,h){this.normandy=g,this.recipe=h}};const e=b.registerAction=c&&c.registerAction||window&&window.registerAction||function(){},f=b.registerAsyncCallback=c&&c.registerAsyncCallback||window&&window.registerAsyncCallback||function(){}}).call(b,function(){return this}())}]); \ No newline at end of file
--- a/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/console-log/index.json +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/console-log/index.json @@ -9,11 +9,11 @@ } }, "required": [ "message" ], "title": "Log a message to the console", "type": "object" }, - "implementation_url": "https://localhost:8000/api/v1/action/console-log/implementation/0765c6302db2846b85543eea743f959c19b42559/", - "name": "console-log" + "name": "console-log", + "implementation_url": "https://localhost:8443/api/v1/action/console-log/implementation/sha384-RGx3rydrSq53UfmW9kFcK0mQYra67XIvZvr4MhmAe--ljiiMQOtgM7Cmca48um3v/" }
--- a/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/index.json +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/index.json @@ -10,22 +10,54 @@ } }, "required": [ "message" ], "title": "Log a message to the console", "type": "object" }, - "implementation_url": "https://localhost:8000/api/v1/action/console-log/implementation/0765c6302db2846b85543eea743f959c19b42559/", + "implementation_url": "https://localhost:8443/api/v1/action/console-log/implementation/sha384-RGx3rydrSq53UfmW9kFcK0mQYra67XIvZvr4MhmAe--ljiiMQOtgM7Cmca48um3v/", "name": "console-log" }, { "arguments_schema": { "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "addonUrl": { + "description": "URL of the add-on XPI file", + "format": "uri", + "minLength": 1, + "type": "string" + }, + "description": { + "description": "User-facing description of the study", + "minLength": 1, + "type": "string" + }, + "name": { + "description": "User-facing name of the study", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "description", + "addonUrl" + ], + "title": "Enroll a user in an opt-out SHIELD study", + "type": "object" + }, + "implementation_url": "https://localhost:8443/api/v1/action/opt-out-study/implementation/sha384-HM_avYcD00o27ufwU1V7PIBtiuMAXML6MMwlYrDEqDX-XzGVuOfL52RCM680JExN/", + "name": "opt-out-study" + }, + { + "arguments_schema": { + "$schema": "http://json-schema.org/draft-04/schema#", "description": "This action shows a single survey.", "properties": { "engagementButtonLabel": { "default": "", "description": "Text for the engagement button. If specified, this button will be shown instead of rating stars.", "type": "string" }, "includeTelemetryUUID": { @@ -48,16 +80,26 @@ "description": "Message to show to the user", "type": "string" }, "postAnswerUrl": { "default": "", "description": "URL to redirect the user to after rating Firefox or clicking the engagement button", "type": "string" }, + "repeatEvery": { + "default": null, + "description": "How often (in days) the action is displayed.", + "type": "number" + }, + "repeatOption": { + "default": "once", + "description": "Determines how often an action executes. (once|nag|xdays)", + "type": "string" + }, "surveyId": { "description": "Slug uniquely identifying this survey in telemetry", "type": "string" }, "thanksMessage": { "default": "", "description": "Thanks message to show to the user after they've rated Firefox", "type": "string" @@ -66,12 +108,98 @@ "required": [ "surveyId", "message", "thanksMessage" ], "title": "Show a Heartbeat survey.", "type": "object" }, - "implementation_url": "https://localhost:8000/api/v1/action/show-heartbeat/implementation/448bbffce82ff27b3d532fd7196bcb190be5c067/", + "implementation_url": "https://localhost:8443/api/v1/action/show-heartbeat/implementation/sha384-dEGiyKPEln8Ns5cQHzGpMIGdirSAAX0X-Kwlu-U3sJ05yNbO-ANij_a6c5SyL7G4/", "name": "show-heartbeat" + }, + { + "arguments_schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "branches": { + "description": "List of experimental branches", + "items": { + "properties": { + "ratio": { + "default": 1, + "description": "Ratio of users who should be grouped into this branch", + "minimum": 1, + "type": "integer" + }, + "slug": { + "description": "Unique identifier for this branch of the experiment", + "pattern": "^[A-Za-z0-9\\-_]+$", + "type": "string" + }, + "value": { + "description": "Value to set the preference to for this branch", + "type": [ + "string", + "number", + "boolean" + ] + } + }, + "required": [ + "slug", + "value", + "ratio" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "experimentDocumentUrl": { + "default": "", + "description": "URL of a document describing the experiment", + "format": "uri", + "type": "string" + }, + "preferenceBranchType": { + "default": "default", + "descript": "Controls whether the default or user value of the preference is modified", + "enum": [ + "user", + "default" + ], + "type": "string" + }, + "preferenceName": { + "default": "", + "description": "Full dotted-path of the preference that controls this experiment", + "type": "string" + }, + "preferenceType": { + "default": "boolean", + "description": "Data type of the preference that controls this experiment", + "enum": [ + "string", + "integer", + "boolean" + ], + "type": "string" + }, + "slug": { + "description": "Unique identifier for this experiment", + "pattern": "^[A-Za-z0-9\\-_]+$", + "type": "string" + } + }, + "required": [ + "slug", + "preferenceName", + "preferenceType", + "branches" + ], + "title": "Run a feature experiment activated by a preference.", + "type": "object" + }, + "implementation_url": "https://localhost:8443/api/v1/action/preference-experiment/implementation/sha384-KQgG38GQ7KZAb2VIB48ANQO6nBcxZoLm2ORzUviRT5nAvSywyPjZ5cJIElw6iXIt/", + "name": "preference-experiment" } ]
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/opt-out-study/implementation/sha384-HM_avYcD00o27ufwU1V7PIBtiuMAXML6MMwlYrDEqDX-XzGVuOfL52RCM680JExN @@ -0,0 +1,1 @@ +(function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={exports:{},id:d,loaded:!1};return a[d].call(e.exports,e,e.exports,b),e.loaded=!0,e.exports}var c={};return b.m=a,b.c=c,b.p="",b(0)})([function(a,b,c){"use strict";async function e(i){const j=i.studies;if(void 0===j)return void i.log("Client does not support studies, aborting.","info");const k=(await j.getAll()).filter((l)=>l.active);for(const l of k)if(!g.includes(l.recipeId)){i.log("Stopping study for recipe ${study.recipeId}.","debug");try{await j.stop(l.recipeId)}catch(m){i.log(`Error while stopping study for recipe ${l.recipeId}: ${m}`,"error")}}}Object.defineProperty(b,"__esModule",{value:!0}),b.resetAction=function(){g=[]},b.postExecutionHook=e;var f=c(1);let g=[];class h extends f.Action{async execute(){const i=this.recipe.id;var j=this.recipe.arguments;const k=j.name,l=j.description,m=j.addonUrl,n=this.normandy.studies;if(void 0===n)return void this.normandy.log("Client does not support studies, aborting.","info");g.push(i);const o=await n.has(i);o?this.normandy.log(`Study for recipe ${i} already exists`,"debug"):(this.normandy.log(`Starting study for recipe ${i}`,"debug"),await n.start({recipeId:i,name:k,description:l,addonUrl:m}))}}b.default=h,(0,f.registerAction)("opt-out-study",h),(0,f.registerAsyncCallback)("postExecution",e)},function(a,b){(function(c){"use strict";Object.defineProperty(b,"__esModule",{value:!0});b.Action=class{constructor(g,h){this.normandy=g,this.recipe=h}};const e=b.registerAction=c&&c.registerAction||window&&window.registerAction||function(){},f=b.registerAsyncCallback=c&&c.registerAsyncCallback||window&&window.registerAsyncCallback||function(){}}).call(b,function(){return this}())}]); \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/opt-out-study/index.json @@ -0,0 +1,32 @@ +{ + "arguments_schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "addonUrl": { + "description": "URL of the add-on XPI file", + "format": "uri", + "minLength": 1, + "type": "string" + }, + "description": { + "description": "User-facing description of the study", + "minLength": 1, + "type": "string" + }, + "name": { + "description": "User-facing name of the study", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "description", + "addonUrl" + ], + "title": "Enroll a user in an opt-out SHIELD study", + "type": "object" + }, + "implementation_url": "https://localhost:8443/api/v1/action/opt-out-study/implementation/sha384-HM_avYcD00o27ufwU1V7PIBtiuMAXML6MMwlYrDEqDX-XzGVuOfL52RCM680JExN/", + "name": "opt-out-study" +}
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/preference-experiment/implementation/sha384-KQgG38GQ7KZAb2VIB48ANQO6nBcxZoLm2ORzUviRT5nAvSywyPjZ5cJIElw6iXIt @@ -0,0 +1,2 @@ +(function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={exports:{},id:d,loaded:!1};return a[d].call(e.exports,e,e.exports,b),e.loaded=!0,e.exports}var c={};return b.m=a,b.c=c,b.p="",b(0)})([function(a,b,c){"use strict";async function e(i){if(void 0===i.preferenceExperiments)return void i.log("Client does not support preference experiments, aborting.","info");const j=await i.preferenceExperiments.getAllActive();for(const k of j)g.includes(k.name)||(await i.preferenceExperiments.stop(k.name,!0))}Object.defineProperty(b,"__esModule",{value:!0}),b.resetAction=function(){g=[]},b.postExecutionHook=e;var f=c(1);let g=[];class h extends f.Action{async execute(){var i=this.recipe.arguments;const j=i.slug,k=i.preferenceName,l=i.preferenceBranchType,m=i.branches,n=i.preferenceType,o=this.normandy.preferenceExperiments;if(void 0===o)return void this.normandy.log("Client does not support preference experiments, aborting.","info");g.push(j);const p=await o.has(j);if(!p){const q=await o.getAllActive(),r=q.some((t)=>t.preferenceName===k);if(r)return void this.normandy.log(`Experiment ${j} ignored; another active experiment is already using the + ${k} preference.`,"warn");const s=await this.chooseBranch(m);await o.start({name:j,branch:s.slug,preferenceName:k,preferenceValue:s.value,preferenceBranchType:l,preferenceType:n})}else{const q=await o.get(j);q.expired?this.normandy.log(`Experiment ${j} has expired, aborting.`,"debug"):await o.markLastSeen(j)}}async chooseBranch(i){const j=this.recipe.arguments.slug,k=i.map((n)=>n.ratio),l=`${this.normandy.userId}-${j}-branch`,m=await this.normandy.ratioSample(l,k);return i[m]}}b.default=h,(0,f.registerAction)("preference-experiment",h),(0,f.registerAsyncCallback)("postExecution",e)},function(a,b){(function(c){"use strict";Object.defineProperty(b,"__esModule",{value:!0});b.Action=class{constructor(g,h){this.normandy=g,this.recipe=h}};const e=b.registerAction=c&&c.registerAction||window&&window.registerAction||function(){},f=b.registerAsyncCallback=c&&c.registerAsyncCallback||window&&window.registerAsyncCallback||function(){}}).call(b,function(){return this}())}]); \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/preference-experiment/index.json @@ -0,0 +1,86 @@ +{ + "arguments_schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "branches": { + "description": "List of experimental branches", + "items": { + "properties": { + "ratio": { + "default": 1, + "description": "Ratio of users who should be grouped into this branch", + "minimum": 1, + "type": "integer" + }, + "slug": { + "description": "Unique identifier for this branch of the experiment", + "pattern": "^[A-Za-z0-9\\-_]+$", + "type": "string" + }, + "value": { + "description": "Value to set the preference to for this branch", + "type": [ + "string", + "number", + "boolean" + ] + } + }, + "required": [ + "slug", + "value", + "ratio" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "experimentDocumentUrl": { + "default": "", + "description": "URL of a document describing the experiment", + "format": "uri", + "type": "string" + }, + "preferenceBranchType": { + "default": "default", + "descript": "Controls whether the default or user value of the preference is modified", + "enum": [ + "user", + "default" + ], + "type": "string" + }, + "preferenceName": { + "default": "", + "description": "Full dotted-path of the preference that controls this experiment", + "type": "string" + }, + "preferenceType": { + "default": "boolean", + "description": "Data type of the preference that controls this experiment", + "enum": [ + "string", + "integer", + "boolean" + ], + "type": "string" + }, + "slug": { + "description": "Unique identifier for this experiment", + "pattern": "^[A-Za-z0-9\\-_]+$", + "type": "string" + } + }, + "required": [ + "slug", + "preferenceName", + "preferenceType", + "branches" + ], + "title": "Run a feature experiment activated by a preference.", + "type": "object" + }, + "implementation_url": "https://localhost:8443/api/v1/action/preference-experiment/implementation/sha384-KQgG38GQ7KZAb2VIB48ANQO6nBcxZoLm2ORzUviRT5nAvSywyPjZ5cJIElw6iXIt/", + "name": "preference-experiment" +}
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/show-heartbeat/implementation/sha384-dEGiyKPEln8Ns5cQHzGpMIGdirSAAX0X-Kwlu-U3sJ05yNbO-ANij_a6c5SyL7G4 @@ -0,0 +1,1 @@ +(function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={exports:{},id:d,loaded:!1};return a[d].call(e.exports,e,e.exports,b),e.loaded=!0,e.exports}var c={};return b.m=a,b.c=c,b.p="",b(0)})([function(a,b,c){"use strict";Object.defineProperty(b,"__esModule",{value:!0});var d=Object.assign||function(k){for(var m,l=1;l<arguments.length;l++)for(var n in m=arguments[l],m)Object.prototype.hasOwnProperty.call(m,n)&&(k[n]=m[n]);return k},e=c(1);const g=24*(3600*1e3);class j extends e.Action{constructor(k,l){super(k,l),this.storage=k.createStorage(l.id),this.heartbeatStorage=k.createStorage("normandy-heartbeat"),this.updateLastInteraction=this.updateLastInteraction.bind(this),this.updateLastShown=this.updateLastShown.bind(this)}generateSurveyId(){var k=this.recipe.arguments;const l=k.includeTelemetryUUID,m=k.surveyId,n=this.normandy.userId;let o=m;return l&&!!n&&(o=`${m}::${n}`),o}async heartbeatShownRecently(){const k=await this.heartbeatStorage.getItem("lastShown"),l=k?new Date-parseFloat(k):Infinity;return l<g}async getLastShown(){const k=await this.storage.getItem("lastShown");return"undefined"==typeof k?null:parseFloat(k)}async hasShownBefore(){return null!==(await this.storage.getItem("lastShown"))}async shownAtleastDaysAgo(k){const l=await this.hasShownBefore();if(!l)return!1;const m=await this.getLastShown(),n=Date.now()-m;return n<g*k}async getLastInteraction(){const k=await this.storage.getItem("lastInteraction");return"undefined"==typeof k?null:parseFloat(k)}async sinceLastInteraction(){const k=await this.getLastInteraction();return"undefined"==typeof k?null:Date.now()-k}async hasHadInteraction(){const k=await this.getLastInteraction();return!!k}async heartbeatHasExecuted(){let k=!1;var l=this.recipe.arguments;const m=l.repeatOption,n=l.repeatEvery;switch(m){default:case"once":k=await this.hasShownBefore();break;case"nag":k=await this.hasHadInteraction();break;case"xdays":k=await this.shownAtleastDaysAgo(n);}return k}async shouldNotExecute(){return!this.normandy.testing&&((await this.heartbeatShownRecently())||(await this.heartbeatHasExecuted()))}async execute(){var k=this.recipe.arguments;const l=k.message,m=k.engagementButtonLabel,n=k.thanksMessage,o=k.postAnswerUrl,p=k.learnMoreMessage,q=k.learnMoreUrl;if(await this.shouldNotExecute())return;this.client=await this.normandy.client();const r=this.normandy.userId,s=this.generateSurveyId(),t={surveyId:s,message:l,engagementButtonLabel:m,thanksMessage:n,learnMoreMessage:p,learnMoreUrl:q,postAnswerUrl:this.generatePostURL(o,r),flowId:this.normandy.uuid(),surveyVersion:this.recipe.revision_id};this.normandy.testing&&(t.testing=1);const u=await this.normandy.showHeartbeat(t);["Voted","Engaged"].forEach((w)=>{u.on(w,this.updateLastInteraction)}),this.updateLastShown()}updateLastShown(){this.storage.setItem("lastShown",Date.now()),this.heartbeatStorage.setItem("lastShown",Date.now())}updateLastInteraction(){this.storage.setItem("lastInteraction",Date.now())}getGAParams(){let k=this.recipe.arguments.message||"";k=k.replace(/\s+/g,""),k=encodeURIComponent(k);const l=new URL("http://mozilla.com");return l.searchParams.set("message",k),k=l.search.replace("?message=",""),{utm_source:"firefox",utm_medium:this.recipe.action,utm_campaign:k}}generatePostURL(k,l){if(!k)return k;const m=d({source:"heartbeat",surveyversion:56,updateChannel:this.client.channel,fxVersion:this.client.version,isDefaultBrowser:this.client.isDefaultBrowser?1:0,searchEngine:this.client.searchEngine,syncSetup:this.client.syncSetup?1:0},this.getGAParams());this.recipe.arguments.includeTelemetryUUID&&l&&(m.userId=l),this.normandy.testing&&(m.testing=1);const n=new URL(k);for(const o in m)m.hasOwnProperty(o)&&n.searchParams.set(o,m[o]);return n.href}}b.default=j,(0,e.registerAction)("show-heartbeat",j)},function(a,b){(function(c){"use strict";Object.defineProperty(b,"__esModule",{value:!0});b.Action=class{constructor(g,h){this.normandy=g,this.recipe=h}};const e=b.registerAction=c&&c.registerAction||window&&window.registerAction||function(){},f=b.registerAsyncCallback=c&&c.registerAsyncCallback||window&&window.registerAsyncCallback||function(){}}).call(b,function(){return this}())}]); \ No newline at end of file
--- a/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/show-heartbeat/index.json +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/show-heartbeat/index.json @@ -28,16 +28,26 @@ "description": "Message to show to the user", "type": "string" }, "postAnswerUrl": { "default": "", "description": "URL to redirect the user to after rating Firefox or clicking the engagement button", "type": "string" }, + "repeatEvery": { + "default": null, + "description": "How often (in days) the action is displayed.", + "type": "number" + }, + "repeatOption": { + "default": "once", + "description": "Determines how often an action executes. (once|nag|xdays)", + "type": "string" + }, "surveyId": { "description": "Slug uniquely identifying this survey in telemetry", "type": "string" }, "thanksMessage": { "default": "", "description": "Thanks message to show to the user after they've rated Firefox", "type": "string" @@ -46,11 +56,11 @@ "required": [ "surveyId", "message", "thanksMessage" ], "title": "Show a Heartbeat survey.", "type": "object" }, - "implementation_url": "https://localhost:8000/api/v1/action/show-heartbeat/implementation/448bbffce82ff27b3d532fd7196bcb190be5c067/", + "implementation_url": "https://localhost:8443/api/v1/action/show-heartbeat/implementation/sha384-dEGiyKPEln8Ns5cQHzGpMIGdirSAAX0X-Kwlu-U3sJ05yNbO-ANij_a6c5SyL7G4/", "name": "show-heartbeat" }
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/action/signed/index.json @@ -0,0 +1,1 @@ +[{"action":{"arguments_schema":{"$schema":"http://json-schema.org/draft-04/schema#","properties":{"message":{"default":"","description":"Message to log to the console","type":"string"}},"required":["message"],"title":"Log a message to the console","type":"object"},"implementation_url":"/api/v1/action/console-log/implementation/sha384-RGx3rydrSq53UfmW9kFcK0mQYra67XIvZvr4MhmAe--ljiiMQOtgM7Cmca48um3v/","name":"console-log"},"signature":{"public_key":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p","signature":"_PnFvgyHCF2ncR50U4nKbWhZcK7OZejvjSWlcAE8kHDDlXoJirtNnN48jEAMEzt1FfacqWjT4QF3aEV1te35FgKQCwjtiDyjmrQR4cQb3T3aqUFGZNSzCBIEWMTN6RoY","timestamp":"2017-08-03T21:10:32.538860Z","x5u":"/normandy.content-signature.mozilla.org-20210705.dev.chain"}},{"action":{"arguments_schema":{"$schema":"http://json-schema.org/draft-04/schema#","properties":{"addonUrl":{"description":"URL of the add-on XPI file","format":"uri","minLength":1,"type":"string"},"description":{"description":"User-facing description of the study","minLength":1,"type":"string"},"name":{"description":"User-facing name of the study","minLength":1,"type":"string"}},"required":["name","description","addonUrl"],"title":"Enroll a user in an opt-out SHIELD study","type":"object"},"implementation_url":"/api/v1/action/opt-out-study/implementation/sha384-HM_avYcD00o27ufwU1V7PIBtiuMAXML6MMwlYrDEqDX-XzGVuOfL52RCM680JExN/","name":"opt-out-study"},"signature":{"public_key":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p","signature":"RllXGVzj-c0ZaHxzKE6k-GJXOtEl-NNprMi4I-TvbBDZUlhq4UPOQidpCvn74A41M6cV7bb9qxw-M1Z9t4BqUMQaOT4yXiEU9DoBMBASA-C_xf7VXtwWwH8GvT8C4yh_","timestamp":"2017-08-03T21:10:32.562478Z","x5u":"/normandy.content-signature.mozilla.org-20210705.dev.chain"}},{"action":{"arguments_schema":{"$schema":"http://json-schema.org/draft-04/schema#","description":"This action shows a single survey.","properties":{"engagementButtonLabel":{"default":"","description":"Text for the engagement button. If specified, this button will be shown instead of rating stars.","type":"string"},"includeTelemetryUUID":{"default":false,"description":"Include unique user ID in post-answer-url and Telemetry","type":"boolean"},"learnMoreMessage":{"default":"","description":"Message to show to the user to learn more","type":"string"},"learnMoreUrl":{"default":"","description":"URL to show to the user when they click Learn More","type":"string"},"message":{"default":"","description":"Message to show to the user","type":"string"},"postAnswerUrl":{"default":"","description":"URL to redirect the user to after rating Firefox or clicking the engagement button","type":"string"},"repeatEvery":{"default":null,"description":"How often (in days) the action is displayed.","type":"number"},"repeatOption":{"default":"once","description":"Determines how often an action executes. (once|nag|xdays)","type":"string"},"surveyId":{"description":"Slug uniquely identifying this survey in telemetry","type":"string"},"thanksMessage":{"default":"","description":"Thanks message to show to the user after they've rated Firefox","type":"string"}},"required":["surveyId","message","thanksMessage"],"title":"Show a Heartbeat survey.","type":"object"},"implementation_url":"/api/v1/action/show-heartbeat/implementation/sha384-dEGiyKPEln8Ns5cQHzGpMIGdirSAAX0X-Kwlu-U3sJ05yNbO-ANij_a6c5SyL7G4/","name":"show-heartbeat"},"signature":{"public_key":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p","signature":"XQLZ7g20PkKdjLgGWqVUTIu-bkeSeOTa6MEaEcfI6lpH1UcJ9YtTz3Ff73GoT2DnVSXgrH00jyxggOT-wLVSX3tJBeAEg_9MpepZoeDQ5XRqV0kIY6emjUP3YKemFGIw","timestamp":"2017-08-03T21:10:32.510980Z","x5u":"/normandy.content-signature.mozilla.org-20210705.dev.chain"}},{"action":{"arguments_schema":{"$schema":"http://json-schema.org/draft-04/schema#","properties":{"branches":{"description":"List of experimental branches","items":{"properties":{"ratio":{"default":1,"description":"Ratio of users who should be grouped into this branch","minimum":1,"type":"integer"},"slug":{"description":"Unique identifier for this branch of the experiment","pattern":"^[A-Za-z0-9\\-_]+$","type":"string"},"value":{"description":"Value to set the preference to for this branch","type":["string","number","boolean"]}},"required":["slug","value","ratio"],"type":"object"},"minItems":1,"type":"array"},"experimentDocumentUrl":{"default":"","description":"URL of a document describing the experiment","format":"uri","type":"string"},"preferenceBranchType":{"default":"default","descript":"Controls whether the default or user value of the preference is modified","enum":["user","default"],"type":"string"},"preferenceName":{"default":"","description":"Full dotted-path of the preference that controls this experiment","type":"string"},"preferenceType":{"default":"boolean","description":"Data type of the preference that controls this experiment","enum":["string","integer","boolean"],"type":"string"},"slug":{"description":"Unique identifier for this experiment","pattern":"^[A-Za-z0-9\\-_]+$","type":"string"}},"required":["slug","preferenceName","preferenceType","branches"],"title":"Run a feature experiment activated by a preference.","type":"object"},"implementation_url":"/api/v1/action/preference-experiment/implementation/sha384-KQgG38GQ7KZAb2VIB48ANQO6nBcxZoLm2ORzUviRT5nAvSywyPjZ5cJIElw6iXIt/","name":"preference-experiment"},"signature":{"public_key":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p","signature":"DqIks6qG4fWYl9riLAwFor-CGI2KrnW0bxiwV2PU45Nb2ziHMaAdm8yGaPrcZ58qvGnHZkHxgIuqyAUHzmzAAr662pmfjiTc75UXWtx4il94sqVLBF6G28U2taEU1GpJ","timestamp":"2017-08-03T20:53:52.948142Z","x5u":"/normandy.content-signature.mozilla.org-20210705.dev.chain"}}]
--- a/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/index.json +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/index.json @@ -1,8 +1,9 @@ { "action-list": "/api/v1/action/", + "action-signed": "/api/v1/action/signed/", "classify-client": "/api/v1/classify_client/", "filters": "/api/v1/filters/", "recipe-list": "/api/v1/recipe/", "recipe-signed": "/api/v1/recipe/signed/", "reciperevision-list": "/api/v1/recipe_revision/" }
--- a/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/recipe/signed/index.json +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/recipe/signed/index.json @@ -1,1 +1,1 @@ -[{"recipe":{"action":"console-log","arguments":{"message":"asdfasfda sdf sa"},"channels":[],"countries":[],"enabled":true,"extra_filter_expression":"true || true","filter_expression":"true || true","id":1,"last_updated":"2017-02-17T18:29:09.839239Z","locales":[],"name":"system-addon-test","revision_id":"b2cb8a26e132182d7d02cf50695d2c7f06cf3b954ff2ff63bca49d724ee91950"},"signature":{"public_key":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p","signature":"p4g3eurmPsJK5UcGT97BRyKstpwZ_2mNJkDGpd6QXlkXfvgwprjeyb5yeIEkKUXqc6krWid4obB_OP9-CwOi9tvKY1pV8p98CT5BhF0IVgpF3b7KBW1a0BVdg5owoG5W","timestamp":"2017-02-17T18:29:09.847614Z","x5u":"/api/v1/recipe/signed/normandy.content-signature.mozilla.org-20210705.dev.chain"}}] +[{"recipe":{"action":"console-log","arguments":{"message":"asdfasfda sdf sa"},"channels":[],"countries":[],"enabled":true,"extra_filter_expression":"true || true","filter_expression":"true || true","id":1,"last_updated":"2017-02-17T18:29:09.839239Z","locales":[],"name":"system-addon-test","revision_id":"b2cb8a26e132182d7d02cf50695d2c7f06cf3b954ff2ff63bca49d724ee91950"},"signature":{"public_key":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p","signature":"p4g3eurmPsJK5UcGT97BRyKstpwZ_2mNJkDGpd6QXlkXfvgwprjeyb5yeIEkKUXqc6krWid4obB_OP9-CwOi9tvKY1pV8p98CT5BhF0IVgpF3b7KBW1a0BVdg5owoG5W","timestamp":"2017-02-17T18:29:09.847614Z","x5u":"/normandy.content-signature.mozilla.org-20210705.dev.chain"}}]
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/test/unit/mock_api/api/v1/recipe/signed/normandy.content-signature.mozilla.org-20210705.dev.chain +++ /dev/null @@ -1,123 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIGRTCCBC2gAwIBAgIEAQAABTANBgkqhkiG9w0BAQwFADBrMQswCQYDVQQGEwJV -UzEQMA4GA1UEChMHQWxsaXpvbTEXMBUGA1UECxMOQ2xvdWQgU2VydmljZXMxMTAv -BgNVBAMTKERldnppbGxhIFNpZ25pbmcgU2VydmljZXMgSW50ZXJtZWRpYXRlIDEw -HhcNMTYwNzA2MjE1NzE1WhcNMjEwNzA1MjE1NzE1WjCBrzELMAkGA1UEBhMCVVMx -EzARBgNVBAgTCkNhbGlmb3JuaWExHDAaBgNVBAoTE01vemlsbGEgQ29ycG9yYXRp -b24xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMS8wLQYDVQQDEyZub3JtYW5keS5j -b250ZW50LXNpZ25hdHVyZS5tb3ppbGxhLm9yZzEjMCEGCSqGSIb3DQEJARYUc2Vj -dXJpdHlAbW96aWxsYS5vcmcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARUQqIIAiTB -GDVUWw/wk5h1IXpreq+BtE+gQr15O4tusHpCLGjOxwpHiJYnxk45fpE8JGAV19UO -hmqMUEU0k31C1EGTSZW0ducSvHrh3a8wXShZ6dxLWHItbbCGA6A7PumjggJYMIIC -VDAdBgNVHQ4EFgQUVfksSjlZ0i1TBiS1vcoObaMeXn0wge8GA1UdIwSB5zCB5IAU -/YboUIXAovChEpudDBuodHKbjUuhgcWkgcIwgb8xCzAJBgNVBAYTAlVTMQswCQYD -VQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVu -dCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNp -Z25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290 -Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIEAQAABDAMBgNVHRMBAf8EAjAA -MA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzBEBgNVHR8E -PTA7MDmgN6A1hjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3Mu -bmV0L2NhL2NybC5wZW0wQgYJYIZIAYb4QgEEBDUWM2h0dHBzOi8vY29udGVudC1z -aWduYXR1cmUuZGV2Lm1vemF3cy5uZXQvY2EvY3JsLnBlbTBOBggrBgEFBQcBAQRC -MEAwPgYIKwYBBQUHMAKGMmh0dHBzOi8vY29udGVudC1zaWduYXR1cmUuZGV2Lm1v -emF3cy5uZXQvY2EvY2EucGVtMDEGA1UdEQQqMCiCJm5vcm1hbmR5LmNvbnRlbnQt -c2lnbmF0dXJlLm1vemlsbGEub3JnMA0GCSqGSIb3DQEBDAUAA4ICAQCwb+8JTAB7 -ZfQmFqPUIV2cQQv696AaDPQCtA9YS4zmUfcLMvfZVAbK397zFr0RMDdLiTUQDoeq -rBEmPXhJRPiv6JAK4n7Jf6Y6XfXcNxx+q3garR09Vm/0CnEq/iV+ZAtPkoKIO9kr -Nkzecd894yQCF4hIuPQ5qtMySeqJmH3Dp13eq4T0Oew1Bu32rNHuBJh2xYBkWdun -aAw/YX0I5EqZBP/XA6gbiA160tTK+hnpnlMtw/ljkvfhHbWpICD4aSiTL8L3vABQ -j7bqjMKR5xDkuGWshZfcmonpvQhGTye/RZ1vz5IzA3VOJt1mz5bdZlitpaOm/Yv0 -x6aODz8GP/PiRWFQ5CW8Uf/7pGc5rSyvnfZV2ix8EzFlo8cUtuN1fjrPFPOFOLvG -iiB6S9nlXiKBGYIDdd8V8iC5xJpzjiAWJQigwSNzuc2K30+iPo3w0zazkwe5V8jW -gj6gItYxh5xwVQTPHD0EOd9HvV1ou42+rH5Y+ISFUm25zz02UtUHEK0BKtL0lmdt -DwVq5jcHn6bx2/iwUtlKvPXtfM/6JjTJlkLZLtS7U5/pwcS0owo9zAL0qg3bdm16 -+v/olmPqQFLUHmamJTzv3rojj5X/uVdx1HMM3wBjV9tRYoYaZw9RIInRmM8Z1pHv -JJ+CIZgCyd5vgp57BKiodRZcgHoCH+BkOQ== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIHijCCBXKgAwIBAgIEAQAABDANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMC -VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQK -Ex1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNv -bnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2Vj -K2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjIx -NDkyNloXDTIxMDcwNTIxNDkyNlowazELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0Fs -bGl6b20xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMTEwLwYDVQQDEyhEZXZ6aWxs -YSBTaWduaW5nIFNlcnZpY2VzIEludGVybWVkaWF0ZSAxMIICIjANBgkqhkiG9w0B -AQEFAAOCAg8AMIICCgKCAgEAypIfUURYgWzGw8G/1Pz9zW+Tsjirx2owThiv2gys -wJiWL/9/2gzKOrYDEqlDUudfA/BjVRtT9+NbYgnhaCkNfADOAacWS83aMhedAqhP -bVd5YhGQdpijI7f1AVTSb0ehrU2nhOZHvHX5Tk2fbRx3ryefIazNTLFGpiMBbsNv -tSI/+fjW8s0MhKNqlLnk6a9mZKo0mEy7HjGYV8nzsgI17rKLx/s2HG4TFG0+JQzc -UGlum3Tg58ritDzWdyKIkmKNZ48oLBX99Qc8j8B1UyiLv6TZmjVX0I+Ds7eSWHZk -0axLEpTyf2r7fHvN4iDNCPajw+ZpuuBfbs80Ha8b8MHvnpf9fbwiirodNQOVpY4c -t5E3Us3eYwBKdqDEbECWxCKGAS2/iVVUCNKHsg0sSxgqcwxrxyrddQRUQ0EM38DZ -F/vHt+vTdHt07kezbjJe0Kklel59uSpghA0iL4vxzbTns1fuwYOgVrNGs3acTkiB -GhFOxRXUPGtpdYmv+AaR9WlWJQY1GIEoVrinPVH7bcCwyh1CcUbHL+oAFTcmc6kZ -7azNg21tWILIRL7R0IZYQm0tF5TTwCsjVC7FuHaBtkxtVrrZqeKjvVXQ8TK5VoI0 -BUQ6BKHGeTtm+0HBpheYBDy3wkOsEGbGHLEM6cMeiz6PyCXF8wXli8Qb/TjN3LHZ -e30CAwEAAaOCAd8wggHbMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGG -MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBT9huhQhcCi8KESm50M -G6h0cpuNSzCB7AYDVR0jBIHkMIHhgBSDx8s0qJaMyQCehKcuzgzVNRA75qGBxaSB -wjCBvzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFp -biBWaWV3MSYwJAYDVQQKEx1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEm -MCQGA1UEAxMdZGV2LmNvbnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG -9w0BCQEWLGNsb3Vkc2VjK2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEu -Y29tggEBMEIGCWCGSAGG+EIBBAQ1FjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJl -LmRldi5tb3phd3MubmV0L2NhL2NybC5wZW0wTgYIKwYBBQUHAQEEQjBAMD4GCCsG -AQUFBzAChjJodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3MubmV0 -L2NhL2NhLnBlbTANBgkqhkiG9w0BAQwFAAOCAgEAbum0z0ccqI1Wp49VtsGmUPHA -gjPPy2Xa5NePmqY87WrGdhjN3xbLVb3hx8T2N6pqDjMY2rEynXKEOek3oJkQ3C6J -8AFP6Y93gaAlNz6EA0J0mqdW5TMI8YEYsu2ma+dQQ8wm9f/5b+/Y8qwfhztP06W5 -H6IG04/RvgUwYMnSR4QvT309fu5UmCRUDzsO53ZmQCfmN94u3NxHc4S6n0Q6AKAM -kh5Ld9SQnlqqqDykzn7hYDi8nTLWc7IYqkGfNMilDEKbAl4CjnSfyEvpdFAJ9sPR -UL+kaWFSMvaqIPNpxS5OpoPZjmxEc9HHnCHxtfDHWdXTJILjijPrCdMaxOCHfIqV -5roOJggI4RZ0YM68IL1MfN4IEVOrHhKjDHtd1gcmy2KU4jfj9LQq9YTnyvZ2z1yS -lS310HG3or1K8Nnu5Utfe7T6ppX8bLRMkS1/w0p7DKxHaf4D/GJcCtM9lcSt9JpW -6ACKFikjWR4ZxczYKgApc0wcjd7XBuO5777xtOeyEUDHdDft3jiXA91dYM5UAzc3 -69z/3zmaELzo0gWcrjLXh7fU9AvbU4EUF6rwzxbPGF78jJcGK+oBf8uWUCkBykDt -VsAEZI1u4EDg8e/C1nFqaH9gNMArAgquYIB9rve+hdprIMnva0S147pflWopBWcb -jwzgpfquuYnnxe0CNBA= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIH3DCCBcSgAwIBAgIBATANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMCVVMx -CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQKEx1D -b250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNvbnRl -bnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2VjK2Rl -dnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjE4MTUy -MloXDTI2MDcwNDE4MTUyMlowgb8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEW -MBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVudCBTaWduYXR1 -cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNpZ25hdHVyZS5y -b290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290Y29udGVudHNp -Z25hdHVyZUBtb3ppbGxhLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC -ggIBAJcPcXhD8MzF6OTn5qZ0L7lX1+PEgLKhI9g1HxxDYDVup4Zm0kZhPGmFSlml -6eVO99OvvHdAlHhQGCIG7h+w1cp66mWjfpcvtQH23uRoKZfiW3jy1jUWrvdXolxR -t1taZosjzo+9OP8TvG6LpJj7AvqUiYD4wYnQJtt0jNRN4d6MUfQwiavSS5uTBuxd -ZJ4TsPvEI+Iv4A4PSobSzxkg79LTMvsGtDLQv7nN5hMs9T18EL5GnIKoqnSQCU0d -n2CN7S3QiQ+cbORWsSYqCTj1gUbFh6X3duEB/ypLcyWFbqeJmPHikjY8q8pLjZSB -IYiTJYLyvYlKdM5QleC/xuBNnMPCftrwwLHjWE4Dd7C9t7k0R5xyOetuiHLCwOcQ -tuckp7RgFKoviMNv3gdkzwVapOklcsaRkRscv6OMTKJNsdJVIDLrPF1wMABhbEQB -64BL0uL4lkFtpXXbJzQ6mgUNQveJkfUWOoB+cA/6GtI4J0aQfvQgloCYI6jxNn/e -Nvk5OV9KFOhXS2dnDft3wHU46sg5yXOuds1u6UrOoATBNFlkS95m4zIX1Svu091+ -CKTiLK85+ZiFtAlU2bPr3Bk3GhL3Z586ae6a4QUEx6SPQVXc18ezB4qxKqFc+avI -ylccYMRhVP+ruADxtUM5Vy6x3U8BwBK5RLdecRx2FEBDImY1AgMBAAGjggHfMIIB -2zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAWBgNVHSUBAf8EDDAK -BggrBgEFBQcDAzAdBgNVHQ4EFgQUg8fLNKiWjMkAnoSnLs4M1TUQO+YwgewGA1Ud -IwSB5DCB4YAUg8fLNKiWjMkAnoSnLs4M1TUQO+ahgcWkgcIwgb8xCzAJBgNVBAYT -AlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UE -ChMdQ29udGVudCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5j -b250ZW50LXNpZ25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNl -YytkZXZyb290Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIBATBCBglghkgB -hvhCAQQENRYzaHR0cHM6Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5l -dC9jYS9jcmwucGVtME4GCCsGAQUFBwEBBEIwQDA+BggrBgEFBQcwAoYyaHR0cHM6 -Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5ldC9jYS9jYS5wZW0wDQYJ -KoZIhvcNAQEMBQADggIBAAAQ+fotZE79FfZ8Lz7eiTUzlwHXCdSE2XD3nMROu6n6 -uLTBPrf2C+k+U1FjKVvL5/WCUj6hIzP2X6Sb8+o0XHX9mKN0yoMORTEYJOnazYPK -KSUUFnE4vGgQkr6k/31gGRMTICdnf3VOOAlUCQ5bOmGIuWi401E3sbd85U+TJe0A -nHlU+XjtfzlqcBvQivdbA0s+GEG55uRPvn952aTBEMHfn+2JqKeLShl4AtUAfu+h -6md3Z2HrEC7B3GK8ekWPu0G/ZuWTuFvOimZ+5C8IPRJXcIR/siPQl1x6dpTCew6t -lPVcVuvg6SQwzvxetkNrGUe2Wb2s9+PK2PUvfOS8ee25SNmfG3XK9qJpqGUhzSBX -T8QQgyxd0Su5G7Wze7aaHZd/fqIm/G8YFR0HiC2xni/lnDTXFDPCe+HCnSk0bH6U -wpr6I1yK8oZ2IdnNVfuABGMmGOhvSQ8r7//ea9WKhCsGNQawpVWVioY7hpyNAJ0O -Vn4xqG5f6allz8lgpwAQ+AeEEClHca6hh6mj9KhD1Of1CC2Vx52GHNh/jMYEc3/g -zLKniencBqn3Y2XH2daITGJddcleN09+a1NaTkT3hgr7LumxM8EVssPkC+z9j4Vf -Gbste+8S5QCMhh00g5vR9QF8EaFqdxCdSxrsA4GmpCa5UQl8jtCnpp2DLKXuOh72 ------END CERTIFICATE-----
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/test/unit/mock_api/normandy.content-signature.mozilla.org-20210705.dev.chain @@ -0,0 +1,123 @@ +-----BEGIN CERTIFICATE----- +MIIGRTCCBC2gAwIBAgIEAQAABTANBgkqhkiG9w0BAQwFADBrMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHQWxsaXpvbTEXMBUGA1UECxMOQ2xvdWQgU2VydmljZXMxMTAv +BgNVBAMTKERldnppbGxhIFNpZ25pbmcgU2VydmljZXMgSW50ZXJtZWRpYXRlIDEw +HhcNMTYwNzA2MjE1NzE1WhcNMjEwNzA1MjE1NzE1WjCBrzELMAkGA1UEBhMCVVMx +EzARBgNVBAgTCkNhbGlmb3JuaWExHDAaBgNVBAoTE01vemlsbGEgQ29ycG9yYXRp +b24xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMS8wLQYDVQQDEyZub3JtYW5keS5j +b250ZW50LXNpZ25hdHVyZS5tb3ppbGxhLm9yZzEjMCEGCSqGSIb3DQEJARYUc2Vj +dXJpdHlAbW96aWxsYS5vcmcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARUQqIIAiTB +GDVUWw/wk5h1IXpreq+BtE+gQr15O4tusHpCLGjOxwpHiJYnxk45fpE8JGAV19UO +hmqMUEU0k31C1EGTSZW0ducSvHrh3a8wXShZ6dxLWHItbbCGA6A7PumjggJYMIIC +VDAdBgNVHQ4EFgQUVfksSjlZ0i1TBiS1vcoObaMeXn0wge8GA1UdIwSB5zCB5IAU +/YboUIXAovChEpudDBuodHKbjUuhgcWkgcIwgb8xCzAJBgNVBAYTAlVTMQswCQYD +VQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVu +dCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNp +Z25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290 +Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIEAQAABDAMBgNVHRMBAf8EAjAA +MA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzBEBgNVHR8E +PTA7MDmgN6A1hjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3Mu +bmV0L2NhL2NybC5wZW0wQgYJYIZIAYb4QgEEBDUWM2h0dHBzOi8vY29udGVudC1z +aWduYXR1cmUuZGV2Lm1vemF3cy5uZXQvY2EvY3JsLnBlbTBOBggrBgEFBQcBAQRC +MEAwPgYIKwYBBQUHMAKGMmh0dHBzOi8vY29udGVudC1zaWduYXR1cmUuZGV2Lm1v +emF3cy5uZXQvY2EvY2EucGVtMDEGA1UdEQQqMCiCJm5vcm1hbmR5LmNvbnRlbnQt +c2lnbmF0dXJlLm1vemlsbGEub3JnMA0GCSqGSIb3DQEBDAUAA4ICAQCwb+8JTAB7 +ZfQmFqPUIV2cQQv696AaDPQCtA9YS4zmUfcLMvfZVAbK397zFr0RMDdLiTUQDoeq +rBEmPXhJRPiv6JAK4n7Jf6Y6XfXcNxx+q3garR09Vm/0CnEq/iV+ZAtPkoKIO9kr +Nkzecd894yQCF4hIuPQ5qtMySeqJmH3Dp13eq4T0Oew1Bu32rNHuBJh2xYBkWdun +aAw/YX0I5EqZBP/XA6gbiA160tTK+hnpnlMtw/ljkvfhHbWpICD4aSiTL8L3vABQ +j7bqjMKR5xDkuGWshZfcmonpvQhGTye/RZ1vz5IzA3VOJt1mz5bdZlitpaOm/Yv0 +x6aODz8GP/PiRWFQ5CW8Uf/7pGc5rSyvnfZV2ix8EzFlo8cUtuN1fjrPFPOFOLvG +iiB6S9nlXiKBGYIDdd8V8iC5xJpzjiAWJQigwSNzuc2K30+iPo3w0zazkwe5V8jW +gj6gItYxh5xwVQTPHD0EOd9HvV1ou42+rH5Y+ISFUm25zz02UtUHEK0BKtL0lmdt +DwVq5jcHn6bx2/iwUtlKvPXtfM/6JjTJlkLZLtS7U5/pwcS0owo9zAL0qg3bdm16 ++v/olmPqQFLUHmamJTzv3rojj5X/uVdx1HMM3wBjV9tRYoYaZw9RIInRmM8Z1pHv +JJ+CIZgCyd5vgp57BKiodRZcgHoCH+BkOQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHijCCBXKgAwIBAgIEAQAABDANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQK +Ex1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNv +bnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2Vj +K2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjIx +NDkyNloXDTIxMDcwNTIxNDkyNlowazELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0Fs +bGl6b20xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMTEwLwYDVQQDEyhEZXZ6aWxs +YSBTaWduaW5nIFNlcnZpY2VzIEludGVybWVkaWF0ZSAxMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAypIfUURYgWzGw8G/1Pz9zW+Tsjirx2owThiv2gys +wJiWL/9/2gzKOrYDEqlDUudfA/BjVRtT9+NbYgnhaCkNfADOAacWS83aMhedAqhP +bVd5YhGQdpijI7f1AVTSb0ehrU2nhOZHvHX5Tk2fbRx3ryefIazNTLFGpiMBbsNv +tSI/+fjW8s0MhKNqlLnk6a9mZKo0mEy7HjGYV8nzsgI17rKLx/s2HG4TFG0+JQzc +UGlum3Tg58ritDzWdyKIkmKNZ48oLBX99Qc8j8B1UyiLv6TZmjVX0I+Ds7eSWHZk +0axLEpTyf2r7fHvN4iDNCPajw+ZpuuBfbs80Ha8b8MHvnpf9fbwiirodNQOVpY4c +t5E3Us3eYwBKdqDEbECWxCKGAS2/iVVUCNKHsg0sSxgqcwxrxyrddQRUQ0EM38DZ +F/vHt+vTdHt07kezbjJe0Kklel59uSpghA0iL4vxzbTns1fuwYOgVrNGs3acTkiB +GhFOxRXUPGtpdYmv+AaR9WlWJQY1GIEoVrinPVH7bcCwyh1CcUbHL+oAFTcmc6kZ +7azNg21tWILIRL7R0IZYQm0tF5TTwCsjVC7FuHaBtkxtVrrZqeKjvVXQ8TK5VoI0 +BUQ6BKHGeTtm+0HBpheYBDy3wkOsEGbGHLEM6cMeiz6PyCXF8wXli8Qb/TjN3LHZ +e30CAwEAAaOCAd8wggHbMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGG +MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBT9huhQhcCi8KESm50M +G6h0cpuNSzCB7AYDVR0jBIHkMIHhgBSDx8s0qJaMyQCehKcuzgzVNRA75qGBxaSB +wjCBvzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFp +biBWaWV3MSYwJAYDVQQKEx1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEm +MCQGA1UEAxMdZGV2LmNvbnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG +9w0BCQEWLGNsb3Vkc2VjK2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEu +Y29tggEBMEIGCWCGSAGG+EIBBAQ1FjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJl +LmRldi5tb3phd3MubmV0L2NhL2NybC5wZW0wTgYIKwYBBQUHAQEEQjBAMD4GCCsG +AQUFBzAChjJodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3MubmV0 +L2NhL2NhLnBlbTANBgkqhkiG9w0BAQwFAAOCAgEAbum0z0ccqI1Wp49VtsGmUPHA +gjPPy2Xa5NePmqY87WrGdhjN3xbLVb3hx8T2N6pqDjMY2rEynXKEOek3oJkQ3C6J +8AFP6Y93gaAlNz6EA0J0mqdW5TMI8YEYsu2ma+dQQ8wm9f/5b+/Y8qwfhztP06W5 +H6IG04/RvgUwYMnSR4QvT309fu5UmCRUDzsO53ZmQCfmN94u3NxHc4S6n0Q6AKAM +kh5Ld9SQnlqqqDykzn7hYDi8nTLWc7IYqkGfNMilDEKbAl4CjnSfyEvpdFAJ9sPR +UL+kaWFSMvaqIPNpxS5OpoPZjmxEc9HHnCHxtfDHWdXTJILjijPrCdMaxOCHfIqV +5roOJggI4RZ0YM68IL1MfN4IEVOrHhKjDHtd1gcmy2KU4jfj9LQq9YTnyvZ2z1yS +lS310HG3or1K8Nnu5Utfe7T6ppX8bLRMkS1/w0p7DKxHaf4D/GJcCtM9lcSt9JpW +6ACKFikjWR4ZxczYKgApc0wcjd7XBuO5777xtOeyEUDHdDft3jiXA91dYM5UAzc3 +69z/3zmaELzo0gWcrjLXh7fU9AvbU4EUF6rwzxbPGF78jJcGK+oBf8uWUCkBykDt +VsAEZI1u4EDg8e/C1nFqaH9gNMArAgquYIB9rve+hdprIMnva0S147pflWopBWcb +jwzgpfquuYnnxe0CNBA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH3DCCBcSgAwIBAgIBATANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQKEx1D +b250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNvbnRl +bnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2VjK2Rl +dnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjE4MTUy +MloXDTI2MDcwNDE4MTUyMlowgb8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEW +MBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVudCBTaWduYXR1 +cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNpZ25hdHVyZS5y +b290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290Y29udGVudHNp +Z25hdHVyZUBtb3ppbGxhLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAJcPcXhD8MzF6OTn5qZ0L7lX1+PEgLKhI9g1HxxDYDVup4Zm0kZhPGmFSlml +6eVO99OvvHdAlHhQGCIG7h+w1cp66mWjfpcvtQH23uRoKZfiW3jy1jUWrvdXolxR +t1taZosjzo+9OP8TvG6LpJj7AvqUiYD4wYnQJtt0jNRN4d6MUfQwiavSS5uTBuxd +ZJ4TsPvEI+Iv4A4PSobSzxkg79LTMvsGtDLQv7nN5hMs9T18EL5GnIKoqnSQCU0d +n2CN7S3QiQ+cbORWsSYqCTj1gUbFh6X3duEB/ypLcyWFbqeJmPHikjY8q8pLjZSB +IYiTJYLyvYlKdM5QleC/xuBNnMPCftrwwLHjWE4Dd7C9t7k0R5xyOetuiHLCwOcQ +tuckp7RgFKoviMNv3gdkzwVapOklcsaRkRscv6OMTKJNsdJVIDLrPF1wMABhbEQB +64BL0uL4lkFtpXXbJzQ6mgUNQveJkfUWOoB+cA/6GtI4J0aQfvQgloCYI6jxNn/e +Nvk5OV9KFOhXS2dnDft3wHU46sg5yXOuds1u6UrOoATBNFlkS95m4zIX1Svu091+ +CKTiLK85+ZiFtAlU2bPr3Bk3GhL3Z586ae6a4QUEx6SPQVXc18ezB4qxKqFc+avI +ylccYMRhVP+ruADxtUM5Vy6x3U8BwBK5RLdecRx2FEBDImY1AgMBAAGjggHfMIIB +2zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAWBgNVHSUBAf8EDDAK +BggrBgEFBQcDAzAdBgNVHQ4EFgQUg8fLNKiWjMkAnoSnLs4M1TUQO+YwgewGA1Ud +IwSB5DCB4YAUg8fLNKiWjMkAnoSnLs4M1TUQO+ahgcWkgcIwgb8xCzAJBgNVBAYT +AlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UE +ChMdQ29udGVudCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5j +b250ZW50LXNpZ25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNl +YytkZXZyb290Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIBATBCBglghkgB +hvhCAQQENRYzaHR0cHM6Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5l +dC9jYS9jcmwucGVtME4GCCsGAQUFBwEBBEIwQDA+BggrBgEFBQcwAoYyaHR0cHM6 +Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5ldC9jYS9jYS5wZW0wDQYJ +KoZIhvcNAQEMBQADggIBAAAQ+fotZE79FfZ8Lz7eiTUzlwHXCdSE2XD3nMROu6n6 +uLTBPrf2C+k+U1FjKVvL5/WCUj6hIzP2X6Sb8+o0XHX9mKN0yoMORTEYJOnazYPK +KSUUFnE4vGgQkr6k/31gGRMTICdnf3VOOAlUCQ5bOmGIuWi401E3sbd85U+TJe0A +nHlU+XjtfzlqcBvQivdbA0s+GEG55uRPvn952aTBEMHfn+2JqKeLShl4AtUAfu+h +6md3Z2HrEC7B3GK8ekWPu0G/ZuWTuFvOimZ+5C8IPRJXcIR/siPQl1x6dpTCew6t +lPVcVuvg6SQwzvxetkNrGUe2Wb2s9+PK2PUvfOS8ee25SNmfG3XK9qJpqGUhzSBX +T8QQgyxd0Su5G7Wze7aaHZd/fqIm/G8YFR0HiC2xni/lnDTXFDPCe+HCnSk0bH6U +wpr6I1yK8oZ2IdnNVfuABGMmGOhvSQ8r7//ea9WKhCsGNQawpVWVioY7hpyNAJ0O +Vn4xqG5f6allz8lgpwAQ+AeEEClHca6hh6mj9KhD1Of1CC2Vx52GHNh/jMYEc3/g +zLKniencBqn3Y2XH2daITGJddcleN09+a1NaTkT3hgr7LumxM8EVssPkC+z9j4Vf +Gbste+8S5QCMhh00g5vR9QF8EaFqdxCdSxrsA4GmpCa5UQl8jtCnpp2DLKXuOh72 +-----END CERTIFICATE-----
deleted file mode 100644 --- a/browser/extensions/shield-recipe-client/test/unit/test_ActionSandboxManager.js +++ /dev/null @@ -1,169 +0,0 @@ -"use strict"; - -Cu.import("resource://shield-recipe-client/lib/ActionSandboxManager.jsm"); -Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm"); - -async function withManager(script, testFunction) { - const manager = new ActionSandboxManager(script); - manager.addHold("testing"); - await testFunction(manager); - manager.removeHold("testing"); -} - -add_task(async function testMissingCallbackName() { - await withManager("1 + 1", async manager => { - equal( - await manager.runAsyncCallback("missingCallback"), - undefined, - "runAsyncCallback returns undefined when given a missing callback name", - ); - }); -}); - -add_task(async function testCallback() { - const script = ` - registerAsyncCallback("testCallback", async function(normandy) { - return 5; - }); - `; - - await withManager(script, async manager => { - const result = await manager.runAsyncCallback("testCallback"); - equal(result, 5, "runAsyncCallback executes the named callback inside the sandbox"); - }); -}); - -add_task(async function testArguments() { - const script = ` - registerAsyncCallback("testCallback", async function(normandy, a, b) { - return a + b; - }); - `; - - await withManager(script, async manager => { - const result = await manager.runAsyncCallback("testCallback", 4, 6); - equal(result, 10, "runAsyncCallback passes arguments to the callback"); - }); -}); - -add_task(async function testCloning() { - const script = ` - registerAsyncCallback("testCallback", async function(normandy, obj) { - return {foo: "bar", baz: obj.baz}; - }); - `; - - await withManager(script, async manager => { - const result = await manager.runAsyncCallback("testCallback", {baz: "biff"}); - - deepEqual( - result, - {foo: "bar", baz: "biff"}, - ( - "runAsyncCallback clones arguments into the sandbox and return values into the " + - "context it was called from" - ), - ); - }); -}); - -add_task(async function testError() { - const script = ` - registerAsyncCallback("testCallback", async function(normandy) { - throw new Error("WHY") - }); - `; - - await withManager(script, async manager => { - try { - await manager.runAsyncCallback("testCallback"); - ok(false, "runAsnycCallbackFromScript throws errors when raised by the sandbox"); - } catch (err) { - equal(err.message, "WHY", "runAsnycCallbackFromScript clones error messages"); - } - }); -}); - -add_task(async function testDriver() { - const script = ` - registerAsyncCallback("testCallback", async function(normandy) { - return normandy; - }); - `; - - await withManager(script, async manager => { - const sandboxDriver = await manager.runAsyncCallback("testCallback"); - const referenceDriver = new NormandyDriver(manager); - equal( - sandboxDriver.constructor.name, - "NormandyDriver", - "runAsyncCallback passes a driver as the first parameter", - ); - for (const prop in referenceDriver) { - ok(prop in sandboxDriver, "runAsyncCallback passes a driver as the first parameter"); - } - }); -}); - -add_task(async function testGlobalObject() { - // Test that window is an alias for the global object, and that it - // has some expected functions available on it. - const script = ` - window.setOnWindow = "set"; - this.setOnGlobal = "set"; - - registerAsyncCallback("testCallback", async function(normandy) { - return { - setOnWindow: setOnWindow, - setOnGlobal: window.setOnGlobal, - setTimeoutExists: setTimeout !== undefined, - clearTimeoutExists: clearTimeout !== undefined, - }; - }); - `; - - await withManager(script, async manager => { - const result = await manager.runAsyncCallback("testCallback"); - Assert.deepEqual(result, { - setOnWindow: "set", - setOnGlobal: "set", - setTimeoutExists: true, - clearTimeoutExists: true, - }, "sandbox.window is the global object and has expected functions."); - }); -}); - -add_task(async function testRegisterActionShim() { - const recipe = { - foo: "bar", - }; - const script = ` - class TestAction { - constructor(driver, recipe) { - this.driver = driver; - this.recipe = recipe; - } - - execute() { - return new Promise(resolve => { - resolve({ - foo: this.recipe.foo, - isDriver: "log" in this.driver, - }); - }); - } - } - - registerAction('test-action', TestAction); - `; - - await withManager(script, async manager => { - const result = await manager.runAsyncCallback("action", recipe); - equal(result.foo, "bar", "registerAction registers an async callback for actions"); - equal( - result.isDriver, - true, - "registerAction passes the driver to the action class constructor", - ); - }); -});
--- a/browser/extensions/shield-recipe-client/test/unit/test_NormandyApi.js +++ b/browser/extensions/shield-recipe-client/test/unit/test_NormandyApi.js @@ -1,26 +1,43 @@ +/* globals sinon */ "use strict"; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/CanonicalJSON.jsm", this); Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm", this); load("utils.js"); /* globals withMockPreferences */ +class MockResponse { + constructor(content) { + this.content = content; + } + + async text() { + return this.content; + } + + async json() { + return JSON.parse(this.content); + } +} + function withServer(server, task) { return withMockPreferences(async function inner(preferences) { const serverUrl = `http://localhost:${server.identity.primaryPort}`; preferences.set("extensions.shield-recipe-client.api_url", `${serverUrl}/api/v1`); preferences.set( "security.content.signature.root_hash", // Hash of the key that signs the normandy dev certificates "4C:35:B1:C3:E3:12:D9:55:E7:78:ED:D0:A7:E7:8A:38:83:04:EF:01:BF:FA:03:29:B2:46:9F:3C:C5:EC:36:04" ); + NormandyApi.clearIndexCache(); try { await task(serverUrl, preferences); } finally { await new Promise(resolve => server.stop(resolve)); } }); } @@ -32,19 +49,19 @@ function makeScriptServer(scriptPath) { server.start(-1); return server; } function withScriptServer(scriptPath, task) { return withServer(makeScriptServer(scriptPath), task); } -function makeMockApiServer() { +function makeMockApiServer(directory) { const server = new HttpServer(); - server.registerDirectory("/", do_get_file("mock_api")); + server.registerDirectory("/", directory); server.setIndexHandler(async function(request, response) { response.processAsync(); const dir = request.getProperty("directory"); const index = dir.clone(); index.append("index.json"); if (!index.exists()) { @@ -65,17 +82,17 @@ function makeMockApiServer() { } }); server.start(-1); return server; } function withMockApiServer(task) { - return withServer(makeMockApiServer(), task); + return withServer(makeMockApiServer(do_get_file("mock_api")), task); } add_task(withMockApiServer(async function test_get(serverUrl) { // Test that NormandyApi can fetch from the test server. const response = await NormandyApi.get(`${serverUrl}/api/v1/`); const data = await response.json(); equal(data["recipe-list"], "/api/v1/recipe/", "Expected data in response"); })); @@ -83,21 +100,17 @@ add_task(withMockApiServer(async functio add_task(withMockApiServer(async function test_getApiUrl(serverUrl) { const apiBase = `${serverUrl}/api/v1`; // Test that NormandyApi can use the self-describing API's index const recipeListUrl = await NormandyApi.getApiUrl("action-list"); equal(recipeListUrl, `${apiBase}/action/`, "Can retrieve action-list URL from API"); })); add_task(withMockApiServer(async function test_getApiUrlSlashes(serverUrl, preferences) { - const fakeResponse = { - async json() { - return {"test-endpoint": `${serverUrl}/test/`}; - }, - }; + const fakeResponse = new MockResponse(JSON.stringify({"test-endpoint": `${serverUrl}/test/`})); const mockGet = sinon.stub(NormandyApi, "get", async () => fakeResponse); // without slash { NormandyApi.clearIndexCache(); preferences.set("extensions.shield-recipe-client.api_url", `${serverUrl}/api/v1`); const endpoint = await NormandyApi.getApiUrl("test-endpoint"); equal(endpoint, `${serverUrl}/test/`); @@ -120,30 +133,103 @@ add_task(withMockApiServer(async functio })); add_task(withMockApiServer(async function test_fetchRecipes() { const recipes = await NormandyApi.fetchRecipes(); equal(recipes.length, 1); equal(recipes[0].name, "system-addon-test"); })); +add_task(async function test_fetchSignedObjects_canonical_mismatch() { + const getApiUrl = sinon.stub(NormandyApi, "getApiUrl"); + + // The object is non-canonical (it has whitespace, properties are out of order) + const response = new MockResponse(`[ + { + "object": {"b": 1, "a": 2}, + "signature": {"signature": "", "x5u": ""} + } + ]`); + const get = sinon.stub(NormandyApi, "get").resolves(response); + + try { + await NormandyApi.fetchSignedObjects("object"); + ok(false, "fetchSignedObjects did not throw for canonical JSON mismatch"); + } catch (err) { + ok(err instanceof NormandyApi.InvalidSignatureError, "Error is an InvalidSignatureError"); + ok(/Canonical/.test(err), "Error is due to canonical JSON mismatch"); + } + + getApiUrl.restore(); + get.restore(); +}); + +// Test validation errors due to validation throwing an exception (e.g. when +// parameters passed to validation are malformed). +add_task(async function test_fetchSignedObjects_validation_error() { + const getApiUrl = sinon.stub(NormandyApi, "getApiUrl").resolves("http://localhost/object/"); + + // Mock two URLs: object and the x5u + const get = sinon.stub(NormandyApi, "get").callsFake(async url => { + if (url.endsWith("object/")) { + return new MockResponse(CanonicalJSON.stringify([ + { + object: {a: 1, b: 2}, + signature: {signature: "invalidsignature", x5u: "http://localhost/x5u/"}, + }, + ])); + } else if (url.endsWith("x5u/")) { + return new MockResponse("certchain"); + } + + return null; + }); + + // Validation should fail due to a malformed x5u and signature. + try { + await NormandyApi.fetchSignedObjects("object"); + ok(false, "fetchSignedObjects did not throw for a validation error"); + } catch (err) { + ok(err instanceof NormandyApi.InvalidSignatureError, "Error is an InvalidSignatureError"); + ok(/signature/.test(err), "Error is due to a validation error"); + } + + getApiUrl.restore(); + get.restore(); +}); + +// Test validation errors due to validation returning false (e.g. when parameters +// passed to validation are correctly formed, but not valid for the data). +const invalidSignatureServer = makeMockApiServer(do_get_file("invalid_recipe_signature_api")); +add_task(withServer(invalidSignatureServer, async function test_fetchSignedObjects_invalid_signature() { + try { + await NormandyApi.fetchSignedObjects("recipe"); + ok(false, "fetchSignedObjects did not throw for an invalid signature"); + } catch (err) { + ok(err instanceof NormandyApi.InvalidSignatureError, "Error is an InvalidSignatureError"); + ok(/signature/.test(err), "Error is due to an invalid signature"); + } +})); + add_task(withMockApiServer(async function test_classifyClient() { const classification = await NormandyApi.classifyClient(); Assert.deepEqual(classification, { country: "US", request_time: new Date("2017-02-22T17:43:24.657841Z"), }); })); add_task(withMockApiServer(async function test_fetchActions() { const actions = await NormandyApi.fetchActions(); - equal(actions.length, 2); + equal(actions.length, 4); const actionNames = actions.map(a => a.name); ok(actionNames.includes("console-log")); + ok(actionNames.includes("opt-out-study")); ok(actionNames.includes("show-heartbeat")); + ok(actionNames.includes("preference-experiment")); })); add_task(withScriptServer("query_server.sjs", async function test_getTestServer(serverUrl) { // Test that NormandyApi can fetch from the test server. const response = await NormandyApi.get(serverUrl); const data = await response.json(); Assert.deepEqual(data, {queryString: {}, body: {}}, "NormandyApi returned incorrect server data."); })); @@ -163,25 +249,26 @@ add_task(withScriptServer("query_server. const response = await NormandyApi.post(serverUrl, {foo: "bar", baz: "biff"}); const data = await response.json(); Assert.deepEqual( data, {queryString: {}, body: {foo: "bar", baz: "biff"}}, "NormandyApi sent an incorrect query string." ); })); -add_task(withScriptServer("echo_server.sjs", async function test_fetchImplementation(serverUrl) { - const action = { - implementation_url: `${serverUrl}?status=200&body=testcontent`, - }; - equal( - await NormandyApi.fetchImplementation(action), - "testcontent", - "fetchImplementation fetches the content at the correct URL", - ); +add_task(withMockApiServer(async function test_fetchImplementation_itWorksWithRealData() { + const [action] = await NormandyApi.fetchActions(); + const implementation = await NormandyApi.fetchImplementation(action); + + const decoder = new TextDecoder(); + const relativePath = `mock_api${action.implementation_url}`; + const file = do_get_file(relativePath); + const expected = decoder.decode(await OS.File.read(file.path)); + + equal(implementation, expected); })); add_task(withScriptServer( "echo_server.sjs", async function test_fetchImplementationFail(serverUrl) { const action = { implementation_url: `${serverUrl}?status=500&body=servererror`, };
--- a/browser/extensions/shield-recipe-client/test/unit/test_SandboxManager.js +++ b/browser/extensions/shield-recipe-client/test/unit/test_SandboxManager.js @@ -1,15 +1,15 @@ "use strict"; Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm"); // wrapAsync should wrap privileged Promises with Promises that are usable by // the sandbox. -add_task(async function() { +add_task(function* () { const manager = new SandboxManager(); manager.addHold("testing"); manager.cloneIntoGlobal("driver", { async privileged() { return "privileged"; }, wrapped: manager.wrapAsync(async function() { @@ -20,17 +20,17 @@ add_task(async function() { return this.aValue; }), }, {cloneFunctions: true}); // Assertion helpers manager.addGlobal("ok", ok); manager.addGlobal("equal", equal); - const sandboxResult = await new Promise(resolve => { + const sandboxResult = yield new Promise(resolve => { manager.addGlobal("resolve", result => resolve(result)); manager.evalInSandbox(` // Unwrapped privileged promises are not accessible in the sandbox try { const privilegedResult = driver.privileged().then(() => false); ok(false, "The sandbox could not use a privileged Promise"); } catch (err) { } @@ -40,31 +40,31 @@ add_task(async function() { // Resolve the Promise around the sandbox with the wrapped result to test // that the Promise in the sandbox works. wrappedResult.then(resolve); `); }); equal(sandboxResult, "wrapped", "wrapAsync methods return Promises that work in the sandbox"); - await manager.evalInSandbox(` + yield manager.evalInSandbox(` (async function sandboxTest() { equal( await driver.wrappedThis(), "aValue", "wrapAsync preserves the behavior of the this keyword", ); })(); `); manager.removeHold("testing"); }); // wrapAsync cloning options -add_task(async function() { +add_task(function* () { const manager = new SandboxManager(); manager.addHold("testing"); // clonedArgument stores the argument passed to cloned(), which we use to test // that arguments from within the sandbox are cloned outside. let clonedArgument = null; manager.cloneIntoGlobal("driver", { uncloned: manager.wrapAsync(async function() { @@ -75,17 +75,17 @@ add_task(async function() { return {value: "cloned"}; }, {cloneInto: true, cloneArguments: true}), }, {cloneFunctions: true}); // Assertion helpers manager.addGlobal("ok", ok); manager.addGlobal("deepEqual", deepEqual); - await new Promise(resolve => { + yield new Promise(resolve => { manager.addGlobal("resolve", resolve); manager.evalInSandbox(` (async function() { // The uncloned return value should be privileged and inaccesible. const uncloned = await driver.uncloned(); ok(!("value" in uncloned), "The sandbox could not use an uncloned return value"); // The cloned return value should be usable.
--- a/browser/extensions/shield-recipe-client/test/unit/xpcshell.ini +++ b/browser/extensions/shield-recipe-client/test/unit/xpcshell.ini @@ -1,11 +1,13 @@ [DEFAULT] head = head_xpc.js support-files = mock_api/** + invalid_recipe_signature_api/** query_server.sjs echo_server.sjs utils.js [test_NormandyApi.js] [test_Sampling.js] [test_SandboxManager.js] +[test_Utils.js]
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/vendor/LICENSE_THIRDPARTY @@ -0,0 +1,262 @@ +fbjs@0.8.14 BSD-3-Clause +BSD License + +For fbjs software + +Copyright (c) 2013-present, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +react-dom@15.6.1 BSD-3-Clause +BSD License + +For React software + +Copyright (c) 2013-present, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +object-assign@4.1.1 MIT +The MIT License (MIT) + +Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +react@15.6.1 BSD-3-Clause +BSD License + +For React software + +Copyright (c) 2013-present, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +prop-types@15.5.10 BSD-3-Clause +BSD License + +For React software + +Copyright (c) 2013-present, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +create-react-class@15.6.0 BSD-3-Clause +BSD License + +For React software + +Copyright (c) 2013-present, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +mozjexl@1.1.5 MIT +Copyright for portions of mozJexl are held by TechnologyAdvice, 2015 as part of Jexl. +All other copyright for mozJexl are held by the Mozilla Foundation, 2017. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +process@0.11.10 MIT +(The MIT License) + +Copyright (c) 2013 Roman Shtylman <shtylman@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +classnames@2.2.5 MIT +The MIT License (MIT) + +Copyright (c) 2016 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/vendor/PropTypes.js @@ -0,0 +1,1 @@ +/* eslint-disable */this.PropTypes=function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={i:d,l:!1,exports:{}};return a[d].call(e.exports,e,e.exports,b),e.l=!0,e.exports}var c={};return b.m=a,b.c=c,b.d=function(a,c,d){b.o(a,c)||Object.defineProperty(a,c,{configurable:!1,enumerable:!0,get:d})},b.n=function(a){var c=a&&a.__esModule?function(){return a['default']}:function(){return a};return b.d(c,'a',c),c},b.o=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)},b.p='',b(b.s=100)}({0:function(a){'use strict';var g=function(){};!1,a.exports=function(h,i,j,a,b,c,d,e){if(g(i),!h){var f;if(void 0===i)f=new Error('Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.');else{var k=[j,a,b,c,d,e],l=0;f=new Error(i.replace(/%s/g,function(){return k[l++]})),f.name='Invariant Violation'}throw f.framesToPop=1,f}}},100:function(a,b,c){a.exports=c(101)()},101:function(a,b,c){'use strict';var d=c(5),e=c(0),f=c(19);a.exports=function(){function a(a,b,c,d,g,h){h===f||e(!1,'Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types')}function b(){return a}a.isRequired=a;var c={array:a,bool:a,func:a,number:a,object:a,string:a,symbol:a,any:a,arrayOf:b,element:a,instanceOf:b,node:a,objectOf:b,oneOf:b,oneOfType:b,shape:b};return c.checkPropTypes=d,c.PropTypes=c,c}},19:function(a){'use strict';a.exports='SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'},5:function(a){'use strict';function b(a){return function(){return a}}var c=function(){};c.thatReturns=b,c.thatReturnsFalse=b(!1),c.thatReturnsTrue=b(!0),c.thatReturnsNull=b(null),c.thatReturnsThis=function(){return this},c.thatReturnsArgument=function(a){return a},a.exports=c}});this.EXPORTED_SYMBOLS = ["PropTypes"]; \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/vendor/React.js @@ -0,0 +1,5 @@ +/* eslint-disable */this.React=function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e['default']}:function(){return e};return t.d(n,'a',n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p='',t(t.s=102)}([function(e){'use strict';var t=function(){};!1,e.exports=function(n,o,r,a,i,p,s,e){if(t(o),!n){var d;if(void 0===o)d=new Error('Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.');else{var l=[r,a,i,p,s,e],u=0;d=new Error(o.replace(/%s/g,function(){return l[u++]})),d.name='Invariant Violation'}throw d.framesToPop=1,d}}},function(e,t,n){'use strict';var o=n(5);e.exports=o},,function(e){'use strict';/* +object-assign +(c) Sindre Sorhus +@license MIT +*/function t(e){if(null===e||e===void 0)throw new TypeError('Object.assign cannot be called with null or undefined');return Object(e)}var n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var e=new String('abc');if(e[5]='de','5'===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;10>n;n++)t['_'+String.fromCharCode(n)]=n;var o=Object.getOwnPropertyNames(t).map(function(e){return t[e]});if('0123456789'!==o.join(''))return!1;var r={};return['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t'].forEach(function(e){r[e]=e}),'abcdefghijklmnopqrst'===Object.keys(Object.assign({},r)).join('')}catch(e){return!1}}()?Object.assign:function(e){for(var a,p,d=t(e),l=1;l<arguments.length;l++){for(var s in a=Object(arguments[l]),a)o.call(a,s)&&(d[s]=a[s]);if(n){p=n(a);for(var u=0;u<p.length;u++)r.call(a,p[u])&&(d[p[u]]=a[p[u]])}}return d}},,function(e){'use strict';function t(e){return function(){return e}}var n=function(){};n.thatReturns=t,n.thatReturnsFalse=t(!1),n.thatReturnsTrue=t(!0),n.thatReturnsNull=t(null),n.thatReturnsThis=function(){return this},n.thatReturnsArgument=function(e){return e},e.exports=n},,function(e,t,n){'use strict';function o(e){return e.ref!==void 0}function r(e){return e.key!==void 0}var a,i,p=n(3),s=n(8),d=n(1),l=n(22),u=Object.prototype.hasOwnProperty,c=n(23),m={key:!0,ref:!0,__self:!0,__source:!0},f=function(e,t,n,o,r,a,i){return!1,{$$typeof:c,type:e,key:t,ref:n,props:i,_owner:a}};f.createElement=function(e,t,n){var a,p={},d=null,l=null,c=null,y=null;if(null!=t)for(a in o(t)&&(l=t.ref),r(t)&&(d=''+t.key),c=void 0===t.__self?null:t.__self,y=void 0===t.__source?null:t.__source,t)u.call(t,a)&&!m.hasOwnProperty(a)&&(p[a]=t[a]);var h=arguments.length-2;if(1==h)p.children=n;else if(1<h){for(var g=Array(h),b=0;b<h;b++)g[b]=arguments[b+2];!1,p.children=g}if(e&&e.defaultProps){var i=e.defaultProps;for(a in i)void 0===p[a]&&(p[a]=i[a])}return f(e,d,l,c,y,s.current,p)},f.createFactory=function(e){var t=f.createElement.bind(null,e);return t.type=e,t},f.cloneAndReplaceKey=function(e,t){var n=f(e.type,t,e.ref,e._self,e._source,e._owner,e.props);return n},f.cloneElement=function(e,t,n){var a,d=p({},e.props),l=e.key,c=e.ref,y=e._self,h=e._source,g=e._owner;if(null!=t){o(t)&&(c=t.ref,g=s.current),r(t)&&(l=''+t.key);var b;for(a in e.type&&e.type.defaultProps&&(b=e.type.defaultProps),t)u.call(t,a)&&!m.hasOwnProperty(a)&&(d[a]=void 0===t[a]&&void 0!==b?b[a]:t[a])}var E=arguments.length-2;if(1==E)d.children=n;else if(1<E){for(var x=Array(E),P=0;P<E;P++)x[P]=arguments[P+2];d.children=x}return f(e.type,l,c,y,h,g,d)},f.isValidElement=function(e){return'object'==typeof e&&null!==e&&e.$$typeof===c},e.exports=f},function(e){'use strict';e.exports={current:null}},,function(e){'use strict';e.exports=function(e){for(var t=arguments.length-1,n='Minified React error #'+e+'; visit http://facebook.github.io/react/docs/error-decoder.html?invariant='+e,o=0;o<t;o++)n+='&args[]='+encodeURIComponent(arguments[o+1]);n+=' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.';var r=new Error(n);throw r.name='Invariant Violation',r.framesToPop=1,r}},,,function(e,t,n){'use strict';var o=n(3),r=n(20),a=n(35),i=n(40),p=n(7),s=n(41),d=n(44),l=n(45),u=n(47),c=p.createElement,m=p.createFactory,f=p.cloneElement;var y={Children:{map:a.map,forEach:a.forEach,count:a.count,toArray:a.toArray,only:u},Component:r.Component,PureComponent:r.PureComponent,createElement:c,cloneElement:f,isValidElement:p.isValidElement,PropTypes:s,createClass:l,createFactory:m,createMixin:function(e){return e},DOM:i,version:d,__spread:o};e.exports=y},function(e){'use strict';!1,e.exports={}},,,,,function(e){'use strict';e.exports='SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'},function(e,t,n){'use strict';function o(e,t,n){this.props=e,this.context=t,this.refs=l,this.updater=n||s}function r(e,t,n){this.props=e,this.context=t,this.refs=l,this.updater=n||s}function a(){}var i=n(10),p=n(3),s=n(21),d=n(22),l=n(14),u=n(0),c=n(34);o.prototype.isReactComponent={},o.prototype.setState=function(e,t){'object'==typeof e||'function'==typeof e||null==e?void 0:i('85'),this.updater.enqueueSetState(this,e),t&&this.updater.enqueueCallback(this,t,'setState')},o.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this),e&&this.updater.enqueueCallback(this,e,'forceUpdate')};a.prototype=o.prototype,r.prototype=new a,r.prototype.constructor=r,p(r.prototype,o.prototype),r.prototype.isPureReactComponent=!0,e.exports={Component:o,PureComponent:r}},function(e,t,n){'use strict';function o(){}var r=n(1);e.exports={isMounted:function(){return!1},enqueueCallback:function(){},enqueueForceUpdate:function(e){o(e,'forceUpdate')},enqueueReplaceState:function(e){o(e,'replaceState')},enqueueSetState:function(e){o(e,'setState')}}},function(e){'use strict';e.exports=!1},function(e){'use strict';var t='function'==typeof Symbol&&Symbol['for']&&Symbol['for']('react.element')||60103;e.exports=t},,,,,function(e,t,n){'use strict';var o=n(42);e.exports=function(e){return o(e,!1)}},,,,,,function(e){'use strict';e.exports=function(){}},function(e,t,n){'use strict';function o(e){return(''+e).replace(h,'$&/')}function r(e,t){this.func=e,this.context=t,this.count=0}function a(e,t){var n=e.func,o=e.context;n.call(o,t,e.count++)}function i(e,t,n,o){this.result=e,this.keyPrefix=t,this.func=n,this.context=o,this.count=0}function p(e,t,n){var r=e.result,a=e.keyPrefix,i=e.func,p=e.context,d=i.call(p,t,e.count++);Array.isArray(d)?s(d,r,n,c.thatReturnsArgument):null!=d&&(u.isValidElement(d)&&(d=u.cloneAndReplaceKey(d,a+(d.key&&(!t||t.key!==d.key)?o(d.key)+'/':'')+n)),r.push(d))}function s(e,t,n,r,a){var s='';null!=n&&(s=o(n)+'/');var d=i.getPooled(t,s,r,a);m(e,p,d),i.release(d)}function d(){return null}var l=n(36),u=n(7),c=n(5),m=n(37),f=l.twoArgumentPooler,y=l.fourArgumentPooler,h=/\/+/g;r.prototype.destructor=function(){this.func=null,this.context=null,this.count=0},l.addPoolingTo(r,f),i.prototype.destructor=function(){this.result=null,this.keyPrefix=null,this.func=null,this.context=null,this.count=0},l.addPoolingTo(i,y);e.exports={forEach:function(e,t,n){if(null==e)return e;var o=r.getPooled(t,n);m(e,a,o),r.release(o)},map:function(e,t,n){if(null==e)return e;var o=[];return s(e,o,null,t,n),o},mapIntoWithKeyPrefixInternal:s,count:function(e){return m(e,d,null)},toArray:function(e){var t=[];return s(e,t,null,c.thatReturnsArgument),t}}},function(e,t,n){'use strict';var o=n(10),r=n(0),a=function(e){var t=this;if(t.instancePool.length){var n=t.instancePool.pop();return t.call(n,e),n}return new t(e)},i=function(e){var t=this;e instanceof t?void 0:o('25'),e.destructor(),t.instancePool.length<t.poolSize&&t.instancePool.push(e)};e.exports={addPoolingTo:function(e,t){var n=e;return n.instancePool=[],n.getPooled=t||a,n.poolSize||(n.poolSize=10),n.release=i,n},oneArgumentPooler:a,twoArgumentPooler:function(e,t){var n=this;if(n.instancePool.length){var o=n.instancePool.pop();return n.call(o,e,t),o}return new n(e,t)},threeArgumentPooler:function(e,t,n){var o=this;if(o.instancePool.length){var r=o.instancePool.pop();return o.call(r,e,t,n),r}return new o(e,t,n)},fourArgumentPooler:function(e,t,n,o){var r=this;if(r.instancePool.length){var a=r.instancePool.pop();return r.call(a,e,t,n,o),a}return new r(e,t,n,o)}}},function(e,t,n){'use strict';function o(e,t){return e&&'object'==typeof e&&null!=e.key?l.escape(e.key):t.toString(36)}function r(e,t,n,d){var u=typeof e;if(('undefined'==u||'boolean'==u)&&(e=null),null===e||'string'==u||'number'==u||'object'==u&&e.$$typeof===p)return n(d,e,''===t?c+o(e,0):t),1;var f,y,h=0,g=''===t?c:t+m;if(Array.isArray(e))for(var b=0;b<e.length;b++)f=e[b],y=g+o(f,b),h+=r(f,y,n,d);else{var i=s(e);if(i){var E,x=i.call(e);if(i!==e.entries)for(var P=0;!(E=x.next()).done;)f=E.value,y=g+o(f,P++),h+=r(f,y,n,d);else for(var _;!(E=x.next()).done;)_=E.value,_&&(f=_[1],y=g+l.escape(_[0])+m+o(f,0),h+=r(f,y,n,d))}else if('object'==u){var N='',I=e+'';a('31','[object Object]'===I?'object with keys {'+Object.keys(e).join(', ')+'}':I,N)}}return h}var a=n(10),i=n(8),p=n(23),s=n(38),d=n(0),l=n(39),u=n(1),c='.',m=':';e.exports=function(e,t,n){return null==e?0:r(e,'',t,n)}},function(e){'use strict';var t='function'==typeof Symbol&&Symbol.iterator;e.exports=function(e){var n=e&&(t&&e[t]||e['@@iterator']);if('function'==typeof n)return n}},function(e){'use strict';e.exports={escape:function(e){var t=/[=:]/g,n={"=":'=0',":":'=2'},o=(''+e).replace(t,function(e){return n[e]});return'$'+o},unescape:function(e){var t=/(=0|=2)/g,n={"=0":'=',"=2":':'},o='.'===e[0]&&'$'===e[1]?e.substring(2):e.substring(1);return(''+o).replace(t,function(e){return n[e]})}}},function(e,t,n){'use strict';var o=n(7),r=o.createFactory;var a={a:r('a'),abbr:r('abbr'),address:r('address'),area:r('area'),article:r('article'),aside:r('aside'),audio:r('audio'),b:r('b'),base:r('base'),bdi:r('bdi'),bdo:r('bdo'),big:r('big'),blockquote:r('blockquote'),body:r('body'),br:r('br'),button:r('button'),canvas:r('canvas'),caption:r('caption'),cite:r('cite'),code:r('code'),col:r('col'),colgroup:r('colgroup'),data:r('data'),datalist:r('datalist'),dd:r('dd'),del:r('del'),details:r('details'),dfn:r('dfn'),dialog:r('dialog'),div:r('div'),dl:r('dl'),dt:r('dt'),em:r('em'),embed:r('embed'),fieldset:r('fieldset'),figcaption:r('figcaption'),figure:r('figure'),footer:r('footer'),form:r('form'),h1:r('h1'),h2:r('h2'),h3:r('h3'),h4:r('h4'),h5:r('h5'),h6:r('h6'),head:r('head'),header:r('header'),hgroup:r('hgroup'),hr:r('hr'),html:r('html'),i:r('i'),iframe:r('iframe'),img:r('img'),input:r('input'),ins:r('ins'),kbd:r('kbd'),keygen:r('keygen'),label:r('label'),legend:r('legend'),li:r('li'),link:r('link'),main:r('main'),map:r('map'),mark:r('mark'),menu:r('menu'),menuitem:r('menuitem'),meta:r('meta'),meter:r('meter'),nav:r('nav'),noscript:r('noscript'),object:r('object'),ol:r('ol'),optgroup:r('optgroup'),option:r('option'),output:r('output'),p:r('p'),param:r('param'),picture:r('picture'),pre:r('pre'),progress:r('progress'),q:r('q'),rp:r('rp'),rt:r('rt'),ruby:r('ruby'),s:r('s'),samp:r('samp'),script:r('script'),section:r('section'),select:r('select'),small:r('small'),source:r('source'),span:r('span'),strong:r('strong'),style:r('style'),sub:r('sub'),summary:r('summary'),sup:r('sup'),table:r('table'),tbody:r('tbody'),td:r('td'),textarea:r('textarea'),tfoot:r('tfoot'),th:r('th'),thead:r('thead'),time:r('time'),title:r('title'),tr:r('tr'),track:r('track'),u:r('u'),ul:r('ul'),var:r('var'),video:r('video'),wbr:r('wbr'),circle:r('circle'),clipPath:r('clipPath'),defs:r('defs'),ellipse:r('ellipse'),g:r('g'),image:r('image'),line:r('line'),linearGradient:r('linearGradient'),mask:r('mask'),path:r('path'),pattern:r('pattern'),polygon:r('polygon'),polyline:r('polyline'),radialGradient:r('radialGradient'),rect:r('rect'),stop:r('stop'),svg:r('svg'),text:r('text'),tspan:r('tspan')};e.exports=a},function(e,t,n){'use strict';var o=n(7),r=o.isValidElement,a=n(28);e.exports=a(r)},function(e,t,n){'use strict';var o=n(5),r=n(0),a=n(1),p=n(19),i=n(43);e.exports=function(e,t){function n(e){var t=e&&(b&&e[b]||e[E]);if('function'==typeof t)return t}function s(e,t){return e===t?0!==e||1/e==1/t:e!==e&&t!==t}function d(e){this.message=e,this.stack=''}function l(e){function n(n,o,a,i,s,l,u){if(i=i||x,l=l||a,u!==p)if(t)r(!1,'Calling PropTypes validators directly is not supported by the `prop-types` package. Use `PropTypes.checkPropTypes()` to call them. Read more at http://fb.me/use-check-prop-types');else;return null==o[a]?n?null===o[a]?new d('The '+s+' `'+l+'` is marked as required '+('in `'+i+'`, but its value is `null`.')):new d('The '+s+' `'+l+'` is marked as required in '+('`'+i+'`, but its value is `undefined`.')):null:e(o,a,i,s,l)}var o=n.bind(null,!1);return o.isRequired=n.bind(null,!0),o}function u(e){return l(function(t,n,o,r,a){var i=t[n],p=f(i);if(p!==e){var s=y(i);return new d('Invalid '+r+' `'+a+'` of type '+('`'+s+'` supplied to `'+o+'`, expected ')+('`'+e+'`.'))}return null})}function c(t){switch(typeof t){case'number':case'string':case'undefined':return!0;case'boolean':return!t;case'object':if(Array.isArray(t))return t.every(c);if(null===t||e(t))return!0;var o=n(t);if(o){var r,a=o.call(t);if(o!==t.entries){for(;!(r=a.next()).done;)if(!c(r.value))return!1;}else for(;!(r=a.next()).done;){var i=r.value;if(i&&!c(i[1]))return!1}}else return!1;return!0;default:return!1;}}function m(e,t){return'symbol'===e||'Symbol'===t['@@toStringTag']||'function'==typeof Symbol&&t instanceof Symbol}function f(e){var t=typeof e;return Array.isArray(e)?'array':e instanceof RegExp?'object':m(t,e)?'symbol':t}function y(e){if('undefined'==typeof e||null===e)return''+e;var t=f(e);if('object'===t){if(e instanceof Date)return'date';if(e instanceof RegExp)return'regexp'}return t}function h(e){var t=y(e);return'array'===t||'object'===t?'an '+t:'boolean'===t||'date'===t||'regexp'===t?'a '+t:t}function g(e){return e.constructor&&e.constructor.name?e.constructor.name:x}var b='function'==typeof Symbol&&Symbol.iterator,E='@@iterator',x='<<anonymous>>',P={array:u('array'),bool:u('boolean'),func:u('function'),number:u('number'),object:u('object'),string:u('string'),symbol:u('symbol'),any:function(){return l(o.thatReturnsNull)}(),arrayOf:function(e){return l(function(t,n,o,r,a){if('function'!=typeof e)return new d('Property `'+a+'` of component `'+o+'` has invalid PropType notation inside arrayOf.');var s=t[n];if(!Array.isArray(s)){var l=f(s);return new d('Invalid '+r+' `'+a+'` of type '+('`'+l+'` supplied to `'+o+'`, expected an array.'))}for(var u,c=0;c<s.length;c++)if(u=e(s,c,o,r,a+'['+c+']',p),u instanceof Error)return u;return null})},element:function(){return l(function(t,n,o,r,a){var i=t[n];if(!e(i)){var p=f(i);return new d('Invalid '+r+' `'+a+'` of type '+('`'+p+'` supplied to `'+o+'`, expected a single ReactElement.'))}return null})}(),instanceOf:function(e){return l(function(t,n,o,r,a){if(!(t[n]instanceof e)){var i=e.name||x,p=g(t[n]);return new d('Invalid '+r+' `'+a+'` of type '+('`'+p+'` supplied to `'+o+'`, expected ')+('instance of `'+i+'`.'))}return null})},node:function(){return l(function(e,t,n,o,r){return c(e[t])?null:new d('Invalid '+o+' `'+r+'` supplied to '+('`'+n+'`, expected a ReactNode.'))})}(),objectOf:function(e){return l(function(t,n,o,r,a){if('function'!=typeof e)return new d('Property `'+a+'` of component `'+o+'` has invalid PropType notation inside objectOf.');var i=t[n],s=f(i);if('object'!==s)return new d('Invalid '+r+' `'+a+'` of type '+('`'+s+'` supplied to `'+o+'`, expected an object.'));for(var l in i)if(i.hasOwnProperty(l)){var u=e(i,l,o,r,a+'.'+l,p);if(u instanceof Error)return u}return null})},oneOf:function(e){return Array.isArray(e)?l(function(t,n,o,r,a){for(var p=t[n],l=0;l<e.length;l++)if(s(p,e[l]))return null;var i=JSON.stringify(e);return new d('Invalid '+r+' `'+a+'` of value `'+p+'` '+('supplied to `'+o+'`, expected one of '+i+'.'))}):(void 0,o.thatReturnsNull)},oneOfType:function(e){if(!Array.isArray(e))return void 0,o.thatReturnsNull;for(var t,n=0;n<e.length;n++)if(t=e[n],'function'!=typeof t)return a(!1,'Invalid argument supplid to oneOfType. Expected an array of check functions, but received %s at index %s.',h(t),n),o.thatReturnsNull;return l(function(t,n,o,r,a){for(var s,l=0;l<e.length;l++)if(s=e[l],null==s(t,n,o,r,a,p))return null;return new d('Invalid '+r+' `'+a+'` supplied to '+('`'+o+'`.'))})},shape:function(e){return l(function(t,n,o,r,a){var i=t[n],s=f(i);if('object'!==s)return new d('Invalid '+r+' `'+a+'` of type `'+s+'` '+('supplied to `'+o+'`, expected `object`.'));for(var l in e){var u=e[l];if(u){var c=u(i,l,o,r,a+'.'+l,p);if(c)return c}}return null})}};return d.prototype=Error.prototype,P.checkPropTypes=i,P.PropTypes=P,P}},function(e){'use strict';e.exports=function(){}},function(e){'use strict';e.exports='15.6.1'},function(e,t,n){'use strict';var o=n(20),r=o.Component,a=n(7),i=a.isValidElement,p=n(21),s=n(46);e.exports=s(r,i,p)},function(e,t,n){'use strict';function o(e){return e}var r=n(3),a=n(14),i=n(0);var p,s='mixins';p={},e.exports=function(e,t,n){function p(e,t){var n=g.hasOwnProperty(t)?g[t]:null;P.hasOwnProperty(t)&&i('OVERRIDE_BASE'===n,'ReactClassInterface: You are attempting to override `%s` from your class specification. Ensure that your method names do not overlap with React methods.',t),e&&i('DEFINE_MANY'===n||'DEFINE_MANY_MERGED'===n,'ReactClassInterface: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.',t)}function d(e,n){if(!n){return}i('function'!=typeof n,'ReactClass: You\'re attempting to use a component class or function as a mixin. Instead, just use a regular object.'),i(!t(n),'ReactClass: You\'re attempting to use a component as a mixin. Instead, just use a regular object.');var o=e.prototype,r=o.__reactAutoBindPairs;for(var a in n.hasOwnProperty(s)&&b.mixins(e,n.mixins),n)if(n.hasOwnProperty(a)&&a!=s){var d=n[a],l=o.hasOwnProperty(a);if(p(l,a),b.hasOwnProperty(a))b[a](e,d);else{var u=g.hasOwnProperty(a),f='function'==typeof d&&!u&&!l&&!1!==n.autobind;if(f)r.push(a,d),o[a]=d;else if(l){var y=g[a];i(u&&('DEFINE_MANY_MERGED'===y||'DEFINE_MANY'===y),'ReactClass: Unexpected spec policy %s for key %s when mixing in component specs.',y,a),'DEFINE_MANY_MERGED'===y?o[a]=c(o[a],d):'DEFINE_MANY'===y&&(o[a]=m(o[a],d))}else o[a]=d,!1}}}function l(e,t){if(t)for(var n in t){var o=t[n];if(t.hasOwnProperty(n)){i(!(n in b),'ReactClass: You are attempting to define a reserved property, `%s`, that shouldn\'t be on the "statics" key. Define it as an instance property instead; it will still be accessible on the constructor.',n);i(!(n in e),'ReactClass: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.',n),e[n]=o}}}function u(e,t){for(var n in i(e&&t&&'object'==typeof e&&'object'==typeof t,'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.'),t)t.hasOwnProperty(n)&&(i(void 0===e[n],'mergeIntoWithNoDuplicateKeys(): Tried to merge two objects with the same key: `%s`. This conflict may be due to a mixin; in particular, this may be caused by two getInitialState() or getDefaultProps() methods returning objects with clashing keys.',n),e[n]=t[n]);return e}function c(e,t){return function(){var n=e.apply(this,arguments),o=t.apply(this,arguments);if(null==n)return o;if(null==o)return n;var r={};return u(r,n),u(r,o),r}}function m(e,t){return function(){e.apply(this,arguments),t.apply(this,arguments)}}function f(e,t){var n=t.bind(e);return n}function y(e){for(var t=e.__reactAutoBindPairs,n=0;n<t.length;n+=2){var o=t[n],r=t[n+1];e[o]=f(e,r)}}var h=[],g={mixins:'DEFINE_MANY',statics:'DEFINE_MANY',propTypes:'DEFINE_MANY',contextTypes:'DEFINE_MANY',childContextTypes:'DEFINE_MANY',getDefaultProps:'DEFINE_MANY_MERGED',getInitialState:'DEFINE_MANY_MERGED',getChildContext:'DEFINE_MANY_MERGED',render:'DEFINE_ONCE',componentWillMount:'DEFINE_MANY',componentDidMount:'DEFINE_MANY',componentWillReceiveProps:'DEFINE_MANY',shouldComponentUpdate:'DEFINE_ONCE',componentWillUpdate:'DEFINE_MANY',componentDidUpdate:'DEFINE_MANY',componentWillUnmount:'DEFINE_MANY',updateComponent:'OVERRIDE_BASE'},b={displayName:function(e,t){e.displayName=t},mixins:function(e,t){if(t)for(var n=0;n<t.length;n++)d(e,t[n])},childContextTypes:function(e,t){!1,e.childContextTypes=r({},e.childContextTypes,t)},contextTypes:function(e,t){!1,e.contextTypes=r({},e.contextTypes,t)},getDefaultProps:function(e,t){e.getDefaultProps=e.getDefaultProps?c(e.getDefaultProps,t):t},propTypes:function(e,t){!1,e.propTypes=r({},e.propTypes,t)},statics:function(e,t){l(e,t)},autobind:function(){}},E={componentDidMount:function(){this.__isMounted=!0}},x={componentWillUnmount:function(){this.__isMounted=!1}},P={replaceState:function(e,t){this.updater.enqueueReplaceState(this,e,t)},isMounted:function(){return!1,!!this.__isMounted}},_=function(){};return r(_.prototype,e.prototype,P),function(e){var t=o(function(e,o,r){!1,this.__reactAutoBindPairs.length&&y(this),this.props=e,this.context=o,this.refs=a,this.updater=r||n,this.state=null;var p=this.getInitialState?this.getInitialState():null;!1,i('object'==typeof p&&!Array.isArray(p),'%s.getInitialState(): must return an object or null',t.displayName||'ReactCompositeComponent'),this.state=p});for(var r in t.prototype=new _,t.prototype.constructor=t,t.prototype.__reactAutoBindPairs=[],h.forEach(d.bind(null,t)),d(t,E),d(t,e),d(t,x),t.getDefaultProps&&(t.defaultProps=t.getDefaultProps()),!1,i(t.prototype.render,'createClass(...): Class specification must implement a `render` method.'),!1,g)t.prototype[r]||(t.prototype[r]=null);return t}}},function(e,t,n){'use strict';var o=n(10),r=n(7),a=n(0);e.exports=function(e){return r.isValidElement(e)?void 0:o('143'),e}},,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,function(e,t,n){'use strict';e.exports=n(13)}]);this.EXPORTED_SYMBOLS = ["React"]; \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/vendor/ReactDOM.js @@ -0,0 +1,18 @@ +/* eslint-disable */this.ReactDOM=function(e){function t(o){if(n[o])return n[o].exports;var a=n[o]={i:o,l:!1,exports:{}};return e[o].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e['default']}:function(){return e};return t.d(n,'a',n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p='',t(t.s=103)}([function(e){'use strict';var t=function(){};!1,e.exports=function(n,o,r,a,i,s,d,e){if(t(o),!n){var p;if(void 0===o)p=new Error('Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.');else{var l=[r,a,i,s,d,e],u=0;p=new Error(o.replace(/%s/g,function(){return l[u++]})),p.name='Invariant Violation'}throw p.framesToPop=1,p}}},function(e,t,n){'use strict';var o=n(5);e.exports=o},function(e){'use strict';e.exports=function(e){for(var t=arguments.length-1,n='Minified React error #'+e+'; visit http://facebook.github.io/react/docs/error-decoder.html?invariant='+e,o=0;o<t;o++)n+='&args[]='+encodeURIComponent(arguments[o+1]);n+=' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.';var a=new Error(n);throw a.name='Invariant Violation',a.framesToPop=1,a}},function(e){'use strict';/* +object-assign +(c) Sindre Sorhus +@license MIT +*/function t(e){if(null===e||e===void 0)throw new TypeError('Object.assign cannot be called with null or undefined');return Object(e)}var n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var e=new String('abc');if(e[5]='de','5'===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;10>n;n++)t['_'+String.fromCharCode(n)]=n;var o=Object.getOwnPropertyNames(t).map(function(e){return t[e]});if('0123456789'!==o.join(''))return!1;var a={};return['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t'].forEach(function(e){a[e]=e}),'abcdefghijklmnopqrst'===Object.keys(Object.assign({},a)).join('')}catch(e){return!1}}()?Object.assign:function(e){for(var r=t(e),d=1,s,p;d<arguments.length;d++){for(var l in s=Object(arguments[d]),s)o.call(s,l)&&(r[l]=s[l]);if(n){p=n(s);for(var u=0;u<p.length;u++)a.call(s,p[u])&&(r[p[u]]=s[p[u]])}}return r}},function(e,t,n){'use strict';function o(e,t){return 1===e.nodeType&&e.getAttribute(c)===t+''||8===e.nodeType&&e.nodeValue===' react-text: '+t+' '||8===e.nodeType&&e.nodeValue===' react-empty: '+t+' '}function a(e){for(var t;t=e._renderedComponent;)e=t;return e}function r(e,t){var n=a(e);n._hostNode=t,t[h]=n}function i(e,t){if(!(e._flags&m.hasCachedChildNodes)){var n=e._renderedChildren,i=t.firstChild;outer:for(var d in n)if(n.hasOwnProperty(d)){var p=n[d],l=a(p)._domID;if(0!==l){for(;null!==i;i=i.nextSibling)if(o(i,l)){r(p,i);continue outer}s('32',l)}}e._flags|=m.hasCachedChildNodes}}function d(e){if(e[h])return e[h];for(var t=[];!e[h];)if(t.push(e),e.parentNode)e=e.parentNode;else return null;for(var n,o;e&&(o=e[h]);e=t.pop())n=o,t.length&&i(o,e);return n}var s=n(2),p=n(16),l=n(66),u=n(0),c=p.ID_ATTRIBUTE_NAME,m=l,h='__reactInternalInstance$'+Math.random().toString(36).slice(2);e.exports={getClosestInstanceFromNode:d,getInstanceFromNode:function(e){var t=d(e);return null!=t&&t._hostNode===e?t:null},getNodeFromInstance:function(e){if(void 0===e._hostNode?s('33'):void 0,e._hostNode)return e._hostNode;for(var t=[];!e._hostNode;)t.push(e),e._hostParent?void 0:s('34'),e=e._hostParent;for(;t.length;e=t.pop())i(e,e._hostNode);return e._hostNode},precacheChildNodes:i,precacheNode:r,uncacheNode:function(e){var t=e._hostNode;t&&(delete t[h],e._hostNode=null)}}},function(e){'use strict';function t(e){return function(){return e}}var n=function(){};n.thatReturns=t,n.thatReturnsFalse=t(!1),n.thatReturnsTrue=t(!0),n.thatReturnsNull=t(null),n.thatReturnsThis=function(){return this},n.thatReturnsArgument=function(e){return e},e.exports=n},function(e){'use strict';var t=!!('undefined'!=typeof window&&window.document&&window.document.createElement),n={canUseDOM:t,canUseWorkers:'undefined'!=typeof Worker,canUseEventListeners:t&&!!(window.addEventListener||window.attachEvent),canUseViewport:t&&!!window.screen,isInWorker:!t};e.exports=n},function(e,t,n){'use strict';function o(e){return e.ref!==void 0}function a(e){return e.key!==void 0}var r=n(3),d=n(8),i=n(1),s=n(22),p=Object.prototype.hasOwnProperty,l=n(23),u={key:!0,ref:!0,__self:!0,__source:!0},c=function(e,t,n,o,a,r,i){return!1,{$$typeof:l,type:e,key:t,ref:n,props:i,_owner:r}},m,h;c.createElement=function(e,t,n){var r={},s=null,l=null,m=null,h=null,g;if(null!=t)for(g in o(t)&&(l=t.ref),a(t)&&(s=''+t.key),m=void 0===t.__self?null:t.__self,h=void 0===t.__source?null:t.__source,t)p.call(t,g)&&!u.hasOwnProperty(g)&&(r[g]=t[g]);var f=arguments.length-2;if(1==f)r.children=n;else if(1<f){for(var y=Array(f),_=0;_<f;_++)y[_]=arguments[_+2];!1,r.children=y}if(e&&e.defaultProps){var i=e.defaultProps;for(g in i)void 0===r[g]&&(r[g]=i[g])}return c(e,s,l,m,h,d.current,r)},c.createFactory=function(e){var t=c.createElement.bind(null,e);return t.type=e,t},c.cloneAndReplaceKey=function(e,t){var n=c(e.type,t,e.ref,e._self,e._source,e._owner,e.props);return n},c.cloneElement=function(e,t,n){var s=r({},e.props),l=e.key,m=e.ref,h=e._self,g=e._source,f=e._owner,y;if(null!=t){o(t)&&(m=t.ref,f=d.current),a(t)&&(l=''+t.key);var _;for(y in e.type&&e.type.defaultProps&&(_=e.type.defaultProps),t)p.call(t,y)&&!u.hasOwnProperty(y)&&(s[y]=void 0===t[y]&&void 0!==_?_[y]:t[y])}var C=arguments.length-2;if(1==C)s.children=n;else if(1<C){for(var b=Array(C),E=0;E<C;E++)b[E]=arguments[E+2];s.children=b}return c(e.type,l,m,h,g,f,s)},c.isValidElement=function(e){return'object'==typeof e&&null!==e&&e.$$typeof===l},e.exports=c},function(e){'use strict';e.exports={current:null}},function(e){'use strict';e.exports={debugTool:null}},function(e){'use strict';e.exports=function(e){for(var t=arguments.length-1,n='Minified React error #'+e+'; visit http://facebook.github.io/react/docs/error-decoder.html?invariant='+e,o=0;o<t;o++)n+='&args[]='+encodeURIComponent(arguments[o+1]);n+=' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.';var a=new Error(n);throw a.name='Invariant Violation',a.framesToPop=1,a}},function(e,t,n){'use strict';function o(){x.ReactReconcileTransaction&&E?void 0:s('123')}function a(){this.reinitializeTransaction(),this.dirtyComponentsLength=null,this.callbackQueue=l.getPooled(),this.reconcileTransaction=x.ReactReconcileTransaction.getPooled(!0)}function r(e,t){return e._mountOrder-t._mountOrder}function i(e){var t=e.dirtyComponentsLength;t===f.length?void 0:s('124',t,f.length),f.sort(r),y++;for(var n=0;n<t;n++){var o=f[n],a=o._pendingCallbacks;o._pendingCallbacks=null;var i;if(c.logTopLevelRenders){var d=o;o._currentElement.type.isReactTopLevelWrapper&&(d=o._renderedComponent),i='React update: '+d.getName(),console.time(i)}if(m.performUpdateIfNecessary(o,e.reconcileTransaction,y),i&&console.timeEnd(i),a)for(var p=0;p<a.length;p++)e.callbackQueue.enqueue(a[p],o.getPublicInstance())}}function d(e){return o(),E.isBatchingUpdates?void(f.push(e),null==e._updateBatchNumber&&(e._updateBatchNumber=y+1)):void E.batchedUpdates(d,e)}var s=n(2),p=n(3),l=n(70),u=n(15),c=n(71),m=n(17),h=n(29),g=n(0),f=[],y=0,_=l.getPooled(),C=!1,E=null,b=[{initialize:function(){this.dirtyComponentsLength=f.length},close:function(){this.dirtyComponentsLength===f.length?f.length=0:(f.splice(0,this.dirtyComponentsLength),v())}},{initialize:function(){this.callbackQueue.reset()},close:function(){this.callbackQueue.notifyAll()}}];p(a.prototype,h,{getTransactionWrappers:function(){return b},destructor:function(){this.dirtyComponentsLength=null,l.release(this.callbackQueue),this.callbackQueue=null,x.ReactReconcileTransaction.release(this.reconcileTransaction),this.reconcileTransaction=null},perform:function(e,t,n){return h.perform.call(this,this.reconcileTransaction.perform,this.reconcileTransaction,e,t,n)}}),u.addPoolingTo(a);var v=function(){for(;f.length||C;){if(f.length){var e=a.getPooled();e.perform(i,null,e),a.release(e)}if(C){C=!1;var t=_;_=l.getPooled(),t.notifyAll(),l.release(t)}}},x={ReactReconcileTransaction:null,batchedUpdates:function(t,n,a,r,i,d){return o(),E.batchedUpdates(t,n,a,r,i,d)},enqueueUpdate:d,flushBatchedUpdates:v,injection:{injectReconcileTransaction:function(e){e?void 0:s('126'),x.ReactReconcileTransaction=e},injectBatchingStrategy:function(e){e?void 0:s('127'),'function'==typeof e.batchedUpdates?void 0:s('128'),'boolean'==typeof e.isBatchingUpdates?void 0:s('129'),E=e}},asap:function(e,t){E.isBatchingUpdates?void 0:s('125'),_.enqueue(e,t),C=!0}};e.exports=x},function(e,t,n){'use strict';function o(e,t,n,o){!1,this.dispatchConfig=e,this._targetInst=t,this.nativeEvent=n;var a=this.constructor.Interface;for(var r in a)if(a.hasOwnProperty(r)){var d=a[r];d?this[r]=d(n):'target'==r?this.target=o:this[r]=n[r]}var s=null==n.defaultPrevented?!1===n.returnValue:n.defaultPrevented;return this.isDefaultPrevented=s?i.thatReturnsTrue:i.thatReturnsFalse,this.isPropagationStopped=i.thatReturnsFalse,this}var a=n(3),r=n(15),i=n(5),d=n(1),s='function'==typeof Proxy,p=['dispatchConfig','_targetInst','nativeEvent','isDefaultPrevented','isPropagationStopped','_dispatchListeners','_dispatchInstances'],l={type:null,target:null,currentTarget:i.thatReturnsNull,eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null};a(o.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():'unknown'!=typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=i.thatReturnsTrue)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():'unknown'!=typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=i.thatReturnsTrue)},persist:function(){this.isPersistent=i.thatReturnsTrue},isPersistent:i.thatReturnsFalse,destructor:function(){var e=this.constructor.Interface;for(var t in e)this[t]=null;for(var n=0;n<p.length;n++)this[p[n]]=null}}),o.Interface=l,!1,o.augmentClass=function(e,t){var n=this,o=function(){};o.prototype=n.prototype;var i=new o;a(i,e.prototype),e.prototype=i,e.prototype.constructor=e,e.Interface=a({},n.Interface,t),e.augmentClass=n.augmentClass,r.addPoolingTo(e,r.fourArgumentPooler)},r.addPoolingTo(o,r.fourArgumentPooler),e.exports=o},function(e,t,n){'use strict';var o=n(3),a=n(20),r=n(35),i=n(40),d=n(7),s=n(41),p=n(44),l=n(45),u=n(47),c=d.createElement,m=d.createFactory,h=d.cloneElement;var g={Children:{map:r.map,forEach:r.forEach,count:r.count,toArray:r.toArray,only:u},Component:a.Component,PureComponent:a.PureComponent,createElement:c,cloneElement:h,isValidElement:d.isValidElement,PropTypes:s,createClass:l,createFactory:m,createMixin:function(e){return e},DOM:i,version:p,__spread:o};e.exports=g},function(e){'use strict';!1,e.exports={}},function(e,t,n){'use strict';var o=n(2),a=n(0),r=function(e){var t=this;if(t.instancePool.length){var n=t.instancePool.pop();return t.call(n,e),n}return new t(e)},i=function(e){var t=this;e instanceof t?void 0:o('25'),e.destructor(),t.instancePool.length<t.poolSize&&t.instancePool.push(e)};e.exports={addPoolingTo:function(e,t){var n=e;return n.instancePool=[],n.getPooled=t||r,n.poolSize||(n.poolSize=10),n.release=i,n},oneArgumentPooler:r,twoArgumentPooler:function(e,t){var n=this;if(n.instancePool.length){var o=n.instancePool.pop();return n.call(o,e,t),o}return new n(e,t)},threeArgumentPooler:function(e,t,n){var o=this;if(o.instancePool.length){var a=o.instancePool.pop();return o.call(a,e,t,n),a}return new o(e,t,n)},fourArgumentPooler:function(e,t,n,o){var a=this;if(a.instancePool.length){var r=a.instancePool.pop();return a.call(r,e,t,n,o),r}return new a(e,t,n,o)}}},function(e,t,n){'use strict';function o(e,t){return(e&t)===t}var a=n(2),r=n(0),i={MUST_USE_PROPERTY:1,HAS_BOOLEAN_VALUE:4,HAS_NUMERIC_VALUE:8,HAS_POSITIVE_NUMERIC_VALUE:24,HAS_OVERLOADED_BOOLEAN_VALUE:32,injectDOMPropertyConfig:function(e){var t=i,n=e.Properties||{},r=e.DOMAttributeNamespaces||{},d=e.DOMAttributeNames||{},p=e.DOMPropertyNames||{},l=e.DOMMutationMethods||{};for(var u in e.isCustomAttribute&&s._isCustomAttributeFunctions.push(e.isCustomAttribute),n){s.properties.hasOwnProperty(u)?a('48',u):void 0;var c=u.toLowerCase(),m=n[u],h={attributeName:c,attributeNamespace:null,propertyName:u,mutationMethod:null,mustUseProperty:o(m,t.MUST_USE_PROPERTY),hasBooleanValue:o(m,t.HAS_BOOLEAN_VALUE),hasNumericValue:o(m,t.HAS_NUMERIC_VALUE),hasPositiveNumericValue:o(m,t.HAS_POSITIVE_NUMERIC_VALUE),hasOverloadedBooleanValue:o(m,t.HAS_OVERLOADED_BOOLEAN_VALUE)};if(1>=h.hasBooleanValue+h.hasNumericValue+h.hasOverloadedBooleanValue?void 0:a('50',u),!1,d.hasOwnProperty(u)){var g=d[u];h.attributeName=g,!1}r.hasOwnProperty(u)&&(h.attributeNamespace=r[u]),p.hasOwnProperty(u)&&(h.propertyName=p[u]),l.hasOwnProperty(u)&&(h.mutationMethod=l[u]),s.properties[u]=h}}},d=':A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD',s={ID_ATTRIBUTE_NAME:'data-reactid',ROOT_ATTRIBUTE_NAME:'data-reactroot',ATTRIBUTE_NAME_START_CHAR:d,ATTRIBUTE_NAME_CHAR:d+'\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040',properties:{},getPossibleStandardName:null,_isCustomAttributeFunctions:[],isCustomAttribute:function(e){for(var t=0,n;t<s._isCustomAttributeFunctions.length;t++)if(n=s._isCustomAttributeFunctions[t],n(e))return!0;return!1},injection:i};e.exports=s},function(e,t,n){'use strict';function o(){a.attachRefs(this,this._currentElement)}var a=n(112),r=n(9),i=n(1);e.exports={mountComponent:function(e,t,n,a,r,i){var d=e.mountComponent(t,n,a,r,i);return e._currentElement&&null!=e._currentElement.ref&&t.getReactMountReady().enqueue(o,e),!1,d},getHostNode:function(e){return e.getHostNode()},unmountComponent:function(e,t){!1,a.detachRefs(e,e._currentElement),e.unmountComponent(t),!1},receiveComponent:function(e,t,n,r){var i=e._currentElement;if(t!==i||r!==e._context){var d=a.shouldUpdateRefs(i,t);d&&a.detachRefs(e,i),e.receiveComponent(t,n,r),d&&e._currentElement&&null!=e._currentElement.ref&&n.getReactMountReady().enqueue(o,e),!1}},performUpdateIfNecessary:function(e,t,n){return e._updateBatchNumber===n?void(!1,e.performUpdateIfNecessary(t),!1):void void 0}}},function(e,t,n){'use strict';function o(e){if(l){var t=e.node,n=e.children;if(n.length)for(var o=0;o<n.length;o++)u(t,n[o],null);else null==e.html?null!=e.text&&p(t,e.text):d(t,e.html)}}function a(){return this.node.nodeName}function r(e){return{node:e,children:[],html:null,text:null,toString:a}}var i=n(55),d=n(31),s=n(56),p=n(75),l='undefined'!=typeof document&&'number'==typeof document.documentMode||'undefined'!=typeof navigator&&'string'==typeof navigator.userAgent&&/\bEdge\/\d/.test(navigator.userAgent),u=s(function(e,t,n){t.node.nodeType===11||t.node.nodeType===1&&'object'===t.node.nodeName.toLowerCase()&&(null==t.node.namespaceURI||t.node.namespaceURI===i.html)?(o(t),e.insertBefore(t.node,n)):(e.insertBefore(t.node,n),o(t))});r.insertTreeBefore=u,r.replaceChildWithTree=function(e,t){e.parentNode.replaceChild(t.node,e),o(t)},r.queueChild=function(e,t){l?e.children.push(t):e.node.appendChild(t.node)},r.queueHTML=function(e,t){l?e.html=t:d(e.node,t)},r.queueText=function(e,t){l?e.text=t:p(e.node,t)},e.exports=r},function(e){'use strict';e.exports='SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'},function(e,t,n){'use strict';function o(e,t,n){this.props=e,this.context=t,this.refs=l,this.updater=n||s}function a(e,t,n){this.props=e,this.context=t,this.refs=l,this.updater=n||s}function r(){}var i=n(10),d=n(3),s=n(21),p=n(22),l=n(14),u=n(0),c=n(34);o.prototype.isReactComponent={},o.prototype.setState=function(e,t){'object'==typeof e||'function'==typeof e||null==e?void 0:i('85'),this.updater.enqueueSetState(this,e),t&&this.updater.enqueueCallback(this,t,'setState')},o.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this),e&&this.updater.enqueueCallback(this,e,'forceUpdate')};r.prototype=o.prototype,a.prototype=new r,a.prototype.constructor=a,d(a.prototype,o.prototype),a.prototype.isPureReactComponent=!0,e.exports={Component:o,PureComponent:a}},function(e,t,n){'use strict';function o(){}var a=n(1);e.exports={isMounted:function(){return!1},enqueueCallback:function(){},enqueueForceUpdate:function(e){o(e,'forceUpdate')},enqueueReplaceState:function(e){o(e,'replaceState')},enqueueSetState:function(e){o(e,'setState')}}},function(e){'use strict';e.exports=!1},function(e){'use strict';var t='function'==typeof Symbol&&Symbol['for']&&Symbol['for']('react.element')||60103;e.exports=t},function(e,t,n){'use strict';function o(e,t,n){var o=t.dispatchConfig.phasedRegistrationNames[n];return h(e,o)}function a(e,t,n){var a=o(e,n,t);a&&(n._dispatchListeners=u(n._dispatchListeners,a),n._dispatchInstances=u(n._dispatchInstances,e))}function r(e){e&&e.dispatchConfig.phasedRegistrationNames&&l.traverseTwoPhase(e._targetInst,a,e)}function i(e){if(e&&e.dispatchConfig.phasedRegistrationNames){var t=e._targetInst,n=t?l.getParentInstance(t):null;l.traverseTwoPhase(n,a,e)}}function d(e,t,n){if(n&&n.dispatchConfig.registrationName){var o=n.dispatchConfig.registrationName,a=h(e,o);a&&(n._dispatchListeners=u(n._dispatchListeners,a),n._dispatchInstances=u(n._dispatchInstances,e))}}function s(e){e&&e.dispatchConfig.registrationName&&d(e._targetInst,null,e)}var p=n(25),l=n(49),u=n(67),c=n(68),m=n(1),h=p.getListener;e.exports={accumulateTwoPhaseDispatches:function(e){c(e,r)},accumulateTwoPhaseDispatchesSkipTarget:function(e){c(e,i)},accumulateDirectDispatches:function(e){c(e,s)},accumulateEnterLeaveDispatches:function(e,t,n,o){l.traverseEnterLeave(n,o,d,e,t)}}},function(e,t,n){'use strict';function o(e){return'button'===e||'input'===e||'select'===e||'textarea'===e}function a(e,t,n){return('onClick'===e||'onClickCapture'===e||'onDoubleClick'===e||'onDoubleClickCapture'===e||'onMouseDown'===e||'onMouseDownCapture'===e||'onMouseMove'===e||'onMouseMoveCapture'===e||'onMouseUp'===e||'onMouseUpCapture'===e)&&!!(n.disabled&&o(t))}var r=n(2),d=n(48),i=n(49),s=n(50),p=n(67),l=n(68),u=n(0),c={},m=null,h=function(e,t){e&&(i.executeDispatchesInOrder(e,t),!e.isPersistent()&&e.constructor.release(e))},g=function(t){return h(t,!0)},f=function(t){return h(t,!1)},y=function(e){return'.'+e._rootNodeID},_={injection:{injectEventPluginOrder:d.injectEventPluginOrder,injectEventPluginsByName:d.injectEventPluginsByName},putListener:function(e,t,n){'function'==typeof n?void 0:r('94',t,typeof n);var o=y(e),a=c[t]||(c[t]={});a[o]=n;var i=d.registrationNameModules[t];i&&i.didPutListener&&i.didPutListener(e,t,n)},getListener:function(e,t){var n=c[t];if(a(t,e._currentElement.type,e._currentElement.props))return null;var o=y(e);return n&&n[o]},deleteListener:function(e,t){var n=d.registrationNameModules[t];n&&n.willDeleteListener&&n.willDeleteListener(e,t);var o=c[t];if(o){var a=y(e);delete o[a]}},deleteAllListeners:function(e){var t=y(e);for(var n in c)if(c.hasOwnProperty(n)&&c[n][t]){var o=d.registrationNameModules[n];o&&o.willDeleteListener&&o.willDeleteListener(e,n),delete c[n][t]}},extractEvents:function(e,t,n,o){for(var a=d.plugins,r=0,i,s;r<a.length;r++)if(s=a[r],s){var l=s.extractEvents(e,t,n,o);l&&(i=p(i,l))}return i},enqueueEvents:function(e){e&&(m=p(m,e))},processEventQueue:function(e){var t=m;m=null,e?l(t,g):l(t,f),!m?void 0:r('95'),s.rethrowCaughtError()},__purge:function(){c={}},__getListenerBank:function(){return c}};e.exports=_},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(12),r=n(51);a.augmentClass(o,{view:function(e){if(e.view)return e.view;var t=r(e);if(t.window===t)return t;var n=t.ownerDocument;return n?n.defaultView||n.parentWindow:window},detail:function(e){return e.detail||0}}),e.exports=o},function(e){'use strict';e.exports={remove:function(e){e._reactInternalInstance=void 0},get:function(e){return e._reactInternalInstance},has:function(e){return e._reactInternalInstance!==void 0},set:function(e,t){e._reactInternalInstance=t}}},function(e,t,n){'use strict';var o=n(42);e.exports=function(e){return o(e,!1)}},function(e,t,n){'use strict';var o=n(2),a=n(0),r={};e.exports={reinitializeTransaction:function(){this.transactionWrappers=this.getTransactionWrappers(),this.wrapperInitData?this.wrapperInitData.length=0:this.wrapperInitData=[],this._isInTransaction=!1},_isInTransaction:!1,getTransactionWrappers:null,isInTransaction:function(){return!!this._isInTransaction},perform:function(t,n,r,a,i,s,d,e){!this.isInTransaction()?void 0:o('27');var p,l;try{this._isInTransaction=!0,p=!0,this.initializeAll(0),l=t.call(n,r,a,i,s,d,e),p=!1}finally{try{if(p)try{this.closeAll(0)}catch(e){}else this.closeAll(0)}finally{this._isInTransaction=!1}}return l},initializeAll:function(e){for(var t=this.transactionWrappers,n=e,o;n<t.length;n++){o=t[n];try{this.wrapperInitData[n]=r,this.wrapperInitData[n]=o.initialize?o.initialize.call(this):null}finally{if(this.wrapperInitData[n]===r)try{this.initializeAll(n+1)}catch(e){}}}},closeAll:function(e){this.isInTransaction()?void 0:o('28');for(var t=this.transactionWrappers,n=e;n<t.length;n++){var a=t[n],i=this.wrapperInitData[n],d;try{d=!0,i!==r&&a.close&&a.close.call(this,i),d=!1}finally{if(d)try{this.closeAll(n+1)}catch(t){}}}this.wrapperInitData.length=0}}},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(26),r=n(74),i=n(53);a.augmentClass(o,{screenX:null,screenY:null,clientX:null,clientY:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,getModifierState:i,button:function(e){var t=e.button;return'which'in e?t:2===t?2:4===t?1:0},buttons:null,relatedTarget:function(e){return e.relatedTarget||(e.fromElement===e.srcElement?e.toElement:e.fromElement)},pageX:function(e){return'pageX'in e?e.pageX:e.clientX+r.currentScrollLeft},pageY:function(e){return'pageY'in e?e.pageY:e.clientY+r.currentScrollTop}}),e.exports=o},function(e,t,n){'use strict';var o=n(6),a=n(55),r=/^[ \r\n\t\f]/,i=/<(!--|link|noscript|meta|script|style)[ \r\n\t\f\/>]/,d=n(56),s=d(function(e,t){if(e.namespaceURI===a.svg&&!('innerHTML'in e)){p=p||document.createElement('div'),p.innerHTML='<svg>'+t+'</svg>';for(var n=p.firstChild;n.firstChild;)e.appendChild(n.firstChild)}else e.innerHTML=t}),p;if(o.canUseDOM){var l=document.createElement('div');l.innerHTML=' ',''===l.innerHTML&&(s=function(e,t){if(e.parentNode&&e.parentNode.replaceChild(e,e),r.test(t)||'<'===t[0]&&i.test(t)){e.innerHTML='\uFEFF'+t;var n=e.firstChild;1===n.data.length?e.removeChild(n):n.deleteData(0,1)}else e.innerHTML=t}),l=null}e.exports=s},function(e){'use strict';function t(e){var t=''+e,o=n.exec(t);if(!o)return t;var a='',r=0,i=0,d;for(r=o.index;r<t.length;r++){switch(t.charCodeAt(r)){case 34:d='"';break;case 38:d='&';break;case 39:d=''';break;case 60:d='<';break;case 62:d='>';break;default:continue;}i!==r&&(a+=t.substring(i,r)),i=r+1,a+=d}return i===r?a:a+t.substring(i,r)}var n=/["'&<>]/;e.exports=function(e){return'boolean'==typeof e||'number'==typeof e?''+e:t(e)}},function(e,t,n){'use strict';function o(e){return Object.prototype.hasOwnProperty.call(e,h)||(e[h]=c++,l[e[h]]={}),l[e[h]]}var a=n(3),r=n(48),i=n(133),d=n(74),s=n(134),p=n(52),l={},u=!1,c=0,m={topAbort:'abort',topAnimationEnd:s('animationend')||'animationend',topAnimationIteration:s('animationiteration')||'animationiteration',topAnimationStart:s('animationstart')||'animationstart',topBlur:'blur',topCanPlay:'canplay',topCanPlayThrough:'canplaythrough',topChange:'change',topClick:'click',topCompositionEnd:'compositionend',topCompositionStart:'compositionstart',topCompositionUpdate:'compositionupdate',topContextMenu:'contextmenu',topCopy:'copy',topCut:'cut',topDoubleClick:'dblclick',topDrag:'drag',topDragEnd:'dragend',topDragEnter:'dragenter',topDragExit:'dragexit',topDragLeave:'dragleave',topDragOver:'dragover',topDragStart:'dragstart',topDrop:'drop',topDurationChange:'durationchange',topEmptied:'emptied',topEncrypted:'encrypted',topEnded:'ended',topError:'error',topFocus:'focus',topInput:'input',topKeyDown:'keydown',topKeyPress:'keypress',topKeyUp:'keyup',topLoadedData:'loadeddata',topLoadedMetadata:'loadedmetadata',topLoadStart:'loadstart',topMouseDown:'mousedown',topMouseMove:'mousemove',topMouseOut:'mouseout',topMouseOver:'mouseover',topMouseUp:'mouseup',topPaste:'paste',topPause:'pause',topPlay:'play',topPlaying:'playing',topProgress:'progress',topRateChange:'ratechange',topScroll:'scroll',topSeeked:'seeked',topSeeking:'seeking',topSelectionChange:'selectionchange',topStalled:'stalled',topSuspend:'suspend',topTextInput:'textInput',topTimeUpdate:'timeupdate',topTouchCancel:'touchcancel',topTouchEnd:'touchend',topTouchMove:'touchmove',topTouchStart:'touchstart',topTransitionEnd:s('transitionend')||'transitionend',topVolumeChange:'volumechange',topWaiting:'waiting',topWheel:'wheel'},h='_reactListenersID'+(Math.random()+'').slice(2),g=a({},i,{ReactEventListener:null,injection:{injectReactEventListener:function(e){e.setHandleTopLevel(g.handleTopLevel),g.ReactEventListener=e}},setEnabled:function(e){g.ReactEventListener&&g.ReactEventListener.setEnabled(e)},isEnabled:function(){return!!(g.ReactEventListener&&g.ReactEventListener.isEnabled())},listenTo:function(e,t){for(var n=t,a=o(n),d=r.registrationNameDependencies[e],s=0,i;s<d.length;s++)i=d[s],a.hasOwnProperty(i)&&a[i]||('topWheel'===i?p('wheel')?g.ReactEventListener.trapBubbledEvent('topWheel','wheel',n):p('mousewheel')?g.ReactEventListener.trapBubbledEvent('topWheel','mousewheel',n):g.ReactEventListener.trapBubbledEvent('topWheel','DOMMouseScroll',n):'topScroll'===i?p('scroll',!0)?g.ReactEventListener.trapCapturedEvent('topScroll','scroll',n):g.ReactEventListener.trapBubbledEvent('topScroll','scroll',g.ReactEventListener.WINDOW_HANDLE):'topFocus'===i||'topBlur'===i?(p('focus',!0)?(g.ReactEventListener.trapCapturedEvent('topFocus','focus',n),g.ReactEventListener.trapCapturedEvent('topBlur','blur',n)):p('focusin')&&(g.ReactEventListener.trapBubbledEvent('topFocus','focusin',n),g.ReactEventListener.trapBubbledEvent('topBlur','focusout',n)),a.topBlur=!0,a.topFocus=!0):m.hasOwnProperty(i)&&g.ReactEventListener.trapBubbledEvent(i,m[i],n),a[i]=!0)},trapBubbledEvent:function(e,t,n){return g.ReactEventListener.trapBubbledEvent(e,t,n)},trapCapturedEvent:function(e,t,n){return g.ReactEventListener.trapCapturedEvent(e,t,n)},supportsEventPageXY:function(){if(!document.createEvent)return!1;var e=document.createEvent('MouseEvent');return null!=e&&'pageX'in e},ensureScrollValueMonitoring:function(){if(void 0==f&&(f=g.supportsEventPageXY()),!f&&!u){var e=d.refreshScrollValues;g.ReactEventListener.monitorScrollValue(e),u=!0}}}),f;e.exports=g},function(e){'use strict';e.exports=function(){}},function(e,t,n){'use strict';function o(e){return(''+e).replace(f,'$&/')}function a(e,t){this.func=e,this.context=t,this.count=0}function r(e,t){var n=e.func,o=e.context;n.call(o,t,e.count++)}function i(e,t,n,o){this.result=e,this.keyPrefix=t,this.func=n,this.context=o,this.count=0}function d(e,t,n){var a=e.result,r=e.keyPrefix,i=e.func,d=e.context,p=i.call(d,t,e.count++);Array.isArray(p)?s(p,a,n,c.thatReturnsArgument):null!=p&&(u.isValidElement(p)&&(p=u.cloneAndReplaceKey(p,r+(p.key&&(!t||t.key!==p.key)?o(p.key)+'/':'')+n)),a.push(p))}function s(e,t,n,a,r){var s='';null!=n&&(s=o(n)+'/');var p=i.getPooled(t,s,a,r);m(e,d,p),i.release(p)}function p(){return null}var l=n(36),u=n(7),c=n(5),m=n(37),h=l.twoArgumentPooler,g=l.fourArgumentPooler,f=/\/+/g;a.prototype.destructor=function(){this.func=null,this.context=null,this.count=0},l.addPoolingTo(a,h),i.prototype.destructor=function(){this.result=null,this.keyPrefix=null,this.func=null,this.context=null,this.count=0},l.addPoolingTo(i,g);e.exports={forEach:function(e,t,n){if(null==e)return e;var o=a.getPooled(t,n);m(e,r,o),a.release(o)},map:function(e,t,n){if(null==e)return e;var o=[];return s(e,o,null,t,n),o},mapIntoWithKeyPrefixInternal:s,count:function(e){return m(e,p,null)},toArray:function(e){var t=[];return s(e,t,null,c.thatReturnsArgument),t}}},function(e,t,n){'use strict';var o=n(10),a=n(0),r=function(e){var t=this;if(t.instancePool.length){var n=t.instancePool.pop();return t.call(n,e),n}return new t(e)},i=function(e){var t=this;e instanceof t?void 0:o('25'),e.destructor(),t.instancePool.length<t.poolSize&&t.instancePool.push(e)};e.exports={addPoolingTo:function(e,t){var n=e;return n.instancePool=[],n.getPooled=t||r,n.poolSize||(n.poolSize=10),n.release=i,n},oneArgumentPooler:r,twoArgumentPooler:function(e,t){var n=this;if(n.instancePool.length){var o=n.instancePool.pop();return n.call(o,e,t),o}return new n(e,t)},threeArgumentPooler:function(e,t,n){var o=this;if(o.instancePool.length){var a=o.instancePool.pop();return o.call(a,e,t,n),a}return new o(e,t,n)},fourArgumentPooler:function(e,t,n,o){var a=this;if(a.instancePool.length){var r=a.instancePool.pop();return a.call(r,e,t,n,o),r}return new a(e,t,n,o)}}},function(e,t,n){'use strict';function o(e,t){return e&&'object'==typeof e&&null!=e.key?l.escape(e.key):t.toString(36)}function a(e,t,n,p){var u=typeof e;if(('undefined'==u||'boolean'==u)&&(e=null),null===e||'string'==u||'number'==u||'object'==u&&e.$$typeof===d)return n(p,e,''===t?c+o(e,0):t),1;var h=0,g=''===t?c:t+m,f,y;if(Array.isArray(e))for(var _=0;_<e.length;_++)f=e[_],y=g+o(f,_),h+=a(f,y,n,p);else{var i=s(e);if(i){var C=i.call(e),b;if(i!==e.entries)for(var E=0;!(b=C.next()).done;)f=b.value,y=g+o(f,E++),h+=a(f,y,n,p);else for(var v;!(b=C.next()).done;)v=b.value,v&&(f=v[1],y=g+l.escape(v[0])+m+o(f,0),h+=a(f,y,n,p))}else if('object'==u){var x='',N=e+'';r('31','[object Object]'===N?'object with keys {'+Object.keys(e).join(', ')+'}':N,x)}}return h}var r=n(10),i=n(8),d=n(23),s=n(38),p=n(0),l=n(39),u=n(1),c='.',m=':';e.exports=function(e,t,n){return null==e?0:a(e,'',t,n)}},function(e){'use strict';var t='function'==typeof Symbol&&Symbol.iterator;e.exports=function(e){var n=e&&(t&&e[t]||e['@@iterator']);if('function'==typeof n)return n}},function(e){'use strict';e.exports={escape:function(e){var t=/[=:]/g,n={"=":'=0',":":'=2'},o=(''+e).replace(t,function(e){return n[e]});return'$'+o},unescape:function(e){var t=/(=0|=2)/g,n={"=0":'=',"=2":':'},o='.'===e[0]&&'$'===e[1]?e.substring(2):e.substring(1);return(''+o).replace(t,function(e){return n[e]})}}},function(e,t,n){'use strict';var o=n(7),a=o.createFactory;var r={a:a('a'),abbr:a('abbr'),address:a('address'),area:a('area'),article:a('article'),aside:a('aside'),audio:a('audio'),b:a('b'),base:a('base'),bdi:a('bdi'),bdo:a('bdo'),big:a('big'),blockquote:a('blockquote'),body:a('body'),br:a('br'),button:a('button'),canvas:a('canvas'),caption:a('caption'),cite:a('cite'),code:a('code'),col:a('col'),colgroup:a('colgroup'),data:a('data'),datalist:a('datalist'),dd:a('dd'),del:a('del'),details:a('details'),dfn:a('dfn'),dialog:a('dialog'),div:a('div'),dl:a('dl'),dt:a('dt'),em:a('em'),embed:a('embed'),fieldset:a('fieldset'),figcaption:a('figcaption'),figure:a('figure'),footer:a('footer'),form:a('form'),h1:a('h1'),h2:a('h2'),h3:a('h3'),h4:a('h4'),h5:a('h5'),h6:a('h6'),head:a('head'),header:a('header'),hgroup:a('hgroup'),hr:a('hr'),html:a('html'),i:a('i'),iframe:a('iframe'),img:a('img'),input:a('input'),ins:a('ins'),kbd:a('kbd'),keygen:a('keygen'),label:a('label'),legend:a('legend'),li:a('li'),link:a('link'),main:a('main'),map:a('map'),mark:a('mark'),menu:a('menu'),menuitem:a('menuitem'),meta:a('meta'),meter:a('meter'),nav:a('nav'),noscript:a('noscript'),object:a('object'),ol:a('ol'),optgroup:a('optgroup'),option:a('option'),output:a('output'),p:a('p'),param:a('param'),picture:a('picture'),pre:a('pre'),progress:a('progress'),q:a('q'),rp:a('rp'),rt:a('rt'),ruby:a('ruby'),s:a('s'),samp:a('samp'),script:a('script'),section:a('section'),select:a('select'),small:a('small'),source:a('source'),span:a('span'),strong:a('strong'),style:a('style'),sub:a('sub'),summary:a('summary'),sup:a('sup'),table:a('table'),tbody:a('tbody'),td:a('td'),textarea:a('textarea'),tfoot:a('tfoot'),th:a('th'),thead:a('thead'),time:a('time'),title:a('title'),tr:a('tr'),track:a('track'),u:a('u'),ul:a('ul'),var:a('var'),video:a('video'),wbr:a('wbr'),circle:a('circle'),clipPath:a('clipPath'),defs:a('defs'),ellipse:a('ellipse'),g:a('g'),image:a('image'),line:a('line'),linearGradient:a('linearGradient'),mask:a('mask'),path:a('path'),pattern:a('pattern'),polygon:a('polygon'),polyline:a('polyline'),radialGradient:a('radialGradient'),rect:a('rect'),stop:a('stop'),svg:a('svg'),text:a('text'),tspan:a('tspan')};e.exports=r},function(e,t,n){'use strict';var o=n(7),a=o.isValidElement,r=n(28);e.exports=r(a)},function(e,t,n){'use strict';var o=n(5),a=n(0),r=n(1),d=n(19),i=n(43);e.exports=function(e,t){function n(e){var t=e&&(_&&e[_]||e[C]);if('function'==typeof t)return t}function s(e,t){return e===t?0!==e||1/e==1/t:e!==e&&t!==t}function p(e){this.message=e,this.stack=''}function l(e){function n(n,o,r,i,s,l,u){if(i=i||b,l=l||r,u!==d)if(t)a(!1,'Calling PropTypes validators directly is not supported by the `prop-types` package. Use `PropTypes.checkPropTypes()` to call them. Read more at http://fb.me/use-check-prop-types');else;return null==o[r]?n?null===o[r]?new p('The '+s+' `'+l+'` is marked as required '+('in `'+i+'`, but its value is `null`.')):new p('The '+s+' `'+l+'` is marked as required in '+('`'+i+'`, but its value is `undefined`.')):null:e(o,r,i,s,l)}var o=n.bind(null,!1);return o.isRequired=n.bind(null,!0),o}function u(e){return l(function(t,n,o,a,r){var i=t[n],d=h(i);if(d!==e){var s=g(i);return new p('Invalid '+a+' `'+r+'` of type '+('`'+s+'` supplied to `'+o+'`, expected ')+('`'+e+'`.'))}return null})}function c(t){switch(typeof t){case'number':case'string':case'undefined':return!0;case'boolean':return!t;case'object':if(Array.isArray(t))return t.every(c);if(null===t||e(t))return!0;var o=n(t);if(o){var a=o.call(t),r;if(o!==t.entries){for(;!(r=a.next()).done;)if(!c(r.value))return!1;}else for(;!(r=a.next()).done;){var i=r.value;if(i&&!c(i[1]))return!1}}else return!1;return!0;default:return!1;}}function m(e,t){return'symbol'===e||'Symbol'===t['@@toStringTag']||'function'==typeof Symbol&&t instanceof Symbol}function h(e){var t=typeof e;return Array.isArray(e)?'array':e instanceof RegExp?'object':m(t,e)?'symbol':t}function g(e){if('undefined'==typeof e||null===e)return''+e;var t=h(e);if('object'===t){if(e instanceof Date)return'date';if(e instanceof RegExp)return'regexp'}return t}function f(e){var t=g(e);return'array'===t||'object'===t?'an '+t:'boolean'===t||'date'===t||'regexp'===t?'a '+t:t}function y(e){return e.constructor&&e.constructor.name?e.constructor.name:b}var _='function'==typeof Symbol&&Symbol.iterator,C='@@iterator',b='<<anonymous>>',E={array:u('array'),bool:u('boolean'),func:u('function'),number:u('number'),object:u('object'),string:u('string'),symbol:u('symbol'),any:function(){return l(o.thatReturnsNull)}(),arrayOf:function(e){return l(function(t,n,o,a,r){if('function'!=typeof e)return new p('Property `'+r+'` of component `'+o+'` has invalid PropType notation inside arrayOf.');var s=t[n];if(!Array.isArray(s)){var l=h(s);return new p('Invalid '+a+' `'+r+'` of type '+('`'+l+'` supplied to `'+o+'`, expected an array.'))}for(var u=0,i;u<s.length;u++)if(i=e(s,u,o,a,r+'['+u+']',d),i instanceof Error)return i;return null})},element:function(){return l(function(t,n,o,a,r){var i=t[n];if(!e(i)){var d=h(i);return new p('Invalid '+a+' `'+r+'` of type '+('`'+d+'` supplied to `'+o+'`, expected a single ReactElement.'))}return null})}(),instanceOf:function(e){return l(function(t,n,o,a,r){if(!(t[n]instanceof e)){var i=e.name||b,d=y(t[n]);return new p('Invalid '+a+' `'+r+'` of type '+('`'+d+'` supplied to `'+o+'`, expected ')+('instance of `'+i+'`.'))}return null})},node:function(){return l(function(e,t,n,o,a){return c(e[t])?null:new p('Invalid '+o+' `'+a+'` supplied to '+('`'+n+'`, expected a ReactNode.'))})}(),objectOf:function(e){return l(function(t,n,o,a,r){if('function'!=typeof e)return new p('Property `'+r+'` of component `'+o+'` has invalid PropType notation inside objectOf.');var i=t[n],s=h(i);if('object'!==s)return new p('Invalid '+a+' `'+r+'` of type '+('`'+s+'` supplied to `'+o+'`, expected an object.'));for(var l in i)if(i.hasOwnProperty(l)){var u=e(i,l,o,a,r+'.'+l,d);if(u instanceof Error)return u}return null})},oneOf:function(e){return Array.isArray(e)?l(function(t,n,o,a,r){for(var d=t[n],l=0;l<e.length;l++)if(s(d,e[l]))return null;var i=JSON.stringify(e);return new p('Invalid '+a+' `'+r+'` of value `'+d+'` '+('supplied to `'+o+'`, expected one of '+i+'.'))}):(void 0,o.thatReturnsNull)},oneOfType:function(e){if(!Array.isArray(e))return void 0,o.thatReturnsNull;for(var t=0,n;t<e.length;t++)if(n=e[t],'function'!=typeof n)return r(!1,'Invalid argument supplid to oneOfType. Expected an array of check functions, but received %s at index %s.',f(n),t),o.thatReturnsNull;return l(function(t,n,o,a,r){for(var s=0,i;s<e.length;s++)if(i=e[s],null==i(t,n,o,a,r,d))return null;return new p('Invalid '+a+' `'+r+'` supplied to '+('`'+o+'`.'))})},shape:function(e){return l(function(t,n,o,a,r){var i=t[n],s=h(i);if('object'!==s)return new p('Invalid '+a+' `'+r+'` of type `'+s+'` '+('supplied to `'+o+'`, expected `object`.'));for(var l in e){var u=e[l];if(u){var c=u(i,l,o,a,r+'.'+l,d);if(c)return c}}return null})}};return p.prototype=Error.prototype,E.checkPropTypes=i,E.PropTypes=E,E}},function(e){'use strict';e.exports=function(){}},function(e){'use strict';e.exports='15.6.1'},function(e,t,n){'use strict';var o=n(20),a=o.Component,r=n(7),i=r.isValidElement,d=n(21),s=n(46);e.exports=s(a,i,d)},function(e,t,n){'use strict';function o(e){return e}var a=n(3),r=n(14),i=n(0);var d='mixins',s;s={},e.exports=function(e,t,n){function s(e,t){var n=y.hasOwnProperty(t)?y[t]:null;E.hasOwnProperty(t)&&i('OVERRIDE_BASE'===n,'ReactClassInterface: You are attempting to override `%s` from your class specification. Ensure that your method names do not overlap with React methods.',t),e&&i('DEFINE_MANY'===n||'DEFINE_MANY_MERGED'===n,'ReactClassInterface: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.',t)}function p(e,n){if(!n){return}i('function'!=typeof n,'ReactClass: You\'re attempting to use a component class or function as a mixin. Instead, just use a regular object.'),i(!t(n),'ReactClass: You\'re attempting to use a component as a mixin. Instead, just use a regular object.');var o=e.prototype,a=o.__reactAutoBindPairs;for(var r in n.hasOwnProperty(d)&&_.mixins(e,n.mixins),n)if(n.hasOwnProperty(r)&&r!=d){var p=n[r],l=o.hasOwnProperty(r);if(s(l,r),_.hasOwnProperty(r))_[r](e,p);else{var u=y.hasOwnProperty(r),h='function'==typeof p&&!u&&!l&&!1!==n.autobind;if(h)a.push(r,p),o[r]=p;else if(l){var g=y[r];i(u&&('DEFINE_MANY_MERGED'===g||'DEFINE_MANY'===g),'ReactClass: Unexpected spec policy %s for key %s when mixing in component specs.',g,r),'DEFINE_MANY_MERGED'===g?o[r]=c(o[r],p):'DEFINE_MANY'===g&&(o[r]=m(o[r],p))}else o[r]=p,!1}}}function l(e,t){if(t)for(var n in t){var o=t[n];if(t.hasOwnProperty(n)){i(!(n in _),'ReactClass: You are attempting to define a reserved property, `%s`, that shouldn\'t be on the "statics" key. Define it as an instance property instead; it will still be accessible on the constructor.',n);i(!(n in e),'ReactClass: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.',n),e[n]=o}}}function u(e,t){for(var n in i(e&&t&&'object'==typeof e&&'object'==typeof t,'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.'),t)t.hasOwnProperty(n)&&(i(void 0===e[n],'mergeIntoWithNoDuplicateKeys(): Tried to merge two objects with the same key: `%s`. This conflict may be due to a mixin; in particular, this may be caused by two getInitialState() or getDefaultProps() methods returning objects with clashing keys.',n),e[n]=t[n]);return e}function c(e,t){return function(){var n=e.apply(this,arguments),o=t.apply(this,arguments);if(null==n)return o;if(null==o)return n;var a={};return u(a,n),u(a,o),a}}function m(e,t){return function(){e.apply(this,arguments),t.apply(this,arguments)}}function h(e,t){var n=t.bind(e);return n}function g(e){for(var t=e.__reactAutoBindPairs,n=0;n<t.length;n+=2){var o=t[n],a=t[n+1];e[o]=h(e,a)}}var f=[],y={mixins:'DEFINE_MANY',statics:'DEFINE_MANY',propTypes:'DEFINE_MANY',contextTypes:'DEFINE_MANY',childContextTypes:'DEFINE_MANY',getDefaultProps:'DEFINE_MANY_MERGED',getInitialState:'DEFINE_MANY_MERGED',getChildContext:'DEFINE_MANY_MERGED',render:'DEFINE_ONCE',componentWillMount:'DEFINE_MANY',componentDidMount:'DEFINE_MANY',componentWillReceiveProps:'DEFINE_MANY',shouldComponentUpdate:'DEFINE_ONCE',componentWillUpdate:'DEFINE_MANY',componentDidUpdate:'DEFINE_MANY',componentWillUnmount:'DEFINE_MANY',updateComponent:'OVERRIDE_BASE'},_={displayName:function(e,t){e.displayName=t},mixins:function(e,t){if(t)for(var n=0;n<t.length;n++)p(e,t[n])},childContextTypes:function(e,t){!1,e.childContextTypes=a({},e.childContextTypes,t)},contextTypes:function(e,t){!1,e.contextTypes=a({},e.contextTypes,t)},getDefaultProps:function(e,t){e.getDefaultProps=e.getDefaultProps?c(e.getDefaultProps,t):t},propTypes:function(e,t){!1,e.propTypes=a({},e.propTypes,t)},statics:function(e,t){l(e,t)},autobind:function(){}},C={componentDidMount:function(){this.__isMounted=!0}},b={componentWillUnmount:function(){this.__isMounted=!1}},E={replaceState:function(e,t){this.updater.enqueueReplaceState(this,e,t)},isMounted:function(){return!1,!!this.__isMounted}},v=function(){};return a(v.prototype,e.prototype,E),function(e){var t=o(function(e,o,a){!1,this.__reactAutoBindPairs.length&&g(this),this.props=e,this.context=o,this.refs=r,this.updater=a||n,this.state=null;var d=this.getInitialState?this.getInitialState():null;!1,i('object'==typeof d&&!Array.isArray(d),'%s.getInitialState(): must return an object or null',t.displayName||'ReactCompositeComponent'),this.state=d});for(var a in t.prototype=new v,t.prototype.constructor=t,t.prototype.__reactAutoBindPairs=[],f.forEach(p.bind(null,t)),p(t,C),p(t,e),p(t,b),t.getDefaultProps&&(t.defaultProps=t.getDefaultProps()),!1,i(t.prototype.render,'createClass(...): Class specification must implement a `render` method.'),!1,y)t.prototype[a]||(t.prototype[a]=null);return t}}},function(e,t,n){'use strict';var o=n(10),a=n(7),r=n(0);e.exports=function(e){return a.isValidElement(e)?void 0:o('143'),e}},function(e,t,n){'use strict';function o(){if(s)for(var e in p){var t=p[e],n=s.indexOf(e);if(-1<n?void 0:i('96',e),!l.plugins[n]){t.extractEvents?void 0:i('97',e),l.plugins[n]=t;var o=t.eventTypes;for(var r in o)a(o[r],t,r)?void 0:i('98',r,e)}}}function a(e,t,n){!l.eventNameDispatchConfigs.hasOwnProperty(n)?void 0:i('99',n),l.eventNameDispatchConfigs[n]=e;var o=e.phasedRegistrationNames;if(o){for(var a in o)if(o.hasOwnProperty(a)){var d=o[a];r(d,t,n)}return!0}return!!e.registrationName&&(r(e.registrationName,t,n),!0)}function r(e,t,n){!l.registrationNameModules[e]?void 0:i('100',e),l.registrationNameModules[e]=t,l.registrationNameDependencies[e]=t.eventTypes[n].dependencies}var i=n(2),d=n(0),s=null,p={},l={plugins:[],eventNameDispatchConfigs:{},registrationNameModules:{},registrationNameDependencies:{},possibleRegistrationNames:null,injectEventPluginOrder:function(e){!s?void 0:i('101'),s=Array.prototype.slice.call(e),o()},injectEventPluginsByName:function(e){var t=!1;for(var n in e)if(e.hasOwnProperty(n)){var a=e[n];p.hasOwnProperty(n)&&p[n]===a||(p[n]?i('102',n):void 0,p[n]=a,t=!0)}t&&o()},getPluginModuleForEvent:function(e){var t=e.dispatchConfig;if(t.registrationName)return l.registrationNameModules[t.registrationName]||null;if(void 0!==t.phasedRegistrationNames){var n=t.phasedRegistrationNames;for(var o in n)if(n.hasOwnProperty(o)){var a=l.registrationNameModules[n[o]];if(a)return a}}return null},_resetEventPlugins:function(){for(var e in s=null,p)p.hasOwnProperty(e)&&delete p[e];l.plugins.length=0;var t=l.eventNameDispatchConfigs;for(var n in t)t.hasOwnProperty(n)&&delete t[n];var o=l.registrationNameModules;for(var a in o)o.hasOwnProperty(a)&&delete o[a]}};e.exports=l},function(e,t,n){'use strict';function o(e,t,n,o){var a=e.type||'unknown-event';e.currentTarget=u.getNodeFromInstance(o),t?i.invokeGuardedCallbackWithCatch(a,n,e):i.invokeGuardedCallback(a,n,e),e.currentTarget=null}function a(e){var t=e._dispatchListeners,n=e._dispatchInstances;if(!1,Array.isArray(t)){for(var o=0;o<t.length&&!e.isPropagationStopped();o++)if(t[o](e,n[o]))return n[o];}else if(t&&t(e,n))return n;return null}var r=n(2),i=n(50),d=n(0),s=n(1),p,l;var u={isEndish:function(e){return'topMouseUp'===e||'topTouchEnd'===e||'topTouchCancel'===e},isMoveish:function(e){return'topMouseMove'===e||'topTouchMove'===e},isStartish:function(e){return'topMouseDown'===e||'topTouchStart'===e},executeDirectDispatch:function(e){var t=e._dispatchListeners,n=e._dispatchInstances;Array.isArray(t)?r('103'):void 0,e.currentTarget=t?u.getNodeFromInstance(n):null;var o=t?t(e):null;return e.currentTarget=null,e._dispatchListeners=null,e._dispatchInstances=null,o},executeDispatchesInOrder:function(e,t){var n=e._dispatchListeners,a=e._dispatchInstances;if(!1,Array.isArray(n))for(var r=0;r<n.length&&!e.isPropagationStopped();r++)o(e,t,n[r],a[r]);else n&&o(e,t,n,a);e._dispatchListeners=null,e._dispatchInstances=null},executeDispatchesInOrderStopAtTrue:function(e){var t=a(e);return e._dispatchInstances=null,e._dispatchListeners=null,t},hasDispatches:function(e){return!!e._dispatchListeners},getInstanceFromNode:function(e){return p.getInstanceFromNode(e)},getNodeFromInstance:function(e){return p.getNodeFromInstance(e)},isAncestor:function(e,t){return l.isAncestor(e,t)},getLowestCommonAncestor:function(e,t){return l.getLowestCommonAncestor(e,t)},getParentInstance:function(e){return l.getParentInstance(e)},traverseTwoPhase:function(e,t,n){return l.traverseTwoPhase(e,t,n)},traverseEnterLeave:function(e,t,n,o,a){return l.traverseEnterLeave(e,t,n,o,a)},injection:{injectComponentTree:function(e){p=e,!1},injectTreeTraversal:function(e){l=e,!1}}};e.exports=u},function(e){'use strict';function t(e,t,o){try{t(o)}catch(e){null==n&&(n=e)}}var n=null;e.exports={invokeGuardedCallback:t,invokeGuardedCallbackWithCatch:t,rethrowCaughtError:function(){if(n){var e=n;throw n=null,e}}}},function(e){'use strict';e.exports=function(e){var t=e.target||e.srcElement||window;return t.correspondingUseElement&&(t=t.correspondingUseElement),3===t.nodeType?t.parentNode:t}},function(e,t,n){'use strict';var o=n(6),a;/** + * Checks if an event is supported in the current execution environment. + * + * NOTE: This will not work correctly for non-generic events such as `change`, + * `reset`, `load`, `error`, and `select`. + * + * Borrows from Modernizr. + * + * @param {string} eventNameSuffix Event name, e.g. "click". + * @param {?boolean} capture Check if the capture phase is supported. + * @return {boolean} True if the event is supported. + * @internal + * @license Modernizr 3.0.0pre (Custom Build) | MIT + */o.canUseDOM&&(a=document.implementation&&document.implementation.hasFeature&&!0!==document.implementation.hasFeature('','')),e.exports=function(e,t){if(!o.canUseDOM||t&&!('addEventListener'in document))return!1;var n='on'+e,r=n in document;if(!r){var i=document.createElement('div');i.setAttribute(n,'return;'),r='function'==typeof i[n]}return!r&&a&&'wheel'===e&&(r=document.implementation.hasFeature('Events.wheel','3.0')),r}},function(e){'use strict';function t(e){var t=this,o=t.nativeEvent;if(o.getModifierState)return o.getModifierState(e);var a=n[e];return!!a&&!!o[a]}var n={Alt:'altKey',Control:'ctrlKey',Meta:'metaKey',Shift:'shiftKey'};e.exports=function(){return t}},function(e,t,n){'use strict';function o(e,t){return Array.isArray(t)&&(t=t[1]),t?t.nextSibling:e.firstChild}function a(e,t,n){p.insertTreeBefore(e,t,n)}function r(e,t,n){Array.isArray(t)?d(e,t[0],t[1],n):f(e,t,n)}function i(e,t){if(Array.isArray(t)){var n=t[1];t=t[0],s(e,t,n),e.removeChild(n)}e.removeChild(t)}function d(e,t,n,o){for(var a=t,r;r=a.nextSibling,f(e,a,o),a!==n;)a=r}function s(e,t,n){for(;;){var o=t.nextSibling;if(o===n)break;else e.removeChild(o)}}var p=n(18),l=n(118),u=n(4),c=n(9),m=n(56),h=n(31),g=n(75),f=m(function(e,t,n){e.insertBefore(t,n)}),y=l.dangerouslyReplaceNodeWithMarkup;e.exports={dangerouslyReplaceNodeWithMarkup:y,replaceDelimitedText:function(e,t,n){var o=e.parentNode,a=e.nextSibling;a===t?n&&f(o,document.createTextNode(n),a):n?(g(a,n),s(o,a,t)):s(o,e,t),!1},processUpdates:function(e,t){for(var n=0,d;n<t.length;n++)switch(d=t[n],d.type){case'INSERT_MARKUP':a(e,d.content,o(e,d.afterNode)),!1;break;case'MOVE_EXISTING':r(e,d.fromNode,o(e,d.afterNode)),!1;break;case'SET_MARKUP':h(e,d.content),!1;break;case'TEXT_CONTENT':g(e,d.content),!1;break;case'REMOVE_NODE':i(e,d.fromNode),!1;}}}},function(e){'use strict';e.exports={html:'http://www.w3.org/1999/xhtml',mathml:'http://www.w3.org/1998/Math/MathML',svg:'http://www.w3.org/2000/svg'}},function(e){'use strict';e.exports=function(e){return'undefined'!=typeof MSApp&&MSApp.execUnsafeLocalFunction?function(t,n,o,a){MSApp.execUnsafeLocalFunction(function(){return e(t,n,o,a)})}:e}},function(e,t,n){'use strict';function o(e){null==e.checkedLink||null==e.valueLink?void 0:d('87')}function a(e){o(e),null==e.value&&null==e.onChange?void 0:d('88')}function r(e){o(e),null==e.checked&&null==e.onChange?void 0:d('89')}function i(e){if(e){var t=e.getName();if(t)return' Check the render method of `'+t+'`.'}return''}var d=n(2),s=n(136),p=n(28),l=n(13),u=p(l.isValidElement),c=n(0),m=n(1),h={button:!0,checkbox:!0,image:!0,hidden:!0,radio:!0,reset:!0,submit:!0},g={value:function(e,t){return!e[t]||h[e.type]||e.onChange||e.readOnly||e.disabled?null:new Error('You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.')},checked:function(e,t){return!e[t]||e.onChange||e.readOnly||e.disabled?null:new Error('You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`.')},onChange:u.func},f={};e.exports={checkPropTypes:function(e,t,n){for(var o in g){if(g.hasOwnProperty(o))var a=g[o](t,o,e,'prop',null,s);if(a instanceof Error&&!(a.message in f)){f[a.message]=!0;var r=i(n);void 0}}},getValue:function(e){return e.valueLink?(a(e),e.valueLink.value):e.value},getChecked:function(e){return e.checkedLink?(r(e),e.checkedLink.value):e.checked},executeOnChange:function(e,t){return e.valueLink?(a(e),e.valueLink.requestChange(t.target.value)):e.checkedLink?(r(e),e.checkedLink.requestChange(t.target.checked)):e.onChange?e.onChange.call(void 0,t):void 0}}},function(e,t,n){'use strict';var o=n(2),a=n(0),r=!1,i={replaceNodeWithMarkup:null,processChildrenUpdates:null,injection:{injectEnvironment:function(e){!r?void 0:o('104'),i.replaceNodeWithMarkup=e.replaceNodeWithMarkup,i.processChildrenUpdates=e.processChildrenUpdates,r=!0}}};e.exports=i},function(e){'use strict';function t(e,t){return e===t?0!==e||0!==t||1/e==1/t:e!==e&&t!==t}var n=Object.prototype.hasOwnProperty;e.exports=function(e,o){if(t(e,o))return!0;if('object'!=typeof e||null===e||'object'!=typeof o||null===o)return!1;var a=Object.keys(e),r=Object.keys(o);if(a.length!==r.length)return!1;for(var d=0;d<a.length;d++)if(!n.call(o,a[d])||!t(e[a[d]],o[a[d]]))return!1;return!0}},function(e){'use strict';e.exports=function(e,t){var n=null===e||!1===e,o=null===t||!1===t;if(n||o)return n==o;var a=typeof e,r=typeof t;return'string'==a||'number'==a?'string'==r||'number'==r:'object'==r&&e.type===t.type&&e.key===t.key}},function(e){'use strict';e.exports={escape:function(e){var t=/[=:]/g,n={"=":'=0',":":'=2'},o=(''+e).replace(t,function(e){return n[e]});return'$'+o},unescape:function(e){var t=/(=0|=2)/g,n={"=0":'=',"=2":':'},o='.'===e[0]&&'$'===e[1]?e.substring(2):e.substring(1);return(''+o).replace(t,function(e){return n[e]})}}},function(e,t,n){'use strict';function o(e){l.enqueueUpdate(e)}function a(e){var t=typeof e;if('object'!=t)return t;var n=e.constructor&&e.constructor.name||t,o=Object.keys(e);return 0<o.length&&20>o.length?n+' (keys: '+o.join(', ')+')':n}function r(e){var t=s.get(e);if(!t){return null}return!1,t}var i=n(2),d=n(8),s=n(27),p=n(9),l=n(11),u=n(0),c=n(1),m={isMounted:function(e){var t=s.get(e);return!!t&&!!t._renderedComponent},enqueueCallback:function(e,t,n){m.validateCallback(t,n);var a=r(e);return a?void(a._pendingCallbacks?a._pendingCallbacks.push(t):a._pendingCallbacks=[t],o(a)):null},enqueueCallbackInternal:function(e,t){e._pendingCallbacks?e._pendingCallbacks.push(t):e._pendingCallbacks=[t],o(e)},enqueueForceUpdate:function(e){var t=r(e,'forceUpdate');t&&(t._pendingForceUpdate=!0,o(t))},enqueueReplaceState:function(e,t,n){var a=r(e,'replaceState');a&&(a._pendingStateQueue=[t],a._pendingReplaceState=!0,n!==void 0&&null!==n&&(m.validateCallback(n,'replaceState'),a._pendingCallbacks?a._pendingCallbacks.push(n):a._pendingCallbacks=[n]),o(a))},enqueueSetState:function(e,t){var n=r(e,'setState');if(n){var a=n._pendingStateQueue||(n._pendingStateQueue=[]);a.push(t),o(n)}},enqueueElementInternal:function(e,t,n){e._pendingElement=t,e._context=n,o(e)},validateCallback:function(e,t){!e||'function'==typeof e?void 0:i('122',t,a(e))}};e.exports=m},function(e,t,n){'use strict';var o=n(3),a=n(5),r=n(1);e.exports=a},function(e){'use strict';e.exports=function(e){var t=e.keyCode,n;return'charCode'in e?(n=e.charCode,0===n&&13===t&&(n=13)):n=t,32<=n||13===n?n:0}},,function(e){'use strict';e.exports={hasCachedChildNodes:1}},function(e,t,n){'use strict';var o=n(2),a=n(0);e.exports=function(e,t){return null==t?o('30'):void 0,null==e?t:Array.isArray(e)?Array.isArray(t)?(e.push.apply(e,t),e):(e.push(t),e):Array.isArray(t)?[e].concat(t):[e,t]}},function(e){'use strict';e.exports=function(e,t,n){Array.isArray(e)?e.forEach(t,n):e&&t.call(n,e)}},function(e,t,n){'use strict';var o=n(6),a=null;e.exports=function(){return!a&&o.canUseDOM&&(a='textContent'in document.documentElement?'textContent':'innerText'),a}},function(e,t,n){'use strict';function o(e,t){if(!(e instanceof t))throw new TypeError('Cannot call a class as a function')}var a=n(2),r=n(15),i=n(0),d=function(){function e(t){o(this,e),this._callbacks=null,this._contexts=null,this._arg=t}return e.prototype.enqueue=function(e,t){this._callbacks=this._callbacks||[],this._callbacks.push(e),this._contexts=this._contexts||[],this._contexts.push(t)},e.prototype.notifyAll=function(){var e=this._callbacks,t=this._contexts,n=this._arg;if(e&&t){e.length===t.length?void 0:a('24'),this._callbacks=null,this._contexts=null;for(var o=0;o<e.length;o++)e[o].call(t[o],n);e.length=0,t.length=0}},e.prototype.checkpoint=function(){return this._callbacks?this._callbacks.length:0},e.prototype.rollback=function(e){this._callbacks&&this._contexts&&(this._callbacks.length=e,this._contexts.length=e)},e.prototype.reset=function(){this._callbacks=null,this._contexts=null},e.prototype.destructor=function(){this.reset()},e}();e.exports=r.addPoolingTo(d)},function(e){'use strict';e.exports={logTopLevelRenders:!1}},function(e,t,n){'use strict';function o(e){var t=e.type,n=e.nodeName;return n&&'input'===n.toLowerCase()&&('checkbox'===t||'radio'===t)}function a(e){return e._wrapperState.valueTracker}function r(e,t){e._wrapperState.valueTracker=t}function i(e){delete e._wrapperState.valueTracker}function d(e){var t;return e&&(t=o(e)?''+e.checked:e.value),t}var s=n(4),p={_getTrackerFromNode:function(e){return a(s.getInstanceFromNode(e))},track:function(e){if(!a(e)){var t=s.getNodeFromInstance(e),n=o(t)?'checked':'value',d=Object.getOwnPropertyDescriptor(t.constructor.prototype,n),p=''+t[n];t.hasOwnProperty(n)||'function'!=typeof d.get||'function'!=typeof d.set||(Object.defineProperty(t,n,{enumerable:d.enumerable,configurable:!0,get:function(){return d.get.call(this)},set:function(e){p=''+e,d.set.call(this,e)}}),r(e,{getValue:function(){return p},setValue:function(e){p=''+e},stopTracking:function(){i(e),delete t[n]}}))}},updateValueIfChanged:function(e){if(!e)return!1;var t=a(e);if(!t)return p.track(e),!0;var n=t.getValue(),o=d(s.getNodeFromInstance(e));return o!==n&&(t.setValue(o),!0)},stopTracking:function(e){var t=a(e);t&&t.stopTracking()}};e.exports=p},function(e){'use strict';var t={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};e.exports=function(e){var n=e&&e.nodeName&&e.nodeName.toLowerCase();return'input'===n?!!t[e.type]:!('textarea'!==n)}},function(e){'use strict';var t={currentScrollLeft:0,currentScrollTop:0,refreshScrollValues:function(e){t.currentScrollLeft=e.x,t.currentScrollTop=e.y}};e.exports=t},function(e,t,n){'use strict';var o=n(6),a=n(32),r=n(31),i=function(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&3===n.nodeType)return void(n.nodeValue=t)}e.textContent=t};o.canUseDOM&&!('textContent'in document.documentElement)&&(i=function(e,t){return 3===e.nodeType?void(e.nodeValue=t):void r(e,a(t))}),e.exports=i},function(e){'use strict';e.exports=function(e){try{e.focus()}catch(t){}}},function(e){'use strict';function t(e,t){return e+t.charAt(0).toUpperCase()+t.substring(1)}var n={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},o=['Webkit','ms','Moz','O'];Object.keys(n).forEach(function(e){o.forEach(function(o){n[t(o,e)]=n[e]})});e.exports={isUnitlessNumber:n,shorthandPropertyExpansions:{background:{backgroundAttachment:!0,backgroundColor:!0,backgroundImage:!0,backgroundPositionX:!0,backgroundPositionY:!0,backgroundRepeat:!0},backgroundPosition:{backgroundPositionX:!0,backgroundPositionY:!0},border:{borderWidth:!0,borderStyle:!0,borderColor:!0},borderBottom:{borderBottomWidth:!0,borderBottomStyle:!0,borderBottomColor:!0},borderLeft:{borderLeftWidth:!0,borderLeftStyle:!0,borderLeftColor:!0},borderRight:{borderRightWidth:!0,borderRightStyle:!0,borderRightColor:!0},borderTop:{borderTopWidth:!0,borderTopStyle:!0,borderTopColor:!0},font:{fontStyle:!0,fontVariant:!0,fontWeight:!0,fontSize:!0,lineHeight:!0,fontFamily:!0},outline:{outlineWidth:!0,outlineStyle:!0,outlineColor:!0}}}},function(e,t,n){'use strict';function o(e){return!!c.hasOwnProperty(e)||!u.hasOwnProperty(e)&&(l.test(e)?(c[e]=!0,!0):(u[e]=!0,void 0,!1))}function a(e,t){return null==t||e.hasBooleanValue&&!t||e.hasNumericValue&&isNaN(t)||e.hasPositiveNumericValue&&1>t||e.hasOverloadedBooleanValue&&!1===t}var r=n(16),i=n(4),d=n(9),s=n(132),p=n(1),l=new RegExp('^['+r.ATTRIBUTE_NAME_START_CHAR+']['+r.ATTRIBUTE_NAME_CHAR+']*$'),u={},c={},m={createMarkupForID:function(e){return r.ID_ATTRIBUTE_NAME+'='+s(e)},setAttributeForID:function(e,t){e.setAttribute(r.ID_ATTRIBUTE_NAME,t)},createMarkupForRoot:function(){return r.ROOT_ATTRIBUTE_NAME+'=""'},setAttributeForRoot:function(e){e.setAttribute(r.ROOT_ATTRIBUTE_NAME,'')},createMarkupForProperty:function(e,t){var n=r.properties.hasOwnProperty(e)?r.properties[e]:null;if(n){if(a(n,t))return'';var o=n.attributeName;return n.hasBooleanValue||n.hasOverloadedBooleanValue&&!0===t?o+'=""':o+'='+s(t)}return r.isCustomAttribute(e)?null==t?'':e+'='+s(t):null},createMarkupForCustomAttribute:function(e,t){return o(e)&&null!=t?e+'='+s(t):''},setValueForProperty:function(e,t,n){var o=r.properties.hasOwnProperty(t)?r.properties[t]:null;if(o){var i=o.mutationMethod;if(i)i(e,n);else{if(a(o,n))return void this.deleteValueForProperty(e,t);if(o.mustUseProperty)e[o.propertyName]=n;else{var d=o.attributeName,s=o.attributeNamespace;s?e.setAttributeNS(s,d,''+n):o.hasBooleanValue||o.hasOverloadedBooleanValue&&!0===n?e.setAttribute(d,''):e.setAttribute(d,''+n)}}}else if(r.isCustomAttribute(t))return void m.setValueForAttribute(e,t,n)},setValueForAttribute:function(e,t,n){if(o(t)){null==n?e.removeAttribute(t):e.setAttribute(t,''+n)}},deleteValueForAttribute:function(e,t){e.removeAttribute(t),!1},deleteValueForProperty:function(e,t){var n=r.properties.hasOwnProperty(t)?r.properties[t]:null;if(n){var o=n.mutationMethod;if(o)o(e,void 0);else if(n.mustUseProperty){var a=n.propertyName;e[a]=!n.hasBooleanValue&&''}else e.removeAttribute(n.attributeName)}else r.isCustomAttribute(t)&&e.removeAttribute(t)}};e.exports=m},function(e,t,n){'use strict';function o(){if(this._rootNodeID&&this._wrapperState.pendingUpdate){this._wrapperState.pendingUpdate=!1;var e=this._currentElement.props,t=d.getValue(e);null!=t&&a(this,!!e.multiple,t)}}function a(e,t,n){var o=s.getNodeFromInstance(e).options,a,r;if(t){for(a={},r=0;r<n.length;r++)a[''+n[r]]=!0;for(r=0;r<o.length;r++){var i=a.hasOwnProperty(o[r].value);o[r].selected!==i&&(o[r].selected=i)}}else{for(a=''+n,r=0;r<o.length;r++)if(o[r].value===a)return void(o[r].selected=!0);o.length&&(o[0].selected=!0)}}function r(e){var t=this._currentElement.props,n=d.executeOnChange(t,e);return this._rootNodeID&&(this._wrapperState.pendingUpdate=!0),p.asap(o,this),n}var i=n(3),d=n(57),s=n(4),p=n(11),l=n(1),u=!1,c=!1,m=['value','defaultValue'];e.exports={getHostProps:function(e,t){return i({},t,{onChange:e._wrapperState.onChange,value:void 0})},mountWrapper:function(e,t){var n=d.getValue(t);e._wrapperState={pendingUpdate:!1,initialValue:null==n?t.defaultValue:n,listeners:null,onChange:r.bind(e),wasMultiple:!!t.multiple},t.value===void 0||t.defaultValue===void 0||c||(void 0,c=!0)},getSelectValueContext:function(e){return e._wrapperState.initialValue},postUpdateWrapper:function(e){var t=e._currentElement.props;e._wrapperState.initialValue=void 0;var n=e._wrapperState.wasMultiple;e._wrapperState.wasMultiple=!!t.multiple;var o=d.getValue(t);null==o?n!==!!t.multiple&&(null==t.defaultValue?a(e,!!t.multiple,t.multiple?[]:''):a(e,!!t.multiple,t.defaultValue)):(e._wrapperState.pendingUpdate=!1,a(e,!!t.multiple,o))}}},function(e){function t(){throw new Error('setTimeout has not been defined')}function n(){throw new Error('clearTimeout has not been defined')}function o(e){if(l===setTimeout)return setTimeout(e,0);if((l===t||!l)&&setTimeout)return l=setTimeout,setTimeout(e,0);try{return l(e,0)}catch(t){try{return l.call(null,e,0)}catch(t){return l.call(this,e,0)}}}function a(e){if(u===clearTimeout)return clearTimeout(e);if((u===n||!u)&&clearTimeout)return u=clearTimeout,clearTimeout(e);try{return u(e)}catch(t){try{return u.call(null,e)}catch(t){return u.call(this,e)}}}function r(){m&&g&&(m=!1,g.length?c=g.concat(c):h=-1,c.length&&d())}function d(){if(!m){var e=o(r);m=!0;for(var t=c.length;t;){for(g=c,c=[];++h<t;)g&&g[h].run();h=-1,t=c.length}g=null,m=!1,a(e)}}function s(e,t){this.fun=e,this.array=t}function i(){}var p=e.exports={},l,u;(function(){try{l='function'==typeof setTimeout?setTimeout:t}catch(n){l=t}try{u='function'==typeof clearTimeout?clearTimeout:n}catch(t){u=n}})();var c=[],m=!1,h=-1,g;p.nextTick=function(e){var t=Array(arguments.length-1);if(1<arguments.length)for(var n=1;n<arguments.length;n++)t[n-1]=arguments[n];c.push(new s(e,t)),1!==c.length||m||o(d)},s.prototype.run=function(){this.fun.apply(null,this.array)},p.title='browser',p.browser=!0,p.env={},p.argv=[],p.version='',p.versions={},p.on=i,p.addListener=i,p.once=i,p.off=i,p.removeListener=i,p.removeAllListeners=i,p.emit=i,p.prependListener=i,p.prependOnceListener=i,p.listeners=function(){return[]},p.binding=function(){throw new Error('process.binding is not supported')},p.cwd=function(){return'/'},p.chdir=function(){throw new Error('process.chdir is not supported')},p.umask=function(){return 0}},function(e,t,n){'use strict';function o(e){if(e){var t=e.getName();if(t)return' Check the render method of `'+t+'`.'}return''}function a(e){return'function'==typeof e&&'undefined'!=typeof e.prototype&&'function'==typeof e.prototype.mountComponent&&'function'==typeof e.prototype.receiveComponent}function r(e){var t;if(null===e||!1===e)t=p.create(r);else if('object'==typeof e){var n=e,d=n.type;if('function'!=typeof d&&'string'!=typeof d){var s='';!1,s+=o(n._owner),i('130',null==d?d:typeof d,s)}'string'==typeof n.type?t=l.createInternalComponent(n):a(n.type)?(t=new n.type(n),!t.getHostNode&&(t.getHostNode=t.getNativeNode)):t=new h(n)}else'string'==typeof e||'number'==typeof e?t=l.createInstanceForText(e):i('131',typeof e);return!1,t._mountIndex=0,t._mountImage=null,!1,!1,t}var i=n(2),d=n(3),s=n(141),p=n(83),l=n(84),u=n(142),c=n(0),m=n(1),h=function(e){this.construct(e)};d(h.prototype,s,{_instantiateReactComponent:r}),e.exports=r},function(e,t,n){'use strict';var o=n(2),a=n(13),r=n(0),i={HOST:0,COMPOSITE:1,EMPTY:2,getType:function(e){if(null===e||!1===e)return i.EMPTY;return a.isValidElement(e)?'function'==typeof e.type?i.COMPOSITE:i.HOST:void o('26',e)}};e.exports=i},function(e){'use strict';var t={create:function(e){return n(e)}},n;t.injection={injectEmptyComponentFactory:function(e){n=e}},e.exports=t},function(e,t,n){'use strict';var o=n(2),a=n(0),r=null,i=null;e.exports={createInternalComponent:function(e){return r?void 0:o('111',e.type),new r(e)},createInstanceForText:function(e){return new i(e)},isTextComponent:function(e){return e instanceof i},injection:{injectGenericComponentClass:function(e){r=e},injectTextComponentClass:function(e){i=e}}}},function(e,t,n){'use strict';function o(e,t){return e&&'object'==typeof e&&null!=e.key?l.escape(e.key):t.toString(36)}function a(e,t,n,p){var u=typeof e;if(('undefined'==u||'boolean'==u)&&(e=null),null===e||'string'==u||'number'==u||'object'==u&&e.$$typeof===d)return n(p,e,''===t?c+o(e,0):t),1;var h=0,g=''===t?c:t+m,f,y;if(Array.isArray(e))for(var _=0;_<e.length;_++)f=e[_],y=g+o(f,_),h+=a(f,y,n,p);else{var i=s(e);if(i){var C=i.call(e),b;if(i!==e.entries)for(var E=0;!(b=C.next()).done;)f=b.value,y=g+o(f,E++),h+=a(f,y,n,p);else for(var v;!(b=C.next()).done;)v=b.value,v&&(f=v[1],y=g+l.escape(v[0])+m+o(f,0),h+=a(f,y,n,p))}else if('object'==u){var x='',N=e+'';r('31','[object Object]'===N?'object with keys {'+Object.keys(e).join(', ')+'}':N,x)}}return h}var r=n(2),i=n(8),d=n(143),s=n(144),p=n(0),l=n(61),u=n(1),c='.',m=':';e.exports=function(e,t,n){return null==e?0:a(e,'',t,n)}},function(e,t,n){'use strict';function o(e){var t=Function.prototype.toString,n=Object.prototype.hasOwnProperty,o=RegExp('^'+t.call(n).replace(/[\\^$.*+?()[\]{}|]/g,'\\$&').replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,'$1.*?')+'$');try{var a=t.call(e);return o.test(a)}catch(e){return!1}}function a(e){var t=h(e);if(t){var n=t.childIDs;g(e),n.forEach(a)}}function r(e,t,n){return'\n in '+(e||'Unknown')+(t?' (at '+t.fileName.replace(/^.*[\\\/]/,'')+':'+t.lineNumber+')':n?' (created by '+n+')':'')}function i(e){return null==e?'#empty':'string'==typeof e||'number'==typeof e?'#text':'string'==typeof e.type?e.type:e.type.displayName||e.type.name||'Unknown'}function d(e){var t=P.getDisplayName(e),n=P.getElement(e),o=P.getOwnerID(e),a;return o&&(a=P.getDisplayName(o)),void 0,r(t,n&&n._source,a)}var s=n(10),p=n(8),l=n(0),u=n(1),c='function'==typeof Array.from&&'function'==typeof Map&&o(Map)&&null!=Map.prototype&&'function'==typeof Map.prototype.keys&&o(Map.prototype.keys)&&'function'==typeof Set&&o(Set)&&null!=Set.prototype&&'function'==typeof Set.prototype.keys&&o(Set.prototype.keys),m,h,g,f,y,_,C;if(c){var b=new Map,E=new Set;m=function(e,t){b.set(e,t)},h=function(e){return b.get(e)},g=function(e){b['delete'](e)},f=function(){return Array.from(b.keys())},y=function(e){E.add(e)},_=function(e){E['delete'](e)},C=function(){return Array.from(E.keys())}}else{var v={},x={},N=function(e){return'.'+e},T=function(e){return parseInt(e.substr(1),10)};m=function(e,t){var n=N(e);v[n]=t},h=function(e){var t=N(e);return v[t]},g=function(e){var t=N(e);delete v[t]},f=function(){return Object.keys(v).map(T)},y=function(e){var t=N(e);x[t]=!0},_=function(e){var t=N(e);delete x[t]},C=function(){return Object.keys(x).map(T)}}var k=[],P={onSetChildren:function(e,t){var n=h(e);n?void 0:s('144'),n.childIDs=t;for(var o=0;o<t.length;o++){var a=t[o],r=h(a);r?void 0:s('140'),null!=r.childIDs||'object'!=typeof r.element||null==r.element?void 0:s('141'),r.isMounted?void 0:s('71'),null==r.parentID&&(r.parentID=e),r.parentID===e?void 0:s('142',a,r.parentID,e)}},onBeforeMountComponent:function(e,t,n){m(e,{element:t,parentID:n,text:null,childIDs:[],isMounted:!1,updateCount:0})},onBeforeUpdateComponent:function(e,t){var n=h(e);n&&n.isMounted&&(n.element=t)},onMountComponent:function(e){var t=h(e);t?void 0:s('144'),t.isMounted=!0;var n=0===t.parentID;n&&y(e)},onUpdateComponent:function(e){var t=h(e);t&&t.isMounted&&t.updateCount++},onUnmountComponent:function(e){var t=h(e);if(t){t.isMounted=!1;var n=0===t.parentID;n&&_(e)}k.push(e)},purgeUnmountedComponents:function(){if(!P._preventPurging){for(var e=0,t;e<k.length;e++)t=k[e],a(t);k.length=0}},isMounted:function(e){var t=h(e);return!!t&&t.isMounted},getCurrentStackAddendum:function(e){var t='';if(e){var n=i(e),o=e._owner;t+=r(n,e._source,o&&o.getName())}var a=p.current,d=a&&a._debugID;return t+=P.getStackAddendumByID(d),t},getStackAddendumByID:function(e){for(var t='';e;)t+=d(e),e=P.getParentID(e);return t},getChildIDs:function(e){var t=h(e);return t?t.childIDs:[]},getDisplayName:function(e){var t=P.getElement(e);return t?i(t):null},getElement:function(e){var t=h(e);return t?t.element:null},getOwnerID:function(e){var t=P.getElement(e);return t&&t._owner?t._owner._debugID:null},getParentID:function(e){var t=h(e);return t?t.parentID:null},getSource:function(e){var t=h(e),n=t?t.element:null,o=null==n?null:n._source;return o},getText:function(e){var t=P.getElement(e);return'string'==typeof t?t:'number'==typeof t?''+t:null},getUpdateCount:function(e){var t=h(e);return t?t.updateCount:0},getRootIDs:C,getRegisteredIDs:f,pushNonStandardWarningStack:function(e,t){if('function'==typeof console.reactStack){var n=[],o=p.current,a=o&&o._debugID;try{for(e&&n.push({name:a?P.getDisplayName(a):null,fileName:t?t.fileName:null,lineNumber:t?t.lineNumber:null});a;){var r=P.getElement(a),i=P.getParentID(a),d=P.getOwnerID(a),s=d?P.getDisplayName(d):null,l=r&&r._source;n.push({name:s,fileName:l?l.fileName:null,lineNumber:l?l.lineNumber:null}),a=i}}catch(e){}console.reactStack(n)}},popNonStandardWarningStack:function(){'function'!=typeof console.reactStackEnd||console.reactStackEnd()}};e.exports=P},function(e,t,n){'use strict';var o=n(5);e.exports={listen:function(e,t,n){return e.addEventListener?(e.addEventListener(t,n,!1),{remove:function(){e.removeEventListener(t,n,!1)}}):e.attachEvent?(e.attachEvent('on'+t,n),{remove:function(){e.detachEvent('on'+t,n)}}):void 0},capture:function(e,t,n){return e.addEventListener?(e.addEventListener(t,n,!0),{remove:function(){e.removeEventListener(t,n,!0)}}):(!1,{remove:o})},registerDefault:function(){}}},function(e,t,n){'use strict';function o(e){return r(document.documentElement,e)}var a=n(156),r=n(158),i=n(76),d=n(89),s={hasSelectionCapabilities:function(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&('input'===t&&'text'===e.type||'textarea'===t||'true'===e.contentEditable)},getSelectionInformation:function(){var e=d();return{focusedElem:e,selectionRange:s.hasSelectionCapabilities(e)?s.getSelection(e):null}},restoreSelection:function(e){var t=d(),n=e.focusedElem,a=e.selectionRange;t!==n&&o(n)&&(s.hasSelectionCapabilities(n)&&s.setSelection(n,a),i(n))},getSelection:function(e){var t;if('selectionStart'in e)t={start:e.selectionStart,end:e.selectionEnd};else if(document.selection&&e.nodeName&&'input'===e.nodeName.toLowerCase()){var n=document.selection.createRange();n.parentElement()===e&&(t={start:-n.moveStart('character',-e.value.length),end:-n.moveEnd('character',-e.value.length)})}else t=a.getOffsets(e);return t||{start:0,end:0}},setSelection:function(e,t){var n=t.start,o=t.end;if(void 0===o&&(o=n),'selectionStart'in e)e.selectionStart=n,e.selectionEnd=Math.min(o,e.value.length);else if(document.selection&&e.nodeName&&'input'===e.nodeName.toLowerCase()){var r=e.createTextRange();r.collapse(!0),r.moveStart('character',n),r.moveEnd('character',o-n),r.select()}else a.setOffsets(e,t)}};e.exports=s},function(e){'use strict';e.exports=function(e){if(e=e||('undefined'==typeof document?void 0:document),'undefined'==typeof e)return null;try{return e.activeElement||e.body}catch(t){return e.body}}},function(e,t,n){'use strict';function o(e,t){for(var n=Math.min(e.length,t.length),o=0;o<n;o++)if(e.charAt(o)!==t.charAt(o))return o;return e.length===t.length?-1:n}function a(e){return e?e.nodeType===F?e.documentElement:e.firstChild:null}function r(e){return e.getAttribute&&e.getAttribute(O)||''}function i(e,t,n,o,a){var r;if(v.logTopLevelRenders){var i=e._currentElement.props.child,d=i.type;r='React mount: '+('string'==typeof d?d:d.displayName||d.name),console.time(r)}var s=k.mountComponent(e,n,null,b(e,t),a,0);r&&console.timeEnd(r),e._renderedComponent._topLevelWrapper=e,H._mountImageIntoNode(s,t,e,o,n)}function d(e,t,n,o){var a=I.ReactReconcileTransaction.getPooled(!n&&E.useCreateElement);a.perform(i,null,e,t,a,n,o),I.ReactReconcileTransaction.release(a)}function s(e,t,n){for(!1,k.unmountComponent(e,n),!1,t.nodeType===F&&(t=t.documentElement);t.lastChild;)t.removeChild(t.lastChild)}function p(e){var t=a(e);if(t){var n=C.getInstanceFromNode(t);return!!(n&&n._hostParent)}}function l(e){return!!(e&&(e.nodeType===U||e.nodeType===F||e.nodeType===V))}function u(e){var t=a(e),n=t&&C.getInstanceFromNode(t);return n&&!n._hostParent?n:null}function c(e){var t=u(e);return t?t._hostContainerInfo._topLevelWrapper:null}var m=n(2),h=n(18),g=n(16),f=n(13),y=n(33),_=n(8),C=n(4),b=n(173),E=n(174),v=n(71),x=n(27),N=n(9),T=n(175),k=n(17),P=n(62),I=n(11),M=n(14),S=n(81),w=n(0),R=n(31),D=n(60),A=n(1),O=g.ID_ATTRIBUTE_NAME,L=g.ROOT_ATTRIBUTE_NAME,U=1,F=9,V=11,j={},B=1,W=function(){this.rootID=B++};W.prototype.isReactComponent={},!1,W.prototype.render=function(){return this.props.child},W.isReactTopLevelWrapper=!0;var H={TopLevelWrapper:W,_instancesByReactRootID:j,scrollMonitor:function(e,t){t()},_updateRootComponent:function(e,t,n,o,a){return H.scrollMonitor(o,function(){P.enqueueElementInternal(e,t,n),a&&P.enqueueCallbackInternal(e,a)}),e},_renderNewRootComponent:function(e,t,n,o){void 0,l(t)?void 0:m('37'),y.ensureScrollValueMonitoring();var a=S(e,!1);I.batchedUpdates(d,a,t,n,o);var r=a._instance.rootID;return j[r]=a,a},renderSubtreeIntoContainer:function(e,t,n,o){return null!=e&&x.has(e)?void 0:m('38'),H._renderSubtreeIntoContainer(e,t,n,o)},_renderSubtreeIntoContainer:function(e,t,n,o){P.validateCallback(o,'ReactDOM.render'),f.isValidElement(t)?void 0:m('39','string'==typeof t?' Instead of passing a string like \'div\', pass React.createElement(\'div\') or <div />.':'function'==typeof t?' Instead of passing a class like Foo, pass React.createElement(Foo) or <Foo />.':null!=t&&void 0!==t.props?' This may be caused by unintentionally loading two independent copies of React.':''),void 0;var i=f.createElement(W,{child:t}),d;if(e){var s=x.get(e);d=s._processChildContext(s._context)}else d=M;var l=c(n);if(l){var u=l._currentElement,h=u.props.child;if(D(h,t)){var g=l._renderedComponent.getPublicInstance(),y=o&&function(){o.call(g)};return H._updateRootComponent(l,i,d,n,y),g}H.unmountComponentAtNode(n)}var _=a(n),C=_&&!!r(_),b=p(n),E=H._renderNewRootComponent(i,n,C&&!l&&!b,d)._renderedComponent.getPublicInstance();return o&&o.call(E),E},render:function(e,t,n){return H._renderSubtreeIntoContainer(null,e,t,n)},unmountComponentAtNode:function(e){void 0,l(e)?void 0:m('40'),!1;var t=c(e);if(!t){var n=p(e),o=1===e.nodeType&&e.hasAttribute(L);return!1,!1}return delete j[t._instance.rootID],I.batchedUpdates(s,t,e,!1),!0},_mountImageIntoNode:function(e,t,n,r,i){if(l(t)?void 0:m('41'),r){var d=a(t);if(T.canReuseMarkup(e,d))return void C.precacheNode(n,d);var s=d.getAttribute(T.CHECKSUM_ATTR_NAME);d.removeAttribute(T.CHECKSUM_ATTR_NAME);var p=d.outerHTML;d.setAttribute(T.CHECKSUM_ATTR_NAME,s);var u=e,c=o(u,p),g=' (client) '+u.substring(c-20,c+20)+'\n (server) '+p.substring(c-20,c+20);t.nodeType===F?m('42',g):void 0,!1}if(t.nodeType===F?m('43'):void 0,i.useCreateElement){for(;t.lastChild;)t.removeChild(t.lastChild);h.insertTreeBefore(t,e,null)}else R(t,e),C.precacheNode(n,t.firstChild)}};e.exports=H},function(e,t,n){'use strict';var o=n(82);e.exports=function(e){for(var t;(t=e._renderedNodeType)===o.COMPOSITE;)e=e._renderedComponent;if(t===o.HOST)return e._renderedComponent;return t===o.EMPTY?null:void 0}},,,,,,,,,,,,function(e,t,n){'use strict';e.exports=n(104)},function(e,t,n){'use strict';var o=n(4),a=n(105),r=n(90),i=n(17),d=n(11),s=n(177),p=n(178),l=n(91),u=n(179),c=n(1);a.inject();var m={findDOMNode:p,render:r.render,unmountComponentAtNode:r.unmountComponentAtNode,version:s,unstable_batchedUpdates:d.batchedUpdates,unstable_renderSubtreeIntoContainer:u};'undefined'!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&'function'==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject&&__REACT_DEVTOOLS_GLOBAL_HOOK__.inject({ComponentTree:{getClosestInstanceFromNode:o.getClosestInstanceFromNode,getNodeFromInstance:function(e){return e._renderedComponent&&(e=l(e)),e?o.getNodeFromInstance(e):null}},Mount:r,Reconciler:i});e.exports=m},function(e,t,n){'use strict';var o=n(106),a=n(107),r=n(111),i=n(114),d=n(115),s=n(116),p=n(117),l=n(123),u=n(4),c=n(148),m=n(149),h=n(150),g=n(151),f=n(152),y=n(154),_=n(155),C=n(161),b=n(162),E=n(163),v=!1;e.exports={inject:function(){v||(v=!0,y.EventEmitter.injectReactEventListener(f),y.EventPluginHub.injectEventPluginOrder(i),y.EventPluginUtils.injectComponentTree(u),y.EventPluginUtils.injectTreeTraversal(m),y.EventPluginHub.injectEventPluginsByName({SimpleEventPlugin:E,EnterLeaveEventPlugin:d,ChangeEventPlugin:r,SelectEventPlugin:b,BeforeInputEventPlugin:a}),y.HostComponent.injectGenericComponentClass(l),y.HostComponent.injectTextComponentClass(h),y.DOMProperty.injectDOMPropertyConfig(o),y.DOMProperty.injectDOMPropertyConfig(s),y.DOMProperty.injectDOMPropertyConfig(C),y.EmptyComponent.injectEmptyComponentFactory(function(e){return new c(e)}),y.Updates.injectReconcileTransaction(_),y.Updates.injectBatchingStrategy(g),y.Component.injectEnvironment(p))}}},function(e){'use strict';e.exports={Properties:{"aria-current":0,"aria-details":0,"aria-disabled":0,"aria-hidden":0,"aria-invalid":0,"aria-keyshortcuts":0,"aria-label":0,"aria-roledescription":0,"aria-autocomplete":0,"aria-checked":0,"aria-expanded":0,"aria-haspopup":0,"aria-level":0,"aria-modal":0,"aria-multiline":0,"aria-multiselectable":0,"aria-orientation":0,"aria-placeholder":0,"aria-pressed":0,"aria-readonly":0,"aria-required":0,"aria-selected":0,"aria-sort":0,"aria-valuemax":0,"aria-valuemin":0,"aria-valuenow":0,"aria-valuetext":0,"aria-atomic":0,"aria-busy":0,"aria-live":0,"aria-relevant":0,"aria-dropeffect":0,"aria-grabbed":0,"aria-activedescendant":0,"aria-colcount":0,"aria-colindex":0,"aria-colspan":0,"aria-controls":0,"aria-describedby":0,"aria-errormessage":0,"aria-flowto":0,"aria-labelledby":0,"aria-owns":0,"aria-posinset":0,"aria-rowcount":0,"aria-rowindex":0,"aria-rowspan":0,"aria-setsize":0},DOMAttributeNames:{},DOMPropertyNames:{}}},function(e,t,n){'use strict';function o(e){return(e.ctrlKey||e.altKey||e.metaKey)&&!(e.ctrlKey&&e.altKey)}function a(e){return'topCompositionStart'===e?T.compositionStart:'topCompositionEnd'===e?T.compositionEnd:'topCompositionUpdate'===e?T.compositionUpdate:void 0}function r(e,t){return'topKeyDown'===e&&t.keyCode===_}function i(e,t){return'topKeyUp'===e?-1!==y.indexOf(t.keyCode):'topKeyDown'===e?t.keyCode!==_:'topKeyPress'==e||'topMouseDown'==e||'topBlur'==e}function d(e){var t=e.detail;return'object'==typeof t&&'data'in t?t.data:null}function s(e,t,n,o){var s,p;if(C?s=a(e):P?i(e,n)&&(s=T.compositionEnd):r(e,n)&&(s=T.compositionStart),!s)return null;v&&(P||s!==T.compositionStart?s===T.compositionEnd&&P&&(p=P.getData()):P=h.getPooled(o));var l=g.getPooled(s,t,n,o);if(p)l.data=p;else{var u=d(n);null!==u&&(l.data=u)}return c.accumulateTwoPhaseDispatches(l),l}function p(e,t){switch(e){case'topCompositionEnd':return d(t);case'topKeyPress':var n=t.which;return n===x?(k=!0,N):null;case'topTextInput':var o=t.data;return o===N&&k?null:o;default:return null;}}function l(e,t){if(P){if('topCompositionEnd'===e||!C&&i(e,t)){var n=P.getData();return h.release(P),P=null,n}return null}return'topPaste'===e?null:'topKeyPress'===e?t.which&&!o(t)?String.fromCharCode(t.which):null:'topCompositionEnd'===e?v?null:t.data:null}function u(e,t,n,o){var a;if(a=E?p(e,n):l(e,n),!a)return null;var r=f.getPooled(T.beforeInput,t,n,o);return r.data=a,c.accumulateTwoPhaseDispatches(r),r}var c=n(24),m=n(6),h=n(108),g=n(109),f=n(110),y=[9,13,27,32],_=229,C=m.canUseDOM&&'CompositionEvent'in window,b=null;m.canUseDOM&&'documentMode'in document&&(b=document.documentMode);var E=m.canUseDOM&&'TextEvent'in window&&!b&&!function(){var e=window.opera;return'object'==typeof e&&'function'==typeof e.version&&12>=parseInt(e.version(),10)}(),v=m.canUseDOM&&(!C||b&&8<b&&11>=b),x=32,N=' ',T={beforeInput:{phasedRegistrationNames:{bubbled:'onBeforeInput',captured:'onBeforeInputCapture'},dependencies:['topCompositionEnd','topKeyPress','topTextInput','topPaste']},compositionEnd:{phasedRegistrationNames:{bubbled:'onCompositionEnd',captured:'onCompositionEndCapture'},dependencies:['topBlur','topCompositionEnd','topKeyDown','topKeyPress','topKeyUp','topMouseDown']},compositionStart:{phasedRegistrationNames:{bubbled:'onCompositionStart',captured:'onCompositionStartCapture'},dependencies:['topBlur','topCompositionStart','topKeyDown','topKeyPress','topKeyUp','topMouseDown']},compositionUpdate:{phasedRegistrationNames:{bubbled:'onCompositionUpdate',captured:'onCompositionUpdateCapture'},dependencies:['topBlur','topCompositionUpdate','topKeyDown','topKeyPress','topKeyUp','topMouseDown']}},k=!1,P=null;e.exports={eventTypes:T,extractEvents:function(e,t,n,o){return[s(e,t,n,o),u(e,t,n,o)]}}},function(e,t,n){'use strict';function o(e){this._root=e,this._startText=this.getText(),this._fallbackText=null}var a=n(3),r=n(15),i=n(69);a(o.prototype,{destructor:function(){this._root=null,this._startText=null,this._fallbackText=null},getText:function(){return'value'in this._root?this._root.value:this._root[i()]},getData:function(){if(this._fallbackText)return this._fallbackText;var e=this._startText,t=e.length,n=this.getText(),o=n.length,a,r;for(a=0;a<t&&e[a]===n[a];a++);var i=t-a;for(r=1;r<=i&&e[t-r]===n[o-r];r++);var d=1<r?1-r:void 0;return this._fallbackText=n.slice(a,d),this._fallbackText}}),r.addPoolingTo(o),e.exports=o},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(12);a.augmentClass(o,{data:null}),e.exports=o},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(12);a.augmentClass(o,{data:null}),e.exports=o},function(e,t,n){'use strict';function o(e,t,n){var o=k.getPooled(w.change,e,t,n);return o.type='change',v.accumulateTwoPhaseDispatches(o),o}function a(e){var t=e.nodeName&&e.nodeName.toLowerCase();return'select'===t||'input'===t&&'file'===e.type}function r(e){var t=o(D,e,I(e));T.batchedUpdates(i,t)}function i(e){E.enqueueEvents(e),E.processEventQueue(!1)}function d(e,t){R=e,D=t,R.attachEvent('onchange',r)}function s(){R&&(R.detachEvent('onchange',r),R=null,D=null)}function p(e,t){var n=P.updateValueIfChanged(e),o=!0===t.simulated&&L._allowSimulatedPassThrough;if(n||o)return e}function l(e,t){if('topChange'===e)return t}function u(e,t,n){'topFocus'===e?(s(),d(t,n)):'topBlur'===e&&s()}function c(e,t){R=e,D=t,R.attachEvent('onpropertychange',h)}function m(){R&&(R.detachEvent('onpropertychange',h),R=null,D=null)}function h(e){'value'!==e.propertyName||p(D,e)&&r(e)}function g(e,t,n){'topFocus'===e?(m(),c(t,n)):'topBlur'===e&&m()}function f(e,t,n){if('topSelectionChange'===e||'topKeyUp'===e||'topKeyDown'===e)return p(D,n)}function y(e){var t=e.nodeName;return t&&'input'===t.toLowerCase()&&('checkbox'===e.type||'radio'===e.type)}function _(e,t,n){if('topClick'===e)return p(t,n)}function C(e,t,n){if('topInput'===e||'topChange'===e)return p(t,n)}function b(e,t){if(null!=e){var n=e._wrapperState||t._wrapperState;if(n&&n.controlled&&'number'===t.type){var o=''+t.value;t.getAttribute('value')!==o&&t.setAttribute('value',o)}}}var E=n(25),v=n(24),x=n(6),N=n(4),T=n(11),k=n(12),P=n(72),I=n(51),M=n(52),S=n(73),w={change:{phasedRegistrationNames:{bubbled:'onChange',captured:'onChangeCapture'},dependencies:['topBlur','topChange','topClick','topFocus','topInput','topKeyDown','topKeyUp','topSelectionChange']}},R=null,D=null,A=!1;x.canUseDOM&&(A=M('change')&&(!document.documentMode||8<document.documentMode));var O=!1;x.canUseDOM&&(O=M('input')&&(!('documentMode'in document)||9<document.documentMode));var L={eventTypes:w,_allowSimulatedPassThrough:!0,_isInputEventSupported:O,extractEvents:function(e,t,n,r){var i=t?N.getNodeFromInstance(t):window,d,s;if(a(i)?A?d=l:s=u:S(i)?O?d=C:(d=f,s=g):y(i)&&(d=_),d){var p=d(e,t,n);if(p){var c=o(p,n,r);return c}}s&&s(e,i,t),'topBlur'===e&&b(t,i)}};e.exports=L},function(e,t,n){'use strict';function o(e,t,n){'function'==typeof e?e(t.getPublicInstance()):r.addComponentAsRefTo(t,e,n)}function a(e,t,n){'function'==typeof e?e(null):r.removeComponentAsRefFrom(t,e,n)}var r=n(113),i={};i.attachRefs=function(e,t){if(null!==t&&'object'==typeof t){var n=t.ref;null!=n&&o(n,e,t._owner)}},i.shouldUpdateRefs=function(e,t){var n=null,o=null;null!==e&&'object'==typeof e&&(n=e.ref,o=e._owner);var a=null,r=null;return null!==t&&'object'==typeof t&&(a=t.ref,r=t._owner),n!==a||'string'==typeof a&&r!==o},i.detachRefs=function(e,t){if(null!==t&&'object'==typeof t){var n=t.ref;null!=n&&a(n,e,t._owner)}},e.exports=i},function(e,t,n){'use strict';function o(e){return!!(e&&'function'==typeof e.attachRef&&'function'==typeof e.detachRef)}var a=n(2),r=n(0);e.exports={addComponentAsRefTo:function(e,t,n){o(n)?void 0:a('119'),n.attachRef(t,e)},removeComponentAsRefFrom:function(e,t,n){o(n)?void 0:a('120');var r=n.getPublicInstance();r&&r.refs[t]===e.getPublicInstance()&&n.detachRef(t)}}},function(e){'use strict';e.exports=['ResponderEventPlugin','SimpleEventPlugin','TapEventPlugin','EnterLeaveEventPlugin','ChangeEventPlugin','SelectEventPlugin','BeforeInputEventPlugin']},function(e,t,n){'use strict';var o=n(24),a=n(4),r=n(30),i={mouseEnter:{registrationName:'onMouseEnter',dependencies:['topMouseOut','topMouseOver']},mouseLeave:{registrationName:'onMouseLeave',dependencies:['topMouseOut','topMouseOver']}};e.exports={eventTypes:i,extractEvents:function(e,t,n,d){if('topMouseOver'===e&&(n.relatedTarget||n.fromElement))return null;if('topMouseOut'!==e&&'topMouseOver'!==e)return null;var s;if(d.window===d)s=d;else{var p=d.ownerDocument;s=p?p.defaultView||p.parentWindow:window}var l,u;if('topMouseOut'===e){l=t;var c=n.relatedTarget||n.toElement;u=c?a.getClosestInstanceFromNode(c):null}else l=null,u=t;if(l===u)return null;var m=null==l?s:a.getNodeFromInstance(l),h=null==u?s:a.getNodeFromInstance(u),g=r.getPooled(i.mouseLeave,l,n,d);g.type='mouseleave',g.target=m,g.relatedTarget=h;var f=r.getPooled(i.mouseEnter,u,n,d);return f.type='mouseenter',f.target=h,f.relatedTarget=m,o.accumulateEnterLeaveDispatches(g,f,l,u),[g,f]}}},function(e,t,n){'use strict';var o=n(16),a=o.injection.MUST_USE_PROPERTY,r=o.injection.HAS_BOOLEAN_VALUE,i=o.injection.HAS_NUMERIC_VALUE,d=o.injection.HAS_POSITIVE_NUMERIC_VALUE,s=o.injection.HAS_OVERLOADED_BOOLEAN_VALUE,p={isCustomAttribute:RegExp.prototype.test.bind(new RegExp('^(data|aria)-['+o.ATTRIBUTE_NAME_CHAR+']*$')),Properties:{accept:0,acceptCharset:0,accessKey:0,action:0,allowFullScreen:r,allowTransparency:0,alt:0,as:0,async:r,autoComplete:0,autoPlay:r,capture:r,cellPadding:0,cellSpacing:0,charSet:0,challenge:0,checked:a|r,cite:0,classID:0,className:0,cols:d,colSpan:0,content:0,contentEditable:0,contextMenu:0,controls:r,coords:0,crossOrigin:0,data:0,dateTime:0,default:r,defer:r,dir:0,disabled:r,download:s,draggable:0,encType:0,form:0,formAction:0,formEncType:0,formMethod:0,formNoValidate:r,formTarget:0,frameBorder:0,headers:0,height:0,hidden:r,high:0,href:0,hrefLang:0,htmlFor:0,httpEquiv:0,icon:0,id:0,inputMode:0,integrity:0,is:0,keyParams:0,keyType:0,kind:0,label:0,lang:0,list:0,loop:r,low:0,manifest:0,marginHeight:0,marginWidth:0,max:0,maxLength:0,media:0,mediaGroup:0,method:0,min:0,minLength:0,multiple:a|r,muted:a|r,name:0,nonce:0,noValidate:r,open:r,optimum:0,pattern:0,placeholder:0,playsInline:r,poster:0,preload:0,profile:0,radioGroup:0,readOnly:r,referrerPolicy:0,rel:0,required:r,reversed:r,role:0,rows:d,rowSpan:i,sandbox:0,scope:0,scoped:r,scrolling:0,seamless:r,selected:a|r,shape:0,size:d,sizes:0,span:d,spellCheck:0,src:0,srcDoc:0,srcLang:0,srcSet:0,start:i,step:0,style:0,summary:0,tabIndex:0,target:0,title:0,type:0,useMap:0,value:0,width:0,wmode:0,wrap:0,about:0,datatype:0,inlist:0,prefix:0,property:0,resource:0,typeof:0,vocab:0,autoCapitalize:0,autoCorrect:0,autoSave:0,color:0,itemProp:0,itemScope:r,itemType:0,itemID:0,itemRef:0,results:0,security:0,unselectable:0},DOMAttributeNames:{acceptCharset:'accept-charset',className:'class',htmlFor:'for',httpEquiv:'http-equiv'},DOMPropertyNames:{},DOMMutationMethods:{value:function(e,t){return null==t?e.removeAttribute('value'):void('number'!==e.type||!1===e.hasAttribute('value')?e.setAttribute('value',''+t):e.validity&&!e.validity.badInput&&e.ownerDocument.activeElement!==e&&e.setAttribute('value',''+t))}}};e.exports=p},function(e,t,n){'use strict';var o=n(54),a=n(122),r={processChildrenUpdates:a.dangerouslyProcessChildrenUpdates,replaceNodeWithMarkup:o.dangerouslyReplaceNodeWithMarkup};e.exports=r},function(e,t,n){'use strict';var o=n(2),a=n(18),r=n(6),i=n(119),d=n(5),s=n(0);e.exports={dangerouslyReplaceNodeWithMarkup:function(e,t){if(r.canUseDOM?void 0:o('56'),t?void 0:o('57'),'HTML'===e.nodeName?o('58'):void 0,'string'==typeof t){var n=i(t,d)[0];e.parentNode.replaceChild(n,e)}else a.replaceChildWithTree(e,t)}}},function(e,t,n){'use strict';function o(e){var t=e.match(p);return t&&t[1].toLowerCase()}var a=n(6),r=n(120),i=n(121),d=n(0),s=a.canUseDOM?document.createElement('div'):null,p=/^\s*<(\w+)/;e.exports=function(e,t){var n=s;!!s?void 0:d(!1);var a=o(e),p=a&&i(a);if(p){n.innerHTML=p[1]+e+p[2];for(var l=p[0];l--;)n=n.lastChild}else n.innerHTML=e;var u=n.getElementsByTagName('script');u.length&&(t?void 0:d(!1),r(u).forEach(t));for(var c=Array.from(n.childNodes);n.lastChild;)n.removeChild(n.lastChild);return c}},function(e,t,n){'use strict';function o(e){var t=e.length;if(Array.isArray(e)||'object'!=typeof e&&'function'!=typeof e?r(!1):void 0,'number'==typeof t?void 0:r(!1),0===t||t-1 in e?void 0:r(!1),'function'==typeof e.callee?r(!1):void 0,e.hasOwnProperty)try{return Array.prototype.slice.call(e)}catch(t){}for(var n=Array(t),o=0;o<t;o++)n[o]=e[o];return n}function a(e){return!!e&&('object'==typeof e||'function'==typeof e)&&'length'in e&&!('setInterval'in e)&&'number'!=typeof e.nodeType&&(Array.isArray(e)||'callee'in e||'item'in e)}var r=n(0);e.exports=function(e){return a(e)?Array.isArray(e)?e.slice():o(e):[e]}},function(e,t,n){'use strict';var o=n(6),a=n(0),r=o.canUseDOM?document.createElement('div'):null,i={},d=[1,'<select multiple="true">','</select>'],s=[1,'<table>','</table>'],p=[3,'<table><tbody><tr>','</tr></tbody></table>'],l=[1,'<svg xmlns="http://www.w3.org/2000/svg">','</svg>'],u={"*":[1,'?<div>','</div>'],area:[1,'<map>','</map>'],col:[2,'<table><tbody></tbody><colgroup>','</colgroup></table>'],legend:[1,'<fieldset>','</fieldset>'],param:[1,'<object>','</object>'],tr:[2,'<table><tbody>','</tbody></table>'],optgroup:d,option:d,caption:s,colgroup:s,tbody:s,tfoot:s,thead:s,td:p,th:p};['circle','clipPath','defs','ellipse','g','image','line','linearGradient','mask','path','pattern','polygon','polyline','radialGradient','rect','stop','text','tspan'].forEach(function(e){u[e]=l,i[e]=!0}),e.exports=function(e){return r?void 0:a(!1),u.hasOwnProperty(e)||(e='*'),i.hasOwnProperty(e)||(r.innerHTML='*'===e?'<link />':'<'+e+'></'+e+'>',i[e]=!r.firstChild),i[e]?u[e]:null}},function(e,t,n){'use strict';var o=n(54),a=n(4);e.exports={dangerouslyProcessChildrenUpdates:function(e,t){var n=a.getNodeFromInstance(e);o.processUpdates(n,t)}}},function(e,t,n){'use strict';function o(e){if(e){var t=e._currentElement._owner||null;if(t){var n=t.getName();if(n)return' This DOM node was rendered by `'+n+'`.'}}return''}function a(e){if('object'==typeof e){if(Array.isArray(e))return'['+e.map(a).join(', ')+']';var t=[];for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){var o=/^[a-z$_][\w$_]*$/i.test(n)?n:JSON.stringify(n);t.push(o+': '+a(e[n]))}return'{'+t.join(', ')+'}'}return'string'==typeof e?JSON.stringify(e):'function'==typeof e?'[function object]':e+''}function r(e,t){t&&(ae[e._tag]&&(null==t.children&&null==t.dangerouslySetInnerHTML?void 0:y('137',e._tag,e._currentElement._owner?' Check the render method of '+e._currentElement._owner.getName()+'.':'')),null!=t.dangerouslySetInnerHTML&&(null==t.children?void 0:y('60'),'object'==typeof t.dangerouslySetInnerHTML&&$ in t.dangerouslySetInnerHTML?void 0:y('61')),!1,null==t.style||'object'==typeof t.style?void 0:y('62',o(e)))}function i(e,t,n,o){if(!(o instanceof L)){var a=e._hostContainerInfo,r=a._node&&a._node.nodeType===J,i=r?a._node:a._ownerDocument;z(t,i),o.getReactMountReady().enqueue(d,{inst:e,registrationName:t,listener:n})}}function d(){var e=this;T.putListener(e.inst,e.registrationName,e.listener)}function s(){var e=this;S.postMountWrapper(e)}function p(){var e=this;D.postMountWrapper(e)}function l(){var e=this;w.postMountWrapper(e)}function u(){W.track(this)}function c(){var e=this;e._rootNodeID?void 0:y('63');var t=Y(e);switch(t?void 0:y('64'),e._tag){case'iframe':case'object':e._wrapperState.listeners=[P.trapBubbledEvent('topLoad','load',t)];break;case'video':case'audio':for(var n in e._wrapperState.listeners=[],te)te.hasOwnProperty(n)&&e._wrapperState.listeners.push(P.trapBubbledEvent(n,te[n],t));break;case'source':e._wrapperState.listeners=[P.trapBubbledEvent('topError','error',t)];break;case'img':e._wrapperState.listeners=[P.trapBubbledEvent('topError','error',t),P.trapBubbledEvent('topLoad','load',t)];break;case'form':e._wrapperState.listeners=[P.trapBubbledEvent('topReset','reset',t),P.trapBubbledEvent('topSubmit','submit',t)];break;case'input':case'select':case'textarea':e._wrapperState.listeners=[P.trapBubbledEvent('topInvalid','invalid',t)];}}function m(){R.postUpdateWrapper(this)}function h(e){de.call(ie,e)||(re.test(e)?void 0:y('65',e),ie[e]=!0)}function g(e,t){return 0<=e.indexOf('-')||null!=t.is}function f(e){var t=e.type;h(t),this._currentElement=e,this._tag=t.toLowerCase(),this._namespaceURI=null,this._renderedChildren=null,this._previousStyle=null,this._previousStyleCopy=null,this._hostNode=null,this._hostParent=null,this._rootNodeID=0,this._domID=0,this._hostContainerInfo=null,this._wrapperState=null,this._topLevelWrapper=null,this._flags=0,!1}var y=n(2),_=n(3),C=n(124),b=n(125),E=n(18),v=n(55),x=n(16),N=n(78),T=n(25),k=n(48),P=n(33),I=n(66),M=n(4),S=n(135),w=n(137),R=n(79),D=n(138),A=n(9),O=n(139),L=n(146),U=n(5),F=n(32),V=n(0),j=n(52),B=n(59),W=n(72),H=n(63),q=n(1),K=T.deleteListener,Y=M.getNodeFromInstance,z=P.listenTo,X=k.registrationNameModules,G={string:!0,number:!0},Q='style',$='__html',Z={children:null,dangerouslySetInnerHTML:null,suppressContentEditableWarning:null},J=11,ee={};var te={topAbort:'abort',topCanPlay:'canplay',topCanPlayThrough:'canplaythrough',topDurationChange:'durationchange',topEmptied:'emptied',topEncrypted:'encrypted',topEnded:'ended',topError:'error',topLoadedData:'loadeddata',topLoadedMetadata:'loadedmetadata',topLoadStart:'loadstart',topPause:'pause',topPlay:'play',topPlaying:'playing',topProgress:'progress',topRateChange:'ratechange',topSeeked:'seeked',topSeeking:'seeking',topStalled:'stalled',topSuspend:'suspend',topTimeUpdate:'timeupdate',topVolumeChange:'volumechange',topWaiting:'waiting'},ne={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0},oe={listing:!0,pre:!0,textarea:!0},ae=_({menuitem:!0},ne),re=/^[a-zA-Z][a-zA-Z:_\.\-\d]*$/,ie={},de={}.hasOwnProperty,se=1;f.displayName='ReactDOMComponent',f.Mixin={mountComponent:function(e,t,n,o){this._rootNodeID=se++,this._domID=n._idCounter++,this._hostParent=t,this._hostContainerInfo=n;var a=this._currentElement.props;switch(this._tag){case'audio':case'form':case'iframe':case'img':case'link':case'object':case'source':case'video':this._wrapperState={listeners:null},e.getReactMountReady().enqueue(c,this);break;case'input':S.mountWrapper(this,a,t),a=S.getHostProps(this,a),e.getReactMountReady().enqueue(u,this),e.getReactMountReady().enqueue(c,this);break;case'option':w.mountWrapper(this,a,t),a=w.getHostProps(this,a);break;case'select':R.mountWrapper(this,a,t),a=R.getHostProps(this,a),e.getReactMountReady().enqueue(c,this);break;case'textarea':D.mountWrapper(this,a,t),a=D.getHostProps(this,a),e.getReactMountReady().enqueue(u,this),e.getReactMountReady().enqueue(c,this);}r(this,a);var i,d;null==t?n._tag&&(i=n._namespaceURI,d=n._tag):(i=t._namespaceURI,d=t._tag),(null==i||i===v.svg&&'foreignobject'===d)&&(i=v.html),i===v.html&&('svg'===this._tag?i=v.svg:'math'===this._tag&&(i=v.mathml)),this._namespaceURI=i;var m;if(e.useCreateElement){var h=n._ownerDocument,g;if(!(i===v.html))g=h.createElementNS(i,this._currentElement.type);else if('script'===this._tag){var f=h.createElement('div'),y=this._currentElement.type;f.innerHTML='<'+y+'></'+y+'>',g=f.removeChild(f.firstChild)}else g=a.is?h.createElement(this._currentElement.type,a.is):h.createElement(this._currentElement.type);M.precacheNode(this,g),this._flags|=I.hasCachedChildNodes,this._hostParent||N.setAttributeForRoot(g),this._updateDOMProperties(null,a,e);var _=E(g);this._createInitialChildren(e,a,o,_),m=_}else{var b=this._createOpenTagMarkupAndPutListeners(e,a),x=this._createContentMarkup(e,a,o);m=!x&&ne[this._tag]?b+'/>':b+'>'+x+'</'+this._currentElement.type+'>'}switch(this._tag){case'input':e.getReactMountReady().enqueue(s,this),a.autoFocus&&e.getReactMountReady().enqueue(C.focusDOMComponent,this);break;case'textarea':e.getReactMountReady().enqueue(p,this),a.autoFocus&&e.getReactMountReady().enqueue(C.focusDOMComponent,this);break;case'select':a.autoFocus&&e.getReactMountReady().enqueue(C.focusDOMComponent,this);break;case'button':a.autoFocus&&e.getReactMountReady().enqueue(C.focusDOMComponent,this);break;case'option':e.getReactMountReady().enqueue(l,this);}return m},_createOpenTagMarkupAndPutListeners:function(e,t){var n='<'+this._currentElement.type;for(var o in t)if(t.hasOwnProperty(o)){var a=t[o];if(null!=a)if(X.hasOwnProperty(o))a&&i(this,o,a,e);else{o==Q&&(a&&(!1,a=this._previousStyleCopy=_({},t.style)),a=b.createMarkupForStyles(a,this));var r=null;null!=this._tag&&g(this._tag,t)?!Z.hasOwnProperty(o)&&(r=N.createMarkupForCustomAttribute(o,a)):r=N.createMarkupForProperty(o,a),r&&(n+=' '+r)}}return e.renderToStaticMarkup?n:(this._hostParent||(n+=' '+N.createMarkupForRoot()),n+=' '+N.createMarkupForID(this._domID),n)},_createContentMarkup:function(e,t,n){var o='',a=t.dangerouslySetInnerHTML;if(null!=a)null!=a.__html&&(o=a.__html);else{var r=G[typeof t.children]?t.children:null,i=null==r?t.children:null;if(null!=r)o=F(r),!1;else if(null!=i){var d=this.mountChildren(i,e,n);o=d.join('')}}return oe[this._tag]&&'\n'===o.charAt(0)?'\n'+o:o},_createInitialChildren:function(e,t,n,o){var a=t.dangerouslySetInnerHTML;if(null!=a)null!=a.__html&&E.queueHTML(o,a.__html);else{var r=G[typeof t.children]?t.children:null,d=null==r?t.children:null;if(null!=r)''!==r&&(!1,E.queueText(o,r));else if(null!=d)for(var s=this.mountChildren(d,e,n),p=0;p<s.length;p++)E.queueChild(o,s[p])}},receiveComponent:function(e,t,n){var o=this._currentElement;this._currentElement=e,this.updateComponent(t,o,e,n)},updateComponent:function(e,t,n,o){var a=t.props,i=this._currentElement.props;switch(this._tag){case'input':a=S.getHostProps(this,a),i=S.getHostProps(this,i);break;case'option':a=w.getHostProps(this,a),i=w.getHostProps(this,i);break;case'select':a=R.getHostProps(this,a),i=R.getHostProps(this,i);break;case'textarea':a=D.getHostProps(this,a),i=D.getHostProps(this,i);}switch(r(this,i),this._updateDOMProperties(a,i,e),this._updateDOMChildren(a,i,e,o),this._tag){case'input':S.updateWrapper(this);break;case'textarea':D.updateWrapper(this);break;case'select':e.getReactMountReady().enqueue(m,this);}},_updateDOMProperties:function(e,t,n){var o,a,r;for(o in e)if(!t.hasOwnProperty(o)&&e.hasOwnProperty(o)&&null!=e[o])if(o===Q){var d=this._previousStyleCopy;for(a in d)d.hasOwnProperty(a)&&(r=r||{},r[a]='');this._previousStyleCopy=null}else X.hasOwnProperty(o)?e[o]&&K(this,o):g(this._tag,e)?Z.hasOwnProperty(o)||N.deleteValueForAttribute(Y(this),o):(x.properties[o]||x.isCustomAttribute(o))&&N.deleteValueForProperty(Y(this),o);for(o in t){var s=t[o],p=o===Q?this._previousStyleCopy:null==e?void 0:e[o];if(t.hasOwnProperty(o)&&s!==p&&(null!=s||null!=p))if(o===Q){if(s?(!1,s=this._previousStyleCopy=_({},s)):this._previousStyleCopy=null,p){for(a in p)!p.hasOwnProperty(a)||s&&s.hasOwnProperty(a)||(r=r||{},r[a]='');for(a in s)s.hasOwnProperty(a)&&p[a]!==s[a]&&(r=r||{},r[a]=s[a])}else r=s;}else if(X.hasOwnProperty(o))s?i(this,o,s,n):p&&K(this,o);else if(g(this._tag,t))Z.hasOwnProperty(o)||N.setValueForAttribute(Y(this),o,s);else if(x.properties[o]||x.isCustomAttribute(o)){var l=Y(this);null==s?N.deleteValueForProperty(l,o):N.setValueForProperty(l,o,s)}}r&&b.setValueForStyles(Y(this),r,this)},_updateDOMChildren:function(e,t,n,o){var a=G[typeof e.children]?e.children:null,r=G[typeof t.children]?t.children:null,i=e.dangerouslySetInnerHTML&&e.dangerouslySetInnerHTML.__html,d=t.dangerouslySetInnerHTML&&t.dangerouslySetInnerHTML.__html,s=null==a?e.children:null,p=null==r?t.children:null;null!=s&&null==p?this.updateChildren(null,n,o):(null!=a||null!=i)&&!(null!=r||null!=d)&&(this.updateTextContent(''),!1),null==r?null==d?null!=p&&(!1,this.updateChildren(p,n,o)):(i!==d&&this.updateMarkup(''+d),!1):a!==r&&(this.updateTextContent(''+r),!1)},getHostNode:function(){return Y(this)},unmountComponent:function(e){switch(this._tag){case'audio':case'form':case'iframe':case'img':case'link':case'object':case'source':case'video':var t=this._wrapperState.listeners;if(t)for(var n=0;n<t.length;n++)t[n].remove();break;case'input':case'textarea':W.stopTracking(this);break;case'html':case'head':case'body':y('66',this._tag);}this.unmountChildren(e),M.uncacheNode(this),T.deleteAllListeners(this),this._rootNodeID=0,this._domID=0,this._wrapperState=null,!1},getPublicInstance:function(){return Y(this)}},_(f.prototype,f.Mixin,O.Mixin),e.exports=f},function(e,t,n){'use strict';var o=n(4),a=n(76);e.exports={focusDOMComponent:function(){a(o.getNodeFromInstance(this))}}},function(e,t,n){'use strict';var o=n(77),a=n(6),r=n(9),i=n(126),d=n(128),s=n(129),p=n(131),l=n(1),u=p(function(e){return s(e)}),c=!1,m='cssFloat';if(a.canUseDOM){var h=document.createElement('div').style;try{h.font=''}catch(t){c=!0}document.documentElement.style.cssFloat===void 0&&(m='styleFloat')}e.exports={createMarkupForStyles:function(e,t){var n='';for(var o in e)if(e.hasOwnProperty(o)){var a=0===o.indexOf('--'),r=e[o];!1,null!=r&&(n+=u(o)+':',n+=d(o,r,t,a)+';')}return n||null},setValueForStyles:function(e,t,n){var a=e.style;for(var r in t)if(t.hasOwnProperty(r)){var i=0===r.indexOf('--');var s=d(r,t[r],n,i);if(('float'==r||'cssFloat'==r)&&(r=m),i)a.setProperty(r,s);else if(s)a[r]=s;else{var p=c&&o.shorthandPropertyExpansions[r];if(p)for(var l in p)a[l]='';else a[r]=''}}}}},function(e,t,n){'use strict';var o=n(127),a=/^-ms-/;e.exports=function(e){return o(e.replace(a,'ms-'))}},function(e){'use strict';var t=/-(.)/g;e.exports=function(e){return e.replace(t,function(e,t){return t.toUpperCase()})}},function(e,t,n){'use strict';var o=n(77),a=n(1),r=o.isUnitlessNumber;e.exports=function(e,t,n,o){var a=null==t||'boolean'==typeof t||''===t;if(a)return'';var i=isNaN(t);if(o||i||0===t||r.hasOwnProperty(e)&&r[e])return''+t;if('string'==typeof t){t=t.trim()}return t+'px'}},function(e,t,n){'use strict';var o=n(130),a=/^ms-/;e.exports=function(e){return o(e).replace(a,'-ms-')}},function(e){'use strict';var t=/([A-Z])/g;e.exports=function(e){return e.replace(t,'-$1').toLowerCase()}},function(e){'use strict';e.exports=function(e){var t={};return function(n){return t.hasOwnProperty(n)||(t[n]=e.call(this,n)),t[n]}}},function(e,t,n){'use strict';var o=n(32);e.exports=function(e){return'"'+o(e)+'"'}},function(e,t,n){'use strict';function o(e){a.enqueueEvents(e),a.processEventQueue(!1)}var a=n(25);e.exports={handleTopLevel:function(e,t,n,r){var i=a.extractEvents(e,t,n,r);o(i)}}},function(e,t,n){'use strict';function o(e,t){var n={};return n[e.toLowerCase()]=t.toLowerCase(),n['Webkit'+e]='webkit'+t,n['Moz'+e]='moz'+t,n['ms'+e]='MS'+t,n['O'+e]='o'+t.toLowerCase(),n}var a=n(6),r={animationend:o('Animation','AnimationEnd'),animationiteration:o('Animation','AnimationIteration'),animationstart:o('Animation','AnimationStart'),transitionend:o('Transition','TransitionEnd')},i={},d={};a.canUseDOM&&(d=document.createElement('div').style,!('AnimationEvent'in window)&&(delete r.animationend.animation,delete r.animationiteration.animation,delete r.animationstart.animation),!('TransitionEvent'in window)&&delete r.transitionend.transition),e.exports=function(e){if(i[e])return i[e];if(!r[e])return e;var t=r[e];for(var n in t)if(t.hasOwnProperty(n)&&n in d)return i[e]=t[n];return''}},function(e,t,n){'use strict';function o(){this._rootNodeID&&h.updateWrapper(this)}function a(e){var t='checkbox'===e.type||'radio'===e.type;return t?null!=e.checked:null!=e.value}function r(e){var t=this._currentElement.props,n=p.executeOnChange(t,e);u.asap(o,this);var a=t.name;if('radio'===t.type&&null!=a){for(var r=l.getNodeFromInstance(this),d=r;d.parentNode;)d=d.parentNode;for(var s=d.querySelectorAll('input[name='+JSON.stringify(''+a)+'][type="radio"]'),c=0,m;c<s.length;c++)if(m=s[c],m!==r&&m.form===r.form){var h=l.getInstanceFromNode(m);h?void 0:i('90'),u.asap(o,h)}}return n}var i=n(2),d=n(3),s=n(78),p=n(57),l=n(4),u=n(11),c=n(0),m=n(1),h={getHostProps:function(e,t){var n=p.getValue(t),o=p.getChecked(t),a=d({type:void 0,step:void 0,min:void 0,max:void 0},t,{defaultChecked:void 0,defaultValue:void 0,value:null==n?e._wrapperState.initialValue:n,checked:null==o?e._wrapperState.initialChecked:o,onChange:e._wrapperState.onChange});return a},mountWrapper:function(e,t){var n=t.defaultValue;e._wrapperState={initialChecked:null==t.checked?t.defaultChecked:t.checked,initialValue:null==t.value?n:t.value,listeners:null,onChange:r.bind(e),controlled:a(t)}},updateWrapper:function(e){var t=e._currentElement.props;var n=t.checked;null!=n&&s.setValueForProperty(l.getNodeFromInstance(e),'checked',n||!1);var o=l.getNodeFromInstance(e),a=p.getValue(t);if(!(null!=a))null==t.value&&null!=t.defaultValue&&o.defaultValue!==''+t.defaultValue&&(o.defaultValue=''+t.defaultValue),null==t.checked&&null!=t.defaultChecked&&(o.defaultChecked=!!t.defaultChecked);else if(0===a&&''===o.value)o.value='0';else if('number'===t.type){var r=parseFloat(o.value,10)||0;(a!=r||a==r&&o.value!=a)&&(o.value=''+a)}else o.value!==''+a&&(o.value=''+a)},postMountWrapper:function(e){var t=e._currentElement.props,n=l.getNodeFromInstance(e);switch(t.type){case'submit':case'reset':break;case'color':case'date':case'datetime':case'datetime-local':case'month':case'time':case'week':n.value='',n.value=n.defaultValue;break;default:n.value=n.value;}var o=n.name;''!==o&&(n.name=''),n.defaultChecked=!n.defaultChecked,n.defaultChecked=!n.defaultChecked,''!==o&&(n.name=o)}};e.exports=h},function(e){'use strict';e.exports='SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'},function(e,t,n){'use strict';function o(e){var t='';return r.Children.forEach(e,function(e){null==e||('string'==typeof e||'number'==typeof e?t+=e:!p&&(p=!0,void 0))}),t}var a=n(3),r=n(13),i=n(4),d=n(79),s=n(1),p=!1;e.exports={mountWrapper:function(e,t,n){var a=null;if(null!=n){var r=n;'optgroup'===r._tag&&(r=r._hostParent),null!=r&&'select'===r._tag&&(a=d.getSelectValueContext(r))}var s=null;if(null!=a){var p;if(p=null==t.value?o(t.children):t.value+'',s=!1,Array.isArray(a)){for(var l=0;l<a.length;l++)if(''+a[l]===p){s=!0;break}}else s=''+a===p}e._wrapperState={selected:s}},postMountWrapper:function(e){var t=e._currentElement.props;if(null!=t.value){var n=i.getNodeFromInstance(e);n.setAttribute('value',t.value)}},getHostProps:function(e,t){var n=a({selected:void 0,children:void 0},t);null!=e._wrapperState.selected&&(n.selected=e._wrapperState.selected);var r=o(t.children);return r&&(n.children=r),n}}},function(e,t,n){'use strict';function o(){this._rootNodeID&&c.updateWrapper(this)}function a(e){var t=this._currentElement.props,n=d.executeOnChange(t,e);return p.asap(o,this),n}var r=n(2),i=n(3),d=n(57),s=n(4),p=n(11),l=n(0),u=n(1),c={getHostProps:function(e,t){null==t.dangerouslySetInnerHTML?void 0:r('91');var n=i({},t,{value:void 0,defaultValue:void 0,children:''+e._wrapperState.initialValue,onChange:e._wrapperState.onChange});return n},mountWrapper:function(e,t){var n=d.getValue(t),o=n;if(null==n){var i=t.defaultValue,s=t.children;null!=s&&(!1,null==i?void 0:r('92'),Array.isArray(s)&&(1>=s.length?void 0:r('93'),s=s[0]),i=''+s),null==i&&(i=''),o=i}e._wrapperState={initialValue:''+o,listeners:null,onChange:a.bind(e)}},updateWrapper:function(e){var t=e._currentElement.props,n=s.getNodeFromInstance(e),o=d.getValue(t);if(null!=o){var a=''+o;a!==n.value&&(n.value=a),null==t.defaultValue&&(n.defaultValue=a)}null!=t.defaultValue&&(n.defaultValue=t.defaultValue)},postMountWrapper:function(e){var t=s.getNodeFromInstance(e),n=t.textContent;n===e._wrapperState.initialValue&&(t.value=n)}};e.exports=c},function(e,t,n){'use strict';function o(e,t,n){return{type:'INSERT_MARKUP',content:e,fromIndex:null,fromNode:null,toIndex:n,afterNode:t}}function a(e,t,n){return{type:'MOVE_EXISTING',content:null,fromIndex:e._mountIndex,fromNode:g.getHostNode(e),toIndex:n,afterNode:t}}function r(e,t){return{type:'REMOVE_NODE',content:null,fromIndex:e._mountIndex,fromNode:t,toIndex:null,afterNode:null}}function i(e){return{type:'SET_MARKUP',content:e,fromIndex:null,fromNode:null,toIndex:null,afterNode:null}}function d(e){return{type:'TEXT_CONTENT',content:e,fromIndex:null,fromNode:null,toIndex:null,afterNode:null}}function s(e,t){return t&&(e=e||[],e.push(t)),e}function p(e,t){u.processChildrenUpdates(e,t)}var l=n(2),u=n(58),c=n(27),m=n(9),h=n(8),g=n(17),f=n(140),y=n(5),_=n(145),C=n(0);e.exports={Mixin:{_reconcilerInstantiateChildren:function(e,t,n){return f.instantiateChildren(e,t,n)},_reconcilerUpdateChildren:function(e,t,n,o,a,r){var i=0,d;return d=_(t,i),f.updateChildren(e,d,n,o,a,this,this._hostContainerInfo,r,i),d},mountChildren:function(e,t,n){var o=this._reconcilerInstantiateChildren(e,t,n);this._renderedChildren=o;var a=[],r=0;for(var i in o)if(o.hasOwnProperty(i)){var d=o[i];var s=g.mountComponent(d,t,this,this._hostContainerInfo,n,0);d._mountIndex=r++,a.push(s)}return!1,a},updateTextContent:function(e){var t=this._renderedChildren;for(var n in f.unmountChildren(t,!1),t)t.hasOwnProperty(n)&&l('118');var o=[d(e)];p(this,o)},updateMarkup:function(e){var t=this._renderedChildren;for(var n in f.unmountChildren(t,!1),t)t.hasOwnProperty(n)&&l('118');var o=[i(e)];p(this,o)},updateChildren:function(e,t,n){this._updateChildren(e,t,n)},_updateChildren:function(e,t,n){var o=Math.max,a=this._renderedChildren,r={},i=[],d=this._reconcilerUpdateChildren(a,e,i,r,t,n);if(d||a){var l=null,u=0,c=0,m=0,h=null,f;for(f in d)if(d.hasOwnProperty(f)){var y=a&&a[f],_=d[f];y===_?(l=s(l,this.moveChild(y,h,u,c)),c=o(y._mountIndex,c),y._mountIndex=u):(y&&(c=o(y._mountIndex,c)),l=s(l,this._mountChildAtIndex(_,i[m],h,u,t,n)),m++),u++,h=g.getHostNode(_)}for(f in r)r.hasOwnProperty(f)&&(l=s(l,this._unmountChild(a[f],r[f])));l&&p(this,l),this._renderedChildren=d,!1}},unmountChildren:function(e){var t=this._renderedChildren;f.unmountChildren(t,e),this._renderedChildren=null},moveChild:function(e,t,n,o){if(e._mountIndex<o)return a(e,t,n)},createChild:function(e,t,n){return o(n,t,e._mountIndex)},removeChild:function(e,t){return r(e,t)},_mountChildAtIndex:function(e,t,n,o){return e._mountIndex=o,this.createChild(e,n,t)},_unmountChild:function(e,t){var n=this.removeChild(e,t);return e._mountIndex=null,n}}}},function(e,t,n){'use strict';(function(t){function o(e,t,n){var o=e[n]===void 0;!1,null!=t&&o&&(e[n]=r(t,!0))}var a=n(17),r=n(81),i=n(61),d=n(60),s=n(85),p=n(1);e.exports={instantiateChildren:function(e,t,n,a){if(null==e)return null;var r={};return s(e,o,r),r},updateChildren:function(e,t,n,o,i,s,p,l,u){if(t||e){var c,m;for(c in t)if(t.hasOwnProperty(c)){m=e&&e[c];var h=m&&m._currentElement,g=t[c];if(null!=m&&d(h,g))a.receiveComponent(m,g,i,l),t[c]=m;else{m&&(o[c]=a.getHostNode(m),a.unmountComponent(m,!1));var f=r(g,!0);t[c]=f;var y=a.mountComponent(f,i,s,p,l,u);n.push(y)}}for(c in e)e.hasOwnProperty(c)&&!(t&&t.hasOwnProperty(c))&&(m=e[c],o[c]=a.getHostNode(m),a.unmountComponent(m,!1))}},unmountChildren:function(e,t){for(var n in e)if(e.hasOwnProperty(n)){var o=e[n];a.unmountComponent(o,t)}}}}).call(t,n(80))},function(e,t,n){'use strict';function o(){}function a(){}function r(e){return!!(e.prototype&&e.prototype.isReactComponent)}function i(e){return!!(e.prototype&&e.prototype.isPureReactComponent)}function d(e,t,n){if(0===t)return e();g.debugTool.onBeginLifeCycleTimer(t,n);try{return e()}finally{g.debugTool.onEndLifeCycleTimer(t,n)}}var s=n(2),p=n(3),l=n(13),u=n(58),c=n(8),m=n(50),h=n(27),g=n(9),f=n(82),y=n(17);var _=n(14),C=n(0),b=n(59),E=n(60),v=n(1),x={ImpureClass:0,PureClass:1,StatelessFunctional:2};o.prototype.render=function(){var e=h.get(this)._currentElement.type,t=e(this.props,this.context,this.updater);return a(e,t),t};var N=1;e.exports={construct:function(e){this._currentElement=e,this._rootNodeID=0,this._compositeType=null,this._instance=null,this._hostParent=null,this._hostContainerInfo=null,this._updateBatchNumber=null,this._pendingElement=null,this._pendingStateQueue=null,this._pendingReplaceState=!1,this._pendingForceUpdate=!1,this._renderedNodeType=null,this._renderedComponent=null,this._context=null,this._mountOrder=0,this._topLevelWrapper=null,this._pendingCallbacks=null,this._calledComponentWillUnmount=!1,!1},mountComponent:function(e,t,n,d){var p=this;this._context=d,this._mountOrder=N++,this._hostParent=t,this._hostContainerInfo=n;var u=this._currentElement.props,c=this._processContext(d),m=this._currentElement.type,g=e.getUpdateQueue(),f=r(m),y=this._constructComponent(f,u,c,g),C;f||null!=y&&null!=y.render?i(m)?this._compositeType=x.PureClass:this._compositeType=x.ImpureClass:(C=y,a(m,C),null===y||!1===y||l.isValidElement(y)?void 0:s('105',m.displayName||m.name||'Component'),y=new o(m),this._compositeType=x.StatelessFunctional);y.props=u,y.context=c,y.refs=_,y.updater=g,this._instance=y,h.set(y,this),!1;var b=y.state;void 0===b&&(y.state=b=null),'object'!=typeof b||Array.isArray(b)?s('106',this.getName()||'ReactCompositeComponent'):void 0,this._pendingStateQueue=null,this._pendingReplaceState=!1,this._pendingForceUpdate=!1;var E;return E=y.unstable_handleError?this.performInitialMountWithErrorHandling(C,t,n,e,d):this.performInitialMount(C,t,n,e,d),y.componentDidMount&&e.getReactMountReady().enqueue(y.componentDidMount,y),E},_constructComponent:function(e,t,n,o){return this._constructComponentWithoutOwner(e,t,n,o)},_constructComponentWithoutOwner:function(e,t,n,o){var a=this._currentElement.type;return e?new a(t,n,o):a(t,n,o)},performInitialMountWithErrorHandling:function(t,n,o,a,r){var i=a.checkpoint(),d;try{d=this.performInitialMount(t,n,o,a,r)}catch(s){a.rollback(i),this._instance.unstable_handleError(s),this._pendingStateQueue&&(this._instance.state=this._processPendingState(this._instance.props,this._instance.context)),i=a.checkpoint(),this._renderedComponent.unmountComponent(!0),a.rollback(i),d=this.performInitialMount(t,n,o,a,r)}return d},performInitialMount:function(e,t,n,o,a){var r=this._instance,i=0;!1,r.componentWillMount&&(r.componentWillMount(),this._pendingStateQueue&&(r.state=this._processPendingState(r.props,r.context))),e===void 0&&(e=this._renderValidatedComponent());var d=f.getType(e);this._renderedNodeType=d;var s=this._instantiateReactComponent(e,d!==f.EMPTY);this._renderedComponent=s;var p=y.mountComponent(s,o,t,n,this._processChildContext(a),i);return p},getHostNode:function(){return y.getHostNode(this._renderedComponent)},unmountComponent:function(e){if(this._renderedComponent){var t=this._instance;if(t.componentWillUnmount&&!t._calledComponentWillUnmount)if(t._calledComponentWillUnmount=!0,e){var n=this.getName()+'.componentWillUnmount()';m.invokeGuardedCallback(n,t.componentWillUnmount.bind(t))}else t.componentWillUnmount();this._renderedComponent&&(y.unmountComponent(this._renderedComponent,e),this._renderedNodeType=null,this._renderedComponent=null,this._instance=null),this._pendingStateQueue=null,this._pendingReplaceState=!1,this._pendingForceUpdate=!1,this._pendingCallbacks=null,this._pendingElement=null,this._context=null,this._rootNodeID=0,this._topLevelWrapper=null,h.remove(t)}},_maskContext:function(e){var t=this._currentElement.type,n=t.contextTypes;if(!n)return _;var o={};for(var a in n)o[a]=e[a];return o},_processContext:function(e){var t=this._maskContext(e);return t},_processChildContext:function(e){var t=this._currentElement.type,n=this._instance,o;if(n.getChildContext&&(o=n.getChildContext()),o){for(var a in'object'==typeof t.childContextTypes?void 0:s('107',this.getName()||'ReactCompositeComponent'),!1,o)a in t.childContextTypes?void 0:s('108',this.getName()||'ReactCompositeComponent',a);return p({},e,o)}return e},_checkContextTypes:function(){},receiveComponent:function(e,t,n){var o=this._currentElement,a=this._context;this._pendingElement=null,this.updateComponent(t,o,e,a,n)},performUpdateIfNecessary:function(e){null==this._pendingElement?null!==this._pendingStateQueue||this._pendingForceUpdate?this.updateComponent(e,this._currentElement,this._currentElement,this._context,this._context):this._updateBatchNumber=null:y.receiveComponent(this,this._pendingElement,e,this._context)},updateComponent:function(e,t,n,o,a){var r=this._instance;null!=r?void 0:s('136',this.getName()||'ReactCompositeComponent');var i=!1,d;this._context===a?d=r.context:(d=this._processContext(a),i=!0);var p=t.props,l=n.props;t!==n&&(i=!0),i&&r.componentWillReceiveProps&&r.componentWillReceiveProps(l,d);var u=this._processPendingState(l,d),c=!0;this._pendingForceUpdate||(r.shouldComponentUpdate?c=r.shouldComponentUpdate(l,u,d):this._compositeType===x.PureClass&&(c=!b(p,l)||!b(r.state,u))),!1,this._updateBatchNumber=null,c?(this._pendingForceUpdate=!1,this._performComponentUpdate(n,l,u,d,e,a)):(this._currentElement=n,this._context=a,r.props=l,r.state=u,r.context=d)},_processPendingState:function(e,t){var n=this._instance,o=this._pendingStateQueue,a=this._pendingReplaceState;if(this._pendingReplaceState=!1,this._pendingStateQueue=null,!o)return n.state;if(a&&1===o.length)return o[0];for(var r=p({},a?o[0]:n.state),d=a?1:0,i;d<o.length;d++)i=o[d],p(r,'function'==typeof i?i.call(n,r,e,t):i);return r},_performComponentUpdate:function(e,t,n,o,a,r){var i=this,d=this._instance,s=!!d.componentDidUpdate,p,l,u;s&&(p=d.props,l=d.state,u=d.context),d.componentWillUpdate&&d.componentWillUpdate(t,n,o),this._currentElement=e,this._context=r,d.props=t,d.state=n,d.context=o,this._updateRenderedComponent(a,r),s&&a.getReactMountReady().enqueue(d.componentDidUpdate.bind(d,p,l,u),d)},_updateRenderedComponent:function(e,t){var n=this._renderedComponent,o=n._currentElement,a=this._renderValidatedComponent();if(!1,E(o,a))y.receiveComponent(n,a,e,this._processChildContext(t));else{var r=y.getHostNode(n);y.unmountComponent(n,!1);var i=f.getType(a);this._renderedNodeType=i;var d=this._instantiateReactComponent(a,i!==f.EMPTY);this._renderedComponent=d;var s=y.mountComponent(d,e,this._hostParent,this._hostContainerInfo,this._processChildContext(t),0);this._replaceNodeWithMarkup(r,s,n)}},_replaceNodeWithMarkup:function(e,t,n){u.replaceNodeWithMarkup(e,t,n)},_renderValidatedComponentWithoutOwnerOrContext:function(){var e=this._instance,t;return t=e.render(),!1,t},_renderValidatedComponent:function(){var e;if(this._compositeType!==x.StatelessFunctional){c.current=this;try{e=this._renderValidatedComponentWithoutOwnerOrContext()}finally{c.current=null}}else e=this._renderValidatedComponentWithoutOwnerOrContext();return null===e||!1===e||l.isValidElement(e)?void 0:s('109',this.getName()||'ReactCompositeComponent'),e},attachRef:function(e,t){var n=this.getPublicInstance();null!=n?void 0:s('110');var o=t.getPublicInstance();var a=n.refs===_?n.refs={}:n.refs;a[e]=o},detachRef:function(e){var t=this.getPublicInstance().refs;delete t[e]},getName:function(){var e=this._currentElement.type,t=this._instance&&this._instance.constructor;return e.displayName||t&&t.displayName||e.name||t&&t.name||null},getPublicInstance:function(){var e=this._instance;return this._compositeType===x.StatelessFunctional?null:e},_instantiateReactComponent:null}},function(e){'use strict';var t=1;e.exports=function(){return t++}},function(e){'use strict';var t='function'==typeof Symbol&&Symbol['for']&&Symbol['for']('react.element')||60103;e.exports=t},function(e){'use strict';var t='function'==typeof Symbol&&Symbol.iterator;e.exports=function(e){var n=e&&(t&&e[t]||e['@@iterator']);if('function'==typeof n)return n}},function(e,t,n){'use strict';(function(t){function o(e,t,n){if(e&&'object'==typeof e){var o=e,a=o[n]===void 0;!1,a&&null!=t&&(o[n]=t)}}var a=n(61),r=n(85),i=n(1);'undefined'!=typeof t&&{NODE_ENV:'production'}&&!1,e.exports=function(e,t){if(null==e)return e;var n={};return r(e,o,n),n}}).call(t,n(80))},function(e,t,n){'use strict';function o(e){this.reinitializeTransaction(),this.renderToStaticMarkup=e,this.useCreateElement=!1,this.updateQueue=new s(this)}var a=n(3),r=n(15),i=n(29),d=n(9),s=n(147),p=[];var l={enqueue:function(){}};a(o.prototype,i,{getTransactionWrappers:function(){return p},getReactMountReady:function(){return l},getUpdateQueue:function(){return this.updateQueue},destructor:function(){},checkpoint:function(){},rollback:function(){}}),r.addPoolingTo(o),e.exports=o},function(e,t,n){'use strict';function o(e,t){if(!(e instanceof t))throw new TypeError('Cannot call a class as a function')}function a(){}var r=n(62),i=n(1),d=function(){function e(t){o(this,e),this.transaction=t}return e.prototype.isMounted=function(){return!1},e.prototype.enqueueCallback=function(e,t,n){this.transaction.isInTransaction()&&r.enqueueCallback(e,t,n)},e.prototype.enqueueForceUpdate=function(e){this.transaction.isInTransaction()?r.enqueueForceUpdate(e):a(e,'forceUpdate')},e.prototype.enqueueReplaceState=function(e,t){this.transaction.isInTransaction()?r.enqueueReplaceState(e,t):a(e,'replaceState')},e.prototype.enqueueSetState=function(e,t){this.transaction.isInTransaction()?r.enqueueSetState(e,t):a(e,'setState')},e}();e.exports=d},function(e,t,n){'use strict';var o=n(3),a=n(18),r=n(4),i=function(){this._currentElement=null,this._hostNode=null,this._hostParent=null,this._hostContainerInfo=null,this._domID=0};o(i.prototype,{mountComponent:function(e,t,n){var o=n._idCounter++;this._domID=o,this._hostParent=t,this._hostContainerInfo=n;var i=' react-empty: '+this._domID+' ';if(e.useCreateElement){var d=n._ownerDocument,s=d.createComment(i);return r.precacheNode(this,s),a(s)}return e.renderToStaticMarkup?'':'<!--'+i+'-->'},receiveComponent:function(){},getHostNode:function(){return r.getNodeFromInstance(this)},unmountComponent:function(){r.uncacheNode(this)}}),e.exports=i},function(e,t,n){'use strict';function o(e,t){'_hostNode'in e?void 0:a('33'),'_hostNode'in t?void 0:a('33');for(var n=0,o=e;o;o=o._hostParent)n++;for(var r=0,i=t;i;i=i._hostParent)r++;for(;0<n-r;)e=e._hostParent,n--;for(;0<r-n;)t=t._hostParent,r--;for(var d=n;d--;){if(e===t)return e;e=e._hostParent,t=t._hostParent}return null}var a=n(2),r=n(0);e.exports={isAncestor:function(e,t){for(('_hostNode'in e)?void 0:a('35'),('_hostNode'in t)?void 0:a('35');t;){if(t===e)return!0;t=t._hostParent}return!1},getLowestCommonAncestor:o,getParentInstance:function(e){return'_hostNode'in e?void 0:a('36'),e._hostParent},traverseTwoPhase:function(e,t,n){for(var o=[];e;)o.push(e),e=e._hostParent;var a;for(a=o.length;0<a--;)t(o[a],'captured',n);for(a=0;a<o.length;a++)t(o[a],'bubbled',n)},traverseEnterLeave:function(e,t,n,a,r){for(var d=e&&t?o(e,t):null,s=[];e&&e!==d;)s.push(e),e=e._hostParent;for(var p=[];t&&t!==d;)p.push(t),t=t._hostParent;var l;for(l=0;l<s.length;l++)n(s[l],'bubbled',a);for(l=p.length;0<l--;)n(p[l],'captured',r)}}},function(e,t,n){'use strict';var o=n(2),a=n(3),r=n(54),i=n(18),d=n(4),s=n(32),p=n(0),l=n(63),u=function(e){this._currentElement=e,this._stringText=''+e,this._hostNode=null,this._hostParent=null,this._domID=0,this._mountIndex=0,this._closingComment=null,this._commentNodes=null};a(u.prototype,{mountComponent:function(e,t,n){var o=n._idCounter++,a=' react-text: '+o+' ',r=' /react-text ';if(this._domID=o,this._hostParent=t,e.useCreateElement){var p=n._ownerDocument,l=p.createComment(a),u=p.createComment(r),c=i(p.createDocumentFragment());return i.queueChild(c,i(l)),this._stringText&&i.queueChild(c,i(p.createTextNode(this._stringText))),i.queueChild(c,i(u)),d.precacheNode(this,l),this._closingComment=u,c}var m=s(this._stringText);return e.renderToStaticMarkup?m:'<!--'+a+'-->'+m+'<!--'+r+'-->'},receiveComponent:function(e){if(e!==this._currentElement){this._currentElement=e;var t=''+e;if(t!==this._stringText){this._stringText=t;var n=this.getHostNode();r.replaceDelimitedText(n[0],n[1],t)}}},getHostNode:function(){var e=this._commentNodes;if(e)return e;if(!this._closingComment)for(var t=d.getNodeFromInstance(this),n=t.nextSibling;;){if(null==n?o('67',this._domID):void 0,8===n.nodeType&&' /react-text '===n.nodeValue){this._closingComment=n;break}n=n.nextSibling}return e=[this._hostNode,this._closingComment],this._commentNodes=e,e},unmountComponent:function(){this._closingComment=null,this._commentNodes=null,d.uncacheNode(this)}}),e.exports=u},function(e,t,n){'use strict';function o(){this.reinitializeTransaction()}var a=n(3),r=n(11),i=n(29),d=n(5),s={initialize:d,close:r.flushBatchedUpdates.bind(r)},p=[s,{initialize:d,close:function(){u.isBatchingUpdates=!1}}];a(o.prototype,i,{getTransactionWrappers:function(){return p}});var l=new o,u={isBatchingUpdates:!1,batchedUpdates:function(t,n,o,a,r,i){var e=u.isBatchingUpdates;return u.isBatchingUpdates=!0,e?t(n,o,a,r,i):l.perform(t,null,n,o,a,r,i)}};e.exports=u},function(e,t,n){'use strict';function o(e){for(;e._hostParent;)e=e._hostParent;var t=u.getNodeFromInstance(e),n=t.parentNode;return u.getClosestInstanceFromNode(n)}function a(e,t){this.topLevelType=e,this.nativeEvent=t,this.ancestors=[]}function r(e){var t=m(e.nativeEvent),n=u.getClosestInstanceFromNode(t),a=n;do e.ancestors.push(a),a=a&&o(a);while(a);for(var r=0;r<e.ancestors.length;r++)n=e.ancestors[r],g._handleTopLevel(e.topLevelType,n,e.nativeEvent,m(e.nativeEvent))}function i(e){var t=h(window);e(t)}var d=n(3),s=n(87),p=n(6),l=n(15),u=n(4),c=n(11),m=n(51),h=n(153);d(a.prototype,{destructor:function(){this.topLevelType=null,this.nativeEvent=null,this.ancestors.length=0}}),l.addPoolingTo(a,l.twoArgumentPooler);var g={_enabled:!0,_handleTopLevel:null,WINDOW_HANDLE:p.canUseDOM?window:null,setHandleTopLevel:function(e){g._handleTopLevel=e},setEnabled:function(e){g._enabled=!!e},isEnabled:function(){return g._enabled},trapBubbledEvent:function(e,t,n){return n?s.listen(n,t,g.dispatchEvent.bind(null,e)):null},trapCapturedEvent:function(e,t,n){return n?s.capture(n,t,g.dispatchEvent.bind(null,e)):null},monitorScrollValue:function(e){var t=i.bind(null,e);s.listen(window,'scroll',t)},dispatchEvent:function(e,t){if(g._enabled){var n=a.getPooled(e,t);try{c.batchedUpdates(r,n)}finally{a.release(n)}}}};e.exports=g},function(e){'use strict';e.exports=function(e){return e.Window&&e instanceof e.Window?{x:e.pageXOffset||e.document.documentElement.scrollLeft,y:e.pageYOffset||e.document.documentElement.scrollTop}:{x:e.scrollLeft,y:e.scrollTop}}},function(e,t,n){'use strict';var o=n(16),a=n(25),r=n(49),i=n(58),d=n(83),s=n(33),p=n(84),l=n(11),u={Component:i.injection,DOMProperty:o.injection,EmptyComponent:d.injection,EventPluginHub:a.injection,EventPluginUtils:r.injection,EventEmitter:s.injection,HostComponent:p.injection,Updates:l.injection};e.exports=u},function(e,t,n){'use strict';function o(e){this.reinitializeTransaction(),this.renderToStaticMarkup=!1,this.reactMountReady=r.getPooled(null),this.useCreateElement=e}var a=n(3),r=n(70),i=n(15),d=n(33),s=n(88),p=n(9),l=n(29),u=n(62),c={initialize:s.getSelectionInformation,close:s.restoreSelection},m=[c,{initialize:function(){var e=d.isEnabled();return d.setEnabled(!1),e},close:function(e){d.setEnabled(e)}},{initialize:function(){this.reactMountReady.reset()},close:function(){this.reactMountReady.notifyAll()}}];a(o.prototype,l,{getTransactionWrappers:function(){return m},getReactMountReady:function(){return this.reactMountReady},getUpdateQueue:function(){return u},checkpoint:function(){return this.reactMountReady.checkpoint()},rollback:function(e){this.reactMountReady.rollback(e)},destructor:function(){r.release(this.reactMountReady),this.reactMountReady=null}}),i.addPoolingTo(o),e.exports=o},function(e,t,n){'use strict';function o(e,t,n,o){return e===n&&t===o}var a=Math.min,r=n(6),i=n(157),d=n(69),s=r.canUseDOM&&'selection'in document&&!('getSelection'in window),p={getOffsets:s?function(e){var t=document.selection,n=t.createRange(),o=n.text.length,a=n.duplicate();a.moveToElementText(e),a.setEndPoint('EndToStart',n);var r=a.text.length;return{start:r,end:r+o}}:function(e){var t=window.getSelection&&window.getSelection();if(!t||0===t.rangeCount)return null;var n=t.anchorNode,a=t.anchorOffset,r=t.focusNode,i=t.focusOffset,d=t.getRangeAt(0);try{d.startContainer.nodeType,d.endContainer.nodeType}catch(t){return null}var s=o(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset),p=s?0:d.toString().length,l=d.cloneRange();l.selectNodeContents(e),l.setEnd(d.startContainer,d.startOffset);var u=o(l.startContainer,l.startOffset,l.endContainer,l.endOffset),c=u?0:l.toString().length,m=c+p,h=document.createRange();h.setStart(n,a),h.setEnd(r,i);var g=h.collapsed;return{start:g?m:c,end:g?c:m}},setOffsets:s?function(e,t){var n=document.selection.createRange().duplicate(),o,a;void 0===t.end?(o=t.start,a=o):t.start>t.end?(o=t.end,a=t.start):(o=t.start,a=t.end),n.moveToElementText(e),n.moveStart('character',o),n.setEndPoint('EndToStart',n),n.moveEnd('character',a-o),n.select()}:function(e,t){if(window.getSelection){var n=window.getSelection(),o=e[d()].length,r=a(t.start,o),s=void 0===t.end?r:a(t.end,o);if(!n.extend&&r>s){var p=s;s=r,r=p}var l=i(e,r),u=i(e,s);if(l&&u){var c=document.createRange();c.setStart(l.node,l.offset),n.removeAllRanges(),r>s?(n.addRange(c),n.extend(u.node,u.offset)):(c.setEnd(u.node,u.offset),n.addRange(c))}}}};e.exports=p},function(e){'use strict';function t(e){for(;e&&e.firstChild;)e=e.firstChild;return e}function n(e){for(;e;){if(e.nextSibling)return e.nextSibling;e=e.parentNode}}e.exports=function(e,o){for(var a=t(e),r=0,i=0;a;){if(3===a.nodeType){if(i=r+a.textContent.length,r<=o&&i>=o)return{node:a,offset:o-r};r=i}a=t(n(a))}}},function(e,t,n){'use strict';function o(e,t){return e&&t&&(e===t||!a(e)&&(a(t)?o(e,t.parentNode):'contains'in e?e.contains(t):!!e.compareDocumentPosition&&!!(16&e.compareDocumentPosition(t))))}var a=n(159);e.exports=o},function(e,t,n){'use strict';var o=n(160);e.exports=function(e){return o(e)&&3==e.nodeType}},function(e){'use strict';e.exports=function(e){var t=e?e.ownerDocument||e:document,n=t.defaultView||window;return!!(e&&('function'==typeof n.Node?e instanceof n.Node:'object'==typeof e&&'number'==typeof e.nodeType&&'string'==typeof e.nodeName))}},function(e){'use strict';var t={xlink:'http://www.w3.org/1999/xlink',xml:'http://www.w3.org/XML/1998/namespace'},n={accentHeight:'accent-height',accumulate:0,additive:0,alignmentBaseline:'alignment-baseline',allowReorder:'allowReorder',alphabetic:0,amplitude:0,arabicForm:'arabic-form',ascent:0,attributeName:'attributeName',attributeType:'attributeType',autoReverse:'autoReverse',azimuth:0,baseFrequency:'baseFrequency',baseProfile:'baseProfile',baselineShift:'baseline-shift',bbox:0,begin:0,bias:0,by:0,calcMode:'calcMode',capHeight:'cap-height',clip:0,clipPath:'clip-path',clipRule:'clip-rule',clipPathUnits:'clipPathUnits',colorInterpolation:'color-interpolation',colorInterpolationFilters:'color-interpolation-filters',colorProfile:'color-profile',colorRendering:'color-rendering',contentScriptType:'contentScriptType',contentStyleType:'contentStyleType',cursor:0,cx:0,cy:0,d:0,decelerate:0,descent:0,diffuseConstant:'diffuseConstant',direction:0,display:0,divisor:0,dominantBaseline:'dominant-baseline',dur:0,dx:0,dy:0,edgeMode:'edgeMode',elevation:0,enableBackground:'enable-background',end:0,exponent:0,externalResourcesRequired:'externalResourcesRequired',fill:0,fillOpacity:'fill-opacity',fillRule:'fill-rule',filter:0,filterRes:'filterRes',filterUnits:'filterUnits',floodColor:'flood-color',floodOpacity:'flood-opacity',focusable:0,fontFamily:'font-family',fontSize:'font-size',fontSizeAdjust:'font-size-adjust',fontStretch:'font-stretch',fontStyle:'font-style',fontVariant:'font-variant',fontWeight:'font-weight',format:0,from:0,fx:0,fy:0,g1:0,g2:0,glyphName:'glyph-name',glyphOrientationHorizontal:'glyph-orientation-horizontal',glyphOrientationVertical:'glyph-orientation-vertical',glyphRef:'glyphRef',gradientTransform:'gradientTransform',gradientUnits:'gradientUnits',hanging:0,horizAdvX:'horiz-adv-x',horizOriginX:'horiz-origin-x',ideographic:0,imageRendering:'image-rendering',in:0,in2:0,intercept:0,k:0,k1:0,k2:0,k3:0,k4:0,kernelMatrix:'kernelMatrix',kernelUnitLength:'kernelUnitLength',kerning:0,keyPoints:'keyPoints',keySplines:'keySplines',keyTimes:'keyTimes',lengthAdjust:'lengthAdjust',letterSpacing:'letter-spacing',lightingColor:'lighting-color',limitingConeAngle:'limitingConeAngle',local:0,markerEnd:'marker-end',markerMid:'marker-mid',markerStart:'marker-start',markerHeight:'markerHeight',markerUnits:'markerUnits',markerWidth:'markerWidth',mask:0,maskContentUnits:'maskContentUnits',maskUnits:'maskUnits',mathematical:0,mode:0,numOctaves:'numOctaves',offset:0,opacity:0,operator:0,order:0,orient:0,orientation:0,origin:0,overflow:0,overlinePosition:'overline-position',overlineThickness:'overline-thickness',paintOrder:'paint-order',panose1:'panose-1',pathLength:'pathLength',patternContentUnits:'patternContentUnits',patternTransform:'patternTransform',patternUnits:'patternUnits',pointerEvents:'pointer-events',points:0,pointsAtX:'pointsAtX',pointsAtY:'pointsAtY',pointsAtZ:'pointsAtZ',preserveAlpha:'preserveAlpha',preserveAspectRatio:'preserveAspectRatio',primitiveUnits:'primitiveUnits',r:0,radius:0,refX:'refX',refY:'refY',renderingIntent:'rendering-intent',repeatCount:'repeatCount',repeatDur:'repeatDur',requiredExtensions:'requiredExtensions',requiredFeatures:'requiredFeatures',restart:0,result:0,rotate:0,rx:0,ry:0,scale:0,seed:0,shapeRendering:'shape-rendering',slope:0,spacing:0,specularConstant:'specularConstant',specularExponent:'specularExponent',speed:0,spreadMethod:'spreadMethod',startOffset:'startOffset',stdDeviation:'stdDeviation',stemh:0,stemv:0,stitchTiles:'stitchTiles',stopColor:'stop-color',stopOpacity:'stop-opacity',strikethroughPosition:'strikethrough-position',strikethroughThickness:'strikethrough-thickness',string:0,stroke:0,strokeDasharray:'stroke-dasharray',strokeDashoffset:'stroke-dashoffset',strokeLinecap:'stroke-linecap',strokeLinejoin:'stroke-linejoin',strokeMiterlimit:'stroke-miterlimit',strokeOpacity:'stroke-opacity',strokeWidth:'stroke-width',surfaceScale:'surfaceScale',systemLanguage:'systemLanguage',tableValues:'tableValues',targetX:'targetX',targetY:'targetY',textAnchor:'text-anchor',textDecoration:'text-decoration',textRendering:'text-rendering',textLength:'textLength',to:0,transform:0,u1:0,u2:0,underlinePosition:'underline-position',underlineThickness:'underline-thickness',unicode:0,unicodeBidi:'unicode-bidi',unicodeRange:'unicode-range',unitsPerEm:'units-per-em',vAlphabetic:'v-alphabetic',vHanging:'v-hanging',vIdeographic:'v-ideographic',vMathematical:'v-mathematical',values:0,vectorEffect:'vector-effect',version:0,vertAdvY:'vert-adv-y',vertOriginX:'vert-origin-x',vertOriginY:'vert-origin-y',viewBox:'viewBox',viewTarget:'viewTarget',visibility:0,widths:0,wordSpacing:'word-spacing',writingMode:'writing-mode',x:0,xHeight:'x-height',x1:0,x2:0,xChannelSelector:'xChannelSelector',xlinkActuate:'xlink:actuate',xlinkArcrole:'xlink:arcrole',xlinkHref:'xlink:href',xlinkRole:'xlink:role',xlinkShow:'xlink:show',xlinkTitle:'xlink:title',xlinkType:'xlink:type',xmlBase:'xml:base',xmlns:0,xmlnsXlink:'xmlns:xlink',xmlLang:'xml:lang',xmlSpace:'xml:space',y:0,y1:0,y2:0,yChannelSelector:'yChannelSelector',z:0,zoomAndPan:'zoomAndPan'},o={Properties:{},DOMAttributeNamespaces:{xlinkActuate:t.xlink,xlinkArcrole:t.xlink,xlinkHref:t.xlink,xlinkRole:t.xlink,xlinkShow:t.xlink,xlinkTitle:t.xlink,xlinkType:t.xlink,xmlBase:t.xml,xmlLang:t.xml,xmlSpace:t.xml},DOMAttributeNames:{}};Object.keys(n).forEach(function(e){o.Properties[e]=0,n[e]&&(o.DOMAttributeNames[e]=n[e])}),e.exports=o},function(e,t,n){'use strict';function o(e){if('selectionStart'in e&&s.hasSelectionCapabilities(e))return{start:e.selectionStart,end:e.selectionEnd};if(window.getSelection){var t=window.getSelection();return{anchorNode:t.anchorNode,anchorOffset:t.anchorOffset,focusNode:t.focusNode,focusOffset:t.focusOffset}}if(document.selection){var n=document.selection.createRange();return{parentElement:n.parentElement(),text:n.text,top:n.boundingTop,left:n.boundingLeft}}}function a(e,t){if(_||null==g||g!==l())return null;var n=o(g);if(!y||!c(y,n)){y=n;var a=p.getPooled(h.select,f,e,t);return a.type='select',a.target=g,r.accumulateTwoPhaseDispatches(a),a}return null}var r=n(24),i=n(6),d=n(4),s=n(88),p=n(12),l=n(89),u=n(73),c=n(59),m=i.canUseDOM&&'documentMode'in document&&11>=document.documentMode,h={select:{phasedRegistrationNames:{bubbled:'onSelect',captured:'onSelectCapture'},dependencies:['topBlur','topContextMenu','topFocus','topKeyDown','topKeyUp','topMouseDown','topMouseUp','topSelectionChange']}},g=null,f=null,y=null,_=!1,C=!1;e.exports={eventTypes:h,extractEvents:function(e,t,n,o){if(!C)return null;var r=t?d.getNodeFromInstance(t):window;switch(e){case'topFocus':(u(r)||'true'===r.contentEditable)&&(g=r,f=t,y=null);break;case'topBlur':g=null,f=null,y=null;break;case'topMouseDown':_=!0;break;case'topContextMenu':case'topMouseUp':return _=!1,a(n,o);case'topSelectionChange':if(m)break;case'topKeyDown':case'topKeyUp':return a(n,o);}return null},didPutListener:function(e,t){'onSelect'===t&&(C=!0)}}},function(e,t,n){'use strict';function o(e){return'.'+e._rootNodeID}function a(e){return'button'===e||'input'===e||'select'===e||'textarea'===e}var r=n(2),i=n(87),d=n(24),s=n(4),p=n(164),l=n(165),u=n(12),c=n(166),m=n(167),h=n(30),g=n(169),f=n(170),y=n(171),_=n(26),C=n(172),b=n(5),E=n(64),v=n(0),x={},N={};['abort','animationEnd','animationIteration','animationStart','blur','canPlay','canPlayThrough','click','contextMenu','copy','cut','doubleClick','drag','dragEnd','dragEnter','dragExit','dragLeave','dragOver','dragStart','drop','durationChange','emptied','encrypted','ended','error','focus','input','invalid','keyDown','keyPress','keyUp','load','loadedData','loadedMetadata','loadStart','mouseDown','mouseMove','mouseOut','mouseOver','mouseUp','paste','pause','play','playing','progress','rateChange','reset','scroll','seeked','seeking','stalled','submit','suspend','timeUpdate','touchCancel','touchEnd','touchMove','touchStart','transitionEnd','volumeChange','waiting','wheel'].forEach(function(e){var t=e[0].toUpperCase()+e.slice(1),n='on'+t,o='top'+t,a={phasedRegistrationNames:{bubbled:n,captured:n+'Capture'},dependencies:[o]};x[e]=a,N[o]=a});var T={};e.exports={eventTypes:x,extractEvents:function(e,t,n,o){var a=N[e];if(!a)return null;var i;switch(e){case'topAbort':case'topCanPlay':case'topCanPlayThrough':case'topDurationChange':case'topEmptied':case'topEncrypted':case'topEnded':case'topError':case'topInput':case'topInvalid':case'topLoad':case'topLoadedData':case'topLoadedMetadata':case'topLoadStart':case'topPause':case'topPlay':case'topPlaying':case'topProgress':case'topRateChange':case'topReset':case'topSeeked':case'topSeeking':case'topStalled':case'topSubmit':case'topSuspend':case'topTimeUpdate':case'topVolumeChange':case'topWaiting':i=u;break;case'topKeyPress':if(0===E(n))return null;case'topKeyDown':case'topKeyUp':i=m;break;case'topBlur':case'topFocus':i=c;break;case'topClick':if(2===n.button)return null;case'topDoubleClick':case'topMouseDown':case'topMouseMove':case'topMouseUp':case'topMouseOut':case'topMouseOver':case'topContextMenu':i=h;break;case'topDrag':case'topDragEnd':case'topDragEnter':case'topDragExit':case'topDragLeave':case'topDragOver':case'topDragStart':case'topDrop':i=g;break;case'topTouchCancel':case'topTouchEnd':case'topTouchMove':case'topTouchStart':i=f;break;case'topAnimationEnd':case'topAnimationIteration':case'topAnimationStart':i=p;break;case'topTransitionEnd':i=y;break;case'topScroll':i=_;break;case'topWheel':i=C;break;case'topCopy':case'topCut':case'topPaste':i=l;}i?void 0:r('86',e);var s=i.getPooled(a,t,n,o);return d.accumulateTwoPhaseDispatches(s),s},didPutListener:function(e,t){if('onClick'===t&&!a(e._tag)){var n=o(e),r=s.getNodeFromInstance(e);T[n]||(T[n]=i.listen(r,'click',b))}},willDeleteListener:function(e,t){if('onClick'===t&&!a(e._tag)){var n=o(e);T[n].remove(),delete T[n]}}}},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(12);a.augmentClass(o,{animationName:null,elapsedTime:null,pseudoElement:null}),e.exports=o},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(12);a.augmentClass(o,{clipboardData:function(e){return'clipboardData'in e?e.clipboardData:window.clipboardData}}),e.exports=o},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(26);a.augmentClass(o,{relatedTarget:null}),e.exports=o},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(26),r=n(64),i=n(168),d=n(53);a.augmentClass(o,{key:i,location:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,repeat:null,locale:null,getModifierState:d,charCode:function(e){return'keypress'===e.type?r(e):0},keyCode:function(e){return'keydown'===e.type||'keyup'===e.type?e.keyCode:0},which:function(e){return'keypress'===e.type?r(e):'keydown'===e.type||'keyup'===e.type?e.keyCode:0}}),e.exports=o},function(e,t,n){'use strict';var o=n(64),a={Esc:'Escape',Spacebar:' ',Left:'ArrowLeft',Up:'ArrowUp',Right:'ArrowRight',Down:'ArrowDown',Del:'Delete',Win:'OS',Menu:'ContextMenu',Apps:'ContextMenu',Scroll:'ScrollLock',MozPrintableKey:'Unidentified'},r={8:'Backspace',9:'Tab',12:'Clear',13:'Enter',16:'Shift',17:'Control',18:'Alt',19:'Pause',20:'CapsLock',27:'Escape',32:' ',33:'PageUp',34:'PageDown',35:'End',36:'Home',37:'ArrowLeft',38:'ArrowUp',39:'ArrowRight',40:'ArrowDown',45:'Insert',46:'Delete',112:'F1',113:'F2',114:'F3',115:'F4',116:'F5',117:'F6',118:'F7',119:'F8',120:'F9',121:'F10',122:'F11',123:'F12',144:'NumLock',145:'ScrollLock',224:'Meta'};e.exports=function(e){if(e.key){var t=a[e.key]||e.key;if('Unidentified'!==t)return t}if('keypress'===e.type){var n=o(e);return 13===n?'Enter':String.fromCharCode(n)}return'keydown'===e.type||'keyup'===e.type?r[e.keyCode]||'Unidentified':''}},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(30);a.augmentClass(o,{dataTransfer:null}),e.exports=o},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(26),r=n(53);a.augmentClass(o,{touches:null,targetTouches:null,changedTouches:null,altKey:null,metaKey:null,ctrlKey:null,shiftKey:null,getModifierState:r}),e.exports=o},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(12);a.augmentClass(o,{propertyName:null,elapsedTime:null,pseudoElement:null}),e.exports=o},function(e,t,n){'use strict';function o(e,t,n,o){return a.call(this,e,t,n,o)}var a=n(30);a.augmentClass(o,{deltaX:function(e){return'deltaX'in e?e.deltaX:'wheelDeltaX'in e?-e.wheelDeltaX:0},deltaY:function(e){return'deltaY'in e?e.deltaY:'wheelDeltaY'in e?-e.wheelDeltaY:'wheelDelta'in e?-e.wheelDelta:0},deltaZ:null,deltaMode:null}),e.exports=o},function(e,t,n){'use strict';var o=n(63);e.exports=function(e,t){var n={_topLevelWrapper:e,_idCounter:1,_ownerDocument:t?t.nodeType===9?t:t.ownerDocument:null,_node:t,_tag:t?t.nodeName.toLowerCase():null,_namespaceURI:t?t.namespaceURI:null};return!1,n}},function(e){'use strict';e.exports={useCreateElement:!0,useFiber:!1}},function(e,t,n){'use strict';var o=n(176),a=/\/?>/,r=/^<\!\-\-/,i={CHECKSUM_ATTR_NAME:'data-react-checksum',addChecksumToMarkup:function(e){var t=o(e);return r.test(e)?e:e.replace(a,' '+i.CHECKSUM_ATTR_NAME+'="'+t+'"$&')},canReuseMarkup:function(e,t){var n=t.getAttribute(i.CHECKSUM_ATTR_NAME);n=n&&parseInt(n,10);var a=o(e);return a===n}};e.exports=i},function(e){'use strict';var t=65521;e.exports=function(e){for(var o=1,a=0,r=0,i=e.length,d=-4&i;r<d;){for(var s=Math.min(r+4096,d);r<s;r+=4)a+=(o+=e.charCodeAt(r))+(o+=e.charCodeAt(r+1))+(o+=e.charCodeAt(r+2))+(o+=e.charCodeAt(r+3));o%=t,a%=t}for(;r<i;r++)a+=o+=e.charCodeAt(r);return o%=t,a%=t,o|a<<16}},function(e){'use strict';e.exports='15.6.1'},function(e,t,n){'use strict';var o=n(2),a=n(8),r=n(4),i=n(27),d=n(91),s=n(0),p=n(1);e.exports=function(e){if(null==e)return null;if(1===e.nodeType)return e;var t=i.get(e);return t?(t=d(t),t?r.getNodeFromInstance(t):null):void('function'==typeof e.render?o('44'):o('45',Object.keys(e)))}},function(e,t,n){'use strict';var o=n(90);e.exports=o.renderSubtreeIntoContainer}]);this.EXPORTED_SYMBOLS = ["ReactDOM"]; \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/vendor/classnames.js @@ -0,0 +1,1 @@ +/* eslint-disable */this.classnames=function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={i:d,l:!1,exports:{}};return a[d].call(e.exports,e,e.exports,b),e.l=!0,e.exports}var c={};return b.m=a,b.c=c,b.d=function(a,c,d){b.o(a,c)||Object.defineProperty(a,c,{configurable:!1,enumerable:!0,get:d})},b.n=function(a){var c=a&&a.__esModule?function(){return a['default']}:function(){return a};return b.d(c,'a',c),c},b.o=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)},b.p='',b(b.s=92)}({92:function(a,b){var c,d;(function(){'use strict';function e(){for(var a,b=[],c=0;c<arguments.length;c++)if(a=arguments[c],a){var d=typeof a;if('string'==d||'number'==d)b.push(a);else if(Array.isArray(a))b.push(e.apply(null,a));else if('object'==d)for(var g in a)f.call(a,g)&&a[g]&&b.push(g)}return b.join(' ')}var f={}.hasOwnProperty;'undefined'!=typeof a&&a.exports?a.exports=e:(c=[],d=function(){return e}.apply(b,c),!(d!==void 0&&(a.exports=d)))})()}});this.EXPORTED_SYMBOLS = ["classnames"]; \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/extensions/shield-recipe-client/vendor/mozjexl.js @@ -0,0 +1,1 @@ +/* eslint-disable */this.mozjexl=function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={i:d,l:!1,exports:{}};return a[d].call(e.exports,e,e.exports,b),e.l=!0,e.exports}var c={};return b.m=a,b.c=c,b.d=function(a,c,d){b.o(a,c)||Object.defineProperty(a,c,{configurable:!1,enumerable:!0,get:d})},b.n=function(a){var c=a&&a.__esModule?function(){return a['default']}:function(){return a};return b.d(c,'a',c),c},b.o=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)},b.p='',b(b.s=93)}({65:function(a,b){b.argVal=function(a){this._cursor.args.push(a)},b.arrayStart=function(){this._placeAtCursor({type:'ArrayLiteral',value:[]})},b.arrayVal=function(a){a&&this._cursor.value.push(a)},b.binaryOp=function(a){for(var b=this._grammar[a.value].precedence||0,c=this._cursor._parent;c&&c.operator&&this._grammar[c.operator].precedence>=b;)this._cursor=c,c=c._parent;var d={type:'BinaryExpression',operator:a.value,left:this._cursor};this._setParent(this._cursor,d),this._cursor=c,this._placeAtCursor(d)},b.dot=function(){this._nextIdentEncapsulate=this._cursor&&('BinaryExpression'!=this._cursor.type||'BinaryExpression'==this._cursor.type&&this._cursor.right)&&'UnaryExpression'!=this._cursor.type,this._nextIdentRelative=!this._cursor||this._cursor&&!this._nextIdentEncapsulate,this._nextIdentRelative&&(this._relative=!0)},b.filter=function(a){this._placeBeforeCursor({type:'FilterExpression',expr:a,relative:this._subParser.isRelative(),subject:this._cursor})},b.identifier=function(a){var b={type:'Identifier',value:a.value};this._nextIdentEncapsulate?(b.from=this._cursor,this._placeBeforeCursor(b),this._nextIdentEncapsulate=!1):(this._nextIdentRelative&&(b.relative=!0),this._placeAtCursor(b))},b.literal=function(a){this._placeAtCursor({type:'Literal',value:a.value})},b.objKey=function(a){this._curObjKey=a.value},b.objStart=function(){this._placeAtCursor({type:'ObjectLiteral',value:{}})},b.objVal=function(a){this._cursor.value[this._curObjKey]=a},b.subExpression=function(a){this._placeAtCursor(a)},b.ternaryEnd=function(a){this._cursor.alternate=a},b.ternaryMid=function(a){this._cursor.consequent=a},b.ternaryStart=function(){this._tree={type:'ConditionalExpression',test:this._tree},this._cursor=this._tree},b.transform=function(a){this._placeBeforeCursor({type:'Transform',name:a.value,args:[],subject:this._cursor})},b.unaryOp=function(a){this._placeAtCursor({type:'UnaryExpression',operator:a.value})}},93:function(a,b,c){function d(){this._customGrammar=null,this._lexer=null,this._transforms={}}var e=c(94),f=c(96),g=c(97),h=c(99).elements;d.prototype.addBinaryOp=function(a,b,c){this._addGrammarElement(a,{type:'binaryOp',precedence:b,eval:c})},d.prototype.addUnaryOp=function(a,b){this._addGrammarElement(a,{type:'unaryOp',weight:Infinity,eval:b})},d.prototype.addTransform=function(a,b){this._transforms[a]=b},d.prototype.addTransforms=function(a){for(var b in a)a.hasOwnProperty(b)&&(this._transforms[b]=a[b])},d.prototype.getTransform=function(a){return this._transforms[a]},d.prototype.eval=function(a,b,c){'function'==typeof b?(c=b,b={}):!b&&(b={});var d=this._eval(a,b);if(c){var e=!1;return d.then(function(a){e=!0,setTimeout(c.bind(null,null,a),0)}).catch(function(a){e||setTimeout(c.bind(null,a),0)})}return d},d.prototype.removeOp=function(a){var b=this._getCustomGrammar();b[a]&&('binaryOp'==b[a].type||'unaryOp'==b[a].type)&&(delete b[a],this._lexer=null)},d.prototype._addGrammarElement=function(a,b){var c=this._getCustomGrammar();c[a]=b,this._lexer=null},d.prototype._eval=function(a,b){var c=this,d=this._getGrammar(),f=new g(d),h=new e(d,this._transforms,b);return Promise.resolve().then(function(){return f.addTokens(c._getLexer().tokenize(a)),h.eval(f.complete())})},d.prototype._getCustomGrammar=function(){if(!this._customGrammar)for(var a in this._customGrammar={},h)h.hasOwnProperty(a)&&(this._customGrammar[a]=h[a]);return this._customGrammar},d.prototype._getGrammar=function(){return this._customGrammar||h},d.prototype._getLexer=function(){return this._lexer||(this._lexer=new f(this._getGrammar())),this._lexer},a.exports=new d,a.exports.Jexl=d},94:function(a,b,c){var d=c(95),e=function(a,b,c,d){this._grammar=a,this._transforms=b||{},this._context=c||{},this._relContext=d||this._context};e.prototype.eval=function(a){var b=this;return Promise.resolve().then(function(){return d[a.type].call(b,a)})},e.prototype.evalArray=function(a){return Promise.all(a.map(function(a){return this.eval(a)},this))},e.prototype.evalMap=function(a){var b=Object.keys(a),c={},d=b.map(function(b){return this.eval(a[b])},this);return Promise.all(d).then(function(a){return a.forEach(function(a,d){c[b[d]]=a}),c})},e.prototype._filterRelative=function(a,b){if(void 0!==a){var c=[];return Array.isArray(a)||(a=[a]),a.forEach(function(a){var d=new e(this._grammar,this._transforms,this._context,a);c.push(d.eval(b))},this),Promise.all(c).then(function(b){var c=[];return b.forEach(function(b,d){b&&c.push(a[d])}),c})}},e.prototype._filterStatic=function(a,b){return this.eval(b).then(function(b){return'boolean'==typeof b?b?a:void 0:void 0===a?void 0:a[b]})},a.exports=e},95:function(a,b){b.ArrayLiteral=function(a){return this.evalArray(a.value)},b.BinaryExpression=function(a){var b=this;return Promise.all([this.eval(a.left),this.eval(a.right)]).then(function(c){return b._grammar[a.operator].eval(c[0],c[1])})},b.ConditionalExpression=function(a){var b=this;return this.eval(a.test).then(function(c){return c?a.consequent?b.eval(a.consequent):c:b.eval(a.alternate)})},b.FilterExpression=function(a){var b=this;return this.eval(a.subject).then(function(c){return a.relative?b._filterRelative(c,a.expr):b._filterStatic(c,a.expr)})},b.Identifier=function(a){return a.from?this.eval(a.from).then(function(b){if(void 0!==b)return Array.isArray(b)&&(b=b[0]),b[a.value]}):a.relative?this._relContext[a.value]:this._context[a.value]},b.Literal=function(a){return a.value},b.ObjectLiteral=function(a){return this.evalMap(a.value)},b.Transform=function(a){var b=this._transforms[a.name];if(!b)throw new Error('Transform \''+a.name+'\' is not defined.');return Promise.all([this.eval(a.subject),this.evalArray(a.args||[])]).then(function(a){return b.apply(null,[a[0]].concat(a[1]))})},b.UnaryExpression=function(a){var b=this;return this.eval(a.right).then(function(c){return b._grammar[a.operator].eval(c)})}},96:function(a){function b(a){this._grammar=a}var c=/^-?(?:(?:[0-9]*\.[0-9]+)|[0-9]+)$/,d=/^[a-zA-Z_\$][a-zA-Z0-9_\$]*$/,e=/\\\\/,f=['\'(?:(?:\\\\\')?[^\'])*\'','"(?:(?:\\\\")?[^"])*"','\\s+','\\btrue\\b','\\bfalse\\b'],g=['\\b[a-zA-Z_\\$][a-zA-Z0-9_\\$]*\\b','(?:(?:[0-9]*\\.[0-9]+)|[0-9]+)'],h=['binaryOp','unaryOp','openParen','openBracket','question','colon'];b.prototype.getElements=function(a){var b=this._getSplitRegex();return a.split(b).filter(function(a){return a})},b.prototype.getTokens=function(a){for(var b=[],c=!1,d=0;d<a.length;d++)this._isWhitespace(a[d])?b.length&&(b[b.length-1].raw+=a[d]):'-'===a[d]&&this._isNegative(b)?c=!0:(c&&(a[d]='-'+a[d],c=!1),b.push(this._createToken(a[d])));return c&&b.push(this._createToken('-')),b},b.prototype.tokenize=function(a){var b=this.getElements(a);return this.getTokens(b)},b.prototype._createToken=function(a){var b={type:'literal',value:a,raw:a};if('"'==a[0]||'\''==a[0])b.value=this._unquote(a);else if(a.match(c))b.value=parseFloat(a);else if('true'===a||'false'===a)b.value='true'==a;else if(this._grammar[a])b.type=this._grammar[a].type;else if(a.match(d))b.type='identifier';else throw new Error('Invalid expression token: '+a);return b},b.prototype._escapeRegExp=function(a){return a=a.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),a.match(d)&&(a='\\b'+a+'\\b'),a},b.prototype._getSplitRegex=function(){if(!this._splitRegex){var a=Object.keys(this._grammar);a=a.sort(function(c,a){return a.length-c.length}).map(function(a){return this._escapeRegExp(a)},this),this._splitRegex=new RegExp('('+[f.join('|'),a.join('|'),g.join('|')].join('|')+')')}return this._splitRegex},b.prototype._isNegative=function(a){return!a.length||h.some(function(b){return b===a[a.length-1].type})};var i=/^\s*$/;b.prototype._isWhitespace=function(a){return i.test(a)},b.prototype._unquote=function(a){var b=a[0],c=new RegExp('\\\\'+b,'g');return a.substr(1,a.length-2).replace(c,b).replace(e,'\\')},a.exports=b},97:function(a,b,c){function d(a,b,c){this._grammar=a,this._state='expectOperand',this._tree=null,this._exprStr=b||'',this._relative=!1,this._stopMap=c||{}}var e=c(65),f=c(98).states;d.prototype.addToken=function(a){if('complete'==this._state)throw new Error('Cannot add a new token to a completed Parser');var b=f[this._state],c=this._exprStr;if(this._exprStr+=a.raw,b.subHandler){this._subParser||this._startSubExpression(c);var d=this._subParser.addToken(a);if(d){if(this._endSubExpression(),this._parentStop)return d;this._state=d}}else if(b.tokenTypes[a.type]){var g=b.tokenTypes[a.type],h=e[a.type];g.handler&&(h=g.handler),h&&h.call(this,a),g.toState&&(this._state=g.toState)}else{if(this._stopMap[a.type])return this._stopMap[a.type];throw new Error('Token '+a.raw+' ('+a.type+') unexpected in expression: '+this._exprStr)}return!1},d.prototype.addTokens=function(a){a.forEach(this.addToken,this)},d.prototype.complete=function(){if(this._cursor&&!f[this._state].completable)throw new Error('Unexpected end of expression: '+this._exprStr);return this._subParser&&this._endSubExpression(),this._state='complete',this._cursor?this._tree:null},d.prototype.isRelative=function(){return this._relative},d.prototype._endSubExpression=function(){f[this._state].subHandler.call(this,this._subParser.complete()),this._subParser=null},d.prototype._placeAtCursor=function(a){this._cursor?(this._cursor.right=a,this._setParent(a,this._cursor)):this._tree=a,this._cursor=a},d.prototype._placeBeforeCursor=function(a){this._cursor=this._cursor._parent,this._placeAtCursor(a)},d.prototype._setParent=function(a,b){Object.defineProperty(a,'_parent',{value:b,writable:!0})},d.prototype._startSubExpression=function(a){var b=f[this._state].endStates;b||(this._parentStop=!0,b=this._stopMap),this._subParser=new d(this._grammar,a,b)},a.exports=d},98:function(a,b,c){var d=c(65);b.states={expectOperand:{tokenTypes:{literal:{toState:'expectBinOp'},identifier:{toState:'identifier'},unaryOp:{},openParen:{toState:'subExpression'},openCurl:{toState:'expectObjKey',handler:d.objStart},dot:{toState:'traverse'},openBracket:{toState:'arrayVal',handler:d.arrayStart}}},expectBinOp:{tokenTypes:{binaryOp:{toState:'expectOperand'},pipe:{toState:'expectTransform'},dot:{toState:'traverse'},question:{toState:'ternaryMid',handler:d.ternaryStart}},completable:!0},expectTransform:{tokenTypes:{identifier:{toState:'postTransform',handler:d.transform}}},expectObjKey:{tokenTypes:{identifier:{toState:'expectKeyValSep',handler:d.objKey},closeCurl:{toState:'expectBinOp'}}},expectKeyValSep:{tokenTypes:{colon:{toState:'objVal'}}},postTransform:{tokenTypes:{openParen:{toState:'argVal'},binaryOp:{toState:'expectOperand'},dot:{toState:'traverse'},openBracket:{toState:'filter'},pipe:{toState:'expectTransform'}},completable:!0},postTransformArgs:{tokenTypes:{binaryOp:{toState:'expectOperand'},dot:{toState:'traverse'},openBracket:{toState:'filter'},pipe:{toState:'expectTransform'}},completable:!0},identifier:{tokenTypes:{binaryOp:{toState:'expectOperand'},dot:{toState:'traverse'},openBracket:{toState:'filter'},pipe:{toState:'expectTransform'},question:{toState:'ternaryMid',handler:d.ternaryStart}},completable:!0},traverse:{tokenTypes:{identifier:{toState:'identifier'}}},filter:{subHandler:d.filter,endStates:{closeBracket:'identifier'}},subExpression:{subHandler:d.subExpression,endStates:{closeParen:'expectBinOp'}},argVal:{subHandler:d.argVal,endStates:{comma:'argVal',closeParen:'postTransformArgs'}},objVal:{subHandler:d.objVal,endStates:{comma:'expectObjKey',closeCurl:'expectBinOp'}},arrayVal:{subHandler:d.arrayVal,endStates:{comma:'arrayVal',closeBracket:'expectBinOp'}},ternaryMid:{subHandler:d.ternaryMid,endStates:{colon:'ternaryEnd'}},ternaryEnd:{subHandler:d.ternaryEnd,completable:!0}}},99:function(a,b){b.elements={".":{type:'dot'},"[":{type:'openBracket'},"]":{type:'closeBracket'},"|":{type:'pipe'},"{":{type:'openCurl'},"}":{type:'closeCurl'},":":{type:'colon'},",":{type:'comma'},"(":{type:'openParen'},")":{type:'closeParen'},"?":{type:'question'},"+":{type:'binaryOp',precedence:30,eval:function(a,b){return a+b}},"-":{type:'binaryOp',precedence:30,eval:function(a,b){return a-b}},"*":{type:'binaryOp',precedence:40,eval:function(a,b){return a*b}},"/":{type:'binaryOp',precedence:40,eval:function(a,b){return a/b}},"//":{type:'binaryOp',precedence:40,eval:function(a,b){return Math.floor(a/b)}},"%":{type:'binaryOp',precedence:50,eval:function(a,b){return a%b}},"^":{type:'binaryOp',precedence:50,eval:function(a,b){return Math.pow(a,b)}},"==":{type:'binaryOp',precedence:20,eval:function(a,b){return a==b}},"!=":{type:'binaryOp',precedence:20,eval:function(a,b){return a!=b}},">":{type:'binaryOp',precedence:20,eval:function(a,b){return a>b}},">=":{type:'binaryOp',precedence:20,eval:function(a,b){return a>=b}},"<":{type:'binaryOp',precedence:20,eval:function(a,b){return a<b}},"<=":{type:'binaryOp',precedence:20,eval:function(a,b){return a<=b}},"&&":{type:'binaryOp',precedence:10,eval:function(a,b){return a&&b}},"||":{type:'binaryOp',precedence:10,eval:function(a,b){return a||b}},in:{type:'binaryOp',precedence:20,eval:function(a,b){return'string'==typeof b?-1!==b.indexOf(a):!!Array.isArray(b)&&b.some(function(b){return b==a})}},"!":{type:'unaryOp',precedence:Infinity,eval:function(a){return!a}}}}});this.EXPORTED_SYMBOLS = ["mozjexl"]; \ No newline at end of file