merge fx-team to mozilla-central a=merge

--HG--
extra : amend_source : ad367ce5c609bdeabd41e252d379aae9bee81e04
This commit is contained in:
Carsten "Tomcat" Book 2016-04-15 11:39:35 +02:00
commit 26fa2ff692
95 changed files with 1765 additions and 725 deletions

View File

@ -696,6 +696,9 @@ html|*#fullscreen-warning[ontop] {
/* Use -10px to hide the border and border-radius on the top */
transform: translate(-50%, -10px);
}
#main-window[OSXLionFullscreen] html|*#fullscreen-warning[ontop] {
transform: translate(-50%, 80px);
}
html|*#fullscreen-domain-text,
html|*#fullscreen-generic-text {

View File

@ -2376,25 +2376,36 @@ function URLBarSetURI(aURI) {
}
function losslessDecodeURI(aURI) {
if (aURI.schemeIs("moz-action"))
let scheme = aURI.scheme;
if (scheme == "moz-action")
throw new Error("losslessDecodeURI should never get a moz-action URI");
var value = aURI.spec;
let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
// Try to decode as UTF-8 if there's no encoding sequence that we would break.
if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value))
try {
value = decodeURI(value)
// 1. decodeURI decodes %25 to %, which creates unintended
// encoding sequences. Re-encode it, unless it's part of
// a sequence that survived decodeURI, i.e. one for:
// ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
// (RFC 3987 section 3.2)
// 2. Re-encode whitespace so that it doesn't get eaten away
// by the location bar (bug 410726).
.replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|[\r\n\t]/ig,
encodeURIComponent);
} catch (e) {}
if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
if (decodeASCIIOnly) {
// This only decodes ascii characters (hex) 20-7e, except 25 (%).
// This avoids both cases stipulated below (%-related issues, and \r, \n
// and \t, which would be %0d, %0a and %09, respectively) as well as any
// non-US-ascii characters.
value = value.replace(/%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, decodeURI);
} else {
try {
value = decodeURI(value)
// 1. decodeURI decodes %25 to %, which creates unintended
// encoding sequences. Re-encode it, unless it's part of
// a sequence that survived decodeURI, i.e. one for:
// ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
// (RFC 3987 section 3.2)
// 2. Re-encode whitespace so that it doesn't get eaten away
// by the location bar (bug 410726).
.replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|[\r\n\t]/ig,
encodeURIComponent);
} catch (e) {}
}
}
// Encode invisible characters (C0/C1 control characters, U+007F [DEL],
// U+00A0 [no-break space], line and paragraph separator,

View File

@ -120,27 +120,27 @@ var tests = [
// data: and javsacript: URIs shouldn't be encoded
{
loadURL: "javascript:('%C3%A9')",
expectedURL: "javascript:('\xe9')",
copyExpected: "javascript:('\xe9')"
loadURL: "javascript:('%C3%A9%20%25%50')",
expectedURL: "javascript:('%C3%A9 %25P')",
copyExpected: "javascript:('%C3%A9 %25P')"
},
{
copyVal: "<javascript:(>'\xe9')",
copyVal: "<javascript:(>'%C3%A9 %25P')",
copyExpected: "javascript:("
},
{
loadURL: "data:text/html,(%C3%A9)",
expectedURL: "data:text/html,(\xe9)",
copyExpected: "data:text/html,(\xe9)"
loadURL: "data:text/html,(%C3%A9%20%25%50)",
expectedURL: "data:text/html,(%C3%A9 %25P)",
copyExpected: "data:text/html,(%C3%A9 %25P)",
},
{
copyVal: "<data:text/html,(>\xe9)",
copyVal: "<data:text/html,(>%C3%A9 %25P)",
copyExpected: "data:text/html,("
},
{
copyVal: "data:<text/html,(\xe9>)",
copyExpected: "text/html,(\xe9"
copyVal: "<data:text/html,(%C3%A9 %25P>)",
copyExpected: "data:text/html,(%C3%A9 %25P",
}
];

View File

