mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-07 18:04:46 +00:00
merge autoland to mozilla-central a=merge
This commit is contained in:
commit
477c85db33
@ -406,7 +406,7 @@ class PluginTimerCallBack final : public nsITimerCallback
|
||||
~PluginTimerCallBack() {}
|
||||
|
||||
public:
|
||||
PluginTimerCallBack(nsIContent* aContent) : mContent(aContent) {}
|
||||
explicit PluginTimerCallBack(nsIContent* aContent) : mContent(aContent) {}
|
||||
|
||||
NS_DECL_ISUPPORTS
|
||||
|
||||
|
@ -16,7 +16,7 @@ namespace a11y {
|
||||
class ProxyAccessibleWrap : public AccessibleWrap
|
||||
{
|
||||
public:
|
||||
ProxyAccessibleWrap(ProxyAccessible* aProxy) :
|
||||
explicit ProxyAccessibleWrap(ProxyAccessible* aProxy) :
|
||||
AccessibleWrap(nullptr, nullptr)
|
||||
{
|
||||
mType = eProxyType;
|
||||
@ -38,7 +38,7 @@ public:
|
||||
class HyperTextProxyAccessibleWrap : public HyperTextAccessibleWrap
|
||||
{
|
||||
public:
|
||||
HyperTextProxyAccessibleWrap(ProxyAccessible* aProxy) :
|
||||
explicit HyperTextProxyAccessibleWrap(ProxyAccessible* aProxy) :
|
||||
HyperTextAccessibleWrap(nullptr, nullptr)
|
||||
{
|
||||
mType = eProxyType;
|
||||
@ -60,7 +60,7 @@ public:
|
||||
class DocProxyAccessibleWrap : public HyperTextProxyAccessibleWrap
|
||||
{
|
||||
public:
|
||||
DocProxyAccessibleWrap(ProxyAccessible* aProxy) :
|
||||
explicit DocProxyAccessibleWrap(ProxyAccessible* aProxy) :
|
||||
HyperTextProxyAccessibleWrap(aProxy)
|
||||
{ mGenericTypes |= eDocument; }
|
||||
|
||||
|
@ -693,7 +693,7 @@ AccessibleWrap::get_accFocus(
|
||||
class AccessibleEnumerator final : public IEnumVARIANT
|
||||
{
|
||||
public:
|
||||
AccessibleEnumerator(const nsTArray<Accessible*>& aArray) :
|
||||
explicit AccessibleEnumerator(const nsTArray<Accessible*>& aArray) :
|
||||
mArray(aArray), mCurIndex(0) { }
|
||||
AccessibleEnumerator(const AccessibleEnumerator& toCopy) :
|
||||
mArray(toCopy.mArray), mCurIndex(toCopy.mCurIndex) { }
|
||||
|
@ -19,7 +19,7 @@ namespace a11y {
|
||||
class ChildrenEnumVariant final : public IEnumVARIANT
|
||||
{
|
||||
public:
|
||||
ChildrenEnumVariant(AccessibleWrap* aAnchor) : mAnchorAcc(aAnchor),
|
||||
explicit ChildrenEnumVariant(AccessibleWrap* aAnchor) : mAnchorAcc(aAnchor),
|
||||
mCurAcc(mAnchorAcc->GetChildAt(0)), mCurIndex(0) { }
|
||||
|
||||
// IUnknown
|
||||
|
@ -18,7 +18,7 @@ namespace a11y {
|
||||
class ServiceProvider final : public IServiceProvider
|
||||
{
|
||||
public:
|
||||
ServiceProvider(AccessibleWrap* aAcc) : mAccessible(aAcc) {}
|
||||
explicit ServiceProvider(AccessibleWrap* aAcc) : mAccessible(aAcc) {}
|
||||
~ServiceProvider() {}
|
||||
|
||||
DECL_IUNKNOWN
|
||||
|
@ -19,7 +19,7 @@ namespace a11y {
|
||||
class sdnAccessible final : public ISimpleDOMNode
|
||||
{
|
||||
public:
|
||||
sdnAccessible(nsINode* aNode) :
|
||||
explicit sdnAccessible(nsINode* aNode) :
|
||||
mNode(aNode)
|
||||
{
|
||||
if (!mNode)
|
||||
|
@ -18,7 +18,7 @@ namespace a11y {
|
||||
class sdnDocAccessible final : public ISimpleDOMDocument
|
||||
{
|
||||
public:
|
||||
sdnDocAccessible(DocAccessibleWrap* aAccessible) : mAccessible(aAccessible) {};
|
||||
explicit sdnDocAccessible(DocAccessibleWrap* aAccessible) : mAccessible(aAccessible) {};
|
||||
~sdnDocAccessible() { };
|
||||
|
||||
DECL_IUNKNOWN
|
||||
|
@ -17,11 +17,11 @@ struct nsPoint;
|
||||
|
||||
namespace mozilla {
|
||||
namespace a11y {
|
||||
|
||||
|
||||
class sdnTextAccessible final : public ISimpleDOMText
|
||||
{
|
||||
public:
|
||||
sdnTextAccessible(AccessibleWrap* aAccessible) : mAccessible(aAccessible) {};
|
||||
explicit sdnTextAccessible(AccessibleWrap* aAccessible) : mAccessible(aAccessible) {};
|
||||
~sdnTextAccessible() {}
|
||||
|
||||
DECL_IUNKNOWN
|
||||
|
@ -56,8 +56,8 @@ uiaRawElmProvider::GetIAccessiblePair(__RPC__deref_out_opt IAccessible** aAcc,
|
||||
return CO_E_OBJNOTCONNECTED;
|
||||
|
||||
*aIdChild = CHILDID_SELF;
|
||||
*aAcc = mAcc;
|
||||
mAcc->AddRef();
|
||||
RefPtr<AccessibleWrap> copy(mAcc);
|
||||
copy.forget(aAcc);
|
||||
|
||||
return S_OK;
|
||||
|
||||
|
@ -24,7 +24,7 @@ class uiaRawElmProvider final : public IAccessibleEx,
|
||||
public IRawElementProviderSimple
|
||||
{
|
||||
public:
|
||||
uiaRawElmProvider(AccessibleWrap* aAcc) : mAcc(aAcc) { }
|
||||
explicit uiaRawElmProvider(AccessibleWrap* aAcc) : mAcc(aAcc) { }
|
||||
|
||||
// IUnknown
|
||||
DECL_IUNKNOWN
|
||||
|
@ -278,79 +278,16 @@ Sanitizer.prototype = {
|
||||
}
|
||||
|
||||
// Clear plugin data.
|
||||
// As evidenced in bug 1253204, clearing plugin data can sometimes be
|
||||
// very, very long, for mysterious reasons. Unfortunately, this is not
|
||||
// something actionable by Mozilla, so crashing here serves no purpose.
|
||||
//
|
||||
// For this reason, instead of waiting for sanitization to always
|
||||
// complete, we introduce a soft timeout. Once this timeout has
|
||||
// elapsed, we proceed with the shutdown of Firefox.
|
||||
let promiseClearPluginCookies;
|
||||
try {
|
||||
// We don't want to wait for this operation to complete...
|
||||
promiseClearPluginCookies = this.promiseClearPluginCookies(range);
|
||||
|
||||
// ... at least, not for more than 10 seconds.
|
||||
yield Promise.race([
|
||||
promiseClearPluginCookies,
|
||||
new Promise(resolve => setTimeout(resolve, 10000 /* 10 seconds */))
|
||||
]);
|
||||
yield Sanitizer.clearPluginData(range);
|
||||
} catch (ex) {
|
||||
seenException = ex;
|
||||
}
|
||||
|
||||
// Detach waiting for plugin cookies to be cleared.
|
||||
promiseClearPluginCookies.catch(() => {
|
||||
// If this exception is raised before the soft timeout, it
|
||||
// will appear in `seenException`. Otherwise, it's too late
|
||||
// to do anything about it.
|
||||
});
|
||||
|
||||
if (seenException) {
|
||||
throw seenException;
|
||||
}
|
||||
}),
|
||||
|
||||
promiseClearPluginCookies: Task.async(function* (range) {
|
||||
const FLAG_CLEAR_ALL = Ci.nsIPluginHost.FLAG_CLEAR_ALL;
|
||||
let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
|
||||
|
||||
// Determine age range in seconds. (-1 means clear all.) We don't know
|
||||
// that range[1] is actually now, so we compute age range based
|
||||
// on the lower bound. If range results in a negative age, do nothing.
|
||||
let age = range ? (Date.now() / 1000 - range[0] / 1000000) : -1;
|
||||
if (!range || age >= 0) {
|
||||
let tags = ph.getPluginTags();
|
||||
for (let tag of tags) {
|
||||
let refObj = {};
|
||||
let probe = "";
|
||||
if (/\bFlash\b/.test(tag.name)) {
|
||||
probe = tag.loaded ? "FX_SANITIZE_LOADED_FLASH"
|
||||
: "FX_SANITIZE_UNLOADED_FLASH";
|
||||
TelemetryStopwatch.start(probe, refObj);
|
||||
}
|
||||
try {
|
||||
let rv = yield new Promise(resolve =>
|
||||
ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve)
|
||||
);
|
||||
// If the plugin doesn't support clearing by age, clear everything.
|
||||
if (rv == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
|
||||
yield new Promise(resolve =>
|
||||
ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, resolve)
|
||||
);
|
||||
}
|
||||
if (probe) {
|
||||
TelemetryStopwatch.finish(probe, refObj);
|
||||
}
|
||||
} catch (ex) {
|
||||
// Ignore errors from plug-ins
|
||||
if (probe) {
|
||||
TelemetryStopwatch.cancel(probe, refObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
offlineApps: {
|
||||
@ -705,6 +642,12 @@ Sanitizer.prototype = {
|
||||
yield promiseReady;
|
||||
})
|
||||
},
|
||||
|
||||
pluginData: {
|
||||
clear: Task.async(function* (range) {
|
||||
yield Sanitizer.clearPluginData(range);
|
||||
}),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@ -774,6 +717,83 @@ Sanitizer.getClearRange = function(ts) {
|
||||
return [startDate, endDate];
|
||||
};
|
||||
|
||||
Sanitizer.clearPluginData = Task.async(function* (range) {
|
||||
// Clear plugin data.
|
||||
// As evidenced in bug 1253204, clearing plugin data can sometimes be
|
||||
// very, very long, for mysterious reasons. Unfortunately, this is not
|
||||
// something actionable by Mozilla, so crashing here serves no purpose.
|
||||
//
|
||||
// For this reason, instead of waiting for sanitization to always
|
||||
// complete, we introduce a soft timeout. Once this timeout has
|
||||
// elapsed, we proceed with the shutdown of Firefox.
|
||||
let seenException;
|
||||
|
||||
let promiseClearPluginData = Task.async(function* () {
|
||||
const FLAG_CLEAR_ALL = Ci.nsIPluginHost.FLAG_CLEAR_ALL;
|
||||
let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
|
||||
|
||||
// Determine age range in seconds. (-1 means clear all.) We don't know
|
||||
// that range[1] is actually now, so we compute age range based
|
||||
// on the lower bound. If range results in a negative age, do nothing.
|
||||
let age = range ? (Date.now() / 1000 - range[0] / 1000000) : -1;
|
||||
if (!range || age >= 0) {
|
||||
let tags = ph.getPluginTags();
|
||||
for (let tag of tags) {
|
||||
let refObj = {};
|
||||
let probe = "";
|
||||
if (/\bFlash\b/.test(tag.name)) {
|
||||
probe = tag.loaded ? "FX_SANITIZE_LOADED_FLASH"
|
||||
: "FX_SANITIZE_UNLOADED_FLASH";
|
||||
TelemetryStopwatch.start(probe, refObj);
|
||||
}
|
||||
try {
|
||||
let rv = yield new Promise(resolve =>
|
||||
ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve)
|
||||
);
|
||||
// If the plugin doesn't support clearing by age, clear everything.
|
||||
if (rv == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
|
||||
yield new Promise(resolve =>
|
||||
ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, resolve)
|
||||
);
|
||||
}
|
||||
if (probe) {
|
||||
TelemetryStopwatch.finish(probe, refObj);
|
||||
}
|
||||
} catch (ex) {
|
||||
// Ignore errors from plug-ins
|
||||
if (probe) {
|
||||
TelemetryStopwatch.cancel(probe, refObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// We don't want to wait for this operation to complete...
|
||||
promiseClearPluginData = promiseClearPluginData(range);
|
||||
|
||||
// ... at least, not for more than 10 seconds.
|
||||
yield Promise.race([
|
||||
promiseClearPluginData,
|
||||
new Promise(resolve => setTimeout(resolve, 10000 /* 10 seconds */))
|
||||
]);
|
||||
} catch (ex) {
|
||||
seenException = ex;
|
||||
}
|
||||
|
||||
// Detach waiting for plugin data to be cleared.
|
||||
promiseClearPluginData.catch(() => {
|
||||
// If this exception is raised before the soft timeout, it
|
||||
// will appear in `seenException`. Otherwise, it's too late
|
||||
// to do anything about it.
|
||||
});
|
||||
|
||||
if (seenException) {
|
||||
throw seenException;
|
||||
}
|
||||
});
|
||||
|
||||
Sanitizer._prefs = null;
|
||||
Sanitizer.__defineGetter__("prefs", function()
|
||||
{
|
||||
|
@ -46,14 +46,14 @@ add_task(function* () {
|
||||
window.focus();
|
||||
gTestBrowser = null;
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* () {
|
||||
Services.prefs.setBoolPref("plugins.click_to_play", true);
|
||||
|
||||
setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in");
|
||||
setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED, "Second Test Plug-in");
|
||||
});
|
||||
|
||||
function* setPrefs(cookies, pluginData) {
|
||||
sanitizer = new Sanitizer();
|
||||
sanitizer.ignoreTimespan = false;
|
||||
sanitizer.prefDomain = "privacy.cpd.";
|
||||
@ -61,67 +61,59 @@ add_task(function* () {
|
||||
itemPrefs.setBoolPref("history", false);
|
||||
itemPrefs.setBoolPref("downloads", false);
|
||||
itemPrefs.setBoolPref("cache", false);
|
||||
itemPrefs.setBoolPref("cookies", true); // plugin data
|
||||
itemPrefs.setBoolPref("cookies", cookies);
|
||||
itemPrefs.setBoolPref("formdata", false);
|
||||
itemPrefs.setBoolPref("offlineApps", false);
|
||||
itemPrefs.setBoolPref("passwords", false);
|
||||
itemPrefs.setBoolPref("sessions", false);
|
||||
itemPrefs.setBoolPref("siteSettings", false);
|
||||
});
|
||||
itemPrefs.setBoolPref("pluginData", pluginData);
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
function* testClearingData(url) {
|
||||
// Load page to set data for the plugin.
|
||||
gBrowser.selectedTab = gBrowser.addTab();
|
||||
gTestBrowser = gBrowser.selectedBrowser;
|
||||
|
||||
yield promiseTabLoadEvent(gBrowser.selectedTab, testURL1);
|
||||
yield promiseTabLoadEvent(gBrowser.selectedTab, url);
|
||||
|
||||
yield promiseUpdatePluginBindings(gTestBrowser);
|
||||
|
||||
ok(stored(["foo.com", "bar.com", "baz.com", "qux.com"]),
|
||||
"Data stored for sites");
|
||||
|
||||
// Clear 20 seconds ago
|
||||
let now_uSec = Date.now() * 1000;
|
||||
sanitizer.range = [now_uSec - 20 * 1000000, now_uSec];
|
||||
yield sanitizer.sanitize();
|
||||
|
||||
ok(stored(["bar.com", "qux.com"]), "Data stored for sites");
|
||||
ok(!stored(["foo.com"]), "Data cleared for foo.com");
|
||||
ok(!stored(["baz.com"]), "Data cleared for baz.com");
|
||||
|
||||
// Clear everything
|
||||
sanitizer.range = null;
|
||||
yield sanitizer.sanitize();
|
||||
|
||||
ok(!stored(null), "All data cleared");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
gTestBrowser = null;
|
||||
});
|
||||
|
||||
add_task(function* () {
|
||||
// Load page to set data for the plugin.
|
||||
gBrowser.selectedTab = gBrowser.addTab();
|
||||
gTestBrowser = gBrowser.selectedBrowser;
|
||||
|
||||
yield promiseTabLoadEvent(gBrowser.selectedTab, testURL2);
|
||||
|
||||
yield promiseUpdatePluginBindings(gTestBrowser);
|
||||
|
||||
ok(stored(["foo.com", "bar.com", "baz.com", "qux.com"]),
|
||||
"Data stored for sites");
|
||||
|
||||
// Attempt to clear 20 seconds ago. The plugin will throw
|
||||
// Clear 20 seconds ago.
|
||||
// In the case of testURL2 the plugin will throw
|
||||
// NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED, which should result in us
|
||||
// clearing all data regardless of age.
|
||||
let now_uSec = Date.now() * 1000;
|
||||
sanitizer.range = [now_uSec - 20 * 1000000, now_uSec];
|
||||
yield sanitizer.sanitize();
|
||||
|
||||
if (url == testURL1) {
|
||||
ok(stored(["bar.com", "qux.com"]), "Data stored for sites");
|
||||
ok(!stored(["foo.com"]), "Data cleared for foo.com");
|
||||
ok(!stored(["baz.com"]), "Data cleared for baz.com");
|
||||
|
||||
// Clear everything.
|
||||
sanitizer.range = null;
|
||||
yield sanitizer.sanitize();
|
||||
}
|
||||
|
||||
ok(!stored(null), "All data cleared");
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
gTestBrowser = null;
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
// Test when santizing cookies.
|
||||
yield setPrefs(true, false);
|
||||
yield testClearingData(testURL1);
|
||||
yield testClearingData(testURL2);
|
||||
|
||||
// Test when santizing pluginData.
|
||||
yield setPrefs(false, true);
|
||||
yield testClearingData(testURL1);
|
||||
yield testClearingData(testURL2);
|
||||
});
|
||||
|
@ -363,9 +363,6 @@ BrowserGlue.prototype = {
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "autocomplete-did-enter-text":
|
||||
this._handleURLBarTelemetry(subject.QueryInterface(Ci.nsIAutoCompleteInput));
|
||||
break;
|
||||
case "test-initialize-sanitizer":
|
||||
this._sanitizer.onStartup();
|
||||
break;
|
||||
@ -375,64 +372,6 @@ BrowserGlue.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
_handleURLBarTelemetry(input) {
|
||||
if (!input ||
|
||||
input.id != "urlbar" ||
|
||||
input.inPrivateContext ||
|
||||
input.popup.selectedIndex < 0) {
|
||||
return;
|
||||
}
|
||||
let controller =
|
||||
input.popup.view.QueryInterface(Ci.nsIAutoCompleteController);
|
||||
let idx = input.popup.selectedIndex;
|
||||
let value = controller.getValueAt(idx);
|
||||
let action = input._parseActionUrl(value);
|
||||
let actionType;
|
||||
if (action) {
|
||||
actionType =
|
||||
action.type == "searchengine" && action.params.searchSuggestion ?
|
||||
"searchsuggestion" :
|
||||
action.type;
|
||||
}
|
||||
if (!actionType) {
|
||||
let styles = new Set(controller.getStyleAt(idx).split(/\s+/));
|
||||
let style = ["autofill", "tag", "bookmark"].find(s => styles.has(s));
|
||||
actionType = style || "history";
|
||||
}
|
||||
|
||||
Services.telemetry
|
||||
.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX")
|
||||
.add(idx);
|
||||
|
||||
// Ideally this would be a keyed histogram and we'd just add(actionType),
|
||||
// but keyed histograms aren't currently shown on the telemetry dashboard
|
||||
// (bug 1151756).
|
||||
//
|
||||
// You can add values but don't change any of the existing values.
|
||||
// Otherwise you'll break our data.
|
||||
let buckets = {
|
||||
autofill: 0,
|
||||
bookmark: 1,
|
||||
history: 2,
|
||||
keyword: 3,
|
||||
searchengine: 4,
|
||||
searchsuggestion: 5,
|
||||
switchtab: 6,
|
||||
tag: 7,
|
||||
visiturl: 8,
|
||||
remotetab: 9,
|
||||
extension: 10,
|
||||
};
|
||||
if (actionType in buckets) {
|
||||
Services.telemetry
|
||||
.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE")
|
||||
.add(buckets[actionType]);
|
||||
} else {
|
||||
Cu.reportError("Unknown FX_URLBAR_SELECTED_RESULT_TYPE type: " +
|
||||
actionType);
|
||||
}
|
||||
},
|
||||
|
||||
// initialization (called on application startup)
|
||||
_init: function BG__init() {
|
||||
let os = Services.obs;
|
||||
@ -469,7 +408,6 @@ BrowserGlue.prototype = {
|
||||
os.addObserver(this, "restart-in-safe-mode", false);
|
||||
os.addObserver(this, "flash-plugin-hang", false);
|
||||
os.addObserver(this, "xpi-signature-changed", false);
|
||||
os.addObserver(this, "autocomplete-did-enter-text", false);
|
||||
|
||||
if (AppConstants.NIGHTLY_BUILD) {
|
||||
os.addObserver(this, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED, false);
|
||||
@ -524,7 +462,6 @@ BrowserGlue.prototype = {
|
||||
os.removeObserver(this, "browser-search-engine-modified");
|
||||
os.removeObserver(this, "flash-plugin-hang");
|
||||
os.removeObserver(this, "xpi-signature-changed");
|
||||
os.removeObserver(this, "autocomplete-did-enter-text");
|
||||
},
|
||||
|
||||
_onAppDefaults: function BG__onAppDefaults() {
|
||||
|
@ -350,7 +350,7 @@
|
||||
<tabpanel id="updatePanel" orient="vertical">
|
||||
#ifdef MOZ_UPDATER
|
||||
<groupbox id="updateApp" align="start">
|
||||
<caption><label>&updateApp.label;</label></caption>
|
||||
<caption><label>&updateApplication.label;</label></caption>
|
||||
<radiogroup id="updateRadioGroup" align="start">
|
||||
<radio id="autoDesktop"
|
||||
value="auto"
|
||||
@ -380,7 +380,7 @@
|
||||
</groupbox>
|
||||
#endif
|
||||
<groupbox id="updateOthers" align="start">
|
||||
<caption><label>&updateOthers.label;</label></caption>
|
||||
<caption><label>&autoUpdateOthers.label;</label></caption>
|
||||
<checkbox id="enableSearchUpdate"
|
||||
label="&enableSearchUpdate.label;"
|
||||
accesskey="&enableSearchUpdate.accesskey;"
|
||||
|
@ -4,9 +4,5 @@ MOZ_AUTOMATION_L10N_CHECK=0
|
||||
. "$topsrcdir/browser/config/mozconfigs/linux32/common-opt"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
unset CC
|
||||
unset CXX
|
||||
unset RUSTC
|
||||
unset CARGO
|
||||
|
@ -4,11 +4,7 @@ MOZ_AUTOMATION_L10N_CHECK=0
|
||||
. "$topsrcdir/build/unix/mozconfig.linux32"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
unset CC
|
||||
unset CXX
|
||||
unset RUSTC
|
||||
unset CARGO
|
||||
|
||||
ac_add_options --enable-debug
|
||||
|
@ -4,9 +4,5 @@ MOZ_AUTOMATION_L10N_CHECK=0
|
||||
. "$topsrcdir/browser/config/mozconfigs/linux64/common-opt"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
unset CC
|
||||
unset CXX
|
||||
unset RUSTC
|
||||
unset CARGO
|
||||
|
@ -4,12 +4,8 @@ MOZ_AUTOMATION_L10N_CHECK=0
|
||||
. "$topsrcdir/build/unix/mozconfig.linux"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
unset CC
|
||||
unset CXX
|
||||
unset RUSTC
|
||||
unset CARGO
|
||||
|
||||
ac_add_options --enable-debug
|
||||
|
||||
|
@ -7,9 +7,5 @@ export MOZILLA_OFFICIAL=1
|
||||
. "$topsrcdir/build/macosx/mozconfig.common"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
unset CC
|
||||
unset CXX
|
||||
unset RUSTC
|
||||
unset CARGO
|
||||
|
@ -4,11 +4,7 @@ MOZ_AUTOMATION_L10N_CHECK=0
|
||||
. "$topsrcdir/build/macosx/mozconfig.common"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
unset CC
|
||||
unset CXX
|
||||
unset RUSTC
|
||||
unset CARGO
|
||||
|
||||
ac_add_options --enable-debug
|
||||
|
@ -9,5 +9,5 @@ export MOZILLA_OFFICIAL=1
|
||||
. "$topsrcdir/build/win32/mozconfig.vs-latest"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
|
||||
ac_add_options --enable-jemalloc
|
||||
ac_add_options --enable-require-all-d3dc-versions
|
||||
|
||||
if [ -f /c/builds/gapi.data ]; then
|
||||
_gapi_keyfile=/c/builds/gapi.data
|
||||
|
@ -6,7 +6,6 @@ ac_add_options --enable-debug
|
||||
ac_add_options --enable-dmd
|
||||
ac_add_options --enable-profiling # needed for --enable-dmd to work on Windows
|
||||
ac_add_options --enable-verify-mar
|
||||
ac_add_options --enable-require-all-d3dc-versions
|
||||
|
||||
# Needed to enable breakpad in application.ini
|
||||
export MOZILLA_OFFICIAL=1
|
||||
|
@ -6,7 +6,7 @@ MOZ_AUTOMATION_L10N_CHECK=0
|
||||
. "$topsrcdir/build/win32/mozconfig.vs-latest"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
|
||||
ac_add_options --enable-debug
|
||||
|
@ -10,5 +10,5 @@ export MOZILLA_OFFICIAL=1
|
||||
. "$topsrcdir/build/win64/mozconfig.vs-latest"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
|
@ -7,7 +7,7 @@ MOZ_AUTOMATION_L10N_CHECK=0
|
||||
. "$topsrcdir/build/win64/mozconfig.vs-latest"
|
||||
. "$topsrcdir/build/mozconfig.common.override"
|
||||
|
||||
ac_add_options --enable-artifact-builds
|
||||
. "$topsrcdir/build/mozconfig.artifact"
|
||||
ac_add_options --enable-artifact-build-symbols
|
||||
|
||||
ac_add_options --enable-debug
|
||||
|
@ -10,6 +10,7 @@ DIRS += [
|
||||
'pdfjs',
|
||||
'pocket',
|
||||
'webcompat',
|
||||
'shield-recipe-client',
|
||||
]
|
||||
|
||||
# Only include the following system add-ons if building Aurora or Nightly
|
||||
|
102
browser/extensions/shield-recipe-client/bootstrap.js
vendored
Normal file
102
browser/extensions/shield-recipe-client/bootstrap.js
vendored
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
60
browser/extensions/shield-recipe-client/data/EventEmitter.js
Normal file
60
browser/extensions/shield-recipe-client/data/EventEmitter.js
Normal file
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
24
browser/extensions/shield-recipe-client/install.rdf.in
Normal file
24
browser/extensions/shield-recipe-client/install.rdf.in
Normal file
@ -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>
|
9
browser/extensions/shield-recipe-client/jar.mn
Normal file
9
browser/extensions/shield-recipe-client/jar.mn
Normal file
@ -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);
|
||||
},
|
||||
};
|
346
browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
Normal file
346
browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
Normal file
@ -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;
|
||||
}
|
||||
};
|
99
browser/extensions/shield-recipe-client/lib/NormandyApi.jsm
Normal file
99
browser/extensions/shield-recipe-client/lib/NormandyApi.jsm
Normal file
@ -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());
|
||||
},
|
||||
};
|
141
browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm
Normal file
141
browser/extensions/shield-recipe-client/lib/NormandyDriver.jsm
Normal file
@ -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}`);
|
||||
},
|
||||
};
|
||||
};
|
162
browser/extensions/shield-recipe-client/lib/RecipeRunner.jsm
Normal file
162
browser/extensions/shield-recipe-client/lib/RecipeRunner.jsm
Normal file
@ -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"));
|
||||
}),
|
||||
};
|
81
browser/extensions/shield-recipe-client/lib/Sampling.jsm
Normal file
81
browser/extensions/shield-recipe-client/lib/Sampling.jsm
Normal file
@ -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;
|
||||
}
|
134
browser/extensions/shield-recipe-client/lib/Storage.jsm
Normal file
134
browser/extensions/shield-recipe-client/lib/Storage.jsm
Normal file
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
22
browser/extensions/shield-recipe-client/moz.build
Normal file
22
browser/extensions/shield-recipe-client/moz.build
Normal file
@ -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
generated
vendored
Normal file
19
browser/extensions/shield-recipe-client/node_modules/jexl/LICENSE.txt
generated
vendored
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
generated
vendored
Normal file
225
browser/extensions/shield-recipe-client/node_modules/jexl/lib/Jexl.js
generated
vendored
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
generated
vendored
Normal file
244
browser/extensions/shield-recipe-client/node_modules/jexl/lib/Lexer.js
generated
vendored
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
generated
vendored
Normal file
153
browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/Evaluator.js
generated
vendored
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
generated
vendored
Normal file
159
browser/extensions/shield-recipe-client/node_modules/jexl/lib/evaluator/handlers.js
generated
vendored
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
generated
vendored
Normal file
66
browser/extensions/shield-recipe-client/node_modules/jexl/lib/grammar.js
generated
vendored
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
generated
vendored
Normal file
188
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/Parser.js
generated
vendored
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
generated
vendored
Normal file
210
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/handlers.js
generated
vendored
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
generated
vendored
Normal file
154
browser/extensions/shield-recipe-client/node_modules/jexl/lib/parser/states.js
generated
vendored
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
|
||||
}
|
||||
};
|
16
browser/extensions/shield-recipe-client/test/.eslintrc.js
Normal file
16
browser/extensions/shield-recipe-client/test/.eslintrc.js
Normal file
@ -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,
|
||||
}
|
||||
};
|
21
browser/extensions/shield-recipe-client/test/TestUtils.jsm
Normal file
21
browser/extensions/shield-recipe-client/test/TestUtils.jsm
Normal file
@ -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());
|
||||
};
|
||||
},
|
||||
};
|
5
browser/extensions/shield-recipe-client/test/browser.ini
Normal file
5
browser/extensions/shield-recipe-client/test/browser.ini
Normal file
@ -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");
|
||||
});
|
@ -61,9 +61,6 @@ DEFINES += -DMOZ_ANGLE_RENDERER=$(MOZ_ANGLE_RENDERER)
|
||||
ifdef MOZ_D3DCOMPILER_VISTA_DLL
|
||||
DEFINES += -DMOZ_D3DCOMPILER_VISTA_DLL=$(MOZ_D3DCOMPILER_VISTA_DLL)
|
||||
endif
|
||||
ifdef MOZ_D3DCOMPILER_XP_DLL
|
||||
DEFINES += -DMOZ_D3DCOMPILER_XP_DLL=$(MOZ_D3DCOMPILER_XP_DLL)
|
||||
endif
|
||||
endif
|
||||
|
||||
DEFINES += -DMOZ_CHILD_PROCESS_NAME=$(MOZ_CHILD_PROCESS_NAME)
|
||||
|
@ -589,10 +589,6 @@
|
||||
#ifdef MOZ_D3DCOMPILER_VISTA_DLL
|
||||
@BINPATH@/@MOZ_D3DCOMPILER_VISTA_DLL@
|
||||
#endif
|
||||
|
||||
#ifdef MOZ_D3DCOMPILER_XP_DLL
|
||||
@BINPATH@/@MOZ_D3DCOMPILER_XP_DLL@
|
||||
#endif
|
||||
#endif # MOZ_ANGLE_RENDERER
|
||||
|
||||
; [Browser Chrome Files]
|
||||
|
@ -732,12 +732,10 @@ decoder.noHWAccelerationVista.message = To improve video quality, you may need t
|
||||
decoder.noPulseAudio.message = To play audio, you may need to install the required PulseAudio software.
|
||||
decoder.unsupportedLibavcodec.message = libavcodec may be vulnerable or is not supported, and should be updated to play video.
|
||||
|
||||
# LOCALIZATION NOTE (captivePortal.infoMessage,
|
||||
# captivePortal.infoMessage2):
|
||||
# LOCALIZATION NOTE (captivePortal.infoMessage2):
|
||||
# Shown in a notification bar when we detect a captive portal is blocking network access
|
||||
# and requires the user to log in before browsing. %1$S is replaced with brandShortName.
|
||||
captivePortal.infoMessage = This network may require you to login to use the internet. %1$S has opened the login page for you.
|
||||
captivePortal.infoMessage2 = This network may require you to login to use the internet.
|
||||
# and requires the user to log in before browsing.
|
||||
captivePortal.infoMessage2 = This network may require you to log in to use the internet.
|
||||
# LOCALIZATION NOTE (captivePortal.showLoginPage):
|
||||
# The label for a button shown in the info bar in all tabs except the login page tab.
|
||||
# The button shows the portal login page tab when clicked.
|
||||
|
@ -82,7 +82,7 @@
|
||||
|
||||
<!ENTITY updateTab.label "Update">
|
||||
|
||||
<!ENTITY updateApp.label "&brandShortName; updates:">
|
||||
<!ENTITY updateApplication.label "&brandShortName; updates">
|
||||
<!ENTITY updateAuto1.label "Automatically install updates (recommended: improved security)">
|
||||
<!ENTITY updateAuto1.accesskey "A">
|
||||
<!ENTITY updateCheck.label "Check for updates, but let me choose whether to install them">
|
||||
@ -96,7 +96,7 @@
|
||||
<!ENTITY useService.label "Use a background service to install updates">
|
||||
<!ENTITY useService.accesskey "b">
|
||||
|
||||
<!ENTITY updateOthers.label "Automatically update:">
|
||||
<!ENTITY autoUpdateOthers.label "Automatically update">
|
||||
<!ENTITY enableSearchUpdate.label "Search Engines">
|
||||
<!ENTITY enableSearchUpdate.accesskey "E">
|
||||
|
||||
|
@ -51,9 +51,9 @@
|
||||
<p>&brandShortName; can’t load this page for some reason.</p>
|
||||
">
|
||||
|
||||
<!ENTITY captivePortal.title "Login to network">
|
||||
<!ENTITY captivePortal.title "Log in to network">
|
||||
<!ENTITY captivePortal.longDesc "
|
||||
<p>This network may require you to login to access the internet.</p>
|
||||
<p>This network may require you to log in to access the internet.</p>
|
||||
">
|
||||
|
||||
<!ENTITY openPortalLoginPage.label "Open Login Page">
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["BrowserUsageTelemetry"];
|
||||
this.EXPORTED_SYMBOLS = ["BrowserUsageTelemetry", "URLBAR_SELECTED_RESULT_TYPES"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
@ -23,6 +23,7 @@ const WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored";
|
||||
const TAB_RESTORING_TOPIC = "SSTabRestoring";
|
||||
const TELEMETRY_SUBSESSIONSPLIT_TOPIC = "internal-telemetry-after-subsession-split";
|
||||
const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
|
||||
const AUTOCOMPLETE_ENTER_TEXT_TOPIC = "autocomplete-did-enter-text";
|
||||
|
||||
// Probe names.
|
||||
const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count";
|
||||
@ -48,6 +49,24 @@ const KNOWN_ONEOFF_SOURCES = [
|
||||
"unknown", // Edge case: this is the searchbar (see bug 1195733 comment 7).
|
||||
];
|
||||
|
||||
/**
|
||||
* The buckets used for logging telemetry to the FX_URLBAR_SELECTED_RESULT_TYPE
|
||||
* histogram.
|
||||
*/
|
||||
const URLBAR_SELECTED_RESULT_TYPES = {
|
||||
autofill: 0,
|
||||
bookmark: 1,
|
||||
history: 2,
|
||||
keyword: 3,
|
||||
searchengine: 4,
|
||||
searchsuggestion: 5,
|
||||
switchtab: 6,
|
||||
tag: 7,
|
||||
visiturl: 8,
|
||||
remotetab: 9,
|
||||
extension: 10,
|
||||
};
|
||||
|
||||
function getOpenTabsAndWinsCounts() {
|
||||
let tabCount = 0;
|
||||
let winCount = 0;
|
||||
@ -187,9 +206,82 @@ let URICountListener = {
|
||||
Ci.nsISupportsWeakReference]),
|
||||
};
|
||||
|
||||
let urlbarListener = {
|
||||
init() {
|
||||
Services.obs.addObserver(this, AUTOCOMPLETE_ENTER_TEXT_TOPIC, true);
|
||||
},
|
||||
|
||||
uninit() {
|
||||
Services.obs.removeObserver(this, AUTOCOMPLETE_ENTER_TEXT_TOPIC, true);
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case AUTOCOMPLETE_ENTER_TEXT_TOPIC:
|
||||
this._handleURLBarTelemetry(subject.QueryInterface(Ci.nsIAutoCompleteInput));
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to log telemetry when the user enters text in the urlbar.
|
||||
*
|
||||
* @param {nsIAutoCompleteInput} input The autocomplete element where the
|
||||
* text was entered.
|
||||
*/
|
||||
_handleURLBarTelemetry(input) {
|
||||
if (!input ||
|
||||
input.id != "urlbar" ||
|
||||
input.inPrivateContext ||
|
||||
input.popup.selectedIndex < 0) {
|
||||
return;
|
||||
}
|
||||
let controller =
|
||||
input.popup.view.QueryInterface(Ci.nsIAutoCompleteController);
|
||||
let idx = input.popup.selectedIndex;
|
||||
let value = controller.getValueAt(idx);
|
||||
let action = input._parseActionUrl(value);
|
||||
let actionType;
|
||||
if (action) {
|
||||
actionType =
|
||||
action.type == "searchengine" && action.params.searchSuggestion ?
|
||||
"searchsuggestion" :
|
||||
action.type;
|
||||
}
|
||||
if (!actionType) {
|
||||
let styles = new Set(controller.getStyleAt(idx).split(/\s+/));
|
||||
let style = ["autofill", "tag", "bookmark"].find(s => styles.has(s));
|
||||
actionType = style || "history";
|
||||
}
|
||||
|
||||
Services.telemetry
|
||||
.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX")
|
||||
.add(idx);
|
||||
|
||||
// Ideally this would be a keyed histogram and we'd just add(actionType),
|
||||
// but keyed histograms aren't currently shown on the telemetry dashboard
|
||||
// (bug 1151756).
|
||||
//
|
||||
// You can add values but don't change any of the existing values.
|
||||
// Otherwise you'll break our data.
|
||||
if (actionType in URLBAR_SELECTED_RESULT_TYPES) {
|
||||
Services.telemetry
|
||||
.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE")
|
||||
.add(URLBAR_SELECTED_RESULT_TYPES[actionType]);
|
||||
} else {
|
||||
Cu.reportError("Unknown FX_URLBAR_SELECTED_RESULT_TYPE type: " +
|
||||
actionType);
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference]),
|
||||
};
|
||||
|
||||
let BrowserUsageTelemetry = {
|
||||
init() {
|
||||
Services.obs.addObserver(this, WINDOWS_RESTORED_TOPIC, false);
|
||||
urlbarListener.init();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -211,6 +303,7 @@ let BrowserUsageTelemetry = {
|
||||
Services.obs.removeObserver(this, DOMWINDOW_OPENED_TOPIC, false);
|
||||
Services.obs.removeObserver(this, TELEMETRY_SUBSESSIONSPLIT_TOPIC, false);
|
||||
Services.obs.removeObserver(this, WINDOWS_RESTORED_TOPIC, false);
|
||||
urlbarListener.uninit();
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
|
@ -33,6 +33,7 @@ add_task(function* setup() {
|
||||
Services.search.currentEngine = originalEngine;
|
||||
Services.search.removeEngine(engineDefault);
|
||||
Services.search.removeEngine(engineOneOff);
|
||||
yield PlacesTestUtils.clearHistory();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -35,6 +35,7 @@ add_task(function* setup() {
|
||||
Services.search.currentEngine = originalEngine;
|
||||
Services.search.removeEngine(engineDefault);
|
||||
Services.search.removeEngine(engineOneOff);
|
||||
yield PlacesTestUtils.clearHistory();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -8,6 +8,21 @@ const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
|
||||
const SUGGESTION_ENGINE_NAME = "browser_UsageTelemetry usageTelemetrySearchSuggestions.xml";
|
||||
const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches";
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "URLBAR_SELECTED_RESULT_TYPES",
|
||||
"resource:///modules/BrowserUsageTelemetry.jsm");
|
||||
|
||||
function checkHistogramResults(resultIndexes, expected, histogram) {
|
||||
for (let i = 0; i < resultIndexes.counts.length; i++) {
|
||||
if (i == expected) {
|
||||
Assert.equal(resultIndexes.counts[i], 1,
|
||||
`expected counts should match for ${histogram} index ${i}`);
|
||||
} else {
|
||||
Assert.equal(resultIndexes.counts[i], 0,
|
||||
`unexpected counts should be zero for ${histogram} index ${i}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let searchInAwesomebar = Task.async(function* (inputText, win = window) {
|
||||
yield new Promise(r => waitForFocus(r, win));
|
||||
// Write the search query in the urlbar.
|
||||
@ -62,12 +77,18 @@ add_task(function* setup() {
|
||||
// Enable Extended Telemetry.
|
||||
yield SpecialPowers.pushPrefEnv({"set": [["toolkit.telemetry.enabled", true]]});
|
||||
|
||||
// Enable local telemetry recording for the duration of the tests.
|
||||
let oldCanRecord = Services.telemetry.canRecordExtended;
|
||||
Services.telemetry.canRecordExtended = true;
|
||||
|
||||
// Make sure to restore the engine once we're done.
|
||||
registerCleanupFunction(function* () {
|
||||
Services.telemetry.canRecordExtended = oldCanRecord;
|
||||
Services.search.currentEngine = originalEngine;
|
||||
Services.search.removeEngine(engine);
|
||||
Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF, true);
|
||||
Services.prefs.clearUserPref(ONEOFF_URLBAR_PREF);
|
||||
yield PlacesTestUtils.clearHistory();
|
||||
});
|
||||
});
|
||||
|
||||
@ -75,6 +96,11 @@ add_task(function* test_simpleQuery() {
|
||||
// Let's reset the counts.
|
||||
Services.telemetry.clearScalars();
|
||||
Services.telemetry.clearEvents();
|
||||
let resultIndexHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX");
|
||||
let resultTypeHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE");
|
||||
resultIndexHist.clear();
|
||||
resultTypeHist.clear();
|
||||
|
||||
let search_hist = getSearchCountsHistogram();
|
||||
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
|
||||
@ -100,6 +126,15 @@ add_task(function* test_simpleQuery() {
|
||||
events = events.filter(e => e[1] == "navigation" && e[2] == "search");
|
||||
checkEvents(events, [["navigation", "search", "urlbar", "enter", {engine: "other-MozSearch"}]]);
|
||||
|
||||
// Check the histograms as well.
|
||||
let resultIndexes = resultIndexHist.snapshot();
|
||||
checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
|
||||
|
||||
let resultTypes = resultTypeHist.snapshot();
|
||||
checkHistogramResults(resultTypes,
|
||||
URLBAR_SELECTED_RESULT_TYPES.searchengine,
|
||||
"FX_URLBAR_SELECTED_RESULT_TYPE");
|
||||
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
@ -107,6 +142,11 @@ add_task(function* test_searchAlias() {
|
||||
// Let's reset the counts.
|
||||
Services.telemetry.clearScalars();
|
||||
Services.telemetry.clearEvents();
|
||||
let resultIndexHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX");
|
||||
let resultTypeHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE");
|
||||
resultIndexHist.clear();
|
||||
resultTypeHist.clear();
|
||||
|
||||
let search_hist = getSearchCountsHistogram();
|
||||
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
|
||||
@ -132,6 +172,15 @@ add_task(function* test_searchAlias() {
|
||||
events = events.filter(e => e[1] == "navigation" && e[2] == "search");
|
||||
checkEvents(events, [["navigation", "search", "urlbar", "alias", {engine: "other-MozSearch"}]]);
|
||||
|
||||
// Check the histograms as well.
|
||||
let resultIndexes = resultIndexHist.snapshot();
|
||||
checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
|
||||
|
||||
let resultTypes = resultTypeHist.snapshot();
|
||||
checkHistogramResults(resultTypes,
|
||||
URLBAR_SELECTED_RESULT_TYPES.searchengine,
|
||||
"FX_URLBAR_SELECTED_RESULT_TYPE");
|
||||
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
@ -139,6 +188,11 @@ add_task(function* test_oneOff() {
|
||||
// Let's reset the counts.
|
||||
Services.telemetry.clearScalars();
|
||||
Services.telemetry.clearEvents();
|
||||
let resultIndexHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX");
|
||||
let resultTypeHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE");
|
||||
resultIndexHist.clear();
|
||||
resultTypeHist.clear();
|
||||
|
||||
let search_hist = getSearchCountsHistogram();
|
||||
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
|
||||
@ -167,6 +221,15 @@ add_task(function* test_oneOff() {
|
||||
events = events.filter(e => e[1] == "navigation" && e[2] == "search");
|
||||
checkEvents(events, [["navigation", "search", "urlbar", "oneoff", {engine: "other-MozSearch"}]]);
|
||||
|
||||
// Check the histograms as well.
|
||||
let resultIndexes = resultIndexHist.snapshot();
|
||||
checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
|
||||
|
||||
let resultTypes = resultTypeHist.snapshot();
|
||||
checkHistogramResults(resultTypes,
|
||||
URLBAR_SELECTED_RESULT_TYPES.searchengine,
|
||||
"FX_URLBAR_SELECTED_RESULT_TYPE");
|
||||
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
@ -174,6 +237,11 @@ add_task(function* test_suggestion() {
|
||||
// Let's reset the counts.
|
||||
Services.telemetry.clearScalars();
|
||||
Services.telemetry.clearEvents();
|
||||
let resultIndexHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX");
|
||||
let resultTypeHist = Services.telemetry.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE");
|
||||
resultIndexHist.clear();
|
||||
resultTypeHist.clear();
|
||||
|
||||
let search_hist = getSearchCountsHistogram();
|
||||
|
||||
// Create an engine to generate search suggestions and add it as default
|
||||
@ -214,6 +282,15 @@ add_task(function* test_suggestion() {
|
||||
events = events.filter(e => e[1] == "navigation" && e[2] == "search");
|
||||
checkEvents(events, [["navigation", "search", "urlbar", "suggestion", {engine: searchEngineId}]]);
|
||||
|
||||
// Check the histograms as well.
|
||||
let resultIndexes = resultIndexHist.snapshot();
|
||||
checkHistogramResults(resultIndexes, 3, "FX_URLBAR_SELECTED_RESULT_INDEX");
|
||||
|
||||
let resultTypes = resultTypeHist.snapshot();
|
||||
checkHistogramResults(resultTypes,
|
||||
URLBAR_SELECTED_RESULT_TYPES.searchsuggestion,
|
||||
"FX_URLBAR_SELECTED_RESULT_TYPE");
|
||||
|
||||
Services.search.currentEngine = previousEngine;
|
||||
Services.search.removeEngine(suggestionEngine);
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
@ -1,5 +1,8 @@
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
|
||||
"resource://testing-common/PlacesTestUtils.jsm");
|
||||
|
||||
const SINGLE_TRY_TIMEOUT = 100;
|
||||
const NUMBER_OF_TRIES = 30;
|
||||
|
||||
|
@ -298,7 +298,10 @@ bool isIgnoredPathForImplicitCtor(const Decl *Declaration) {
|
||||
Begin->compare_lower(StringRef("hunspell")) == 0 ||
|
||||
Begin->compare_lower(StringRef("scoped_ptr.h")) == 0 ||
|
||||
Begin->compare_lower(StringRef("graphite2")) == 0 ||
|
||||
Begin->compare_lower(StringRef("icu")) == 0) {
|
||||
Begin->compare_lower(StringRef("icu")) == 0 ||
|
||||
Begin->compare_lower(StringRef("libcubeb")) == 0 ||
|
||||
Begin->compare_lower(StringRef("libstagefright")) == 0 ||
|
||||
Begin->compare_lower(StringRef("cairo")) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (Begin->compare_lower(StringRef("chromium")) == 0) {
|
||||
|
@ -1,9 +1,11 @@
|
||||
#define MOZ_NONHEAP_CLASS __attribute__((annotate("moz_nonheap_class")))
|
||||
#ifndef MOZ_HEAP_ALLOCATOR
|
||||
#define MOZ_HEAP_ALLOCATOR \
|
||||
_Pragma("GCC diagnostic push") \
|
||||
_Pragma("GCC diagnostic ignored \"-Wgcc-compat\"") \
|
||||
__attribute__((annotate("moz_heap_allocator"))) \
|
||||
_Pragma("GCC diagnostic pop")
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <memory>
|
||||
|
@ -11,11 +11,11 @@ into Firefox.
|
||||
|
||||
.. important::
|
||||
|
||||
Rust code is not currently enabled by default in Firefox builds.
|
||||
This should change soon (`bug 1283898 <https://bugzilla.mozilla.org/show_bug.cgi?id=1283898>`_),
|
||||
but the option to build without Rust code will likely last a little longer
|
||||
(`bug 1284816 <https://bugzilla.mozilla.org/show_bug.cgi?id=1284816>`_),
|
||||
so Rust code cannot currently be used for required components.
|
||||
Rust code is enabled by default in Firefox builds. Until we have
|
||||
a required component written in Rust, you can build without by
|
||||
setting ``ac_add_options --disable-rust`` in your mozconfig.
|
||||
This option will be around for a little longer
|
||||
(`bug 1284816 <https://bugzilla.mozilla.org/show_bug.cgi?id=1284816>`_).
|
||||
|
||||
|
||||
Linking Rust Crates into libxul
|
||||
|
@ -214,7 +214,6 @@ def old_configure_options(*options):
|
||||
'--enable-readline',
|
||||
'--enable-reflow-perf',
|
||||
'--enable-release',
|
||||
'--enable-require-all-d3dc-versions',
|
||||
'--enable-safe-browsing',
|
||||
'--enable-sandbox',
|
||||
'--enable-signmar',
|
||||
|
@ -125,6 +125,7 @@ def rust_triple_alias(host_or_target):
|
||||
('x86', 'Linux'): 'i686-unknown-linux-gnu',
|
||||
# Linux
|
||||
('x86_64', 'Linux'): 'x86_64-unknown-linux-gnu',
|
||||
('aarch64', 'Linux'): 'aarch64-unknown-linux-gnu',
|
||||
# OS X and iOS
|
||||
('x86', 'OSX'): 'i686-apple-darwin',
|
||||
('x86', 'iOS'): 'i386-apple-ios',
|
||||
@ -132,6 +133,7 @@ def rust_triple_alias(host_or_target):
|
||||
# Android
|
||||
('x86', 'Android'): 'i686-linux-android',
|
||||
('arm', 'Android'): 'armv7-linux-androideabi',
|
||||
('aarch64', 'Android'): 'aarch64-linux-android',
|
||||
# Windows
|
||||
# XXX better detection of CXX needed here, to figure out whether
|
||||
# we need i686-pc-windows-gnu instead, since mingw32 builds work.
|
||||
|
12
build/mozconfig.artifact
Normal file
12
build/mozconfig.artifact
Normal file
@ -0,0 +1,12 @@
|
||||
# Common options for testing artifact builds in automation.
|
||||
|
||||
# Enable the artifact build.
|
||||
ac_add_options --enable-artifact-builds
|
||||
|
||||
# Override any toolchain defines we've inherited from other mozconfigs.
|
||||
unset CC
|
||||
unset CXX
|
||||
unset HOST_CC
|
||||
unset HOST_CXX
|
||||
unset RUSTC
|
||||
unset CARGO
|
@ -987,5 +987,10 @@ nsChromeRegistryChrome::ManifestResource(ManifestProcessingContext& cx, int line
|
||||
return;
|
||||
}
|
||||
|
||||
rph->SetSubstitution(host, resolved);
|
||||
rv = rph->SetSubstitution(host, resolved);
|
||||
if (NS_FAILED(rv)) {
|
||||
LogMessageWithContext(cx.GetManifestURI(), lineno, nsIScriptError::warningFlag,
|
||||
"Warning: cannot set substitution for '%s'.",
|
||||
uri);
|
||||
}
|
||||
}
|
||||
|
@ -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: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
@ -1348,4 +1348,3 @@ function* initWorkerDebugger(TAB_URL, WORKER_URL) {
|
||||
|
||||
return {client, tab, tabClient, workerClient, toolbox, gDebugger};
|
||||
}
|
||||
|
||||
|
@ -11770,6 +11770,13 @@ nsDocument::SetPointerLock(Element* aElement, int aCursorStyle)
|
||||
nsIPresShell* shell = GetShell();
|
||||
if (!shell) {
|
||||
NS_WARNING("SetPointerLock(): No PresShell");
|
||||
if (!aElement) {
|
||||
// If we are unlocking pointer lock, but for some reason the doc
|
||||
// has already detached from the presshell, just ask the event
|
||||
// state manager to release the pointer.
|
||||
EventStateManager::SetPointerLock(nullptr, nullptr);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
nsPresContext* presContext = shell->GetPresContext();
|
||||
@ -11795,7 +11802,7 @@ nsDocument::SetPointerLock(Element* aElement, int aCursorStyle)
|
||||
RefPtr<EventStateManager> esm = presContext->EventStateManager();
|
||||
esm->SetCursor(aCursorStyle, nullptr, false,
|
||||
0.0f, 0.0f, widget, true);
|
||||
esm->SetPointerLock(widget, aElement);
|
||||
EventStateManager::SetPointerLock(widget, aElement);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -268,6 +268,7 @@ bool EventStateManager::sNormalLMouseEventInProcess = false;
|
||||
EventStateManager* EventStateManager::sActiveESM = nullptr;
|
||||
nsIDocument* EventStateManager::sMouseOverDocument = nullptr;
|
||||
nsWeakFrame EventStateManager::sLastDragOverFrame = nullptr;
|
||||
LayoutDeviceIntPoint EventStateManager::sPreLockPoint = LayoutDeviceIntPoint(0, 0);
|
||||
LayoutDeviceIntPoint EventStateManager::sLastRefPoint = kInvalidRefPoint;
|
||||
CSSIntPoint EventStateManager::sLastScreenPoint = CSSIntPoint(0, 0);
|
||||
LayoutDeviceIntPoint EventStateManager::sSynthCenteringPoint = kInvalidRefPoint;
|
||||
@ -290,7 +291,6 @@ EventStateManager::DeltaAccumulator*
|
||||
EventStateManager::EventStateManager()
|
||||
: mLockCursor(0)
|
||||
, mLastFrameConsumedSetCursor(false)
|
||||
, mPreLockPoint(0,0)
|
||||
, mCurrentTarget(nullptr)
|
||||
// init d&d gesture state machine variables
|
||||
, mGestureDownPoint(0,0)
|
||||
@ -4357,7 +4357,7 @@ EventStateManager::GetWrapperByEventID(WidgetMouseEvent* aEvent)
|
||||
return helper;
|
||||
}
|
||||
|
||||
void
|
||||
/* static */ void
|
||||
EventStateManager::SetPointerLock(nsIWidget* aWidget,
|
||||
nsIContent* aElement)
|
||||
{
|
||||
@ -4375,7 +4375,7 @@ EventStateManager::SetPointerLock(nsIWidget* aWidget,
|
||||
MOZ_ASSERT(aWidget, "Locking pointer requires a widget");
|
||||
|
||||
// Store the last known ref point so we can reposition the pointer after unlock.
|
||||
mPreLockPoint = sLastRefPoint;
|
||||
sPreLockPoint = sLastRefPoint;
|
||||
|
||||
// Fire a synthetic mouse move to ensure event state is updated. We first
|
||||
// set the mouse to the center of the window, so that the mouse event
|
||||
@ -4393,13 +4393,13 @@ EventStateManager::SetPointerLock(nsIWidget* aWidget,
|
||||
// synthetic mouse event. We first reset sLastRefPoint to its
|
||||
// pre-pointerlock position, so that the synthetic mouse event reports
|
||||
// no movement.
|
||||
sLastRefPoint = mPreLockPoint;
|
||||
sLastRefPoint = sPreLockPoint;
|
||||
// Reset SynthCenteringPoint to invalid so that next time we start
|
||||
// locking pointer, it has its initial value.
|
||||
sSynthCenteringPoint = kInvalidRefPoint;
|
||||
if (aWidget) {
|
||||
aWidget->SynthesizeNativeMouseMove(
|
||||
mPreLockPoint + aWidget->WidgetToScreenOffset(), nullptr);
|
||||
sPreLockPoint + aWidget->WidgetToScreenOffset(), nullptr);
|
||||
}
|
||||
|
||||
// Unsuppress DnD
|
||||
|
@ -921,7 +921,7 @@ private:
|
||||
// Last mouse event mRefPoint (the offset from the widget's origin in
|
||||
// device pixels) when mouse was locked, used to restore mouse position
|
||||
// after unlocking.
|
||||
LayoutDeviceIntPoint mPreLockPoint;
|
||||
static LayoutDeviceIntPoint sPreLockPoint;
|
||||
|
||||
// Stores the mRefPoint of the last synthetic mouse move we dispatched
|
||||
// to re-center the mouse when we were pointer locked. If this is (-1,-1) it
|
||||
@ -1015,7 +1015,7 @@ public:
|
||||
void KillClickHoldTimer();
|
||||
void FireContextClick();
|
||||
|
||||
void SetPointerLock(nsIWidget* aWidget, nsIContent* aElement) ;
|
||||
static void SetPointerLock(nsIWidget* aWidget, nsIContent* aElement) ;
|
||||
static void sClickHoldCallback ( nsITimer* aTimer, void* aESM ) ;
|
||||
};
|
||||
|
||||
|
@ -192,7 +192,7 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
operator bool() {
|
||||
explicit operator bool() {
|
||||
return module && mXInputGetState;
|
||||
}
|
||||
|
||||
@ -306,7 +306,7 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
operator bool() {
|
||||
explicit operator bool() {
|
||||
return mModule &&
|
||||
mHidD_GetProductString &&
|
||||
mHidP_GetCaps &&
|
||||
|
@ -2163,18 +2163,9 @@ void HTMLMediaElement::LoadFromSourceChildren()
|
||||
return;
|
||||
}
|
||||
}
|
||||
nsAutoString media;
|
||||
HTMLSourceElement *childSrc = HTMLSourceElement::FromContent(child);
|
||||
MOZ_ASSERT(childSrc, "Expect child to be HTMLSourceElement");
|
||||
if (childSrc && !childSrc->MatchesCurrentMedia()) {
|
||||
const char16_t* params[] = { media.get(), src.get() };
|
||||
ReportLoadError("MediaLoadSourceMediaNotMatched", params, ArrayLength(params));
|
||||
DealWithFailedElement(child);
|
||||
return;
|
||||
}
|
||||
LOG(LogLevel::Debug, ("%p Trying load from <source>=%s type=%s media=%s", this,
|
||||
NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get(),
|
||||
NS_ConvertUTF16toUTF8(media).get()));
|
||||
LOG(LogLevel::Debug, ("%p Trying load from <source>=%s type=%s", this,
|
||||
NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get()));
|
||||
|
||||
nsCOMPtr<nsIURI> uri;
|
||||
NewURIFromString(src, getter_AddRefs(uri));
|
||||
|
@ -1248,8 +1248,9 @@ public:
|
||||
const char* aTopic,
|
||||
const char16_t* aData) override
|
||||
{
|
||||
if (!mTabParent) {
|
||||
// We already sent the notification
|
||||
if (!mTabParent || !mObserverId) {
|
||||
// We already sent the notification, or we don't actually need to
|
||||
// send any notification at all.
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,8 @@
|
||||
#include "MediaInfo.h"
|
||||
#include "VideoUtils.h"
|
||||
#include "ImageContainer.h"
|
||||
#include "mozilla/layers/SharedRGBImage.h"
|
||||
#include "YCbCrUtils.h"
|
||||
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
#include <cutils/properties.h>
|
||||
@ -93,6 +95,45 @@ ValidatePlane(const VideoData::YCbCrBuffer::Plane& aPlane)
|
||||
aPlane.mStride > 0;
|
||||
}
|
||||
|
||||
static bool ValidateBufferAndPicture(const VideoData::YCbCrBuffer& aBuffer,
|
||||
const IntRect& aPicture)
|
||||
{
|
||||
// The following situation should never happen unless there is a bug
|
||||
// in the decoder
|
||||
if (aBuffer.mPlanes[1].mWidth != aBuffer.mPlanes[2].mWidth ||
|
||||
aBuffer.mPlanes[1].mHeight != aBuffer.mPlanes[2].mHeight) {
|
||||
NS_ERROR("C planes with different sizes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The following situations could be triggered by invalid input
|
||||
if (aPicture.width <= 0 || aPicture.height <= 0) {
|
||||
// In debug mode, makes the error more noticeable
|
||||
MOZ_ASSERT(false, "Empty picture rect");
|
||||
return false;
|
||||
}
|
||||
if (!ValidatePlane(aBuffer.mPlanes[0]) ||
|
||||
!ValidatePlane(aBuffer.mPlanes[1]) ||
|
||||
!ValidatePlane(aBuffer.mPlanes[2])) {
|
||||
NS_WARNING("Invalid plane size");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the picture size specified in the headers can be extracted out of
|
||||
// the frame we've been supplied without indexing out of bounds.
|
||||
CheckedUint32 xLimit = aPicture.x + CheckedUint32(aPicture.width);
|
||||
CheckedUint32 yLimit = aPicture.y + CheckedUint32(aPicture.height);
|
||||
if (!xLimit.isValid() || xLimit.value() > aBuffer.mPlanes[0].mStride ||
|
||||
!yLimit.isValid() || yLimit.value() > aBuffer.mPlanes[0].mHeight)
|
||||
{
|
||||
// The specified picture dimensions can't be contained inside the video
|
||||
// frame, we'll stomp memory if we try to copy it. Fail.
|
||||
NS_WARNING("Overflowing picture rect");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
static bool
|
||||
IsYV12Format(const VideoData::YCbCrBuffer::Plane& aYPlane,
|
||||
@ -261,36 +302,7 @@ VideoData::CreateAndCopyData(const VideoInfo& aInfo,
|
||||
return v.forget();
|
||||
}
|
||||
|
||||
// The following situation should never happen unless there is a bug
|
||||
// in the decoder
|
||||
if (aBuffer.mPlanes[1].mWidth != aBuffer.mPlanes[2].mWidth ||
|
||||
aBuffer.mPlanes[1].mHeight != aBuffer.mPlanes[2].mHeight) {
|
||||
NS_ERROR("C planes with different sizes");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The following situations could be triggered by invalid input
|
||||
if (aPicture.width <= 0 || aPicture.height <= 0) {
|
||||
// In debug mode, makes the error more noticeable
|
||||
MOZ_ASSERT(false, "Empty picture rect");
|
||||
return nullptr;
|
||||
}
|
||||
if (!ValidatePlane(aBuffer.mPlanes[0]) || !ValidatePlane(aBuffer.mPlanes[1]) ||
|
||||
!ValidatePlane(aBuffer.mPlanes[2])) {
|
||||
NS_WARNING("Invalid plane size");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Ensure the picture size specified in the headers can be extracted out of
|
||||
// the frame we've been supplied without indexing out of bounds.
|
||||
CheckedUint32 xLimit = aPicture.x + CheckedUint32(aPicture.width);
|
||||
CheckedUint32 yLimit = aPicture.y + CheckedUint32(aPicture.height);
|
||||
if (!xLimit.isValid() || xLimit.value() > aBuffer.mPlanes[0].mStride ||
|
||||
!yLimit.isValid() || yLimit.value() > aBuffer.mPlanes[0].mHeight)
|
||||
{
|
||||
// The specified picture dimensions can't be contained inside the video
|
||||
// frame, we'll stomp memory if we try to copy it. Fail.
|
||||
NS_WARNING("Overflowing picture rect");
|
||||
if (!ValidateBufferAndPicture(aBuffer, aPicture)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@ -348,6 +360,74 @@ VideoData::CreateAndCopyData(const VideoInfo& aInfo,
|
||||
return v.forget();
|
||||
}
|
||||
|
||||
|
||||
/* static */
|
||||
already_AddRefed<VideoData>
|
||||
VideoData::CreateAndCopyData(const VideoInfo& aInfo,
|
||||
ImageContainer* aContainer,
|
||||
int64_t aOffset,
|
||||
int64_t aTime,
|
||||
int64_t aDuration,
|
||||
const YCbCrBuffer& aBuffer,
|
||||
const YCbCrBuffer::Plane &aAlphaPlane,
|
||||
bool aKeyframe,
|
||||
int64_t aTimecode,
|
||||
const IntRect& aPicture)
|
||||
{
|
||||
if (!aContainer) {
|
||||
// Create a dummy VideoData with no image. This gives us something to
|
||||
// send to media streams if necessary.
|
||||
RefPtr<VideoData> v(new VideoData(aOffset,
|
||||
aTime,
|
||||
aDuration,
|
||||
aKeyframe,
|
||||
aTimecode,
|
||||
aInfo.mDisplay,
|
||||
0));
|
||||
return v.forget();
|
||||
}
|
||||
|
||||
if (!ValidateBufferAndPicture(aBuffer, aPicture)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<VideoData> v(new VideoData(aOffset,
|
||||
aTime,
|
||||
aDuration,
|
||||
aKeyframe,
|
||||
aTimecode,
|
||||
aInfo.mDisplay,
|
||||
0));
|
||||
|
||||
// Convert from YUVA to BGRA format on the software side.
|
||||
RefPtr<layers::SharedRGBImage> videoImage =
|
||||
aContainer->CreateSharedRGBImage();
|
||||
v->mImage = videoImage;
|
||||
|
||||
if (!v->mImage) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!videoImage->Allocate(IntSize(aBuffer.mPlanes[0].mWidth,
|
||||
aBuffer.mPlanes[0].mHeight),
|
||||
SurfaceFormat::B8G8R8A8)) {
|
||||
return nullptr;
|
||||
}
|
||||
uint8_t* argb_buffer = videoImage->GetBuffer();
|
||||
IntSize size = videoImage->GetSize();
|
||||
|
||||
// The naming convention for libyuv and associated utils is word-order.
|
||||
// The naming convention in the gfx stack is byte-order.
|
||||
ConvertYCbCrAToARGB(aBuffer.mPlanes[0].mData,
|
||||
aBuffer.mPlanes[1].mData,
|
||||
aBuffer.mPlanes[2].mData,
|
||||
aAlphaPlane.mData,
|
||||
aBuffer.mPlanes[0].mStride, aBuffer.mPlanes[1].mStride,
|
||||
argb_buffer, size.width * 4,
|
||||
size.width, size.height);
|
||||
|
||||
return v.forget();
|
||||
}
|
||||
|
||||
/* static */
|
||||
already_AddRefed<VideoData>
|
||||
VideoData::CreateFromImage(const VideoInfo& aInfo,
|
||||
|
@ -479,6 +479,17 @@ public:
|
||||
int64_t aTimecode,
|
||||
const IntRect& aPicture);
|
||||
|
||||
static already_AddRefed<VideoData> CreateAndCopyData(const VideoInfo& aInfo,
|
||||
ImageContainer* aContainer,
|
||||
int64_t aOffset,
|
||||
int64_t aTime,
|
||||
int64_t aDuration,
|
||||
const YCbCrBuffer &aBuffer,
|
||||
const YCbCrBuffer::Plane &aAlphaPlane,
|
||||
bool aKeyframe,
|
||||
int64_t aTimecode,
|
||||
const IntRect& aPicture);
|
||||
|
||||
static already_AddRefed<VideoData> CreateAndCopyIntoTextureClient(const VideoInfo& aInfo,
|
||||
int64_t aOffset,
|
||||
int64_t aTime,
|
||||
|
@ -1278,11 +1278,12 @@ private:
|
||||
return aSampleTime <= currentTime;
|
||||
});
|
||||
|
||||
if (!IsVideoRequestPending() && NeedMoreVideo()) {
|
||||
if (!NeedMoreVideo()) {
|
||||
FinishSeek();
|
||||
} else if (!Reader()->IsRequestingVideoData() &&
|
||||
!Reader()->IsWaitingVideoData()) {
|
||||
RequestVideoData();
|
||||
}
|
||||
|
||||
MaybeFinishSeek(); // Might resolve mSeekTaskPromise and modify audio queue.
|
||||
}
|
||||
|
||||
class AysncNextFrameSeekTask : public Runnable
|
||||
@ -1326,70 +1327,59 @@ private:
|
||||
|
||||
void HandleAudioDecoded(MediaData* aAudio) override
|
||||
{
|
||||
MOZ_ASSERT(aAudio);
|
||||
MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
|
||||
mMaster->Push(aAudio);
|
||||
MaybeFinishSeek();
|
||||
}
|
||||
|
||||
void HandleVideoDecoded(MediaData* aVideo, TimeStamp aDecodeStart) override
|
||||
{
|
||||
MOZ_ASSERT(aVideo);
|
||||
MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
|
||||
MOZ_ASSERT(NeedMoreVideo());
|
||||
|
||||
if (aVideo->mTime > mCurrentTime) {
|
||||
mMaster->Push(aVideo);
|
||||
}
|
||||
|
||||
if (NeedMoreVideo()) {
|
||||
FinishSeek();
|
||||
} else {
|
||||
RequestVideoData();
|
||||
return;
|
||||
}
|
||||
|
||||
MaybeFinishSeek();
|
||||
}
|
||||
|
||||
void HandleNotDecoded(MediaData::Type aType, const MediaResult& aError) override
|
||||
{
|
||||
MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
|
||||
MOZ_ASSERT(NeedMoreVideo());
|
||||
|
||||
switch (aType) {
|
||||
case MediaData::AUDIO_DATA:
|
||||
{
|
||||
// We don't really handle audio deocde error here. Let MDSM to trigger further
|
||||
// audio decoding tasks if it needs to play audio, and MDSM will then receive
|
||||
// the decoding state from MediaDecoderReader.
|
||||
|
||||
MaybeFinishSeek();
|
||||
// We don't care about audio decode errors in this state which will be
|
||||
// handled by other states after seeking.
|
||||
break;
|
||||
}
|
||||
case MediaData::VIDEO_DATA:
|
||||
{
|
||||
if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
|
||||
VideoQueue().Finish();
|
||||
FinishSeek();
|
||||
break;
|
||||
}
|
||||
|
||||
// Video seek not finished.
|
||||
if (NeedMoreVideo()) {
|
||||
switch (aError.Code()) {
|
||||
case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
|
||||
Reader()->WaitForData(MediaData::VIDEO_DATA);
|
||||
break;
|
||||
case NS_ERROR_DOM_MEDIA_CANCELED:
|
||||
RequestVideoData();
|
||||
break;
|
||||
case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
|
||||
MOZ_ASSERT(false, "Shouldn't want more data for ended video.");
|
||||
break;
|
||||
default:
|
||||
// Raise an error since we can't finish video seek anyway.
|
||||
mMaster->DecodeError(aError);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
switch (aError.Code()) {
|
||||
case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
|
||||
Reader()->WaitForData(MediaData::VIDEO_DATA);
|
||||
break;
|
||||
case NS_ERROR_DOM_MEDIA_CANCELED:
|
||||
RequestVideoData();
|
||||
break;
|
||||
case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
|
||||
MOZ_ASSERT(false, "Shouldn't want more data for ended video.");
|
||||
break;
|
||||
default:
|
||||
// Raise an error since we can't finish video seek anyway.
|
||||
mMaster->DecodeError(aError);
|
||||
break;
|
||||
}
|
||||
|
||||
MaybeFinishSeek();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -1399,44 +1389,31 @@ private:
|
||||
|
||||
void HandleAudioWaited(MediaData::Type aType) override
|
||||
{
|
||||
MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
|
||||
|
||||
// We don't make an audio decode request here, instead, let MDSM to
|
||||
// trigger further audio decode tasks if MDSM itself needs to play audio.
|
||||
MaybeFinishSeek();
|
||||
// We don't care about audio in this state.
|
||||
}
|
||||
|
||||
void HandleVideoWaited(MediaData::Type aType) override
|
||||
{
|
||||
MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
|
||||
|
||||
if (NeedMoreVideo()) {
|
||||
RequestVideoData();
|
||||
return;
|
||||
}
|
||||
MaybeFinishSeek();
|
||||
MOZ_ASSERT(NeedMoreVideo());
|
||||
RequestVideoData();
|
||||
}
|
||||
|
||||
void HandleNotWaited(const WaitForDataRejectValue& aRejection) override
|
||||
{
|
||||
MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
|
||||
MOZ_ASSERT(NeedMoreVideo());
|
||||
|
||||
switch(aRejection.mType) {
|
||||
case MediaData::AUDIO_DATA:
|
||||
{
|
||||
// We don't make an audio decode request here, instead, let MDSM to
|
||||
// trigger further audio decode tasks if MDSM itself needs to play audio.
|
||||
MaybeFinishSeek();
|
||||
// We don't care about audio in this state.
|
||||
break;
|
||||
}
|
||||
case MediaData::VIDEO_DATA:
|
||||
{
|
||||
if (NeedMoreVideo()) {
|
||||
// Error out if we can't finish video seeking.
|
||||
mMaster->DecodeError(NS_ERROR_DOM_MEDIA_CANCELED);
|
||||
return;
|
||||
}
|
||||
MaybeFinishSeek();
|
||||
// Error out if we can't finish video seeking.
|
||||
mMaster->DecodeError(NS_ERROR_DOM_MEDIA_CANCELED);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -1463,25 +1440,6 @@ private:
|
||||
!VideoQueue().IsFinished();
|
||||
}
|
||||
|
||||
bool IsVideoRequestPending() const
|
||||
{
|
||||
return Reader()->IsRequestingVideoData() || Reader()->IsWaitingVideoData();
|
||||
}
|
||||
|
||||
bool IsAudioSeekComplete() const
|
||||
{
|
||||
// Don't finish seek until there are no pending requests. Otherwise, we might
|
||||
// lose audio samples for the promise is resolved asynchronously.
|
||||
return !Reader()->IsRequestingAudioData() && !Reader()->IsWaitingAudioData();
|
||||
}
|
||||
|
||||
bool IsVideoSeekComplete() const
|
||||
{
|
||||
// Don't finish seek until there are no pending requests. Otherwise, we might
|
||||
// lose video samples for the promise is resolved asynchronously.
|
||||
return !IsVideoRequestPending() && !NeedMoreVideo();
|
||||
}
|
||||
|
||||
// Update the seek target's time before resolving this seek task, the updated
|
||||
// time will be used in the MDSM::SeekCompleted() to update the MDSM's position.
|
||||
void UpdateSeekTargetTime()
|
||||
@ -1489,25 +1447,21 @@ private:
|
||||
RefPtr<MediaData> data = VideoQueue().PeekFront();
|
||||
if (data) {
|
||||
mSeekJob.mTarget->SetTime(TimeUnit::FromMicroseconds(data->mTime));
|
||||
} else if (VideoQueue().AtEndOfStream()) {
|
||||
mSeekJob.mTarget->SetTime(mDuration);
|
||||
} else {
|
||||
MOZ_ASSERT(false, "No data!");
|
||||
MOZ_ASSERT(VideoQueue().AtEndOfStream());
|
||||
mSeekJob.mTarget->SetTime(mDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void MaybeFinishSeek()
|
||||
void FinishSeek()
|
||||
{
|
||||
if (IsAudioSeekComplete() && IsVideoSeekComplete()) {
|
||||
UpdateSeekTargetTime();
|
||||
|
||||
auto time = mSeekJob.mTarget->GetTime().ToMicroseconds();
|
||||
DiscardFrames(AudioQueue(), [time] (int64_t aSampleTime) {
|
||||
return aSampleTime < time;
|
||||
});
|
||||
|
||||
SeekCompleted();
|
||||
}
|
||||
MOZ_ASSERT(!NeedMoreVideo());
|
||||
UpdateSeekTargetTime();
|
||||
auto time = mSeekJob.mTarget->GetTime().ToMicroseconds();
|
||||
DiscardFrames(AudioQueue(), [time] (int64_t aSampleTime) {
|
||||
return aSampleTime < time;
|
||||
});
|
||||
SeekCompleted();
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -46,7 +46,7 @@ class SourceFilter;
|
||||
class DirectShowReader : public MediaDecoderReader
|
||||
{
|
||||
public:
|
||||
DirectShowReader(AbstractMediaDecoder* aDecoder);
|
||||
explicit DirectShowReader(AbstractMediaDecoder* aDecoder);
|
||||
|
||||
virtual ~DirectShowReader();
|
||||
|
||||
|
@ -26,7 +26,7 @@ namespace mozilla {
|
||||
class Signal {
|
||||
public:
|
||||
|
||||
Signal(CriticalSection* aLock)
|
||||
explicit Signal(CriticalSection* aLock)
|
||||
: mLock(aLock)
|
||||
{
|
||||
CriticalSectionAutoEnter lock(*mLock);
|
||||
|
@ -64,7 +64,7 @@ ToHexString(const uint8_t * aBytes, uint32_t aLength)
|
||||
nsCString str;
|
||||
for (uint32_t i = 0; i < aLength; i++) {
|
||||
char buf[3];
|
||||
buf[0] = hex[aBytes[i] & 0xf0 >> 4];
|
||||
buf[0] = hex[(aBytes[i] & 0xf0) >> 4];
|
||||
buf[1] = hex[aBytes[i] & 0x0f];
|
||||
buf[2] = 0;
|
||||
str.AppendASCII(buf);
|
||||
|
@ -30,10 +30,10 @@ void TestSplitAt(const char* aInput,
|
||||
EXPECT_TRUE(tokens[i].EqualsASCII(aExpectedTokens[i]))
|
||||
<< "Tokenize fail; expected=" << aExpectedTokens[i] << " got=" <<
|
||||
tokens[i].BeginReading();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(GeckoMediaPlugins, GMPUtils) {
|
||||
TEST(GeckoMediaPlugins, TestSplitAt) {
|
||||
{
|
||||
const char* input = "1,2,3,4";
|
||||
const char* delims = ",";
|
||||
@ -59,3 +59,35 @@ TEST(GeckoMediaPlugins, GMPUtils) {
|
||||
TestSplitAt(input, delims, MOZ_ARRAY_LENGTH(tokens), tokens);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(GeckoMediaPlugins, ToHexString) {
|
||||
struct Test {
|
||||
nsTArray<uint8_t> bytes;
|
||||
string hex;
|
||||
};
|
||||
|
||||
static const Test tests[] = {
|
||||
{ {0x00, 0x00}, "0000" },
|
||||
{ {0xff, 0xff}, "ffff" },
|
||||
{ {0xff, 0x00}, "ff00" },
|
||||
{ {0x00, 0xff}, "00ff" },
|
||||
{ {0xf0, 0x10}, "f010" },
|
||||
{ {0x05, 0x50}, "0550" },
|
||||
{ {0xf}, "0f" },
|
||||
{ {0x10}, "10" },
|
||||
{ {}, "" },
|
||||
{
|
||||
{
|
||||
0x00, 0x11, 0x22, 0x33,
|
||||
0x44, 0x55, 0x66, 0x77,
|
||||
0x88, 0x99, 0xaa, 0xbb,
|
||||
0xcc, 0xdd, 0xee, 0xff
|
||||
},
|
||||
"00112233445566778899aabbccddeeff"
|
||||
},
|
||||
};
|
||||
|
||||
for (const Test& test : tests) {
|
||||
EXPECT_STREQ(test.hex.c_str(), ToHexString(test.bytes).get());
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,38 @@ static int MimeTypeToCodec(const nsACString& aMimeType)
|
||||
return -1;
|
||||
}
|
||||
|
||||
static nsresult
|
||||
InitContext(vpx_codec_ctx_t* aCtx,
|
||||
const VideoInfo& aInfo,
|
||||
const int aCodec)
|
||||
{
|
||||
int decode_threads = 2;
|
||||
|
||||
vpx_codec_iface_t* dx = nullptr;
|
||||
if (aCodec == VPXDecoder::Codec::VP8) {
|
||||
dx = vpx_codec_vp8_dx();
|
||||
}
|
||||
else if (aCodec == VPXDecoder::Codec::VP9) {
|
||||
dx = vpx_codec_vp9_dx();
|
||||
if (aInfo.mDisplay.width >= 2048) {
|
||||
decode_threads = 8;
|
||||
}
|
||||
else if (aInfo.mDisplay.width >= 1024) {
|
||||
decode_threads = 4;
|
||||
}
|
||||
}
|
||||
decode_threads = std::min(decode_threads, PR_GetNumberOfProcessors());
|
||||
|
||||
vpx_codec_dec_cfg_t config;
|
||||
config.threads = decode_threads;
|
||||
config.w = config.h = 0; // set after decode
|
||||
|
||||
if (!dx || vpx_codec_dec_init(aCtx, dx, &config, 0)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
VPXDecoder::VPXDecoder(const CreateDecoderParams& aParams)
|
||||
: mImageContainer(aParams.mImageContainer)
|
||||
, mTaskQueue(aParams.mTaskQueue)
|
||||
@ -44,6 +76,7 @@ VPXDecoder::VPXDecoder(const CreateDecoderParams& aParams)
|
||||
{
|
||||
MOZ_COUNT_CTOR(VPXDecoder);
|
||||
PodZero(&mVPX);
|
||||
PodZero(&mVPXAlpha);
|
||||
}
|
||||
|
||||
VPXDecoder::~VPXDecoder()
|
||||
@ -60,29 +93,18 @@ VPXDecoder::Shutdown()
|
||||
RefPtr<MediaDataDecoder::InitPromise>
|
||||
VPXDecoder::Init()
|
||||
{
|
||||
int decode_threads = 2;
|
||||
|
||||
vpx_codec_iface_t* dx = nullptr;
|
||||
if (mCodec == Codec::VP8) {
|
||||
dx = vpx_codec_vp8_dx();
|
||||
} else if (mCodec == Codec::VP9) {
|
||||
dx = vpx_codec_vp9_dx();
|
||||
if (mInfo.mDisplay.width >= 2048) {
|
||||
decode_threads = 8;
|
||||
} else if (mInfo.mDisplay.width >= 1024) {
|
||||
decode_threads = 4;
|
||||
if (NS_FAILED(InitContext(&mVPX, mInfo, mCodec))) {
|
||||
return VPXDecoder::InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
|
||||
__func__);
|
||||
}
|
||||
if (mInfo.HasAlpha()) {
|
||||
if (NS_FAILED(InitContext(&mVPXAlpha, mInfo, mCodec))) {
|
||||
return VPXDecoder::InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
|
||||
__func__);
|
||||
}
|
||||
}
|
||||
decode_threads = std::min(decode_threads, PR_GetNumberOfProcessors());
|
||||
|
||||
vpx_codec_dec_cfg_t config;
|
||||
config.threads = decode_threads;
|
||||
config.w = config.h = 0; // set after decode
|
||||
|
||||
if (!dx || vpx_codec_dec_init(&mVPX, dx, &config, 0)) {
|
||||
return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
|
||||
}
|
||||
return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
|
||||
return VPXDecoder::InitPromise::CreateAndResolve(TrackInfo::kVideoTrack,
|
||||
__func__);
|
||||
}
|
||||
|
||||
void
|
||||
@ -121,14 +143,27 @@ VPXDecoder::DoDecode(MediaRawData* aSample)
|
||||
RESULT_DETAIL("VPX error: %s", vpx_codec_err_to_string(r)));
|
||||
}
|
||||
|
||||
vpx_codec_iter_t iter = nullptr;
|
||||
vpx_image_t *img;
|
||||
vpx_codec_iter_t iter = nullptr;
|
||||
vpx_image_t *img;
|
||||
vpx_image_t *img_alpha = nullptr;
|
||||
bool alpha_decoded = false;
|
||||
|
||||
while ((img = vpx_codec_get_frame(&mVPX, &iter))) {
|
||||
NS_ASSERTION(img->fmt == VPX_IMG_FMT_I420 ||
|
||||
img->fmt == VPX_IMG_FMT_I444,
|
||||
"WebM image format not I420 or I444");
|
||||
NS_ASSERTION(!alpha_decoded,
|
||||
"Multiple frames per packet that contains alpha");
|
||||
|
||||
if (aSample->AlphaSize() > 0) {
|
||||
if(!alpha_decoded){
|
||||
MediaResult rv = DecodeAlpha(&img_alpha, aSample);
|
||||
if (NS_FAILED(rv)) {
|
||||
return(rv);
|
||||
}
|
||||
alpha_decoded = true;
|
||||
}
|
||||
}
|
||||
// Chroma shifts are rounded down as per the decoding examples in the SDK
|
||||
VideoData::YCbCrBuffer b;
|
||||
b.mPlanes[0].mData = img->planes[0];
|
||||
@ -163,17 +198,38 @@ VPXDecoder::DoDecode(MediaRawData* aSample)
|
||||
RESULT_DETAIL("VPX Unknown image format"));
|
||||
}
|
||||
|
||||
RefPtr<VideoData> v =
|
||||
VideoData::CreateAndCopyData(mInfo,
|
||||
mImageContainer,
|
||||
aSample->mOffset,
|
||||
aSample->mTime,
|
||||
aSample->mDuration,
|
||||
b,
|
||||
aSample->mKeyframe,
|
||||
aSample->mTimecode,
|
||||
mInfo.ScaledImageRect(img->d_w,
|
||||
img->d_h));
|
||||
RefPtr<VideoData> v;
|
||||
if (!img_alpha) {
|
||||
v = VideoData::CreateAndCopyData(mInfo,
|
||||
mImageContainer,
|
||||
aSample->mOffset,
|
||||
aSample->mTime,
|
||||
aSample->mDuration,
|
||||
b,
|
||||
aSample->mKeyframe,
|
||||
aSample->mTimecode,
|
||||
mInfo.ScaledImageRect(img->d_w,
|
||||
img->d_h));
|
||||
} else {
|
||||
VideoData::YCbCrBuffer::Plane alpha_plane;
|
||||
alpha_plane.mData = img_alpha->planes[0];
|
||||
alpha_plane.mStride = img_alpha->stride[0];
|
||||
alpha_plane.mHeight = img_alpha->d_h;
|
||||
alpha_plane.mWidth = img_alpha->d_w;
|
||||
alpha_plane.mOffset = alpha_plane.mSkip = 0;
|
||||
v = VideoData::CreateAndCopyData(mInfo,
|
||||
mImageContainer,
|
||||
aSample->mOffset,
|
||||
aSample->mTime,
|
||||
aSample->mDuration,
|
||||
b,
|
||||
alpha_plane,
|
||||
aSample->mKeyframe,
|
||||
aSample->mTimecode,
|
||||
mInfo.ScaledImageRect(img->d_w,
|
||||
img->d_h));
|
||||
|
||||
}
|
||||
|
||||
if (!v) {
|
||||
LOG("Image allocation error source %ldx%ld display %ldx%ld picture %ldx%ld",
|
||||
@ -223,6 +279,32 @@ VPXDecoder::Drain()
|
||||
mTaskQueue->Dispatch(NewRunnableMethod(this, &VPXDecoder::ProcessDrain));
|
||||
}
|
||||
|
||||
MediaResult
|
||||
VPXDecoder::DecodeAlpha(vpx_image_t** aImgAlpha,
|
||||
MediaRawData* aSample)
|
||||
{
|
||||
vpx_codec_err_t r = vpx_codec_decode(&mVPXAlpha,
|
||||
aSample->AlphaData(),
|
||||
aSample->AlphaSize(),
|
||||
nullptr,
|
||||
0);
|
||||
if (r) {
|
||||
LOG("VPX decode alpha error: %s", vpx_codec_err_to_string(r));
|
||||
return MediaResult(
|
||||
NS_ERROR_DOM_MEDIA_DECODE_ERR,
|
||||
RESULT_DETAIL("VPX decode alpha error: %s", vpx_codec_err_to_string(r)));
|
||||
}
|
||||
|
||||
vpx_codec_iter_t iter = nullptr;
|
||||
|
||||
*aImgAlpha = vpx_codec_get_frame(&mVPXAlpha, &iter);
|
||||
NS_ASSERTION((*aImgAlpha)->fmt == VPX_IMG_FMT_I420 ||
|
||||
(*aImgAlpha)->fmt == VPX_IMG_FMT_I444,
|
||||
"WebM image format not I420 or I444");
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */
|
||||
bool
|
||||
VPXDecoder::IsVPX(const nsACString& aMimeType, uint8_t aCodecMask)
|
||||
|
@ -50,6 +50,8 @@ private:
|
||||
void ProcessDecode(MediaRawData* aSample);
|
||||
MediaResult DoDecode(MediaRawData* aSample);
|
||||
void ProcessDrain();
|
||||
MediaResult DecodeAlpha(vpx_image_t** aImgAlpha,
|
||||
MediaRawData* aSample);
|
||||
|
||||
const RefPtr<ImageContainer> mImageContainer;
|
||||
const RefPtr<TaskQueue> mTaskQueue;
|
||||
@ -59,6 +61,9 @@ private:
|
||||
// VPx decoder state
|
||||
vpx_codec_ctx_t mVPX;
|
||||
|
||||
// VPx alpha decoder state
|
||||
vpx_codec_ctx_t mVPXAlpha;
|
||||
|
||||
const VideoInfo& mInfo;
|
||||
|
||||
const int mCodec;
|
||||
|
@ -174,6 +174,13 @@ AndroidDecoderModule::SupportsMimeType(const nsACString& aMimeType,
|
||||
already_AddRefed<MediaDataDecoder>
|
||||
AndroidDecoderModule::CreateVideoDecoder(const CreateDecoderParams& aParams)
|
||||
{
|
||||
// Temporary - forces use of VPXDecoder when alpha is present.
|
||||
// Bug 1263836 will handle alpha scenario once implemented. It will shift
|
||||
// the check for alpha to PDMFactory but not itself remove the need for a
|
||||
// check.
|
||||
if (aParams.VideoConfig().HasAlpha()) {
|
||||
return nullptr;
|
||||
}
|
||||
MediaFormat::LocalRef format;
|
||||
|
||||
const VideoInfo& config = aParams.VideoConfig();
|
||||
|
@ -33,6 +33,13 @@ public:
|
||||
already_AddRefed<MediaDataDecoder>
|
||||
CreateVideoDecoder(const CreateDecoderParams& aParams) override
|
||||
{
|
||||
// Temporary - forces use of VPXDecoder when alpha is present.
|
||||
// Bug 1263836 will handle alpha scenario once implemented. It will shift
|
||||
// the check for alpha to PDMFactory but not itself remove the need for a
|
||||
// check.
|
||||
if (aParams.VideoConfig().HasAlpha()) {
|
||||
return nullptr;
|
||||
}
|
||||
RefPtr<MediaDataDecoder> decoder =
|
||||
new FFmpegVideoDecoder<V>(mLib,
|
||||
aParams.mTaskQueue,
|
||||
|
@ -18,7 +18,7 @@ namespace mozilla {
|
||||
|
||||
class WMFAudioMFTManager : public MFTManager {
|
||||
public:
|
||||
WMFAudioMFTManager(const AudioInfo& aConfig);
|
||||
explicit WMFAudioMFTManager(const AudioInfo& aConfig);
|
||||
~WMFAudioMFTManager();
|
||||
|
||||
bool Init();
|
||||
|
@ -882,7 +882,6 @@ skip-if = true # bug 1021673
|
||||
[test_seekToNextFrame.html]
|
||||
tags=seektonextframe
|
||||
[test_source.html]
|
||||
[test_source_media.html]
|
||||
[test_source_null.html]
|
||||
[test_source_write.html]
|
||||
[test_standalone.html]
|
||||
|
@ -1,71 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Media test: media attribute for the source element.</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<script type="text/javascript" src="manifest.js"></script>
|
||||
<script type="text/javascript" src="../../../dom/html/test/reflect.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script type="text/javascript">
|
||||
var testCount = 0;
|
||||
function notifyFinished() {
|
||||
testCount++;
|
||||
if (testCount == 2) {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
}
|
||||
|
||||
function clearNode(n) {
|
||||
n.remove();
|
||||
n.src = "";
|
||||
while (n.firstChild) {
|
||||
n.removeChild(n.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
reflectString({
|
||||
element: document.createElement("source"),
|
||||
attribute: "media",
|
||||
});
|
||||
|
||||
var media = getPlayableVideo(gSmallTests);
|
||||
|
||||
if (media == null) {
|
||||
todo(false, "No media supported.");
|
||||
SimpleTest.finish();
|
||||
} else {
|
||||
var v = document.createElement('video');
|
||||
v.preload = "metadata";
|
||||
v.innerHTML = "<source src=\"" + media.name + "?fail\" media=\"not all\">" +
|
||||
"<source src=\""+ media.name + "?pass\" media=\"all\">";
|
||||
var v2 = document.createElement("video");
|
||||
v2.preload = "metadata";
|
||||
v2.innerHTML = "<source src=\""+ media.name +"?pass\">" +
|
||||
"<source src=\""+ media.name + "?fail\" media=\"all\">";
|
||||
document.body.appendChild(v);
|
||||
document.body.appendChild(v2);
|
||||
|
||||
v.addEventListener("loadedmetadata", function(e) {
|
||||
ok(/pass/.test(e.target.currentSrc),
|
||||
"The source has been chosen according to the media attribute.");
|
||||
clearNode(e.target);
|
||||
notifyFinished();
|
||||
});
|
||||
v2.addEventListener("loadedmetadata", function(e) {
|
||||
ok(/pass/.test(e.target.currentSrc),
|
||||
"If no media attribute is specified, it defaults to \'all\'.")
|
||||
clearNode(e.target);
|
||||
notifyFinished();
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -13,6 +13,7 @@
|
||||
#include "mozilla/dom/RTCCertificateBinding.h"
|
||||
#include "mozilla/dom/WebCryptoCommon.h"
|
||||
#include "mozilla/dom/WebCryptoTask.h"
|
||||
#include "mozilla/Move.h"
|
||||
#include "mozilla/Sprintf.h"
|
||||
|
||||
#include <cstdio>
|
||||
@ -333,9 +334,9 @@ RTCCertificate::CreateDtlsIdentity() const
|
||||
if (isAlreadyShutDown() || !mPrivateKey || !mCertificate) {
|
||||
return nullptr;
|
||||
}
|
||||
SECKEYPrivateKey* key = SECKEY_CopyPrivateKey(mPrivateKey.get());
|
||||
CERTCertificate* cert = CERT_DupCertificate(mCertificate.get());
|
||||
RefPtr<DtlsIdentity> id = new DtlsIdentity(key, cert, mAuthType);
|
||||
UniqueSECKEYPrivateKey key(SECKEY_CopyPrivateKey(mPrivateKey.get()));
|
||||
UniqueCERTCertificate cert(CERT_DupCertificate(mCertificate.get()));
|
||||
RefPtr<DtlsIdentity> id = new DtlsIdentity(Move(key), Move(cert), mAuthType);
|
||||
return id;
|
||||
}
|
||||
|
||||
|
@ -159,7 +159,7 @@ static bool ProcessFlashMessageDelayed(nsPluginNativeWindowWin * aWin, nsNPAPIPl
|
||||
class nsDelayedPopupsEnabledEvent : public Runnable
|
||||
{
|
||||
public:
|
||||
nsDelayedPopupsEnabledEvent(nsNPAPIPluginInstance *inst)
|
||||
explicit nsDelayedPopupsEnabledEvent(nsNPAPIPluginInstance *inst)
|
||||
: mInst(inst)
|
||||
{}
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
[DEFAULT]
|
||||
skip-if = toolkit == 'android'
|
||||
|
||||
[test_closewindow-with-pointerlock.html]
|
||||
|
||||
[test_pointerlock-api.html]
|
||||
tags = fullscreen
|
||||
support-files =
|
||||
pointerlock_utils.js
|
||||
file_pointerlock-api.html
|
||||
@ -20,7 +26,3 @@ support-files =
|
||||
file_allowPointerLockSandboxFlag.html
|
||||
file_changeLockElement.html
|
||||
iframe_differentDOM.html
|
||||
|
||||
[test_pointerlock-api.html]
|
||||
tags = fullscreen
|
||||
skip-if = toolkit == 'android'
|
||||
|
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bug 1323983 - Auto-close window after holding pointerlock</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body style="width: 100vw; height: 100vh; margin: 0;">
|
||||
<script>
|
||||
if (!opener) {
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
}
|
||||
|
||||
var newwin = null;
|
||||
function finish() {
|
||||
newwin.close()
|
||||
setTimeout(function() {
|
||||
SimpleTest.finish();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
addLoadEvent(function() {
|
||||
SimpleTest.waitForFocus(function() {
|
||||
if (!opener) {
|
||||
newwin = window.open(location);
|
||||
} else {
|
||||
document.addEventListener("pointerlockchange", function() {
|
||||
opener.is(document.pointerLockElement, document.body,
|
||||
"Check we have locked the pointer");
|
||||
opener.finish();
|
||||
}, {once: true});
|
||||
document.addEventListener("pointerlockerror", function() {
|
||||
opener.info("Fail to lock pointer");
|
||||
opener.finish();
|
||||
});
|
||||
document.addEventListener("click", function() {
|
||||
opener.info("Clicked");
|
||||
document.body.requestPointerLock();
|
||||
}, {once: true});
|
||||
setTimeout(function() {
|
||||
opener.info("Clicking");
|
||||
synthesizeMouseAtCenter(document.body, {});
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user