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) 2009-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) var com = {}; // A generic wrapper variable
 38 // A wrapper for all GCS functions and variables
 39 if (!com.gContactSync) com.gContactSync = {};
 40 
 41 window.addEventListener("load",
 42   /** Initializes the Accounts class when the window has finished loading */
 43   function gCS_AccountsLoadListener(e) {
 44     com.gContactSync.Accounts.initDialog();
 45   },
 46 false);
 47 
 48 /**
 49  * The JavaScript variables and functions that handle different gContactSync
 50  * accounts allowing each synchronized address book to have its own preferences.
 51  * @class
 52  */
 53 com.gContactSync.Accounts = {
 54   /** Stores whether there are any unsaved changes in the Accounts dialog */
 55   mUnsavedChange: false,
 56   /** The column index of the address book name
 57    * change this if adding a column before the AB name
 58    */
 59   mAbNameIndex:  0,
 60   /** Stores the URIs of the ABs displayed in the Accounts dialog's tree */
 61   mAbURIs: [],
 62   /** Element IDs used when enabling/disabling the preferences */
 63   mPrefElemIDs: [
 64     "Username",
 65     "Groups",
 66     "showAdvanced",
 67     "Plugin",
 68     "SyncDirection",
 69     "disabled"
 70   ],
 71   /**
 72    * Initializes the Accounts dialog by filling the tree of address books,
 73    * filling in the usernames, hiding the advanced settings, etc.
 74    */
 75   initDialog:  function Accounts_initDialog() {
 76     try {
 77       this.fillAbTree();
 78       this.fillUsernames();
 79       this.showAdvancedSettings(document.getElementById("showAdvanced").checked);
 80       this.selectedAbChange();
 81     }
 82     catch (e) {
 83       com.gContactSync.LOGGER.LOG_WARNING("Error in Accounts.initDialog", e);
 84       // TODO remove the alert
 85       com.gContactSync.alertError(e);
 86     }
 87   },
 88   /**
 89    * Create a new username/account for the selected plugin.
 90    * @returns {boolean} True if an authentication HTTP request was sent.
 91    */
 92   newUsername: function Accounts_newUsername() {
 93     var prompt   = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
 94                              .getService(Components.interfaces.nsIPromptService)
 95                              .promptUsernameAndPassword,
 96         username = {},
 97         password = {},
 98         // opens a username/password prompt
 99         ok = prompt(window, com.gContactSync.StringBundle.getStr("loginTitle"),
100                     com.gContactSync.StringBundle.getStr("loginText"), username, password, null,
101                     {value: false});
102     if (!ok) {
103       return false;
104     }
105     if (com.gContactSync.LoginManager.getAuthToken(username.value)) { // the username already exists
106       com.gContactSync.alertWarning(com.gContactSync.StringBundle.getStr("usernameExists"));
107       return false;
108     }
109     // This is a primitive way of validating an e-mail address, but Google takes
110     // care of the rest.  It seems to allow getting an auth token w/ only the
111     // username, but returns an error when trying to do anything w/ that token
112     // so this makes sure it is a full e-mail address.
113     if (username.value.indexOf("@") < 1) {
114       com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("invalidEmail"));
115       return this.newUsername();
116     }
117     // fix the username before authenticating
118     username.value = com.gContactSync.fixUsername(username.value);
119     var body    = com.gContactSync.gdata.makeAuthBody(username.value, password.value),
120         httpReq = new com.gContactSync.GHttpRequest("authenticate", null, null, body);
121     // if it succeeds and Google returns the auth token, store it and then start
122     // a new sync
123     httpReq.mOnSuccess = function newUsernameSuccess(httpReq) {
124       com.gContactSync.LoginManager.addAuthToken(username.value,
125                                                  'GoogleLogin' + httpReq.responseText.split("\n")[2]);
126       com.gContactSync.Accounts.selectedAbChange();
127       com.gContactSync.Accounts.fillUsernames();
128     };
129     // if it fails, alert the user and prompt them to try again
130     httpReq.mOnError   = function newUsernameError(httpReq) {
131       com.gContactSync.alertError(com.gContactSync.StringBundle.getStr('authErr'));
132       com.gContactSync.LOGGER.LOG_ERROR('Authentication Error - ' +
133                                         httpReq.status,
134                                         httpReq.responseText);
135       com.gContactSync.Accounts.newUsername();
136     };
137     // if the user is offline, alert them and quit
138     httpReq.mOnOffline = function newUsernameOffline(httpReq) {
139       com.gContactSync.alertWarning(com.gContactSync.StringBundle.getStr('offlineErr'));
140       com.gContactSync.LOGGER.LOG_ERROR(com.gContactSync.StringBundle.getStr('offlineErr'));
141     };
142     httpReq.send();
143     return true;
144   },
145   /**
146    * Returns the GAddressBook corresponding to the currently-selected address
147    * book in the accounts tree.
148    * @returns {com.gContactSync.GAddressBook} A GAddressBook if one is selected, else false.
149    */
150   getSelectedAb: function Accounts_getSelectedAb() {
151     var tree = document.getElementById("loginTree");
152     if (tree.currentIndex < 0) {
153       this.enablePreferences(false);
154       return false;
155     }
156     this.enablePreferences(true);
157     var ab = tree.currentIndex > -1 && tree.currentIndex < this.mAbURIs.length ?
158               com.gContactSync.GAbManager.mABs[this.mAbURIs[tree.currentIndex]] :
159               null;
160     if (!ab) {
161       return false;
162     }
163     return ab;
164   },
165   /**
166    * Creates and returns a new address book after requesting a name for it.
167    * If an AB of any type already exists this function will do nothing.
168    * @returns {nsIAbDirectory} The new address book.
169    */
170   newAddressBook: function Accounts_newAddressBook() {
171     var name = com.gContactSync.prompt(com.gContactSync.StringBundle.getStr("newABPrompt"), null, window);
172     if (!name)
173       return false;
174     var ab = com.gContactSync.AbManager.getAbByName(name);
175     this.fillAbTree();
176     return ab;
177   },
178   /**
179    * Saves the preferences for the selected address book.
180    * @returns {boolean} True if the preferences were saved
181    */
182   saveSelectedAccount: function Accounts_saveSelectedAccount() {
183     var usernameElem  = document.getElementById("Username"),
184         groupElem     = document.getElementById("Groups"),
185         directionElem = document.getElementById("SyncDirection"),
186         pluginElem    = document.getElementById("Plugin"),
187         disableElem   = document.getElementById("disabled"),
188         updateGElem   = document.getElementById("updateGoogleInConflicts"),
189         ab            = this.getSelectedAb(),
190         needsReset    = false;
191     if (!ab) {
192       return null;
193     }
194 
195     if (!usernameElem || !groupElem || !directionElem || !pluginElem || !disableElem) {
196       return false;
197     }
198     var syncGroups = String(groupElem.value === "All"),
199         myContacts = String(groupElem.value !== "All" && groupElem.value !== "false");
200     // the simple preferences
201     ab.savePref("Username",                usernameElem.value);
202     ab.savePref("Plugin",                  pluginElem.value);
203     ab.savePref("Disabled",                String(disableElem.checked));
204     ab.savePref("updateGoogleInConflicts", String(updateGElem.checked));
205     // this is for backward compatibility
206     ab.savePref("Primary",  "true");
207     // Group to sync
208     ab.savePref("syncGroups",     syncGroups);
209     ab.savePref("myContacts",     myContacts);
210     ab.savePref("myContactsName", groupElem.value);
211     // Sync Direction
212     ab.savePref("writeOnly", String(directionElem.value === "WriteOnly"));
213     ab.savePref("readOnly",  String(directionElem.value === "ReadOnly"));
214     // this is done before the needsReset call in case something happens
215     // reset the unsaved change
216     this.mUnsavedChange = false;
217     this.fillUsernames();
218     this.fillAbTree();
219     this.selectedAbChange();
220     // check if the AB should be reset based on the new values
221     needsReset = this.needsReset(ab, usernameElem.value, syncGroups, myContacts, groupElem.value);
222     if (needsReset) {
223       ab.reset();
224       com.gContactSync.alert(com.gContactSync.StringBundle.getStr("finishedAcctSave"));
225     }
226     else {
227       com.gContactSync.alert(com.gContactSync.StringBundle.getStr("finishedAcctSaveNoRestart"));
228     }
229     return true;
230   },
231   /**
232    * Enables or disables the preference elements.
233    * @param aEnable {boolean} Set to true to enable elements or false to disable
234    *                          them.
235    */
236   enablePreferences: function Accounts_enablePreferences(aEnable) {
237     var elem, i;
238     for (i = 0; i < this.mPrefElemIDs.length; i++) {
239       elem = document.getElementById(this.mPrefElemIDs[i]);
240       if (!elem) {
241         com.gContactSync.LOGGER.LOG_WARNING(this.mPrefElemIDs[i] + " not found");
242         continue;
243       }
244       elem.disabled = aEnable ? false : true;
245     }
246   },
247   /**
248    * Show or hide the advanced settings and then call window.sizeToContent().
249    * @param aShow {boolean} Set to true to show the advanced settings or false
250    *                        to hide them.
251    * @returns {boolean} True if the advanced settings were shown or hidden.
252    */
253   showAdvancedSettings: function Accounts_showAdvanceDsettings(aShow) {
254     var elem = document.getElementById("advancedGroupBox");
255     if (!elem) return false;
256     elem.setAttribute("collapsed", aShow ? "false" : "true");
257     window.sizeToContent();
258     return true;
259   },
260   /**
261    * Called when the selected address book changes in the accounts tree.
262    * @returns {boolean} true if there is currently an address book selected.
263    */
264   selectedAbChange: function Accounts_selectedAbChange() {
265     var usernameElem  = document.getElementById("Username"),
266         groupElem     = document.getElementById("Groups"),
267         directionElem = document.getElementById("SyncDirection"),
268         pluginElem    = document.getElementById("Plugin"),
269         disableElem   = document.getElementById("disabled"),
270         updateGElem   = document.getElementById("updateGoogleInConflicts"),
271         ab            = this.getSelectedAb();
272     this.restoreGroups();
273     if (!usernameElem || !groupElem || !directionElem || !pluginElem || !disableElem || !ab) {
274       return false;
275     }
276     // Username/Account
277     this.fillUsernames(ab.mPrefs.Username);
278     // Group
279     // The myContacts pref (enable sync w/ one group) has priority
280     // If that is checked an the myContactsName is pref sync just that group
281     // Otherwise sync all or no groups based on the syncGroups pref
282     var group = ab.mPrefs.myContacts ?
283                (ab.mPrefs.myContactsName ? ab.mPrefs.myContactsName : "false") :
284                (ab.mPrefs.syncGroups !== "false" ? "All" : "false");
285     com.gContactSync.selectMenuItem(groupElem, group, true);
286     // Sync Direction
287     var direction = ab.mPrefs.readOnly === "true" ? "ReadOnly" :
288                       ab.mPrefs.writeOnly === "true" ? "WriteOnly" : "Complete";
289     com.gContactSync.selectMenuItem(directionElem, direction, true);
290     // Temporarily disable synchronization with the address book
291     disableElem.checked = ab.mPrefs.Disabled === "true";
292     // Overwrite remote changes with local changes in a conflict
293     updateGElem.checked = ab.mPrefs.updateGoogleInConflicts === "true";
294     // Select the correct plugin
295     com.gContactSync.selectMenuItem(pluginElem, ab.mPrefs.Plugin, true);
296     
297     return true;
298   },
299   /**
300    * Fills the 'Username' menulist with all the usernames of the current plugin.
301    * @param aDefault {string} The default account to select.  If not present or
302    *                          evaluating to 'false' then 'None' will be
303    *                          selected.
304    */
305   fillUsernames: function Accounts_fillUsernames(aDefault) {
306     var usernameElem = document.getElementById("Username"),
307         tokens       = com.gContactSync.LoginManager.getAuthTokens(),
308         item,
309         username,
310         index = -1;
311     if (!usernameElem) {
312       return false;
313     }
314     // Remove all existing logins from the menulist
315     usernameElem.removeAllItems();
316 
317     usernameElem.appendItem(com.gContactSync.StringBundle.getStr("noAccount"), "none");
318     // Add a menuitem for each account with an auth token
319     for (username in tokens) {
320       item = usernameElem.appendItem(username, username);
321       if (aDefault === username && aDefault !== undefined) {
322         index = usernameElem.menupopup.childNodes.length - 1;
323       }
324     }
325 
326     if (index > -1) {
327       usernameElem.selectedIndex = index;
328     }
329     // if the default value isn't in the menu list, add & select it
330     // this can happen when an account is added through one version of the
331     // login manager and the Accounts dialog was opened in another
332     // This isn't retained (for now?) to prevent anyone from setting up a new
333     // synchronized account with it and expecting it to work.
334     else if (aDefault) {
335       com.gContactSync.selectMenuItem(usernameElem, aDefault, true);
336     }
337     // Otherwise select None
338     else {
339       usernameElem.selectedIndex = 0;
340     }
341 
342     return true;
343   },
344   /**
345    * Populates the address book tree with all Personal/Mork Address Books
346    */
347   fillAbTree: function Accounts_fillAbTree() {
348     var tree          = document.getElementById("loginTree"),
349         treechildren  = document.getElementById("loginTreeChildren"),
350         newTreeChildren,
351         abs,
352         i;
353   
354     if (treechildren) {
355       try { tree.removeChild(treechildren); } catch (e) {}
356     }
357     this.mAbURIs = [];
358     newTreeChildren = document.createElement("treechildren");
359     newTreeChildren.setAttribute("id", "loginTreeChildren");
360     tree.appendChild(newTreeChildren);
361 
362     // Get all Personal/Mork DB Address Books (type == 2,
363     // see mailnews/addrbook/src/nsDirPrefs.h)
364     // TODO - there should be a way to change the allowed dir types...
365     abs = com.gContactSync.GAbManager.getAllAddressBooks(2);
366     for (i in abs) {
367       if (abs[i] instanceof com.gContactSync.GAddressBook) {
368         this.addToTree(newTreeChildren, abs[i]);
369       }
370     }
371     return true;
372   },
373   /**
374    * Adds login information (username and directory name) to the tree.
375    * @param aTreeChildren {object} The <treechildren> XUL element.
376    * @param aAB           {GAddressBook} The GAddressBook to add.
377    */
378   addToTree: function Accounts_addToTree(aTreeChildren, aAB) {
379     if (!aAB || !aAB instanceof com.gContactSync.GAddressBook) {
380       throw "Error - Invalid AB passed to addToTree";
381     }
382     var treeitem    = document.createElement("treeitem"),
383         treerow     = document.createElement("treerow"),
384         addressbook = document.createElement("treecell"),
385         synced      = document.createElement("treecell");
386 
387     addressbook.setAttribute("label", aAB.getName());
388     synced.setAttribute("label",      aAB.mPrefs.Username ||
389                                       com.gContactSync.StringBundle.getStr("noAccount"));
390 
391     treerow.appendChild(addressbook);
392     treerow.appendChild(synced);
393     treeitem.appendChild(treerow);
394     aTreeChildren.appendChild(treeitem);
395     
396     this.mAbURIs.push(aAB.mURI);
397 
398     return true;
399   },
400   /**
401    * Shows an alert dialog that briefly explains the synchronization direction
402    * preference.
403    */
404   directionPopup: function Accounts_directionPopup() {
405     com.gContactSync.alert(com.gContactSync.StringBundle.getStr("directionPopup")); 
406   },
407   /**
408    * Restores the Groups menulist to contain only the default groups.
409    */
410   restoreGroups: function Accounts_restoreGroups() {
411     var groupElem = document.getElementById("GroupsPopup");
412     for (var i = groupElem.childNodes.length - 1; i > -1; i--) {
413       if (groupElem.childNodes[i].getAttribute("class") !== "default")
414         groupElem.removeChild(groupElem.childNodes[i]);
415     }
416   },
417   /**
418    * Fetch all groups for the selected account and add custom groups to the
419    * menulist.
420    */
421   getAllGroups: function Accounts_getAllGroups() {
422     var usernameElem  = document.getElementById("Username");
423     this.restoreGroups();
424     if (usernameElem.value === "none" || !usernameElem.value)
425       return false;
426     var token = com.gContactSync.LoginManager.getAuthTokens()[usernameElem.value];
427     if (!token) {
428       com.gContactSync.LOGGER.LOG_WARNING("Unable to find the token for username " + usernameElem.value);
429       return false;
430     }
431     com.gContactSync.LOGGER.VERBOSE_LOG("Fetching groups for username: " + usernameElem.value);
432     var httpReq = new com.gContactSync.GHttpRequest("getGroups", token, null,
433                                    null, usernameElem.value);
434     httpReq.mOnSuccess = function getAllGroupsSuccess(httpReq) {
435       com.gContactSync.LOGGER.VERBOSE_LOG(com.gContactSync.serializeFromText(httpReq.responseText));
436       com.gContactSync.Accounts.addGroups(httpReq.responseXML,
437                                           usernameElem.value);
438     };
439     httpReq.mOnError   = function getAllGroupsError(httpReq) {
440       com.gContactSync.LOGGER.LOG_ERROR(httpReq.responseText);
441     };
442     httpReq.mOnOffline = null;
443     httpReq.send();
444     return true;
445   },
446   /**
447    * Adds groups in the given atom feed to the Groups menulist provided the
448    * username hasn't changed since the groups request was sent and the username
449    * isn't blank.
450    */
451   addGroups: function Accounts_addGroups(aAtom, aUsername) {
452     var usernameElem  = document.getElementById("Username"),
453         menulistElem  = document.getElementById("Groups"),
454         group,
455         title,
456         i,
457         arr;
458     if (!aAtom) {
459       return false;
460     }
461     if (usernameElem.value === "none" || usernameElem.value !== aUsername) {
462       return false;
463     }
464     arr = aAtom.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.ATOM.url, "entry");
465     com.gContactSync.LOGGER.VERBOSE_LOG("Adding groups from username: " + aUsername);
466     var names = [];
467     for (i = 0; i < arr.length; i++) {
468       group = new com.gContactSync.Group(arr[i]);
469       title = group.getTitle();
470       com.gContactSync.LOGGER.VERBOSE_LOG(" * " + title);
471       // don't add system groups again
472       if (!title || group.isSystemGroup()) {
473         com.gContactSync.LOGGER.VERBOSE_LOG("    - Skipping system group");
474       }
475       else {
476         names.push(title);
477       }
478     }
479     
480     // Sort the group names, but only the non-system groups similar to how
481     // Google Contacts sorts them.
482     names.sort();
483     // Now add the group names to the Groups menulist
484     for (i = 0; i < names.length; i++) {
485       menulistElem.appendItem(names[i], names[i]);
486     }
487     return true;
488   },
489   /**
490    * Returns whether the given address book should be reset and prompts the user
491    * before returning true.
492    * Resetting an address book is necessary when ALL of the following
493    * conditions marked with * are met:
494    *  * The username was NOT originally blank
495    *  * The new username is NOT blank
496    *  * The last sync date of the AB is > 0
497    *  * The user agrees that the AB should be reset (using a confirm dialog)
498    *  * AND at least one of the following is true:
499    *    o The username has changed (and wasn't originally blank)
500    *    o OR The group to sync has been changed
501    *
502    * @param aAB {string}              The GAddressBook being modified.  If this
503    *                                  function returns true this AB should be
504    *                                  reset.
505    * @param aUsername {string}        The new username for the account with
506    *                                  which aAB will be synchronized.
507    * @param aSyncGroups {string}      The new value for the syncGroups pref.
508    * @param aMyContacts {string}      The new value for the myContacts pref.
509    * @param aMyContactsName {string}  The new value for the myContactsName pref.
510    *
511    * @return {boolean} true if the AB should be reset.  See the detailed
512    *                        description for more details.
513    */
514   needsReset: function Accounts_needsReset(aAB, aUsername, aSyncGroups, aMyContacts, aMyContactsName) {
515     // This should not be necessary, but it's better to be safe
516     aAB.getPrefs();
517     com.gContactSync.LOGGER.VERBOSE_LOG
518       (
519        "**Determining if the address book '" + aAB.getName() +
520        "' should be reset:\n" +
521       "  * " + aUsername       + " <- " + aAB.mPrefs.Username + "\n" +
522       "  * " + aSyncGroups     + " <- " + aAB.mPrefs.syncGroups + "\n" +
523       "  * " + aMyContacts     + " <- " + aAB.mPrefs.myContacts + "\n" +
524       "  * " + aMyContactsName + " <- " + aAB.mPrefs.myContactsName + "\n" +
525       "  * Last sync date: " + aAB.mPrefs.lastSync
526      );
527     // NOTE: mUnsavedChange is reset to false before this method is called
528     if ((aAB.mPrefs.Username && aAB.mPrefs.Username !== "none") &&
529          aUsername !== "none" &&
530          parseInt(aAB.mPrefs.lastSync, 10) > 0 &&
531          (
532           aAB.mPrefs.Username !== aUsername ||
533           aAB.mPrefs.syncGroups !== aSyncGroups ||
534           aAB.mPrefs.myContacts !== aMyContacts ||
535           aAB.mPrefs.myContactsName !== aMyContactsName
536          )) {
537       var reset = com.gContactSync.confirm(com.gContactSync.StringBundle.getStr("confirmABReset"));
538       com.gContactSync.LOGGER.VERBOSE_LOG("  * Confirmation result: " + reset + "\n");
539       return reset;
540     }
541     com.gContactSync.LOGGER.VERBOSE_LOG("  * The AB will NOT be reset\n");
542     return false;
543   },
544   /**
545    * This method is called when the user clicks the Accept button
546    * (labeled Close) or when acceptDialog() is called.
547    * If there are unsaved changes it will let the user save changes if
548    * desired.
549    * @returns {boolean} Always returns true (close the dialog).
550    */
551   close: function Accounts_close() {
552     if (this.mUnsavedChange &&
553         com.gContactSync.confirm(com.gContactSync.StringBundle.getStr("unsavedAcctChanges"))) {
554       this.saveSelectedAccount();
555     }
556     return true;
557   }
558 };
559