Merge latest green inbound changeset and mozilla-central

This commit is contained in:
Ed Morley 2013-09-20 10:18:21 +01:00
Родитель c56305c3a6 d984c2e9c2
Коммит 2f579f2c8b
96 изменённых файлов: 3544 добавлений и 1342 удалений

Просмотреть файл

@ -860,6 +860,8 @@ pref("browser.sessionstore.restore_pinned_tabs_on_demand", false);
pref("browser.sessionstore.upgradeBackup.latestBuildID", "");
// End-users should not run sessionstore in debug mode
pref("browser.sessionstore.debug", false);
// Enable asynchronous data collection by default.
pref("browser.sessionstore.async", true);
// allow META refresh by default
pref("accessibility.blockautorefresh", false);

Просмотреть файл

@ -524,7 +524,8 @@
observes="devtoolsMenuBroadcaster_DevAppMgr"
accesskey="&devAppMgrMenu.accesskey;"/>
<menuitem id="menu_chromeDebugger"
observes="devtoolsMenuBroadcaster_ChromeDebugger"/>
observes="devtoolsMenuBroadcaster_ChromeDebugger"
accesskey="&chromeDebuggerMenu.accesskey;"/>
<menuitem id="menu_browserConsole"
observes="devtoolsMenuBroadcaster_BrowserConsole"
accesskey="&browserConsoleCmd.accesskey;"/>

Просмотреть файл

@ -215,13 +215,20 @@ SocialUI = {
// enabled == true means we at least have a defaultProvider
let provider = Social.provider || Social.defaultProvider;
// We only need to update the command itself - all our menu items use it.
let label = gNavigatorBundle.getFormattedString(Social.provider ?
"social.turnOff.label" :
"social.turnOn.label",
[provider.name]);
let accesskey = gNavigatorBundle.getString(Social.provider ?
"social.turnOff.accesskey" :
"social.turnOn.accesskey");
let label;
if (Social.providers.length == 1) {
label = gNavigatorBundle.getFormattedString(Social.provider
? "social.turnOff.label"
: "social.turnOn.label",
[provider.name]);
} else {
label = gNavigatorBundle.getString(Social.provider
? "social.turnOffAll.label"
: "social.turnOnAll.label");
}
let accesskey = gNavigatorBundle.getString(Social.provider
? "social.turnOff.accesskey"
: "social.turnOn.accesskey");
toggleCommand.setAttribute("label", label);
toggleCommand.setAttribute("accesskey", accesskey);
}

Просмотреть файл

