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  * @overview
 26  * This file contains the contact controller class.
 27  * 
 28  */
 29 
 30 /**
 31  * Creates the contact controller.
 32  * @class
 33  * This class represents the contact controller.
 34  *
 35  * @param {DwtShell}	container	the containing shell
 36  * @param {ZmApp}		abApp		the containing app
 37  * @param {constant}	type		controller type
 38  * @param {string}		sessionId	the session id
 39  *
 40  * @extends		ZmListController
 41  */
 42 ZmContactController = function(container, abApp, type, sessionId) {
 43 
 44 	ZmListController.apply(this, arguments);
 45 
 46 	this._listeners[ZmOperation.SAVE]	= this._saveListener.bind(this);
 47 	this._listeners[ZmOperation.CANCEL]	= this._cancelListener.bind(this);
 48 
 49 	this._tabGroupDone = {};
 50 	this._elementsToHide = ZmAppViewMgr.LEFT_NAV;
 51 };
 52 
 53 ZmContactController.prototype = new ZmListController();
 54 ZmContactController.prototype.constructor = ZmContactController;
 55 
 56 ZmContactController.prototype.isZmContactController = true;
 57 ZmContactController.prototype.toString = function() { return "ZmContactController"; };
 58 
 59 
 60 ZmContactController.getDefaultViewType =
 61 function() {
 62 	return ZmId.VIEW_CONTACT;
 63 };
 64 ZmContactController.prototype.getDefaultViewType = ZmContactController.getDefaultViewType;
 65 
 66 /**
 67  * Shows the contact.
 68  *
 69  * @param	{ZmContact}	contact		the contact
 70  * @param	{Boolean}	isDirty		<code>true</code> to mark the contact as dirty
 71  * @param	{Boolean}	isBack		<code>true</code> in case of DL, we load (or reload) all the DL info, so we have to call back here. isBack indicates this is after the reload so we can continue.
 72  */
 73 ZmContactController.prototype.show =
 74 function(contact, isDirty, isBack) {
 75 	if (contact.id && contact.isDistributionList() && !isBack) {
 76 		//load the full DL info available for the owner, for edit.
 77 		var callback = this.show.bind(this, contact, isDirty, true); //callback HERE
 78 		contact.clearDlInfo();
 79 		contact.gatherExtraDlStuff(callback);
 80 		return;
 81 	}
 82 
 83 	this._contact = contact;
 84 	if (isDirty) {
 85 		this._contactDirty = true;
 86 	}
 87 	this.setList(contact.list);
 88 
 89 	if (!this.getCurrentToolbar()) {
 90 		this._initializeToolBar(this._currentViewId);
 91 	}
 92 	this._resetOperations(this.getCurrentToolbar(), 1); // enable all buttons
 93 
 94 	this._createView(this._currentViewId);
 95 
 96 	this._setViewContents();
 97 	this._initializeTabGroup(this._currentViewId);
 98 	this._app.pushView(this._currentViewId);
 99 	this.updateTabTitle();
100 };
101 
102 ZmContactController.prototype._createView =
103 function(viewId) {
104 	if (this._contactView) {
105 		return;
106 	}
107 	var view = this._contactView = this._createContactView();
108 	//Note - I store this in this._view just to be consistent with certain calls such as for ZmBaseController.prototype._initializeTabGroup. Even though there's no real reason to keep an array of views per type since each controller would only have one view and therefor one type
109 	this._view[viewId] = view;
110 
111 	var callbacks = {};
112 		callbacks[ZmAppViewMgr.CB_PRE_HIDE] = this._preHideCallback.bind(this);
113 		callbacks[ZmAppViewMgr.CB_PRE_UNLOAD] = this._preUnloadCallback.bind(this);
114 		callbacks[ZmAppViewMgr.CB_POST_SHOW] = this._postShowCallback.bind(this);
115 	var elements = this.getViewElements(null, view, this._toolbar[viewId]);
116 
117 	this._app.createView({	viewId:		viewId,
118 							viewType:	this._currentViewType,
119 							elements:	elements, 
120 							hide:		this._elementsToHide,
121 							controller:	this,
122 							callbacks:	callbacks,
123 							tabParams:	this._getTabParams()});
124 };
125 
126 ZmContactController.prototype._postShowCallback =
127 function() {
128 	//have to call it since it's overridden in ZmBaseController to do nothing.
129 	ZmController.prototype._postShowCallback.call(this);
130 	if (this._contactView.postShow) {
131 		this._contactView.postShow();
132 	}
133 };
134 
135 ZmContactController.prototype._getDefaultTabText=
136 function() {
137 	return this._contact.isDistributionList()
138 			? ZmMsg.distributionList
139 				: this._isGroup()
140 			? ZmMsg.group
141 				: ZmMsg.contact;
142 };
143 
144 ZmContactController.prototype._getTabParams =
145 function() {
146 	var text = this._isGroup() ? ZmMsg.group : ZmMsg.contact;
147 	return {id:this.tabId,
148 			image:"CloseGray",
149             hoverImage:"Close",
150 			text: null, //we update it using updateTabTitle since before calling _setViewContents _getFullName does not return the name
151 			textPrecedence:77,
152 			tooltip: text,
153             style: DwtLabel.IMAGE_RIGHT};
154 };
155 
156 ZmContactController.prototype.updateTabTitle =
157 function() {
158 	var	tabTitle = this._contactView._getFullName(true);
159 	if (!tabTitle) {
160 		tabTitle = this._getDefaultTabText();
161 	}
162 	tabTitle = 	tabTitle.substr(0, ZmAppViewMgr.TAB_BUTTON_MAX_TEXT);
163 
164 	appCtxt.getAppViewMgr().setTabTitle(this._currentViewId, tabTitle);
165 };
166 
167 
168 
169 ZmContactController.prototype.getKeyMapName =
170 function() {
171 	return ZmKeyMap.MAP_EDIT_CONTACT;
172 };
173 
174 ZmContactController.prototype.handleKeyAction =
175 function(actionCode) {
176 	DBG.println("ZmContactController.handleKeyAction");
177 	switch (actionCode) {
178 
179 		case ZmKeyMap.SAVE:
180 			var tb = this.getCurrentToolbar();
181 			var saveButton = tb.getButton(ZmOperation.SAVE);
182 			if (!saveButton.getEnabled()) {
183 				break;
184 			}
185 			this._saveListener();
186 			break;
187 
188 		case ZmKeyMap.CANCEL:
189 			this._cancelListener();
190 			break;
191 	}
192 	return true;
193 };
194 
195 /**
196  * Enables the toolbar.
197  *
198  * @param	{Boolean}	enable	<code>true</code> to enable
199  */
200 ZmContactController.prototype.enableToolbar =
201 function(enable) {
202 	if (enable) {
203 		this._resetOperations(this.getCurrentToolbar(), 1);
204 	} else {
205 		this.getCurrentToolbar().enableAll(enable);
206 	}
207 };
208 
209 // Private methods (mostly overrides of ZmListController protected methods)
210 
211 /**
212  * @private
213  */
214 ZmContactController.prototype._getToolBarOps =
215 function() {
216 	return [ZmOperation.SAVE, ZmOperation.CANCEL,
217 			ZmOperation.SEP,
218 			ZmOperation.PRINT, ZmOperation.DELETE,
219 			ZmOperation.SEP,
220 			ZmOperation.TAG_MENU];
221 };
222 
223 /**
224  * @private
225  */
226 ZmContactController.prototype._getActionMenuOps =
227 function() {
228 	return null;
229 };
230 
231 /**
232  * @private
233  */
234 ZmContactController.prototype._isGroup =
235 function() {
236 	return this._contact.isGroup();
237 };
238 
239 
240 ZmContactController.prototype._createContactView =
241 function() {
242 	return this._isGroup()
243 			? new ZmGroupView(this._container, this)
244 			: new ZmEditContactView(this._container, this);
245 };
246 
247 /**
248  * @private
249  */
250 ZmContactController.prototype._initializeToolBar =
251 function(view) {
252 	ZmListController.prototype._initializeToolBar.call(this, view);
253 
254 	var tb = this._toolbar[view];
255 
256 	// change the cancel button to "close" if editing existing contact
257 	var cancelButton = tb.getButton(ZmOperation.CANCEL);
258 	if (this._contact.id == undefined || (this._contact.isGal && !this._contact.isDistributionList())) {
259 		cancelButton.setText(ZmMsg.cancel);
260 	} else {
261 		cancelButton.setText(ZmMsg.close);
262 	}
263 
264 	var saveButton = tb.getButton(ZmOperation.SAVE);
265 	if (saveButton) {
266 		saveButton.setToolTipContent(ZmMsg.saveContactTooltip);
267 	}
268 
269 	appCtxt.notifyZimlets("initializeToolbar", [this._app, tb, this, view], {waitUntilLoaded:true});
270 };
271 
272 /**
273  * @private
274  */
275 ZmContactController.prototype._getTagMenuMsg =
276 function() {
277 	return ZmMsg.AB_TAG_CONTACT;
278 };
279 
280 /**
281  * @private
282  */
283 ZmContactController.prototype._setViewContents =
284 function() {
285 	var cv = this._contactView;
286 	cv.set(this._contact, this._contactDirty);
287 	if (this._contactDirty) {
288 		delete this._contactDirty;
289 	}
290 
291 };
292 
293 /**
294  * @private
295  */
296 ZmContactController.prototype._paginate =
297 function(view, bPageForward) {
298 	// TODO? - page to next/previous contact
299 };
300 
301 /**
302  * @private
303  */
304 ZmContactController.prototype._resetOperations =
305 function(parent, num) {
306 	if (!parent) return;
307 	if (!this._contact.id) {
308 		// disble all buttons except SAVE and CANCEL
309 		parent.enableAll(false);
310 		parent.enable([ZmOperation.SAVE, ZmOperation.CANCEL], true);
311 	}
312 	else if (this._contact.isGal) {
313 		//GAL item or DL.
314 		parent.enableAll(false);
315 		parent.enable([ZmOperation.SAVE, ZmOperation.CANCEL], true);
316 		//for editing a GAL contact - need to check special case for DLs that are owned by current user and if current user has permission to delete on this domain.
317 		var deleteAllowed = ZmContactList.deleteGalItemsAllowed([this._contact]);
318 		parent.enable(ZmOperation.DELETE, deleteAllowed);
319 	} else if (this._contact.isReadOnly()) {
320 		parent.enableAll(true);
321 		parent.enable(ZmOperation.TAG_MENU, false);
322 	} else {
323 		ZmListController.prototype._resetOperations.call(this, parent, num);
324 	}
325 };
326 
327 /**
328  * @private
329  */
330 ZmContactController.prototype._saveListener = function(ev, bIsPopCallback) {
331 
332 	var fileAsChanged = false;
333 	var view = this._contactView;
334 	if (view instanceof DwtForm) {
335 		view.validate();
336     }
337 
338 	if (!view.isValid()) {
339 		var invalidItems = view.getInvalidItems();
340 		// This flag will be set to false when the view.validate() detects some invalid fields (other than EMAIL) which does not have an error message.  If the EMAIL field is the only invalid one, ignore the error and move on.
341 		var onlyEmailInvalid = true;
342 		for (var i = 0; i < invalidItems.length; i++) {
343 			msg = view.getErrorMessage(invalidItems[i]);
344 			var isInvalidEmailAddr = (invalidItems[i].indexOf("EMAIL") != -1);
345 			if (AjxUtil.isString(msg) && !isInvalidEmailAddr) {
346 				msg = msg ? AjxMessageFormat.format(ZmMsg.errorSavingWithMessage, msg) : ZmMsg.errorSaving;
347 				var msgDlg = appCtxt.getMsgDialog();
348 				msgDlg.setMessage(msg, DwtMessageDialog.CRITICAL_STYLE);
349 				msgDlg.popup();
350 				return;
351 			}
352 			onlyEmailInvalid = onlyEmailInvalid && isInvalidEmailAddr;
353 		}
354 		if (!onlyEmailInvalid) {
355 			return;
356 		}
357 	}
358 
359 	var mods = view.getModifiedAttrs();
360 	view.enableInputs(false);
361 
362 	var contact = view.getContact();
363 	if (mods && AjxUtil.arraySize(mods) > 0) {
364 
365 		// bug fix #22041 - when moving betw. shared/local folders, dont modify
366 		// the contact since it will be created/deleted into the new folder
367 		var newFolderId = mods[ZmContact.F_folderId];
368 		var newFolder = newFolderId ? appCtxt.getById(newFolderId) : null;
369 		if (contact.id != null && newFolderId && (contact.isShared() || (newFolder && newFolder.link)) && !contact.isGal) {
370 			// update existing contact with new attrs
371 			for (var a in mods) {
372 				if (a != ZmContact.F_folderId && a != ZmContact.F_groups) {
373 					contact.attr[a] = mods[a];
374 				}
375 			}
376 			// set folder will do the right thing for this shared contact
377 			contact._setFolder(newFolderId);
378 		}
379 		else {
380 			if (contact.id && (!contact.isGal || contact.isDistributionList())) {
381 				if (view.isEmpty()) { //If contact empty, alert the user
382 					var ed = appCtxt.getMsgDialog();
383 					ed.setMessage(ZmMsg.emptyContactSave, DwtMessageDialog.CRITICAL_STYLE);
384 					ed.popup();
385 					view.enableInputs(true);
386 					bIsPopCallback = true;
387 				}
388                 else {
389 					var contactFileAsBefore = ZmContact.computeFileAs(contact),
390 					    contactFileAsAfter = ZmContact.computeFileAs(AjxUtil.hashUpdate(AjxUtil.hashCopy(contact.getAttrs()), mods, true)),
391                         fileAsBefore = contactFileAsBefore ? contactFileAsBefore.toLowerCase()[0] : null,
392                         fileAsAfter = contactFileAsAfter ? contactFileAsAfter.toLowerCase()[0] : null;
393 					this._doModify(contact, mods);
394 					if (fileAsBefore !== fileAsAfter) {
395 						fileAsChanged = true;
396 					}
397 				}
398 			}
399             else {
400 				var isEmpty = true;
401 				for (var a in mods) {
402 					if (mods[a]) {
403 						isEmpty = false;
404 						break;
405 					}
406 				}
407 				if (isEmpty) {
408 					var msg = this._isGroup() ? ZmMsg.emptyGroup : ZmMsg.emptyContact;
409 					appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_WARNING);
410 				}
411 				else {
412 					if (contact.isDistributionList()) {
413 						contact.create(mods);
414 					}
415 					else {
416 						var clc = AjxDispatcher.run("GetContactListController");
417 						var list = (clc && clc.getList()) || new ZmContactList(null);
418 						fileAsChanged = true;
419 						this._doCreate(list, mods);
420 					}
421 				}
422 			}
423 		}
424 	}
425     else {
426 		if (contact.isDistributionList()) {
427 			//in this case, we need to pop the view since we did not call the server to modify the DL.
428 			this.popView();
429 		}
430 		// bug fix #5829 - differentiate betw. an empty contact and saving
431 		//                 an existing contact w/o editing
432 		if (view.isEmpty()) {
433 			var msg = this._isGroup()
434 				? ZmMsg.emptyGroup
435 				: ZmMsg.emptyContact;
436 			appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_WARNING);
437 		}
438         else {
439 			var msg = contact.isDistributionList()
440 				? ZmMsg.dlSaved
441 				: this._isGroup()
442 				? ZmMsg.groupSaved
443 				: ZmMsg.contactSaved;
444 			appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_INFO);
445 		}
446 	}
447 
448 	if (!bIsPopCallback && !contact.isDistributionList()) {
449 		//in the DL case it might fail so wait to pop the view when we receive success from server.
450 		this.popView();
451 	}
452 	else {
453 		view.enableInputs(true);
454 	}
455 	if (fileAsChanged) // bug fix #45069 - if the contact is new, change the search to "all" instead of displaying contacts beginning with a specific letter
456 		ZmContactAlphabetBar.alphabetClicked(null);
457 
458     return true;
459 };
460 
461 ZmContactController.prototype.popView =
462 function() {
463 	this._app.popView(true);
464 	if (this._contactView) { //not sure why _contactView is undefined sometimes. Maybe it's a different instance of ZmContactController.
465 		this._contactView.cleanup();
466 	}
467 };
468 
469 
470 /**
471  * @private
472  */
473 ZmContactController.prototype._cancelListener =
474 function(ev) {
475 	this._app.popView();
476 };
477 
478 /**
479  * @private
480  */
481 ZmContactController.prototype._printListener =
482 function(ev) {
483 	var url = "/h/printcontacts?id=" + this._contact.id;
484     if (appCtxt.isOffline) {
485         var acctName = this._contact.getAccount().name;
486         url+="&acct=" + acctName ;
487     }
488 	window.open(appContextPath+url, "_blank");
489 };
490 
491 /**
492  * @private
493  */
494 ZmContactController.prototype._doDelete =
495 function(items, hardDelete, attrs, skipPostProcessing) {
496 	ZmListController.prototype._doDelete.call(this, items, hardDelete, attrs);
497 	if (items.isDistributionList()) { //items === this._contact here
498 		//do not pop the view as we are not sure the user will confirm the hard delete
499 		return;
500 	}
501 	appCtxt.getApp(ZmApp.CONTACTS).updateIdHash(items, true);
502 
503 	if (!skipPostProcessing) {
504 		// disable input fields (to prevent blinking cursor from bleeding through)
505 		this._contactView.enableInputs(false);
506 		this._app.popView(true);
507 	}
508 };
509 
510 /**
511  * @private
512  */
513 ZmContactController.prototype._preHideCallback =
514 function(view, force) {
515 	ZmController.prototype._preHideCallback.call(this);
516 
517 	if (force) return true;
518 
519 	var view = this._contactView;
520 	if (!view.isDirty()) {
521 		view.cleanup();
522 		return true;
523 	}
524 
525 	var ps = this._popShield = appCtxt.getYesNoCancelMsgDialog();
526 	ps.reset();
527 	ps.setMessage(ZmMsg.askToSave, DwtMessageDialog.WARNING_STYLE);
528 	ps.registerCallback(DwtDialog.YES_BUTTON, this._popShieldYesCallback, this);
529 	ps.registerCallback(DwtDialog.NO_BUTTON, this._popShieldNoCallback, this);
530 	ps.popup(view._getDialogXY());
531 
532 	return false;
533 };
534 
535 /**
536  * @private
537  */
538 ZmContactController.prototype._preUnloadCallback =
539 function(view) {
540 	return this._contactView.clean || !this._contactView.isDirty();
541 };
542 
543 /**
544  * @private
545  */
546 ZmContactController.prototype._popShieldYesCallback =
547 function() {
548     this._popShield.popdown();
549 	if (this._saveListener(null, true)) {
550         this._popShieldCallback();
551     }
552 };
553 
554 /**
555  * @private
556  */
557 ZmContactController.prototype._popShieldNoCallback =
558 function() {
559     this._popShield.popdown();
560     this._popShieldCallback();
561 };
562 
563 /**
564  * @private
565  */
566 ZmContactController.prototype._popShieldCallback = function() {
567     appCtxt.getAppViewMgr().showPendingView(true);
568     this._contactView.cleanup();
569 };
570 
571 /**
572  * @private
573  */
574 ZmContactController.prototype._menuPopdownActionListener =
575 function(ev) {
576 	// bug fix #3719 - do nothing
577 };
578 
579 /**
580  * @private
581  */
582 ZmContactController.prototype._getDefaultFocusItem =
583 function() {
584 	return this._contactView._getDefaultFocusItem();
585 };
586