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-2010 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 = "0"; 55 /** The suffix for the current version of gContactSync (ie a7 for Alpha 7) */ 56 com.gContactSync.versionSuffix = "b1"; 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 = com.gContactSync.StringBundle.getStr("dummy2"), 172 id = null; 173 // GContact and TBContact may not be defined 174 try { 175 if (aContact instanceof com.gContactSync.GContact) 176 id = aContact.getID(true); 177 // otherwise it is from Thunderbird, so try to get the Google ID, if any 178 else if (aContact instanceof com.gContactSync.TBContact) 179 id = aContact.getID(); 180 else 181 id = com.gContactSync.GAbManager.getCardValue(aContact, "GoogleID"); 182 } catch (e) { 183 try { 184 // try getting the card's value 185 if (aContact.getProperty) // post Bug 413260 186 id = aContact.getProperty("GoogleID", null); 187 else // pre Bug 413260 188 id = aContact.getStringAttribute("GoogleID"); 189 } 190 catch (ex) {} 191 } 192 if (id) { 193 // take just the ID and not the whole URL 194 return prefix + id.substr(1 + id.lastIndexOf("/")) + suffix; 195 } 196 // if there is no ID make a random number and remove the "0." 197 else { 198 return prefix + String(Math.random()).replace("0.", "") + suffix; 199 } 200 }; 201 202 /** 203 * Returns true if the given e-mail address is a fake 'dummy' address. 204 * 205 * @param aEmail {string} The e-mail address to check. 206 * @returns {boolean} true if aEmail is a dummy e-mail address 207 * false otherwise 208 */ 209 com.gContactSync.isDummyEmail = function gCS_isDummyEmail(aEmail) { 210 return aEmail && aEmail.indexOf && 211 aEmail.indexOf(com.gContactSync.StringBundle.getStr("dummy2")) !== -1; 212 }; 213 214 /** 215 * Selects the menuitem with the given value (value or label attribute) in the 216 * given menulist. 217 * Optionally creates the menuitem if it cannot be found. 218 * 219 * @param aMenuList {menulist} The menu list element to search. 220 * @param aValue {string} The value to find in a menuitem. This can be 221 * either the 'value' or 'label' attribute of the 222 * matched item. Case insensitive. 223 * @param aCreate {boolean} Set as true to create and select a new menuitem 224 * if a match cannot be found. 225 */ 226 com.gContactSync.selectMenuItem = function gCS_selectMenuItem(aMenuList, aValue, aCreate) { 227 if (!aMenuList || !aMenuList.menupopup || !aValue) 228 throw "Invalid parameter sent to selectMenuItem"; 229 230 var arr = aMenuList.menupopup.childNodes, 231 i, 232 item, 233 aValueLC = aValue.toLowerCase(); 234 for (i = 0; i < arr.length; i++) { 235 item = arr[i]; 236 if (item.getAttribute("value").toLowerCase() === aValueLC || 237 item.getAttribute("label").toLowerCase() === aValueLC) { 238 aMenuList.selectedIndex = i; 239 return true; 240 } 241 } 242 if (!aCreate) 243 return false; 244 item = aMenuList.appendItem(aValue, aValue); 245 // getIndexOfItem was added in TB/FF 3 246 aMenuList.selectedIndex = aMenuList.menupopup.childNodes.length - 1; 247 return true; 248 }; 249 250 /** 251 * Attempts a few basic fixes for 'broken' usernames. 252 * In the past, gContactSync didn't check that a username included the domain 253 * which would pass authentication and then fail to do anything else. 254 * It also didn't make sure there were no spaces in a username which would 255 * also pass authentication and break for everything else. 256 * See Bug 21567 257 * 258 * @param aUsername {string} The username to fix. 259 * 260 * @returns {string} A username with a domain and no spaces. 261 */ 262 com.gContactSync.fixUsername = function gCS_fixUsername(aUsername) { 263 if (!aUsername) 264 return null; 265 // Add @gmail.com if necessary 266 if (aUsername.indexOf("@") === -1) 267 aUsername += "@gmail.com"; 268 // replace any spaces or tabs 269 aUsername = aUsername.replace(/[ \t\n\r]/g, ""); 270 return aUsername; 271 }; 272 273 /** 274 * Displays an alert dialog with the given text and an optional title. 275 * 276 * @param aText {string} The message to display. 277 * @param aTitle {string} The title for the message (optional - default is 278 * "gContactSync Notification"). 279 * @param aParent {nsIDOMWindow} The parent window (also optional). 280 */ 281 com.gContactSync.alert = function gCS_alert(aText, aTitle, aParent) { 282 if (!aTitle) { 283 aTitle = com.gContactSync.StringBundle.getStr("alertTitle"); 284 } 285 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 286 .getService(Components.interfaces.nsIPromptService); 287 promptService.alert(aParent, aTitle, aText); 288 }; 289 290 /** 291 * Displays an alert dialog titled "gContactSync Error" (in English). 292 * 293 * @param aText {string} The message to display. 294 */ 295 com.gContactSync.alertError = function gCS_alertError(aText) { 296 var title = com.gContactSync.StringBundle.getStr("alertError"); 297 com.gContactSync.alert(aText, title, null); 298 }; 299 300 /** 301 * Displays an alert dialog titled "gContactSync Warning" (in English). 302 * 303 * @param aText {string} The message to display. 304 */ 305 com.gContactSync.alertWarning = function gCS_alertWarning(aText) { 306 var title = com.gContactSync.StringBundle.getStr("alertWarning"); 307 com.gContactSync.alert(aText, title, null); 308 }; 309 310 /** 311 * Displays a confirmation dialog with the given text and an optional title. 312 * 313 * @param aText {string} The message to display. 314 * @param aTitle {string} The title for the message (optional - default is 315 * "gContactSync Confirmation"). 316 * @param aParent {nsIDOMWindow} The parent window (also optional). 317 */ 318 com.gContactSync.confirm = function gCS_confirm(aText, aTitle, aParent) { 319 if (!aTitle) { 320 aTitle = com.gContactSync.StringBundle.getStr("confirmTitle"); 321 } 322 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 323 .getService(Components.interfaces.nsIPromptService); 324 return promptService.confirm(aParent, aTitle, aText); 325 }; 326 327 /** 328 * Displays a prompt with the given text and an optional title. 329 * 330 * @param aText {string} The message to display. 331 * @param aTitle {string} The title for the message (optional - default is 332 * "gContactSync Prompt"). 333 * @param aParent {nsIDOMWindow} The parent window (also optional). 334 * @param aDefault {string} The default value for the textbox. 335 */ 336 com.gContactSync.prompt = function gCS_prompt(aText, aTitle, aParent, aDefault) { 337 if (!aTitle) { 338 aTitle = com.gContactSync.StringBundle.getStr("promptTitle"); 339 } 340 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 341 .getService(Components.interfaces.nsIPromptService), 342 input = { value: aDefault }, 343 response = promptService.prompt(aParent, aTitle, aText, input, null, {}); 344 return response ? input.value : false; 345 }; 346 347 /** 348 * Opens the Accounts dialog for gContactSync 349 */ 350 com.gContactSync.openAccounts = function gCS_openAccounts() { 351 window.open("chrome://gcontactsync/content/Accounts.xul", 352 "gContactSync_Accts", 353 "chrome=yes,resizable=yes,toolbar=yes,centerscreen=yes"); 354 }; 355 356 /** 357 * Opens the Preferences dialog for gContactSync 358 */ 359 com.gContactSync.openPreferences = function gCS_openPreferences() { 360 window.open("chrome://gcontactsync/content/options.xul", 361 "gContactSync_Prefs", 362 "chrome=yes,resizable=yes,toolbar=yes,centerscreen=yes"); 363 }; 364 365 /** 366 * Opens the given URL using the openFormattedURL and 367 * openFormattedRegionURL functions. 368 * 369 * @param aURL {string} THe URL to open. 370 */ 371 com.gContactSync.openURL = function gCS_openURL(aURL) { 372 com.gContactSync.LOGGER.VERBOSE_LOG("Opening the following URL: " + aURL); 373 if (!aURL) { 374 com.gContactSync.LOGGER.LOG_WARNING("Caught an attempt to load a blank URL"); 375 return; 376 } 377 try { 378 if (openFormattedURL) { 379 openFormattedURL(aURL); 380 return; 381 } 382 } 383 catch (e) { 384 com.gContactSync.LOGGER.LOG_WARNING(" - Error in openFormattedURL", e); 385 } 386 try { 387 if (openFormattedRegionURL) { 388 openFormattedRegionURL(aURL); 389 return; 390 } 391 } 392 catch (e) { 393 com.gContactSync.LOGGER.LOG_WARNING(" - Error in openFormattedURL", e); 394 } 395 com.gContactSync.LOGGER.LOG_WARNING("Could not open the URL: " + aURL); 396 return; 397 }; 398 399 /** 400 * Opens the "view source" window with the log file. 401 */ 402 com.gContactSync.showLog = function gCS_showLog() { 403 try { 404 window.open("view-source:file://" + com.gContactSync.FileIO.mLogFile.path, 405 "gContactSyncLog", 406 "chrome=yes,resizable=yes,height=480,width=600"); 407 } 408 catch(e) { 409 com.gContactSync.LOGGER.LOG_WARNING("Unable to open the log", e); 410 } 411 }; 412 413 /** 414 * Replaces https://... with http://... in URLs as a permanent workaround for 415 * the issue described here: 416 * http://www.google.com/support/forum/p/apps-apis/thread?tid=6fde249ce2ffe7a9&hl=en 417 * 418 * @param aURL {string} The URL to fix. 419 * @return {string} The URL using https instead of http 420 */ 421 com.gContactSync.fixURL = function gCS_fixURL(aURL) { 422 if (!aURL) { 423 return aURL; 424 } 425 return aURL.replace(/^https:/i, "http:"); 426 }; 427 428 /** 429 * Fetches and saves a local copy of this contact's photo, if present. 430 * NOTE: Portions of this code are from Thunderbird written by me (Josh Geenen) 431 * See https://bugzilla.mozilla.org/show_bug.cgi?id=119459 432 * @param aURL {string} The URL of the photo to download 433 * @param aFilename {string} The name of the file to which the photo will be 434 * written. The extenion of the photo will be 435 * appended to this name, and the photo will be in the 436 * TB profile folder under the "Photos" directory. 437 * @param aRedirect {string} The number of times the request was redirected. 438 * If > 5 then the download attempt will be aborted. 439 */ 440 com.gContactSync.writePhoto = function gCS_writePhoto(aURL, aFilename, aRedirect) { 441 if (!aURL) { 442 com.gContactSync.LOGGER.LOG_WARNING("No aURL passed to writePhoto"); 443 return null; 444 } 445 if (aRedirect > 5) { 446 com.gContactSync.LOGGER.LOG_WARNING("Caught > 5 redirection attempts, aborting photo download"); 447 return null; 448 } 449 450 // Get the profile directory 451 var file = Components.classes["@mozilla.org/file/directory_service;1"] 452 .getService(Components.interfaces.nsIProperties) 453 .get("ProfD", Components.interfaces.nsIFile); 454 // Get (or make) the Photos directory 455 file.append("Photos"); 456 if (!file.exists() || !file.isDirectory()) 457 file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0777); 458 var ios = Components.classes["@mozilla.org/network/io-service;1"] 459 .getService(Components.interfaces.nsIIOService); 460 var ch = ios.newChannel(aURL, null, null); 461 ch.QueryInterface(Components.interfaces.nsIHttpChannel); 462 //ch.setRequestHeader("Authorization", aAuthToken, false); 463 var istream = ch.open(); 464 // Quit if the request failed 465 if (!ch.requestSucceeded) { 466 // At least Facebook returns a 302 with a new Location for the photo. 467 if (ch.responseStatus == 302) { 468 var newURL = ch.getResponseHeader("Location"); 469 com.gContactSync.LOGGER.VERBOSE_LOG("Received a 302, Location: " + newURL); 470 return com.gContactSync.writePhoto(newURL, aFilename, aRedirect + 1); 471 } 472 com.gContactSync.LOGGER.LOG_WARNING("The request to retrive the photo returned with a status ", 473 ch.responseStatus); 474 return null; 475 } 476 477 // Create a name for the photo with the contact's ID and the photo extension 478 try { 479 var ext = com.gContactSync.findPhotoExt(ch); 480 aFilename += (ext ? "." + ext : ""); 481 } 482 catch (e) { 483 com.gContactSync.LOGGER.LOG_WARNING("Couldn't find an extension for the photo"); 484 } 485 file.append(aFilename); 486 com.gContactSync.LOGGER.VERBOSE_LOG(" * Writing the photo to " + file.path); 487 488 var output = Components.classes["@mozilla.org/network/file-output-stream;1"] 489 .createInstance(Components.interfaces.nsIFileOutputStream); 490 491 // Now write that input stream to the file 492 var fstream = Components.classes["@mozilla.org/network/safe-file-output-stream;1"] 493 .createInstance(Components.interfaces.nsIFileOutputStream); 494 var buffer = Components.classes["@mozilla.org/network/buffered-output-stream;1"] 495 .createInstance(Components.interfaces.nsIBufferedOutputStream); 496 fstream.init(file, 0x04 | 0x08 | 0x20, 0600, 0); // write, create, truncate 497 buffer.init(fstream, 8192); 498 while (istream.available() > 0) { 499 buffer.writeFrom(istream, istream.available()); 500 } 501 502 // Close the output streams 503 if (buffer instanceof Components.interfaces.nsISafeOutputStream) 504 buffer.finish(); 505 else 506 buffer.close(); 507 if (fstream instanceof Components.interfaces.nsISafeOutputStream) 508 fstream.finish(); 509 else 510 fstream.close(); 511 // Close the input stream 512 istream.close(); 513 return file; 514 }; 515 516 /** 517 * NOTE: This function was originally from Thunderbird in abCardOverlay.js 518 * Finds the file extension of the photo identified by the URI, if possible. 519 * This function can be overridden (with a copy of the original) for URIs that 520 * do not identify the extension or when the Content-Type response header is 521 * either not set or isn't 'image/png', 'image/jpeg', or 'image/gif'. 522 * The original function can be called if the URI does not match. 523 * 524 * @param aChannel {nsIHttpChannel} The opened channel for the URI. 525 * 526 * @return The extension of the file, if any, excluding the period. 527 */ 528 com.gContactSync.findPhotoExt = function gCS_findPhotoExt(aChannel) { 529 var mimeSvc = Components.classes["@mozilla.org/mime;1"] 530 .getService(Components.interfaces.nsIMIMEService), 531 ext = "", 532 uri = aChannel.URI; 533 if (uri instanceof Components.interfaces.nsIURL) 534 ext = uri.fileExtension; 535 try { 536 return mimeSvc.getPrimaryExtension(aChannel.contentType, ext); 537 } catch (e) {} 538 return ext === "jpe" ? "jpeg" : ext; 539 };