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 };