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