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