@ -475,11 +475,13 @@ const CustomizableWidgets = [
_createTabElement(doc, tabInfo) {
let win = doc.defaultView;
let item = doc.createElementNS(kNSXUL, "toolbarbutton");
let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
item.setAttribute("itemtype", "tab");
item.setAttribute("class", "subviewbutton");
item.setAttribute("targetURI", tabInfo.url);
item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url);
item.setAttribute("image", tabInfo.icon);
item.setAttribute("tooltiptext", tooltipText);
// We need to use "click" instead of "command" here so openUILink
// respects different buttons (eg, to open in a new tab).
item.addEventListener("click", e => {

View File

@ -234,15 +234,16 @@ CustomizeMode.prototype = {
Task.spawn(function*() {
// We shouldn't start customize mode until after browser-delayed-startup has finished:
if (!this.window.gBrowserInit.delayedStartupFinished) {
let delayedStartupDeferred = Promise.defer();
let delayedStartupObserver = function(aSubject) {
if (aSubject == this.window) {
Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
delayedStartupDeferred.resolve();
}
}.bind(this);
Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
yield delayedStartupDeferred.promise;
yield new Promise(resolve => {
let delayedStartupObserver = aSubject => {
if (aSubject == this.window) {
Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
resolve();
}
};
Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
});
}
let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn);
@ -562,33 +563,35 @@ CustomizeMode.prototype = {
* excluding certain styles while in any phase of customize mode.
*/
_doTransition: function(aEntering) {
let deferred = Promise.defer();
let deck = this.document.getElementById("content-deck");
let customizeTransitionEnd = (aEvent) => {
if (aEvent != "timedout" &&
(aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
return;
}
this.window.clearTimeout(catchAllTimeout);
// We request an animation frame to do the final stage of the transition
// to improve perceived performance. (bug 962677)
this.window.requestAnimationFrame(() => {
deck.removeEventListener("transitionend", customizeTransitionEnd);
if (!aEntering) {
this.document.documentElement.removeAttribute("customize-exiting");
this.document.documentElement.removeAttribute("customizing");
} else {
this.document.documentElement.setAttribute("customize-entered", true);
this.document.documentElement.removeAttribute("customize-entering");
let customizeTransitionEndPromise = new Promise(resolve => {
let customizeTransitionEnd = (aEvent) => {
if (aEvent != "timedout" &&
(aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
return;
}
CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window);
this.window.clearTimeout(catchAllTimeout);
// We request an animation frame to do the final stage of the transition
// to improve perceived performance. (bug 962677)
this.window.requestAnimationFrame(() => {
deck.removeEventListener("transitionend", customizeTransitionEnd);
deferred.resolve();
});
};
deck.addEventListener("transitionend", customizeTransitionEnd);
if (!aEntering) {
this.document.documentElement.removeAttribute("customize-exiting");
this.document.documentElement.removeAttribute("customizing");
} else {
this.document.documentElement.setAttribute("customize-entered", true);
this.document.documentElement.removeAttribute("customize-entering");
}
CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window);
resolve();
});
};
deck.addEventListener("transitionend", customizeTransitionEnd);
let catchAll = () => customizeTransitionEnd("timedout");
let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs);
});
if (gDisableAnimation) {
this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true);
@ -602,9 +605,7 @@ CustomizeMode.prototype = {
this.document.documentElement.removeAttribute("customize-entered");
}
let catchAll = () => customizeTransitionEnd("timedout");
let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs);
return deferred.promise;
return customizeTransitionEndPromise;
},
updateLWTStyling: function(aData) {
@ -868,14 +869,12 @@ CustomizeMode.prototype = {
},
deferredWrapToolbarItem: function(aNode, aPlace) {
let deferred = Promise.defer();
dispatchFunction(function() {
let wrapper = this.wrapToolbarItem(aNode, aPlace);
deferred.resolve(wrapper);
}.bind(this));
return deferred.promise;
return new Promise(resolve => {
dispatchFunction(() => {
let wrapper = this.wrapToolbarItem(aNode, aPlace);
resolve(wrapper);
});
});
},
wrapToolbarItem: function(aNode, aPlace) {
@ -985,17 +984,17 @@ CustomizeMode.prototype = {
},
deferredUnwrapToolbarItem: function(aWrapper) {
let deferred = Promise.defer();
dispatchFunction(function() {
let item = null;
try {
item = this.unwrapToolbarItem(aWrapper);
} catch (ex) {
Cu.reportError(ex);
}
deferred.resolve(item);
}.bind(this));
return deferred.promise;
return new Promise(resolve => {
dispatchFunction(() => {
let item = null;
try {
item = this.unwrapToolbarItem(aWrapper);
} catch (ex) {
Cu.reportError(ex);
}
resolve(item);
});
});
},
unwrapToolbarItem: function(aWrapper) {

View File

@ -2223,7 +2223,7 @@ BrowserGlue.prototype = {
// be set to the version it has been added in. We will compare its value
// to users' smartBookmarksVersion and add new smart bookmarks without
// recreating old deleted ones.
const SMART_BOOKMARKS_VERSION = 7;
const SMART_BOOKMARKS_VERSION = 8;
const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion";
@ -2256,18 +2256,6 @@ BrowserGlue.prototype = {
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
newInVersion: 1
},
RecentlyBookmarked: {
title: bundle.GetStringFromName("recentlyBookmarkedTitle"),
url: "place:folder=BOOKMARKS_MENU" +
"&folder=UNFILED_BOOKMARKS" +
"&folder=TOOLBAR" +
"&queryType=" + queryOptions.QUERY_TYPE_BOOKMARKS +
"&sort=" + queryOptions.SORT_BY_DATEADDED_DESCENDING +
"&maxResults=" + MAX_RESULTS +
"&excludeQueries=1",
parentGuid: PlacesUtils.bookmarks.menuGuid,
newInVersion: 1
},
RecentTags: {
title: bundle.GetStringFromName("recentTagsTitle"),
url: "place:type=" + queryOptions.RESULTS_AS_TAG_QUERY +

View File

@ -38,9 +38,9 @@ updateAppInfo({
});
// Smart bookmarks constants.
const SMART_BOOKMARKS_VERSION = 7;
const SMART_BOOKMARKS_VERSION = 8;
const SMART_BOOKMARKS_ON_TOOLBAR = 1;
const SMART_BOOKMARKS_ON_MENU = 3; // Takes into account the additional separator.
const SMART_BOOKMARKS_ON_MENU = 2; // Takes into account the additional separator.
// Default bookmarks constants.
const DEFAULT_BOOKMARKS_ON_TOOLBAR = 1;

View File

@ -142,13 +142,6 @@ add_task(function* test_version_change_pos() {
yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
let firstItemTitle = bm.title;
bm = yield PlacesUtils.bookmarks.fetch({
parentGuid: PlacesUtils.bookmarks.menuGuid,
index: 1
});
yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
let secondItemTitle = bm.title;
// Set preferences.
Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
@ -168,13 +161,6 @@ add_task(function* test_version_change_pos() {
yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
Assert.equal(bm.title, firstItemTitle);
bm = yield PlacesUtils.bookmarks.fetch({
parentGuid: PlacesUtils.bookmarks.menuGuid,
index: 1
});
yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
Assert.equal(bm.title, secondItemTitle);
// Check version has been updated.
Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
SMART_BOOKMARKS_VERSION);
@ -196,13 +182,6 @@ add_task(function* test_version_change_pos_moved() {
yield checkItemHasAnnotation(bm1.guid, SMART_BOOKMARKS_ANNO);
let firstItemTitle = bm1.title;
let bm2 = yield PlacesUtils.bookmarks.fetch({
parentGuid: PlacesUtils.bookmarks.menuGuid,
index: 1
});
yield checkItemHasAnnotation(bm2.guid, SMART_BOOKMARKS_ANNO);
let secondItemTitle = bm2.title;
// Move the first smart bookmark to the end of the menu.
yield PlacesUtils.bookmarks.update({
parentGuid: PlacesUtils.bookmarks.menuGuid,
@ -227,14 +206,6 @@ add_task(function* test_version_change_pos_moved() {
Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
// Check smart bookmarks are still in correct position.
bm2 = yield PlacesUtils.bookmarks.fetch({
parentGuid: PlacesUtils.bookmarks.menuGuid,
index: 0
});
yield checkItemHasAnnotation(bm2.guid, SMART_BOOKMARKS_ANNO);
Assert.equal(bm2.title, secondItemTitle);
bm1 = yield PlacesUtils.bookmarks.fetch({
parentGuid: PlacesUtils.bookmarks.menuGuid,
index: PlacesUtils.bookmarks.DEFAULT_INDEX

View File

@ -10,6 +10,9 @@ MOZ_AUTOMATION_L10N_CHECK=0
# Needed to enable breakpad in application.ini
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
#Use ccache
# Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).

View File

@ -13,6 +13,9 @@ ac_add_options --enable-valgrind
export PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:/usr/share/pkgconfig
. $topsrcdir/build/unix/mozconfig.gtk
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1

View File

@ -9,6 +9,9 @@ ac_add_options --with-branding=browser/branding/nightly
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
ac_add_options --disable-stdcxx-compat
. "$topsrcdir/build/mozconfig.common.override"

View File

@ -15,6 +15,9 @@ export PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:/usr/share/pkgconfig
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
# Need this to prevent name conflicts with the normal nightly build packages
export MOZ_PKG_SPECIAL=asan

View File

@ -10,6 +10,9 @@ MOZ_AUTOMATION_L10N_CHECK=0
# Needed to enable breakpad in application.ini
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
# Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
ac_add_options --enable-warnings-as-errors

View File

@ -13,6 +13,9 @@ ac_add_options --enable-valgrind
export PKG_CONFIG_LIBDIR=/usr/lib64/pkgconfig:/usr/share/pkgconfig
. $topsrcdir/build/unix/mozconfig.gtk
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1

View File

@ -9,6 +9,9 @@ ac_add_options --with-branding=browser/branding/nightly
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
ac_add_options --disable-stdcxx-compat
. "$topsrcdir/build/mozconfig.common.override"

View File

@ -12,5 +12,8 @@ fi
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
. "$topsrcdir/build/mozconfig.common.override"
. "$topsrcdir/build/mozconfig.cache"

View File

@ -8,6 +8,9 @@ ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
# Needed to enable breakpad in application.ini
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
ac_add_options --with-macbundlename-prefix=Firefox
fi

View File

@ -5,6 +5,9 @@ ac_add_options --enable-debug
ac_add_options --enable-optimize="-O1"
ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1

View File

@ -6,6 +6,9 @@ ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
# Needed to enable breakpad in application.ini
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
ac_add_options --with-macbundlename-prefix=Firefox
fi

View File

@ -18,6 +18,9 @@ ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
# Needed to enable breakpad in application.ini
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
. $topsrcdir/build/win32/mozconfig.vs2015-win64
# Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).

View File

@ -8,6 +8,9 @@ ac_add_options --with-branding=browser/branding/nightly
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
if test "$PROCESSOR_ARCHITECTURE" = "AMD64" -o "$PROCESSOR_ARCHITEW6432" = "AMD64"; then
. $topsrcdir/build/win32/mozconfig.vs2015-win64
else

View File

@ -20,6 +20,9 @@ ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
# Needed to enable breakpad in application.ini
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
# Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
ac_add_options --enable-warnings-as-errors

View File

@ -9,6 +9,9 @@ ac_add_options --with-branding=browser/branding/nightly
export MOZILLA_OFFICIAL=1
# Enable Telemetry
export MOZ_TELEMETRY_REPORTING=1
. $topsrcdir/build/win64/mozconfig.vs2015
. "$topsrcdir/build/mozconfig.common.override"

View File

@ -65,7 +65,6 @@ detailsPane.noItems=No items
detailsPane.itemsCountLabel=One item;#1 items
mostVisitedTitle=Most Visited
recentlyBookmarkedTitle=Recently Bookmarked
recentTagsTitle=Recent Tags
OrganizerQueryHistory=History

View File

@ -107,15 +107,11 @@
}
#nav-bar {
box-shadow: 0 1px 0 @toolbarHighlight@ inset;
box-shadow: 0 1px 0 @navbarInsetHighlight@ inset;
padding-top: 2px;
padding-bottom: 2px;
}
#nav-bar:-moz-lwtheme {
box-shadow: 0 1px 0 @toolbarHighlightLWT@ inset;
}
#nav-bar-overflow-button {
-moz-image-region: rect(-5px, 12px, 11px, -4px);
}

View File

@ -6,6 +6,8 @@
%define toolbarHighlight hsla(0,0%,100%,.05)
%define toolbarHighlightLWT rgba(255,255,255,.4)
/* navbarInsetHighlight is tightly coupled to the toolbarHighlight constant. */
%define navbarInsetHighlight hsla(0,0%,100%,.4)
%define fgTabTexture linear-gradient(transparent 2px, @toolbarHighlight@ 2px, @toolbarHighlight@)
%define fgTabTextureLWT linear-gradient(transparent 2px, @toolbarHighlightLWT@ 2px, @toolbarHighlightLWT@)
%define fgTabBackgroundColor -moz-dialog

View File

@ -4,10 +4,6 @@
%include ../../shared/customizableui/panelUIOverlay.inc.css
:root {
--panel-separator-color: hsla(210,4%,10%,.15);
}
.panel-subviews {
background-color: hsla(0,0%,100%,.97);
}
@ -77,4 +73,4 @@ menu.subviewbutton > .menu-right > image {
toolbarpaletteitem[place="palette"] > .toolbarbutton-1 > .toolbarbutton-menubutton-button {
padding: 3px 1px;
}
}

View File

@ -21,7 +21,6 @@
%include ../browser.inc
:root {
--panel-separator-color: ThreeDShadow;
--panel-ui-exit-subview-gutter-width: 38px;
}

View File

@ -33,7 +33,7 @@
// Rules from the mozilla plugin
"mozilla/mark-test-function-used": 1,
"mozilla/no-aArgs": 1,
"mozilla/no-cpows-in-tests": 1,
"mozilla/no-cpows-in-tests": 2,
// See bug 1224289.
"mozilla/reject-importGlobalProperties": 1,
"mozilla/var-only-at-top-level": 1,

62
devtools/bootstrap.js vendored
View File

@ -74,19 +74,54 @@ function reload(event) {
// We automatically reload the toolbox if we are on a browser tab
// with a toolbox already opened
let top = getTopLevelWindow(event.view)
let isBrowser = top.location.href.includes("/browser.xul") && top.gDevToolsBrowser;
let isBrowser = top.location.href.includes("/browser.xul");
let reloadToolbox = false;
if (isBrowser && top.gDevToolsBrowser.hasToolboxOpened) {
reloadToolbox = top.gDevToolsBrowser.hasToolboxOpened(top);
if (isBrowser && top.gBrowser) {
// We do not use any devtools code before the call to Loader.jsm reload as
// any attempt to use Loader.jsm to load a module will instanciate a new
// Loader.
let nbox = top.gBrowser.getNotificationBox();
reloadToolbox =
top.document.getAnonymousElementByAttribute(nbox, "class",
"devtools-toolbox-bottom-iframe") ||
top.document.getAnonymousElementByAttribute(nbox, "class",
"devtools-toolbox-side-iframe") ||
Services.wm.getMostRecentWindow("devtools:toolbox");
}
let browserConsole = Services.wm.getMostRecentWindow("devtools:webconsole");
let reopenBrowserConsole = false;
if (browserConsole) {
browserConsole.close();
reopenBrowserConsole = true;
}
dump("Reload DevTools. (reload-toolbox:"+reloadToolbox+")\n");
// Invalidate xul cache in order to see changes made to chrome:// files
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
// Ask the loader to update itself and reopen the toolbox if needed
// This frame script is going to be executed in all processes: parent and childs
Services.ppmm.loadProcessScript("data:,new " + function () {
/* Flush message manager cached frame scripts as well as chrome locales */
let obs = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
obs.notifyObservers(null, "message-manager-flush-caches", null);
/* Also purge cached modules in child processes, we do it a few lines after
in the parent process */
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
Services.obs.notifyObservers(null, "devtools-unload", "reload");
}
}, false);
// As we can't get a reference to existing Loader.jsm instances, we send them
// an observer service notification to unload them.
Services.obs.notifyObservers(null, "devtools-unload", "reload");
// Then spawn a brand new Loader.jsm instance and start the main module
Cu.unload("resource://devtools/shared/Loader.jsm");
const {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
devtools.reload();
devtools.require("devtools/client/framework/devtools-browser");
// Go over all top level windows to reload all devtools related things
let windowsEnum = Services.wm.getEnumerator(null);
@ -114,15 +149,6 @@ function reload(event) {
}
} else if (windowtype === "devtools:webide") {
window.location.reload();
} else if (windowtype === "devtools:webconsole") {
// Browser console document can't just be reloaded.
// HUDService is going to close it on unload.
// Instead we have to manually toggle it.
let HUDService = devtools.require("devtools/client/webconsole/hudservice");
HUDService.toggleBrowserConsole()
.then(() => {
HUDService.toggleBrowserConsole();
});
}
}
@ -140,6 +166,14 @@ function reload(event) {
}, 1000);
}
// Browser console document can't just be reloaded.
// HUDService is going to close it on unload.
// Instead we have to manually toggle it.
if (reopenBrowserConsole) {
let HUDService = devtools.require("devtools/client/webconsole/hudservice");
HUDService.toggleBrowserConsole();
}
actionOccurred("reloadAddonReload");
}

View File

@ -10,7 +10,7 @@
loader.lazyImporter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
const { Cc, Ci } = require("chrome");
const { Cc, Ci, Cu } = require("chrome");
const { createFactory, createClass, DOM: dom } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
@ -57,6 +57,7 @@ module.exports = createClass({
AddonManager.installTemporaryAddon(file)
.catch(e => {
Cu.reportError(e);
this.setState({ installError: e.message });
});
},

View File

@ -17,8 +17,7 @@ var gNewChromeSource = promise.defer()
var { DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
var loader = new DevToolsLoader();
loader.invisibleToDebugger = true;
loader.main("devtools/server/main");
var DebuggerServer = loader.DebuggerServer;
var { DebuggerServer } = loader.require("devtools/server/main");
function test() {
if (!DebuggerServer.initialized) {

View File

@ -70,7 +70,7 @@ DevToolsStartup.prototype = {
let { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
// Ensure loading main devtools module that hooks up into browser UI
// and initialize all devtools machinery.
loader.main("devtools/client/main");
loader.require("devtools/client/framework/devtools-browser");
},
handleConsoleFlag: function(cmdLine) {
@ -154,8 +154,8 @@ DevToolsStartup.prototype = {
// settings).
let serverLoader = new DevToolsLoader();
serverLoader.invisibleToDebugger = true;
serverLoader.main("devtools/server/main");
let debuggerServer = serverLoader.DebuggerServer;
let { DebuggerServer: debuggerServer } =
serverLoader.require("devtools/server/main");
debuggerServer.init();
debuggerServer.addBrowserActors();
debuggerServer.allowChromeProcess = true;

View File

@ -127,8 +127,8 @@ BrowserToolboxProcess.prototype = {
// invisible to the debugger (unlike the usual loader settings).
this.loader = new DevToolsLoader();
this.loader.invisibleToDebugger = true;
this.loader.main("devtools/server/main");
this.debuggerServer = this.loader.DebuggerServer;
let { DebuggerServer } = this.loader.require("devtools/server/main");
this.debuggerServer = DebuggerServer;
dumpn("Created a separate loader instance for the DebuggerServer.");
// Forward interesting events.

View File

@ -10,7 +10,7 @@ var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
// Require this module to setup core modules
loader.main("devtools/client/main");
loader.require("devtools/client/framework/devtools-browser");
var { gDevTools } = require("devtools/client/framework/devtools");
var { TargetFactory } = require("devtools/client/framework/target");

View File

@ -3,6 +3,7 @@ tags = devtools
subsuite = devtools
support-files =
doc_author-sheet.html
doc_blob_stylesheet.html
doc_content_stylesheet.html
doc_content_stylesheet_imported.css
doc_content_stylesheet_imported2.css
@ -55,6 +56,7 @@ support-files =
[browser_rules_authored.js]
[browser_rules_authored_color.js]
[browser_rules_authored_override.js]
[browser_rules_blob_stylesheet.js]
[browser_rules_colorpicker-and-image-tooltip_01.js]
[browser_rules_colorpicker-and-image-tooltip_02.js]
[browser_rules_colorpicker-appears-on-swatch-click.js]

View File

@ -0,0 +1,20 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the rule-view content is correct for stylesheet generated
// with createObjectURL(cssBlob)
const TEST_URL = URL_ROOT + "doc_blob_stylesheet.html";
add_task(function* () {
yield addTab(TEST_URL);
let {inspector, view} = yield openRuleView();
yield selectNode("h1", inspector);
is(view.element.querySelectorAll("#noResults").length, 0,
"The no-results element is not displayed");
is(view.element.querySelectorAll(".ruleview-rule").length, 2,
"There are 2 displayed rules");
});

View File

@ -0,0 +1,39 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
</html>
<html>
<head>
<meta charset="utf-8">
<title>Blob stylesheet sourcemap</title>
</head>
<body>
<h1>Test</h1>
<script>
"use strict";
var cssContent = `body {
background-color: black;
}
body > h1 {
color: white;
}
` +
"/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtYX" +
"BwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLOztBQUN2QixTQUFPO0VBQ0" +
"wsS0FBSyxFQUFFLEtBQUsiLAoic291cmNlcyI6IFsidGVzdC5zY3NzIl0sCiJzb3VyY2VzQ29udG" +
"VudCI6IFsiYm9keSB7XG4gIGJhY2tncm91bmQtY29sb3I6IGJsYWNrO1xuICAmID4gaDEge1xuIC" +
"AgIGNvbG9yOiB3aGl0ZTsgIFxuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc3" +
"QuY3NzIgp9Cg== */";
var cssBlob = new Blob([cssContent], {type: "text/css"});
var url = URL.createObjectURL(cssBlob);
var head = document.querySelector("head");
var link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = url;
head.appendChild(link);
</script>
</body>
</html>

View File

@ -65,13 +65,16 @@ toolbar.displayBy=Group by:
# describing the select menu options of the display options.
toolbar.displayBy.tooltip=Change how objects are grouped
# TODO FITZGEN
# LOCALIZATION NOTE (toolbar.pop-view): The text in the button to go back to the
# previous view.
toolbar.pop-view=
# TODO FITZGEN
# LOCALIZATION NOTE (toolbar.pop-view.label): The text for the label for the
# button to go back to the previous view.
toolbar.pop-view.label=Go back to aggregates
# TODO FITZGEN
# LOCALIZATION NOTE (toolbar.viewing-individuals): The text letting the user
# know that they are viewing individual nodes from a census group.
toolbar.viewing-individuals=⁂ Viewing individuals in group
# LOCALIZATION NOTE (censusDisplays.coarseType.tooltip): The tooltip for the
@ -178,7 +181,8 @@ filter.placeholder=Filter
# tool's filter search box.
filter.tooltip=Filter the contents of the heap snapshot
# TODO FITZGEN
# LOCALIZATION NOTE (tree-item.view-individuals.tooltip): The tooltip for the
# button to view individuals in this group.
tree-item.view-individuals.tooltip=View individual nodes in this group and their retaining paths
# LOCALIZATION NOTE (tree-item.load-more): The label for the links to fetch the
@ -307,22 +311,28 @@ snapshot.state.saving-tree-map.full=Saving tree map…
# snapshot state ERROR, used in the main heap view.
snapshot.state.error.full=There was an error processing this snapshot.
# TODO FITZGEN
# LOCALIZATION NOTE (individuals.state.error): The short message displayed when
# there is an error fetching individuals from a group.
individuals.state.error=Error
# TODO FITZGEN
# LOCALIZATION NOTE (individuals.state.error.full): The longer message displayed
# when there is an error fetching individuals from a group.
individuals.state.error.full=There was an error while fetching individuals in the group
# TODO FITZGEN
# LOCALIZATION NOTE (individuals.state.fetching): The short message displayed
# while fetching individuals.
individuals.state.fetching=Fetching…
# TODO FITZGEN
# LOCALIZATION NOTE (individuals.state.fetching.full): The longer message
# displayed while fetching individuals.
individuals.state.fetching.full=Fetching individuals in group…
# TODO FITZGEN
# LOCALIZATION NOTE (individuals.field.node): The header label for an individual
# node.
individuals.field.node=Node
# TODO FITZGEN
# LOCALIZATION NOTE (individuals.field.node.tooltip): The tooltip for the header
# label for an individual node.
individuals.field.node.tooltip=The individual node in the snapshot
# LOCALIZATION NOTE (snapshot.state.saving): The label describing the snapshot

View File

@ -1,22 +0,0 @@
/* 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 module could have been devtools-browser.js.
* But we need this wrapper in order to define precisely what we are exporting
* out of client module loader (Loader.jsm): only Toolbox and TargetFactory.
*/
// For compatiblity reasons, exposes these symbols on "devtools":
Object.defineProperty(exports, "Toolbox", {
get: () => require("devtools/client/framework/toolbox").Toolbox
});
Object.defineProperty(exports, "TargetFactory", {
get: () => require("devtools/client/framework/target").TargetFactory
});
// Load the main browser module
require("devtools/client/framework/devtools-browser");

View File

@ -9,7 +9,6 @@ const { refresh } = require("./refresh");
exports.setCensusDisplayAndRefresh = function(heapWorker, display) {
return function*(dispatch, getState) {
console.log("FITZGEN: setCensusDisplayAndRefresh", display);
dispatch(setCensusDisplay(display));
yield dispatch(refresh(heapWorker));
};

View File

@ -14,7 +14,6 @@ const snapshot = require("./snapshot");
* @param {HeapAnalysesWorker} heapWorker
*/
exports.refresh = function (heapWorker) {
console.log("FITZGEN: refresh");
return function* (dispatch, getState) {
switch (getState().view.state) {
case viewState.DIFFING:

View File

@ -140,8 +140,6 @@ TaskCache.declareCacheableTask({
},
task: function*(heapWorker, id, removeFromCache, dispatch, getState) {
console.log("FITZGEN: readSnapshot");
const snapshot = getSnapshot(getState(), id);
assert([states.SAVED, states.IMPORTING].includes(snapshot.state),
`Should only read a snapshot once. Found snapshot in state ${snapshot.state}`);
@ -153,14 +151,12 @@ TaskCache.declareCacheableTask({
yield heapWorker.readHeapSnapshot(snapshot.path);
creationTime = yield heapWorker.getCreationTime(snapshot.path);
} catch (error) {
console.log("FITZGEN: readSnapshot: error", error);
removeFromCache();
reportException("readSnapshot", error);
dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
return;
}
console.log("FITZGEN: readSnapshot: done reading");
removeFromCache();
dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime });
}
@ -196,10 +192,8 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
},
task: function*(heapWorker, id, removeFromCache, dispatch, getState) {
console.log("FITZGEN: takeCensus");
const snapshot = getSnapshot(getState(), id);
if (!snapshot) {
console.log("FITZGEN: no snapshot");
removeFromCache();
return;
}
@ -214,7 +208,6 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
// If display, filter and inversion haven't changed, don't do anything.
if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
console.log("FITZGEN: census is up to date");
removeFromCache();
return;
}
@ -226,7 +219,6 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
display = getDisplay(getState());
filter = getState().filter;
console.log("FITZGEN: taking census with display =", display.displayName);
dispatch({
type: beginAction,
id,
@ -246,7 +238,6 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
{ breakdown: display.breakdown },
opts));
} catch (error) {
console.log("FITZGEN: error taking census: " + error + "\n" + error.stack);
removeFromCache();
reportException("takeCensus", error);
dispatch({ type: errorAction, id, error });
@ -256,8 +247,6 @@ function makeTakeCensusTask({ getDisplay, getFilter, getCensus, beginAction,
while (filter !== getState().filter ||
display !== getDisplay(getState()));
console.log("FITZGEN: done taking census");
removeFromCache();
dispatch({
type: endAction,
@ -465,11 +454,9 @@ const refreshIndividuals = exports.refreshIndividuals = function(heapWorker) {
* @param {HeapAnalysesClient} heapWorker
*/
const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) {
console.log("FITZGEN: refreshSelectedCensus");
return function*(dispatch, getState) {
let snapshot = getState().snapshots.find(s => s.selected);
if (!snapshot || snapshot.state !== states.READ) {
console.log("FITZGEN: nothing to do");
return;
}
@ -481,7 +468,6 @@ const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWork
// task action will follow through and ensure that a census is taken.
if ((snapshot.census && snapshot.census.state === censusState.SAVED) ||
!snapshot.census) {
console.log("FITZGEN: taking census");
yield dispatch(takeCensus(heapWorker, snapshot.id));
}
};

View File

@ -45,7 +45,6 @@ const TaskCache = module.exports = class TaskCache {
* @param {Any} key
*/
remove(key) {
console.log("FITZGEN: removing task from cache with keky =", key);
assert(this._cache.has(key),
`Should have an extant entry for key = ${key}`);
@ -71,12 +70,9 @@ TaskCache.declareCacheableTask = function({ getCacheKey, task }) {
const extantResult = cache.get(key);
if (extantResult) {
console.log("FITZGEN: re-using task with cache key =", key);
return extantResult;
}
console.log("FITZGEN: creating new task with cache key =", key);
// Ensure that we have our new entry in the cache *before* dispatching the
// task!
let resolve;

View File

@ -51,6 +51,5 @@ JAR_MANIFESTS += ['jar.mn']
DevToolsModules(
'definitions.js',
'main.js',
'menus.js',
)

View File

@ -24,7 +24,7 @@ addRDMTask(TEST_URL, function* ({ ui }) {
// Browser's location should match original tab
yield waitForFrameLoad(ui, TEST_URL);
let location = yield spawnViewportTask(ui, {}, function* () {
return content.location.href;
return content.location.href; // eslint-disable-line
});
is(location, TEST_URL, "Viewport location matches");
});

View File

@ -16,15 +16,18 @@ add_task(function*() {
let hud = yield openConsole(null);
info("console opened");
let button = content.document.querySelector("button");
ok(button, "we have the button on the page");
// On e10s, the exception is triggered in child process
// and is ignored by test harness
if (!Services.appinfo.browserTabsRemoteAutostart) {
expectUncaughtException();
}
EventUtils.sendMouseEvent({ type: "click" }, button, content);
ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
let button = content.document.querySelector("button");
ok(button, "we have the button on the page");
button.click();
});
let [result] = yield waitForMessages({
webconsole: hud,

View File

@ -1957,49 +1957,66 @@ const ThreadActor = ActorClass({
this.scripts.addScripts(this.dbg.findScripts({ source: aSource }));
let sourceActor = this.sources.createNonSourceMappedActor(aSource);
// Set any stored breakpoints.
let bpActors = [...this.breakpointActorMap.findActors()];
let promises = [];
// Go ahead and establish the source actors for this script, which
// fetches sourcemaps if available and sends onNewSource
// notifications.
let sourceActorsCreated = this.sources.createSourceActors(aSource);
if (this._options.useSourceMaps) {
let promises = [];
if (bpActors.length) {
// We need to use unsafeSynchronize here because if the page is being reloaded,
// this call will replace the previous set of source actors for this source
// with a new one. If the source actors have not been replaced by the time
// we try to reset the breakpoints below, their location objects will still
// point to the old set of source actors, which point to different
// scripts.
this.unsafeSynchronize(sourceActorsCreated);
}
// Go ahead and establish the source actors for this script, which
// fetches sourcemaps if available and sends onNewSource
// notifications.
let sourceActorsCreated = this.sources._createSourceMappedActors(aSource);
for (let _actor of bpActors) {
// XXX bug 1142115: We do async work in here, so we need to create a fresh
// binding because for/of does not yet do that in SpiderMonkey.
let actor = _actor;
if (actor.isPending) {
promises.push(actor.originalLocation.originalSourceActor._setBreakpoint(actor));
} else {
promises.push(this.sources.getAllGeneratedLocations(actor.originalLocation)
.then((generatedLocations) => {
if (generatedLocations.length > 0 &&
generatedLocations[0].generatedSourceActor.actorID === sourceActor.actorID) {
sourceActor._setBreakpointAtAllGeneratedLocations(
actor,
generatedLocations
);
}
}));
if (bpActors.length) {
// We need to use unsafeSynchronize here because if the page is being reloaded,
// this call will replace the previous set of source actors for this source
// with a new one. If the source actors have not been replaced by the time
// we try to reset the breakpoints below, their location objects will still
// point to the old set of source actors, which point to different
// scripts.
this.unsafeSynchronize(sourceActorsCreated);
}
}
if (promises.length > 0) {
this.unsafeSynchronize(promise.all(promises));
for (let _actor of bpActors) {
// XXX bug 1142115: We do async work in here, so we need to create a fresh
// binding because for/of does not yet do that in SpiderMonkey.
let actor = _actor;
if (actor.isPending) {
promises.push(actor.originalLocation.originalSourceActor._setBreakpoint(actor));
} else {
promises.push(this.sources.getAllGeneratedLocations(actor.originalLocation)
.then((generatedLocations) => {
if (generatedLocations.length > 0 &&
generatedLocations[0].generatedSourceActor.actorID === sourceActor.actorID) {
sourceActor._setBreakpointAtAllGeneratedLocations(
actor,
generatedLocations
);
}
}));
}
}
if (promises.length > 0) {
this.unsafeSynchronize(promise.all(promises));
}
} else {
// Bug 1225160: If addSource is called in response to a new script
// notification, and this notification was triggered by loading a JSM from
// chrome code, calling unsafeSynchronize could cause a debuggee timer to
// fire. If this causes the JSM to be loaded a second time, the browser
// will crash, because loading JSMS is not reentrant, and the first load
// has not completed yet.
//
// The root of the problem is that unsafeSynchronize can cause debuggee
// code to run. Unfortunately, fixing that is prohibitively difficult. The
// best we can do at the moment is disable source maps for the browser
// debugger, and carefully avoid the use of unsafeSynchronize in this
// function when source maps are disabled.
for (let actor of bpActors) {
actor.originalLocation.originalSourceActor._setBreakpoint(actor);
}
}
this._debuggerSourcesSeen.add(aSource);

View File

@ -803,6 +803,9 @@ var StyleSheetActor = protocol.ActorClass({
* Sets the source map's sourceRoot to be relative to the source map url.
*/
_setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) {
if (aScriptURL.startsWith("blob:")) {
aScriptURL = aScriptURL.replace("blob:", "");
}
const base = dirname(
aAbsSourceMapURL.startsWith("data:")
? aScriptURL

View File

@ -329,7 +329,13 @@ TabSources.prototype = {
}
} catch (e) {
// This only needs to be here because URL is not yet exposed to
// workers.
// workers. (BUG 1258892)
const filename = url;
const index = filename.lastIndexOf(".");
const extension = index >= 0 ? filename.slice(index + 1) : "";
if (extension === "js") {
spec.contentType = "text/javascript";
}
}
}
}

View File

@ -19,8 +19,7 @@ function init(msg) {
// in the same process.
let devtools = new DevToolsLoader();
devtools.invisibleToDebugger = true;
devtools.main("devtools/server/main");
let { DebuggerServer, ActorPool } = devtools;
let { DebuggerServer, ActorPool } = devtools.require("devtools/server/main");
if (!DebuggerServer.initialized) {
DebuggerServer.init();

View File

@ -1,7 +1,6 @@
/* 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 NetUtil, FileUtils, OS */
"use strict";
@ -14,10 +13,6 @@ var { Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
var { Loader } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
var promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
@ -135,11 +130,9 @@ BuiltinProvider.prototype = {
var gNextLoaderID = 0;
/**
* The main devtools API.
* In addition to a few loader-related details, this object will also include all
* exports from the main module. The standard instance of this loader is
* exported as |devtools| below, but if a fresh copy of the loader is needed,
* then a new one can also be created.
* The main devtools API. The standard instance of this loader is exported as
* |devtools| below, but if a fresh copy of the loader is needed, then a new
* one can also be created.
*/
this.DevToolsLoader = function DevToolsLoader() {
this.require = this.require.bind(this);
@ -147,7 +140,8 @@ this.DevToolsLoader = function DevToolsLoader() {
this.lazyImporter = XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils);
this.lazyServiceGetter = XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils);
this.lazyRequireGetter = this.lazyRequireGetter.bind(this);
this.main = this.main.bind(this);
Services.obs.addObserver(this, "devtools-unload", false);
};
DevToolsLoader.prototype = {
@ -218,46 +212,6 @@ DevToolsLoader.prototype = {
});
},
/**
* Add a URI to the loader.
* @param string id
* The module id that can be used within the loader to refer to this module.
* @param string uri
* The URI to load as a module.
* @returns The module's exports.
*/
loadURI: function(id, uri) {
let module = Loader.Module(id, uri);
return Loader.load(this.provider.loader, module).exports;
},
/**
* Let the loader know the ID of the main module to load.
*
* The loader doesn't need a main module, but it's nice to have. This
* will be called by the browser devtools to load the devtools/main module.
*
* When only using the server, there's no main module, and this method
* can be ignored.
*/
main: function(id) {
// Ensure the main module isn't loaded twice, because it may have observable
// side-effects.
if (this._mainid) {
return;
}
this._mainid = id;
this._main = Loader.main(this.provider.loader, id);
// Mirror the main module's exports on this object.
Object.getOwnPropertyNames(this._main).forEach(key => {
XPCOMUtils.defineLazyGetter(this, key, () => this._main[key]);
});
var events = this.require("sdk/system/events");
events.emit("devtools-loaded", {});
},
/**
* Override the provider used to load the tools.
*/
@ -286,8 +240,7 @@ DevToolsLoader.prototype = {
lazyImporter: this.lazyImporter,
lazyServiceGetter: this.lazyServiceGetter,
lazyRequireGetter: this.lazyRequireGetter,
id: this.id,
main: this.main
id: this.id
},
// Make sure `define` function exists. This allows defining some modules
// in AMD format while retaining CommonJS compatibility through this hook.
@ -323,18 +276,21 @@ DevToolsLoader.prototype = {
},
/**
* Reload the current provider.
* Handles "devtools-unload" event
*
* @param String data
* reason passed to modules when unloaded
*/
reload: function() {
var events = this.require("sdk/system/events");
events.emit("startupcache-invalidate", {});
observe: function(subject, topic, data) {
if (topic != "devtools-unload") {
return;
}
Services.obs.removeObserver(this, "devtools-unload");
this._provider.unload("reload");
delete this._provider;
let mainid = this._mainid;
delete this._mainid;
this._loadProvider();
this.main(mainid);
if (this._provider) {
this._provider.unload(data);
delete this._provider;
}
},
/**
@ -353,3 +309,11 @@ DevToolsLoader.prototype = {
this.devtools = this.loader = new DevToolsLoader();
this.require = this.devtools.require.bind(this.devtools);
// For compatibility reasons, expose these symbols on "devtools":
Object.defineProperty(this.devtools, "Toolbox", {
get: () => this.require("devtools/client/framework/toolbox").Toolbox
});
Object.defineProperty(this.devtools, "TargetFactory", {
get: () => this.require("devtools/client/framework/target").TargetFactory
});

View File

@ -25,8 +25,7 @@ XPCOMUtils.defineLazyGetter(this, "debuggerServer", () => {
// settings).
let serverLoader = new DevToolsLoader();
serverLoader.invisibleToDebugger = true;
serverLoader.main("devtools/server/main");
let debuggerServer = serverLoader.DebuggerServer;
let { DebuggerServer: debuggerServer } = serverLoader.require("devtools/server/main");
debuggerServer.init();
debuggerServer.addBrowserActors();
debuggerServer.allowChromeProcess = !l10n.hiddenByChromePref();

View File

@ -1073,7 +1073,7 @@ this.PushService = {
Services.telemetry.getHistogramById("PUSH_API_SUBSCRIBE_FAILED").add()
if (!reply.error) {
console.warn("onRegisterError: Called without valid error message!",
reply, String(reply));
reply);
throw new Error("Registration error");
}
throw reply.error;

View File

@ -945,7 +945,7 @@ nsCSPContext::SendReports(nsISupports* aBlockedContentSource,
continue;
}
rv = uploadChannel->SetUploadStream(sis, NS_LITERAL_CSTRING("application/json"), -1);
rv = uploadChannel->SetUploadStream(sis, NS_LITERAL_CSTRING("application/csp-report"), -1);
NS_ENSURE_SUCCESS(rv, rv);
// if this is an HTTP channel, set the request method to post

View File

@ -33,6 +33,14 @@ function makeReportHandler(testpath, message, expectedJSON) {
return;
}
// check content-type of report is "application/csp-report"
var contentType = request.hasHeader("Content-Type")
? request.getHeader("Content-Type") : undefined;
if (contentType !== "application/csp-report") {
do_throw("violation report should have the 'application/csp-report' " +
"content-type, when in fact it is " + contentType.toString())
}
// obtain violation report
var reportObj = JSON.parse(
NetUtil.readInputStreamToString(

View File

@ -81,6 +81,8 @@ AccessibleCaretManager::sCaretsScriptUpdates = false;
AccessibleCaretManager::sCaretsAllowDraggingAcrossOtherCaret = true;
/*static*/ bool
AccessibleCaretManager::sHapticFeedback = false;
/*static*/ bool
AccessibleCaretManager::sExtendSelectionForPhoneNumber = false;
AccessibleCaretManager::AccessibleCaretManager(nsIPresShell* aPresShell)
: mPresShell(aPresShell)
@ -110,6 +112,8 @@ AccessibleCaretManager::AccessibleCaretManager(nsIPresShell* aPresShell)
"layout.accessiblecaret.allow_dragging_across_other_caret", true);
Preferences::AddBoolVarCache(&sHapticFeedback,
"layout.accessiblecaret.hapticfeedback");
Preferences::AddBoolVarCache(&sExtendSelectionForPhoneNumber,
"layout.accessiblecaret.extend_selection_for_phone_number");
addedPrefs = true;
}
}
@ -820,6 +824,11 @@ AccessibleCaretManager::SelectWord(nsIFrame* aFrame, const nsPoint& aPoint) cons
SetSelectionDragState(false);
ClearMaintainedSelection();
// Smart-select phone numbers if possible.
if (sExtendSelectionForPhoneNumber) {
SelectMoreIfPhoneNumber();
}
return rs;
}
@ -841,6 +850,53 @@ AccessibleCaretManager::SetSelectionDragState(bool aState) const
#endif
}
void
AccessibleCaretManager::SelectMoreIfPhoneNumber() const
{
SetSelectionDirection(eDirNext);
ExtendPhoneNumberSelection(NS_LITERAL_STRING("forward"));
SetSelectionDirection(eDirPrevious);
ExtendPhoneNumberSelection(NS_LITERAL_STRING("backward"));
}
void
AccessibleCaretManager::ExtendPhoneNumberSelection(const nsAString& aDirection) const
{
nsIDocument* doc = mPresShell->GetDocument();
// Extend the phone number selection until we find a boundary.
Selection* selection = GetSelection();
while (selection) {
// Save current Focus position, and extend the selection one char.
nsINode* focusNode = selection->GetFocusNode();
uint32_t focusOffset = selection->FocusOffset();
selection->Modify(NS_LITERAL_STRING("extend"),
aDirection,
NS_LITERAL_STRING("character"));
// If the selection didn't change, (can't extend further), we're done.
if (selection->GetFocusNode() == focusNode &&
selection->FocusOffset() == focusOffset) {
return;
}
// If the changed selection isn't a valid phone number, we're done.
nsAutoString selectedText;
selection->Stringify(selectedText);
nsAutoString phoneRegex(NS_LITERAL_STRING("(^\\+)?[0-9\\s,\\-.()*#pw]{1,30}$"));
if (!nsContentUtils::IsPatternMatching(selectedText, phoneRegex, doc)) {
// Backout the undesired selection extend, (collapse to original
// Anchor, extend to original Focus), before exit.
selection->Collapse(selection->GetAnchorNode(), selection->AnchorOffset());
selection->Extend(focusNode, focusOffset);
return;
}
}
}
void
AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const
{

View File

@ -155,6 +155,12 @@ protected:
nsresult SelectWord(nsIFrame* aFrame, const nsPoint& aPoint) const;
void SetSelectionDragState(bool aState) const;
// Called to extend a selection if possible that it's a phone number.
void SelectMoreIfPhoneNumber() const;
// Extend the current phone number selection in the requested direction.
void ExtendPhoneNumberSelection(const nsAString& aDirection) const;
void SetSelectionDirection(nsDirection aDir) const;
// If aDirection is eDirNext, get the frame for the range start in the first
@ -282,6 +288,10 @@ protected:
// selection bar is always disabled in cursor mode.
static bool sSelectionBarEnabled;
// Preference to allow smarter selection of phone numbers,
// when user long presses text to start.
static bool sExtendSelectionForPhoneNumber;
// Preference to show caret in cursor mode when long tapping on an empty
// content. This also changes the default update behavior in cursor mode,
// which is based on the emptiness of the content, into something more

View File

@ -936,6 +936,10 @@ pref("layout.accessiblecaret.allow_script_change_updates", true);
// Optionally provide haptic feedback on longPress selection events.
pref("layout.accessiblecaret.hapticfeedback", true);
// Initial text selection on long-press is enhanced to provide
// a smarter phone-number selection for direct-dial ActionBar action.
pref("layout.accessiblecaret.extend_selection_for_phone_number", true);
// Disable sending console to logcat on release builds.
#ifdef RELEASE_BUILD
pref("consoleservice.logcat", false);

View File

@ -357,6 +357,12 @@
android:name="org.mozilla.gecko.feeds.FeedService">
</service>
<!-- DON'T EXPORT THIS, please! An attacker could delete arbitrary files. -->
<service
android:exported="false"
android:name="org.mozilla.gecko.cleanup.FileCleanupService">
</service>
<receiver
android:name="org.mozilla.gecko.feeds.FeedAlarmReceiver"
android:exported="false" />

View File

@ -20,6 +20,7 @@ import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.Tabs.TabEvents;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.cleanup.FileCleanupController;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.SuggestedSites;
@ -1062,6 +1063,11 @@ public class BrowserApp extends GeckoApp
// have been shown.
GuestSession.hideNotification(BrowserApp.this);
}
// It'd be better to launch this once, in onCreate, but there's ambiguity for when the
// profile is created so we run here instead. Don't worry, call start short-circuits pretty fast.
final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(BrowserApp.this, profile.getName());
FileCleanupController.startIfReady(BrowserApp.this, sharedPrefs, profile.getDir().getAbsolutePath());
}
});

View File

@ -557,13 +557,21 @@ public final class GeckoProfile {
return CUSTOM_PROFILE.equals(mName);
}
/**
* Retrieves the directory backing the profile. This method acts
* as a lazy initializer for the GeckoProfile instance.
*/
@RobocopTarget
public synchronized File getDir() {
forceCreate();
return mProfileDir;
}
public synchronized GeckoProfile forceCreate() {
/**
* Forces profile creation. Consider using {@link #getDir()} to initialize the profile instead - it is the
* lazy initializer and, for our code reasoning abilities, we should initialize the profile in one place.
*/
private synchronized GeckoProfile forceCreate() {
if (mProfileDir != null) {
return this;
}

View File

@ -377,7 +377,7 @@ public class GeckoThread extends Thread {
} else {
// Make sure a profile exists.
final GeckoProfile profile = getProfile();
profile.forceCreate();
profile.getDir(); // call the lazy initializer
// If args don't include the profile, make sure it's included.
if (args == null || !args.matches(".*\\B-(P|profile)\\s+\\S+.*")) {

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/.
*/
package org.mozilla.gecko.cleanup;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.support.annotation.VisibleForTesting;
import java.io.File;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* Encapsulates the code to run the {@link FileCleanupService}. Call
* {@link #startIfReady(Context, SharedPreferences, String)} to start the clean-up.
*
* Note: for simplicity, the current implementation does not cache which
* files have been cleaned up and will attempt to delete the same files
* each time it is run. If the file deletion list grows large, consider
* keeping a cache.
*/
public class FileCleanupController {
private static final long MILLIS_BETWEEN_CLEANUPS = TimeUnit.DAYS.toMillis(7);
@VisibleForTesting static final String PREF_LAST_CLEANUP_MILLIS = "cleanup.lastFileCleanupMillis";
// These will be prepended with the path of the profile we're cleaning up.
private static final String[] PROFILE_FILES_TO_CLEANUP = new String[] {
"health.db",
"health.db-journal",
"health.db-shm",
"health.db-wal",
};
/**
* Starts the clean-up if it's time to clean-up, otherwise returns. For simplicity,
* it does not schedule the cleanup for some point in the future - this method will
* have to be called again (i.e. polled) in order to run the clean-up service.
*
* @param context Context of the calling {@link android.app.Activity}
* @param sharedPrefs The {@link SharedPreferences} instance to store the controller state to
* @param profilePath The path to the profile the service should clean-up files from
*/
public static void startIfReady(final Context context, final SharedPreferences sharedPrefs, final String profilePath) {
if (!isCleanupReady(sharedPrefs)) {
return;
}
recordCleanupScheduled(sharedPrefs);
final Intent fileCleanupIntent = new Intent(context, FileCleanupService.class);
fileCleanupIntent.setAction(FileCleanupService.ACTION_DELETE_FILES);
fileCleanupIntent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, getFilesToCleanup(profilePath + "/"));
context.startService(fileCleanupIntent);
}
private static boolean isCleanupReady(final SharedPreferences sharedPrefs) {
final long lastCleanupMillis = sharedPrefs.getLong(PREF_LAST_CLEANUP_MILLIS, -1);
return lastCleanupMillis + MILLIS_BETWEEN_CLEANUPS < System.currentTimeMillis();
}
private static void recordCleanupScheduled(final SharedPreferences sharedPrefs) {
final SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putLong(PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis()).apply();
}
@VisibleForTesting
static ArrayList<String> getFilesToCleanup(final String profilePath) {
final ArrayList<String> out = new ArrayList<>(PROFILE_FILES_TO_CLEANUP.length);
for (final String path : PROFILE_FILES_TO_CLEANUP) {
// Append a file separator, just in-case the caller didn't include one.
out.add(profilePath + File.separator + path);
}
return out;
}
}

View File

@ -0,0 +1,80 @@
/*
* 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/.
*/
package org.mozilla.gecko.cleanup;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
/**
* An IntentService to delete files.
*
* It takes an {@link ArrayList} of String file paths to delete via the extra
* {@link #EXTRA_FILE_PATHS_TO_DELETE}. If these file paths are directories, they will
* not be traversed recursively and will only be deleted if empty. This is to avoid accidentally
* trashing a users' profile if a folder is accidentally listed.
*
* An IntentService was chosen because:
* * It generally won't be killed when the Activity is
* * (unlike HandlerThread) The system handles scheduling, prioritizing,
* and shutting down the underlying background thread
* * (unlike an existing background thread) We don't block our background operations
* for this, which doesn't directly affect the user.
*
* The major trade-off is that this Service is very dangerous if it's exported... so don't do that!
*/
public class FileCleanupService extends IntentService {
private static final String LOGTAG = "Gecko" + FileCleanupService.class.getSimpleName();
private static final String WORKER_THREAD_NAME = LOGTAG + "Worker";
public static final String ACTION_DELETE_FILES = "org.mozilla.gecko.intent.action.DELETE_FILES";
public static final String EXTRA_FILE_PATHS_TO_DELETE = "org.mozilla.gecko.file_paths_to_delete";
public FileCleanupService() {
super(WORKER_THREAD_NAME);
// We're likely to get scheduled again - let's wait until then in order to avoid:
// * The coding complexity of re-running this
// * Consuming system resources: we were probably killed for resource conservation purposes
setIntentRedelivery(false);
}
@Override
protected void onHandleIntent(final Intent intent) {
if (!isIntentValid(intent)) {
return;
}
final ArrayList<String> filesToDelete = intent.getStringArrayListExtra(EXTRA_FILE_PATHS_TO_DELETE);
for (final String path : filesToDelete) {
final File file = new File(path);
file.delete();
}
}
private static boolean isIntentValid(final Intent intent) {
if (intent == null) {
Log.w(LOGTAG, "Received null intent");
return false;
}
if (!intent.getAction().equals(ACTION_DELETE_FILES)) {
Log.w(LOGTAG, "Received unknown intent action: " + intent.getAction());
return false;
}
if (!intent.hasExtra(EXTRA_FILE_PATHS_TO_DELETE)) {
Log.w(LOGTAG, "Received intent with no files extra");
return false;
}
return true;
}
}

View File

@ -24,25 +24,4 @@ public class TelemetryConstants {
public static final String PREF_SERVER_URL = "telemetry-serverUrl";
public static final String PREF_SEQ_COUNT = "telemetry-seqCount";
public static class CorePing {
private CorePing() { /* To prevent instantiation */ }
public static final String NAME = "core";
public static final int VERSION_VALUE = 4; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
public static final String OS_VALUE = "Android";
public static final String ARCHITECTURE = "arch";
public static final String CLIENT_ID = "clientId";
public static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
public static final String DEVICE = "device";
public static final String DISTRIBUTION_ID = "distributionId";
public static final String EXPERIMENTS = "experiments";
public static final String LOCALE = "locale";
public static final String OS_ATTR = "os";
public static final String OS_VERSION = "osversion";
public static final String PROFILE_CREATION_DATE = "profileDate";
public static final String SEQ = "seq";
public static final String VERSION_ATTR = "v";
}
}

View File

@ -1,107 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko.telemetry;
import android.content.Context;
import android.os.Build;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.telemetry.TelemetryConstants.CorePing;
import org.mozilla.gecko.util.Experiments;
import org.mozilla.gecko.util.StringUtils;
import java.io.IOException;
import java.util.Locale;
/**
* A class with static methods to generate the various Java-created Telemetry pings to upload to the telemetry server.
*/
public class TelemetryPingGenerator {
// In the server url, the initial path directly after the "scheme://host:port/"
private static final String SERVER_INITIAL_PATH = "submit/telemetry";
/**
* Returns a url of the format:
* http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
*
* @param docId A unique document ID for the ping associated with the upload to this server
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
* @param docType The name of the ping (e.g. "main")
* @return a url at which to POST the telemetry data to
*/
private static String getTelemetryServerURL(final String docId, final String serverURLSchemeHostPort,
final String docType) {
final String appName = AppConstants.MOZ_APP_BASENAME;
final String appVersion = AppConstants.MOZ_APP_VERSION;
final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
final String appBuildId = AppConstants.MOZ_APP_BUILDID;
// The compiler will optimize a single String concatenation into a StringBuilder statement.
// If you change this `return`, be sure to keep it as a single statement to keep it optimized!
return serverURLSchemeHostPort + '/' +
SERVER_INITIAL_PATH + '/' +
docId + '/' +
docType + '/' +
appName + '/' +
appVersion + '/' +
appUpdateChannel + '/' +
appBuildId;
}
/**
* @param docId A unique document ID for the ping associated with the upload to this server
* @param clientId The client ID of this profile (from Gecko)
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
* @param profileCreationDateDays The profile creation date in days to the UNIX epoch, NOT MILLIS.
* @throws IOException when client ID could not be created
*/
public static TelemetryPing createCorePing(final Context context, final String docId, final String clientId,
final String serverURLSchemeHostPort, final int seq, final long profileCreationDateDays,
@Nullable final String distributionId, @Nullable final String defaultSearchEngine) {
final String serverURL = getTelemetryServerURL(docId, serverURLSchemeHostPort, CorePing.NAME);
final ExtendedJSONObject payload =
createCorePingPayload(context, clientId, seq, profileCreationDateDays, distributionId, defaultSearchEngine);
return new TelemetryPing(serverURL, payload);
}
private static ExtendedJSONObject createCorePingPayload(final Context context, final String clientId,
final int seq, final long profileCreationDate, @Nullable final String distributionId,
@Nullable final String defaultSearchEngine) {
final ExtendedJSONObject ping = new ExtendedJSONObject();
ping.put(CorePing.VERSION_ATTR, CorePing.VERSION_VALUE);
ping.put(CorePing.OS_ATTR, CorePing.OS_VALUE);
// We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
// manufacturer because we're less likely to have manufacturers with similar names than we are for a
// manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
final String deviceDescriptor =
StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
ping.put(CorePing.ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
ping.put(CorePing.CLIENT_ID, clientId);
ping.put(CorePing.DEFAULT_SEARCH_ENGINE, TextUtils.isEmpty(defaultSearchEngine) ? null : defaultSearchEngine);
ping.put(CorePing.DEVICE, deviceDescriptor);
ping.put(CorePing.LOCALE, Locales.getLanguageTag(Locale.getDefault()));
ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
ping.put(CorePing.SEQ, seq);
ping.putArray(CorePing.EXPERIMENTS, Experiments.getActiveExperiments(context));
// Optional.
if (distributionId != null) {
ping.put(CorePing.DISTRIBUTION_ID, distributionId);
}
// `null` indicates failure more clearly than < 0.
final Long finalProfileCreationDate = (profileCreationDate < 0) ? null : profileCreationDate;
ping.put(CorePing.PROFILE_CREATION_DATE, finalProfileCreationDate);
return ping;
}
}

View File

@ -10,6 +10,7 @@ import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
@ -21,6 +22,8 @@ import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.telemetry.pings.TelemetryCorePingBuilder;
import org.mozilla.gecko.telemetry.pings.TelemetryPing;
import org.mozilla.gecko.util.StringUtils;
import java.io.IOException;
@ -170,7 +173,6 @@ public class TelemetryUploadService extends BackgroundService {
private void uploadCorePing(@NonNull final String docId, final int seq, @NonNull final String profileName,
@NonNull final String profilePath, @Nullable final String defaultSearchEngine) {
final GeckoProfile profile = GeckoProfile.get(this, profileName, profilePath);
final long profileCreationDate = getProfileCreationDate(profile);
final String clientId;
try {
clientId = profile.getClientId();
@ -184,9 +186,20 @@ public class TelemetryUploadService extends BackgroundService {
// TODO (bug 1241685): Sync this preference with the gecko preference.
final String serverURLSchemeHostPort =
sharedPrefs.getString(TelemetryConstants.PREF_SERVER_URL, TelemetryConstants.DEFAULT_SERVER_URL);
final long profileCreationDate = getProfileCreationDate(profile);
final TelemetryCorePingBuilder builder = new TelemetryCorePingBuilder(this, serverURLSchemeHostPort)
.setClientID(clientId)
.setDefaultSearchEngine(TextUtils.isEmpty(defaultSearchEngine) ? null : defaultSearchEngine)
.setProfileCreationDate(profileCreationDate < 0 ? null : profileCreationDate)
.setSequenceNumber(seq);
final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
final TelemetryPing corePing = TelemetryPingGenerator.createCorePing(this, docId, clientId,
serverURLSchemeHostPort, seq, profileCreationDate, distributionId, defaultSearchEngine);
if (distributionId != null) {
builder.setOptDistributionID(distributionId);
}
final TelemetryPing corePing = builder.build();
final CorePingResultDelegate resultDelegate = new CorePingResultDelegate();
uploadPing(corePing, resultDelegate);
}

View File

@ -0,0 +1,138 @@
/*
* 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/.
*/
package org.mozilla.gecko.telemetry.pings;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.util.Experiments;
import org.mozilla.gecko.util.StringUtils;
import java.util.Locale;
/**
* Builds a {@link TelemetryPing} representing a core ping.
*
* See https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/core-ping.html
* for details on the core ping.
*/
public class TelemetryCorePingBuilder extends TelemetryPingBuilder {
private static final String NAME = "core";
private static final int VERSION_VALUE = 4; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
private static final String OS_VALUE = "Android";
private static final String ARCHITECTURE = "arch";
private static final String CLIENT_ID = "clientId";
private static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
private static final String DEVICE = "device";
private static final String DISTRIBUTION_ID = "distributionId";
private static final String EXPERIMENTS = "experiments";
private static final String LOCALE = "locale";
private static final String OS_ATTR = "os";
private static final String OS_VERSION = "osversion";
private static final String PROFILE_CREATION_DATE = "profileDate";
private static final String SEQ = "seq";
private static final String VERSION_ATTR = "v";
public TelemetryCorePingBuilder(final Context context, final String serverURLSchemeHostPort) {
super(serverURLSchemeHostPort);
initPayloadConstants(context);
}
private void initPayloadConstants(final Context context) {
payload.put(VERSION_ATTR, VERSION_VALUE);
payload.put(OS_ATTR, OS_VALUE);
// We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
// manufacturer because we're less likely to have manufacturers with similar names than we are for a
// manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
final String deviceDescriptor =
StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
payload.put(ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
payload.put(DEVICE, deviceDescriptor);
payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault()));
payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
payload.putArray(EXPERIMENTS, Experiments.getActiveExperiments(context));
}
@Override
String getDocType() {
return NAME;
}
@Override
String[] getMandatoryFields() {
return new String[] {
ARCHITECTURE,
CLIENT_ID,
DEFAULT_SEARCH_ENGINE,
DEVICE,
LOCALE,
OS_ATTR,
OS_VERSION,
PROFILE_CREATION_DATE,
SEQ,
VERSION_ATTR,
};
}
public TelemetryCorePingBuilder setClientID(@NonNull final String clientID) {
if (clientID == null) {
throw new IllegalArgumentException("Expected non-null clientID");
}
payload.put(CLIENT_ID, clientID);
return this;
}
/**
* @param engine the default search engine identifier, or null if there is an error.
*/
public TelemetryCorePingBuilder setDefaultSearchEngine(@Nullable final String engine) {
if (engine != null && engine.isEmpty()) {
throw new IllegalArgumentException("Received empty string. Expected identifier or null.");
}
payload.put(DEFAULT_SEARCH_ENGINE, engine);
return this;
}
public TelemetryCorePingBuilder setOptDistributionID(@NonNull final String distributionID) {
if (distributionID == null) {
throw new IllegalArgumentException("Expected non-null distribution ID");
}
payload.put(DISTRIBUTION_ID, distributionID);
return this;
}
/**
* @param date a positive date value, or null if there is an error.
*/
public TelemetryCorePingBuilder setProfileCreationDate(@Nullable final Long date) {
if (date != null && date < 0) {
throw new IllegalArgumentException("Expect positive date value. Received: " + date);
}
payload.put(PROFILE_CREATION_DATE, date);
return this;
}
// TODO (mcomella): We can potentially build two pings with the same seq no if we leave seq as an argument.
/**
* @param seq a positive sequence number.
*/
public TelemetryCorePingBuilder setSequenceNumber(final int seq) {
if (seq < 0) {
throw new IllegalArgumentException("Expected positive sequence number. Recived: " + seq);
}
payload.put(SEQ, seq);
return this;
}
}

View File

@ -3,12 +3,15 @@
* 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/. */
package org.mozilla.gecko.telemetry;
package org.mozilla.gecko.telemetry.pings;
import org.mozilla.gecko.sync.ExtendedJSONObject;
/**
* Container for telemetry data and the data necessary to upload it.
*
* If you want to create one of these, consider extending
* {@link TelemetryPingBuilder} or one of its descendants.
*/
public class TelemetryPing {
private final String url;

View File

@ -0,0 +1,88 @@
/*
* 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/.
*/
package org.mozilla.gecko.telemetry.pings;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import java.util.Set;
import java.util.UUID;
/**
* A generic Builder for {@link TelemetryPing} instances. Each overriding class is
* expected to create a specific type of ping (e.g. "core").
*
* This base class handles the common ping operations under the hood:
* * Validating mandatory fields
* * Forming the server url
*/
abstract class TelemetryPingBuilder {
// In the server url, the initial path directly after the "scheme://host:port/"
private static final String SERVER_INITIAL_PATH = "submit/telemetry";
private final String serverUrl;
protected final ExtendedJSONObject payload;
public TelemetryPingBuilder(final String serverURLSchemeHostPort) {
serverUrl = getTelemetryServerURL(getDocType(), serverURLSchemeHostPort);
payload = new ExtendedJSONObject();
}
/**
* @return the name of the ping (e.g. "core")
*/
abstract String getDocType();
/**
* @return the fields that are mandatory for the resultant ping to be uploaded to
* the server. These will be validated before the ping is built.
*/
abstract String[] getMandatoryFields();
public TelemetryPing build() {
validatePayload();
return new TelemetryPing(serverUrl, payload);
}
private void validatePayload() {
final Set<String> keySet = payload.keySet();
for (final String mandatoryField : getMandatoryFields()) {
if (!keySet.contains(mandatoryField)) {
throw new IllegalArgumentException("Builder does not contain mandatory field: " +
mandatoryField);
}
}
}
/**
* Returns a url of the format:
* http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
*
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
* @param docType The name of the ping (e.g. "main")
* @return a url at which to POST the telemetry data to
*/
private static String getTelemetryServerURL(final String docType,
final String serverURLSchemeHostPort) {
final String docId = UUID.randomUUID().toString();
final String appName = AppConstants.MOZ_APP_BASENAME;
final String appVersion = AppConstants.MOZ_APP_VERSION;
final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
final String appBuildId = AppConstants.MOZ_APP_BUILDID;
// The compiler will optimize a single String concatenation into a StringBuilder statement.
// If you change this `return`, be sure to keep it as a single statement to keep it optimized!
return serverURLSchemeHostPort + '/' +
SERVER_INITIAL_PATH + '/' +
docId + '/' +
docType + '/' +
appName + '/' +
appVersion + '/' +
appUpdateChannel + '/' +
appBuildId;
}
}

View File

@ -107,7 +107,7 @@
<!ENTITY settings "Settings">
<!ENTITY settings_title "Settings">
<!ENTITY pref_category_general "General">
<!ENTITY pref_category_general_summary2 "Home, language, URL bar">
<!ENTITY pref_category_general_summary3 "Home, language, tab queue">
<!-- Localization note (pref_category_language) : This is the preferences
section in which the user picks the locale in which to display Firefox
@ -146,11 +146,11 @@
<!ENTITY overlay_no_synced_devices "No Firefox Account connected devices found">
<!ENTITY pref_category_search3 "Search">
<!ENTITY pref_category_search_summary "Customize your search providers">
<!ENTITY pref_category_search_summary2 "Add, set default, show suggestions">
<!ENTITY pref_category_accessibility "Accessibility">
<!ENTITY pref_category_accessibility_summary2 "Text size, zoom, voice input">
<!ENTITY pref_category_privacy_short "Privacy">
<!ENTITY pref_category_privacy_summary3 "Tracking, cookies, data choices">
<!ENTITY pref_category_privacy_summary4 "Tracking, logins, data choices">
<!ENTITY pref_category_vendor2 "&vendorShortName; &brandShortName;">
<!ENTITY pref_category_vendor_summary2 "About &brandShortName;, FAQs, feedback">
<!ENTITY pref_category_datareporting "Data choices">
@ -168,7 +168,7 @@
it is applicable. -->
<!ENTITY pref_search_hint2 "TIP: Add any website to your list of search providers by long-pressing on its search field and then touching the &formatI; icon.">
<!ENTITY pref_category_advanced "Advanced">
<!ENTITY pref_category_advanced_summary2 "Restore tabs, plugins, developer tools">
<!ENTITY pref_category_advanced_summary3 "Restore tabs, data saver, developer tools">
<!ENTITY pref_category_notifications "Notifications">
<!ENTITY pref_category_notifications_summary "New features, website updates">
<!ENTITY pref_content_notifications "Website updates">
@ -349,7 +349,7 @@ size. -->
<!ENTITY pref_private_data_downloadFiles2 "Downloads">
<!ENTITY pref_private_data_syncedTabs "Synced tabs">
<!ENTITY pref_default_browser "Make default browser">
<!ENTITY pref_about_firefox "About &brandShortName;">
<!ENTITY pref_vendor_faqs "FAQs">
<!ENTITY pref_vendor_feedback "Give feedback">

View File

@ -206,6 +206,8 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'BootReceiver.java',
'BrowserApp.java',
'BrowserLocaleManager.java',
'cleanup/FileCleanupController.java',
'cleanup/FileCleanupService.java',
'ContactService.java',
'ContextGetter.java',
'CrashHandler.java',
@ -572,9 +574,10 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'tabs/TabsPanel.java',
'tabs/TabsPanelThumbnailView.java',
'Telemetry.java',
'telemetry/pings/TelemetryCorePingBuilder.java',
'telemetry/pings/TelemetryPing.java',
'telemetry/pings/TelemetryPingBuilder.java',
'telemetry/TelemetryConstants.java',
'telemetry/TelemetryPing.java',
'telemetry/TelemetryPingGenerator.java',
'telemetry/TelemetryUploadService.java',
'TelemetryContract.java',
'text/FloatingActionModeCallback.java',

View File

@ -70,6 +70,11 @@
gecko:entryKeys="@array/pref_private_data_keys"
gecko:initialValues="@array/pref_private_data_defaults" />
<org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.default_browser.link"
android:title="@string/pref_default_browser"
android:persistent="false"
url="https://support.mozilla.org/kb/make-firefox-default-browser-android?utm_source=inproduct&amp;utm_medium=settings&amp;utm_campaign=mobileandroid"/>
<PreferenceScreen android:title="@string/pref_category_vendor"
android:summary="@string/pref_category_vendor_summary"
android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >

View File

@ -76,13 +76,6 @@
<string name="url_bar_default_text">&url_bar_default_text2;</string>
<!-- https://input.mozilla.org/feedback/android/%VERSION%/%CHANNEL%/?utm_source=feedback-settings
This should be kept in sync with the "app.feedbackURL" pref defined in mobile.js -->
<string name="feedback_link">https://input.mozilla.org/feedback/android/&formatS1;/&formatS2;/?utm_source=feedback-settings</string>
<!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/faq -->
<string name="faq_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/faq</string>
<!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/ -->
<string name="help_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/</string>
<string name="help_menu">&help_menu;</string>
@ -136,14 +129,14 @@
<string name="settings">&settings;</string>
<string name="settings_title">&settings_title;</string>
<string name="pref_category_general">&pref_category_general;</string>
<string name="pref_category_general_summary">&pref_category_general_summary2;</string>
<string name="pref_category_general_summary">&pref_category_general_summary3;</string>
<string name="pref_category_search">&pref_category_search3;</string>
<string name="pref_category_search_summary">&pref_category_search_summary;</string>
<string name="pref_category_search_summary">&pref_category_search_summary2;</string>
<string name="pref_category_accessibility">&pref_category_accessibility;</string>
<string name="pref_category_accessibility_summary">&pref_category_accessibility_summary2;</string>
<string name="pref_category_privacy_short">&pref_category_privacy_short;</string>
<string name="pref_category_privacy_summary">&pref_category_privacy_summary3;</string>
<string name="pref_category_privacy_summary">&pref_category_privacy_summary4;</string>
<string name="pref_category_vendor">&pref_category_vendor2;</string>
<string name="pref_category_vendor_summary">&pref_category_vendor_summary2;</string>
<string name="pref_category_datareporting">&pref_category_datareporting;</string>
@ -161,7 +154,7 @@
<string name="locale_system_default">&locale_system_default;</string>
<string name="pref_category_advanced">&pref_category_advanced;</string>
<string name="pref_category_advanced_summary">&pref_category_advanced_summary2;</string>
<string name="pref_category_advanced_summary">&pref_category_advanced_summary3;</string>
<string name="pref_developer_remotedebugging_usb">&pref_developer_remotedebugging_usb;</string>
<string name="pref_developer_remotedebugging_wifi">&pref_developer_remotedebugging_wifi;</string>
<string name="pref_developer_remotedebugging_wifi_disabled_summary">&pref_developer_remotedebugging_wifi_disabled_summary;</string>
@ -306,9 +299,18 @@
<string name="content_notification_action_settings">&content_notification_action_settings;</string>
<string name="content_notification_updated_on">&content_notification_updated_on;</string>
<string name="pref_default_browser">&pref_default_browser;</string>
<string name="pref_about_firefox">&pref_about_firefox;</string>
<string name="pref_vendor_faqs">&pref_vendor_faqs;</string>
<!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/faq -->
<string name="faq_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/faq</string>
<string name="pref_vendor_feedback">&pref_vendor_feedback;</string>
<!-- https://input.mozilla.org/feedback/android/%VERSION%/%CHANNEL%/?utm_source=feedback-settings
This should be kept in sync with the "app.feedbackURL" pref defined in mobile.js -->
<string name="feedback_link">https://input.mozilla.org/feedback/android/&formatS1;/&formatS2;/?utm_source=feedback-settings</string>
<string name="pref_dialog_set_default">&pref_dialog_set_default;</string>
<string name="pref_default">&pref_dialog_default;</string>

View File

@ -60,6 +60,9 @@ public class AutopushClient {
public static final String JSON_KEY_ERROR = "error";
public static final String JSON_KEY_MESSAGE = "message";
protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE };
protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
/**
* The server's URI.
* <p>
@ -136,8 +139,8 @@ public class AutopushClient {
if (200 <= status && status <= 299) {
return status;
}
int code;
int errno;
long code;
long errno;
String error;
String message;
String info;
@ -146,9 +149,10 @@ public class AutopushClient {
body = new SyncStorageResponse(response).jsonObjectBody();
// TODO: The service doesn't do the right thing yet :(
// body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
// body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
code = body.getLong(JSON_KEY_CODE).intValue();
errno = body.getLong(JSON_KEY_ERRNO).intValue();
body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
// Would throw above if missing; the -1 defaults quiet NPE warnings.
code = body.getLong(JSON_KEY_CODE, -1);
errno = body.getLong(JSON_KEY_ERRNO, -1);
error = body.getString(JSON_KEY_ERROR);
message = body.getString(JSON_KEY_MESSAGE);
} catch (Exception e) {

View File

@ -0,0 +1,92 @@
/*
* 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/.
*/
package org.mozilla.gecko.cleanup;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Tests functionality of the {@link FileCleanupController}.
*/
@RunWith(TestRunner.class)
public class TestFileCleanupController {
@Test
public void testStartIfReadyEmptySharedPrefsRunsCleanup() {
final Context context = mock(Context.class);
FileCleanupController.startIfReady(context, getSharedPreferences(), "");
verify(context).startService(any(Intent.class));
}
@Test
public void testStartIfReadyLastRunNowDoesNotRun() {
final SharedPreferences sharedPrefs = getSharedPreferences();
sharedPrefs.edit()
.putLong(FileCleanupController.PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis())
.commit(); // synchronous to finish before test runs.
final Context context = mock(Context.class);
FileCleanupController.startIfReady(context, sharedPrefs, "");
verify(context, never()).startService((any(Intent.class)));
}
/**
* Depends on {@link #testStartIfReadyEmptySharedPrefsRunsCleanup()} success
* i.e. we expect the cleanup to run with empty prefs.
*/
@Test
public void testStartIfReadyDoesNotRunTwiceInSuccession() {
final Context context = mock(Context.class);
final SharedPreferences sharedPrefs = getSharedPreferences();
FileCleanupController.startIfReady(context, sharedPrefs, "");
verify(context).startService(any(Intent.class));
// Note: the Controller relies on SharedPrefs.apply, but
// robolectric made this a synchronous call. Yay!
FileCleanupController.startIfReady(context, sharedPrefs, "");
verify(context, atMost(1)).startService(any(Intent.class));
}
@Test
public void testGetFilesToCleanupContainsProfilePath() {
final String profilePath = "/a/profile/path";
final ArrayList<String> fileList = FileCleanupController.getFilesToCleanup(profilePath);
assertNotNull("Returned file list is non-null", fileList);
boolean atLeastOneStartsWithProfilePath = false;
final String pathToCheck = profilePath + "/"; // Ensure the calling code adds a slash to divide the path.
for (final String path : fileList) {
if (path.startsWith(pathToCheck)) {
// It'd be great if we could assert these individually so
// we could display the Strings in console output.
atLeastOneStartsWithProfilePath = true;
}
}
assertTrue("At least one returned String starts with a profile path", atLeastOneStartsWithProfilePath);
}
private SharedPreferences getSharedPreferences() {
return RuntimeEnvironment.application.getSharedPreferences("TestFileCleanupController", 0);
}
}

View File

@ -0,0 +1,106 @@
/*
* 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/.
*/
package org.mozilla.gecko.cleanup;
import android.content.Intent;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
/**
* Tests the methods of {@link FileCleanupService}.
*/
@RunWith(TestRunner.class)
public class TestFileCleanupService {
@Rule
public final TemporaryFolder tempFolder = new TemporaryFolder();
private void assertAllFilesExist(final List<File> fileList) {
for (final File file : fileList) {
assertTrue("File exists", file.exists());
}
}
private void assertAllFilesDoNotExist(final List<File> fileList) {
for (final File file : fileList) {
assertFalse("File does not exist", file.exists());
}
}
private void onHandleIntent(final ArrayList<String> filePaths) {
final FileCleanupService service = new FileCleanupService();
final Intent intent = new Intent(FileCleanupService.ACTION_DELETE_FILES);
intent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, filePaths);
service.onHandleIntent(intent);
}
@Test
public void testOnHandleIntentDeleteSpecifiedFiles() throws Exception {
final int fileListCount = 3;
final ArrayList<File> filesToDelete = generateFileList(fileListCount);
final ArrayList<String> pathsToDelete = new ArrayList<>(fileListCount);
for (final File file : filesToDelete) {
pathsToDelete.add(file.getAbsolutePath());
}
assertAllFilesExist(filesToDelete);
onHandleIntent(pathsToDelete);
assertAllFilesDoNotExist(filesToDelete);
}
@Test
public void testOnHandleIntentDoesNotDeleteUnrelatedFiles() throws Exception {
final ArrayList<File> filesShouldNotBeDeleted = generateFileList(3);
assertAllFilesExist(filesShouldNotBeDeleted);
onHandleIntent(new ArrayList<String>());
assertAllFilesExist(filesShouldNotBeDeleted);
}
@Test
public void testOnHandleIntentDeletesEmptyDirectory() throws Exception {
final File dir = tempFolder.newFolder();
final ArrayList<String> filesToDelete = new ArrayList<>(1);
filesToDelete.add(dir.getAbsolutePath());
assertTrue("Empty directory exists", dir.exists());
onHandleIntent(filesToDelete);
assertFalse("Empty directory deleted by service", dir.exists());
}
@Test
public void testOnHandleIntentDoesNotDeleteNonEmptyDirectory() throws Exception {
final File dir = tempFolder.newFolder();
final ArrayList<String> filesCannotDelete = new ArrayList<>(1);
filesCannotDelete.add(dir.getAbsolutePath());
assertTrue("Directory exists", dir.exists());
final File fileInDir = new File(dir, "file_in_dir");
assertTrue("File in dir created", fileInDir.createNewFile());
onHandleIntent(filesCannotDelete);
assertTrue("Non-empty directory not deleted", dir.exists());
assertTrue("File in directory not deleted", fileInDir.exists());
}
private ArrayList<File> generateFileList(final int size) throws IOException {
final ArrayList<File> fileList = new ArrayList<>(size);
for (int i = 0; i < size; ++i) {
fileList.add(tempFolder.newFile());
}
return fileList;
}
}

View File

@ -0,0 +1,92 @@
/*
* 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/.
*/
package org.mozilla.gecko.telemetry.pings;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import static org.junit.Assert.*;
/**
* Unit test methods of the {@link TelemetryPingBuilder} class.
*/
@RunWith(TestRunner.class)
public class TestTelemetryPingBuilder {
@Test
public void testMandatoryFieldsNone() {
final NoMandatoryFieldsBuilder builder = new NoMandatoryFieldsBuilder();
builder.setNonMandatoryField();
assertNotNull("Builder does not throw and returns a non-null value", builder.build());
}
@Test(expected = IllegalArgumentException.class)
public void testMandatoryFieldsMissing() {
final MandatoryFieldsBuilder builder = new MandatoryFieldsBuilder();
builder.setNonMandatoryField()
.build(); // should throw
}
@Test
public void testMandatoryFieldsIncluded() {
final MandatoryFieldsBuilder builder = new MandatoryFieldsBuilder();
builder.setNonMandatoryField()
.setMandatoryField();
assertNotNull("Builder does not throw and returns non-null value", builder.build());
}
private static class NoMandatoryFieldsBuilder extends TelemetryPingBuilder {
public NoMandatoryFieldsBuilder() {
super("");
}
@Override
String getDocType() {
return "";
}
@Override
String[] getMandatoryFields() {
return new String[0];
}
public NoMandatoryFieldsBuilder setNonMandatoryField() {
payload.put("non-mandatory", true);
return this;
}
}
private static class MandatoryFieldsBuilder extends TelemetryPingBuilder {
private static final String MANDATORY_FIELD = "mandatory-field";
public MandatoryFieldsBuilder() {
super("");
}
@Override
String getDocType() {
return "";
}
@Override
String[] getMandatoryFields() {
return new String[] {
MANDATORY_FIELD,
};
}
public MandatoryFieldsBuilder setNonMandatoryField() {
payload.put("non-mandatory", true);
return this;
}
public MandatoryFieldsBuilder setMandatoryField() {
payload.put(MANDATORY_FIELD, true);
return this;
}
}
}

View File

@ -31,5 +31,13 @@
rows="3" cols="8">Words in a box</textarea>
<textarea id="RTLtextarea" style="direction: rtl;"
rows="3" cols="8">הספר הוא טוב</textarea>
<br>
<input id="LTRphone" style="direction: ltr;" size="40"
value="09876543210 .-.)(wp#*1034103410341034X">
<br>
<input id="RTLphone" style="direction: rtl;" size="40"
value="התקשר +972 3 7347514 במשך זמן טוב">
</body>
</html>

View File

@ -60,18 +60,18 @@ function elementSelection(element) {
}
/**
* Select the first character of a target element, w/o affecting focus.
* Select the requested character of a target element, w/o affecting focus.
*/
function selectElementFirstChar(doc, element) {
function selectElementChar(doc, element, char) {
if (isInputOrTextarea(element)) {
element.setSelectionRange(0, 1);
element.setSelectionRange(char, char + 1);
return;
}
// Simple test cases designed firstChild == #text node.
let range = doc.createRange();
range.setStart(element.firstChild, 0);
range.setEnd(element.firstChild, 1);
range.setStart(element.firstChild, char);
range.setEnd(element.firstChild, char + 1);
let selection = elementSelection(element);
selection.removeAllRanges();
@ -79,14 +79,14 @@ function selectElementFirstChar(doc, element) {
}
/**
* Get longpress point. Determine the midpoint in the first character of
* Get longpress point. Determine the midpoint in the requested character of
* the content in the element. X will be midpoint from left to right.
* Y will be 1/3 of the height up from the bottom to account for both
* LTR and smaller RTL characters. ie: |X| vs. |א|
*/
function getFirstCharPressPoint(doc, element, expected) {
function getCharPressPoint(doc, element, char, expected) {
// Select the first char in the element.
selectElementFirstChar(doc, element);
selectElementChar(doc, element, char);
// Reality check selected char to expected.
let selection = elementSelection(element);
@ -162,17 +162,22 @@ add_task(function* testAccessibleCarets() {
let i_RTL_elem = doc.getElementById("RTLinput");
let ta_RTL_elem = doc.getElementById("RTLtextarea");
let ip_LTR_elem = doc.getElementById("LTRphone");
let ip_RTL_elem = doc.getElementById("RTLphone");
// Locate longpress midpoints for test elements, ensure expactations.
let ce_LTR_midPoint = getFirstCharPressPoint(doc, ce_LTR_elem, "F");
let tc_LTR_midPoint = getFirstCharPressPoint(doc, tc_LTR_elem, "O");
let i_LTR_midPoint = getFirstCharPressPoint(doc, i_LTR_elem, "T");
let ta_LTR_midPoint = getFirstCharPressPoint(doc, ta_LTR_elem, "W");
let ce_LTR_midPoint = getCharPressPoint(doc, ce_LTR_elem, 0, "F");
let tc_LTR_midPoint = getCharPressPoint(doc, tc_LTR_elem, 0, "O");
let i_LTR_midPoint = getCharPressPoint(doc, i_LTR_elem, 0, "T");
let ta_LTR_midPoint = getCharPressPoint(doc, ta_LTR_elem, 0, "W");
let ce_RTL_midPoint = getFirstCharPressPoint(doc, ce_RTL_elem, "א");
let tc_RTL_midPoint = getFirstCharPressPoint(doc, tc_RTL_elem, "ת");
let i_RTL_midPoint = getFirstCharPressPoint(doc, i_RTL_elem, "ל");
let ta_RTL_midPoint = getFirstCharPressPoint(doc, ta_RTL_elem, "ה");
let ce_RTL_midPoint = getCharPressPoint(doc, ce_RTL_elem, 0, "א");
let tc_RTL_midPoint = getCharPressPoint(doc, tc_RTL_elem, 0, "ת");
let i_RTL_midPoint = getCharPressPoint(doc, i_RTL_elem, 0, "ל");
let ta_RTL_midPoint = getCharPressPoint(doc, ta_RTL_elem, 0, "ה");
let ip_LTR_midPoint = getCharPressPoint(doc, ip_LTR_elem, 8, "2");
let ip_RTL_midPoint = getCharPressPoint(doc, ip_RTL_elem, 9, "2");
// Longpress various LTR content elements. Test focused element against
// expected, and selected text against expected.
@ -192,6 +197,13 @@ add_task(function* testAccessibleCarets() {
is(result.focusedElement, ta_LTR_elem, "Focused element should match expected.");
is(result.text, "Words", "Selected text should match expected text.");
result = getLongPressResult(browser, ip_LTR_midPoint);
is(result.focusedElement, ip_LTR_elem, "Focused element should match expected.");
is(result.text, "09876543210 .-.)(wp#*103410341",
"Selected phone number should match expected text.");
is(result.text.length, 30,
"Selected phone number length should match expected maximum.");
// Longpress various RTL content elements. Test focused element against
// expected, and selected text against expected.
result = getLongPressResult(browser, ce_RTL_midPoint);
@ -210,6 +222,11 @@ add_task(function* testAccessibleCarets() {
is(result.focusedElement, ta_RTL_elem, "Focused element should match expected.");
is(result.text, "הספר", "Selected text should match expected text.");
result = getLongPressResult(browser, ip_RTL_midPoint);
is(result.focusedElement, ip_RTL_elem, "Focused element should match expected.");
is(result.text, "+972 3 7347514 ",
"Selected phone number should match expected text.");
ok(true, "Finished all tests.");
});

View File

@ -421,7 +421,8 @@ FxAccountsInternal.prototype = {
return Promise.reject(new Error(
"checkEmailStatus called without a session token"));
}
return this.fxAccountsClient.recoveryEmailStatus(sessionToken, options);
return this.fxAccountsClient.recoveryEmailStatus(sessionToken,
options).catch(error => this._handleTokenError(error));
},
/**
@ -673,10 +674,11 @@ FxAccountsInternal.prototype = {
return null;
}
if (!this.isUserEmailVerified(data)) {
log.trace("checkVerificationStatus - forcing verification status check");
this.pollEmailStatus(currentState, data.sessionToken, "push");
}
// Always check the verification status, even if the local state indicates
// we're already verified. If the user changed their password, the check
// will fail, and we'll enter the reauth state.
log.trace("checkVerificationStatus - forcing verification status check");
return this.pollEmailStatus(currentState, data.sessionToken, "push");
});
},
@ -1099,7 +1101,9 @@ FxAccountsInternal.prototype = {
}
}
this.checkEmailStatus(sessionToken, { reason: why })
// We return a promise for testing only. Other callers can ignore this,
// since verification polling continues in the background.
return this.checkEmailStatus(sessionToken, { reason: why })
.then((response) => {
log.debug("checkEmailStatus -> " + JSON.stringify(response));
if (response && response.verified) {

View File

@ -38,6 +38,26 @@ const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password";
const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
/**
* A helper function that extracts the message and stack from an error object.
* Returns a `{ message, stack }` tuple. `stack` will be null if the error
* doesn't have a stack trace.
*/
function getErrorDetails(error) {
let details = { message: String(error), stack: null };
// Adapted from Console.jsm.
if (error.stack) {
let frames = [];
for (let frame = error.stack; frame; frame = frame.caller) {
frames.push(String(frame).padStart(4));
}
details.stack = frames.join("\n");
}
return details;
}
/**
* Create a new FxAccountsWebChannel to listen for account updates
*
@ -116,6 +136,59 @@ this.FxAccountsWebChannel.prototype = {
}
},
_receiveMessage(message, sendingContext) {
let command = message.command;
let data = message.data;
switch (command) {
case COMMAND_PROFILE_CHANGE:
Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, data.uid);
break;
case COMMAND_LOGIN:
this._helpers.login(data).catch(error =>
this._sendError(error, message, sendingContext));
break;
case COMMAND_LOGOUT:
case COMMAND_DELETE:
this._helpers.logout(data.uid).catch(error =>
this._sendError(error, message, sendingContext));
break;
case COMMAND_CAN_LINK_ACCOUNT:
let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
let response = {
command: command,
messageId: message.messageId,
data: { ok: canLinkAccount }
};
log.debug("FxAccountsWebChannel response", response);
this._channel.send(response, sendingContext);
break;
case COMMAND_SYNC_PREFERENCES:
this._helpers.openSyncPreferences(sendingContext.browser, data.entryPoint);
break;
case COMMAND_CHANGE_PASSWORD:
this._helpers.changePassword(data).catch(error =>
this._sendError(error, message, sendingContext));
break;
default:
log.warn("Unrecognized FxAccountsWebChannel command", command);
break;
}
},
_sendError(error, incomingMessage, sendingContext) {
log.error("Failed to handle FxAccountsWebChannel message", error);
this._channel.send({
command: incomingMessage.command,
messageId: incomingMessage.messageId,
data: {
error: getErrorDetails(error),
},
}, sendingContext);
},
/**
* Create a new channel with the WebChannelBroker, setup a callback listener
* @private
@ -146,41 +219,10 @@ this.FxAccountsWebChannel.prototype = {
if (logPII) {
log.debug("FxAccountsWebChannel message details", message);
}
let command = message.command;
let data = message.data;
switch (command) {
case COMMAND_PROFILE_CHANGE:
Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, data.uid);
break;
case COMMAND_LOGIN:
this._helpers.login(data);
break;
case COMMAND_LOGOUT:
case COMMAND_DELETE:
this._helpers.logout(data.uid);
break;
case COMMAND_CAN_LINK_ACCOUNT:
let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
let response = {
command: command,
messageId: message.messageId,
data: { ok: canLinkAccount }
};
log.debug("FxAccountsWebChannel response", response);
this._channel.send(response, sendingContext);
break;
case COMMAND_SYNC_PREFERENCES:
this._helpers.openSyncPreferences(sendingContext.browser, data.entryPoint);
break;
case COMMAND_CHANGE_PASSWORD:
this._helpers.changePassword(data);
break;
default:
log.warn("Unrecognized FxAccountsWebChannel command", command);
break;
try {
this._receiveMessage(message, sendingContext);
} catch (error) {
this._sendError(error, message, sendingContext);
}
}
};
@ -299,9 +341,7 @@ this.FxAccountsWebChannelHelpers.prototype = {
log.info("changePassword ignoring unsupported field", name);
}
}
this._fxAccounts.updateUserAccountData(newCredentials).catch(err => {
log.error("Failed to update account data on password change", err);
});
return this._fxAccounts.updateUserAccountData(newCredentials);
},
/**

View File

@ -1438,6 +1438,33 @@ add_test(function test_getSignedInUserProfile_no_account_data() {
});
add_task(function* test_checkVerificationStatusFailed() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let client = fxa.internal.fxAccountsClient;
client.recoveryEmailStatus = () => {
return Promise.reject({
code: 401,
errno: ERRNO_INVALID_AUTH_TOKEN,
});
};
client.accountStatus = () => Promise.resolve(true);
yield fxa.setSignedInUser(alice);
let user = yield fxa.internal.getUserAccountData();
do_check_neq(alice.sessionToken, null);
do_check_eq(user.email, alice.email);
do_check_eq(user.verified, true);
yield fxa.checkVerificationStatus();
user = yield fxa.internal.getUserAccountData();
do_check_eq(user.email, alice.email);
do_check_eq(user.sessionToken, null);
});
/*
* End of tests.
* Utility functions follow.

View File

@ -38,6 +38,80 @@ add_test(function () {
run_next_test();
});
add_task(function* test_rejection_reporting() {
let mockMessage = {
command: 'fxaccounts:login',
messageId: '1234',
data: { email: 'testuser@testuser.com' },
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING,
helpers: {
login(accountData) {
equal(accountData.email, 'testuser@testuser.com',
'Should forward incoming message data to the helper');
return Promise.reject(new Error('oops'));
},
},
});
let promiseSend = new Promise(resolve => {
channel._channel.send = (message, context) => {
resolve({ message, context });
};
});
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
let { message, context } = yield promiseSend;
equal(context, mockSendingContext, 'Should forward the original context');
equal(message.command, 'fxaccounts:login',
'Should include the incoming command');
equal(message.messageId, '1234', 'Should include the message ID');
equal(message.data.error.message, 'Error: oops',
'Should convert the error message to a string');
notStrictEqual(message.data.error.stack, null,
'Should include the stack for JS error rejections');
});
add_test(function test_exception_reporting() {
let mockMessage = {
command: 'fxaccounts:sync_preferences',
messageId: '5678',
data: { entryPoint: 'fxa:verification_complete' }
};
let channel = new FxAccountsWebChannel({
channel_id: WEBCHANNEL_ID,
content_uri: URL_STRING,
helpers: {
openSyncPreferences(browser, entryPoint) {
equal(entryPoint, 'fxa:verification_complete',
'Should forward incoming message data to the helper');
throw new TypeError('splines not reticulated');
},
},
});
channel._channel.send = (message, context) => {
equal(context, mockSendingContext, 'Should forward the original context');
equal(message.command, 'fxaccounts:sync_preferences',
'Should include the incoming command');
equal(message.messageId, '5678', 'Should include the message ID');
equal(message.data.error.message, 'TypeError: splines not reticulated',
'Should convert the exception to a string');
notStrictEqual(message.data.error.stack, null,
'Should include the stack for JS exceptions');
run_next_test();
};
channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});
add_test(function test_profile_image_change_message() {
var mockMessage = {
command: "profile:change",
@ -70,6 +144,7 @@ add_test(function test_login_message() {
login: function (accountData) {
do_check_eq(accountData.email, 'testuser@testuser.com');
run_next_test();
return Promise.resolve();
}
}
});
@ -90,6 +165,7 @@ add_test(function test_logout_message() {
logout: function (uid) {
do_check_eq(uid, 'foo');
run_next_test();
return Promise.resolve();
}
}
});
@ -110,6 +186,7 @@ add_test(function test_delete_message() {
logout: function (uid) {
do_check_eq(uid, 'foo');
run_next_test();
return Promise.resolve();
}
}
});
@ -199,23 +276,25 @@ add_test(function test_helpers_should_allow_relink_different_email() {
run_next_test();
});
add_test(function test_helpers_login_without_customize_sync() {
add_task(function* test_helpers_login_without_customize_sync() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
setSignedInUser: function(accountData) {
// ensure fxAccounts is informed of the new user being signed in.
do_check_eq(accountData.email, 'testuser@testuser.com');
return new Promise(resolve => {
// ensure fxAccounts is informed of the new user being signed in.
do_check_eq(accountData.email, 'testuser@testuser.com');
// verifiedCanLinkAccount should be stripped in the data.
do_check_false('verifiedCanLinkAccount' in accountData);
// verifiedCanLinkAccount should be stripped in the data.
do_check_false('verifiedCanLinkAccount' in accountData);
// the customizeSync pref should not update
do_check_false(helpers.getShowCustomizeSyncPref());
// the customizeSync pref should not update
do_check_false(helpers.getShowCustomizeSyncPref());
// previously signed in user preference is updated.
do_check_eq(helpers.getPreviousAccountNameHashPref(), helpers.sha256('testuser@testuser.com'));
// previously signed in user preference is updated.
do_check_eq(helpers.getPreviousAccountNameHashPref(), helpers.sha256('testuser@testuser.com'));
run_next_test();
resolve();
});
}
}
});
@ -226,27 +305,29 @@ add_test(function test_helpers_login_without_customize_sync() {
// ensure the previous account pref is overwritten.
helpers.setPreviousAccountNameHashPref('lastuser@testuser.com');
helpers.login({
yield helpers.login({
email: 'testuser@testuser.com',
verifiedCanLinkAccount: true,
customizeSync: false
});
});
add_test(function test_helpers_login_with_customize_sync() {
add_task(function* test_helpers_login_with_customize_sync() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
setSignedInUser: function(accountData) {
// ensure fxAccounts is informed of the new user being signed in.
do_check_eq(accountData.email, 'testuser@testuser.com');
return new Promise(resolve => {
// ensure fxAccounts is informed of the new user being signed in.
do_check_eq(accountData.email, 'testuser@testuser.com');
// customizeSync should be stripped in the data.
do_check_false('customizeSync' in accountData);
// customizeSync should be stripped in the data.
do_check_false('customizeSync' in accountData);
// the customizeSync pref should not update
do_check_true(helpers.getShowCustomizeSyncPref());
// the customizeSync pref should not update
do_check_true(helpers.getShowCustomizeSyncPref());
run_next_test();
resolve();
});
}
}
});
@ -254,34 +335,36 @@ add_test(function test_helpers_login_with_customize_sync() {
// the customize sync pref should be overwritten
helpers.setShowCustomizeSyncPref(false);
helpers.login({
yield helpers.login({
email: 'testuser@testuser.com',
verifiedCanLinkAccount: true,
customizeSync: true
});
});
add_test(function test_helpers_login_with_customize_sync_and_declined_engines() {
add_task(function* test_helpers_login_with_customize_sync_and_declined_engines() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
setSignedInUser: function(accountData) {
// ensure fxAccounts is informed of the new user being signed in.
do_check_eq(accountData.email, 'testuser@testuser.com');
return new Promise(resolve => {
// ensure fxAccounts is informed of the new user being signed in.
do_check_eq(accountData.email, 'testuser@testuser.com');
// customizeSync should be stripped in the data.
do_check_false('customizeSync' in accountData);
do_check_false('declinedSyncEngines' in accountData);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), false);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), false);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
// customizeSync should be stripped in the data.
do_check_false('customizeSync' in accountData);
do_check_false('declinedSyncEngines' in accountData);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), false);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), false);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
// the customizeSync pref should be disabled
do_check_false(helpers.getShowCustomizeSyncPref());
// the customizeSync pref should be disabled
do_check_false(helpers.getShowCustomizeSyncPref());
run_next_test();
resolve();
});
}
}
});
@ -295,7 +378,7 @@ add_test(function test_helpers_login_with_customize_sync_and_declined_engines()
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), true);
do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
helpers.login({
yield helpers.login({
email: 'testuser@testuser.com',
verifiedCanLinkAccount: true,
customizeSync: true,
@ -319,24 +402,26 @@ add_test(function test_helpers_open_sync_preferences() {
helpers.openSyncPreferences(mockBrowser, "fxa:verification_complete");
});
add_test(function test_helpers_change_password() {
add_task(function* test_helpers_change_password() {
let updateCalled = false;
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
updateUserAccountData(credentials) {
do_check_true(credentials.hasOwnProperty("email"));
do_check_true(credentials.hasOwnProperty("uid"));
do_check_true(credentials.hasOwnProperty("kA"));
// "foo" isn't a field known by storage, so should be dropped.
do_check_false(credentials.hasOwnProperty("foo"));
updateCalled = true;
return Promise.resolve();
return new Promise(resolve => {
do_check_true(credentials.hasOwnProperty("email"));
do_check_true(credentials.hasOwnProperty("uid"));
do_check_true(credentials.hasOwnProperty("kA"));
// "foo" isn't a field known by storage, so should be dropped.
do_check_false(credentials.hasOwnProperty("foo"));
updateCalled = true;
resolve();
});
}
}
});
helpers.changePassword({ email: "email", uid: "uid", kA: "kA", foo: "foo" });
yield helpers.changePassword({ email: "email", uid: "uid", kA: "kA", foo: "foo" });
do_check_true(updateCalled);
run_next_test();
});
function run_test() {

View File

@ -22,12 +22,18 @@ var cpows = [
/^window\.content/
];
var isInContentTask = false;
module.exports = function(context) {
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function showError(node, identifier) {
if (isInContentTask) {
return;
}
context.report({
node: node,
message: identifier +
@ -35,11 +41,32 @@ module.exports = function(context) {
});
}
function isContentTask(node) {
return node &&
node.type === "MemberExpression" &&
node.property.type === "Identifier" &&
node.property.name === "spawn" &&
node.object.type === "Identifier" &&
node.object.name === "ContentTask";
}
// ---------------------------------------------------------------------------
// Public
// ---------------------------------------------------------------------------
return {
CallExpression: function(node) {
if (isContentTask(node.callee)) {
isInContentTask = true;
}
},
"CallExpression:exit": function(node) {
if (isContentTask(node.callee)) {
isInContentTask = false;
}
},
MemberExpression: function(node) {
if (!helpers.getIsBrowserMochitest(this)) {
return;
@ -77,7 +104,6 @@ module.exports = function(context) {
node.parent.object.name != "content") {
return;
}
showError(node, expression);
return;
}

View File

@ -38,6 +38,10 @@ extensions.registerSchemaAPI("extension", null, (extension, context) => {
isAllowedIncognitoAccess() {
return Promise.resolve(true);
},
isAllowedFileSchemeAccess() {
return Promise.resolve(true);
},
},
};
});

View File

@ -117,7 +117,6 @@
},
{
"name": "isAllowedFileSchemeAccess",
"unsupported": true,
"type": "function",
"description": "Retrieves the state of the extension's access to the 'file://' scheme (as determined by the user-controlled 'Allow access to File URLs' checkbox.",
"async": "callback",

View File

@ -270,7 +270,7 @@ function backgroundScript() {
return browser.bookmarks.search("Menu Item");
}).then(results => {
browser.test.assertEq(1, results.length, "Expected number of results returned for menu item search");
checkBookmark({title: "Menu Item", url: "http://menu.org/", index: 4, parentId: bookmarkGuids.menuGuid}, results[0]);
checkBookmark({title: "Menu Item", url: "http://menu.org/", index: 3, parentId: bookmarkGuids.menuGuid}, results[0]);
// finds toolbar items
return browser.bookmarks.search("Toolbar Item");

View File

@ -33,6 +33,26 @@ add_task(function* test_is_allowed_incognito_access() {
info("extension unloaded");
});
add_task(function* test_is_allowed_file_scheme_access() {
function backgroundScript() {
browser.extension.isAllowedFileSchemeAccess().then(isAllowedFileSchemeAccess => {
browser.test.assertEq(true, isAllowedFileSchemeAccess, "isAllowedFileSchemeAccess is true");
browser.test.notifyPass("isAllowedFileSchemeAccess");
});
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript})()`,
manifest: {},
});
yield extension.startup();
info("extension loaded");
yield extension.awaitFinish("isAllowedFileSchemeAccess");
yield extension.unload();
info("extension unloaded");
});
</script>
</body>

View File

@ -5,69 +5,83 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet
href="chrome://mochikit/content/tests/SimpleTest/test.css"
type="text/css"?>
<window id="360437Test"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
width="600"
height="600"
onload="onLoad();"
onload="startTest();"
title="360437 test">
<script type="application/javascript"><![CDATA[
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cr = Components.results;
const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://testing-common/ContentTask.jsm");
ContentTask.setTestScope(window.opener.wrappedJSObject);
var gFindBar = null;
var gBrowser;
function ok(condition, message) {
window.opener.wrappedJSObject.SimpleTest.ok(condition, message);
}
function finish() {
window.close();
window.opener.wrappedJSObject.SimpleTest.finish();
var imports = ["SimpleTest", "ok", "is", "info"];
for (var name of imports) {
window[name] = window.opener.wrappedJSObject[name];
}
function onLoad() {
var _delayedOnLoad = function() {
function startTest() {
Task.spawn(function* () {
gFindBar = document.getElementById("FindToolbar");
gBrowser = document.getElementById("content");
gBrowser.addEventListener("pageshow", onPageShow, false);
gBrowser.loadURI("data:text/html,<form><input id='input' type='text' value='text inside an input element'></form>");
}
setTimeout(_delayedOnLoad, 1000);
for (let browserId of ["content", "content-remote"]) {
yield startTestWithBrowser(browserId);
}
}).then(() => {
window.close();
SimpleTest.finish();
});
}
function onPageShow() {
testNormalFind();
function* startTestWithBrowser(browserId) {
info("Starting test with browser '" + browserId + "'");
gBrowser = document.getElementById(browserId);
gFindBar.browser = gBrowser;
let promise = ContentTask.spawn(gBrowser, null, function* () {
return new Promise(resolve => {
addEventListener("DOMContentLoaded", function listener() {
removeEventListener("DOMContentLoaded", listener);
resolve();
});
});
});
gBrowser.loadURI("data:text/html,<form><input id='input' type='text' value='text inside an input element'></form>");
yield promise;
yield onDocumentLoaded();
}
function enterStringIntoFindField(aString) {
for (var i=0; i < aString.length; i++) {
var event = document.createEvent("KeyEvents");
event.initKeyEvent("keypress", true, true, null, false, false,
false, false, 0, aString.charCodeAt(i));
gFindBar._findField.inputField.dispatchEvent(event);
}
}
function testNormalFind() {
function* onDocumentLoaded() {
gFindBar.onFindCommand();
// Make sure the findfield is correctly focused on open
var searchStr = "text inside an input element";
enterStringIntoFindField(searchStr);
ok(document.commandDispatcher.focusedElement ==
yield* enterStringIntoFindField(searchStr);
is(document.commandDispatcher.focusedElement,
gFindBar._findField.inputField, "Find field isn't focused");
// Make sure "find again" correctly transfers focus to the content element
// when the find bar is closed.
gFindBar.close();
gFindBar.onFindAgainCommand(false);
ok(document.commandDispatcher.focusedElement ==
gBrowser.contentDocument.getElementById("input"),
"Input Element isn't focused");
// For remote browsers, the content document DOM tree is not accessible, thus
// the focused element should fall back to the browser element.
if (gBrowser.hasAttribute("remote")) {
is(document.commandDispatcher.focusedElement, gBrowser,
"Browser element isn't focused");
}
yield ContentTask.spawn(gBrowser, null, function* () {
Assert.equal(content.document.activeElement,
content.document.getElementById("input"), "Input Element isn't focused");
});
// Make sure "find again" doesn't focus the content element if focus
// isn't in the content document.
@ -77,10 +91,29 @@
gFindBar.onFindAgainCommand(false);
ok(textbox.hasAttribute("focused"),
"Focus was stolen from a chrome element");
finish();
}
function* enterStringIntoFindField(aString) {
for (let i = 0; i < aString.length; i++) {
let event = document.createEvent("KeyEvents");
let promise = new Promise(resolve => {
let listener = {
onFindResult: function() {
gFindBar.browser.finder.removeResultListener(listener);
resolve();
}
};
gFindBar.browser.finder.addResultListener(listener);
});
event.initKeyEvent("keypress", true, true, null, false, false,
false, false, 0, aString.charCodeAt(i));
gFindBar._findField.inputField.dispatchEvent(event);
yield promise;
}
}
]]></script>
<textbox id="textbox"/>
<browser type="content-primary" flex="1" id="content" src="about:blank"/>
<browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/>
<findbar id="FindToolbar" browserid="content"/>
</window>

View File

@ -68,6 +68,7 @@ skip-if = buildapp == 'mulet'
[test_bug331215.xul]
[test_bug360220.xul]
[test_bug360437.xul]
skip-if = os == 'linux' # Bug 1264604
[test_bug365773.xul]
[test_bug366992.xul]
[test_bug382990.xul]

View File

@ -5,6 +5,9 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet
href="chrome://mochikit/content/tests/SimpleTest/test.css"
type="text/css"?>
<window id="FindbarTest"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
@ -14,151 +17,163 @@
title="findbar events test">
<script type="application/javascript"><![CDATA[
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cr = Components.results;
const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://testing-common/ContentTask.jsm");
ContentTask.setTestScope(window.opener.wrappedJSObject);
var gFindBar = null;
var gBrowser;
const kTimeout = 5000; // 5 seconds.
var imports = ["SimpleTest", "ok", "is"];
var imports = ["SimpleTest", "ok", "is", "info"];
for (var name of imports) {
window[name] = window.opener.wrappedJSObject[name];
}
function finish() {
window.close();
SimpleTest.finish();
}
SimpleTest.requestLongerTimeout(2);
function startTest() {
gFindBar = document.getElementById("FindToolbar");
gBrowser = document.getElementById("content");
gBrowser.addEventListener("pageshow", onPageShow, false);
gBrowser.loadURI('data:text/html,hello there');
Task.spawn(function* () {
gFindBar = document.getElementById("FindToolbar");
for (let browserId of ["content", "content-remote"]) {
yield startTestWithBrowser(browserId);
}
}).then(() => {
window.close();
SimpleTest.finish();
});
}
var tests = [
testFind,
testFindAgain,
testCaseSensitivity,
testHighlight,
finish
];
// Iterates through the above tests and takes care of passing the done
// callback for any async tests.
function nextTest() {
if (!tests.length) {
return;
}
var func = tests.shift();
if (!func.length) {
// Test isn't async advance to the next test here.
func();
SimpleTest.executeSoon(nextTest);
} else {
func(nextTest);
}
function* startTestWithBrowser(browserId) {
info("Starting test with browser '" + browserId + "'");
gBrowser = document.getElementById(browserId);
gFindBar.browser = gBrowser;
let promise = ContentTask.spawn(gBrowser, null, function* () {
return new Promise(resolve => {
addEventListener("DOMContentLoaded", function listener() {
removeEventListener("DOMContentLoaded", listener);
resolve();
});
});
});
gBrowser.loadURI("data:text/html,hello there");
yield promise;
yield onDocumentLoaded();
}
function onPageShow() {
function* onDocumentLoaded() {
gFindBar.open();
gFindBar.onFindCommand();
nextTest();
yield testFind();
yield testFindAgain();
yield testCaseSensitivity();
yield testHighlight();
}
function checkSelection(done) {
SimpleTest.executeSoon(function() {
var selected = gBrowser.contentWindow.getSelection();
is(String(selected), "", "No text is selected");
function checkSelection() {
return new Promise(resolve => {
SimpleTest.executeSoon(() => {
ContentTask.spawn(gBrowser, null, function* () {
let selected = content.getSelection();
Assert.equal(String(selected), "", "No text is selected");
var controller = gFindBar.browser.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsISelectionDisplay)
.QueryInterface(Ci.nsISelectionController);
var selection = controller.getSelection(controller.SELECTION_FIND);
is(selection.rangeCount, 0, "No text is highlighted");
done();
let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsISelectionDisplay)
.QueryInterface(Ci.nsISelectionController);
let selection = controller.getSelection(controller.SELECTION_FIND);
Assert.equal(selection.rangeCount, 0, "No text is highlighted");
}).then(resolve);
});
});
}
function once(node, eventName, callback) {
node.addEventListener(eventName, function clb(e) {
node.removeEventListener(eventName, clb);
callback(e);
})
function once(node, eventName, preventDefault = true) {
return new Promise((resolve, reject) => {
let timeout = window.setTimeout(() => {
reject("Event wasn't fired within " + kTimeout + "ms for event '" +
eventName + "'.");
}, kTimeout);
node.addEventListener(eventName, function clb(e) {
window.clearTimeout(timeout);
node.removeEventListener(eventName, clb);
if (preventDefault)
e.preventDefault();
resolve(e);
});
});
}
function testFind(done) {
var eventTriggered = false;
var query = "t";
once(gFindBar, "find", function(e) {
eventTriggered = true;
ok(e.detail.query === query, "find event query should match '" + query + "'");
e.preventDefault();
// Since we're preventing the default make sure nothing was selected.
checkSelection(done);
});
function* testFind() {
info("Testing normal find.");
let query = "t";
let promise = once(gFindBar, "find");
// Put some text in the find box.
var event = document.createEvent("KeyEvents");
let event = document.createEvent("KeyEvents");
event.initKeyEvent("keypress", true, true, null, false, false,
false, false, 0, query.charCodeAt(0));
gFindBar._findField.inputField.dispatchEvent(event);
ok(eventTriggered, "find event should be triggered");
let e = yield promise;
ok(e.detail.query === query, "find event query should match '" + query + "'");
// Since we're preventing the default make sure nothing was selected.
yield checkSelection();
}
function testFindAgain(done) {
var eventTriggered = false;
once(gFindBar, "findagain", function(e) {
eventTriggered = true;
e.preventDefault();
// Since we're preventing the default make sure nothing was selected.
checkSelection(done);
});
function testFindAgain() {
info("Testing repeating normal find.");
let promise = once(gFindBar, "findagain");
gFindBar.onFindAgainCommand();
ok(eventTriggered, "findagain event should be triggered");
yield promise;
// Since we're preventing the default make sure nothing was selected.
yield checkSelection();
}
function testCaseSensitivity() {
var eventTriggered = false;
once(gFindBar, "findcasesensitivitychange", function(e) {
eventTriggered = true;
ok(e.detail.caseSensitive, "find should be case sensitive");
});
function* testCaseSensitivity() {
info("Testing normal case sensitivity.");
let promise = once(gFindBar, "findcasesensitivitychange", false);
var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive");
let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive");
matchCaseCheckbox.click();
let e = yield promise;
ok(e.detail.caseSensitive, "find should be case sensitive");
// Toggle it back to the original setting.
matchCaseCheckbox.click();
ok(eventTriggered, "findcasesensitivitychange should be triggered");
// Changing case sensitivity does the search so clear the selected text
// before the next test.
gBrowser.contentWindow.getSelection().removeAllRanges();
yield ContentTask.spawn(gBrowser, null, () => content.getSelection().removeAllRanges());
}
function testHighlight(done) {
function* testHighlight() {
info("Testing find with highlight all.");
// Update the find state so the highlight button is clickable.
gFindBar.updateControlState(Ci.nsITypeAheadFind.FIND_FOUND, false);
var eventTriggered = false;
once(gFindBar, "findhighlightallchange", function(e) {
eventTriggered = true;
ok(e.detail.highlightAll, "find event should have highlight all set");
e.preventDefault();
// Since we're preventing the default make sure nothing was highlighted.
SimpleTest.executeSoon(function() {
checkSelection(done);
});
});
var highlightButton = gFindBar.getElement("highlight");
if (!highlightButton.checked) {
let promise = once(gFindBar, "findhighlightallchange");
let highlightButton = gFindBar.getElement("highlight");
if (!highlightButton.checked)
highlightButton.click();
let e = yield promise;
ok(e.detail.highlightAll, "find event should have highlight all set");
// Since we're preventing the default make sure nothing was highlighted.
yield checkSelection();
// Toggle it back to the original setting.
if (highlightButton.checked)
highlightButton.click();
}
ok(eventTriggered, "findhighlightallchange should be triggered");
}
]]></script>
<browser type="content-primary" flex="1" id="content" src="about:blank"/>
<browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/>
<findbar id="FindToolbar" browserid="content"/>
</window>

View File

@ -105,6 +105,21 @@ function getCtorName(aObj) {
return Object.prototype.toString.call(aObj).slice(8, -1);
}
/**
* Indicates whether an object is a JS or `Components.Exception` error.
*
* @param {object} aThing
The object to check
* @return {boolean}
Is this object an error?
*/
function isError(aThing) {
return aThing && (
(typeof aThing.name == "string" &&
aThing.name.startsWith("NS_ERROR_")) ||
getCtorName(aThing).endsWith("Error"));
}
/**
* A single line stringification of an object designed for use by humans
*
@ -124,6 +139,10 @@ function stringify(aThing, aAllowNewLines) {
return "null";
}
if (isError(aThing)) {
return "Message: " + aThing;
}
if (typeof aThing == "object") {
let type = getCtorName(aThing);
if (aThing instanceof Components.interfaces.nsIDOMNode && aThing.tagName) {
@ -203,9 +222,7 @@ function log(aThing) {
i++;
}
}
else if (type.match("Error$") ||
(typeof aThing.name == "string" &&
aThing.name.match("NS_ERROR_"))) {
else if (isError(aThing)) {
reply += " Message: " + aThing + "\n";
if (aThing.stack) {
reply += " Stack:\n";

View File

@ -3842,6 +3842,7 @@ GetManifestContents(const NS_tchar *manifest)
size_t c = fread(rb, 1, count, mfile);
if (c != count) {
LOG(("GetManifestContents: error reading manifest file: " LOG_S, manifest));
free(mbuf);
return nullptr;
}
@ -3855,8 +3856,10 @@ GetManifestContents(const NS_tchar *manifest)
return rb;
#else
NS_tchar *wrb = (NS_tchar *) malloc((ms.st_size + 1) * sizeof(NS_tchar));
if (!wrb)
if (!wrb) {
free(mbuf);
return nullptr;
}
if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, rb, -1, wrb,
ms.st_size + 1)) {