merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2015-07-13 11:39:17 +02:00
commit e09f5bb061
102 changed files with 1990 additions and 845 deletions

View File

@ -404,8 +404,13 @@ pref("browser.search.order.1", "chrome://browser-region/locale/re
pref("browser.search.order.2", "chrome://browser-region/locale/region.properties");
pref("browser.search.order.3", "chrome://browser-region/locale/region.properties");
// Market-specific search defaults (US market only)
pref("browser.search.geoSpecificDefaults", true);
// Market-specific search defaults
// This is disabled globally, and then enabled for individual locales
// in firefox-l10n.js (eg. it's enabled for en-US).
pref("browser.search.geoSpecificDefaults", false);
pref("browser.search.geoSpecificDefaults.url", "");
// US specific default (used as a fallback if the geoSpecificDefaults request fails).
pref("browser.search.defaultenginename.US", "data:text/plain,browser.search.defaultenginename.US=Yahoo");
pref("browser.search.order.US.1", "data:text/plain,browser.search.order.US.1=Yahoo");
pref("browser.search.order.US.2", "data:text/plain,browser.search.order.US.2=Google");
@ -1359,7 +1364,6 @@ pref("services.sync.prefs.sync.xpinstall.whitelist.required", true);
// Developer edition preferences
#ifdef MOZ_DEV_EDITION
sticky_pref("lightweightThemes.selectedThemeID", "firefox-devedition@mozilla.org");
sticky_pref("browser.devedition.theme.enabled", true);
#else
sticky_pref("lightweightThemes.selectedThemeID", "");
#endif

View File

@ -53,8 +53,10 @@ function test() {
ok(data.days.hasDay(now), "Have data for today.");
let day = data.days.getDay(now);
// Will need to be changed if Yahoo isn't the default search engine.
let defaultProviderID = "yahoo";
// Will need to be changed if Google isn't the default search engine.
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
// non-US en-US default.
let defaultProviderID = "google";
let field = defaultProviderID + ".contextmenu";
ok(day.has(field), "Have search recorded for context menu.");
@ -66,4 +68,3 @@ function test() {
});
});
}

View File

@ -1,8 +1,8 @@
/*
* Testing changes for Developer Edition theme.
* A special stylesheet should be added to the browser.xul document
* when browser.devedition.theme.enabled is set to true and no themes
* are applied.
* when the firefox-devedition@mozilla.org lightweight theme
* is applied.
*/
const PREF_LWTHEME_USED_THEMES = "lightweightThemes.usedThemes";

View File

