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-2009
 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 /**
 42  * Makes a new GContact object that has functions to get and set various values
 43  * for a Google Contact's Atom/XML representation.  If the parameter aXml is not
 44  * supplied, this constructor will make a new contact.
 45  * @param aXml Optional.  The Atom/XML representation of this contact.  If not
 46  *             supplied, will make a new contact.
 47  * @class
 48  * @constructor
 49  */
 50 com.gContactSync.GContact = function gCS_GContact(aXml) {
 51   // if the contact exists, check its IM addresses
 52   if (aXml) {
 53     this.xml = aXml;
 54     this.checkIMAddress(); // check for invalid IM addresses
 55   }
 56   // otherwise, make a new contact
 57   else {
 58     this.mIsNew  = true;
 59     var atom     = com.gContactSync.gdata.namespaces.ATOM,
 60         gd       = com.gContactSync.gdata.namespaces.GD,
 61         xml      = document.createElementNS(atom.url, atom.prefix + "entry"),
 62         category = document.createElementNS(atom.url, atom.prefix + "category");
 63     category.setAttribute("scheme", gd.url + "#kind");
 64     category.setAttribute("term", gd.url + "#contact");
 65     xml.appendChild(category);
 66     this.xml = xml;
 67   }
 68   /** The current element being modified or returned (internal use only) */
 69   this.mCurrentElement = null;
 70   /** The groups that this contact is in */
 71   this.mGroups = {};
 72   /** The URI of a photo to add to this contact */
 73   this.mNewPhotoURI = null;
 74 };
 75 
 76 com.gContactSync.GContact.prototype = {
 77   /**
 78    * Checks for an invalid IM address as explained here:
 79    * http://pi3141.wordpress.com/2008/07/30/update-2/
 80    */
 81   checkIMAddress: function GContact_checkIMAddress() {
 82     var element = {},
 83         ns      = com.gContactSync.gdata.namespaces.GD.url,
 84         arr     = this.xml.getElementsByTagNameNS(ns, "im"),
 85         i       = 0,
 86         length  = arr.length,
 87         address;
 88     for (; i < length; i++) {
 89       address = arr[i].getAttribute("address");
 90       if (address && address.indexOf(": ") !== -1)
 91         arr[i].setAttribute("address", address.replace(": ", ""));
 92     }
 93   },
 94   /**
 95    * Gets the name and e-mail address of a contact from it's Atom
 96    * representation.
 97    */
 98   getName: function GContact_getName() {
 99     var contactName = "",
100         titleElem   = this.xml.getElementsByTagName('title')[0],
101         emailElem;
102     try {
103       if (titleElem && titleElem.childNodes[0]) {
104         contactName = titleElem.childNodes[0].nodeValue;
105       }
106       emailElem = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url,
107                                                       "email")[0];
108       if (emailElem && emailElem.getAttribute) {
109         if (contactName !== "")
110           contactName += " - ";
111         contactName += this.xml
112                            .getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url,
113                                                    "email")[0].getAttribute("address");
114       }
115     }
116     catch (e) {
117       com.gContactSync.LOGGER.LOG_WARNING("Unable to get the name or e-mail address of a contact", e);
118     }
119     return contactName;
120   },
121   /**
122    * Returns the value of an element with a type where the value is in the
123    * value of the child node.
124    * @param aElement {GElement} The GElement object with information about the
125    *                            value to get.
126    * @param aIndex   {int} The index of the value (ie 0 for primary email, 1 for
127    *                       second...).  Set to 0 if not supplied.
128    * @param aType    {string} The type, if the element can have types.
129    * @returns {Property} A new Property object with the value of the element, if
130    *                    found.  The type of the Property will be aType.
131    */
132   getElementValue: function GContact_getElementValue(aElement, aIndex, aType) {
133     if (!aIndex)
134       aIndex = 0;
135     this.mCurrentElement = null;
136     var arr = this.xml.getElementsByTagNameNS(aElement.namespace.url,
137                                               aElement.tagName),
138         counter = 0,
139         i       = 0,
140         length  = arr.length,
141         type;
142     // iterate through each of the elements that match the tag name
143     for (; i < length; i++) {
144       // if the current element matches the type (true if there isn't a type)...
145       if (this.isMatch(aElement, arr[i], aType)) {
146         // some properties, like e-mail, can have multiple elements in Google,
147         // so if this isn't the right one, go to the next element
148         if (counter !== aIndex) {
149           counter++;
150           continue;
151         }
152         this.mCurrentElement = arr[i];
153         // otherwise there is a match and it should be returned
154         // get the contact's "type" as defined in gdata and return the attribute's
155         // value based on where the value is actually stored in the element
156         switch (aElement.contactType) {
157         case com.gContactSync.gdata.contacts.types.TYPED_WITH_CHILD:
158           if (arr[i].childNodes[0]) {
159             type = arr[i].getAttribute("rel");
160             if (!type)
161               type = arr[i].getAttribute("label");
162             if (type)
163               type = type.substring(type.indexOf("#") + 1);
164             return new com.gContactSync.Property(arr[i].childNodes[0].nodeValue,
165                                                  type);
166           }
167           return null;
168         case com.gContactSync.gdata.contacts.types.TYPED_WITH_ATTR:
169           if (!aElement.attribute)
170             com.gContactSync.LOGGER.LOG_WARNING("Error - invalid element passed to the " +
171                                "getElementValue method." +
172                                com.gContactSync.StringBundle.getStr("pleaseReport"));
173           else {
174             if (aElement.tagName == "im")
175               type = arr[i].getAttribute("protocol");
176             else {
177               type = arr[i].getAttribute("rel");
178               if (!type)
179                 type = arr[i].getAttribute("label");
180             }
181             type = type.substring(type.indexOf("#") + 1);
182             return new com.gContactSync.Property(arr[i].getAttribute(aElement.attribute),
183                                                  type);
184           }
185         // fall through
186         case com.gContactSync.gdata.contacts.types.UNTYPED:
187         case com.gContactSync.gdata.contacts.types.PARENT_TYPED:
188           if (aElement.tagName === "birthday")
189             return new com.gContactSync.Property(arr[i].getAttribute("when"));
190           if (arr[i].childNodes[0])
191             return new com.gContactSync.Property(arr[i].childNodes[0].nodeValue);
192           return null;
193         default:
194           com.gContactSync.LOGGER.LOG_WARNING("Error - invalid contact type passed to the " +
195                                               "getElementValue method." +
196                                               com.gContactSync.StringBundle.getStr("pleaseReport"));
197           return null;
198         }
199       }
200     }
201     return null;
202   },
203   /**
204    * Google's contacts schema puts the organization name and job title in a
205    * separate element, so this function handles those two attributes separately.
206    * @param aElement {GElement} The GElement object with a valid org tag name
207    *                            (orgDepartment, orgJobDescription, orgName,
208    *                             orgSymbol, or orgTitle)
209    * @param aValue  {string}    The value to set.  Null if the XML Element
210    *                            should be removed.
211    */
212   setOrg: function GContact_setOrg(aElement, aValue) {
213     var tagName      = aElement ? aElement.tagName : null,
214         organization = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url,
215                                                        "organization")[0],
216         thisElem     = this.mCurrentElement;
217     if (!tagName || !com.gContactSync.gdata.contacts.isOrgTag(tagName))
218       return null;
219 
220     if (thisElem) {
221       // if there is an existing value that should be updated, do so
222       if (aValue)
223         this.mCurrentElement.childNodes[0].nodeValue = aValue;
224       // else the element should be removed
225       else {
226         thisElem.parentNode.removeChild(thisElem);
227         // If the org elem is empty remove it
228         if (!organization.childNodes.length) {
229           organization.parentNode.removeChild(organization);
230         }
231       }
232       return true;
233     }
234     // if it gets here, the node must be added, so add <organization> if necessary
235     if (!organization) {
236       organization = document.createElementNS(com.gContactSync.gdata.namespaces.GD.url,
237                                               "organization");
238       organization.setAttribute("rel", com.gContactSync.gdata.contacts.rel + "#other");
239       this.xml.appendChild(organization);
240     }
241     var elem = document.createElementNS(aElement.namespace.url,
242                                         aElement.tagName),
243         text = document.createTextNode(aValue);
244     elem.appendChild(text);
245 
246     organization.appendChild(elem);
247     return true;
248   },
249   /**
250    * Google's contacts schema puts several components of a name into a
251    * separate element, so this function handles those attributes separately.
252    * @param aElement {GElement} The GElement object with a valid gd:name tag
253    *                           (givenName, additionalName, familyName,
254    *                            namePrefix, nameSuffix, or fullName).
255    * @param aValue   {string}  The value to set.  Null if the XML Element should
256    *                           be removed.
257    */
258   setName: function GContact_setName(aElement, aValue) {
259     var tagName  = aElement ? aElement.tagName : null,
260         name     = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url,
261                                                        "name")[0],
262         thisElem = this.mCurrentElement;
263     if (!tagName || !com.gContactSync.gdata.contacts.isNameTag(tagName))
264       return null;
265 
266     if (thisElem) {
267       // if there is an existing value that should be updated, do so
268       if (aValue)
269         this.mCurrentElement.childNodes[0].nodeValue = aValue;
270       // else the element should be removed
271       else {
272         thisElem.parentNode.removeChild(thisElem);
273         // If the org elem is empty remove it
274         if (!name.childNodes.length)
275           name.parentNode.removeChild(name);
276       }
277       return true;
278     }
279     // if it gets here, the node must be added, so add <name> if necessary
280     if (!name) {
281       name = document.createElementNS(com.gContactSync.gdata.namespaces.GD.url,
282                                       "name");
283       this.xml.appendChild(name);
284     }
285     var elem = document.createElementNS(aElement.namespace.url,
286                                         aElement.tagName),
287         text = document.createTextNode(aValue);
288     elem.appendChild(text);
289 
290     name.appendChild(elem);    
291     return true;
292   },
293   /**
294    * Google's contacts schema puts several components of an address into a
295    * separate element, so this function handles those attributes separately.
296    * @param aElement {GElement} The GElement object with a valid
297    *                            gd:structuredPostalAddress tag name
298    * @param aValue   {string}   The value to set.  Null if the XML Element
299    *                            should be removed.
300    * @param aType    {string}   The 'type' of address (home, work, or other)
301    * @param aIndex   {int}      The index of the address (0 for the first, 1 for
302    *                            the second, etc)
303    */
304   setAddress: function GContact_setAddress(aElement, aValue, aType, aIndex) {
305     var tagName   = aElement ? aElement.tagName : null,
306         addresses = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url,
307                                                     "structuredPostalAddress"),
308         address   = null,
309         thisElem,
310         i         = 0;
311     if (!tagName || !com.gContactSync.gdata.contacts.isAddressTag(tagName))
312       return null;
313 
314     for (; i < addresses.length; i++) {
315       var type = addresses[i].hasAttribute("rel") ?
316         addresses[i].getAttribute("rel") :
317         addresses[i].getAttribute("label");
318       if (type && type.indexOf(aType) !== -1) {
319         address = addresses[i];
320         break;
321       }
322     }
323     // TODO how will this work w/ multiple addresses...
324     this.getElementValue(aElement, (aIndex ? aIndex : 0), aType);
325     thisElem = this.mCurrentElement;
326     com.gContactSync.LOGGER.VERBOSE_LOG("  - Setting address..." + address + " " + aValue + " " + aType + " " + thisElem);
327     if (thisElem && address) {
328       // if there is an existing value that should be updated, do so
329       if (aValue) {
330         // If a formatted address exists and we are updating the postal address
331         // then remove the old formatted address so Google can update it based on
332         // the new structured data
333         // http://groups.google.com/group/google-contacts-api/browse_thread/thread/ea623b18efb16963?hl=en&pli=1
334         for (i = 0; i < thisElem.parentNode.childNodes.length; i++) {
335           var node = thisElem.parentNode.childNodes[i];
336           if (node && node.tagName === "gd:formattedAddress") {
337             com.gContactSync.LOGGER.VERBOSE_LOG("Removing formatted address: " + node.childNodes[0].nodeValue);
338             node.parentNode.removeChild(node);
339             break;
340           }
341         }
342         this.mCurrentElement.childNodes[0].nodeValue = aValue;
343       }
344       // else the element should be removed
345       else {
346         thisElem.parentNode.removeChild(thisElem);
347         // If the elem is empty remove it
348         if (!address.childNodes.length)
349           address.parentNode.removeChild(address);
350       }
351       return true;
352     }
353     if (!aValue)
354       return true;
355     // if it gets here, the node must be added, so add <structuredPostalAddress> if necessary
356     if (!address) {
357       address = document.createElementNS(com.gContactSync.gdata.namespaces.GD.url,
358                                          "structuredPostalAddress");
359       com.gContactSync.gdata.contacts.setRelOrLabel(address, aType);
360       this.xml.appendChild(address);
361     }
362     var elem = document.createElementNS(aElement.namespace.url,
363                                         aElement.tagName);
364     var text = document.createTextNode(aValue);
365     elem.appendChild(text);
366 
367     address.appendChild(elem);    
368     return true;
369   },  
370   /**
371    * Sets the value of the specified element.
372    * @param aElement {GElement} The GElement object with information about the
373    *                            value to get.
374    * @param aIndex  {int}  The index of the value (ie 0 for primary email, 1 for
375    *                       second...).  Set to 0 if not supplied.
376    * @param aType    {string} The type, if the element can have types.
377    * @param aValue   {string} The value to set for the element.
378    */
379   setElementValue: function GContact_setElementValue(aElement, aIndex, aType, aValue) {
380     // Postal addresses are different...
381     if (com.gContactSync.gdata.contacts.isAddressTag(aElement.tagName))
382       return this.setAddress(aElement, aValue, aType, aIndex);
383     // get the current element (as this.mCurrentElement) and it's value (returned)
384     var property = this.getElementValue(aElement, aIndex, aType);
385     property = property ? property : new com.gContactSync.Property(null, null);
386     var value = property.value;
387     // if the current value is already good, check the type and return
388     if (value == aValue) {
389       if (value && property.type != aType) {
390         com.gContactSync.LOGGER.VERBOSE_LOG("Value is already good, changing type to: " + aType);
391         com.gContactSync.gdata.contacts.setRelOrLabel(this.mCurrentElement, aType);
392       }
393       else if (value)
394         com.gContactSync.LOGGER.VERBOSE_LOG("   - value " + value + " and type " + property.type + " are good");
395       return null;
396     }
397     // organization tags are special cases
398     if (com.gContactSync.gdata.contacts.isOrgTag(aElement.tagName))
399       return this.setOrg(aElement, aValue);
400     // name tags are as well
401     if (com.gContactSync.gdata.contacts.isNameTag(aElement.tagName))
402       return this.setName(aElement, aValue);
403 
404     // if the element should be removed
405     if (!aValue && this.mCurrentElement) {
406       try { this.mCurrentElement.parentNode.removeChild(this.mCurrentElement); }
407       catch (e) {
408         com.gContactSync.LOGGER.LOG_WARNING("Error while removing element: " + e + "\n" +
409                                             this.mCurrentElement);
410       }
411       this.mCurrentElement = null;
412     }
413     // otherwise set the value of the element
414     else {
415       switch (aElement.contactType) {
416         case com.gContactSync.gdata.contacts.types.TYPED_WITH_CHILD:
417           if (this.mCurrentElement && this.mCurrentElement.childNodes[0])
418             this.mCurrentElement.childNodes[0].nodeValue = aValue;
419           else {
420             if (!aType) {
421               com.gContactSync.LOGGER.LOG_WARNING("Invalid aType supplied to the 'setElementValue' "
422                                  + "method." + com.gContactSync.StringBundle.getStr("pleaseReport"));
423               return null;
424             }
425             var elem = this.mCurrentElement ? this.mCurrentElement :
426                                               document.createElementNS
427                                                        (aElement.namespace.url,
428                                                         aElement.tagName);
429             com.gContactSync.gdata.contacts.setRelOrLabel(elem, aType);
430             elem.appendChild(document.createTextNode(aValue));
431             this.xml.appendChild(elem);
432           }
433           break;
434         case com.gContactSync.gdata.contacts.types.TYPED_WITH_ATTR:
435           if (this.mCurrentElement)
436             this.mCurrentElement.setAttribute(aElement.attribute, aValue);
437           else {
438             var elem = document.createElementNS(aElement.namespace.url,
439                                                 aElement.tagName);
440             com.gContactSync.gdata.contacts.setRelOrLabel(elem, aType);
441             elem.setAttribute(aElement.attribute, aValue);
442             this.xml.appendChild(elem);
443           }
444           break;
445         case com.gContactSync.gdata.contacts.types.UNTYPED:
446         case com.gContactSync.gdata.contacts.types.PARENT_TYPED:
447           if (aElement.tagName == "birthday") {
448             // make sure the value at least has two -s
449             // valid formats: YYYY-M-D and --M-D
450             if (aValue.split("-").length < 3) {
451               com.gContactSync.LOGGER.LOG_WARNING("Detected an invalid birthday: " + aValue);
452               return null;
453             }
454             var elem = this.mCurrentElement ? this.mCurrentElement:
455                                               document.createElementNS
456                                                        (aElement.namespace.url,
457                                                         aElement.tagName);
458             elem.setAttribute("when", aValue);
459             // add the element to the XML feed if it is new
460             if (elem != this.mCurrentElement)
461               this.xml.appendChild(elem);
462             return true;
463           }
464           if (this.mCurrentElement && this.mCurrentElement.childNodes[0])
465             this.mCurrentElement.childNodes[0].nodeValue = aValue;
466           else {
467             var elem = this.mCurrentElement ? this.mCurrentElement:
468                                               document.createElementNS
469                                                        (aElement.namespace.url,
470                                                         aElement.tagName);
471             var text = document.createTextNode(aValue);
472             elem.appendChild(text);
473             this.xml.appendChild(elem);
474           }
475           break;
476         default:
477           com.gContactSync.LOGGER.LOG_WARNING("Invalid aType parameter sent to the setElementValue"
478                              + "method" + com.gContactSync.StringBundle.getStr("pleaseReport"));
479           return null;
480       }
481     }
482     return true;
483   },
484   /**
485    * Gets the last modified date from an contacts's XML feed in milliseconds
486    * since 1970.
487    * @returns {int} The last modified date of the entry in milliseconds from 1970
488    */
489   getLastModifiedDate: function GContact_getLastModifiedDate() {
490     try {
491       if (com.gContactSync.Preferences.mSyncPrefs.writeOnly.value) {
492         return 1;
493       }
494       var sModified = this.xml.getElementsByTagName('updated')[0].childNodes[0].nodeValue,
495           year      = sModified.substring(0,4),
496           month     = sModified.substring(5,7),
497           day       = sModified.substring(8,10),
498           hrs       = sModified.substring(11,13),
499           mins      = sModified.substring(14,16),
500           sec       = sModified.substring(17,19),
501           ms        = sModified.substring(20,23);
502       return parseInt(Date.UTC(year, parseInt(month, 10) - 1, day, hrs, mins, sec, ms));
503     }
504     catch(e) {
505       com.gContactSync.LOGGER.LOG_WARNING("Unable to get last modified date from a contact:\n" + e);
506     }
507     return 0;
508   },
509   /**
510    * Removes all extended properties from this contact.
511    */
512   removeExtendedProperties: function GContact_removeExtendedProperties() {
513     var arr = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url, "extendedProperty");
514     for (var i = arr.length - 1; i > -1 ; i--) {
515       arr[i].parentNode.removeChild(arr[i]);
516     }
517   },
518   /**
519    * Returns the value of the extended property with a matching name attribute.
520    * @param aName {string} The name of the extended property to return.
521    * @returns {Property} A Property object with the value of the extended
522    *                    property with the name attribute aName.
523    */
524   getExtendedProperty: function GContact_getExtendedProperty(aName) {
525     var arr = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url, "extendedProperty");
526     for (var i = 0, length = arr.length; i < length; i++)
527       if (arr[i].getAttribute("name") == aName)
528         return new com.gContactSync.Property(arr[i].getAttribute("value"));
529     return null;
530   },
531   /**
532    * Sets an extended property with the given name and value if there are less
533    * than 10 existing.  Logs a warning if there are already 10 or more.
534    * @param aName  {string} The name of the property.
535    * @param aValue {string} The value of the property.
536    */
537   setExtendedProperty: function GContact_setExtendedProperty(aName, aValue) {
538     if (this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url,
539         "extendedProperty").length >= 10) {
540       com.gContactSync.LOGGER.LOG_WARNING("Attempt to add too many properties aborted");
541       return null;
542     }
543     if (aValue && aValue != "") {
544       var property = document.createElementNS(com.gContactSync.gdata.namespaces.GD.url,
545                                               "extendedProperty");
546       property.setAttribute("name", aName);
547       property.setAttribute("value", aValue);
548       this.xml.appendChild(property);
549       return true;
550     }
551     return null;
552   },
553   /**
554    * Returns the value of the XML Element with the supplied tag name at the
555    * given index of the given type (home, work, other, etc.)
556    * @param aName  {string} The tag name of the value to get.  See gdata for
557                             valid tag names.
558    * @param aIndex {int} Optional.  The index, if non-zero, of the value to get.
559    * @param aType  {string} The type of element to get if the tag name has
560    *                        different types (home, work, other, etc.).
561    * @returns {Property} A new Property object with the value and type, if
562    *                    applicable.
563    *                    If aName is groupMembership info, returns an array of
564    *                    the group IDs
565    */
566   getValue: function GContact_getValue(aName, aIndex, aType) {
567     // TODO uncomment
568     //try {
569       // if the value to obtain is a link, get the value for the link
570       if (com.gContactSync.gdata.contacts.links[aName]) {
571         var arr = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.ATOM.url, "link");
572         for (var i = 0, length = arr.length; i < length; i++)
573           if (arr[i].getAttribute("rel") == com.gContactSync.gdata.contacts.links[aName])
574             return new com.gContactSync.Property(arr[i].getAttribute("href"));
575       }
576       else if (aName == "groupMembershipInfo")
577         return this.getGroups();
578       // otherwise, if it is a normal attribute, get it's value
579       else if (com.gContactSync.gdata.contacts[aName])
580         return this.getElementValue(com.gContactSync.gdata.contacts[aName], aIndex, aType);
581       // if the name of the value to get is something else, throw an error
582       else
583         com.gContactSync.LOGGER.LOG_WARNING("Unable to getValue for " + aName);
584     //}
585     //catch(e) {
586     //  com.gContactSync.LOGGER.LOG_WARNING("Error in GContact.getValue:\n" + e);
587     //}
588     return null;
589   },
590   /**
591    * Sets the value with the name aName to the value aValue based on the type
592    * and index.
593    * @param aName  {string} The tag name of the value to set.
594    * @param aIndex {int}    The index of the element whose value is set.
595    * @param aType  {string} The type of the element (home, work, other, etc.).
596    * @param aValue {string} The value to set.  null if the element should be
597    *                        removed.
598    */
599   setValue: function GContact_setValue(aName, aIndex, aType, aValue) {
600     try {
601       if (aValue == "")
602         aValue = null;
603       if (aType == "Home" || aType == "Work" || aType == "Other") {
604         com.gContactSync.LOGGER.LOG_WARNING("Found and fixed an invalid type: " + aType);
605         aType = aType.toLowerCase();
606       }
607       com.gContactSync.LOGGER.VERBOSE_LOG("   - " + aName + " - " + aIndex + " - " + aType + " - " + aValue);
608       if (com.gContactSync.gdata.contacts[aName] && aName != "groupMembershipInfo")
609         return this.setElementValue(com.gContactSync.gdata.contacts[aName],
610                                     aIndex, aType, aValue);
611       // if the name of the value to get is something else, throw an error
612       else
613         com.gContactSync.LOGGER.LOG_WARNING("Unable to setValue for " + aName + " - " + aValue);
614     }
615     catch(e) {
616       com.gContactSync.LOGGER.LOG_WARNING("Error in GContact.setValue:\n" + e);
617     }
618     return null;
619   },
620   /**
621    * Returns an array of the names of the groups to which this contact belongs.
622    */
623   getGroups: function GContact_getGroups() {
624     var groupInfo = com.gContactSync.gdata.contacts.groupMembershipInfo;
625     var arr = this.xml.getElementsByTagNameNS(groupInfo.namespace.url,
626                                               groupInfo.tagName);
627     var groups = {};
628     // iterate through each group and add the group as a new property of the
629     // groups object with the ID as the name of the property.
630     for (var i = 0, length = arr.length; i < length; i++) {
631       var id    = com.gContactSync.fixURL(arr[i].getAttribute("href")),
632           group = com.gContactSync.Sync.mGroups[id];
633       if (group)
634         groups[id] = group;
635       else {
636         if (com.gContactSync.Preferences.mSyncPrefs.myContacts)
637           groups[id] = true;
638         else
639           com.gContactSync.LOGGER.LOG_WARNING("Unable to find group: " + id);
640       }
641     }
642     // return the object with the groups this contact belongs to
643     return groups;
644   },
645   /**
646    * Removes all groups from this contact.
647    */
648   clearGroups: function GContact_clearGroups() {
649     var groupInfo = com.gContactSync.gdata.contacts.groupMembershipInfo;
650     var arr = this.xml.getElementsByTagNameNS(groupInfo.namespace.url,
651                                               groupInfo.tagName);
652     // iterate through every group element and remove it from the XML
653     for (var i = 0; i < arr.length; i++) {
654       try {
655         if (arr[i]) {
656           arr[i].parentNode.removeChild(arr[i]);
657         }
658       }
659       catch(e) {
660         com.gContactSync.LOGGER.LOG_WARNING("Error while trying to clear group: " + arr[i], e);
661       }
662     }
663     this.mGroups = {};
664   },
665   /**
666    * Sets the groups of that this contact is in based on the array of IDs.
667    * @param aGroups {array} An array of the IDs of the groups to which the
668    *                        contact should belong.
669    */
670   setGroups: function GContact_setGroups(aGroups) {
671     this.clearGroups(); // clear existing groups
672     if (!aGroups)
673       return null;
674     // make sure the group 
675     for (var i = 0, length = aGroups.length; i < length; i++) {
676       var id = aGroups[i];
677       // if the ID isn't valid log a warning and go to the next ID
678       if (!id || !id.indexOf || id.indexOf("www.google.com/m8/feeds/groups") == -1) {
679         com.gContactSync.LOGGER.LOG_WARNING("Invalid id in aGroups: " + id);
680         continue;
681       }
682       this.addToGroup(id);
683     }
684     return true;
685   },
686   /**
687    * Removes the contact from the given group element.
688    * @param aGroup {Group} The group from which the contact should be removed.
689    */
690   removeFromGroup: function GContact_removeFromGroup(aGroup) {
691     if (!aGroup) {
692       com.gContactSync.LOGGER.LOG_WARNING("Attempt to remove a contact from a non-existant group");
693       return null;
694     }
695     try {
696       aGroup.parentNode.removeChild(aGroup);
697       return true;
698     }
699     catch (e) {
700       com.gContactSync.LOGGER.LOG_WARNING("Error while trying to remove a contact from a group: " + e);
701     }
702     return null;
703   },
704   /**
705    * Adds the contact to the given, existing, group.
706    * @param aGroupURL {string} The URL of an existing group to which the contact
707    *                           will be added.
708    */
709   addToGroup: function GContact_addToGroup(aGroupURL) {
710     if (!aGroupURL) {
711       com.gContactSync.LOGGER.LOG_WARNING("Attempt to add a contact to a non-existant group");
712       return null;
713     }
714     try {
715       var ns = com.gContactSync.gdata.namespaces.GCONTACT;
716       var group = document.createElementNS(ns.url,
717                                            ns.prefix + "groupMembershipInfo");
718       group.setAttribute("deleted", false);
719       group.setAttribute("href", aGroupURL);
720       this.xml.appendChild(group);
721       return true;
722     }
723     catch(e) {
724       com.gContactSync.LOGGER.LOG_WARNING("Error while trying to add a contact to a group: " + e);
725     }
726     return null;
727   },
728   /**
729    * Returns true if the given XML Element is a match for the GElement object
730    * and the type (ie home, work, other, etc.)
731    * @param aElement {GElement}    The GElement object (@see GElement.js)
732    * @param aXmlElem {XML Element} The XML Element to check
733    * @param aType    {string}      The type (home, work, other, etc.)
734    */
735   isMatch: function GContact_isMatch(aElement, aXmlElem, aType, aDontSkip) {
736     if (aElement.contactType === com.gContactSync.gdata.contacts.types.UNTYPED)
737       return true;
738     // if the parent contains the type then get the XML element's parent
739     else if (aElement.contactType === com.gContactSync.gdata.contacts.types.PARENT_TYPED)
740       aXmlElem = aXmlElem.parentNode;
741     // If this is a phone number, check the phoneTypes pref
742     // If the pref is true, then always say that this is a match
743     // If the pref is false, continue with the normal type check
744     if (aElement.tagName === "phoneNumber" &&
745         com.gContactSync.Preferences.mSyncPrefs.phoneTypes.value) {
746       return true;
747     }
748     switch (aElement.tagName) {
749       case "email":
750       case "website": // TODO - should this be typed?
751       case "relation":
752         if (!aDontSkip) // always return true for e-mail by default
753           return true;
754       case "im":
755         if (!aDontSkip) // always return true for e-mail by default
756           return true;
757         var str = aXmlElem.getAttribute("protocol");
758         break;
759       default:
760         var str = aXmlElem.getAttribute("rel");
761     }
762     if (!str)
763       return false;
764     // get only the very end
765     var str = str.substring(str.length - aType.length);
766     return str == aType; // return true if the end is equal to aType
767   },
768   /**
769    * Returns the last portion of this contact's ID, or optionally, the full ID.
770    * @param {boolean} aFull Set this to true to return the complete ID for this
771    *                        contact (the entire URL).
772    *                        Otherwise just the portion after the last / is
773    *                        returned.
774    * @returns {string} The ID of this contact.
775    */
776   getID: function GContact_getID(aFull) {
777     var val   = this.getValue("id").value;
778     if (aFull) {
779       return com.gContactSync.fixURL(val); // make sure to change http to https
780     }
781     var index = val.lastIndexOf("/");
782     return val.substr(index + 1);
783   },
784   /**
785    * Sets the photo for this contact.  Note that this may not immediately take
786    * effect as contacts must be added to Google and then retrieved before a
787    * photo can be added.
788    *
789    * @param aURI {string|nsIURI} A string with the URI of a contact photo.
790    */
791   setPhoto: function GContact_setPhoto(aURI) {
792     com.gContactSync.LOGGER.VERBOSE_LOG("Entering GContact.setPhoto:");
793     var photoInfo = this.getPhotoInfo();
794     // If the URI is empty or a chrome URL remove the photo, if present
795     // TODO - this should probably just check if it is the default photo
796     if (!aURI) {
797       // Easy case: URI is empty and this contact doesn't have a photo
798       if (!photoInfo || !(photoInfo.etag)) {
799         com.gContactSync.LOGGER.VERBOSE_LOG(" * URI is empty, contact has no photo");
800         return;
801       }
802       com.gContactSync.LOGGER.VERBOSE_LOG(" * URI is empty, photo will be removed");
803       // Remove the photo
804       var httpReq = new com.gContactSync.GHttpRequest("delete",
805                                                       com.gContactSync.Sync.mCurrentAuthToken,
806                                                       photoInfo.url,
807                                                       null,
808                                                       com.gContactSync.Sync.mCurrentUsername);
809       httpReq.mOnSuccess = function setPhotoSuccess() {
810         com.gContactSync.LOGGER.VERBOSE_LOG(" * Photo successfully removed");
811       };
812       httpReq.mOnError   = function setPhotoError(httpReq) {
813         com.gContactSync.LOGGER.LOG_ERROR('Error while removing photo',
814                                           httpReq.responseText);
815       };
816       httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
817       httpReq.addHeaderItem("If-Match", "*");
818       httpReq.send();
819       return;
820     }
821     // The URI exists, so update or add the photo
822     // NOTE: A photo cannot be added until the contact has been added
823     else {
824       // If this is a new contact then nothing can be done until the contact is
825       // added to Google
826       if (this.mIsNew || !photoInfo) {
827         com.gContactSync.LOGGER.VERBOSE_LOG(" * Photo will be added after the contact is created");
828         this.mNewPhotoURI = aURI;
829         return;
830       }
831       com.gContactSync.LOGGER.VERBOSE_LOG(" * Photo will be updated");
832       // Otherwise send the PUT request
833       // TODO - this really needs error handling...
834       var ios = Components.classes["@mozilla.org/network/io-service;1"]
835                           .getService(Components.interfaces.nsIIOService),
836           outChannel = ios.newChannel(photoInfo.url, null, null),
837           inChannel  = aURI instanceof Components.interfaces.nsIURI ?
838                          ios.newChannelFromURI(aURI) :
839                          ios.newChannel(aURI, null, null);
840       outChannel = outChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
841       // Set the upload data
842       outChannel = outChannel.QueryInterface(Components.interfaces.nsIUploadChannel);
843       // Set the input stream as the photo URI
844       // See https://www.mozdev.org/bugs/show_bug.cgi?id=22757 for the try/catch
845       // block, I didn't see a way to tell if the item pointed to by aURI exists
846       try {
847         outChannel.setUploadStream(inChannel.open(), photoInfo.type, -1);
848       }
849       catch (e) {
850         com.gContactSync.LOGGER.LOG_WARNING("The photo at '" + aURI + "' doesn't exist", e);
851         return;
852       }
853       // set the request type to PUT (this has to be after setting the upload data)
854       outChannel = outChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
855       outChannel.requestMethod = "PUT";
856       // Setup the header: Authorization and Content-Type: image/*
857       outChannel.setRequestHeader("Authorization", com.gContactSync.Sync.mCurrentAuthToken, false);
858       outChannel.setRequestHeader("Content-Type",  photoInfo.type, false);
859       outChannel.setRequestHeader("If-Match",      "*", false);
860       // set the status bar text since this can take a minute
861       com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr("uploadingPhoto"));
862       outChannel.open();
863       try {
864         com.gContactSync.LOGGER.VERBOSE_LOG(" * Update status: " + outChannel.responseStatus);
865       }
866       catch (e) {
867         com.gContactSync.LOGGER.LOG_WARNING(" * outChannel.responseStatus failed", e);
868       }
869     }
870   },
871   /**
872    * Returns an object with information about this contact's photo.
873    * @returns An object containing the following properties:
874    *  - url - The URL of the contact's photo
875    *  - type - The type of photo (ex "image/*")
876    *  - etag - The etag of the photo (if a photo exists).
877    * If there was no photo found (no etag) the etag is blank.
878    * If this contact is new then this function returns null.
879    */
880   getPhotoInfo: function GContact_hasPhoto() {
881     // Sample photo XML:
882     // <link rel='http://schemas.google.com/contacts/2008/rel#photo' type='image/*'
883     //  href='http://google.com/m8/feeds/photos/media/liz%40gmail.com/c9012de'
884     // gd:etag='"KTlcZWs1bCp7ImBBPV43VUV4LXEZCXERZAc."'/>
885     var arr = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.ATOM.url, "link");
886     var elem, etag;
887     for (var i = 0, length = arr.length; i < length; i++) {
888       elem = arr[i];
889       if (elem.getAttribute("rel") == com.gContactSync.gdata.contacts.links["PhotoURL"]) {
890         return {
891           url:  elem.getAttribute("href"),
892           type: elem.getAttribute("type"),
893           etag: elem.getAttributeNS(com.gContactSync.gdata.namespaces.GD.url, "etag")
894         }
895       }
896     }
897     return null;
898   },
899   /**
900    * Fetches and saves a local copy of this contact's photo, if present.
901    * NOTE: Portions of this code are from Thunderbird written by me (Josh Geenen)
902    * See https://bugzilla.mozilla.org/show_bug.cgi?id=119459
903    *
904    * TODO - merge w/ com.gContactSync.writePhoto
905    * @param aAuthToken {string} The authentication token for the account to
906    *                            which this contact belongs.
907    */
908   writePhoto: function GContact_writePhoto(aAuthToken) {
909     com.gContactSync.LOGGER.VERBOSE_LOG(" * Checking for a contact photo");
910     if (!aAuthToken) {
911       com.gContactSync.LOGGER.LOG_WARNING("No auth token passed to GContact.writePhoto");
912       return null;
913     }
914     var info = this.getPhotoInfo();
915     if (!info) {
916       com.gContactSync.LOGGER.VERBOSE_LOG(" * This contact does not have a photo");
917       return null;
918     }
919     // Get the profile directory
920     var file = Components.classes["@mozilla.org/file/directory_service;1"]
921                          .getService(Components.interfaces.nsIProperties)
922                          .get("ProfD", Components.interfaces.nsIFile);
923     // Get (or make) the Photos directory
924     file.append("Photos");
925     if (!file.exists() || !file.isDirectory())
926       file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0777);
927     var ios = Components.classes["@mozilla.org/network/io-service;1"]
928                         .getService(Components.interfaces.nsIIOService);
929     var ch = ios.newChannel(info.url, null, null);
930     ch.QueryInterface(Components.interfaces.nsIHttpChannel);
931     ch.setRequestHeader("Authorization", aAuthToken, false);
932     var istream = ch.open();
933     // quit if the request failed
934     if (!ch.requestSucceeded) {
935       com.gContactSync.LOGGER.LOG_WARNING("The request to retrive the photo returned with a status ",
936                          ch.responseStatus);
937       return null;
938     }
939 
940     // Create a name for the photo with the contact's ID and the photo extension
941     var filename = this.getID(false);
942     try {
943       var ext = com.gContactSync.findPhotoExt(ch);
944       filename = filename + (ext ? "." + ext : "");
945     }
946     catch (e) {
947       com.gContactSync.LOGGER.LOG_WARNING("Couldn't find an extension for the photo");
948     }
949     file.append(filename);
950     com.gContactSync.LOGGER.VERBOSE_LOG(" * Writing the photo to " + file.path);
951 
952     var output = Components.classes["@mozilla.org/network/file-output-stream;1"]
953                            .createInstance(Components.interfaces.nsIFileOutputStream);
954 
955     // Now write that input stream to the file
956     var fstream = Components.classes["@mozilla.org/network/safe-file-output-stream;1"]
957                             .createInstance(Components.interfaces.nsIFileOutputStream);
958     var buffer = Components.classes["@mozilla.org/network/buffered-output-stream;1"]
959                            .createInstance(Components.interfaces.nsIBufferedOutputStream);
960     fstream.init(file, 0x04 | 0x08 | 0x20, 0600, 0); // write, create, truncate
961     buffer.init(fstream, 8192);
962     while (istream.available() > 0) {
963       buffer.writeFrom(istream, istream.available());
964     }
965 
966     // Close the output streams
967     if (buffer instanceof Components.interfaces.nsISafeOutputStream)
968         buffer.finish();
969     else
970         buffer.close();
971     if (fstream instanceof Components.interfaces.nsISafeOutputStream)
972         fstream.finish();
973     else
974         fstream.close();
975     // Close the input stream
976     istream.close();
977     return file;
978   },
979   /**
980    * Sets the value of a given attribute for the ith element with the given
981    * tag name and namespace.
982    *
983    * @param aTagName       {string} The name of the tag.
984    * @param aNamespace     {string} The namespace of the tag.
985    * @param aIndex         {int}    The index of the element whose attribute is
986    *                                to be set.
987    * @param aAttributeName {string} The name of the attribute to set.
988    * @param aValue         {string} The value to set.
989    *
990    * @returns {boolean} True if the element was found and the attribute was set.
991    */
992   setAttribute: function GContact_setAttribute(aTagName, aNamespace, aIndex, aAttributeName, aValue) {
993     var elems = this.xml.getElementsByTagNameNS(aNamespace, aTagName);
994     if (elems.length > aIndex && aIndex >= 0) {
995       elems[aIndex].setAttribute(aAttributeName, aValue);
996       return true;
997     }
998     return false;
999   },
1000   /**
1001    * Gets the value of a given attribute for the ith element with the given
1002    * tag name and namespace.
1003    *
1004    * @param aTagName       {string} The name of the tag.
1005    * @param aNamespace     {string} The namespace of the tag.
1006    * @param aIndex         {int}    The index of the element whose attribute is
1007    *                                to be returned.
1008    * @param aAttributeName {string} The name of the attribute to get.
1009    *
1010    * @returns {boolean} The value of the attribute for the described element.
1011    */
1012   getAttribute: function GContact_getAttribute(aTagName, aNamespace, aIndex, aAttributeName) {
1013     var elems = this.xml.getElementsByTagNameNS(aNamespace, aTagName);
1014     if (elems.length > aIndex && aIndex >= 0) {
1015       return elems[aIndex].getAttribute(aAttributeName);
1016     }
1017     return null;
1018   }
1019 };
1020