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-2011 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) { 38 /** A generic wrapper variable */ 39 var com = {}; 40 } 41 42 if (!com.gContactSync) { 43 /** A wrapper for all GCS functions and variables */ 44 com.gContactSync = {}; 45 } 46 47 /** The attribute where the dummy e-mail address is stored */ 48 com.gContactSync.dummyEmailName = "PrimaryEmail"; 49 /** The major version of gContactSync (ie 0 in 0.2.18) */ 50 com.gContactSync.versionMajor = "0"; 51 /** The minor version of gContactSync (ie 3 in 0.3.0b1) */ 52 com.gContactSync.versionMinor = "3"; 53 /** The release for the current version of gContactSync (ie 1 in 0.3.1a7) */ 54 com.gContactSync.versionRelease = "3"; 55 /** The suffix for the current version of gContactSync (ie a7 for Alpha 7) */ 56 com.gContactSync.versionSuffix = "pre"; 57 58 /** 59 * Returns a string of the current version for logging. This can print either 60 * the current version (aGetLast == false) or the previous version 61 * (aGetLast == true). 62 * The format is: <major>.<minor>.<release><suffix> 63 * Don't use this to compare versions. 64 * 65 * @param aGetLast {boolean} Set this to true if you want to get the version 66 * string for the last version of gContactSync. 67 * @returns {string} A string of the current or previous version of 68 * gContactSync in the following form: 69 * <major>.<minor>.<release><suffix> 70 */ 71 com.gContactSync.getVersionString = function gCS_getVersionString(aGetLast) { 72 var major, minor, release, suffix; 73 if (aGetLast) { 74 var prefs = com.gContactSync.Preferences; 75 major = prefs.mSyncPrefs.lastVersionMajor.value; 76 minor = prefs.mSyncPrefs.lastVersionMinor.value; 77 release = prefs.mSyncPrefs.lastVersionRelease.value; 78 suffix = prefs.mSyncPrefs.lastVersionSuffix.value; 79 } 80 else { 81 major = com.gContactSync.versionMajor; 82 minor = com.gContactSync.versionMinor; 83 release = com.gContactSync.versionRelease; 84 suffix = com.gContactSync.versionSuffix; 85 } 86 return major + 87 "." + minor + 88 "." + release + 89 suffix; 90 } 91 92 /** 93 * Creates an XMLSerializer to serialize the given XML then create a more 94 * human-friendly string representation of that XML. 95 * This is an expensive method of serializing XML but results in the most 96 * human-friendly string from XML. 97 * 98 * Also see serializeFromText. 99 * 100 * @param aXML {XML} The XML to serialize into a human-friendly string. 101 * @returns {string} A formatted string of the given XML. 102 */ 103 com.gContactSync.serialize = function gCS_serialize(aXML) { 104 if (!aXML) 105 return ""; 106 try { 107 var serializer = new XMLSerializer(), 108 str = serializer.serializeToString(aXML); 109 // source: http://developer.mozilla.org/en/E4X#Known_bugs_and_limitations 110 str = str.replace(/^<\?xml\s+version\s*=\s*(["'])[^\1]+\1[^?]*\?>/, ""); // bug 336551 111 return XML(str).toXMLString(); 112 } 113 catch (e) { 114 com.gContactSync.LOGGER.LOG_WARNING("Error while serializing the following XML: " + 115 aXML, e); 116 } 117 return ""; 118 }; 119 120 /** 121 * A less expensive (but still costly) function that serializes a string of XML 122 * adding newlines between adjacent tags (...><...). 123 * If the verboseLog preference is set as false then this function does nothing. 124 * 125 * @param aString {string} The XML string to serialize. 126 * @param aForce {boolean} Set to true to force a serialization regardless of 127 * verboseLog. 128 * @returns {string} The serialized text if verboseLog is true; else the original 129 * text. 130 */ 131 com.gContactSync.serializeFromText = function gCS_serializeFromText(aString, aForce) { 132 // if verbose logging is disabled, don't replace >< with >\n< because it only 133 // wastes time 134 if (aForce || com.gContactSync.Preferences.mSyncPrefs.verboseLog.value) { 135 var arr = aString.split("><"); 136 aString = arr.join(">\n<"); 137 } 138 return aString; 139 }; 140 141 /** 142 * Creates a 'dummy' e-mail for the given contact if possible. 143 * The dummy e-mail contains 'nobody' (localized) and '@nowhere.invalid' (not 144 * localized) as well as a string of numbers. The numbers are the ID from 145 * Google, if any, or a random sequence. The numbers are fairly unique because 146 * mailing lists require contacts with distinct e-mail addresses otherwise they 147 * fail silently. 148 * 149 * The purpose of the dummy e-mail addresses is to prevent mailing list bugs 150 * relating to contacts without e-mail addresses. 151 * 152 * This function checks the 'dummyEmail' pref and if that pref is set as true 153 * then this function will not set the e-mail unless the ignorePref parameter is 154 * supplied and evaluates to true. 155 * 156 * @param aContact A contact from Thunderbird. It can be one of the following: 157 * TBContact, GContact, or an nsIAbCard (Thunderbird 2 or 3) 158 * @param ignorePref {boolean} Set this as true to ignore the preference 159 * disabling dummy e-mail addresses. Use this in 160 * situations where not adding an address would 161 * definitely cause problems. 162 * @returns {string} A dummy e-mail address. 163 */ 164 com.gContactSync.makeDummyEmail = function gCS_makeDummyEmail(aContact, ignorePref) { 165 if (!aContact) throw "Invalid contact sent to makeDummyEmail"; 166 if (!ignorePref && !com.gContactSync.Preferences.mSyncPrefs.dummyEmail.value) { 167 com.gContactSync.LOGGER.VERBOSE_LOG(" * Not setting dummy e-mail"); 168 return ""; 169 } 170 var prefix = com.gContactSync.StringBundle.getStr("dummy1"), 171 suffix = "@nowhere.invalid", // Note - this is hard-coded so locales can 172 // be switched without gContactSync failing 173 // to recognize a dummy e-mail address 174 id = null; 175 // GContact and TBContact may not be defined 176 try { 177 if (aContact instanceof com.gContactSync.GContact) 178 id = aContact.getID(true); 179 // otherwise it is from Thunderbird, so try to get the Google ID, if any 180 else if (aContact instanceof com.gContactSync.TBContact) 181 id = aContact.getID(); 182 else 183 id = com.gContactSync.GAbManager.getCardValue(aContact, "GoogleID"); 184 } catch (e) { 185 try { 186 // try getting the card's value 187 if (aContact.getProperty) // post Bug 413260 188 id = aContact.getProperty("GoogleID", null); 189 else // pre Bug 413260 190 id = aContact.getStringAttribute("GoogleID"); 191 } 192 catch (ex) {} 193 } 194 if (id) { 195 // take just the ID and not the whole URL 196 return prefix + id.substr(1 + id.lastIndexOf("/")) + suffix; 197 } 198 // if there is no ID make a random number and remove the "0." 199 else { 200 return prefix + String(Math.random()).replace("0.", "") + suffix; 201 } 202 }; 203 204 /** 205 * Returns true if the given e-mail address is a fake 'dummy' address. 206 * 207 * @param aEmail {string} The e-mail address to check. 208 * @returns {boolean} true if aEmail is a dummy e-mail address 209 * false otherwise 210 */ 211 com.gContactSync.isDummyEmail = function gCS_isDummyEmail(aEmail) { 212 return aEmail && aEmail.indexOf && 213 (aEmail.indexOf("@nowhere.invalid") !== -1 || 214 // This is here for when the sv-SE locale had a translated string 215 // for the dummy e-mail suffix in 0.3.0 so gContactSync recognizes any 216 // dummy e-mail addresses created in that version and locale. 217 aEmail.indexOf("@ingenstans.ogiltig") !== -1); 218 }; 219 220 /** 221 * Selects the menuitem with the given value (value or label attribute) in the 222 * given menulist. 223 * Optionally creates the menuitem if it cannot be found. 224 * 225 * @param aMenuList {menulist} The menu list element to search. 226 * @param aValue {string} The value to find in a menuitem. This can be 227 * either the 'value' or 'label' attribute of the 228 * matched item. Case insensitive. 229 * @param aCreate {boolean} Set as true to create and select a new menuitem 230 * if a match cannot be found. 231 */ 232 com.gContactSync.selectMenuItem = function gCS_selectMenuItem(aMenuList, aValue, aCreate) { 233 if (!aMenuList || !aMenuList.menupopup || !aValue) 234 throw "Invalid parameter sent to selectMenuItem"; 235 236 var arr = aMenuList.menupopup.childNodes, 237 i, 238 item, 239 aValueLC = aValue.toLowerCase(); 240 for (i = 0; i < arr.length; i++) { 241 item = arr[i]; 242 if (item.getAttribute("value").toLowerCase() === aValueLC || 243 item.getAttribute("label").toLowerCase() === aValueLC) { 244 aMenuList.selectedIndex = i; 245 return true; 246 } 247 } 248 if (!aCreate) 249 return false; 250 item = aMenuList.appendItem(aValue, aValue); 251 // getIndexOfItem was added in TB/FF 3 252 aMenuList.selectedIndex = aMenuList.menupopup.childNodes.length - 1; 253 return true; 254 }; 255 256 /** 257 * Attempts a few basic fixes for 'broken' usernames. 258 * In the past, gContactSync didn't check that a username included the domain 259 * which would pass authentication and then fail to do anything else. 260 * It also didn't make sure there were no spaces in a username which would 261 * also pass authentication and break for everything else. 262 * See Bug 21567 263 * 264 * @param aUsername {string} The username to fix. 265 * 266 * @returns {string} A username with a domain and no spaces. 267 */ 268 com.gContactSync.fixUsername = function gCS_fixUsername(aUsername) { 269 if (!aUsername) 270 return null; 271 // Add @gmail.com if necessary 272 if (aUsername.indexOf("@") === -1) 273 aUsername += "@gmail.com"; 274 // replace any spaces or tabs 275 aUsername = aUsername.replace(/[ \t\n\r]/g, ""); 276 return aUsername; 277 }; 278 279 /** 280 * Displays an alert dialog with the given text and an optional title. 281 * 282 * @param aText {string} The message to display. 283 * @param aTitle {string} The title for the message (optional - default is 284 * "gContactSync Notification"). 285 * @param aParent {nsIDOMWindow} The parent window (also optional). 286 */ 287 com.gContactSync.alert = function gCS_alert(aText, aTitle, aParent) { 288 if (!aTitle) { 289 aTitle = com.gContactSync.StringBundle.getStr("alertTitle"); 290 } 291 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 292 .getService(Components.interfaces.nsIPromptService); 293 promptService.alert(aParent, aTitle, aText); 294 }; 295 296 /** 297 * Displays an alert dialog titled "gContactSync Error" (in English). 298 * 299 * @param aText {string} The message to display. 300 */ 301 com.gContactSync.alertError = function gCS_alertError(aText) { 302 var title = com.gContactSync.StringBundle.getStr("alertError"); 303 com.gContactSync.alert(aText, title, window); 304 }; 305 306 /** 307 * Displays an alert dialog titled "gContactSync Warning" (in English). 308 * 309 * @param aText {string} The message to display. 310 */ 311 com.gContactSync.alertWarning = function gCS_alertWarning(aText) { 312 var title = com.gContactSync.StringBundle.getStr("alertWarning"); 313 com.gContactSync.alert(aText, title, window); 314 }; 315 316 /** 317 * Displays a confirmation dialog with the given text and an optional title. 318 * 319 * @param aText {string} The message to display. 320 * @param aTitle {string} The title for the message (optional - default is 321 * "gContactSync Confirmation"). 322 * @param aParent {nsIDOMWindow} The parent window (also optional). 323 */ 324 com.gContactSync.confirm = function gCS_confirm(aText, aTitle, aParent) { 325 if (!aTitle) { 326 aTitle = com.gContactSync.StringBundle.getStr("confirmTitle"); 327 } 328 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 329 .getService(Components.interfaces.nsIPromptService); 330 return promptService.confirm(aParent, aTitle, aText); 331 }; 332 333 /** 334 * Displays a prompt with the given text and an optional title. 335 * 336 * @param aText {string} The message to display. 337 * @param aTitle {string} The title for the message (optional - default is 338 * "gContactSync Prompt"). 339 * @param aParent {nsIDOMWindow} The parent window (also optional). 340 * @param aDefault {string} The default value for the textbox. 341 */ 342 com.gContactSync.prompt = function gCS_prompt(aText, aTitle, aParent, aDefault) { 343 if (!aTitle) { 344 aTitle = com.gContactSync.StringBundle.getStr("promptTitle"); 345 } 346 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 347 .getService(Components.interfaces.nsIPromptService), 348 input = { value: aDefault }, 349 response = promptService.prompt(aParent, aTitle, aText, input, null, {}); 350 return response ? input.value : false; 351 }; 352 353 /** 354 * Opens the Accounts dialog for gContactSync 355 */ 356 com.gContactSync.openAccounts = function gCS_openAccounts() { 357 window.open("chrome://gcontactsync/content/Accounts.xul", 358 "gContactSync_Accts", 359 "chrome=yes,resizable=yes,toolbar=yes,centerscreen=yes"); 360 }; 361 362 /** 363 * Opens the Preferences dialog for gContactSync 364 */ 365 com.gContactSync.openPreferences = function gCS_openPreferences() { 366 window.open("chrome://gcontactsync/content/options.xul", 367 "gContactSync_Prefs", 368 "chrome=yes,resizable=yes,toolbar=yes,centerscreen=yes"); 369 }; 370 371 /** 372 * Opens the given URL using the openFormattedURL and 373 * openFormattedRegionURL functions. 374 * 375 * @param aURL {string} THe URL to open. 376 */ 377 com.gContactSync.openURL = function gCS_openURL(aURL) { 378 com.gContactSync.LOGGER.VERBOSE_LOG("Opening the following URL: " + aURL); 379 if (!aURL) { 380 com.gContactSync.LOGGER.LOG_WARNING("Caught an attempt to load a blank URL"); 381 return; 382 } 383 try { 384 if (openFormattedURL) { 385 openFormattedURL(aURL); 386 return; 387 } 388 } 389 catch (e) { 390 com.gContactSync.LOGGER.LOG_WARNING(" - Error in openFormattedURL", e); 391 } 392 try { 393 if (openFormattedRegionURL) { 394 openFormattedRegionURL(aURL); 395 return; 396 } 397 } 398 catch (e) { 399 com.gContactSync.LOGGER.LOG_WARNING(" - Error in openFormattedRegionURL", e); 400 } 401 try { 402 if (openTopWin) { 403 var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] 404 .getService(Components.interfaces.nsIURLFormatter) 405 .formatURLPref(aURL); 406 openTopWin(url); 407 return; 408 } 409 } 410 catch (e) { 411 com.gContactSync.LOGGER.LOG_WARNING(" - Error in openTopWin", e); 412 } 413 // If all else fails try doing it manually 414 try { 415 var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] 416 .getService(Components.interfaces.nsIURLFormatter) 417 .formatURLPref(aURL); 418 var uri = Components.classes["@mozilla.org/network/io-service;1"] 419 .getService(Components.interfaces.nsIIOService) 420 .newURI(url, null, null); 421 422 Components.classes["@mozilla.org/uriloader/external-protocol-service;1"] 423 .getService(Components.interfaces.nsIExternalProtocolService) 424 .loadURI(uri); 425 return; 426 } 427 catch (e) { 428 com.gContactSync.LOGGER.LOG_WARNING(" - Error opening the URL", e); 429 } 430 com.gContactSync.LOGGER.LOG_WARNING("Could not open the URL: " + aURL); 431 return; 432 }; 433 434 /** 435 * Opens the "view source" window with the log file. 436 */ 437 com.gContactSync.showLog = function gCS_showLog() { 438 try { 439 window.open("view-source:file://" + com.gContactSync.FileIO.mLogFile.path, 440 "gContactSyncLog", 441 "chrome=yes,resizable=yes,height=480,width=600"); 442 } 443 catch(e) { 444 com.gContactSync.LOGGER.LOG_WARNING("Unable to open the log", e); 445 } 446 }; 447 448 /** 449 * Replaces https://... with http://... in URLs as a permanent workaround for 450 * the issue described here: 451 * http://www.google.com/support/forum/p/apps-apis/thread?tid=6fde249ce2ffe7a9&hl=en 452 * 453 * @param aURL {string} The URL to fix. 454 * @return {string} The URL using https instead of http 455 */ 456 com.gContactSync.fixURL = function gCS_fixURL(aURL) { 457 if (!aURL) { 458 return aURL; 459 } 460 return aURL.replace(/^https:/i, "http:"); 461 }; 462 463 /** 464 * Fetches and saves a local copy of this contact's photo, if present. 465 * NOTE: Portions of this code are from Thunderbird written by me (Josh Geenen) 466 * See https://bugzilla.mozilla.org/show_bug.cgi?id=119459 467 * @param aURL {string} The URL of the photo to download 468 * @param aFilename {string} The name of the file to which the photo will be 469 * written. The extenion of the photo will be 470 * appended to this name, and the photo will be in the 471 * TB profile folder under the "Photos" directory. 472 * @param aRedirect {string} The number of times the request was redirected. 473 * If > 5 then the download attempt will be aborted. 474 */ 475 com.gContactSync.writePhoto = function gCS_writePhoto(aURL, aFilename, aRedirect) { 476 if (!aURL) { 477 com.gContactSync.LOGGER.LOG_WARNING("No aURL passed to writePhoto"); 478 return null; 479 } 480 if (aRedirect > 5) { 481 com.gContactSync.LOGGER.LOG_WARNING("Caught > 5 redirection attempts, aborting photo download"); 482 return null; 483 } 484 485 // Get the profile directory 486 var file = Components.classes["@mozilla.org/file/directory_service;1"] 487 .getService(Components.interfaces.nsIProperties) 488 .get("ProfD", Components.interfaces.nsIFile); 489 // Get (or make) the Photos directory 490 file.append("Photos"); 491 if (!file.exists() || !file.isDirectory()) 492 file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0777); 493 var ios = Components.classes["@mozilla.org/network/io-service;1"] 494 .getService(Components.interfaces.nsIIOService); 495 var ch = ios.newChannel(aURL, null, null); 496 ch.QueryInterface(Components.interfaces.nsIHttpChannel); 497 //ch.setRequestHeader("Authorization", aAuthToken, false); 498 var istream = ch.open(); 499 // Quit if the request failed 500 if (!ch.requestSucceeded) { 501 // At least Facebook returns a 302 with a new Location for the photo. 502 if (ch.responseStatus == 302) { 503 var newURL = ch.getResponseHeader("Location"); 504 com.gContactSync.LOGGER.VERBOSE_LOG("Received a 302, Location: " + newURL); 505 return com.gContactSync.writePhoto(newURL, aFilename, aRedirect + 1); 506 } 507 com.gContactSync.LOGGER.LOG_WARNING("The request to retrive the photo returned with a status ", 508 ch.responseStatus); 509 return null; 510 } 511 512 // Create a name for the photo with the contact's ID and the photo extension 513 try { 514 var ext = com.gContactSync.findPhotoExt(ch); 515 aFilename += (ext ? "." + ext : ""); 516 } 517 catch (e) { 518 com.gContactSync.LOGGER.LOG_WARNING("Couldn't find an extension for the photo"); 519 } 520 file.append(aFilename); 521 com.gContactSync.LOGGER.VERBOSE_LOG(" * Writing the photo to " + file.path); 522 523 var output = Components.classes["@mozilla.org/network/file-output-stream;1"] 524 .createInstance(Components.interfaces.nsIFileOutputStream); 525 526 // Now write that input stream to the file 527 var fstream = Components.classes["@mozilla.org/network/safe-file-output-stream;1"] 528 .createInstance(Components.interfaces.nsIFileOutputStream); 529 var buffer = Components.classes["@mozilla.org/network/buffered-output-stream;1"] 530 .createInstance(Components.interfaces.nsIBufferedOutputStream); 531 fstream.init(file, 0x04 | 0x08 | 0x20, 0600, 0); // write, create, truncate 532 buffer.init(fstream, 8192); 533 while (istream.available() > 0) { 534 buffer.writeFrom(istream, istream.available()); 535 } 536 537 // Close the output streams 538 if (buffer instanceof Components.interfaces.nsISafeOutputStream) 539 buffer.finish(); 540 else 541 buffer.close(); 542 if (fstream instanceof Components.interfaces.nsISafeOutputStream) 543 fstream.finish(); 544 else 545 fstream.close(); 546 // Close the input stream 547 istream.close(); 548 return file; 549 }; 550 551 /** 552 * NOTE: This function was originally from Thunderbird in abCardOverlay.js 553 * Finds the file extension of the photo identified by the URI, if possible. 554 * This function can be overridden (with a copy of the original) for URIs that 555 * do not identify the extension or when the Content-Type response header is 556 * either not set or isn't 'image/png', 'image/jpeg', or 'image/gif'. 557 * The original function can be called if the URI does not match. 558 * 559 * @param aChannel {nsIHttpChannel} The opened channel for the URI. 560 * 561 * @return The extension of the file, if any, excluding the period. 562 */ 563 com.gContactSync.findPhotoExt = function gCS_findPhotoExt(aChannel) { 564 var mimeSvc = Components.classes["@mozilla.org/mime;1"] 565 .getService(Components.interfaces.nsIMIMEService), 566 ext = "", 567 uri = aChannel.URI; 568 if (uri instanceof Components.interfaces.nsIURL) 569 ext = uri.fileExtension; 570 try { 571 return mimeSvc.getPrimaryExtension(aChannel.contentType, ext); 572 } catch (e) {} 573 return ext === "jpe" ? "jpeg" : ext; 574 }; 575