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