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