1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Creates a new, empty preferences controller. 26 * @constructor 27 * @class 28 * This class represents the preferences controller. This controller manages 29 * the options pages. 30 * 31 * @author Conrad Damon 32 * 33 * @param {DwtShell} container the shell 34 * @param {ZmPreferencesApp} prefsApp the preferences application 35 * 36 * @extends ZmController 37 */ 38 ZmPrefController = function(container, prefsApp) { 39 40 if (arguments.length == 0) { return; } 41 42 ZmController.call(this, container, prefsApp); 43 44 this._listeners = {}; 45 this._listeners[ZmOperation.SAVE] = this._saveListener.bind(this); 46 this._listeners[ZmOperation.CANCEL] = this._backListener.bind(this); 47 this._listeners[ZmOperation.REVERT_PAGE] = 48 this._resetPageListener.bind(this); 49 50 this._filtersEnabled = appCtxt.get(ZmSetting.FILTERS_ENABLED); 51 this._dirty = {}; 52 }; 53 54 ZmPrefController.prototype = new ZmController; 55 ZmPrefController.prototype.constructor = ZmPrefController; 56 57 ZmPrefController.prototype.isZmPrefController = true; 58 ZmPrefController.prototype.toString = function() { return "ZmPrefController"; }; 59 60 61 ZmPrefController.getDefaultViewType = 62 function() { 63 return ZmId.VIEW_PREF; 64 }; 65 ZmPrefController.prototype.getDefaultViewType = ZmPrefController.getDefaultViewType; 66 67 /** 68 * Shows the tab options pages. 69 */ 70 ZmPrefController.prototype.show = 71 function() { 72 this._setView(); 73 this._prefsView.show(); 74 this._app.pushView(this._currentViewId); 75 }; 76 77 /** 78 * Gets the preferences view (a view with tabs). 79 * 80 * @return {ZmPrefView} the preferences view 81 */ 82 ZmPrefController.prototype.getPrefsView = 83 function() { 84 return this._prefsView; 85 }; 86 87 /** 88 * Gets the current preferences page 89 * 90 * @return {ZmPreferencesPage} the current page 91 */ 92 ZmPrefController.prototype.getCurrentPage = 93 function() { 94 var tabKey = this._prefsView.getCurrentTab(); 95 return this._prefsView.getTabView(tabKey); 96 }; 97 98 /** 99 * Gets the account test dialog. 100 * 101 * @return {ZmAccountTestDialog} the account test dialog 102 */ 103 ZmPrefController.prototype.getTestDialog = 104 function() { 105 if (!this._testDialog) { 106 this._testDialog = new ZmAccountTestDialog(this._container); 107 } 108 return this._testDialog; 109 }; 110 111 /** 112 * Gets the filter controller. 113 * 114 * @return {ZmFilterController} the filter controller 115 */ 116 ZmPrefController.prototype.getFilterController = 117 function(section) { 118 if (!this._filterController) { 119 this._filterController = new ZmFilterController(this._container, this._app, this._prefsView, section || ZmPref.getPrefSectionWithPref(ZmSetting.FILTERS), this); 120 } 121 return this._filterController; 122 }; 123 124 /** 125 * Gets the mobile devices controller. 126 * 127 * @return {ZmMobileDevicesController} the mobile devices controller 128 */ 129 ZmPrefController.prototype.getMobileDevicesController = 130 function() { 131 if (!this._mobileDevicesController) { 132 this._mobileDevicesController = new ZmMobileDevicesController(this._container, this._app, this._prefsView); 133 } 134 return this._mobileDevicesController; 135 }; 136 137 /** 138 * Checks for a precondition on the given object. If one is found, it is 139 * evaluated based on its type. Note that the precondition must be contained 140 * within the object in a property named "precondition". 141 * 142 * @param obj [object] an object, possibly with a "precondition" property. 143 * @param precondition [object]* explicit precondition to check 144 * 145 * @private 146 */ 147 ZmPrefController.prototype.checkPreCondition = 148 function(obj, precondition) { 149 // No object, nothing to check 150 if (!obj && !ZmSetting[precondition]) { 151 return true; 152 } 153 // Object lacks "precondition" property, nothing to check 154 if (obj && !("precondition" in obj)) { 155 return true; 156 } 157 var p = (obj && obj.precondition) || precondition; 158 // Object has a precondition that didn't get defined, probably because its 159 // app is not enabled. That equates to failure for the precondition. 160 if (p == null) { 161 return false; 162 } 163 // Precondition is set to true or false 164 if (AjxUtil.isBoolean(p)) { 165 return p; 166 } 167 // Precondition is a function, look at its result 168 if (AjxUtil.isFunction(p)) { 169 return p(); 170 } 171 // A list of preconditions is ORed together via a recursive call 172 if (AjxUtil.isArray(p)) { 173 for (var i = 0, count = p.length; i < count; i++) { 174 if (this.checkPreCondition(null, p[i])) { 175 return true; 176 } 177 } 178 return false; 179 } 180 // Assume that the precondition is a setting, and return its value 181 return Boolean(appCtxt.get(p)); 182 }; 183 184 ZmPrefController.prototype.getKeyMapName = 185 function() { 186 return ZmKeyMap.MAP_OPTIONS; 187 }; 188 189 ZmPrefController.prototype.handleKeyAction = 190 function(actionCode) { 191 DBG.println("ZmPrefController.handleKeyAction"); 192 switch (actionCode) { 193 194 case ZmKeyMap.CANCEL: 195 this._backListener(); 196 break; 197 198 case ZmKeyMap.SAVE: 199 this._saveListener(); 200 break; 201 202 default: 203 return ZmController.prototype.handleKeyAction.call(this, actionCode); 204 break; 205 } 206 return true; 207 }; 208 209 ZmPrefController.prototype.mapSupported = 210 function(map) { 211 return (map == "tabView"); 212 }; 213 214 /** 215 * Gets the tab view. 216 * 217 * @return {ZmPrefView} the preferences view 218 * 219 * @see #getPrefsView 220 */ 221 ZmPrefController.prototype.getTabView = 222 function() { 223 return this.getPrefsView(); 224 }; 225 226 ZmPrefController.prototype.resetDirty = 227 function(view, dirty) { 228 this._dirty = {}; 229 }; 230 231 ZmPrefController.prototype.setDirty = 232 function(view, dirty) { 233 this._dirty[view] = dirty; 234 }; 235 236 ZmPrefController.prototype.isDirty = 237 function(view) { 238 return this._dirty[view]; 239 }; 240 241 /** 242 * Public method called to save prefs - does not check for dirty flag. 243 * 244 * @param {AjxCallback} callback the async callback 245 * @param {Boolean} noPop if <code>true</code>, do not pop view after save 246 * 247 * TODO: shouldn't have to call getChangedPrefs() twice 248 * 249 * @private 250 */ 251 ZmPrefController.prototype.save = 252 function(callback, noPop) { 253 // perform pre-save ops, if needed 254 var preSaveCallbacks = this._prefsView.getPreSaveCallbacks(); 255 if (preSaveCallbacks && preSaveCallbacks.length > 0) { 256 var continueCallback = new AjxCallback(this, this._doPreSave); 257 continueCallback.args = [continueCallback, preSaveCallbacks, callback, noPop]; 258 this._doPreSave.apply(this, continueCallback.args); 259 } 260 else { // do basic save 261 this._doSave(callback, noPop); 262 } 263 }; 264 265 /** 266 * Enables/disables toolbar buttons. 267 * 268 * @param {ZmButtonToolBar} parent the toolbar 269 * @param {constant} view the current view (tab) 270 * 271 * @private 272 */ 273 ZmPrefController.prototype._resetOperations = 274 function(parent, view) { 275 var section = ZmPref.getPrefSectionMap()[view]; 276 var manageChanges = section && section.manageChanges; 277 parent.enable(ZmOperation.SAVE, !manageChanges); 278 parent.enable(ZmOperation.CANCEL, true); 279 }; 280 281 /** 282 * Creates the prefs view, with a tab for each preferences page. 283 * 284 * @private 285 */ 286 ZmPrefController.prototype._setView = 287 function() { 288 if (!this._prefsView) { 289 this._initializeToolBar(); 290 this._initializeLeftToolBar(); 291 var callbacks = new Object(); 292 callbacks[ZmAppViewMgr.CB_PRE_HIDE] = this._preHideCallback.bind(this); 293 callbacks[ZmAppViewMgr.CB_PRE_UNLOAD] = this._preUnloadCallback.bind(this); 294 callbacks[ZmAppViewMgr.CB_PRE_SHOW] = this._preShowCallback.bind(this); 295 callbacks[ZmAppViewMgr.CB_POST_SHOW] = this._postShowCallback.bind(this); 296 297 this._prefsView = new ZmPrefView({parent:this._container, posStyle:Dwt.ABSOLUTE_STYLE, controller:this}); 298 var elements = {}; 299 elements[ZmAppViewMgr.C_NEW_BUTTON] = this._lefttoolbar; 300 elements[ZmAppViewMgr.C_TOOLBAR_TOP] = this._toolbar; 301 elements[ZmAppViewMgr.C_APP_CONTENT] = this._prefsView; 302 303 this._app.createView({ viewId: this._currentViewId, 304 elements: elements, 305 controller: this, 306 callbacks: callbacks, 307 isAppView: true}); 308 this._initializeTabGroup(); 309 } 310 }; 311 312 /** 313 * Initializes the left toolbar and sets up the listeners. 314 * 315 * @private 316 */ 317 ZmPrefController.prototype._initializeLeftToolBar = 318 function () { 319 if (this._lefttoolbar) return; 320 321 var buttons = [ZmOperation.SAVE, ZmOperation.CANCEL]; 322 this._lefttoolbar = new ZmButtonToolBar({parent:this._container, buttons:buttons, context:this._currentViewId}); 323 buttons = this._lefttoolbar.opList; 324 for (var i = 0; i < buttons.length; i++) { 325 var button = buttons[i]; 326 if (this._listeners[button]) { 327 this._lefttoolbar.addSelectionListener(button, this._listeners[button]); 328 } 329 } 330 this._lefttoolbar.getButton(ZmOperation.SAVE).setToolTipContent(ZmMsg.savePrefs); 331 }; 332 333 /** 334 * Initializes the toolbar and sets up the listeners. 335 * 336 * @private 337 */ 338 ZmPrefController.prototype._initializeToolBar = 339 function () { 340 if (this._toolbar) return; 341 342 var buttons = this._getToolBarOps(); 343 this._toolbar = new ZmButtonToolBar({parent:this._container, buttons:buttons, context:this._currentViewId}); 344 buttons = this._toolbar.opList; 345 for (var i = 0; i < buttons.length; i++) { 346 var button = buttons[i]; 347 if (this._listeners[button]) { 348 this._toolbar.addSelectionListener(button, this._listeners[button]); 349 } 350 } 351 352 appCtxt.notifyZimlets("initializeToolbar", [this._app, this._toolbar, this, this._currentViewId], {waitUntilLoaded:true}); 353 354 }; 355 356 /** 357 * Returns the current tool bar (the one on the left with Save/Cancel). 358 * 359 * @return {ZmButtonToolbar} the toolbar 360 */ 361 ZmPrefController.prototype.getCurrentToolbar = function() { 362 return this._lefttoolbar; 363 }; 364 365 ZmPrefController.prototype._getToolBarOps = 366 function () { 367 return [ZmOperation.REVERT_PAGE]; 368 }; 369 370 ZmPrefController.prototype._initializeTabGroup = 371 function () { 372 var tg = this._createTabGroup(); 373 var rootTg = appCtxt.getRootTabGroup(); 374 tg.newParent(rootTg); 375 tg.addMember(this._lefttoolbar.getTabGroupMember()); 376 tg.addMember(this._toolbar.getTabGroupMember()); 377 tg.addMember(this._prefsView.getTabGroupMember()); 378 }; 379 380 /** 381 * Saves any options that have been changed. This method first sees if any of the 382 * preference pages need to perform any logic prior to the actual save. See the 383 * <code>ZmPrefView#getPreSaveCallbacks</code> documentation for further details. 384 * 385 * @param ev [DwtEvent] click event 386 * @param callback [AjxCallback] async callback 387 * @param noPop [boolean] if true, don't pop view after save 388 * 389 * TODO: shouldn't have to call getChangedPrefs() twice 390 * 391 * @private 392 */ 393 ZmPrefController.prototype._saveListener = 394 function(ev, callback, noPop) { 395 // is there anything to do? 396 var dirty = this._prefsView.getChangedPrefs(true, true); 397 if (!dirty) { 398 appCtxt.getAppViewMgr().popView(true); 399 return; 400 } 401 402 this.save(callback, noPop); 403 }; 404 405 ZmPrefController.prototype._doPreSave = 406 function(continueCallback, preSaveCallbacks, callback, noPop, success) { 407 // cancel save 408 if (success != null && !success) { return; } 409 410 // perform save 411 if (preSaveCallbacks.length == 0) { 412 this._doSave(callback, noPop); 413 } 414 415 // continue pre-save operations 416 else { 417 var preSaveCallback = preSaveCallbacks.shift(); 418 preSaveCallback.run(continueCallback); 419 } 420 }; 421 422 ZmPrefController.prototype._doSave = 423 function(callback, noPop) { 424 var batchCommand = new ZmBatchCommand(false); 425 426 // get changed prefs 427 var list; 428 try { 429 list = this._prefsView.getChangedPrefs(false, false, batchCommand); 430 } 431 catch (e) { 432 // getChangedPrefs throws an AjxException if any of the values have not passed validation. 433 if (e instanceof AjxException) { 434 appCtxt.setStatusMsg(e.msg, ZmStatusView.LEVEL_CRITICAL); 435 } else { 436 throw e; 437 } 438 return; 439 } 440 441 // save generic settings 442 appCtxt.getSettings().save(list, null, batchCommand); 443 this.resetDirty(); 444 445 // save any extra commands that may have been added 446 if (batchCommand.size()) { 447 var respCallback = this._handleResponseSaveListener.bind(this, true, callback, noPop, list); 448 var errorCallback = this._handleResponseSaveError.bind(this); 449 batchCommand.run(respCallback, errorCallback); 450 } 451 else { 452 this._handleResponseSaveListener(list.length > 0, callback, noPop, list); 453 } 454 }; 455 456 ZmPrefController.prototype._handleResponseSaveError = 457 function(exception1/*, ..., exceptionN*/) { 458 for (var i = 0; i < arguments.length; i++) { 459 var exception = arguments[i]; 460 var message = exception instanceof AjxException ? 461 (exception.msg || exception.code) : String(exception); 462 if (exception.code == ZmCsfeException.ACCT_INVALID_ATTR_VALUE || 463 exception.code == ZmCsfeException.INVALID_REQUEST) { 464 // above faults come with technical/cryptic LDAP error msg; input validation 465 // should keep us from getting here 466 message = ZmMsg.invalidPrefValue; 467 } 468 else if(exception.code == ZmCsfeException.TOO_MANY_IDENTITIES) { 469 //Bug fix # 80409 - Show a custom/localized message and not the server error 470 message = ZmMsg.errorTooManyIdentities; 471 } 472 else if(exception.code == ZmCsfeException.IDENTITY_EXISTS) { 473 //Displaying custom message in case of identity already exists 474 message = AjxMessageFormat.format(ZmMsg.errorIdentityAlreadyExists, message.substring(message.length, message.lastIndexOf(':') + 2)); 475 } 476 appCtxt.setStatusMsg(message, ZmStatusView.LEVEL_CRITICAL); 477 } 478 }; 479 480 ZmPrefController.prototype._handleResponseSaveListener = 481 function(optionsSaved, callback, noPop, list, result) { 482 if (optionsSaved) { 483 appCtxt.setStatusMsg(ZmMsg.optionsSaved); 484 } 485 486 var hasFault = result && result._data && result._data.BatchResponse 487 ? result._data.BatchResponse.Fault : null; 488 489 if (!noPop && (!result || !hasFault)) { 490 try { 491 // pass force flag - we just saved, so we know view isn't dirty 492 appCtxt.getAppViewMgr().popView(true); 493 } catch (ex) { 494 // do nothing - sometimes popView throws an exception ala history mgr 495 } 496 } 497 498 if (callback) { 499 callback.run(result); 500 } 501 502 var changed = {}; 503 for (var i = 0; i < list.length; i++) { 504 changed[list[i].id] = true; 505 } 506 var postSaveCallbacks = this._prefsView.getPostSaveCallbacks(); 507 if (postSaveCallbacks && postSaveCallbacks.length) { 508 for (var i = 0; i < postSaveCallbacks.length; i++) { 509 postSaveCallbacks[i].run(changed); 510 } 511 } 512 //Once preference is saved, reload the application cache to get the latest changes 513 appCtxt.reloadAppCache(); 514 }; 515 516 ZmPrefController.prototype._backListener = 517 function() { 518 appCtxt.getAppViewMgr().popView(); 519 }; 520 521 ZmPrefController.prototype._resetPageListener = 522 function() { 523 var viewPage = this.getCurrentPage(); 524 525 viewPage.reset(false); 526 appCtxt.setStatusMsg(ZmMsg.defaultsPageRestore); 527 }; 528 529 ZmPrefController.prototype._stateChangeListener = 530 function (ev) { 531 var resetbutton = this._toolbar.getButton(ZmOperation.REVERT_PAGE); 532 resetbutton.setEnabled(this.getCurrentPage().hasResetButton()); 533 } 534 535 ZmPrefController.prototype._preHideCallback = 536 function(view, force) { 537 ZmController.prototype._preHideCallback.call(this); 538 return force ? true : this.popShield(); 539 }; 540 541 ZmPrefController.prototype._preUnloadCallback = 542 function(view) { 543 return !this._prefsView.isDirty(); 544 }; 545 546 ZmPrefController.prototype._preShowCallback = 547 function() { 548 if (appCtxt.multiAccounts) { 549 var viewPage = this.getCurrentPage(); 550 if (viewPage) { 551 // bug: 42399 - the active account may not be "owned" by what is 552 // initially shown in prefs 553 var active = appCtxt.accountList.activeAccount; 554 if (!this._activeAccount) { 555 this._activeAccount = active; 556 } 557 else if (this._activeAccount != active) { 558 appCtxt.accountList.setActiveAccount(this._activeAccount); 559 } 560 561 viewPage.showMe(); 562 } 563 } 564 return true; // *always* return true! 565 }; 566 567 ZmPrefController.prototype._postShowCallback = 568 function() { 569 ZmController.prototype._postShowCallback.call(this); 570 // NOTE: Some pages need to know when they are being shown again in order to 571 // display the state correctly. 572 this._prefsView.reset(); 573 }; 574 575 ZmPrefController.prototype.popShield = 576 function() { 577 if (!this._prefsView.isDirty()) { return true; } 578 579 var ps = this._popShield = appCtxt.getYesNoCancelMsgDialog(); 580 ps.reset(); 581 ps.setMessage(ZmMsg.confirmExitPreferences, DwtMessageDialog.WARNING_STYLE); 582 ps.registerCallback(DwtDialog.YES_BUTTON, this._popShieldYesCallback, this); 583 ps.registerCallback(DwtDialog.NO_BUTTON, this._popShieldNoCallback, this); 584 ps.registerCallback(DwtDialog.CANCEL_BUTTON, this._popShieldCancelCallback, this); 585 ps.popup(); 586 587 return false; 588 }; 589 590 ZmPrefController.prototype._popShieldYesCallback = 591 function() { 592 var respCallback = new AjxCallback(this, this._handleResponsePopShieldYesCallback); 593 this._saveListener(null, respCallback, true); 594 this._popShield.popdown(); 595 }; 596 597 ZmPrefController.prototype._handleResponsePopShieldYesCallback = 598 function() { 599 appCtxt.getAppViewMgr().showPendingView(true); 600 }; 601 602 ZmPrefController.prototype._popShieldNoCallback = 603 function() { 604 this._prefsView.reset(); 605 this._popShield.popdown(); 606 this.resetDirty(); 607 appCtxt.getAppViewMgr().showPendingView(true); 608 }; 609 610 ZmPrefController.prototype._popShieldCancelCallback = 611 function() { 612 this._popShield.popdown(); 613 appCtxt.getAppViewMgr().showPendingView(false); 614 }; 615 616 ZmPrefController.prototype._getDefaultFocusItem = 617 function() { 618 return this._prefsView.getTabGroupMember() || this._lefttoolbar || this._toolbar || null; 619 }; 620