1 /* ***** BEGIN LICENSE BLOCK *****
  2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3  *
  4  * The contents of this file are subject to the Mozilla Public License Version
  5  * 1.1 (the "License"); you may not use this file except in compliance with
  6  * the License. You may obtain a copy of the License at
  7  * http://www.mozilla.org/MPL/
  8  *
  9  * Software distributed under the License is distributed on an "AS IS" basis,
 10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 11  * for the specific language governing rights and limitations under the
 12  * License.
 13  *
 14  * The Original Code is gContactSync.
 15  *
 16  * The Initial Developer of the Original Code is
 17  * Josh Geenen <gcontactsync@pirules.org>.
 18  * Portions created by the Initial Developer are Copyright (C) 2008-2010
 19  * the Initial Developer. All Rights Reserved.
 20  *
 21  * Contributor(s):
 22  *
 23  * Alternatively, the contents of this file may be used under the terms of
 24  * either the GNU General Public License Version 2 or later (the "GPL"), or
 25  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 26  * in which case the provisions of the GPL or the LGPL are applicable instead
 27  * of those above. If you wish to allow use of your version of this file only
 28  * under the terms of either the GPL or the LGPL, and not to allow others to
 29  * use your version of this file under the terms of the MPL, indicate your
 30  * decision by deleting the provisions above and replace them with the notice
 31  * and other provisions required by the GPL or the LGPL. If you do not delete
 32  * the provisions above, a recipient may use your version of this file under
 33  * the terms of any one of the MPL, the GPL or the LGPL.
 34  *
 35  * ***** END LICENSE BLOCK ***** */
 36 
 37 if (!com) var com = {}; // A generic wrapper variable
 38 // A wrapper for all GCS functions and variables
 39 if (!com.gContactSync) com.gContactSync = {};
 40 
 41 window.addEventListener("load",
 42   /** Initializes the ContactConverter class when the window has finished loading */
 43   function gCS_ContactConverterLoadListener(e) {
 44     com.gContactSync.ContactConverter.init();
 45   },
 46 false);
 47 
 48 
 49 /**
 50  * Converts contacts between Thunderbird's format (a 'card') and the Atom/XML
 51  * representation of a contact.  Must be initialized before the first use by
 52  * calling the init() function.
 53  * NOTE: The first 6 screennames of a contact from Google are stored as:
 54  * _AimScreenName, TalkScreenName, ICQScreenName, YahooScreenName, MSNScreenName
 55  * and JabberScreenName for compatibility with gContactSync 0.1b1 and the
 56  * default type for those textboxes.
 57  * @class
 58  */
 59 com.gContactSync.ContactConverter = {
 60   /** The GD Namespace */
 61   GD:            {},
 62   /** The ATOM/XML Namespace */
 63   ATOM:          {},
 64   /** The current TBContact being converted into a GContact */
 65   mCurrentCard:  {},
 66   /** An array of ContactConverter objects */
 67   mConverterArr: [],
 68   /**
 69    * Extra attributes added by this extension.  Doesn't include GoogleID or any
 70    * of the URLs.  Should be obtained w/ ContactConverter.getExtraSyncAttributes
 71    */
 72   mAddedAttributes: [
 73     "HomeFaxNumber", "OtherNumber", "ThirdEmail", "FourthEmail",
 74     "TalkScreenName", "ICQScreenName", "YahooScreenName", "MSNScreenName", 
 75     "JabberScreenName",  "PrimaryEmailType", "SecondEmailType",
 76     "ThirdEmailType", "FourthEmailType", "_AimScreenNameType",
 77     "TalkScreenNameType", "ICQScreenNameType", "YahooScreenNameType",
 78     "MSNScreenNameType", "JabberScreenNameType", "HomePhoneType",
 79     "WorkPhoneType", "FaxNumberType", "CellularNumberType", "PagerNumberType",
 80     "HomeFaxNumberType", "OtherNumberType", "Relation0", "Relation0Type",
 81     "Relation1", "Relation1Type", "Relation2", "Relation2Type", "Relation3",
 82     "Relation3Type", "CompanySymbol", "JobDescription",
 83     "WebPage1Type", "WebPage2Type"
 84   ],
 85   /** Stores whether this object has been initialized yet */
 86   mInitialized: false,
 87   /**
 88    * Initializes this object by populating the array of ConverterElement
 89    * objects and the two namespaces most commonly used by this object.
 90    */
 91   init: function ContactConverter_init() {
 92     this.GD = com.gContactSync.gdata.namespaces.GD;
 93     this.ATOM = com.gContactSync.gdata.namespaces.ATOM;
 94     var phoneTypes = com.gContactSync.Preferences.mSyncPrefs.phoneTypes.value;
 95     // ConverterElement(aElement, aTbName, aIndex, aType)
 96     // This array stores info on what tags in Google's feed sync with which
 97     // properties in Thunderbird.  gdata.contacts has info on these tags
 98     this.mConverterArr = [
 99       // Various components of a name
100       new com.gContactSync.ConverterElement("fullName",       "DisplayName",    0),
101       new com.gContactSync.ConverterElement("givenName",      "FirstName",      0),
102       new com.gContactSync.ConverterElement("familyName",     "LastName",       0),
103       new com.gContactSync.ConverterElement("additionalName", "AdditionalName", 0),
104       new com.gContactSync.ConverterElement("namePrefix",     "namePrefix",     0),
105       new com.gContactSync.ConverterElement("nameSuffix",     "nameSuffix",     0),
106       new com.gContactSync.ConverterElement("nickname",       "NickName",       0),
107       // general
108       new com.gContactSync.ConverterElement("notes",          "Notes",          0),
109       new com.gContactSync.ConverterElement("id",             "GoogleID",       0),
110       // e-mail addresses
111       new com.gContactSync.ConverterElement("email", "PrimaryEmail", 0, "other"),
112       new com.gContactSync.ConverterElement("email", "SecondEmail",  1, "other"),
113       new com.gContactSync.ConverterElement("email", "ThirdEmail",   2, "other"),
114       new com.gContactSync.ConverterElement("email", "FourthEmail",  3, "other"),
115       // IM screennames
116       new com.gContactSync.ConverterElement("im", "_AimScreenName",   0, "AIM"),
117       new com.gContactSync.ConverterElement("im", "TalkScreenName",   1, "GOOGLE_TALK"),
118       new com.gContactSync.ConverterElement("im", "ICQScreenName",    2, "ICQ"),
119       new com.gContactSync.ConverterElement("im", "YahooScreenName",  3, "YAHOO"),
120       new com.gContactSync.ConverterElement("im", "MSNScreenName",    4, "MSN"),
121       new com.gContactSync.ConverterElement("im", "JabberScreenName", 5, "JABBER"),
122       // the phone numbers
123       new com.gContactSync.ConverterElement("phoneNumber", "WorkPhone",      0, "work"),
124       new com.gContactSync.ConverterElement("phoneNumber", "HomePhone",      (phoneTypes ? 1 : 0), "home"),
125       new com.gContactSync.ConverterElement("phoneNumber", "FaxNumber",      (phoneTypes ? 2 : 0), "work_fax"),
126       new com.gContactSync.ConverterElement("phoneNumber", "CellularNumber", (phoneTypes ? 3 : 0), "mobile"),
127       new com.gContactSync.ConverterElement("phoneNumber", "PagerNumber",    (phoneTypes ? 4 : 0), "pager"),
128       new com.gContactSync.ConverterElement("phoneNumber", "HomeFaxNumber",  (phoneTypes ? 5 : 0), "home_fax"),
129       new com.gContactSync.ConverterElement("phoneNumber", "OtherNumber",    (phoneTypes ? 6 : 0), "other"),
130       // company info
131       new com.gContactSync.ConverterElement("orgTitle",          "JobTitle",       0),
132       new com.gContactSync.ConverterElement("orgName",           "Company",        0),
133       new com.gContactSync.ConverterElement("orgDepartment",     "Department",     0),
134       new com.gContactSync.ConverterElement("orgJobDescription", "JobDescription", 0),
135       new com.gContactSync.ConverterElement("orgSymbol",         "CompanySymbol",  0),
136       // the URLs from Google - Photo, Self, and Edit
137       new com.gContactSync.ConverterElement("PhotoURL", "PhotoURL", 0),
138       new com.gContactSync.ConverterElement("SelfURL",  "SelfURL",  0),
139       new com.gContactSync.ConverterElement("EditURL",  "EditURL",  0),
140       // Relation fields
141       new com.gContactSync.ConverterElement("relation", "Relation0", 0, ""),
142       new com.gContactSync.ConverterElement("relation", "Relation1", 1, ""),
143       new com.gContactSync.ConverterElement("relation", "Relation2", 2, ""),
144       new com.gContactSync.ConverterElement("relation", "Relation3", 3, ""),
145       // Websites
146       new com.gContactSync.ConverterElement("website",   "WebPage1", 0, "work"),
147       new com.gContactSync.ConverterElement("website",   "WebPage2", 1, "home"),
148     ];
149 
150     // Only synchronize (if possible) postal addresses if the preference was
151     // changed to true
152     if (com.gContactSync.Preferences.mSyncPrefs.syncAddresses.value) {
153       // Home address
154       this.mConverterArr.push(new com.gContactSync.ConverterElement("street",   "HomeAddress", 0, "home"));
155       this.mConverterArr.push(new com.gContactSync.ConverterElement("city",     "HomeCity",    0, "home"));
156       this.mConverterArr.push(new com.gContactSync.ConverterElement("region",   "HomeState",   0, "home"));
157       this.mConverterArr.push(new com.gContactSync.ConverterElement("postcode", "HomeZipCode", 0, "home"));
158       this.mConverterArr.push(new com.gContactSync.ConverterElement("country",  "HomeCountry", 0, "home"));
159       // Work address
160       this.mConverterArr.push(new com.gContactSync.ConverterElement("street",   "WorkAddress", 0, "work"));
161       this.mConverterArr.push(new com.gContactSync.ConverterElement("city",     "WorkCity",    0, "work"));
162       this.mConverterArr.push(new com.gContactSync.ConverterElement("region",   "WorkState",   0, "work"));
163       this.mConverterArr.push(new com.gContactSync.ConverterElement("postcode", "WorkZipCode", 0, "work"));
164       this.mConverterArr.push(new com.gContactSync.ConverterElement("country",  "WorkCountry", 0, "work"));
165       // full (formatted) addresses at the bottom so they have priority
166       this.mConverterArr.push(new com.gContactSync.ConverterElement("formattedAddress", "FullHomeAddress", 0, "home"));
167       this.mConverterArr.push(new com.gContactSync.ConverterElement("formattedAddress", "FullWorkAddress", 0, "work"));
168       this.mConverterArr.push(new com.gContactSync.ConverterElement("formattedAddress", "OtherAddress",    0, "other"));
169     }
170     this.mInitialized = true;
171   },
172   /**
173    * Returns an array of all of the extra attributes synced by this extension.
174    * @param aIncludeURLs {boolean} Should be true if the URL-related attributes
175    *                               should be returned.
176    */
177   getExtraSyncAttributes: function ContactConverter_getExtraSyncAttributes(aIncludeURLs, aIncludeAddresses) {
178     if (!this.mInitialized)
179       this.init();
180     var arr = this.mAddedAttributes.slice();
181     if (aIncludeURLs)
182       arr = arr.concat("PhotoURL", "SelfURL", "EditURL", "GoogleID");
183     if (aIncludeAddresses)
184       arr = arr.concat("FullHomeAddress", "FullWorkAddress", "OtherAddress");
185     return arr;
186   },
187   /**
188    * Updates or creates a GContact object's Atom/XML representation using its 
189    * complementary Address Book card.
190    * @param aTBContact    {TBContact} The address book card used to update the Atom
191    *                             feed.  Must be in an address book.
192    * @param aGContact {GContact} Optional. The GContact object with the Atom/XML
193    *                            representation of the contact, if it exists.  If
194    *                            not supplied, a contact and feed will be created.
195    * @returns {GContact} A GContact object with the Atom feed for the contact.
196    */
197   cardToAtomXML: function ContactConverter_cardToAtomXML(aTBContact, aGContact) {
198     var isNew = !aGContact,
199         ab    = aTBContact.mAddressBook,
200         arr   = this.mConverterArr,
201         i     = 0,
202         obj,
203         value,
204         type;
205     if (!aGContact)
206       aGContact = new com.gContactSync.GContact();
207     if (!this.mInitialized)
208       this.init();
209     if (!(aTBContact instanceof com.gContactSync.TBContact)) {
210       throw "Invalid TBContact sent to ContactConverter.cardToAtomXML from " +
211             this.caller;
212     }
213     if (!(ab instanceof com.gContactSync.AddressBook)) {
214       throw "Invalid TBContact (no mAddressBook) sent to " +
215             "ContactConverter.cardToAtomXML from " + this.caller;
216     }
217     this.mCurrentCard = aTBContact;
218     // set the regular properties from the array mConverterArr
219     for (i = 0, length = arr.length; i < length; i++) {
220       // skip the URLs
221       if (arr[i].tbName.indexOf("URL") !== -1 || arr[i].tbName === "GoogleID")
222         continue;
223       obj = arr[i];
224       com.gContactSync.LOGGER.VERBOSE_LOG(" * " + obj.tbName);
225       value = this.checkValue(aTBContact.getValue(obj.tbName));
226       // for the type, get the type from the card, or use its default
227       type = aTBContact.getValue(obj.tbName + "Type");
228       if (!type || type === "")
229         type = obj.type;
230       // see the dummy e-mail note below
231       if (obj.tbName === com.gContactSync.dummyEmailName &&
232           com.gContactSync.isDummyEmail(value)) {
233         value = null;
234         type  = null;
235       }
236       com.gContactSync.LOGGER.VERBOSE_LOG("   - " + value + " type: " + type);
237       aGContact.setValue(obj.elementName, obj.index, type, value);
238     }
239     // Birthday can be either YYYY-M-D or --M-D for no year.
240     // TB can have all three, just a day/month, or just a year through the UI
241     var birthDay    = aTBContact.getValue("BirthDay"),
242         birthMonth  = isNaN(parseInt(birthDay, 10)) ?
243                       null : aTBContact.getValue("BirthMonth"),
244         birthdayVal = null;
245     // if the contact has a birth month (and birth day) add it to the contact
246     // from Google
247     if (birthMonth && !isNaN(parseInt(birthMonth, 10))) {
248       var birthYear = aTBContact.getValue("BirthYear");
249       // if the birth year is empty, use '-'
250       if (!birthYear || isNaN(parseInt(birthYear, 10))) {
251         birthYear = "-";
252       }
253       // otherwise pad it to 4 characters
254       else {
255         birthYear = String(birthYear);
256         while (birthYear.length < 4) {
257           birthYear = "0" + birthYear;
258         }
259       }
260       // Pad the birth month to 2 characters
261       birthMonth = String(birthMonth);
262       while (birthMonth.length < 2) {
263         birthMonth = "0" + birthMonth;
264       }
265       // Pad the birth day to 2 characters
266       birthDay = String(birthDay);
267       while (birthDay.length < 2) {
268         birthDay = "0" + birthDay;
269       }
270       // form the birthday string: year-month-day
271       birthdayVal = birthYear + "-" + birthMonth + "-" + birthDay;
272     }
273     com.gContactSync.LOGGER.VERBOSE_LOG(" * Birthday: " + birthdayVal);
274     aGContact.setValue("birthday", 0, null, birthdayVal);
275 
276     // set the extended properties
277     aGContact.removeExtendedProperties();
278     arr = com.gContactSync.Preferences.mExtendedProperties;
279     var props = {};
280     for (i = 0, length = arr.length; i < length; i++) {
281       // add this extended property if it isn't a duplicate
282       if (!props[arr[i]]) {
283         props[arr[i]] = true;
284         value = this.checkValue(aTBContact.getValue(arr[i]));
285         aGContact.setExtendedProperty(arr[i], value);
286       }
287       else {
288         com.gContactSync.LOGGER.LOG_WARNING("Found a duplicate extended property: " +
289                                             arr[i]);
290       }
291     }
292     // If the myContacts pref is set and this contact is new then add the
293     // myContactsName group
294     if (ab.mPrefs.myContacts === "true") {
295       if (isNew && com.gContactSync.Sync.mContactsUrl) {
296         aGContact.setGroups([com.gContactSync.Sync.mContactsUrl]);
297       }
298     }
299     else {
300       // set the groups
301       var groups = [],
302           list;
303       com.gContactSync.LOGGER.VERBOSE_LOG(" * Determining the groups this contact belongs to");
304       for (i in com.gContactSync.Sync.mLists) {
305         list = com.gContactSync.Sync.mLists[i];
306         if (list instanceof com.gContactSync.GMailList) {
307           if (list.hasContact(aTBContact)) {
308             com.gContactSync.LOGGER.VERBOSE_LOG("   - " + list.getName());
309             groups.push(i);
310           }
311         }
312         else {
313           com.gContactSync.LOGGER.LOG_WARNING("   - Found an invalid list: " + i);
314         }
315       }
316       aGContact.setGroups(groups);
317     }
318     // Upload the photo
319     if (com.gContactSync.Preferences.mSyncPrefs.sendPhotos.value) {
320       // Get the profile directory
321       var file = Components.classes["@mozilla.org/file/directory_service;1"]
322                            .getService(Components.interfaces.nsIProperties)
323                            .get("ProfD", Components.interfaces.nsIFile);
324       // Get (or make) the Photos directory
325       file.append("Photos");
326       if (!file.exists() || !file.isDirectory())
327         file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0777);
328       file.append(aTBContact.getValue("PhotoName"));
329       if (file.exists() && file.isFile()) {
330         aGContact.setPhoto(Components.classes["@mozilla.org/network/io-service;1"]
331                                      .getService(Components.interfaces.nsIIOService)
332                                      .newFileURI(file));
333       }
334       else {
335         aGContact.setPhoto("");
336       }
337     }
338     return aGContact;
339   },
340   /**
341    * Converts an GContact's Atom/XML representation of a contact to
342    * Thunderbird's address book card format.
343    * @param aGContact {GContact} A GContact object with the contact to convert.
344    * @param aTBContact {TBContact}   An existing card that can be QI'd to
345    *                            Components.interfaces.nsIAbMDBCard if this is
346    *                            before 413260 landed.
347    * @returns {TBContact} The updated TBContact.
348    */
349   makeCard: function ContactConverter_makeCard(aGContact, aTBContact) {
350     if (!aGContact)
351       throw "Invalid aGContact parameter supplied to the 'makeCard' method" +
352             com.gContactSync.StringBundle.getStr("pleaseReport");
353     if (!this.mInitialized)
354       this.init();
355     if (!(aTBContact instanceof com.gContactSync.TBContact)) {
356       throw "Invalid TBContact sent to ContactConverter.makeCard from " +
357             this.caller;
358     }
359     var ab = aTBContact.mAddressBook;
360     if (!(ab instanceof com.gContactSync.AddressBook)) {
361       throw "Invalid TBContact (no mAddressBook) sent to " +
362             "ContactConverter.cardToAtomXML from " + this.caller;
363     }
364     var arr = this.mConverterArr,
365         blankProp = new com.gContactSync.Property("", "");
366     // get the regular properties from the array mConverterArr
367     for (var i = 0, length = arr.length; i < length; i++) {
368       var obj = arr[i],
369           property = aGContact.getValue(obj.elementName, obj.index, obj.type);
370       property = property || blankProp;
371       com.gContactSync.LOGGER.VERBOSE_LOG(obj.tbName + ": '" + property.value +
372                                           "', type: '" + property.type + "'");
373       // Thunderbird has problems with contacts who do not have an e-mail addr
374       // and are in Mailing Lists.  To avoid problems, use a dummy e-mail addr
375       // that is hidden from the user
376       if (obj.tbName === com.gContactSync.dummyEmailName && !property.value) {
377         property.value = com.gContactSync.makeDummyEmail(aGContact);
378         property.type  = "home";
379       }
380       // don't wipe out structured address info
381       if (property.value ||
382            (obj.elementName !== 'street' && obj.elementName !== 'city' &&
383             obj.elementName !== 'region' && obj.elementName !== 'postcode' &&
384             obj.elementName !== 'country')) {
385         aTBContact.setValue(obj.tbName, property.value);
386         // set the type, if it is an attribute with a type
387         if (property.type)
388           aTBContact.setValue(obj.tbName + "Type", property.type);
389       }
390       else {
391         com.gContactSync.LOGGER.VERBOSE_LOG("Going to avoid wiping out " + obj.tbName);
392       }
393     }
394     // get the extended properties
395     arr = com.gContactSync.Preferences.mExtendedProperties;
396     for (i = 0, length = arr.length; i < length; i++) {
397       var value = aGContact.getExtendedProperty(arr[i]);
398       value = value ? value.value : null;
399       aTBContact.setValue(arr[i], value);
400     }
401 
402     // parse the DisplayName into FirstName and LastName
403     if (com.gContactSync.Preferences.mSyncPrefs.parseNames.value) {
404       var name  = aTBContact.getValue("DisplayName"),
405           first = aTBContact.getValue("FirstName"),
406           last  = aTBContact.getValue("LastName");
407       // only parse if the contact has a name and there isn't already a first
408       // or last name set
409       if (name && !first && !last) {
410         var nameArr = [],
411             commaIndex;
412         if (name.split && name.indexOf) {
413           // If the name has a comma, it is probably <last>, <first>
414           commaIndex = name.indexOf(",");
415           if (commaIndex !== -1) {
416             name = name.replace(", ", ",");
417             var tmpArr = name.split(",");
418             nameArr.push(tmpArr[1]);
419             nameArr.push(tmpArr[0]);
420             // now fix the DisplayName
421             aTBContact.setValue("DisplayName", tmpArr[1] + " " + tmpArr[0]);
422           }
423           // If are there any DBCS characters in name, That's an asian name,
424           // <last> <first>
425           else if (encodeURI(name).replace(/%../g,"x").length != name.length) {
426             var tmpArr = name.split(" ");
427             if (tmpArr.length > 1) {
428               nameArr.push(tmpArr[1]);
429               nameArr.push(tmpArr[0]);
430             }
431             else
432               nameArr = [name];
433           }
434           // Otherwise assume it is <first> <last>
435           else
436             nameArr = name.split(" ");
437         }
438         else
439           nameArr = [name];
440         // take the first part of the name and set it as the first name
441         // then take the last and set it as the last name
442         first = nameArr.shift();
443         last  = nameArr.join(" ");
444         com.gContactSync.LOGGER.VERBOSE_LOG("FirstName\n" + first + "\nLastName\n" + last);
445         aTBContact.setValue("FirstName", first);
446         aTBContact.setValue("LastName", last);
447       }
448     }
449     
450     // Get the birthday info
451     var bday = aGContact.getValue("birthday", 0, com.gContactSync.gdata.contacts.types.UNTYPED),
452         year  = null,
453         month = null,
454         day   = null;
455     // If it has a birthday...
456     if (bday && bday.value) {
457       com.gContactSync.LOGGER.VERBOSE_LOG(" * Found a birthday value of " + bday.value);
458       // If it consists of all three date elements: YYYY-M-D
459       if (bday.value.indexOf("--") === -1) {
460         arr = bday.value.split("-");
461         year  = arr[0];
462         month = arr[1];
463         day   = arr[2];
464       }
465       // Else it is just a month and day: --M-D
466       else {
467         arr   = bday.value.replace("--", "").split("-");
468         month = arr[0];
469         day   = arr[1];
470       }
471       com.gContactSync.LOGGER.VERBOSE_LOG("  - Year:  " +  year);
472       com.gContactSync.LOGGER.VERBOSE_LOG("  - Month: " +  month);
473       com.gContactSync.LOGGER.VERBOSE_LOG("  - Day:   " +  day);
474     }
475     aTBContact.setValue("BirthYear",  year);
476     aTBContact.setValue("BirthMonth", month);
477     aTBContact.setValue("BirthDay",   day);
478 
479     if (com.gContactSync.Preferences.mSyncPrefs.getPhotos.value) {
480       var info = aGContact.getPhotoInfo();
481       // If the contact has a photo then save it to a local file and update
482       // the related attributes
483       if (info && info.etag &&
484           (file = aGContact.writePhoto(com.gContactSync.Sync.mCurrentAuthToken))) {
485         com.gContactSync.LOGGER.VERBOSE_LOG("Wrote photo...name: " + file.leafName);
486         aTBContact.setValue("PhotoName", file.leafName);
487         aTBContact.setValue("PhotoType", "file");
488         aTBContact.setValue("PhotoURI",
489                             Components.classes["@mozilla.org/network/io-service;1"]
490                                       .getService(Components.interfaces.nsIIOService)
491                                       .newFileURI(file)
492                                       .spec);
493         aTBContact.setValue("PhotoEtag", info.etag);
494       }
495       // If the contact doesn't have a photo then clear the related attributes
496       else {
497         aTBContact.setValue("PhotoName", "");
498         aTBContact.setValue("PhotoType", "");
499         aTBContact.setValue("PhotoURI",  "");
500         aTBContact.setValue("PhotoEtag", "");
501       }
502     }
503 
504     aTBContact.update();
505     if (ab.mPrefs.syncGroups == "true" && ab.mPrefs.myContacts != "true") {
506       // get the groups after updating the card
507       var groups = aGContact.getValue("groupMembershipInfo"),
508           lists  = com.gContactSync.Sync.mLists,
509           list,
510           group;
511       for (var i in lists) {
512         group = groups[i];
513         list  = lists[i];
514         // delete the card from the list, if necessary
515         if (list.hasContact(aTBContact)) {
516           if (!group) {
517             list.deleteContacts([aTBContact]);
518           }
519           aTBContact.update();
520         }
521         // add the card to the list, if necessary
522         else if (group) {
523           list.addContact(aTBContact);
524         }
525       }
526     }
527   },
528   /**
529    * Check if the given string is null, of length 0, or consists only of spaces
530    * and return null if any of the listed conditions is true.
531    * This function was added to fix Bug 20389: Values with only spaces should be
532    * treated as empty
533    * @param aValue {string} The string to check.
534    * @returns null   - The string is null, of length 0, or consists only of
535                       spaces
536    *         aValue - The string has at least one character that is not a space
537    */
538   checkValue: function ContactConverter_checkValue(aValue) {
539     if (!aValue || !aValue.length) return null;
540     for (var i = 0; i < aValue.length; i++)
541       if (aValue[i] != " ") return aValue;
542     return null;
543   }
544 };
545