@ -7,6 +7,7 @@ function test() {
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
runSocialTestWithProvider(gProviders, function (finishcb) {
Social.enabled = true;
runSocialTests(tests, undefined, undefined, function() {
Services.prefs.clearUserPref("social.allowMultipleWorkers");
finishcb();

Просмотреть файл

@ -24,7 +24,11 @@
<content>
<xul:stringbundle src="chrome://browser/locale/search.properties"
anonid="searchbar-stringbundle"/>
<!--
There is a dependency between "maxrows" attribute and
"SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing
one of them requires changing the other one.
-->
<xul:textbox class="searchbar-textbox"
anonid="searchbar-textbox"
type="autocomplete"

Просмотреть файл

@ -2,12 +2,19 @@
* 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";
function debug(msg) {
Services.console.logStringMessage("SessionStoreContent: " + msg);
}
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
"resource:///modules/sessionstore/SessionHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
"resource:///modules/sessionstore/SessionStorage.jsm");
/**
* Listens for and handles content events that we need for the
* session store service to be notified of state changes in content.
@ -55,7 +62,37 @@ let EventListener = {
}
}
};
EventListener.init();
/**
* Listens for and handles messages sent by the session store service.
*/
let MessageListener = {
MESSAGES: [
"SessionStore:collectSessionHistory",
"SessionStore:collectSessionStorage"
],
init: function () {
this.MESSAGES.forEach(m => addMessageListener(m, this));
},
receiveMessage: function ({name, data: {id}}) {
switch (name) {
case "SessionStore:collectSessionHistory":
let history = SessionHistory.read(docShell);
sendAsyncMessage(name, {id: id, data: history});
break;
case "SessionStore:collectSessionStorage":
let storage = SessionStorage.serialize(docShell);
sendAsyncMessage(name, {id: id, data: storage});
break;
default:
debug("received unknown message '" + name + "'");
break;
}
}
};
let ProgressListener = {
init: function() {
@ -74,4 +111,7 @@ let ProgressListener = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference])
};
EventListener.init();
MessageListener.init();
ProgressListener.init();

Просмотреть файл

@ -0,0 +1,71 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["Messenger"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/Timer.jsm", this);
/**
* The external API exported by this module.
*/
this.Messenger = Object.freeze({
send: function (tab, type, options = {}) {
return MessengerInternal.send(tab, type, options);
}
});
/**
* A module that handles communication between the main and content processes.
*/
let MessengerInternal = {
// The id of the last message we sent. This is used to assign a unique ID to
// every message we send to handle multiple responses from the same browser.
_latestMessageID: 0,
/**
* Sends a message to the given tab and waits for a response.
*
* @param tab
* tabbrowser tab
* @param type
* {string} the type of the message
* @param options (optional)
* {timeout: int} to set the timeout in milliseconds
* @return {Promise} A promise that will resolve to the response message or
* be reject when timing out.
*/
send: function (tab, type, options = {}) {
let browser = tab.linkedBrowser;
let mm = browser.messageManager;
let deferred = Promise.defer();
let id = ++this._latestMessageID;
let timeout;
function onMessage({data: {id: mid, data}}) {
if (mid == id) {
mm.removeMessageListener(type, onMessage);
clearTimeout(timeout);
deferred.resolve(data);
}
}
mm.addMessageListener(type, onMessage);
mm.sendAsyncMessage(type, {id: id});
function onTimeout() {
mm.removeMessageListener(type, onMessage);
deferred.reject(new Error("Timed out while waiting for a " + type + " " +
"response message."));
}
let delay = (options && options.timeout) || 5000;
timeout = setTimeout(onTimeout, delay);
return deferred.promise;
}
};

Просмотреть файл

@ -0,0 +1,82 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["PrivacyLevel"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
const PREF_NORMAL = "browser.sessionstore.privacy_level";
const PREF_DEFERRED = "browser.sessionstore.privacy_level_deferred";
// The following constants represent the different possible privacy levels that
// can be set by the user and that we need to consider when collecting text
// data, cookies, and POSTDATA.
//
// Collect data from all sites (http and https).
const PRIVACY_NONE = 0;
// Collect data from unencrypted sites (http), only.
const PRIVACY_ENCRYPTED = 1;
// Collect no data.
const PRIVACY_FULL = 2;
/**
* Returns whether we will resume the session automatically on next startup.
*/
function willResumeAutomatically() {
return Services.prefs.getIntPref("browser.startup.page") == 3 ||
Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
}
/**
* Determines the current privacy level as set by the user.
*
* @param isPinned
* Whether to return the privacy level for pinned tabs.
* @return {int} The privacy level as read from the user's preferences.
*/
function getCurrentLevel(isPinned) {
let pref = PREF_NORMAL;
// If we're in the process of quitting and we're not autoresuming the session
// then we will use the deferred privacy level for non-pinned tabs.
if (!isPinned && Services.startup.shuttingDown && !willResumeAutomatically()) {
pref = PREF_DEFERRED;
}
return Services.prefs.getIntPref(pref);
}
/**
* The external API as exposed by this module.
*/
let PrivacyLevel = Object.freeze({
/**
* Checks whether we're allowed to save data for a specific site.
*
* @param {isHttps: boolean, isPinned: boolean}
* An object that must have two properties: 'isHttps' and 'isPinned'.
* 'isHttps' tells whether the site us secure communication (HTTPS).
* 'isPinned' tells whether the site is loaded in a pinned tab.
* @return {bool} Whether we can save data for the specified site.
*/
canSave: function ({isHttps, isPinned}) {
let level = getCurrentLevel(isPinned);
// Never save any data when full privacy is requested.
if (level == PRIVACY_FULL) {
return false;
}
// Don't save data for encrypted sites when requested.
if (isHttps && level == PRIVACY_ENCRYPTED) {
return false;
}
return true;
}
});

Просмотреть файл

@ -12,8 +12,8 @@ const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
"resource:///modules/sessionstore/PrivacyLevel.jsm");
// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
const MAX_EXPIRY = Math.pow(2, 62);
@ -67,8 +67,8 @@ let SessionCookiesInternal = {
for (let cookie of CookieStore.getCookiesForHost(host)) {
// _getCookiesForHost() will only return hosts with the right privacy
// rules, so there is no need to do anything special with this call
// to checkPrivacyLevel().
if (SessionStore.checkPrivacyLevel(cookie.secure, isPinned)) {
// to PrivacyLevel.canSave().
if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) {
cookies.push(cookie);
}
}
@ -209,7 +209,7 @@ let SessionCookiesInternal = {
// case testing scheme will be sufficient.
if (/https?/.test(scheme) && !hosts[host] &&
(!checkPrivacy ||
SessionStore.checkPrivacyLevel(scheme == "https", isPinned))) {
PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) {
// By setting this to true or false, we can determine when looking at
// the host in update() if we should check for privacy.
hosts[host] = isPinned;

Просмотреть файл

@ -0,0 +1,272 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["SessionHistory"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
"resource:///modules/sessionstore/PrivacyLevel.jsm");
function debug(msg) {
Services.console.logStringMessage("SessionHistory: " + msg);
}
// The preference value that determines how much post data to save.
XPCOMUtils.defineLazyGetter(this, "gPostData", function () {
const PREF = "browser.sessionstore.postdata";
// Observer that updates the cached value when the preference changes.
Services.prefs.addObserver(PREF, () => {
this.gPostData = Services.prefs.getIntPref(PREF);
}, false);
return Services.prefs.getIntPref(PREF);
});
/**
* The external API exported by this module.
*/
this.SessionHistory = Object.freeze({
read: function (docShell, includePrivateData) {
return SessionHistoryInternal.read(docShell, includePrivateData);
}
});
/**
* The internal API for the SessionHistory module.
*/
let SessionHistoryInternal = {
/**
* Collects session history data for a given docShell.
*
* @param docShell
* The docShell that owns the session history.
* @param includePrivateData (optional)
* True to always include private data and skip any privacy checks.
*/
read: function (docShell, includePrivateData = false) {
let data = {entries: []};
let isPinned = docShell.isAppTab;
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
let history = webNavigation.sessionHistory;
if (history && history.count > 0) {
try {
for (let i = 0; i < history.count; i++) {
let shEntry = history.getEntryAtIndex(i, false);
let entry = this._serializeEntry(shEntry, includePrivateData, isPinned);
data.entries.push(entry);
}
} catch (ex) {
// In some cases, getEntryAtIndex will throw. This seems to be due to
// history.count being higher than it should be. By doing this in a
// try-catch, we'll update history to where it breaks, print an error
// message, and still save sessionstore.js.
debug("SessionStore failed gathering complete history " +
"for the focused window/tab. See bug 669196.");
}
data.index = history.index + 1;
} else {
let uri = webNavigation.currentURI.spec;
// We landed here because the history is inaccessible or there are no
// history entries. In that case we should at least record the docShell's
// current URL as a single history entry. If the URL is not about:blank
// or it's a blank tab that was modified (like a custom newtab page),
// record it. For about:blank we explicitly want an empty array without
// an 'index' property to denote that there are no history entries.
if (uri != "about:blank" || webNavigation.document.body.hasChildNodes()) {
data.entries.push({ url: uri });
data.index = 1;
}
}
return data;
},
/**
* Get an object that is a serialized representation of a History entry.
*
* @param shEntry
* nsISHEntry instance
* @param includePrivateData
* Always return privacy sensitive data (use with care).
* @param isPinned
* The tab is pinned and should be treated differently for privacy.
* @return object
*/
_serializeEntry: function (shEntry, includePrivateData, isPinned) {
let entry = { url: shEntry.URI.spec };
// Save some bytes and don't include the title property
// if that's identical to the current entry's URL.
if (shEntry.title && shEntry.title != entry.url) {
entry.title = shEntry.title;
}
if (shEntry.isSubFrame) {
entry.subframe = true;
}
let cacheKey = shEntry.cacheKey;
if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 &&
cacheKey.data != 0) {
// XXXbz would be better to have cache keys implement
// nsISerializable or something.
entry.cacheKey = cacheKey.data;
}
entry.ID = shEntry.ID;
entry.docshellID = shEntry.docshellID;
// We will include the property only if it's truthy to save a couple of
// bytes when the resulting object is stringified and saved to disk.
if (shEntry.referrerURI)
entry.referrer = shEntry.referrerURI.spec;
if (shEntry.srcdocData)
entry.srcdocData = shEntry.srcdocData;
if (shEntry.isSrcdocEntry)
entry.isSrcdocEntry = shEntry.isSrcdocEntry;
if (shEntry.contentType)
entry.contentType = shEntry.contentType;
let x = {}, y = {};
shEntry.getScrollPosition(x, y);
if (x.value != 0 || y.value != 0)
entry.scroll = x.value + "," + y.value;
// Collect post data for the current history entry.
try {
let postdata = this._serializePostData(shEntry, isPinned);
if (postdata) {
entry.postdata_b64 = postdata;
}
} catch (ex) {
// POSTDATA is tricky - especially since some extensions don't get it right
debug("Failed serializing post data: " + ex);
}
// Collect owner data for the current history entry.
try {
let owner = this._serializeOwner(shEntry);
if (owner) {
entry.owner_b64 = owner;
}
} catch (ex) {
// Not catching anything specific here, just possible errors
// from writeCompoundObject() and the like.
debug("Failed serializing owner data: " + ex);
}
entry.docIdentifier = shEntry.BFCacheEntry.ID;
if (shEntry.stateData != null) {
entry.structuredCloneState = shEntry.stateData.getDataAsBase64();
entry.structuredCloneVersion = shEntry.stateData.formatVersion;
}
if (!(shEntry instanceof Ci.nsISHContainer)) {
return entry;
}
if (shEntry.childCount > 0) {
let children = [];
for (let i = 0; i < shEntry.childCount; i++) {
let child = shEntry.GetChildAt(i);
if (child) {
// Don't try to restore framesets containing wyciwyg URLs.
// (cf. bug 424689 and bug 450595)
if (child.URI.schemeIs("wyciwyg")) {
children.length = 0;
break;
}
children.push(this._serializeEntry(child, includePrivateData, isPinned));
}
}
if (children.length) {
entry.children = children;
}
}
return entry;
},
/**
* Serialize post data contained in the given session history entry.
*
* @param shEntry
* The session history entry.
* @param isPinned
* Whether the docShell is owned by a pinned tab.
* @return The base64 encoded post data.
*/
_serializePostData: function (shEntry, isPinned) {
let isHttps = shEntry.URI.schemeIs("https");
if (!shEntry.postData || !gPostData ||
!PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
return null;
}
shEntry.postData.QueryInterface(Ci.nsISeekableStream)
.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
let stream = Cc["@mozilla.org/binaryinputstream;1"]
.createInstance(Ci.nsIBinaryInputStream);
stream.setInputStream(shEntry.postData);
let postBytes = stream.readByteArray(stream.available());
let postdata = String.fromCharCode.apply(null, postBytes);
if (gPostData != -1 &&
postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length > gPostData) {
return null;
}
// We can stop doing base64 encoding once our serialization into JSON
// is guaranteed to handle all chars in strings, including embedded
// nulls.
return btoa(postdata);
},
/**
* Serialize owner data contained in the given session history entry.
*
* @param shEntry
* The session history entry.
* @return The base64 encoded owner data.
*/
_serializeOwner: function (shEntry) {
if (!shEntry.owner) {
return null;
}
let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].
createInstance(Ci.nsIObjectOutputStream);
let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
pipe.init(false, false, 0, 0xffffffff, null);
binaryStream.setOutputStream(pipe.outputStream);
binaryStream.writeCompoundObject(shEntry.owner, Ci.nsISupports, true);
binaryStream.close();
// Now we want to read the data from the pipe's input end and encode it.
let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].
createInstance(Ci.nsIBinaryInputStream);
scriptableStream.setInputStream(pipe.inputStream);
let ownerBytes =
scriptableStream.readByteArray(scriptableStream.available());
// We can stop doing base64 encoding once our serialization into JSON
// is guaranteed to handle all chars in strings, including embedded
// nulls.
return btoa(String.fromCharCode.apply(null, ownerBytes));
}
};

Просмотреть файл

@ -151,7 +151,7 @@ let SessionSaverInternal = {
delay = Math.max(this._lastSaveTime + gInterval - Date.now(), delay, 0);
// Schedule a state save.
this._timeoutID = setTimeout(() => this._saveState(), delay);
this._timeoutID = setTimeout(() => this._saveStateAsync(), delay);
},
/**
@ -186,8 +186,7 @@ let SessionSaverInternal = {
* update the corresponding caches.
*/
_saveState: function (forceUpdateAllWindows = false) {
// Cancel any pending timeouts or just clear
// the timeout if this is why we've been called.
// Cancel any pending timeouts.
this.cancel();
stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
@ -246,6 +245,33 @@ let SessionSaverInternal = {
this._writeState(state);
},
/**
* Saves the current session state. Collects data asynchronously and calls
* _saveState() to collect data again (with a cache hit rate of hopefully
* 100%) and write to disk afterwards.
*/
_saveStateAsync: function () {
// Allow scheduling delayed saves again.
this._timeoutID = null;
// Check whether asynchronous data collection is disabled.
if (!Services.prefs.getBoolPref("browser.sessionstore.async")) {
this._saveState();
return;
}
// Update the last save time to make sure we wait at least another interval
// length until we call _saveStateAsync() again.
this.updateLastSaveTime();
// Save state synchronously after all tab caches have been filled. The data
// for the tab caches is collected asynchronously. We will reuse this
// cached data if the tab hasn't been invalidated in the meantime. In that
// case we will just fall back to synchronous data collection for single
// tabs.
SessionStore.fillTabCachesAsynchronously().then(() => this._saveState());
},
/**
* Write the given state object to disk.
*/

Просмотреть файл

@ -7,12 +7,13 @@
this.EXPORTED_SYMBOLS = ["SessionStorage"];
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
"resource:///modules/sessionstore/PrivacyLevel.jsm");
this.SessionStorage = {
/**
@ -51,16 +52,17 @@ let DomStorage = {
read: function DomStorage_read(aDocShell, aFullData) {
let data = {};
let isPinned = aDocShell.isAppTab;
let shistory = aDocShell.sessionHistory;
let webNavigation = aDocShell.QueryInterface(Ci.nsIWebNavigation);
let shistory = webNavigation.sessionHistory;
for (let i = 0; i < shistory.count; i++) {
for (let i = 0; shistory && i < shistory.count; i++) {
let principal = History.getPrincipalForEntry(shistory, i, aDocShell);
if (!principal)
continue;
// Check if we're allowed to store sessionStorage data.
let isHTTPS = principal.URI && principal.URI.schemeIs("https");
if (aFullData || SessionStore.checkPrivacyLevel(isHTTPS, isPinned)) {
let isHttps = principal.URI && principal.URI.schemeIs("https");
if (aFullData || PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
let origin = principal.jarPrefix + principal.origin;
// Don't read a host twice.
@ -90,8 +92,8 @@ let DomStorage = {
let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager);
// There is no need to pass documentURI, it's only used to fill documentURI property of
// domstorage event, which in this case has no consumer. Prevention of events in case
// of missing documentURI will be solved in a followup bug to bug 600307.
// domstorage event, which in this case has no consumer. Prevention of events in case
// of missing documentURI will be solved in a followup bug to bug 600307.
let storage = storageManager.createStorage(principal, "", aDocShell.usePrivateBrowsing);
for (let [key, value] in Iterator(data)) {

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -14,7 +14,10 @@ JS_MODULES_PATH = 'modules/sessionstore'
EXTRA_JS_MODULES = [
'DocumentUtils.jsm',
'Messenger.jsm',
'PrivacyLevel.jsm',
'SessionCookies.jsm',
'SessionHistory.jsm',
'SessionMigration.jsm',
'SessionStorage.jsm',
'SessionWorker.js',

Просмотреть файл

@ -58,7 +58,6 @@ function test() {
ss.getBrowserState();
is(gBrowser.tabs[1], tab, "newly created tab should exist by now");
ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
// Start a load and interrupt it by closing the tab
tab.linkedBrowser.loadURI(URI_TO_LOAD);

Просмотреть файл

@ -17,7 +17,7 @@ const COLLAPSE_DATA_URL_LENGTH = 60;
const {UndoStack} = require("devtools/shared/undo");
const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
const {colorUtils} = require("devtools/shared/css-color");
const {colorUtils} = require("devtools/css-color");
const promise = require("sdk/core/promise");
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");

Просмотреть файл

@ -6,15 +6,9 @@ const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
let origColorUnit;
let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
let {colorUtils} = devtools.require("devtools/shared/css-color");
let {colorUtils} = devtools.require("devtools/css-color");
function test() {
// FIXME: Enable this test on Linux once bug 916544 is fixed
if (Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS === "Linux") {
Services = colorUtils.CssColor = Loader = null;
return;
}
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
@ -302,7 +296,7 @@ function getTestData() {
{authored: "hsla(0, 0%, 0%, 0)", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
{authored: "rgba(50, 60, 70, 0.5)", name: "rgba(50, 60, 70, 0.5)", hex: "rgba(50, 60, 70, 0.5)", hsl: "hsla(210, 17%, 24%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)"},
{authored: "rgba(0, 0, 0, 0.3)", name: "rgba(0, 0, 0, 0.3)", hex: "rgba(0, 0, 0, 0.3)", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)"},
{authored: "rgba(255, 255, 255, 0.7)", name: "rgba(255, 255, 255, 0.7)", hex: "rgba(255, 255, 255, 0.7)", hsl: "hsla(0, 0%, 100%, 0.7)", rgb: "rgba(255, 255, 255, 0.7)"},
{authored: "rgba(255, 255, 255, 0.6)", name: "rgba(255, 255, 255, 0.6)", hex: "rgba(255, 255, 255, 0.6)", hsl: "hsla(0, 0%, 100%, 0.6)", rgb: "rgba(255, 255, 255, 0.6)"},
{authored: "rgba(127, 89, 45, 1)", name: "#7F592D", hex: "#7F592D", hsl: "hsl(32.195, 48%, 34%)", rgb: "rgb(127, 89, 45)"},
{authored: "hsla(19.304, 56%, 40%, 1)", name: "#9F512C", hex: "#9F512C", hsl: "hsl(19.304, 57%, 40%)", rgb: "rgb(159, 81, 44)"},
{authored: "invalidColor", name: "", hex: "", hsl: "", rgb: ""}

Просмотреть файл

@ -11,7 +11,7 @@ let {CssLogic} = require("devtools/styleinspector/css-logic");
let {ELEMENT_STYLE} = require("devtools/server/actors/styles");
let promise = require("sdk/core/promise");
let {EventEmitter} = require("devtools/shared/event-emitter");
let {colorUtils} = require("devtools/shared/css-color");
let {colorUtils} = require("devtools/css-color");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");

Просмотреть файл

@ -12,7 +12,7 @@ const promise = require("sdk/core/promise");
let {CssLogic} = require("devtools/styleinspector/css-logic");
let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
let {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
let {colorUtils} = require("devtools/shared/css-color");
let {colorUtils} = require("devtools/css-color");
let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
Cu.import("resource://gre/modules/Services.jsm");

Просмотреть файл

@ -6,6 +6,10 @@
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-error.html";
let containsValue;
let Sources;
let containsValueInvoked = false;
function test() {
addTab(TEST_URI);
browser.addEventListener("load", function onLoad() {
@ -15,21 +19,32 @@ function test() {
}
function testViewSource(hud) {
info("console opened");
let button = content.document.querySelector("button");
ok(button, "we have the button on the page");
expectUncaughtException();
EventUtils.sendMouseEvent({ type: "click" }, button, content);
waitForMessages({
webconsole: hud,
messages: [{
text: "fooBazBaz is not defined",
category: CATEGORY_JS,
severity: SEVERITY_ERROR,
}],
}).then(([result]) => {
Cu.forceGC();
openDebugger().then(({panelWin: { DebuggerView }}) => {
info("debugger openeed");
Sources = DebuggerView.Sources;
openConsole(null, (hud) => {
info("console opened again");
waitForMessages({
webconsole: hud,
messages: [{
text: "fooBazBaz is not defined",
category: CATEGORY_JS,
severity: SEVERITY_ERROR,
}],
}).then(onMessage);
});
});
function onMessage([result]) {
let msg = [...result.matched][0];
ok(msg, "error message");
let locationNode = msg.querySelector(".location");
@ -37,8 +52,14 @@ function testViewSource(hud) {
Services.ww.registerNotification(observer);
containsValue = Sources.containsValue;
Sources.containsValue = () => {
containsValueInvoked = true;
return false;
};
EventUtils.sendMouseEvent({ type: "click" }, locationNode);
});
}
}
let observer = {
@ -53,6 +74,9 @@ let observer = {
// executeSoon() is necessary to avoid crashing Firefox. See bug 611543.
executeSoon(function() {
aSubject.close();
ok(containsValueInvoked, "custom containsValue() was invoked");
Sources.containsValue = containsValue;
Sources = containsValue = null;
finishTest();
});
}

Просмотреть файл

@ -246,6 +246,7 @@ These should match what Safari and other Apple applications use on OS X Lion. --
<!-- LOCALIZATION NOTE (chromeDebuggerMenu.label): This is the label for the
- application menu item that opens the browser debugger UI in the Tools menu. -->
<!ENTITY chromeDebuggerMenu.label "Browser Debugger">
<!ENTITY chromeDebuggerMenu.accesskey "e">
<!ENTITY devToolbarCloseButton.tooltiptext "Close Developer Toolbar">
<!ENTITY devToolbarMenu.label "Developer Toolbar">

Просмотреть файл

@ -428,6 +428,8 @@ social.turnOff.accesskey=T
# LOCALIZATION NOTE (social.turnOn.label): %S is the name of the social provider
social.turnOn.label=Turn on %S
social.turnOn.accesskey=T
social.turnOffAll.label=Turn off all Services
social.turnOnAll.label=Turn on all Services
# LOCALIZATION NOTE (social.markpageMenu.label): %S is the name of the social provider
social.markpageMenu.label=Save Page to %S

Просмотреть файл

@ -0,0 +1,20 @@
<!-- 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/. -->
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
<ShortName>Bing</ShortName>
<Description>Bing. Search by Microsoft.</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAAAAAD/AAAAJgAAACAgAAAAAAAA+AMAACUBAACJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAADGSURBVDjLY/i/TPQ/JZiBOgZsd/z/f08AhCbLgJdH/4MBiKaaAWtUIK4i4DJUA95d/v//xsz//788+o8VPN4GMRCnATAAMuByF8IFx3NR1dxbjscAkCTI+dicDDIIBkAuxWoALs0wDLIABH5+QDIA5DeY0wmFPMhrMAA34Gw1QhAtkFAwyHWwAIZahkiJoBiAOQ1kILpmkMHIaqBRy4BiOihgYACkCBQ2IIwcrSBDkNIFZl7YaARxAShcYAaAMCxaaZOZKMAAOzkOfhyf540AAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAAO/SURBVFjDxZRJTxRBFMfrJiC7enKJ28nEGL+AXo2J+gFcrkr0C4gLKjCAoILKIgKyjSZG40FFMephHERcLm4XVzTRi7I4DHRPL8/3qqq7q6d7ZIgRO/mFzlT1+/3fq6ZZ8lzB9smq3JGxQwtgPhmvyHk1XpO3kZF8/EgOjB2eX8hJbkY3/4WjOTBRkQuMbuaVCsHEsVwOoxTzhpROHs8TnMjDAMe8hX/KcQFJOZVIFQaYPEE//mMqBb9QyKlGInmQiFCASpkG0WPVoF7m5xio63OmSvCreqEgIkjUILVEPjCeRhIWQF2fCwmCZI5QSqeIunyYOilg7iZEf5QWYDQG6nrW1OQLaj08aQFM1RdAskHApmhRkopHAgHU9ayp84RJop6Q0lMFME2cRs4UYoA674HQAHIt2bgUtNt7+R76PYzUUAS0gb2QbF8npA35rjTpSKV4urEQZhopQL0YCUEFfAG+xEC7s4//netFz+iDZZA8XSiEUsrFTUUwc1bAnHRE6nEk6+K012U4AtbX8JD25Cjo17e6wplzRaAR55FmCnAG5RIqlOnihQb3wXTLMnf/tAp11roc9HtlfG/6lbpf5ko5LcWgI2xGngVhPKkJ7/hNP2hYXN3LaZLjVEZKaO0rwHwbDdQx7uzkUr2VKIFUWwkGOItFJMZIMID54SaoewRFvpH6xup2WQzWx1v+YtoE6G1CnLqAtGMA9yHEfBoMoPesB3WPdr5Y0OyhN4txujgdRjcEG7q3i4uNi6UcphYyn9YGHlDXOS0enkzSVsw71J0OkfTLelEHRkepoBMDqMnN+MHgBMJEUpZSueBJeYe8y5AAT8rBRDGniwLIxDx12MiGyr11pTNV5iLHysEOreHyYL2rG1G8CMxLApZe0PqU9uLoE2Bc2+TvTOlQFTpQd9aNzfxZ37/y6G0utYhuAfN1QHSvBPvHy0AI6kYIJYqQztJwxkrykfKg/OcrsC6vdsVWj4D5Cjn0rgL7Wzz4QUiMgv26FayBbWD2r+JnyOnHwgPbwX7TyvcEPmLf42BdWe1KrZ7FYPUKmJM+DCu2P7Rg1lfiC9jxA5641xPbRB8FwBeCMDNB5/VgN9jvLmcXhqTvr4D9cI9P6Ir7/DDnbcyEe2YOeI72XRz3YDo7cMxrgl2GSDn9AmZiUZWAsOcPhHWYSahIVRh/IWYjrKvZZBmlSwTRJQAISxdk7CobWYYuXXHUA5wARlfJkD1XyawyD1Bk6Rhdpc+Z1lG4Fm+G/1bkEhX8SczlnaXP8Juz5TddEmZvDz4eOQAAAABJRU5ErkJggg==</Image>
<Url type="application/x-suggestions+json" template="http://api.bing.com/osjson.aspx">
<Param name="query" value="{searchTerms}"/>
<Param name="form" value="MOZW"/>
</Url>
<Url type="text/html" method="GET" template="http://www.bing.com/search">
<Param name="q" value="{searchTerms}"/>
<MozParam name="pc" condition="pref" pref="ms-pc"/>
<Param name="form" value="MOZW"/>
</Url>
<SearchForm>http://www.bing.com/search</SearchForm>
</SearchPlugin>

Просмотреть файл

@ -1,4 +1,4 @@
bing
bingmetrofx
googlemetrofx
wikipedia
yahoo

Просмотреть файл

@ -88,12 +88,6 @@ var Appbar = {
this._updateStarButton();
},
onDownloadButton: function() {
let notificationBox = Browser.getNotificationBox();
notificationBox.notificationsHidden = !notificationBox.notificationsHidden;
ContextUI.dismiss();
},
onPinButton: function() {
if (this.pinButton.checked) {
Browser.pinSite();

Просмотреть файл

@ -254,7 +254,7 @@
<observes element="bcast_urlbarState" attribute="*"/>
<hbox id="toolbar-context-page" pack="end">
<circularprogressindicator id="download-progress"
oncommand="Appbar.onDownloadButton()"/>
oncommand="Downloads.onDownloadButton()"/>
<toolbarbutton id="star-button" class="appbar-primary"
type="checkbox"
oncommand="Appbar.onStarButton()"/>

Просмотреть файл

@ -88,7 +88,7 @@ var Downloads = {
case 0: // Downloading
case 5: // Queued
this.watchDownload(dl);
this.updateInfobar(dl);
this.updateInfobar();
break;
}
}
@ -194,7 +194,6 @@ var Downloads = {
},
showNotification: function dh_showNotification(title, msg, buttons, priority) {
this._notificationBox.notificationsHidden = false;
let notification = this._notificationBox.appendNotification(msg,
title,
URI_GENERIC_ICON_DOWNLOAD,
@ -335,9 +334,10 @@ var Downloads = {
this._downloadProgressIndicator.updateProgress(percentComplete);
},
_computeDownloadProgressString: function dv_computeDownloadProgressString(aDownload) {
_computeDownloadProgressString: function dv_computeDownloadProgressString() {
let totTransferred = 0, totSize = 0, totSecondsLeft = 0;
for (let [guid, info] of this._progressNotificationInfo) {
let guid, info;
for ([guid, info] of this._progressNotificationInfo) {
let size = info.download.size;
let amountTransferred = info.download.amountTransferred;
let speed = info.download.speed;
@ -358,7 +358,7 @@ var Downloads = {
if (this._downloadCount == 1) {
return Strings.browser.GetStringFromName("alertDownloadsStart2")
.replace("#1", aDownload.displayName)
.replace("#1", info.download.displayName)
.replace("#2", progress)
.replace("#3", timeLeft)
}
@ -380,12 +380,20 @@ var Downloads = {
this._progressNotificationInfo.set(aDownload.guid, infoObj);
},
updateInfobar: function dv_updateInfobar(aDownload) {
let message = this._computeDownloadProgressString(aDownload);
this._updateCircularProgressMeter();
onDownloadButton: function dv_onDownloadButton() {
if (this._progressNotification) {
let progressBar = this._notificationBox.getNotificationWithValue("download-progress");
if (progressBar) {
this._notificationBox.removeNotification(progressBar);
} else {
this.updateInfobar();
}
}
},
if (this._notificationBox && this._notificationBox.notificationsHidden)
this._notificationBox.notificationsHidden = false;
updateInfobar: function dv_updateInfobar() {
let message = this._computeDownloadProgressString();
this._updateCircularProgressMeter();
if (this._progressNotification == null ||
!this._notificationBox.getNotificationWithValue("download-progress")) {
@ -457,7 +465,7 @@ var Downloads = {
case "dl-start":
let download = aSubject.QueryInterface(Ci.nsIDownload);
this.watchDownload(download);
this.updateInfobar(download);
this.updateInfobar();
break;
case "dl-done":
this._downloadsInProgress--;

Просмотреть файл

@ -124,7 +124,6 @@ HelperAppLauncherDialog.prototype = {
className: "download-host-text"
}
);
notificationBox.notificationsHidden = false;
let newBar = notificationBox.appendNotification("",
"save-download",
URI_GENERIC_ICON_DOWNLOAD,

Просмотреть файл

@ -34,9 +34,8 @@ function prefObserver(subject, topic, data) {
if (enable && !Social.provider) {
// this will result in setting Social.provider
SocialService.getOrderedProviderList(function(providers) {
Social._updateProviderCache(providers);
Social.enabled = true;
Services.obs.notifyObservers(null, "social:providers-changed", null);
Social._updateProviderCache(providers);
});
} else if (!enable && Social.provider) {
Social.provider = null;
@ -168,7 +167,7 @@ this.Social = {
// Retrieve the current set of providers, and set the current provider.
SocialService.getOrderedProviderList(function (providers) {
Social._updateProviderCache(providers);
Social._updateWorkerState(true);
Social._updateWorkerState(SocialService.enabled);
});
}
@ -181,10 +180,16 @@ this.Social = {
Services.obs.notifyObservers(null, "social:" + topic, origin);
return;
}
if (topic == "provider-enabled" || topic == "provider-disabled") {
if (topic == "provider-enabled") {
Social._updateProviderCache(providers);
Social._updateWorkerState(Social.enabled);
Services.obs.notifyObservers(null, "social:" + topic, origin);
return;
}
if (topic == "provider-disabled") {
// a provider was removed from the list of providers, that does not
// affect worker state for other providers
Social._updateProviderCache(providers);
Social._updateWorkerState(true);
Services.obs.notifyObservers(null, "social:providers-changed", null);
Services.obs.notifyObservers(null, "social:" + topic, origin);
return;
}
@ -194,7 +199,6 @@ this.Social = {
Social._updateProviderCache(providers);
let provider = Social._getProviderFromOrigin(origin);
provider.reload();
Services.obs.notifyObservers(null, "social:providers-changed", null);
}
});
},
@ -210,6 +214,7 @@ this.Social = {
// Called to update our cache of providers and set the current provider
_updateProviderCache: function (providers) {
this.providers = providers;
Services.obs.notifyObservers(null, "social:providers-changed", null);
// If social is currently disabled there's nothing else to do other than
// to notify about the lack of a provider.

Просмотреть файл

@ -4,4 +4,4 @@
# 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/.
DIRS += ['chrome']
DIRS += ['chrome', 'unit']

Просмотреть файл

@ -0,0 +1,9 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += ['social']
XPCSHELL_TESTS_MANIFESTS += ['social/xpcshell.ini']

Просмотреть файл

@ -0,0 +1,11 @@
# 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/.
XPCSHELL_RESOURCES = \
xpcshell.ini \
head.js \
blocklist.xml \
test_social.js \
test_socialDisabledStartup.js \
$(NULL)

Просмотреть файл

@ -0,0 +1,6 @@
<?xml version="1.0"?>
<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
<emItems>
<emItem blockID="s1" id="bad.com@services.mozilla.org"></emItem>
</emItems>
</blocklist>

Просмотреть файл

@ -0,0 +1,146 @@
/* 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/. */
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
var Social, SocialService;
let manifests = [
{
name: "provider 1",
origin: "https://example1.com",
sidebarURL: "https://example1.com/sidebar/",
},
{
name: "provider 2",
origin: "https://example2.com",
sidebarURL: "https://example1.com/sidebar/",
}
];
const MANIFEST_PREFS = Services.prefs.getBranch("social.manifest.");
// SocialProvider class relies on blocklisting being enabled. To enable
// blocklisting, we have to setup an app and initialize the blocklist (see
// initApp below).
const gProfD = do_get_profile();
const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1";
const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}");
function createAppInfo(id, name, version, platformVersion) {
gAppInfo = {
// nsIXULAppInfo
vendor: "Mozilla",
name: name,
ID: id,
version: version,
appBuildID: "2007010101",
platformVersion: platformVersion ? platformVersion : "1.0",
platformBuildID: "2007010101",
// nsIXULRuntime
inSafeMode: false,
logConsoleErrors: true,
OS: "XPCShell",
XPCOMABI: "noarch-spidermonkey",
invalidateCachesOnRestart: function invalidateCachesOnRestart() {
// Do nothing
},
// nsICrashReporter
annotations: {},
annotateCrashReport: function(key, data) {
this.annotations[key] = data;
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo,
Ci.nsIXULRuntime,
Ci.nsICrashReporter,
Ci.nsISupports])
};
var XULAppInfoFactory = {
createInstance: function (outer, iid) {
if (outer != null)
throw Components.results.NS_ERROR_NO_AGGREGATION;
return gAppInfo.QueryInterface(iid);
}
};
var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo",
XULAPPINFO_CONTRACTID, XULAppInfoFactory);
}
function initApp() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
// prepare a blocklist file for the blocklist service
var blocklistFile = gProfD.clone();
blocklistFile.append("blocklist.xml");
if (blocklistFile.exists())
blocklistFile.remove(false);
var source = do_get_file("blocklist.xml");
source.copyTo(gProfD, "blocklist.xml");
}
function setManifestPref(manifest) {
let string = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
string.data = JSON.stringify(manifest);
Services.prefs.setComplexValue("social.manifest." + manifest.origin, Ci.nsISupportsString, string);
}
function do_wait_observer(topic, cb) {
function observer(subject, topic, data) {
Services.obs.removeObserver(observer, topic);
cb();
}
Services.obs.addObserver(observer, topic, false);
}
function do_initialize_social(enabledOnStartup, cb) {
initApp();
manifests.forEach(function (manifest) {
setManifestPref(manifest);
});
// Set both providers active and flag the first one as "current"
let activeVal = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
let active = {};
for (let m of manifests)
active[m.origin] = 1;
activeVal.data = JSON.stringify(active);
Services.prefs.setComplexValue("social.activeProviders",
Ci.nsISupportsString, activeVal);
Services.prefs.setCharPref("social.provider.current", manifests[0].origin);
Services.prefs.setBoolPref("social.enabled", enabledOnStartup);
do_register_cleanup(function() {
manifests.forEach(function (manifest) {
Services.prefs.clearUserPref("social.manifest." + manifest.origin);
});
Services.prefs.clearUserPref("social.enabled");
Services.prefs.clearUserPref("social.provider.current");
Services.prefs.clearUserPref("social.activeProviders");
});
// expecting 2 providers installed
do_wait_observer("social:providers-changed", function() {
do_check_eq(Social.providers.length, 2, "2 providers installed");
cb();
});
// import and initialize everything
SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
do_check_eq(SocialService.enabled, enabledOnStartup, "service is doing its thing");
do_check_true(SocialService.hasEnabledProviders, "Service has enabled providers");
Social = Cu.import("resource:///modules/Social.jsm", {}).Social;
do_check_false(Social.initialized, "Social is not initialized");
Social.init();
do_check_true(Social.initialized, "Social is initialized");
}

Просмотреть файл

@ -0,0 +1,6 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

Просмотреть файл

@ -0,0 +1,34 @@
/* 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/. */
function run_test() {
// we are testing worker startup specifically
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
do_register_cleanup(function() {
Services.prefs.clearUserPref("social.allowMultipleWorkers");
});
do_test_pending();
add_test(testStartupEnabled);
add_test(testDisableAfterStartup);
do_initialize_social(true, run_next_test);
}
function testStartupEnabled() {
// wait on startup before continuing
do_check_eq(Social.providers.length, 2, "two social providers enabled");
do_check_true(Social.providers[0].enabled, "provider is enabled");
do_check_true(Social.providers[1].enabled, "provider is enabled");
run_next_test();
}
function testDisableAfterStartup() {
do_wait_observer("social:provider-set", function() {
do_check_eq(Social.enabled, false, "Social is disabled");
do_check_false(Social.providers[0].enabled, "provider is enabled");
do_check_false(Social.providers[1].enabled, "provider is enabled");
do_test_finished();
run_next_test();
});
Social.enabled = false;
}

Просмотреть файл

@ -0,0 +1,35 @@
/* 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/. */
function run_test() {
// we are testing worker startup specifically
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
do_register_cleanup(function() {
Services.prefs.clearUserPref("social.allowMultipleWorkers");
});
do_test_pending();
add_test(testStartupDisabled);
add_test(testEnableAfterStartup);
do_initialize_social(false, run_next_test);
}
function testStartupDisabled() {
// wait on startup before continuing
do_check_eq(Social.providers.length, 2, "two social providers available");
do_check_false(Social.providers[0].enabled, "provider is enabled");
do_check_false(Social.providers[1].enabled, "provider is enabled");
run_next_test();
}
function testEnableAfterStartup() {
do_wait_observer("social:provider-set", function() {
do_check_true(Social.enabled, "Social is enabled");
do_check_eq(Social.providers.length, 2, "two social providers available");
do_check_true(Social.providers[0].enabled, "provider is enabled");
do_check_true(Social.providers[1].enabled, "provider is enabled");
do_test_finished();
run_next_test();
});
Social.enabled = true;
}

Просмотреть файл

@ -0,0 +1,8 @@
[DEFAULT]
head = head.js
tail =
firefox-appdir = browser
[test_social.js]
[test_socialDisabledStartup.js]

Просмотреть файл

@ -1540,10 +1540,9 @@ abstract public class GeckoApp
final Context context = GeckoApp.this;
AnnouncementsBroadcastService.recordLastLaunch(context);
// Kick off our background services that fetch product
// announcements and upload health reports. We do this by
// invoking the broadcast receiver, which uses the system alarm
// infrastructure to perform tasks at intervals.
// Kick off our background services. We do this by invoking the broadcast
// receiver, which uses the system alarm infrastructure to perform tasks at
// intervals.
GeckoPreferences.broadcastAnnouncementsPref(context);
GeckoPreferences.broadcastHealthReportUploadPref(context);
@ -1938,6 +1937,7 @@ abstract public class GeckoApp
public void onPause()
{
final BrowserHealthRecorder rec = mHealthRecorder;
final Context context = this;
// In some way it's sad that Android will trigger StrictMode warnings
// here as the whole point is to save to disk while the activity is not
@ -1952,6 +1952,11 @@ abstract public class GeckoApp
rec.recordSessionEnd("P", editor);
}
editor.commit();
// In theory, the first browser session will not run long enough that we need to
// prune during it and we'd rather run it when the browser is inactive so we wait
// until here to register the prune service.
GeckoPreferences.broadcastHealthReportPrune(context);
}
});

