1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 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 defines the import/export controller.
 27  *
 28  */
 29 
 30 /**
 31  * Creates an import/export controller.
 32  * @class
 33  * This class represents an import/export controller.
 34  * 
 35  * @extends		ZmController
 36  */
 37 ZmImportExportController = function() {
 38 	ZmController.call(this, null);
 39 };
 40 
 41 ZmImportExportController.prototype = new ZmController;
 42 ZmImportExportController.prototype.constructor = ZmImportExportController;
 43 
 44 /**
 45  * Returns a string representation of the object.
 46  * 
 47  * @return		{String}		a string representation of the object
 48  */
 49 ZmImportExportController.prototype.toString = function() {
 50 	return "ZmImportExportController";
 51 };
 52 
 53 //
 54 // Constants
 55 //
 56 
 57 ZmImportExportController.IMPORT_TIMEOUT = 300;
 58 
 59 /**
 60  * Defines the "CSV" type.
 61  * @type {String}
 62  */
 63 ZmImportExportController.TYPE_CSV = "csv";
 64 /**
 65  * Defines the "ICS" type.
 66  * @type {String}
 67  */
 68 ZmImportExportController.TYPE_ICS = "ics";
 69 /**
 70  * Defines the "TGZ" type.
 71  * @type {String}
 72  */
 73 ZmImportExportController.TYPE_TGZ = "tgz";
 74 
 75 /**
 76  * Defines the default type.
 77  * 
 78  * @see		ZmImportExportController.TYPE_TGZ
 79  */
 80 ZmImportExportController.TYPE_DEFAULT = ZmImportExportController.TYPE_TGZ;
 81 
 82 /**
 83  * Defines the sub-type default array
 84  */
 85 ZmImportExportController.SUBTYPE_DEFAULT = {};
 86 ZmImportExportController.SUBTYPE_DEFAULT[ZmImportExportController.TYPE_TGZ] = ZmImportExportController.SUBTYPE_ZIMBRA_TGZ;
 87 ZmImportExportController.SUBTYPE_DEFAULT[ZmImportExportController.TYPE_CSV] = ZmImportExportController.SUBTYPE_ZIMBRA_CSV;
 88 ZmImportExportController.SUBTYPE_DEFAULT[ZmImportExportController.TYPE_ICS] = ZmImportExportController.SUBTYPE_ZIMBRA_ICS;
 89 
 90 ZmImportExportController.TYPE_EXTS = {};
 91 ZmImportExportController.TYPE_EXTS[ZmImportExportController.TYPE_CSV] = [ "csv", "vcf" ];
 92 ZmImportExportController.TYPE_EXTS[ZmImportExportController.TYPE_ICS] = [ "ics" ];
 93 ZmImportExportController.TYPE_EXTS[ZmImportExportController.TYPE_TGZ] = [ "tgz", "zip", "tar" ];
 94 
 95 ZmImportExportController.EXTS_TYPE = {};
 96 AjxUtil.foreach(ZmImportExportController.TYPE_EXTS, function(exts, p) {
 97 	for (var i = 0; i < exts.length; i++) {
 98 		ZmImportExportController.EXTS_TYPE[exts[i]] = p;
 99 	}
100 });
101 
102 ZmImportExportController.__FAULT_ARGS_MAPPING = {
103 	"formatter.INVALID_FORMAT": [ "filename" ],
104 	"formatter.INVALID_TYPE": [ "view", "path" ],
105 	"formatter.MISMATCHED_META": [ "path" ],
106 	"formatter.MISMATCHED_SIZE": [ "path" ],
107 	"formatter.MISMATCHED_TYPE": [ "path" ],
108 	"formatter.MISSING_BLOB": [ "path" ],
109 	"formatter.MISSING_META": [ "path" ],
110 	"formatter.MISSING_VCARD_FIELDS": [ "path" ],
111 	"formatter.UNKNOWN_ERROR": [ "path", "message" ],
112 	"mail.EXPORT_PERIOD_TOO_LONG": [ "limit" ]
113 };
114 
115 
116 ZmImportExportController.CSRF_TOKEN_HIDDEN_INPUT_ID = "ZmImportExportCsrfToken";
117 //
118 // Public methods
119 //
120 
121 /**
122  * Imports user data as specified in the <code>params</code> object.
123  * 
124  * @param {Hash}	params			a hash of parameters
125  * @param {Element}	params.form		the form containing file input field
126  * @param {String}	params.folderId	the folder id for import. If not specified, assumes import to root folder.
127  * @param {String}	params.type		the type (defaults to {@link TYPE_TGZ})
128  * @param {String}	params.subType	the sub-type (defaults to <code>SUBTYPE_DEFAULT[type]</code>)
129  * @param {String}	params.resolve	resolve duplicates: "" (ignore), "modify", "replace", "reset" (defaults to ignore).
130  * @param {String}	params.views	a comma-separated list of views
131  * @param {AjxCallback}	callback	the callback for success
132  * @param {AjxCallback}	errorCallback	the callback for errors
133  *        
134  * @return	{Boolean}	<code>true</code> if the import is successful
135  */
136 ZmImportExportController.prototype.importData =
137 function(params) {
138 	// error checking
139 	params = params || {};
140 	var folderId = params.folderId || -1;
141 	if (folderId == -1) {
142 		var params = {
143 			msg:	ZmMsg.importErrorMissingFolder,
144 			level:	ZmStatusView.LEVEL_CRITICAL
145 		};
146 		appCtxt.setStatusMsg(params);
147 		return false;
148 	}
149 
150 	params.filename = params.form && params.form.elements["file"].value;
151 	if (!params.filename) {
152 		var params = {
153 			msg:	ZmMsg.importErrorMissingFile,
154 			level:	ZmStatusView.LEVEL_CRITICAL
155 		};
156 		appCtxt.setStatusMsg(params);
157 		return false;
158 	}
159 
160 	params.ext = params.filename.replace(/^.*\./,"").toLowerCase();
161     if (!ZmImportExportController.EXTS_TYPE[params.ext]) {
162         var params = {
163             msg:	AjxMessageFormat.format(ZmMsg.importErrorTypeNotSupported, params.ext),
164             level:	ZmStatusView.LEVEL_CRITICAL
165         };
166         appCtxt.setStatusMsg(params);
167         return false;
168     }
169 	params.defaultType = params.type || ZmImportExportController.EXTS_TYPE[params.ext] || ZmImportExportController.TYPE_DEFAULT;
170 	var isZimbra = ZmImportExportController.EXTS_TYPE[params.defaultType] == ZmImportExportController.TYPE_TGZ;
171 	var folder = appCtxt.getById(folderId);
172 	if (!isZimbra && folder && folder.nId == ZmOrganizer.ID_ROOT) {
173 		var params = {
174 			msg:	ZmMsg.importErrorRootNotAllowed,
175 			level:	ZmStatusView.LEVEL_CRITICAL
176 		};
177 		appCtxt.setStatusMsg(params);
178 		return false;
179 	}
180 
181 	if (params.resolve == "reset") {
182 		var dialog = appCtxt.getOkCancelMsgDialog();
183 		dialog.registerCallback(DwtDialog.OK_BUTTON, this._confirmImportReset, this, [params]);
184 		dialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._cancelImportReset, this);
185 		var msg = ZmMsg.importResetWarning;
186 		var style = DwtMessageDialog.WARNING_STYLE;
187 		dialog.setMessage(msg, style);
188 		dialog.popup();
189 		return false;
190 	}
191 
192 	// import
193 	return this._doImportData(params);
194 };
195 
196 /**
197  * Exports user data as specified in the <code>params</code> object.
198  *
199  * @param {Hash}	params		a hash of parameters
200  * @param {String}	params.folderId		the folder id for export. If not specified, all folders want to be exported.
201  * @param {String}	params.type			the type (defaults to {@link TYPE_TGZ})
202  * @param {String}	params.subType		the sub-type (defaults to <code>SUBTYPE_DEFAULT[type]</code>)
203  * @param {String}	params.views		a comma-separated list of views
204  * @param {String}	params.filename		the filename for exported file
205  * @param {String}	params.searchFilter	the search filter
206  * @param {Boolean} params.skipMeta		if <code>true</code>, skip export of meta-data
207  * @param {AjxCallback}	callback	the callback for success
208  * @param {AjxCallback}	errorCallback	the callback for errors
209  *        
210  * @return	{Boolean}	<code>true</code> if the export is successful
211  */
212 ZmImportExportController.prototype.exportData = function(params) {
213 	// error checking
214 	params = params || {};
215 	var folderId = params.folderId || -1;
216 	if (folderId == -1) {
217 		var params = {
218 			msg:	ZmMsg.exportErrorMissingFolder,
219 			level:	ZmStatusView.LEVEL_CRITICAL
220 		};
221 		appCtxt.setStatusMsg(params);
222 		return false;
223 	}
224 
225 	var type = params.type = params.type || ZmImportExportController.TYPE_DEFAULT;
226 	var isZimbra = ZmImportExportController.EXTS_TYPE[type] == ZmImportExportController.TYPE_TGZ;
227 	var folder = appCtxt.getById(folderId);
228 	if (!isZimbra && folder && folder.nId == ZmOrganizer.ID_ROOT) {
229 		var params = {
230 			msg:	ZmMsg.exportErrorRootNotAllowed,
231 			level:	ZmStatusView.LEVEL_CRITICAL
232 		};
233 		appCtxt.setStatusMsg(params);
234 		return false;
235 	}
236 
237 	// export
238 	return this._doExportData(params);
239 };
240 
241 //
242 // Protected methods
243 //
244 
245 /**
246  * @private
247  */
248 ZmImportExportController.prototype._doImportData =
249 function(params) {
250 	if (params.folderId == -1 && params.defaultType != ZmImportExportController.TYPE_TGZ) {
251 		return this._doImportSelectFolder(params);
252 	}
253 	return this._doImport(params);
254 };
255 
256 /**
257  * @private
258  */
259 ZmImportExportController.prototype._doImportSelectFolder =
260 function(params) {
261 	var dialog = appCtxt.getChooseFolderDialog();
262 	dialog.reset();
263 	dialog.setTitle(ZmMsg._import);
264 	dialog.registerCallback(DwtDialog.OK_BUTTON, this._doImportSelectFolderDone, this, [params]);
265 
266 	var overviewId = dialog.getOverviewId(ZmSetting.IMPORT);
267 	var omit = {};
268 	omit[ZmFolder.ID_TRASH] = true;
269 	var type = params.defaultType;
270 	if (type == ZmImportExportController.TYPE_CSV) {
271 		AjxDispatcher.require(["ContactsCore", "Contacts"]);
272 		var noNew = !appCtxt.get(ZmSetting.NEW_ADDR_BOOK_ENABLED);
273 		dialog.popup({
274 			treeIds:		[ZmOrganizer.ADDRBOOK],
275 			title:			ZmMsg.chooseAddrBook,
276 			overviewId:		overviewId,
277 			description:	ZmMsg.chooseAddrBookToImport,
278 			skipReadOnly:	true,
279 			hideNewButton:	noNew,
280 			omit:			omit,
281 			appName:		ZmApp.CONTACTS
282 		});
283 	}
284 	else if (type == ZmImportExportController.TYPE_ICS) {
285 		AjxDispatcher.require(["MailCore", "CalendarCore", "Calendar"]);
286 		dialog.popup({
287 			treeIds: [ZmOrganizer.CALENDAR],
288 			title: ZmMsg.chooseCalendar,
289 			overviewId: overviewId,
290 			description: ZmMsg.chooseCalendarToImport,
291 			skipReadOnly: true,
292 			omit:omit,
293 			appName:ZmApp.CALENDAR
294 		});
295 	}
296 };
297 
298 /**
299  * @private
300  */
301 ZmImportExportController.prototype._doImportSelectFolderDone =
302 function(params, organizer) {
303 	params.folderId = organizer.id;
304 	this._doImport(params);
305 };
306 
307 /**
308  * @private
309  */
310 ZmImportExportController.prototype._doImport =
311 function(params) {
312 	// create custom callback function for this import request
313 	var funcName = "ZmImportExportController__callback__"+Dwt.getNextId("import");
314 	window[funcName] = AjxCallback.simpleClosure(this._handleImportResponse, this, funcName, params);
315 
316 	// generate request url
317 	var folder = params.folderId && appCtxt.getById(params.folderId);
318 	if (folder && folder.nId == ZmOrganizer.ID_ROOT) folder = null;
319 	var path = folder ? folder.getPath(null, null, null, true, true) : "";
320 	var type = params.type || params.ext;
321 	var url = [
322 		"/home/",
323 		encodeURIComponent(appCtxt.get(ZmSetting.USERNAME)),
324 		"/",
325 		encodeURIComponent(path),
326 		"?",
327 		type ? "fmt="+encodeURIComponent(type) : "",
328 		params.views ? "&types="+encodeURIComponent(params.views) : "",
329 		params.resolve ? "&resolve="+encodeURIComponent(params.resolve) : "",
330 		"&callback="+funcName,
331 		"&charset="+appCtxt.getCharset()
332 	].join("");
333 
334 	// initialize form
335 	var form = params.form;
336 	form.action = url;
337 	form.method = "POST";
338 	form.enctype = "multipart/form-data";
339 
340 	this._setCsrfTokenInput(form);
341 
342 	// destination iframe
343 	var onload = null;
344 	var onerror = AjxCallback.simpleClosure(this._importError, this, params.errorCallback);
345 	params.iframe = ZmImportExportController.__createIframe(form, onload, onerror);
346 
347 	// import
348 	form.submit();
349 	return true;
350 };
351 
352 /**
353  * lazily generate the hidden csrf token input field for the form.
354  */
355 ZmImportExportController.prototype._setCsrfTokenInput =
356 function(form) {
357 	var csrfTokenInput = document.getElementById(ZmImportExportController.CSRF_TOKEN_HIDDEN_INPUT_ID);
358 	if (csrfTokenInput) {
359 		return;
360 	}
361 	csrfTokenInput = document.createElement("input");
362 	csrfTokenInput.type  = "hidden";
363 	csrfTokenInput.name  = "csrfToken";
364 	csrfTokenInput.id    = ZmImportExportController.CSRF_TOKEN_HIDDEN_INPUT_ID;
365 	csrfTokenInput.value = window.csrfToken;
366 
367 	var firstChildEl = form.firstChild;
368 	form.insertBefore(csrfTokenInput, firstChildEl);
369 };
370 
371 /**
372  * @private
373  */
374 ZmImportExportController.prototype._handleImportResponse =
375 function(funcName, params, type, fault1 /* , ... , faultN */) {
376 	// gather error/warning messages
377 	var messages = [];
378 	if (fault1) {
379 		for (var j = 3; j < arguments.length; j++) {
380 			var fault = arguments[j];
381 			var code = fault.Detail.Error.Code;
382 			var message = fault.Reason.Text;
383 			var args = ZmImportExportController.__faultArgs(fault.Detail.Error.a);
384 			if (code == "formatter.UNKNOWN_ERROR") {
385 				args.path = args.path || ["(",ZmMsg.unknown,")"].join("");
386 				args.message = message;
387 			}
388 			var mappings = ZmImportExportController.__FAULT_ARGS_MAPPING[code];
389 			var formatArgs = new Array(mappings ? mappings.length : 0);
390 			for (var i = 0; i < formatArgs.length; i++) {
391 				formatArgs[i] = args[mappings[i]];
392 			}
393 			var errorMsg = ZMsg[code] ? AjxMessageFormat.format(ZMsg[code], formatArgs) : "";
394 			// be a little more verbose if there was a failure
395 			if (type == "fail") {
396 				errorMsg = message ? errorMsg + '<br><br>' + AjxStringUtil.htmlEncode(message) : errorMsg;
397 			}
398 			else if (type == "warn") {
399 				errorMsg = errorMsg || message;
400 			}
401 			messages.push(errorMsg);
402 		}
403 	}
404 	// show success or failure
405 	if (type == "fail") {
406 		this._importError(params.errorCallback, messages[0]);
407 	}
408 	else if (type == "warn") {
409 		this._importWarnings(params.callback, messages);
410 	}
411 	else {
412 		this._importSuccess(params.callback);
413 		appCtxt.getAppController().sendNoOp(); //send no-op to refresh
414 	}
415 
416 	// cleanup
417 	try {
418 		delete window[funcName]; // IE fails on this one (bug #57952)
419 	} catch (e) {
420 		if (window[funcName]) {
421 			window[funcName] = undefined;
422 		}
423 	}
424 	var iframe = params.iframe;
425 	setTimeout(function() { // Right now we are actually in the iframe's onload handler, so we defer killing the iframe until we're out of it
426 		iframe.parentNode.removeChild(iframe);
427 	}, 0);
428 };
429 
430 /**
431  * @private
432  */
433 ZmImportExportController.__faultArgs =
434 function(array) {
435 	var args = {};
436 	for (var i = 0; array && i < array.length; i++) {
437 		args[array[i].n] = array[i]._content;
438 	}
439 	return args;
440 };
441 
442 /**
443  * @private
444  */
445 ZmImportExportController.prototype._confirmImportReset =
446 function(params) {
447 	this._cancelImportReset();
448 	this._doImportData(params);
449 };
450 
451 /**
452  * @private
453  */
454 ZmImportExportController.prototype._cancelImportReset =
455 function() {
456 	var dialog = appCtxt.getOkCancelMsgDialog();
457 	dialog.reset();
458 	dialog.popdown();
459 };
460 
461 /**
462  * @private
463  */
464 ZmImportExportController.prototype._doExportData =
465 function(params) {
466 	// create custom error callback function for this export request
467 	var funcName = "exportErrorCallback__" + Dwt.getNextId("export");
468 	ZmImportExportController[funcName] = this._handleExportErrorResponse.bind(this, funcName, params);
469 
470 	var type = params.type;
471 	var isTGZ = type == ZmImportExportController.TYPE_TGZ;
472 	var isCSV = type == ZmImportExportController.TYPE_CSV;
473 	var subType = params.subType || ZmImportExportController.SUBTYPE_DEFAULT[type];
474 
475 	var folder = params.folderId && appCtxt.getById(params.folderId);
476 	if (folder && folder.nId == ZmOrganizer.ID_ROOT) folder = null;
477 	var path = folder ? folder.getPath(null, null, null, true, true) : "";
478 
479 	// generate request URL
480 	var url = [
481 		"/home/",
482 		encodeURIComponent(appCtxt.get(ZmSetting.USERNAME)),
483 		"/",
484 		encodeURIComponent(path)
485 	].join("");
486 
487 	var formParams = { "fmt" : type };
488 	if (isCSV) {
489         formParams[type+"fmt"] = subType;
490     }
491 	var startDate = params.start ? AjxDateUtil.simpleParseDateStr(params.start) : null;
492 	var endDate = params.end ? AjxDateUtil.simpleParseDateStr(params.end) : null;
493 	if (isTGZ && params.views) { formParams["types"] = params.views; }
494     if (type == ZmImportExportController.TYPE_ICS) {
495         formParams["icalAttach"] = "inline";
496     }
497     if(startDate) {
498         formParams["start"] = startDate.getTime();
499     }
500     if(endDate) {
501         endDate = AjxDateUtil.roll(endDate, AjxDateUtil.DAY, 1);
502         formParams["end"] = endDate.getTime();
503     }
504 	if (isTGZ && params.searchFilter) { formParams["query"] = params.searchFilter; }
505 	if (params.skipMeta) { formParams["meta"] = "0"; }
506 	if (params.filename) { formParams["filename"] = params.filename; }
507 	formParams.emptyname = ZmMsg.exportEmptyName;
508     formParams["charset"] = (subType === "windows-live-mail-csv" || subType === "thunderbird-csv") ? "UTF-8" : appCtxt.getCharset();
509 	formParams["callback"] = "ZmImportExportController." + funcName;
510 
511 	// initialize form
512 	var form = ZmImportExportController.__createForm(url, formParams);
513 
514 	// destination form
515 	var onload = null;
516 	var onerror = this._exportError.bind(this, params.errorCallback);
517 	params.iframe = ZmImportExportController.__createIframe(form, onload, onerror);
518 	params.form = form;
519 	// export
520 	form.submit();
521 };
522 
523 /**
524  * @private
525  */
526 ZmImportExportController.prototype._importSuccess =
527 function(callback) {
528 	if (callback) {
529 		callback.run(true);
530 	}
531 	ZmImportExportController.__showMessage(ZmMsg.importSuccess, DwtMessageDialog.INFO_STYLE);
532 	return true;
533 };
534 
535 /**
536  * @private
537  */
538 ZmImportExportController.prototype._importWarnings =
539 function(callback, messages) {
540 	if (callback) {
541 		callback.run(false);
542 	}
543 	// remove duplicates
544 	var msgmap = {};
545 	for (var i = 0; i < messages.length; i++) {
546 		msgmap[messages[i]] = true;
547 	}
548 	messages = AjxUtil.map(AjxUtil.keys(msgmap), AjxStringUtil.htmlEncode);
549 	if (messages.length > 5) {
550 		var count = messages.length - 5;
551 		messages.splice(5, count, AjxMessageFormat.format(ZmMsg.importAdditionalWarnings, [count]));
552 	}
553 	// show warnings
554 	var msglist = [];
555 	for (var i = 0; i < messages.length; i++) {
556 		msglist.push("<li>", messages[i]);
557 	}
558 	var msg = AjxMessageFormat.format(ZmMsg.importSuccessWithWarnings, [ messages.length, msglist.join("") ]);
559 	ZmImportExportController.__showMessage(msg, DwtMessageDialog.WARNING_STYLE);
560 	return true;
561 };
562 
563 /**
564  * @private
565  */
566 ZmImportExportController.prototype._importError =
567 function(errorCallback, message) {
568 	if (errorCallback) {
569 		errorCallback.run(false);
570 	}
571 	var msg = message || ZmMsg.importFailed;
572 	ZmImportExportController.__showMessage(msg, DwtMessageDialog.CRITICAL_STYLE);
573 	return true;
574 };
575 
576 /**
577  * @private
578  */
579 ZmImportExportController.prototype._handleExportErrorResponse =
580 function(funcName, params, type, fault1 /* , ... , faultN */) {
581 	// gather error messages
582 	var messages = [];
583 	if (fault1) {
584 		for (var j = 3; j < arguments.length; j++) {
585 			var fault = arguments[j];
586 			var code = fault.Detail.Error.Code;
587 			var message = fault.Reason.Text;
588 			var args = ZmImportExportController.__faultArgs(fault.Detail.Error.a);
589 			if (code == "mail.EXPORT_PERIOD_NOT_SPECIFIED" || code == "mail.EXPORT_PERIOD_TOO_LONG") {
590 				message = "";
591 			} else if (code == "formatter.UNKNOWN_ERROR") {
592 				args.path = args.path || ["(",ZmMsg.unknown,")"].join("");
593 				args.message = message;
594 			}
595 			var mappings = ZmImportExportController.__FAULT_ARGS_MAPPING[code];
596 			var formatArgs = new Array(mappings ? mappings.length : 0);
597 			for (var i = 0; i < formatArgs.length; i++) {
598 				formatArgs[i] = args[mappings[i]];
599 			}
600 			var errorMsg = ZMsg[code] ? AjxMessageFormat.format(ZMsg[code], formatArgs) : "";
601 			if (type == "fail") {
602 				errorMsg = message ? errorMsg + '<br><br>' + AjxStringUtil.htmlEncode(message) : errorMsg;
603 			}
604 			messages.push(errorMsg);
605 		}
606 	}
607 	if (type == "fail") {
608 		this._exportError(params.errorCallback, messages[0]);
609 	}
610 	// cleanup
611 	try {
612 		delete ZmImportExportController[funcName]; // IE fails on this one (bug #57952)
613 	} catch (e) {
614 		if (ZmImportExportController[funcName]) {
615 			ZmImportExportController[funcName] = undefined;
616 		}
617 	}
618 	var iframe = params.iframe;
619 	var form = params.form;
620 	setTimeout(function() { // Right now we are actually in the iframe's onload handler, so we defer killing the iframe until we're out of it
621 		iframe.parentNode.removeChild(iframe);
622 		form.parentNode.removeChild(form);
623 	}, 0);
624 };
625 
626 /**
627  * @private
628  */
629 ZmImportExportController.prototype._exportError =
630 function(errorCallback, message) {
631 	if (errorCallback) {
632 		errorCallback.run(false);
633 	}
634 	var msg = message || ZmMsg.exportFailed;
635 	ZmImportExportController.__showMessage(msg, DwtMessageDialog.CRITICAL_STYLE);
636 	return true;
637 };
638 
639 //
640 // Private methods
641 //
642 
643 /**
644  * @private
645  */
646 ZmImportExportController.__showMessage =
647 function(msg, level) {
648 	var dialog = appCtxt.getErrorDialog();
649 	dialog.setMessage(msg, null, level);
650 	dialog.popup(null, true);
651 };
652 
653 /**
654  * @private
655  */
656 ZmImportExportController.__createForm =
657 function(action, params, method) {
658 	var form = document.createElement("FORM");
659 	form.action = action;
660 	form.method = method || "GET";
661 	for (var name in params) {
662 		var value = params[name];
663 		if (!value) continue;
664 		var input = document.createElement("INPUT");
665 		input.type = "hidden";
666 		input.name = name;
667 		input.value = value;
668 		form.appendChild(input);
669 	}
670 	form.style.display = "none";
671 	document.body.appendChild(form);
672 	return form;
673 };
674 
675 /**
676  * @private
677  */
678 ZmImportExportController.__createIframe =
679 function(form, onload, onerror) {
680 	var id = Dwt.getNextId() + "_iframe";
681 	var iframe;
682 	if (AjxEnv.isIE) {
683         try {
684             // NOTE: This has to be done because IE doesn't recognize the name
685             //       attribute if set programmatically. And without that, the
686             //       form target will cause it to return in a new window which
687             //       breaks the callback.
688             var html = [ "<IFRAME id='",id,"' name='",id,"'>" ].join("");
689             iframe = document.createElement(html);
690         } catch (e) {
691             // Unless its IE9+ in non-quirks mode, then the above throws an exception
692             iframe = document.createElement("IFRAME");
693             iframe.name = iframe.id = id;
694         }
695 	}
696 	else {
697 		iframe = document.createElement("IFRAME");
698 		iframe.name = iframe.id = id;
699 	}
700 	// NOTE: Event handlers won't be called when iframe hidden.
701 //	iframe.style.display = "none";
702 	iframe.style.position = "absolute";
703 	iframe.style.width = 1;
704 	iframe.style.height = 1;
705 	iframe.style.top = 10;
706 	iframe.style.left = -10;
707 	document.body.appendChild(iframe);
708 	form.target = iframe.name;
709 
710 	iframe.onload = onload;
711 	iframe.onerror = onerror;
712 
713 	return iframe;
714 };
715