зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound to fix m1 orange
This commit is contained in:
Коммит
349dddd912
|
@ -1412,8 +1412,6 @@ pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; im
|
||||||
#else
|
#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:");
|
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
|
#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.tokendata", "");
|
||||||
pref("loop.fxa_oauth.profile", "");
|
pref("loop.fxa_oauth.profile", "");
|
||||||
pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");
|
pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
// Gecko + Loop Globals.
|
// Gecko + Loop Globals.
|
||||||
"CardDavImporter": true,
|
|
||||||
"Chat": false,
|
"Chat": false,
|
||||||
"ChromeWorker": false,
|
"ChromeWorker": false,
|
||||||
"CommonUtils": false,
|
"CommonUtils": false,
|
||||||
|
@ -27,7 +26,6 @@
|
||||||
"gBrowser": false,
|
"gBrowser": false,
|
||||||
"gDNSService": false,
|
"gDNSService": false,
|
||||||
"gLoopBundle": false,
|
"gLoopBundle": false,
|
||||||
"GoogleImporter": true,
|
|
||||||
"gWM": false,
|
"gWM": false,
|
||||||
"HawkClient": false,
|
"HawkClient": false,
|
||||||
"injectLoopAPI": true,
|
"injectLoopAPI": true,
|
||||||
|
@ -36,11 +34,9 @@
|
||||||
"log": true,
|
"log": true,
|
||||||
"LOOP_SESSION_TYPE": true,
|
"LOOP_SESSION_TYPE": true,
|
||||||
"LoopCalls": true,
|
"LoopCalls": true,
|
||||||
"LoopContacts": true,
|
|
||||||
"loopCrypto": false,
|
"loopCrypto": false,
|
||||||
"LoopRooms": true,
|
"LoopRooms": true,
|
||||||
"LoopRoomsCache": true,
|
"LoopRoomsCache": true,
|
||||||
"LoopStorage": true,
|
|
||||||
"MozLoopPushHandler": true,
|
"MozLoopPushHandler": true,
|
||||||
"MozLoopService": true,
|
"MozLoopService": true,
|
||||||
"OS": false,
|
"OS": false,
|
||||||
|
|
|
@ -611,7 +611,7 @@ loop.panel = (function(_, mozL10n) {
|
||||||
className: "dropdown-menu-item",
|
className: "dropdown-menu-item",
|
||||||
onClick: this.props.handleDeleteButtonClick,
|
onClick: this.props.handleDeleteButtonClick,
|
||||||
ref: "deleteButton"},
|
ref: "deleteButton"},
|
||||||
mozL10n.get("delete_conversation_menuitem")
|
mozL10n.get("delete_conversation_menuitem2")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -611,7 +611,7 @@ loop.panel = (function(_, mozL10n) {
|
||||||
className="dropdown-menu-item"
|
className="dropdown-menu-item"
|
||||||
onClick={this.props.handleDeleteButtonClick}
|
onClick={this.props.handleDeleteButtonClick}
|
||||||
ref="deleteButton">
|
ref="deleteButton">
|
||||||
{mozL10n.get("delete_conversation_menuitem")}
|
{mozL10n.get("delete_conversation_menuitem2")}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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://gre/modules/XPCOMUtils.jsm");
|
||||||
Cu.import("resource:///modules/loop/MozLoopService.jsm");
|
Cu.import("resource:///modules/loop/MozLoopService.jsm");
|
||||||
Cu.import("resource:///modules/loop/LoopRooms.jsm");
|
Cu.import("resource:///modules/loop/LoopRooms.jsm");
|
||||||
Cu.import("resource:///modules/loop/LoopContacts.jsm");
|
|
||||||
Cu.importGlobalProperties(["Blob"]);
|
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",
|
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
|
||||||
"resource://gre/modules/MozSocialAPI.jsm");
|
"resource://gre/modules/MozSocialAPI.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
|
XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
|
||||||
|
@ -223,7 +218,6 @@ function injectLoopAPI(targetWindow) {
|
||||||
let ringer;
|
let ringer;
|
||||||
let ringerStopper;
|
let ringerStopper;
|
||||||
let appVersionInfo;
|
let appVersionInfo;
|
||||||
let contactsAPI;
|
|
||||||
let roomsAPI;
|
let roomsAPI;
|
||||||
let callsAPI;
|
let callsAPI;
|
||||||
let savedWindowListeners = new Map();
|
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.
|
* 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
|
* Returns translated strings associated with an element. Designed
|
||||||
* for use with l10n.js
|
* for use with l10n.js
|
||||||
|
|
|
@ -147,12 +147,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
|
XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
|
||||||
"resource://services-common/hawkrequest.js");
|
"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",
|
XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms",
|
||||||
"resource:///modules/loop/LoopRooms.jsm");
|
"resource:///modules/loop/LoopRooms.jsm");
|
||||||
|
|
||||||
|
@ -307,7 +301,6 @@ var MozLoopServiceInternal = {
|
||||||
notifyStatusChanged: function(aReason = null) {
|
notifyStatusChanged: function(aReason = null) {
|
||||||
log.debug("notifyStatusChanged with reason:", aReason);
|
log.debug("notifyStatusChanged with reason:", aReason);
|
||||||
let profile = MozLoopService.userProfile;
|
let profile = MozLoopService.userProfile;
|
||||||
LoopStorage.switchDatabase(profile && profile.uid);
|
|
||||||
LoopRooms.maybeRefresh(profile && profile.uid);
|
LoopRooms.maybeRefresh(profile && profile.uid);
|
||||||
Services.obs.notifyObservers(null, "loop-status-changed", aReason);
|
Services.obs.notifyObservers(null, "loop-status-changed", aReason);
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,12 +15,8 @@ BROWSER_CHROME_MANIFESTS += [
|
||||||
EXTRA_JS_MODULES.loop += [
|
EXTRA_JS_MODULES.loop += [
|
||||||
'content/shared/js/crypto.js',
|
'content/shared/js/crypto.js',
|
||||||
'content/shared/js/utils.js',
|
'content/shared/js/utils.js',
|
||||||
'modules/CardDavImporter.jsm',
|
|
||||||
'modules/GoogleImporter.jsm',
|
|
||||||
'modules/LoopContacts.jsm',
|
|
||||||
'modules/LoopRooms.jsm',
|
'modules/LoopRooms.jsm',
|
||||||
'modules/LoopRoomsCache.jsm',
|
'modules/LoopRoomsCache.jsm',
|
||||||
'modules/LoopStorage.jsm',
|
|
||||||
'modules/MozLoopAPI.jsm',
|
'modules/MozLoopAPI.jsm',
|
||||||
'modules/MozLoopPushHandler.jsm',
|
'modules/MozLoopPushHandler.jsm',
|
||||||
'modules/MozLoopService.jsm',
|
'modules/MozLoopService.jsm',
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
support-files =
|
support-files =
|
||||||
fixtures/google_auth.txt
|
|
||||||
fixtures/google_contacts.txt
|
|
||||||
fixtures/google_groups.txt
|
|
||||||
fixtures/google_token.txt
|
|
||||||
google_service.sjs
|
|
||||||
head.js
|
head.js
|
||||||
loop_fxa.sjs
|
loop_fxa.sjs
|
||||||
test_loopLinkClicker_channel.html
|
test_loopLinkClicker_channel.html
|
||||||
../../../../base/content/test/general/browser_fxa_oauth_with_keys.html
|
../../../../base/content/test/general/browser_fxa_oauth_with_keys.html
|
||||||
|
|
||||||
[browser_CardDavImporter.js]
|
|
||||||
[browser_fxa_login.js]
|
[browser_fxa_login.js]
|
||||||
[browser_GoogleImporter.js]
|
|
||||||
skip-if = e10s
|
|
||||||
[browser_loop_fxa_server.js]
|
[browser_loop_fxa_server.js]
|
||||||
[browser_LoopRooms_channel.js]
|
[browser_LoopRooms_channel.js]
|
||||||
[browser_mozLoop_appVersionInfo.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();
|
document.getElementById("fxaSyncComputerName").blur();
|
||||||
},
|
},
|
||||||
|
|
||||||
_focusChangeDeviceNameButton: function() {
|
_focusAfterComputerNameTextbox: function() {
|
||||||
document.getElementById("fxaChangeDeviceName").focus();
|
// 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) {
|
_updateComputerNameValue: function(save) {
|
||||||
|
@ -227,17 +230,21 @@ var gSyncPane = {
|
||||||
this._focusComputerNameTextbox();
|
this._focusComputerNameTextbox();
|
||||||
});
|
});
|
||||||
setEventListener("fxaCancelChangeDeviceName", "command", function () {
|
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._blurComputerNameTextbox();
|
||||||
this._toggleComputerNameControls(false);
|
this._toggleComputerNameControls(false);
|
||||||
this._updateComputerNameValue(false);
|
this._updateComputerNameValue(false);
|
||||||
this._focusChangeDeviceNameButton();
|
this._focusAfterComputerNameTextbox();
|
||||||
});
|
});
|
||||||
setEventListener("fxaSaveChangeDeviceName", "command", function () {
|
setEventListener("fxaSaveChangeDeviceName", "command", function () {
|
||||||
|
// Work around bug 75324 - see above.
|
||||||
this._blurComputerNameTextbox();
|
this._blurComputerNameTextbox();
|
||||||
this._toggleComputerNameControls(false);
|
this._toggleComputerNameControls(false);
|
||||||
this._updateComputerNameValue(true);
|
this._updateComputerNameValue(true);
|
||||||
this._focusChangeDeviceNameButton();
|
this._focusAfterComputerNameTextbox();
|
||||||
});
|
});
|
||||||
setEventListener("unlinkDevice", "click", function () {
|
setEventListener("unlinkDevice", "click", function () {
|
||||||
gSyncPane.startOver(true);
|
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.
|
## These menu items are displayed from a panel's context menu for a conversation.
|
||||||
copy_link_menuitem=Copy Link
|
copy_link_menuitem=Copy Link
|
||||||
email_link_menuitem=Email 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
|
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
|
* Return the title to use for this preference fragment.
|
||||||
* for us to redisplay this fragment in a different locale.
|
|
||||||
*
|
*
|
||||||
* We only return titles for the preference screens that are
|
* We only return titles for the preference screens that are
|
||||||
* launched directly, and thus might need to be redisplayed.
|
* launched directly, and thus might need to be redisplayed.
|
||||||
|
@ -96,13 +95,12 @@ public class GeckoPreferenceFragment extends PreferenceFragment {
|
||||||
return getString(R.string.settings_title);
|
return getString(R.string.settings_title);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need this because we can launch straight into this category
|
// We can launch this category from the Data Reporting notification.
|
||||||
// from the Data Reporting notification.
|
|
||||||
if (res == R.xml.preferences_privacy) {
|
if (res == R.xml.preferences_privacy) {
|
||||||
return getString(R.string.pref_category_privacy_short);
|
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) {
|
if (res == R.xml.preferences_search) {
|
||||||
return getString(R.string.pref_category_search);
|
return getString(R.string.pref_category_search);
|
||||||
}
|
}
|
||||||
|
@ -110,6 +108,33 @@ public class GeckoPreferenceFragment extends PreferenceFragment {
|
||||||
return null;
|
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() {
|
private void updateTitle() {
|
||||||
final String newTitle = getTitle();
|
final String newTitle = getTitle();
|
||||||
if (newTitle == null) {
|
if (newTitle == null) {
|
||||||
|
@ -122,6 +147,7 @@ public class GeckoPreferenceFragment extends PreferenceFragment {
|
||||||
// In a multi-pane activity, the title is "Settings", and the action
|
// 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.
|
// bar is along the top of the screen. We don't want to change those.
|
||||||
activity.showBreadCrumbs(newTitle, newTitle);
|
activity.showBreadCrumbs(newTitle, newTitle);
|
||||||
|
((GeckoPreferences) activity).switchToHeader(getHeader());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
package org.mozilla.gecko.preferences;
|
package org.mozilla.gecko.preferences;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import org.mozilla.gecko.AboutPages;
|
import org.mozilla.gecko.AboutPages;
|
||||||
import org.mozilla.gecko.AppConstants;
|
import org.mozilla.gecko.AppConstants;
|
||||||
import org.mozilla.gecko.AppConstants.Versions;
|
import org.mozilla.gecko.AppConstants.Versions;
|
||||||
|
@ -108,7 +109,7 @@ OnSharedPreferenceChangeListener
|
||||||
private static boolean sIsCharEncodingEnabled;
|
private static boolean sIsCharEncodingEnabled;
|
||||||
private boolean mInitialized;
|
private boolean mInitialized;
|
||||||
private int mPrefsRequestId;
|
private int mPrefsRequestId;
|
||||||
private PanelsPreferenceCategory mPanelsPreferenceCategory;
|
private List<Header> mHeaders;
|
||||||
|
|
||||||
// These match keys in resources/xml*/preferences*.xml
|
// These match keys in resources/xml*/preferences*.xml
|
||||||
private static final String PREFS_SEARCH_RESTORE_DEFAULTS = NON_PREF_PREFIX + "search.restore_defaults";
|
private static final String PREFS_SEARCH_RESTORE_DEFAULTS = NON_PREF_PREFIX + "search.restore_defaults";
|
||||||
|
@ -499,6 +500,23 @@ OnSharedPreferenceChangeListener
|
||||||
iterator.remove();
|
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--;
|
i--;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (pref instanceof PanelsPreferenceCategory) {
|
|
||||||
mPanelsPreferenceCategory = (PanelsPreferenceCategory) pref;
|
|
||||||
}
|
}
|
||||||
if (PREFS_ADVANCED.equals(key) &&
|
if (PREFS_ADVANCED.equals(key) &&
|
||||||
!RestrictedProfiles.isAllowed(this, Restriction.DISALLOW_DEVELOPER_TOOLS)) {
|
!RestrictedProfiles.isAllowed(this, Restriction.DISALLOW_DEVELOPER_TOOLS)) {
|
||||||
|
|
|
@ -12,5 +12,8 @@
|
||||||
<item type="id" name="menu_margin"/>
|
<item type="id" name="menu_margin"/>
|
||||||
<item type="id" name="recycler_view_click_support" />
|
<item type="id" name="recycler_view_click_support" />
|
||||||
<item type="id" name="range_list"/>
|
<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>
|
</resources>
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
|
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
|
<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"
|
<extra android:name="resource"
|
||||||
android:value="preferences_general_tablet"/>
|
android:value="preferences_general_tablet"/>
|
||||||
</header>
|
</header>
|
||||||
|
@ -23,7 +24,8 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<header android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment"
|
<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"
|
<extra android:name="resource"
|
||||||
android:value="preferences_privacy"/>
|
android:value="preferences_privacy"/>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -290,9 +290,6 @@ user_pref("browser.newtabpage.directory.ping", "");
|
||||||
user_pref("loop.debug.loglevel", "All");
|
user_pref("loop.debug.loglevel", "All");
|
||||||
user_pref("loop.enabled", true);
|
user_pref("loop.enabled", true);
|
||||||
user_pref("loop.throttled", false);
|
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.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://*");
|
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 &&
|
if (!(this.rootURI instanceof Ci.nsIJARURI &&
|
||||||
this.rootURI.JARFile instanceof Ci.nsIFileURL)) {
|
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.
|
// FIXME: We need a way to do this without main thread IO.
|
||||||
|
|
Загрузка…
Ссылка в новой задаче