Просмотреть файл

@ -77,6 +77,7 @@ public class GeckoPreferences
private static String PREFS_HEALTHREPORT_LINK = NON_PREF_PREFIX + "healthreport.link";
private static String PREFS_DEVTOOLS_REMOTE_ENABLED = "devtools.debugger.remote-enabled";
private static String PREFS_DISPLAY_REFLOW_ON_ZOOM = "browser.zoom.reflowOnZoom";
private static String PREFS_SYNC = NON_PREF_PREFIX + "sync";
public static String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
@ -342,6 +343,11 @@ public class GeckoPreferences
CharSequence selectedEntry = listPref.getEntry();
listPref.setSummary(selectedEntry);
continue;
} else if (PREFS_SYNC.equals(key) && GeckoProfile.get(this).inGuestMode()) {
// Don't show sync prefs while in guest mode.
preferences.removePreference(pref);
i--;
continue;
}
// Some Preference UI elements are not actually preferences,
@ -379,6 +385,11 @@ public class GeckoPreferences
return sIsCharEncodingEnabled;
}
public static void broadcastAction(final Context context, final Intent intent) {
fillIntentWithProfileInfo(context, intent);
context.sendBroadcast(intent, GlobalConstants.PER_ANDROID_PACKAGE_PERMISSION);
}
/**
* Broadcast an intent with <code>pref</code>, <code>branch</code>, and
* <code>enabled</code> extras. This is intended to represent the
@ -391,24 +402,23 @@ public class GeckoPreferences
final String action,
final String pref,
final boolean value) {
final Intent intent = new Intent(action);
intent.setAction(action);
intent.putExtra("pref", pref);
intent.putExtra("branch", GeckoApp.PREFS_NAME);
intent.putExtra("enabled", value);
final Intent intent = new Intent(action)
.putExtra("pref", pref)
.putExtra("branch", GeckoApp.PREFS_NAME)
.putExtra("enabled", value);
broadcastAction(context, intent);
}
private static void fillIntentWithProfileInfo(final Context context, final Intent intent) {
// There is a race here, but GeckoProfile returns the default profile
// when Gecko is not explicitly running for a different profile. In a
// multi-profile world, this will need to be updated (possibly to
// broadcast settings for all profiles). See Bug 882182.
GeckoProfile profile = GeckoProfile.get(context);
if (profile != null) {
intent.putExtra("profileName", profile.getName());
intent.putExtra("profilePath", profile.getDir().getAbsolutePath());
intent.putExtra("profileName", profile.getName())
.putExtra("profilePath", profile.getDir().getAbsolutePath());
}
Log.d(LOGTAG, "Broadcast: " + action + ", " + pref + ", " + GeckoApp.PREFS_NAME + ", " + value);
context.sendBroadcast(intent, GlobalConstants.PER_ANDROID_PACKAGE_PERMISSION);
}
/**
@ -451,6 +461,11 @@ public class GeckoPreferences
broadcastHealthReportUploadPref(context, value);
}
public static void broadcastHealthReportPrune(final Context context) {
final Intent intent = new Intent(HealthReportConstants.ACTION_HEALTHREPORT_PRUNE);
broadcastAction(context, intent);
}
/**
* Return the value of the named preference in the default preferences file.
*

Просмотреть файл

@ -66,13 +66,15 @@ public final class GeckoProfile {
if (((GeckoApp)context).mProfile != null) {
return ((GeckoApp)context).mProfile;
}
}
GeckoProfile guest = GeckoProfile.getGuestProfile(context);
// if the guest profile is locked, return it
if (guest != null && guest.locked()) {
return guest;
}
// If the guest profile exists and is locked, return it
GeckoProfile guest = GeckoProfile.getGuestProfile(context);
if (guest != null && guest.locked()) {
return guest;
}
if (context instanceof GeckoApp) {
// Otherwise, get the default profile for the Activity
return get(context, ((GeckoApp)context).getDefaultProfileName());
}

Просмотреть файл

@ -109,7 +109,10 @@ public class TabsPanel extends LinearLayout
mTabWidget.addTab(R.drawable.tabs_normal, R.string.tabs_normal);
mTabWidget.addTab(R.drawable.tabs_private, R.string.tabs_private);
mTabWidget.addTab(R.drawable.tabs_synced, R.string.tabs_synced);
if (!GeckoProfile.get(mContext).inGuestMode()) {
mTabWidget.addTab(R.drawable.tabs_synced, R.string.tabs_synced);
}
mTabWidget.setTabSelectionListener(this);
}

Просмотреть файл

@ -19,7 +19,6 @@ SYNC_JAVA_FILES := \
background/announcements/AnnouncementsFetcher.java \
background/announcements/AnnouncementsFetchResourceDelegate.java \
background/announcements/AnnouncementsService.java \
background/announcements/AnnouncementsStartReceiver.java \
background/BackgroundService.java \
background/bagheera/BagheeraClient.java \
background/bagheera/BagheeraRequestDelegate.java \
@ -41,6 +40,8 @@ SYNC_JAVA_FILES := \
background/db/Tab.java \
background/healthreport/Environment.java \
background/healthreport/EnvironmentBuilder.java \
background/healthreport/HealthReportBroadcastReceiver.java \
background/healthreport/HealthReportBroadcastService.java \
background/healthreport/HealthReportDatabases.java \
background/healthreport/HealthReportDatabaseStorage.java \
background/healthreport/HealthReportGenerator.java \
@ -48,11 +49,12 @@ SYNC_JAVA_FILES := \
background/healthreport/HealthReportStorage.java \
background/healthreport/HealthReportUtils.java \
background/healthreport/ProfileInformationCache.java \
background/healthreport/prune/HealthReportPruneService.java \
background/healthreport/prune/PrunePolicy.java \
background/healthreport/prune/PrunePolicyDatabaseStorage.java \
background/healthreport/prune/PrunePolicyStorage.java \
background/healthreport/upload/AndroidSubmissionClient.java \
background/healthreport/upload/HealthReportBroadcastReceiver.java \
background/healthreport/upload/HealthReportBroadcastService.java \
background/healthreport/upload/HealthReportUploadService.java \
background/healthreport/upload/HealthReportUploadStartReceiver.java \
background/healthreport/upload/ObsoleteDocumentTracker.java \
background/healthreport/upload/SubmissionClient.java \
background/healthreport/upload/SubmissionPolicy.java \

Просмотреть файл

@ -12,7 +12,6 @@ import org.mozilla.gecko.background.common.log.Logger;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
@ -53,11 +52,6 @@ public abstract class BackgroundService extends IntentService {
return networkInfo.isAvailable();
}
protected static PendingIntent createPendingIntent(Context context, Class<? extends BroadcastReceiver> broadcastReceiverClass) {
final Intent service = new Intent(context, broadcastReceiverClass);
return PendingIntent.getBroadcast(context, 0, service, PendingIntent.FLAG_CANCEL_CURRENT);
}
protected AlarmManager getAlarmManager() {
return getAlarmManager(this.getApplicationContext());
}
@ -79,6 +73,8 @@ public abstract class BackgroundService extends IntentService {
protected void cancelAlarm(PendingIntent pendingIntent) {
final AlarmManager alarm = getAlarmManager();
alarm.cancel(pendingIntent);
// For testing - allows us to see if the intent, and thus the alarm, has been cancelled.
pendingIntent.cancel();
}
/**

Просмотреть файл

@ -18,7 +18,7 @@ import android.content.SharedPreferences.Editor;
/**
* A service which listens to broadcast intents from the system and from the
* browser, registering or unregistering the main
* {@link AnnouncementsStartReceiver} with the {@link AlarmManager}.
* {@link AnnouncementsService} with the {@link AlarmManager}.
*/
public class AnnouncementsBroadcastService extends BackgroundService {
private static final String WORKER_THREAD_NAME = "AnnouncementsBroadcastServiceWorker";
@ -28,10 +28,24 @@ public class AnnouncementsBroadcastService extends BackgroundService {
super(WORKER_THREAD_NAME);
}
private void toggleAlarm(final Context context, boolean enabled) {
Logger.info(LOG_TAG, (enabled ? "R" : "Unr") + "egistering announcements broadcast receiver...");
protected static SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH,
GlobalConstants.SHARED_PREFERENCES_MODE);
}
final PendingIntent pending = createPendingIntent(context, AnnouncementsStartReceiver.class);
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH,
GlobalConstants.SHARED_PREFERENCES_MODE);
}
private void toggleAlarm(final Context context, boolean enabled) {
final Class<?> serviceClass = AnnouncementsService.class;
Logger.info(LOG_TAG, (enabled ? "R" : "Unr") + "egistering " + serviceClass.getSimpleName() +
".");
final Intent service = new Intent(context, serviceClass);
final PendingIntent pending = PendingIntent.getService(context, 0, service,
PendingIntent.FLAG_CANCEL_CURRENT);
if (!enabled) {
cancelAlarm(pending);
@ -51,7 +65,7 @@ public class AnnouncementsBroadcastService extends BackgroundService {
*/
public static void recordLastLaunch(final Context context) {
final long now = System.currentTimeMillis();
final SharedPreferences preferences = context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
final SharedPreferences preferences = getSharedPreferences(context);
// One of several things might be true, according to our logs:
//
@ -93,12 +107,12 @@ public class AnnouncementsBroadcastService extends BackgroundService {
}
public static long getPollInterval(final Context context) {
SharedPreferences preferences = context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
final SharedPreferences preferences = getSharedPreferences(context);
return preferences.getLong(AnnouncementsConstants.PREF_ANNOUNCE_FETCH_INTERVAL_MSEC, AnnouncementsConstants.DEFAULT_ANNOUNCE_FETCH_INTERVAL_MSEC);
}
public static void setPollInterval(final Context context, long interval) {
SharedPreferences preferences = context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
final SharedPreferences preferences = getSharedPreferences(context);
preferences.edit().putLong(AnnouncementsConstants.PREF_ANNOUNCE_FETCH_INTERVAL_MSEC, interval).commit();
}
@ -146,8 +160,7 @@ public class AnnouncementsBroadcastService extends BackgroundService {
// Primarily intended for debugging and testing, but this doesn't do any harm.
if (!enabled) {
Logger.info(LOG_TAG, "!enabled: clearing last fetch.");
final SharedPreferences sharedPreferences = this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH,
GlobalConstants.SHARED_PREFERENCES_MODE);
final SharedPreferences sharedPreferences = getSharedPreferences();
final Editor editor = sharedPreferences.edit();
editor.remove(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME);
editor.remove(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH);

Просмотреть файл

@ -118,6 +118,11 @@ public class AnnouncementsService extends BackgroundService implements Announcem
Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG);
Logger.debug(LOG_TAG, "Running AnnouncementsService.");
if (AnnouncementsConstants.DISABLED) {
Logger.debug(LOG_TAG, "Announcements disabled. Returning from AnnouncementsService.");
return;
}
if (!shouldFetchAnnouncements()) {
Logger.debug(LOG_TAG, "Not fetching.");
return;
@ -136,7 +141,7 @@ public class AnnouncementsService extends BackgroundService implements Announcem
return getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_LAUNCH, 0);
}
private SharedPreferences getSharedPreferences() {
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
}

Просмотреть файл

@ -47,6 +47,7 @@ public class GlobalConstants {
public static String GECKO_PREFERENCES_CLASS = "org.mozilla.gecko.GeckoPreferences";
public static String GECKO_BROADCAST_ANNOUNCEMENTS_PREF_METHOD = "broadcastAnnouncementsPref";
public static String GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD = "broadcastHealthReportUploadPref";
public static String GECKO_BROADCAST_HEALTHREPORT_PRUNE_METHOD = "broadcastHealthReportPrune";
// Common time values.
public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;

Просмотреть файл

@ -32,8 +32,11 @@ public class EnvironmentBuilder {
* Fetch the storage object associated with the provided
* {@link ContentProviderClient}. If no storage instance can be found --
* perhaps because the {@link ContentProvider} is running in a different
* process -- returns <code>null</code>.
*
* process -- returns <code>null</code>. On success, the returned
* {@link HealthReportDatabaseStorage} instance is owned by the underlying
* {@link HealthReportProvider} and thus does not need to be closed by the
* caller.
*
* If the provider is not a {@link HealthReportProvider}, throws a
* {@link ClassCastException}, because that would be disastrous.
*/

Просмотреть файл

@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport;
import org.mozilla.gecko.background.common.log.Logger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* Watch for notifications to start Health Report background services.
*
* Some observations:
*
* From the Android documentation: "Also note that as of Android 3.0 the user
* needs to have started the application at least once before your application
* can receive android.intent.action.BOOT_COMPLETED events."
*
* We really do want to launch on BOOT_COMPLETED, since it's possible for a user
* to run Firefox, shut down the phone, then power it on again on the same day.
* We want to submit a health report in this case, even though they haven't
* launched Firefox since boot.
*/
public class HealthReportBroadcastReceiver extends BroadcastReceiver {
public static final String LOG_TAG = HealthReportBroadcastReceiver.class.getSimpleName();
/**
* Forward the intent to an IntentService to do background processing.
*/
@Override
public void onReceive(Context context, Intent intent) {
Logger.debug(LOG_TAG, "Received intent - forwarding to BroadcastService.");
Intent service = new Intent(context, HealthReportBroadcastService.class);
service.putExtras(intent);
service.setAction(intent.getAction());
context.startService(service);
}
}

Просмотреть файл

@ -0,0 +1,244 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport;
import org.mozilla.gecko.background.BackgroundService;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService;
import org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService;
import org.mozilla.gecko.background.healthreport.upload.ObsoleteDocumentTracker;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
/**
* A service which listens to broadcast intents from the system and from the
* browser, registering or unregistering the background health report services with the
* {@link AlarmManager}.
*/
public class HealthReportBroadcastService extends BackgroundService {
public static final String LOG_TAG = HealthReportBroadcastService.class.getSimpleName();
public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker";
public HealthReportBroadcastService() {
super(WORKER_THREAD_NAME);
}
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
}
public long getSubmissionPollInterval() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, HealthReportConstants.DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC);
}
public void setSubmissionPollInterval(final long interval) {
getSharedPreferences().edit().putLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, interval).commit();
}
public long getPrunePollInterval() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC,
HealthReportConstants.DEFAULT_PRUNE_INTENT_INTERVAL_MSEC);
}
public void setPrunePollInterval(final long interval) {
getSharedPreferences().edit().putLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC,
interval).commit();
}
/**
* Set or cancel an alarm to submit data for a profile.
*
* @param context
* Android context.
* @param profileName
* to submit data for.
* @param profilePath
* to submit data for.
* @param enabled
* whether the user has enabled submitting health report data for
* this profile.
* @param serviceEnabled
* whether submitting should be scheduled. If the user turns off
* submitting, <code>enabled</code> could be false but we could need
* to delete so <code>serviceEnabled</code> could be true.
*/
protected void toggleSubmissionAlarm(final Context context, String profileName, String profilePath,
boolean enabled, boolean serviceEnabled) {
final Class<?> serviceClass = HealthReportUploadService.class;
Logger.info(LOG_TAG, (serviceEnabled ? "R" : "Unr") + "egistering " +
serviceClass.getSimpleName() + ".");
// PendingIntents are compared without reference to their extras. Therefore
// even though we pass the profile details to the action, different
// profiles will share the *same* pending intent. In a multi-profile future,
// this will need to be addressed. See Bug 882182.
final Intent service = new Intent(context, serviceClass);
service.setAction("upload"); // PendingIntents "lose" their extras if no action is set.
service.putExtra("uploadEnabled", enabled);
service.putExtra("profileName", profileName);
service.putExtra("profilePath", profilePath);
final PendingIntent pending = PendingIntent.getService(context, 0, service, PendingIntent.FLAG_CANCEL_CURRENT);
if (!serviceEnabled) {
cancelAlarm(pending);
return;
}
final long pollInterval = getSubmissionPollInterval();
scheduleAlarm(pollInterval, pending);
}
@Override
protected void onHandleIntent(Intent intent) {
Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
// The same intent can be handled by multiple methods so do not short-circuit evaluate.
boolean handled = attemptHandleIntentForUpload(intent);
handled = attemptHandleIntentForPrune(intent) ? true : handled;
if (!handled) {
Logger.warn(LOG_TAG, "Unhandled intent with action " + intent.getAction() + ".");
}
}
/**
* Attempts to handle the given intent for FHR document upload. If it cannot, false is returned.
*/
protected boolean attemptHandleIntentForUpload(final Intent intent) {
if (HealthReportConstants.UPLOAD_FEATURE_DISABLED) {
Logger.debug(LOG_TAG, "Health report upload feature is compile-time disabled; not handling intent.");
return false;
}
final String action = intent.getAction();
Logger.debug(LOG_TAG, "Health report upload feature is compile-time enabled; handling intent with action " + action + ".");
if (HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF.equals(action)) {
handleUploadPrefIntent(intent);
return true;
}
if (Intent.ACTION_BOOT_COMPLETED.equals(action) ||
Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
BackgroundService.reflectContextToFennec(this,
GlobalConstants.GECKO_PREFERENCES_CLASS,
GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD);
return true;
}
return false;
}
/**
* Handle the intent sent by the browser when it wishes to notify us
* of the value of the user preference. Look at the value and toggle the
* alarm service accordingly.
*/
protected void handleUploadPrefIntent(Intent intent) {
if (!intent.hasExtra("enabled")) {
Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without enabled. Ignoring.");
return;
}
final boolean enabled = intent.getBooleanExtra("enabled", true);
Logger.debug(LOG_TAG, intent.getStringExtra("branch") + "/" +
intent.getStringExtra("pref") + " = " +
(intent.hasExtra("enabled") ? enabled : ""));
String profileName = intent.getStringExtra("profileName");
String profilePath = intent.getStringExtra("profilePath");
if (profileName == null || profilePath == null) {
Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without profilePath or profileName. Ignoring.");
return;
}
Logger.pii(LOG_TAG, "Updating health report upload alarm for profile " + profileName + " at " +
profilePath + ".");
final SharedPreferences sharedPrefs = getSharedPreferences();
final ObsoleteDocumentTracker tracker = new ObsoleteDocumentTracker(sharedPrefs);
final boolean hasObsoleteIds = tracker.hasObsoleteIds();
if (!enabled) {
final Editor editor = sharedPrefs.edit();
editor.remove(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID);
if (hasObsoleteIds) {
Logger.debug(LOG_TAG, "Health report upload disabled; scheduling deletion of " + tracker.numberOfObsoleteIds() + " documents.");
tracker.limitObsoleteIds();
} else {
// Primarily intended for debugging and testing.
Logger.debug(LOG_TAG, "Health report upload disabled and no deletes to schedule: clearing prefs.");
editor.remove(HealthReportConstants.PREF_FIRST_RUN);
editor.remove(HealthReportConstants.PREF_NEXT_SUBMISSION);
}
editor.commit();
}
// The user can toggle us off or on, or we can have obsolete documents to
// remove.
final boolean serviceEnabled = hasObsoleteIds || enabled;
toggleSubmissionAlarm(this, profileName, profilePath, enabled, serviceEnabled);
}
/**
* Attempts to handle the given intent for FHR data pruning. If it cannot, false is returned.
*/
protected boolean attemptHandleIntentForPrune(final Intent intent) {
final String action = intent.getAction();
Logger.debug(LOG_TAG, "Prune: Handling intent with action, " + action + ".");
if (HealthReportConstants.ACTION_HEALTHREPORT_PRUNE.equals(action)) {
handlePruneIntent(intent);
return true;
}
if (Intent.ACTION_BOOT_COMPLETED.equals(action) ||
Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
BackgroundService.reflectContextToFennec(this,
GlobalConstants.GECKO_PREFERENCES_CLASS,
GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_PRUNE_METHOD);
return true;
}
return false;
}
protected void handlePruneIntent(final Intent intent) {
final String profileName = intent.getStringExtra("profileName");
final String profilePath = intent.getStringExtra("profilePath");
if (profileName == null || profilePath == null) {
Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_PRUNE + " intent " +
"without profilePath or profileName. Ignoring.");
return;
}
final Class<?> serviceClass = HealthReportPruneService.class;
final Intent service = new Intent(this, serviceClass);
service.setAction("prune"); // Intents without actions have their extras removed.
service.putExtra("profileName", profileName);
service.putExtra("profilePath", profilePath);
final PendingIntent pending = PendingIntent.getService(this, 0, service,
PendingIntent.FLAG_CANCEL_CURRENT);
// Set a regular alarm to start PruneService. Since the various actions that PruneService can
// take occur on irregular intervals, we can be more efficient by only starting the Service
// when one of these time limits runs out. However, subsequent Service invocations must then
// be registered by the PruneService itself, which would fail if the PruneService crashes.
// Thus, we set this regular (and slightly inefficient) alarm.
Logger.info(LOG_TAG, "Registering " + serviceClass.getSimpleName() + ".");
final long pollInterval = getPrunePollInterval();
scheduleAlarm(pollInterval, pending);
}
}

