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       if (addresses[i].getAttribute("rel").indexOf(aType) !== -1)
316         address = addresses[i];
317     }
318     // TODO how will this work w/ multiple addresses...
319     this.getElementValue(aElement, (aIndex ? aIndex : 0), aType);
320     thisElem = this.mCurrentElement;
321     com.gContactSync.LOGGER.VERBOSE_LOG("  - Setting address..." + address + " " + aValue + " " + aType + " " + thisElem);
322     if (thisElem && address) {
323       // if there is an existing value that should be updated, do so
324       if (aValue) {
325         // If a formatted address exists and we are updating the postal address
326         // then remove the old formatted address so Google can update it based on
327         // the new structured data
328         // http://groups.google.com/group/google-contacts-api/browse_thread/thread/ea623b18efb16963?hl=en&pli=1
329         for (i = 0; i < thisElem.parentNode.childNodes.length; i++) {
330           var node = thisElem.parentNode.childNodes[i];
331           if (node && node.tagName === "gd:formattedAddress") {
332             com.gContactSync.LOGGER.VERBOSE_LOG("Removing formatted address: " + node.childNodes[0].nodeValue);
333             node.parentNode.removeChild(node);
334             break;
335           }
336         }
337         this.mCurrentElement.childNodes[0].nodeValue = aValue;
338       }
339       // else the element should be removed
340       else {
341         thisElem.parentNode.removeChild(thisElem);
342         // If the elem is empty remove it
343         if (!address.childNodes.length)
344           address.parentNode.removeChild(address);
345       }
346       return true;
347     }
348     if (!aValue)
349       return true;
350     // if it gets here, the node must be added, so add <structuredPostalAddress> if necessary
351     if (!address) {
352       address = document.createElementNS(com.gContactSync.gdata.namespaces.GD.url,
353                                               "structuredPostalAddress");
354       address.setAttribute("rel", "http://schemas.google.com/g/2005#" + aType);
355       this.xml.appendChild(address);
356     }
357     var elem = document.createElementNS(aElement.namespace.url,
358                                         aElement.tagName);
359     var text = document.createTextNode(aValue);
360     elem.appendChild(text);
361 
362     address.appendChild(elem);    
363     return true;
364   },  
365   /**
366    * Sets the value of the specified element.
367    * @param aElement {GElement} The GElement object with information about the
368    *                            value to get.
369    * @param aIndex  {int}  The index of the value (ie 0 for primary email, 1 for
370    *                       second...).  Set to 0 if not supplied.
371    * @param aType    {string} The type, if the element can have types.
372    * @param aValue   {string} The value to set for the element.
373    */
374   setElementValue: function GContact_setElementValue(aElement, aIndex, aType, aValue) {
375     // Postal addresses are different...
376     if (com.gContactSync.gdata.contacts.isAddressTag(aElement.tagName))
377       return this.setAddress(aElement, aValue, aType, aIndex);
378     // get the current element (as this.mCurrentElement) and it's value (returned)
379     var property = this.getElementValue(aElement, aIndex, aType);
380     property = property ? property : new com.gContactSync.Property(null, null);
381     var value = property.value;
382     // if the current value is already good, check the type and return
383     if (value == aValue) {
384       if (value && property.type != aType) {
385         com.gContactSync.LOGGER.VERBOSE_LOG("Value is already good, changing type to: " + aType);
386         if (aElement.tagName == "im")
387           this.mCurrentElement.setAttribute("protocol", com.gContactSync.gdata.contacts.rel + "#" + aType);
388         else if (aElement.tagName == "relation") {
389           if (com.gContactSync.gdata.contacts.RELATION_TYPES[aType])
390             this.mCurrentElement.setAttribute("rel", aType);
391           else
392             this.mCurrentElement.setAttribute("label", aType);
393         }
394         else if (aElement.tagName == "website")
395           this.mCurrentElement.setAttribute("rel", aType);
396         else
397           this.mCurrentElement.setAttribute("rel", com.gContactSync.gdata.contacts.rel + "#" + aType);
398       }
399       else if (value)
400         com.gContactSync.LOGGER.VERBOSE_LOG("   - value " + value + " and type " + property.type + " are good");
401       return null;
402     }
403     // organization tags are special cases
404     if (com.gContactSync.gdata.contacts.isOrgTag(aElement.tagName))
405       return this.setOrg(aElement, aValue);
406     // name tags are as well
407     if (com.gContactSync.gdata.contacts.isNameTag(aElement.tagName))
408       return this.setName(aElement, aValue);
409 
410     // if the element should be removed
411     if (!aValue && this.mCurrentElement) {
412       try { this.mCurrentElement.parentNode.removeChild(this.mCurrentElement); }
413       catch (e) {
414         com.gContactSync.LOGGER.LOG_WARNING("Error while removing element: " + e + "\n" +
415                                             this.mCurrentElement);
416       }
417       this.mCurrentElement = null;
418     }
419     // otherwise set the value of the element
420     else {
421       switch (aElement.contactType) {
422         case com.gContactSync.gdata.contacts.types.TYPED_WITH_CHILD:
423           if (this.mCurrentElement && this.mCurrentElement.childNodes[0])
424             this.mCurrentElement.childNodes[0].nodeValue = aValue;
425           else {
426             if (!aType) {
427               com.gContactSync.LOGGER.LOG_WARNING("Invalid aType supplied to the 'setElementValue' "
428                                  + "method." + com.gContactSync.StringBundle.getStr("pleaseReport"));
429               return null;
430             }
431             var elem = this.mCurrentElement ? this.mCurrentElement :
432                                               document.createElementNS
433                                                        (aElement.namespace.url,
434                                                         aElement.tagName);
435             if (elem.tagName == "relation") {
436               if (com.gContactSync.gdata.contacts.RELATION_TYPES[aType])
437                 elem.setAttribute("rel", aType);
438               else
439                 elem.setAttribute("label", aType);
440             }
441             else
442               elem.setAttribute("rel", com.gContactSync.gdata.contacts.rel + "#" + aType);
443             var text = document.createTextNode(aValue);
444             elem.appendChild(text);
445             this.xml.appendChild(elem);
446           }
447           break;
448         case com.gContactSync.gdata.contacts.types.TYPED_WITH_ATTR:
449           if (this.mCurrentElement)
450             this.mCurrentElement.setAttribute(aElement.attribute, aValue);
451           else {
452             var elem = document.createElementNS(aElement.namespace.url,
453                                                 aElement.tagName);
454             if (aElement.tagName == "im") {
455               elem.setAttribute("label", "CUSTOM");
456               elem.setAttribute("protocol", com.gContactSync.gdata.contacts.rel + "#" + aType);
457             }
458             else if (elem.tagName == "website")
459               elem.setAttribute("rel", aType);
460             else
461               elem.setAttribute("rel", com.gContactSync.gdata.contacts.rel + "#" + aType);
462             elem.setAttribute(aElement.attribute, aValue);
463             this.xml.appendChild(elem);
464           }
465           break;
466         case com.gContactSync.gdata.contacts.types.UNTYPED:
467         case com.gContactSync.gdata.contacts.types.PARENT_TYPED:
468           if (aElement.tagName == "birthday") {
469             // make sure the value at least has two -s
470             // valid formats: YYYY-M-D and --M-D
471             if (aValue.split("-").length < 3) {
472               com.gContactSync.LOGGER.LOG_WARNING("Detected an invalid birthday: " + aValue);
473               return null;
474             }
475             var elem = this.mCurrentElement ? this.mCurrentElement:
476                                               document.createElementNS
477                                                        (aElement.namespace.url,
478                                                         aElement.tagName);
479             elem.setAttribute("when", aValue);
480             // add the element to the XML feed if it is new
481             if (elem != this.mCurrentElement)
482               this.xml.appendChild(elem);
483             return true;
484           }
485           if (this.mCurrentElement && this.mCurrentElement.childNodes[0])
486             this.mCurrentElement.childNodes[0].nodeValue = aValue;
487           else {
488             var elem = this.mCurrentElement ? this.mCurrentElement:
489                                               document.createElementNS
490                                                        (aElement.namespace.url,
491                                                         aElement.tagName);
492             var text = document.createTextNode(aValue);
493             elem.appendChild(text);
494             this.xml.appendChild(elem);
495           }
496           break;
497         default:
498           com.gContactSync.LOGGER.LOG_WARNING("Invalid aType parameter sent to the setElementValue"
499                              + "method" + com.gContactSync.StringBundle.getStr("pleaseReport"));
500           return null;
501       }
502     }
503     return true;
504   },
505   /**
506    * Gets the last modified date from an contacts's XML feed in milliseconds
507    * since 1970.
508    * @returns {int} The last modified date of the entry in milliseconds from 1970
509    */
510   getLastModifiedDate: function GContact_getLastModifiedDate() {
511     try {
512       if (com.gContactSync.Preferences.mSyncPrefs.writeOnly.value) {
513         return 1;
514       }
515       var sModified = this.xml.getElementsByTagName('updated')[0].childNodes[0].nodeValue,
516           year      = sModified.substring(0,4),
517           month     = sModified.substring(5,7),
518           day       = sModified.substring(8,10),
519           hrs       = sModified.substring(11,13),
520           mins      = sModified.substring(14,16),
521           sec       = sModified.substring(17,19),
522           ms        = sModified.substring(20,23);
523       return parseInt(Date.UTC(year, parseInt(month, 10) - 1, day, hrs, mins, sec, ms));
524     }
525     catch(e) {
526       com.gContactSync.LOGGER.LOG_WARNING("Unable to get last modified date from a contact:\n" + e);
527     }
528     return 0;
529   },
530   /**
531    * Removes all extended properties from this contact.
532    */
533   removeExtendedProperties: function GContact_removeExtendedProperties() {
534     var arr = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url, "extendedProperty");
535     for (var i = arr.length - 1; i > -1 ; i--) {
536       arr[i].parentNode.removeChild(arr[i]);
537     }
538   },
539   /**
540    * Returns the value of the extended property with a matching name attribute.
541    * @param aName {string} The name of the extended property to return.
542    * @returns {Property} A Property object with the value of the extended
543    *                    property with the name attribute aName.
544    */
545   getExtendedProperty: function GContact_getExtendedProperty(aName) {
546     var arr = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url, "extendedProperty");
547     for (var i = 0, length = arr.length; i < length; i++)
548       if (arr[i].getAttribute("name") == aName)
549         return new com.gContactSync.Property(arr[i].getAttribute("value"));
550     return null;
551   },
552   /**
553    * Sets an extended property with the given name and value if there are less
554    * than 10 existing.  Logs a warning if there are already 10 or more.
555    * @param aName  {string} The name of the property.
556    * @param aValue {string} The value of the property.
557    */
558   setExtendedProperty: function GContact_setExtendedProperty(aName, aValue) {
559     if (this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.GD.url,
560         "extendedProperty").length >= 10) {
561       com.gContactSync.LOGGER.LOG_WARNING("Attempt to add too many properties aborted");
562       return null;
563     }
564     if (aValue && aValue != "") {
565       var property = document.createElementNS(com.gContactSync.gdata.namespaces.GD.url,
566                                               "extendedProperty");
567       property.setAttribute("name", aName);
568       property.setAttribute("value", aValue);
569       this.xml.appendChild(property);
570       return true;
571     }
572     return null;
573   },
574   /**
575    * Returns the value of the XML Element with the supplied tag name at the
576    * given index of the given type (home, work, other, etc.)
577    * @param aName  {string} The tag name of the value to get.  See gdata for
578                             valid tag names.
579    * @param aIndex {int} Optional.  The index, if non-zero, of the value to get.
580    * @param aType  {string} The type of element to get if the tag name has
581    *                        different types (home, work, other, etc.).
582    * @returns {Property} A new Property object with the value and type, if
583    *                    applicable.
584    *                    If aName is groupMembership info, returns an array of
585    *                    the group IDs
586    */
587   getValue: function GContact_getValue(aName, aIndex, aType) {
588     // TODO uncomment
589     //try {
590       // if the value to obtain is a link, get the value for the link
591       if (com.gContactSync.gdata.contacts.links[aName]) {
592         var arr = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.ATOM.url, "link");
593         for (var i = 0, length = arr.length; i < length; i++)
594           if (arr[i].getAttribute("rel") == com.gContactSync.gdata.contacts.links[aName])
595             return new com.gContactSync.Property(arr[i].getAttribute("href"));
596       }
597       else if (aName == "groupMembershipInfo")
598         return this.getGroups();
599       // otherwise, if it is a normal attribute, get it's value
600       else if (com.gContactSync.gdata.contacts[aName])
601         return this.getElementValue(com.gContactSync.gdata.contacts[aName], aIndex, aType);
602       // if the name of the value to get is something else, throw an error
603       else
604         com.gContactSync.LOGGER.LOG_WARNING("Unable to getValue for " + aName);
605     //}
606     //catch(e) {
607     //  com.gContactSync.LOGGER.LOG_WARNING("Error in GContact.getValue:\n" + e);
608     //}
609     return null;
610   },
611   /**
612    * Sets the value with the name aName to the value aValue based on the type
613    * and index.
614    * @param aName  {string} The tag name of the value to set.
615    * @param aIndex {int}    The index of the element whose value is set.
616    * @param aType  {string} The type of the element (home, work, other, etc.).
617    * @param aValue {string} The value to set.  null if the element should be
618    *                        removed.
619    */
620   setValue: function GContact_setValue(aName, aIndex, aType, aValue) {
621     try {
622       if (aValue == "")
623         aValue = null;
624       if (aType == "Home" || aType == "Work" || aType == "Other") {
625         com.gContactSync.LOGGER.LOG_WARNING("Found and fixed an invalid type: " + aType);
626         aType = aType.toLowerCase();
627       }
628       com.gContactSync.LOGGER.VERBOSE_LOG("   - " + aName + " - " + aIndex + " - " + aType + " - " + aValue);
629       if (com.gContactSync.gdata.contacts[aName] && aName != "groupMembershipInfo")
630         return this.setElementValue(com.gContactSync.gdata.contacts[aName],
631                                     aIndex, aType, aValue);
632       // if the name of the value to get is something else, throw an error
633       else
634         com.gContactSync.LOGGER.LOG_WARNING("Unable to setValue for " + aName + " - " + aValue);
635     }
636     catch(e) {
637       com.gContactSync.LOGGER.LOG_WARNING("Error in GContact.setValue:\n" + e);
638     }
639     return null;
640   },
641   /**
642    * Returns an array of the names of the groups to which this contact belongs.
643    */
644   getGroups: function GContact_getGroups() {
645     var groupInfo = com.gContactSync.gdata.contacts.groupMembershipInfo;
646     var arr = this.xml.getElementsByTagNameNS(groupInfo.namespace.url,
647                                               groupInfo.tagName);
648     var groups = {};
649     // iterate through each group and add the group as a new property of the
650     // groups object with the ID as the name of the property.
651     for (var i = 0, length = arr.length; i < length; i++) {
652       var id    = com.gContactSync.fixURL(arr[i].getAttribute("href")),
653           group = com.gContactSync.Sync.mGroups[id];
654       if (group)
655         groups[id] = group;
656       else {
657         if (com.gContactSync.Preferences.mSyncPrefs.myContacts)
658           groups[id] = true;
659         else
660           com.gContactSync.LOGGER.LOG_WARNING("Unable to find group: " + id);
661       }
662     }
663     // return the object with the groups this contact belongs to
664     return groups;
665   },
666   /**
667    * Removes all groups from this contact.
668    */
669   clearGroups: function GContact_clearGroups() {
670     var groupInfo = com.gContactSync.gdata.contacts.groupMembershipInfo;
671     var arr = this.xml.getElementsByTagNameNS(groupInfo.namespace.url,
672                                               groupInfo.tagName);
673     // iterate through every group element and remove it from the XML
674     for (var i = 0; i < arr.length; i++) {
675       try {
676         if (arr[i]) {
677           arr[i].parentNode.removeChild(arr[i]);
678         }
679       }
680       catch(e) {
681         com.gContactSync.LOGGER.LOG_WARNING("Error while trying to clear group: " + arr[i], e);
682       }
683     }
684     this.mGroups = {};
685   },
686   /**
687    * Sets the groups of that this contact is in based on the array of IDs.
688    * @param aGroups {array} An array of the IDs of the groups to which the
689    *                        contact should belong.
690    */
691   setGroups: function GContact_setGroups(aGroups) {
692     this.clearGroups(); // clear existing groups
693     if (!aGroups)
694       return null;
695     // make sure the group 
696     for (var i = 0, length = aGroups.length; i < length; i++) {
697       var id = aGroups[i];
698       // if the ID isn't valid log a warning and go to the next ID
699       if (!id || !id.indexOf || id.indexOf("www.google.com/m8/feeds/groups") == -1) {
700         com.gContactSync.LOGGER.LOG_WARNING("Invalid id in aGroups: " + id);
701         continue;
702       }
703       this.addToGroup(id);
704     }
705     return true;
706   },
707   /**
708    * Removes the contact from the given group element.
709    * @param aGroup {Group} The group from which the contact should be removed.
710    */
711   removeFromGroup: function GContact_removeFromGroup(aGroup) {
712     if (!aGroup) {
713       com.gContactSync.LOGGER.LOG_WARNING("Attempt to remove a contact from a non-existant group");
714       return null;
715     }
716     try {
717       aGroup.parentNode.removeChild(aGroup);
718       return true;
719     }
720     catch (e) {
721       com.gContactSync.LOGGER.LOG_WARNING("Error while trying to remove a contact from a group: " + e);
722     }
723     return null;
724   },
725   /**
726    * Adds the contact to the given, existing, group.
727    * @param aGroupURL {string} The URL of an existing group to which the contact
728    *                           will be added.
729    */
730   addToGroup: function GContact_addToGroup(aGroupURL) {
731     if (!aGroupURL) {
732       com.gContactSync.LOGGER.LOG_WARNING("Attempt to add a contact to a non-existant group");
733       return null;
734     }
735     try {
736       var ns = com.gContactSync.gdata.namespaces.GCONTACT;
737       var group = document.createElementNS(ns.url,
738                                            ns.prefix + "groupMembershipInfo");
739       group.setAttribute("deleted", false);
740       group.setAttribute("href", aGroupURL);
741       this.xml.appendChild(group);
742       return true;
743     }
744     catch(e) {
745       com.gContactSync.LOGGER.LOG_WARNING("Error while trying to add a contact to a group: " + e);
746     }
747     return null;
748   },
749   /**
750    * Returns true if the given XML Element is a match for the GElement object
751    * and the type (ie home, work, other, etc.)
752    * @param aElement {GElement}    The GElement object (@see GElement.js)
753    * @param aXmlElem {XML Element} The XML Element to check
754    * @param aType    {string}      The type (home, work, other, etc.)
755    */
756   isMatch: function GContact_isMatch(aElement, aXmlElem, aType, aDontSkip) {
757     if (aElement.contactType === com.gContactSync.gdata.contacts.types.UNTYPED)
758       return true;
759     // if the parent contains the type then get the XML element's parent
760     else if (aElement.contactType === com.gContactSync.gdata.contacts.types.PARENT_TYPED)
761       aXmlElem = aXmlElem.parentNode;
762     // If this is a phone number, check the phoneTypes pref
763     // If the pref is true, then always say that this is a match
764     // If the pref is false, continue with the normal type check
765     if (aElement.tagName === "phoneNumber" &&
766         com.gContactSync.Preferences.mSyncPrefs.phoneTypes.value) {
767       return true;
768     }
769     switch (aElement.tagName) {
770       case "email":
771       case "website": // TODO - should this be typed?
772       case "relation":
773         if (!aDontSkip) // always return true for e-mail by default
774           return true;
775       case "im":
776         if (!aDontSkip) // always return true for e-mail by default
777           return true;
778         var str = aXmlElem.getAttribute("protocol");
779         break;
780       default:
781         var str = aXmlElem.getAttribute("rel");
782     }
783     if (!str)
784       return false;
785     // get only the very end
786     var str = str.substring(str.length - aType.length);
787     return str == aType; // return true if the end is equal to aType
788   },
789   /**
790    * Returns the last portion of this contact's ID, or optionally, the full ID.
791    * @param {boolean} aFull Set this to true to return the complete ID for this
792    *                        contact (the entire URL).
793    *                        Otherwise just the portion after the last / is
794    *                        returned.
795    * @returns {string} The ID of this contact.
796    */
797   getID: function GContact_getID(aFull) {
798     var val   = this.getValue("id").value;
799     if (aFull) {
800       return com.gContactSync.fixURL(val); // make sure to change http to https
801     }
802     var index = val.lastIndexOf("/");
803     return val.substr(index + 1);
804   },
805   /**
806    * Sets the photo for this contact.  Note that this may not immediately take
807    * effect as contacts must be added to Google and then retrieved before a
808    * photo can be added.
809    *
810    * @param aURI {string|nsIURI} A string with the URI of a contact photo.
811    */
812   setPhoto: function GContact_setPhoto(aURI) {
813     com.gContactSync.LOGGER.VERBOSE_LOG("Entering GContact.setPhoto:");
814     var photoInfo = this.getPhotoInfo();
815     // If the URI is empty or a chrome URL remove the photo, if present
816     // TODO - this should probably just check if it is the default photo
817     if (!aURI) {
818       // Easy case: URI is empty and this contact doesn't have a photo
819       if (!photoInfo || !(photoInfo.etag)) {
820         com.gContactSync.LOGGER.VERBOSE_LOG(" * URI is empty, contact has no photo");
821         return;
822       }
823       com.gContactSync.LOGGER.VERBOSE_LOG(" * URI is empty, photo will be removed");
824       // Remove the photo
825       var httpReq = new com.gContactSync.GHttpRequest("delete",
826                                                       com.gContactSync.Sync.mCurrentAuthToken,
827                                                       photoInfo.url,
828                                                       null,
829                                                       com.gContactSync.Sync.mCurrentUsername);
830       httpReq.mOnSuccess = function setPhotoSuccess() {
831         com.gContactSync.LOGGER.VERBOSE_LOG(" * Photo successfully removed");
832       };
833       httpReq.mOnError   = function setPhotoError(httpReq) {
834         com.gContactSync.LOGGER.LOG_ERROR('Error while removing photo',
835                                           httpReq.responseText);
836       };
837       httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
838       httpReq.addHeaderItem("If-Match", "*");
839       httpReq.send();
840       return;
841     }
842     // The URI exists, so update or add the photo
843     // NOTE: A photo cannot be added until the contact has been added
844     else {
845       // If this is a new contact then nothing can be done until the contact is
846       // added to Google
847       if (this.mIsNew || !photoInfo) {
848         com.gContactSync.LOGGER.VERBOSE_LOG(" * Photo will be added after the contact is created");
849         this.mNewPhotoURI = aURI;
850         return;
851       }
852       com.gContactSync.LOGGER.VERBOSE_LOG(" * Photo will be updated");
853       // Otherwise send the PUT request
854       // TODO - this really needs error handling...
855       var ios = Components.classes["@mozilla.org/network/io-service;1"]
856                           .getService(Components.interfaces.nsIIOService),
857           outChannel = ios.newChannel(photoInfo.url, null, null),
858           inChannel  = aURI instanceof Components.interfaces.nsIURI ?
859                          ios.newChannelFromURI(aURI) :
860                          ios.newChannel(aURI, null, null);
861       outChannel = outChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
862       // Set the upload data
863       outChannel = outChannel.QueryInterface(Components.interfaces.nsIUploadChannel);
864       // Set the input stream as the photo URI
865       // See https://www.mozdev.org/bugs/show_bug.cgi?id=22757 for the try/catch
866       // block, I didn't see a way to tell if the item pointed to by aURI exists
867       try {
868         outChannel.setUploadStream(inChannel.open(), photoInfo.type, -1);
869       }
870       catch (e) {
871         com.gContactSync.LOGGER.LOG_WARNING("The photo at '" + aURI + "' doesn't exist", e);
872         return;
873       }
874       // set the request type to PUT (this has to be after setting the upload data)
875       outChannel = outChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
876       outChannel.requestMethod = "PUT";
877       // Setup the header: Authorization and Content-Type: image/*
878       outChannel.setRequestHeader("Authorization", com.gContactSync.Sync.mCurrentAuthToken, false);
879       outChannel.setRequestHeader("Content-Type",  photoInfo.type, false);
880       outChannel.setRequestHeader("If-Match",      "*", false);
881       // set the status bar text since this can take a minute
882       com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr("uploadingPhoto"));
883       outChannel.open();
884       try {
885         com.gContactSync.LOGGER.VERBOSE_LOG(" * Update status: " + outChannel.responseStatus);
886       }
887       catch (e) {
888         com.gContactSync.LOGGER.LOG_WARNING(" * outChannel.responseStatus failed", e);
889       }
890     }
891   },
892   /**
893    * Returns an object with information about this contact's photo.
894    * @returns An object containing the following properties:
895    *  - url - The URL of the contact's photo
896    *  - type - The type of photo (ex "image/*")
897    *  - etag - The etag of the photo (if a photo exists).
898    * If there was no photo found (no etag) the etag is blank.
899    * If this contact is new then this function returns null.
900    */
901   getPhotoInfo: function GContact_hasPhoto(a) {
902     // Sample photo XML:
903     // <link rel='http://schemas.google.com/contacts/2008/rel#photo' type='image/*'
904     //  href='http://google.com/m8/feeds/photos/media/liz%40gmail.com/c9012de'
905     // gd:etag='"KTlcZWs1bCp7ImBBPV43VUV4LXEZCXERZAc."'/>
906     var arr = this.xml.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.ATOM.url, "link");
907     var elem, etag;
908     for (var i = 0, length = arr.length; i < length; i++) {
909       elem = arr[i];
910       if (elem.getAttribute("rel") == com.gContactSync.gdata.contacts.links["PhotoURL"]) {
911         return {
912           url:  elem.getAttribute("href"),
913           type: elem.getAttribute("type"),
914           etag: elem.getAttributeNS(com.gContactSync.gdata.namespaces.GD.url, "etag")
915         }
916       }
917     }
918     return null;
919   },
920   /**
921    * Fetches and saves a local copy of this contact's photo, if present.
922    * NOTE: Portions of this code are from Thunderbird written by me (Josh Geenen)
923    * See https://bugzilla.mozilla.org/show_bug.cgi?id=119459
924    *
925    * TODO - merge w/ com.gContactSync.writePhoto
926    * @param aAuthToken {string} The authentication token for the account to
927    *                            which this contact belongs.
928    */
929   writePhoto: function GContact_writePhoto(aAuthToken) {
930     com.gContactSync.LOGGER.VERBOSE_LOG(" * Checking for a contact photo");
931     if (!aAuthToken) {
932       com.gContactSync.LOGGER.LOG_WARNING("No auth token passed to GContact.writePhoto");
933       return null;
934     }
935     var info = this.getPhotoInfo();
936     if (!info) {
937       com.gContactSync.LOGGER.VERBOSE_LOG(" * This contact does not have a photo");
938       return null;
939     }
940     // Get the profile directory
941     var file = Components.classes["@mozilla.org/file/directory_service;1"]
942                          .getService(Components.interfaces.nsIProperties)
943                          .get("ProfD", Components.interfaces.nsIFile);
944     // Get (or make) the Photos directory
945     file.append("Photos");
946     if (!file.exists() || !file.isDirectory())
947       file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0777);
948     var ios = Components.classes["@mozilla.org/network/io-service;1"]
949                         .getService(Components.interfaces.nsIIOService);
950     var ch = ios.newChannel(info.url, null, null);
951     ch.QueryInterface(Components.interfaces.nsIHttpChannel);
952     ch.setRequestHeader("Authorization", aAuthToken, false);
953     var istream = ch.open();
954     // quit if the request failed
955     if (!ch.requestSucceeded) {
956       com.gContactSync.LOGGER.LOG_WARNING("The request to retrive the photo returned with a status ",
957                          ch.responseStatus);
958       return null;
959     }
960 
961     // Create a name for the photo with the contact's ID and the photo extension
962     var filename = this.getID(false);
963     try {
964       var ext = com.gContactSync.findPhotoExt(ch);
965       filename = filename + (ext ? "." + ext : "");
966     }
967     catch (e) {
968       com.gContactSync.LOGGER.LOG_WARNING("Couldn't find an extension for the photo");
969     }
970     file.append(filename);
971     com.gContactSync.LOGGER.VERBOSE_LOG(" * Writing the photo to " + file.path);
972 
973     var output = Components.classes["@mozilla.org/network/file-output-stream;1"]
974                            .createInstance(Components.interfaces.nsIFileOutputStream);
975 
976     // Now write that input stream to the file
977     var fstream = Components.classes["@mozilla.org/network/safe-file-output-stream;1"]
978                             .createInstance(Components.interfaces.nsIFileOutputStream);
979     var buffer = Components.classes["@mozilla.org/network/buffered-output-stream;1"]
980                            .createInstance(Components.interfaces.nsIBufferedOutputStream);
981     fstream.init(file, 0x04 | 0x08 | 0x20, 0600, 0); // write, create, truncate
982     buffer.init(fstream, 8192);
983     while (istream.available() > 0) {
984       buffer.writeFrom(istream, istream.available());
985     }
986 
987     // Close the output streams
988     if (buffer instanceof Components.interfaces.nsISafeOutputStream)
989         buffer.finish();
990     else
991         buffer.close();
992     if (fstream instanceof Components.interfaces.nsISafeOutputStream)
993         fstream.finish();
994     else
995         fstream.close();
996     // Close the input stream
997     istream.close();
998     return file;
999   }
1000 };
1001