Bug 1089011: make sure to only import contacts that are part of the default contacts group. r=MattN

This commit is contained in:
Mike de Boer 2014-10-29 17:40:57 +01:00
parent 7a2555531e
commit ba7411aaf5
7 changed files with 166 additions and 32 deletions

View File

@ -48,7 +48,7 @@ const extractFieldsFromNode = function(fieldMap, node, ns = null, target = {}, w
if (!nodeList[0].firstChild) {
continue;
}
let value = nodeList[0].firstChild.nodeValue;
let value = nodeList[0].textContent;
target[field] = wrapInArray ? [value] : value;
}
}
@ -168,8 +168,8 @@ this.GoogleImporter.prototype = {
Task.spawn(function* () {
let code = yield this._promiseAuthCode(windowRef);
let tokenSet = yield this._promiseTokenSet(code);
let contactEntries = yield this._promiseContactEntries(tokenSet);
let {total, success, ids} = yield this._processContacts(contactEntries, db);
let contactEntries = yield this._getContactEntries(tokenSet);
let {total, success, ids} = yield this._processContacts(contactEntries, db, tokenSet);
yield this._purgeContacts(ids, db);
return {
@ -286,22 +286,12 @@ this.GoogleImporter.prototype = {
});
},
/**
* 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.
*/
_promiseContactEntries: function(tokenSet) {
return new Promise(function(resolve, reject) {
_promiseRequestXML: function(URL, tokenSet) {
return new Promise((resolve, reject) => {
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
request.open("GET", getUrlParam("https://www.google.com/m8/feeds/contacts/default/full",
"loop.oauth.google.getContactsURL",
false) + "?max-results=" + kContactsMaxResults);
request.open("GET", URL);
request.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
request.setRequestHeader("GData-Version", "3.0");
@ -310,19 +300,17 @@ this.GoogleImporter.prototype = {
request.onload = function() {
if (request.status < 400) {
let doc = request.responseXML;
// First get the profile id.
// 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.firstChild.nodeValue;
gProfileId = currNode.textContent;
break;
}
currNode = currNode.nextSibling;
}
// Then kick of the importing of contact entries.
let entries = Array.prototype.slice.call(doc.querySelectorAll("entry"));
resolve(entries);
resolve(doc);
} else {
reject(new Error(request.status + " " + request.statusText));
}
@ -336,6 +324,46 @@ this.GoogleImporter.prototype = {
});
},
/**
* 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,
@ -343,21 +371,28 @@ this.GoogleImporter.prototype = {
* 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) {
_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);
@ -367,6 +402,12 @@ this.GoogleImporter.prototype = {
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)) {
@ -450,7 +491,7 @@ this.GoogleImporter.prototype = {
for (let [,phoneNode] of Iterator(phoneNodes)) {
let phoneNumber = phoneNode.hasAttribute("uri") ?
phoneNode.getAttribute("uri").replace("tel:", "") :
phoneNode.firstChild.nodeValue;
phoneNode.textContent;
contact.tel.push({
pref: (phoneNode.getAttribute("primary") == "true"),
type: [getFieldType(phoneNode)],
@ -466,8 +507,8 @@ this.GoogleImporter.prototype = {
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.firstChild.nodeValue : "")
contact.jobTitle.push(titleElement ? titleElement.firstChild.nodeValue : "");
contact.org.push(orgElement ? orgElement.textContent : "")
contact.jobTitle.push(titleElement ? titleElement.textContent : "");
}
}

View File

@ -2,6 +2,7 @@
support-files =
fixtures/google_auth.txt
fixtures/google_contacts.txt
fixtures/google_groups.txt
fixtures/google_token.txt
google_service.sjs
head.js

View File

@ -17,23 +17,22 @@ function promiseImport() {
});
}
const kContactsCount = 7;
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();
let contactsCount = mockDb.size;
// Assert the world.
Assert.equal(stats.total, contactsCount, "Five contacts should get processed");
Assert.equal(stats.success, contactsCount, "Five contacts should be imported");
Assert.equal(stats.total, kIncomingTotalContactsCount, kIncomingTotalContactsCount + " contacts should get processed");
Assert.equal(stats.success, kExpectedImportCount, kExpectedImportCount + " contacts should be imported");
yield promiseImport();
Assert.equal(Object.keys(mockDb._store).length, contactsCount, "Database should be the same size after reimport");
Assert.equal(mockDb.size, kExpectedImportCount, "Database should be the same size after reimport");
let currentContact = contactsCount;
let currentContact = kExpectedImportCount;
let c = mockDb._store[mockDb._next_guid - currentContact];
Assert.equal(c.name[0], "John Smith", "Full name should match");
@ -96,4 +95,7 @@ add_task(function* test_GoogleImport() {
Assert.equal(c.tel[0].pref, false, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6", "UID should match and be scoped to provider");
c = yield mockDb.promise("getByServiceId", "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9");
Assert.equal(c, null, "Contacts that are not part of the default group should not be imported");
});

View File

@ -34,6 +34,7 @@
</gd:name>
<gd:email address="john.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/109576547678240773721" rel="profile"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1</id>
@ -51,6 +52,7 @@
</gd:name>
<gd:email address="jane.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/112886528199784431028" rel="profile"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2</id>
@ -68,6 +70,7 @@
</gd:name>
<gd:email address="davy.jones@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/109710625881478599011" rel="profile"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3</id>
@ -79,6 +82,7 @@
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="edit" type="application/atom+xml"/>
<gd:email address="noname@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7</id>
@ -90,6 +94,7 @@
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="edit" type="application/atom+xml"/>
<gd:email address="lycnix" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;RXkzfjVSLit7I2A9XRdRGUgITgA.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/8</id>
@ -101,6 +106,7 @@
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="edit" type="application/atom+xml"/>
<gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile" uri="tel:+31-6-12345678">0612345678</gd:phoneNumber>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;SX8-ejVSLit7I2A9XRdQFUkDRgY.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6</id>
@ -114,4 +120,21 @@
<gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile">215234523452345</gd:phoneNumber>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;Rn8zejVSLit7I2A9WhVRFUQOQQc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9</id>
<updated>2012-03-24T13:10:37.182Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-03-24T13:10:37.182Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>Little Smith</title>
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/9" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="edit" type="application/atom+xml"/>
<gd:name>
<gd:fullName>Little Smith</gd:fullName>
<gd:givenName>Little</gd:givenName>
<gd:familyName>Smith</gd:familyName>
</gd:name>
<gd:email address="littlebabysmith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/111456826635924971693" rel="profile"/>
</entry>
</feed>

View File

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

View File

@ -143,5 +143,15 @@ const methodHandlers = {
}
respondWithFile(res, "google_contacts.txt", "text/xml");
},
groups: function(req, res, params) {
try {
checkAuth(req);
} catch (ex) {
sendError(res, ex, ex.code);
}
respondWithFile(res, "google_groups.txt", "text/xml");
}
};

View File

@ -252,6 +252,7 @@ user_pref("loop.enabled", true);
user_pref("loop.throttled", false);
user_pref("loop.oauth.google.URL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=");
user_pref("loop.oauth.google.getContactsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=contacts");
user_pref("loop.oauth.google.getGroupsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=groups");
user_pref("loop.CSP","default-src 'self' about: file: chrome: data: wss://* http://* https://*");
// Ensure UITour won't hit the network