Просмотреть файл

@ -24,13 +24,17 @@ public class HealthReportConstants {
// settings are stored.
public static final String PREFS_BRANCH = "background";
// How frequently the submission policy is ticked over. This is how frequently our
// intent is scheduled to be called by the Android Alarm Manager, not how
// frequently we actually submit.
// How frequently the submission and prune policies are ticked over. This is how frequently our
// intent is scheduled to be called by the Android Alarm Manager, not how frequently we
// actually submit. These values are set as preferences rather than constants so that testing
// addons can change their values.
public static final String PREF_SUBMISSION_INTENT_INTERVAL_MSEC = "healthreport_submission_intent_interval_msec";
public static final long DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC = GlobalConstants.MILLISECONDS_PER_DAY / 24;
public static final String PREF_PRUNE_INTENT_INTERVAL_MSEC = "healthreport_prune_intent_interval_msec";
public static final long DEFAULT_PRUNE_INTENT_INTERVAL_MSEC = GlobalConstants.MILLISECONDS_PER_DAY;
public static final String ACTION_HEALTHREPORT_UPLOAD_PREF = "@ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF";
public static final String ACTION_HEALTHREPORT_PRUNE = "@ANDROID_PACKAGE_NAME@.HEALTHREPORT_PRUNE";
public static final String PREF_MINIMUM_TIME_BETWEEN_UPLOADS = "healthreport_time_between_uploads";
public static final long DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS = GlobalConstants.MILLISECONDS_PER_DAY;
@ -104,4 +108,19 @@ public class HealthReportConstants {
// will handle such API usage. This obsoletes 2 days worth of old documents
// at a time.
public static final int MAXIMUM_DELETIONS_PER_POST = ((int) DEFAULT_MAXIMUM_FAILURES_PER_DAY + 1) * 2;
public static final String PREF_PRUNE_BY_SIZE_TIME = "healthreport_prune_by_size_time";
public static final long MINIMUM_TIME_BETWEEN_PRUNE_BY_SIZE_CHECKS_MILLIS =
GlobalConstants.MILLISECONDS_PER_DAY;
public static final int MAX_ENVIRONMENT_COUNT = 50;
public static final int ENVIRONMENT_COUNT_AFTER_PRUNE = 35;
public static final int MAX_EVENT_COUNT = 10000;
public static final int EVENT_COUNT_AFTER_PRUNE = 8000;
public static final String PREF_EXPIRATION_TIME = "healthreport_expiration_time";
public static final long MINIMUM_TIME_BETWEEN_EXPIRATION_CHECKS_MILLIS = GlobalConstants.MILLISECONDS_PER_DAY * 7;
public static final long EVENT_EXISTENCE_DURATION = GlobalConstants.MILLISECONDS_PER_SIX_MONTHS;
public static final String PREF_CLEANUP_TIME = "healthreport_cleanup_time";
public static final long MINIMUM_TIME_BETWEEN_CLEANUP_CHECKS_MILLIS = GlobalConstants.MILLISECONDS_PER_DAY * 30;
}

Просмотреть файл

@ -247,111 +247,104 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
@Override
public void onCreate(SQLiteDatabase db) {
db.beginTransaction();
try {
db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" body TEXT, " +
" UNIQUE (body) " +
")");
db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" body TEXT, " +
" UNIQUE (body) " +
")");
db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" hash TEXT, " +
" profileCreation INTEGER, " +
" cpuCount INTEGER, " +
" memoryMB INTEGER, " +
" isBlocklistEnabled INTEGER, " +
" isTelemetryEnabled INTEGER, " +
" extensionCount INTEGER, " +
" pluginCount INTEGER, " +
" themeCount INTEGER, " +
" architecture TEXT, " +
" sysName TEXT, " +
" sysVersion TEXT, " +
" vendor TEXT, " +
" appName TEXT, " +
" appID TEXT, " +
" appVersion TEXT, " +
" appBuildID TEXT, " +
" platformVersion TEXT, " +
" platformBuildID TEXT, " +
" os TEXT, " +
" xpcomabi TEXT, " +
" updateChannel TEXT, " +
" addonsID INTEGER, " +
" FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " +
" UNIQUE (hash) " +
")");
db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" hash TEXT, " +
" profileCreation INTEGER, " +
" cpuCount INTEGER, " +
" memoryMB INTEGER, " +
" isBlocklistEnabled INTEGER, " +
" isTelemetryEnabled INTEGER, " +
" extensionCount INTEGER, " +
" pluginCount INTEGER, " +
" themeCount INTEGER, " +
" architecture TEXT, " +
" sysName TEXT, " +
" sysVersion TEXT, " +
" vendor TEXT, " +
" appName TEXT, " +
" appID TEXT, " +
" appVersion TEXT, " +
" appBuildID TEXT, " +
" platformVersion TEXT, " +
" platformBuildID TEXT, " +
" os TEXT, " +
" xpcomabi TEXT, " +
" updateChannel TEXT, " +
" addonsID INTEGER, " +
" FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " +
" UNIQUE (hash) " +
")");
db.execSQL("CREATE TABLE measurements (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" name TEXT, " +
" version INTEGER, " +
" UNIQUE (name, version) " +
")");
db.execSQL("CREATE TABLE measurements (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" name TEXT, " +
" version INTEGER, " +
" UNIQUE (name, version) " +
")");
db.execSQL("CREATE TABLE fields (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" measurement INTEGER, " +
" name TEXT, " +
" flags INTEGER, " +
" FOREIGN KEY (measurement) REFERENCES measurements(id) ON DELETE CASCADE, " +
" UNIQUE (measurement, name)" +
")");
db.execSQL("CREATE TABLE fields (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" measurement INTEGER, " +
" name TEXT, " +
" flags INTEGER, " +
" FOREIGN KEY (measurement) REFERENCES measurements(id) ON DELETE CASCADE, " +
" UNIQUE (measurement, name)" +
")");
db.execSQL("CREATE TABLE " + EVENTS_INTEGER + "(" +
" date INTEGER, " +
" env INTEGER, " +
" field INTEGER, " +
" value INTEGER, " +
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
")");
db.execSQL("CREATE TABLE " + EVENTS_INTEGER + "(" +
" date INTEGER, " +
" env INTEGER, " +
" field INTEGER, " +
" value INTEGER, " +
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
")");
db.execSQL("CREATE TABLE " + EVENTS_TEXTUAL + "(" +
" date INTEGER, " +
" env INTEGER, " +
" field INTEGER, " +
" value TEXT, " +
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
")");
db.execSQL("CREATE TABLE " + EVENTS_TEXTUAL + "(" +
" date INTEGER, " +
" env INTEGER, " +
" field INTEGER, " +
" value TEXT, " +
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
")");
db.execSQL("CREATE INDEX idx_events_integer_date_env_field ON events_integer (date, env, field)");
db.execSQL("CREATE INDEX idx_events_textual_date_env_field ON events_textual (date, env, field)");
db.execSQL("CREATE INDEX idx_events_integer_date_env_field ON events_integer (date, env, field)");
db.execSQL("CREATE INDEX idx_events_textual_date_env_field ON events_textual (date, env, field)");
db.execSQL("CREATE VIEW events AS " +
"SELECT date, env, field, value FROM " + EVENTS_INTEGER + " " +
"UNION ALL " +
"SELECT date, env, field, value FROM " + EVENTS_TEXTUAL);
db.execSQL("CREATE VIEW events AS " +
"SELECT date, env, field, value FROM " + EVENTS_INTEGER + " " +
"UNION ALL " +
"SELECT date, env, field, value FROM " + EVENTS_TEXTUAL);
db.execSQL("CREATE VIEW named_events AS " +
"SELECT date, " +
" environments.hash AS environment, " +
" measurements.name AS measurement_name, " +
" measurements.version AS measurement_version, " +
" fields.name AS field_name, " +
" fields.flags AS field_flags, " +
" value FROM " +
"events JOIN environments ON events.env = environments.id " +
" JOIN fields ON events.field = fields.id " +
" JOIN measurements ON fields.measurement = measurements.id");
db.execSQL("CREATE VIEW named_events AS " +
"SELECT date, " +
" environments.hash AS environment, " +
" measurements.name AS measurement_name, " +
" measurements.version AS measurement_version, " +
" fields.name AS field_name, " +
" fields.flags AS field_flags, " +
" value FROM " +
"events JOIN environments ON events.env = environments.id " +
" JOIN fields ON events.field = fields.id " +
" JOIN measurements ON fields.measurement = measurements.id");
db.execSQL("CREATE VIEW named_fields AS " +
"SELECT measurements.name AS measurement_name, " +
" measurements.id AS measurement_id, " +
" measurements.version AS measurement_version, " +
" fields.name AS field_name, " +
" fields.id AS field_id, " +
" fields.flags AS field_flags " +
"FROM fields JOIN measurements ON fields.measurement = measurements.id");
db.execSQL("CREATE VIEW named_fields AS " +
"SELECT measurements.name AS measurement_name, " +
" measurements.id AS measurement_id, " +
" measurements.version AS measurement_version, " +
" fields.name AS field_name, " +
" fields.id AS field_id, " +
" fields.flags AS field_flags " +
"FROM fields JOIN measurements ON fields.measurement = measurements.id");
db.execSQL("CREATE VIEW current_measurements AS " +
"SELECT name, MAX(version) AS version FROM measurements GROUP BY name");
db.execSQL("CREATE VIEW current_measurements AS " +
"SELECT name, MAX(version) AS version FROM measurements GROUP BY name");
createAddonsEnvironmentsView(db);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
createAddonsEnvironmentsView(db);
}
@Override
@ -432,7 +425,6 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
Logger.info(LOG_TAG, "onUpgrade: from " + oldVersion + " to " + newVersion + ".");
try {
db.beginTransaction();
switch (oldVersion) {
case 2:
upgradeDatabaseFrom2To3(db);
@ -441,12 +433,9 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
case 4:
upgradeDatabaseFrom4to5(db);
}
db.setTransactionSuccessful();
} catch (Exception e) {
Logger.error(LOG_TAG, "Failure in onUpgrade.", e);
throw new RuntimeException(e);
} finally {
db.endTransaction();
}
}
@ -1028,6 +1017,19 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
this.helper.getWritableDatabase().endTransaction();
}
protected int getIntFromQuery(final String sql, final String[] selectionArgs) {
final SQLiteDatabase db = this.helper.getReadableDatabase();
final Cursor c = db.rawQuery(sql, selectionArgs);
try {
if (!c.moveToFirst()) {
throw new IllegalStateException("Cursor is empty.");
}
return c.getInt(0);
} finally {
c.close();
}
}
@Override
public int getDay(long time) {
return DateUtils.getDay(time);
@ -1251,6 +1253,135 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
"date, environment, measurement_name, measurement_version, field_name");
}
public int getEventCount() {
return getRowCount("events");
}
public int getEnvironmentCount() {
return getRowCount("environments");
}
private int getRowCount(String table) {
// table should be parameterized, but SQL throws a compilation error if the table in unknown
// in advance.
return getIntFromQuery("SELECT COUNT(*) from " + table, null);
}
/**
* Deletes all environments, addons, and events from the database before the given time. If this
* data does not have recorded dates but are no longer referenced by other fields, they are also
* removed (with exception to the current environment).
*
* @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
* @param curEnv The ID of the current environment.
* @return The number of environments and addon entries deleted.
*/
public int deleteDataBefore(final long time, final int curEnv) {
final SQLiteDatabase db = this.helper.getWritableDatabase();
db.beginTransaction();
int numRowsDeleted = 0;
try {
numRowsDeleted += deleteEnvAndEventsBefore(db, time, curEnv);
numRowsDeleted += deleteOrphanedAddons(db);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return numRowsDeleted;
}
/**
* Deletes environments and their referring events recorded before the given time. Environments
* referenced by no events are deleted, except for the current environment.
*
* @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
* @param curEnv The ID of the current environment.
* @return The number of environments (not events) deleted.
*/
protected int deleteEnvAndEventsBefore(final long time, final int curEnv) {
final SQLiteDatabase db = this.helper.getWritableDatabase();
return deleteEnvAndEventsBefore(db, time, curEnv);
}
// Called internally only to ensure the same db instance is used.
protected int deleteEnvAndEventsBefore(final SQLiteDatabase db, final long time, final int curEnv) {
// Delete environments only referenced by events occuring before the given time. Cascade
// delete these events.
String whereClause =
"(SELECT COUNT(*) FROM events WHERE date >= ? " +
" AND events.env = environments.id) = 0 " +
"AND id IN (SELECT DISTINCT env FROM events WHERE date < ?)";
final int day = this.getDay(time);
final String dayString = Integer.toString(day, 10);
String[] whereArgs = new String[] {dayString, dayString};
int numEnvDeleted = 0;
db.beginTransaction();
try {
numEnvDeleted += db.delete("environments", whereClause, whereArgs);
numEnvDeleted += deleteOrphanedEnv(db, curEnv);
// We can't get the number of events deleted through cascading deletions so we do not record
// the number of events deleted here.
deleteEventsBefore(db, dayString);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return numEnvDeleted;
}
/**
* Deletes environments not referenced by any events except for the given current environment.
*/
protected int deleteOrphanedEnv(final int curEnv) {
final SQLiteDatabase db = this.helper.getWritableDatabase();
return deleteOrphanedEnv(db, curEnv);
}
// Called internally only to ensure the same db instance is used.
protected int deleteOrphanedEnv(final SQLiteDatabase db, final int curEnv) {
final String whereClause =
"id != ? AND " +
"id NOT IN (SELECT env FROM events)";
final String[] whereArgs = new String[] {Integer.toString(curEnv)};
return db.delete("environments", whereClause, whereArgs);
}
protected int deleteEventsBefore(final String dayString) {
final SQLiteDatabase db = this.helper.getWritableDatabase();
return deleteEventsBefore(db, dayString);
}
// Called internally only to ensure the same db instance is used.
protected int deleteEventsBefore(final SQLiteDatabase db, final String dayString) {
final String whereClause = "date < ?";
final String[] whereArgs = new String[] {dayString};
int numEventsDeleted = 0;
db.beginTransaction();
try {
numEventsDeleted += db.delete("events_integer", whereClause, whereArgs);
numEventsDeleted += db.delete("events_textual", whereClause, whereArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return numEventsDeleted;
}
/**
* Deletes addons not referenced by any environments.
*/
protected int deleteOrphanedAddons() {
final SQLiteDatabase db = this.helper.getWritableDatabase();
return deleteOrphanedAddons(db);
}
// Called internally only to ensure the same db instance is used.
protected int deleteOrphanedAddons(final SQLiteDatabase db) {
final String whereClause = "id NOT IN (SELECT addonsID FROM environments)";
return db.delete("addons", whereClause, null);
}
/**
* Retrieve a mapping from a table. Keys should be unique; only one key-value
* pair will be returned for each key.
@ -1336,4 +1467,78 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
db.endTransaction();
}
}
/**
* Prunes the given number of least-recently used environments. Note that orphaned environments
* are not removed.
*/
public void pruneEnvironments(final int numToPrune) {
final SQLiteDatabase db = this.helper.getWritableDatabase();
db.beginTransaction();
try {
db.delete("environments",
"id in (SELECT env " +
" FROM events " +
" GROUP BY env " +
" ORDER BY MAX(date), env " +
" LIMIT " + numToPrune + ")",
null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* Prunes up to a maximum of the given number of the oldest events. While it is more correct to
* prune the exact given amount, there is no unique identifier among events so we cannot be so
* precise. Instead, we prune on date, deleting all events up to the day before our count of
* events reaches the given maximum. Note that this technicality means this method cannot be
* used to delete all events.
*/
public void pruneEvents(final int maxNumToPrune) {
final SQLiteDatabase db = this.helper.getWritableDatabase();
final Cursor c = db.rawQuery(
"SELECT MAX(date) " +
"FROM (SELECT date " +
" FROM events " +
" ORDER BY date " +
" LIMIT " + maxNumToPrune + ")",
null);
long pruneDate = -1;
try {
if (!c.moveToFirst()) {
Logger.debug(LOG_TAG, "No max date found in events: table is likely empty. Not pruning " +
"events.");
return;
}
pruneDate = c.getLong(0);
} finally {
c.close();
}
final String selection = "date < " + pruneDate;
db.beginTransaction();
try {
db.delete(EVENTS_INTEGER, selection, null);
db.delete(EVENTS_TEXTUAL, selection, null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void vacuum() {
final SQLiteDatabase db = this.helper.getWritableDatabase();
db.execSQL("vacuum");
}
/**
* Disables auto_vacuuming. Changes may only take effect after a "vacuum" command.
*/
public void disableAutoVacuuming() {
final SQLiteDatabase db = this.helper.getWritableDatabase();
db.execSQL("PRAGMA auto_vacuum=0");
}
}

Просмотреть файл

@ -99,6 +99,9 @@ public class HealthReportProvider extends ContentProvider {
@Override
public void onLowMemory() {
// While we could prune the database here, it wouldn't help - it would restore disk space
// rather then lower our RAM usage. Additionally, pruning the database may use even more
// memory and take too long to run in this method.
super.onLowMemory();
databases.closeDatabaseHelpers();
}

Просмотреть файл

@ -219,6 +219,20 @@ public interface HealthReportStorage {
public void deleteEverything();
public void deleteEnvironments();
public void deleteMeasurements();
/**
* Deletes all environments, addons, and events from the database before the given time.
*
* @param time milliseconds since epoch.
* @param curEnv The ID of the current environment.
* @return The number of environments and addon entries deleted.
*/
public int deleteDataBefore(final long time, final int curEnv);
public int getEventCount();
public int getEnvironmentCount();
public void pruneEvents(final int num);
public void pruneEnvironments(final int num);
public void enqueueOperation(Runnable runnable);
}

Просмотреть файл

@ -0,0 +1,79 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport.prune;
import org.mozilla.gecko.background.BackgroundService;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
/**
* A <code>Service</code> to prune unnecessary or excessive health report data.
*
* We extend <code>IntentService</code>, rather than just <code>Service</code>,
* because this gives us a worker thread to avoid excessive main-thread disk access.
*/
public class HealthReportPruneService extends BackgroundService {
public static final String LOG_TAG = HealthReportPruneService.class.getSimpleName();
public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker";
public HealthReportPruneService() {
super(WORKER_THREAD_NAME);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
public void onHandleIntent(Intent intent) {
Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
Logger.debug(LOG_TAG, "Handling prune intent.");
if (!isIntentValid(intent)) {
Logger.warn(LOG_TAG, "Intent not valid - returning.");
return;
}
final String profileName = intent.getStringExtra("profileName");
final String profilePath = intent.getStringExtra("profilePath");
Logger.debug(LOG_TAG, "Ticking for profile " + profileName + " at " + profilePath + ".");
final PrunePolicy policy = getPrunePolicy(profilePath);
policy.tick(System.currentTimeMillis());
}
// Generator function wraps constructor for testing purposes.
protected PrunePolicy getPrunePolicy(final String profilePath) {
final PrunePolicyStorage storage = new PrunePolicyDatabaseStorage(this, profilePath);
return new PrunePolicy(storage, getSharedPreferences());
}
protected boolean isIntentValid(final Intent intent) {
boolean isValid = true;
final String profileName = intent.getStringExtra("profileName");
if (profileName == null) {
Logger.warn(LOG_TAG, "Got intent without profileName.");
isValid = false;
}
final String profilePath = intent.getStringExtra("profilePath");
if (profilePath == null) {
Logger.warn(LOG_TAG, "Got intent without profilePath.");
isValid = false;
}
return isValid;
}
}

Просмотреть файл

@ -0,0 +1,230 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport.prune;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import android.content.SharedPreferences;
/**
* Manages scheduling of the pruning of old Firefox Health Report data.
*
* There are three main actions that take place:
* 1) Excessive storage pruning: The recorded data is taking up an unreasonable amount of space.
* 2) Expired data pruning: Data that is kept around longer than is useful.
* 3) Cleanup: To deal with storage maintenance (e.g. bloat and fragmentation)
*
* (1) and (2) are performed periodically on their own schedules. (3) will activate after a
* certain duration but only after (1) or (2) is performed.
*/
public class PrunePolicy {
public static final String LOG_TAG = PrunePolicy.class.getSimpleName();
protected final PrunePolicyStorage storage;
protected final SharedPreferences sharedPreferences;
protected final Editor editor;
public PrunePolicy(final PrunePolicyStorage storage, final SharedPreferences sharedPrefs) {
this.storage = storage;
this.sharedPreferences = sharedPrefs;
this.editor = new Editor(this.sharedPreferences.edit());
}
protected SharedPreferences getSharedPreferences() {
return this.sharedPreferences;
}
public void tick(final long time) {
try {
try {
boolean pruned = attemptPruneBySize(time);
pruned = attemptExpiration(time) ? true : pruned;
// We only need to cleanup after a large pruning.
if (pruned) {
attemptStorageCleanup(time);
}
} catch (Exception e) {
// While catching Exception is ordinarily bad form, this Service runs in the same process
// as Fennec so if we crash, it crashes. Additionally, this Service runs regularly so
// these crashes could be regular. Thus, we choose to quietly fail instead.
Logger.error(LOG_TAG, "Got exception pruning document.", e);
} finally {
editor.commit();
}
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception committing to SharedPreferences.", e);
} finally {
storage.close();
}
}
protected boolean attemptPruneBySize(final long time) {
final long nextPrune = getNextPruneBySizeTime();
if (nextPrune < 0) {
Logger.debug(LOG_TAG, "Initializing prune-by-size time.");
editor.setNextPruneBySizeTime(time + getMinimumTimeBetweenPruneBySizeChecks());
return false;
}
// If the system clock is skewed into the past, making the time between prunes too long, reset
// the clock.
if (nextPrune > getMinimumTimeBetweenPruneBySizeChecks() + time) {
Logger.debug(LOG_TAG, "Clock skew detected - resetting prune-by-size time.");
editor.setNextPruneBySizeTime(time + getMinimumTimeBetweenPruneBySizeChecks());
return false;
}
if (nextPrune > time) {
Logger.debug(LOG_TAG, "Skipping prune-by-size - wait period has not yet elapsed.");
return false;
}
// Prune environments first because their cascading deletions may delete some events. These
// environments are pruned in order of least-recently used first. Note that orphaned
// environments are ignored here and should be removed elsewhere.
final int environmentCount = storage.getEnvironmentCount();
if (environmentCount > getMaxEnvironmentCount()) {
final int environmentPruneCount = environmentCount - getEnvironmentCountAfterPrune();
Logger.debug(LOG_TAG, "Pruning " + environmentPruneCount + " environments.");
storage.pruneEnvironments(environmentPruneCount);
}
final int eventCount = storage.getEventCount();
if (eventCount > getMaxEventCount()) {
final int eventPruneCount = eventCount - getEventCountAfterPrune();
Logger.debug(LOG_TAG, "Pruning up to " + eventPruneCount + " events.");
storage.pruneEvents(eventPruneCount);
}
editor.setNextPruneBySizeTime(time + getMinimumTimeBetweenPruneBySizeChecks());
return true;
}
protected boolean attemptExpiration(final long time) {
final long nextPrune = getNextExpirationTime();
if (nextPrune < 0) {
Logger.debug(LOG_TAG, "Initializing expiration time.");
editor.setNextExpirationTime(time + getMinimumTimeBetweenExpirationChecks());
return false;
}
// If the system clock is skewed into the past, making the time between prunes too long, reset
// the clock.
if (nextPrune > getMinimumTimeBetweenExpirationChecks() + time) {
Logger.debug(LOG_TAG, "Clock skew detected - resetting expiration time.");
editor.setNextExpirationTime(time + getMinimumTimeBetweenExpirationChecks());
return false;
}
if (nextPrune > time) {
Logger.debug(LOG_TAG, "Skipping expiration - wait period has not yet elapsed.");
return false;
}
final long oldEventTime = time - getEventExistenceDuration();
Logger.debug(LOG_TAG, "Pruning data older than " + oldEventTime + ".");
storage.deleteDataBefore(oldEventTime);
editor.setNextExpirationTime(time + getMinimumTimeBetweenExpirationChecks());
return true;
}
protected boolean attemptStorageCleanup(final long time) {
// Cleanup if max duration since last cleanup is exceeded.
final long nextCleanup = getNextCleanupTime();
if (nextCleanup < 0) {
Logger.debug(LOG_TAG, "Initializing cleanup time.");
editor.setNextCleanupTime(time + getMinimumTimeBetweenCleanupChecks());
return false;
}
// If the system clock is skewed into the past, making the time between cleanups too long,
// reset the clock.
if (nextCleanup > getMinimumTimeBetweenCleanupChecks() + time) {
Logger.debug(LOG_TAG, "Clock skew detected - resetting cleanup time.");
editor.setNextCleanupTime(time + getMinimumTimeBetweenCleanupChecks());
return false;
}
if (nextCleanup > time) {
Logger.debug(LOG_TAG, "Skipping cleanup - wait period has not yet elapsed.");
return false;
}
editor.setNextCleanupTime(time + getMinimumTimeBetweenCleanupChecks());
storage.cleanup();
return true;
}
protected static class Editor {
protected final SharedPreferences.Editor editor;
public Editor(final SharedPreferences.Editor editor) {
this.editor = editor;
}
public void commit() {
editor.commit();
}
public Editor setNextExpirationTime(final long time) {
editor.putLong(HealthReportConstants.PREF_EXPIRATION_TIME, time);
return this;
}
public Editor setNextPruneBySizeTime(final long time) {
editor.putLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, time);
return this;
}
public Editor setNextCleanupTime(final long time) {
editor.putLong(HealthReportConstants.PREF_CLEANUP_TIME, time);
return this;
}
}
private long getNextExpirationTime() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_EXPIRATION_TIME, -1L);
}
private long getEventExistenceDuration() {
return HealthReportConstants.EVENT_EXISTENCE_DURATION;
}
private long getMinimumTimeBetweenExpirationChecks() {
return HealthReportConstants.MINIMUM_TIME_BETWEEN_EXPIRATION_CHECKS_MILLIS;
}
private long getNextPruneBySizeTime() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, -1L);
}
private long getMinimumTimeBetweenPruneBySizeChecks() {
return HealthReportConstants.MINIMUM_TIME_BETWEEN_PRUNE_BY_SIZE_CHECKS_MILLIS;
}
private int getMaxEnvironmentCount() {
return HealthReportConstants.MAX_ENVIRONMENT_COUNT;
}
private int getEnvironmentCountAfterPrune() {
return HealthReportConstants.ENVIRONMENT_COUNT_AFTER_PRUNE;
}
private int getMaxEventCount() {
return HealthReportConstants.MAX_EVENT_COUNT;
}
private int getEventCountAfterPrune() {
return HealthReportConstants.EVENT_COUNT_AFTER_PRUNE;
}
private long getNextCleanupTime() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_CLEANUP_TIME, -1L);
}
private long getMinimumTimeBetweenCleanupChecks() {
return HealthReportConstants.MINIMUM_TIME_BETWEEN_CLEANUP_CHECKS_MILLIS;
}
}

Просмотреть файл

@ -0,0 +1,131 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport.prune;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.Environment;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
import android.content.ContentProviderClient;
import android.content.Context;
/**
* Abstracts over the Storage instance behind the PrunePolicy. The underlying storage instance is
* a {@link HealthReportDatabaseStorage} instance. Since our cleanup routine vacuums, auto_vacuum
* can be disabled. It is enabled by default, however, turning it off requires an expensive vacuum
* so we wait until our first {@link cleanup} call since we are vacuuming anyway.
*/
public class PrunePolicyDatabaseStorage implements PrunePolicyStorage {
public static final String LOG_TAG = PrunePolicyDatabaseStorage.class.getSimpleName();
private final Context context;
private final String profilePath;
private ContentProviderClient client;
private HealthReportDatabaseStorage storage;
private int currentEnvironmentID; // So we don't prune the current environment.
public PrunePolicyDatabaseStorage(final Context context, final String profilePath) {
this.context = context;
this.profilePath = profilePath;
this.currentEnvironmentID = -1;
}
public void pruneEvents(final int count) {
getStorage().pruneEvents(count);
}
public void pruneEnvironments(final int count) {
getStorage().pruneEnvironments(count);
}
/**
* Deletes data recorded before the given time. Note that if this method fails to retrieve the
* current environment from the profile cache, it will not delete data so be sure to prune by
* other methods (e.g. {@link pruneEvents}) as well.
*/
public int deleteDataBefore(final long time) {
return getStorage().deleteDataBefore(time, getCurrentEnvironmentID());
}
public void cleanup() {
final HealthReportDatabaseStorage storage = getStorage();
// The change to auto_vacuum will only take affect after a vacuum.
storage.disableAutoVacuuming();
storage.vacuum();
}
public int getEventCount() {
return getStorage().getEventCount();
}
public int getEnvironmentCount() {
return getStorage().getEnvironmentCount();
}
public void close() {
if (client != null) {
client.release();
client = null;
}
}
/**
* Retrieves the {@link HealthReportDatabaseStorage} associated with the profile of the policy.
* For efficiency, the underlying {@link ContentProviderClient} and
* {@link HealthReportDatabaseStorage} are cached for later invocations. However, this means a
* call to this method MUST be accompanied by a call to {@link close}. Throws
* {@link IllegalStateException} if the storage instance could not be retrieved - note that the
* {@link ContentProviderClient} instance will not be closed in this case and
* {@link releaseClient} should still be called.
*/
protected HealthReportDatabaseStorage getStorage() {
if (storage != null) {
return storage;
}
client = EnvironmentBuilder.getContentProviderClient(context);
if (client == null) {
// TODO: Record prune failures and submit as part of FHR upload.
Logger.warn(LOG_TAG, "Unable to get ContentProviderClient - throwing.");
throw new IllegalStateException("Unable to get ContentProviderClient.");
}
try {
storage = EnvironmentBuilder.getStorage(client, profilePath);
if (storage == null) {
// TODO: Record prune failures and submit as part of FHR upload.
Logger.warn(LOG_TAG,"Unable to get HealthReportDatabaseStorage for " + profilePath +
" - throwing.");
throw new IllegalStateException("Unable to get HealthReportDatabaseStorage for " +
profilePath + " (== null).");
}
} catch (ClassCastException ex) {
// TODO: Record prune failures and submit as part of FHR upload.
Logger.warn(LOG_TAG,"Unable to get HealthReportDatabaseStorage for " + profilePath +
profilePath + " (ClassCastException).");
throw new IllegalStateException("Unable to get HealthReportDatabaseStorage for " +
profilePath + ".", ex);
}
return storage;
}
protected int getCurrentEnvironmentID() {
if (currentEnvironmentID < 0) {
final ProfileInformationCache cache = new ProfileInformationCache(profilePath);
if (!cache.restoreUnlessInitialized()) {
throw new IllegalStateException("Current environment unknown.");
}
final Environment env = EnvironmentBuilder.getCurrentEnvironment(cache);
currentEnvironmentID = env.register();
}
return currentEnvironmentID;
}
}

Просмотреть файл

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport.prune;
/**
* Abstracts over the Storage instance behind the PrunePolicy.
*/
public interface PrunePolicyStorage {
public void pruneEvents(final int count);
public void pruneEnvironments(final int count);
public int deleteDataBefore(final long time);
public void cleanup();
public int getEventCount();
public int getEnvironmentCount();
/**
* Release the resources owned by this helper. MUST be called before this helper is garbage
* collected.
*/
public void close();
}

Просмотреть файл

@ -11,7 +11,8 @@
xmlns:gecko="http://schemas.android.com/apk/res-auto"
android:enabled="false">
<org.mozilla.gecko.SyncPreference android:title="@string/pref_sync"
<org.mozilla.gecko.SyncPreference android:key="android.not_a_preference.sync"
android:title="@string/pref_sync"
android:persistent="false" />
<PreferenceScreen android:title="@string/pref_category_customize"

Просмотреть файл

@ -12,7 +12,8 @@
android:title="@string/pref_category_customize"
android:enabled="false">
<org.mozilla.gecko.SyncPreference android:title="@string/pref_sync"
<org.mozilla.gecko.SyncPreference android:key="android.not_a_preference.sync"
android:title="@string/pref_sync"
android:persistent="false" />
<PreferenceScreen android:title="@string/pref_category_search"

Просмотреть файл

@ -12,7 +12,8 @@
xmlns:gecko="http://schemas.android.com/apk/res-auto"
android:enabled="false">
<org.mozilla.gecko.SyncPreference android:title="@string/pref_sync"
<org.mozilla.gecko.SyncPreference android:key="android.not_a_preference.sync"
android:title="@string/pref_sync"
android:persistent="false" />
<PreferenceScreen android:title="@string/pref_category_customize" >

Просмотреть файл

@ -49,7 +49,7 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
private static final int VERIFY_URL_TIMEOUT = 2000;
private static final int MAX_LIST_ATTEMPTS = 3;
private static final int MAX_WAIT_ENABLED_TEXT_MS = 10000;
private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 10000;
private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 15000;
public static final int MAX_WAIT_MS = 4500;
// IDs for UI views

Просмотреть файл

@ -4,11 +4,12 @@ package @ANDROID_PACKAGE_NAME@.tests;
import @ANDROID_PACKAGE_NAME@.*;
abstract class PixelTest extends BaseTest {
private static final long PAINT_CLEAR_DELAY = 3000; // milliseconds
private static final long PAINT_CLEAR_DELAY = 10000; // milliseconds
protected final PaintedSurface loadAndGetPainted(String url) {
Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
loadUrl(url);
verifyHomePagerHidden();
paintExpecter.blockUntilClear(PAINT_CLEAR_DELAY);
paintExpecter.unregisterListener();
PaintedSurface p = mDriver.getPaintedSurface();

Просмотреть файл

@ -12,7 +12,6 @@ public class testCheck2 extends PixelTest {
blockForGeckoReady();
loadAndPaint(url);
verifyHomePagerHidden();
mDriver.setupScrollHandling();

Просмотреть файл

@ -18,7 +18,7 @@ public class testSystemPages extends PixelTest {
public void testSystemPages() {
blockForGeckoReady();
String urls [] = { "about:firefox", "about:rights", "about:home", "about:addons", "about:downloads", "about:buildconfig", "about:feedback", "about:healthreport", "about:" };
String urls [] = { "about:firefox", "about:rights", "about:addons", "about:downloads", "about:buildconfig", "about:feedback", "about:healthreport", "about:" };
// Pages to be tested from the menu and their expected urls. This if of the form { {{ <path to item> }, { <expected url> }}* }
String menuItems [][][] = {{{ "Apps" }, { "about:apps" }},
{{ "Downloads" }, { "about:downloads" }},

Просмотреть файл

@ -6,7 +6,6 @@ background/announcements/AnnouncementsFetchDelegate.java
background/announcements/AnnouncementsFetcher.java
background/announcements/AnnouncementsFetchResourceDelegate.java
background/announcements/AnnouncementsService.java
background/announcements/AnnouncementsStartReceiver.java
background/BackgroundService.java
background/bagheera/BagheeraClient.java
background/bagheera/BagheeraRequestDelegate.java
@ -28,6 +27,8 @@ background/db/CursorDumper.java
background/db/Tab.java
background/healthreport/Environment.java
background/healthreport/EnvironmentBuilder.java
background/healthreport/HealthReportBroadcastReceiver.java
background/healthreport/HealthReportBroadcastService.java
background/healthreport/HealthReportDatabases.java
background/healthreport/HealthReportDatabaseStorage.java
background/healthreport/HealthReportGenerator.java
@ -35,11 +36,12 @@ background/healthreport/HealthReportProvider.java
background/healthreport/HealthReportStorage.java
background/healthreport/HealthReportUtils.java
background/healthreport/ProfileInformationCache.java
background/healthreport/prune/HealthReportPruneService.java
background/healthreport/prune/PrunePolicy.java
background/healthreport/prune/PrunePolicyDatabaseStorage.java
background/healthreport/prune/PrunePolicyStorage.java
background/healthreport/upload/AndroidSubmissionClient.java
background/healthreport/upload/HealthReportBroadcastReceiver.java
background/healthreport/upload/HealthReportBroadcastService.java
background/healthreport/upload/HealthReportUploadService.java
background/healthreport/upload/HealthReportUploadStartReceiver.java
background/healthreport/upload/ObsoleteDocumentTracker.java
background/healthreport/upload/SubmissionClient.java
background/healthreport/upload/SubmissionPolicy.java

Просмотреть файл

@ -15,6 +15,3 @@
<action android:name="@ANDROID_PACKAGE_NAME@.ANNOUNCEMENTS_PREF" />
</intent-filter>
</receiver>
<receiver android:name="org.mozilla.gecko.background.announcements.AnnouncementsStartReceiver" >
</receiver>

Просмотреть файл

@ -7,12 +7,10 @@
Report upload preference changed" notifications sent by
Fennec: @ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF.
We have two receivers. BroadcastReceiver is a thin
receiver whose single purpose is to start the background
service in response to external events. UploadStartReceiver
handles scheduling background work.
BroadcastReceiver is a thin receiver whose purpose is to
start the background service in response to external events.
-->
<receiver android:name="org.mozilla.gecko.background.healthreport.upload.HealthReportBroadcastReceiver" >
<receiver android:name="org.mozilla.gecko.background.healthreport.HealthReportBroadcastReceiver" >
<intent-filter>
<!-- Startup. -->
<action android:name="android.intent.action.BOOT_COMPLETED" />
@ -24,7 +22,7 @@
<intent-filter >
<action android:name="@ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF" />
</intent-filter>
</receiver>
<receiver android:name="org.mozilla.gecko.background.healthreport.upload.HealthReportUploadStartReceiver" >
<intent-filter >
<action android:name="@ANDROID_PACKAGE_NAME@.HEALTHREPORT_PRUNE" />
</intent-filter>
</receiver>

Просмотреть файл

@ -6,9 +6,13 @@
-->
<service
android:exported="false"
android:name="org.mozilla.gecko.background.healthreport.upload.HealthReportBroadcastService" >
android:name="org.mozilla.gecko.background.healthreport.HealthReportBroadcastService" >
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService" >
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService" >
</service>

Просмотреть файл

@ -487,6 +487,9 @@ pref("devtools.debugger.enable-content-actors", true);
// Block tools from seeing / interacting with certified apps
pref("devtools.debugger.forbid-certified-apps", true);
// DevTools default color unit
pref("devtools.defaultColorUnit", "hex");
// view source
pref("view_source.syntax_highlight", true);
pref("view_source.wrap_long_lines", false);

Просмотреть файл

@ -47,6 +47,11 @@
#include "zlib.h"
#include <algorithm>
#ifdef MOZ_WIDGET_GONK
#include "nsINetworkManager.h"
#include "nsINetworkStatsServiceProxy.h"
#endif
// rather than slurp up all of nsIWebSocket.idl, which lives outside necko, just
// dupe one constant we need from it
#define CLOSE_GOING_AWAY 1001
@ -953,7 +958,12 @@ WebSocketChannel::WebSocketChannel() :
mDynamicOutputSize(0),
mDynamicOutput(nullptr),
mPrivateBrowsing(false),
mConnectionLogService(nullptr)
mConnectionLogService(nullptr),
mCountRecv(0),
mCountSent(0),
mAppId(0),
mConnectionType(NETWORK_NO_TYPE),
mIsInBrowser(false)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "not main thread");
@ -1062,6 +1072,16 @@ WebSocketChannel::BeginOpen()
return;
}
// obtain app info
if (localChannel) {
NS_GetAppInfo(localChannel, &mAppId, &mIsInBrowser);
}
// obtain active connection type
if (mAppId != NECKO_NO_APP_ID) {
GetConnectionType(&mConnectionType);
}
rv = localChannel->AsyncOpen(this, mHttpChannel);
if (NS_FAILED(rv)) {
LOG(("WebSocketChannel::BeginOpen: cannot async open\n"));
@ -2709,6 +2729,9 @@ WebSocketChannel::Close(uint16_t code, const nsACString & reason)
LOG(("WebSocketChannel::Close() %p\n", this));
NS_ABORT_IF_FALSE(NS_IsMainThread(), "not main thread");
// save the networkstats (bug 855949)
SaveNetworkStats(true);
if (mRequestedClose) {
return NS_OK;
}
@ -3035,6 +3058,9 @@ WebSocketChannel::OnInputStreamReady(nsIAsyncInputStream *aStream)
rv = mSocketIn->Read((char *)buffer, 2048, &count);
LOG(("WebSocketChannel::OnInputStreamReady: read %u rv %x\n", count, rv));
// accumulate received bytes
CountRecvBytes(count);
if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
mSocketIn->AsyncWait(this, 0, 0, mSocketThread);
return NS_OK;
@ -3114,6 +3140,9 @@ WebSocketChannel::OnOutputStreamReady(nsIAsyncOutputStream *aStream)
LOG(("WebSocketChannel::OnOutputStreamReady: write %u rv %x\n",
amtSent, rv));
// accumulate sent bytes
CountSentBytes(amtSent);
if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
mSocketOut->AsyncWait(this, 0, 0, nullptr);
return NS_OK;
@ -3239,5 +3268,74 @@ WebSocketChannel::OnDataAvailable(nsIRequest *aRequest,
return NS_OK;
}
nsresult
WebSocketChannel::GetConnectionType(int32_t *type)
{
#ifdef MOZ_WIDGET_GONK
MOZ_ASSERT(NS_IsMainThread());
nsresult result;
nsCOMPtr<nsINetworkManager> networkManager = do_GetService("@mozilla.org/network/manager;1", &result);
if (NS_FAILED(result) || !networkManager) {
*type = NETWORK_NO_TYPE;
}
nsCOMPtr<nsINetworkInterface> networkInterface;
result = networkManager->GetActive(getter_AddRefs(networkInterface));
if (networkInterface) {
result = networkInterface->GetType(type);
}
return NS_OK;
#else
return NS_ERROR_NOT_IMPLEMENTED;
#endif
}
nsresult
WebSocketChannel::SaveNetworkStats(bool enforce)
{
#ifdef MOZ_WIDGET_GONK
// Check if the connection type and app id are valid.
if(mConnectionType == NETWORK_NO_TYPE ||
mAppId == NECKO_NO_APP_ID) {
return NS_OK;
}
if (mCountRecv <= 0 && mCountSent <= 0) {
// There is no traffic, no need to save.
return NS_OK;
}
// If |enforce| is false, the traffic amount is saved
// only when the total amount exceeds the predefined
// threshold.
uint64_t totalBytes = mCountRecv + mCountSent;
if (!enforce && totalBytes < NETWORK_STATS_THRESHOLD) {
return NS_OK;
}
nsresult rv;
nsCOMPtr<nsINetworkStatsServiceProxy> mNetworkStatsServiceProxy =
do_GetService("@mozilla.org/networkstatsServiceProxy;1", &rv);
if (NS_FAILED(rv)) {
return rv;
}
mNetworkStatsServiceProxy->SaveAppStats(mAppId, mConnectionType, PR_Now() / 1000,
mCountRecv, mCountSent, nullptr);
// Reset the counters after saving.
mCountSent = 0;
mCountRecv = 0;
return NS_OK;
#else
return NS_ERROR_NOT_IMPLEMENTED;
#endif
}
} // namespace mozilla::net
} // namespace mozilla

Просмотреть файл

@ -252,6 +252,31 @@ private:
nsCOMPtr<nsIDashboardEventNotifier> mConnectionLogService;
uint32_t mSerial;
static uint32_t sSerialSeed;
// These members are used for network per-app metering (bug 855949)
// Currently, they are only available on gonk.
public:
const static int32_t NETWORK_NO_TYPE = -1; // default conntection type
const static uint64_t NETWORK_STATS_THRESHOLD = 65536;
private:
uint64_t mCountRecv;
uint64_t mCountSent;
uint32_t mAppId;
int32_t mConnectionType;
bool mIsInBrowser;
nsresult GetConnectionType(int32_t *);
nsresult SaveNetworkStats(bool);
void CountRecvBytes(uint64_t recvBytes)
{
mCountRecv += recvBytes;
SaveNetworkStats(false);
}
void CountSentBytes(uint64_t sentBytes)
{
mCountSent += sentBytes;
SaveNetworkStats(false);
}
};
class WebSocketSSLChannel : public WebSocketChannel

Просмотреть файл

@ -5,15 +5,20 @@
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
let gDashboard = Cc['@mozilla.org/network/dashboard;1']
Cu.import("resource://testing-common/httpd.js");
const gDashboard = Cc['@mozilla.org/network/dashboard;1']
.getService(Ci.nsIDashboard);
const RESOLVE_DISABLE_IPV6 = (1 << 5);
const gServerSocket = Components.classes["@mozilla.org/network/server-socket;1"]
.createInstance(Components.interfaces.nsIServerSocket);
const gHttpServer = new HttpServer();
add_test(function test_http() {
gDashboard.requestHttpConnections(function(data) {
do_check_neq(data.host.indexOf("example.com"), -1);
do_check_neq(data.host.indexOf("localhost"), -1);
run_next_test();
});
@ -21,32 +26,54 @@ add_test(function test_http() {
add_test(function test_dns() {
gDashboard.requestDNSInfo(function(data) {
do_check_neq(data.hostname.indexOf("example.com"), -1);
do_check_neq(data.hostname.indexOf("localhost"), -1);
do_test_pending();
gHttpServer.stop(do_test_finished);
run_next_test();
});
});
add_test(function test_sockets() {
let dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
let record = dns.resolve("example.com", RESOLVE_DISABLE_IPV6);
let answer = record.getNextAddrAsString();
let sts = Cc["@mozilla.org/network/socket-transport-service;1"]
.getService(Ci.nsISocketTransportService);
let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
gDashboard.requestSockets(function(data) {
do_check_neq(data.host.indexOf(answer), -1);
let transport = sts.createTransport(null, 0, "127.0.0.1",
gServerSocket.port, null);
let listener = {
onTransportStatus: function(aTransport, aStatus, aProgress, aProgressMax) {
if (aStatus == Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
gDashboard.requestSockets(function(data) {
gServerSocket.close();
run_next_test();
});
do_check_neq(data.host.indexOf("127.0.0.1"), -1);
run_next_test();
});
}
}
};
transport.setEventSink(listener, threadManager.currentThread);
transport.openOutputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0);
});
function run_test() {
let ioService = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService);
let uri = ioService.newURI("http://example.com", null, null);
gHttpServer.start(-1);
let uri = ioService.newURI("http://localhost:" + gHttpServer.identity.primaryPort,
null, null);
let channel = ioService.newChannelFromURI(uri);
channel.open();
gServerSocket.init(-1, true, -1);
run_next_test();
}

Просмотреть файл

@ -0,0 +1,64 @@
/* -*- Mode: Javasript; indent-tab-mode: nil; js-indent-level: 2 -*- */
/* 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/. */
const Cc = Components.classes;
const Ci = Components.interfaces;
const gDashboard = Cc['@mozilla.org/network/dashboard;1']
.getService(Ci.nsIDashboard);
function connectionFailed(status) {
let status_ok = [
"NS_NET_STATUS_RESOLVING_HOST"
,"NS_NET_STATUS_RESOLVED_HOST"
,"NS_NET_STATUS_CONNECTING_TO"
,"NS_NET_STATUS_CONNECTED_TO"
];
for (let i = 0; i < status_ok.length; i++) {
if (status == status_ok[i]) {
return false;
}
}
return true;
}
function run_test() {
let serverSocket = Components.classes["@mozilla.org/network/server-socket;1"]
.createInstance(Ci.nsIServerSocket);
serverSocket.init(-1, true, -1);
do_test_pending();
gDashboard.requestConnection("localhost", serverSocket.port,
"tcp", 15, function(connInfo) {
if (connInfo.status == "NS_NET_STATUS_CONNECTED_TO") {
do_test_pending();
gDashboard.requestDNSInfo(function(data) {
do_check_neq(data.hostname.indexOf("localhost"), -1);
do_test_finished();
});
do_test_pending();
gDashboard.requestSockets(function(data) {
let index = data.host.indexOf("127.0.0.1");
do_check_neq(index, -1);
do_check_eq(data.port[index], serverSocket.port);
do_check_eq(data.tcp[index], 1);
serverSocket.close();
do_test_finished();
});
do_test_finished();
}
if (connectionFailed(connInfo.status)) {
do_throw(connInfo.status);
}
});
}

Просмотреть файл

@ -247,3 +247,4 @@ skip-if = os == "android"
[test_about_protocol.js]
[test_bug856978.js]
[test_about_networking.js]
[test_ping_aboutnetworking.js]

Просмотреть файл

@ -401,19 +401,29 @@ Statement::internalFinalize(bool aDestructing)
//
char *msg = ::PR_smprintf("SQL statement (%x) should have been finalized"
"before garbage-collection. For more details on this statement, set"
"NSPR_LOG_MESSAGES=mozStorage:5 .",
" before garbage-collection. For more details on this statement, set"
" NSPR_LOG_MESSAGES=mozStorage:5 .",
mDBStatement);
//
// Note that we can't display the statement itself, as the data structure
// is not valid anymore. However, the address shown here should help
// developers correlate with the more complete debug message triggered
// by AsyncClose().
//
#if 0
// Deactivate the warning until we have fixed the exising culprit
// (see bug 914070).
NS_WARNING(msg);
#endif // 0
PR_LOG(gStorageLog, PR_LOG_WARNING, (msg));
::PR_smprintf_free(msg);
}
#endif // DEBUG
#endif
mDBStatement = nullptr;

Просмотреть файл

@ -445,7 +445,12 @@ class XPCShellTestThread(Thread):
def report_message(self, line):
""" Reports a message to a consumer, both as a strucutured and
human-readable log message. """
message = self.message_from_line(line).strip()
message = self.message_from_line(line)
if message.endswith('\n'):
# A new line is always added by head.js to delimit messages,
# however consumers will want to supply their own.
message = message[:-1]
if self.on_message:
self.on_message(line, message)

Просмотреть файл

@ -18,6 +18,7 @@ EXTRA_JS_MODULES += [
'osfile_win_allthreads.jsm',
'osfile_win_back.jsm',
'osfile_win_front.jsm',
'ospath_unix_back.jsm',
'ospath_win_back.jsm',
'ospath.jsm',
'ospath_unix.jsm',
'ospath_win.jsm',
]

Просмотреть файл

@ -1,5 +1,3 @@
"use strict";
/* 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/. */
@ -19,11 +17,16 @@
* @rejects {B} reason
*/
"use strict";
this.EXPORTED_SYMBOLS = ["OS"];
const Cu = Components.utils;
const Ci = Components.interfaces;
let SharedAll = {};
Components.utils.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
Components.utils.import("resource://gre/modules/Deprecated.jsm", this);
Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
Cu.import("resource://gre/modules/Deprecated.jsm", this);
// Boilerplate, to simplify the transition to require()
let OS = SharedAll.OS;
@ -35,27 +38,27 @@ let isTypedArray = OS.Shared.isTypedArray;
// The constructor for file errors.
let OSError;
if (OS.Constants.Win) {
Components.utils.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", this);
Components.utils.import("resource://gre/modules/osfile/ospath_win_back.jsm", this);
Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", this);
OSError = OS.Shared.Win.Error;
} else if (OS.Constants.libc) {
Components.utils.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", this);
Components.utils.import("resource://gre/modules/osfile/ospath_unix_back.jsm", this);
Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", this);
OSError = OS.Shared.Unix.Error;
} else {
throw new Error("I am neither under Windows nor under a Posix system");
}
let Type = OS.Shared.Type;
let Path = {};
Cu.import("resource://gre/modules/osfile/ospath.jsm", Path);
// The library of promises.
Components.utils.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
// The implementation of communications
Components.utils.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
Components.utils.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/Services.jsm", this);
Components.utils.import("resource://gre/modules/AsyncShutdown.jsm", this);
Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
LOG("Checking profileDir", OS.Constants.Path);
@ -66,7 +69,7 @@ if (!("profileDir" in OS.Constants.Path)) {
get: function() {
let path = undefined;
try {
path = Services.dirsvc.get("ProfD", Components.interfaces.nsIFile).path;
path = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
delete OS.Constants.Path.profileDir;
OS.Constants.Path.profileDir = path;
} catch (ex) {
@ -84,7 +87,7 @@ if (!("localProfileDir" in OS.Constants.Path)) {
get: function() {
let path = undefined;
try {
path = Services.dirsvc.get("ProfLD", Components.interfaces.nsIFile).path;
path = Services.dirsvc.get("ProfLD", Ci.nsIFile).path;
delete OS.Constants.Path.localProfileDir;
OS.Constants.Path.localProfileDir = path;
} catch (ex) {
@ -1002,6 +1005,7 @@ Object.defineProperty(File, "POS_END", {value: OS.Shared.POS_END});
OS.File = File;
OS.File.Error = OSError;
OS.File.DirectoryIterator = DirectoryIterator;
OS.Path = Path;
// Auto-flush OS.File during profile-before-change. This ensures that any I/O

Просмотреть файл

@ -390,6 +390,6 @@ if (this.Components) {
};
} catch(ex) {
dump("WORKER ERROR DURING SETUP " + ex + "\n");
dump("WORKER ERROR DETAIL " + ex.stack + "\n");
dump("WORKER ERROR DETAIL " + ("moduleStack" in ex?ex.moduleStack:ex.stack) + "\n");
}
})(this);

Просмотреть файл

@ -20,6 +20,8 @@
"use strict";
exports.OS = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm").OS;
let Path = require("resource://gre/modules/osfile/ospath.jsm");
// exports.OS.Unix is created by osfile_unix_back.jsm
if (exports.OS && exports.OS.File) {
return; // Avoid double-initialization
@ -652,7 +654,7 @@
let isDir, isSymLink;
if (!("d_type" in contents)) {
// |dirent| doesn't have d_type on some platforms (e.g. Solaris).
let path = OS.Unix.Path.join(this._path, name);
let path = Path.join(this._path, name);
throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr));
isDir = (gStatData.st_mode & OS.Constants.libc.S_IFMT) == OS.Constants.libc.S_IFDIR;
isSymLink = (gStatData.st_mode & OS.Constants.libc.S_IFMT) == OS.Constants.libc.S_IFLNK;
@ -702,7 +704,7 @@
// Copy the relevant part of |unix_entry| to ensure that
// our data is not overwritten prematurely.
this._parent = parent;
let path = OS.Unix.Path.join(this._parent, name);
let path = Path.join(this._parent, name);
exports.OS.Shared.Unix.AbstractEntry.call(this, isDir, isSymLink, name, path);
};

Просмотреть файл

@ -20,6 +20,8 @@
"use strict";
exports.OS = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm").OS;
let Path = require("resource://gre/modules/osfile/ospath.jsm");
// exports.OS.Win is created by osfile_win_back.jsm
if (exports.OS && exports.OS.File) {
return; // Avoid double-initialization
@ -646,7 +648,7 @@
}
this._parent = parent;
let path = OS.Win.Path.join(this._parent, name);
let path = Path.join(this._parent, name);
exports.OS.Shared.Win.AbstractEntry.call(this, isDir, isSymLink, name,
winCreationDate, winLastWriteDate,
@ -841,7 +843,7 @@
File.Error = exports.OS.Shared.Win.Error;
exports.OS.File = File;
exports.OS.Path = exports.OS.Win.Path;
exports.OS.Path = exports.Path;
Object.defineProperty(File, "POS_START", { value: OS.Shared.POS_START });
Object.defineProperty(File, "POS_CURRENT", { value: OS.Shared.POS_CURRENT });

Просмотреть файл

@ -0,0 +1,45 @@
/* 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/. */
/**
* Handling native paths.
*
* This module contains a number of functions destined to simplify
* working with native paths through a cross-platform API. Functions
* of this module will only work with the following assumptions:
*
* - paths are valid;
* - paths are defined with one of the grammars that this module can
* parse (see later);
* - all path concatenations go through function |join|.
*/
"use strict";
if (typeof Components == "undefined") {
let Path;
if (OS.Constants.Win) {
Path = require("resource://gre/modules/osfile/ospath_win.jsm");
} else {
Path = require("resource://gre/modules/osfile/ospath_unix.jsm");
}
module.exports = Path;
} else {
let Cu = Components.utils;
let Scope = {};
Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", Scope);
let Path = {};
if (Scope.OS.Constants.Win) {
Cu.import("resource://gre/modules/osfile/ospath_win.jsm", Path);
} else {
Cu.import("resource://gre/modules/osfile/ospath_unix.jsm", Path);
}
this.EXPORTED_SYMBOLS = [];
for (let k in Path) {
this.EXPORTED_SYMBOLS.push(k);
this[k] = Path[k];
}
}

Просмотреть файл

@ -0,0 +1,157 @@
/* 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/. */
/**
* Handling native paths.
*
* This module contains a number of functions destined to simplify
* working with native paths through a cross-platform API. Functions
* of this module will only work with the following assumptions:
*
* - paths are valid;
* - paths are defined with one of the grammars that this module can
* parse (see later);
* - all path concatenations go through function |join|.
*/
"use strict";
// Boilerplate used to be able to import this module both from the main
// thread and from worker threads.
if (typeof Components != "undefined") {
// Global definition of |exports|, to keep everybody happy.
// In non-main thread, |exports| is provided by the module
// loader.
this.exports = {};
} else if (typeof "module" == "undefined" || typeof "exports" == "undefined") {
throw new Error("Please load this module using require()");
}
let EXPORTED_SYMBOLS = [
"basename",
"dirname",
"join",
"normalize",
"split"
];
/**
* Return the final part of the path.
* The final part of the path is everything after the last "/".
*/
let basename = function(path) {
return path.slice(path.lastIndexOf("/") + 1);
};
exports.basename = basename;
/**
* Return the directory part of the path.
* The directory part of the path is everything before the last
* "/". If the last few characters of this part are also "/",
* they are ignored.
*
* If the path contains no directory, return ".".
*/
let dirname = function(path) {
let index = path.lastIndexOf("/");
if (index == -1) {
return ".";
}
while (index >= 0 && path[index] == "/") {
--index;
}
return path.slice(0, index + 1);
};
exports.dirname = dirname;
/**
* Join path components.
* This is the recommended manner of getting the path of a file/subdirectory
* in a directory.
*
* Example: Obtaining $TMP/foo/bar in an OS-independent manner
* var tmpDir = OS.Constants.Path.tmpDir;
* var path = OS.Path.join(tmpDir, "foo", "bar");
*
* Under Unix, this will return "/tmp/foo/bar".
*/
let join = function(path /*...*/) {
// If there is a path that starts with a "/", eliminate everything before
let paths = [];
for each(let i in arguments) {
if (i.length != 0 && i[0] == "/") {
paths = [i];
} else {
paths.push(i);
}
}
return paths.join("/");
};
exports.join = join;
/**
* Normalize a path by removing any unneeded ".", "..", "//".
*/
let normalize = function(path) {
let stack = [];
let absolute;
if (path.length >= 0 && path[0] == "/") {
absolute = true;
} else {
absolute = false;
}
path.split("/").forEach(function(v) {
switch (v) {
case "": case ".":// fallthrough
break;
case "..":
if (stack.length == 0) {
if (absolute) {
throw new Error("Path is ill-formed: attempting to go past root");
} else {
stack.push("..");
}
} else {
if (stack[stack.length - 1] == "..") {
stack.push("..");
} else {
stack.pop();
}
}
break;
default:
stack.push(v);
}
});
let string = stack.join("/");
return absolute ? "/" + string : string;
};
exports.normalize = normalize;
/**
* Return the components of a path.
* You should generally apply this function to a normalized path.
*
* @return {{
* {bool} absolute |true| if the path is absolute, |false| otherwise
* {array} components the string components of the path
* }}
*
* Other implementations may add additional OS-specific informations.
*/
let split = function(path) {
return {
absolute: path.length && path[0] == "/",
components: path.split("/")
};
};
exports.split = split;
//////////// Boilerplate
if (typeof Components != "undefined") {
this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
for (let symbol of EXPORTED_SYMBOLS) {
this[symbol] = exports[symbol];
}
}

Просмотреть файл

@ -1,155 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Handling native paths.
*
* This module contains a number of functions destined to simplify
* working with native paths through a cross-platform API. Functions
* of this module will only work with the following assumptions:
*
* - paths are valid;
* - paths are defined with one of the grammars that this module can
* parse (see later);
* - all path concatenations go through function |join|.
*/
if (typeof Components != "undefined") {
this.EXPORTED_SYMBOLS = ["OS"];
let Scope = {};
Components.utils.import("resource://gre/modules/Services.jsm", Scope);
// Some tests need to import this module from any platform.
// We detect this by setting a bogus preference "toolkit.osfile.test.syslib_necessary"
// from the test suite
let syslib_necessary = true;
try {
syslib_necessary = Scope.Services.prefs.getBoolPref("toolkit.osfile.test.syslib_necessary");
} catch (x) {
// Ignore errors
}
try {
Components.utils.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", this);
} catch (ex if !syslib_necessary && ex.message.startsWith("Could not open system library:")) {
// Executing this module without a libc is acceptable for this test
}
}
(function(exports) {
"use strict";
if (!exports.OS) {
exports.OS = {};
}
if (!exports.OS.Unix) {
exports.OS.Unix = {};
}
if (exports.OS.Unix.Path) {
return; // Avoid double-initialization
}
exports.OS.Unix.Path = {
/**
* Return the final part of the path.
* The final part of the path is everything after the last "/".
*/
basename: function basename(path) {
return path.slice(path.lastIndexOf("/") + 1);
},
/**
* Return the directory part of the path.
* The directory part of the path is everything before the last
* "/". If the last few characters of this part are also "/",
* they are ignored.
*
* If the path contains no directory, return ".".
*/
dirname: function dirname(path) {
let index = path.lastIndexOf("/");
if (index == -1) {
return ".";
}
while (index >= 0 && path[index] == "/") {
--index;
}
return path.slice(0, index + 1);
},
/**
* Join path components.
* This is the recommended manner of getting the path of a file/subdirectory
* in a directory.
*
* Example: Obtaining $TMP/foo/bar in an OS-independent manner
* var tmpDir = OS.Constants.Path.tmpDir;
* var path = OS.Path.join(tmpDir, "foo", "bar");
*
* Under Unix, this will return "/tmp/foo/bar".
*/
join: function join(path /*...*/) {
// If there is a path that starts with a "/", eliminate everything before
let paths = [];
for each(let i in arguments) {
if (i.length != 0 && i[0] == "/") {
paths = [i];
} else {
paths.push(i);
}
}
return paths.join("/");
},
/**
* Normalize a path by removing any unneeded ".", "..", "//".
*/
normalize: function normalize(path) {
let stack = [];
let absolute;
if (path.length >= 0 && path[0] == "/") {
absolute = true;
} else {
absolute = false;
}
path.split("/").forEach(function loop(v) {
switch (v) {
case "": case ".":// fallthrough
break;
case "..":
if (stack.length == 0) {
if (absolute) {
throw new Error("Path is ill-formed: attempting to go past root");
} else {
stack.push("..");
}
} else {
if (stack[stack.length - 1] == "..") {
stack.push("..");
} else {
stack.pop();
}
}
break;
default:
stack.push(v);
}
});
let string = stack.join("/");
return absolute ? "/" + string : string;
},
/**
* Return the components of a path.
* You should generally apply this function to a normalized path.
*
* @return {{
* {bool} absolute |true| if the path is absolute, |false| otherwise
* {array} components the string components of the path
* }}
*
* Other implementations may add additional OS-specific informations.
*/
split: function split(path) {
return {
absolute: path.length && path[0] == "/",
components: path.split("/")
};
}
};
exports.OS.Path = exports.OS.Unix.Path;
}(this));

Просмотреть файл

@ -0,0 +1,302 @@
/**
* Handling native paths.
*
* This module contains a number of functions destined to simplify
* working with native paths through a cross-platform API. Functions
* of this module will only work with the following assumptions:
*
* - paths are valid;
* - paths are defined with one of the grammars that this module can
* parse (see later);
* - all path concatenations go through function |join|.
*
* Limitations of this implementation.
*
* Windows supports 6 distinct grammars for paths. For the moment, this
* implementation supports the following subset:
*
* - drivename:backslash-separated components
* - backslash-separated components
* - \\drivename\ followed by backslash-separated components
*
* Additionally, |normalize| can convert a path containing slash-
* separated components to a path containing backslash-separated
* components.
*/
"use strict";
// Boilerplate used to be able to import this module both from the main
// thread and from worker threads.
if (typeof Components != "undefined") {
// Global definition of |exports|, to keep everybody happy.
// In non-main thread, |exports| is provided by the module
// loader.
this.exports = {};
} else if (typeof "module" == "undefined" || typeof "exports" == "undefined") {
throw new Error("Please load this module using require()");
}
let EXPORTED_SYMBOLS = [
"basename",
"dirname",
"join",
"normalize",
"split",
"winGetDrive",
"winIsAbsolute"
];
/**
* Return the final part of the path.
* The final part of the path is everything after the last "\\".
*/
let basename = function(path) {
if (path.startsWith("\\\\")) {
// UNC-style path
let index = path.lastIndexOf("\\");
if (index != 1) {
return path.slice(index + 1);
}
return ""; // Degenerate case
}
return path.slice(Math.max(path.lastIndexOf("\\"),
path.lastIndexOf(":")) + 1);
};
exports.basename = basename;
/**
* Return the directory part of the path.
*
* If the path contains no directory, return the drive letter,
* or "." if the path contains no drive letter or if option
* |winNoDrive| is set.
*
* Otherwise, return everything before the last backslash,
* including the drive/server name.
*
*
* @param {string} path The path.
* @param {*=} options Platform-specific options controlling the behavior
* of this function. This implementation supports the following options:
* - |winNoDrive| If |true|, also remove the letter from the path name.
*/
let dirname = function(path, options) {
let noDrive = (options && options.winNoDrive);
// Find the last occurrence of "\\"
let index = path.lastIndexOf("\\");
if (index == -1) {
// If there is no directory component...
if (!noDrive) {
// Return the drive path if possible, falling back to "."
return this.winGetDrive(path) || ".";
} else {
// Or just "."
return ".";
}
}
if (index == 1 && path.charAt(0) == "\\") {
// The path is reduced to a UNC drive
if (noDrive) {
return ".";
} else {
return path;
}
}
// Ignore any occurrence of "\\: immediately before that one
while (index >= 0 && path[index] == "\\") {
--index;
}
// Compute what is left, removing the drive name if necessary
let start;
if (noDrive) {
start = (this.winGetDrive(path) || "").length;
} else {
start = 0;
}
return path.slice(start, index + 1);
};
exports.dirname = dirname;
/**
* Join path components.
* This is the recommended manner of getting the path of a file/subdirectory
* in a directory.
*
* Example: Obtaining $TMP/foo/bar in an OS-independent manner
* var tmpDir = OS.Constants.Path.tmpDir;
* var path = OS.Path.join(tmpDir, "foo", "bar");
*
* Under Windows, this will return "$TMP\foo\bar".
*/
let join = function(...path) {
let paths = [];
let root;
let absolute = false;
for each(let subpath in path) {
let drive = this.winGetDrive(subpath);
let abs = this.winIsAbsolute(subpath);
if (drive) {
root = drive;
let component = trimBackslashes(subpath.slice(drive.length));
if (component) {
paths = [component];
} else {
paths = [];
}
absolute = abs;
} else if (abs) {
paths = [trimBackslashes(subpath)];
absolute = true;
} else {
paths.push(trimBackslashes(subpath));
}
}
let result = "";
if (root) {
result += root;
}
if (absolute) {
result += "\\";
}
result += paths.join("\\");
return result;
};
exports.join = join;
/**
* Return the drive name of a path, or |null| if the path does
* not contain a drive name.
*
* Drive name appear either as "DriveName:..." (the return drive
* name includes the ":") or "\\\\DriveName..." (the returned drive name
* includes "\\\\").
*/
let winGetDrive = function(path) {
if (path.startsWith("\\\\")) {
// UNC path
if (path.length == 2) {
return null;
}
let index = path.indexOf("\\", 2);
if (index == -1) {
return path;
}
return path.slice(0, index);
}
// Non-UNC path
let index = path.indexOf(":");
if (index <= 0) return null;
return path.slice(0, index + 1);
};
exports.winGetDrive = winGetDrive;
/**
* Return |true| if the path is absolute, |false| otherwise.
*
* We consider that a path is absolute if it starts with "\\"
* or "driveletter:\\".
*/
let winIsAbsolute = function(path) {
let index = path.indexOf(":");
return path.length > index + 1 && path[index + 1] == "\\";
};
exports.winIsAbsolute = winIsAbsolute;
/**
* Normalize a path by removing any unneeded ".", "..", "\\".
* Also convert any "/" to a "\\".
*/
let normalize = function(path) {
let stack = [];
// Remove the drive (we will put it back at the end)
let root = this.winGetDrive(path);
if (root) {
path = path.slice(root.length);
}
// Remember whether we need to restore a leading "\\" or drive name.
let absolute = this.winIsAbsolute(path);
// Normalize "/" to "\\"
path = path.replace("/", "\\");
// And now, fill |stack| from the components,
// popping whenever there is a ".."
path.split("\\").forEach(function loop(v) {
switch (v) {
case "": case ".": // Ignore
break;
case "..":
if (stack.length == 0) {
if (absolute) {
throw new Error("Path is ill-formed: attempting to go past root");
} else {
stack.push("..");
}
} else {
if (stack[stack.length - 1] == "..") {
stack.push("..");
} else {
stack.pop();
}
}
break;
default:
stack.push(v);
}
});
// Put everything back together
let result = stack.join("\\");
if (absolute) {
result = "\\" + result;
}
if (root) {
result = root + result;
}
return result;
};
exports.normalize = normalize;
/**
* Return the components of a path.
* You should generally apply this function to a normalized path.
*
* @return {{
* {bool} absolute |true| if the path is absolute, |false| otherwise
* {array} components the string components of the path
* {string?} winDrive the drive or server for this path
* }}
*
* Other implementations may add additional OS-specific informations.
*/
let split = function(path) {
return {
absolute: this.winIsAbsolute(path),
winDrive: this.winGetDrive(path),
components: path.split("\\")
};
};
exports.split = split;
/**
* Utility function: Remove any leading/trailing backslashes
* from a string.
*/
let trimBackslashes = function trimBackslashes(string) {
return string.replace(/^\\+|\\+$/g,'');
};
//////////// Boilerplate
if (typeof Components != "undefined") {
this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
for (let symbol of EXPORTED_SYMBOLS) {
this[symbol] = exports[symbol];
}
}

Просмотреть файл

@ -1,300 +0,0 @@
/**
* Handling native paths.
*
* This module contains a number of functions destined to simplify
* working with native paths through a cross-platform API. Functions
* of this module will only work with the following assumptions:
*
* - paths are valid;
* - paths are defined with one of the grammars that this module can
* parse (see later);
* - all path concatenations go through function |join|.
*
* Limitations of this implementation.
*
* Windows supports 6 distinct grammars for paths. For the moment, this
* implementation supports the following subset:
*
* - drivename:backslash-separated components
* - backslash-separated components
* - \\drivename\ followed by backslash-separated components
*
* Additionally, |normalize| can convert a path containing slash-
* separated components to a path containing backslash-separated
* components.
*/
if (typeof Components != "undefined") {
this.EXPORTED_SYMBOLS = ["OS"];
let Scope = {};
Components.utils.import("resource://gre/modules/Services.jsm", Scope);
// Some tests need to import this module from any platform.
// We detect this by setting a bogus preference "toolkit.osfile.test.syslib_necessary"
// from the test suite
let syslib_necessary = true;
try {
syslib_necessary = Scope.Services.prefs.getBoolPref("toolkit.osfile.test.syslib_necessary");
} catch (x) {
// Ignore errors
}
try {
Components.utils.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", this);
} catch (ex if !syslib_necessary && ex.message.startsWith("Could not open system library:")) {
// Executing this module without a libc is acceptable for this test
}
}
(function(exports) {
"use strict";
if (!exports.OS) {
exports.OS = {};
}
if (!exports.OS.Win) {
exports.OS.Win = {};
}
if (exports.OS.Win.Path) {
return; // Avoid double-initialization
}
exports.OS.Win.Path = {
/**
* Return the final part of the path.
* The final part of the path is everything after the last "\\".
*/
basename: function basename(path) {
if (path.startsWith("\\\\")) {
// UNC-style path
let index = path.lastIndexOf("\\");
if (index != 1) {
return path.slice(index + 1);
}
return ""; // Degenerate case
}
return path.slice(Math.max(path.lastIndexOf("\\"),
path.lastIndexOf(":")) + 1);
},
/**
* Return the directory part of the path.
*
* If the path contains no directory, return the drive letter,
* or "." if the path contains no drive letter or if option
* |winNoDrive| is set.
*
* Otherwise, return everything before the last backslash,
* including the drive/server name.
*
*
* @param {string} path The path.
* @param {*=} options Platform-specific options controlling the behavior
* of this function. This implementation supports the following options:
* - |winNoDrive| If |true|, also remove the letter from the path name.
*/
dirname: function dirname(path, options) {
let noDrive = (options && options.winNoDrive);
// Find the last occurrence of "\\"
let index = path.lastIndexOf("\\");
if (index == -1) {
// If there is no directory component...
if (!noDrive) {
// Return the drive path if possible, falling back to "."
return this.winGetDrive(path) || ".";
} else {
// Or just "."
return ".";
}
}
if (index == 1 && path.charAt(0) == "\\") {
// The path is reduced to a UNC drive
if (noDrive) {
return ".";
} else {
return path;
}
}
// Ignore any occurrence of "\\: immediately before that one
while (index >= 0 && path[index] == "\\") {
--index;
}
// Compute what is left, removing the drive name if necessary
let start;
if (noDrive) {
start = (this.winGetDrive(path) || "").length;
} else {
start = 0;
}
return path.slice(start, index + 1);
},
/**
* Join path components.
* This is the recommended manner of getting the path of a file/subdirectory
* in a directory.
*
* Example: Obtaining $TMP/foo/bar in an OS-independent manner
* var tmpDir = OS.Constants.Path.tmpDir;
* var path = OS.Path.join(tmpDir, "foo", "bar");
*
* Under Windows, this will return "$TMP\foo\bar".
*/
join: function join(path /*...*/) {
let paths = [];
let root;
let absolute = false;
for each(let subpath in arguments) {
let drive = this.winGetDrive(subpath);
let abs = this.winIsAbsolute(subpath);
if (drive) {
root = drive;
let component = trimBackslashes(subpath.slice(drive.length));
if (component) {
paths = [component];
} else {
paths = [];
}
absolute = abs;
} else if (abs) {
paths = [trimBackslashes(subpath)];
absolute = true;
} else {
paths.push(trimBackslashes(subpath));
}
}
let result = "";
if (root) {
result += root;
}
if (absolute) {
result += "\\";
}
result += paths.join("\\");
return result;
},
/**
* Return the drive name of a path, or |null| if the path does
* not contain a drive name.
*
* Drive name appear either as "DriveName:..." (the return drive
* name includes the ":") or "\\\\DriveName..." (the returned drive name
* includes "\\\\").
*/
winGetDrive: function winGetDrive(path) {
if (path.startsWith("\\\\")) {
// UNC path
if (path.length == 2) {
return null;
}
let index = path.indexOf("\\", 2);
if (index == -1) {
return path;
}
return path.slice(0, index);
}
// Non-UNC path
let index = path.indexOf(":");
if (index <= 0) return null;
return path.slice(0, index + 1);
},
/**
* Return |true| if the path is absolute, |false| otherwise.
*
* We consider that a path is absolute if it starts with "\\"
* or "driveletter:\\".
*/
winIsAbsolute: function winIsAbsolute(path) {
let index = path.indexOf(":");
return path.length > index + 1 && path[index + 1] == "\\";
},
/**
* Normalize a path by removing any unneeded ".", "..", "\\".
* Also convert any "/" to a "\\".
*/
normalize: function normalize(path) {
let stack = [];
// Remove the drive (we will put it back at the end)
let root = this.winGetDrive(path);
if (root) {
path = path.slice(root.length);
}
// Remember whether we need to restore a leading "\\" or drive name.
let absolute = this.winIsAbsolute(path);
// Normalize "/" to "\\"
path = path.replace("/", "\\");
// And now, fill |stack| from the components,
// popping whenever there is a ".."
path.split("\\").forEach(function loop(v) {
switch (v) {
case "": case ".": // Ignore
break;
case "..":
if (stack.length == 0) {
if (absolute) {
throw new Error("Path is ill-formed: attempting to go past root");
} else {
stack.push("..");
}
} else {
if (stack[stack.length - 1] == "..") {
stack.push("..");
} else {
stack.pop();
}
}
break;
default:
stack.push(v);
}
});
// Put everything back together
let result = stack.join("\\");
if (absolute) {
result = "\\" + result;
}
if (root) {
result = root + result;
}
return result;
},
/**
* Return the components of a path.
* You should generally apply this function to a normalized path.
*
* @return {{
* {bool} absolute |true| if the path is absolute, |false| otherwise
* {array} components the string components of the path
* {string?} winDrive the drive or server for this path
* }}
*
* Other implementations may add additional OS-specific informations.
*/
split: function split(path) {
return {
absolute: this.winIsAbsolute(path),
winDrive: this.winGetDrive(path),
components: path.split("\\")
};
}
};
/**
* Utility function: Remove any leading/trailing backslashes
* from a string.
*/
let trimBackslashes = function trimBackslashes(string) {
return string.replace(/^\\+|\\+$/g,'');
};
exports.OS.Path = exports.OS.Win.Path;
}(this));

Просмотреть файл

@ -17,7 +17,6 @@ if (typeof Components != "undefined") {
importScripts(
"resource://gre/modules/workers/require.js",
"resource://gre/modules/osfile/osfile_win_allthreads.jsm",
"resource://gre/modules/osfile/ospath_win_back.jsm",
"resource://gre/modules/osfile/osfile_win_back.jsm",
"resource://gre/modules/osfile/osfile_shared_front.jsm",
"resource://gre/modules/osfile/osfile_win_front.jsm"
@ -26,10 +25,10 @@ if (typeof Components != "undefined") {
importScripts(
"resource://gre/modules/workers/require.js",
"resource://gre/modules/osfile/osfile_unix_allthreads.jsm",
"resource://gre/modules/osfile/ospath_unix_back.jsm",
"resource://gre/modules/osfile/osfile_unix_back.jsm",
"resource://gre/modules/osfile/osfile_shared_front.jsm",
"resource://gre/modules/osfile/osfile_unix_front.jsm"
);
#endif
OS.Path = require("resource://gre/modules/osfile/ospath.jsm");
}

Просмотреть файл

@ -10,11 +10,11 @@ Services.prefs.setBoolPref("toolkit.osfile.test.syslib_necessary", false);
let ImportWin = {};
let ImportUnix = {};
Components.utils.import("resource://gre/modules/osfile/ospath_win_back.jsm", ImportWin);
Components.utils.import("resource://gre/modules/osfile/ospath_unix_back.jsm", ImportUnix);
Components.utils.import("resource://gre/modules/osfile/ospath_win.jsm", ImportWin);
Components.utils.import("resource://gre/modules/osfile/ospath_unix.jsm", ImportUnix);
let Win = ImportWin.OS;
let Unix = ImportUnix.OS;
let Win = ImportWin;
let Unix = ImportUnix;
function do_check_fail(f)
{
@ -32,99 +32,104 @@ function run_test()
do_print("Testing Windows paths");
do_print("Backslash-separated, no drive");
do_check_eq(Win.Path.basename("a\\b"), "b");
do_check_eq(Win.Path.basename("a\\b\\"), "");
do_check_eq(Win.Path.basename("abc"), "abc");
do_check_eq(Win.Path.dirname("a\\b"), "a");
do_check_eq(Win.Path.dirname("a\\b\\"), "a\\b");
do_check_eq(Win.Path.dirname("a\\\\\\\\b"), "a");
do_check_eq(Win.Path.dirname("abc"), ".");
do_check_eq(Win.Path.normalize("\\a\\b\\c"), "\\a\\b\\c");
do_check_eq(Win.Path.normalize("\\a\\b\\\\\\\\c"), "\\a\\b\\c");
do_check_eq(Win.Path.normalize("\\a\\b\\c\\\\\\"), "\\a\\b\\c");
do_check_eq(Win.Path.normalize("\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "\\d\\e\\f");
do_check_eq(Win.Path.normalize("a\\b\\c\\..\\..\\..\\d\\e\\f"), "d\\e\\f");
do_check_fail(function() Win.Path.normalize("\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
do_check_eq(Win.basename("a\\b"), "b");
do_check_eq(Win.basename("a\\b\\"), "");
do_check_eq(Win.basename("abc"), "abc");
do_check_eq(Win.dirname("a\\b"), "a");
do_check_eq(Win.dirname("a\\b\\"), "a\\b");
do_check_eq(Win.dirname("a\\\\\\\\b"), "a");
do_check_eq(Win.dirname("abc"), ".");
do_check_eq(Win.normalize("\\a\\b\\c"), "\\a\\b\\c");
do_check_eq(Win.normalize("\\a\\b\\\\\\\\c"), "\\a\\b\\c");
do_check_eq(Win.normalize("\\a\\b\\c\\\\\\"), "\\a\\b\\c");
do_check_eq(Win.normalize("\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "\\d\\e\\f");
do_check_eq(Win.normalize("a\\b\\c\\..\\..\\..\\d\\e\\f"), "d\\e\\f");
do_check_fail(function() Win.normalize("\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
do_check_eq(Win.Path.join("\\tmp", "foo", "bar"), "\\tmp\\foo\\bar", "join \\tmp,foo,bar");
do_check_eq(Win.Path.join("\\tmp", "\\foo", "bar"), "\\foo\\bar", "join \\tmp,\\foo,bar");
do_check_eq(Win.Path.winGetDrive("\\tmp"), null);
do_check_eq(Win.Path.winGetDrive("\\tmp\\a\\b\\c\\d\\e"), null);
do_check_eq(Win.Path.winGetDrive("\\"), null);
do_check_eq(Win.join("\\tmp", "foo", "bar"), "\\tmp\\foo\\bar", "join \\tmp,foo,bar");
do_check_eq(Win.join("\\tmp", "\\foo", "bar"), "\\foo\\bar", "join \\tmp,\\foo,bar");
do_check_eq(Win.winGetDrive("\\tmp"), null);
do_check_eq(Win.winGetDrive("\\tmp\\a\\b\\c\\d\\e"), null);
do_check_eq(Win.winGetDrive("\\"), null);
do_print("Backslash-separated, with a drive");
do_check_eq(Win.Path.basename("c:a\\b"), "b");
do_check_eq(Win.Path.basename("c:a\\b\\"), "");
do_check_eq(Win.Path.basename("c:abc"), "abc");
do_check_eq(Win.Path.dirname("c:a\\b"), "c:a");
do_check_eq(Win.Path.dirname("c:a\\b\\"), "c:a\\b");
do_check_eq(Win.Path.dirname("c:a\\\\\\\\b"), "c:a");
do_check_eq(Win.Path.dirname("c:abc"), "c:");
do_check_eq(Win.basename("c:a\\b"), "b");
do_check_eq(Win.basename("c:a\\b\\"), "");
do_check_eq(Win.basename("c:abc"), "abc");
do_check_eq(Win.dirname("c:a\\b"), "c:a");
do_check_eq(Win.dirname("c:a\\b\\"), "c:a\\b");
do_check_eq(Win.dirname("c:a\\\\\\\\b"), "c:a");
do_check_eq(Win.dirname("c:abc"), "c:");
let options = {
winNoDrive: true
};
do_check_eq(Win.Path.dirname("c:a\\b", options), "a");
do_check_eq(Win.Path.dirname("c:a\\b\\", options), "a\\b");
do_check_eq(Win.Path.dirname("c:a\\\\\\\\b", options), "a");
do_check_eq(Win.Path.dirname("c:abc", options), ".");
do_check_eq(Win.dirname("c:a\\b", options), "a");
do_check_eq(Win.dirname("c:a\\b\\", options), "a\\b");
do_check_eq(Win.dirname("c:a\\\\\\\\b", options), "a");
do_check_eq(Win.dirname("c:abc", options), ".");
do_check_eq(Win.Path.normalize("c:\\a\\b\\c"), "c:\\a\\b\\c");
do_check_eq(Win.Path.normalize("c:\\a\\b\\\\\\\\c"), "c:\\a\\b\\c");
do_check_eq(Win.Path.normalize("c:\\\\\\\\a\\b\\c"), "c:\\a\\b\\c");
do_check_eq(Win.Path.normalize("c:\\a\\b\\c\\\\\\"), "c:\\a\\b\\c");
do_check_eq(Win.Path.normalize("c:\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f");
do_check_eq(Win.Path.normalize("c:a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:d\\e\\f");
do_check_fail(function() Win.Path.normalize("c:\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
do_check_eq(Win.normalize("c:\\a\\b\\c"), "c:\\a\\b\\c");
do_check_eq(Win.normalize("c:\\a\\b\\\\\\\\c"), "c:\\a\\b\\c");
do_check_eq(Win.normalize("c:\\\\\\\\a\\b\\c"), "c:\\a\\b\\c");
do_check_eq(Win.normalize("c:\\a\\b\\c\\\\\\"), "c:\\a\\b\\c");
do_check_eq(Win.normalize("c:\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f");
do_check_eq(Win.normalize("c:a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:d\\e\\f");
do_check_fail(function() Win.normalize("c:\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
do_check_eq(Win.Path.join("c:\\tmp", "foo", "bar"), "c:\\tmp\\foo\\bar", "join c:\\tmp,foo,bar");
do_check_eq(Win.Path.join("c:\\tmp", "\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,\\foo,bar");
do_check_eq(Win.Path.join("c:\\tmp", "c:\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,c:\\foo,bar");
do_check_eq(Win.Path.join("c:\\tmp", "c:foo", "bar"), "c:foo\\bar", "join c:\\tmp,c:foo,bar");
do_check_eq(Win.Path.winGetDrive("c:"), "c:");
do_check_eq(Win.Path.winGetDrive("c:\\"), "c:");
do_check_eq(Win.Path.winGetDrive("c:abc"), "c:");
do_check_eq(Win.Path.winGetDrive("c:abc\\d\\e\\f\\g"), "c:");
do_check_eq(Win.Path.winGetDrive("c:\\abc"), "c:");
do_check_eq(Win.Path.winGetDrive("c:\\abc\\d\\e\\f\\g"), "c:");
do_check_eq(Win.join("c:\\tmp", "foo", "bar"), "c:\\tmp\\foo\\bar", "join c:\\tmp,foo,bar");
do_check_eq(Win.join("c:\\tmp", "\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,\\foo,bar");
do_check_eq(Win.join("c:\\tmp", "c:\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,c:\\foo,bar");
do_check_eq(Win.join("c:\\tmp", "c:foo", "bar"), "c:foo\\bar", "join c:\\tmp,c:foo,bar");
do_check_eq(Win.winGetDrive("c:"), "c:");
do_check_eq(Win.winGetDrive("c:\\"), "c:");
do_check_eq(Win.winGetDrive("c:abc"), "c:");
do_check_eq(Win.winGetDrive("c:abc\\d\\e\\f\\g"), "c:");
do_check_eq(Win.winGetDrive("c:\\abc"), "c:");
do_check_eq(Win.winGetDrive("c:\\abc\\d\\e\\f\\g"), "c:");
do_print("Backslash-separated, UNC-style");
do_check_eq(Win.Path.basename("\\\\a\\b"), "b");
do_check_eq(Win.Path.basename("\\\\a\\b\\"), "");
do_check_eq(Win.Path.basename("\\\\abc"), "");
do_check_eq(Win.Path.dirname("\\\\a\\b"), "\\\\a");
do_check_eq(Win.Path.dirname("\\\\a\\b\\"), "\\\\a\\b");
do_check_eq(Win.Path.dirname("\\\\a\\\\\\\\b"), "\\\\a");
do_check_eq(Win.Path.dirname("\\\\abc"), "\\\\abc");
do_check_eq(Win.Path.normalize("\\\\a\\b\\c"), "\\\\a\\b\\c");
do_check_eq(Win.Path.normalize("\\\\a\\b\\\\\\\\c"), "\\\\a\\b\\c");
do_check_eq(Win.Path.normalize("\\\\a\\b\\c\\\\\\"), "\\\\a\\b\\c");
do_check_eq(Win.Path.normalize("\\\\a\\b\\c\\..\\..\\d\\e\\f"), "\\\\a\\d\\e\\f");
do_check_fail(function() Win.Path.normalize("\\\\a\\b\\c\\..\\..\\..\\d\\e\\f"));
do_check_eq(Win.basename("\\\\a\\b"), "b");
do_check_eq(Win.basename("\\\\a\\b\\"), "");
do_check_eq(Win.basename("\\\\abc"), "");
do_check_eq(Win.dirname("\\\\a\\b"), "\\\\a");
do_check_eq(Win.dirname("\\\\a\\b\\"), "\\\\a\\b");
do_check_eq(Win.dirname("\\\\a\\\\\\\\b"), "\\\\a");
do_check_eq(Win.dirname("\\\\abc"), "\\\\abc");
do_check_eq(Win.normalize("\\\\a\\b\\c"), "\\\\a\\b\\c");
do_check_eq(Win.normalize("\\\\a\\b\\\\\\\\c"), "\\\\a\\b\\c");
do_check_eq(Win.normalize("\\\\a\\b\\c\\\\\\"), "\\\\a\\b\\c");
do_check_eq(Win.normalize("\\\\a\\b\\c\\..\\..\\d\\e\\f"), "\\\\a\\d\\e\\f");
do_check_fail(function() Win.normalize("\\\\a\\b\\c\\..\\..\\..\\d\\e\\f"));
do_check_eq(Win.Path.join("\\\\a\\tmp", "foo", "bar"), "\\\\a\\tmp\\foo\\bar");
do_check_eq(Win.Path.join("\\\\a\\tmp", "\\foo", "bar"), "\\\\a\\foo\\bar");
do_check_eq(Win.Path.join("\\\\a\\tmp", "\\\\foo\\", "bar"), "\\\\foo\\bar");
do_check_eq(Win.Path.winGetDrive("\\\\"), null);
do_check_eq(Win.Path.winGetDrive("\\\\c"), "\\\\c");
do_check_eq(Win.Path.winGetDrive("\\\\c\\abc"), "\\\\c");
do_check_eq(Win.join("\\\\a\\tmp", "foo", "bar"), "\\\\a\\tmp\\foo\\bar");
do_check_eq(Win.join("\\\\a\\tmp", "\\foo", "bar"), "\\\\a\\foo\\bar");
do_check_eq(Win.join("\\\\a\\tmp", "\\\\foo\\", "bar"), "\\\\foo\\bar");
do_check_eq(Win.winGetDrive("\\\\"), null);
do_check_eq(Win.winGetDrive("\\\\c"), "\\\\c");
do_check_eq(Win.winGetDrive("\\\\c\\abc"), "\\\\c");
do_print("Testing unix paths");
do_check_eq(Unix.Path.basename("a/b"), "b");
do_check_eq(Unix.Path.basename("a/b/"), "");
do_check_eq(Unix.Path.basename("abc"), "abc");
do_check_eq(Unix.Path.dirname("a/b"), "a");
do_check_eq(Unix.Path.dirname("a/b/"), "a/b");
do_check_eq(Unix.Path.dirname("a////b"), "a");
do_check_eq(Unix.Path.dirname("abc"), ".");
do_check_eq(Unix.Path.normalize("/a/b/c"), "/a/b/c");
do_check_eq(Unix.Path.normalize("/a/b////c"), "/a/b/c");
do_check_eq(Unix.Path.normalize("////a/b/c"), "/a/b/c");
do_check_eq(Unix.Path.normalize("/a/b/c///"), "/a/b/c");
do_check_eq(Unix.Path.normalize("/a/b/c/../../../d/e/f"), "/d/e/f");
do_check_eq(Unix.Path.normalize("a/b/c/../../../d/e/f"), "d/e/f");
do_check_fail(function() Unix.Path.normalize("/a/b/c/../../../../d/e/f"));
do_check_eq(Unix.basename("a/b"), "b");
do_check_eq(Unix.basename("a/b/"), "");
do_check_eq(Unix.basename("abc"), "abc");
do_check_eq(Unix.dirname("a/b"), "a");
do_check_eq(Unix.dirname("a/b/"), "a/b");
do_check_eq(Unix.dirname("a////b"), "a");
do_check_eq(Unix.dirname("abc"), ".");
do_check_eq(Unix.normalize("/a/b/c"), "/a/b/c");
do_check_eq(Unix.normalize("/a/b////c"), "/a/b/c");
do_check_eq(Unix.normalize("////a/b/c"), "/a/b/c");
do_check_eq(Unix.normalize("/a/b/c///"), "/a/b/c");
do_check_eq(Unix.normalize("/a/b/c/../../../d/e/f"), "/d/e/f");
do_check_eq(Unix.normalize("a/b/c/../../../d/e/f"), "d/e/f");
do_check_fail(function() Unix.normalize("/a/b/c/../../../../d/e/f"));
do_check_eq(Unix.Path.join("/tmp", "foo", "bar"), "/tmp/foo/bar", "join /tmp,foo,bar");
do_check_eq(Unix.Path.join("/tmp", "/foo", "bar"), "/foo/bar", "join /tmp,/foo,bar");
do_check_eq(Unix.join("/tmp", "foo", "bar"), "/tmp/foo/bar", "join /tmp,foo,bar");
do_check_eq(Unix.join("/tmp", "/foo", "bar"), "/foo/bar", "join /tmp,/foo,bar");
do_print("Testing the presence of ospath.jsm");
let Scope = {};
Components.utils.import("resource://gre/modules/osfile/ospath.jsm", Scope);
do_check_true(!!Scope.basename);
}

Просмотреть файл

@ -186,6 +186,14 @@ SuggestAutoComplete.prototype = {
*/
_formHistoryTimer: null,
/**
* Maximum number of history items displayed. This is capped at 7
* because the primary consumer (Firefox search bar) displays 10 rows
* by default, and so we want to leave some space for suggestions
* to be visible.
*/
_historyLimit: 7,
/**
* This clears all the per-request state.
*/
@ -319,7 +327,8 @@ SuggestAutoComplete.prototype = {
if (this._includeFormHistory && this._formHistoryResult &&
(this._formHistoryResult.searchResult ==
Ci.nsIAutoCompleteResult.RESULT_SUCCESS)) {
for (var i = 0; i < this._formHistoryResult.matchCount; ++i) {
var maxHistoryItems = Math.min(this._formHistoryResult.matchCount, this._historyLimit);
for (var i = 0; i < maxHistoryItems; ++i) {
var term = this._formHistoryResult.getValueAt(i);
// we don't want things to appear in both history and suggestions

Просмотреть файл

@ -66,6 +66,11 @@ function injectController(doc, topic, data) {
return;
}
// we always handle window.close on social content, even if they are not
// "enabled". "enabled" is about the worker state and a provider may
// still be in e.g. the share panel without having their worker enabled.
handleWindowClose(window);
SocialService.getProvider(doc.nodePrincipal.origin, function(provider) {
if (provider && provider.enabled) {
attachToWindow(provider, window);
@ -214,7 +219,9 @@ function attachToWindow(provider, targetWindow) {
schedule(function () { port.close(); });
});
}
}
function handleWindowClose(targetWindow) {
// We allow window.close() to close the panel, so add an event handler for
// this, then cancel the event (so the window itself doesn't die) and
// close the panel instead.

Просмотреть файл

@ -172,18 +172,17 @@
}
modules.set(path, module);
// Load source of module, synchronously
let xhr = new XMLHttpRequest();
xhr.open("GET", uri, false);
xhr.responseType = "text";
xhr.send();
let source = xhr.responseText;
let name = ":" + path;
let objectURL;
try {
// Load source of module, synchronously
let xhr = new XMLHttpRequest();
xhr.open("GET", uri, false);
xhr.responseType = "text";
xhr.send();
let source = xhr.responseText;
if (source == "") {
// There doesn't seem to be a better way to detect that the file couldn't be found
throw new Error("Could not find module " + path);

Просмотреть файл

@ -57,6 +57,7 @@ var BuiltinProvider = {
"devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole",
"devtools/app-actor-front": "resource://gre/modules/devtools/app-actor-front.js",
"devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic",
"devtools/css-color": "resource://gre/modules/devtools/css-color",
"devtools/client": "resource://gre/modules/devtools/client",
"escodegen/escodegen": "resource://gre/modules/devtools/escodegen/escodegen",

Просмотреть файл

@ -6,6 +6,28 @@
const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
const {Cc, Ci, Cu} = require("chrome");
const REGEX_JUST_QUOTES = /^""$/;
const REGEX_RGB_3_TUPLE = /^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/i;
const REGEX_RGBA_4_TUPLE = /^rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+|1|0)\)$/i;
const REGEX_HSL_3_TUPLE = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/i;
/**
* This regex matches:
* - #F00
* - #FF0000
* - hsl()
* - hsla()
* - rgb()
* - rgba()
* - red
*
* It also matches css keywords e.g. "background-color" otherwise
* "background" would be replaced with #6363CE ("background" is a platform
* color).
*/
const REGEX_ALL_COLORS = /#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b|hsl\(.*?\)|hsla\(.*?\)|rgba?\(.*?\)|\b[a-zA-Z-]+\b/g;
let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
/**
@ -13,7 +35,7 @@ let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
*
* Usage:
* let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
* let {colorUtils} = devtools.require("devtools/shared/css-color");
* let {colorUtils} = devtools.require("devtools/css-color");
*
* color.authored === "red"
* color.hasAlpha === false
@ -152,6 +174,10 @@ CssColor.prototype = {
return "transparent";
}
if (!this.hasAlpha) {
if (this.authored.startsWith("rgb(")) {
// The color is valid and begins with rgb(. Return the authored value.
return this.authored;
}
let tuple = this._getRGBATuple();
return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
}
@ -165,6 +191,10 @@ CssColor.prototype = {
if (this.transparent) {
return "transparent";
}
if (this.authored.startsWith("rgba(")) {
// The color is valid and begins with rgba(. Return the authored value.
return this.authored;
}
let components = this._getRGBATuple();
return "rgba(" + components.r + ", " +
components.g + ", " +
@ -179,6 +209,10 @@ CssColor.prototype = {
if (this.transparent) {
return "transparent";
}
if (this.authored.startsWith("hsl(")) {
// The color is valid and begins with hsl(. Return the authored value.
return this.authored;
}
if (this.hasAlpha) {
return this.hsla;
}
@ -192,11 +226,9 @@ CssColor.prototype = {
if (this.transparent) {
return "transparent";
}
// Because an hsla rbg roundtrip would lose accuracy we use the authored
// values if this is an hsla color.
if (this.authored.startsWith("hsla(")) {
let [, h, s, l, a] = /^\bhsla\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%),\s*([\d.]+|0|1)\)$/gi.exec(this.authored);
return "hsla(" + h + ", " + s + ", " + l + ", " + a + ")";
// The color is valid and begins with hsla(. Return the authored value.
return this.authored;
}
if (this.hasAlpha) {
let a = this._getRGBATuple().a;
@ -261,13 +293,13 @@ CssColor.prototype = {
return "transparent";
}
let rgba = /^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+\.\d+|1|0)\)$/gi.exec(computed);
let rgba = computed.match(REGEX_RGBA_4_TUPLE);
if (rgba) {
let [, r, g, b, a] = rgba;
return {r: r, g: g, b: b, a: a};
} else {
let rgb = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/gi.exec(computed);
let rgb = computed.match(REGEX_RGB_3_TUPLE);
let [, r, g, b] = rgb;
return {r: r, g: g, b: b, a: 1};
@ -277,10 +309,10 @@ CssColor.prototype = {
_hslNoAlpha: function() {
let {r, g, b} = this._getRGBATuple();
// Because an hsl rbg roundtrip would lose accuracy we use the authored
// values if this is an hsla color.
if (this.authored.startsWith("hsl(")) {
let [, h, s, l] = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/gi.exec(this.authored);
// We perform string manipulations on our output so let's ensure that it
// is formatted as we expect.
let [, h, s, l] = this.authored.match(REGEX_HSL_3_TUPLE);
return "hsl(" + h + ", " + s + ", " + l + ")";
}
@ -365,23 +397,11 @@ CssColor.prototype = {
* Converted CSS String e.g. "color:#F00; background-color:#0F0;"
*/
function processCSSString(value) {
if (value && /^""$/.test(value)) {
if (value && REGEX_JUST_QUOTES.test(value)) {
return value;
}
// This regex matches:
// - #F00
// - #FF0000
// - hsl()
// - hsla()
// - rgb()
// - rgba()
// - red
//
// It also matches css keywords e.g. "background-color" otherwise
// "background" would be replaced with #6363CE ("background" is a platform
// color).
let colorPattern = /#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b|hsl\(.*?\)|hsla\(.*?\)|rgba?\(.*?\)|\b[a-zA-Z-]+\b/g;
let colorPattern = REGEX_ALL_COLORS;
value = value.replace(colorPattern, function(match) {
let color = new CssColor(match);

Просмотреть файл

@ -80,9 +80,7 @@ loadSubScript.call(this, "resource://gre/modules/devtools/DevToolsUtils.js");
function dumpn(str) {
if (wantLogging) {
for (let line of str.split(/\n/g)) {
dump("DBG-SERVER: " + line + "\n");
}
dump("DBG-SERVER: " + str + "\n");
}
}

Просмотреть файл

@ -51,7 +51,7 @@ const RX_PSEUDO = /\s*:?:([\w-]+)(\(?\)?)\s*/g;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
let {colorUtils} = require("devtools/shared/css-color");
let {colorUtils} = require("devtools/css-color");
function CssLogic()
{