@ -9,6 +9,7 @@
// * A page with tracking elements is loaded and they are not blocked.
// See also Bugs 1175327, 1043801, 1178985
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const PREF = "privacy.trackingprotection.enabled";
const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html";
@ -16,8 +17,11 @@ const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/
let TrackingProtection = null;
let browser = null;
let {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
registerCleanupFunction(function() {
TrackingProtection = browser = null;
UrlClassifierTestUtils.cleanupTestTrackers();
Services.prefs.clearUserPref(PREF);
Services.prefs.clearUserPref(PB_PREF);
while (gBrowser.tabs.length > 1) {
@ -108,7 +112,7 @@ function* testTrackingProtectionForTab(tab) {
}
add_task(function* testNormalBrowsing() {
yield updateTrackingProtectionDatabase();
yield UrlClassifierTestUtils.addTestTrackers();
browser = gBrowser;
let tab = browser.selectedTab = browser.addTab();

View File

@ -6,6 +6,7 @@
// Control Center when the feature is off.
// See also Bugs 1175327, 1043801, 1178985.
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const PREF = "privacy.trackingprotection.enabled";
const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/benignPage.html";
@ -13,8 +14,11 @@ const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/
let TrackingProtection = null;
let browser = null;
let {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
registerCleanupFunction(function() {
TrackingProtection = browser = null;
UrlClassifierTestUtils.cleanupTestTrackers();
Services.prefs.clearUserPref(PREF);
Services.prefs.clearUserPref(PB_PREF);
while (gBrowser.tabs.length > 1) {
@ -23,7 +27,7 @@ registerCleanupFunction(function() {
});
add_task(function* testNormalBrowsing() {
yield updateTrackingProtectionDatabase();
yield UrlClassifierTestUtils.addTestTrackers();
browser = gBrowser;
let tab = browser.selectedTab = browser.addTab();

View File

@ -27,8 +27,10 @@ add_task(function* test_healthreport_search_recording() {
let now = new Date();
let oldCount = 0;
// This will to be need changed if default search engine is not Yahoo.
let defaultEngineID = "yahoo";
// This will to be need changed if default search engine is not Google.
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
// non-US en-US default.
let defaultEngineID = "google";
let field = defaultEngineID + ".urlbar";

View File

@ -660,55 +660,6 @@ function promiseIndicatorWindow() {
return promiseWindow("chrome://browser/content/webrtcIndicator.xul");
}
/**
* Add some entries to a test tracking protection database, and reset
* back to the default database after the test ends.
*/
function updateTrackingProtectionDatabase() {
let TABLE = "urlclassifier.trackingTable";
Services.prefs.setCharPref(TABLE, "test-track-simple");
registerCleanupFunction(function() {
Services.prefs.clearUserPref(TABLE);
});
// Add some URLs to the tracking database (to be blocked)
let testData = "tracking.example.com/";
let testUpdate =
"n:1000\ni:test-track-simple\nad:1\n" +
"a:524:32:" + testData.length + "\n" +
testData;
return new Promise((resolve, reject) => {
let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
.getService(Ci.nsIUrlClassifierDBService);
let listener = {
QueryInterface: iid => {
if (iid.equals(Ci.nsISupports) ||
iid.equals(Ci.nsIUrlClassifierUpdateObserver))
return listener;
throw Cr.NS_ERROR_NO_INTERFACE;
},
updateUrlRequested: url => { },
streamFinished: status => { },
updateError: errorCode => {
ok(false, "Couldn't update classifier.");
resolve();
},
updateSuccess: requestedTimeout => {
resolve();
}
};
dbService.beginUpdate(listener, "test-track-simple", "");
dbService.beginStream("", "");
dbService.updateStream(testUpdate);
dbService.finishStream();
dbService.finishUpdate();
});
}
function assertWebRTCIndicatorStatus(expected) {
let ui = Cu.import("resource:///modules/webrtcUI.jsm", {}).webrtcUI;
let expectedState = expected ? "visible" : "hidden";

View File

@ -216,17 +216,16 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
if (!this._formattingEnabled || !this.editor)
return;
// Always clear the strike-out selection first.
let controller = this.editor.selectionController;
let strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT);
strikeOut.removeAllRanges();
if (this.focused)
return;
let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
selection.removeAllRanges();
if (this.focused)
return;
let textNode = this.editor.rootElement.firstChild;
let value = textNode.textContent;
@ -286,17 +285,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
]]></body>
</method>
<method name="_clearFormatting">
<body><![CDATA[
if (!this._formattingEnabled)
return;
let controller = this.editor.selectionController;
let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
selection.removeAllRanges();
]]></body>
</method>
<method name="handleRevert">
<body><![CDATA[
var isScrolling = this.popupOpen;
@ -953,6 +941,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
<handler event="focus"><![CDATA[
if (event.originalTarget == this.inputField) {
this._hideURLTooltip();
this.formatValue();
}
]]></handler>
@ -991,11 +980,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
event.stopPropagation();
]]></handler>
<handler event="focus" phase="capturing"><![CDATA[
this._hideURLTooltip();
this._clearFormatting();
]]></handler>
<handler event="dragover" phase="capturing" action="this.onDragOver(event, this);"/>
<handler event="drop" phase="capturing" action="this.onDrop(event, this);"/>
<handler event="select"><![CDATA[

View File

@ -19,7 +19,7 @@ Our views use [React](http://facebook.github.io/react/) written in JSX files
and transpiled to JS before we commit. You need to install the JSX compiler
using npm in order to compile the .jsx files into regular .js ones:
npm install -g react-tools
npm install -g react-tools@0.12.2
Once installed, run build-jsx with the --watch option from
browser/components/loop, eg.:

View File

@ -7,7 +7,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
Components.utils.import("resource://testing-common/TelemetryArchiveTesting.jsm", this);
function test() {

View File

@ -7,8 +7,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}

View File

@ -7,9 +7,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
requestLongerTimeout(2);
function test() {

View File

@ -13,8 +13,6 @@ let gContentWindow;
let highlight = document.getElementById("UITourHighlightContainer");
let tooltip = document.getElementById("UITourTooltip");
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}

View File

@ -7,8 +7,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
let hasWebIDE = Services.prefs.getBoolPref("devtools.webide.widget.enabled");
let hasPocket = false;

View File

@ -12,8 +12,6 @@ let gContentAPI;
let gContentWindow;
let gContentDoc;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
registerCleanupFunction(function() {
gContentDoc = null;

View File

@ -4,8 +4,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}

View File

@ -8,8 +8,6 @@ let gContentAPI;
let gContentWindow;
let notificationBox = document.getElementById("high-priority-global-notificationbox");
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}

View File

@ -9,7 +9,6 @@ let gContentWindow;
let loopButton;
let loopPanel = document.getElementById("loop-notification-panel");
Components.utils.import("resource:///modules/UITour.jsm");
const { LoopRooms } = Components.utils.import("resource:///modules/loop/LoopRooms.jsm", {});
const { MozLoopServiceInternal } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});

View File

@ -74,8 +74,6 @@ function getDialogDoc() {
return null;
}
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}

View File

@ -4,8 +4,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
requestLongerTimeout(2);
UITourTest();

View File

@ -13,8 +13,6 @@ let gContentWindow;
let highlight = document.getElementById("UITourHighlight");
let tooltip = document.getElementById("UITourTooltip");
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
registerCleanupFunction(() => {
// Close the find bar in case it's open in the remaining tab

View File

@ -8,8 +8,6 @@ let gContentAPI;
let gContentWindow;
let button;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}

View File

@ -7,7 +7,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
Components.utils.import("resource://gre/modules/UITelemetry.jsm");
Components.utils.import("resource:///modules/BrowserUITelemetry.jsm");

View File

@ -7,8 +7,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}

View File

@ -7,8 +7,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
registerCleanupFunction(function() {
Services.prefs.clearUserPref("services.sync.username");

View File

@ -4,8 +4,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}

View File

@ -4,8 +4,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function test() {
requestLongerTimeout(2);
UITourTest();

View File

@ -9,8 +9,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Cu.import("resource:///modules/UITour.jsm");
function test() {
UITourTest();
}

View File

@ -7,9 +7,6 @@ let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
function test() {
UITourTest();
}

View File

@ -204,8 +204,6 @@ function UITourTest() {
waitForExplicitFinish();
registerCleanupFunction(function() {
delete window.UITour;
delete window.UITourMetricsProvider;
delete window.gContentWindow;
delete window.gContentAPI;
if (gTestTab)

View File

@ -1886,6 +1886,12 @@ EventListeners.prototype = {
definitionSite = fetchedDefinitions.get(listener.function.actor);
} else if (listener.function.class == "Function") {
definitionSite = yield this._getDefinitionSite(listener.function);
if (!definitionSite) {
// We don't know where this listener comes from so don't show it in
// the UI as breaking on it doesn't work (bug 942899).
continue;
}
fetchedDefinitions.set(listener.function.actor, definitionSite);
}
listener.function.url = definitionSite;
@ -1926,8 +1932,10 @@ EventListeners.prototype = {
// Don't make this error fatal, because it would break the entire events pane.
const msg = "Error getting function definition site: " + aResponse.message;
DevToolsUtils.reportException("_getDefinitionSite", msg);
deferred.resolve(null);
} else {
deferred.resolve(aResponse.source.url);
}
deferred.resolve(aResponse.source.url);
});
return deferred.promise;

View File

@ -235,6 +235,8 @@ skip-if = e10s && debug
skip-if = e10s && debug
[browser_dbg_event-listeners-03.js]
skip-if = e10s && debug
[browser_dbg_event-listeners-04.js]
skip-if = debug || e10s # debug bug 1142597, e10s bug 1146603.
[browser_dbg_file-reload.js]
skip-if = e10s && debug
[browser_dbg_function-display-name.js]
@ -351,7 +353,7 @@ skip-if = e10s && debug
[browser_dbg_promises-allocation-stack.js]
skip-if = e10s && debug
[browser_dbg_promises-chrome-allocation-stack.js]
skip-if = e10s && debug
skip-if = (e10s || os == "linux") && debug
[browser_dbg_reload-preferred-script-01.js]
skip-if = e10s && debug
[browser_dbg_reload-preferred-script-02.js]

View File

@ -0,0 +1,43 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that event listeners are properly fetched even if one of the listeners
* don't have a Debugger.Source object (bug 942899).
*
* This test is skipped on debug and e10s builds for following reasons:
* - debug: requiring sdk/tabs causes memory leaks when new windows are opened
* in tests executed after this one. Bug 1142597.
* - e10s: tab.attach is not e10s safe and only works when add-on compatibility
* shims are in place. Bug 1146603.
*/
const TAB_URL = EXAMPLE_URL + "doc_event-listeners-01.html";
add_task(function* () {
let tab = yield addTab(TAB_URL);
// Create a sandboxed content script the Add-on SDK way. Inspired by bug
// 1145996.
let tabs = require('sdk/tabs');
let sdkTab = [...tabs].find(tab => tab.url === TAB_URL);
ok(sdkTab, "Add-on SDK found the loaded tab.");
info("Attaching an event handler via add-on sdk content scripts.");
let worker = sdkTab.attach({
contentScript: "document.body.addEventListener('click', e => alert(e))",
onError: ok.bind(this, false)
});
let [,, panel, win] = yield initDebugger(tab);
let gDebugger = panel.panelWin;
let fetched = waitForDebuggerEvents(panel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
info("Scheduling event listener fetch.");
gDebugger.DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
info("Waiting for updated event listeners to arrive.");
yield fetched;
ok(true, "The listener update did not hang.");
});

View File

@ -34,6 +34,9 @@ support-files =
[browser_filter-editor-08.js]
[browser_filter-editor-09.js]
[browser_filter-editor-10.js]
[browser_filter-presets-01.js]
[browser_filter-presets-02.js]
[browser_filter-presets-03.js]
[browser_flame-graph-01.js]
[browser_flame-graph-02.js]
[browser_flame-graph-03a.js]

View File

@ -68,7 +68,7 @@ add_task(function*() {
widget.setCssValue(cssValue);
if (cssValue === "none") {
const text = container.querySelector(".filters").textContent;
const text = container.querySelector("#filters").textContent;
ok(text.indexOf(L10N.getStr("emptyFilterList")) > -1,
"Contains |emptyFilterList| string when given value 'none'");
ok(text.indexOf(L10N.getStr("addUsingList")) > -1,

View File

@ -17,7 +17,7 @@ add_task(function*() {
const initialValue = "blur(2px) contrast(200%) brightness(200%)";
let widget = new CSSFilterEditorWidget(container, initialValue);
const filters = widget.el.querySelector(".filters");
const filters = widget.el.querySelector("#filters");
function first() {
return filters.children[0];
}
@ -28,6 +28,7 @@ add_task(function*() {
return filters.children[2];
}
info("Test re-ordering neighbour filters");
widget._mouseDown({
target: first().querySelector("i"),
@ -67,7 +68,7 @@ add_task(function*() {
pageY: 0
});
widget._mouseMove({ pageY: -LIST_ITEM_HEIGHT * 5 });
ok(first().offsetTop >= boundaries.top,
ok(first().getBoundingClientRect().top >= boundaries.top,
"First filter should not move outside filter list");
widget._mouseUp();
@ -78,7 +79,7 @@ add_task(function*() {
pageY: 0
});
widget._mouseMove({ pageY: -LIST_ITEM_HEIGHT * 5 });
ok(last().offsetTop <= boundaries.bottom,
ok(last().getBoundingClientRect().bottom <= boundaries.bottom,
"Last filter should not move outside filter list");
widget._mouseUp();

View File

@ -22,7 +22,7 @@ add_task(function*() {
const container = doc.querySelector("#container");
let widget = new CSSFilterEditorWidget(container, "grayscale(0%) url(test.svg)");
const filters = widget.el.querySelector(".filters");
const filters = widget.el.querySelector("#filters");
const grayscale = filters.children[0],
url = filters.children[1];

View File

@ -22,7 +22,7 @@ add_task(function*() {
let widget = new CSSFilterEditorWidget(container, "none");
const select = widget.el.querySelector("select"),
add = widget.el.querySelector("button");
add = widget.el.querySelector("#add-filter");
const TEST_DATA = [
{
@ -62,7 +62,8 @@ add_task(function*() {
is(widget.getValueAt(index), `0${filter.unit}`,
`Should add ${filter.unit} to ${filter.type} filters`);
} else if (filter.placeholder) {
const input = widget.el.querySelector(`.filter:nth-child(${index + 1}) input`);
let i = index + 1;
const input = widget.el.querySelector(`.filter:nth-child(${i}) input`);
is(input.placeholder, filter.placeholder,
"Should set the appropriate placeholder for string-type filters");
}

View File

@ -70,7 +70,7 @@ add_task(function*() {
// Triggers the specified keyCode and modifier key on
// first filter's input
function triggerKey(key, modifier) {
const filter = this.el.querySelector(".filters").children[0];
const filter = this.el.querySelector("#filters").children[0];
const input = filter.querySelector("input");
this._keyDown({

View File

@ -20,7 +20,7 @@ add_task(function*() {
const container = doc.querySelector("#container");
const initialValue = "drop-shadow(rgb(0, 0, 0) 1px 1px 0px)";
let widget = new CSSFilterEditorWidget(container, initialValue);
widget.el.querySelector("input").setSelectionRange(13, 13);
widget.el.querySelector("#filters input").setSelectionRange(13, 13);
let value = 1;
@ -102,7 +102,7 @@ add_task(function*() {
// Triggers the specified keyCode and modifier key on
// first filter's input
function triggerKey(key, modifier) {
const filter = this.el.querySelector(".filters").children[0];
const filter = this.el.querySelector("#filters").children[0];
const input = filter.querySelector("input");
this._keyDown({

View File

@ -20,7 +20,7 @@ add_task(function*() {
const container = doc.querySelector("#container");
const initialValue = "drop-shadow(rgb(0, 0, 0) 10px 1px 0px)";
let widget = new CSSFilterEditorWidget(container, initialValue);
const input = widget.el.querySelector("input");
const input = widget.el.querySelector("#filters input");
let value = 10;
@ -66,7 +66,7 @@ add_task(function*() {
// Triggers the specified keyCode and modifier key on
// first filter's input
function triggerKey(key, modifier) {
const filter = this.el.querySelector(".filters").children[0];
const filter = this.el.querySelector("#filters").children[0];
const input = filter.querySelector("input");
this._keyDown({

View File

@ -0,0 +1,98 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests saving presets
const TEST_URI = "chrome://browser/content/devtools/filter-frame.xhtml";
const {CSSFilterEditorWidget} = require("devtools/shared/widgets/FilterWidget");
add_task(function* () {
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", TEST_URI);
const container = doc.querySelector("#container");
let widget = new CSSFilterEditorWidget(container, "none");
// First render
yield widget.once("render");
const VALUE = "blur(2px) contrast(150%)";
const NAME = "Test";
yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
let preset = widget.el.querySelector(".preset");
is(preset.querySelector("label").textContent, NAME,
"Should show preset name correctly");
is(preset.querySelector("span").textContent, VALUE,
"Should show preset value preview correctly");
let list = yield widget.getPresets();
let input = widget.el.querySelector(".presets-list .footer input");
let data = list[0];
is(data.name, NAME,
"Should add the preset to asyncStorage - name property");
is(data.value, VALUE,
"Should add the preset to asyncStorage - name property");
info("Test overriding preset by using the same name");
const VALUE_2 = "saturate(50%) brightness(10%)";
widget.setCssValue(VALUE_2);
yield savePreset(widget);
is(widget.el.querySelectorAll(".preset").length, 1,
"Should override the preset with the same name - render");
list = yield widget.getPresets();
data = list[0];
is(list.length, 1,
"Should override the preset with the same name - asyncStorage");
is(data.name, NAME,
"Should override the preset with the same name - prop name");
is(data.value, VALUE_2,
"Should override the preset with the same name - prop value");
yield widget.setPresets([]);
info("Test saving a preset without name");
input.value = "";
yield savePreset(widget, "preset-save-error");
list = yield widget.getPresets();
is(list.length, 0,
"Should not add a preset without name");
info("Test saving a preset without filters");
input.value = NAME;
widget.setCssValue("none");
yield savePreset(widget, "preset-save-error");
list = yield widget.getPresets();
is(list.length, 0,
"Should not add a preset without filters (value: none)");
});
/**
* Call savePreset on widget and wait for the specified event to emit
* @param {CSSFilterWidget} widget
* @param {string} expectEvent="render" The event to listen on
* @return {Promise}
*/
function savePreset(widget, expectEvent = "render") {
let onEvent = widget.once(expectEvent);
widget._savePreset({
preventDefault: () => {},
});
return onEvent;
}

View File

@ -0,0 +1,44 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests loading presets
const TEST_URI = "chrome://browser/content/devtools/filter-frame.xhtml";
const {CSSFilterEditorWidget} = require("devtools/shared/widgets/FilterWidget");
add_task(function* () {
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", TEST_URI);
const container = doc.querySelector("#container");
let widget = new CSSFilterEditorWidget(container, "none");
// First render
yield widget.once("render");
const VALUE = "blur(2px) contrast(150%)";
const NAME = "Test";
yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
let onRender = widget.once("render");
// reset value
widget.setCssValue("saturate(100%) brightness(150%)");
yield onRender;
let preset = widget.el.querySelector(".preset");
onRender = widget.once("render");
widget._presetClick({
target: preset
});
yield onRender;
is(widget.getCssValue(), VALUE,
"Should set widget's value correctly");
is(widget.el.querySelector(".presets-list .footer input").value, NAME,
"Should set input's value to name");
});

View File

@ -0,0 +1,39 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests deleting presets
const TEST_URI = "chrome://browser/content/devtools/filter-frame.xhtml";
const {CSSFilterEditorWidget} = require("devtools/shared/widgets/FilterWidget");
add_task(function* () {
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", TEST_URI);
const container = doc.querySelector("#container");
let widget = new CSSFilterEditorWidget(container, "none");
// First render
yield widget.once("render");
const NAME = "Test";
const VALUE = "blur(2px) contrast(150%)";
yield showFilterPopupPresetsAndCreatePreset(widget, NAME, VALUE);
let removeButton = widget.el.querySelector(".preset .remove-button");
let onRender = widget.once("render");
widget._presetClick({
target: removeButton
});
yield onRender;
is(widget.el.querySelector(".preset"), null,
"Should re-render after removing preset");
let list = yield widget.getPresets();
is(list.length, 0,
"Should remove presets from asyncStorage");
});

View File

@ -283,3 +283,39 @@ function waitUntil(predicate, interval = 10) {
}, interval);
});
}
/**
* Show the presets list sidebar in the cssfilter widget popup
* @param {CSSFilterWidget} widget
* @return {Promise}
*/
function showFilterPopupPresets(widget) {
let onRender = widget.once("render");
widget._togglePresets();
return onRender;
}
/**
* Show presets list and create a sample preset with the name and value provided
* @param {CSSFilterWidget} widget
* @param {string} name
* @param {string} value
* @return {Promise}
*/
let showFilterPopupPresetsAndCreatePreset = Task.async(function*(widget, name, value) {
yield showFilterPopupPresets(widget);
let onRender = widget.once("render");
widget.setCssValue(value);
yield onRender;
let footer = widget.el.querySelector(".presets-list .footer");
footer.querySelector("input").value = name;
onRender = widget.once("render");
widget._savePreset({
preventDefault: () => {}
});
yield onRender;
});

View File

@ -16,6 +16,9 @@ const STRINGS_URI = "chrome://browser/locale/devtools/filterwidget.properties";
const L10N = new ViewHelpers.L10N(STRINGS_URI);
const {cssTokenizer} = require("devtools/sourceeditor/css-tokenizer");
loader.lazyGetter(this, "asyncStorage",
() => require("devtools/toolkit/shared/async-storage"));
const DEFAULT_FILTER_TYPE = "length";
const UNIT_MAPPING = {
percentage: "%",
@ -116,31 +119,54 @@ function CSSFilterEditorWidget(el, value = "") {
this._mouseDown = this._mouseDown.bind(this);
this._keyDown = this._keyDown.bind(this);
this._input = this._input.bind(this);
this._presetClick = this._presetClick.bind(this);
this._savePreset = this._savePreset.bind(this);
this._togglePresets = this._togglePresets.bind(this);
// Passed to asyncStorage, requires binding
this.renderPresets = this.renderPresets.bind(this);
this._initMarkup();
this._buildFilterItemMarkup();
this._buildPresetItemMarkup();
this._addEventListeners();
EventEmitter.decorate(this);
this.filters = [];
this.setCssValue(value);
this.renderPresets();
}
exports.CSSFilterEditorWidget = CSSFilterEditorWidget;
CSSFilterEditorWidget.prototype = {
_initMarkup: function() {
const list = this.el.querySelector(".filters");
this.el.appendChild(list);
this.el.insertBefore(list, this.el.firstChild);
this.container = this.el;
this.list = list;
this.filtersList = this.el.querySelector("#filters");
this.presetsList = this.el.querySelector("#presets");
this.togglePresets = this.el.querySelector("#toggle-presets");
this.filterSelect = this.el.querySelector("select");
this.addPresetButton = this.el.querySelector(".presets-list .add");
this.addPresetInput = this.el.querySelector(".presets-list .footer input");
this.el.querySelector(".presets-list input").value = "";
this._populateFilterSelect();
},
_destroyMarkup: function() {
this._filterItemMarkup.remove();
this.el.remove();
this.el = this.filtersList = this._filterItemMarkup = null;
this.presetsList = this.togglePresets = this.filterSelect = null;
this.addPresetButton = null;
},
destroy: function() {
this._removeEventListeners();
this._destroyMarkup();
},
/**
* Creates <option> elements for each filter definition
* in filterList
@ -184,20 +210,42 @@ CSSFilterEditorWidget.prototype = {
let removeButton = this.doc.createElement("button");
removeButton.className = "remove-button";
value.appendChild(removeButton);
base.appendChild(name);
base.appendChild(value);
base.appendChild(removeButton);
this._filterItemMarkup = base;
},
_buildPresetItemMarkup: function() {
let base = this.doc.createElement("div");
base.classList.add("preset");
let name = this.doc.createElement("label");
base.appendChild(name);
let value = this.doc.createElement("span");
base.appendChild(value);
let removeButton = this.doc.createElement("button");
removeButton.classList.add("remove-button");
base.appendChild(removeButton);
this._presetItemMarkup = base;
},
_addEventListeners: function() {
this.addButton = this.el.querySelector("#add-filter");
this.addButton.addEventListener("click", this._addButtonClick);
this.list.addEventListener("click", this._removeButtonClick);
this.list.addEventListener("mousedown", this._mouseDown);
this.list.addEventListener("keydown", this._keyDown);
this.filtersList.addEventListener("click", this._removeButtonClick);
this.filtersList.addEventListener("mousedown", this._mouseDown);
this.filtersList.addEventListener("keydown", this._keyDown);
this.presetsList.addEventListener("click", this._presetClick);
this.togglePresets.addEventListener("click", this._togglePresets);
this.addPresetButton.addEventListener("click", this._savePreset);
// These events are event delegators for
// drag-drop re-ordering and label-dragging
@ -205,11 +253,29 @@ CSSFilterEditorWidget.prototype = {
this.win.addEventListener("mouseup", this._mouseUp);
// Used to workaround float-precision problems
this.list.addEventListener("input", this._input);
this.filtersList.addEventListener("input", this._input);
},
_removeEventListeners: function() {
this.addButton.removeEventListener("click", this._addButtonClick);
this.filtersList.removeEventListener("click", this._removeButtonClick);
this.filtersList.removeEventListener("mousedown", this._mouseDown);
this.filtersList.removeEventListener("keydown", this._keyDown);
this.presetsList.removeEventListener("click", this._presetClick);
this.togglePresets.removeEventListener("click", this._togglePresets);
this.addPresetButton.removeEventListener("click", this._savePreset);
// These events are used for drag drop re-ordering
this.win.removeEventListener("mousemove", this._mouseMove);
this.win.removeEventListener("mouseup", this._mouseUp);
// Used to workaround float-precision problems
this.filtersList.removeEventListener("input", this._input);
},
_getFilterElementIndex: function(el) {
return [...this.list.children].indexOf(el);
return [...this.filtersList.children].indexOf(el);
},
_keyDown: function(e) {
@ -297,7 +363,7 @@ CSSFilterEditorWidget.prototype = {
filterEl.startingY = e.pageY;
filterEl.classList.add("dragging");
this.container.classList.add("dragging");
this.el.classList.add("dragging");
// label-dragging
} else if (e.target.classList.contains("devtools-draglabel")) {
let label = e.target,
@ -357,7 +423,7 @@ CSSFilterEditorWidget.prototype = {
},
_dragFilterElement: function(e) {
const rect = this.list.getBoundingClientRect(),
const rect = this.filtersList.getBoundingClientRect(),
top = e.pageY - LIST_PADDING,
bottom = e.pageY + LIST_PADDING;
// don't allow dragging over top/bottom of list
@ -365,7 +431,7 @@ CSSFilterEditorWidget.prototype = {
return;
}
const filterEl = this.list.querySelector(".dragging");
const filterEl = this.filtersList.querySelector(".dragging");
const delta = e.pageY - filterEl.startingY;
filterEl.style.top = delta + "px";
@ -376,7 +442,7 @@ CSSFilterEditorWidget.prototype = {
change = change > 0 ? Math.floor(change) :
change < 0 ? Math.ceil(change) : change;
const children = this.list.children;
const children = this.filtersList.children;
const index = [...children].indexOf(filterEl);
const destination = index + change;
@ -392,9 +458,9 @@ CSSFilterEditorWidget.prototype = {
const target = change > 0 ? children[destination + 1]
: children[destination];
if (target) {
this.list.insertBefore(filterEl, target);
this.filtersList.insertBefore(filterEl, target);
} else {
this.list.appendChild(filterEl);
this.filtersList.appendChild(filterEl);
}
filterEl.removeAttribute("style");
@ -442,30 +508,83 @@ CSSFilterEditorWidget.prototype = {
if (!this.isReorderingFilter) {
return;
}
let filterEl = this.list.querySelector(".dragging");
let filterEl = this.filtersList.querySelector(".dragging");
this.isReorderingFilter = false;
filterEl.classList.remove("dragging");
this.container.classList.remove("dragging");
this.el.classList.remove("dragging");
filterEl.removeAttribute("style");
this.emit("updated", this.getCssValue());
this.render();
},
_presetClick: function(e) {
let el = e.target;
let preset = el.closest(".preset");
if (!preset) {
return;
}
let id = +preset.dataset.id;
this.getPresets().then(presets => {
if (el.classList.contains("remove-button")) {
// If the click happened on the remove button.
presets.splice(id, 1);
this.setPresets(presets).then(this.renderPresets, Cu.reportError);
} else {
// Or if the click happened on a preset.
let p = presets[id];
this.setCssValue(p.value);
this.addPresetInput.value = p.name;
}
}, Cu.reportError);
},
_togglePresets: function() {
this.el.classList.toggle("show-presets");
this.emit("render");
},
_savePreset: function(e) {
e.preventDefault();
let name = this.addPresetInput.value,
value = this.getCssValue();
if (!name || !value || value === "none") {
this.emit("preset-save-error");
return;
}
this.getPresets().then(presets => {
let index = presets.findIndex(preset => preset.name === name);
if (index > -1) {
presets[index].value = value;
} else {
presets.push({name, value});
}
this.setPresets(presets).then(this.renderPresets, Cu.reportError);
}, Cu.reportError);
},
/**
* Clears the list and renders filters, binding required events.
* There are some delegated events bound in _addEventListeners method
*/
render: function() {
if (!this.filters.length) {
this.list.innerHTML = `<p> ${L10N.getStr("emptyFilterList")} <br />
this.filtersList.innerHTML = `<p> ${L10N.getStr("emptyFilterList")} <br />
${L10N.getStr("addUsingList")} </p>`;
this.emit("render");
return;
}
this.list.innerHTML = "";
this.filtersList.innerHTML = "";
let base = this._filterItemMarkup;
@ -515,19 +634,48 @@ CSSFilterEditorWidget.prototype = {
unitPreview.remove();
}
this.list.appendChild(el);
this.filtersList.appendChild(el);
}
let el = this.list.querySelector(`.filter:last-of-type input`);
if (el) {
el.focus();
let lastInput = this.filtersList.querySelector(`.filter:last-of-type input`);
if (lastInput) {
lastInput.focus();
// move cursor to end of input
el.setSelectionRange(el.value.length, el.value.length);
const end = lastInput.value.length;
lastInput.setSelectionRange(end, end);
}
this.emit("render");
},
renderPresets: function() {
this.getPresets().then(presets => {
if (!presets || !presets.length) {
this.presetsList.innerHTML = `<p>${L10N.getStr("emptyPresetList")}</p>`;
this.emit("render");
return;
}
let base = this._presetItemMarkup;
this.presetsList.innerHTML = "";
for (let [index, preset] of presets.entries()) {
let el = base.cloneNode(true);
let [label, span] = el.children;
el.dataset.id = index;
label.textContent = preset.name;
span.textContent = preset.value;
this.presetsList.appendChild(el);
}
this.emit("render");
});
},
/**
* returns definition of a filter as defined in filterList
*
@ -689,29 +837,19 @@ CSSFilterEditorWidget.prototype = {
this.emit("updated", this.getCssValue());
},
_removeEventListeners: function() {
this.addButton.removeEventListener("click", this._addButtonClick);
this.list.removeEventListener("click", this._removeButtonClick);
this.list.removeEventListener("mousedown", this._mouseDown);
this.list.removeEventListener("keydown", this._keyDown);
getPresets: function() {
return asyncStorage.getItem("cssFilterPresets").then(presets => {
if (!presets) {
return [];
}
// These events are used for drag drop re-ordering
this.win.removeEventListener("mousemove", this._mouseMove);
this.win.removeEventListener("mouseup", this._mouseUp);
// Used to workaround float-precision problems
this.list.removeEventListener("input", this._input);
return presets;
}, Cu.reportError);
},
_destroyMarkup: function() {
this._filterItemMarkup.remove();
this.el.remove();
this.el = this.list = this.container = this._filterItemMarkup = null;
},
destroy: function() {
this._removeEventListeners();
this._destroyMarkup();
setPresets: function(presets) {
return asyncStorage.setItem("cssFilterPresets", presets)
.catch(Cu.reportError);
}
};

View File

@ -863,8 +863,9 @@ Tooltip.prototype = {
* that resolves to the instance of the widget when ready.
*/
setFilterContent: function(filter) {
let dimensions = {width: "350", height: "350"};
let dimensions = {width: "500", height: "200"};
let panel = this.panel;
return this.setIFrameContent(dimensions, FILTER_FRAME).then(onLoaded);
function onLoaded(iframe) {
@ -874,14 +875,8 @@ Tooltip.prototype = {
let container = win.document.getElementById("container");
let widget = new CSSFilterEditorWidget(container, filter);
iframe.height = doc.offsetHeight;
widget.on("render", () => {
iframe.height = doc.offsetHeight;
});
// Resolve to the widget instance whenever the popup becomes visible
if (panel.state == "open") {
if (panel.state === "open") {
def.resolve(widget);
} else {
panel.addEventListener("popupshown", function shown() {

View File

@ -16,14 +16,26 @@
<body>
<div id="container">
<div class="filters">
<div class="filters-list">
<div id="filters"></div>
<div class="footer">
<select value="">
<option value="">&filterListSelectPlaceholder;</option>
</select>
<button id="add-filter" class="add">&addNewFilterButton;</button>
<button id="toggle-presets">&presetsToggleButton;</button>
</div>
</div>
<div id="editor-footer">
<select value="">
<option value="">&filterListSelectPlaceholder;</option>
</select>
<button id="add-filter">&addNewFilterButton;</button>
<div class="presets-list">
<div id="presets"></div>
<div class="footer">
<input value="" class="devtools-textinput"
placeholder="&newPresetPlaceholder;"></input>
<button class="add">&savePresetButton;</button>
</div>
</div>
</div>
</body>
</html>

View File

@ -2,74 +2,103 @@
* 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/. */
#container {
color: var(--theme-body-color);
padding: 5px;
html, body {
height: 100%;
margin: 0;
overflow: hidden;
font: message-box;
color: var(--theme-body-color);
}
/* Main container: Displays the filters and presets in 2 columns */
#container {
height: 100%;
display: flex;
position: relative;
}
#container.dragging {
-moz-user-select: none;
}
.theme-light #add-filter,
.theme-light .remove-button {
filter: invert(1);
}
.filter {
.filters-list,
.presets-list {
display: flex;
margin-bottom: 10px;
flex-direction: column;
box-sizing: border-box;
}
.filters-list {
/* Allow the filters list to take the full width when the presets list is
hidden */
flex-grow: 1;
padding: 0 6px;
}
.presets-list {
/* Make sure that when the presets list is shown, it has a fixed width */
width: 200px;
padding-left: 6px;
transition: width .1s;
flex-shrink: 0;
border-left: 1px solid var(--theme-splitter-color);
}
.presets-list .add {
margin-top: 4px;
}
#container:not(.show-presets) .presets-list {
width: 0;
border-left: none;
padding-left: 0;
}
#container.show-presets .filters-list {
width: 300px;
}
/* The list of filters and list of presets should push their footers to the
bottom, so they can take as much space as there is */
#filters,
#presets {
flex-grow: 1;
/* Avoid pushing below the tooltip's area */
overflow-y: auto;
}
/* The filters and presets list both have footers displayed at the bottom.
These footers have some input (taking up as much space as possible) and an
add button next */
.footer {
display: flex;
margin: 10px 3px;
}
.footer :not(button) {
flex-grow: 1;
margin-right: 3px;
}
/* Styles for 1 filter function item */
.filter,
.filter-name,
.filter-value {
display: flex;
align-items: center;
}
.filter {
margin: 5px 0;
}
.filter-name {
padding-right: 10px;
flex: 1;
}
.filter-value {
min-width: 150px;
flex: 2;
}
.remove-button {
width: 16px;
height: 16px;
background: url(chrome://browser/skin/devtools/close@2x.png);
background-size: 16px;
font-size: 0;
border: none;
cursor: pointer;
}
/* drag/drop handle */
#container i {
width: 10px;
margin-right: 15px;
padding: 10px 0;
cursor: grab;
}
#container i::before {
content: '';
display: block;
width: 10px;
height: 1px;
background: currentColor;
box-shadow: 0 3px 0 0 currentColor,
0 -3px 0 0 currentColor;
}
#container .dragging {
position: relative;
z-index: 1;
cursor: grab;
width: 120px;
margin-right: 10px;
}
.filter-name label {
@ -81,14 +110,105 @@
cursor: ew-resize;
}
/* drag/drop handle */
.filter-name i {
width: 10px;
height: 10px;
margin-right: 10px;
cursor: grab;
background: linear-gradient(to bottom,
currentColor 0,
currentcolor 1px,
transparent 1px,
transparent 2px);
background-repeat: repeat-y;
background-size: auto 4px;
background-position: 0 1px;
}
.filter-value {
min-width: 150px;
margin-right: 10px;
flex: 1;
}
.filter-value input {
min-width: 50%;
flex-grow: 1;
}
.filter-value span {
max-width: 20px;
width: 20px;
.theme-light .add,
.theme-light .remove-button {
filter: invert(1);
}
.preset {
display: flex;
margin-bottom: 10px;
cursor: pointer;
padding: 3px 5px;
flex-direction: row;
flex-wrap: wrap;
}
.preset label,
.preset span {
display: flex;
align-items: center;
}
.preset label {
flex: 1 0;
cursor: pointer;
color: var(--theme-body-color);
}
.preset:hover {
background: var(--theme-selection-background);
}
.preset:hover label, .preset:hover span {
color: var(--theme-selection-color);
}
.theme-light .preset:hover .remove-button {
filter: invert(0);
}
.preset .remove-button {
order: 2;
}
.preset span {
flex: 2 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
order: 3;
color: var(--theme-body-color-alt);
}
.remove-button {
width: 16px;
height: 16px;
background: url(chrome://browser/skin/devtools/close@2x.png);
background-size: cover;
font-size: 0;
border: none;
cursor: pointer;
}
.hidden {
display: none !important;
}
#container .dragging {
position: relative;
z-index: 10;
cursor: grab;
}
/* message shown when there's no filter specified */
@ -97,20 +217,7 @@
line-height: 20px;
}
#editor-footer {
display: flex;
justify-content: flex-end;
}
#editor-footer select {
flex-grow: 1;
box-sizing: border-box;
font: inherit;
margin: 0 3px;
}
#add-filter {
-moz-appearance: none;
.add {
background: url(chrome://browser/skin/devtools/add.svg);
background-size: cover;
border: none;

View File

@ -18,7 +18,6 @@ const overlays = require("devtools/styleinspector/style-inspector-overlays");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/Templater.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
@ -221,37 +220,6 @@ CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
}
};
/**
* Clone the given template node, and process it by resolving ${} references
* in the template.
*
* @param {nsIDOMElement} aTemplate the template note to use.
* @param {nsIDOMElement} aDestination the destination node where the
* processed nodes will be displayed.
* @param {object} aData the data to pass to the template.
* @param {Boolean} aPreserveDestination If true then the template will be
* appended to aDestination's content else aDestination.innerHTML will be
* cleared before the template is appended.
*/
CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate,
aDestination, aData, aPreserveDestination)
{
if (!aPreserveDestination) {
aDestination.innerHTML = "";
}
// All the templater does is to populate a given DOM tree with the given
// values, so we need to clone the template first.
let duplicated = aTemplate.cloneNode(true);
// See https://github.com/mozilla/domtemplate/blob/master/README.md
// for docs on the template() function
template(duplicated, aData, { allowEval: true });
while (duplicated.firstChild) {
aDestination.appendChild(duplicated.firstChild);
}
};
XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() {
return Services.strings.createBundle(
"chrome://global/locale/devtools/styleinspector.properties");
@ -1407,6 +1375,7 @@ PropertyView.prototype = {
promises.push(selector.ready);
}
this.matchedSelectorsContainer.innerHTML = "";
this.matchedSelectorsContainer.appendChild(frag);
return promise.all(promises);
},

View File

@ -12,3 +12,13 @@
<!-- LOCALIZATION NOTE (addNewFilterButton): This string is displayed on a button used to add new filters -->
<!ENTITY addNewFilterButton "Add">
<!-- LOCALIZATION NOTE (newPresetPlaceholder): This string is used as
- a placeholder in the list of presets which is used to save a new preset -->
<!ENTITY newPresetPlaceholder "Preset Name">
<!-- LOCALIZATION NOTE (savePresetButton): This string is displayed on a button used to save a new preset -->
<!ENTITY savePresetButton "Save">
<!-- LOCALIZATION NOTE(presetsToggleButton): This string is used in a button which toggles the presets list -->
<!ENTITY presetsToggleButton "Presets">

View File

@ -11,6 +11,12 @@
# (no filter specified / all removed)
emptyFilterList=No filter specified
# LOCALIZATION NOTE (emptyPresetList):
# This string is displayed when preset's list is empty
emptyPresetList=You don't have any saved presets, \
You can store filter presets by choosing a name and saving them. \
Presets are quickly accessible and you can re-use them with ease.
# LOCALIZATION NOTE (addUsingList):
# This string is displayed under [emptyFilterList] when filter's
# list is empty, guiding user to add a filter using the list below it

View File

@ -4,4 +4,8 @@
#filter substitution
# LOCALIZATION NOTE: this preference is set to true for en-US specifically,
# locales without this line have the setting set to false by default.
pref("browser.search.geoSpecificDefaults", true);
pref("general.useragent.locale", "@AB_CD@");

View File

@ -124,11 +124,6 @@ body {
border-bottom-width: 1px;
}
/* This rule is necessary because Templater.jsm breaks LTR TDs in RTL docs */
.rule-text {
direction: ltr;
}
.matched {
text-decoration: line-through;
}

View File

@ -3,20 +3,13 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg height="0" xmlns="http://www.w3.org/2000/svg">
<filter id="invert" x="0%" y="0%" width="100%" height="100%" >
<feComponentTransfer>
<feFuncR type="table" tableValues=".1 0"/>
<feFuncG type="table" tableValues=".1 0"/>
<feFuncB type="table" tableValues=".1 0"/>
</feComponentTransfer>
</filter>
<filter id="invert-white" x="0%" y="0%" width="100%" height="100%" >
<feComponentTransfer>
<feFuncR type="table" tableValues=".6 0"/>
<feFuncG type="table" tableValues=".6 0"/>
<feFuncB type="table" tableValues=".6 0"/>
</feComponentTransfer>
</filter>
<filter id="invert" x="0%" y="0%" width="100%" height="100%" >
<feComponentTransfer>
<feFuncR type="table" tableValues=".1 0"/>
<feFuncG type="table" tableValues=".1 0"/>
<feFuncB type="table" tableValues=".1 0"/>
</feComponentTransfer>
</filter>
<!-- Web Audio Gradients -->
<linearGradient id="bypass-light" x1="6%" y1="8%" x2="12%" y2="12%" spreadMethod="repeat">

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -2,6 +2,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/. -->
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="#000" width="16" height="16">
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="#0A0805" width="16" height="16">
<path d="M 0.00,160.00 L 512.00,160.00 L 480.00,480.00 L 32.00,480.00 L 0.00,160.00 Z M 464.00,96.00 L 480.00,128.00 L 32.00,128.00 L 64.00,64.00 L 240.00,64.00 L 256.00,96.00 L 464.00,96.00 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 561 B

After

Width:  |  Height:  |  Size: 564 B

View File

@ -2,6 +2,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/. -->
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="#000" width="16" height="16">
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="#0A0805" width="16" height="16">
<path d="M 416.00,480.00L 512.00,224.00L 96.00,224.00L0.00,480.00 zM 64.00,192.00 L 0.00,480.00 L 0.00,64.00 L 144.00,64.00 L 208.00,128.00 L 416.00,128.00 L 416.00,192.00 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 545 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -1,7 +1,7 @@
<!-- 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/. -->
<svg width="16" xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 16">
<svg width="16" xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 16" fill="#0A0805">
<path d="m1.3,12.5v-2.4c0,0 0,2.5 6.7,2.5 6.7,0 6.7-2.5 6.7-2.5v2.4c0,0 0,2.7-6.8,2.7-6.6,0-6.6-2.7-6.6-2.7z"/>
<path d="m14.7,3.4c0-1.4-3-2.5-6.7-2.5s-6.7,1.1-6.7,2.5c0,.2 0,.3 .1,.5-.1-.3-.1-.4-.1-.4v1.5c0,0 0,2.7 6.7,2.7 6.7,0 6.8-2.7 6.8-2.7v-1.6c0,.1 0,.2-.1,.5-0-.2-0-.3-0-.5z"/>
<path d="m1.3,8.7v-2.4c0,0 0,2.5 6.7,2.5 6.7,0 6.7-2.5 6.7-2.5v2.4c0,0 0,2.7-6.8,2.7-6.6-0-6.6-2.7-6.6-2.7z"/>

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 721 B

View File

@ -10,8 +10,8 @@
overflow: auto;
}
.theme-dark #storage-tree {
background: #343c45; /* Toolbars */
#storage-tree {
background: var(--theme-sidebar-background);
}
#storage-tree .tree-widget-item[type="store"]:after {

View File

@ -10,10 +10,14 @@
%define smw_itemDarkBottomBorder rgba(128,128,128,0.15)
%define smw_itemLightTopBorder rgba(128,128,128,0.15)
%define smw_itemLightBottomBorder transparent
%define table_itemDarkStartBorder rgba(0,0,0,0.2)
%define table_itemDarkEndBorder rgba(128,128,128,0.15)
%define table_itemLightStartBorder rgba(128,128,128,0.25)
%define table_itemLightEndBorder transparent
.theme-dark {
--table-splitter-color: rgba(255,255,255,0.15);
--table-zebra-background: rgba(255,255,255,0.05);
}
.theme-light {
--table-splitter-color: rgba(0,0,0,0.15);
--table-zebra-background: rgba(0,0,0,0.05);
}
/* Generic pane helpers */
@ -1206,43 +1210,16 @@
overflow: auto;
}
.theme-light .table-widget-body {
background: var(--theme-sidebar-background);
}
.theme-dark .table-widget-body,
.theme-dark .table-widget-empty-text {
background-color: var(--theme-toolbar-background);
}
.theme-dark .table-widget-body:-moz-locale-dir(ltr) {
box-shadow: inset -1px 0 0 @smw_marginDark@;
}
.theme-dark .table-widget-body:-moz-locale-dir(rtl) {
box-shadow: inset 1px 0 0 @smw_marginDark@;
}
.table-widget-body:-moz-locale-dir(ltr) {
box-shadow: inset -1px 0 0 @smw_marginLight@;
}
.table-widget-body:-moz-locale-dir(rtl) {
box-shadow: inset 1px 0 0 @smw_marginLight@;
.table-widget-body,
.table-widget-empty-text {
background-color: var(--theme-body-background);
}
/* Column Headers */
.theme-dark .table-widget-column-header,
.theme-dark .table-widget-cell {
-moz-border-end: 1px solid @table_itemDarkStartBorder@;
box-shadow: inset 1px 0 0 @table_itemDarkEndBorder@;
}
.theme-light .table-widget-column-header,
.theme-light .table-widget-cell {
-moz-border-end: 1px solid @table_itemLightStartBorder@;
box-shadow: inset 1px 0 0 @table_itemLightEndBorder@;
.table-widget-column-header,
.table-widget-cell {
-moz-border-end: 1px solid var(--table-splitter-color) !important;
}
/* Table widget column header colors are taken from netmonitor.inc.css to match
@ -1253,10 +1230,8 @@
background: rgba(0,0,0,0);
position: sticky;
top: 0;
min-height: 32px;
width: 100%;
border: none;
padding: 8px 0 0 !important;
padding: 5px 0 0 !important;
color: inherit;
text-align: center;
font-weight: inherit !important;
@ -1264,27 +1239,29 @@
}
.table-widget-column-header:hover {
background: rgba(0,0,0,0.10);
background-image: linear-gradient(rgba(0,0,0,0.10), rgba(0,0,0,0.10));
}
.table-widget-column-header:hover:active {
background: rgba(0,0,0,0.25);
background-image: linear-gradient(rgba(0,0,0,0.25), rgba(0,0,0,0.25));
}
.table-widget-column-header:not(:active)[sorted] {
background: rgba(0,0,0,0.15);
background-image: linear-gradient(rgba(0,0,0,0.15), rgba(0,0,0,0.15));
}
.table-widget-column-header:not(:active)[sorted=ascending] {
background-image: radial-gradient(farthest-side at center top, hsla(200,100%,70%,.7), hsla(200,100%,70%,0.3));
background-size: 100% 1px;
background-repeat: no-repeat;
background-image: radial-gradient(farthest-side at center top, hsla(200,100%,70%,.7), hsla(200,100%,70%,0.3)),
linear-gradient(rgba(0,0,0,0.15), rgba(0,0,0,0.15));
background-size: 100% 1px, auto;
background-repeat: no-repeat, repeat;
}
.table-widget-column-header:not(:active)[sorted=descending] {
background-image: radial-gradient(farthest-side at center bottom, hsla(200,100%,70%,.7), hsla(200,100%,70%,0.3));
background-size: 100% 1px;
background-repeat: no-repeat;
background-image: radial-gradient(farthest-side at center bottom, hsla(200,100%,70%,.7), hsla(200,100%,70%,0.3)),
linear-gradient(rgba(0,0,0,0.15), rgba(0,0,0,0.15));
background-size: 100% 1px, auto;
background-repeat: no-repeat, repeat;
background-position: bottom;
}
@ -1292,44 +1269,22 @@
.table-widget-cell {
width: 100%;
margin: -1px 0 !important;
padding: 3px 4px;
background-clip: padding-box;
min-width: 100px;
-moz-user-focus: normal;
}
.theme-dark .table-widget-cell {
border-top: 1px solid @smw_itemDarkTopBorder@;
border-bottom: 1px solid @smw_itemDarkBottomBorder@;
color: var(--theme-selection-color);
}
.theme-dark:not(.filtering) .table-widget-cell:nth-child(odd):not(.theme-selected),
.theme-dark .table-widget-cell:not(.theme-selected)[odd] {
background: rgba(255,255,255,0.05);
}
.theme-dark .table-widget-cell:last-of-type {
box-shadow: inset 0 -1px 0 @smw_itemDarkTopBorder@;
}
.theme-light .table-widget-cell {
border-top: 1px solid @smw_itemLightTopBorder@;
border-bottom: 1px solid @smw_itemLightBottomBorder@;
}
.theme-light .table-widget-cell:not(.theme-selected) {
margin-bottom: -1px !important;
border-bottom: 1px solid transparent;
color: var(--theme-body-color);
}
.theme-light:not(.filtering) .table-widget-cell:nth-child(odd):not(.theme-selected),
.theme-light .table-widget-cell:not(.theme-selected)[odd] {
background: rgba(128,128,128,0.05);
.table-widget-cell:last-child {
border-bottom: 1px solid var(--table-splitter-color);
}
.theme-light .table-widget-cell:last-of-type {
box-shadow: inset 0 -1px 0 @smw_itemLightTopBorder@;
:root:not(.filtering) .table-widget-cell:nth-child(odd):not(.theme-selected),
.table-widget-cell:not(.theme-selected)[odd] {
background: var(--table-zebra-background);
}
.table-widget-cell.flash-out {
@ -1351,10 +1306,6 @@
margin-top: -20px !important;
}
.theme-light .table-widget-empty-text {
background: #F7F7F7; /* Background-Sidebar */
}
.table-widget-body:empty + .table-widget-empty-text:not([value=""]),
.table-widget-body[empty] + .table-widget-empty-text:not([value=""]) {
display: block;
@ -1406,11 +1357,11 @@
}
.tree-widget-item[level="1"] {
font-weight: 800;
font-weight: 700;
}
/* Twisties */
.tree-widget-item:before {
.tree-widget-item::before {
content: "";
width: 14px;
height: 14px;
@ -1423,24 +1374,24 @@
background-position: -28px -14px;
}
.tree-widget-item:-moz-locale-dir(rtl):before {
.tree-widget-item:-moz-locale-dir(rtl)::before {
float: right;
transform: scaleX(-1);
}
.theme-light .tree-widget-item:before {
.theme-light .tree-widget-item:not(.theme-selected)::before {
background-position: 0 -14px;
}
.tree-widget-item[empty]:before {
.tree-widget-item[empty]::before {
background: transparent;
}
.tree-widget-item[expanded]:before {
.tree-widget-item[expanded]::before {
background-position: -42px -14px;
}
.theme-light .tree-widget-item[expanded]:before {
.theme-light .tree-widget-item:not(.theme-selected)[expanded]:before {
background-position: -14px -14px;
}
@ -1518,7 +1469,7 @@
/* Custom icons for certain tree items indicating the type of the item */
.tree-widget-item[type]:after {
.tree-widget-item[type]::after {
content: "";
float: left;
width: 16px;
@ -1526,31 +1477,36 @@
-moz-margin-end: 4px;
background-repeat: no-repeat;
background-size: 20px auto;
filter: url('filters.svg#invert');
background-position: 0 0;
background-size: auto 20px;
opacity: 0.75;
}
.tree-widget-item:-moz-locale-dir(rtl):after {
.tree-widget-item.theme-selected[type]::after {
opacity: 1;
}
.tree-widget-item:-moz-locale-dir(rtl)::after {
float: right;
}
.theme-dark .tree-widget-item[type]:after {
filter: url('filters.svg#invert-white');
.theme-light .tree-widget-item.theme-selected[type]::after,
.theme-dark .tree-widget-item[type]::after {
filter: invert(1);
}
.tree-widget-item[type="dir"]:after {
.tree-widget-item[type="dir"]::after {
background-image: url(chrome://browser/skin/devtools/filetype-dir-close.svg);
background-position: 2px 0;
background-size: auto 16px;
width: 20px;
}
.tree-widget-item[type="dir"][expanded]:not([empty]):after {
.tree-widget-item[type="dir"][expanded]:not([empty])::after {
background-image: url(chrome://browser/skin/devtools/filetype-dir-open.svg);
}
.tree-widget-item[type="url"]:after {
.tree-widget-item[type="url"]::after {
background-image: url(chrome://browser/skin/devtools/filetype-globe.svg);
background-size: auto 18px;
width: 18px;

View File

@ -1538,15 +1538,18 @@ html|*.urlbar-input:-moz-lwtheme::-moz-placeholder,
.ac-result-type-bookmark,
.autocomplete-treebody::-moz-tree-image(bookmark, treecolAutoCompleteImage) {
list-style-image: url("chrome://browser/skin/places/bookmark.png");
-moz-image-region: rect(0px 32px 16px 16px);
list-style-image: url("chrome://browser/skin/places/autocomplete-star.png");
-moz-image-region: rect(0 16px 16px 0);
width: 16px;
height: 16px;
}
richlistitem[selected="true"][current="true"] > .ac-title-box > .ac-result-type-bookmark,
.autocomplete-treebody::-moz-tree-image(selected, current, bookmark, treecolAutoCompleteImage) {
-moz-image-region: rect(0px 48px 16px 32px);
@media (min-resolution: 1.1dppx) {
.ac-result-type-bookmark,
.autocomplete-treebody::-moz-tree-image(bookmark, treecolAutoCompleteImage) {
list-style-image: url("chrome://browser/skin/places/autocomplete-star@2x.png");
-moz-image-region: rect(0 32px 32px 0);
}
}
.ac-result-type-keyword,
@ -1559,6 +1562,18 @@ richlistitem[type~="action"][actiontype="searchengine"] > .ac-title-box > .ac-si
@media not all and (-moz-os-version: windows-vista) and (-moz-windows-default-theme) {
@media not all and (-moz-os-version: windows-win7) and (-moz-windows-default-theme) {
richlistitem[selected="true"][current="true"] > .ac-title-box > .ac-result-type-bookmark,
.autocomplete-treebody::-moz-tree-image(selected, current, bookmark, treecolAutoCompleteImage) {
-moz-image-region: rect(0 32px 16px 16px);
}
@media (min-resolution: 1.1dppx) {
richlistitem[selected="true"][current="true"] > .ac-title-box > .ac-result-type-bookmark,
.autocomplete-treebody::-moz-tree-image(selected, current, bookmark, treecolAutoCompleteImage) {
-moz-image-region: rect(0 64px 32px 32px);
}
}
.ac-result-type-keyword[selected="true"],
.autocomplete-treebody::-moz-tree-image(keyword, treecolAutoCompleteImage, selected),
richlistitem[type~="action"][actiontype="searchengine"][selected="true"] > .ac-title-box > .ac-site-icon {
@ -1864,15 +1879,6 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
/* bookmarks menu-button */
#bookmarks-menu-button.bookmark-item {
list-style-image: url("chrome://browser/skin/places/bookmark.png");
-moz-image-region: rect(0px 16px 16px 0px);
}
#bookmarks-menu-button.bookmark-item[starred] {
-moz-image-region: rect(0px 32px 16px 16px);
}
#bookmarks-menu-button.bookmark-item > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
-moz-margin-start: 5px;
}
@ -2860,7 +2866,13 @@ chatbox {
%include ../shared/UITour.inc.css
#UITourTooltipButtons {
margin: 24px -4px -4px;
/**
* Override the --panel-arrowcontent-padding so the background extends
* to the sides and bottom of the panel.
*/
margin-left: -10px;
margin-right: -10px;
margin-bottom: -10px;
}
%include ../shared/contextmenu.inc.css

View File

@ -240,10 +240,11 @@ browser.jar:
skin/classic/browser/panic-panel/header-small@2x.png (../shared/panic-panel/header-small@2x.png)
skin/classic/browser/panic-panel/icons.png (../shared/panic-panel/icons.png)
skin/classic/browser/panic-panel/icons@2x.png (../shared/panic-panel/icons@2x.png)
skin/classic/browser/places/autocomplete-star.png (places/autocomplete-star.png)
skin/classic/browser/places/autocomplete-star@2x.png (places/autocomplete-star@2x.png)
skin/classic/browser/places/autocomplete-star-XPVista7.png (places/autocomplete-star-XPVista7.png)
skin/classic/browser/places/places.css (places/places.css)
* skin/classic/browser/places/organizer.css (places/organizer.css)
skin/classic/browser/places/bookmark.png (places/bookmark.png)
skin/classic/browser/places/bookmark-XP.png (places/bookmark-XP.png)
skin/classic/browser/places/query.png (places/query.png)
skin/classic/browser/places/query-XP.png (places/query-XP.png)
skin/classic/browser/places/bookmarksMenu.png (places/bookmarksMenu.png)
@ -603,7 +604,6 @@ browser.jar:
% override chrome://browser/skin/downloads/buttons.png chrome://browser/skin/downloads/buttons-XP.png os=WINNT osversion<6
% override chrome://browser/skin/feeds/feedIcon.png chrome://browser/skin/feeds/feedIcon-XP.png os=WINNT osversion<6
% override chrome://browser/skin/feeds/feedIcon16.png chrome://browser/skin/feeds/feedIcon16-XP.png os=WINNT osversion<6
% override chrome://browser/skin/places/bookmark.png chrome://browser/skin/places/bookmark-XP.png os=WINNT osversion<6
% override chrome://browser/skin/places/query.png chrome://browser/skin/places/query-XP.png os=WINNT osversion<6
% override chrome://browser/skin/places/bookmarksMenu.png chrome://browser/skin/places/bookmarksMenu-XP.png os=WINNT osversion<6
% override chrome://browser/skin/places/bookmarksToolbar.png chrome://browser/skin/places/bookmarksToolbar-XP.png os=WINNT osversion<6
@ -630,6 +630,7 @@ browser.jar:
% override chrome://browser/skin/syncProgress-toolbar.png chrome://browser/skin/syncProgress-toolbar-XPVista7.png os=WINNT osversion<=6.1
% override chrome://browser/skin/syncProgress-toolbar@2x.png chrome://browser/skin/syncProgress-toolbar-XPVista7@2x.png os=WINNT osversion<=6.1
% override chrome://browser/skin/toolbarbutton-dropdown-arrow.png chrome://browser/skin/toolbarbutton-dropdown-arrow-XPVista7.png os=WINNT osversion<=6.1
% override chrome://browser/skin/places/autocomplete-star.png chrome://browser/skin/places/autocomplete-star-XPVista7.png os=WINNT osversion<=6.1
% override chrome://browser/skin/tabbrowser/newtab.png chrome://browser/skin/tabbrowser/newtab-XPVista7.png os=WINNT osversion<=6.1
% override chrome://browser/skin/tabbrowser/newtab@2x.png chrome://browser/skin/tabbrowser/newtab-XPVista7@2x.png os=WINNT osversion<=6.1
% override chrome://browser/skin/tabbrowser/tab-arrow-left.png chrome://browser/skin/tabbrowser/tab-arrow-left-XPVista7.png os=WINNT osversion<=6.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 993 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 B

View File

@ -265,8 +265,13 @@ pref("browser.search.order.1", "chrome://browser/locale/region.properties");
pref("browser.search.order.2", "chrome://browser/locale/region.properties");
pref("browser.search.order.3", "chrome://browser/locale/region.properties");
// Market-specific search defaults (US market only)
pref("browser.search.geoSpecificDefaults", true);
// Market-specific search defaults
// This is disabled globally, and then enabled for individual locales
// in firefox-l10n.js (eg. it's enabled for en-US).
pref("browser.search.geoSpecificDefaults", false);
pref("browser.search.geoSpecificDefaults.url", "");
// US specific default (used as a fallback if the geoSpecificDefaults request fails).
pref("browser.search.defaultenginename.US", "chrome://browser/locale/region.properties");
pref("browser.search.order.US.1", "chrome://browser/locale/region.properties");
pref("browser.search.order.US.2", "chrome://browser/locale/region.properties");

View File

@ -2,29 +2,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/.
ifneq (,$(findstring -march=armv7,$(OS_CFLAGS)))
MIN_CPU_VERSION=7
else
MIN_CPU_VERSION=5
endif
MOZ_APP_BUILDID=$(shell cat $(DEPTH)/config/buildid)
# See Bug 1137586 for more details on version code computation.
ifeq (,$(ANDROID_VERSION_CODE))
ifeq ($(CPU_ARCH),arm)
# Increment by MIN_SDK_VERSION -- this adds 9 to every build ID as a minimum.
# Our split APK starts at 11.
ANDROID_VERSION_CODE=$(shell echo $$((`cat $(DEPTH)/config/buildid | cut -c1-10` + $(MOZ_ANDROID_MIN_SDK_VERSION) + 0)))
else # not ARM, so x86.
# Increment the version code by 3 for x86 builds so they are offered to x86 phones that have ARM emulators,
# beating the 2-point advantage that the v11+ ARMv7 APK has.
# If we change our splits in the future, we'll need to do this further still.
ANDROID_VERSION_CODE=$(shell echo $$((`cat $(DEPTH)/config/buildid | cut -c1-10` + $(MOZ_ANDROID_MIN_SDK_VERSION) + 3)))
endif
endif
UA_BUILDID=$(shell echo $(ANDROID_VERSION_CODE) | cut -c1-8)
ANDROID_VERSION_CODE:=$(shell $(PYTHON) \
$(topsrcdir)/python/mozbuild/mozbuild/android_version_code.py \
--verbose \
--with-android-cpu-arch=$(ANDROID_CPU_ARCH) \
$(if $(MOZ_ANDROID_MIN_SDK_VERSION),--with-android-min-sdk=$(MOZ_ANDROID_MIN_SDK_VERSION)) \
$(if $(MOZ_ANDROID_MAX_SDK_VERSION),--with-android-max-sdk=$(MOZ_ANDROID_MAX_SDK_VERSION)) \
$(MOZ_APP_BUILDID))
DEFINES += \
-DANDROID_VERSION_CODE=$(ANDROID_VERSION_CODE) \
@ -32,7 +18,6 @@ DEFINES += \
-DMOZ_ANDROID_SHARED_ACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_ACCOUNT_TYPE)" \
-DMOZ_ANDROID_SHARED_FXACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_FXACCOUNT_TYPE)" \
-DMOZ_APP_BUILDID=$(MOZ_APP_BUILDID) \
-DUA_BUILDID=$(UA_BUILDID) \
$(NULL)
GARBAGE += \

View File

@ -4,6 +4,7 @@
package org.mozilla.gecko.fxa.sync;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.IntentService;
import android.content.Intent;
@ -15,6 +16,7 @@ import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException;
import org.mozilla.gecko.background.fxa.profile.FxAccountProfileClient10;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import java.util.concurrent.Executor;
@ -66,6 +68,14 @@ public class FxAccountProfileService extends IntentService {
@Override
public void handleFailure(FxAccountAbstractClientException.FxAccountAbstractClientRemoteException e) {
Logger.warn(LOG_TAG, "Failed to fetch Account profile.", e);
if (e.isInvalidAuthentication()) {
// The profile server rejected the cached oauth token! Invalidate it.
// A new token will be generated upon next request.
Logger.info(LOG_TAG, "Invalidating oauth token after 401!");
AccountManager.get(FxAccountProfileService.this).invalidateAuthToken(FxAccountConstants.ACCOUNT_TYPE, authToken);
}
sendResult("Failed to fetch Account profile.", resultReceiver, Activity.RESULT_CANCELED);
}

View File

@ -36,7 +36,7 @@
<org.mozilla.gecko.toolbar.ToolbarEditLayout android:id="@+id/edit_layout"
style="@style/UrlBar.Button"
android:paddingLeft="12dp"
android:paddingLeft="10dp"
android:paddingRight="12dp"
android:visibility="gone"
android:orientation="horizontal"

View File

@ -8,11 +8,13 @@ package org.mozilla.gecko.toolbar;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.CustomEditText;
import org.mozilla.gecko.InputMethods;
import org.mozilla.gecko.R;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.HardwareUtils;
import android.content.Context;
import android.graphics.Rect;
@ -88,12 +90,19 @@ public class ToolbarEditText extends CustomEditText
setOnKeyPreImeListener(new KeyPreImeListener());
setOnSelectionChangedListener(new SelectionChangeListener());
addTextChangedListener(new TextChangeListener());
// Set an inactive search icon on tablet devices when in editing mode
updateSearchIcon(false);
}
@Override
public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
// Make search icon inactive when edit toolbar search term isn't a user entered
// search term
final boolean isActive = !TextUtils.isEmpty(getText());
updateSearchIcon(isActive);
if (gainFocus) {
resetAutocompleteState();
return;
@ -138,6 +147,26 @@ public class ToolbarEditText extends CustomEditText
mPrefs = prefs;
}
/**
* Update the search icon at the left of the edittext based
* on its state.
*
* @param isActive The state of the edittext. Active is when the initialized
* text has changed and is not empty.
*/
void updateSearchIcon(boolean isActive) {
if (!HardwareUtils.isTablet()) {
return;
}
// When on tablet show a magnifying glass in editing mode
if (isActive) {
setCompoundDrawablesWithIntrinsicBounds(R.drawable.search_icon_active, 0, 0, 0);
} else {
setCompoundDrawablesWithIntrinsicBounds(R.drawable.search_icon_inactive, 0, 0, 0);
}
}
/**
* Mark the start of autocomplete changes so our text change
* listener does not react to changes in autocomplete text
@ -514,6 +543,9 @@ public class ToolbarEditText extends CustomEditText
removeAutocomplete(editable);
}
// Update search icon with an active state since user is typing
updateSearchIcon(textLength > 0);
if (mFilterListener != null) {
mFilterListener.onFilter(text, doAutocomplete ? ToolbarEditText.this : null);
}

View File

@ -41,6 +41,7 @@ const FILTER_DELAY = 500;
let Logins = {
_logins: [],
_filterTimer: null,
_selectedLogin: null,
_getLogins: function() {
let logins;
@ -60,6 +61,8 @@ let Logins = {
window.addEventListener("popstate", this , false);
Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
document.getElementById("save-btn").addEventListener("click", this._onSaveEditLogin.bind(this), false);
document.getElementById("password-btn").addEventListener("click", this._onPasswordBtn.bind(this), false);
this._loadList(this._getLogins());
@ -101,6 +104,8 @@ let Logins = {
}, false);
this._showList();
this._updatePasswordBtn(true);
},
uninit: function () {
@ -121,8 +126,130 @@ let Logins = {
},
_showList: function () {
let list = document.getElementById("logins-list");
list.removeAttribute("hidden");
let loginsListPage = document.getElementById("logins-list-page");
loginsListPage.classList.remove("hidden");
let editLoginPage = document.getElementById("edit-login-page");
editLoginPage.classList.add("hidden");
// If the Show/Hide password button has been flipped, reset it
if (this._isPasswordBtnInHideMode()) {
this._updatePasswordBtn(true);
}
},
_onPopState: function (event) {
// Called when back/forward is used to change the state of the page
if (event.state) {
this._showEditLoginDialog(event.state.id);
} else {
this._selectedLogin = null;
this._showList();
}
},
_showEditLoginDialog: function (login) {
let listPage = document.getElementById("logins-list-page");
listPage.classList.add("hidden");
let editLoginPage = document.getElementById("edit-login-page");
editLoginPage.classList.remove("hidden");
let usernameField = document.getElementById("username");
usernameField.value = login.username;
let passwordField = document.getElementById("password");
passwordField.value = login.password;
let domainField = document.getElementById("hostname");
domainField.value = login.hostname;
let img = document.getElementById("favicon");
this._loadFavicon(img, login.hostname);
let headerText = document.getElementById("edit-login-header-text");
if (login.hostname && (login.hostname != "")) {
headerText.textContent = login.hostname;
}
else {
headerText.textContent = gStringBundle.GetStringFromName("editLogin.fallbackTitle");
}
},
_onSaveEditLogin: function() {
let newUsername = document.getElementById("username").value;
let newPassword = document.getElementById("password").value;
let newDomain = document.getElementById("hostname").value;
let origUsername = this._selectedLogin.username;
let origPassword = this._selectedLogin.password;
let origDomain = this._selectedLogin.hostname;
try {
if ((newUsername === origUsername) &&
(newPassword === origPassword) &&
(newDomain === origDomain) ) {
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short");
this._showList();
return;
}
let logins = Services.logins.findLogins({}, origDomain, origDomain, null);
for (let i = 0; i < logins.length; i++) {
if (logins[i].username == origUsername) {
let clone = logins[i].clone();
clone.username = newUsername;
clone.password = newPassword;
clone.hostname = newDomain;
Services.logins.removeLogin(logins[i]);
Services.logins.addLogin(clone);
break;
}
}
} catch (e) {
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.couldNotSave"), "short");
return;
}
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short");
this._showList();
},
_onPasswordBtn: function () {
this._updatePasswordBtn(this._isPasswordBtnInHideMode());
},
_updatePasswordBtn: function (aShouldShow) {
let passwordField = document.getElementById("password");
let button = document.getElementById("password-btn");
let show = gStringBundle.GetStringFromName("password-btn.show");
let hide = gStringBundle.GetStringFromName("password-btn.hide");
if (aShouldShow) {
passwordField.type = "password";
button.textContent = show;
button.classList.remove("password-btn-hide");
} else {
passwordField.type = "text";
button.textContent= hide;
button.classList.add("password-btn-hide");
}
},
_isPasswordBtnInHideMode: function () {
let button = document.getElementById("password-btn");
return button.classList.contains("password-btn-hide");
},
_showPassword: function(password) {
let passwordPrompt = new Prompt({
window: window,
message: password,
buttons: [
gStringBundle.GetStringFromName("loginsDialog.copy"),
gStringBundle.GetStringFromName("loginsDialog.cancel") ]
}).show((data) => {
switch (data.button) {
case 0:
// Corresponds to "Copy password" button.
copyStringAndToast(password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
}
});
},
_onLoginClick: function (event) {
@ -140,6 +267,7 @@ let Logins = {
{ label: gStringBundle.GetStringFromName("loginsMenu.showPassword") },
{ label: gStringBundle.GetStringFromName("loginsMenu.copyPassword") },
{ label: gStringBundle.GetStringFromName("loginsMenu.copyUsername") },
{ label: gStringBundle.GetStringFromName("loginsMenu.editLogin") },
{ label: gStringBundle.GetStringFromName("loginsMenu.delete") }
];
@ -148,19 +276,7 @@ let Logins = {
// Switch on indices of buttons, as they were added when creating login item.
switch (data.button) {
case 0:
let passwordPrompt = new Prompt({
window: window,
message: login.password,
buttons: [
gStringBundle.GetStringFromName("loginsDialog.copy"),
gStringBundle.GetStringFromName("loginsDialog.cancel") ]
}).show((data) => {
switch (data.button) {
case 0:
// Corresponds to "Copy password" button.
copyStringAndToast(login.password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
}
});
this._showPassword(login.password);
break;
case 1:
copyStringAndToast(login.password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
@ -169,6 +285,11 @@ let Logins = {
copyStringAndToast(login.username, gStringBundle.GetStringFromName("loginsDetails.usernameCopied"));
break;
case 3:
this._selectedLogin = login;
this._showEditLoginDialog(login);
history.pushState({ id: login.guid }, document.title);
break;
case 4:
let confirmPrompt = new Prompt({
window: window,
message: gStringBundle.GetStringFromName("loginsDialog.confirmDelete"),
@ -187,6 +308,20 @@ let Logins = {
});
},
_loadFavicon: function (aImg, aHostname) {
// Load favicon from cache.
Messaging.sendRequestForResult({
type: "Favicon:CacheLoad",
url: aHostname,
}).then(function(faviconUrl) {
aImg.style.backgroundImage= "url('" + faviconUrl + "')";
aImg.style.visibility = "visible";
}, function(data) {
debug("Favicon cache failure : " + data);
aImg.style.visibility = "visible";
});
},
_createItemForLogin: function (login) {
let loginItem = document.createElement("div");
@ -199,17 +334,7 @@ let Logins = {
let img = document.createElement("div");
img.className = "icon";
// Load favicon from cache.
Messaging.sendRequestForResult({
type: "Favicon:CacheLoad",
url: login.hostname,
}).then(function(faviconUrl) {
img.style.backgroundImage= "url('" + faviconUrl + "')";
img.style.visibility = "visible";
}, function(data) {
debug("Favicon cache failure : " + data);
img.style.visibility = "visible";
});
this._loadFavicon(img, login.hostname);
loginItem.appendChild(img);
// Create item details.

View File

@ -21,17 +21,42 @@
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutLogins.js"></script>
</head>
<body dir="&locale.dir;">
<div id="logins-header" class="header">
<div>&aboutLogins.title;</div>
<ul class="toolbar-buttons">
<li id="filter-button"></li>
</ul>
<div id="logins-list-page">
<div id="logins-header" class="header">
<div>&aboutLogins.title;</div>
<ul class="toolbar-buttons">
<li id="filter-button"></li>
</ul>
</div>
<div id="logins-list-nonempty-body">
<div id="logins-list" class="list"/>
<div id="filter-input-container" hidden="true">
<input id="filter-input" type="search"/>
<div id="filter-clear"/>
</div>
</div>
</div>
<div id="logins-list" class="list" hidden="true">
</div>
<div id="filter-input-container" hidden="true">
<input id="filter-input" type="search"/>
<div id="filter-clear"></div>
<div id="edit-login-page" class="hidden">
<div id="edit-login-header" class="header">
<div id="edit-login-header-text"/>
</div>
<div class="edit-login-div">
<div id="favicon" class="edit-login-icon"/>
<input type="text" name="hostname" id="hostname" class="edit-login-input"/>
</div>
<div class="edit-login-div">
<input type="text" name="username" id="username" class="edit-login-input"/>
</div>
<div class="edit-login-div">
<input type="password" id="password" name="password" value="password" class="edit-login-input" />
<button id="password-btn"></button>
</div>
<div class="edit-login-div">
<button id="save-btn">&aboutLogins.save;</button>
</div>
</div>
</body>
</html>

View File

@ -3,3 +3,4 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!ENTITY aboutLogins.title "Logins">
<!ENTITY aboutLogins.save "Save">

View File

@ -5,6 +5,7 @@
loginsMenu.showPassword=Show password
loginsMenu.copyPassword=Copy password
loginsMenu.copyUsername=Copy username
loginsMenu.editLogin=Edit login
loginsMenu.delete=Delete
loginsDialog.confirmDelete=Delete this login?
@ -12,8 +13,15 @@ loginsDialog.copy=Copy
loginsDialog.confirm=OK
loginsDialog.cancel=Cancel
editLogin.fallbackTitle=Edit Login
editLogin.saved=Saved your changes
editLogin.couldNotSave=Changes could not be saved
loginsDetails.age=Age: %S days
loginsDetails.copyFailed=Copy failed
loginsDetails.passwordCopied=Password copied
loginsDetails.usernameCopied=Username copied
password-btn.show=Show
password-btn.hide=Hide

View File

@ -4,4 +4,8 @@
#filter substitution
# LOCALIZATION NOTE: this preference is set to true for en-US specifically,
# locales without this line have the setting set to false by default.
pref("browser.search.geoSpecificDefaults", true);
pref("general.useragent.locale", "@AB_CD@");

View File

@ -5,6 +5,14 @@
%filter substitution
%include defines.inc
html {
height: 100%;
}
body {
height: 100%;
}
.hidden {
display: none;
}
@ -75,6 +83,70 @@
margin: 0 5px;
}
.edit-login-icon {
background-image: url("resource://android/res/drawable-hdpi-v4/favicon_globe.png");
background-position: center;
background-size: 32px 32px;
background-repeat: no-repeat;
height: 32px;
width: 32px;
padding: 5px;
}
#edit-login-page {
background-color: #FFFFFF;
height: 100%;
}
#save-btn {
flex: 1;
height: 60px;
background-color: #E66000; /*matched to action_orange in java codebase*/
color: #FFFFFF;
font-size: 20px;
font-weight: bold;
border-radius: 4px;
border-width: 0px;
margin-top: 10px;
}
.password-btn-hide {
background-color: #777777;
color: white;
}
.edit-login-input {
flex: 1;
height: 36px;
font-size: 15px;
color: #222222;
border-radius: 4px;
border: solid 1px #AFB1B3;
padding-left: 10px;
}
.edit-login-div {
margin: 20px 30px;
display: flex;
flex-flow: row;
align-items: center;
}
#password-btn {
border-radius: 4px;
border-top-left-radius: 0em 0em;
border-bottom-left-radius: 0em 0em;
font-size: 15px;
height: 40px;
}
#password {
margin: none;
border-top-right-radius: 0em 0em;
border-bottom-right-radius: 0em 0em;
}
.icon {
background-image: url("resource://android/res/drawable-mdpi-v4/favicon_globe.png");
background-position: center;

View File

@ -0,0 +1,57 @@
# 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/.
from __future__ import absolute_import, print_function
import argparse
import sys
def android_version_code(buildid, cpu_arch=None, min_sdk=0, max_sdk=0):
base = int(str(buildid)[:10])
# None is interpreted as arm.
if not cpu_arch or cpu_arch in ['armeabi', 'armeabi-v7a']:
# Increment by MIN_SDK_VERSION -- this adds 9 to every build ID as a
# minimum. Our split APK starts at 11.
return base + min_sdk + 0
elif cpu_arch in ['x86']:
# Increment the version code by 3 for x86 builds so they are offered to
# x86 phones that have ARM emulators, beating the 2-point advantage that
# the v11+ ARMv7 APK has. If we change our splits in the future, we'll
# need to do this further still.
return base + min_sdk + 3
else:
raise ValueError("Don't know how to compute android:versionCode "
"for CPU arch " + cpu_arch)
def main(argv):
parser = argparse.ArgumentParser('Generate an android:versionCode',
add_help=False)
parser.add_argument('--verbose', action='store_true',
default=False,
help='Be verbose')
parser.add_argument('--with-android-cpu-arch', dest='cpu_arch',
choices=['armeabi', 'armeabi-v7a', 'mips', 'x86'],
help='The target CPU architecture')
parser.add_argument('--with-android-min-sdk-version', dest='min_sdk',
type=int, default=0,
help='The minimum target SDK')
parser.add_argument('--with-android-max-sdk-version', dest='max_sdk',
type=int, default=0,
help='The maximum target SDK')
parser.add_argument('buildid', type=int,
help='The input build ID')
args = parser.parse_args(argv)
code = android_version_code(args.buildid,
cpu_arch=args.cpu_arch,
min_sdk=args.min_sdk,
max_sdk=args.max_sdk)
print(code)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@ -0,0 +1,23 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from mozunit import main
import unittest
from mozbuild.android_version_code import android_version_code
class TestAndroidVersionCode(unittest.TestCase):
def test_basic(self):
# From https://treeherder.mozilla.org/#/jobs?repo=mozilla-central&revision=e25de9972a77.
buildid = '20150708104620'
arm_api9 = 2015070819
arm_api11 = 2015070821
x86_api9 = 2015070822
self.assertEqual(android_version_code(buildid, cpu_arch='armeabi', min_sdk=9, max_sdk=None), arm_api9)
self.assertEqual(android_version_code(buildid, cpu_arch='armeabi-v7a', min_sdk=11, max_sdk=None), arm_api11)
self.assertEqual(android_version_code(buildid, cpu_arch='x86', min_sdk=9, max_sdk=None), x86_api9)
if __name__ == '__main__':
main()

View File

@ -290,6 +290,8 @@ user_pref("browser.uitour.url", "http://%(server)s/uitour-dummy/tour");
// side-effect of preventing our geoip lookup.
user_pref("browser.search.isUS", true);
user_pref("browser.search.countryCode", "US");
// This will prevent HTTP requests for region defaults.
user_pref("browser.search.geoSpecificDefaults", false);
// Make sure the self support tab doesn't hit the network.
user_pref("browser.selfsupport.url", "https://%(server)s/selfsupport-dummy/");
@ -319,7 +321,6 @@ user_pref("network.proxy.pac_generator", false);
// Make tests run consistently on DevEdition (which has a lightweight theme
// selected by default).
user_pref("lightweightThemes.selectedThemeID", "");
user_pref("browser.devedition.theme.enabled", false);
// Disable periodic updates of service workers.
user_pref("dom.serviceWorkers.periodic-updates.enabled", false);

View File

@ -5,7 +5,7 @@
},
"global": {
"talos_repo": "https://hg.mozilla.org/build/talos",
"talos_revision": "554aa164ba0b"
"talos_revision": "deac3ce69268"
},
"extra_options": {
"android": [ "--apkPath=%(apk_path)s" ]

View File

@ -16,6 +16,8 @@ const LoginInfo =
Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
"nsILoginInfo", "init");
const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
/**
* Constants for password prompt telemetry.
* Mirrored in mobile/android/components/LoginManagerPrompter.js */
@ -775,22 +777,27 @@ LoginManagerPrompter.prototype = {
let { browser } = this._getNotifyWindow();
let saveMsgNames = {
prompt: "rememberPasswordMsgNoUsername",
buttonLabel: "notifyBarRememberPasswordButtonText",
buttonAccessKey: "notifyBarRememberPasswordButtonAccessKey",
prompt: login.username === "" ? "rememberLoginMsgNoUser"
: "rememberLoginMsg",
buttonLabel: "rememberLoginButtonText",
buttonAccessKey: "rememberLoginButtonAccessKey",
};
let changeMsgNames = {
// We reuse the existing message, even if it expects a username, until we
// switch to the final terminology in bug 1144856.
prompt: "updatePasswordMsg",
buttonLabel: "notifyBarUpdateButtonText",
buttonAccessKey: "notifyBarUpdateButtonAccessKey",
prompt: login.username === "" ? "updateLoginMsgNoUser"
: "updateLoginMsg",
buttonLabel: "updateLoginButtonText",
buttonAccessKey: "updateLoginButtonAccessKey",
};
let initialMsgNames = type == "password-save" ? saveMsgNames
: changeMsgNames;
let brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
let brandShortName = brandBundle.GetStringFromName("brandShortName");
let promptMsg = type == "password-save" ? this._getLocalizedString(saveMsgNames.prompt, [brandShortName])
: this._getLocalizedString(changeMsgNames.prompt);
let histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION"
: "PWMGR_PROMPT_UPDATE_ACTION";
let histogram = Services.telemetry.getHistogramById(histogramName);
@ -960,7 +967,7 @@ LoginManagerPrompter.prototype = {
this._getPopupNote().show(
browser,
"password",
this._getLocalizedString(initialMsgNames.prompt, [displayHost]),
promptMsg,
"password-notification-icon",
mainAction,
secondaryActions,

View File

@ -21,6 +21,10 @@ Login Manager test: notifications
/** Test for Login Manager: notifications. **/
const { Services } = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm");
const BRAND_BUNDLE = "chrome://branding/locale/brand.properties";
// Set testpath to the directory where we live. Used to load tests from
// alternate Mochitest servers (different hostnames, same content).
var testpath = document.location.pathname + "/../";
@ -95,6 +99,8 @@ function checkTest() {
var gotUser = SpecialPowers.wrap(iframe).contentDocument.getElementById("user").textContent;
var gotPass = SpecialPowers.wrap(iframe).contentDocument.getElementById("pass").textContent;
let brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
let brandShortName = brandBundle.GetStringFromName("brandShortName");
switch(testNum) {
@ -356,8 +362,8 @@ function checkTest() {
ok(popup, "got notification popup");
// Check the text, which comes from the localized saveLoginText string.
notificationText = popup.message;
expectedText = /^Would you like to remember the password on example.org\?$/;
ok(expectedText.test(notificationText), "Checking text: " + notificationText);
expectedText = "Would you like " + brandShortName + " to remember this login?";
is(expectedText, notificationText, "Checking text: " + notificationText);
popup.remove();
break;
@ -369,8 +375,8 @@ function checkTest() {
ok(popup, "got notification popup");
// Check the text, which comes from the localized saveLoginText string.
notificationText = popup.message;
expectedText = /^Would you like to remember the password on example.org\?$/;
ok(expectedText.test(notificationText), "Checking text: " + notificationText);
expectedText = "Would you like " + brandShortName + " to remember this login\?";
is(expectedText, notificationText, "Checking text: " + notificationText);
popup.remove();
break;
@ -382,8 +388,8 @@ function checkTest() {
ok(popup, "got notification popup");
// Check the text, which comes from the localized saveLoginTextNoUser string.
notificationText = popup.message;
expectedText = /^Would you like to remember the password on example.org\?$/;
ok(expectedText.test(notificationText), "Checking text: " + notificationText);
expectedText = "Would you like " + brandShortName + " to remember this password\?";
is(expectedText, notificationText, "Checking text: " + notificationText);
popup.remove();
break;

View File

@ -9,6 +9,7 @@ function run_test() {
// desired side-effect of preventing our geoip lookup.
Services.prefs.setBoolPref("browser.search.isUS", true);
Services.prefs.setCharPref("browser.search.countryCode", "US");
Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
run_next_test();
}

View File

@ -210,6 +210,11 @@ var OS_UNSUPPORTED_PARAMS = [
// specifies an updateURL, but not an updateInterval.
const SEARCH_DEFAULT_UPDATE_INTERVAL = 7;
// The default interval before checking again for the name of the
// default engine for the region, in seconds. Only used if the response
// from the server doesn't specify an interval.
const SEARCH_GEO_DEFAULT_UPDATE_INTERVAL = 2592000; // 30 days.
// Returns false for whitespace-only or commented out lines in a
// Sherlock file, true otherwise.
function isUsefulLine(aLine) {
@ -408,20 +413,21 @@ loadListener.prototype = {
onStatus: function (aRequest, aContext, aStatus, aStatusArg) {}
}
// Method to determine if we should be using geo-specific defaults
function geoSpecificDefaultsEnabled() {
// check to see if this is a partner build. Partner builds should not use geo-specific defaults.
let distroID;
function isPartnerBuild() {
try {
distroID = Services.prefs.getCharPref("distribution.id");
let distroID = Services.prefs.getCharPref("distribution.id");
// Mozilla-provided builds (i.e. funnelcake) are not partner builds
if (distroID && !distroID.startsWith("mozilla")) {
return false;
return true;
}
} catch (e) {}
// if we make it here, the pref should dictate behaviour
return false;
}
// Method to determine if we should be using geo-specific defaults
function geoSpecificDefaultsEnabled() {
let geoSpecificDefaults = false;
try {
geoSpecificDefaults = Services.prefs.getBoolPref("browser.search.geoSpecificDefaults");
@ -500,7 +506,7 @@ function getIsUS() {
// Helper method to modify preference keys with geo-specific modifiers, if needed.
function getGeoSpecificPrefName(basepref) {
if (!geoSpecificDefaultsEnabled())
if (!geoSpecificDefaultsEnabled() || isPartnerBuild())
return basepref;
if (getIsUS())
return basepref + ".US";
@ -532,16 +538,53 @@ function isUSTimezone() {
// If it fails we don't touch that pref so isUS() does its normal thing.
let ensureKnownCountryCode = Task.async(function* () {
// If we have a country-code already stored in our prefs we trust it.
let countryCode;
try {
Services.prefs.getCharPref("browser.search.countryCode");
return; // pref exists, so we've done this before.
countryCode = Services.prefs.getCharPref("browser.search.countryCode");
} catch(e) {}
// We don't have it cached, so fetch it. fetchCountryCode() will call
// storeCountryCode if it gets a result (even if that happens after the
// promise resolves)
yield fetchCountryCode();
if (!countryCode) {
// We don't have it cached, so fetch it. fetchCountryCode() will call
// storeCountryCode if it gets a result (even if that happens after the
// promise resolves) and fetchRegionDefault.
yield fetchCountryCode();
} else {
// if nothing to do, return early.
if (!geoSpecificDefaultsEnabled())
return;
let expir = engineMetadataService.getGlobalAttr("searchDefaultExpir") || 0;
if (expir > Date.now()) {
// The territory default we have already fetched hasn't expired yet.
// If we have an engine saved, the hash should be valid, verify it now.
let defaultEngine = engineMetadataService.getGlobalAttr("searchDefault");
if (!defaultEngine ||
engineMetadataService.getGlobalAttr("searchDefaultHash") == getVerificationHash(defaultEngine)) {
// No geo default, or valid hash; nothing to do.
return;
}
}
yield new Promise(resolve => {
let timeoutMS = Services.prefs.getIntPref("browser.search.geoip.timeout");
let timerId = setTimeout(() => {
timerId = null;
resolve();
}, timeoutMS);
let callback = () => {
clearTimeout(timerId);
resolve();
};
fetchRegionDefault().then(callback).catch(err => {
Components.utils.reportError(err);
callback();
});
});
}
// If gInitialized is true then the search service was forced to perform
// a sync initialization during our XHR - capture this via telemetry.
// a sync initialization during our XHRs - capture this via telemetry.
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT").add(gInitialized);
});
@ -628,9 +671,11 @@ function fetchCountryCode() {
// large value just incase the request never completes - we don't want the
// XHR object to live forever)
let timeoutMS = Services.prefs.getIntPref("browser.search.geoip.timeout");
let geoipTimeoutPossible = true;
let timerId = setTimeout(() => {
LOG("_fetchCountryCode: timeout fetching country information");
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(1);
if (geoipTimeoutPossible)
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(1);
timerId = null;
resolve();
}, timeoutMS);
@ -646,14 +691,29 @@ function fetchCountryCode() {
// This notification is just for tests...
Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "geoip-lookup-xhr-complete");
// If we've already timed out then we've already resolved the promise,
// so there's nothing else to do.
if (timerId == null) {
return;
if (timerId) {
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(0);
geoipTimeoutPossible = false;
}
let callback = () => {
// If we've already timed out then we've already resolved the promise,
// so there's nothing else to do.
if (timerId == null) {
return;
}
clearTimeout(timerId);
resolve();
};
if (result && geoSpecificDefaultsEnabled()) {
fetchRegionDefault().then(callback).catch(err => {
Components.utils.reportError(err);
callback();
});
} else {
callback();
}
Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT").add(0);
clearTimeout(timerId);
resolve();
};
let request = new XMLHttpRequest();
@ -683,6 +743,123 @@ function fetchCountryCode() {
});
}
// This will make an HTTP request to a Mozilla server that will return
// JSON data telling us what engine should be set as the default for
// the current region, and how soon we should check again.
//
// The optional cohort value returned by the server is to be kept locally
// and sent to the server the next time we ping it. It lets the server
// identify profiles that have been part of a specific experiment.
//
// This promise may take up to 100s to resolve, it's the caller's
// responsibility to ensure with a timer that we are not going to
// block the async init for too long.
let fetchRegionDefault = () => new Promise(resolve => {
let urlTemplate = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
.getCharPref("geoSpecificDefaults.url");
let endpoint = Services.urlFormatter.formatURL(urlTemplate);
// As an escape hatch, no endpoint means no region specific defaults.
if (!endpoint) {
resolve();
return;
}
// Append the optional cohort value.
const cohortPref = "browser.search.cohort";
let cohort;
try {
cohort = Services.prefs.getCharPref(cohortPref);
} catch(e) {}
if (cohort)
endpoint += "/" + cohort;
LOG("fetchRegionDefault starting with endpoint " + endpoint);
let startTime = Date.now();
let request = new XMLHttpRequest();
request.timeout = 100000; // 100 seconds as the last-chance fallback
request.onload = function(event) {
let took = Date.now() - startTime;
let status = event.target.status;
if (status != 200) {
LOG("fetchRegionDefault failed with HTTP code " + status);
let retryAfter = request.getResponseHeader("retry-after");
if (retryAfter) {
engineMetadataService.setGlobalAttr("searchDefaultExpir",
Date.now() + retryAfter * 1000);
}
resolve();
return;
}
let response = event.target.response || {};
LOG("received " + response.toSource());
if (response.cohort) {
Services.prefs.setCharPref(cohortPref, response.cohort);
} else {
Services.prefs.clearUserPref(cohortPref);
}
if (response.settings && response.settings.searchDefault) {
let defaultEngine = response.settings.searchDefault;
engineMetadataService.setGlobalAttr("searchDefault", defaultEngine);
let hash = getVerificationHash(defaultEngine);
LOG("fetchRegionDefault saved searchDefault: " + defaultEngine +
" with verification hash: " + hash);
engineMetadataService.setGlobalAttr("searchDefaultHash", hash);
}
let interval = response.interval || SEARCH_GEO_DEFAULT_UPDATE_INTERVAL;
let milliseconds = interval * 1000; // |interval| is in seconds.
engineMetadataService.setGlobalAttr("searchDefaultExpir",
Date.now() + milliseconds);
LOG("fetchRegionDefault got success response in " + took + "ms");
resolve();
};
request.ontimeout = function(event) {
LOG("fetchRegionDefault: XHR finally timed-out");
resolve();
};
request.onerror = function(event) {
LOG("fetchRegionDefault: failed to retrieve territory default information");
resolve();
};
request.open("GET", endpoint, true);
request.setRequestHeader("Content-Type", "application/json");
request.responseType = "json";
request.send();
});
function getVerificationHash(aName) {
let disclaimer = "By modifying this file, I agree that I am doing so " +
"only within $appName itself, using official, user-driven search " +
"engine selection processes, and in a way which does not circumvent " +
"user consent. I acknowledge that any attempt to change this file " +
"from outside of $appName is a malicious act, and will be responded " +
"to accordingly."
let salt = OS.Path.basename(OS.Constants.Path.profileDir) + aName +
disclaimer.replace(/\$appName/g, Services.appinfo.name);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
// Data is an array of bytes.
let data = converter.convertToByteArray(salt, {});
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA256);
hasher.update(data, data.length);
return hasher.finish(true);
}
/**
* Used to verify a given DOM node's localName and namespaceURI.
* @param aElement
@ -3368,25 +3545,34 @@ SearchService.prototype = {
return this.__sortedEngines;
},
// Get the original Engine object that belongs to the defaultenginename pref
// of the default branch.
// Get the original Engine object that is the default for this region,
// ignoring changes the user may have subsequently made.
get _originalDefaultEngine() {
let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
let nsIPLS = Ci.nsIPrefLocalizedString;
let defaultEngine;
let defPref = getGeoSpecificPrefName("defaultenginename");
try {
defaultEngine = defaultPrefB.getComplexValue(defPref, nsIPLS).data;
} catch (ex) {
// If the default pref is invalid (e.g. an add-on set it to a bogus value)
// getEngineByName will just return null, which is the best we can do.
let defaultEngine = engineMetadataService.getGlobalAttr("searchDefault");
if (defaultEngine &&
engineMetadataService.getGlobalAttr("searchDefaultHash") != getVerificationHash(defaultEngine)) {
LOG("get _originalDefaultEngine, invalid searchDefaultHash for: " + defaultEngine);
defaultEngine = "";
}
if (!defaultEngine) {
let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
let nsIPLS = Ci.nsIPrefLocalizedString;
let defPref = getGeoSpecificPrefName("defaultenginename");
try {
defaultEngine = defaultPrefB.getComplexValue(defPref, nsIPLS).data;
} catch (ex) {
// If the default pref is invalid (e.g. an add-on set it to a bogus value)
// getEngineByName will just return null, which is the best we can do.
}
}
return this.getEngineByName(defaultEngine);
},
resetToOriginalDefaultEngine: function SRCH_SVC__resetToOriginalDefaultEngine() {
this.defaultEngine = this._originalDefaultEngine;
this.currentEngine = this._originalDefaultEngine;
},
_buildCache: function SRCH_SVC__buildCache() {
@ -3714,6 +3900,7 @@ SearchService.prototype = {
},
_asyncReInit: function () {
LOG("_asyncReInit");
// Start by clearing the initialized state, so we don't abort early.
gInitialized = false;
@ -3729,8 +3916,14 @@ SearchService.prototype = {
Task.spawn(function* () {
try {
LOG("Restarting engineMetadataService");
yield engineMetadataService.init();
yield this._asyncLoadEngines();
yield ensureKnownCountryCode();
// Due to the HTTP requests done by ensureKnownCountryCode, it's possible that
// at this point a synchronous init has been forced by other code.
if (!gInitialized)
yield this._asyncLoadEngines();
// Typically we'll re-init as a result of a pref observer,
// so signal to 'callers' that we're done.
@ -4295,31 +4488,6 @@ SearchService.prototype = {
});
},
_getVerificationHash: function SRCH_SVC__getVerificationHash(aName) {
let disclaimer = "By modifying this file, I agree that I am doing so " +
"only within $appName itself, using official, user-driven search " +
"engine selection processes, and in a way which does not circumvent " +
"user consent. I acknowledge that any attempt to change this file " +
"from outside of $appName is a malicious act, and will be responded " +
"to accordingly."
let salt = OS.Path.basename(OS.Constants.Path.profileDir) + aName +
disclaimer.replace(/\$appName/g, Services.appinfo.name);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
// Data is an array of bytes.
let data = converter.convertToByteArray(salt, {});
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA256);
hasher.update(data, data.length);
return hasher.finish(true);
},
// nsIBrowserSearchService
init: function SRCH_SVC_init(observer) {
LOG("SearchService.init");
@ -4673,7 +4841,7 @@ SearchService.prototype = {
this._ensureInitialized();
if (!this._currentEngine) {
let name = engineMetadataService.getGlobalAttr("current");
if (engineMetadataService.getGlobalAttr("hash") == this._getVerificationHash(name)) {
if (engineMetadataService.getGlobalAttr("hash") == getVerificationHash(name)) {
this._currentEngine = this.getEngineByName(name);
}
}
@ -4713,7 +4881,7 @@ SearchService.prototype = {
}
engineMetadataService.setGlobalAttr("current", newName);
engineMetadataService.setGlobalAttr("hash", this._getVerificationHash(newName));
engineMetadataService.setGlobalAttr("hash", getVerificationHash(newName));
notifyAction(this._currentEngine, SEARCH_ENGINE_CURRENT);
},

View File

@ -251,6 +251,22 @@ function isUSTimezone() {
return UTCOffset >= 150 && UTCOffset <= 600;
}
const kDefaultenginenamePref = "browser.search.defaultenginename";
const kTestEngineName = "Test search engine";
const kLocalePref = "general.useragent.locale";
function getDefaultEngineName(isUS) {
const nsIPLS = Ci.nsIPrefLocalizedString;
// Copy the logic from nsSearchService
let pref = kDefaultenginenamePref;
if (isUS === undefined)
isUS = Services.prefs.getCharPref(kLocalePref) == "en-US" && isUSTimezone();
if (isUS) {
pref += ".US";
}
return Services.prefs.getComplexValue(pref, nsIPLS).data;
}
/**
* Waits for metadata being committed.
* @return {Promise} Resolved when the metadata is committed to disk.
@ -317,6 +333,8 @@ if (!isChild) {
Services.prefs.setIntPref("browser.search.geoip.timeout", 2000);
// But still disable geoip lookups - tests that need it will re-configure this.
Services.prefs.setCharPref("browser.search.geoip.url", "");
// Also disable region defaults - tests using it will also re-configure it.
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref("geoSpecificDefaults.url", "");
}
/**
@ -489,6 +507,24 @@ function waitForSearchNotification(aExpectedData) {
});
}
function asyncInit() {
return new Promise(resolve => {
Services.search.init(function() {
do_check_true(Services.search.isInitialized);
resolve();
});
});
}
function asyncReInit() {
let promise = waitForSearchNotification("reinit-complete");
Services.search.QueryInterface(Ci.nsIObserver)
.observe(null, "nsPref:changed", kLocalePref);
return promise;
}
// This "enum" from nsSearchService.js
const TELEMETRY_RESULT_ENUM = {
SUCCESS: 0,

View File

@ -0,0 +1,284 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var requests = [];
var gServerCohort = "";
const kUrlPref = "geoSpecificDefaults.url";
const kDayInSeconds = 86400;
const kYearInSeconds = kDayInSeconds * 365;
function run_test() {
updateAppInfo();
installTestEngine();
let srv = new HttpServer();
srv.registerPathHandler("/lookup_defaults", (metadata, response) => {
response.setStatusLine("1.1", 200, "OK");
let data = {interval: kYearInSeconds,
settings: {searchDefault: "Test search engine"}};
if (gServerCohort)
data.cohort = gServerCohort;
response.write(JSON.stringify(data));
requests.push(metadata);
});
srv.registerPathHandler("/lookup_fail", (metadata, response) => {
response.setStatusLine("1.1", 404, "Not Found");
requests.push(metadata);
});
srv.registerPathHandler("/lookup_unavailable", (metadata, response) => {
response.setStatusLine("1.1", 503, "Service Unavailable");
response.setHeader("Retry-After", kDayInSeconds.toString());
requests.push(metadata);
});
srv.start(-1);
do_register_cleanup(() => srv.stop(() => {}));
let url = "http://localhost:" + srv.identity.primaryPort + "/lookup_defaults?";
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
// Set a bogus user value so that running the test ensures we ignore it.
Services.prefs.setCharPref(BROWSER_SEARCH_PREF + kUrlPref, "about:blank");
Services.prefs.setCharPref("browser.search.geoip.url",
'data:application/json,{"country_code": "FR"}');
run_next_test();
}
function checkNoRequest() {
do_check_eq(requests.length, 0);
}
function checkRequest(cohort = "") {
do_check_eq(requests.length, 1);
let req = requests.pop();
do_check_eq(req._method, "GET");
do_check_eq(req._queryString, cohort ? "/" + cohort : "");
}
function promiseGlobalMetadata() {
return new Promise(resolve => Task.spawn(function* () {
let path = OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json");
let bytes = yield OS.File.read(path);
resolve(JSON.parse(new TextDecoder().decode(bytes))["[global]"]);
}));
}
function promiseSaveGlobalMetadata(globalData) {
return new Promise(resolve => Task.spawn(function* () {
let path = OS.Path.join(OS.Constants.Path.profileDir, "search-metadata.json");
let bytes = yield OS.File.read(path);
let data = JSON.parse(new TextDecoder().decode(bytes));
data["[global]"] = globalData;
yield OS.File.writeAtomic(path,
new TextEncoder().encode(JSON.stringify(data)));
resolve();
}));
}
let forceExpiration = Task.async(function* () {
let metadata = yield promiseGlobalMetadata();
// Make the current geodefaults expire 1s ago.
metadata.searchdefaultexpir = Date.now() - 1000;
yield promiseSaveGlobalMetadata(metadata);
});
add_task(function* no_request_if_prefed_off() {
// Disable geoSpecificDefaults and check no HTTP request is made.
Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
yield asyncInit();
checkNoRequest();
// The default engine should be set based on the prefs.
do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
// Change the default engine and then revert to default to ensure
// the metadata file exists.
let commitPromise = promiseAfterCommit();
Services.search.currentEngine = Services.search.getEngineByName(kTestEngineName);
Services.search.resetToOriginalDefaultEngine();
yield commitPromise;
// Ensure nothing related to geoSpecificDefaults has been written in the metadata.
let metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaultexpir, "undefined");
do_check_eq(typeof metadata.searchdefault, "undefined");
do_check_eq(typeof metadata.searchdefaulthash, "undefined");
Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
});
add_task(function* should_get_geo_defaults_only_once() {
// (Re)initializing the search service should trigger a request,
// and set the default engine based on it.
let commitPromise = promiseAfterCommit();
// Due to the previous initialization, we expect the countryCode to already be set.
do_check_true(Services.prefs.prefHasUserValue("browser.search.countryCode"));
do_check_eq(Services.prefs.getCharPref("browser.search.countryCode"), "FR");
yield asyncReInit();
checkRequest();
do_check_eq(Services.search.currentEngine.name, kTestEngineName);
yield commitPromise;
// Verify the metadata was written correctly.
let metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaultexpir, "number");
do_check_true(metadata.searchdefaultexpir > Date.now());
do_check_eq(typeof metadata.searchdefault, "string");
do_check_eq(metadata.searchdefault, "Test search engine");
do_check_eq(typeof metadata.searchdefaulthash, "string");
do_check_eq(metadata.searchdefaulthash.length, 44);
// The next restart shouldn't trigger a request.
yield asyncReInit();
checkNoRequest();
do_check_eq(Services.search.currentEngine.name, kTestEngineName);
});
add_task(function* should_request_when_countryCode_not_set() {
Services.prefs.clearUserPref("browser.search.countryCode");
let commitPromise = promiseAfterCommit();
yield asyncReInit();
checkRequest();
yield commitPromise;
});
add_task(function* should_recheck_if_interval_expired() {
yield forceExpiration();
let commitPromise = promiseAfterCommit();
let date = Date.now();
yield asyncReInit();
checkRequest();
yield commitPromise;
// Check that the expiration timestamp has been updated.
let metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaultexpir, "number");
do_check_true(metadata.searchdefaultexpir >= date + kYearInSeconds * 1000);
do_check_true(metadata.searchdefaultexpir < date + (kYearInSeconds + 3600) * 1000);
});
add_task(function* should_recheck_when_broken_hash() {
// This test verifies both that we ignore saved geo-defaults if the
// hash is invalid, and that we keep the local preferences-based
// default for all of the session in case a synchronous
// initialization was triggered before our HTTP request completed.
let metadata = yield promiseGlobalMetadata();
// Break the hash.
let hash = metadata.searchdefaulthash;
metadata.searchdefaulthash = "broken";
yield promiseSaveGlobalMetadata(metadata);
let commitPromise = promiseAfterCommit();
let reInitPromise = asyncReInit();
// Synchronously check the current default engine, to force a sync init.
// The hash is wrong, so we should fallback to the default engine from prefs.
// XXX For some reason forcing a sync init while already asynchronously
// reinitializing causes a shutdown warning related to engineMetadataService's
// finalize method having already been called. Seems harmless for the purpose
// of this test.
do_check_false(Services.search.isInitialized)
do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
do_check_true(Services.search.isInitialized)
yield reInitPromise;
checkRequest();
yield commitPromise;
// Check that the hash is back to its previous value.
metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaulthash, "string");
do_check_eq(metadata.searchdefaulthash, hash);
// The current default engine shouldn't change during a session.
do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
// After another restart, the current engine should be back to the geo default,
// without doing yet another request.
yield asyncReInit();
checkNoRequest();
do_check_eq(Services.search.currentEngine.name, kTestEngineName);
});
add_task(function* should_remember_cohort_id() {
// Check that initially the cohort pref doesn't exist.
const cohortPref = "browser.search.cohort";
do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_INVALID);
// Make the server send a cohort id.
let cohort = gServerCohort = "xpcshell";
// Trigger a new request.
yield forceExpiration();
let commitPromise = promiseAfterCommit();
yield asyncReInit();
checkRequest();
yield commitPromise;
// Check that the cohort was saved.
do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_STRING);
do_check_eq(Services.prefs.getCharPref(cohortPref), cohort);
// Make the server stop sending the cohort.
gServerCohort = "";
// Check that the next request sends the previous cohort id, and
// will remove it from the prefs due to the server no longer sending it.
yield forceExpiration();
commitPromise = promiseAfterCommit();
yield asyncReInit();
checkRequest(cohort);
yield commitPromise;
do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_INVALID);
});
add_task(function* should_retry_after_failure() {
let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
let originalUrl = defaultBranch.getCharPref(kUrlPref);
defaultBranch.setCharPref(kUrlPref, originalUrl.replace("defaults", "fail"));
// Trigger a new request.
yield forceExpiration();
yield asyncReInit();
checkRequest();
// After another restart, a new request should be triggered automatically without
// the test having to call forceExpiration again.
yield asyncReInit();
checkRequest();
});
add_task(function* should_honor_retry_after_header() {
let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
let originalUrl = defaultBranch.getCharPref(kUrlPref);
defaultBranch.setCharPref(kUrlPref, originalUrl.replace("fail", "unavailable"));
// Trigger a new request.
yield forceExpiration();
let date = Date.now();
let commitPromise = promiseAfterCommit();
yield asyncReInit();
checkRequest();
yield commitPromise;
// Check that the expiration timestamp has been updated.
let metadata = yield promiseGlobalMetadata();
do_check_eq(typeof metadata.searchdefaultexpir, "number");
do_check_true(metadata.searchdefaultexpir >= date + kDayInSeconds * 1000);
do_check_true(metadata.searchdefaultexpir < date + (kDayInSeconds + 3600) * 1000);
// After another restart, a new request should not be triggered.
yield asyncReInit();
checkNoRequest();
});

View File

@ -3,73 +3,11 @@
Components.utils.import("resource://gre/modules/osfile.jsm");
const kDefaultenginenamePref = "browser.search.defaultenginename";
const kSelectedEnginePref = "browser.search.selectedEngine";
const kTestEngineName = "Test search engine";
// These two functions (getLocale and getIsUS) are copied from nsSearchService.js
function getLocale() {
let LOCALE_PREF = "general.useragent.locale";
return Services.prefs.getCharPref(LOCALE_PREF);
}
function getIsUS() {
if (getLocale() != "en-US") {
return false;
}
// Timezone assumptions! We assume that if the system clock's timezone is
// between Newfoundland and Hawaii, that the user is in North America.
// This includes all of South America as well, but we have relatively few
// en-US users there, so that's OK.
// 150 minutes = 2.5 hours (UTC-2.5), which is
// Newfoundland Daylight Time (http://www.timeanddate.com/time/zones/ndt)
// 600 minutes = 10 hours (UTC-10), which is
// Hawaii-Aleutian Standard Time (http://www.timeanddate.com/time/zones/hast)
let UTCOffset = (new Date()).getTimezoneOffset();
let isNA = UTCOffset >= 150 && UTCOffset <= 600;
return isNA;
}
function getDefaultEngineName() {
const nsIPLS = Ci.nsIPrefLocalizedString;
// Copy the logic from nsSearchService
let pref = kDefaultenginenamePref;
if (getIsUS()) {
pref += ".US";
}
return Services.prefs.getComplexValue(pref, nsIPLS).data;
}
// waitForSearchNotification is in head_search.js
let waitForNotification = waitForSearchNotification;
function asyncInit() {
let deferred = Promise.defer();
Services.search.init(function() {
do_check_true(Services.search.isInitialized);
deferred.resolve();
});
return deferred.promise;
}
function asyncReInit() {
let promise = waitForNotification("reinit-complete");
Services.search.QueryInterface(Ci.nsIObserver)
.observe(null, "nsPref:changed", "general.useragent.locale");
return promise;
}
// Check that the default engine matches the defaultenginename pref
add_task(function* test_defaultEngine() {
yield asyncInit();
@ -138,7 +76,7 @@ add_task(function* test_ignoreInvalidHash() {
json["[global]"].hash = "invalid";
let data = new TextEncoder().encode(JSON.stringify(json));
let promise = OS.File.writeAtomic(path, data);//, { tmpPath: path + ".tmp" });
yield OS.File.writeAtomic(path, data);//, { tmpPath: path + ".tmp" });
// Re-init the search service, and check that the json file is ignored.
yield asyncReInit();

View File

@ -75,3 +75,4 @@ support-files =
[test_sync_profile_engine.js]
[test_rel_searchform.js]
[test_selectedEngine.js]
[test_geodefaults.js]

View File

@ -0,0 +1,67 @@
"use strict";
this.EXPORTED_SYMBOLS = ["UrlClassifierTestUtils"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
this.UrlClassifierTestUtils = {
addTestTrackers() {
const TABLE_PREF = "urlclassifier.trackingTable";
const TABLE_NAME = "test-track-simple";
// Add some URLs to the tracking database (to be blocked)
let testData = "tracking.example.com/";
let testUpdate =
"n:1000\ni:test-track-simple\nad:1\n" +
"a:524:32:" + testData.length + "\n" +
testData;
return this.useTestDatabase(TABLE_PREF, TABLE_NAME, testUpdate);
},
cleanupTestTrackers() {
const TABLE_PREF = "urlclassifier.trackingTable";
Services.prefs.clearUserPref(TABLE_PREF);
},
/**
* Add some entries to a test tracking protection database, and resets
* back to the default database after the test ends.
*
* @return {Promise}
*/
useTestDatabase(tablePref, tableName, update) {
Services.prefs.setCharPref(tablePref, tableName);
return new Promise((resolve, reject) => {
let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"].
getService(Ci.nsIUrlClassifierDBService);
let listener = {
QueryInterface: iid => {
if (iid.equals(Ci.nsISupports) ||
iid.equals(Ci.nsIUrlClassifierUpdateObserver))
return listener;
throw Cr.NS_ERROR_NO_INTERFACE;
},
updateUrlRequested: url => { },
streamFinished: status => { },
updateError: errorCode => {
reject("Couldn't update classifier.");
},
updateSuccess: requestedTimeout => {
resolve();
}
};
dbService.beginUpdate(listener, tableName, "");
dbService.beginStream("", "");
dbService.updateStream(update);
dbService.finishStream();
dbService.finishUpdate();
});
},
};

View File

@ -17,21 +17,13 @@
var Cc = SpecialPowers.Cc;
var Ci = SpecialPowers.Ci;
Components.utils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
// Add https://allowlisted.example.com to the permissions manager
SpecialPowers.addPermission("trackingprotection",
Ci.nsIPermissionManager.ALLOW_ACTION,
{ url: "https://allowlisted.example.com" });
// Add some URLs to the tracking database
var testData = "tracking.example.com/";
var testUpdate =
"n:1000\ni:test-track-simple\nad:1\n" +
"a:524:32:" + testData.length + "\n" +
testData;
var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
.getService(Ci.nsIUrlClassifierDBService);
function clearPermissions() {
SpecialPowers.removePermission("trackingprotection",
{ url: "https://allowlisted.example.com" });
@ -40,40 +32,18 @@ function clearPermissions() {
{ url: "https://allowlisted.example.com" }));
}
function doUpdate(update) {
var listener = {
QueryInterface: function(iid)
{
if (iid.equals(Ci.nsISupports) ||
iid.equals(Ci.nsIUrlClassifierUpdateObserver))
return this;
throw Cr.NS_ERROR_NO_INTERFACE;
},
updateUrlRequested: function(url) { },
streamFinished: function(status) { },
updateError: function(errorCode) {
ok(false, "Couldn't update classifier.");
// Abort test.
SimpleTest.finish();
},
updateSuccess: function(requestedTimeout) {
document.getElementById("testFrame").src = "allowlistAnnotatedFrame.html";
}
};
dbService.beginUpdate(listener, "test-track-simple", "");
dbService.beginStream("", "");
dbService.updateStream(update);
dbService.finishStream();
dbService.finishUpdate();
}
SpecialPowers.pushPrefEnv(
{"set" : [["urlclassifier.trackingTable", "test-track-simple"],
["privacy.trackingprotection.enabled", true],
["channelclassifier.allowlist_example", true]]},
function() { doUpdate(testUpdate); });
test);
function test() {
SimpleTest.registerCleanupFunction(UrlClassifierTestUtils.cleanupTestTrackers);
UrlClassifierTestUtils.addTestTrackers().then(() => {
document.getElementById("testFrame").src = "allowlistAnnotatedFrame.html";
});
}
// Expected finish() call is in "allowlistedAnnotatedFrame.html".
SimpleTest.waitForExplicitFinish();

View File

@ -17,58 +17,27 @@
var Cc = SpecialPowers.Cc;
var Ci = SpecialPowers.Ci;
// Add some URLs to the tracking database
var testData = "tracking.example.com/";
var testUpdate =
"n:1000\ni:test-track-simple\nad:1\n" +
"a:524:32:" + testData.length + "\n" +
testData;
Components.utils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
function cleanup() {
SpecialPowers.clearUserPref("privacy.trackingprotection.enabled");
SpecialPowers.clearUserPref("channelclassifier.allowlist_example");
}
var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
.getService(Ci.nsIUrlClassifierDBService);
function doUpdate(update) {
var listener = {
QueryInterface: function(iid)
{
if (iid.equals(Ci.nsISupports) ||
iid.equals(Ci.nsIUrlClassifierUpdateObserver))
return this;
throw Cr.NS_ERROR_NO_INTERFACE;
},
updateUrlRequested: function(url) { },
streamFinished: function(status) { },
updateError: function(errorCode) {
ok(false, "Couldn't update classifier.");
// Abort test.
SimpleTest.finish();
},
updateSuccess: function(requestedTimeout) {
SpecialPowers.setBoolPref("privacy.trackingprotection.enabled", true);
// Make sure chrome:// URIs are processed. This does not white-list
// any URIs unless 'https://allowlisted.example.com' is added in the
// permission manager (see test_allowlisted_annotations.html)
SpecialPowers.setBoolPref("channelclassifier.allowlist_example", true);
document.getElementById("testFrame").src = "classifiedAnnotatedFrame.html";
}
};
dbService.beginUpdate(listener, "test-track-simple", "");
dbService.beginStream("", "");
dbService.updateStream(update);
dbService.finishStream();
dbService.finishUpdate();
}
SpecialPowers.pushPrefEnv(
{"set" : [["urlclassifier.trackingTable", "test-track-simple"]]},
function() { doUpdate(testUpdate); });
test);
function test() {
UrlClassifierTestUtils.addTestTrackers().then(() => {
SpecialPowers.setBoolPref("privacy.trackingprotection.enabled", true);
// Make sure chrome:// URIs are processed. This does not white-list
// any URIs unless 'https://allowlisted.example.com' is added in the
// permission manager (see test_allowlisted_annotations.html)
SpecialPowers.setBoolPref("channelclassifier.allowlist_example", true);
document.getElementById("testFrame").src = "classifiedAnnotatedFrame.html";
});
}
// Expected finish() call is in "classifiedAnnotatedFrame.html".
SimpleTest.waitForExplicitFinish();

View File

@ -28,6 +28,7 @@ var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
var contentPage = "chrome://mochitests/content/chrome/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedPBFrame.html"
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
function whenDelayedStartupFinished(aWindow, aCallback) {
Services.obs.addObserver(function observer(aSubject, aTopic) {
@ -60,13 +61,6 @@ function testOnWindow(aPrivate, aCallback) {
}, true);
}
// Add some URLs to the tracking database
var testData = "tracking.example.com/";
var testUpdate =
"n:1000\ni:test-track-simple\nad:1\n" +
"a:524:32:" + testData.length + "\n" +
testData;
var badids = [
"badscript",
"badimage",
@ -117,62 +111,37 @@ function checkLoads(aWindow, aBlocked) {
is(allNodeMatch, aBlocked, "All tracking nodes are expected to be annotated as such");
}
var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
.getService(Ci.nsIUrlClassifierDBService);
function doUpdate(update) {
var listener = {
QueryInterface: function(iid)
{
if (iid.equals(Ci.nsISupports) ||
iid.equals(Ci.nsIUrlClassifierUpdateObserver))
return this;
throw Cr.NS_ERROR_NO_INTERFACE;
},
updateUrlRequested: function(url) { },
streamFinished: function(status) { },
updateError: function(errorCode) {
ok(false, "Couldn't update classifier.");
// Abort test.
SimpleTest.finish();
},
updateSuccess: function(requestedTimeout) {
// Normal mode, with the pref (trackers should be loaded)
testOnWindow(false, function(aWindow) {
checkLoads(aWindow, false);
aWindow.close();
// Private Browsing, with the pref (trackers should be blocked)
testOnWindow(true, function(aWindow) {
checkLoads(aWindow, true);
aWindow.close();
// Private Browsing, without the pref (trackers should be loaded)
SpecialPowers.setBoolPref("privacy.trackingprotection.pbmode.enabled", false);
testOnWindow(true, function(aWindow) {
checkLoads(aWindow, false);
aWindow.close();
SimpleTest.finish();
});
});
});
}
};
dbService.beginUpdate(listener, "test-track-simple", "");
dbService.beginStream("", "");
dbService.updateStream(update);
dbService.finishStream();
dbService.finishUpdate();
}
SpecialPowers.pushPrefEnv(
{"set" : [["urlclassifier.trackingTable", "test-track-simple"],
["privacy.trackingprotection.enabled", false],
["privacy.trackingprotection.pbmode.enabled", true],
["channelclassifier.allowlist_example", true]]},
function() { doUpdate(testUpdate); });
test);
function test() {
SimpleTest.registerCleanupFunction(UrlClassifierTestUtils.cleanupTestTrackers);
UrlClassifierTestUtils.addTestTrackers().then(() => {
// Normal mode, with the pref (trackers should be loaded)
testOnWindow(false, function(aWindow) {
checkLoads(aWindow, false);
aWindow.close();
// Private Browsing, with the pref (trackers should be blocked)
testOnWindow(true, function(aWindow) {
checkLoads(aWindow, true);
aWindow.close();
// Private Browsing, without the pref (trackers should be loaded)
SpecialPowers.setBoolPref("privacy.trackingprotection.pbmode.enabled", false);
testOnWindow(true, function(aWindow) {
checkLoads(aWindow, false);
aWindow.close();
SimpleTest.finish();
});
});
});
});
}
SimpleTest.waitForExplicitFinish();

View File

@ -28,6 +28,7 @@ var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
var contentPage = "chrome://mochitests/content/chrome/toolkit/components/url-classifier/tests/mochitest/classifiedAnnotatedPBFrame.html"
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
function whenDelayedStartupFinished(aWindow, aCallback) {
Services.obs.addObserver(function observer(aSubject, aTopic) {
@ -60,13 +61,6 @@ function testOnWindow(aCallback) {
}, true);
}
// Add some URLs to the tracking database
var testData = "tracking.example.com/";
var testUpdate =
"n:1000\ni:test-track-simple\nad:1\n" +
"a:524:32:" + testData.length + "\n" +
testData;
var badids = [
"badscript"
];
@ -76,57 +70,32 @@ function checkLoads(aWindow, aBlocked) {
is(win.document.getElementById("badscript").dataset.touched, aBlocked ? "no" : "yes", "Should not load tracking javascript");
}
var dbService = Cc["@mozilla.org/url-classifier/dbservice;1"]
.getService(Ci.nsIUrlClassifierDBService);
function doUpdate(update) {
var listener = {
QueryInterface: function(iid)
{
if (iid.equals(Ci.nsISupports) ||
iid.equals(Ci.nsIUrlClassifierUpdateObserver))
return this;
throw Cr.NS_ERROR_NO_INTERFACE;
},
updateUrlRequested: function(url) { },
streamFinished: function(status) { },
updateError: function(errorCode) {
ok(false, "Couldn't update classifier.");
// Abort test.
SimpleTest.finish();
},
updateSuccess: function(requestedTimeout) {
// Safe Browsing turned OFF, tracking protection should work nevertheless
testOnWindow(function(aWindow) {
checkLoads(aWindow, true);
aWindow.close();
// Safe Browsing turned ON, tracking protection should still work
SpecialPowers.setBoolPref("browser.safebrowsing.enabled", true);
testOnWindow(function(aWindow) {
checkLoads(aWindow, true);
aWindow.close();
SimpleTest.finish();
});
});
}
};
dbService.beginUpdate(listener, "test-track-simple", "");
dbService.beginStream("", "");
dbService.updateStream(update);
dbService.finishStream();
dbService.finishUpdate();
}
SpecialPowers.pushPrefEnv(
{"set" : [["urlclassifier.trackingTable", "test-track-simple"],
["privacy.trackingprotection.enabled", true],
["browser.safebrowsing.malware.enabled", false],
["browser.safebrowsing.enabled", false],
["channelclassifier.allowlist_example", true]]},
function() { doUpdate(testUpdate); });
test);
function test() {
SimpleTest.registerCleanupFunction(UrlClassifierTestUtils.cleanupTestTrackers);
UrlClassifierTestUtils.addTestTrackers().then(() => {
// Safe Browsing turned OFF, tracking protection should work nevertheless
testOnWindow(function(aWindow) {
checkLoads(aWindow, true);
aWindow.close();
// Safe Browsing turned ON, tracking protection should still work
SpecialPowers.setBoolPref("browser.safebrowsing.enabled", true);
testOnWindow(function(aWindow) {
checkLoads(aWindow, true);
aWindow.close();
SimpleTest.finish();
});
});
});
}
SimpleTest.waitForExplicitFinish();

View File

@ -10,6 +10,10 @@ XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
JAR_MANIFESTS += ['jar.mn']
TESTING_JS_MODULES += [
'UrlClassifierTestUtils.jsm',
]
#DEFINES['MOZILLA_INTERNAL_API'] = True
# XXX Get this to work in libxul builds.

View File

@ -90,6 +90,15 @@ nsURLFormatterService.prototype = {
LOCALE: function() Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry).
getSelectedLocale('global'),
REGION: function() {
try {
// When the geoip lookup failed to identify the region, we fallback to
// the 'ZZ' region code to mean 'unknown'.
return Services.prefs.getCharPref("browser.search.region") || "ZZ";
} catch(e) {
return "ZZ";
}
},
VENDOR: function() this.appInfo.vendor,
NAME: function() this.appInfo.name,
ID: function() this.appInfo.ID,

View File

@ -133,10 +133,13 @@ exports.items = [
}
// Click handler
if (imageSummary.filename) {
if (imageSummary.href || imageSummary.filename) {
root.style.cursor = "pointer";
root.addEventListener("click", () => {
if (imageSummary.filename) {
if (imageSummary.href) {
const gBrowser = context.environment.chromeWindow.gBrowser;
gBrowser.selectedTab = gBrowser.addTab(imageSummary.href);
} else if (imageSummary.filename) {
const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
file.initWithPath(imageSummary.filename);
file.reveal();
@ -406,16 +409,15 @@ function uploadToImgur(reply) {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
reply.href = xhr.response.data.link;
reply.destinations.push(l10n.lookupFormat("screenshotImgurError",
reply.destinations.push(l10n.lookupFormat("screenshotImgurUploaded",
[ reply.href ]));
}
else {
} else {
reply.destinations.push(l10n.lookup("screenshotImgurError"));
}
resolve();
}
}
};
});
}

View File

@ -45,6 +45,11 @@ function addParamGroups(command) {
* Get a data block for the help_man.html/help_man.txt templates
*/
function getHelpManData(commandData, context) {
// Filter out hidden parameters
commandData.command.params = commandData.command.params.filter(
param => !param.hidden
);
addParamGroups(commandData.command);
commandData.subcommands.forEach(addParamGroups);

Some files were not shown because too many files have changed in this diff Show More