mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge mozilla-central to mozilla-inbound to fix m1 orange
This commit is contained in:
commit
5738f07f6f
@ -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");
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
@ -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)
|
||||
});
|
@ -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
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -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]
|
||||
|
@ -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");
|
||||
});
|
@ -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");
|
||||
});
|
@ -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]);
|
||||
});
|
@ -1,5 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Success code=test-code</title></head>
|
||||
<body>Le Code.</body>
|
||||
</html>
|
@ -1,140 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<feed gd:etag="W/"DUQNRHc8cCt7I2A9XRdSF04."" 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&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=""R3YyejRVLit7I2A9WhJWEkkNQwc."">
|
||||
<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=""Ug92D34SfCt7I2BmLHJTRgVzTlgrJXEAU08."" 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=""R3YyejRVLit7I2A9WhJWEkkNQwc."">
|
||||
<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=""WA9BY1xFWit7I2BhLEkieCxLHEYTGCYuNxo."" 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=""R3YyejRVLit7I2A9WhJWEkkNQwc."">
|
||||
<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=""KiV2PkYRfCt7I2BuD1AzEBFxD1VcGjwBUyA."" 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=""Q3w7ezVSLit7I2A9WB5WGUkNRgE."">
|
||||
<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=""Q3w7ezVSLit7I2A9WB5WGUkNRgE."">
|
||||
<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=""RXkzfjVSLit7I2A9XRdRGUgITgA."">
|
||||
<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=""SX8-ejVSLit7I2A9XRdQFUkDRgY."">
|
||||
<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=""Rn8zejVSLit7I2A9WhVRFUQOQQc."">
|
||||
<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>
|
@ -1,56 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<feed gd:etag="W/"CEIAQngzfyt7I2A9XRdXFEQ."" 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=""YDwreyM."">
|
||||
<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=""YDwreyM."">
|
||||
<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=""YDwreyM."">
|
||||
<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=""YDwreyM."">
|
||||
<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>
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"access_token": "test-token"
|
||||
}
|
@ -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");
|
||||
}
|
||||
};
|
@ -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);
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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://*");
|
||||
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user