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