Merge mozilla-central to mozilla-inbound to fix m1 orange

This commit is contained in:
Carsten "Tomcat" Book 2015-11-06 14:39:59 +01:00
commit 5738f07f6f
27 changed files with 75 additions and 3399 deletions

View File

@ -1412,8 +1412,6 @@ pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; im
#else
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
#endif
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
pref("loop.fxa_oauth.tokendata", "");
pref("loop.fxa_oauth.profile", "");
pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");

View File

@ -13,7 +13,6 @@
},
"globals": {
// Gecko + Loop Globals.
"CardDavImporter": true,
"Chat": false,
"ChromeWorker": false,
"CommonUtils": false,
@ -27,7 +26,6 @@
"gBrowser": false,
"gDNSService": false,
"gLoopBundle": false,
"GoogleImporter": true,
"gWM": false,
"HawkClient": false,
"injectLoopAPI": true,
@ -36,11 +34,9 @@
"log": true,
"LOOP_SESSION_TYPE": true,
"LoopCalls": true,
"LoopContacts": true,
"loopCrypto": false,
"LoopRooms": true,
"LoopRoomsCache": true,
"LoopStorage": true,
"MozLoopPushHandler": true,
"MozLoopService": true,
"OS": false,

View File

@ -611,7 +611,7 @@ loop.panel = (function(_, mozL10n) {
className: "dropdown-menu-item",
onClick: this.props.handleDeleteButtonClick,
ref: "deleteButton"},
mozL10n.get("delete_conversation_menuitem")
mozL10n.get("delete_conversation_menuitem2")
)
)
);

View File

