Merge latest green b2g-inbound changeset and mozilla-central; a=merge

This commit is contained in:
Ed Morley 2014-08-21 13:44:28 +01:00
Родитель 3959dd4484 7412a6ccf2
Коммит 453a8e5d2b
415 изменённых файлов: 6250 добавлений и 2071 удалений

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

@ -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); }
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше