1 /* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 * ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 *
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
9 *
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
13 * License.
14 *
15 * The Original Code is Mozilla Calendar code.
16 *
17 * The Initial Developer of the Original Code is
18 * ArentJan Banck <ajbanck@planet.nl>.
19 * Portions created by the Initial Developer are Copyright (C) 2002
20 * the Initial Developer. All Rights Reserved.
21 *
22 * Contributor(s): ArentJan Banck <ajbanck@planet.nl>
23 * Steve Hampton <mvgrad78@yahoo.com>
24 * Eric Belhaire <belhaire@ief.u-psud.fr>
25 * Jussi Kukkonen <jussi.kukkonen@welho.com>
26 * Michiel van Leeuwen <mvl@exedo.nl>
27 * Stefan Sitter <ssitter@gmail.com>
28 * Philipp Kewisch <mozilla@kewis.ch>
29 *
30 * Alternatively, the contents of this file may be used under the terms of
31 * either the GNU General Public License Version 2 or later (the "GPL"), or
32 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
33 * in which case the provisions of the GPL or the LGPL are applicable instead
34 * of those above. If you wish to allow use of your version of this file only
35 * under the terms of either the GPL or the LGPL, and not to allow others to
36 * use your version of this file under the terms of the MPL, indicate your
37 * decision by deleting the provisions above and replace them with the notice
38 * and other provisions required by the GPL or the LGPL. If you do not delete
39 * the provisions above, a recipient may use your version of this file under
40 * the terms of any one of the MPL, the GPL or the LGPL.
41 *
42 * ***** END LICENSE BLOCK ***** */
43
44 // File constants copied from file-utils.js
45 const MODE_RDONLY = 0x01;
46 const MODE_WRONLY = 0x02;
47 const MODE_RDWR = 0x04;
48 const MODE_CREATE = 0x08;
49 const MODE_APPEND = 0x10;
50 const MODE_TRUNCATE = 0x20;
51 const MODE_SYNC = 0x40;
52 const MODE_EXCL = 0x80;
53
54 /**
55 * loadEventsFromFile
56 * shows a file dialog, reads the selected file(s) and tries to parse events from it.
57 *
58 * @param aCalendar (optional) If specified, the items will be imported directly
59 * into the calendar
60 */
loadEventsFromFile
61 function loadEventsFromFile(aCalendar)
62 {
63 const nsIFilePicker = Components.interfaces.nsIFilePicker;
64
65 var fp = Components.classes["@mozilla.org/filepicker;1"]
66 .createInstance(nsIFilePicker);
67 fp.init(window, calGetString("calendar", "filepickerTitleImport"), nsIFilePicker.modeOpen);
68 fp.defaultExtension = "ics";
69
70 // Get a list of exporters
71 var contractids = new Array();
72 var catman = Components.classes["@mozilla.org/categorymanager;1"]
73 .getService(Components.interfaces.nsICategoryManager);
74 var catenum = catman.enumerateCategory('cal-importers');
75 while (catenum.hasMoreElements()) {
76 var entry = catenum.getNext();
77 entry = entry.QueryInterface(Components.interfaces.nsISupportsCString);
78 var contractid = catman.getCategoryEntry('cal-importers', entry);
79 var exporter = Components.classes[contractid]
80 .getService(Components.interfaces.calIImporter);
81 var types = exporter.getFileTypes({});
82 var type;
83 for each (type in types) {
84 fp.appendFilter(type.description, type.extensionFilter);
85 contractids.push(contractid);
86 }
87 }
88
89 fp.show();
90
91 if (fp.file && fp.file.path && fp.file.path.length > 0) {
92 var filePath = fp.file.path;
93 var importer = Components.classes[contractids[fp.filterIndex]]
94 .getService(Components.interfaces.calIImporter);
95
96 const nsIFileInputStream = Components.interfaces.nsIFileInputStream;
97 const nsIScriptableInputStream = Components.interfaces.nsIScriptableInputStream;
98
99 var inputStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
100 .createInstance(nsIFileInputStream);
101 var items = [];
102
103 try {
104 inputStream.init( fp.file, MODE_RDONLY, 0444, {});
105 items = importer.importFromStream(inputStream, {});
106 } catch(ex) {
107 switch (ex.result) {
108 case Components.interfaces.calIErrors.INVALID_TIMEZONE:
109 showError(calGetString("calendar", "timezoneError", [filePath] , 1));
110 break;
111 default:
112 showError(calGetString("calendar", "unableToRead") + filePath + "\n"+ ex);
113 }
114 } finally {
115 inputStream.close();
116 }
117
118 if (aCalendar) {
119 putItemsIntoCal(aCalendar, items);
120 return;
121 }
122
123 var calendars = getCalendarManager().getCalendars({});
124 calendars = calendars.filter(isCalendarWritable);
125
126 if (calendars.length < 1) {
127 // XXX alert something?
128 return;
129 } else if (calendars.length == 1) {
130 // There's only one calendar, so it's silly to ask what calendar
131 // the user wants to import into.
132 putItemsIntoCal(calendars[0], items, filePath);
133 } else {
134 // Ask what calendar to import into
135 var args = new Object();
136 args.onOk = function putItems(aCal) { putItemsIntoCal(aCal, items, filePath); };
137 args.calendars = calendars;
138 args.promptText = calGetString("calendar", "importPrompt");
139 openDialog("chrome://calendar/content/chooseCalendarDialog.xul",
140 "_blank", "chrome,titlebar,modal,resizable", args);
141 }
142 }
143 }
144
putItemsIntoCal
145 function putItemsIntoCal(destCal, aItems, aFilePath) {
146 // Set batch for the undo/redo transaction manager
147 startBatchTransaction();
148
149 // And set batch mode on the calendar, to tell the views to not
150 // redraw until all items are imported
151 destCal.startBatch();
152
153 // This listener is needed to find out when the last addItem really
154 // finished. Using a counter to find the last item (which might not
155 // be the last item added)
156 var count = 0;
157 var failedCount = 0;
158 var duplicateCount = 0;
159 // Used to store the last error. Only the last error, because we don't
160 // wan't to bomb the user with thousands of error messages in case
161 // something went really wrong.
162 // (example of something very wrong: importing the same file twice.
163 // quite easy to trigger, so we really should do this)
164 var lastError;
165 var listener = {
166 onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
167 count++;
168 if (!Components.isSuccessCode(aStatus)) {
169 if (aStatus == Components.interfaces.calIErrors.DUPLICATE_ID) {
170 duplicateCount++;
171 } else {
172 failedCount++;
173 lastError = aStatus;
174 }
175 }
176 // See if it is time to end the calendar's batch.
177 if (count == aItems.length) {
178 destCal.endBatch();
179 if (!failedCount && duplicateCount) {
180 showError(calGetString("calendar", "duplicateError", [duplicateCount, aFilePath] , 2));
181 } else if (failedCount) {
182 showError(calGetString("calendar", "importItemsFailed", [failedCount, lastError.toString()] , 2));
183 }
184 }
185 }
186 }
187
188 for each (item in aItems) {
189 // XXX prompt when finding a duplicate.
190 try {
191 destCal.addItem(item, listener);
192 } catch(e) {
193 failedCount++;
194 lastError = e;
195 // Call the listener's operationComplete, to increase the
196 // counter and not miss failed items. Otherwise, endBatch might
197 // never be called.
198 listener.onOperationComplete(null, null, null, null, null);
199 Components.utils.reportError("Import error: "+e);
200 }
201 }
202
203 // End transmgr batch
204 endBatchTransaction();
205 }
206
207 /**
208 * saveEventsToFile
209 *
210 * Save data to a file. Create the file or overwrite an existing file.
211 *
212 * @param calendarEventArray (required) Array of calendar events that should
213 * be saved to file.
214 * @param aDefaultFileName (optional) Initial filename shown in SaveAs dialog.
215 */
216
saveEventsToFile
217 function saveEventsToFile(calendarEventArray, aDefaultFileName)
218 {
219 if (!calendarEventArray)
220 return;
221
222 // Show the 'Save As' dialog and ask for a filename to save to
223 const nsIFilePicker = Components.interfaces.nsIFilePicker;
224
225 var fp = Components.classes["@mozilla.org/filepicker;1"]
226 .createInstance(nsIFilePicker);
227
228 fp.init(window, calGetString("calendar", "filepickerTitleExport"), nsIFilePicker.modeSave);
229
230 if (aDefaultFileName && aDefaultFileName.length && aDefaultFileName.length > 0) {
231 fp.defaultString = aDefaultFileName;
232 } else if (calendarEventArray.length == 1 && calendarEventArray[0].title) {
233 fp.defaultString = calendarEventArray[0].title;
234 } else {
235 fp.defaultString = calGetString("calendar", "defaultFileName");
236 }
237
238 fp.defaultExtension = "ics";
239
240 // Get a list of exporters
241 var contractids = new Array();
242 var catman = Components.classes["@mozilla.org/categorymanager;1"]
243 .getService(Components.interfaces.nsICategoryManager);
244 var catenum = catman.enumerateCategory('cal-exporters');
245 while (catenum.hasMoreElements()) {
246 var entry = catenum.getNext();
247 entry = entry.QueryInterface(Components.interfaces.nsISupportsCString);
248 var contractid = catman.getCategoryEntry('cal-exporters', entry);
249 var exporter = Components.classes[contractid]
250 .getService(Components.interfaces.calIExporter);
251 var types = exporter.getFileTypes({});
252 var type;
253 for each (type in types) {
254 fp.appendFilter(type.description, type.extensionFilter);
255 contractids.push(contractid);
256 }
257 }
258
259
260 fp.show();
261
262 // Now find out as what to save, convert the events and save to file.
263 if (fp.file && fp.file.path.length > 0)
264 {
265 const UTF8 = "UTF-8";
266 var aDataStream;
267 var extension;
268 var charset;
269
270 var exporter = Components.classes[contractids[fp.filterIndex]]
271 .getService(Components.interfaces.calIExporter);
272
273 var filePath = fp.file.path;
274 if(filePath.indexOf(".") == -1 )
275 filePath += "."+exporter.getFileTypes({})[0].defaultExtension;
276
277 const LOCALFILE_CTRID = "@mozilla.org/file/local;1";
278 const FILEOUT_CTRID = "@mozilla.org/network/file-output-stream;1";
279 const nsILocalFile = Components.interfaces.nsILocalFile;
280 const nsIFileOutputStream = Components.interfaces.nsIFileOutputStream;
281
282 var outputStream;
283
284 var localFileInstance = Components.classes[LOCALFILE_CTRID]
285 .createInstance(nsILocalFile);
286 localFileInstance.initWithPath(filePath);
287
288 outputStream = Components.classes[FILEOUT_CTRID]
289 .createInstance(nsIFileOutputStream);
290 try
291 {
292 outputStream.init(localFileInstance, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, 0664, 0);
293 // XXX Do the right thing with unicode and stuff. Or, again, should the
294 // exporter handle that?
295 exporter.exportToStream(outputStream, calendarEventArray.length, calendarEventArray, null);
296 outputStream.close();
297 }
298 catch(ex)
299 {
300 showError(calGetString("calendar", "unableToWrite") + filePath);
301 }
302 }
303 }
304
305 /* Exports all the events and tasks in a calendar. If aCalendar is not specified,
306 * the user will be prompted with a list of calendars to choose which one to export.
307 */
exportEntireCalendar
308 function exportEntireCalendar(aCalendar) {
309 var itemArray = [];
310 var getListener = {
311 onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail)
312 {
313 saveEventsToFile(itemArray, aCalendar.name);
314 },
315 onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems)
316 {
317 for each (item in aItems) {
318 itemArray.push(item);
319 }
320 }
321 };
322
323 function getItemsFromCal(aCal) {
324 aCal.getItems(Components.interfaces.calICalendar.ITEM_FILTER_ALL_ITEMS,
325 0, null, null, getListener);
326 }
327
328 if (!aCalendar) {
329 var count = new Object();
330 var calendars = getCalendarManager().getCalendars(count);
331
332 if (count.value == 1) {
333 // There's only one calendar, so it's silly to ask what calendar
334 // the user wants to import into.
335 getItemsFromCal(calendars[0]);
336 } else {
337 // Ask what calendar to import into
338 var args = new Object();
339 args.onOk = getItemsFromCal;
340 args.promptText = calGetString("calendar", "exportPrompt");
341 openDialog("chrome://calendar/content/chooseCalendarDialog.xul",
342 "_blank", "chrome,titlebar,modal,resizable", args);
343 }
344 } else {
345 getItemsFromCal(aCalendar);
346 }
347 }