@ -611,7 +611,7 @@ loop.panel = (function(_, mozL10n) {
className="dropdown-menu-item"
onClick={this.props.handleDeleteButtonClick}
ref="deleteButton">
{mozL10n.get("delete_conversation_menuitem")}
{mozL10n.get("delete_conversation_menuitem2")}
</li>
</ul>
);

View File

@ -1,463 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
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://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Log.jsm");
this.EXPORTED_SYMBOLS = ["CardDavImporter"];
var log = Log.repository.getLogger("Loop.Importer.CardDAV");
log.level = Log.Level.Debug;
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
const DEPTH_RESOURCE_ONLY = "0";
const DEPTH_RESOURCE_AND_CHILDREN = "1";
const DEPTH_RESOURCE_AND_ALL_DESCENDENTS = "infinity";
this.CardDavImporter = function() {
};
/**
* CardDAV Address Book importer for Loop.
*
* The model for address book importers is to have a single public method,
* "startImport." When the import is done (or upon a fatal error), the
* caller's callback method is called.
*
* The current model for this importer is based on the subset of CardDAV
* implemented by Google. In theory, it should work with other CardDAV
* sources, but it has only been tested against Google at the moment.
*
* At the moment, this importer assumes that no local changes will be made
* to data retreived from a remote source: when performing a re-import,
* any records that have been previously imported will be completely
* removed and replaced with the data received from the CardDAV server.
* Witout this behavior, it would be impossible for users to take any
* actions to remove fields that are no longer valid.
*/
this.CardDavImporter.prototype = {
/**
* Begin import of an address book from a CardDAV server.
*
* @param {Object} options Information needed to perform the address
* book import. The following fields are currently
* defined:
* - "host": CardDAV server base address
* (e.g., "google.com")
* - "auth": Authentication mechanism to use.
* Currently, only "basic" is implemented.
* - "user": Username to use for basic auth
* - "password": Password to use for basic auth
* @param {Function} callback Callback function that will be invoked once the
* import operation is complete. The first argument
* passed to the callback will be an 'Error' object
* or 'null'. If the import operation was
* successful, then the second parameter will be a
* count of the number of contacts that were
* successfully imported.
* @param {Object} db Database to add imported contacts into.
* Nominally, this is the LoopContacts API. In
* practice, anything with the same interface
* should work here.
*/
startImport: function(options, callback, db) {
let auth;
if (!("auth" in options)) {
callback(new Error("No authentication specified"));
return;
}
if (options.auth === "basic") {
if (!("user" in options) || !("password" in options)) {
callback(new Error("Missing user or password for basic authentication"));
return;
}
auth = { method: "basic",
user: options.user,
password: options.password };
} else {
callback(new Error("Unknown authentication method"));
return;
}
if (!("host" in options)) {
callback(new Error("Missing host for CardDav import"));
return;
}
let host = options.host;
Task.spawn(function* () {
log.info("Starting CardDAV import from " + host);
let baseURL = "https://" + host;
let startURL = baseURL + "/.well-known/carddav";
let abookURL;
// Get list of contact URLs
let body = "<d:propfind xmlns:d='DAV:'><d:prop><d:getetag />" +
"</d:prop></d:propfind>";
let abook = yield this._davPromise("PROPFIND", startURL, auth,
DEPTH_RESOURCE_AND_CHILDREN, body);
// Build multiget REPORT body from URLs in PROPFIND result
let contactElements = abook.responseXML.getElementsByTagNameNS(
"DAV:", "href");
body = "<c:addressbook-multiget xmlns:d='DAV:' " +
"xmlns:c='urn:ietf:params:xml:ns:carddav'>" +
"<d:prop><d:getetag /> <c:address-data /></d:prop>\n";
for (let element of contactElements) {
let href = element.textContent;
if (href.substr(-1) == "/") {
abookURL = baseURL + href;
} else {
body += "<d:href>" + href + "</d:href>\n";
}
}
body += "</c:addressbook-multiget>";
// Retreive contact URL contents
let allEntries = yield this._davPromise("REPORT", abookURL, auth,
DEPTH_RESOURCE_AND_CHILDREN,
body);
// Parse multiget entites and add to DB
let addressData = allEntries.responseXML.getElementsByTagNameNS(
"urn:ietf:params:xml:ns:carddav", "address-data");
log.info("Retreived " + addressData.length + " contacts from " +
host + "; importing into database");
let importCount = 0;
for (let i = 0; i < addressData.length; i++) {
let vcard = addressData.item(i).textContent;
let contact = this._convertVcard(vcard);
contact.id += "@" + host;
contact.category = ["carddav@" + host];
let existing = yield this._dbPromise(db, "getByServiceId", contact.id);
if (existing) {
yield this._dbPromise(db, "remove", existing._guid);
}
// If the contact contains neither email nor phone number, then it
// is not useful in the Loop address book: do not add.
if (!("tel" in contact) && !("email" in contact)) {
continue;
}
yield this._dbPromise(db, "add", contact);
importCount++;
}
return importCount;
}.bind(this)).then(
(result) => {
log.info("Import complete: " + result + " contacts imported.");
callback(null, result);
},
(error) => {
log.error("Aborting import: " + error.fileName + ":" +
error.lineNumber + ": " + error.message);
callback(error);
}).then(null,
(error) => {
log.error("Error in callback: " + error.fileName +
":" + error.lineNumber + ": " + error.message);
callback(error);
}).then(null,
(error) => {
log.error("Error calling failure callback, giving up: " +
error.fileName + ":" + error.lineNumber + ": " +
error.message);
});
},
/**
* Wrap a LoopContacts-style operation in a promise. The operation is run
* immediately, and a corresponding Promise is returned. Error callbacks
* cause the promise to be rejected, and success cause it to be resolved.
*
* @param {Object} db Object the operation is to be performed on
* @param {String} method Name of operation being wrapped
* @param {Object} param Parameter to be passed to the operation
*
* @return {Object} Promise corresponding to the result of the operation.
*/
_dbPromise: function(db, method, param) {
return new Promise((resolve, reject) => {
db[method](param, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
},
/**
* Converts a contact in VCard format (see RFC 6350) to the format used
* by the LoopContacts class.
*
* @param {String} vcard The contact to convert, in vcard format
* @return {Object} a LoopContacts-style contact object containing
* the relevant fields from the vcard.
*/
_convertVcard: function(vcard) {
let contact = {};
let nickname;
vcard.split(/[\r\n]+(?! )/).forEach(
function(contentline) {
contentline = contentline.replace(/[\r\n]+ /g, "");
let match = /^(.*?[^\\]):(.*)$/.exec(contentline);
if (match) {
let nameparam = match[1];
let value = match[2];
// Poor-man's unescaping
value = value.replace(/\\:/g, ":");
value = value.replace(/\\,/g, ",");
value = value.replace(/\\n/gi, "\n");
value = value.replace(/\\\\/g, "\\");
let param = nameparam.split(/;/);
let name = param[0];
let pref = false;
let type = [];
for (let i = 1; i < param.length; i++) {
if (/^PREF/.exec(param[i]) || /^TYPE=PREF/.exec(param[i])) {
pref = true;
}
let typeMatch = /^TYPE=(.*)/.exec(param[i]);
if (typeMatch) {
type.push(typeMatch[1].toLowerCase());
}
}
if (!type.length) {
type.push("other");
}
if (name === "FN") {
value = value.replace(/\\;/g, ";");
contact.name = [value];
}
if (name === "N") {
// Because we don't have lookbehinds, matching unescaped
// semicolons is a pain. Luckily, we know that \r and \n
// cannot appear in the strings, so we use them to swap
// unescaped semicolons for \n.
value = value.replace(/\\;/g, "\r");
value = value.replace(/;/g, "\n");
value = value.replace(/\r/g, ";");
let family, given, additional, prefix, suffix;
let values = value.split(/\n/);
if (values.length >= 5) {
[family, given, additional, prefix, suffix] = values;
if (prefix.length) {
contact.honorificPrefix = [prefix];
}
if (given.length) {
contact.givenName = [given];
}
if (additional.length) {
contact.additionalName = [additional];
}
if (family.length) {
contact.familyName = [family];
}
if (suffix.length) {
contact.honorificSuffix = [suffix];
}
}
}
if (name === "EMAIL") {
value = value.replace(/\\;/g, ";");
if (!("email" in contact)) {
contact.email = [];
}
contact.email.push({
pref: pref,
type: type,
value: value
});
}
if (name === "NICKNAME") {
value = value.replace(/\\;/g, ";");
// We don't store nickname in contact because it's not
// a supported field. We're saving it off here in case we
// need to use it if the fullname is blank.
nickname = value;
}
if (name === "ADR") {
value = value.replace(/\\;/g, "\r");
value = value.replace(/;/g, "\n");
value = value.replace(/\r/g, ";");
let pobox, extra, street, locality, region, code, country;
let values = value.split(/\n/);
if (values.length >= 7) {
[pobox, extra, street, locality, region, code, country] = values;
if (!("adr" in contact)) {
contact.adr = [];
}
contact.adr.push({
pref: pref,
type: type,
streetAddress: (street || pobox) + (extra ? (" " + extra) : ""),
locality: locality,
region: region,
postalCode: code,
countryName: country
});
}
}
if (name === "TEL") {
value = value.replace(/\\;/g, ";");
if (!("tel" in contact)) {
contact.tel = [];
}
contact.tel.push({
pref: pref,
type: type,
value: value
});
}
if (name === "ORG") {
value = value.replace(/\\;/g, "\r");
value = value.replace(/;/g, "\n");
value = value.replace(/\r/g, ";");
if (!("org" in contact)) {
contact.org = [];
}
contact.org.push(value.replace(/\n.*/, ""));
}
if (name === "TITLE") {
value = value.replace(/\\;/g, ";");
if (!("jobTitle" in contact)) {
contact.jobTitle = [];
}
contact.jobTitle.push(value);
}
if (name === "BDAY") {
value = value.replace(/\\;/g, ";");
contact.bday = Date.parse(value);
}
if (name === "UID") {
contact.id = value;
}
if (name === "NOTE") {
value = value.replace(/\\;/g, ";");
if (!("note" in contact)) {
contact.note = [];
}
contact.note.push(value);
}
}
}
);
// Basic sanity checking: make sure the name field isn't empty
if (!("name" in contact) || contact.name[0].length == 0) {
if (("familyName" in contact) && ("givenName" in contact)) {
// First, try to synthesize a full name from the name fields.
// Ordering is culturally sensitive, but we don't have
// cultural origin information available here. The best we
// can really do is "family, given additional"
contact.name = [contact.familyName[0] + ", " + contact.givenName[0]];
if (("additionalName" in contact)) {
contact.name[0] += " " + contact.additionalName[0];
}
} else {
if (nickname) {
contact.name = [nickname];
} else if ("familyName" in contact) {
contact.name = [contact.familyName[0]];
} else if ("givenName" in contact) {
contact.name = [contact.givenName[0]];
} else if ("org" in contact) {
contact.name = [contact.org[0]];
} else if ("email" in contact) {
contact.name = [contact.email[0].value];
} else if ("tel" in contact) {
contact.name = [contact.tel[0].value];
}
}
}
return contact;
},
/**
* Issues a CardDAV request (see RFC 6352) and returns a Promise to represent
* the success or failure state of the request.
*
* @param {String} method WebDAV method to use (e.g., "PROPFIND")
* @param {String} url HTTP URL to use for the request
* @param {Object} auth Object with authentication-related configuration.
* See documentation for startImport for details.
* @param {Number} depth Value to use for the WebDAV (HTTP) "Depth" header
* @param {String} body Body to include in the WebDAV (HTTP) request
*
* @return {Object} Promise representing the request operation outcome.
* If resolved, the resolution value is the XMLHttpRequest
* that was used to perform the request.
*/
_davPromise: function(method, url, auth, depth, body) {
return new Promise((resolve, reject) => {
let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(
Ci.nsIXMLHttpRequest);
let user = "";
let password = "";
if (auth.method == "basic") {
user = auth.user;
password = auth.password;
}
req.open(method, url, true, user, password);
req.setRequestHeader("Depth", depth);
req.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
req.onload = function() {
if (req.status < 400) {
resolve(req);
} else {
reject(new Error(req.status + " " + req.statusText));
}
};
req.onerror = function(error) {
reject(error);
};
req.send(body);
});
}
};

View File

@ -1,603 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
this.EXPORTED_SYMBOLS = ["GoogleImporter"];
var log = Log.repository.getLogger("Loop.Importer.Google");
log.level = Log.Level.Debug;
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
/**
* Helper function that reads and maps the respective node value from specific
* XML DOMNodes to fields on a `target` object.
* Example: the value for field 'fullName' can be read from the XML DOMNode
* 'name', so that's the mapping we need to make; get the nodeValue of
* the node called 'name' and tack it to the target objects' 'fullName'
* property.
*
* @param {Map} fieldMap Map object containing the field name -> node
* name mapping
* @param {XMLDOMNode} node DOM node to fetch the values from for each field
* @param {String} ns XML namespace for the DOM nodes to retrieve. Optional.
* @param {Object} target Object to store the values found. Optional.
* Defaults to a new object.
* @param {Boolean} wrapInArray Indicates whether to map the field values in
* an Array. Optional. Defaults to `false`.
* @returns The `target` object with the node values mapped to the appropriate fields.
*/
const extractFieldsFromNode = function(fieldMap, node, ns = null, target = {}, wrapInArray = false) {
for (let [field, nodeName] of fieldMap) {
let nodeList = ns ? node.getElementsByTagNameNS(ns, nodeName) :
node.getElementsByTagName(nodeName);
if (nodeList.length) {
if (!nodeList[0].firstChild) {
continue;
}
let value = nodeList[0].textContent;
target[field] = wrapInArray ? [value] : value;
}
}
return target;
};
/**
* Helper function that reads the type of (email-)address or phone number from an
* XMLDOMNode.
*
* @param {XMLDOMNode} node
* @returns String that depicts the type of field value.
*/
const getFieldType = function(node) {
if (node.hasAttribute("rel")) {
let rel = node.getAttribute("rel");
// The 'rel' attribute is formatted like: http://schemas.google.com/g/2005#work.
return rel.substr(rel.lastIndexOf("#") + 1);
}
if (node.hasAttribute("label")) {
return node.getAttribute("label");
}
return "other";
};
/**
* Fetch the preferred entry of a contact. Returns the first entry when no
* preferred flag is set.
*
* @param {Object} contact The contact object to check for preferred entries
* @param {String} which Type of entry to check. Optional, defaults to 'email'
* @throws An Error when no (preferred) entries are listed for this contact.
*/
const getPreferred = function(contact, which = "email") {
if (!(which in contact) || !contact[which].length) {
throw new Error("No " + which + " entry available.");
}
let preferred = contact[which][0];
contact[which].some(function(entry) {
if (entry.pref) {
preferred = entry;
return true;
}
return false;
});
return preferred;
};
/**
* Fetch an auth token (clientID or client secret), which may be overridden by
* a pref if it's set.
*
* @param {String} paramValue Initial, default, value of the parameter
* @param {String} prefName Fully qualified name of the pref to check for
* @param {Boolean} encode Whether to URLEncode the param string
*/
const getUrlParam = function(paramValue, prefName, encode = true) {
if (Services.prefs.getPrefType(prefName)) {
paramValue = Services.prefs.getCharPref(prefName);
}
paramValue = Services.urlFormatter.formatURL(paramValue);
return encode ? encodeURIComponent(paramValue) : paramValue;
};
var gAuthWindow, gProfileId;
const kAuthWindowSize = {
width: 420,
height: 460
};
const kContactsMaxResults = 10000000;
const kContactsChunkSize = 100;
const kTitlebarPollTimeout = 200;
const kNS_GD = "http://schemas.google.com/g/2005";
/**
* GoogleImporter class.
*
* Main entrypoint is the `startImport` method which calls several tasks necessary
* to import contacts from Google.
* Authentication is performed using an OAuth strategy which is loaded in a popup
* window.
*/
this.GoogleImporter = function() {};
this.GoogleImporter.prototype = {
/**
* Start the import process of contacts from the Google service, using its Contacts
* API - https://developers.google.com/google-apps/contacts/v3/.
* The import consists of four tasks:
* 1. Get the authentication code which can be used to retrieve an OAuth token
* pair. This is the bulk of the authentication flow that will be handled in
* a popup window by Google. The user will need to login to the Google service
* with his or her account and grant permission to our app to manage their
* contacts.
* 2. Get the tokenset from the Google service, using the authentication code
* that was retrieved in task 1.
* 3. Fetch all the contacts from the Google service, using the OAuth tokenset
* that was retrieved in task 2.
* 4. Process the contacts, map them to the MozContact format and store each
* contact in the database, if it doesn't exist yet.
*
* @param {Object} options Options to control the behavior of the import.
* Not used by this importer class.
* @param {Function} callback Function to invoke when the import process
* is done or when an error occurs that halts
* the import process. The first argument passed
* in an Error object or `null` and the second
* argument is an object with import statistics.
* @param {LoopContacts} db Instance of the LoopContacts database object,
* which will store the newly found contacts
* @param {nsIDomWindow} windowRef Reference to the ChromeWindow the import is
* invoked from. It will be used to be able to
* open a window for the OAuth process with chrome
* privileges.
*/
startImport: function(options, callback, db, windowRef) {
Task.spawn(function* () {
let code = yield this._promiseAuthCode(windowRef);
let tokenSet = yield this._promiseTokenSet(code);
let contactEntries = yield this._getContactEntries(tokenSet);
let { total, success, ids } = yield this._processContacts(contactEntries, db, tokenSet);
yield this._purgeContacts(ids, db);
return {
total: total,
success: success
};
}.bind(this)).then(stats => callback(null, stats),
error => callback(error))
.then(null, ex => log.error(ex.fileName + ":" + ex.lineNumber + ": " + ex.message));
},
/**
* Task that yields an authentication code that is returned after the user signs
* in to the Google service. This code can be used by this class to retrieve an
* OAuth tokenset.
*
* @param {nsIDOMWindow} windowRef Reference to the ChromeWindow the import is
* invoked from. It will be used to be able to
* open a window for the OAuth process with chrome
* privileges.
* @throws An `Error` object when authentication fails, or the authentication
* code as a String.
*/
_promiseAuthCode: Task.async(function* (windowRef) {
// Close a window that got lost in a previous login attempt.
if (gAuthWindow && !gAuthWindow.closed) {
gAuthWindow.close();
gAuthWindow = null;
}
let url = getUrlParam("https://accounts.google.com/o/oauth2/",
"loop.oauth.google.URL", false) +
"auth?response_type=code&client_id=" +
getUrlParam("%GOOGLE_OAUTH_API_CLIENTID%", "loop.oauth.google.clientIdOverride");
for (let param of ["redirect_uri", "scope"]) {
url += "&" + param + "=" + encodeURIComponent(
Services.prefs.getCharPref("loop.oauth.google." + param));
}
const features = "centerscreen,resizable=yes,toolbar=no,menubar=no,status=no,directories=no," +
"width=" + kAuthWindowSize.width + ",height=" + kAuthWindowSize.height;
gAuthWindow = windowRef.openDialog(windowRef.getBrowserURL(), "_blank", features, url);
gAuthWindow.focus();
let code;
function promiseTimeOut() {
return new Promise(resolve => {
setTimeout(resolve, kTitlebarPollTimeout);
});
}
// The following loops runs as long as the OAuth windows' titlebar doesn't
// yield a response from the Google service. If an error occurs, the loop
// will terminate early.
while (!code) {
if (!gAuthWindow || gAuthWindow.closed) {
throw new Error("Popup window was closed before authentication succeeded");
}
let matches = gAuthWindow.document.title.match(/(error|code)=([^\s]+)/);
if (matches && matches.length) {
let [, type, message] = matches;
gAuthWindow.close();
gAuthWindow = null;
if (type == "error") {
throw new Error("Google authentication failed with error: " + message.trim());
} else if (type == "code") {
code = message.trim();
} else {
throw new Error("Unknown response from Google");
}
} else {
yield promiseTimeOut();
}
}
return code;
}),
/**
* Fetch an OAuth tokenset, that will be used to authenticate Google API calls,
* using the authentication token retrieved in `_promiseAuthCode`.
*
* @param {String} code The authentication code.
* @returns an `Error` object upon failure or an object containing OAuth tokens.
*/
_promiseTokenSet: function(code) {
return new Promise(function(resolve, reject) {
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
request.open("POST", getUrlParam("https://accounts.google.com/o/oauth2/",
"loop.oauth.google.URL",
false) + "token");
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.onload = function() {
if (request.status < 400) {
let tokenSet = JSON.parse(request.responseText);
tokenSet.date = Date.now();
resolve(tokenSet);
} else {
reject(new Error(request.status + " " + request.statusText));
}
};
request.onerror = function(error) {
reject(error);
};
let body = "grant_type=authorization_code&code=" + encodeURIComponent(code) +
"&client_id=" + getUrlParam("%GOOGLE_OAUTH_API_CLIENTID%",
"loop.oauth.google.clientIdOverride") +
"&client_secret=" + getUrlParam("%GOOGLE_OAUTH_API_KEY%",
"loop.oauth.google.clientSecretOverride") +
"&redirect_uri=" + encodeURIComponent(Services.prefs.getCharPref(
"loop.oauth.google.redirect_uri"));
request.send(body);
});
},
_promiseRequestXML: function(URL, tokenSet) {
return new Promise((resolve, reject) => {
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
request.open("GET", URL);
request.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
request.setRequestHeader("GData-Version", "3.0");
request.setRequestHeader("Authorization", "Bearer " + tokenSet.access_token);
request.onload = function() {
if (request.status < 400) {
let doc = request.responseXML;
// First get the profile id, which is present in each XML request.
let currNode = doc.documentElement.firstChild;
while (currNode) {
if (currNode.nodeType == 1 && currNode.localName == "id") {
gProfileId = currNode.textContent;
break;
}
currNode = currNode.nextSibling;
}
resolve(doc);
} else {
reject(new Error(request.status + " " + request.statusText));
}
};
request.onerror = function(error) {
reject(error);
};
request.send();
});
},
/**
* Fetches all the contacts in a users' address book.
*
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contacts
*
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
* @returns An `Error` object upon failure or an Array of contact XML nodes.
*/
_getContactEntries: Task.async(function* (tokenSet) {
let URL = getUrlParam("https://www.google.com/m8/feeds/contacts/default/full",
"loop.oauth.google.getContactsURL",
false) + "?max-results=" + kContactsMaxResults;
let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
// Then kick of the importing of contact entries.
return Array.prototype.slice.call(xmlDoc.querySelectorAll("entry"));
}),
/**
* Fetches the default group from a users' address book, called 'Contacts'.
*
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contact_groups
*
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
* @returns An `Error` object upon failure or the String group ID.
*/
_getContactsGroupId: Task.async(function* (tokenSet) {
let URL = getUrlParam("https://www.google.com/m8/feeds/groups/default/full",
"loop.oauth.google.getGroupsURL",
false) + "?max-results=" + kContactsMaxResults;
let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
let contactsEntry = xmlDoc.querySelector("systemGroup[id=\"Contacts\"]");
if (!contactsEntry) {
throw new Error("Contacts group not present");
}
// Select the actual <entry> node, which is the parent of the <systemGroup>
// node we just selected.
contactsEntry = contactsEntry.parentNode;
return contactsEntry.getElementsByTagName("id")[0].textContent;
}),
/**
* Process the contact XML nodes that Google provides, convert them to the MozContact
* format, check if the contact already exists in the database and when it doesn't,
* store it permanently.
* During this process statistics are collected about the amount of successful
* imports. The consumer of this class may use these statistics to inform the
* user.
* Note: only contacts that are part of the 'Contacts' system group will be
* imported.
*
* @param {Array} contactEntries List of XML DOMNodes contact entries.
* @param {LoopContacts} db Instance of the LoopContacts database
* object, which will store the newly found
* contacts.
* @param {Object} tokenSet OAuth tokenset used to authenticate a
* request
* @returns An `Error` object upon failure or an Object with statistics in the
* following format: `{ total: 25, success: 13, ids: {} }`.
*/
_processContacts: Task.async(function* (contactEntries, db, tokenSet) {
let stats = {
total: contactEntries.length,
success: 0,
ids: {}
};
// Contacts that are _not_ part of the 'Contacts' group will be ignored.
let contactsGroupId = yield this._getContactsGroupId(tokenSet);
for (let entry of contactEntries) {
let contact = this._processContactFields(entry);
stats.ids[contact.id] = 1;
let existing = yield db.promise("getByServiceId", contact.id);
if (existing) {
yield db.promise("remove", existing._guid);
}
// After contact removal, check if the entry is part of the correct group.
if (!entry.querySelector("groupMembershipInfo[deleted=\"false\"][href=\"" +
contactsGroupId + "\"]")) {
continue;
}
// If the contact contains neither email nor phone number, then it is not
// useful in the Loop address book: do not add.
if (!("email" in contact) && !("tel" in contact)) {
continue;
}
yield db.promise("add", contact);
stats.success++;
}
return stats;
}),
/**
* Parse an XML node to map the appropriate data to MozContact field equivalents.
*
* @param {XMLDOMNode} entry The contact XML node in Google format to process.
* @returns `null` if the contact entry appears to be invalid or an Object containing
* all the contact data found in the XML.
*/
_processContactFields: function(entry) {
// Basic fields in the main 'atom' namespace.
let contact = extractFieldsFromNode(new Map([
["id", "id"],
// published: n/a
["updated", "updated"]
// bday: n/a
]), entry);
// Fields that need to wrapped in an Array.
extractFieldsFromNode(new Map([
["name", "fullName"],
["givenName", "givenName"],
["familyName", "familyName"],
["additionalName", "additionalName"]
]), entry, kNS_GD, contact, true);
// The 'note' field needs to wrapped in an array, but its source node is not
// namespaced.
extractFieldsFromNode(new Map([
["note", "content"]
]), entry, null, contact, true);
// Process physical, earthly addresses.
let addressNodes = entry.getElementsByTagNameNS(kNS_GD, "structuredPostalAddress");
if (addressNodes.length) {
contact.adr = [];
for (let [, addressNode] of Iterator(addressNodes)) {
let adr = extractFieldsFromNode(new Map([
["countryName", "country"],
["locality", "city"],
["postalCode", "postcode"],
["region", "region"],
["streetAddress", "street"]
]), addressNode, kNS_GD);
if (Object.keys(adr).length) {
adr.pref = (addressNode.getAttribute("primary") == "true");
adr.type = [getFieldType(addressNode)];
contact.adr.push(adr);
}
}
}
// Process email addresses.
let emailNodes = entry.getElementsByTagNameNS(kNS_GD, "email");
if (emailNodes.length) {
contact.email = [];
for (let [, emailNode] of Iterator(emailNodes)) {
contact.email.push({
pref: (emailNode.getAttribute("primary") == "true"),
type: [getFieldType(emailNode)],
value: emailNode.getAttribute("address")
});
}
}
// Process telephone numbers.
let phoneNodes = entry.getElementsByTagNameNS(kNS_GD, "phoneNumber");
if (phoneNodes.length) {
contact.tel = [];
for (let [, phoneNode] of Iterator(phoneNodes)) {
let phoneNumber = phoneNode.hasAttribute("uri") ?
phoneNode.getAttribute("uri").replace("tel:", "") :
phoneNode.textContent;
contact.tel.push({
pref: (phoneNode.getAttribute("primary") == "true"),
type: [getFieldType(phoneNode)],
value: phoneNumber
});
}
}
let orgNodes = entry.getElementsByTagNameNS(kNS_GD, "organization");
if (orgNodes.length) {
contact.org = [];
contact.jobTitle = [];
for (let [, orgNode] of Iterator(orgNodes)) {
let orgElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0];
let titleElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0];
contact.org.push(orgElement ? orgElement.textContent : "");
contact.jobTitle.push(titleElement ? titleElement.textContent : "");
}
}
contact.category = ["google"];
// Basic sanity checking: make sure the name field isn't empty
if (!("name" in contact) || contact.name[0].length == 0) {
if (("familyName" in contact) && ("givenName" in contact)) {
// First, try to synthesize a full name from the name fields.
// Ordering is culturally sensitive, but we don't have
// cultural origin information available here. The best we
// can really do is "family, given additional"
contact.name = [contact.familyName[0] + ", " + contact.givenName[0]];
if (("additionalName" in contact)) {
contact.name[0] += " " + contact.additionalName[0];
}
} else {
let profileTitle = extractFieldsFromNode(new Map([["title", "title"]]), entry);
if (("title" in profileTitle)) {
contact.name = [profileTitle.title];
} else if ("familyName" in contact) {
contact.name = [contact.familyName[0]];
} else if ("givenName" in contact) {
contact.name = [contact.givenName[0]];
} else if ("org" in contact) {
contact.name = [contact.org[0]];
} else {
let email;
try {
email = getPreferred(contact);
} catch (ex) {
// Do nothing
}
if (email) {
contact.name = [email.value];
} else {
let tel;
try {
tel = getPreferred(contact, "tel");
} catch (ex) {
// Do nothing
}
if (tel) {
contact.name = [tel.value];
}
}
}
}
}
return contact;
},
/**
* Remove all contacts from the database that are not present anymore in the
* remote data-source.
*
* @param {Object} ids Map of IDs collected earlier of all the contacts
* that are available on the remote data-source
* @param {LoopContacts} db Instance of the LoopContacts database object, which
* will store the newly found contacts
*/
_purgeContacts: Task.async(function* (ids, db) {
let contacts = yield db.promise("getAll");
let profileId = "https://www.google.com/m8/feeds/contacts/" + encodeURIComponent(gProfileId);
let processed = 0;
function promiseSkipABeat() {
return new Promise(resolve => Services.tm.currentThread.dispatch(resolve,
Ci.nsIThread.DISPATCH_NORMAL));
}
for (let [guid, contact] of Iterator(contacts)) {
if (++processed % kContactsChunkSize === 0) {
// Skip a beat every time we processed a chunk.
yield promiseSkipABeat;
}
if (contact.id.indexOf(profileId) >= 0 && !ids[contact.id]) {
yield db.promise("remove", guid);
}
}
})
};

View File

@ -1,961 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CardDavImporter",
"resource:///modules/loop/CardDavImporter.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "GoogleImporter",
"resource:///modules/loop/GoogleImporter.jsm");
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
const { EventEmitter } = Cu.import("resource://devtools/shared/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.
*/
var LoopContactsInternal = Object.freeze({
/**
* Map of contact importer names to instances
*/
_importServices: {
"carddav": new CardDavImporter(),
"google": new GoogleImporter()
},
/**
* 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, (error, store) => {
if (error) {
callback(error);
return;
}
let request;
try {
request = store.delete(guid);
} catch (ex) {
callback(ex);
return;
}
request.onsuccess = event => {
if (contact) {
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.
* If no object matching guid could be found,
* then the callback is called with both arguments
* set to `null`.
*/
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(null, null);
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.
* If no object matching serviceId could be found,
* then the callback is called with both arguments
* set to `null`.
*/
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(null, null);
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;
}
if (!contact) {
callback(new Error("Contact with " + kKeyPath + " '" +
guid + "' could not be found"));
return;
}
LoopStorage.getStore(kObjectStoreName, (error, store) => {
if (error) {
callback(error);
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;
}
if (!contact) {
callback(new Error("Contact with " + kKeyPath + " '" +
guid + "' could not be found"));
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;
}
if (!contact) {
callback(new Error("Contact with " + kKeyPath + " '" +
guid + "' could not be found"));
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, windowRef, callback) {
if (!("service" in options)) {
callback(new Error("No import service specified in options"));
return;
}
if (!(options.service in this._importServices)) {
callback(new Error("Unknown import service specified: " + options.service));
return;
}
this._importServices[options.service].startImport(options, callback,
LoopContacts, windowRef);
},
/**
* Search through the data store for contacts that match a certain (sub-)string.
* NB: The current implementation is very simple, naive if you will; we fetch
* _all_ the contacts via `getAll()` and iterate over all of them to find
* the contacts matching the supplied query (brute-force search in
* exponential time).
*
* @param {Object} 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.
*
* Example:
* LoopContacts.search({
* q: "foo@bar.com",
* field: "email" // 'email' is the default.
* }, function(err, contacts) {
* if (err) {
* throw err;
* }
* console.dir(contacts);
* });
*/
search: function(query, callback) {
if (!("q" in query) || !query.q) {
callback(new Error("Nothing to search for. 'q' is required."));
return;
}
if (!("field" in query)) {
query.field = "email";
}
let queryValue = query.q;
if (query.field == "tel") {
queryValue = queryValue.replace(/[\D]+/g, "");
}
const checkForMatch = function(fieldValue) {
if (typeof fieldValue == "string") {
if (query.field == "tel") {
return fieldValue.replace(/[\D]+/g, "").endsWith(queryValue);
}
return fieldValue == queryValue;
}
if (typeof fieldValue == "number" || typeof fieldValue == "boolean") {
return fieldValue == queryValue;
}
if ("value" in fieldValue) {
return checkForMatch(fieldValue.value);
}
return false;
};
let foundContacts = [];
this.getAll((err, contacts) => {
if (err) {
callback(err);
return;
}
for (let contact of contacts) {
let matchWith = contact[query.field];
if (!matchWith) {
continue;
}
// Many fields are defined as Arrays.
if (Array.isArray(matchWith)) {
for (let fieldValue of matchWith) {
if (checkForMatch(fieldValue)) {
foundContacts.push(contact);
break;
}
}
} else if (checkForMatch(matchWith)) {
foundContacts.push(contact);
}
}
callback(null, foundContacts);
});
}
});
/**
* 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, windowRef, callback) {
return LoopContactsInternal.startImport(options, windowRef, callback);
},
search: function(query, callback) {
return LoopContactsInternal.search(query, callback);
},
promise: function(method, ...params) {
return new Promise((resolve, reject) => {
this[method](...params, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
},
on: (...params) => eventEmitter.on(...params),
once: (...params) => eventEmitter.once(...params),
off: (...params) => eventEmitter.off(...params)
});

View File

@ -11,13 +11,8 @@ 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/LoopRooms.jsm");
Cu.import("resource:///modules/loop/LoopContacts.jsm");
Cu.importGlobalProperties(["Blob"]);
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
"resource:///modules/loop/LoopContacts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
"resource://gre/modules/MozSocialAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
@ -223,7 +218,6 @@ function injectLoopAPI(targetWindow) {
let ringer;
let ringerStopper;
let appVersionInfo;
let contactsAPI;
let roomsAPI;
let callsAPI;
let savedWindowListeners = new Map();
@ -386,27 +380,6 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* Returns the contacts API.
*
* @returns {Object} The contacts API object
*/
contacts: {
enumerable: true,
get: function() {
if (contactsAPI) {
return contactsAPI;
}
// Make a database switch when a userProfile is active already.
let profile = MozLoopService.userProfile;
if (profile) {
LoopStorage.switchDatabase(profile.uid);
}
return contactsAPI = injectObjectAPI(LoopContacts, targetWindow);
}
},
/**
* Returns the rooms API.
*
@ -422,25 +395,6 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* 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: {
enumerable: true,
writable: true,
value: function(options, callback) {
LoopContacts.startImport(options, getChromeWindow(targetWindow), function(...results) {
invokeCallback(callback, ...[cloneValueInto(r, targetWindow) for (r of results)]);
});
}
},
/**
* Returns translated strings associated with an element. Designed
* for use with l10n.js

View File

@ -147,12 +147,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
"resource://services-common/hawkrequest.js");
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
"resource:///modules/loop/LoopContacts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms",
"resource:///modules/loop/LoopRooms.jsm");
@ -307,7 +301,6 @@ var MozLoopServiceInternal = {
notifyStatusChanged: function(aReason = null) {
log.debug("notifyStatusChanged with reason:", aReason);
let profile = MozLoopService.userProfile;
LoopStorage.switchDatabase(profile && profile.uid);
LoopRooms.maybeRefresh(profile && profile.uid);
Services.obs.notifyObservers(null, "loop-status-changed", aReason);
},

View File

@ -15,12 +15,8 @@ BROWSER_CHROME_MANIFESTS += [
EXTRA_JS_MODULES.loop += [
'content/shared/js/crypto.js',
'content/shared/js/utils.js',
'modules/CardDavImporter.jsm',
'modules/GoogleImporter.jsm',
'modules/LoopContacts.jsm',
'modules/LoopRooms.jsm',
'modules/LoopRoomsCache.jsm',
'modules/LoopStorage.jsm',
'modules/MozLoopAPI.jsm',
'modules/MozLoopPushHandler.jsm',
'modules/MozLoopService.jsm',

View File

@ -1,19 +1,11 @@
[DEFAULT]
support-files =
fixtures/google_auth.txt
fixtures/google_contacts.txt
fixtures/google_groups.txt
fixtures/google_token.txt
google_service.sjs
head.js
loop_fxa.sjs
test_loopLinkClicker_channel.html
../../../../base/content/test/general/browser_fxa_oauth_with_keys.html
[browser_CardDavImporter.js]
[browser_fxa_login.js]
[browser_GoogleImporter.js]
skip-if = e10s
[browser_loop_fxa_server.js]
[browser_LoopRooms_channel.js]
[browser_mozLoop_appVersionInfo.js]

View File

@ -1,326 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { CardDavImporter } = Cu.import("resource:///modules/loop/CardDavImporter.jsm", {});
const kAuth = {
"method": "basic",
"user": "username",
"password": "p455w0rd"
};
// "pid" for "provider ID"
var vcards = [
"VERSION:3.0\n" +
"N:Smith;John;;;\n" +
"FN:John Smith\n" +
"EMAIL;TYPE=work:john.smith@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid1\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"N:Smith;Jane;;;\n" +
"FN:Jane Smith\n" +
"EMAIL:jane.smith@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid2\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"N:García Fernández;Miguel Angel;José Antonio;Mr.;Jr.\n" +
"FN:Mr. Miguel Angel José Antonio\n García Fernández, Jr.\n" +
"EMAIL:mike@example.org\n" +
"EMAIL;PREF=1;TYPE=work:miguel.angel@example.net\n" +
"EMAIL;TYPE=home;UNKNOWNPARAMETER=frotz:majacf@example.com\n" +
"TEL:+3455555555\n" +
"TEL;PREF=1;TYPE=work:+3455556666\n" +
"TEL;TYPE=home;UNKNOWNPARAMETER=frotz:+3455557777\n" +
"ADR:;Suite 123;Calle Aduana\\, 29;MADRID;;28070;SPAIN\n" +
"ADR;TYPE=work:P.O. BOX 555;;;Washington;DC;20024-00555;USA\n" +
"ORG:Acme España SL\n" +
"TITLE:President\n" +
"BDAY:1965-05-05\n" +
"NOTE:Likes tulips\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid3\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"N:Jones;Bob;;;\n" +
"EMAIL:bob.jones@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid4\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"N:Jones;Davy;Randall;;\n" +
"EMAIL:davy.jones@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid5\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"EMAIL:trip@example.com\n" +
"NICKNAME:Trip\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid6\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"EMAIL:acme@example.com\n" +
"ORG:Acme, Inc.\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid7\n" +
"END:VCARD\n",
"VERSION:3.0\n" +
"EMAIL:anyone@example.com\n" +
"REV:2011-07-12T14:43:20Z\n" +
"UID:pid8\n" +
"END:VCARD\n"
];
const monkeyPatchImporter = function(importer) {
// Set up the response bodies
let listPropfind =
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<d:multistatus xmlns:card="urn:ietf:params:xml:ns:carddav"\n' +
' xmlns:d="DAV:">\n' +
" <d:response>\n" +
" <d:href>/carddav/abook/</d:href>\n" +
" <d:propstat>\n" +
" <d:status>HTTP/1.1 200 OK</d:status>\n" +
" </d:propstat>\n" +
" <d:propstat>\n" +
" <d:status>HTTP/1.1 404 Not Found</d:status>\n" +
" <d:prop>\n" +
" <d:getetag/>\n" +
" </d:prop>\n" +
" </d:propstat>\n" +
" </d:response>\n";
let listReportMultiget =
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<d:multistatus xmlns:card="urn:ietf:params:xml:ns:carddav"\n' +
' xmlns:d="DAV:">\n';
vcards.forEach(vcard => {
let uid = /\nUID:(.*?)\n/.exec(vcard);
listPropfind +=
" <d:response>\n" +
" <d:href>/carddav/abook/" + uid + "</d:href>\n" +
" <d:propstat>\n" +
" <d:status>HTTP/1.1 200 OK</d:status>\n" +
" <d:prop>\n" +
' <d:getetag>"2011-07-12T07:43:20.855-07:00"</d:getetag>\n' +
" </d:prop>\n" +
" </d:propstat>\n" +
" </d:response>\n";
listReportMultiget +=
" <d:response>\n" +
" <d:href>/carddav/abook/" + uid + "</d:href>\n" +
" <d:propstat>\n" +
" <d:status>HTTP/1.1 200 OK</d:status>\n" +
" <d:prop>\n" +
' <d:getetag>"2011-07-12T07:43:20.855-07:00"</d:getetag>\n' +
" <card:address-data>" + vcard + "</card:address-data>\n" +
" </d:prop>\n" +
" </d:propstat>\n" +
" </d:response>\n";
});
listPropfind += "</d:multistatus>\n";
listReportMultiget += "</d:multistatus>\n";
importer._davPromise = function(method, url, auth, depth, body) {
return new Promise((resolve, reject) => {
if (auth.method != "basic" ||
auth.user != kAuth.user ||
auth.password != kAuth.password) {
reject(new Error("401 Auth Failure"));
return;
}
let request = method + " " + url + " " + depth;
let xmlParser = new DOMParser();
let responseXML;
switch (request) {
case "PROPFIND https://example.com/.well-known/carddav 1":
responseXML = xmlParser.parseFromString(listPropfind, "text/xml");
break;
case "REPORT https://example.com/carddav/abook/ 1":
responseXML = xmlParser.parseFromString(listReportMultiget, "text/xml");
break;
default:
reject(new Error("404 Not Found"));
return;
}
resolve({ "responseXML": responseXML });
});
}.bind(importer);
return importer;
};
add_task(function* test_CardDavImport() {
let importer = monkeyPatchImporter(new CardDavImporter());
yield new Promise((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com",
"auth": kAuth.method,
"user": kAuth.user,
"password": kAuth.password
}, (err, result) => { err ? reject(err) : resolve(result); }, mockDb);
});
info("Import succeeded");
Assert.equal(vcards.length, Object.keys(mockDb._store).length,
"Should import all VCards into database");
// Basic checks
let c = mockDb._store[1];
Assert.equal(c.name[0], "John Smith", "Full name should match");
Assert.equal(c.givenName[0], "John", "Given name should match");
Assert.equal(c.familyName[0], "Smith", "Family name should match");
Assert.equal(c.email[0].type, "work", "Email type should match");
Assert.equal(c.email[0].value, "john.smith@example.com", "Email should match");
Assert.equal(c.email[0].pref, false, "Pref should match");
Assert.equal(c.id, "pid1@example.com", "UID should match and be scoped to provider");
c = mockDb._store[2];
Assert.equal(c.name[0], "Jane Smith", "Full name should match");
Assert.equal(c.givenName[0], "Jane", "Given name should match");
Assert.equal(c.familyName[0], "Smith", "Family name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "jane.smith@example.com", "Email should match");
Assert.equal(c.email[0].pref, false, "Pref should match");
Assert.equal(c.id, "pid2@example.com", "UID should match and be scoped to provider");
// Check every field
c = mockDb._store[3];
Assert.equal(c.name[0], "Mr. Miguel Angel José Antonio García Fernández, Jr.", "Full name should match");
Assert.equal(c.givenName[0], "Miguel Angel", "Given name should match");
Assert.equal(c.additionalName[0], "José Antonio", "Other name should match");
Assert.equal(c.familyName[0], "García Fernández", "Family name should match");
Assert.equal(c.email.length, 3, "Email count should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "mike@example.org", "Email should match");
Assert.equal(c.email[0].pref, false, "Pref should match");
Assert.equal(c.email[1].type, "work", "Email type should match");
Assert.equal(c.email[1].value, "miguel.angel@example.net", "Email should match");
Assert.equal(c.email[1].pref, true, "Pref should match");
Assert.equal(c.email[2].type, "home", "Email type should match");
Assert.equal(c.email[2].value, "majacf@example.com", "Email should match");
Assert.equal(c.email[2].pref, false, "Pref should match");
Assert.equal(c.tel.length, 3, "Phone number count should match");
Assert.equal(c.tel[0].type, "other", "Phone type should match");
Assert.equal(c.tel[0].value, "+3455555555", "Phone number should match");
Assert.equal(c.tel[0].pref, false, "Pref should match");
Assert.equal(c.tel[1].type, "work", "Phone type should match");
Assert.equal(c.tel[1].value, "+3455556666", "Phone number should match");
Assert.equal(c.tel[1].pref, true, "Pref should match");
Assert.equal(c.tel[2].type, "home", "Phone type should match");
Assert.equal(c.tel[2].value, "+3455557777", "Phone number should match");
Assert.equal(c.tel[2].pref, false, "Pref should match");
Assert.equal(c.adr.length, 2, "Address count should match");
Assert.equal(c.adr[0].pref, false, "Pref should match");
Assert.equal(c.adr[0].type, "other", "Type should match");
Assert.equal(c.adr[0].streetAddress, "Calle Aduana, 29 Suite 123", "Street address should match");
Assert.equal(c.adr[0].locality, "MADRID", "Locality should match");
Assert.equal(c.adr[0].postalCode, "28070", "Post code should match");
Assert.equal(c.adr[0].countryName, "SPAIN", "Country should match");
Assert.equal(c.adr[1].pref, false, "Pref should match");
Assert.equal(c.adr[1].type, "work", "Type should match");
Assert.equal(c.adr[1].streetAddress, "P.O. BOX 555", "Street address should match");
Assert.equal(c.adr[1].locality, "Washington", "Locality should match");
Assert.equal(c.adr[1].region, "DC", "Region should match");
Assert.equal(c.adr[1].postalCode, "20024-00555", "Post code should match");
Assert.equal(c.adr[1].countryName, "USA", "Country should match");
Assert.equal(c.org[0], "Acme España SL", "Org should match");
Assert.equal(c.jobTitle[0], "President", "Title should match");
Assert.equal(c.note[0], "Likes tulips", "Note should match");
let bday = new Date(c.bday);
Assert.equal(bday.getUTCFullYear(), 1965, "Birthday year should match");
Assert.equal(bday.getUTCMonth(), 4, "Birthday month should match");
Assert.equal(bday.getUTCDate(), 5, "Birthday day should match");
Assert.equal(c.id, "pid3@example.com", "UID should match and be scoped to provider");
// Check name synthesis
c = mockDb._store[4];
Assert.equal(c.name[0], "Jones, Bob", "Full name should be synthesized correctly");
c = mockDb._store[5];
Assert.equal(c.name[0], "Jones, Davy Randall", "Full name should be synthesized correctly");
c = mockDb._store[6];
Assert.equal(c.name[0], "Trip", "Full name should be synthesized correctly");
c = mockDb._store[7];
Assert.equal(c.name[0], "Acme, Inc.", "Full name should be synthesized correctly");
c = mockDb._store[8];
Assert.equal(c.name[0], "anyone@example.com", "Full name should be synthesized correctly");
// Check that a re-import doesn't cause contact duplication.
yield new Promise((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com",
"auth": kAuth.method,
"user": kAuth.user,
"password": kAuth.password
}, (err, result) => { err ? reject(err) : resolve(result); }, mockDb);
});
Assert.equal(vcards.length, Object.keys(mockDb._store).length,
"Second import shouldn't increase DB size");
// Check that errors are propagated back to caller
let error = yield new Promise((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com",
"auth": kAuth.method,
"user": kAuth.user,
"password": "invalidpassword"
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
});
Assert.equal(error.message, "401 Auth Failure", "Auth error should propagate");
error = yield new Promise((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.invalid",
"auth": kAuth.method,
"user": kAuth.user,
"password": kAuth.password
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
});
Assert.equal(error.message, "404 Not Found", "Not found error should propagate");
let tmp = mockDb.getByServiceId;
mockDb.getByServiceId = function(serviceId, callback) {
callback(new Error("getByServiceId failed"));
};
error = yield new Promise((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com",
"auth": kAuth.method,
"user": kAuth.user,
"password": kAuth.password
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
});
Assert.equal(error.message, "getByServiceId failed", "Database error should propagate");
mockDb.getByServiceId = tmp;
error = yield new Promise((resolve, reject) => {
info("Initiating import");
importer.startImport({
"host": "example.com"
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
});
Assert.equal(error.message, "No authentication specified", "Missing parameters should generate error");
});

View File

@ -1,103 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { GoogleImporter } = Cu.import("resource:///modules/loop/GoogleImporter.jsm", {});
var importer = new GoogleImporter();
function promiseImport() {
return new Promise(function(resolve, reject) {
importer.startImport({}, function(err, stats) {
if (err) {
reject(err);
} else {
resolve(stats);
}
}, mockDb, window);
});
}
const kIncomingTotalContactsCount = 8;
const kExpectedImportCount = 7;
add_task(function* test_GoogleImport() {
let stats;
// An error may throw and the test will fail when that happens.
stats = yield promiseImport();
// Assert the world.
Assert.equal(stats.total, kIncomingTotalContactsCount, kIncomingTotalContactsCount + " contacts should get processed");
Assert.equal(stats.success, kExpectedImportCount, kExpectedImportCount + " contacts should be imported");
yield promiseImport();
Assert.equal(mockDb.size, kExpectedImportCount, "Database should be the same size after reimport");
let currentContact = kExpectedImportCount;
let c = mockDb._store[mockDb._next_guid - currentContact];
Assert.equal(c.name[0], "John Smith", "Full name should match");
Assert.equal(c.givenName[0], "John", "Given name should match");
Assert.equal(c.familyName[0], "Smith", "Family name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "john.smith@example.com", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/0", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - (--currentContact)];
Assert.equal(c.name[0], "Jane Smith", "Full name should match");
Assert.equal(c.givenName[0], "Jane", "Given name should match");
Assert.equal(c.familyName[0], "Smith", "Family name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "jane.smith@example.com", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - (--currentContact)];
Assert.equal(c.name[0], "Davy Randall Jones", "Full name should match");
Assert.equal(c.givenName[0], "Davy Randall", "Given name should match");
Assert.equal(c.familyName[0], "Jones", "Family name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "davy.jones@example.com", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - (--currentContact)];
Assert.equal(c.name[0], "noname@example.com", "Full name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "noname@example.com", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - (--currentContact)];
Assert.equal(c.name[0], "lycnix", "Full name should match");
Assert.equal(c.email[0].type, "other", "Email type should match");
Assert.equal(c.email[0].value, "lycnix", "Email should match");
Assert.equal(c.email[0].pref, true, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - (--currentContact)];
Assert.equal(c.name[0], "+31-6-12345678", "Full name should match");
Assert.equal(c.tel[0].type, "mobile", "Phone type should match");
Assert.equal(c.tel[0].value, "+31-6-12345678", "Phone should match");
Assert.equal(c.tel[0].pref, false, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/8", "UID should match and be scoped to provider");
c = mockDb._store[mockDb._next_guid - (--currentContact)];
Assert.equal(c.name[0], "215234523452345", "Full name should match");
Assert.equal(c.tel[0].type, "mobile", "Phone type should match");
Assert.equal(c.tel[0].value, "215234523452345", "Phone should match");
Assert.equal(c.tel[0].pref, false, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6", "UID should match and be scoped to provider");
c = yield mockDb.promise("getByServiceId", "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9");
Assert.equal(c, null, "Contacts that are not part of the default group should not be imported");
});

View File

@ -1,489 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { LoopContacts } = Cu.import("resource:///modules/loop/LoopContacts.jsm", {});
const { LoopStorage } = Cu.import("resource:///modules/loop/LoopStorage.jsm", {});
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
const kContacts = [{
id: 1,
name: ["Ally Avocado"],
email: [{
"pref": true,
"type": ["work"],
"value": "ally@mail.com"
}],
tel: [{
"pref": true,
"type": ["mobile"],
"value": "+31-6-12345678"
}],
category: ["google"],
published: 1406798311748,
updated: 1406798311748
}, {
id: 2,
name: ["Bob Banana"],
email: [{
"pref": true,
"type": ["work"],
"value": "bob@gmail.com"
}],
tel: [{
"pref": true,
"type": ["mobile"],
"value": "+1-214-5551234"
}],
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() {
return new Promise((resolve, reject) => {
LoopContacts.removeAll(err => {
if (err) {
reject(err);
return;
}
gExpectedAdds.push(...kContacts);
LoopContacts.addMany(kContacts, (error, contacts) => {
if (error) {
reject(error);
return;
}
resolve(contacts);
});
});
});
};
// 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.
var gExpectedAdds = [];
var gExpectedRemovals = [];
var gExpectedUpdates = [];
const onContactAdded = function(e, contact) {
let expectedIds = gExpectedAdds.map(contactEntry => contactEntry.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]);
}
info("Add a contact.");
yield new Promise((resolve, reject) => {
gExpectedAdds.push(kDanglingContact);
LoopContacts.add(kDanglingContact, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kDanglingContact);
info("Check if it's persisted.");
LoopContacts.get(contact._guid, (error, contactEntry) => {
Assert.ok(!error, "There shouldn't be an error");
compareContacts(contactEntry, kDanglingContact);
resolve();
});
});
});
});
add_task(function* () {
info("Test removing all contacts.");
let contacts = yield promiseLoadContacts();
yield new Promise((resolve, reject) => {
LoopContacts.removeAll(function(err) {
Assert.ok(!err, "There shouldn't be an error");
LoopContacts.getAll(function(error, found) {
Assert.ok(!error, "There shouldn't be an error");
Assert.equal(found.length, 0, "There shouldn't be any contacts left");
resolve();
});
});
});
});
// Test retrieving a contact.
add_task(function* () {
let contacts = yield promiseLoadContacts();
info("Get a single contact.");
yield new Promise((resolve, reject) => {
LoopContacts.get(contacts[1]._guid, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kContacts[1]);
resolve();
});
});
info("Get a single contact by id.");
yield new Promise((resolve, reject) => {
LoopContacts.getByServiceId(2, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kContacts[1]);
resolve();
});
});
info("Get a couple of contacts.");
yield new Promise((resolve, reject) => {
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");
function resultFilter(c) {
return c._guid == this._guid;
}
for (let contact of toRetrieve) {
let found = result.filter(resultFilter.bind(contact));
Assert.ok(found.length, "Contact " + contact._guid + " should be in the list");
compareContacts(found[0], contact);
}
resolve();
});
});
info("Get all contacts.");
yield new Promise((resolve, reject) => {
LoopContacts.getAll((err, allContacts) => {
Assert.ok(!err, "There shouldn't be an error");
for (let i = 0, l = allContacts.length; i < l; ++i) {
compareContacts(allContacts[i], kContacts[i]);
}
resolve();
});
});
info("Get a non-existent contact.");
return new Promise((resolve, reject) => {
LoopContacts.get(1000, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.ok(!contact, "There shouldn't be a contact");
resolve();
});
});
});
// Test removing a contact.
add_task(function* () {
let contacts = yield promiseLoadContacts();
info("Remove a single contact.");
yield new Promise((resolve, reject) => {
let toRemove = contacts[2]._guid;
gExpectedRemovals.push(toRemove);
LoopContacts.remove(toRemove, err => {
Assert.ok(!err, "There shouldn't be an error");
LoopContacts.get(toRemove, (error, contact) => {
Assert.ok(!error, "There shouldn't be an error");
Assert.ok(!contact, "There shouldn't be a contact");
resolve();
});
});
});
info("Remove a non-existing contact.");
yield new Promise((resolve, reject) => {
LoopContacts.remove(1000, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.ok(!contact, "There shouldn't be a contact");
resolve();
});
});
info("Remove multiple contacts.");
yield new Promise((resolve, reject) => {
let 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((error, allContacts) => {
Assert.ok(!error, "There shouldn't be an error");
let ids = allContacts.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");
resolve();
});
});
});
});
// Test updating a contact.
add_task(function* () {
let contacts = yield promiseLoadContacts();
const newBday = (new Date(403920000000)).toISOString();
info("Update a single contact.");
yield new Promise((resolve, reject) => {
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, (error, contact) => {
Assert.ok(!error, "There shouldn't be an error");
Assert.equal(contact.bday, newBday, "Birthday should be the same");
info("Check that all other properties were left intact.");
contacts[2].bday = newBday;
compareContacts(contact, contacts[2]);
resolve();
});
});
});
info("Update a non-existing contact.");
yield new Promise((resolve, reject) => {
let toUpdate = {
_guid: 1000,
bday: newBday
};
LoopContacts.update(toUpdate, (err, contact) => {
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");
resolve();
});
});
});
// Test blocking and unblocking a contact.
add_task(function* () {
let contacts = yield promiseLoadContacts();
info("Block contact.");
yield new Promise((resolve, reject) => {
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, (error, contact) => {
Assert.ok(!error, "There shouldn't be an error");
Assert.strictEqual(contact.blocked, true, "Blocked status should be set");
info("Check that all other properties were left intact.");
delete contact.blocked;
compareContacts(contact, contacts[1]);
resolve();
});
});
});
info("Block a non-existing contact.");
yield new Promise((resolve, reject) => {
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");
resolve();
});
});
info("Unblock a contact.");
yield new Promise((resolve, reject) => {
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, (error, contact) => {
Assert.ok(!error, "There shouldn't be an error");
Assert.strictEqual(contact.blocked, false, "Blocked status should be set");
info("Check that all other properties were left intact.");
delete contact.blocked;
compareContacts(contact, contacts[1]);
resolve();
});
});
});
info("Unblock a non-existing contact.");
yield new Promise((resolve, reject) => {
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");
resolve();
});
});
});
// 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");
});
// Test switching between different databases.
add_task(function* () {
Assert.equal(LoopStorage.databaseName, "default", "First active partition should be the default");
yield promiseLoadContacts();
let uuid = uuidgen.generateUUID().toString().replace(/[{}]+/g, "");
LoopStorage.switchDatabase(uuid);
Assert.equal(LoopStorage.databaseName, uuid, "The active partition should have changed");
yield promiseLoadContacts();
let contacts = yield promiseLoadContacts();
for (let i = 0, l = contacts.length; i < l; ++i) {
compareContacts(contacts[i], kContacts[i]);
}
LoopStorage.switchDatabase();
Assert.equal(LoopStorage.databaseName, "default", "The active partition should have changed");
contacts = yield LoopContacts.promise("getAll");
for (let i = 0, l = contacts.length; i < l; ++i) {
compareContacts(contacts[i], kContacts[i]);
}
});
// Test searching for contacts.
add_task(function* () {
yield promiseLoadContacts();
let contacts = yield LoopContacts.promise("search", {
q: "bob@gmail.com"
});
Assert.equal(contacts.length, 1, "There should be one contact found");
compareContacts(contacts[0], kContacts[1]);
// Test searching by name.
contacts = yield LoopContacts.promise("search", {
q: "Ally Avocado",
field: "name"
});
Assert.equal(contacts.length, 1, "There should be one contact found");
compareContacts(contacts[0], kContacts[0]);
// Test searching for multiple contacts.
contacts = yield LoopContacts.promise("search", {
q: "google",
field: "category"
});
Assert.equal(contacts.length, 2, "There should be two contacts found");
// Test searching for telephone numbers.
contacts = yield LoopContacts.promise("search", {
q: "+31612345678",
field: "tel"
});
Assert.equal(contacts.length, 1, "There should be one contact found");
compareContacts(contacts[0], kContacts[0]);
// Test searching for telephone numbers without prefixes.
contacts = yield LoopContacts.promise("search", {
q: "5551234",
field: "tel"
});
Assert.equal(contacts.length, 1, "There should be one contact found");
compareContacts(contacts[0], kContacts[1]);
});

View File

@ -1,5 +0,0 @@
<!DOCTYPE html>
<html>
<head><title>Success code=test-code</title></head>
<body>Le Code.</body>
</html>

View File

@ -1,140 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<feed gd:etag="W/&quot;DUQNRHc8cCt7I2A9XRdSF04.&quot;" xmlns="http://www.w3.org/2005/Atom" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:gd="http://schemas.google.com/g/2005" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/">
<id>tester@mochi.com</id>
<updated>2014-09-26T13:16:35.978Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>Mochi Tester's Contacts</title>
<link href="http://www.google.com/" rel="alternate" type="text/html"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#post" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/batch" rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full?max-results=25" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full?start-index=26&amp;max-results=25" rel="next" type="application/atom+xml"/>
<author>
<name>Mochi Tester</name>
<email>tester@mochi.com</email>
</author>
<generator uri="http://www.google.com/m8/feeds" version="1.0">Contacts</generator>
<openSearch:totalResults>25</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>10000000</openSearch:itemsPerPage>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/0</id>
<updated>2012-08-17T23:50:36.892Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>John Smith</title>
<link gd:etag="&quot;Ug92D34SfCt7I2BmLHJTRgVzTlgrJXEAU08.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/0" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/0" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/0" rel="edit" type="application/atom+xml"/>
<gd:name>
<gd:fullName>John Smith</gd:fullName>
<gd:givenName>John</gd:givenName>
<gd:familyName>Smith</gd:familyName>
</gd:name>
<gd:email address="john.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/109576547678240773721" rel="profile"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1</id>
<updated>2012-08-17T23:50:36.892Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>Jane Smith</title>
<link gd:etag="&quot;WA9BY1xFWit7I2BhLEkieCxLHEYTGCYuNxo.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/1" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/1" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/1" rel="edit" type="application/atom+xml"/>
<gd:name>
<gd:fullName>Jane Smith</gd:fullName>
<gd:givenName>Jane</gd:givenName>
<gd:familyName>Smith</gd:familyName>
</gd:name>
<gd:email address="jane.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/112886528199784431028" rel="profile"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2</id>
<updated>2012-08-17T23:50:36.892Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-08-17T23:50:36.892Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>Davy Randall Jones</title>
<link gd:etag="&quot;KiV2PkYRfCt7I2BuD1AzEBFxD1VcGjwBUyA.&quot;" href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/2" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/2" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/2" rel="edit" type="application/atom+xml"/>
<gd:name>
<gd:fullName>Davy Randall Jones</gd:fullName>
<gd:givenName>Davy Randall</gd:givenName>
<gd:familyName>Jones</gd:familyName>
</gd:name>
<gd:email address="davy.jones@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/109710625881478599011" rel="profile"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3</id>
<updated>2007-08-01T05:45:52.203Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T05:45:52.203Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title/>
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/3" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="edit" type="application/atom+xml"/>
<gd:email address="noname@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7</id>
<updated>2007-08-01T05:45:52.203Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T05:45:52.203Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title/>
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/7" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="edit" type="application/atom+xml"/>
<gd:email address="lycnix" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;RXkzfjVSLit7I2A9XRdRGUgITgA.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/8</id>
<updated>2014-10-10T14:55:44.786Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2014-10-10T14:55:44.786Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title/>
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/8" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="edit" type="application/atom+xml"/>
<gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile" uri="tel:+31-6-12345678">0612345678</gd:phoneNumber>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;SX8-ejVSLit7I2A9XRdQFUkDRgY.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6</id>
<updated>2014-10-17T12:32:08.152Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2014-10-17T12:32:08.152Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title/>
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/6" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/6" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/6" rel="edit" type="application/atom+xml"/>
<gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile">215234523452345</gd:phoneNumber>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;Rn8zejVSLit7I2A9WhVRFUQOQQc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9</id>
<updated>2012-03-24T13:10:37.182Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-03-24T13:10:37.182Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>Little Smith</title>
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/9" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="edit" type="application/atom+xml"/>
<gd:name>
<gd:fullName>Little Smith</gd:fullName>
<gd:givenName>Little</gd:givenName>
<gd:familyName>Smith</gd:familyName>
</gd:name>
<gd:email address="littlebabysmith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/111456826635924971693" rel="profile"/>
</entry>
</feed>

View File

@ -1,56 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<feed gd:etag="W/&quot;CEIAQngzfyt7I2A9XRdXFEQ.&quot;" xmlns="http://www.w3.org/2005/Atom" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:gd="http://schemas.google.com/g/2005" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/">
<id>tester@mochi.com</id>
<updated>2014-10-28T10:35:43.687Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>Mochi Tester's Contact Groups</title>
<link href="http://www.google.com/" rel="alternate" type="text/html"/>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#post" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/batch" rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full?max-results=10000000" rel="self" type="application/atom+xml"/>
<author>
<name>Mochi Tester</name>
<email>tester@mochi.com</email>
</author>
<generator uri="http://www.google.com/m8/feeds" version="1.0">Contacts</generator>
<openSearch:totalResults>4</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>10000000</openSearch:itemsPerPage>
<entry gd:etag="&quot;YDwreyM.&quot;">
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6</id>
<updated>1970-01-01T00:00:00.000Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>System Group: My Contacts</title>
<content>System Group: My Contacts</content>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/6" rel="self" type="application/atom+xml"/>
<gContact:systemGroup id="Contacts"/>
</entry>
<entry gd:etag="&quot;YDwreyM.&quot;">
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/d</id>
<updated>1970-01-01T00:00:00.000Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>System Group: Friends</title>
<content>System Group: Friends</content>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/d" rel="self" type="application/atom+xml"/>
<gContact:systemGroup id="Friends"/>
</entry>
<entry gd:etag="&quot;YDwreyM.&quot;">
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/e</id>
<updated>1970-01-01T00:00:00.000Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>System Group: Family</title>
<content>System Group: Family</content>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/e" rel="self" type="application/atom+xml"/>
<gContact:systemGroup id="Family"/>
</entry>
<entry gd:etag="&quot;YDwreyM.&quot;">
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/f</id>
<updated>1970-01-01T00:00:00.000Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>System Group: Coworkers</title>
<content>System Group: Coworkers</content>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/f" rel="self" type="application/atom+xml"/>
<gContact:systemGroup id="Coworkers"/>
</entry>
</feed>

View File

@ -1,3 +0,0 @@
{
"access_token": "test-token"
}

View File

@ -1,157 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {classes: Cc, interfaces: Ci, Constructor: CC} = Components;
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream");
function handleRequest(req, res) {
try {
reallyHandleRequest(req, res);
} catch (ex) {
res.setStatusLine("1.0", 200, "AlmostOK");
let msg = "Error handling request: " + ex + "\n" + ex.stack;
log(msg);
res.write(msg);
}
}
function log(msg) {
// dump("GOOGLE-SERVER-MOCK: " + msg + "\n");
}
const kBasePath = "browser/browser/components/loop/test/mochitest/fixtures/";
const kStatusCodes = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
500: "Internal Server Error",
501: "Not Implemented",
503: "Service Unavailable"
};
function HTTPError(code = 500, message) {
this.code = code;
this.name = kStatusCodes[code] || "HTTPError";
this.message = message || this.name;
}
HTTPError.prototype = new Error();
HTTPError.prototype.constructor = HTTPError;
function sendError(res, err) {
if (!(err instanceof HTTPError)) {
err = new HTTPError(typeof err == "number" ? err : 500,
err.message || typeof err == "string" ? err : "");
}
res.setStatusLine("1.1", err.code, err.name);
res.write(err.message);
}
function parseQuery(query, params = {}) {
for (let param of query.replace(/^[?&]/, "").split(/(?:&|\?)/)) {
param = param.split("=");
if (!param[0])
continue;
params[unescape(param[0])] = unescape(param[1]);
}
return params;
}
function getRequestBody(req) {
let avail;
let bytes = [];
let body = new BinaryInputStream(req.bodyInputStream);
while ((avail = body.available()) > 0)
Array.prototype.push.apply(bytes, body.readByteArray(avail));
return String.fromCharCode.apply(null, bytes);
}
function getInputStream(path) {
let file = Cc["@mozilla.org/file/directory_service;1"]
.getService(Ci.nsIProperties)
.get("CurWorkD", Ci.nsILocalFile);
for (let part of path.split("/"))
file.append(part);
let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
fileStream.init(file, 1, 0, false);
return fileStream;
}
function checkAuth(req) {
if (!req.hasHeader("Authorization"))
throw new HTTPError(401, "No Authorization header provided.");
let auth = req.getHeader("Authorization");
if (auth != "Bearer test-token")
throw new HTTPError(401, "Invalid Authorization header content: '" + auth + "'");
}
function reallyHandleRequest(req, res) {
log("method: " + req.method);
let body = getRequestBody(req);
log("body: " + body);
let contentType = req.hasHeader("Content-Type") ? req.getHeader("Content-Type") : null;
log("contentType: " + contentType);
let params = parseQuery(req.queryString);
parseQuery(body, params);
log("params: " + JSON.stringify(params));
// Delegate an authentication request to the correct handler.
if ("action" in params) {
methodHandlers[params.action](req, res, params);
} else {
sendError(res, 501);
}
}
function respondWithFile(res, fileName, mimeType) {
res.setStatusLine("1.1", 200, "OK");
res.setHeader("Content-Type", mimeType);
let inputStream = getInputStream(kBasePath + fileName);
res.bodyOutputStream.writeFrom(inputStream, inputStream.available());
inputStream.close();
}
const methodHandlers = {
auth: function(req, res, params) {
respondWithFile(res, "google_auth.txt", "text/html");
},
token: function(req, res, params) {
respondWithFile(res, "google_token.txt", "application/json");
},
contacts: function(req, res, params) {
try {
checkAuth(req);
} catch (ex) {
sendError(res, ex, ex.code);
return;
}
respondWithFile(res, "google_contacts.txt", "text/xml");
},
groups: function(req, res, params) {
try {
checkAuth(req);
} catch (ex) {
sendError(res, ex, ex.code);
}
respondWithFile(res, "google_groups.txt", "text/xml");
}
};

View File

@ -164,8 +164,11 @@ var gSyncPane = {
document.getElementById("fxaSyncComputerName").blur();
},
_focusChangeDeviceNameButton: function() {
document.getElementById("fxaChangeDeviceName").focus();
_focusAfterComputerNameTextbox: function() {
// Focus the most appropriate element that's *not* the "computer name" box.
Services.focus.moveFocus(window,
document.getElementById("fxaSyncComputerName"),
Services.focus.MOVEFOCUS_FORWARD, 0);
},
_updateComputerNameValue: function(save) {
@ -227,17 +230,21 @@ var gSyncPane = {
this._focusComputerNameTextbox();
});
setEventListener("fxaCancelChangeDeviceName", "command", function () {
// We explicitly blur the textbox because of bug 1194032
// We explicitly blur the textbox because of bug 75324, then after
// changing the state of the buttons, force focus to whatever the focus
// manager thinks should be next (which on the mac, depends on an OSX
// keyboard access preference)
this._blurComputerNameTextbox();
this._toggleComputerNameControls(false);
this._updateComputerNameValue(false);
this._focusChangeDeviceNameButton();
this._focusAfterComputerNameTextbox();
});
setEventListener("fxaSaveChangeDeviceName", "command", function () {
// Work around bug 75324 - see above.
this._blurComputerNameTextbox();
this._toggleComputerNameControls(false);
this._updateComputerNameValue(true);
this._focusChangeDeviceNameButton();
this._focusAfterComputerNameTextbox();
});
setEventListener("unlinkDevice", "click", function () {
gSyncPane.startOver(true);

View File

@ -79,7 +79,7 @@ share_add_service_button=Add a Service
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copy Link
email_link_menuitem=Email Link
delete_conversation_menuitem=Delete conversation
delete_conversation_menuitem2=Delete
panel_footer_signin_or_signup_link=Sign In or Sign Up

View File

@ -82,8 +82,7 @@ public class GeckoPreferenceFragment extends PreferenceFragment {
}
/**
* Return the title to use for this preference fragment. This allows
* for us to redisplay this fragment in a different locale.
* Return the title to use for this preference fragment.
*
* We only return titles for the preference screens that are
* launched directly, and thus might need to be redisplayed.
@ -96,13 +95,12 @@ public class GeckoPreferenceFragment extends PreferenceFragment {
return getString(R.string.settings_title);
}
// We need this because we can launch straight into this category
// from the Data Reporting notification.
// We can launch this category from the Data Reporting notification.
if (res == R.xml.preferences_privacy) {
return getString(R.string.pref_category_privacy_short);
}
// from the Awesomescreen with the magnifying glass.
// We can launch this category from the the magnifying glass in the quick search bar.
if (res == R.xml.preferences_search) {
return getString(R.string.pref_category_search);
}
@ -110,6 +108,33 @@ public class GeckoPreferenceFragment extends PreferenceFragment {
return null;
}
/**
* Return the header id for this preference fragment. This allows
* us to select the correct header when launching a preference
* screen directly.
*
* We only return titles for the preference screens that are
* launched directly.
*/
private int getHeader() {
final int res = getResource();
if (res == R.xml.preferences) {
return R.id.pref_header_general;
}
// We can launch this category from the Data Reporting notification.
if (res == R.xml.preferences_privacy) {
return R.id.pref_header_privacy;
}
// We can launch this category from the the magnifying glass in the quick search bar.
if (res == R.xml.preferences_search) {
return R.id.pref_header_search;
}
return -1;
}
private void updateTitle() {
final String newTitle = getTitle();
if (newTitle == null) {
@ -122,6 +147,7 @@ public class GeckoPreferenceFragment extends PreferenceFragment {
// In a multi-pane activity, the title is "Settings", and the action
// bar is along the top of the screen. We don't want to change those.
activity.showBreadCrumbs(newTitle, newTitle);
((GeckoPreferences) activity).switchToHeader(getHeader());
return;
}

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.preferences;
import android.annotation.TargetApi;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.AppConstants.Versions;
@ -108,7 +109,7 @@ OnSharedPreferenceChangeListener
private static boolean sIsCharEncodingEnabled;
private boolean mInitialized;
private int mPrefsRequestId;
private PanelsPreferenceCategory mPanelsPreferenceCategory;
private List<Header> mHeaders;
// These match keys in resources/xml*/preferences*.xml
private static final String PREFS_SEARCH_RESTORE_DEFAULTS = NON_PREF_PREFIX + "search.restore_defaults";
@ -499,6 +500,23 @@ OnSharedPreferenceChangeListener
iterator.remove();
}
}
mHeaders = target;
}
}
@TargetApi(11)
public void switchToHeader(int id) {
if (mHeaders == null) {
// Can't switch to a header if there are no headers!
return;
}
for (Header header : mHeaders) {
if (header.id == id) {
switchToHeader(header);
return;
}
}
}
@ -692,8 +710,6 @@ OnSharedPreferenceChangeListener
i--;
continue;
}
} else if (pref instanceof PanelsPreferenceCategory) {
mPanelsPreferenceCategory = (PanelsPreferenceCategory) pref;
}
if (PREFS_ADVANCED.equals(key) &&
!RestrictedProfiles.isAllowed(this, Restriction.DISALLOW_DEVELOPER_TOOLS)) {

View File

@ -12,5 +12,8 @@
<item type="id" name="menu_margin"/>
<item type="id" name="recycler_view_click_support" />
<item type="id" name="range_list"/>
<item type="id" name="pref_header_general"/>
<item type="id" name="pref_header_privacy"/>
<item type="id" name="pref_header_search"/>
</resources>

View File

@ -10,7 +10,8 @@
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
android:title="@string/pref_header_general">
android:title="@string/pref_header_general"
android:id="@+id/pref_header_general">
<extra android:name="resource"
android:value="preferences_general_tablet"/>
</header>
@ -23,7 +24,8 @@
</header>
<header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
android:title="@string/pref_header_privacy_short">
android:title="@string/pref_header_privacy_short"
android:id="@+id/pref_header_privacy">
<extra android:name="resource"
android:value="preferences_privacy"/>
</header>

View File

@ -290,9 +290,6 @@ user_pref("browser.newtabpage.directory.ping", "");
user_pref("loop.debug.loglevel", "All");
user_pref("loop.enabled", true);
user_pref("loop.throttled", false);
user_pref("loop.oauth.google.URL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=");
user_pref("loop.oauth.google.getContactsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=contacts");
user_pref("loop.oauth.google.getGroupsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=groups");
user_pref("loop.server", "http://%(server)s/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?");
user_pref("loop.CSP","default-src 'self' about: file: chrome: data: wss://* http://* https://*");

View File

@ -481,7 +481,9 @@ ExtensionData.prototype = {
if (!(this.rootURI instanceof Ci.nsIJARURI &&
this.rootURI.JARFile instanceof Ci.nsIFileURL)) {
throw Error("Invalid extension root URL");
// This currently happens for app:// URLs passed to us by
// UserCustomizations.jsm
return [];
}
// FIXME: We need a way to do this without main thread IO.