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