зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1203168 - ask the user for confirmation when searching with a default search engine of unknown origin, data-review=bsmedberg, r=adw.
This commit is contained in:
Родитель
b08e506896
Коммит
e9ee2349cc
|
@ -369,6 +369,8 @@ pref("browser.search.redirectWindowsSearch", true);
|
|||
pref("browser.search.redirectWindowsSearch", false);
|
||||
#endif
|
||||
|
||||
pref("browser.search.reset.enabled", true);
|
||||
|
||||
pref("browser.usedOnWindows10", false);
|
||||
pref("browser.usedOnWindows10.introURL", "https://www.mozilla.org/%LOCALE%/firefox/windows-10/welcome/?utm_source=firefox-browser&utm_medium=firefox-browser");
|
||||
|
||||
|
|
|
@ -7052,7 +7052,7 @@ var gIdentityHandler = {
|
|||
this._uriHasHost = false;
|
||||
}
|
||||
|
||||
let whitelist = /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
|
||||
let whitelist = /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|searchreset|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
|
||||
this._isSecureInternalUI = uri.schemeIs("about") && whitelist.test(uri.path);
|
||||
|
||||
// Create a channel for the sole purpose of getting the resolved URI
|
||||
|
|
|
@ -73,6 +73,9 @@ static RedirEntry kRedirMap[] = {
|
|||
{ "robots", "chrome://browser/content/aboutRobots.xhtml",
|
||||
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
|
||||
nsIAboutModule::ALLOW_SCRIPT },
|
||||
{ "searchreset", "chrome://browser/content/search/searchReset.xhtml",
|
||||
nsIAboutModule::ALLOW_SCRIPT |
|
||||
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
|
||||
{ "sessionrestore", "chrome://browser/content/aboutSessionRestore.xhtml",
|
||||
nsIAboutModule::ALLOW_SCRIPT },
|
||||
{ "welcomeback", "chrome://browser/content/aboutWelcomeBack.xhtml",
|
||||
|
|
|
@ -96,6 +96,7 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
|
|||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "privatebrowsing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "rights", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "robots", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "searchreset", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "sessionrestore", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "welcomeback", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-tabs", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const TELEMETRY_RESULT_ENUM = {
|
||||
RESTORED_DEFAULT: 0,
|
||||
KEPT_CURRENT: 1,
|
||||
CHANGED_ENGINE: 2,
|
||||
CLOSED_PAGE: 3
|
||||
};
|
||||
|
||||
window.onload = function() {
|
||||
let list = document.getElementById("defaultEngine");
|
||||
let originalDefault = Services.search.originalDefaultEngine.name;
|
||||
Services.search.getDefaultEngines().forEach(e => {
|
||||
let opt = document.createElement("option");
|
||||
opt.setAttribute("value", e.name);
|
||||
opt.engine = e;
|
||||
opt.textContent = e.name;
|
||||
if (e.iconURI)
|
||||
opt.style.backgroundImage = 'url("' + e.iconURI.spec + '")';
|
||||
if (e.name == originalDefault)
|
||||
opt.setAttribute("selected", "true");
|
||||
list.appendChild(opt);
|
||||
});
|
||||
|
||||
let updateIcon = () => {
|
||||
list.style.setProperty("--engine-icon-url",
|
||||
list.selectedOptions[0].style.backgroundImage);
|
||||
};
|
||||
|
||||
list.addEventListener("change", updateIcon);
|
||||
// When selecting using the keyboard, the 'change' event is only fired after
|
||||
// the user presses <enter> or moves the focus elsewhere.
|
||||
// keypress/keyup fire too late and cause flicker when updating the icon.
|
||||
// keydown fires too early and the selected option isn't changed yet.
|
||||
list.addEventListener("keydown", () => {
|
||||
Services.tm.mainThread.dispatch(updateIcon, Ci.nsIThread.DISPATCH_NORMAL);
|
||||
});
|
||||
updateIcon();
|
||||
|
||||
document.getElementById("searchResetChangeEngine").focus();
|
||||
window.addEventListener("unload", recordPageClosed);
|
||||
};
|
||||
|
||||
function doSearch() {
|
||||
let queryString = "";
|
||||
let purpose = "";
|
||||
let params = window.location.href.match(/^about:searchreset\?([^#]*)/);
|
||||
if (params) {
|
||||
params = params[1].split("&");
|
||||
for (let param of params) {
|
||||
if (param.startsWith("data="))
|
||||
queryString = decodeURIComponent(param.slice(5));
|
||||
else if (param.startsWith("purpose="))
|
||||
purpose = param.slice(8);
|
||||
}
|
||||
}
|
||||
|
||||
let engine = Services.search.currentEngine;
|
||||
let submission = engine.getSubmission(queryString, null, purpose);
|
||||
|
||||
window.removeEventListener("unload", recordPageClosed);
|
||||
|
||||
let win = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
win.openUILinkIn(submission.uri.spec, "current", false, submission.postData);
|
||||
}
|
||||
|
||||
function record(result) {
|
||||
Services.telemetry.getHistogramById("SEARCH_RESET_RESULT").add(result);
|
||||
}
|
||||
|
||||
function keepCurrentEngine() {
|
||||
// Calling the currentEngine setter will force a correct loadPathHash to be
|
||||
// written for this engine, so that we don't prompt the user again.
|
||||
Services.search.currentEngine = Services.search.currentEngine;
|
||||
record(TELEMETRY_RESULT_ENUM.KEPT_CURRENT);
|
||||
doSearch();
|
||||
}
|
||||
|
||||
function changeSearchEngine() {
|
||||
let list = document.getElementById("defaultEngine");
|
||||
let engine = list.selectedOptions[0].engine;
|
||||
if (engine.hidden)
|
||||
engine.hidden = false;
|
||||
Services.search.currentEngine = engine;
|
||||
|
||||
// Record if we restored the original default or changed to another engine.
|
||||
let originalDefault = Services.search.originalDefaultEngine.name;
|
||||
let code = TELEMETRY_RESULT_ENUM.CHANGED_ENGINE;
|
||||
if (Services.search.originalDefaultEngine.name == engine.name)
|
||||
code = TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT;
|
||||
record(code);
|
||||
|
||||
doSearch();
|
||||
}
|
||||
|
||||
function recordPageClosed() {
|
||||
record(TELEMETRY_RESULT_ENUM.CLOSED_PAGE);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
|
||||
%htmlDTD;
|
||||
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
|
||||
%globalDTD;
|
||||
<!ENTITY % searchresetDTD SYSTEM "chrome://browser/locale/aboutSearchReset.dtd">
|
||||
%searchresetDTD;
|
||||
]>
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<head>
|
||||
<title>&searchreset.tabtitle;</title>
|
||||
<link rel="stylesheet" type="text/css" media="all"
|
||||
href="chrome://global/skin/in-content/info-pages.css"/>
|
||||
<link rel="stylesheet" type="text/css" media="all"
|
||||
href="chrome://browser/skin/searchReset.css"/>
|
||||
<link rel="icon" type="image/png"
|
||||
href="chrome://browser/skin/favicon-search-16.svg"/>
|
||||
|
||||
<script type="application/javascript;version=1.8"
|
||||
src="chrome://browser/content/search/searchReset.js"/>
|
||||
</head>
|
||||
|
||||
<body dir="&locale.dir;">
|
||||
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
<h1 class="title-text">&searchreset.pageTitle;</h1>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
<p>&searchreset.pageInfo1;</p>
|
||||
<p>&searchreset.selector.label;
|
||||
<select id="defaultEngine"></select>
|
||||
</p>
|
||||
|
||||
<p>&searchreset.beforelink.pageInfo2;<a id="linkSettingsPage" href="about:preferences#search">&searchreset.link.pageInfo2;</a>&searchreset.afterlink.pageInfo2;</p>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<xul:button id="searchResetKeepCurrent"
|
||||
label="&searchreset.noChangeButton;"
|
||||
accesskey="&searchreset.noChangeButton.access;"
|
||||
oncommand="keepCurrentEngine();"/>
|
||||
<xul:button class="primary"
|
||||
id="searchResetChangeEngine"
|
||||
label="&searchreset.changeEngineButton;"
|
||||
accesskey="&searchreset.changeEngineButton.access;"
|
||||
oncommand="changeSearchEngine();"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -5,3 +5,5 @@
|
|||
browser.jar:
|
||||
content/browser/search/search.xml (content/search.xml)
|
||||
content/browser/search/searchbarBindings.css (content/searchbarBindings.css)
|
||||
content/browser/search/searchReset.xhtml (content/searchReset.xhtml)
|
||||
content/browser/search/searchReset.js (content/searchReset.js)
|
||||
|
|
|
@ -37,6 +37,7 @@ skip-if = os == "mac" # bug 967013
|
|||
[browser_yahoo_behavior.js]
|
||||
[browser_abouthome_behavior.js]
|
||||
skip-if = true # Bug ??????, Bug 1100301 - leaks windows until shutdown when --run-by-dir
|
||||
[browser_aboutSearchReset.js]
|
||||
[browser_searchbar_openpopup.js]
|
||||
skip-if = os == "linux" # Linux has different focus behaviours.
|
||||
[browser_searchbar_keyboard_navigation.js]
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
const TELEMETRY_RESULT_ENUM = {
|
||||
RESTORED_DEFAULT: 0,
|
||||
KEPT_CURRENT: 1,
|
||||
CHANGED_ENGINE: 2,
|
||||
CLOSED_PAGE: 3
|
||||
};
|
||||
|
||||
const kSearchStr = "a search";
|
||||
const kSearchPurpose = "searchbar";
|
||||
|
||||
const kTestEngine = "testEngine.xml";
|
||||
|
||||
function checkTelemetryRecords(expectedValue) {
|
||||
let histogram = Services.telemetry.getHistogramById("SEARCH_RESET_RESULT");
|
||||
let snapshot = histogram.snapshot();
|
||||
// The probe is declared with 5 values, but we get 6 back from .counts
|
||||
let expectedCounts = [0, 0, 0, 0, 0, 0];
|
||||
if (expectedValue != null) {
|
||||
expectedCounts[expectedValue] = 1;
|
||||
}
|
||||
Assert.deepEqual(snapshot.counts, expectedCounts,
|
||||
"histogram has expected content");
|
||||
histogram.clear();
|
||||
}
|
||||
|
||||
function promiseStoppedLoad(expectedURL) {
|
||||
return new Promise(resolve => {
|
||||
let browser = gBrowser.selectedBrowser;
|
||||
let original = browser.loadURIWithFlags;
|
||||
browser.loadURIWithFlags = function(URI) {
|
||||
if (URI == expectedURL) {
|
||||
browser.loadURIWithFlags = original;
|
||||
ok(true, "loaded expected url: " + URI);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
original.apply(browser, arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
var gTests = [
|
||||
|
||||
{
|
||||
desc: "Test the 'Keep Current Settings' button.",
|
||||
run: function* () {
|
||||
let engine = yield promiseNewEngine(kTestEngine, {setAsCurrent: true});
|
||||
|
||||
let expectedURL = engine.
|
||||
getSubmission(kSearchStr, null, kSearchPurpose).
|
||||
uri.spec;
|
||||
|
||||
let rawEngine = engine.wrappedJSObject;
|
||||
let initialHash = rawEngine.getAttr("loadPathHash");
|
||||
rawEngine.setAttr("loadPathHash", "broken");
|
||||
|
||||
let loadPromise = promiseStoppedLoad(expectedURL);
|
||||
gBrowser.contentDocument.getElementById("searchResetKeepCurrent").click();
|
||||
yield loadPromise;
|
||||
|
||||
is(engine, Services.search.currentEngine,
|
||||
"the custom engine is still default");
|
||||
is(rawEngine.getAttr("loadPathHash"), initialHash,
|
||||
"the loadPathHash has been fixed");
|
||||
|
||||
checkTelemetryRecords(TELEMETRY_RESULT_ENUM.KEPT_CURRENT);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "Test the 'Restore Search Defaults' button.",
|
||||
run: function* () {
|
||||
let currentEngine = Services.search.currentEngine;
|
||||
let originalEngine = Services.search.originalDefaultEngine;
|
||||
let expectedURL = originalEngine.
|
||||
getSubmission(kSearchStr, null, kSearchPurpose).
|
||||
uri.spec;
|
||||
|
||||
let loadPromise = promiseStoppedLoad(expectedURL);
|
||||
let doc = gBrowser.contentDocument;
|
||||
let button = doc.getElementById("searchResetChangeEngine");
|
||||
is(doc.activeElement, button,
|
||||
"the 'Change Search Engine' button is focused");
|
||||
button.click();
|
||||
yield loadPromise;
|
||||
|
||||
is(originalEngine, Services.search.currentEngine,
|
||||
"the default engine is back to the original one");
|
||||
|
||||
checkTelemetryRecords(TELEMETRY_RESULT_ENUM.RESTORED_DEFAULT);
|
||||
Services.search.currentEngine = currentEngine;
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "Test the engine selector drop down.",
|
||||
run: function* () {
|
||||
let originalEngineName = Services.search.originalDefaultEngine.name;
|
||||
|
||||
let doc = gBrowser.contentDocument;
|
||||
let list = doc.getElementById("defaultEngine");
|
||||
is(list.value, originalEngineName,
|
||||
"the default selection of the dropdown is the original default engine");
|
||||
|
||||
let defaultEngines = Services.search.getDefaultEngines();
|
||||
is(list.childNodes.length, defaultEngines.length,
|
||||
"the dropdown has the correct count of engines");
|
||||
|
||||
// Select an engine that isn't the original default one.
|
||||
let engine;
|
||||
for (let i = 0; i < defaultEngines.length; ++i) {
|
||||
if (defaultEngines[i].name != originalEngineName) {
|
||||
engine = defaultEngines[i];
|
||||
engine.hidden = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
list.value = engine.name;
|
||||
|
||||
let expectedURL = engine.getSubmission(kSearchStr, null, kSearchPurpose)
|
||||
.uri.spec;
|
||||
let loadPromise = promiseStoppedLoad(expectedURL);
|
||||
doc.getElementById("searchResetChangeEngine").click();
|
||||
yield loadPromise;
|
||||
|
||||
ok(!engine.hidden, "the selected engine has been unhidden");
|
||||
is(engine, Services.search.currentEngine,
|
||||
"the current engine is what was selected in the drop down");
|
||||
|
||||
checkTelemetryRecords(TELEMETRY_RESULT_ENUM.CHANGED_ENGINE);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: "Load another page without clicking any of the buttons.",
|
||||
run: function* () {
|
||||
yield promiseTabLoadEvent(gBrowser.selectedTab, "about:mozilla");
|
||||
|
||||
checkTelemetryRecords(TELEMETRY_RESULT_ENUM.CLOSED_PAGE);
|
||||
}
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
function test()
|
||||
{
|
||||
waitForExplicitFinish();
|
||||
Task.spawn(function* () {
|
||||
let oldCanRecord = Services.telemetry.canRecordExtended;
|
||||
Services.telemetry.canRecordExtended = true;
|
||||
checkTelemetryRecords();
|
||||
|
||||
for (let test of gTests) {
|
||||
info(test.desc);
|
||||
|
||||
// Create a tab to run the test.
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
|
||||
|
||||
// Start loading about:searchreset and wait for it to complete.
|
||||
let url = "about:searchreset?data=" + encodeURIComponent(kSearchStr) +
|
||||
"&purpose=" + kSearchPurpose;
|
||||
yield promiseTabLoadEvent(tab, url);
|
||||
|
||||
info("Running test");
|
||||
yield test.run();
|
||||
|
||||
info("Cleanup");
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
|
||||
Services.telemetry.canRecordExtended = oldCanRecord;
|
||||
}).then(finish, ex => {
|
||||
ok(false, "Unexpected Exception: " + ex);
|
||||
finish();
|
||||
});
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
/**
|
||||
* Recursively compare two objects and check that every property of expectedObj has the same value
|
||||
* on actualObj.
|
||||
|
@ -85,3 +87,52 @@ function promiseNewEngine(basename, options = {}) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a load (or custom) event to finish in a given tab. If provided
|
||||
* load an uri into the tab.
|
||||
*
|
||||
* @param tab
|
||||
* The tab to load into.
|
||||
* @param [optional] url
|
||||
* The url to load, or the current url.
|
||||
* @return {Promise} resolved when the event is handled.
|
||||
* @resolves to the received event
|
||||
* @rejects if a valid load event is not received within a meaningful interval
|
||||
*/
|
||||
function promiseTabLoadEvent(tab, url)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
info("Wait tab event: load");
|
||||
|
||||
function handle(loadedUrl) {
|
||||
if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
|
||||
info(`Skipping spurious load event for ${loadedUrl}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
info("Tab event received: load");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create two promises: one resolved from the content process when the page
|
||||
// loads and one that is rejected if we take too long to load the url.
|
||||
let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
deferred.reject(new Error("Timed out while waiting for a 'load' event"));
|
||||
}, 30000);
|
||||
|
||||
loaded.then(() => {
|
||||
clearTimeout(timeout);
|
||||
deferred.resolve()
|
||||
});
|
||||
|
||||
if (url)
|
||||
BrowserTestUtils.loadURI(tab.linkedBrowser, url);
|
||||
|
||||
// Promise.all rejects if either promise rejects (i.e. if we time out) and
|
||||
// if our loaded promise resolves before the timeout, then we resolve the
|
||||
// timeout promise as well, causing the all promise to resolve.
|
||||
return Promise.all([deferred.promise, loaded]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<!-- 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/. -->
|
||||
|
||||
<!ENTITY searchreset.tabtitle "Restore Search Settings">
|
||||
|
||||
<!ENTITY searchreset.pageTitle "Restore your search settings?">
|
||||
|
||||
<!ENTITY searchreset.pageInfo1 "Your search settings might be out-of-date. Firefox can help you restore the default search settings.">
|
||||
|
||||
<!ENTITY searchreset.selector.label "This will set your default search engine to">
|
||||
|
||||
<!-- LOCALIZATION NOTE (searchreset.beforelink.pageInfo,
|
||||
searchreset.afterlink.pageInfo): these two string are used respectively
|
||||
before and after the the "Settings page" link (searchreset.link.pageInfo).
|
||||
Localizers can use one of them, or both, to better adapt this sentence to
|
||||
their language.
|
||||
-->
|
||||
<!ENTITY searchreset.beforelink.pageInfo2 "You can change these settings at any time from the ">
|
||||
<!ENTITY searchreset.afterlink.pageInfo2 ".">
|
||||
|
||||
<!ENTITY searchreset.link.pageInfo2 "Settings page">
|
||||
|
||||
<!ENTITY searchreset.noChangeButton "Don’t Change">
|
||||
<!ENTITY searchreset.noChangeButton.access "D">
|
||||
|
||||
<!ENTITY searchreset.changeEngineButton "Change Search Engine">
|
||||
<!ENTITY searchreset.changeEngineButton.access "C">
|
|
@ -17,6 +17,7 @@
|
|||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
locale/browser/aboutHealthReport.dtd (%chrome/browser/aboutHealthReport.dtd)
|
||||
#endif
|
||||
locale/browser/aboutSearchReset.dtd (%chrome/browser/aboutSearchReset.dtd)
|
||||
locale/browser/aboutSessionRestore.dtd (%chrome/browser/aboutSessionRestore.dtd)
|
||||
locale/browser/aboutTabCrashed.dtd (%chrome/browser/aboutTabCrashed.dtd)
|
||||
locale/browser/syncCustomize.dtd (%chrome/browser/syncCustomize.dtd)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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 xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<circle cx="8" cy="8" r="8" fill="#58bf43"/>
|
||||
<circle cx="8" cy="8" r="7.5" stroke="#41a833" stroke-width="1" fill="none"/>
|
||||
<path d="M12.879,12L12,12.879,9.015,9.9A4.276,4.276,0,1,1,9.9,9.015ZM6.5,3.536A2.964,2.964,0,1,0,9.464,6.5,2.964,2.964,0,0,0,6.5,3.536Z" stroke="#41a833" stroke-width="2" fill="none"/>
|
||||
<path d="M12.879,12L12,12.879,9.015,9.9A4.276,4.276,0,1,1,9.9,9.015ZM6.5,3.536A2.964,2.964,0,1,0,9.464,6.5,2.964,2.964,0,0,0,6.5,3.536Z" fill="#fff"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 809 B |
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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 xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<ellipse cx="32" cy="34" rx="29.5" ry="30" fill="#000" fill-opacity=".1"/>
|
||||
<circle cx="32" cy="32" r="30" fill="#58bf43"/>
|
||||
<circle cx="32" cy="32" r="29.5" stroke="#41a833" stroke-width="1" fill="none"/>
|
||||
<path d="M50,47.131L47.131,50,36.776,39.647a16.038,16.038,0,1,1,2.871-2.871ZM27,15A12,12,0,1,0,39,27,12,12,0,0,0,27,15Z" stroke="#41a833" stroke-width="2" fill="none"/>
|
||||
<path d="M50,47.131L47.131,50,36.776,39.647a16.038,16.038,0,1,1,2.871-2.871ZM27,15A12,12,0,1,0,39,27,12,12,0,0,0,27,15Z" fill="#fff"/>
|
||||
<circle cx="27" cy="27" r="13" fill="#fff" fill-opacity=".2"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 925 B |
|
@ -88,6 +88,7 @@
|
|||
skin/classic/browser/search-indicator@2x.png (../shared/search/search-indicator@2x.png)
|
||||
skin/classic/browser/search-engine-placeholder.png (../shared/search/search-engine-placeholder.png)
|
||||
skin/classic/browser/search-engine-placeholder@2x.png (../shared/search/search-engine-placeholder@2x.png)
|
||||
skin/classic/browser/searchReset.css (../shared/searchReset.css)
|
||||
skin/classic/browser/badge-add-engine.png (../shared/search/badge-add-engine.png)
|
||||
skin/classic/browser/badge-add-engine@2x.png (../shared/search/badge-add-engine@2x.png)
|
||||
skin/classic/browser/search-indicator-badge-add.png (../shared/search/search-indicator-badge-add.png)
|
||||
|
@ -119,6 +120,8 @@
|
|||
skin/classic/browser/cert-error.svg (../shared/incontent-icons/cert-error.svg)
|
||||
skin/classic/browser/session-restore.svg (../shared/incontent-icons/session-restore.svg)
|
||||
skin/classic/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg)
|
||||
skin/classic/browser/favicon-search-16.svg (../shared/favicon-search-16.svg)
|
||||
skin/classic/browser/icon-search-64.svg (../shared/incontent-icons/icon-search-64.svg)
|
||||
skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
|
||||
skin/classic/browser/reader-tour.png (../shared/reader/reader-tour.png)
|
||||
skin/classic/browser/reader-tour@2x.png (../shared/reader/reader-tour@2x.png)
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/* 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/. */
|
||||
|
||||
body {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
background-image: url("chrome://browser/skin/icon-search-64.svg");
|
||||
}
|
||||
|
||||
select {
|
||||
font: inherit;
|
||||
padding-inline-end: 24px;
|
||||
padding-inline-start: 26px;
|
||||
background-image: var(--engine-icon-url),
|
||||
url("chrome://global/skin/in-content/dropdown.svg#dropdown");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center, calc(100% - 4px) center;
|
||||
background-size: 16px, 16px;
|
||||
}
|
||||
|
||||
select:-moz-focusring {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 var(--in-content-text-color);
|
||||
}
|
||||
|
||||
option {
|
||||
padding: 4px;
|
||||
padding-inline-start: 30px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center;
|
||||
background-size: 16px;
|
||||
background-color: var(--in-content-page-background);
|
||||
}
|
|
@ -5226,6 +5226,8 @@ pref("browser.search.update", true);
|
|||
pref("browser.search.update.log", false);
|
||||
pref("browser.search.update.interval", 21600);
|
||||
pref("browser.search.suggest.enabled", true);
|
||||
pref("browser.search.reset.enabled", false);
|
||||
pref("browser.search.reset.whitelist", "");
|
||||
pref("browser.search.geoSpecificDefaults", false);
|
||||
pref("browser.search.geoip.url", "https://location.services.mozilla.com/v1/country?key=%MOZILLA_API_KEY%");
|
||||
// NOTE: this timeout figure is also the "high" value for the telemetry probe
|
||||
|
|
|
@ -436,6 +436,12 @@ interface nsIBrowserSearchService : nsISupports
|
|||
*/
|
||||
void removeEngine(in nsISearchEngine engine);
|
||||
|
||||
/**
|
||||
* The original Engine object that is the default for this region,
|
||||
* ignoring changes the user may have subsequently made.
|
||||
*/
|
||||
readonly attribute nsISearchEngine originalDefaultEngine;
|
||||
|
||||
/**
|
||||
* Alias for the currentEngine attribute, kept for add-on compatibility.
|
||||
*/
|
||||
|
|
|
@ -2408,6 +2408,21 @@ Engine.prototype = {
|
|||
},
|
||||
#endif
|
||||
|
||||
get _isWhiteListed() {
|
||||
let url = this._getURLOfType(URLTYPE_SEARCH_HTML).template;
|
||||
let hostname = makeURI(url).host;
|
||||
let whitelist = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
|
||||
.getCharPref("reset.whitelist")
|
||||
.split(",");
|
||||
if (whitelist.includes(hostname)) {
|
||||
LOG("The hostname " + hostname + " is white listed, " +
|
||||
"we won't show the search reset prompt");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// from nsISearchEngine
|
||||
getSubmission: function SRCH_ENG_getSubmission(aData, aResponseType, aPurpose) {
|
||||
#ifdef ANDROID
|
||||
|
@ -2419,6 +2434,24 @@ Engine.prototype = {
|
|||
aResponseType = URLTYPE_SEARCH_HTML;
|
||||
}
|
||||
|
||||
if (aResponseType == URLTYPE_SEARCH_HTML &&
|
||||
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).getBoolPref("reset.enabled") &&
|
||||
this.name == Services.search.currentEngine.name &&
|
||||
!this._isDefault &&
|
||||
(!this.getAttr("loadPathHash") ||
|
||||
this.getAttr("loadPathHash") != getVerificationHash(this._loadPath)) &&
|
||||
!this._isWhiteListed) {
|
||||
let url = "about:searchreset";
|
||||
let data = [];
|
||||
if (aData)
|
||||
data.push("data=" + encodeURIComponent(aData));
|
||||
if (aPurpose)
|
||||
data.push("purpose=" + aPurpose);
|
||||
if (data.length)
|
||||
url += "?" + data.join("&");
|
||||
return new Submission(makeURI(url));
|
||||
}
|
||||
|
||||
var url = this._getURLOfType(aResponseType);
|
||||
|
||||
if (!url)
|
||||
|
@ -2426,7 +2459,7 @@ Engine.prototype = {
|
|||
|
||||
if (!aData) {
|
||||
// Return a dummy submission object with our searchForm attribute
|
||||
return new Submission(makeURI(this._getSearchFormWithPurpose(aPurpose)), null);
|
||||
return new Submission(makeURI(this._getSearchFormWithPurpose(aPurpose)));
|
||||
}
|
||||
|
||||
LOG("getSubmission: In data: \"" + aData + "\"; Purpose: \"" + aPurpose + "\"");
|
||||
|
@ -2822,7 +2855,7 @@ SearchService.prototype = {
|
|||
|
||||
// Get the original Engine object that is the default for this region,
|
||||
// ignoring changes the user may have subsequently made.
|
||||
get _originalDefaultEngine() {
|
||||
get originalDefaultEngine() {
|
||||
let defaultEngine = this.getVerifiedGlobalAttr("searchDefault");
|
||||
if (!defaultEngine) {
|
||||
let defaultPrefB = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
|
||||
|
@ -2841,7 +2874,7 @@ SearchService.prototype = {
|
|||
},
|
||||
|
||||
resetToOriginalDefaultEngine: function SRCH_SVC__resetToOriginalDefaultEngine() {
|
||||
this.currentEngine = this._originalDefaultEngine;
|
||||
this.currentEngine = this.originalDefaultEngine;
|
||||
},
|
||||
|
||||
_buildCache: function SRCH_SVC__buildCache() {
|
||||
|
@ -3948,6 +3981,7 @@ SearchService.prototype = {
|
|||
var engine = new Engine(sanitizeName(aName), false);
|
||||
engine._initFromMetadata(aName, aIconURL, aAlias, aDescription,
|
||||
aMethod, aTemplate, aExtensionID);
|
||||
engine._loadPath = "[other]addEngineWithDetails";
|
||||
this._addEngineToStore(engine);
|
||||
},
|
||||
|
||||
|
@ -4111,13 +4145,13 @@ SearchService.prototype = {
|
|||
this._currentEngine = engine;
|
||||
}
|
||||
if (!name)
|
||||
this._currentEngine = this._originalDefaultEngine;
|
||||
this._currentEngine = this.originalDefaultEngine;
|
||||
}
|
||||
|
||||
// If the current engine is not set or hidden, we fallback...
|
||||
if (!this._currentEngine || this._currentEngine.hidden) {
|
||||
// first to the original default engine
|
||||
let originalDefault = this._originalDefaultEngine;
|
||||
let originalDefault = this.originalDefaultEngine;
|
||||
if (!originalDefault || originalDefault.hidden) {
|
||||
// then to the first visible engine
|
||||
let firstVisible = this._getSortedEngines(false)[0];
|
||||
|
@ -4154,9 +4188,11 @@ SearchService.prototype = {
|
|||
if (!newCurrentEngine)
|
||||
FAIL("Can't find engine in store!", Cr.NS_ERROR_UNEXPECTED);
|
||||
|
||||
if (!newCurrentEngine._isDefault && newCurrentEngine._loadPath) {
|
||||
if (!newCurrentEngine._isDefault) {
|
||||
// If a non default engine is being set as the current engine, ensure
|
||||
// its loadPath has a verification hash.
|
||||
if (!newCurrentEngine._loadPath)
|
||||
newCurrentEngine._loadPath = "[other]unknown";
|
||||
let loadPathHash = getVerificationHash(newCurrentEngine._loadPath);
|
||||
let currentHash = newCurrentEngine.getAttr("loadPathHash");
|
||||
if (!currentHash || currentHash != loadPathHash) {
|
||||
|
@ -4176,7 +4212,7 @@ SearchService.prototype = {
|
|||
// build's default engine, so that the currentEngine getter falls back to
|
||||
// whatever the default is.
|
||||
let newName = this._currentEngine.name;
|
||||
if (this._currentEngine == this._originalDefaultEngine) {
|
||||
if (this._currentEngine == this.originalDefaultEngine) {
|
||||
newName = "";
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const kSearchEngineID = "addEngineWithDetails_test_engine";
|
||||
const kSearchEngineURL = "http://example.com/?search={searchTerms}";
|
||||
const kSearchTerm = "foo";
|
||||
|
||||
add_task(function* test_addEngineWithDetails() {
|
||||
do_check_false(Services.search.isInitialized);
|
||||
|
||||
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
|
||||
.setBoolPref("reset.enabled", true);
|
||||
|
||||
yield asyncInit();
|
||||
|
||||
Services.search.addEngineWithDetails(kSearchEngineID, "", "", "", "get",
|
||||
kSearchEngineURL);
|
||||
|
||||
// An engine added with addEngineWithDetails should have a load path, even
|
||||
// though we can't point to a specific file.
|
||||
let engine = Services.search.getEngineByName(kSearchEngineID);
|
||||
do_check_eq(engine.wrappedJSObject._loadPath, "[other]addEngineWithDetails");
|
||||
|
||||
// Set the engine as default; this should set a loadPath verification hash,
|
||||
// which should ensure we don't show the search reset prompt.
|
||||
Services.search.currentEngine = engine;
|
||||
|
||||
let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
|
||||
let submission =
|
||||
Services.search.currentEngine.getSubmission(kSearchTerm, null, "searchbar");
|
||||
do_check_eq(submission.uri.spec, expectedURL);
|
||||
});
|
|
@ -0,0 +1,137 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
|
||||
|
||||
const kTestEngineShortName = "engine";
|
||||
const kWhiteListPrefName = "reset.whitelist";
|
||||
|
||||
function run_test() {
|
||||
// Copy an engine to [profile]/searchplugin/
|
||||
let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
|
||||
if (!dir.exists())
|
||||
dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
do_get_file("data/engine.xml").copyTo(dir, kTestEngineShortName + ".xml");
|
||||
|
||||
let file = dir.clone();
|
||||
file.append(kTestEngineShortName + ".xml");
|
||||
do_check_true(file.exists());
|
||||
|
||||
do_check_false(Services.search.isInitialized);
|
||||
|
||||
Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
|
||||
.setBoolPref("reset.enabled", true);
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function* removeLoadPathHash() {
|
||||
// Remove the loadPathHash and re-initialize the search service.
|
||||
let cache = yield promiseCacheData();
|
||||
for (let engine of cache.engines) {
|
||||
if (engine._shortName == kTestEngineShortName) {
|
||||
delete engine._metaData["loadPathHash"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
yield promiseSaveCacheData(cache);
|
||||
yield asyncReInit();
|
||||
}
|
||||
|
||||
add_task(function* test_no_prompt_when_valid_loadPathHash() {
|
||||
yield asyncInit();
|
||||
|
||||
// test the engine is loaded ok.
|
||||
let engine = Services.search.getEngineByName(kTestEngineName);
|
||||
do_check_neq(engine, null);
|
||||
|
||||
yield promiseAfterCache();
|
||||
|
||||
// The test engine has been found in the profile directory and imported,
|
||||
// so it shouldn't have a loadPathHash.
|
||||
let metadata = yield promiseEngineMetadata();
|
||||
do_check_true(kTestEngineShortName in metadata);
|
||||
do_check_false("loadPathHash" in metadata[kTestEngineShortName]);
|
||||
|
||||
// After making it the currentEngine with the search service API,
|
||||
// the test engine should have a valid loadPathHash.
|
||||
Services.search.currentEngine = engine;
|
||||
yield promiseAfterCache();
|
||||
metadata = yield promiseEngineMetadata();
|
||||
do_check_true("loadPathHash" in metadata[kTestEngineShortName]);
|
||||
let loadPathHash = metadata[kTestEngineShortName].loadPathHash;
|
||||
do_check_eq(typeof loadPathHash, "string");
|
||||
do_check_eq(loadPathHash.length, 44);
|
||||
|
||||
// A search should not cause the search reset prompt.
|
||||
let submission =
|
||||
Services.search.currentEngine.getSubmission("foo", null, "searchbar");
|
||||
do_check_eq(submission.uri.spec,
|
||||
"http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t");
|
||||
});
|
||||
|
||||
add_task(function* test_promptURLs() {
|
||||
yield removeLoadPathHash();
|
||||
|
||||
// The default should still be the test engine.
|
||||
let currentEngine = Services.search.currentEngine;
|
||||
do_check_eq(currentEngine.name, kTestEngineName);
|
||||
// but the submission url should be about:searchreset
|
||||
let url = (data, purpose) =>
|
||||
currentEngine.getSubmission(data, null, purpose).uri.spec;
|
||||
do_check_eq(url("foo", "searchbar"),
|
||||
"about:searchreset?data=foo&purpose=searchbar");
|
||||
do_check_eq(url("foo"), "about:searchreset?data=foo");
|
||||
do_check_eq(url("", "searchbar"), "about:searchreset?purpose=searchbar");
|
||||
do_check_eq(url(""), "about:searchreset");
|
||||
do_check_eq(url("", ""), "about:searchreset");
|
||||
|
||||
// Calling the currentEngine setter for the same engine should
|
||||
// prevent further prompts.
|
||||
Services.search.currentEngine = Services.search.currentEngine;
|
||||
do_check_eq(url("foo", "searchbar"),
|
||||
"http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t");
|
||||
|
||||
// And the loadPathHash should be back.
|
||||
yield promiseAfterCache();
|
||||
let metadata = yield promiseEngineMetadata();
|
||||
do_check_true("loadPathHash" in metadata[kTestEngineShortName]);
|
||||
let loadPathHash = metadata[kTestEngineShortName].loadPathHash;
|
||||
do_check_eq(typeof loadPathHash, "string");
|
||||
do_check_eq(loadPathHash.length, 44);
|
||||
});
|
||||
|
||||
add_task(function* test_whitelist() {
|
||||
yield removeLoadPathHash();
|
||||
|
||||
// The default should still be the test engine.
|
||||
let currentEngine = Services.search.currentEngine;
|
||||
do_check_eq(currentEngine.name, kTestEngineName);
|
||||
let expectPrompt = shouldPrompt => {
|
||||
let expectedURL =
|
||||
shouldPrompt ? "about:searchreset?data=foo&purpose=searchbar"
|
||||
: "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t";
|
||||
let url = currentEngine.getSubmission("foo", null, "searchbar").uri.spec;
|
||||
do_check_eq(url, expectedURL);
|
||||
};
|
||||
expectPrompt(true);
|
||||
|
||||
// Unless we whitelist our test engine.
|
||||
let branch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
|
||||
let initialWhiteList = branch.getCharPref(kWhiteListPrefName);
|
||||
branch.setCharPref(kWhiteListPrefName, "example.com,test.tld");
|
||||
expectPrompt(true);
|
||||
branch.setCharPref(kWhiteListPrefName, "www.google.com");
|
||||
expectPrompt(false);
|
||||
branch.setCharPref(kWhiteListPrefName, "example.com,www.google.com,test.tld");
|
||||
expectPrompt(false);
|
||||
|
||||
// The loadPathHash should not be back after the prompt was skipped due to the
|
||||
// whitelist.
|
||||
yield asyncReInit();
|
||||
let metadata = yield promiseEngineMetadata();
|
||||
do_check_false("loadPathHash" in metadata[kTestEngineShortName]);
|
||||
|
||||
branch.setCharPref(kWhiteListPrefName, initialWhiteList);
|
||||
expectPrompt(true);
|
||||
});
|
|
@ -94,3 +94,5 @@ tags = addons
|
|||
[test_require_engines_in_cache.js]
|
||||
[test_update_telemetry.js]
|
||||
[test_svg_icon.js]
|
||||
[test_searchReset.js]
|
||||
[test_addEngineWithDetails.js]
|
||||
|
|
|
@ -5518,6 +5518,15 @@
|
|||
"releaseChannelCollection": "opt-out",
|
||||
"description": "Record the search counts for search engines"
|
||||
},
|
||||
"SEARCH_RESET_RESULT": {
|
||||
"alert_emails": ["fqueze@mozilla.com"],
|
||||
"bug_numbers": [1203168],
|
||||
"expires_in_version": "53",
|
||||
"kind": "enumerated",
|
||||
"n_values": 5,
|
||||
"releaseChannelCollection": "opt-out",
|
||||
"description": "Result of showing the search reset prompt to the user. 0=restored original default, 1=kept current engine, 2=changed engine, 3=closed the page"
|
||||
},
|
||||
"SEARCH_SERVICE_INIT_MS": {
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
|
|
|
@ -1389,8 +1389,8 @@ add_task(function* test_defaultSearchEngine() {
|
|||
|
||||
const EXPECTED_SEARCH_ENGINE_DATA = {
|
||||
name: "telemetry_default",
|
||||
loadPath: null,
|
||||
origin: "unverified"
|
||||
loadPath: "[other]addEngineWithDetails",
|
||||
origin: "verified"
|
||||
};
|
||||
Assert.deepEqual(data.settings.defaultSearchEngineData, EXPECTED_SEARCH_ENGINE_DATA);
|
||||
TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
|
||||
|
|
|
@ -175,6 +175,7 @@ html|button {
|
|||
/* xul buttons and menulists */
|
||||
|
||||
*|button,
|
||||
html|select,
|
||||
xul|colorpicker[type="button"],
|
||||
xul|menulist {
|
||||
-moz-appearance: none;
|
||||
|
@ -190,6 +191,7 @@ xul|menulist {
|
|||
}
|
||||
|
||||
html|button:enabled:hover,
|
||||
html|select:enabled:hover,
|
||||
xul|button:not([disabled="true"]):hover,
|
||||
xul|colorpicker[type="button"]:not([disabled="true"]):hover,
|
||||
xul|menulist:not([disabled="true"]):hover {
|
||||
|
@ -197,6 +199,7 @@ xul|menulist:not([disabled="true"]):hover {
|
|||
}
|
||||
|
||||
html|button:enabled:hover:active,
|
||||
html|select:enabled:hover:active,
|
||||
xul|button:not([disabled="true"]):hover:active,
|
||||
xul|colorpicker[type="button"]:not([disabled="true"]):hover:active,
|
||||
xul|menulist[open="true"]:not([disabled="true"]) {
|
||||
|
@ -204,6 +207,7 @@ xul|menulist[open="true"]:not([disabled="true"]) {
|
|||
}
|
||||
|
||||
html|button:disabled,
|
||||
html|select:disabled,
|
||||
xul|button[disabled="true"],
|
||||
xul|colorpicker[type="button"][disabled="true"],
|
||||
xul|menulist[disabled="true"] {
|
||||
|
|
Загрузка…
Ссылка в новой задаче