зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1308656 - Add shield-recipe-client as system add-on r=Gijs,rhelmer
MozReview-Commit-ID: KNTGKOFXDlH --HG-- extra : rebase_source : 5b7ac9e5a1c004b1123b852e7b59729357a1dae8
This commit is contained in:
Родитель
ddd0e3be33
Коммит
a443eb66e9
|
@ -10,6 +10,7 @@ DIRS += [
|
||||||
'pdfjs',
|
'pdfjs',
|
||||||
'pocket',
|
'pocket',
|
||||||
'webcompat',
|
'webcompat',
|
||||||
|
'shield-recipe-client',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Only include the following system add-ons if building Aurora or Nightly
|
# Only include the following system add-ons if building Aurora or Nightly
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
/* 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/Preferences.jsm");
|
||||||
|
|
||||||
|
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 PREFS = {
|
||||||
|
api_url: "https://self-repair.mozilla.org/api/v1",
|
||||||
|
dev_mode: false,
|
||||||
|
enabled: true,
|
||||||
|
startup_delay_seconds: 300,
|
||||||
|
};
|
||||||
|
const PREF_DEV_MODE = "extensions.shield-recipe-client.dev_mode";
|
||||||
|
const PREF_SELF_SUPPORT_ENABLED = "browser.selfsupport.enabled";
|
||||||
|
|
||||||
|
let shouldRun = true;
|
||||||
|
|
||||||
|
this.install = function() {
|
||||||
|
// Self Repair only checks its pref on start, so if we disable it, wait until
|
||||||
|
// next startup to run, unless the dev_mode preference is set.
|
||||||
|
if (Preferences.get(PREF_SELF_SUPPORT_ENABLED, true)) {
|
||||||
|
Preferences.set(PREF_SELF_SUPPORT_ENABLED, false);
|
||||||
|
if (!Services.prefs.getBoolPref(PREF_DEV_MODE, false)) {
|
||||||
|
shouldRun = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.startup = function() {
|
||||||
|
setDefaultPrefs();
|
||||||
|
|
||||||
|
if (!shouldRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm");
|
||||||
|
RecipeRunner.init();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.shutdown = function(data, reason) {
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm");
|
||||||
|
|
||||||
|
CleanupManager.cleanup();
|
||||||
|
|
||||||
|
if (reason === REASONS.ADDON_DISABLE || reason === REASONS.ADDON_UNINSTALL) {
|
||||||
|
Services.prefs.setBoolPref(PREF_SELF_SUPPORT_ENABLED, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modules = [
|
||||||
|
"data/EventEmitter.js",
|
||||||
|
"lib/CleanupManager.jsm",
|
||||||
|
"lib/EnvExpressions.jsm",
|
||||||
|
"lib/Heartbeat.jsm",
|
||||||
|
"lib/NormandyApi.jsm",
|
||||||
|
"lib/NormandyDriver.jsm",
|
||||||
|
"lib/RecipeRunner.jsm",
|
||||||
|
"lib/Sampling.jsm",
|
||||||
|
"lib/SandboxManager.jsm",
|
||||||
|
"lib/Storage.jsm",
|
||||||
|
];
|
||||||
|
for (const module in modules) {
|
||||||
|
Cu.unload(`resource://shield-recipe-client/${module}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.uninstall = function() {
|
||||||
|
};
|
||||||
|
|
||||||
|
function setDefaultPrefs() {
|
||||||
|
const branch = Services.prefs.getDefaultBranch(PREF_BRANCH);
|
||||||
|
for (const [key, val] of Object.entries(PREFS)) {
|
||||||
|
// If someone beat us to setting a default, don't overwrite it.
|
||||||
|
if (branch.getPrefType(key) !== branch.PREF_INVALID)
|
||||||
|
continue;
|
||||||
|
switch (typeof val) {
|
||||||
|
case "boolean":
|
||||||
|
branch.setBoolPref(key, val);
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
branch.setIntPref(key, val);
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
branch.setCharPref(key, val);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
// This file is meant to run inside action sandboxes
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
this.EventEmitter = function(driver) {
|
||||||
|
if (!driver) {
|
||||||
|
throw new Error("driver must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
emit(eventName, event) {
|
||||||
|
// Fire events async
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
if (!(eventName in listeners)) {
|
||||||
|
driver.log(`EventEmitter: Event fired with no listeners: ${eventName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// freeze event to prevent handlers from modifying it
|
||||||
|
const frozenEvent = Object.freeze(event);
|
||||||
|
// Clone callbacks array to avoid problems with mutation while iterating
|
||||||
|
const callbacks = Array.from(listeners[eventName]);
|
||||||
|
for (const cb of callbacks) {
|
||||||
|
cb(frozenEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
on(eventName, callback) {
|
||||||
|
if (!(eventName in listeners)) {
|
||||||
|
listeners[eventName] = [];
|
||||||
|
}
|
||||||
|
listeners[eventName].push(callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
off(eventName, callback) {
|
||||||
|
if (eventName in listeners) {
|
||||||
|
const index = listeners[eventName].indexOf(callback);
|
||||||
|
if (index !== -1) {
|
||||||
|
listeners[eventName].splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
once(eventName, callback) {
|
||||||
|
const inner = event => {
|
||||||
|
callback(event);
|
||||||
|
this.off(eventName, inner);
|
||||||
|
};
|
||||||
|
this.on(eventName, inner);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
#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>1.0.0</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>
|
||||||
|
<em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
|
||||||
|
</Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
</Description>
|
||||||
|
</RDF>
|
|
@ -0,0 +1,9 @@
|
||||||
|
# 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/data/ (./data/*)
|
||||||
|
content/node_modules/jexl/ (./node_modules/jexl/*)
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["CleanupManager"];
|
||||||
|
|
||||||
|
const cleanupHandlers = [];
|
||||||
|
|
||||||
|
this.CleanupManager = {
|
||||||
|
addCleanupHandler(handler) {
|
||||||
|
cleanupHandlers.push(handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
for (const handler of cleanupHandlers) {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,65 @@
|
||||||
|
/* 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");
|
||||||
|
Cu.import("resource://gre/modules/TelemetryArchive.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/Sampling.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Log.jsm");
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["EnvExpressions"];
|
||||||
|
|
||||||
|
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();
|
||||||
|
jexl.addTransforms({
|
||||||
|
date: dateString => new Date(dateString),
|
||||||
|
stableSample: Sampling.stableSample,
|
||||||
|
});
|
||||||
|
return jexl;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getLatestTelemetry = Task.async(function *() {
|
||||||
|
const pings = yield TelemetryArchive.promiseArchivedPingList();
|
||||||
|
|
||||||
|
// get most recent ping per type
|
||||||
|
const mostRecentPings = {};
|
||||||
|
for (const ping of pings) {
|
||||||
|
if (ping.type in mostRecentPings) {
|
||||||
|
if (mostRecentPings[ping.type].timeStampCreated < ping.timeStampCreated) {
|
||||||
|
mostRecentPings[ping.type] = ping;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mostRecentPings[ping.type] = ping;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const telemetry = {};
|
||||||
|
for (const key in mostRecentPings) {
|
||||||
|
const ping = mostRecentPings[key];
|
||||||
|
telemetry[ping.type] = yield TelemetryArchive.promiseArchivedPingById(ping.id);
|
||||||
|
}
|
||||||
|
return telemetry;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.EnvExpressions = {
|
||||||
|
eval(expr, extraContext = {}) {
|
||||||
|
const context = Object.assign({telemetry: getLatestTelemetry()}, extraContext);
|
||||||
|
const onelineExpr = expr.replace(/[\t\n\r]/g, " ");
|
||||||
|
return jexl.eval(onelineExpr, context);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,346 @@
|
||||||
|
/* 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/Preferences.jsm");
|
||||||
|
Cu.import("resource://gre/modules/TelemetryController.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Timer.jsm"); /* globals setTimeout, clearTimeout */
|
||||||
|
Cu.import("resource://gre/modules/Log.jsm");
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm");
|
||||||
|
|
||||||
|
Cu.importGlobalProperties(["URL"]); /* globals URL */
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["Heartbeat"];
|
||||||
|
|
||||||
|
const log = Log.repository.getLogger("shield-recipe-client");
|
||||||
|
const PREF_SURVEY_DURATION = "browser.uitour.surveyDuration";
|
||||||
|
const NOTIFICATION_TIME = 3000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the Heartbeat UI to request user feedback.
|
||||||
|
*
|
||||||
|
* @param chromeWindow
|
||||||
|
* The chrome window that the heartbeat notification is displayed in.
|
||||||
|
* @param eventEmitter
|
||||||
|
* An EventEmitter instance to report status to.
|
||||||
|
* @param sandboxManager
|
||||||
|
* The manager for the sandbox this was called from. Heartbeat will
|
||||||
|
* increment the hold counter on the manager.
|
||||||
|
* @param {Object} options Options object.
|
||||||
|
* @param {String} options.message
|
||||||
|
* The message, or question, to display on the notification.
|
||||||
|
* @param {String} options.thanksMessage
|
||||||
|
* The thank you message to display after user votes.
|
||||||
|
* @param {String} options.flowId
|
||||||
|
* An identifier for this rating flow. Please note that this is only used to
|
||||||
|
* identify the notification box.
|
||||||
|
* @param {String} [options.engagementButtonLabel=null]
|
||||||
|
* The text of the engagement button to use instad of stars. If this is null
|
||||||
|
* or invalid, rating stars are used.
|
||||||
|
* @param {String} [options.learnMoreMessage=null]
|
||||||
|
* The label of the learn more link. No link will be shown if this is null.
|
||||||
|
* @param {String} [options.learnMoreUrl=null]
|
||||||
|
* The learn more URL to open when clicking on the learn more link. No learn more
|
||||||
|
* will be shown if this is an invalid URL.
|
||||||
|
* @param {String} [options.surveyId]
|
||||||
|
* An ID for the survey, reflected in the Telemetry ping.
|
||||||
|
* @param {Number} [options.surveyVersion]
|
||||||
|
* Survey's version number, reflected in the Telemetry ping.
|
||||||
|
* @param {boolean} [options.testing]
|
||||||
|
* Whether this is a test survey, reflected in the Telemetry ping.
|
||||||
|
* @param {String} [options.postAnswerURL=null]
|
||||||
|
* The url to visit after the user answers the question.
|
||||||
|
*/
|
||||||
|
this.Heartbeat = class {
|
||||||
|
constructor(chromeWindow, eventEmitter, sandboxManager, options) {
|
||||||
|
if (typeof options.flowId !== "string") {
|
||||||
|
throw new Error("flowId must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.flowId) {
|
||||||
|
throw new Error("flowId must not be an empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.message !== "string") {
|
||||||
|
throw new Error("message must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.message) {
|
||||||
|
throw new Error("message must not be an empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sandboxManager) {
|
||||||
|
throw new Error("sandboxManager must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.postAnswerUrl) {
|
||||||
|
options.postAnswerUrl = new URL(options.postAnswerUrl);
|
||||||
|
} else {
|
||||||
|
options.postAnswerUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.learnMoreUrl) {
|
||||||
|
try {
|
||||||
|
options.learnMoreUrl = new URL(options.learnMoreUrl);
|
||||||
|
} catch (e) {
|
||||||
|
options.learnMoreUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chromeWindow = chromeWindow;
|
||||||
|
this.eventEmitter = eventEmitter;
|
||||||
|
this.sandboxManager = sandboxManager;
|
||||||
|
this.options = options;
|
||||||
|
this.surveyResults = {};
|
||||||
|
this.buttons = null;
|
||||||
|
|
||||||
|
// so event handlers are consistent
|
||||||
|
this.handleWindowClosed = this.handleWindowClosed.bind(this);
|
||||||
|
|
||||||
|
if (this.options.engagementButtonLabel) {
|
||||||
|
this.buttons = [{
|
||||||
|
label: this.options.engagementButtonLabel,
|
||||||
|
callback: () => {
|
||||||
|
// Let the consumer know user engaged.
|
||||||
|
this.maybeNotifyHeartbeat("Engaged");
|
||||||
|
|
||||||
|
this.userEngaged({
|
||||||
|
type: "button",
|
||||||
|
flowId: this.options.flowId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return true so that the notification bar doesn't close itself since
|
||||||
|
// we have a thank you message to show.
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notificationBox = this.chromeWindow.document.querySelector("#high-priority-global-notificationbox");
|
||||||
|
this.notice = this.notificationBox.appendNotification(
|
||||||
|
this.options.message,
|
||||||
|
"heartbeat-" + this.options.flowId,
|
||||||
|
"chrome://browser/skin/heartbeat-icon.svg",
|
||||||
|
this.notificationBox.PRIORITY_INFO_HIGH,
|
||||||
|
this.buttons,
|
||||||
|
eventType => {
|
||||||
|
if (eventType !== "removed") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.maybeNotifyHeartbeat("NotificationClosed");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Holds the rating UI
|
||||||
|
const frag = this.chromeWindow.document.createDocumentFragment();
|
||||||
|
|
||||||
|
// Build the heartbeat stars
|
||||||
|
if (!this.options.engagementButtonLabel) {
|
||||||
|
const numStars = this.options.engagementButtonLabel ? 0 : 5;
|
||||||
|
const ratingContainer = this.chromeWindow.document.createElement("hbox");
|
||||||
|
ratingContainer.id = "star-rating-container";
|
||||||
|
|
||||||
|
for (let i = 0; i < numStars; i++) {
|
||||||
|
// create a star rating element
|
||||||
|
const ratingElement = this.chromeWindow.document.createElement("toolbarbutton");
|
||||||
|
|
||||||
|
// style it
|
||||||
|
const starIndex = numStars - i;
|
||||||
|
ratingElement.className = "plain star-x";
|
||||||
|
ratingElement.id = "star" + starIndex;
|
||||||
|
ratingElement.setAttribute("data-score", starIndex);
|
||||||
|
|
||||||
|
// Add the click handler
|
||||||
|
ratingElement.addEventListener("click", ev => {
|
||||||
|
const rating = parseInt(ev.target.getAttribute("data-score"));
|
||||||
|
this.maybeNotifyHeartbeat("Voted", {score: rating});
|
||||||
|
this.userEngaged({type: "stars", score: rating, flowId: this.options.flowId});
|
||||||
|
});
|
||||||
|
|
||||||
|
ratingContainer.appendChild(ratingElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
frag.appendChild(ratingContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageImage = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "messageImage");
|
||||||
|
this.messageImage.classList.add("heartbeat", "pulse-onshow");
|
||||||
|
|
||||||
|
this.messageText = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "messageText");
|
||||||
|
this.messageText.classList.add("heartbeat");
|
||||||
|
|
||||||
|
// Make sure the stars are not pushed to the right by the spacer.
|
||||||
|
const rightSpacer = this.chromeWindow.document.createElement("spacer");
|
||||||
|
rightSpacer.flex = 20;
|
||||||
|
frag.appendChild(rightSpacer);
|
||||||
|
|
||||||
|
// collapse the space before the stars
|
||||||
|
this.messageText.flex = 0;
|
||||||
|
const leftSpacer = this.messageText.nextSibling;
|
||||||
|
leftSpacer.flex = 0;
|
||||||
|
|
||||||
|
// Add Learn More Link
|
||||||
|
if (this.options.learnMoreMessage && this.options.learnMoreUrl) {
|
||||||
|
const learnMore = this.chromeWindow.document.createElement("label");
|
||||||
|
learnMore.className = "text-link";
|
||||||
|
learnMore.href = this.options.learnMoreUrl.toString();
|
||||||
|
learnMore.setAttribute("value", this.options.learnMoreMessage);
|
||||||
|
learnMore.addEventListener("click", () => this.maybeNotifyHeartbeat("LearnMore"));
|
||||||
|
frag.appendChild(learnMore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the fragment and apply the styling
|
||||||
|
this.notice.appendChild(frag);
|
||||||
|
this.notice.classList.add("heartbeat");
|
||||||
|
|
||||||
|
// Let the consumer know the notification was shown.
|
||||||
|
this.maybeNotifyHeartbeat("NotificationOffered");
|
||||||
|
this.chromeWindow.addEventListener("SSWindowClosing", this.handleWindowClosed);
|
||||||
|
|
||||||
|
const surveyDuration = Preferences.get(PREF_SURVEY_DURATION, 300) * 1000;
|
||||||
|
this.surveyEndTimer = setTimeout(() => {
|
||||||
|
this.maybeNotifyHeartbeat("SurveyExpired");
|
||||||
|
this.close();
|
||||||
|
}, surveyDuration);
|
||||||
|
|
||||||
|
this.sandboxManager.addHold("heartbeat");
|
||||||
|
CleanupManager.addCleanupHandler(() => this.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeNotifyHeartbeat(name, data = {}) {
|
||||||
|
if (this.pingSent) {
|
||||||
|
log.warn("Heartbeat event recieved after Telemetry ping sent. name:", name, "data:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
let sendPing = false;
|
||||||
|
let cleanup = false;
|
||||||
|
|
||||||
|
const phases = {
|
||||||
|
NotificationOffered: () => {
|
||||||
|
this.surveyResults.flowId = this.options.flowId;
|
||||||
|
this.surveyResults.offeredTS = timestamp;
|
||||||
|
},
|
||||||
|
LearnMore: () => {
|
||||||
|
if (!this.surveyResults.learnMoreTS) {
|
||||||
|
this.surveyResults.learnMoreTS = timestamp;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Engaged: () => {
|
||||||
|
this.surveyResults.engagedTS = timestamp;
|
||||||
|
},
|
||||||
|
Voted: () => {
|
||||||
|
this.surveyResults.votedTS = timestamp;
|
||||||
|
this.surveyResults.score = data.score;
|
||||||
|
},
|
||||||
|
SurveyExpired: () => {
|
||||||
|
this.surveyResults.expiredTS = timestamp;
|
||||||
|
},
|
||||||
|
NotificationClosed: () => {
|
||||||
|
this.surveyResults.closedTS = timestamp;
|
||||||
|
cleanup = true;
|
||||||
|
sendPing = true;
|
||||||
|
},
|
||||||
|
WindowClosed: () => {
|
||||||
|
this.surveyResults.windowClosedTS = timestamp;
|
||||||
|
cleanup = true;
|
||||||
|
sendPing = true;
|
||||||
|
},
|
||||||
|
default: () => {
|
||||||
|
log.error("Unrecognized Heartbeat event:", name);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
(phases[name] || phases.default)();
|
||||||
|
|
||||||
|
data.timestamp = timestamp;
|
||||||
|
data.flowId = this.options.flowId;
|
||||||
|
this.eventEmitter.emit(name, Cu.cloneInto(data, this.sandboxManager.sandbox));
|
||||||
|
|
||||||
|
if (sendPing) {
|
||||||
|
// Send the ping to Telemetry
|
||||||
|
const payload = Object.assign({version: 1}, this.surveyResults);
|
||||||
|
for (const meta of ["surveyId", "surveyVersion", "testing"]) {
|
||||||
|
if (this.options.hasOwnProperty(meta)) {
|
||||||
|
payload[meta] = this.options[meta];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Sending telemetry");
|
||||||
|
TelemetryController.submitExternalPing("heartbeat", payload, {
|
||||||
|
addClientId: true,
|
||||||
|
addEnvironment: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// only for testing
|
||||||
|
this.eventEmitter.emit("TelemetrySent", Cu.cloneInto(payload, this.sandboxManager.sandbox));
|
||||||
|
|
||||||
|
// Survey is complete, clear out the expiry timer & survey configuration
|
||||||
|
if (this.surveyEndTimer) {
|
||||||
|
clearTimeout(this.surveyEndTimer);
|
||||||
|
this.surveyEndTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pingSent = true;
|
||||||
|
this.surveyResults = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanup) {
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userEngaged(engagementParams) {
|
||||||
|
// Make the heartbeat icon pulse twice
|
||||||
|
this.notice.label = this.options.thanksMessage;
|
||||||
|
this.messageImage.classList.remove("pulse-onshow");
|
||||||
|
this.messageImage.classList.add("pulse-twice");
|
||||||
|
|
||||||
|
// Remove all the children of the notice (rating container, and the flex)
|
||||||
|
while (this.notice.firstChild) {
|
||||||
|
this.notice.firstChild.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the engagement tab if we have a valid engagement URL.
|
||||||
|
if (this.options.postAnswerUrl) {
|
||||||
|
for (const key in engagementParams) {
|
||||||
|
this.options.postAnswerUrl.searchParams.append(key, engagementParams[key]);
|
||||||
|
}
|
||||||
|
// Open the engagement URL in a new tab.
|
||||||
|
this.chromeWindow.gBrowser.selectedTab = this.chromeWindow.gBrowser.addTab(this.options.postAnswerUrl.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.surveyEndTimer) {
|
||||||
|
clearTimeout(this.surveyEndTimer);
|
||||||
|
this.surveyEndTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => this.close(), NOTIFICATION_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWindowClosed() {
|
||||||
|
this.maybeNotifyHeartbeat("WindowClosed");
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.notificationBox.removeNotification(this.notice);
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.sandboxManager.removeHold("heartbeat");
|
||||||
|
// remove listeners
|
||||||
|
this.chromeWindow.removeEventListener("SSWindowClosing", this.handleWindowClosed);
|
||||||
|
// remove references for garbage collection
|
||||||
|
this.chromeWindow = null;
|
||||||
|
this.notificationBox = null;
|
||||||
|
this.notification = null;
|
||||||
|
this.eventEmitter = null;
|
||||||
|
this.sandboxManager = null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,99 @@
|
||||||
|
/* 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/. */
|
||||||
|
/* globals URLSearchParams */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
Cu.import("resource://gre/modules/CanonicalJSON.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Log.jsm");
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["NormandyApi"];
|
||||||
|
|
||||||
|
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||||
|
const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
|
||||||
|
|
||||||
|
this.NormandyApi = {
|
||||||
|
apiCall(method, endpoint, data = {}) {
|
||||||
|
const api_url = prefs.getCharPref("api_url");
|
||||||
|
let url = `${api_url}/${endpoint}`;
|
||||||
|
method = method.toLowerCase();
|
||||||
|
|
||||||
|
if (method === "get") {
|
||||||
|
if (data === {}) {
|
||||||
|
const paramObj = new URLSearchParams();
|
||||||
|
for (const key in data) {
|
||||||
|
paramObj.append(key, data[key]);
|
||||||
|
}
|
||||||
|
url += "?" + paramObj.toString();
|
||||||
|
}
|
||||||
|
data = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {"Accept": "application/json"};
|
||||||
|
return fetch(url, {
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get(endpoint, data) {
|
||||||
|
return this.apiCall("get", endpoint, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
post(endpoint, data) {
|
||||||
|
return this.apiCall("post", endpoint, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchRecipes: Task.async(function* (filters = {}) {
|
||||||
|
const recipeResponse = yield this.get("recipe/signed/", filters);
|
||||||
|
const rawText = yield recipeResponse.text();
|
||||||
|
const recipesWithSigs = JSON.parse(rawText);
|
||||||
|
|
||||||
|
const verifiedRecipes = [];
|
||||||
|
|
||||||
|
for (const {recipe, signature: {signature, x5u}} of recipesWithSigs) {
|
||||||
|
const serialized = CanonicalJSON.stringify(recipe);
|
||||||
|
if (!rawText.includes(serialized)) {
|
||||||
|
log.debug(rawText, serialized);
|
||||||
|
throw new Error("Canonical recipe serialization does not match!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const certChainResponse = yield fetch(x5u);
|
||||||
|
const certChain = yield certChainResponse.text();
|
||||||
|
const builtSignature = `p384ecdsa=${signature}`;
|
||||||
|
|
||||||
|
const verifier = Cc["@mozilla.org/security/contentsignatureverifier;1"]
|
||||||
|
.createInstance(Ci.nsIContentSignatureVerifier);
|
||||||
|
|
||||||
|
if (!verifier.verifyContentSignature(serialized, builtSignature, certChain, "normandy.content-signature.mozilla.org")) {
|
||||||
|
throw new Error("Recipe signature is not valid");
|
||||||
|
}
|
||||||
|
verifiedRecipes.push(recipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`Fetched ${verifiedRecipes.length} recipes from the server:`, verifiedRecipes.map(r => r.name).join(", "));
|
||||||
|
|
||||||
|
return verifiedRecipes;
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch metadata about this client determined by the server.
|
||||||
|
* @return {object} Metadata specified by the server
|
||||||
|
*/
|
||||||
|
classifyClient() {
|
||||||
|
return this.get("classify_client/")
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(clientData => {
|
||||||
|
clientData.request_time = new Date(clientData.request_time);
|
||||||
|
return clientData;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchAction(name) {
|
||||||
|
return this.get(`action/${name}/`).then(req => req.json());
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,141 @@
|
||||||
|
/* 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";
|
||||||
|
/* globals Components */
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
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"); /* globals setTimeout, clearTimeout */
|
||||||
|
Cu.import("resource://gre/modules/Log.jsm");
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/Storage.jsm");
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/Heartbeat.jsm");
|
||||||
|
|
||||||
|
const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["NormandyDriver"];
|
||||||
|
|
||||||
|
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||||
|
const actionLog = Log.repository.getLogger("extensions.shield-recipe-client.actions");
|
||||||
|
|
||||||
|
this.NormandyDriver = function(sandboxManager, extraContext = {}) {
|
||||||
|
if (!sandboxManager) {
|
||||||
|
throw new Error("sandboxManager is required");
|
||||||
|
}
|
||||||
|
const {sandbox} = sandboxManager;
|
||||||
|
|
||||||
|
return {
|
||||||
|
testing: false,
|
||||||
|
|
||||||
|
get locale() {
|
||||||
|
return Cc["@mozilla.org/chrome/chrome-registry;1"]
|
||||||
|
.getService(Ci.nsIXULChromeRegistry)
|
||||||
|
.getSelectedLocale("browser");
|
||||||
|
},
|
||||||
|
|
||||||
|
log(message, level = "debug") {
|
||||||
|
const levels = ["debug", "info", "warn", "error"];
|
||||||
|
if (levels.indexOf(level) === -1) {
|
||||||
|
throw new Error(`Invalid log level "${level}"`);
|
||||||
|
}
|
||||||
|
actionLog[level](message);
|
||||||
|
},
|
||||||
|
|
||||||
|
showHeartbeat(options) {
|
||||||
|
log.info(`Showing heartbeat prompt "${options.message}"`);
|
||||||
|
const aWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
|
||||||
|
if (!aWindow) {
|
||||||
|
return sandbox.Promise.reject(new sandbox.Error("No window to show heartbeat in"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sandboxedDriver = Cu.cloneInto(this, sandbox, {cloneFunctions: true});
|
||||||
|
const ee = new sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
|
||||||
|
const internalOptions = Object.assign({}, options, {testing: this.testing});
|
||||||
|
new Heartbeat(aWindow, ee, sandboxManager, internalOptions);
|
||||||
|
return sandbox.Promise.resolve(ee);
|
||||||
|
},
|
||||||
|
|
||||||
|
saveHeartbeatFlow() {
|
||||||
|
// no-op required by spec
|
||||||
|
},
|
||||||
|
|
||||||
|
client() {
|
||||||
|
const appinfo = {
|
||||||
|
version: Services.appinfo.version,
|
||||||
|
channel: Services.appinfo.defaultUpdateChannel,
|
||||||
|
isDefaultBrowser: ShellService.isDefaultBrowser() || null,
|
||||||
|
searchEngine: null,
|
||||||
|
syncSetup: Preferences.isSet("services.sync.username"),
|
||||||
|
plugins: {},
|
||||||
|
doNotTrack: Preferences.get("privacy.donottrackheader.enabled", false),
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchEnginePromise = new Promise(resolve => {
|
||||||
|
Services.search.init(rv => {
|
||||||
|
if (Components.isSuccessCode(rv)) {
|
||||||
|
appinfo.searchEngine = Services.search.defaultEngine.identifier;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const pluginsPromise = new Promise(resolve => {
|
||||||
|
AddonManager.getAddonsByTypes(["plugin"], plugins => {
|
||||||
|
plugins.forEach(plugin => appinfo.plugins[plugin.name] = plugin);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new sandbox.Promise(resolve => {
|
||||||
|
Promise.all([searchEnginePromise, pluginsPromise]).then(() => {
|
||||||
|
resolve(Cu.cloneInto(appinfo, sandbox));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
uuid() {
|
||||||
|
let ret = generateUUID().toString();
|
||||||
|
ret = ret.slice(1, ret.length - 1);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
|
createStorage(keyPrefix) {
|
||||||
|
let storage;
|
||||||
|
try {
|
||||||
|
storage = Storage.makeStorage(keyPrefix, sandbox);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e.stack);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return storage;
|
||||||
|
},
|
||||||
|
|
||||||
|
location() {
|
||||||
|
const location = Cu.cloneInto({countryCode: extraContext.country}, sandbox);
|
||||||
|
return sandbox.Promise.resolve(location);
|
||||||
|
},
|
||||||
|
|
||||||
|
setTimeout(cb, time) {
|
||||||
|
if (typeof cb !== "function") {
|
||||||
|
throw new sandbox.Error(`setTimeout must be called with a function, got "${typeof cb}"`);
|
||||||
|
}
|
||||||
|
const token = setTimeout(() => {
|
||||||
|
cb();
|
||||||
|
sandboxManager.removeHold(`setTimeout-${token}`);
|
||||||
|
}, time);
|
||||||
|
sandboxManager.addHold(`setTimeout-${token}`);
|
||||||
|
return Cu.cloneInto(token, sandbox);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearTimeout(token) {
|
||||||
|
clearTimeout(token);
|
||||||
|
sandboxManager.removeHold(`setTimeout-${token}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,162 @@
|
||||||
|
/* 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/Timer.jsm"); /* globals setTimeout */
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Log.jsm");
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm");
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/EnvExpressions.jsm");
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm");
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm");
|
||||||
|
Cu.importGlobalProperties(["fetch"]); /* globals fetch */
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["RecipeRunner"];
|
||||||
|
|
||||||
|
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||||
|
const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
|
||||||
|
|
||||||
|
this.RecipeRunner = {
|
||||||
|
init() {
|
||||||
|
if (!this.checkPrefs()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delay;
|
||||||
|
if (prefs.getBoolPref("dev_mode")) {
|
||||||
|
delay = 0;
|
||||||
|
} else {
|
||||||
|
// startup delay is in seconds
|
||||||
|
delay = prefs.getIntPref("startup_delay_seconds") * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(this.start.bind(this), delay);
|
||||||
|
},
|
||||||
|
|
||||||
|
checkPrefs() {
|
||||||
|
// Only run if Unified Telemetry is enabled.
|
||||||
|
if (!Services.prefs.getBoolPref("toolkit.telemetry.unified")) {
|
||||||
|
log.info("Disabling RecipeRunner because Unified Telemetry is disabled.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prefs.getBoolPref("enabled")) {
|
||||||
|
log.info("Recipe Client is disabled.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrl = prefs.getCharPref("api_url");
|
||||||
|
if (!apiUrl || !apiUrl.startsWith("https://")) {
|
||||||
|
log.error(`Non HTTPS URL provided for extensions.shield-recipe-client.api_url: ${apiUrl}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
start: Task.async(function* () {
|
||||||
|
let recipes;
|
||||||
|
try {
|
||||||
|
recipes = yield NormandyApi.fetchRecipes({enabled: true});
|
||||||
|
} catch (e) {
|
||||||
|
const apiUrl = prefs.getCharPref("api_url");
|
||||||
|
log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraContext;
|
||||||
|
try {
|
||||||
|
extraContext = yield this.getExtraContext();
|
||||||
|
} catch (e) {
|
||||||
|
log.warning(`Couldn't get extra filter context: ${e}`);
|
||||||
|
extraContext = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipesToRun = [];
|
||||||
|
|
||||||
|
for (const recipe of recipes) {
|
||||||
|
if (yield this.checkFilter(recipe, extraContext)) {
|
||||||
|
recipesToRun.push(recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipesToRun.length === 0) {
|
||||||
|
log.debug("No recipes to execute");
|
||||||
|
} else {
|
||||||
|
for (const recipe of recipesToRun) {
|
||||||
|
try {
|
||||||
|
log.debug(`Executing recipe "${recipe.name}" (action=${recipe.action})`);
|
||||||
|
yield this.executeRecipe(recipe, extraContext);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`Could not execute recipe ${recipe.name}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
getExtraContext() {
|
||||||
|
return NormandyApi.classifyClient()
|
||||||
|
.then(clientData => ({normandy: clientData}));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate a recipe's filter expression against the environment.
|
||||||
|
* @param {object} recipe
|
||||||
|
* @param {string} recipe.filter The expression to evaluate against the environment.
|
||||||
|
* @param {object} extraContext Any extra context to provide to the filter environment.
|
||||||
|
* @return {boolean} The result of evaluating the filter, cast to a bool.
|
||||||
|
*/
|
||||||
|
checkFilter(recipe, extraContext) {
|
||||||
|
return EnvExpressions.eval(recipe.filter_expression, extraContext)
|
||||||
|
.then(result => {
|
||||||
|
return !!result;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log.error(`Error checking filter for "${recipe.name}"`);
|
||||||
|
log.error(`Filter: "${recipe.filter_expression}"`);
|
||||||
|
log.error(`Error: "${error}"`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a recipe by fetching it action and executing it.
|
||||||
|
* @param {Object} recipe A recipe to execute
|
||||||
|
* @promise Resolves when the action has executed
|
||||||
|
*/
|
||||||
|
executeRecipe: Task.async(function* (recipe, extraContext) {
|
||||||
|
const sandboxManager = new SandboxManager();
|
||||||
|
const {sandbox} = sandboxManager;
|
||||||
|
|
||||||
|
const action = yield NormandyApi.fetchAction(recipe.action);
|
||||||
|
const response = yield fetch(action.implementation_url);
|
||||||
|
|
||||||
|
const actionScript = yield response.text();
|
||||||
|
const prepScript = `
|
||||||
|
var pendingAction = null;
|
||||||
|
|
||||||
|
function registerAction(name, Action) {
|
||||||
|
let a = new Action(sandboxedDriver, sandboxedRecipe);
|
||||||
|
pendingAction = a.execute()
|
||||||
|
.catch(err => sandboxedDriver.log(err, 'error'));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.registerAction = registerAction;
|
||||||
|
window.setTimeout = sandboxedDriver.setTimeout;
|
||||||
|
window.clearTimeout = sandboxedDriver.clearTimeout;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const driver = new NormandyDriver(sandboxManager, extraContext);
|
||||||
|
sandbox.sandboxedDriver = Cu.cloneInto(driver, sandbox, {cloneFunctions: true});
|
||||||
|
sandbox.sandboxedRecipe = Cu.cloneInto(recipe, sandbox);
|
||||||
|
|
||||||
|
Cu.evalInSandbox(prepScript, sandbox);
|
||||||
|
Cu.evalInSandbox(actionScript, sandbox);
|
||||||
|
|
||||||
|
sandboxManager.addHold("recipeExecution");
|
||||||
|
sandbox.pendingAction.then(() => sandboxManager.removeHold("recipeExecution"));
|
||||||
|
}),
|
||||||
|
};
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* 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/Log.jsm");
|
||||||
|
Cu.importGlobalProperties(["crypto", "TextEncoder"]);
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["Sampling"];
|
||||||
|
|
||||||
|
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map from the range [0, 1] to [0, max(sha256)].
|
||||||
|
* @param {number} frac A float from 0.0 to 1.0.
|
||||||
|
* @return {string} A 48 bit number represented in hex, padded to 12 characters.
|
||||||
|
*/
|
||||||
|
function fractionToKey(frac) {
|
||||||
|
const hashBits = 48;
|
||||||
|
const hashLength = hashBits / 4;
|
||||||
|
|
||||||
|
if (frac < 0 || frac > 1) {
|
||||||
|
throw new Error(`frac must be between 0 and 1 inclusive (got ${frac})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mult = Math.pow(2, hashBits) - 1;
|
||||||
|
const inDecimal = Math.floor(frac * mult);
|
||||||
|
let hexDigits = inDecimal.toString(16);
|
||||||
|
if (hexDigits.length < hashLength) {
|
||||||
|
// Left pad with zeroes
|
||||||
|
// If N zeroes are needed, generate an array of nulls N+1 elements long,
|
||||||
|
// and inserts zeroes between each null.
|
||||||
|
hexDigits = Array(hashLength - hexDigits.length + 1).join("0") + hexDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saturate at 2**48 - 1
|
||||||
|
if (hexDigits.length > hashLength) {
|
||||||
|
hexDigits = Array(hashLength + 1).join("f");
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexDigits;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bufferToHex(buffer) {
|
||||||
|
const hexCodes = [];
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
for (let i = 0; i < view.byteLength; i += 4) {
|
||||||
|
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
|
||||||
|
const value = view.getUint32(i);
|
||||||
|
// toString(16) will give the hex representation of the number without padding
|
||||||
|
hexCodes.push(value.toString(16).padStart(8, "0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join all the hex strings into one
|
||||||
|
return hexCodes.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Sampling = {
|
||||||
|
stableSample(input, rate) {
|
||||||
|
const hasher = crypto.subtle;
|
||||||
|
|
||||||
|
return hasher.digest("SHA-256", new TextEncoder("utf-8").encode(JSON.stringify(input)))
|
||||||
|
.then(hash => {
|
||||||
|
// truncate hash to 12 characters (2^48)
|
||||||
|
const inputHash = bufferToHex(hash).slice(0, 12);
|
||||||
|
const samplePoint = fractionToKey(rate);
|
||||||
|
|
||||||
|
if (samplePoint.length !== 12 || inputHash.length !== 12) {
|
||||||
|
throw new Error("Unexpected hash length");
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputHash < samplePoint;
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log.error(`Error: ${error}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,65 @@
|
||||||
|
const {utils: Cu} = Components;
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["SandboxManager"];
|
||||||
|
|
||||||
|
this.SandboxManager = class {
|
||||||
|
constructor() {
|
||||||
|
this._sandbox = makeSandbox();
|
||||||
|
this.holds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get sandbox() {
|
||||||
|
if (this._sandbox) {
|
||||||
|
return this._sandbox;
|
||||||
|
}
|
||||||
|
throw new Error("Tried to use sandbox after it was nuked");
|
||||||
|
}
|
||||||
|
|
||||||
|
addHold(name) {
|
||||||
|
this.holds.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHold(name) {
|
||||||
|
const index = this.holds.indexOf(name);
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error(`Tried to remove non-existant hold "${name}"`);
|
||||||
|
}
|
||||||
|
this.holds.splice(index, 1);
|
||||||
|
this.tryCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
tryCleanup() {
|
||||||
|
if (this.holds.length === 0) {
|
||||||
|
const sandbox = this._sandbox;
|
||||||
|
this._sandbox = null;
|
||||||
|
Cu.nukeSandbox(sandbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isNuked() {
|
||||||
|
// Do this in a promise, so other async things can resolve.
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this._sandbox) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Sandbox is not nuked. Holds left: ${this.holds}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function makeSandbox() {
|
||||||
|
const sandbox = new Cu.Sandbox(null, {
|
||||||
|
wantComponents: false,
|
||||||
|
wantGlobalProperties: ["URL", "URLSearchParams"],
|
||||||
|
});
|
||||||
|
|
||||||
|
sandbox.window = Cu.cloneInto({}, sandbox);
|
||||||
|
|
||||||
|
const url = "resource://shield-recipe-client/data/EventEmitter.js";
|
||||||
|
Services.scriptloader.loadSubScript(url, sandbox);
|
||||||
|
|
||||||
|
return sandbox;
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
/* 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");
|
||||||
|
Cu.import("resource://gre/modules/Log.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "JSONFile", "resource://gre/modules/JSONFile.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["Storage"];
|
||||||
|
|
||||||
|
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||||
|
let storePromise;
|
||||||
|
|
||||||
|
function loadStorage() {
|
||||||
|
if (storePromise === undefined) {
|
||||||
|
const path = OS.Path.join(OS.Constants.Path.profileDir, "shield-recipe-client.json");
|
||||||
|
const storage = new JSONFile({path});
|
||||||
|
storePromise = Task.spawn(function* () {
|
||||||
|
yield storage.load();
|
||||||
|
return storage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return storePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Storage = {
|
||||||
|
makeStorage(prefix, sandbox) {
|
||||||
|
if (!sandbox) {
|
||||||
|
throw new Error("No sandbox passed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageInterface = {
|
||||||
|
/**
|
||||||
|
* Sets an item in the prefixed storage.
|
||||||
|
* @returns {Promise}
|
||||||
|
* @resolves With the stored value, or null.
|
||||||
|
* @rejects Javascript exception.
|
||||||
|
*/
|
||||||
|
getItem(keySuffix) {
|
||||||
|
return new sandbox.Promise((resolve, reject) => {
|
||||||
|
loadStorage()
|
||||||
|
.then(store => {
|
||||||
|
const namespace = store.data[prefix] || {};
|
||||||
|
const value = namespace[keySuffix] || null;
|
||||||
|
resolve(Cu.cloneInto(value, sandbox));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
log.error(err);
|
||||||
|
reject(new sandbox.Error());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an item in the prefixed storage.
|
||||||
|
* @returns {Promise}
|
||||||
|
* @resolves When the operation is completed succesfully
|
||||||
|
* @rejects Javascript exception.
|
||||||
|
*/
|
||||||
|
setItem(keySuffix, value) {
|
||||||
|
return new sandbox.Promise((resolve, reject) => {
|
||||||
|
loadStorage()
|
||||||
|
.then(store => {
|
||||||
|
if (!(prefix in store.data)) {
|
||||||
|
store.data[prefix] = {};
|
||||||
|
}
|
||||||
|
store.data[prefix][keySuffix] = value;
|
||||||
|
store.saveSoon();
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
log.error(err);
|
||||||
|
reject(new sandbox.Error());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a single item from the prefixed storage.
|
||||||
|
* @returns {Promise}
|
||||||
|
* @resolves When the operation is completed succesfully
|
||||||
|
* @rejects Javascript exception.
|
||||||
|
*/
|
||||||
|
removeItem(keySuffix) {
|
||||||
|
return new sandbox.Promise((resolve, reject) => {
|
||||||
|
loadStorage()
|
||||||
|
.then(store => {
|
||||||
|
if (!(prefix in store.data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete store.data[prefix][keySuffix];
|
||||||
|
store.saveSoon();
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
log.error(err);
|
||||||
|
reject(new sandbox.Error());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all storage for the prefix.
|
||||||
|
* @returns {Promise}
|
||||||
|
* @resolves When the operation is completed succesfully
|
||||||
|
* @rejects Javascript exception.
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
return new sandbox.Promise((resolve, reject) => {
|
||||||
|
return loadStorage()
|
||||||
|
.then(store => {
|
||||||
|
store.data[prefix] = {};
|
||||||
|
store.saveSoon();
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
log.error(err);
|
||||||
|
reject(new sandbox.Error());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return Cu.cloneInto(storageInterface, sandbox, {
|
||||||
|
cloneFunctions: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||||
|
# vim: set filetype=python:
|
||||||
|
# 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/.
|
||||||
|
|
||||||
|
DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
|
||||||
|
DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
|
||||||
|
|
||||||
|
FINAL_TARGET_FILES.features['shield-recipe-client@mozilla.org'] += [
|
||||||
|
'bootstrap.js',
|
||||||
|
]
|
||||||
|
|
||||||
|
FINAL_TARGET_PP_FILES.features['shield-recipe-client@mozilla.org'] += [
|
||||||
|
'install.rdf.in'
|
||||||
|
]
|
||||||
|
|
||||||
|
BROWSER_CHROME_MANIFESTS += [
|
||||||
|
'test/browser.ini',
|
||||||
|
]
|
||||||
|
|
||||||
|
JAR_MANIFESTS += ['jar.mn']
|
19
browser/extensions/shield-recipe-client/node_modules/jexl/LICENSE.txt
сгенерированный
поставляемый
Normal file
19
browser/extensions/shield-recipe-client/node_modules/jexl/LICENSE.txt
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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.
|
225
browser/extensions/shield-recipe-client/node_modules/jexl/lib/Jexl.js
сгенерированный
поставляемый
Normal file
225
browser/extensions/shield-recipe-client/node_modules/jexl/lib/Jexl.js
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
244
browser/extensions/shield-recipe-client/node_modules/jexl/lib/Lexer.js
сгенерированный
поставляемый
Normal file
244
browser/extensions/shield-recipe-client/node_modules/jexl/lib/Lexer.js
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
153
browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/Evaluator.js
сгенерированный
поставляемый
Normal file
153
browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/Evaluator.js
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
159
browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/handlers.js
сгенерированный
поставляемый
Normal file
159
browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/handlers.js
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
|
};
|
66
browser/extensions/shield-recipe-client/node_modules/jexl/lib/grammar.js
сгенерированный
поставляемый
Normal file
66
browser/extensions/shield-recipe-client/node_modules/jexl/lib/grammar.js
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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; }}
|
||||||
|
};
|
188
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/Parser.js
сгенерированный
поставляемый
Normal file
188
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/Parser.js
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
210
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/handlers.js
сгенерированный
поставляемый
Normal file
210
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/handlers.js
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
});
|
||||||
|
};
|
154
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/states.js
сгенерированный
поставляемый
Normal file
154
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/states.js
сгенерированный
поставляемый
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
globals: {
|
||||||
|
Assert: false,
|
||||||
|
BrowserTestUtils: false,
|
||||||
|
add_task: false,
|
||||||
|
is: false,
|
||||||
|
isnot: false,
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"spaced-comment": 2,
|
||||||
|
"space-before-function-paren": 2,
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* 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-console */
|
||||||
|
this.EXPORTED_SYMBOLS = ["TestUtils"];
|
||||||
|
|
||||||
|
this.TestUtils = {
|
||||||
|
promiseTest(test) {
|
||||||
|
return function(assert, done) {
|
||||||
|
test(assert)
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
assert.ok(false, err);
|
||||||
|
})
|
||||||
|
.then(() => done());
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
[browser_driver_uuids.js]
|
||||||
|
[browser_env_expressions.js]
|
||||||
|
[browser_EventEmitter.js]
|
||||||
|
[browser_Storage.js]
|
||||||
|
[browser_Heartbeat.js]
|
|
@ -0,0 +1,92 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {utils: Cu} = Components;
|
||||||
|
Cu.import("resource://gre/modules/Log.jsm", this);
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this);
|
||||||
|
|
||||||
|
const sandboxManager = new SandboxManager();
|
||||||
|
sandboxManager.addHold("test running");
|
||||||
|
const driver = new NormandyDriver(sandboxManager);
|
||||||
|
const sandboxedDriver = Cu.cloneInto(driver, sandboxManager.sandbox, {cloneFunctions: true});
|
||||||
|
const eventEmitter = new sandboxManager.sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
|
||||||
|
|
||||||
|
|
||||||
|
const evidence = {
|
||||||
|
a: 0,
|
||||||
|
b: 0,
|
||||||
|
c: 0,
|
||||||
|
log: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function listenerA(x = 1) {
|
||||||
|
evidence.a += x;
|
||||||
|
evidence.log += "a";
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenerB(x = 1) {
|
||||||
|
evidence.b += x;
|
||||||
|
evidence.log += "b";
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenerC(x = 1) {
|
||||||
|
evidence.c += x;
|
||||||
|
evidence.log += "c";
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(function* () {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
// Spin the event loop to run events, so we can safely "off"
|
||||||
|
yield Promise.resolve();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// And another unrelated event
|
||||||
|
eventEmitter.on("nothing");
|
||||||
|
|
||||||
|
// Spin the event loop to run events
|
||||||
|
yield Promise.resolve();
|
||||||
|
|
||||||
|
Assert.deepEqual(evidence, {
|
||||||
|
a: 111,
|
||||||
|
b: 11,
|
||||||
|
c: 1,
|
||||||
|
log: "abcaba", // events are in order
|
||||||
|
}, "events fired as expected");
|
||||||
|
|
||||||
|
sandboxManager.removeHold("test running");
|
||||||
|
|
||||||
|
yield sandboxManager.isNuked()
|
||||||
|
.then(() => ok(true, "sandbox is nuked"))
|
||||||
|
.catch(e => ok(false, "sandbox is nuked", e));
|
||||||
|
});
|
|
@ -0,0 +1,188 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm", this);
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/Heartbeat.jsm", this);
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this);
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert an array is in non-descending order, and that every element is a number
|
||||||
|
*/
|
||||||
|
function assertOrdered(arr) {
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
Assert.equal(typeof arr[i], "number", `element ${i} is type "number"`);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < arr.length - 1; i++) {
|
||||||
|
Assert.lessOrEqual(arr[i], arr[i + 1],
|
||||||
|
`element ${i} is less than or equal to element ${i + 1}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close every notification in a target window and notification box */
|
||||||
|
function closeAllNotifications(targetWindow, notificationBox) {
|
||||||
|
if (notificationBox.allNotifications.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const notificationSet = new Set(notificationBox.allNotifications);
|
||||||
|
|
||||||
|
const observer = new targetWindow.MutationObserver(mutations => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
||||||
|
const node = mutation.removedNodes.item(i);
|
||||||
|
if (notificationSet.has(node)) {
|
||||||
|
notificationSet.delete(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (notificationSet.size === 0) {
|
||||||
|
Assert.equal(notificationBox.allNotifications.length, 0, "No notifications left");
|
||||||
|
observer.disconnect();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(notificationBox, {childList: true});
|
||||||
|
|
||||||
|
for (const notification of notificationBox.allNotifications) {
|
||||||
|
notification.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check that the correct telmetry was sent */
|
||||||
|
function assertTelemetrySent(hb, eventNames) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
hb.eventEmitter.once("TelemetrySent", payload => {
|
||||||
|
const events = [0];
|
||||||
|
for (const name of eventNames) {
|
||||||
|
Assert.equal(typeof payload[name], "number", `payload field ${name} is a number`);
|
||||||
|
events.push(payload[name]);
|
||||||
|
}
|
||||||
|
events.push(Date.now());
|
||||||
|
|
||||||
|
assertOrdered(events);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const sandboxManager = new SandboxManager();
|
||||||
|
const driver = new NormandyDriver(sandboxManager);
|
||||||
|
sandboxManager.addHold("test running");
|
||||||
|
const sandboxedDriver = Cu.cloneInto(driver, sandboxManager.sandbox, {cloneFunctions: true});
|
||||||
|
|
||||||
|
|
||||||
|
// Several of the behaviors of heartbeat prompt are mutually exclusive, so checks are broken up
|
||||||
|
// into three batches.
|
||||||
|
|
||||||
|
/* Batch #1 - General UI, Stars, and telemetry data */
|
||||||
|
add_task(function* () {
|
||||||
|
const eventEmitter = new sandboxManager.sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
|
||||||
|
const targetWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
const notificationBox = targetWindow.document.querySelector("#high-priority-global-notificationbox");
|
||||||
|
|
||||||
|
const preCount = notificationBox.childElementCount;
|
||||||
|
const hb = new Heartbeat(targetWindow, eventEmitter, sandboxManager, {
|
||||||
|
testing: true,
|
||||||
|
flowId: "test",
|
||||||
|
message: "test",
|
||||||
|
engagementButtonLabel: undefined,
|
||||||
|
learnMoreMessage: "Learn More",
|
||||||
|
learnMoreUrl: "https://example.org/learnmore",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check UI
|
||||||
|
const learnMoreEl = hb.notice.querySelector(".text-link");
|
||||||
|
const messageEl = targetWindow.document.getAnonymousElementByAttribute(hb.notice, "anonid", "messageText");
|
||||||
|
Assert.equal(notificationBox.childElementCount, preCount + 1, "Correct number of notifications open");
|
||||||
|
Assert.equal(hb.notice.querySelectorAll(".star-x").length, 5, "Correct number of stars");
|
||||||
|
Assert.equal(hb.notice.querySelectorAll(".notification-button").length, 0, "Engagement button not shown");
|
||||||
|
Assert.equal(learnMoreEl.href, "https://example.org/learnmore", "Learn more url correct");
|
||||||
|
Assert.equal(learnMoreEl.value, "Learn More", "Learn more label correct");
|
||||||
|
Assert.equal(messageEl.textContent, "test", "Message is correct");
|
||||||
|
|
||||||
|
// Check that when clicking the learn more link, a tab opens with the right URL
|
||||||
|
const tabOpenPromise = BrowserTestUtils.waitForNewTab(targetWindow.gBrowser);
|
||||||
|
learnMoreEl.click();
|
||||||
|
const tab = yield tabOpenPromise;
|
||||||
|
const tabUrl = yield BrowserTestUtils.browserLoaded(
|
||||||
|
tab.linkedBrowser, true, url => url && url !== "about:blank");
|
||||||
|
|
||||||
|
Assert.equal(tabUrl, "https://example.org/learnmore", "Learn more link opened the right url");
|
||||||
|
|
||||||
|
const telemetrySentPromise = assertTelemetrySent(hb, ["offeredTS", "learnMoreTS", "closedTS"]);
|
||||||
|
// Close notification to trigger telemetry to be sent
|
||||||
|
yield closeAllNotifications(targetWindow, notificationBox);
|
||||||
|
yield telemetrySentPromise;
|
||||||
|
yield BrowserTestUtils.removeTab(tab);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Batch #2 - Engagement buttons
|
||||||
|
add_task(function* () {
|
||||||
|
const eventEmitter = new sandboxManager.sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
|
||||||
|
const targetWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
const notificationBox = targetWindow.document.querySelector("#high-priority-global-notificationbox");
|
||||||
|
const hb = new Heartbeat(targetWindow, eventEmitter, sandboxManager, {
|
||||||
|
testing: true,
|
||||||
|
flowId: "test",
|
||||||
|
message: "test",
|
||||||
|
engagementButtonLabel: "Click me!",
|
||||||
|
postAnswerUrl: "https://example.org/postAnswer",
|
||||||
|
learnMoreMessage: "Learn More",
|
||||||
|
learnMoreUrl: "https://example.org/learnMore",
|
||||||
|
});
|
||||||
|
const engagementButton = hb.notice.querySelector(".notification-button");
|
||||||
|
|
||||||
|
Assert.equal(hb.notice.querySelectorAll(".star-x").length, 0, "Stars not shown");
|
||||||
|
Assert.ok(engagementButton, "Engagement button added");
|
||||||
|
Assert.equal(engagementButton.label, "Click me!", "Engagement button has correct label");
|
||||||
|
|
||||||
|
const engagementEl = hb.notice.querySelector(".notification-button");
|
||||||
|
const tabOpenPromise = BrowserTestUtils.waitForNewTab(targetWindow.gBrowser);
|
||||||
|
engagementEl.click();
|
||||||
|
const tab = yield tabOpenPromise;
|
||||||
|
const tabUrl = yield BrowserTestUtils.browserLoaded(
|
||||||
|
tab.linkedBrowser, true, url => url && url !== "about:blank");
|
||||||
|
// the postAnswer url gets query parameters appended onto the end, so use Assert.startsWith instead of Assert.equal
|
||||||
|
Assert.ok(tabUrl.startsWith("https://example.org/postAnswer"), "Engagement button opened the right url");
|
||||||
|
|
||||||
|
const telemetrySentPromise = assertTelemetrySent(hb, ["offeredTS", "engagedTS", "closedTS"]);
|
||||||
|
// Close notification to trigger telemetry to be sent
|
||||||
|
yield closeAllNotifications(targetWindow, notificationBox);
|
||||||
|
yield telemetrySentPromise;
|
||||||
|
yield BrowserTestUtils.removeTab(tab);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Batch 3 - Closing the window while heartbeat is open
|
||||||
|
add_task(function* () {
|
||||||
|
const eventEmitter = new sandboxManager.sandbox.EventEmitter(sandboxedDriver).wrappedJSObject;
|
||||||
|
const targetWindow = yield BrowserTestUtils.openNewBrowserWindow();
|
||||||
|
|
||||||
|
const hb = new Heartbeat(targetWindow, eventEmitter, sandboxManager, {
|
||||||
|
testing: true,
|
||||||
|
flowId: "test",
|
||||||
|
message: "test",
|
||||||
|
});
|
||||||
|
|
||||||
|
const telemetrySentPromise = assertTelemetrySent(hb, ["offeredTS", "windowClosedTS"]);
|
||||||
|
// triggers sending ping to normandy
|
||||||
|
yield BrowserTestUtils.closeWindow(targetWindow);
|
||||||
|
yield telemetrySentPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
add_task(function* () {
|
||||||
|
// Make sure the sandbox is clean.
|
||||||
|
sandboxManager.removeHold("test running");
|
||||||
|
yield sandboxManager.isNuked()
|
||||||
|
.then(() => ok(true, "sandbox is nuked"))
|
||||||
|
.catch(e => ok(false, "sandbox is nuked", e));
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {utils: Cu} = Components;
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/Storage.jsm", this);
|
||||||
|
|
||||||
|
const fakeSandbox = {Promise};
|
||||||
|
const store1 = Storage.makeStorage("prefix1", fakeSandbox);
|
||||||
|
const store2 = Storage.makeStorage("prefix2", fakeSandbox);
|
||||||
|
|
||||||
|
add_task(function* () {
|
||||||
|
// Make sure values return null before being set
|
||||||
|
Assert.equal(yield store1.getItem("key"), null);
|
||||||
|
Assert.equal(yield store2.getItem("key"), null);
|
||||||
|
|
||||||
|
// Set values to check
|
||||||
|
yield store1.setItem("key", "value1");
|
||||||
|
yield store2.setItem("key", "value2");
|
||||||
|
|
||||||
|
// Check that they are available
|
||||||
|
Assert.equal(yield store1.getItem("key"), "value1");
|
||||||
|
Assert.equal(yield store2.getItem("key"), "value2");
|
||||||
|
|
||||||
|
// Remove them, and check they are gone
|
||||||
|
yield store1.removeItem("key");
|
||||||
|
yield store2.removeItem("key");
|
||||||
|
Assert.equal(yield store1.getItem("key"), null);
|
||||||
|
Assert.equal(yield store2.getItem("key"), null);
|
||||||
|
|
||||||
|
// Check that numbers are stored as numbers (not strings)
|
||||||
|
yield store1.setItem("number", 42);
|
||||||
|
Assert.equal(yield store1.getItem("number"), 42);
|
||||||
|
|
||||||
|
// Check complex types work
|
||||||
|
const complex = {a: 1, b: [2, 3], c: {d: 4}};
|
||||||
|
yield store1.setItem("complex", complex);
|
||||||
|
Assert.deepEqual(yield store1.getItem("complex"), complex);
|
||||||
|
});
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {utils: Cu} = Components;
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this);
|
||||||
|
|
||||||
|
add_task(function* () {
|
||||||
|
const sandboxManager = new SandboxManager();
|
||||||
|
sandboxManager.addHold("test running");
|
||||||
|
let driver = new NormandyDriver(sandboxManager);
|
||||||
|
|
||||||
|
// Test that UUID look about right
|
||||||
|
const uuid1 = driver.uuid();
|
||||||
|
ok(/^[a-f0-9-]{36}$/.test(uuid1), "valid uuid format");
|
||||||
|
|
||||||
|
// Test that UUIDs are different each time
|
||||||
|
const uuid2 = driver.uuid();
|
||||||
|
isnot(uuid1, uuid2, "uuids are unique");
|
||||||
|
|
||||||
|
driver = null;
|
||||||
|
sandboxManager.removeHold("test running");
|
||||||
|
|
||||||
|
yield sandboxManager.isNuked()
|
||||||
|
.then(() => ok(true, "sandbox is nuked"))
|
||||||
|
.catch(e => ok(false, "sandbox is nuked", e));
|
||||||
|
});
|
|
@ -0,0 +1,56 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {utils: Cu} = Components;
|
||||||
|
Cu.import("resource://gre/modules/TelemetryController.jsm", this);
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm", this);
|
||||||
|
|
||||||
|
Cu.import("resource://shield-recipe-client/lib/EnvExpressions.jsm", this);
|
||||||
|
Cu.import("resource://gre/modules/Log.jsm", this);
|
||||||
|
|
||||||
|
add_task(function* () {
|
||||||
|
// setup
|
||||||
|
yield TelemetryController.submitExternalPing("testfoo", {foo: 1});
|
||||||
|
yield TelemetryController.submitExternalPing("testbar", {bar: 2});
|
||||||
|
|
||||||
|
let val;
|
||||||
|
// Test that basic expressions work
|
||||||
|
val = yield EnvExpressions.eval("2+2");
|
||||||
|
is(val, 4, "basic expression works");
|
||||||
|
|
||||||
|
// Test that multiline expressions work
|
||||||
|
val = yield EnvExpressions.eval(`
|
||||||
|
2
|
||||||
|
+
|
||||||
|
2
|
||||||
|
`);
|
||||||
|
is(val, 4, "multiline expression works");
|
||||||
|
|
||||||
|
// Test it can access telemetry
|
||||||
|
val = yield EnvExpressions.eval("telemetry");
|
||||||
|
is(typeof val, "object", "Telemetry is accesible");
|
||||||
|
|
||||||
|
// Test it reads different types of telemetry
|
||||||
|
val = yield EnvExpressions.eval("telemetry");
|
||||||
|
is(val.testfoo.payload.foo, 1, "value 'foo' is in mock telemetry");
|
||||||
|
is(val.testbar.payload.bar, 2, "value 'bar' is in mock telemetry");
|
||||||
|
|
||||||
|
// Test has a date transform
|
||||||
|
val = yield EnvExpressions.eval('"2016-04-22"|date');
|
||||||
|
const d = new Date(Date.UTC(2016, 3, 22)); // months are 0 based
|
||||||
|
is(val.toString(), d.toString(), "Date transform works");
|
||||||
|
|
||||||
|
// Test dates are comparable
|
||||||
|
const context = {someTime: Date.UTC(2016, 0, 1)};
|
||||||
|
val = yield EnvExpressions.eval('"2015-01-01"|date < someTime', context);
|
||||||
|
ok(val, "dates are comparable with less-than");
|
||||||
|
val = yield EnvExpressions.eval('"2017-01-01"|date > someTime', context);
|
||||||
|
ok(val, "dates are comparable with greater-than");
|
||||||
|
|
||||||
|
// Test stable sample returns true for matching samples
|
||||||
|
val = yield EnvExpressions.eval('["test"]|stableSample(1)');
|
||||||
|
is(val, true, "Stable sample returns true for 100% sample");
|
||||||
|
|
||||||
|
// Test stable sample returns true for matching samples
|
||||||
|
val = yield EnvExpressions.eval('["test"]|stableSample(0)');
|
||||||
|
is(val, false, "Stable sample returns false for 0% sample");
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||||
/* Any copyright is dedicated to the Public Domain.
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
@ -1348,4 +1348,3 @@ function* initWorkerDebugger(TAB_URL, WORKER_URL) {
|
||||||
|
|
||||||
return {client, tab, tabClient, workerClient, toolbox, gDebugger};
|
return {client, tab, tabClient, workerClient, toolbox, gDebugger};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ user_pref("browser.search.geoSpecificDefaults", false);
|
||||||
|
|
||||||
// Make sure SelfSupport doesn't hit the network.
|
// Make sure SelfSupport doesn't hit the network.
|
||||||
user_pref("browser.selfsupport.url", "https://localhost/selfsupport-dummy/");
|
user_pref("browser.selfsupport.url", "https://localhost/selfsupport-dummy/");
|
||||||
|
user_pref("extensions.shield-recipe-client.api_url", "https://localhost/selfsupport-dummy/");
|
||||||
|
|
||||||
// use about:blank, not browser.startup.homepage
|
// use about:blank, not browser.startup.homepage
|
||||||
user_pref("browser.startup.page", 0);
|
user_pref("browser.startup.page", 0);
|
||||||
|
|
|
@ -303,8 +303,9 @@ user_pref("browser.search.countryCode", "US");
|
||||||
// This will prevent HTTP requests for region defaults.
|
// This will prevent HTTP requests for region defaults.
|
||||||
user_pref("browser.search.geoSpecificDefaults", false);
|
user_pref("browser.search.geoSpecificDefaults", false);
|
||||||
|
|
||||||
// Make sure the self support tab doesn't hit the network.
|
// Make sure self support doesn't hit the network.
|
||||||
user_pref("browser.selfsupport.url", "https://%(server)s/selfsupport-dummy/");
|
user_pref("browser.selfsupport.url", "https://%(server)s/selfsupport-dummy/");
|
||||||
|
user_pref("extensions.shield-recipe-client.api_url", "https://%(server)s/selfsupport-dummy/");
|
||||||
|
|
||||||
user_pref("media.eme.enabled", true);
|
user_pref("media.eme.enabled", true);
|
||||||
|
|
||||||
|
|
|
@ -143,6 +143,8 @@ DEFAULTS = dict(
|
||||||
'media.gmp-manager.updateEnabled': False,
|
'media.gmp-manager.updateEnabled': False,
|
||||||
'extensions.systemAddon.update.url':
|
'extensions.systemAddon.update.url':
|
||||||
'http://127.0.0.1/dummy-system-addons.xml',
|
'http://127.0.0.1/dummy-system-addons.xml',
|
||||||
|
'extensions.shield-recipe-client.api_url':
|
||||||
|
'https://127.0.0.1/selfsupport-dummy/',
|
||||||
'media.navigator.enabled': True,
|
'media.navigator.enabled': True,
|
||||||
'media.peerconnection.enabled': True,
|
'media.peerconnection.enabled': True,
|
||||||
'media.navigator.permission.disabled': True,
|
'media.navigator.permission.disabled': True,
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"{firefox}\\browser\\features\\firefox@getpocket.com.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
|
"{firefox}\\browser\\features\\firefox@getpocket.com.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
|
||||||
"{firefox}\\browser\\features\\presentation@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
|
"{firefox}\\browser\\features\\presentation@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
|
||||||
"{firefox}\\browser\\features\\webcompat@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
|
"{firefox}\\browser\\features\\webcompat@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
|
||||||
|
"{firefox}\\browser\\features\\shield-recipe-client@mozilla.org.xpi": {"mincount": 0, "maxcount": 100, "minbytes": 0, "maxbytes": 10000000},
|
||||||
"{talos}\\tests\\tp5n\\tp5n.manifest": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
|
"{talos}\\tests\\tp5n\\tp5n.manifest": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
|
||||||
"{talos}\\talos\\tests\\tp5n\\tp5n.manifest": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
|
"{talos}\\talos\\tests\\tp5n\\tp5n.manifest": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
|
||||||
"{talos}\\tests\\tp5n\\tp5n.manifest.develop": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
|
"{talos}\\tests\\tp5n\\tp5n.manifest.develop": {"mincount": 0, "maxcount": 8, "minbytes": 0, "maxbytes": 32786},
|
||||||
|
|
|
@ -1608,7 +1608,6 @@ try {
|
||||||
prefs.setBoolPref("geo.provider.testing", true);
|
prefs.setBoolPref("geo.provider.testing", true);
|
||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
|
|
||||||
// We need to avoid hitting the network with certain components.
|
// We need to avoid hitting the network with certain components.
|
||||||
try {
|
try {
|
||||||
if (runningInParent) {
|
if (runningInParent) {
|
||||||
|
@ -1619,6 +1618,8 @@ try {
|
||||||
prefs.setCharPref("media.gmp-manager.updateEnabled", false);
|
prefs.setCharPref("media.gmp-manager.updateEnabled", false);
|
||||||
prefs.setCharPref("extensions.systemAddon.update.url", "http://%(server)s/dummy-system-addons.xml");
|
prefs.setCharPref("extensions.systemAddon.update.url", "http://%(server)s/dummy-system-addons.xml");
|
||||||
prefs.setCharPref("browser.selfsupport.url", "https://%(server)s/selfsupport-dummy/");
|
prefs.setCharPref("browser.selfsupport.url", "https://%(server)s/selfsupport-dummy/");
|
||||||
|
prefs.setCharPref("extensions.shield-recipe-client.api_url",
|
||||||
|
"https://%(server)s/selfsupport-dummy/");
|
||||||
prefs.setCharPref("toolkit.telemetry.server", "https://%(server)s/telemetry-dummy");
|
prefs.setCharPref("toolkit.telemetry.server", "https://%(server)s/telemetry-dummy");
|
||||||
prefs.setCharPref("browser.search.geoip.url", "https://%(server)s/geoip-dummy");
|
prefs.setCharPref("browser.search.geoip.url", "https://%(server)s/geoip-dummy");
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче