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 an empty view of the preference pages. 26 * @constructor 27 * @class 28 * This class represents a tabbed view of the preference pages. 29 * 30 * @author Conrad Damon 31 * 32 * @param {Hash} params a hash of parameters 33 * @param {DwtComposite} parent the parent widget 34 * @param {constant} posStyle the positioning style 35 * @param {ZmController} controller the owning controller 36 * 37 * @extends DwtTabView 38 */ 39 ZmPrefView = function(params) { 40 41 params.className = "ZmPrefView"; 42 DwtTabView.call(this, params); 43 44 this._parent = params.parent; 45 this._controller = params.controller; 46 47 48 this.prefView = {}; 49 this._tabId = {}; 50 this._sectionId = {}; 51 this.hasRendered = false; 52 53 this.setVisible(false); 54 this.setScrollStyle(Dwt.CLIP); 55 this.getTabBar().setVisible(false); 56 this.getTabBar().noTab = true; 57 this.addStateChangeListener(new AjxListener(this, this._stateChangeListener)); 58 }; 59 60 ZmPrefView.prototype = new DwtTabView; 61 ZmPrefView.prototype.constructor = ZmPrefView; 62 63 ZmPrefView.prototype.toString = 64 function () { 65 return "ZmPrefView"; 66 }; 67 68 ZmPrefView.prototype.getController = 69 function() { 70 return this._controller; 71 }; 72 73 ZmPrefView.prototype.getSectionForTab = 74 function(tabKey) { 75 var sectionId = this._sectionId[tabKey]; 76 return ZmPref.getPrefSectionMap()[sectionId]; 77 }; 78 79 ZmPrefView.prototype.getTabForSection = 80 function(sectionOrId) { 81 var section = (typeof sectionOrId == "string") 82 ? ZmPref.getPrefSectionMap()[sectionOrId] : sectionOrId; 83 var sectionId = section && section.id; 84 return this._tabId[sectionId]; 85 }; 86 87 ZmPrefView.prototype.show = 88 function() { 89 if (this.hasRendered) { return; } 90 91 // add sections that have been registered so far 92 var sections = ZmPref.getPrefSectionArray(); 93 for (var i = 0; i < sections.length; i++) { 94 var section = sections[i]; 95 this._addSection(section); 96 } 97 98 // add listener for sections added/removed later... 99 var account = appCtxt.isOffline && appCtxt.accountList.mainAccount; 100 var setting = appCtxt.getSettings(account).getSetting(ZmSetting.PREF_SECTIONS); 101 setting.addChangeListener(new AjxListener(this, this._prefSectionsModified)); 102 103 // display 104 this.resetKeyBindings(); 105 this.hasRendered = true; 106 this.setVisible(true); 107 }; 108 109 ZmPrefView.prototype._prefSectionsModified = 110 function(evt) { 111 var sectionId = evt.getDetails(); 112 var section = appCtxt.get(ZmSetting.PREF_SECTIONS, sectionId); 113 if (section) { 114 this._prefSectionAdded(section); 115 } 116 else { 117 this._prefSectionRemoved(sectionId); 118 } 119 }; 120 121 ZmPrefView.prototype._prefSectionAdded = 122 function(section) { 123 // add section to tabs 124 var index = this._getIndexForSection(section.id); 125 var added = this._addSection(section, index); 126 127 if (added) { 128 // create new page pref organizer 129 var organizer = ZmPrefPage.createFromSection(section); 130 var treeController = appCtxt.getOverviewController().getTreeController(ZmOrganizer.PREF_PAGE); 131 var tree = treeController.getDataTree(); 132 133 if (tree) { 134 var parent = tree.getById(ZmId.getPrefPageId(section.parentId)) || tree.root; 135 organizer.pageId = this.getNumTabs(); 136 organizer.parent = parent; 137 138 // find index within parent's children 139 var index = null; 140 var children = parent.children.getArray(); 141 for (var i = 0; i < children.length; i++) { 142 if (section.priority < this.getSectionForTab(children[i].pageId).priority) { 143 index = i; 144 break; 145 } 146 } 147 parent.children.add(organizer, index); 148 149 // notify so that views can be updated 150 organizer._notify(ZmEvent.E_CREATE); 151 } 152 } 153 }; 154 155 ZmPrefView.prototype._prefSectionRemoved = 156 function(sectionId) { 157 var index = this._getIndexForSection(sectionId); 158 var tree = appCtxt.getTree(ZmOrganizer.PREF_PAGE); 159 var organizer = tree && tree.getById(ZmId.getPrefPageId(sectionId)); 160 if (organizer) { 161 organizer.notifyDelete(); 162 } 163 }; 164 165 /** 166 * <strong>Note:</strong> 167 * This is typically called automatically when adding sections. 168 * 169 * @param section [object] The section to add. 170 * @param index [number] (Optional) The index where to add. 171 * 172 * @private 173 */ 174 ZmPrefView.prototype._addSection = function(section, index) { 175 176 // does the section meet the precondition? 177 if ((!appCtxt.multiAccounts || (appCtxt.multiAccounts && appCtxt.getActiveAccount().isMain)) && 178 !appCtxt.checkPrecondition(section.precondition, section.preconditionAny)) { 179 180 return false; 181 } 182 183 if (this.prefView[section.id]) { 184 return false; // Section already exists 185 } 186 187 // create pref page's view 188 var view = (section.createView) 189 ? (section.createView(this, section, this._controller)) 190 : (new ZmPreferencesPage(this, section, this._controller)); 191 this.prefView[section.id] = view; 192 193 // add section as a tab 194 var tabButtonId = ZmId.getTabId(this._controller.getCurrentViewId(), ZmId.getPrefPageId(section.id)); 195 var tabId = this.addTab(section.title, view, tabButtonId, index); 196 this._tabId[section.id] = tabId; 197 this._sectionId[tabId] = section.id; 198 return true; 199 }; 200 201 ZmPrefView.prototype._getIndexForSection = 202 function(id) { 203 var sections = ZmPref.getPrefSectionArray(); 204 for (var i = 0; i < sections.length; i++) { 205 if (sections[i].id == id) break; 206 } 207 return i; 208 }; 209 210 ZmPrefView.prototype.reset = 211 function() { 212 for (var id in this.prefView) { 213 var viewPage = this.prefView[id]; 214 // if feature is disabled, may not have a view page 215 // or if page hasn't rendered, nothing has changed 216 if (!viewPage || (viewPage && !viewPage.hasRendered)) { continue; } 217 viewPage.reset(); 218 } 219 }; 220 221 ZmPrefView.prototype.resetOnAccountChange = 222 function() { 223 for (var id in this.prefView) { 224 this.prefView[id].resetOnAccountChange(); 225 } 226 }; 227 228 ZmPrefView.prototype.getTitle = 229 function() { 230 return (this.hasRendered && this.getActiveView().getTitle()); 231 }; 232 233 ZmPrefView.prototype.getView = 234 function(view) { 235 return this.prefView[view]; 236 }; 237 238 /** 239 * This method iterates over the preference pages to see if any of them have 240 * actions to perform <em>before</em> saving. If the page has a 241 * <code>getPreSaveCallback</code> method and it returns a callback, the pref 242 * controller will call it before performing any save. This is done for each 243 * page that returns a callback. 244 * <p> 245 * The pre-save callback is passed a callback that <em>MUST</em> be called upon 246 * completion of the pre-save code. This is so the page can perform its pre-save 247 * behavior asynchronously without the need to immediately return to the pref 248 * controller. 249 * <p> 250 * <strong>Note:</strong> 251 * When calling the continue callback, the pre-save code <em>MUST</em> pass a 252 * single boolean signifying the success of the the pre-save operation. 253 * <p> 254 * An example pre-save callback implementation: 255 * <pre> 256 * MyPrefView.prototype.getPreSaveCallback = function() { 257 * return new AjxCallback(this, this._preSaveAction, []); 258 * }; 259 * 260 * MyPrefView.prototype._preSaveAction = 261 * function(continueCallback, batchCommand) { 262 * var success = true; 263 * // perform some operation 264 * continueCallback.run(success); 265 * }; 266 * </pre> 267 * 268 * @return {Array} an array of {AjxCallback} objects 269 */ 270 ZmPrefView.prototype.getPreSaveCallbacks = 271 function() { 272 var callbacks = []; 273 for (var id in this.prefView) { 274 var viewPage = this.prefView[id]; 275 if (viewPage && viewPage.getPreSaveCallback && viewPage.hasRendered) { 276 var callback = viewPage.getPreSaveCallback(); 277 if (callback) { 278 callbacks.push(callback); 279 } 280 } 281 } 282 return callbacks; 283 }; 284 285 /** 286 * This method iterates over the preference pages to see if any of them have 287 * actions to perform <em>after</em> saving. If the page has a 288 * <code>getPostSaveCallback</code> method and it returns a callback, the pref 289 * controller will call it after performing any save. This is done for each page 290 * that returns a callback. 291 * 292 * @return {Array} an array of {AjxCallback} objects 293 */ 294 ZmPrefView.prototype.getPostSaveCallbacks = 295 function() { 296 var callbacks = []; 297 for (var id in this.prefView) { 298 var viewPage = this.prefView[id]; 299 var callback = viewPage && viewPage.hasRendered && 300 viewPage.getPostSaveCallback && viewPage.getPostSaveCallback(); 301 if (callback) { 302 callbacks.push(callback); 303 } 304 } 305 return callbacks; 306 }; 307 308 /** 309 * Gets the changed preferences. Each prefs page is checked in 310 * turn. This method can also be used to check simply whether <em>_any_</em> 311 * prefs have changed, in which case it short-circuits as soon as it finds one that has changed. 312 * 313 * @param {Boolean} dirtyCheck if <code>true</code>, only check if any prefs have changed 314 * @param {Boolean} noValidation if <code>true</code>, don't perform any validation 315 * @param {ZmBatchCommand} batchCommand if not <code>null</code>, add soap docs to this batch command 316 * 317 * @return {Array|Boolean} an array of {ZmPref} objects or <code>false</code> if no changed prefs 318 */ 319 ZmPrefView.prototype.getChangedPrefs = 320 function(dirtyCheck, noValidation, batchCommand) { 321 var list = []; 322 var errors= []; 323 var sections = ZmPref.getPrefSectionMap(); 324 var pv = this.prefView; 325 for (var view in pv) { 326 var section = sections[view]; 327 if (!section || (section && section.manageChanges)) { continue; } 328 329 var viewPage = pv[view]; 330 if (!viewPage || (viewPage && !viewPage.hasRendered)) { continue; } 331 332 if (section.manageDirty) { 333 var isDirty = viewPage.isDirty(section, list, errors); 334 if (isDirty) { 335 if (dirtyCheck) { 336 return true; 337 } else { 338 this._controller.setDirty(view, true); 339 } 340 } 341 if (!noValidation) { 342 if (!viewPage.validate()) { 343 throw new AjxException(viewPage.getErrorMessage()); 344 } 345 } 346 if (!dirtyCheck && batchCommand) { 347 viewPage.addCommand(batchCommand); 348 } 349 } 350 var isSaveCommand = (batchCommand) ? true : false; 351 try { 352 var result = this._checkSection(section, viewPage, dirtyCheck, noValidation, list, errors, view, isSaveCommand); 353 } catch (e) { 354 throw(e); 355 } 356 if (dirtyCheck && result) { 357 return true; 358 } 359 360 // errors can only have a value if noValidation is false 361 if (errors.length) { 362 throw new AjxException(errors.join("\n")); 363 } 364 } 365 return dirtyCheck ? false : list; 366 }; 367 368 ZmPrefView.prototype._checkSection = function(section, viewPage, dirtyCheck, noValidation, list, errors, view, isSaveCommand) { 369 370 var settings = appCtxt.getSettings(); 371 var prefs = section && section.prefs; 372 var isAllDayVacation = false; 373 for (var j = 0, count = prefs ? prefs.length : 0; j < count; j++) { 374 var id = prefs[j]; 375 if (!viewPage._prefPresent || !viewPage._prefPresent[id]) { continue; } 376 var setup = ZmPref.SETUP[id]; 377 var defaultError = setup.errorMessage; 378 if (!appCtxt.checkPrecondition(setup.precondition, setup.preconditionAny)) { 379 continue; 380 } 381 382 var type = setup ? setup.displayContainer : null; 383 // ignore non-form elements 384 if (type == ZmPref.TYPE_PASSWORD || type == ZmPref.TYPE_CUSTOM) { continue; } 385 386 // check if value has changed 387 var value; 388 try { 389 value = viewPage.getFormValue(id); 390 } catch (e) { 391 if (dirtyCheck) { 392 return true; 393 } else { 394 throw e; 395 } 396 } 397 var pref = settings.getSetting(id); 398 var origValue = pref.origValue; 399 if (setup.approximateFunction) { 400 if (setup.displayFunction) { 401 origValue = setup.displayFunction(origValue); 402 } 403 origValue = setup.approximateFunction(origValue); 404 if (setup.valueFunction) { 405 origValue = setup.valueFunction(origValue); 406 } 407 } 408 409 if (pref.name == "zimbraPrefAutoSaveDraftInterval"){ 410 // We are checking if zimbraPrefAutoSaveDraftInterval is set or not 411 var orig = !(!origValue); 412 var current = !(!value); 413 if (orig == current) 414 origValue = value; 415 } 416 417 //this is ugly but it's all due to keeping the information on whether the duration is all-day by setting end hour to 23:59:59, instead of having a separate flag on the server. See Bug 80059. 418 //the field does not support seconds so we set to 23:59 and so we need to take care of it not to think the vacation_until has changed. 419 if (id === "VACATION_DURATION_ALL_DAY") { 420 isAllDayVacation = value; //keep this info for the iteration that checks VACATION_UNTIL (luckily it's after... a bit hacky to rely on it maybe). 421 } 422 var comparableValue = value; 423 var comparableOrigValue = origValue; 424 if (id === "VACATION_UNTIL" && isAllDayVacation) { 425 //for comparing, compare just the dates (e.g. 20130214) since it's an all day, so only significant change is the date, not the time. See bug 80059 426 comparableValue = value.substr(0, 8); 427 comparableOrigValue = origValue.substr(0, 8); 428 } 429 /** 430 In OOO vacation external select, first three options have same value i.e false, so we do 431 comparableValue = !comparableOrigValue; 432 so that it enters the inner "_prefChanged" function and from there we add pref to list, depending upon which 433 option is selected and it maps to which pref. Both comparableValue and comparableOrigValue are local variables 434 to this function, so no issues. 435 */ 436 if (id === "VACATION_EXTERNAL_SUPPRESS" && (dirtyCheck || isSaveCommand)) { 437 comparableValue = !comparableOrigValue; 438 } 439 440 if (this._prefChanged(pref.dataType, comparableOrigValue, comparableValue)) { 441 var isValid = true; 442 if (!noValidation) { 443 var maxLength = setup ? setup.maxLength : null; 444 var validationFunc = setup ? setup.validationFunction : null; 445 if (!noValidation && maxLength && (value.length > maxLength)) { 446 isValid = false; 447 } else if (!noValidation && validationFunc) { 448 isValid = validationFunc(value); 449 } 450 } 451 if (isValid) { 452 if (!dirtyCheck && isSaveCommand) { 453 if (setup.setFunction) { 454 setup.setFunction(pref, value, list, viewPage); 455 } else { 456 pref.setValue(value); 457 if (pref.name) { 458 list.push(pref); 459 } 460 } 461 } else if (!dirtyCheck) { 462 //for logging 463 list.push({name: section.title + "." + id, origValue: origValue, value:value}); 464 } 465 } else { 466 errors.push(AjxMessageFormat.format(setup.errorMessage, AjxStringUtil.htmlEncode(value))); 467 setup.errorMessage = defaultError; 468 } 469 this._controller.setDirty(view, true); 470 if (dirtyCheck) { 471 return true; 472 } 473 } 474 } 475 }; 476 477 ZmPrefView.prototype._prefChanged = 478 function(type, origValue, value) { 479 480 var test1 = (typeof value == "undefined" || value === null || value === "") ? null : value; 481 var test2 = (typeof origValue == "undefined" || origValue === null || origValue === "") ? null : origValue; 482 483 if (type == ZmSetting.D_LIST) { 484 return !AjxUtil.arrayCompare(test1, test2); 485 } 486 if (type == ZmSetting.D_HASH) { 487 return !AjxUtil.hashCompare(test1, test2); 488 } 489 if (type == ZmSetting.D_INT) { 490 test1 = parseInt(test1); 491 test2 = parseInt(test2); 492 } 493 return Boolean(test1 != test2); 494 }; 495 496 /** 497 * Checks if any preference has changed. 498 * 499 * @return {Boolean} <code>true</code> if any preference has changed 500 */ 501 ZmPrefView.prototype.isDirty = 502 function() { 503 try { 504 var printPref = function(pref) { 505 if (AjxUtil.isArray(pref)) { 506 return AjxUtil.map(pref, printPref).join("<br>"); 507 } 508 return [pref.name, ": from ", (pref.origValue!=="" ? pref.origValue : "[empty]"), " to ", (pref.value!=="" ? pref.value : "[empty]")].join(""); 509 } 510 511 var changed = this.getChangedPrefs(false, true); // Will also update this._controller._dirty 512 if (changed && changed.length) { 513 AjxDebug.println(AjxDebug.PREFS, "Dirty preferences:<br>" + printPref(changed)); 514 return true; 515 } 516 517 var dirtyViews = AjxUtil.keys(this._controller._dirty, function(key,obj){return obj[key]}); 518 if (dirtyViews.length) { 519 AjxDebug.println(AjxDebug.PREFS, "Dirty preference views:<br>" + dirtyViews.join("<br>")); 520 return true; 521 } 522 523 return false; 524 } catch (e) { 525 AjxDebug.println(AjxDebug.PREFS, "Exception in preferences: " + e.name + ": " + e.message); 526 return true; 527 } 528 }; 529 530 /** 531 * Selects the section (tab) with the given id. 532 * 533 * @param {String} sectionId the section id 534 * 535 */ 536 ZmPrefView.prototype.selectSection = 537 function(sectionId) { 538 this.switchToTab(this._tabId[sectionId]); 539 540 // Mark the correct organizer entry 541 var tree = appCtxt.getTree(ZmOrganizer.PREF_PAGE); 542 var organizer = tree && tree.getById(ZmId.getPrefPageId(sectionId)); 543 if (organizer) { 544 var treeController = appCtxt.getOverviewController().getTreeController(ZmOrganizer.PREF_PAGE); 545 var treeView = treeController && treeController.getTreeView(appCtxt.getCurrentApp().getOverviewId()); 546 if (treeView) 547 treeView.setSelected(organizer, true, false); 548 } 549 }; 550 551 ZmPrefView.prototype._stateChangeListener = 552 function(ev) { 553 if (ev && ev.item && ev.item instanceof ZmPrefView) { 554 var view = ev.item.getActiveView(); 555 view._controller._stateChangeListener(ev); 556 } 557 558 }; 559