зеркало из https://github.com/mozilla/gecko-dev.git
Merge latest green b2g-inbound changeset and mozilla-central; a=merge
This commit is contained in:
Коммит
453a8e5d2b
|
@ -304,12 +304,16 @@ setUpdateTrackingId();
|
|||
})();
|
||||
|
||||
// ================ Accessibility ============
|
||||
SettingsListener.observe("accessibility.screenreader", false, function(value) {
|
||||
if (value && !("AccessFu" in this)) {
|
||||
Cu.import('resource://gre/modules/accessibility/AccessFu.jsm');
|
||||
AccessFu.attach(window);
|
||||
}
|
||||
});
|
||||
(function setupAccessibility() {
|
||||
let accessibilityScope = {};
|
||||
SettingsListener.observe("accessibility.screenreader", false, function(value) {
|
||||
if (!('AccessFu' in accessibilityScope)) {
|
||||
Cu.import('resource://gre/modules/accessibility/AccessFu.jsm',
|
||||
accessibilityScope);
|
||||
accessibilityScope.AccessFu.attach(window);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// ================ Theming ============
|
||||
(function themingSettingsListener() {
|
||||
|
|
|
@ -149,7 +149,8 @@ let AlertsHelper = {
|
|||
dir: listener.dir,
|
||||
id: listener.id,
|
||||
tag: listener.tag,
|
||||
timestamp: listener.timestamp
|
||||
timestamp: listener.timestamp,
|
||||
data: listener.dataObj
|
||||
},
|
||||
Services.io.newURI(listener.target, null, null),
|
||||
Services.io.newURI(listener.manifestURL, null, null)
|
||||
|
@ -199,8 +200,32 @@ let AlertsHelper = {
|
|||
});
|
||||
},
|
||||
|
||||
deserializeStructuredClone: function(dataString) {
|
||||
if (!dataString) {
|
||||
return null;
|
||||
}
|
||||
let scContainer = Cc["@mozilla.org/docshell/structured-clone-container;1"].
|
||||
createInstance(Ci.nsIStructuredCloneContainer);
|
||||
|
||||
// The maximum supported structured-clone serialization format version
|
||||
// as defined in "js/public/StructuredClone.h"
|
||||
let JS_STRUCTURED_CLONE_VERSION = 4;
|
||||
scContainer.initFromBase64(dataString, JS_STRUCTURED_CLONE_VERSION);
|
||||
let dataObj = scContainer.deserializeToVariant();
|
||||
|
||||
// We have to check whether dataObj contains DOM objects (supported by
|
||||
// nsIStructuredCloneContainer, but not by Cu.cloneInto), e.g. ImageData.
|
||||
// After the structured clone callback systems will be unified, we'll not
|
||||
// have to perform this check anymore.
|
||||
try {
|
||||
let data = Cu.cloneInto(dataObj, {});
|
||||
} catch(e) { dataObj = null; }
|
||||
|
||||
return dataObj;
|
||||
},
|
||||
|
||||
showNotification: function(imageURL, title, text, textClickable, cookie,
|
||||
uid, bidi, lang, manifestURL, timestamp) {
|
||||
uid, bidi, lang, dataObj, manifestURL, timestamp) {
|
||||
function send(appName, appIcon) {
|
||||
SystemAppProxy._sendCustomEvent(kMozChromeNotificationEvent, {
|
||||
type: kDesktopNotification,
|
||||
|
@ -213,7 +238,8 @@ let AlertsHelper = {
|
|||
appName: appName,
|
||||
appIcon: appIcon,
|
||||
manifestURL: manifestURL,
|
||||
timestamp: timestamp
|
||||
timestamp: timestamp,
|
||||
data: dataObj
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -238,15 +264,17 @@ let AlertsHelper = {
|
|||
currentListener.observer.observe(null, kTopicAlertFinished, currentListener.cookie);
|
||||
}
|
||||
|
||||
let dataObj = this.deserializeStructuredClone(data.dataStr);
|
||||
this.registerListener(data.name, data.cookie, data.alertListener);
|
||||
this.showNotification(data.imageURL, data.title, data.text,
|
||||
data.textClickable, data.cookie, data.name, data.bidi,
|
||||
data.lang, null);
|
||||
data.lang, dataObj, null);
|
||||
},
|
||||
|
||||
showAppNotification: function(aMessage) {
|
||||
let data = aMessage.data;
|
||||
let details = data.details;
|
||||
let dataObject = this.deserializeStructuredClone(details.data);
|
||||
let listener = {
|
||||
mm: aMessage.target,
|
||||
title: data.title,
|
||||
|
@ -257,12 +285,14 @@ let AlertsHelper = {
|
|||
id: details.id || undefined,
|
||||
dir: details.dir || undefined,
|
||||
tag: details.tag || undefined,
|
||||
timestamp: details.timestamp || undefined
|
||||
timestamp: details.timestamp || undefined,
|
||||
dataObj: dataObject || undefined
|
||||
};
|
||||
this.registerAppListener(data.uid, listener);
|
||||
this.showNotification(data.imageURL, data.title, data.text,
|
||||
details.textClickable, null, data.uid, details.dir,
|
||||
details.lang, details.manifestURL, details.timestamp);
|
||||
details.lang, dataObject, details.manifestURL,
|
||||
details.timestamp);
|
||||
},
|
||||
|
||||
closeAlert: function(name) {
|
||||
|
|
|
@ -69,7 +69,7 @@ AlertsService.prototype = {
|
|||
// nsIAlertsService
|
||||
showAlertNotification: function(aImageUrl, aTitle, aText, aTextClickable,
|
||||
aCookie, aAlertListener, aName, aBidi,
|
||||
aLang) {
|
||||
aLang, aDataStr) {
|
||||
cpmm.sendAsyncMessage(kMessageAlertNotificationSend, {
|
||||
imageURL: aImageUrl,
|
||||
title: aTitle,
|
||||
|
@ -79,7 +79,8 @@ AlertsService.prototype = {
|
|||
listener: aAlertListener,
|
||||
id: aName,
|
||||
dir: aBidi,
|
||||
lang: aLang
|
||||
lang: aLang,
|
||||
dataStr: aDataStr
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -95,6 +96,7 @@ AlertsService.prototype = {
|
|||
let uid = (aDetails.id == "") ?
|
||||
"app-notif-" + uuidGenerator.generateUUID() : aDetails.id;
|
||||
|
||||
let dataObj = this.deserializeStructuredClone(aDetails.data);
|
||||
this._listeners[uid] = {
|
||||
observer: aAlertListener,
|
||||
title: aTitle,
|
||||
|
@ -106,7 +108,8 @@ AlertsService.prototype = {
|
|||
dbId: aDetails.dbId || undefined,
|
||||
dir: aDetails.dir || undefined,
|
||||
tag: aDetails.tag || undefined,
|
||||
timestamp: aDetails.timestamp || undefined
|
||||
timestamp: aDetails.timestamp || undefined,
|
||||
dataObj: dataObj || undefined
|
||||
};
|
||||
|
||||
cpmm.sendAsyncMessage(kMessageAppNotificationSend, {
|
||||
|
@ -151,7 +154,8 @@ AlertsService.prototype = {
|
|||
id: listener.id,
|
||||
tag: listener.tag,
|
||||
dbId: listener.dbId,
|
||||
timestamp: listener.timestamp
|
||||
timestamp: listener.timestamp,
|
||||
data: listener.dataObj || undefined,
|
||||
},
|
||||
Services.io.newURI(data.target, null, null),
|
||||
Services.io.newURI(listener.manifestURL, null, null)
|
||||
|
@ -167,6 +171,30 @@ AlertsService.prototype = {
|
|||
}
|
||||
delete this._listeners[data.uid];
|
||||
}
|
||||
},
|
||||
|
||||
deserializeStructuredClone: function(dataString) {
|
||||
if (!dataString) {
|
||||
return null;
|
||||
}
|
||||
let scContainer = Cc["@mozilla.org/docshell/structured-clone-container;1"].
|
||||
createInstance(Ci.nsIStructuredCloneContainer);
|
||||
|
||||
// The maximum supported structured-clone serialization format version
|
||||
// as defined in "js/public/StructuredClone.h"
|
||||
let JS_STRUCTURED_CLONE_VERSION = 4;
|
||||
scContainer.initFromBase64(dataString, JS_STRUCTURED_CLONE_VERSION);
|
||||
let dataObj = scContainer.deserializeToVariant();
|
||||
|
||||
// We have to check whether dataObj contains DOM objects (supported by
|
||||
// nsIStructuredCloneContainer, but not by Cu.cloneInto), e.g. ImageData.
|
||||
// After the structured clone callback systems will be unified, we'll not
|
||||
// have to perform this check anymore.
|
||||
try {
|
||||
let data = Cu.cloneInto(dataObj, {});
|
||||
} catch(e) { dataObj = null; }
|
||||
|
||||
return dataObj;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"filename": "setup.sh"
|
||||
},
|
||||
{
|
||||
"size": 165226,
|
||||
"digest": "79280f7595bc9e1613e05f8b2f0db3798ac739b96191e0f133e8ccd8ad149fedc84a1046e59863574189db28363a01712ae7b368ad1714e30ff88e7ebd5dad23",
|
||||
"size": 166407,
|
||||
"digest": "88fcc94f21818621e9e10107db913a3c787c6a68219c1e3e5fb26ebdf0864efdca4f05bd168d4851fee35c6b8d9ca4f9eb3ec229f565b7e6ce55ff6e7e899c24",
|
||||
"algorithm": "sha512",
|
||||
"filename": "sccache.tar.bz2"
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"filename": "setup.sh"
|
||||
},
|
||||
{
|
||||
"size": 165226,
|
||||
"digest": "79280f7595bc9e1613e05f8b2f0db3798ac739b96191e0f133e8ccd8ad149fedc84a1046e59863574189db28363a01712ae7b368ad1714e30ff88e7ebd5dad23",
|
||||
"size": 166407,
|
||||
"digest": "88fcc94f21818621e9e10107db913a3c787c6a68219c1e3e5fb26ebdf0864efdca4f05bd168d4851fee35c6b8d9ca4f9eb3ec229f565b7e6ce55ff6e7e899c24",
|
||||
"algorithm": "sha512",
|
||||
"filename": "sccache.tar.bz2"
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
"filename": "clang.tar.bz2"
|
||||
},
|
||||
{
|
||||
"size": 165226,
|
||||
"digest": "79280f7595bc9e1613e05f8b2f0db3798ac739b96191e0f133e8ccd8ad149fedc84a1046e59863574189db28363a01712ae7b368ad1714e30ff88e7ebd5dad23",
|
||||
"size": 166407,
|
||||
"digest": "88fcc94f21818621e9e10107db913a3c787c6a68219c1e3e5fb26ebdf0864efdca4f05bd168d4851fee35c6b8d9ca4f9eb3ec229f565b7e6ce55ff6e7e899c24",
|
||||
"algorithm": "sha512",
|
||||
"filename": "sccache.tar.bz2"
|
||||
}
|
||||
|
|
|
@ -53,9 +53,9 @@ pref("extensions.blocklist.interval", 86400);
|
|||
// Controls what level the blocklist switches from warning about items to forcibly
|
||||
// blocking them.
|
||||
pref("extensions.blocklist.level", 2);
|
||||
pref("extensions.blocklist.url", "https://addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
|
||||
pref("extensions.blocklist.url", "https://blocklist.addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
|
||||
pref("extensions.blocklist.detailsURL", "https://www.mozilla.org/%LOCALE%/blocklist/");
|
||||
pref("extensions.blocklist.itemURL", "https://addons.mozilla.org/%LOCALE%/%APP%/blocked/%blockID%");
|
||||
pref("extensions.blocklist.itemURL", "https://blocklist.addons.mozilla.org/%LOCALE%/%APP%/blocked/%blockID%");
|
||||
|
||||
pref("extensions.update.autoUpdateDefault", true);
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
<commandset id="editMenuCommands"/>
|
||||
|
||||
<command id="View:PageSource" oncommand="BrowserViewSourceOfDocument(content.document);" observes="isImage"/>
|
||||
<command id="View:PageSource" oncommand="BrowserViewSourceOfDocument(window.gBrowser.selectedBrowser.contentDocumentAsCPOW);" observes="isImage"/>
|
||||
<command id="View:PageInfo" oncommand="BrowserPageInfo();"/>
|
||||
<command id="View:FullScreen" oncommand="BrowserFullScreen();"/>
|
||||
<command id="cmd_find"
|
||||
|
|
|
@ -58,6 +58,10 @@ var tabPreviews = {
|
|||
thumbnail.height = this.height;
|
||||
thumbnail.width = this.width;
|
||||
|
||||
// drawWindow doesn't yet work with e10s (bug 698371)
|
||||
if (gMultiProcessBrowser)
|
||||
return thumbnail;
|
||||
|
||||
var ctx = thumbnail.getContext("2d");
|
||||
var win = aTab.linkedBrowser.contentWindow;
|
||||
var snippetWidth = win.innerWidth * .6;
|
||||
|
|
|
@ -290,6 +290,7 @@ toolbarpaletteitem > #personal-bookmarks > #bookmarks-toolbar-placeholder,
|
|||
display: -moz-box;
|
||||
}
|
||||
|
||||
#nav-bar-customization-target > #personal-bookmarks,
|
||||
toolbar:not(#TabsToolbar) > #wrapper-personal-bookmarks,
|
||||
toolbar:not(#TabsToolbar) > #personal-bookmarks {
|
||||
-moz-box-flex: 1;
|
||||
|
|
|
@ -1122,11 +1122,15 @@ var gBrowserInit = {
|
|||
|
||||
// This pageshow listener needs to be registered before we may call
|
||||
// swapBrowsersAndCloseOther() to receive pageshow events fired by that.
|
||||
gBrowser.addEventListener("pageshow", function(event) {
|
||||
// Filter out events that are not about the document load we are interested in
|
||||
if (content && event.target == content.document)
|
||||
setTimeout(pageShowEventHandlers, 0, event.persisted);
|
||||
}, true);
|
||||
if (!gMultiProcessBrowser) {
|
||||
// pageshow handlers are being migrated to
|
||||
// content.js. Eventually this code should be removed.
|
||||
gBrowser.addEventListener("pageshow", function(event) {
|
||||
// Filter out events that are not about the document load we are interested in
|
||||
if (content && event.target == content.document)
|
||||
setTimeout(pageShowEventHandlers, 0, event.persisted);
|
||||
}, true);
|
||||
}
|
||||
|
||||
if (uriToLoad && uriToLoad != "about:blank") {
|
||||
if (uriToLoad instanceof Ci.nsISupportsArray) {
|
||||
|
@ -2258,7 +2262,7 @@ function BrowserPageInfo(doc, initialTab, imageElement) {
|
|||
var args = {doc: doc, initialTab: initialTab, imageElement: imageElement};
|
||||
var windows = Services.wm.getEnumerator("Browser:page-info");
|
||||
|
||||
var documentURL = doc ? doc.location : window.content.document.location;
|
||||
var documentURL = doc ? doc.location : window.gBrowser.selectedBrowser.contentDocumentAsCPOW.location;
|
||||
|
||||
// Check for windows matching the url
|
||||
while (windows.hasMoreElements()) {
|
||||
|
@ -4361,10 +4365,6 @@ nsBrowserAccess.prototype = {
|
|||
isTabContentWindow: function (aWindow) {
|
||||
return gBrowser.browsers.some(function (browser) browser.contentWindow == aWindow);
|
||||
},
|
||||
|
||||
get contentWindow() {
|
||||
return gBrowser.contentWindow;
|
||||
}
|
||||
}
|
||||
|
||||
function getTogglableToolbars() {
|
||||
|
|
|
@ -129,8 +129,6 @@ chatBrowserAccess.prototype = {
|
|||
},
|
||||
|
||||
isTabContentWindow: function (aWindow) this.contentWindow == aWindow,
|
||||
|
||||
get contentWindow() document.getElementById("chatter").contentWindow
|
||||
};
|
||||
|
||||
</script>
|
||||
|
|
|
@ -128,7 +128,7 @@ var security = {
|
|||
window.openDialog("chrome://browser/content/preferences/cookies.xul",
|
||||
"Browser:Cookies", "", {filterString : eTLD});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Open the login manager window
|
||||
*/
|
||||
|
@ -143,7 +143,7 @@ var security = {
|
|||
}
|
||||
else
|
||||
window.openDialog("chrome://passwordmgr/content/passwordManager.xul",
|
||||
"Toolkit:PasswordManager", "",
|
||||
"Toolkit:PasswordManager", "",
|
||||
{filterString : this._getSecurityInfo().hostName});
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,834 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "console",
|
||||
"resource://gre/modules/devtools/Console.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
|
||||
"resource:///modules/loop/LoopStorage.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
return new EventEmitter();
|
||||
});
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LoopContacts"];
|
||||
|
||||
const kObjectStoreName = "contacts";
|
||||
|
||||
/*
|
||||
* The table used to store contacts information contains two identifiers,
|
||||
* both of which can be used to look up entries in the table. The table
|
||||
* key path (primary index, which must be unique) is "_guid", and is
|
||||
* automatically generated by IndexedDB when an entry is first inserted.
|
||||
* The other identifier, "id", is the supposedly unique key assigned to this
|
||||
* entry by whatever service generated it (e.g., Google Contacts). While
|
||||
* this key should, in theory, be completely unique, we don't use it
|
||||
* as the key path to avoid generating errors when an external database
|
||||
* violates this constraint. This second ID is referred to as the "serviceId".
|
||||
*/
|
||||
const kKeyPath = "_guid";
|
||||
const kServiceIdIndex = "id";
|
||||
|
||||
/**
|
||||
* Contacts validation.
|
||||
*
|
||||
* To allow for future integration with the Contacts API and/ or potential
|
||||
* integration with contact synchronization across devices (including Firefox OS
|
||||
* devices), we are using objects with properties having the same names and
|
||||
* structure as those used by mozContact.
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/mozContact for more
|
||||
* information.
|
||||
*/
|
||||
const kFieldTypeString = "string";
|
||||
const kFieldTypeNumber = "number";
|
||||
const kFieldTypeNumberOrString = "number|string";
|
||||
const kFieldTypeArray = "array";
|
||||
const kFieldTypeBool = "boolean";
|
||||
const kContactFields = {
|
||||
"id": {
|
||||
// Because "id" is externally generated, it might be numeric
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"published": {
|
||||
// mozContact, from which we are derived, defines dates as
|
||||
// "a Date object, which will eventually be converted to a
|
||||
// long long" -- to be forwards compatible, we allow both
|
||||
// formats for now.
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"updated": {
|
||||
// mozContact, from which we are derived, defines dates as
|
||||
// "a Date object, which will eventually be converted to a
|
||||
// long long" -- to be forwards compatible, we allow both
|
||||
// formats for now.
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"bday": {
|
||||
// mozContact, from which we are derived, defines dates as
|
||||
// "a Date object, which will eventually be converted to a
|
||||
// long long" -- to be forwards compatible, we allow both
|
||||
// formats for now.
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"blocked": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"adr": {
|
||||
type: kFieldTypeArray,
|
||||
contains: {
|
||||
"countryName": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"locality": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"postalCode": {
|
||||
// In some (but not all) locations, postal codes can be strictly numeric
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"pref": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"region": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"streetAddress": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"type": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
}
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
type: kFieldTypeArray,
|
||||
contains: {
|
||||
"pref": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"type": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"value": {
|
||||
type: kFieldTypeString
|
||||
}
|
||||
}
|
||||
},
|
||||
"tel": {
|
||||
type: kFieldTypeArray,
|
||||
contains: {
|
||||
"pref": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"type": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"value": {
|
||||
type: kFieldTypeString
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"honorificPrefix": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"givenName": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"additionalName": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"familyName": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"honorificSuffix": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"category": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"org": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"jobTitle": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"note": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares the properties contained in an object to the definition as defined in
|
||||
* `kContactFields`.
|
||||
* If a property is encountered that is not found in the spec, an Error is thrown.
|
||||
* If a property is encountered with an invalid value, an Error is thrown.
|
||||
*
|
||||
* Please read the spec at https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
||||
* for more information.
|
||||
*
|
||||
* @param {Object} obj The contact object, or part of it when called recursively
|
||||
* @param {Object} def The definition of properties to validate against. Defaults
|
||||
* to `kContactFields`
|
||||
*/
|
||||
const validateContact = function(obj, def = kContactFields) {
|
||||
for (let propName of Object.getOwnPropertyNames(obj)) {
|
||||
// Ignore internal properties.
|
||||
if (propName.startsWith("_")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let propDef = def[propName];
|
||||
if (!propDef) {
|
||||
throw new Error("Field '" + propName + "' is not supported for contacts");
|
||||
}
|
||||
|
||||
let val = obj[propName];
|
||||
|
||||
switch (propDef.type) {
|
||||
case kFieldTypeString:
|
||||
if (typeof val != kFieldTypeString) {
|
||||
throw new Error("Field '" + propName + "' must be of type String");
|
||||
}
|
||||
break;
|
||||
case kFieldTypeNumberOrString:
|
||||
let type = typeof val;
|
||||
if (type != kFieldTypeNumber && type != kFieldTypeString) {
|
||||
throw new Error("Field '" + propName + "' must be of type Number or String");
|
||||
}
|
||||
break;
|
||||
case kFieldTypeBool:
|
||||
if (typeof val != kFieldTypeBool) {
|
||||
throw new Error("Field '" + propName + "' must be of type Boolean");
|
||||
}
|
||||
break;
|
||||
case kFieldTypeArray:
|
||||
if (!Array.isArray(val)) {
|
||||
throw new Error("Field '" + propName + "' must be an Array");
|
||||
}
|
||||
|
||||
let contains = propDef.contains;
|
||||
// If the type of `contains` is a scalar value, it means that the array
|
||||
// consists of items of only that type.
|
||||
let isScalarCheck = (typeof contains == kFieldTypeString);
|
||||
for (let arrayValue of val) {
|
||||
if (isScalarCheck) {
|
||||
if (typeof arrayValue != contains) {
|
||||
throw new Error("Field '" + propName + "' must be of type " + contains);
|
||||
}
|
||||
} else {
|
||||
validateContact(arrayValue, contains);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides a method to perform multiple operations in a single transaction on the
|
||||
* contacts store.
|
||||
*
|
||||
* @param {String} operation Name of an operation supported by `IDBObjectStore`
|
||||
* @param {Array} data List of objects that will be passed to the object
|
||||
* store operation
|
||||
* @param {Function} callback Function that will be invoked once the operations
|
||||
* have finished. The first argument passed will be
|
||||
* an `Error` object or `null`. The second argument
|
||||
* will be the `data` Array, if all operations finished
|
||||
* successfully.
|
||||
*/
|
||||
const batch = function(operation, data, callback) {
|
||||
let processed = [];
|
||||
if (!LoopContactsInternal.hasOwnProperty(operation) ||
|
||||
typeof LoopContactsInternal[operation] != 'function') {
|
||||
callback(new Error ("LoopContactsInternal does not contain a '" +
|
||||
operation + "' method"));
|
||||
return;
|
||||
}
|
||||
LoopStorage.asyncForEach(data, (item, next) => {
|
||||
LoopContactsInternal[operation](item, (err, result) => {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
processed.push(result);
|
||||
next();
|
||||
});
|
||||
}, err => {
|
||||
if (err) {
|
||||
callback(err, processed);
|
||||
return;
|
||||
}
|
||||
callback(null, processed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend a `target` object with the properties defined in `source`.
|
||||
*
|
||||
* @param {Object} target The target object to receive properties defined in `source`
|
||||
* @param {Object} source The source object to copy properties from
|
||||
*/
|
||||
const extend = function(target, source) {
|
||||
for (let key of Object.getOwnPropertyNames(source)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
LoopStorage.on("upgrade", function(e, db) {
|
||||
if (db.objectStoreNames.contains(kObjectStoreName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the 'contacts' store as it doesn't exist yet.
|
||||
let store = db.createObjectStore(kObjectStoreName, {
|
||||
keyPath: kKeyPath,
|
||||
autoIncrement: true
|
||||
});
|
||||
store.createIndex(kServiceIdIndex, kServiceIdIndex, {unique: false});
|
||||
});
|
||||
|
||||
/**
|
||||
* The Contacts class.
|
||||
*
|
||||
* Each method that is a member of this class requires the last argument to be a
|
||||
* callback Function. MozLoopAPI will cause things to break if this invariant is
|
||||
* violated. You'll notice this as well in the documentation for each method.
|
||||
*/
|
||||
let LoopContactsInternal = Object.freeze({
|
||||
/**
|
||||
* Add a contact to the data store.
|
||||
*
|
||||
* @param {Object} details An object that will be added to the data store
|
||||
* as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
||||
* for more information of this objects' structure
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if it was stored successfully.
|
||||
*/
|
||||
add: function(details, callback) {
|
||||
if (!(kServiceIdIndex in details)) {
|
||||
callback(new Error("No '" + kServiceIdIndex + "' field present"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateContact(details);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let contact = extend({}, details);
|
||||
let now = Date.now();
|
||||
// The data source should have included "published" and "updated" values
|
||||
// for any imported records, and we need to keep track of those dated for
|
||||
// sync purposes (i.e., when we add functionality to push local changes to
|
||||
// a remote server from which we originally got a contact). We also need
|
||||
// to track the time at which *we* added and most recently changed the
|
||||
// contact, so as to determine whether the local or the remote store has
|
||||
// fresher data.
|
||||
//
|
||||
// For clarity: the fields "published" and "updated" indicate when the
|
||||
// *remote* data source published and updated the contact. The fields
|
||||
// "_date_add" and "_date_lch" track when the *local* data source
|
||||
// created and updated the contact.
|
||||
contact.published = contact.published ? new Date(contact.published).getTime() : now;
|
||||
contact.updated = contact.updated ? new Date(contact.updated).getTime() : now;
|
||||
contact._date_add = contact._date_lch = now;
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.add(contact);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
contact[kKeyPath] = event.target.result;
|
||||
eventEmitter.emit("add", contact);
|
||||
callback(null, contact);
|
||||
};
|
||||
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a batch of contacts to the data store.
|
||||
*
|
||||
* @param {Array} contacts A list of contact objects to be added
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the list of added contacts.
|
||||
*/
|
||||
addMany: function(contacts, callback) {
|
||||
batch("add", contacts, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a contact from the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to remove
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation.
|
||||
*/
|
||||
remove: function(guid, callback) {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.delete(guid);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
eventEmitter.emit("remove", contact);
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a batch of contacts from the data store.
|
||||
*
|
||||
* @param {Array} guids A list of IDs of the contacts to remove
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the list of IDs, if successfull.
|
||||
*/
|
||||
removeMany: function(guids, callback) {
|
||||
batch("remove", guids, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove _all_ contacts from the data store.
|
||||
* CAUTION: this method will clear the whole data store - you won't have any
|
||||
* contacts left!
|
||||
*
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation, if successfull.
|
||||
*/
|
||||
removeAll: function(callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.clear();
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
eventEmitter.emit("removeAll", event.target.result);
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a specific contact from the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to retrieve
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successful.
|
||||
*/
|
||||
get: function(guid, callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.get(guid);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
if (!event.target.result) {
|
||||
callback(new Error("Contact with " + kKeyPath + " '" +
|
||||
guid + "' could not be found"));;
|
||||
return;
|
||||
}
|
||||
let contact = extend({}, event.target.result);
|
||||
contact[kKeyPath] = guid;
|
||||
callback(null, contact);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a specific contact from the data store using the kServiceIdIndex
|
||||
* property.
|
||||
*
|
||||
* @param {String} serviceId String identifier of the contact to retrieve
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
getByServiceId: function(serviceId, callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let index = store.index(kServiceIdIndex);
|
||||
let request;
|
||||
try {
|
||||
request = index.get(serviceId);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
if (!event.target.result) {
|
||||
callback(new Error("Contact with " + kServiceIdIndex + " '" +
|
||||
serviceId + "' could not be found"));
|
||||
return;
|
||||
}
|
||||
|
||||
let contact = extend({}, event.target.result);
|
||||
callback(null, contact);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve _all_ contacts from the data store.
|
||||
* CAUTION: If the amount of contacts is very large (say > 100000), this method
|
||||
* may slow down your application!
|
||||
*
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be an `Array` of contact objects, if successfull.
|
||||
*/
|
||||
getAll: function(callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let cursorRequest = store.openCursor();
|
||||
let contactsList = [];
|
||||
|
||||
cursorRequest.onsuccess = event => {
|
||||
let cursor = event.target.result;
|
||||
// No more results, return the list.
|
||||
if (!cursor) {
|
||||
callback(null, contactsList);
|
||||
return;
|
||||
}
|
||||
|
||||
let contact = extend({}, cursor.value);
|
||||
contact[kKeyPath] = cursor.key;
|
||||
contactsList.push(contact);
|
||||
|
||||
cursor.continue();
|
||||
};
|
||||
|
||||
cursorRequest.onerror = event => callback(event.target.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve an arbitrary amount of contacts from the data store.
|
||||
* CAUTION: If the amount of contacts is very large (say > 1000), this method
|
||||
* may slow down your application!
|
||||
*
|
||||
* @param {Array} guids List of contact IDs to retrieve contact objects of
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be an `Array` of contact objects, if successfull.
|
||||
*/
|
||||
getMany: function(guids, callback) {
|
||||
let contacts = [];
|
||||
LoopStorage.asyncParallel(guids, (guid, next) => {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
contacts.push(contact);
|
||||
next();
|
||||
});
|
||||
}, err => {
|
||||
callback(err, !err ? contacts : null);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a specific contact in the data store.
|
||||
* The contact object is modified by replacing the fields passed in the `details`
|
||||
* param and any fields not passed in are left unchanged.
|
||||
*
|
||||
* @param {Object} details An object that will be updated in the data store
|
||||
* as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
||||
* for more information of this objects' structure
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
update: function(details, callback) {
|
||||
if (!(kKeyPath in details)) {
|
||||
callback(new Error("No '" + kKeyPath + "' field present"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateContact(details);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
let guid = details[kKeyPath];
|
||||
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let previous = extend({}, contact);
|
||||
// Update the contact with properties provided by `details`.
|
||||
extend(contact, details);
|
||||
|
||||
details._date_lch = Date.now();
|
||||
let request;
|
||||
try {
|
||||
request = store.put(contact);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
eventEmitter.emit("update", contact, previous);
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Block a specific contact in the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to block
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
block: function(guid, callback) {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
contact.blocked = true;
|
||||
this.update(contact, callback);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Un-block a specific contact in the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to unblock
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
unblock: function(guid, callback) {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
contact.blocked = false;
|
||||
this.update(contact, callback);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Import a list of (new) contacts from an external data source.
|
||||
*
|
||||
* @param {Object} options Property bag of options for the importer
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation, if successfull.
|
||||
*/
|
||||
startImport: function(options, callback) {
|
||||
//TODO in bug 972000.
|
||||
callback(new Error("Not implemented yet!"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Search through the data store for contacts that match a certain (sub-)string.
|
||||
*
|
||||
* @param {String} query Needle to search for in our haystack of contacts
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be an `Array` of contact objects, if successfull.
|
||||
*/
|
||||
search: function(query, callback) {
|
||||
//TODO in bug 1037114.
|
||||
callback(new Error("Not implemented yet!"));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Public Loop Contacts API.
|
||||
*
|
||||
* LoopContacts implements the EventEmitter interface by exposing three methods -
|
||||
* `on`, `once` and `off` - to subscribe to events.
|
||||
* At this point the following events may be subscribed to:
|
||||
* - 'add': A new contact object was successfully added to the data store.
|
||||
* - 'remove': A contact was successfully removed from the data store.
|
||||
* - 'removeAll': All contacts were successfully removed from the data store.
|
||||
* - 'update': A contact object was successfully updated with changed
|
||||
* properties in the data store.
|
||||
*/
|
||||
this.LoopContacts = Object.freeze({
|
||||
add: function(details, callback) {
|
||||
return LoopContactsInternal.add(details, callback);
|
||||
},
|
||||
|
||||
addMany: function(contacts, callback) {
|
||||
return LoopContactsInternal.addMany(contacts, callback);
|
||||
},
|
||||
|
||||
remove: function(guid, callback) {
|
||||
return LoopContactsInternal.remove(guid, callback);
|
||||
},
|
||||
|
||||
removeMany: function(guids, callback) {
|
||||
return LoopContactsInternal.removeMany(guids, callback);
|
||||
},
|
||||
|
||||
removeAll: function(callback) {
|
||||
return LoopContactsInternal.removeAll(callback);
|
||||
},
|
||||
|
||||
get: function(guid, callback) {
|
||||
return LoopContactsInternal.get(guid, callback);
|
||||
},
|
||||
|
||||
getByServiceId: function(serviceId, callback) {
|
||||
return LoopContactsInternal.getByServiceId(serviceId, callback);
|
||||
},
|
||||
|
||||
getAll: function(callback) {
|
||||
return LoopContactsInternal.getAll(callback);
|
||||
},
|
||||
|
||||
getMany: function(guids, callback) {
|
||||
return LoopContactsInternal.getMany(guids, callback);
|
||||
},
|
||||
|
||||
update: function(details, callback) {
|
||||
return LoopContactsInternal.update(details, callback);
|
||||
},
|
||||
|
||||
block: function(guid, callback) {
|
||||
return LoopContactsInternal.block(guid, callback);
|
||||
},
|
||||
|
||||
unblock: function(guid, callback) {
|
||||
return LoopContactsInternal.unblock(guid, callback);
|
||||
},
|
||||
|
||||
startImport: function(options, callback) {
|
||||
return LoopContactsInternal.startImport(options, callback);
|
||||
},
|
||||
|
||||
search: function(query, callback) {
|
||||
return LoopContactsInternal.search(query, callback);
|
||||
},
|
||||
|
||||
on: (...params) => eventEmitter.on(...params),
|
||||
|
||||
once: (...params) => eventEmitter.once(...params),
|
||||
|
||||
off: (...params) => eventEmitter.off(...params)
|
||||
});
|
|
@ -0,0 +1,319 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.importGlobalProperties(["indexedDB"]);
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
return new EventEmitter();
|
||||
});
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LoopStorage"];
|
||||
|
||||
const kDatabaseName = "loop";
|
||||
const kDatabaseVersion = 1;
|
||||
|
||||
let gWaitForOpenCallbacks = new Set();
|
||||
let gDatabase = null;
|
||||
let gClosed = false;
|
||||
|
||||
/**
|
||||
* Properly shut the database instance down. This is done on application shutdown.
|
||||
*/
|
||||
const closeDatabase = function() {
|
||||
Services.obs.removeObserver(closeDatabase, "quit-application");
|
||||
if (!gDatabase) {
|
||||
return;
|
||||
}
|
||||
gDatabase.close();
|
||||
gDatabase = null;
|
||||
gClosed = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a connection to the IndexedDB database.
|
||||
* This function is different than IndexedDBHelper.jsm provides, as it ensures
|
||||
* only one connection is open during the lifetime of this API. Callbacks are
|
||||
* queued when a connection attempt is in progress and are invoked once the
|
||||
* connection is established.
|
||||
*
|
||||
* @param {Function} onOpen Callback to be invoked once a database connection is
|
||||
* established. It takes an Error object as first argument
|
||||
* and the database connection object as second argument,
|
||||
* if successful.
|
||||
*/
|
||||
const ensureDatabaseOpen = function(onOpen) {
|
||||
if (gClosed) {
|
||||
onOpen(new Error("Database already closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gDatabase) {
|
||||
onOpen(null, gDatabase);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gWaitForOpenCallbacks.has(onOpen)) {
|
||||
gWaitForOpenCallbacks.add(onOpen);
|
||||
|
||||
if (gWaitForOpenCallbacks.size !== 1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let invokeCallbacks = err => {
|
||||
for (let callback of gWaitForOpenCallbacks) {
|
||||
callback(err, gDatabase);
|
||||
}
|
||||
gWaitForOpenCallbacks.clear();
|
||||
};
|
||||
|
||||
let openRequest = indexedDB.open(kDatabaseName, kDatabaseVersion);
|
||||
|
||||
openRequest.onblocked = function(event) {
|
||||
invokeCallbacks(new Error("Database cannot be upgraded cause in use: " + event.target.error));
|
||||
};
|
||||
|
||||
openRequest.onerror = function(event) {
|
||||
// Try to delete the old database so that we can start this process over
|
||||
// next time.
|
||||
indexedDB.deleteDatabase(kDatabaseName);
|
||||
invokeCallbacks(new Error("Error while opening database: " + event.target.errorCode));
|
||||
};
|
||||
|
||||
openRequest.onupgradeneeded = function(event) {
|
||||
let db = event.target.result;
|
||||
eventEmitter.emit("upgrade", db, event.oldVersion, kDatabaseVersion);
|
||||
};
|
||||
|
||||
openRequest.onsuccess = function(event) {
|
||||
gDatabase = event.target.result;
|
||||
invokeCallbacks();
|
||||
// Close the database instance properly on application shutdown.
|
||||
Services.obs.addObserver(closeDatabase, "quit-application", false);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return it.
|
||||
*
|
||||
* @param {String} store Name of the object store to start a transaction on
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* transaction object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
const getTransaction = function(store, callback, mode) {
|
||||
ensureDatabaseOpen((err, db) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let trans;
|
||||
try {
|
||||
trans = db.transaction(store, mode);
|
||||
} catch(ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
callback(null, trans);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return the requested store.
|
||||
*
|
||||
* @param {String} store Name of the object store to retrieve
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* store object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
const getStore = function(store, callback, mode) {
|
||||
getTransaction(store, (err, trans) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, trans.objectStore(store));
|
||||
}, mode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Public Loop Storage API.
|
||||
*
|
||||
* Since IndexedDB transaction can not stand a spin of the event loop _before_
|
||||
* using a IDBTransaction object, we can't use Promise.jsm promises. Therefore
|
||||
* LoopStorage provides two async helper functions, `asyncForEach` and `asyncParallel`.
|
||||
*
|
||||
* LoopStorage implements the EventEmitter interface by exposing two methods, `on`
|
||||
* and `off`, to subscribe to events.
|
||||
* At this point only the `upgrade` event will be emitted. This happens when the
|
||||
* database is loaded in memory and consumers will be able to change its structure.
|
||||
*/
|
||||
this.LoopStorage = Object.freeze({
|
||||
/**
|
||||
* Open a connection to the IndexedDB database and return the database object.
|
||||
*
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established. It takes an Error object as first
|
||||
* argument and the database connection object as
|
||||
* second argument, if successful.
|
||||
*/
|
||||
getSingleton: function(callback) {
|
||||
ensureDatabaseOpen(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return it.
|
||||
* If only two arguments are passed, the default mode will be assumed and the
|
||||
* second argument is assumed to be a callback.
|
||||
*
|
||||
* @param {String} store Name of the object store to start a transaction on
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* transaction object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
getTransaction: function(store, callback, mode = "readonly") {
|
||||
getTransaction(store, callback, mode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return the requested store.
|
||||
* If only two arguments are passed, the default mode will be assumed and the
|
||||
* second argument is assumed to be a callback.
|
||||
*
|
||||
* @param {String} store Name of the object store to retrieve
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* store object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
getStore: function(store, callback, mode = "readonly") {
|
||||
getStore(store, callback, mode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an async function in serial on each of the list items and call a
|
||||
* callback Function when all list items are done.
|
||||
* IMPORTANT: only use this iteration method if you are sure that the operations
|
||||
* performed in `onItem` are guaranteed to be async in the success case.
|
||||
*
|
||||
* @param {Array} list Non-empty list of items to iterate
|
||||
* @param {Function} onItem Callback to invoke for each item in the list. It
|
||||
* takes the item is first argument and a callback
|
||||
* function as second, which is to be invoked once
|
||||
* the consumer is done with its async operation. If
|
||||
* an error is passed as the first argument to this
|
||||
* callback function, the iteration will stop and
|
||||
* `onDone` callback will be invoked with that error.
|
||||
* @param {callback} onDone Callback to invoke when the list is completed or
|
||||
* on error. It takes an Error object as first
|
||||
* argument.
|
||||
*/
|
||||
asyncForEach: function(list, onItem, onDone) {
|
||||
let i = 0;
|
||||
let len = list.length;
|
||||
|
||||
if (!len) {
|
||||
onDone(new Error("Argument error: empty list"));
|
||||
return;
|
||||
}
|
||||
|
||||
onItem(list[i], function handler(err) {
|
||||
if (err) {
|
||||
onDone(err);
|
||||
return;
|
||||
}
|
||||
|
||||
i++;
|
||||
if (i < len) {
|
||||
onItem(list[i], handler, i);
|
||||
} else {
|
||||
onDone();
|
||||
}
|
||||
}, i);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an async function in parallel on each of the list items and call a
|
||||
* callback Function when all list items are done.
|
||||
* IMPORTANT: only use this iteration method if you are sure that the operations
|
||||
* performed in `onItem` are guaranteed to be async in the success case.
|
||||
*
|
||||
* @param {Array} list Non-empty list of items to iterate
|
||||
* @param {Function} onItem Callback to invoke for each item in the list. It
|
||||
* takes the item is first argument and a callback
|
||||
* function as second, which is to be invoked once
|
||||
* the consumer is done with its async operation. If
|
||||
* an error is passed as the first argument to this
|
||||
* callback function, the iteration will stop and
|
||||
* `onDone` callback will be invoked with that error.
|
||||
* @param {callback} onDone Callback to invoke when the list is completed or
|
||||
* on error. It takes an Error object as first
|
||||
* argument.
|
||||
*/
|
||||
asyncParallel: function(list, onItem, onDone) {
|
||||
let i = 0;
|
||||
let done = 0;
|
||||
let callbackCalled = false;
|
||||
let len = list.length;
|
||||
|
||||
if (!len) {
|
||||
onDone(new Error("Argument error: empty list"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (; i < len; ++i) {
|
||||
onItem(list[i], function handler(err) {
|
||||
if (callbackCalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
onDone(err);
|
||||
callbackCalled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (++done === len) {
|
||||
onDone();
|
||||
callbackCalled = true;
|
||||
}
|
||||
}, i);
|
||||
}
|
||||
},
|
||||
|
||||
on: (...params) => eventEmitter.on(...params),
|
||||
|
||||
off: (...params) => eventEmitter.off(...params)
|
||||
});
|
|
@ -9,6 +9,7 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
|||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/loop/MozLoopService.jsm");
|
||||
Cu.import("resource:///modules/loop/LoopContacts.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
|
||||
"resource://gre/modules/MozSocialAPI.jsm");
|
||||
|
@ -22,6 +23,62 @@ XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
|
|||
"nsIClipboardHelper");
|
||||
this.EXPORTED_SYMBOLS = ["injectLoopAPI"];
|
||||
|
||||
/**
|
||||
* Trying to clone an Error object into a different container will yield an error.
|
||||
* We can work around this by copying the properties we care about onto a regular
|
||||
* object.
|
||||
*
|
||||
* @param {Error} error Error object to copy
|
||||
* @param {nsIDOMWindow} targetWindow The content window to attach the API
|
||||
*/
|
||||
const cloneErrorObject = function(error, targetWindow) {
|
||||
let obj = new targetWindow.Error();
|
||||
for (let prop of Object.getOwnPropertyNames(error)) {
|
||||
obj[prop] = String(error[prop]);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject any API containing _only_ function properties into the given window.
|
||||
*
|
||||
* @param {Object} api Object containing functions that need to
|
||||
* be exposed to content
|
||||
* @param {nsIDOMWindow} targetWindow The content window to attach the API
|
||||
*/
|
||||
const injectObjectAPI = function(api, targetWindow) {
|
||||
let injectedAPI = {};
|
||||
// Wrap all the methods in `api` to help results passed to callbacks get
|
||||
// through the priv => unpriv barrier with `Cu.cloneInto()`.
|
||||
Object.keys(api).forEach(func => {
|
||||
injectedAPI[func] = function(...params) {
|
||||
let callback = params.pop();
|
||||
api[func](...params, function(...results) {
|
||||
results = results.map(result => {
|
||||
if (result && typeof result == "object") {
|
||||
// Inspect for an error this way, because the Error object is special.
|
||||
if (result.constructor.name == "Error") {
|
||||
return cloneErrorObject(result.message)
|
||||
}
|
||||
return Cu.cloneInto(result, targetWindow);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
callback(...results);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
let contentObj = Cu.cloneInto(injectedAPI, targetWindow, {cloneFunctions: true});
|
||||
// Since we deny preventExtensions on XrayWrappers, because Xray semantics make
|
||||
// it difficult to act like an object has actually been frozen, we try to seal
|
||||
// the `contentObj` without Xrays.
|
||||
try {
|
||||
Object.seal(Cu.waiveXrays(contentObj));
|
||||
} catch (ex) {}
|
||||
return contentObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject the loop API into the given window. The caller must be sure the
|
||||
* window is a loop content window (eg, a panel, chatwindow, or similar).
|
||||
|
@ -34,6 +91,7 @@ function injectLoopAPI(targetWindow) {
|
|||
let ringer;
|
||||
let ringerStopper;
|
||||
let appVersionInfo;
|
||||
let contactsAPI;
|
||||
|
||||
let api = {
|
||||
/**
|
||||
|
@ -61,6 +119,21 @@ function injectLoopAPI(targetWindow) {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the contacts API.
|
||||
*
|
||||
* @returns {Object} The contacts API object
|
||||
*/
|
||||
contacts: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
if (contactsAPI) {
|
||||
return contactsAPI;
|
||||
}
|
||||
return contactsAPI = injectObjectAPI(LoopContacts, targetWindow);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns translated strings associated with an element. Designed
|
||||
* for use with l10n.js
|
||||
|
|
|
@ -13,6 +13,8 @@ BROWSER_CHROME_MANIFESTS += [
|
|||
]
|
||||
|
||||
EXTRA_JS_MODULES.loop += [
|
||||
'LoopContacts.jsm',
|
||||
'LoopStorage.jsm',
|
||||
'MozLoopAPI.jsm',
|
||||
'MozLoopPushHandler.jsm',
|
||||
'MozLoopWorker.js',
|
||||
|
|
|
@ -7,6 +7,7 @@ support-files =
|
|||
[browser_fxa_login.js]
|
||||
skip-if = !debug
|
||||
[browser_loop_fxa_server.js]
|
||||
[browser_LoopContacts.js]
|
||||
[browser_mozLoop_appVersionInfo.js]
|
||||
[browser_mozLoop_prefs.js]
|
||||
[browser_mozLoop_doNotDisturb.js]
|
||||
|
|
|
@ -0,0 +1,405 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const {LoopContacts} = Cu.import("resource:///modules/loop/LoopContacts.jsm", {});
|
||||
|
||||
const kContacts = [{
|
||||
id: 1,
|
||||
name: ["Ally Avocado"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "ally@mail.com"
|
||||
}],
|
||||
category: ["google"],
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
},{
|
||||
id: 2,
|
||||
name: ["Bob Banana"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "bob@gmail.com"
|
||||
}],
|
||||
category: ["local"],
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
}, {
|
||||
id: 3,
|
||||
name: ["Caitlin Cantaloupe"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "caitlin.cant@hotmail.com"
|
||||
}],
|
||||
category: ["local"],
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
}, {
|
||||
id: 4,
|
||||
name: ["Dave Dragonfruit"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "dd@dragons.net"
|
||||
}],
|
||||
category: ["google"],
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
}];
|
||||
|
||||
const kDanglingContact = {
|
||||
id: 5,
|
||||
name: ["Ellie Eggplant"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "ellie@yahoo.com"
|
||||
}],
|
||||
category: ["google"],
|
||||
blocked: true,
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
};
|
||||
|
||||
const promiseLoadContacts = function() {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
LoopContacts.removeAll(err => {
|
||||
if (err) {
|
||||
deferred.reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
gExpectedAdds.push(...kContacts);
|
||||
LoopContacts.addMany(kContacts, (err, contacts) => {
|
||||
if (err) {
|
||||
deferred.reject(err);
|
||||
return;
|
||||
}
|
||||
deferred.resolve(contacts);
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
// Get a copy of a contact without private properties.
|
||||
const normalizeContact = function(contact) {
|
||||
let result = {};
|
||||
// Get a copy of contact without private properties.
|
||||
for (let prop of Object.getOwnPropertyNames(contact)) {
|
||||
if (!prop.startsWith("_")) {
|
||||
result[prop] = contact[prop]
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const compareContacts = function(contact1, contact2) {
|
||||
Assert.ok("_guid" in contact1, "First contact should have an ID.");
|
||||
Assert.deepEqual(normalizeContact(contact1), normalizeContact(contact2));
|
||||
};
|
||||
|
||||
// LoopContacts emits various events. Test if they work as expected here.
|
||||
let gExpectedAdds = [];
|
||||
let gExpectedRemovals = [];
|
||||
let gExpectedUpdates = [];
|
||||
|
||||
const onContactAdded = function(e, contact) {
|
||||
let expectedIds = gExpectedAdds.map(contact => contact.id);
|
||||
let idx = expectedIds.indexOf(contact.id);
|
||||
Assert.ok(idx > -1, "Added contact should be expected");
|
||||
let expected = gExpectedAdds[idx];
|
||||
compareContacts(contact, expected);
|
||||
gExpectedAdds.splice(idx, 1);
|
||||
};
|
||||
|
||||
const onContactRemoved = function(e, contact) {
|
||||
let idx = gExpectedRemovals.indexOf(contact._guid);
|
||||
Assert.ok(idx > -1, "Removed contact should be expected");
|
||||
gExpectedRemovals.splice(idx, 1);
|
||||
};
|
||||
|
||||
const onContactUpdated = function(e, contact) {
|
||||
let idx = gExpectedUpdates.indexOf(contact._guid);
|
||||
Assert.ok(idx > -1, "Updated contact should be expected");
|
||||
gExpectedUpdates.splice(idx, 1);
|
||||
};
|
||||
|
||||
LoopContacts.on("add", onContactAdded);
|
||||
LoopContacts.on("remove", onContactRemoved);
|
||||
LoopContacts.on("update", onContactUpdated);
|
||||
|
||||
registerCleanupFunction(function () {
|
||||
LoopContacts.removeAll(() => {});
|
||||
LoopContacts.off("add", onContactAdded);
|
||||
LoopContacts.off("remove", onContactRemoved);
|
||||
LoopContacts.off("update", onContactUpdated);
|
||||
});
|
||||
|
||||
// Test adding a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
for (let i = 0, l = contacts.length; i < l; ++i) {
|
||||
compareContacts(contacts[i], kContacts[i]);
|
||||
}
|
||||
|
||||
// Add a contact.
|
||||
let deferred = Promise.defer();
|
||||
gExpectedAdds.push(kDanglingContact);
|
||||
LoopContacts.add(kDanglingContact, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
compareContacts(contact, kDanglingContact);
|
||||
|
||||
// Check if it's persisted.
|
||||
LoopContacts.get(contact._guid, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
compareContacts(contact, kDanglingContact);
|
||||
deferred.resolve();
|
||||
});
|
||||
});
|
||||
yield deferred.promise;
|
||||
});
|
||||
|
||||
// Test removing all contacts.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
let deferred = Promise.defer();
|
||||
LoopContacts.removeAll(function(err) {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
LoopContacts.getAll(function(err, found) {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(found.length, 0, "There shouldn't be any contacts left");
|
||||
deferred.resolve();
|
||||
})
|
||||
});
|
||||
yield deferred.promise;
|
||||
});
|
||||
|
||||
// Test retrieving a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
// Get a single contact.
|
||||
let deferred = Promise.defer();
|
||||
LoopContacts.get(contacts[1]._guid, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
compareContacts(contact, kContacts[1]);
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Get a single contact by id.
|
||||
let deferred = Promise.defer();
|
||||
LoopContacts.getByServiceId(2, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
compareContacts(contact, kContacts[1]);
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Get a couple of contacts.
|
||||
let deferred = Promise.defer();
|
||||
let toRetrieve = [contacts[0], contacts[2], contacts[3]];
|
||||
LoopContacts.getMany(toRetrieve.map(contact => contact._guid), (err, result) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(result.length, toRetrieve.length, "Result list should be the same " +
|
||||
"size as the list of items to retrieve");
|
||||
for (let contact of toRetrieve) {
|
||||
let found = result.filter(c => c._guid == contact._guid);
|
||||
Assert.ok(found.length, "Contact " + contact._guid + " should be in the list");
|
||||
compareContacts(found[0], contact);
|
||||
}
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Get all contacts.
|
||||
deferred = Promise.defer();
|
||||
LoopContacts.getAll((err, contacts) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
for (let i = 0, l = contacts.length; i < l; ++i) {
|
||||
compareContacts(contacts[i], kContacts[i]);
|
||||
}
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Get a non-existent contact.
|
||||
deferred = Promise.defer();
|
||||
LoopContacts.get(1000, err => {
|
||||
Assert.ok(err, "There should be an error");
|
||||
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
|
||||
"Error message should be correct");
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
});
|
||||
|
||||
// Test removing a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
// Remove a single contact.
|
||||
let deferred = Promise.defer();
|
||||
let toRemove = contacts[2]._guid;
|
||||
gExpectedRemovals.push(toRemove);
|
||||
LoopContacts.remove(toRemove, err => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
|
||||
LoopContacts.get(toRemove, err => {
|
||||
Assert.ok(err, "There should be an error");
|
||||
Assert.equal(err.message, "Contact with _guid '" + toRemove + "' could not be found",
|
||||
"Error message should be correct");
|
||||
deferred.resolve();
|
||||
});
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Remove a non-existing contact.
|
||||
deferred = Promise.defer();
|
||||
LoopContacts.remove(1000, err => {
|
||||
Assert.ok(err, "There should be an error");
|
||||
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
|
||||
"Error message should be correct");
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Remove multiple contacts.
|
||||
deferred = Promise.defer();
|
||||
toRemove = [contacts[0]._guid, contacts[1]._guid];
|
||||
gExpectedRemovals.push(...toRemove);
|
||||
LoopContacts.removeMany(toRemove, err => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
|
||||
LoopContacts.getAll((err, contacts) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
let ids = contacts.map(contact => contact._guid);
|
||||
Assert.equal(ids.indexOf(toRemove[0]), -1, "Contact '" + toRemove[0] +
|
||||
"' shouldn't be there");
|
||||
Assert.equal(ids.indexOf(toRemove[1]), -1, "Contact '" + toRemove[1] +
|
||||
"' shouldn't be there");
|
||||
deferred.resolve();
|
||||
});
|
||||
});
|
||||
yield deferred.promise;
|
||||
});
|
||||
|
||||
// Test updating a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
const newBday = (new Date(403920000000)).toISOString();
|
||||
|
||||
// Update a single contact.
|
||||
let deferred = Promise.defer();
|
||||
let toUpdate = {
|
||||
_guid: contacts[2]._guid,
|
||||
bday: newBday
|
||||
};
|
||||
gExpectedUpdates.push(contacts[2]._guid);
|
||||
LoopContacts.update(toUpdate, (err, result) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(result, toUpdate._guid, "Result should be the same as the contact ID");
|
||||
|
||||
LoopContacts.get(toUpdate._guid, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(contact.bday, newBday, "Birthday should be the same");
|
||||
// Check that all other properties were left intact.
|
||||
contacts[2].bday = newBday;
|
||||
compareContacts(contact, contacts[2]);
|
||||
deferred.resolve();
|
||||
});
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Update a non-existing contact.
|
||||
deferred = Promise.defer();
|
||||
toUpdate = {
|
||||
_guid: 1000,
|
||||
bday: newBday
|
||||
};
|
||||
LoopContacts.update(toUpdate, err => {
|
||||
Assert.ok(err, "There should be an error");
|
||||
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
|
||||
"Error message should be correct");
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
});
|
||||
|
||||
// Test blocking and unblocking a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
// Block contact.
|
||||
let deferred = Promise.defer();
|
||||
let toBlock = contacts[1]._guid;
|
||||
gExpectedUpdates.push(toBlock);
|
||||
LoopContacts.block(toBlock, (err, result) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(result, toBlock, "Result should be the same as the contact ID");
|
||||
|
||||
LoopContacts.get(toBlock, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.strictEqual(contact.blocked, true, "Blocked status should be set");
|
||||
// Check that all other properties were left intact.
|
||||
delete contact.blocked;
|
||||
compareContacts(contact, contacts[1]);
|
||||
deferred.resolve();
|
||||
});
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Block a non-existing contact.
|
||||
deferred = Promise.defer();
|
||||
LoopContacts.block(1000, err => {
|
||||
Assert.ok(err, "There should be an error");
|
||||
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
|
||||
"Error message should be correct");
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Unblock a contact.
|
||||
deferred = Promise.defer();
|
||||
let toUnblock = contacts[1]._guid;
|
||||
gExpectedUpdates.push(toUnblock);
|
||||
LoopContacts.unblock(toUnblock, (err, result) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(result, toUnblock, "Result should be the same as the contact ID");
|
||||
|
||||
LoopContacts.get(toUnblock, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.strictEqual(contact.blocked, false, "Blocked status should be set");
|
||||
// Check that all other properties were left intact.
|
||||
delete contact.blocked;
|
||||
compareContacts(contact, contacts[1]);
|
||||
deferred.resolve();
|
||||
});
|
||||
});
|
||||
yield deferred.promise;
|
||||
|
||||
// Unblock a non-existing contact.
|
||||
deferred = Promise.defer();
|
||||
LoopContacts.unblock(1000, err => {
|
||||
Assert.ok(err, "There should be an error");
|
||||
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
|
||||
"Error message should be correct");
|
||||
deferred.resolve();
|
||||
});
|
||||
yield deferred.promise;
|
||||
});
|
||||
|
||||
// Test if the event emitter implementation doesn't leak and is working as expected.
|
||||
add_task(function* () {
|
||||
yield promiseLoadContacts();
|
||||
|
||||
Assert.strictEqual(gExpectedAdds.length, 0, "No contact additions should be expected anymore");
|
||||
Assert.strictEqual(gExpectedRemovals.length, 0, "No contact removals should be expected anymore");
|
||||
Assert.strictEqual(gExpectedUpdates.length, 0, "No contact updates should be expected anymore");
|
||||
});
|
|
@ -414,7 +414,7 @@ var gAdvancedPane = {
|
|||
introText : bundlePreferences.getString("offlinepermissionstext") };
|
||||
document.documentElement.openWindow("Browser:Permissions",
|
||||
"chrome://browser/content/preferences/permissions.xul",
|
||||
"", params);
|
||||
"resizable", params);
|
||||
},
|
||||
|
||||
// XXX: duplicated in browser.js
|
||||
|
|
|
@ -62,7 +62,7 @@ var gContentPane = {
|
|||
params.introText = bundlePreferences.getString("popuppermissionstext");
|
||||
document.documentElement.openWindow("Browser:Permissions",
|
||||
"chrome://browser/content/preferences/permissions.xul",
|
||||
"", params);
|
||||
"resizable", params);
|
||||
},
|
||||
|
||||
|
||||
|
@ -187,7 +187,7 @@ var gContentPane = {
|
|||
{
|
||||
document.documentElement.openWindow("Browser:TranslationExceptions",
|
||||
"chrome://browser/content/preferences/translation.xul",
|
||||
"", null);
|
||||
"resizable", null);
|
||||
},
|
||||
|
||||
openTranslationProviderAttribution: function ()
|
||||
|
|
|
@ -109,6 +109,5 @@
|
|||
label="&button.close.label;" accesskey="&button.close.accesskey;"/>
|
||||
#endif
|
||||
</hbox>
|
||||
<resizer type="window" dir="bottomend"/>
|
||||
</hbox>
|
||||
</window>
|
||||
|
|
|
@ -62,15 +62,16 @@ let gSubDialog = {
|
|||
},
|
||||
|
||||
open: function(aURL, aFeatures = null, aParams = null, aClosingCallback = null) {
|
||||
let features = aFeatures || "modal,centerscreen,resizable=no";
|
||||
let features = (!!aFeatures ? aFeatures + "," : "") + "resizable,dialog=no,centerscreen";
|
||||
let dialog = window.openDialog(aURL, "dialogFrame", features, aParams);
|
||||
if (aClosingCallback) {
|
||||
this._closingCallback = aClosingCallback.bind(dialog);
|
||||
}
|
||||
features = features.replace(/,/g, "&");
|
||||
let featureParams = new URLSearchParams(features.toLowerCase());
|
||||
this._box.setAttribute("resizable", featureParams.has("resizable") &&
|
||||
featureParams.get("resizable") != "no" &&
|
||||
featureParams.get("resizable") != 0);
|
||||
featureParams.get("resizable") != "0");
|
||||
return dialog;
|
||||
},
|
||||
|
||||
|
|
|
@ -78,6 +78,5 @@
|
|||
label="&button.close.label;" accesskey="&button.close.accesskey;"/>
|
||||
#endif
|
||||
</hbox>
|
||||
<resizer type="window" dir="bottomend"/>
|
||||
</hbox>
|
||||
</window>
|
||||
|
|
|
@ -474,7 +474,7 @@ var gPrivacyPane = {
|
|||
introText : bundlePreferences.getString("cookiepermissionstext") };
|
||||
document.documentElement.openWindow("Browser:Permissions",
|
||||
"chrome://browser/content/preferences/permissions.xul",
|
||||
"", params);
|
||||
"resizable", params);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -484,7 +484,7 @@ var gPrivacyPane = {
|
|||
{
|
||||
document.documentElement.openWindow("Browser:Cookies",
|
||||
"chrome://browser/content/preferences/cookies.xul",
|
||||
"", null);
|
||||
"resizable", null);
|
||||
},
|
||||
|
||||
// CLEAR PRIVATE DATA
|
||||
|
|
|
@ -111,7 +111,7 @@ var gSecurityPane = {
|
|||
{
|
||||
document.documentElement.openWindow("Toolkit:PasswordManagerExceptions",
|
||||
"chrome://passwordmgr/content/passwordManagerExceptions.xul",
|
||||
"", null);
|
||||
"resizable", null);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -221,7 +221,7 @@ var gSecurityPane = {
|
|||
{
|
||||
document.documentElement.openWindow("Toolkit:PasswordManager",
|
||||
"chrome://passwordmgr/content/passwordManager.xul",
|
||||
"", null);
|
||||
"resizable", null);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -85,6 +85,5 @@
|
|||
label="&button.close.label;" accesskey="&button.close.accesskey;"/>
|
||||
#endif
|
||||
</hbox>
|
||||
<resizer type="window" dir="bottomend"/>
|
||||
</hbox>
|
||||
</window>
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
"filename": "gcc.tar.xz"
|
||||
},
|
||||
{
|
||||
"size": 165226,
|
||||
"digest": "79280f7595bc9e1613e05f8b2f0db3798ac739b96191e0f133e8ccd8ad149fedc84a1046e59863574189db28363a01712ae7b368ad1714e30ff88e7ebd5dad23",
|
||||
"size": 166407,
|
||||
"digest": "88fcc94f21818621e9e10107db913a3c787c6a68219c1e3e5fb26ebdf0864efdca4f05bd168d4851fee35c6b8d9ca4f9eb3ec229f565b7e6ce55ff6e7e899c24",
|
||||
"algorithm": "sha512",
|
||||
"filename": "sccache.tar.bz2"
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
"filename": "gcc.tar.xz"
|
||||
},
|
||||
{
|
||||
"size": 165226,
|
||||
"digest": "79280f7595bc9e1613e05f8b2f0db3798ac739b96191e0f133e8ccd8ad149fedc84a1046e59863574189db28363a01712ae7b368ad1714e30ff88e7ebd5dad23",
|
||||
"size": 166407,
|
||||
"digest": "88fcc94f21818621e9e10107db913a3c787c6a68219c1e3e5fb26ebdf0864efdca4f05bd168d4851fee35c6b8d9ca4f9eb3ec229f565b7e6ce55ff6e7e899c24",
|
||||
"algorithm": "sha512",
|
||||
"filename": "sccache.tar.bz2"
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
"filename": "clang.tar.bz2"
|
||||
},
|
||||
{
|
||||
"size": 165226,
|
||||
"digest": "79280f7595bc9e1613e05f8b2f0db3798ac739b96191e0f133e8ccd8ad149fedc84a1046e59863574189db28363a01712ae7b368ad1714e30ff88e7ebd5dad23",
|
||||
"size": 166407,
|
||||
"digest": "88fcc94f21818621e9e10107db913a3c787c6a68219c1e3e5fb26ebdf0864efdca4f05bd168d4851fee35c6b8d9ca4f9eb3ec229f565b7e6ce55ff6e7e899c24",
|
||||
"algorithm": "sha512",
|
||||
"filename": "sccache.tar.bz2"
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
"filename": "setup.sh"
|
||||
},
|
||||
{
|
||||
"size": 165226,
|
||||
"digest": "79280f7595bc9e1613e05f8b2f0db3798ac739b96191e0f133e8ccd8ad149fedc84a1046e59863574189db28363a01712ae7b368ad1714e30ff88e7ebd5dad23",
|
||||
"size": 166407,
|
||||
"digest": "88fcc94f21818621e9e10107db913a3c787c6a68219c1e3e5fb26ebdf0864efdca4f05bd168d4851fee35c6b8d9ca4f9eb3ec229f565b7e6ce55ff6e7e899c24",
|
||||
"algorithm": "sha512",
|
||||
"filename": "sccache.tar.bz2"
|
||||
}
|
||||
|
|
|
@ -229,12 +229,13 @@ TabTarget.prototype = {
|
|||
},
|
||||
|
||||
get name() {
|
||||
return this._tab ? this._tab.linkedBrowser.contentDocument.title :
|
||||
this._form.title;
|
||||
return this._tab && this._tab.linkedBrowser.contentDocument ?
|
||||
this._tab.linkedBrowser.contentDocument.title :
|
||||
this._form.title;
|
||||
},
|
||||
|
||||
get url() {
|
||||
return this._tab ? this._tab.linkedBrowser.contentDocument.location.href :
|
||||
return this._tab ? this._tab.linkedBrowser.currentURI.spec :
|
||||
this._form.url;
|
||||
},
|
||||
|
||||
|
@ -298,17 +299,20 @@ TabTarget.prototype = {
|
|||
this._client.listTabs(aResponse => {
|
||||
this._root = aResponse;
|
||||
|
||||
let windowUtils = this.window
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
let outerWindow = windowUtils.outerWindowID;
|
||||
aResponse.tabs.some((tab) => {
|
||||
if (tab.outerWindowID === outerWindow) {
|
||||
this._form = tab;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (this.window) {
|
||||
let windowUtils = this.window
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
let outerWindow = windowUtils.outerWindowID;
|
||||
aResponse.tabs.some((tab) => {
|
||||
if (tab.outerWindowID === outerWindow) {
|
||||
this._form = tab;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (!this._form) {
|
||||
this._form = aResponse.tabs[aResponse.selected];
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ const console = require("resource://gre/modules/devtools/Console.jsm").console;
|
|||
|
||||
const LOAD_ERROR = "error-load";
|
||||
const STYLE_EDITOR_TEMPLATE = "stylesheet";
|
||||
const SELECTOR_HIGHLIGHTER_TYPE = "SelectorHighlighter";
|
||||
const PREF_MEDIA_SIDEBAR = "devtools.styleeditor.showMediaSidebar";
|
||||
const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.mediaSidebarWidth";
|
||||
const PREF_NAV_WIDTH = "devtools.styleeditor.navSidebarWidth";
|
||||
|
@ -111,18 +112,24 @@ StyleEditorUI.prototype = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Initiates the style editor ui creation and the inspector front to get
|
||||
* reference to the walker.
|
||||
* Initiates the style editor ui creation, the inspector front to get
|
||||
* reference to the walker and the selector highlighter if available
|
||||
*/
|
||||
initialize: function() {
|
||||
let toolbox = gDevTools.getToolbox(this._target);
|
||||
return toolbox.initInspector().then(() => {
|
||||
return Task.spawn(function*() {
|
||||
let toolbox = gDevTools.getToolbox(this._target);
|
||||
yield toolbox.initInspector();
|
||||
this._walker = toolbox.walker;
|
||||
}).then(() => {
|
||||
|
||||
let hUtils = toolbox.highlighterUtils;
|
||||
if (hUtils.hasCustomHighlighter(SELECTOR_HIGHLIGHTER_TYPE)) {
|
||||
this._highlighter =
|
||||
yield hUtils.getHighlighterByType(SELECTOR_HIGHLIGHTER_TYPE);
|
||||
}
|
||||
}.bind(this)).then(() => {
|
||||
this.createUI();
|
||||
this._debuggee.getStyleSheets().then((styleSheets) => {
|
||||
this._resetStyleSheetList(styleSheets);
|
||||
|
||||
this._resetStyleSheetList(styleSheets);
|
||||
this._target.on("will-navigate", this._clear);
|
||||
this._target.on("navigate", this._onNewDocument);
|
||||
});
|
||||
|
@ -292,8 +299,8 @@ StyleEditorUI.prototype = {
|
|||
file = savedFile;
|
||||
}
|
||||
|
||||
let editor =
|
||||
new StyleSheetEditor(styleSheet, this._window, file, isNew, this._walker);
|
||||
let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew,
|
||||
this._walker, this._highlighter);
|
||||
|
||||
editor.on("property-change", this._summaryChange.bind(this, editor));
|
||||
editor.on("media-rules-changed", this._updateMediaList.bind(this, editor));
|
||||
|
@ -820,6 +827,11 @@ StyleEditorUI.prototype = {
|
|||
},
|
||||
|
||||
destroy: function() {
|
||||
if (this._highlighter) {
|
||||
this._highlighter.finalize();
|
||||
this._highlighter = null;
|
||||
}
|
||||
|
||||
this._clearStyleSheetEditors();
|
||||
|
||||
let sidebar = this._panelDoc.querySelector(".splitview-controller");
|
||||
|
|
|
@ -20,6 +20,7 @@ Cu.import("resource://gre/modules/Services.jsm");
|
|||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/event-emitter.js");
|
||||
Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
|
||||
|
||||
|
@ -46,6 +47,10 @@ const MAX_CHECK_COUNT=10;
|
|||
// The classname used to show a line that is not used
|
||||
const UNUSED_CLASS = "cm-unused-line";
|
||||
|
||||
// How much time should the mouse be still before the selector at that position
|
||||
// gets highlighted?
|
||||
const SELECTOR_HIGHLIGHT_TIMEOUT = 500;
|
||||
|
||||
/**
|
||||
* StyleSheetEditor controls the editor linked to a particular StyleSheet
|
||||
* object.
|
||||
|
@ -65,8 +70,11 @@ const UNUSED_CLASS = "cm-unused-line";
|
|||
* Optional whether the sheet was created by the user
|
||||
* @param {Walker} walker
|
||||
* Optional walker used for selectors autocompletion
|
||||
* @param {CustomHighlighterFront} highlighter
|
||||
* Optional highlighter front for the SelectorHighligher used to
|
||||
* highlight selectors
|
||||
*/
|
||||
function StyleSheetEditor(styleSheet, win, file, isNew, walker) {
|
||||
function StyleSheetEditor(styleSheet, win, file, isNew, walker, highlighter) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.styleSheet = styleSheet;
|
||||
|
@ -75,6 +83,7 @@ function StyleSheetEditor(styleSheet, win, file, isNew, walker) {
|
|||
this._window = win;
|
||||
this._isNew = isNew;
|
||||
this.walker = walker;
|
||||
this.highlighter = highlighter;
|
||||
|
||||
this._state = { // state to use when inputElement attaches
|
||||
text: "",
|
||||
|
@ -99,6 +108,7 @@ function StyleSheetEditor(styleSheet, win, file, isNew, walker) {
|
|||
this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this);
|
||||
this.saveToFile = this.saveToFile.bind(this);
|
||||
this.updateStyleSheet = this.updateStyleSheet.bind(this);
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
|
||||
this._focusOnSourceEditorReady = false;
|
||||
this.cssSheet.on("property-change", this._onPropertyChange);
|
||||
|
@ -377,6 +387,10 @@ StyleSheetEditor.prototype = {
|
|||
sourceEditor.setSelection(this._state.selection.start,
|
||||
this._state.selection.end);
|
||||
|
||||
if (this.highlighter && this.walker) {
|
||||
sourceEditor.container.addEventListener("mousemove", this._onMouseMove);
|
||||
}
|
||||
|
||||
this.emit("source-editor-load");
|
||||
});
|
||||
},
|
||||
|
@ -469,6 +483,52 @@ StyleSheetEditor.prototype = {
|
|||
this.styleSheet.update(this._state.text, transitionsEnabled);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle mousemove events, calling _highlightSelectorAt after a delay only
|
||||
* and reseting the delay everytime.
|
||||
*/
|
||||
_onMouseMove: function(e) {
|
||||
this.highlighter.hide();
|
||||
|
||||
if (this.mouseMoveTimeout) {
|
||||
this._window.clearTimeout(this.mouseMoveTimeout);
|
||||
this.mouseMoveTimeout = null;
|
||||
}
|
||||
|
||||
this.mouseMoveTimeout = this._window.setTimeout(() => {
|
||||
this._highlightSelectorAt(e.clientX, e.clientY);
|
||||
}, SELECTOR_HIGHLIGHT_TIMEOUT);
|
||||
},
|
||||
|
||||
/**
|
||||
* Highlight nodes matching the selector found at coordinates x,y in the
|
||||
* editor, if any.
|
||||
*
|
||||
* @param {Number} x
|
||||
* @param {Number} y
|
||||
*/
|
||||
_highlightSelectorAt: Task.async(function*(x, y) {
|
||||
// Need to catch parsing exceptions as long as bug 1051900 isn't fixed
|
||||
let info;
|
||||
try {
|
||||
let pos = this.sourceEditor.getPositionFromCoords({left: x, top: y});
|
||||
info = this.sourceEditor.getInfoAt(pos);
|
||||
} catch (e) {}
|
||||
if (!info || info.state !== "selector") {
|
||||
return;
|
||||
}
|
||||
|
||||
let node = yield this.walker.getStyleSheetOwnerNode(this.styleSheet.actorID);
|
||||
yield this.highlighter.show(node, {
|
||||
selector: info.selector,
|
||||
hideInfoBar: true,
|
||||
showOnly: "border",
|
||||
region: "border"
|
||||
});
|
||||
|
||||
this.emit("node-highlighted");
|
||||
}),
|
||||
|
||||
/**
|
||||
* Save the editor contents into a file and set savedFile property.
|
||||
* A file picker UI will open if file is not set and editor is not headless.
|
||||
|
@ -639,6 +699,10 @@ StyleSheetEditor.prototype = {
|
|||
this._sourceEditor.off("dirty-change", this._onPropertyChange);
|
||||
this._sourceEditor.off("save", this.saveToFile);
|
||||
this._sourceEditor.off("change", this.updateStyleSheet);
|
||||
if (this.highlighter && this.walker && this._sourceEditor.container) {
|
||||
this._sourceEditor.container.removeEventListener("mousemove",
|
||||
this._onMouseMove);
|
||||
}
|
||||
this._sourceEditor.destroy();
|
||||
}
|
||||
this.cssSheet.off("property-change", this._onPropertyChange);
|
||||
|
|
|
@ -49,6 +49,7 @@ skip-if = os == "linux" || "mac" # bug 949355
|
|||
[browser_styleeditor_enabled.js]
|
||||
[browser_styleeditor_fetch-from-cache.js]
|
||||
[browser_styleeditor_filesave.js]
|
||||
[browser_styleeditor_highlight-selector.js]
|
||||
[browser_styleeditor_import.js]
|
||||
[browser_styleeditor_import_rule.js]
|
||||
[browser_styleeditor_init.js]
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that hovering over a simple selector in the style-editor requests the
|
||||
// highlighting of the corresponding nodes
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
const TEST_URL = "data:text/html;charset=utf8," +
|
||||
"<style>div{color:red}</style><div>highlighter test</div>";
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
let {UI} = yield addTabAndOpenStyleEditors(1, null, TEST_URL);
|
||||
let editor = UI.editors[0];
|
||||
|
||||
// Mock the highlighter so we can locally assert that things happened
|
||||
// correctly instead of accessing the highlighter elements
|
||||
editor.highlighter = {
|
||||
isShown: false,
|
||||
options: null,
|
||||
|
||||
show: function(node, options) {
|
||||
this.isShown = true;
|
||||
this.options = options;
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.isShown = false;
|
||||
}
|
||||
};
|
||||
|
||||
info("Expecting a node-highlighted event");
|
||||
let onHighlighted = editor.once("node-highlighted");
|
||||
|
||||
info("Simulate a mousemove event on the div selector");
|
||||
editor._onMouseMove({clientX: 40, clientY: 10});
|
||||
yield onHighlighted;
|
||||
|
||||
ok(editor.highlighter.isShown, "The highlighter is now shown");
|
||||
is(editor.highlighter.options.selector, "div", "The selector is correct");
|
||||
|
||||
info("Simulate a mousemove event elsewhere in the editor");
|
||||
editor._onMouseMove({clientX: 0, clientY: 0});
|
||||
|
||||
ok(!editor.highlighter.isShown, "The highlighter is now hidden");
|
||||
});
|
|
@ -1087,10 +1087,6 @@ nsBrowserAccess.prototype = {
|
|||
isTabContentWindow: function(aWindow) {
|
||||
return Browser.browsers.some(function (browser) browser.contentWindow == aWindow);
|
||||
},
|
||||
|
||||
get contentWindow() {
|
||||
return Browser.selectedBrowser.contentWindow;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -237,7 +237,7 @@ pref("extensions.update.enabled", false);
|
|||
/* blocklist preferences */
|
||||
pref("extensions.blocklist.enabled", true);
|
||||
pref("extensions.blocklist.interval", 86400);
|
||||
pref("extensions.blocklist.url", "https://addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
|
||||
pref("extensions.blocklist.url", "https://blocklist.addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
|
||||
pref("extensions.blocklist.detailsURL", "https://www.mozilla.org/%LOCALE%/blocklist/");
|
||||
pref("extensions.showMismatchUI", false);
|
||||
|
||||
|
|
|
@ -314,11 +314,6 @@ description > html|a {
|
|||
width: 66ch;
|
||||
}
|
||||
|
||||
/* needs to be removed with bug 1035625 */
|
||||
:-moz-any(dialog, window, prefwindow) resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tree:not(#rejectsTree) {
|
||||
min-height: 15em;
|
||||
}
|
||||
|
|
|
@ -62,8 +62,9 @@ leak:gsmsdp_add_default_video_formats_to_local_sdp
|
|||
leak:CCAPI_CallInfo_getMediaStreams
|
||||
|
||||
# Intermittent Mochitest 3 WebRTC leaks, as seen in bug 1055910.
|
||||
leak:sdp_build_attr_ice_attr
|
||||
leak:sdp_build_attr_
|
||||
leak:VcmSIPCCBinding::CandidateReady
|
||||
leak:fsmdef_ev_foundcandidate
|
||||
|
||||
|
||||
###
|
||||
|
|
|
@ -1371,8 +1371,6 @@ if test "$GNU_CC"; then
|
|||
AC_MSG_RESULT("$result")
|
||||
if test "$result" = "yes"; then
|
||||
HAVE_X86_AVX2=1
|
||||
AC_DEFINE(HAVE_X86_AVX2)
|
||||
AC_SUBST(HAVE_X86_AVX2)
|
||||
fi
|
||||
esac
|
||||
|
||||
|
@ -2168,6 +2166,10 @@ ia64*-hpux*)
|
|||
dnl both SSSE3 and SSE4.1.
|
||||
HAVE_TOOLCHAIN_SUPPORT_MSSSE3=1
|
||||
HAVE_TOOLCHAIN_SUPPORT_MSSE4_1=1
|
||||
if test "$_CC_SUITE" -ge "11"; then
|
||||
dnl allow AVX2 code from VS2012
|
||||
HAVE_X86_AVX2=1
|
||||
fi
|
||||
MOZ_MEMORY=1
|
||||
fi
|
||||
AC_DEFINE(HAVE_SNPRINTF)
|
||||
|
@ -8703,6 +8705,7 @@ AC_SUBST(CPU_ARCH)
|
|||
AC_SUBST(INTEL_ARCHITECTURE)
|
||||
AC_SUBST(HAVE_TOOLCHAIN_SUPPORT_MSSSE3)
|
||||
AC_SUBST(HAVE_TOOLCHAIN_SUPPORT_MSSE4_1)
|
||||
AC_SUBST(HAVE_X86_AVX2)
|
||||
AC_SUBST(GCC_USE_GNU_LD)
|
||||
|
||||
AC_SUBST(MOZ_CHROME_FILE_FORMAT)
|
||||
|
|
|
@ -9,7 +9,7 @@ interface nsIInputStream;
|
|||
interface nsIDOMDocument;
|
||||
interface nsIURI;
|
||||
interface nsIPrincipal;
|
||||
interface nsIScriptGlobalObject;
|
||||
interface nsIGlobalObject;
|
||||
|
||||
/**
|
||||
* The nsIDOMParser interface is a non-SAX interface that can be used
|
||||
|
@ -89,7 +89,7 @@ interface nsIDOMParser : nsISupports
|
|||
[noscript] void init(in nsIPrincipal principal,
|
||||
in nsIURI documentURI,
|
||||
in nsIURI baseURI,
|
||||
in nsIScriptGlobalObject scriptObject);
|
||||
in nsIGlobalObject scriptObject);
|
||||
};
|
||||
|
||||
%{ C++
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "nsPIDOMWindow.h"
|
||||
#include "mozilla/LoadInfo.h"
|
||||
#include "mozilla/dom/BindingUtils.h"
|
||||
#include "mozilla/dom/ScriptSettings.h"
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::dom;
|
||||
|
@ -315,7 +316,7 @@ DOMParser::ParseFromStream(nsIInputStream *stream,
|
|||
|
||||
NS_IMETHODIMP
|
||||
DOMParser::Init(nsIPrincipal* principal, nsIURI* documentURI,
|
||||
nsIURI* baseURI, nsIScriptGlobalObject* aScriptObject)
|
||||
nsIURI* baseURI, nsIGlobalObject* aScriptObject)
|
||||
{
|
||||
NS_ENSURE_STATE(!mAttemptedInit);
|
||||
mAttemptedInit = true;
|
||||
|
@ -432,7 +433,7 @@ DOMParser::InitInternal(nsISupports* aOwner, nsIPrincipal* prin,
|
|||
}
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIScriptGlobalObject> scriptglobal = do_QueryInterface(aOwner);
|
||||
nsCOMPtr<nsIGlobalObject> scriptglobal = do_QueryInterface(aOwner);
|
||||
return Init(prin, documentURI, baseURI, scriptglobal);
|
||||
}
|
||||
|
||||
|
@ -442,26 +443,22 @@ DOMParser::Init(nsIPrincipal* aPrincipal, nsIURI* aDocumentURI,
|
|||
{
|
||||
AttemptedInitMarker marker(&mAttemptedInit);
|
||||
|
||||
JSContext *cx = nsContentUtils::GetCurrentJSContext();
|
||||
if (!cx) {
|
||||
rv.Throw(NS_ERROR_UNEXPECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
nsIScriptContext* scriptContext = GetScriptContextFromJSContext(cx);
|
||||
|
||||
nsCOMPtr<nsIPrincipal> principal = aPrincipal;
|
||||
if (!principal && !aDocumentURI) {
|
||||
principal = nsContentUtils::SubjectPrincipal();
|
||||
}
|
||||
|
||||
rv = Init(principal, aDocumentURI, aBaseURI,
|
||||
scriptContext ? scriptContext->GetGlobalObject() : nullptr);
|
||||
rv = Init(principal, aDocumentURI, aBaseURI, GetEntryGlobal());
|
||||
}
|
||||
|
||||
nsresult
|
||||
DOMParser::SetUpDocument(DocumentFlavor aFlavor, nsIDOMDocument** aResult)
|
||||
{
|
||||
// We should really QI to nsIGlobalObject here, but nsDocument gets confused
|
||||
// if we pass it a scriptHandlingObject that doesn't QI to
|
||||
// nsIScriptGlobalObject, and test_isequalnode.js (an xpcshell test without
|
||||
// a window global) breaks. The correct solution is just to wean nsDocument
|
||||
// off of nsIScriptGlobalObject, but that's a yak to shave another day.
|
||||
nsCOMPtr<nsIScriptGlobalObject> scriptHandlingObject =
|
||||
do_QueryReferent(mScriptHandlingObject);
|
||||
nsresult rv;
|
||||
|
|
|
@ -166,7 +166,7 @@
|
|||
let func = message.objects.func;
|
||||
let result = func(n => 2*n);
|
||||
ok(result == 20, "result == 20");
|
||||
let obj = {a:1};
|
||||
let obj = {a:1, __exposedProps__: {"a": "r"}};
|
||||
savedMM.sendAsyncMessage("cpows:from_parent", {}, {obj: obj});
|
||||
}
|
||||
|
||||
|
@ -191,7 +191,7 @@
|
|||
}
|
||||
|
||||
let savedWilldieObj;
|
||||
let wontDie = {f:2};
|
||||
let wontDie = {f:2, __exposedProps__: {"f": "r"}};
|
||||
function recvLifetimeTest1(message) {
|
||||
let obj = message.objects.obj;
|
||||
savedWilldieObj = obj.will_die;
|
||||
|
|
|
@ -119,6 +119,10 @@ public:
|
|||
// required to begin playback have been acquired. Can be called on any thread.
|
||||
virtual void NotifyWaitingForResourcesStatusChanged() = 0;
|
||||
|
||||
// Called by the reader's MediaResource as data arrives over the network.
|
||||
// Must be called on the main thread.
|
||||
virtual void NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset) = 0;
|
||||
|
||||
// Set by Reader if the current audio track can be offloaded
|
||||
virtual void SetPlatformCanOffloadAudio(bool aCanOffloadAudio) {}
|
||||
|
||||
|
|
|
@ -302,7 +302,7 @@ AudioSink::PlayFromAudioQueue()
|
|||
GetReentrantMonitor().NotifyAll();
|
||||
}
|
||||
SINK_LOG_V("playing %u frames of audio at time %lld",
|
||||
this, audio->mFrames, audio->mTime);
|
||||
audio->mFrames, audio->mTime);
|
||||
mAudioStream->Write(audio->mAudioData, audio->mFrames);
|
||||
|
||||
StartAudioStreamPlaybackIfNeeded();
|
||||
|
|
|
@ -575,13 +575,13 @@ void AudioStream::PanOutputIfNeeded(bool aMicrophoneActive)
|
|||
}
|
||||
|
||||
if (!strncmp(name, "MacBookPro", 10)) {
|
||||
if (cubeb_stream_get_current_device(mCubebStream, &device) == CUBEB_OK) {
|
||||
if (cubeb_stream_get_current_device(mCubebStream.get(), &device) == CUBEB_OK) {
|
||||
// Check if we are currently outputing sound on external speakers.
|
||||
if (!strcmp(device->output_name, "ispk")) {
|
||||
// Pan everything to the right speaker.
|
||||
if (aMicrophoneActive) {
|
||||
LOG(("%p Panning audio output to the right.", this));
|
||||
if (cubeb_stream_set_panning(mCubebStream, 1.0) != CUBEB_OK) {
|
||||
if (cubeb_stream_set_panning(mCubebStream.get(), 1.0) != CUBEB_OK) {
|
||||
NS_WARNING("Could not pan audio output to the right.");
|
||||
}
|
||||
} else {
|
||||
|
@ -592,11 +592,11 @@ void AudioStream::PanOutputIfNeeded(bool aMicrophoneActive)
|
|||
}
|
||||
if (panCenter) {
|
||||
LOG(("%p Panning audio output to the center.", this));
|
||||
if (cubeb_stream_set_panning(mCubebStream, 0.0) != CUBEB_OK) {
|
||||
if (cubeb_stream_set_panning(mCubebStream.get(), 0.0) != CUBEB_OK) {
|
||||
NS_WARNING("Could not pan audio output to the center.");
|
||||
}
|
||||
}
|
||||
cubeb_stream_device_destroy(mCubebStream, device);
|
||||
cubeb_stream_device_destroy(mCubebStream.get(), device);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -609,14 +609,14 @@ void AudioStream::ResetStreamIfNeeded()
|
|||
if (!mMicrophoneActive || mLatencyRequest != LowLatency) {
|
||||
return;
|
||||
}
|
||||
if (cubeb_stream_get_current_device(mCubebStream, &device) == CUBEB_OK) {
|
||||
if (cubeb_stream_get_current_device(mCubebStream.get(), &device) == CUBEB_OK) {
|
||||
// This a microphone that goes through the headphone plug, reset the
|
||||
// output to prevent echo building up.
|
||||
if (strcmp(device->input_name, "emic") == 0) {
|
||||
LOG(("Resetting audio output"));
|
||||
Reset();
|
||||
}
|
||||
cubeb_stream_device_destroy(mCubebStream, device);
|
||||
cubeb_stream_device_destroy(mCubebStream.get(), device);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -660,7 +660,7 @@ AudioStream::OpenCubeb(cubeb_stream_params &aParams,
|
|||
latency, DataCallback_S, StateCallback_S, this) == CUBEB_OK) {
|
||||
MonitorAutoLock mon(mMonitor);
|
||||
MOZ_ASSERT(mState != SHUTDOWN);
|
||||
mCubebStream.own(stream);
|
||||
mCubebStream.reset(stream);
|
||||
// We can't cubeb_stream_start() the thread from a transient thread due to
|
||||
// cubeb API requirements (init can be called from another thread, but
|
||||
// not start/stop/destroy/etc)
|
||||
|
@ -672,7 +672,7 @@ AudioStream::OpenCubeb(cubeb_stream_params &aParams,
|
|||
}
|
||||
}
|
||||
|
||||
cubeb_stream_register_device_changed_callback(mCubebStream,
|
||||
cubeb_stream_register_device_changed_callback(mCubebStream.get(),
|
||||
AudioStream::DeviceChangedCallback_s);
|
||||
|
||||
mState = INITIALIZED;
|
||||
|
@ -844,7 +844,7 @@ AudioStream::SetVolume(double aVolume)
|
|||
{
|
||||
NS_ABORT_IF_FALSE(aVolume >= 0.0 && aVolume <= 1.0, "Invalid volume");
|
||||
|
||||
if (cubeb_stream_set_volume(mCubebStream, aVolume * GetVolumeScale()) != CUBEB_OK) {
|
||||
if (cubeb_stream_set_volume(mCubebStream.get(), aVolume * GetVolumeScale()) != CUBEB_OK) {
|
||||
NS_WARNING("Could not change volume on cubeb stream.");
|
||||
}
|
||||
}
|
||||
|
@ -902,7 +902,7 @@ AudioStream::StartUnlocked()
|
|||
int r;
|
||||
{
|
||||
MonitorAutoUnlock mon(mMonitor);
|
||||
r = cubeb_stream_start(mCubebStream);
|
||||
r = cubeb_stream_start(mCubebStream.get());
|
||||
|
||||
PanOutputIfNeeded(mMicrophoneActive);
|
||||
}
|
||||
|
@ -924,7 +924,7 @@ AudioStream::Pause()
|
|||
int r;
|
||||
{
|
||||
MonitorAutoUnlock mon(mMonitor);
|
||||
r = cubeb_stream_stop(mCubebStream);
|
||||
r = cubeb_stream_stop(mCubebStream.get());
|
||||
}
|
||||
if (mState != ERRORED && r == CUBEB_OK) {
|
||||
mState = STOPPED;
|
||||
|
@ -942,7 +942,7 @@ AudioStream::Resume()
|
|||
int r;
|
||||
{
|
||||
MonitorAutoUnlock mon(mMonitor);
|
||||
r = cubeb_stream_start(mCubebStream);
|
||||
r = cubeb_stream_start(mCubebStream.get());
|
||||
}
|
||||
if (mState != ERRORED && r == CUBEB_OK) {
|
||||
mState = STARTED;
|
||||
|
@ -962,7 +962,7 @@ AudioStream::Shutdown()
|
|||
if (mCubebStream) {
|
||||
MonitorAutoUnlock mon(mMonitor);
|
||||
// Force stop to put the cubeb stream in a stable state before deletion.
|
||||
cubeb_stream_stop(mCubebStream);
|
||||
cubeb_stream_stop(mCubebStream.get());
|
||||
// Must not try to shut down cubeb from within the lock! wasapi may still
|
||||
// call our callback after Pause()/stop()!?! Bug 996162
|
||||
mCubebStream.reset();
|
||||
|
@ -1004,7 +1004,7 @@ AudioStream::GetPositionInFramesUnlocked()
|
|||
uint64_t position = 0;
|
||||
{
|
||||
MonitorAutoUnlock mon(mMonitor);
|
||||
if (cubeb_stream_get_position(mCubebStream, &position) != CUBEB_OK) {
|
||||
if (cubeb_stream_get_position(mCubebStream.get(), &position) != CUBEB_OK) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
@ -1016,7 +1016,7 @@ int64_t
|
|||
AudioStream::GetLatencyInFrames()
|
||||
{
|
||||
uint32_t latency;
|
||||
if (cubeb_stream_get_latency(mCubebStream, &latency)) {
|
||||
if (cubeb_stream_get_latency(mCubebStream.get(), &latency)) {
|
||||
NS_WARNING("Could not get cubeb latency.");
|
||||
return 0;
|
||||
}
|
||||
|
@ -1291,7 +1291,7 @@ AudioStream::DataCallback(void* aBuffer, long aFrames)
|
|||
mState != SHUTDOWN &&
|
||||
insertTime != INT64_MAX && servicedFrames > underrunFrames) {
|
||||
uint32_t latency = UINT32_MAX;
|
||||
if (cubeb_stream_get_latency(mCubebStream, &latency)) {
|
||||
if (cubeb_stream_get_latency(mCubebStream.get(), &latency)) {
|
||||
NS_WARNING("Could not get latency from cubeb.");
|
||||
}
|
||||
TimeStamp now = TimeStamp::Now();
|
||||
|
|
|
@ -8,29 +8,31 @@
|
|||
|
||||
#include "AudioSampleFormat.h"
|
||||
#include "nsAutoPtr.h"
|
||||
#include "nsAutoRef.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "Latency.h"
|
||||
#include "mozilla/dom/AudioChannelBinding.h"
|
||||
#include "mozilla/StaticMutex.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "mozilla/StaticMutex.h"
|
||||
#include "mozilla/UniquePtr.h"
|
||||
|
||||
#include "cubeb/cubeb.h"
|
||||
|
||||
template <>
|
||||
class nsAutoRefTraits<cubeb_stream> : public nsPointerRefTraits<cubeb_stream>
|
||||
{
|
||||
public:
|
||||
static void Release(cubeb_stream* aStream) { cubeb_stream_destroy(aStream); }
|
||||
};
|
||||
|
||||
namespace soundtouch {
|
||||
class SoundTouch;
|
||||
}
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
template<>
|
||||
struct DefaultDelete<cubeb_stream>
|
||||
{
|
||||
void operator()(cubeb_stream* aStream) const
|
||||
{
|
||||
cubeb_stream_destroy(aStream);
|
||||
}
|
||||
};
|
||||
|
||||
class AudioStream;
|
||||
class FrameHistory;
|
||||
|
||||
|
@ -387,9 +389,8 @@ private:
|
|||
// frames.
|
||||
CircularByteBuffer mBuffer;
|
||||
|
||||
// Owning reference to a cubeb_stream. cubeb_stream_destroy is called by
|
||||
// nsAutoRef's destructor.
|
||||
nsAutoRef<cubeb_stream> mCubebStream;
|
||||
// Owning reference to a cubeb_stream.
|
||||
UniquePtr<cubeb_stream> mCubebStream;
|
||||
|
||||
uint32_t mBytesPerFrame;
|
||||
|
||||
|
|
|
@ -188,6 +188,12 @@ BufferDecoder::NotifyWaitingForResourcesStatusChanged()
|
|||
// ignore
|
||||
}
|
||||
|
||||
void
|
||||
BufferDecoder::NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
MediaDecoderOwner*
|
||||
BufferDecoder::GetOwner()
|
||||
{
|
||||
|
|
|
@ -75,6 +75,8 @@ public:
|
|||
|
||||
virtual void NotifyWaitingForResourcesStatusChanged() MOZ_OVERRIDE;
|
||||
|
||||
virtual void NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset) MOZ_OVERRIDE;
|
||||
|
||||
protected:
|
||||
virtual ~BufferDecoder();
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ private:
|
|||
|
||||
virtual void Pin() {}
|
||||
virtual void Unpin() {}
|
||||
virtual double GetDownloadRate(bool* aIsReliable) { return 0.; }
|
||||
virtual double GetDownloadRate(bool* aIsReliable) { *aIsReliable = false; return 0.; }
|
||||
virtual int64_t GetLength() { return mLength; }
|
||||
virtual int64_t GetNextCachedData(int64_t aOffset) { return aOffset; }
|
||||
virtual int64_t GetCachedDataEnd(int64_t aOffset) { return mLength; }
|
||||
|
|
|
@ -570,7 +570,7 @@ public:
|
|||
|
||||
// Called as data arrives on the stream and is read into the cache. Called
|
||||
// on the main thread only.
|
||||
virtual void NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset);
|
||||
virtual void NotifyDataArrived(const char* aBuffer, uint32_t aLength, int64_t aOffset) MOZ_OVERRIDE;
|
||||
|
||||
// Called by MediaResource when the principal of the resource has
|
||||
// changed. Called on main thread only.
|
||||
|
|
|
@ -2594,20 +2594,20 @@ void MediaDecoderStateMachine::AdvanceFrame()
|
|||
int64_t remainingTime = AUDIO_DURATION_USECS;
|
||||
NS_ASSERTION(clock_time >= mStartTime, "Should have positive clock time.");
|
||||
nsAutoPtr<VideoData> currentFrame;
|
||||
#ifdef PR_LOGGING
|
||||
int32_t droppedFrames = 0;
|
||||
#endif
|
||||
if (VideoQueue().GetSize() > 0) {
|
||||
VideoData* frame = VideoQueue().PeekFront();
|
||||
#ifdef PR_LOGGING
|
||||
int32_t droppedFrames = 0;
|
||||
#endif
|
||||
while (mScheduler->IsRealTime() || clock_time >= frame->mTime) {
|
||||
mVideoFrameEndTime = frame->GetEndTime();
|
||||
currentFrame = frame;
|
||||
#ifdef PR_LOGGING
|
||||
VERBOSE_LOG("discarding video frame %lld", frame->mTime);
|
||||
if (droppedFrames++) {
|
||||
VERBOSE_LOG("discarding video frame %lld (%d so far)", frame->mTime, droppedFrames-1);
|
||||
if (currentFrame) {
|
||||
VERBOSE_LOG("discarding video frame mTime=%lld clock_time=%lld (%d so far)",
|
||||
currentFrame->mTime, ++droppedFrames);
|
||||
}
|
||||
#endif
|
||||
currentFrame = frame;
|
||||
VideoQueue().PopFront();
|
||||
// Notify the decode thread that the video queue's buffers may have
|
||||
// free'd up space for more frames.
|
||||
|
|
|
@ -146,7 +146,7 @@ public:
|
|||
virtual bool IsSuspended() MOZ_OVERRIDE { return false; }
|
||||
virtual bool IsTransportSeekable() MOZ_OVERRIDE { return true; }
|
||||
// dummy
|
||||
virtual double GetDownloadRate(bool* aIsReliable) MOZ_OVERRIDE { return 0; }
|
||||
virtual double GetDownloadRate(bool* aIsReliable) MOZ_OVERRIDE { *aIsReliable = false; return 0; }
|
||||
|
||||
virtual int64_t GetLength() MOZ_OVERRIDE {
|
||||
if (mRealTime) {
|
||||
|
|
|
@ -311,6 +311,14 @@ void
|
|||
CDMProxy::gmp_Shutdown()
|
||||
{
|
||||
MOZ_ASSERT(IsOnGMPThread());
|
||||
|
||||
// Abort any pending decrypt jobs, to awaken any clients waiting on a job.
|
||||
for (size_t i = 0; i < mDecryptionJobs.Length(); i++) {
|
||||
DecryptJob* job = mDecryptionJobs[i];
|
||||
job->mClient->Decrypted(NS_ERROR_ABORT, nullptr);
|
||||
}
|
||||
mDecryptionJobs.Clear();
|
||||
|
||||
if (mCDM) {
|
||||
mCDM->Close();
|
||||
mCDM = nullptr;
|
||||
|
@ -487,11 +495,14 @@ CDMProxy::gmp_Decrypted(uint32_t aId,
|
|||
if (aDecryptedData.Length() != job->mSample->size) {
|
||||
NS_WARNING("CDM returned incorrect number of decrypted bytes");
|
||||
}
|
||||
PodCopy(job->mSample->data,
|
||||
aDecryptedData.Elements(),
|
||||
std::min<size_t>(aDecryptedData.Length(), job->mSample->size));
|
||||
nsresult rv = GMP_SUCCEEDED(aResult) ? NS_OK : NS_ERROR_FAILURE;
|
||||
job->mClient->Decrypted(rv, job->mSample.forget());
|
||||
if (GMP_SUCCEEDED(aResult)) {
|
||||
PodCopy(job->mSample->data,
|
||||
aDecryptedData.Elements(),
|
||||
std::min<size_t>(aDecryptedData.Length(), job->mSample->size));
|
||||
job->mClient->Decrypted(NS_OK, job->mSample.forget());
|
||||
} else {
|
||||
job->mClient->Decrypted(NS_ERROR_FAILURE, nullptr);
|
||||
}
|
||||
mDecryptionJobs.RemoveElementAt(i);
|
||||
return;
|
||||
} else {
|
||||
|
|
|
@ -317,6 +317,8 @@ MP4Reader::ReadMetadata(MediaInfo* aInfo,
|
|||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
mInfo.mAudio.mHasAudio = mAudio.mActive = mDemuxer->HasValidAudio();
|
||||
|
||||
{
|
||||
ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
|
||||
mIsEncrypted = mDemuxer->Crypto().valid;
|
||||
|
@ -377,9 +379,8 @@ MP4Reader::ReadMetadata(MediaInfo* aInfo,
|
|||
NS_ENSURE_TRUE(mPlatform, NS_ERROR_FAILURE);
|
||||
}
|
||||
|
||||
if (mDemuxer->HasValidAudio()) {
|
||||
if (HasAudio()) {
|
||||
const AudioDecoderConfig& audio = mDemuxer->AudioConfig();
|
||||
mInfo.mAudio.mHasAudio = mAudio.mActive = true;
|
||||
if (mInfo.mAudio.mHasAudio && !IsSupportedAudioMimeType(audio.mime_type)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
@ -595,6 +596,8 @@ MP4Reader::Output(TrackType aTrack, MediaData* aSample)
|
|||
// Don't accept output while we're flushing.
|
||||
MonitorAutoLock mon(data.mMonitor);
|
||||
if (data.mIsFlushing) {
|
||||
delete aSample;
|
||||
LOG("MP4Reader produced output while flushing, discarding.");
|
||||
mon.NotifyAll();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ AppleATDecoder::AppleATDecoder(const mp4_demuxer::AudioDecoderConfig& aConfig,
|
|||
|
||||
AppleATDecoder::~AppleATDecoder()
|
||||
{
|
||||
MOZ_COUNT_DTOR(AppleATDecoer);
|
||||
MOZ_COUNT_DTOR(AppleATDecoder);
|
||||
MOZ_ASSERT(!mConverter);
|
||||
MOZ_ASSERT(!mStream);
|
||||
}
|
||||
|
@ -129,6 +129,7 @@ nsresult
|
|||
AppleATDecoder::Flush()
|
||||
{
|
||||
LOG("Flushing AudioToolbox AAC decoder");
|
||||
mTaskQueue->Flush();
|
||||
OSStatus rv = AudioConverterReset(mConverter);
|
||||
if (rv) {
|
||||
LOG("Error %d resetting AudioConverter", rv);
|
||||
|
|
|
@ -111,6 +111,7 @@ AppleVTDecoder::Input(mp4_demuxer::MP4Sample* aSample)
|
|||
nsresult
|
||||
AppleVTDecoder::Flush()
|
||||
{
|
||||
mTaskQueue->Flush();
|
||||
nsresult rv = WaitForAsynchronousFrames();
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG("AppleVTDecoder::Drain failed waiting for platform decoder.");
|
||||
|
|
|
@ -83,7 +83,7 @@ public:
|
|||
mp4_demuxer::MP4Sample* aSample) MOZ_OVERRIDE {
|
||||
if (NS_FAILED(aResult)) {
|
||||
mDecryptor->mCallback->Error();
|
||||
delete aSample;
|
||||
MOZ_ASSERT(!aSample);
|
||||
} else {
|
||||
RefPtr<nsIRunnable> task;
|
||||
task = NS_NewRunnableMethodWithArg<MP4Sample*>(mDecryptor,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include "AsyncEventRunner.h"
|
||||
#include "DecoderTraits.h"
|
||||
#include "MediaSourceUtils.h"
|
||||
#include "SourceBuffer.h"
|
||||
#include "SourceBufferList.h"
|
||||
#include "mozilla/ErrorResult.h"
|
||||
|
@ -23,7 +24,7 @@
|
|||
#include "nsIEventTarget.h"
|
||||
#include "nsIRunnable.h"
|
||||
#include "nsPIDOMWindow.h"
|
||||
#include "nsStringGlue.h"
|
||||
#include "nsString.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "prlog.h"
|
||||
|
||||
|
@ -333,37 +334,42 @@ MediaSource::Detach()
|
|||
void
|
||||
MediaSource::GetBuffered(TimeRanges* aBuffered)
|
||||
{
|
||||
MOZ_ASSERT(aBuffered->Length() == 0);
|
||||
if (mActiveSourceBuffers->IsEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
nsTArray<nsRefPtr<TimeRanges>> ranges;
|
||||
double highestEndTime = 0;
|
||||
|
||||
nsTArray<nsRefPtr<TimeRanges>> activeRanges;
|
||||
for (uint32_t i = 0; i < mActiveSourceBuffers->Length(); ++i) {
|
||||
bool found;
|
||||
SourceBuffer* sourceBuffer = mActiveSourceBuffers->IndexedGetter(i, found);
|
||||
|
||||
ErrorResult dummy;
|
||||
*ranges.AppendElement() = sourceBuffer->GetBuffered(dummy);
|
||||
*activeRanges.AppendElement() = sourceBuffer->GetBuffered(dummy);
|
||||
|
||||
highestEndTime = std::max(highestEndTime, activeRanges.LastElement()->GetEndTime());
|
||||
}
|
||||
|
||||
double highestEndTime = mActiveSourceBuffers->GetHighestBufferedEndTime();
|
||||
if (highestEndTime <= 0) {
|
||||
return;
|
||||
}
|
||||
TimeRanges* intersectionRanges = aBuffered;
|
||||
intersectionRanges->Add(0, highestEndTime);
|
||||
|
||||
MOZ_ASSERT(aBuffered->Length() == 0);
|
||||
aBuffered->Add(0, highestEndTime);
|
||||
for (uint32_t i = 0; i < activeRanges.Length(); ++i) {
|
||||
TimeRanges* sourceRanges = activeRanges[i];
|
||||
|
||||
for (uint32_t i = 0; i < ranges.Length(); ++i) {
|
||||
if (mReadyState == MediaSourceReadyState::Ended) {
|
||||
ranges[i]->Add(ranges[i]->GetEndTime(), highestEndTime);
|
||||
// Set the end time on the last range to highestEndTime by adding a
|
||||
// new range spanning the current end time to highestEndTime, which
|
||||
// Normalize() will then merge with the old last range.
|
||||
sourceRanges->Add(sourceRanges->GetEndTime(), highestEndTime);
|
||||
sourceRanges->Normalize();
|
||||
}
|
||||
|
||||
aBuffered->Intersection(ranges[i]);
|
||||
intersectionRanges->Intersection(sourceRanges);
|
||||
}
|
||||
|
||||
MSE_DEBUG("MediaSource(%p)::GetBuffered start=%f end=%f length=%u",
|
||||
this, aBuffered->GetStartTime(), aBuffered->GetEndTime(), aBuffered->Length());
|
||||
MSE_DEBUG("MediaSource(%p)::GetBuffered ranges=%s", this, DumpTimeRanges(intersectionRanges).get());
|
||||
}
|
||||
|
||||
MediaSource::MediaSource(nsPIDOMWindow* aWindow)
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "MediaSource.h"
|
||||
#include "MediaSourceReader.h"
|
||||
#include "MediaSourceResource.h"
|
||||
#include "MediaSourceUtils.h"
|
||||
|
||||
#ifdef PR_LOGGING
|
||||
extern PRLogModuleInfo* GetMediaSourceLog();
|
||||
|
@ -59,14 +60,12 @@ MediaSourceDecoder::Load(nsIStreamListener**, MediaDecoder*)
|
|||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
|
||||
nsresult rv = mDecoderStateMachine->Init(nullptr);
|
||||
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
SetStateMachineParameters();
|
||||
|
||||
return rv;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
|
@ -82,8 +81,7 @@ MediaSourceDecoder::GetSeekable(dom::TimeRanges* aSeekable)
|
|||
} else {
|
||||
aSeekable->Add(0, duration);
|
||||
}
|
||||
MSE_DEBUG("MediaSourceDecoder(%p)::GetSeekable startTime=%f endTime=%f",
|
||||
this, aSeekable->GetStartTime(), aSeekable->GetEndTime());
|
||||
MSE_DEBUG("MediaSourceDecoder(%p)::GetSeekable ranges=%s", this, DumpTimeRanges(aSeekable).get());
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -110,7 +108,8 @@ MediaSourceDecoder::DetachMediaSource()
|
|||
already_AddRefed<SubBufferDecoder>
|
||||
MediaSourceDecoder::CreateSubDecoder(const nsACString& aType)
|
||||
{
|
||||
return mReader->CreateSubDecoder(aType, this);
|
||||
MOZ_ASSERT(mReader);
|
||||
return mReader->CreateSubDecoder(aType);
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "MediaDecoderOwner.h"
|
||||
#include "MediaSource.h"
|
||||
#include "MediaSourceDecoder.h"
|
||||
#include "MediaSourceUtils.h"
|
||||
#include "SubBufferDecoder.h"
|
||||
|
||||
#ifdef MOZ_FMP4
|
||||
|
@ -36,9 +37,8 @@ namespace mozilla {
|
|||
MediaSourceReader::MediaSourceReader(MediaSourceDecoder* aDecoder, dom::MediaSource* aSource)
|
||||
: MediaDecoderReader(aDecoder)
|
||||
, mTimeThreshold(-1)
|
||||
, mDropAudioBeforeThreshold(false)
|
||||
, mDropVideoBeforeThreshold(false)
|
||||
, mActiveVideoDecoder(-1)
|
||||
, mActiveAudioDecoder(-1)
|
||||
, mMediaSource(aSource)
|
||||
{
|
||||
}
|
||||
|
@ -52,41 +52,60 @@ MediaSourceReader::IsWaitingMediaResources()
|
|||
void
|
||||
MediaSourceReader::RequestAudioData()
|
||||
{
|
||||
if (!GetAudioReader()) {
|
||||
if (!mAudioReader) {
|
||||
MSE_DEBUG("MediaSourceReader(%p)::RequestAudioData called with no audio reader", this);
|
||||
MOZ_ASSERT(mPendingDecoders.IsEmpty());
|
||||
GetCallback()->OnDecodeError();
|
||||
return;
|
||||
}
|
||||
GetAudioReader()->RequestAudioData();
|
||||
SwitchReaders(SWITCH_OPTIONAL);
|
||||
mAudioReader->RequestAudioData();
|
||||
}
|
||||
|
||||
void
|
||||
MediaSourceReader::OnAudioDecoded(AudioData* aSample)
|
||||
{
|
||||
if (mDropAudioBeforeThreshold) {
|
||||
if (aSample->mTime < mTimeThreshold) {
|
||||
MSE_DEBUG("MediaSourceReader(%p)::OnAudioDecoded mTime=%lld < mTimeThreshold=%lld",
|
||||
this, aSample->mTime, mTimeThreshold);
|
||||
delete aSample;
|
||||
mAudioReader->RequestAudioData();
|
||||
return;
|
||||
}
|
||||
mDropAudioBeforeThreshold = false;
|
||||
}
|
||||
GetCallback()->OnAudioDecoded(aSample);
|
||||
}
|
||||
|
||||
void
|
||||
MediaSourceReader::OnAudioEOS()
|
||||
{
|
||||
MSE_DEBUG("MediaSourceReader(%p)::OnAudioEOS %d (%p) EOS (readers=%u)",
|
||||
this, mActiveAudioDecoder, mDecoders[mActiveAudioDecoder].get(), mDecoders.Length());
|
||||
GetCallback()->OnAudioEOS();
|
||||
MSE_DEBUG("MediaSourceReader(%p)::OnAudioEOS reader=%p (readers=%u)",
|
||||
this, mAudioReader.get(), mDecoders.Length());
|
||||
if (SwitchReaders(SWITCH_FORCED)) {
|
||||
// Success! Resume decoding with next audio decoder.
|
||||
RequestAudioData();
|
||||
} else {
|
||||
// End of stream.
|
||||
MSE_DEBUG("MediaSourceReader(%p)::OnAudioEOS reader=%p EOS (readers=%u)",
|
||||
this, mAudioReader.get(), mDecoders.Length());
|
||||
GetCallback()->OnAudioEOS();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MediaSourceReader::RequestVideoData(bool aSkipToNextKeyframe, int64_t aTimeThreshold)
|
||||
{
|
||||
if (!GetVideoReader()) {
|
||||
if (!mVideoReader) {
|
||||
MSE_DEBUG("MediaSourceReader(%p)::RequestVideoData called with no video reader", this);
|
||||
MOZ_ASSERT(mPendingDecoders.IsEmpty());
|
||||
GetCallback()->OnDecodeError();
|
||||
return;
|
||||
}
|
||||
mTimeThreshold = aTimeThreshold;
|
||||
SwitchVideoReaders(SWITCH_OPTIONAL);
|
||||
GetVideoReader()->RequestVideoData(aSkipToNextKeyframe, aTimeThreshold);
|
||||
SwitchReaders(SWITCH_OPTIONAL);
|
||||
mVideoReader->RequestVideoData(aSkipToNextKeyframe, aTimeThreshold);
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -97,7 +116,7 @@ MediaSourceReader::OnVideoDecoded(VideoData* aSample)
|
|||
MSE_DEBUG("MediaSourceReader(%p)::OnVideoDecoded mTime=%lld < mTimeThreshold=%lld",
|
||||
this, aSample->mTime, mTimeThreshold);
|
||||
delete aSample;
|
||||
GetVideoReader()->RequestVideoData(false, mTimeThreshold);
|
||||
mVideoReader->RequestVideoData(false, mTimeThreshold);
|
||||
return;
|
||||
}
|
||||
mDropVideoBeforeThreshold = false;
|
||||
|
@ -109,15 +128,15 @@ void
|
|||
MediaSourceReader::OnVideoEOS()
|
||||
{
|
||||
// End of stream. See if we can switch to another video decoder.
|
||||
MSE_DEBUG("MediaSourceReader(%p)::OnVideoEOS %d (%p) (readers=%u)",
|
||||
this, mActiveVideoDecoder, mDecoders[mActiveVideoDecoder].get(), mDecoders.Length());
|
||||
if (SwitchVideoReaders(SWITCH_FORCED)) {
|
||||
MSE_DEBUG("MediaSourceReader(%p)::OnVideoEOS reader=%p (readers=%u)",
|
||||
this, mVideoReader.get(), mDecoders.Length());
|
||||
if (SwitchReaders(SWITCH_FORCED)) {
|
||||
// Success! Resume decoding with next video decoder.
|
||||
RequestVideoData(false, mTimeThreshold);
|
||||
} else {
|
||||
// End of stream.
|
||||
MSE_DEBUG("MediaSourceReader(%p)::OnVideoEOS %d (%p) EOS (readers=%u)",
|
||||
this, mActiveVideoDecoder, mDecoders[mActiveVideoDecoder].get(), mDecoders.Length());
|
||||
MSE_DEBUG("MediaSourceReader(%p)::OnVideoEOS reader=%p EOS (readers=%u)",
|
||||
this, mVideoReader.get(), mDecoders.Length());
|
||||
GetCallback()->OnVideoEOS();
|
||||
}
|
||||
}
|
||||
|
@ -147,63 +166,86 @@ MediaSourceReader::BreakCycles()
|
|||
}
|
||||
|
||||
bool
|
||||
MediaSourceReader::SwitchVideoReaders(SwitchType aType)
|
||||
MediaSourceReader::SwitchAudioReader(MediaDecoderReader* aTargetReader)
|
||||
{
|
||||
if (aTargetReader == mAudioReader) {
|
||||
return false;
|
||||
}
|
||||
if (mAudioReader) {
|
||||
AudioInfo targetInfo = aTargetReader->GetMediaInfo().mAudio;
|
||||
AudioInfo currentInfo = mAudioReader->GetMediaInfo().mAudio;
|
||||
|
||||
// TODO: We can't handle switching audio formats yet.
|
||||
if (currentInfo.mRate != targetInfo.mRate ||
|
||||
currentInfo.mChannels != targetInfo.mChannels) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mAudioReader->SetIdle();
|
||||
}
|
||||
mAudioReader = aTargetReader;
|
||||
mDropAudioBeforeThreshold = true;
|
||||
MSE_DEBUG("MediaDecoderReader(%p)::SwitchReaders(%p) switching audio reader",
|
||||
this, mAudioReader.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MediaSourceReader::SwitchVideoReader(MediaDecoderReader* aTargetReader)
|
||||
{
|
||||
if (aTargetReader == mVideoReader) {
|
||||
return false;
|
||||
}
|
||||
if (mVideoReader) {
|
||||
mVideoReader->SetIdle();
|
||||
}
|
||||
mVideoReader = aTargetReader;
|
||||
mDropVideoBeforeThreshold = true;
|
||||
MSE_DEBUG("MediaDecoderReader(%p)::SwitchVideoReader(%p) switching video reader",
|
||||
this, mVideoReader.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MediaSourceReader::SwitchReaders(SwitchType aType)
|
||||
{
|
||||
ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
|
||||
MOZ_ASSERT(mActiveVideoDecoder != -1);
|
||||
|
||||
InitializePendingDecoders();
|
||||
|
||||
for (uint32_t i = mActiveVideoDecoder + 1; i < mDecoders.Length(); ++i) {
|
||||
bool didSwitch = false;
|
||||
double decodeTarget = double(mTimeThreshold) / USECS_PER_S;
|
||||
|
||||
for (uint32_t i = 0; i < mDecoders.Length(); ++i) {
|
||||
SubBufferDecoder* decoder = mDecoders[i];
|
||||
const MediaInfo& info = decoder->GetReader()->GetMediaInfo();
|
||||
|
||||
nsRefPtr<dom::TimeRanges> ranges = new dom::TimeRanges();
|
||||
decoder->GetBuffered(ranges);
|
||||
|
||||
MSE_DEBUGV("MediaDecoderReader(%p)::SwitchVideoReaders(%d) decoder=%u (%p) discarded=%d"
|
||||
" hasVideo=%d timeThreshold=%f startTime=%f endTime=%f length=%u",
|
||||
MSE_DEBUGV("MediaDecoderReader(%p)::SwitchReaders(%d) decoder=%u (%p) discarded=%d"
|
||||
" reader=%p audioReader=%p videoReader=%p"
|
||||
" hasAudio=%d hasVideo=%d decodeTarget=%f ranges=%s",
|
||||
this, aType, i, decoder, decoder->IsDiscarded(),
|
||||
decoder->GetReader()->GetMediaInfo().HasVideo(),
|
||||
double(mTimeThreshold) / USECS_PER_S,
|
||||
ranges->GetStartTime(), ranges->GetEndTime(), ranges->Length());
|
||||
decoder->GetReader(), mAudioReader.get(), mVideoReader.get(),
|
||||
info.HasAudio(), info.HasVideo(), decodeTarget,
|
||||
DumpTimeRanges(ranges).get());
|
||||
|
||||
if (decoder->IsDiscarded() ||
|
||||
!decoder->GetReader()->GetMediaInfo().HasVideo() ||
|
||||
ranges->Length() == 0) {
|
||||
if (decoder->IsDiscarded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (aType == SWITCH_FORCED ||
|
||||
ranges->Find(double(mTimeThreshold) / USECS_PER_S) != dom::TimeRanges::NoIndex) {
|
||||
GetVideoReader()->SetIdle();
|
||||
|
||||
mActiveVideoDecoder = i;
|
||||
mDropVideoBeforeThreshold = true;
|
||||
MSE_DEBUG("MediaDecoderReader(%p)::SwitchVideoReaders(%d) switching to %d (%p)",
|
||||
this, aType, mActiveVideoDecoder, mDecoders[mActiveVideoDecoder].get());
|
||||
return true;
|
||||
if (aType == SWITCH_FORCED || ranges->Find(decodeTarget) != dom::TimeRanges::NoIndex) {
|
||||
if (info.HasAudio()) {
|
||||
didSwitch |= SwitchAudioReader(mDecoders[i]->GetReader());
|
||||
}
|
||||
if (info.HasVideo()) {
|
||||
didSwitch |= SwitchVideoReader(mDecoders[i]->GetReader());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
MediaDecoderReader*
|
||||
MediaSourceReader::GetAudioReader()
|
||||
{
|
||||
if (mActiveAudioDecoder == -1) {
|
||||
return nullptr;
|
||||
}
|
||||
return mDecoders[mActiveAudioDecoder]->GetReader();
|
||||
}
|
||||
|
||||
MediaDecoderReader*
|
||||
MediaSourceReader::GetVideoReader()
|
||||
{
|
||||
if (mActiveVideoDecoder == -1) {
|
||||
return nullptr;
|
||||
}
|
||||
return mDecoders[mActiveVideoDecoder]->GetReader();
|
||||
return didSwitch;
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -298,12 +340,11 @@ CreateReaderForType(const nsACString& aType, AbstractMediaDecoder* aDecoder)
|
|||
}
|
||||
|
||||
already_AddRefed<SubBufferDecoder>
|
||||
MediaSourceReader::CreateSubDecoder(const nsACString& aType,
|
||||
MediaSourceDecoder* aParentDecoder)
|
||||
MediaSourceReader::CreateSubDecoder(const nsACString& aType)
|
||||
{
|
||||
// XXX: Why/when is mDecoder null here, since it should be equal to aParentDecoder?!
|
||||
MOZ_ASSERT(GetTaskQueue());
|
||||
nsRefPtr<SubBufferDecoder> decoder =
|
||||
new SubBufferDecoder(new SourceBufferResource(nullptr, aType), aParentDecoder);
|
||||
new SubBufferDecoder(new SourceBufferResource(nullptr, aType), mDecoder);
|
||||
nsRefPtr<MediaDecoderReader> reader(CreateReaderForType(aType, decoder));
|
||||
if (!reader) {
|
||||
return nullptr;
|
||||
|
@ -316,7 +357,7 @@ MediaSourceReader::CreateSubDecoder(const nsACString& aType,
|
|||
reader->SetCallback(callback);
|
||||
reader->SetTaskQueue(GetTaskQueue());
|
||||
reader->Init(nullptr);
|
||||
ReentrantMonitorAutoEnter mon(aParentDecoder->GetReentrantMonitor());
|
||||
ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
|
||||
MSE_DEBUG("MediaSourceReader(%p)::CreateSubDecoder subdecoder %p subreader %p",
|
||||
this, decoder.get(), reader.get());
|
||||
decoder->SetReader(reader);
|
||||
|
@ -379,11 +420,10 @@ MediaSourceReader::Seek(int64_t aTime, int64_t aStartTime, int64_t aEndTime,
|
|||
// This is a workaround for our lack of async functionality in the
|
||||
// MediaDecoderStateMachine. Bug 979104 implements what we need and
|
||||
// we'll remove this for an async approach based on that in bug XXXXXXX.
|
||||
while (!DecodersContainTime(target)
|
||||
&& !IsShutdown()) {
|
||||
while (!DecodersContainTime(target) && !IsShutdown()) {
|
||||
MSE_DEBUG("MediaSourceReader(%p)::Seek waiting for target=%f", this, target);
|
||||
mMediaSource->WaitForData();
|
||||
SwitchVideoReaders(SWITCH_FORCED);
|
||||
SwitchReaders(SWITCH_FORCED);
|
||||
}
|
||||
|
||||
if (IsShutdown()) {
|
||||
|
@ -391,14 +431,14 @@ MediaSourceReader::Seek(int64_t aTime, int64_t aStartTime, int64_t aEndTime,
|
|||
}
|
||||
|
||||
ResetDecode();
|
||||
if (GetAudioReader()) {
|
||||
nsresult rv = GetAudioReader()->Seek(aTime, aStartTime, aEndTime, aCurrentTime);
|
||||
if (mAudioReader) {
|
||||
nsresult rv = mAudioReader->Seek(aTime, aStartTime, aEndTime, aCurrentTime);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
if (GetVideoReader()) {
|
||||
nsresult rv = GetVideoReader()->Seek(aTime, aStartTime, aEndTime, aCurrentTime);
|
||||
if (mVideoReader) {
|
||||
nsresult rv = mVideoReader->Seek(aTime, aStartTime, aEndTime, aCurrentTime);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
@ -427,20 +467,20 @@ MediaSourceReader::ReadMetadata(MediaInfo* aInfo, MetadataTags** aTags)
|
|||
MediaInfo mi = reader->GetMediaInfo();
|
||||
|
||||
if (mi.HasVideo() && !mInfo.HasVideo()) {
|
||||
MOZ_ASSERT(mActiveVideoDecoder == -1);
|
||||
mActiveVideoDecoder = i;
|
||||
MOZ_ASSERT(!mVideoReader);
|
||||
mVideoReader = reader;
|
||||
mInfo.mVideo = mi.mVideo;
|
||||
maxDuration = std::max(maxDuration, mDecoders[i]->GetMediaDuration());
|
||||
MSE_DEBUG("MediaSourceReader(%p)::ReadMetadata video decoder=%u maxDuration=%lld",
|
||||
this, i, maxDuration);
|
||||
MSE_DEBUG("MediaSourceReader(%p)::ReadMetadata video reader=%p maxDuration=%lld",
|
||||
this, reader, maxDuration);
|
||||
}
|
||||
if (mi.HasAudio() && !mInfo.HasAudio()) {
|
||||
MOZ_ASSERT(mActiveAudioDecoder == -1);
|
||||
mActiveAudioDecoder = i;
|
||||
MOZ_ASSERT(!mAudioReader);
|
||||
mAudioReader = reader;
|
||||
mInfo.mAudio = mi.mAudio;
|
||||
maxDuration = std::max(maxDuration, mDecoders[i]->GetMediaDuration());
|
||||
MSE_DEBUG("MediaSourceReader(%p)::ReadMetadata audio decoder=%u maxDuration=%lld",
|
||||
this, i, maxDuration);
|
||||
MSE_DEBUG("MediaSourceReader(%p)::ReadMetadata audio reader=%p maxDuration=%lld",
|
||||
this, reader, maxDuration);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
#include "mozilla/ReentrantMonitor.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsError.h"
|
||||
#include "nsStringGlue.h"
|
||||
#include "nsString.h"
|
||||
#include "nsTArray.h"
|
||||
#include "MediaDecoderReader.h"
|
||||
|
||||
|
@ -70,8 +70,7 @@ public:
|
|||
nsresult ReadMetadata(MediaInfo* aInfo, MetadataTags** aTags) MOZ_OVERRIDE;
|
||||
nsresult Seek(int64_t aTime, int64_t aStartTime, int64_t aEndTime,
|
||||
int64_t aCurrentTime) MOZ_OVERRIDE;
|
||||
already_AddRefed<SubBufferDecoder> CreateSubDecoder(const nsACString& aType,
|
||||
MediaSourceDecoder* aParentDecoder);
|
||||
already_AddRefed<SubBufferDecoder> CreateSubDecoder(const nsACString& aType);
|
||||
|
||||
void Shutdown();
|
||||
|
||||
|
@ -94,22 +93,24 @@ private:
|
|||
SWITCH_FORCED
|
||||
};
|
||||
|
||||
bool SwitchVideoReaders(SwitchType aType);
|
||||
bool SwitchReaders(SwitchType aType);
|
||||
|
||||
MediaDecoderReader* GetAudioReader();
|
||||
MediaDecoderReader* GetVideoReader();
|
||||
bool SwitchAudioReader(MediaDecoderReader* aTargetReader);
|
||||
bool SwitchVideoReader(MediaDecoderReader* aTargetReader);
|
||||
|
||||
void SetMediaSourceDuration(double aDuration) ;
|
||||
|
||||
// These are read and written on the decode task queue threads.
|
||||
int64_t mTimeThreshold;
|
||||
bool mDropAudioBeforeThreshold;
|
||||
bool mDropVideoBeforeThreshold;
|
||||
|
||||
nsTArray<nsRefPtr<SubBufferDecoder>> mPendingDecoders;
|
||||
nsTArray<nsRefPtr<SubBufferDecoder>> mDecoders;
|
||||
|
||||
int32_t mActiveVideoDecoder;
|
||||
int32_t mActiveAudioDecoder;
|
||||
nsRefPtr<MediaDecoderReader> mAudioReader;
|
||||
nsRefPtr<MediaDecoderReader> mVideoReader;
|
||||
|
||||
dom::MediaSource* mMediaSource;
|
||||
};
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ public:
|
|||
virtual int64_t Tell() MOZ_OVERRIDE { return -1; }
|
||||
virtual void Pin() MOZ_OVERRIDE {}
|
||||
virtual void Unpin() MOZ_OVERRIDE {}
|
||||
virtual double GetDownloadRate(bool* aIsReliable) MOZ_OVERRIDE { return 0; }
|
||||
virtual double GetDownloadRate(bool* aIsReliable) MOZ_OVERRIDE { *aIsReliable = false; return 0; }
|
||||
virtual int64_t GetLength() MOZ_OVERRIDE { return -1; }
|
||||
virtual int64_t GetNextCachedData(int64_t aOffset) MOZ_OVERRIDE { return aOffset; }
|
||||
virtual int64_t GetCachedDataEnd(int64_t aOffset) MOZ_OVERRIDE { return GetLength(); }
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/* -*- mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
#include "MediaSourceUtils.h"
|
||||
|
||||
#include "prlog.h"
|
||||
#include "mozilla/dom/TimeRanges.h"
|
||||
#include "nsPrintfCString.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
#if defined(PR_LOGGING)
|
||||
nsCString
|
||||
DumpTimeRanges(dom::TimeRanges* aRanges)
|
||||
{
|
||||
nsCString dump;
|
||||
|
||||
dump = "[";
|
||||
|
||||
for (uint32_t i = 0; i < aRanges->Length(); ++i) {
|
||||
if (i > 0) {
|
||||
dump += ", ";
|
||||
}
|
||||
ErrorResult dummy;
|
||||
dump += nsPrintfCString("(%f, %f)", aRanges->Start(i, dummy), aRanges->End(i, dummy));
|
||||
}
|
||||
|
||||
dump += "]";
|
||||
|
||||
return dump;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace mozilla
|
|
@ -0,0 +1,22 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef MOZILLA_MEDIASOURCEUTILS_H_
|
||||
#define MOZILLA_MEDIASOURCEUTILS_H_
|
||||
|
||||
#include "nsString.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
namespace dom {
|
||||
class TimeRanges;
|
||||
} // namespace dom
|
||||
|
||||
nsCString DumpTimeRanges(dom::TimeRanges* aRanges);
|
||||
|
||||
} // namespace mozilla
|
||||
|
||||
#endif /* MOZILLA_MEDIASOURCEUTILS_H_ */
|
|
@ -0,0 +1,178 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef MOZILLA_RESOURCEQUEUE_H_
|
||||
#define MOZILLA_RESOURCEQUEUE_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include "nsDeque.h"
|
||||
#include "nsTArray.h"
|
||||
#include "prlog.h"
|
||||
|
||||
#ifdef PR_LOGGING
|
||||
extern PRLogModuleInfo* GetSourceBufferResourceLog();
|
||||
|
||||
#define SBR_DEBUG(...) PR_LOG(GetSourceBufferResourceLog(), PR_LOG_DEBUG, (__VA_ARGS__))
|
||||
#define SBR_DEBUGV(...) PR_LOG(GetSourceBufferResourceLog(), PR_LOG_DEBUG+1, (__VA_ARGS__))
|
||||
#else
|
||||
#define SBR_DEBUG(...)
|
||||
#define SBR_DEBUGV(...)
|
||||
#endif
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
// A SourceBufferResource has a queue containing the data that is appended
|
||||
// to it. The queue holds instances of ResourceItem which is an array of the
|
||||
// bytes. Appending data to the SourceBufferResource pushes this onto the
|
||||
// queue.
|
||||
|
||||
// Data is evicted once it reaches a size threshold. This pops the items off
|
||||
// the front of the queue and deletes it. If an eviction happens then the
|
||||
// MediaSource is notified (done in SourceBuffer::AppendData) which then
|
||||
// requests all SourceBuffers to evict data up to approximately the same
|
||||
// timepoint.
|
||||
|
||||
struct ResourceItem {
|
||||
ResourceItem(const uint8_t* aData, uint32_t aSize) {
|
||||
mData.AppendElements(aData, aSize);
|
||||
}
|
||||
|
||||
size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
|
||||
// size including this
|
||||
size_t size = aMallocSizeOf(this);
|
||||
|
||||
// size excluding this
|
||||
size += mData.SizeOfExcludingThis(aMallocSizeOf);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
nsTArray<uint8_t> mData;
|
||||
};
|
||||
|
||||
class ResourceQueueDeallocator : public nsDequeFunctor {
|
||||
virtual void* operator() (void* aObject) {
|
||||
delete static_cast<ResourceItem*>(aObject);
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
class ResourceQueue : private nsDeque {
|
||||
public:
|
||||
ResourceQueue()
|
||||
: nsDeque(new ResourceQueueDeallocator())
|
||||
, mLogicalLength(0)
|
||||
, mOffset(0)
|
||||
{
|
||||
}
|
||||
|
||||
// Returns the logical byte offset of the start of the data.
|
||||
uint64_t GetOffset() {
|
||||
return mOffset;
|
||||
}
|
||||
|
||||
// Returns the length of all items in the queue plus the offset.
|
||||
// This is the logical length of the resource.
|
||||
uint64_t GetLength() {
|
||||
return mLogicalLength;
|
||||
}
|
||||
|
||||
// Copies aCount bytes from aOffset in the queue into aDest.
|
||||
void CopyData(uint64_t aOffset, uint32_t aCount, char* aDest) {
|
||||
uint32_t offset = 0;
|
||||
uint32_t start = GetAtOffset(aOffset, &offset);
|
||||
uint32_t end = std::min(GetAtOffset(aOffset + aCount, nullptr) + 1, uint32_t(GetSize()));
|
||||
for (uint32_t i = start; i < end; ++i) {
|
||||
ResourceItem* item = ResourceAt(i);
|
||||
uint32_t bytes = std::min(aCount, uint32_t(item->mData.Length() - offset));
|
||||
if (bytes != 0) {
|
||||
memcpy(aDest, &item->mData[offset], bytes);
|
||||
offset = 0;
|
||||
aCount -= bytes;
|
||||
aDest += bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppendItem(const uint8_t* aData, uint32_t aLength) {
|
||||
mLogicalLength += aLength;
|
||||
Push(new ResourceItem(aData, aLength));
|
||||
}
|
||||
|
||||
// Evict data in queue if the total queue size is greater than
|
||||
// aThreshold past the offset. Returns true if some data was
|
||||
// actually evicted.
|
||||
bool Evict(uint64_t aOffset, uint32_t aThreshold) {
|
||||
bool evicted = false;
|
||||
while (GetLength() - mOffset > aThreshold) {
|
||||
ResourceItem* item = ResourceAt(0);
|
||||
if (item->mData.Length() + mOffset > aOffset) {
|
||||
break;
|
||||
}
|
||||
mOffset += item->mData.Length();
|
||||
SBR_DEBUGV("ResourceQueue(%p)::Evict(%llu, %u) removed chunk length=%u",
|
||||
this, aOffset, aThreshold, item->mData.Length());
|
||||
delete PopFront();
|
||||
evicted = true;
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
|
||||
// Calculate the size of the internal deque.
|
||||
size_t size = nsDeque::SizeOfExcludingThis(aMallocSizeOf);
|
||||
|
||||
// Sum the ResourceItems.
|
||||
for (uint32_t i = 0; i < uint32_t(GetSize()); ++i) {
|
||||
const ResourceItem* item = ResourceAt(i);
|
||||
size += item->SizeOfIncludingThis(aMallocSizeOf);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private:
|
||||
ResourceItem* ResourceAt(uint32_t aIndex) const {
|
||||
return static_cast<ResourceItem*>(ObjectAt(aIndex));
|
||||
}
|
||||
|
||||
// Returns the index of the resource that contains the given
|
||||
// logical offset. aResourceOffset will contain the offset into
|
||||
// the resource at the given index returned if it is not null. If
|
||||
// no such resource exists, returns GetSize() and aOffset is
|
||||
// untouched.
|
||||
uint32_t GetAtOffset(uint64_t aOffset, uint32_t *aResourceOffset) {
|
||||
MOZ_ASSERT(aOffset >= mOffset);
|
||||
uint64_t offset = mOffset;
|
||||
for (uint32_t i = 0; i < uint32_t(GetSize()); ++i) {
|
||||
ResourceItem* item = ResourceAt(i);
|
||||
// If the item contains the start of the offset we want to
|
||||
// break out of the loop.
|
||||
if (item->mData.Length() + offset > aOffset) {
|
||||
if (aResourceOffset) {
|
||||
*aResourceOffset = aOffset - offset;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
offset += item->mData.Length();
|
||||
}
|
||||
return GetSize();
|
||||
}
|
||||
|
||||
ResourceItem* PopFront() {
|
||||
return static_cast<ResourceItem*>(nsDeque::PopFront());
|
||||
}
|
||||
|
||||
// Logical length of the resource.
|
||||
uint64_t mLogicalLength;
|
||||
|
||||
// Logical offset into the resource of the first element in the queue.
|
||||
uint64_t mOffset;
|
||||
};
|
||||
|
||||
|
||||
} // namespace mozilla
|
||||
#endif /* MOZILLA_RESOURCEQUEUE_H_ */
|
|
@ -9,6 +9,7 @@
|
|||
#include "DecoderTraits.h"
|
||||
#include "MediaDecoder.h"
|
||||
#include "MediaSourceDecoder.h"
|
||||
#include "MediaSourceUtils.h"
|
||||
#include "SourceBufferResource.h"
|
||||
#include "mozilla/Endian.h"
|
||||
#include "mozilla/ErrorResult.h"
|
||||
|
@ -172,6 +173,7 @@ SourceBuffer::GetBuffered(ErrorResult& aRv)
|
|||
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
||||
return nullptr;
|
||||
}
|
||||
double highestEndTime = 0;
|
||||
nsRefPtr<TimeRanges> ranges = new TimeRanges();
|
||||
// TODO: Need to adjust mDecoders so it only tracks active decoders.
|
||||
// Once we have an abstraction for track buffers, this needs to report the
|
||||
|
@ -179,11 +181,19 @@ SourceBuffer::GetBuffered(ErrorResult& aRv)
|
|||
for (uint32_t i = 0; i < mDecoders.Length(); ++i) {
|
||||
nsRefPtr<TimeRanges> r = new TimeRanges();
|
||||
mDecoders[i]->GetBuffered(r);
|
||||
ranges->Union(r);
|
||||
if (r->Length() > 0) {
|
||||
highestEndTime = std::max(highestEndTime, r->GetEndTime());
|
||||
ranges->Union(r);
|
||||
}
|
||||
}
|
||||
ranges->Normalize();
|
||||
MSE_DEBUGV("SourceBuffer(%p)::GetBuffered startTime=%f endTime=%f length=%u",
|
||||
this, ranges->GetStartTime(), ranges->GetEndTime(), ranges->Length());
|
||||
if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
|
||||
// Set the end time on the last range to highestEndTime by adding a
|
||||
// new range spanning the current end time to highestEndTime, which
|
||||
// Normalize() will then merge with the old last range.
|
||||
ranges->Add(ranges->GetEndTime(), highestEndTime);
|
||||
ranges->Normalize();
|
||||
}
|
||||
MSE_DEBUGV("SourceBuffer(%p)::GetBuffered ranges=%s", this, DumpTimeRanges(ranges).get());
|
||||
return ranges.forget();
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
#include "nsCycleCollectionNoteChild.h"
|
||||
#include "nsCycleCollectionParticipant.h"
|
||||
#include "nsISupports.h"
|
||||
#include "nsStringGlue.h"
|
||||
#include "nsString.h"
|
||||
#include "nscore.h"
|
||||
|
||||
class JSObject;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
#include "nsCOMPtr.h"
|
||||
#include "nsIEventTarget.h"
|
||||
#include "nsIRunnable.h"
|
||||
#include "nsStringGlue.h"
|
||||
#include "nsString.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "prlog.h"
|
||||
|
||||
|
@ -144,11 +144,11 @@ double
|
|||
SourceBufferList::GetHighestBufferedEndTime()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
double highestEnd = 0;
|
||||
double highestEndTime = 0;
|
||||
for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
|
||||
highestEnd = std::max(highestEnd, mSourceBuffers[i]->GetBufferedEnd());
|
||||
highestEndTime = std::max(highestEndTime, mSourceBuffers[i]->GetBufferedEnd());
|
||||
}
|
||||
return highestEnd;
|
||||
return highestEndTime;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
#include "nsISeekableStream.h"
|
||||
#include "nsISupportsImpl.h"
|
||||
#include "prenv.h"
|
||||
#include "prlog.h"
|
||||
|
||||
#ifdef PR_LOGGING
|
||||
|
@ -172,7 +171,7 @@ SourceBufferResource::AppendData(const uint8_t* aData, uint32_t aLength)
|
|||
{
|
||||
SBR_DEBUG("SourceBufferResource(%p)::AppendData(aData=%p, aLength=%u)", this, aData, aLength);
|
||||
ReentrantMonitorAutoEnter mon(mMonitor);
|
||||
mInputBuffer.PushBack(new ResourceItem(aData, aLength));
|
||||
mInputBuffer.AppendItem(aData, aLength);
|
||||
mon.NotifyAll();
|
||||
}
|
||||
|
||||
|
|
|
@ -7,17 +7,16 @@
|
|||
#ifndef MOZILLA_SOURCEBUFFERRESOURCE_H_
|
||||
#define MOZILLA_SOURCEBUFFERRESOURCE_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include "MediaCache.h"
|
||||
#include "MediaResource.h"
|
||||
#include "ResourceQueue.h"
|
||||
#include "mozilla/Attributes.h"
|
||||
#include "mozilla/ReentrantMonitor.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsError.h"
|
||||
#include "nsIPrincipal.h"
|
||||
#include "nsStringGlue.h"
|
||||
#include "nsString.h"
|
||||
#include "nsTArray.h"
|
||||
#include "nsDeque.h"
|
||||
#include "nscore.h"
|
||||
|
||||
class nsIStreamListener;
|
||||
|
@ -34,167 +33,9 @@ class SourceBuffer;
|
|||
|
||||
class SourceBufferResource MOZ_FINAL : public MediaResource
|
||||
{
|
||||
private:
|
||||
// A SourceBufferResource has a queue containing the data
|
||||
// that is appended to it. The queue holds instances of
|
||||
// ResourceItem which is an array of the bytes. Appending
|
||||
// data to the SourceBufferResource pushes this onto the
|
||||
// queue. As items are played they are taken off the front
|
||||
// of the queue.
|
||||
// Data is evicted once it reaches a size threshold. This
|
||||
// pops the items off the front of the queue and deletes it.
|
||||
// If an eviction happens then the MediaSource is notified
|
||||
// (done in SourceBuffer::AppendData) which then requests
|
||||
// all SourceBuffers to evict data up to approximately
|
||||
// the same timepoint.
|
||||
struct ResourceItem {
|
||||
ResourceItem(uint8_t const* aData, uint32_t aSize) {
|
||||
mData.AppendElements(aData, aSize);
|
||||
}
|
||||
nsTArray<uint8_t> mData;
|
||||
|
||||
size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
|
||||
// size including this
|
||||
size_t size = aMallocSizeOf(this);
|
||||
|
||||
// size excluding this
|
||||
size += mData.SizeOfExcludingThis(aMallocSizeOf);
|
||||
|
||||
return size;
|
||||
}
|
||||
};
|
||||
|
||||
class ResourceQueueDeallocator : public nsDequeFunctor {
|
||||
virtual void* operator() (void* aObject) {
|
||||
delete static_cast<ResourceItem*>(aObject);
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
class ResourceQueue : private nsDeque {
|
||||
public:
|
||||
ResourceQueue() :
|
||||
nsDeque(new ResourceQueueDeallocator()),
|
||||
mLogicalLength(0),
|
||||
mOffset(0)
|
||||
{
|
||||
}
|
||||
|
||||
// Returns the logical byte offset of the start of the data.
|
||||
inline uint64_t GetOffset() {
|
||||
return mOffset;
|
||||
}
|
||||
|
||||
// Returns the length of all items in the queue plus the offset.
|
||||
// This is the logical length of the resource.
|
||||
inline uint64_t GetLength() {
|
||||
return mLogicalLength;
|
||||
}
|
||||
|
||||
// Copies aCount bytes from aOffset in the queue into aDest.
|
||||
inline void CopyData(uint64_t aOffset, uint32_t aCount, char* aDest) {
|
||||
uint32_t offset = 0;
|
||||
uint32_t start = GetAtOffset(aOffset, &offset);
|
||||
uint32_t end = std::min(GetAtOffset(aOffset + aCount, nullptr) + 1, GetSize());
|
||||
for (uint32_t i = start; i < end; ++i) {
|
||||
ResourceItem* item = ResourceAt(i);
|
||||
uint32_t bytes = std::min(aCount, uint32_t(item->mData.Length() - offset));
|
||||
if (bytes != 0) {
|
||||
memcpy(aDest, &item->mData[offset], bytes);
|
||||
offset = 0;
|
||||
aCount -= bytes;
|
||||
aDest += bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void PushBack(ResourceItem* aItem) {
|
||||
mLogicalLength += aItem->mData.Length();
|
||||
nsDeque::Push(aItem);
|
||||
}
|
||||
|
||||
// Evict data in queue if the total queue size is greater than
|
||||
// aThreshold past the offset. Returns true if some data was
|
||||
// actually evicted.
|
||||
inline bool Evict(uint64_t aOffset, uint32_t aThreshold) {
|
||||
bool evicted = false;
|
||||
while (GetLength() - mOffset > aThreshold) {
|
||||
ResourceItem* item = ResourceAt(0);
|
||||
if (item->mData.Length() + mOffset > aOffset) {
|
||||
break;
|
||||
}
|
||||
mOffset += item->mData.Length();
|
||||
delete PopFront();
|
||||
evicted = true;
|
||||
}
|
||||
return evicted;
|
||||
}
|
||||
|
||||
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
|
||||
// Calculate the size of the internal deque.
|
||||
size_t size = nsDeque::SizeOfExcludingThis(aMallocSizeOf);
|
||||
|
||||
// Sum the ResourceItems.
|
||||
for (int32_t i = 0; i < nsDeque::GetSize(); ++i) {
|
||||
const ResourceItem* item =
|
||||
static_cast<const ResourceItem*>(nsDeque::ObjectAt(i));
|
||||
size += item->SizeOfIncludingThis(aMallocSizeOf);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private:
|
||||
// Returns the number of items in the queue
|
||||
inline uint32_t GetSize() {
|
||||
return nsDeque::GetSize();
|
||||
}
|
||||
|
||||
inline ResourceItem* ResourceAt(uint32_t aIndex) {
|
||||
return static_cast<ResourceItem*>(nsDeque::ObjectAt(aIndex));
|
||||
}
|
||||
|
||||
// Returns the index of the resource that contains the given
|
||||
// logical offset. aResourceOffset will contain the offset into
|
||||
// the resource at the given index returned if it is not null. If
|
||||
// no such resource exists, returns GetSize() and aOffset is
|
||||
// untouched.
|
||||
inline uint32_t GetAtOffset(uint64_t aOffset, uint32_t *aResourceOffset) {
|
||||
MOZ_ASSERT(aOffset >= mOffset);
|
||||
uint64_t offset = mOffset;
|
||||
for (uint32_t i = 0; i < GetSize(); ++i) {
|
||||
ResourceItem* item = ResourceAt(i);
|
||||
// If the item contains the start of the offset we want to
|
||||
// break out of the loop.
|
||||
if (item->mData.Length() + offset > aOffset) {
|
||||
if (aResourceOffset) {
|
||||
*aResourceOffset = aOffset - offset;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
offset += item->mData.Length();
|
||||
}
|
||||
return GetSize();
|
||||
}
|
||||
|
||||
inline ResourceItem* PopFront() {
|
||||
return static_cast<ResourceItem*>(nsDeque::PopFront());
|
||||
}
|
||||
|
||||
// Logical length of the resource.
|
||||
uint64_t mLogicalLength;
|
||||
|
||||
// Logical offset into the resource of the first element in the queue.
|
||||
uint64_t mOffset;
|
||||
};
|
||||
|
||||
public:
|
||||
SourceBufferResource(nsIPrincipal* aPrincipal,
|
||||
const nsACString& aType);
|
||||
protected:
|
||||
~SourceBufferResource();
|
||||
|
||||
public:
|
||||
virtual nsresult Close() MOZ_OVERRIDE;
|
||||
virtual void Suspend(bool aCloseImmediately) MOZ_OVERRIDE {}
|
||||
virtual void Resume() MOZ_OVERRIDE {}
|
||||
|
@ -219,7 +60,7 @@ public:
|
|||
virtual int64_t Tell() MOZ_OVERRIDE { return mOffset; }
|
||||
virtual void Pin() MOZ_OVERRIDE {}
|
||||
virtual void Unpin() MOZ_OVERRIDE {}
|
||||
virtual double GetDownloadRate(bool* aIsReliable) MOZ_OVERRIDE { return 0; }
|
||||
virtual double GetDownloadRate(bool* aIsReliable) MOZ_OVERRIDE { *aIsReliable = false; return 0; }
|
||||
virtual int64_t GetLength() MOZ_OVERRIDE { return mInputBuffer.GetLength(); }
|
||||
virtual int64_t GetNextCachedData(int64_t aOffset) MOZ_OVERRIDE { return GetLength() == aOffset ? -1 : aOffset; }
|
||||
virtual int64_t GetCachedDataEnd(int64_t aOffset) MOZ_OVERRIDE { return GetLength(); }
|
||||
|
@ -272,6 +113,7 @@ public:
|
|||
void EvictBefore(uint64_t aOffset);
|
||||
|
||||
private:
|
||||
~SourceBufferResource();
|
||||
nsresult SeekInternal(int64_t aOffset);
|
||||
|
||||
nsCOMPtr<nsIPrincipal> mPrincipal;
|
||||
|
@ -283,7 +125,7 @@ private:
|
|||
// data is available in mData.
|
||||
mutable ReentrantMonitor mMonitor;
|
||||
|
||||
// The buffer holding resource data is a queue of ResourceItem's.
|
||||
// The buffer holding resource data.
|
||||
ResourceQueue mInputBuffer;
|
||||
|
||||
uint64_t mOffset;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "SubBufferDecoder.h"
|
||||
#include "MediaSourceDecoder.h"
|
||||
#include "AbstractMediaDecoder.h"
|
||||
#include "MediaDecoderReader.h"
|
||||
#include "mozilla/dom/TimeRanges.h"
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ class SubBufferDecoder : public BufferDecoder
|
|||
public:
|
||||
// This class holds a weak pointer to MediaResource. It's the responsibility
|
||||
// of the caller to manage the memory of the MediaResource object.
|
||||
SubBufferDecoder(MediaResource* aResource, MediaSourceDecoder* aParentDecoder)
|
||||
SubBufferDecoder(MediaResource* aResource, AbstractMediaDecoder* aParentDecoder)
|
||||
: BufferDecoder(aResource), mParentDecoder(aParentDecoder), mReader(nullptr)
|
||||
, mMediaDuration(-1), mDiscarded(false)
|
||||
{
|
||||
|
@ -84,7 +84,7 @@ public:
|
|||
bool ContainsTime(double aTime);
|
||||
|
||||
private:
|
||||
MediaSourceDecoder* mParentDecoder;
|
||||
AbstractMediaDecoder* mParentDecoder;
|
||||
nsRefPtr<MediaDecoderReader> mReader;
|
||||
int64_t mMediaDuration;
|
||||
bool mDiscarded;
|
||||
|
|
|
@ -20,6 +20,7 @@ UNIFIED_SOURCES += [
|
|||
'MediaSource.cpp',
|
||||
'MediaSourceDecoder.cpp',
|
||||
'MediaSourceReader.cpp',
|
||||
'MediaSourceUtils.cpp',
|
||||
'SourceBuffer.cpp',
|
||||
'SourceBufferList.cpp',
|
||||
'SourceBufferResource.cpp',
|
||||
|
|
|
@ -69,7 +69,6 @@ public:
|
|||
ogg_packet* PopFront() { return static_cast<ogg_packet*>(nsDeque::PopFront()); }
|
||||
ogg_packet* PeekFront() { return static_cast<ogg_packet*>(nsDeque::PeekFront()); }
|
||||
void PushFront(ogg_packet* aPacket) { nsDeque::PushFront(aPacket); }
|
||||
void PushBack(ogg_packet* aPacket) { nsDeque::PushFront(aPacket); }
|
||||
void Erase() { nsDeque::Erase(); }
|
||||
};
|
||||
|
||||
|
|
|
@ -40,7 +40,8 @@ function getPref(name) {
|
|||
|
||||
var haveMp4 = (getPref("media.windows-media-foundation.enabled") && IsWindowsVistaOrLater()) ||
|
||||
getPref("media.omx.enabled") ||
|
||||
getPref("media.gstreamer.enabled");
|
||||
getPref("media.gstreamer.enabled") ||
|
||||
getPref("media.fragmented-mp4.exposed");
|
||||
// TODO: Add "getPref("media.plugins.enabled")" once MP4 works on Gingerbread.
|
||||
|
||||
check_mp4(document.getElementById('v'), haveMp4);
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
#include "nsNodeInfoManager.h"
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsCCUncollectableMarker.h"
|
||||
#include "nsDOMJSUtils.h" // for GetScriptContextFromJSContext
|
||||
#include "xpcpublic.h"
|
||||
#include "mozilla/dom/BindingUtils.h"
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
interface nsIDocShellTreeItem;
|
||||
|
||||
[scriptable, uuid(6cd89e60-1060-491e-8c31-ce969435ec56)]
|
||||
[scriptable, uuid(05b5b240-1f61-11e4-8c21-0800200c9a66)]
|
||||
interface nsIDocShellTreeOwner : nsISupports
|
||||
{
|
||||
/*
|
||||
|
@ -64,9 +64,6 @@ interface nsIDocShellTreeOwner : nsISupports
|
|||
*/
|
||||
readonly attribute nsIDocShellTreeItem primaryContentShell;
|
||||
|
||||
[implicit_jscontext]
|
||||
readonly attribute jsval contentWindow;
|
||||
|
||||
/*
|
||||
Tells the tree owner to size its window or parent window in such a way
|
||||
that the shell passed along will be the size specified.
|
||||
|
|
|
@ -2424,8 +2424,9 @@ nsGlobalWindow::SetNewDocument(nsIDocument* aDocument,
|
|||
bool thisChrome = IsChromeWindow();
|
||||
|
||||
// Check if we're near the stack limit before we get anywhere near the
|
||||
// transplanting code.
|
||||
JS_CHECK_RECURSION(cx, return NS_ERROR_FAILURE);
|
||||
// transplanting code. We use a conservative check since we'll use a little
|
||||
// more space before we actually hit the critical "can't fail" path.
|
||||
JS_CHECK_RECURSION_CONSERVATIVE(cx, return NS_ERROR_FAILURE);
|
||||
|
||||
nsCOMPtr<WindowStateHolder> wsh = do_QueryInterface(aState);
|
||||
NS_ASSERTION(!aState || wsh, "What kind of weird state are you giving me here?");
|
||||
|
@ -3995,25 +3996,8 @@ nsGlobalWindow::GetContent(JSContext* aCx,
|
|||
return;
|
||||
}
|
||||
|
||||
if (!nsContentUtils::IsCallerChrome() || !IsChromeWindow()) {
|
||||
aError.Throw(NS_ERROR_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Something tries to get .content on a ChromeWindow, try to fetch the CPOW.
|
||||
nsCOMPtr<nsIDocShellTreeOwner> treeOwner = GetTreeOwner();
|
||||
if (!treeOwner) {
|
||||
aError.Throw(NS_ERROR_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
JS::Rooted<JS::Value> val(aCx, JS::NullValue());
|
||||
aError = treeOwner->GetContentWindow(aCx, &val);
|
||||
if (aError.Failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
aRetval.set(val.toObjectOrNull());
|
||||
aRetval.set(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
already_AddRefed<nsIDOMWindow>
|
||||
|
|
|
@ -1741,8 +1741,9 @@ ReparentWrapper(JSContext* aCx, JS::Handle<JSObject*> aObjArg)
|
|||
js::AssertSameCompartment(aCx, aObjArg);
|
||||
|
||||
// Check if we're near the stack limit before we get anywhere near the
|
||||
// transplanting code.
|
||||
JS_CHECK_RECURSION(aCx, return NS_ERROR_FAILURE);
|
||||
// transplanting code. We use a conservative check since we'll use a little
|
||||
// more space before we actually hit the critical "can't fail" path.
|
||||
JS_CHECK_RECURSION_CONSERVATIVE(aCx, return NS_ERROR_FAILURE);
|
||||
|
||||
JS::Rooted<JSObject*> aObj(aCx, aObjArg);
|
||||
const DOMJSClass* domClass = GetDOMClass(aObj);
|
||||
|
|
|
@ -306,8 +306,12 @@ WebGLFramebuffer::Attachment::IsComplete() const
|
|||
if (mAttachmentPoint == LOCAL_GL_DEPTH_ATTACHMENT)
|
||||
return IsValidFBOTextureDepthFormat(webGLFormat);
|
||||
|
||||
if (mAttachmentPoint == LOCAL_GL_DEPTH_STENCIL_ATTACHMENT)
|
||||
if (mAttachmentPoint == LOCAL_GL_STENCIL_ATTACHMENT)
|
||||
return false; // Textures can't have the correct format for stencil buffers
|
||||
|
||||
if (mAttachmentPoint == LOCAL_GL_DEPTH_STENCIL_ATTACHMENT) {
|
||||
return IsValidFBOTextureDepthStencilFormat(webGLFormat);
|
||||
}
|
||||
|
||||
if (mAttachmentPoint >= LOCAL_GL_COLOR_ATTACHMENT0 &&
|
||||
mAttachmentPoint < GLenum(LOCAL_GL_COLOR_ATTACHMENT0 +
|
||||
|
|
|
@ -9,3 +9,6 @@ support-files =
|
|||
|
||||
[test_WebCrypto.html]
|
||||
[test_WebCrypto_ECDH.html]
|
||||
[test_WebCrypto_JWK.html]
|
||||
[test_WebCrypto_PBKDF2.html]
|
||||
[test_WebCrypto_RSA_OAEP.html]
|
||||
|
|
|
@ -999,318 +999,6 @@ TestArray.addTest(
|
|||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Import raw PBKDF2 key",
|
||||
function() {
|
||||
var that = this;
|
||||
var alg = "PBKDF2";
|
||||
var key = new TextEncoder("utf-8").encode("password");
|
||||
|
||||
crypto.subtle.importKey("raw", key, alg, false, ["deriveKey"]).then(
|
||||
complete(that, hasKeyFields),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Import raw PBKDF2 key and derive bits using HMAC-SHA-1",
|
||||
function() {
|
||||
var that = this;
|
||||
var alg = "PBKDF2";
|
||||
var key = tv.pbkdf2_sha1.password;
|
||||
|
||||
function doDerive(x) {
|
||||
console.log("deriving");
|
||||
if (!hasKeyFields(x)) {
|
||||
throw "Invalid key; missing field(s)";
|
||||
}
|
||||
|
||||
var alg = {
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-1",
|
||||
salt: tv.pbkdf2_sha1.salt,
|
||||
iterations: tv.pbkdf2_sha1.iterations
|
||||
};
|
||||
return crypto.subtle.deriveBits(alg, x, tv.pbkdf2_sha1.length);
|
||||
}
|
||||
function fail(x) { console.log("failing"); error(that)(x); }
|
||||
|
||||
crypto.subtle.importKey("raw", key, alg, false, ["deriveKey"])
|
||||
.then( doDerive, fail )
|
||||
.then( memcmp_complete(that, tv.pbkdf2_sha1.derived), fail );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Import raw PBKDF2 key and derive a new key using HMAC-SHA-1",
|
||||
function() {
|
||||
var that = this;
|
||||
var alg = "PBKDF2";
|
||||
var key = tv.pbkdf2_sha1.password;
|
||||
|
||||
function doDerive(x) {
|
||||
console.log("deriving");
|
||||
if (!hasKeyFields(x)) {
|
||||
throw "Invalid key; missing field(s)";
|
||||
}
|
||||
|
||||
var alg = {
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-1",
|
||||
salt: tv.pbkdf2_sha1.salt,
|
||||
iterations: tv.pbkdf2_sha1.iterations
|
||||
};
|
||||
|
||||
var algDerived = {
|
||||
name: "HMAC",
|
||||
hash: {name: "SHA-1"}
|
||||
};
|
||||
|
||||
return crypto.subtle.deriveKey(alg, x, algDerived, false, ["sign", "verify"])
|
||||
.then(function (x) {
|
||||
if (!hasKeyFields(x)) {
|
||||
throw "Invalid key; missing field(s)";
|
||||
}
|
||||
|
||||
if (x.algorithm.length != 512) {
|
||||
throw "Invalid key; incorrect length";
|
||||
}
|
||||
|
||||
return x;
|
||||
});
|
||||
}
|
||||
|
||||
function doSignAndVerify(x) {
|
||||
var data = new Uint8Array(1024);
|
||||
|
||||
return crypto.subtle.sign("HMAC", x, data)
|
||||
.then(function (sig) {
|
||||
return crypto.subtle.verify("HMAC", x, sig, data);
|
||||
});
|
||||
}
|
||||
|
||||
function fail(x) { console.log("failing"); error(that)(x); }
|
||||
|
||||
crypto.subtle.importKey("raw", key, alg, false, ["deriveKey"])
|
||||
.then( doDerive, fail )
|
||||
.then( doSignAndVerify, fail )
|
||||
.then( complete(that), fail );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Import raw PBKDF2 key and derive a new key using HMAC-SHA-1 with custom length",
|
||||
function() {
|
||||
var that = this;
|
||||
|
||||
function doDerive(x) {
|
||||
var alg = {
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-1",
|
||||
salt: tv.pbkdf2_sha1.salt,
|
||||
iterations: tv.pbkdf2_sha1.iterations
|
||||
};
|
||||
|
||||
var algDerived = {name: "HMAC", hash: "SHA-1", length: 128};
|
||||
return crypto.subtle.deriveKey(alg, x, algDerived, false, ["sign"]);
|
||||
}
|
||||
|
||||
var password = crypto.getRandomValues(new Uint8Array(8));
|
||||
crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveKey"])
|
||||
.then(doDerive)
|
||||
.then(complete(that, function (x) {
|
||||
return hasKeyFields(x) && x.algorithm.length == 128;
|
||||
}), error(that));
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
/*TestArray.addTest(
|
||||
"Import raw PBKDF2 key and derive bits using HMAC-SHA-256",
|
||||
function() {
|
||||
var that = this;
|
||||
var alg = "PBKDF2";
|
||||
var key = tv.pbkdf2_sha256.password;
|
||||
|
||||
function doDerive(x) {
|
||||
console.log("deriving");
|
||||
if (!hasKeyFields(x)) {
|
||||
throw "Invalid key; missing field(s)";
|
||||
}
|
||||
|
||||
var alg = {
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-256",
|
||||
salt: tv.pbkdf2_sha256.salt,
|
||||
iterations: tv.pbkdf2_sha256.iterations
|
||||
};
|
||||
return crypto.subtle.deriveBits(alg, x, tv.pbkdf2_sha256.length);
|
||||
}
|
||||
function fail(x) { console.log("failing"); error(that)(x); }
|
||||
|
||||
crypto.subtle.importKey("raw", key, alg, false, ["deriveKey"])
|
||||
.then( doDerive, fail )
|
||||
.then( memcmp_complete(that, tv.pbkdf2_sha256.derived), fail );
|
||||
}
|
||||
);*/
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP encrypt/decrypt round-trip",
|
||||
function () {
|
||||
var that = this;
|
||||
var privKey, pubKey;
|
||||
var alg = {name: "RSA-OAEP", hash: "SHA-1"};
|
||||
|
||||
var privKey, pubKey;
|
||||
function setPriv(x) { privKey = x; }
|
||||
function setPub(x) { pubKey = x; }
|
||||
function doEncrypt() {
|
||||
return crypto.subtle.encrypt(alg, pubKey, tv.rsaoaep.data);
|
||||
}
|
||||
function doDecrypt(x) {
|
||||
return crypto.subtle.decrypt(alg, privKey, x);
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
crypto.subtle.importKey("pkcs8", tv.rsaoaep.pkcs8, alg, false, ['decrypt'])
|
||||
.then(setPriv, error(that)),
|
||||
crypto.subtle.importKey("spki", tv.rsaoaep.spki, alg, false, ['encrypt'])
|
||||
.then(setPub, error(that))
|
||||
]).then(doEncrypt, error(that))
|
||||
.then(doDecrypt, error(that))
|
||||
.then(
|
||||
memcmp_complete(that, tv.rsaoaep.data),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP key generation and encrypt/decrypt round-trip (SHA-256)",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = {
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-256",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01])
|
||||
};
|
||||
|
||||
var privKey, pubKey, data = crypto.getRandomValues(new Uint8Array(128));
|
||||
function setKey(x) { pubKey = x.publicKey; privKey = x.privateKey; }
|
||||
function doEncrypt() {
|
||||
return crypto.subtle.encrypt(alg, pubKey, data);
|
||||
}
|
||||
function doDecrypt(x) {
|
||||
return crypto.subtle.decrypt(alg, privKey, x);
|
||||
}
|
||||
|
||||
crypto.subtle.generateKey(alg, false, ['encrypt', 'decrypt'])
|
||||
.then(setKey, error(that))
|
||||
.then(doEncrypt, error(that))
|
||||
.then(doDecrypt, error(that))
|
||||
.then(
|
||||
memcmp_complete(that, data),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP decryption known answer",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = {name: "RSA-OAEP", hash: "SHA-1"};
|
||||
|
||||
function doDecrypt(x) {
|
||||
return crypto.subtle.decrypt(alg, x, tv.rsaoaep.result);
|
||||
}
|
||||
function fail() { error(that); }
|
||||
|
||||
crypto.subtle.importKey("pkcs8", tv.rsaoaep.pkcs8, alg, false, ['decrypt'])
|
||||
.then( doDecrypt, fail )
|
||||
.then( memcmp_complete(that, tv.rsaoaep.data), fail );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP input data length checks (2048-bit key)",
|
||||
function () {
|
||||
var that = this;
|
||||
var privKey, pubKey;
|
||||
var alg = {
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-1",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01])
|
||||
};
|
||||
|
||||
var privKey, pubKey;
|
||||
function setKey(x) { pubKey = x.publicKey; privKey = x.privateKey; }
|
||||
function doEncrypt(n) {
|
||||
return function () {
|
||||
return crypto.subtle.encrypt(alg, pubKey, new Uint8Array(n));
|
||||
}
|
||||
}
|
||||
|
||||
crypto.subtle.generateKey(alg, false, ['encrypt'])
|
||||
.then(setKey, error(that))
|
||||
.then(doEncrypt(214), error(that))
|
||||
.then(doEncrypt(215), error(that))
|
||||
.then(error(that), complete(that));
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP key import with invalid hash",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = {name: "RSA-OAEP", hash: "SHA-123"};
|
||||
|
||||
crypto.subtle.importKey("pkcs8", tv.rsaoaep.pkcs8, alg, false, ['decrypt'])
|
||||
.then(error(that), complete(that));
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Test that RSA-OAEP encrypt/decrypt accepts strings as AlgorithmIdentifiers",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = {
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-256",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01])
|
||||
};
|
||||
|
||||
var privKey, pubKey, data = crypto.getRandomValues(new Uint8Array(128));
|
||||
function setKey(x) { pubKey = x.publicKey; privKey = x.privateKey; }
|
||||
function doEncrypt() {
|
||||
return crypto.subtle.encrypt("RSA-OAEP", pubKey, data);
|
||||
}
|
||||
function doDecrypt(x) {
|
||||
return crypto.subtle.decrypt("RSA-OAEP", privKey, x);
|
||||
}
|
||||
|
||||
crypto.subtle.generateKey(alg, false, ["encrypt", "decrypt"])
|
||||
.then(setKey)
|
||||
.then(doEncrypt)
|
||||
.then(doDecrypt)
|
||||
.then(memcmp_complete(that, data), error(that));
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Key wrap known answer, using AES-GCM",
|
||||
|
@ -1466,229 +1154,6 @@ TestArray.addTest(
|
|||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import and use of an AES-GCM key",
|
||||
function () {
|
||||
var that = this;
|
||||
|
||||
function doEncrypt(x) {
|
||||
return crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: tv.aes_gcm_enc.iv,
|
||||
additionalData: tv.aes_gcm_enc.adata,
|
||||
tagLength: 128
|
||||
},
|
||||
x, tv.aes_gcm_enc.data);
|
||||
}
|
||||
|
||||
crypto.subtle.importKey("jwk", tv.aes_gcm_enc.key_jwk, "AES-GCM", false, ['encrypt'])
|
||||
.then(doEncrypt)
|
||||
.then(
|
||||
memcmp_complete(that, tv.aes_gcm_enc.result),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import and use of an RSASSA-PKCS1-v1_5 private key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
||||
|
||||
function doSign(x) {
|
||||
return crypto.subtle.sign(alg.name, x, tv.rsassa.data);
|
||||
}
|
||||
function fail(x) { console.log(x); error(that); }
|
||||
|
||||
crypto.subtle.importKey("jwk", tv.rsassa.jwk_priv, alg, false, ['sign'])
|
||||
.then( doSign, fail )
|
||||
.then( memcmp_complete(that, tv.rsassa.sig256), fail );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import and use of an RSASSA-PKCS1-v1_5 public key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
||||
|
||||
function doVerify(x) {
|
||||
return crypto.subtle.verify(alg.name, x, tv.rsassa.sig256, tv.rsassa.data);
|
||||
}
|
||||
function fail(x) { error(that); }
|
||||
|
||||
crypto.subtle.importKey("jwk", tv.rsassa.jwk_pub, alg, false, ['verify'])
|
||||
.then( doVerify, fail )
|
||||
.then(
|
||||
complete(that, function(x) { return x; }),
|
||||
fail
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import failure on incomplete RSA private key (missing 'qi')",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSA-OAEP", hash: "SHA-256" };
|
||||
var jwk = {
|
||||
kty: "RSA",
|
||||
n: tv.rsassa.jwk_priv.n,
|
||||
e: tv.rsassa.jwk_priv.e,
|
||||
d: tv.rsassa.jwk_priv.d,
|
||||
p: tv.rsassa.jwk_priv.p,
|
||||
q: tv.rsassa.jwk_priv.q,
|
||||
dp: tv.rsassa.jwk_priv.dp,
|
||||
dq: tv.rsassa.jwk_priv.dq,
|
||||
};
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt', 'decrypt'])
|
||||
.then( error(that), complete(that) );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import failure on algorithm mismatch",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = "AES-GCM";
|
||||
var jwk = { k: "c2l4dGVlbiBieXRlIGtleQ", alg: "A256GCM" };
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt', 'decrypt'])
|
||||
.then( error(that), complete(that) );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import failure on usages mismatch",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = "AES-GCM";
|
||||
var jwk = { k: "c2l4dGVlbiBieXRlIGtleQ", key_ops: ['encrypt'] };
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt', 'decrypt'])
|
||||
.then( error(that), complete(that) );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import failure on extractable mismatch",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = "AES-GCM";
|
||||
var jwk = { k: "c2l4dGVlbiBieXRlIGtleQ", ext: false };
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt'])
|
||||
.then( error(that), complete(that) );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK export of a symmetric key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = "AES-GCM";
|
||||
var jwk = { k: "c2l4dGVlbiBieXRlIGtleQ" };
|
||||
|
||||
function doExport(k) {
|
||||
return crypto.subtle.exportKey("jwk", k);
|
||||
}
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt', 'decrypt'])
|
||||
.then(doExport)
|
||||
.then(
|
||||
complete(that, function(x) {
|
||||
return hasBaseJwkFields(x) &&
|
||||
hasFields(x, ['k']) &&
|
||||
x.kty == 'oct' &&
|
||||
x.alg == 'A128GCM' &&
|
||||
x.ext &&
|
||||
shallowArrayEquals(x.key_ops, ['encrypt','decrypt']) &&
|
||||
x.k == jwk.k
|
||||
}),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK export of an RSA private key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
||||
var jwk = tv.rsassa.jwk_priv;
|
||||
|
||||
function doExport(k) {
|
||||
return crypto.subtle.exportKey("jwk", k);
|
||||
}
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['sign'])
|
||||
.then(doExport)
|
||||
.then(
|
||||
complete(that, function(x) {
|
||||
window.jwk_priv = x;
|
||||
console.log(JSON.stringify(x));
|
||||
return hasBaseJwkFields(x) &&
|
||||
hasFields(x, ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi']) &&
|
||||
x.kty == 'RSA' &&
|
||||
x.alg == 'RS256' &&
|
||||
x.ext &&
|
||||
shallowArrayEquals(x.key_ops, ['sign']) &&
|
||||
x.n == jwk.n &&
|
||||
x.e == jwk.e &&
|
||||
x.d == jwk.d &&
|
||||
x.p == jwk.p &&
|
||||
x.q == jwk.q &&
|
||||
x.dp == jwk.dp &&
|
||||
x.dq == jwk.dq &&
|
||||
x.qi == jwk.qi;
|
||||
}),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK export of an RSA public key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
||||
var jwk = tv.rsassa.jwk_pub;
|
||||
|
||||
function doExport(k) {
|
||||
return crypto.subtle.exportKey("jwk", k);
|
||||
}
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['verify'])
|
||||
.then(doExport)
|
||||
.then(
|
||||
complete(that, function(x) {
|
||||
window.jwk_pub = x;
|
||||
return hasBaseJwkFields(x) &&
|
||||
hasFields(x, ['n', 'e']) &&
|
||||
x.kty == 'RSA' &&
|
||||
x.alg == 'RS256' &&
|
||||
x.ext &&
|
||||
shallowArrayEquals(x.key_ops, ['verify']) &&
|
||||
x.n == jwk.n &&
|
||||
x.e == jwk.e;
|
||||
}),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK wrap/unwrap round-trip, with AES-GCM",
|
||||
|
|
|
@ -0,0 +1,281 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>WebCrypto Test Suite</title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
<link rel="stylesheet" href="./test_WebCrypto.css"/>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
|
||||
<!-- Utilities for manipulating ABVs -->
|
||||
<script src="util.js"></script>
|
||||
|
||||
<!-- A simple wrapper around IndexedDB -->
|
||||
<script src="simpledb.js"></script>
|
||||
|
||||
<!-- Test vectors drawn from the literature -->
|
||||
<script src="./test-vectors.js"></script>
|
||||
|
||||
<!-- General testing framework -->
|
||||
<script src="./test-array.js"></script>
|
||||
|
||||
<script>/*<![CDATA[*/
|
||||
"use strict";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import and use of an AES-GCM key",
|
||||
function () {
|
||||
var that = this;
|
||||
|
||||
function doEncrypt(x) {
|
||||
return crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: tv.aes_gcm_enc.iv,
|
||||
additionalData: tv.aes_gcm_enc.adata,
|
||||
tagLength: 128
|
||||
},
|
||||
x, tv.aes_gcm_enc.data);
|
||||
}
|
||||
|
||||
crypto.subtle.importKey("jwk", tv.aes_gcm_enc.key_jwk, "AES-GCM", false, ['encrypt'])
|
||||
.then(doEncrypt)
|
||||
.then(
|
||||
memcmp_complete(that, tv.aes_gcm_enc.result),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import and use of an RSASSA-PKCS1-v1_5 private key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
||||
|
||||
function doSign(x) {
|
||||
return crypto.subtle.sign(alg.name, x, tv.rsassa.data);
|
||||
}
|
||||
function fail(x) { console.log(x); error(that); }
|
||||
|
||||
crypto.subtle.importKey("jwk", tv.rsassa.jwk_priv, alg, false, ['sign'])
|
||||
.then( doSign, fail )
|
||||
.then( memcmp_complete(that, tv.rsassa.sig256), fail );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import and use of an RSASSA-PKCS1-v1_5 public key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
||||
|
||||
function doVerify(x) {
|
||||
return crypto.subtle.verify(alg.name, x, tv.rsassa.sig256, tv.rsassa.data);
|
||||
}
|
||||
function fail(x) { error(that); }
|
||||
|
||||
crypto.subtle.importKey("jwk", tv.rsassa.jwk_pub, alg, false, ['verify'])
|
||||
.then( doVerify, fail )
|
||||
.then(
|
||||
complete(that, function(x) { return x; }),
|
||||
fail
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import failure on incomplete RSA private key (missing 'qi')",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSA-OAEP", hash: "SHA-256" };
|
||||
var jwk = {
|
||||
kty: "RSA",
|
||||
n: tv.rsassa.jwk_priv.n,
|
||||
e: tv.rsassa.jwk_priv.e,
|
||||
d: tv.rsassa.jwk_priv.d,
|
||||
p: tv.rsassa.jwk_priv.p,
|
||||
q: tv.rsassa.jwk_priv.q,
|
||||
dp: tv.rsassa.jwk_priv.dp,
|
||||
dq: tv.rsassa.jwk_priv.dq,
|
||||
};
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt', 'decrypt'])
|
||||
.then( error(that), complete(that) );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import failure on algorithm mismatch",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = "AES-GCM";
|
||||
var jwk = { k: "c2l4dGVlbiBieXRlIGtleQ", alg: "A256GCM" };
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt', 'decrypt'])
|
||||
.then( error(that), complete(that) );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import failure on usages mismatch",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = "AES-GCM";
|
||||
var jwk = { k: "c2l4dGVlbiBieXRlIGtleQ", key_ops: ['encrypt'] };
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt', 'decrypt'])
|
||||
.then( error(that), complete(that) );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK import failure on extractable mismatch",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = "AES-GCM";
|
||||
var jwk = { k: "c2l4dGVlbiBieXRlIGtleQ", ext: false };
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt'])
|
||||
.then( error(that), complete(that) );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK export of a symmetric key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = "AES-GCM";
|
||||
var jwk = { k: "c2l4dGVlbiBieXRlIGtleQ" };
|
||||
|
||||
function doExport(k) {
|
||||
return crypto.subtle.exportKey("jwk", k);
|
||||
}
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['encrypt', 'decrypt'])
|
||||
.then(doExport)
|
||||
.then(
|
||||
complete(that, function(x) {
|
||||
return hasBaseJwkFields(x) &&
|
||||
hasFields(x, ['k']) &&
|
||||
x.kty == 'oct' &&
|
||||
x.alg == 'A128GCM' &&
|
||||
x.ext &&
|
||||
shallowArrayEquals(x.key_ops, ['encrypt','decrypt']) &&
|
||||
x.k == jwk.k
|
||||
}),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK export of an RSA private key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
||||
var jwk = tv.rsassa.jwk_priv;
|
||||
|
||||
function doExport(k) {
|
||||
return crypto.subtle.exportKey("jwk", k);
|
||||
}
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['sign'])
|
||||
.then(doExport)
|
||||
.then(
|
||||
complete(that, function(x) {
|
||||
window.jwk_priv = x;
|
||||
console.log(JSON.stringify(x));
|
||||
return hasBaseJwkFields(x) &&
|
||||
hasFields(x, ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi']) &&
|
||||
x.kty == 'RSA' &&
|
||||
x.alg == 'RS256' &&
|
||||
x.ext &&
|
||||
shallowArrayEquals(x.key_ops, ['sign']) &&
|
||||
x.n == jwk.n &&
|
||||
x.e == jwk.e &&
|
||||
x.d == jwk.d &&
|
||||
x.p == jwk.p &&
|
||||
x.q == jwk.q &&
|
||||
x.dp == jwk.dp &&
|
||||
x.dq == jwk.dq &&
|
||||
x.qi == jwk.qi;
|
||||
}),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"JWK export of an RSA public key",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
||||
var jwk = tv.rsassa.jwk_pub;
|
||||
|
||||
function doExport(k) {
|
||||
return crypto.subtle.exportKey("jwk", k);
|
||||
}
|
||||
|
||||
crypto.subtle.importKey("jwk", jwk, alg, true, ['verify'])
|
||||
.then(doExport)
|
||||
.then(
|
||||
complete(that, function(x) {
|
||||
window.jwk_pub = x;
|
||||
return hasBaseJwkFields(x) &&
|
||||
hasFields(x, ['n', 'e']) &&
|
||||
x.kty == 'RSA' &&
|
||||
x.alg == 'RS256' &&
|
||||
x.ext &&
|
||||
shallowArrayEquals(x.key_ops, ['verify']) &&
|
||||
x.n == jwk.n &&
|
||||
x.e == jwk.e;
|
||||
}),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
/*]]>*/</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="content">
|
||||
<div id="head">
|
||||
<b>Web</b>Crypto<br>
|
||||
</div>
|
||||
|
||||
<div id="start" onclick="start();">RUN ALL</div>
|
||||
|
||||
<div id="resultDiv" class="content">
|
||||
Summary:
|
||||
<span class="pass"><span id="passN">0</span> passed, </span>
|
||||
<span class="fail"><span id="failN">0</span> failed, </span>
|
||||
<span class="pending"><span id="pendingN">0</span> pending.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<table id="results">
|
||||
<tr>
|
||||
<th>Test</th>
|
||||
<th>Result</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="foot"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,218 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>WebCrypto Test Suite</title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
<link rel="stylesheet" href="./test_WebCrypto.css"/>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
|
||||
<!-- Utilities for manipulating ABVs -->
|
||||
<script src="util.js"></script>
|
||||
|
||||
<!-- A simple wrapper around IndexedDB -->
|
||||
<script src="simpledb.js"></script>
|
||||
|
||||
<!-- Test vectors drawn from the literature -->
|
||||
<script src="./test-vectors.js"></script>
|
||||
|
||||
<!-- General testing framework -->
|
||||
<script src="./test-array.js"></script>
|
||||
|
||||
<script>/*<![CDATA[*/
|
||||
"use strict";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Import raw PBKDF2 key",
|
||||
function() {
|
||||
var that = this;
|
||||
var alg = "PBKDF2";
|
||||
var key = new TextEncoder("utf-8").encode("password");
|
||||
|
||||
crypto.subtle.importKey("raw", key, alg, false, ["deriveKey"]).then(
|
||||
complete(that, hasKeyFields),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Import raw PBKDF2 key and derive bits using HMAC-SHA-1",
|
||||
function() {
|
||||
var that = this;
|
||||
var alg = "PBKDF2";
|
||||
var key = tv.pbkdf2_sha1.password;
|
||||
|
||||
function doDerive(x) {
|
||||
console.log("deriving");
|
||||
if (!hasKeyFields(x)) {
|
||||
throw "Invalid key; missing field(s)";
|
||||
}
|
||||
|
||||
var alg = {
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-1",
|
||||
salt: tv.pbkdf2_sha1.salt,
|
||||
iterations: tv.pbkdf2_sha1.iterations
|
||||
};
|
||||
return crypto.subtle.deriveBits(alg, x, tv.pbkdf2_sha1.length);
|
||||
}
|
||||
function fail(x) { console.log("failing"); error(that)(x); }
|
||||
|
||||
crypto.subtle.importKey("raw", key, alg, false, ["deriveKey"])
|
||||
.then( doDerive, fail )
|
||||
.then( memcmp_complete(that, tv.pbkdf2_sha1.derived), fail );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Import raw PBKDF2 key and derive a new key using HMAC-SHA-1",
|
||||
function() {
|
||||
var that = this;
|
||||
var alg = "PBKDF2";
|
||||
var key = tv.pbkdf2_sha1.password;
|
||||
|
||||
function doDerive(x) {
|
||||
console.log("deriving");
|
||||
if (!hasKeyFields(x)) {
|
||||
throw "Invalid key; missing field(s)";
|
||||
}
|
||||
|
||||
var alg = {
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-1",
|
||||
salt: tv.pbkdf2_sha1.salt,
|
||||
iterations: tv.pbkdf2_sha1.iterations
|
||||
};
|
||||
|
||||
var algDerived = {
|
||||
name: "HMAC",
|
||||
hash: {name: "SHA-1"}
|
||||
};
|
||||
|
||||
return crypto.subtle.deriveKey(alg, x, algDerived, false, ["sign", "verify"])
|
||||
.then(function (x) {
|
||||
if (!hasKeyFields(x)) {
|
||||
throw "Invalid key; missing field(s)";
|
||||
}
|
||||
|
||||
if (x.algorithm.length != 512) {
|
||||
throw "Invalid key; incorrect length";
|
||||
}
|
||||
|
||||
return x;
|
||||
});
|
||||
}
|
||||
|
||||
function doSignAndVerify(x) {
|
||||
var data = new Uint8Array(1024);
|
||||
|
||||
return crypto.subtle.sign("HMAC", x, data)
|
||||
.then(function (sig) {
|
||||
return crypto.subtle.verify("HMAC", x, sig, data);
|
||||
});
|
||||
}
|
||||
|
||||
function fail(x) { console.log("failing"); error(that)(x); }
|
||||
|
||||
crypto.subtle.importKey("raw", key, alg, false, ["deriveKey"])
|
||||
.then( doDerive, fail )
|
||||
.then( doSignAndVerify, fail )
|
||||
.then( complete(that), fail );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Import raw PBKDF2 key and derive a new key using HMAC-SHA-1 with custom length",
|
||||
function() {
|
||||
var that = this;
|
||||
|
||||
function doDerive(x) {
|
||||
var alg = {
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-1",
|
||||
salt: tv.pbkdf2_sha1.salt,
|
||||
iterations: tv.pbkdf2_sha1.iterations
|
||||
};
|
||||
|
||||
var algDerived = {name: "HMAC", hash: "SHA-1", length: 128};
|
||||
return crypto.subtle.deriveKey(alg, x, algDerived, false, ["sign"]);
|
||||
}
|
||||
|
||||
var password = crypto.getRandomValues(new Uint8Array(8));
|
||||
crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveKey"])
|
||||
.then(doDerive)
|
||||
.then(complete(that, function (x) {
|
||||
return hasKeyFields(x) && x.algorithm.length == 128;
|
||||
}), error(that));
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
/*TestArray.addTest(
|
||||
"Import raw PBKDF2 key and derive bits using HMAC-SHA-256",
|
||||
function() {
|
||||
var that = this;
|
||||
var alg = "PBKDF2";
|
||||
var key = tv.pbkdf2_sha256.password;
|
||||
|
||||
function doDerive(x) {
|
||||
console.log("deriving");
|
||||
if (!hasKeyFields(x)) {
|
||||
throw "Invalid key; missing field(s)";
|
||||
}
|
||||
|
||||
var alg = {
|
||||
name: "PBKDF2",
|
||||
hash: "SHA-256",
|
||||
salt: tv.pbkdf2_sha256.salt,
|
||||
iterations: tv.pbkdf2_sha256.iterations
|
||||
};
|
||||
return crypto.subtle.deriveBits(alg, x, tv.pbkdf2_sha256.length);
|
||||
}
|
||||
function fail(x) { console.log("failing"); error(that)(x); }
|
||||
|
||||
crypto.subtle.importKey("raw", key, alg, false, ["deriveKey"])
|
||||
.then( doDerive, fail )
|
||||
.then( memcmp_complete(that, tv.pbkdf2_sha256.derived), fail );
|
||||
}
|
||||
);*/
|
||||
/*]]>*/</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="content">
|
||||
<div id="head">
|
||||
<b>Web</b>Crypto<br>
|
||||
</div>
|
||||
|
||||
<div id="start" onclick="start();">RUN ALL</div>
|
||||
|
||||
<div id="resultDiv" class="content">
|
||||
Summary:
|
||||
<span class="pass"><span id="passN">0</span> passed, </span>
|
||||
<span class="fail"><span id="failN">0</span> failed, </span>
|
||||
<span class="pending"><span id="pendingN">0</span> pending.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<table id="results">
|
||||
<tr>
|
||||
<th>Test</th>
|
||||
<th>Result</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="foot"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,210 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>WebCrypto Test Suite</title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
<link rel="stylesheet" href="./test_WebCrypto.css"/>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
|
||||
<!-- Utilities for manipulating ABVs -->
|
||||
<script src="util.js"></script>
|
||||
|
||||
<!-- A simple wrapper around IndexedDB -->
|
||||
<script src="simpledb.js"></script>
|
||||
|
||||
<!-- Test vectors drawn from the literature -->
|
||||
<script src="./test-vectors.js"></script>
|
||||
|
||||
<!-- General testing framework -->
|
||||
<script src="./test-array.js"></script>
|
||||
|
||||
<script>/*<![CDATA[*/
|
||||
"use strict";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP encrypt/decrypt round-trip",
|
||||
function () {
|
||||
var that = this;
|
||||
var privKey, pubKey;
|
||||
var alg = {name: "RSA-OAEP", hash: "SHA-1"};
|
||||
|
||||
var privKey, pubKey;
|
||||
function setPriv(x) { privKey = x; }
|
||||
function setPub(x) { pubKey = x; }
|
||||
function doEncrypt() {
|
||||
return crypto.subtle.encrypt(alg, pubKey, tv.rsaoaep.data);
|
||||
}
|
||||
function doDecrypt(x) {
|
||||
return crypto.subtle.decrypt(alg, privKey, x);
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
crypto.subtle.importKey("pkcs8", tv.rsaoaep.pkcs8, alg, false, ['decrypt'])
|
||||
.then(setPriv, error(that)),
|
||||
crypto.subtle.importKey("spki", tv.rsaoaep.spki, alg, false, ['encrypt'])
|
||||
.then(setPub, error(that))
|
||||
]).then(doEncrypt, error(that))
|
||||
.then(doDecrypt, error(that))
|
||||
.then(
|
||||
memcmp_complete(that, tv.rsaoaep.data),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP key generation and encrypt/decrypt round-trip (SHA-256)",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = {
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-256",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01])
|
||||
};
|
||||
|
||||
var privKey, pubKey, data = crypto.getRandomValues(new Uint8Array(128));
|
||||
function setKey(x) { pubKey = x.publicKey; privKey = x.privateKey; }
|
||||
function doEncrypt() {
|
||||
return crypto.subtle.encrypt(alg, pubKey, data);
|
||||
}
|
||||
function doDecrypt(x) {
|
||||
return crypto.subtle.decrypt(alg, privKey, x);
|
||||
}
|
||||
|
||||
crypto.subtle.generateKey(alg, false, ['encrypt', 'decrypt'])
|
||||
.then(setKey, error(that))
|
||||
.then(doEncrypt, error(that))
|
||||
.then(doDecrypt, error(that))
|
||||
.then(
|
||||
memcmp_complete(that, data),
|
||||
error(that)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP decryption known answer",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = {name: "RSA-OAEP", hash: "SHA-1"};
|
||||
|
||||
function doDecrypt(x) {
|
||||
return crypto.subtle.decrypt(alg, x, tv.rsaoaep.result);
|
||||
}
|
||||
function fail() { error(that); }
|
||||
|
||||
crypto.subtle.importKey("pkcs8", tv.rsaoaep.pkcs8, alg, false, ['decrypt'])
|
||||
.then( doDecrypt, fail )
|
||||
.then( memcmp_complete(that, tv.rsaoaep.data), fail );
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP input data length checks (2048-bit key)",
|
||||
function () {
|
||||
var that = this;
|
||||
var privKey, pubKey;
|
||||
var alg = {
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-1",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01])
|
||||
};
|
||||
|
||||
var privKey, pubKey;
|
||||
function setKey(x) { pubKey = x.publicKey; privKey = x.privateKey; }
|
||||
function doEncrypt(n) {
|
||||
return function () {
|
||||
return crypto.subtle.encrypt(alg, pubKey, new Uint8Array(n));
|
||||
}
|
||||
}
|
||||
|
||||
crypto.subtle.generateKey(alg, false, ['encrypt'])
|
||||
.then(setKey, error(that))
|
||||
.then(doEncrypt(214), error(that))
|
||||
.then(doEncrypt(215), error(that))
|
||||
.then(error(that), complete(that));
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"RSA-OAEP key import with invalid hash",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = {name: "RSA-OAEP", hash: "SHA-123"};
|
||||
|
||||
crypto.subtle.importKey("pkcs8", tv.rsaoaep.pkcs8, alg, false, ['decrypt'])
|
||||
.then(error(that), complete(that));
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
TestArray.addTest(
|
||||
"Test that RSA-OAEP encrypt/decrypt accepts strings as AlgorithmIdentifiers",
|
||||
function () {
|
||||
var that = this;
|
||||
var alg = {
|
||||
name: "RSA-OAEP",
|
||||
hash: "SHA-256",
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([0x01, 0x00, 0x01])
|
||||
};
|
||||
|
||||
var privKey, pubKey, data = crypto.getRandomValues(new Uint8Array(128));
|
||||
function setKey(x) { pubKey = x.publicKey; privKey = x.privateKey; }
|
||||
function doEncrypt() {
|
||||
return crypto.subtle.encrypt("RSA-OAEP", pubKey, data);
|
||||
}
|
||||
function doDecrypt(x) {
|
||||
return crypto.subtle.decrypt("RSA-OAEP", privKey, x);
|
||||
}
|
||||
|
||||
crypto.subtle.generateKey(alg, false, ["encrypt", "decrypt"])
|
||||
.then(setKey)
|
||||
.then(doEncrypt)
|
||||
.then(doDecrypt)
|
||||
.then(memcmp_complete(that, data), error(that));
|
||||
}
|
||||
);
|
||||
/*]]>*/</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="content">
|
||||
<div id="head">
|
||||
<b>Web</b>Crypto<br>
|
||||
</div>
|
||||
|
||||
<div id="start" onclick="start();">RUN ALL</div>
|
||||
|
||||
<div id="resultDiv" class="content">
|
||||
Summary:
|
||||
<span class="pass"><span id="passN">0</span> passed, </span>
|
||||
<span class="fail"><span id="failN">0</span> failed, </span>
|
||||
<span class="pending"><span id="pendingN">0</span> pending.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<table id="results">
|
||||
<tr>
|
||||
<th>Test</th>
|
||||
<th>Result</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="foot"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -9,7 +9,7 @@ interface nsIDOMWindow;
|
|||
interface nsIURI;
|
||||
interface nsIFrameLoaderOwner;
|
||||
|
||||
[scriptable, uuid(e420bd32-b8c4-4b47-8cca-09e0bddbb0c3)]
|
||||
[scriptable, uuid(699b8f60-2898-11e4-8c21-0800200c9a66)]
|
||||
|
||||
/**
|
||||
* The C++ source has access to the browser script source through
|
||||
|
@ -92,12 +92,5 @@ interface nsIBrowserDOMWindow : nsISupports
|
|||
* currently open tab in this toplevel browser window.
|
||||
*/
|
||||
boolean isTabContentWindow(in nsIDOMWindow aWindow);
|
||||
|
||||
/**
|
||||
* The contentWindow property of the currently selected browser.
|
||||
* This is used to implement .content in remote-Firefox.
|
||||
*/
|
||||
|
||||
readonly attribute jsval contentWindow;
|
||||
};
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ interface nsIStructuredCloneContainer : nsISupports
|
|||
void initFromBase64(in AString aData,in unsigned long aFormatVersion);
|
||||
|
||||
/**
|
||||
* Deserialize the object this conatiner holds, returning it wrapped as
|
||||
* Deserialize the object this container holds, returning it wrapped as
|
||||
* an nsIVariant.
|
||||
*/
|
||||
[implicit_jscontext]
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#include "domstubs.idl"
|
||||
|
||||
[scriptable, uuid(fb089720-1c5c-11e3-b773-0800200c9a66)]
|
||||
[scriptable, uuid(9b12f566-2c7f-48ef-990d-e5092a71d11e)]
|
||||
interface nsINotificationStorageCallback : nsISupports
|
||||
{
|
||||
/**
|
||||
|
@ -26,7 +26,8 @@ interface nsINotificationStorageCallback : nsISupports
|
|||
in DOMString lang,
|
||||
in DOMString body,
|
||||
in DOMString tag,
|
||||
in DOMString icon);
|
||||
in DOMString icon,
|
||||
in DOMString data);
|
||||
|
||||
/**
|
||||
* Callback function used to notify C++ the we have returned
|
||||
|
@ -39,7 +40,7 @@ interface nsINotificationStorageCallback : nsISupports
|
|||
/**
|
||||
* Interface for notification persistence layer.
|
||||
*/
|
||||
[scriptable, uuid(cc4656d7-2a2a-47f1-8016-55891e833d64)]
|
||||
[scriptable, uuid(1be733d9-d614-43f2-9fd4-8f573a33b215)]
|
||||
interface nsINotificationStorage : nsISupports
|
||||
{
|
||||
|
||||
|
@ -68,7 +69,8 @@ interface nsINotificationStorage : nsISupports
|
|||
in DOMString body,
|
||||
in DOMString tag,
|
||||
in DOMString icon,
|
||||
in DOMString alertName);
|
||||
in DOMString alertName,
|
||||
in DOMString data);
|
||||
|
||||
/**
|
||||
* Retrieve a list of notifications.
|
||||
|
|
|
@ -3361,6 +3361,7 @@ ContentParent::RecvShowAlertNotification(const nsString& aImageUrl, const nsStri
|
|||
const nsString& aText, const bool& aTextClickable,
|
||||
const nsString& aCookie, const nsString& aName,
|
||||
const nsString& aBidi, const nsString& aLang,
|
||||
const nsString& aData,
|
||||
const IPC::Principal& aPrincipal)
|
||||
{
|
||||
#ifdef MOZ_CHILD_PERMISSIONS
|
||||
|
@ -3374,7 +3375,8 @@ ContentParent::RecvShowAlertNotification(const nsString& aImageUrl, const nsStri
|
|||
nsCOMPtr<nsIAlertsService> sysAlerts(do_GetService(NS_ALERTSERVICE_CONTRACTID));
|
||||
if (sysAlerts) {
|
||||
sysAlerts->ShowAlertNotification(aImageUrl, aTitle, aText, aTextClickable,
|
||||
aCookie, this, aName, aBidi, aLang, aPrincipal);
|
||||
aCookie, this, aName, aBidi, aLang,
|
||||
aData, aPrincipal);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -530,6 +530,7 @@ private:
|
|||
const nsString& aText, const bool& aTextClickable,
|
||||
const nsString& aCookie, const nsString& aName,
|
||||
const nsString& aBidi, const nsString& aLang,
|
||||
const nsString& aData,
|
||||
const IPC::Principal& aPrincipal) MOZ_OVERRIDE;
|
||||
|
||||
virtual bool RecvCloseAlert(const nsString& aName,
|
||||
|
|
|
@ -542,6 +542,7 @@ parent:
|
|||
nsString name,
|
||||
nsString bidi,
|
||||
nsString lang,
|
||||
nsString data,
|
||||
Principal principal);
|
||||
|
||||
CloseAlert(nsString name, Principal principal);
|
||||
|
|
|
@ -13,11 +13,6 @@ if CONFIG['MOZ_WEBRTC']:
|
|||
'/media/webrtc/trunk',
|
||||
]
|
||||
|
||||
if CONFIG['GNU_CXX']:
|
||||
CXXFLAGS += [
|
||||
'-Wno-unused-local-typedefs', # Workaround until we fix bug 1020661
|
||||
]
|
||||
|
||||
MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
|
||||
WEBRTC_SIGNALLING_TEST_MANIFESTS += ['tests/mochitest/steeplechase.ini']
|
||||
|
||||
|
|
|
@ -163,6 +163,9 @@ PluginModuleChild::Init(const std::string& aPluginFilename,
|
|||
true,
|
||||
getter_AddRefs(localFile));
|
||||
|
||||
if (!localFile)
|
||||
return false;
|
||||
|
||||
bool exists;
|
||||
localFile->Exists(&exists);
|
||||
NS_ASSERTION(exists, "plugin file ain't there");
|
||||
|
|
|
@ -60,7 +60,8 @@ ChromeNotifications.prototype = {
|
|||
lang: notification.lang,
|
||||
tag: notification.tag,
|
||||
dbId: notification.id,
|
||||
timestamp: notification.timestamp
|
||||
timestamp: notification.timestamp,
|
||||
data: notification.data
|
||||
}
|
||||
);
|
||||
resentNotifications++;
|
||||
|
|
|
@ -111,6 +111,7 @@ DesktopNotification::PostDesktopNotification()
|
|||
uniqueName,
|
||||
NS_LITERAL_STRING("auto"),
|
||||
EmptyString(),
|
||||
EmptyString(),
|
||||
principal);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include "mozilla/dom/Notification.h"
|
||||
#include "mozilla/dom/AppNotificationServiceOptionsBinding.h"
|
||||
#include "mozilla/dom/BindingUtils.h"
|
||||
#include "mozilla/dom/OwningNonNull.h"
|
||||
#include "mozilla/dom/Promise.h"
|
||||
#include "mozilla/Preferences.h"
|
||||
|
@ -16,10 +17,12 @@
|
|||
#include "nsIPermissionManager.h"
|
||||
#include "nsIUUIDGenerator.h"
|
||||
#include "nsServiceManagerUtils.h"
|
||||
#include "nsStructuredCloneContainer.h"
|
||||
#include "nsToolkitCompsCID.h"
|
||||
#include "nsGlobalWindow.h"
|
||||
#include "nsDOMJSUtils.h"
|
||||
#include "nsIScriptSecurityManager.h"
|
||||
#include "nsIXPConnect.h"
|
||||
#include "mozilla/dom/PermissionMessageUtils.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "nsContentPermissionHelper.h"
|
||||
|
@ -57,11 +60,12 @@ public:
|
|||
const nsAString& aBody,
|
||||
const nsAString& aTag,
|
||||
const nsAString& aIcon,
|
||||
const nsAString& aData,
|
||||
JSContext* aCx)
|
||||
{
|
||||
MOZ_ASSERT(!aID.IsEmpty());
|
||||
|
||||
NotificationOptions options;
|
||||
RootedDictionary<NotificationOptions> options(aCx);
|
||||
options.mDir = Notification::StringToDirection(nsString(aDir));
|
||||
options.mLang = aLang;
|
||||
options.mBody = aBody;
|
||||
|
@ -71,6 +75,12 @@ public:
|
|||
aID,
|
||||
aTitle,
|
||||
options);
|
||||
ErrorResult rv;
|
||||
notification->InitFromBase64(aCx, aData, rv);
|
||||
if (rv.Failed()) {
|
||||
return rv.ErrorCode();
|
||||
}
|
||||
|
||||
JSAutoCompartment ac(aCx, mGlobal);
|
||||
JS::Rooted<JSObject*> element(aCx, notification->WrapObject(aCx));
|
||||
NS_ENSURE_TRUE(element, NS_ERROR_FAILURE);
|
||||
|
@ -371,7 +381,7 @@ NotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
|
|||
Notification::Notification(const nsAString& aID, const nsAString& aTitle, const nsAString& aBody,
|
||||
NotificationDirection aDir, const nsAString& aLang,
|
||||
const nsAString& aTag, const nsAString& aIconUrl,
|
||||
nsPIDOMWindow* aWindow)
|
||||
nsPIDOMWindow* aWindow)
|
||||
: DOMEventTargetHelper(aWindow),
|
||||
mID(aID), mTitle(aTitle), mBody(aBody), mDir(aDir), mLang(aLang),
|
||||
mTag(aTag), mIconUrl(aIconUrl), mIsClosed(false)
|
||||
|
@ -404,11 +414,19 @@ Notification::Constructor(const GlobalObject& aGlobal,
|
|||
MOZ_ASSERT(NS_IsMainThread());
|
||||
nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aGlobal.GetAsSupports());
|
||||
MOZ_ASSERT(window, "Window should not be null.");
|
||||
|
||||
nsRefPtr<Notification> notification = CreateInternal(window,
|
||||
EmptyString(),
|
||||
aTitle,
|
||||
aOptions);
|
||||
|
||||
// Make a structured clone of the aOptions.mData object
|
||||
JS::Rooted<JS::Value> data(aGlobal.Context(), aOptions.mData);
|
||||
notification->InitFromJSVal(aGlobal.Context(), data, aRv);
|
||||
if (aRv.Failed()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Queue a task to show the notification.
|
||||
nsCOMPtr<nsIRunnable> showNotificationTask =
|
||||
new NotificationTask(notification, NotificationTask::eShow);
|
||||
|
@ -435,6 +453,13 @@ Notification::Constructor(const GlobalObject& aGlobal,
|
|||
nsString alertName;
|
||||
notification->GetAlertName(alertName);
|
||||
|
||||
nsString dataString;
|
||||
nsCOMPtr<nsIStructuredCloneContainer> scContainer;
|
||||
scContainer = notification->GetDataCloneContainer();
|
||||
if (scContainer) {
|
||||
scContainer->GetDataAsBase64(dataString);
|
||||
}
|
||||
|
||||
aRv = notificationStorage->Put(origin,
|
||||
id,
|
||||
aTitle,
|
||||
|
@ -443,7 +468,9 @@ Notification::Constructor(const GlobalObject& aGlobal,
|
|||
aOptions.mBody,
|
||||
aOptions.mTag,
|
||||
aOptions.mIcon,
|
||||
alertName);
|
||||
alertName,
|
||||
dataString);
|
||||
|
||||
if (aRv.Failed()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -481,10 +508,29 @@ Notification::CreateInternal(nsPIDOMWindow* aWindow,
|
|||
aOptions.mLang,
|
||||
aOptions.mTag,
|
||||
aOptions.mIcon,
|
||||
aWindow);
|
||||
aWindow);
|
||||
return notification.forget();
|
||||
}
|
||||
|
||||
Notification::~Notification() {}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_CLASS(Notification)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Notification, DOMEventTargetHelper)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mData)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDataObjectContainer)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Notification, DOMEventTargetHelper)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mData)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDataObjectContainer)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
||||
|
||||
NS_IMPL_ADDREF_INHERITED(Notification, DOMEventTargetHelper)
|
||||
NS_IMPL_RELEASE_INHERITED(Notification, DOMEventTargetHelper)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Notification)
|
||||
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
|
||||
|
||||
nsIPrincipal*
|
||||
Notification::GetPrincipal()
|
||||
{
|
||||
|
@ -531,6 +577,13 @@ Notification::ShowInternal()
|
|||
|
||||
nsCOMPtr<nsIObserver> observer = new NotificationObserver(this);
|
||||
|
||||
// mDataObjectContainer might be uninitialized here because the notification
|
||||
// was constructed with an undefined data property.
|
||||
nsString dataStr;
|
||||
if (mDataObjectContainer) {
|
||||
mDataObjectContainer->GetDataAsBase64(dataStr);
|
||||
}
|
||||
|
||||
#ifdef MOZ_B2G
|
||||
nsCOMPtr<nsIAppNotificationService> appNotifier =
|
||||
do_GetService("@mozilla.org/system-alerts-service;1");
|
||||
|
@ -553,6 +606,7 @@ Notification::ShowInternal()
|
|||
ops.mDir = DirectionToString(mDir);
|
||||
ops.mLang = mLang;
|
||||
ops.mTag = mTag;
|
||||
ops.mData = dataStr;
|
||||
|
||||
if (!ToJSValue(cx, ops, &val)) {
|
||||
NS_WARNING("Converting dict to object failed!");
|
||||
|
@ -574,7 +628,7 @@ Notification::ShowInternal()
|
|||
alertService->ShowAlertNotification(absoluteUrl, mTitle, mBody, true,
|
||||
uniqueCookie, observer, mAlertName,
|
||||
DirectionToString(mDir), mLang,
|
||||
GetPrincipal());
|
||||
dataStr, GetPrincipal());
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -780,6 +834,54 @@ Notification::GetOrigin(nsPIDOMWindow* aWindow, nsString& aOrigin)
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
nsIStructuredCloneContainer* Notification::GetDataCloneContainer()
|
||||
{
|
||||
return mDataObjectContainer;
|
||||
}
|
||||
|
||||
void
|
||||
Notification::GetData(JSContext* aCx,
|
||||
JS::MutableHandle<JS::Value> aRetval)
|
||||
{
|
||||
if (!mData && mDataObjectContainer) {
|
||||
nsresult rv;
|
||||
rv = mDataObjectContainer->DeserializeToVariant(aCx, getter_AddRefs(mData));
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
aRetval.setNull();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!mData) {
|
||||
aRetval.setNull();
|
||||
return;
|
||||
}
|
||||
VariantToJsval(aCx, mData, aRetval);
|
||||
}
|
||||
|
||||
void
|
||||
Notification::InitFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aData,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (mDataObjectContainer || aData.isNull()) {
|
||||
return;
|
||||
}
|
||||
mDataObjectContainer = new nsStructuredCloneContainer();
|
||||
aRv = mDataObjectContainer->InitFromJSVal(aData);
|
||||
}
|
||||
|
||||
void Notification::InitFromBase64(JSContext* aCx, const nsAString& aData,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
if (mDataObjectContainer || aData.IsEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto container = new nsStructuredCloneContainer();
|
||||
aRv = container->InitFromBase64(aData, JS_STRUCTURED_CLONE_VERSION,
|
||||
aCx);
|
||||
mDataObjectContainer = container;
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
#include "nsCycleCollectionParticipant.h"
|
||||
|
||||
class nsIPrincipal;
|
||||
class nsIStructuredCloneContainer;
|
||||
class nsIVariant;
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
@ -34,6 +36,9 @@ public:
|
|||
IMPL_EVENT_HANDLER(error)
|
||||
IMPL_EVENT_HANDLER(close)
|
||||
|
||||
NS_DECL_ISUPPORTS_INHERITED
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Notification, DOMEventTargetHelper)
|
||||
|
||||
static already_AddRefed<Notification> Constructor(const GlobalObject& aGlobal,
|
||||
const nsAString& aTitle,
|
||||
const NotificationOptions& aOption,
|
||||
|
@ -72,6 +77,8 @@ public:
|
|||
aRetval = mIconUrl;
|
||||
}
|
||||
|
||||
nsIStructuredCloneContainer* GetDataCloneContainer();
|
||||
|
||||
static void RequestPermission(const GlobalObject& aGlobal,
|
||||
const Optional<OwningNonNull<NotificationPermissionCallback> >& aCallback,
|
||||
ErrorResult& aRv);
|
||||
|
@ -91,11 +98,17 @@ public:
|
|||
}
|
||||
|
||||
virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
|
||||
|
||||
void GetData(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);
|
||||
|
||||
void InitFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aData, ErrorResult& aRv);
|
||||
|
||||
void InitFromBase64(JSContext* aCx, const nsAString& aData, ErrorResult& aRv);
|
||||
protected:
|
||||
Notification(const nsAString& aID, const nsAString& aTitle, const nsAString& aBody,
|
||||
NotificationDirection aDir, const nsAString& aLang,
|
||||
const nsAString& aTag, const nsAString& aIconUrl,
|
||||
nsPIDOMWindow* aWindow);
|
||||
nsPIDOMWindow* aWindow);
|
||||
|
||||
static already_AddRefed<Notification> CreateInternal(nsPIDOMWindow* aWindow,
|
||||
const nsAString& aID,
|
||||
|
@ -145,6 +158,10 @@ protected:
|
|||
nsString mLang;
|
||||
nsString mTag;
|
||||
nsString mIconUrl;
|
||||
nsCOMPtr<nsIStructuredCloneContainer> mDataObjectContainer;
|
||||
|
||||
// It's null until GetData is first called
|
||||
nsCOMPtr<nsIVariant> mData;
|
||||
|
||||
nsString mAlertName;
|
||||
|
||||
|
@ -153,6 +170,8 @@ protected:
|
|||
static uint32_t sCount;
|
||||
|
||||
private:
|
||||
virtual ~Notification();
|
||||
|
||||
nsIPrincipal* GetPrincipal();
|
||||
};
|
||||
|
||||
|
|
|
@ -71,7 +71,8 @@ NotificationStorage.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
put: function(origin, id, title, dir, lang, body, tag, icon, alertName) {
|
||||
put: function(origin, id, title, dir, lang, body, tag, icon, alertName,
|
||||
data) {
|
||||
if (DEBUG) { debug("PUT: " + id + ": " + title); }
|
||||
var notification = {
|
||||
id: id,
|
||||
|
@ -83,7 +84,8 @@ NotificationStorage.prototype = {
|
|||
icon: icon,
|
||||
alertName: alertName,
|
||||
timestamp: new Date().getTime(),
|
||||
origin: origin
|
||||
origin: origin,
|
||||
data: data
|
||||
};
|
||||
|
||||
this._notifications[id] = notification;
|
||||
|
@ -202,7 +204,8 @@ NotificationStorage.prototype = {
|
|||
notification.lang,
|
||||
notification.body,
|
||||
notification.tag,
|
||||
notification.icon);
|
||||
notification.icon,
|
||||
notification.data);
|
||||
} catch (e) {
|
||||
if (DEBUG) { debug("Error calling callback handle: " + e); }
|
||||
}
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче