merge autoland to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2016-12-22 16:18:39 +01:00
commit 477c85db33
393 changed files with 12740 additions and 5364 deletions

View File

@ -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

View File

@ -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; }

View File

@ -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) { }

View File

@ -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

View File

@ -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

View File

@ -19,7 +19,7 @@ namespace a11y {
class sdnAccessible final : public ISimpleDOMNode
{
public:
sdnAccessible(nsINode* aNode) :
explicit sdnAccessible(nsINode* aNode) :
mNode(aNode)
{
if (!mNode)

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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()
{

View File

@ -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);
});

View File

@ -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() {

View File

@ -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;"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -10,6 +10,7 @@ DIRS += [
'pdfjs',
'pocket',
'webcompat',
'shield-recipe-client',
]
# Only include the following system add-ons if building Aurora or Nightly

View 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;
}
}
}

View 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);
},
};
};

View 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>

View 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/*)

View 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";
this.EXPORTED_SYMBOLS = ["CleanupManager"];
const cleanupHandlers = [];
this.CleanupManager = {
addCleanupHandler(handler) {
cleanupHandlers.push(handler);
},
cleanup() {
for (const handler of cleanupHandlers) {
handler();
}
},
};

View File

@ -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);
},
};

View 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;
}
};

View 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());
},
};

View 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}`);
},
};
};

View 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"));
}),
};

View 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}`);
});
},
};

View File

@ -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;
}

View 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,
});
},
};

View 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']

View 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.

View 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;

View 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;

View 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;

View 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);
});
};

View 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; }}
};

View 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;

View 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
});
};

View 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
}
};

View 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,
}
};

View 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());
};
},
};

View File

@ -0,0 +1,5 @@
[browser_driver_uuids.js]
[browser_env_expressions.js]
[browser_EventEmitter.js]
[browser_Storage.js]
[browser_Heartbeat.js]

View File

@ -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));
});

View File

@ -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));
});

View File

@ -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);
});

View File

@ -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));
});

View File

@ -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");
});

View File

@ -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)

View File

@ -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]

View File

@ -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.

View File

@ -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">

View File

@ -51,9 +51,9 @@
<p>&brandShortName; cant 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">

View File

@ -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) {

View File

@ -33,6 +33,7 @@ add_task(function* setup() {
Services.search.currentEngine = originalEngine;
Services.search.removeEngine(engineDefault);
Services.search.removeEngine(engineOneOff);
yield PlacesTestUtils.clearHistory();
});
});

View File

@ -35,6 +35,7 @@ add_task(function* setup() {
Services.search.currentEngine = originalEngine;
Services.search.removeEngine(engineDefault);
Services.search.removeEngine(engineOneOff);
yield PlacesTestUtils.clearHistory();
});
});

View File

@ -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);

View File

@ -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;

View File

@ -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) {

View File

@ -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>

View File

@ -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

View File

@ -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',

View File

@ -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
View 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

View File

@ -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);
}
}

View File

@ -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};
}

View File

@ -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;
}

View File

@ -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

View File

@ -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 ) ;
};

View File

@ -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 &&

View File

@ -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));

View File

@ -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;
}

View File

@ -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,

View File

@ -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,

View File

@ -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();
}
/*

View File

@ -46,7 +46,7 @@ class SourceFilter;
class DirectShowReader : public MediaDecoderReader
{
public:
DirectShowReader(AbstractMediaDecoder* aDecoder);
explicit DirectShowReader(AbstractMediaDecoder* aDecoder);
virtual ~DirectShowReader();

View File

@ -26,7 +26,7 @@ namespace mozilla {
class Signal {
public:
Signal(CriticalSection* aLock)
explicit Signal(CriticalSection* aLock)
: mLock(aLock)
{
CriticalSectionAutoEnter lock(*mLock);

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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)

View File

@ -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;

View File

@ -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();

View File

@ -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,

View File

@ -18,7 +18,7 @@ namespace mozilla {
class WMFAudioMFTManager : public MFTManager {
public:
WMFAudioMFTManager(const AudioInfo& aConfig);
explicit WMFAudioMFTManager(const AudioInfo& aConfig);
~WMFAudioMFTManager();
bool Init();

View File

@ -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]

View File

@ -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>

View File

@ -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;
}

View File

@ -159,7 +159,7 @@ static bool ProcessFlashMessageDelayed(nsPluginNativeWindowWin * aWin, nsNPAPIPl
class nsDelayedPopupsEnabledEvent : public Runnable
{
public:
nsDelayedPopupsEnabledEvent(nsNPAPIPluginInstance *inst)
explicit nsDelayedPopupsEnabledEvent(nsNPAPIPluginInstance *inst)
: mInst(inst)
{}

View File

@ -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'

View File

@ -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