1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 *
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
8 *
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
12 * License.
13 *
14 * The Original Code is mozilla calendar code.
15 *
16 * The Initial Developer of the Original Code is
17 * Michiel van Leeuwen <mvl@exedo.nl>
18 * Portions created by the Initial Developer are Copyright (C) 2004
19 * the Initial Developer. All Rights Reserved.
20 *
21 * Contributor(s):
22 * Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
23 * Dan Mosedale <dan.mosedale@oracle.com>
24 * Joey Minta <jminta@gmail.com>
25 * Philipp Kewisch <mozilla@kewis.ch>
26 *
27 * Alternatively, the contents of this file may be used under the terms of
28 * either the GNU General Public License Version 2 or later (the "GPL"), or
29 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
30 * in which case the provisions of the GPL or the LGPL are applicable instead
31 * of those above. If you wish to allow use of your version of this file only
32 * under the terms of either the GPL or the LGPL, and not to allow others to
33 * use your version of this file under the terms of the MPL, indicate your
34 * decision by deleting the provisions above and replace them with the notice
35 * and other provisions required by the GPL or the LGPL. If you do not delete
36 * the provisions above, a recipient may use your version of this file under
37 * the terms of any one of the MPL, the GPL or the LGPL.
38 *
39 * ***** END LICENSE BLOCK ***** */
40
41 //
42 // calICSCalendar.js
43 //
44 // This is a non-sync ics file. It reads the file pointer to by uri when set,
45 // then writes it on updates. External changes to the file will be
46 // ignored and overwritten.
47 //
48 // XXX Should do locks, so that external changes are not overwritten.
49
50 const CI = Components.interfaces;
51 const calIOperationListener = Components.interfaces.calIOperationListener;
52 const calICalendar = Components.interfaces.calICalendar;
53 const calIErrors = Components.interfaces.calIErrors;
54
55 var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].
56 getService(Components.interfaces.nsIXULAppInfo);
57 var isOnBranch = appInfo.platformVersion.indexOf("1.8") == 0;
58
59 function calICSCalendar() {
60 this.initProviderBase();
61 this.initICSCalendar();
62
63 this.unmappedComponents = [];
64 this.unmappedProperties = [];
65 this.queue = new Array();
66 }
67
68 calICSCalendar.prototype = {
69 __proto__: calProviderBase.prototype,
70
71 mObserver: null,
72 locked: false,
73
74 QueryInterface: function (aIID) {
75 return doQueryInterface(this, calICSCalendar.prototype, aIID,
76 [Components.interfaces.calICalendarProvider,
77 Components.interfaces.nsIStreamListener,
78 Components.interfaces.nsIStreamLoaderObserver,
79 Components.interfaces.nsIInterfaceRequestor]);
80 },
81
82 initICSCalendar: function() {
83 this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
84 .createInstance(Components.interfaces.calICalendar);
85
86 this.mMemoryCalendar.superCalendar = this;
87 this.mObserver = new calICSObserver(this);
88 this.mMemoryCalendar.addObserver(this.mObserver); // XXX Not removed
89 },
90
91 //
92 // calICalendarProvider interface
93 //
94 get prefChromeOverlay() {
95 return null;
96 },
97
98 get displayName() {
99 return calGetString("calendar", "icsName");
100 },
101
102 createCalendar: function ics_createCal() {
103 throw NS_ERROR_NOT_IMPLEMENTED;
104 },
105
106 deleteCalendar: function ics_deleteCal(cal, listener) {
107 throw NS_ERROR_NOT_IMPLEMENTED;
108 },
109
110 //
111 // calICalendar interface
112 //
113 get type() { return "ics"; },
114
115 get canRefresh() {
116 return true;
117 },
118
119 get uri() { return this.mUri },
120 set uri(aUri) {
121 this.mUri = aUri;
122 this.mMemoryCalendar.uri = this.mUri;
123
124 // Use the ioservice, to create a channel, which makes finding the
125 // right hooks to use easier.
126 var ioService = Components.classes["@mozilla.org/network/io-service;1"]
127 .getService(Components.interfaces.nsIIOService);
128 var channel = ioService.newChannelFromURI(this.mUri);
129
130 if (channel instanceof Components.interfaces.nsIHttpChannel) {
131 this.mHooks = new httpHooks();
132 } else {
133 this.mHooks = new dummyHooks();
134 }
135
136 this.refresh();
137 },
138
139 getProperty: function calICSCalendar_getProperty(aName) {
140 switch (aName) {
141 case "requiresNetwork":
142 return (!this.uri.schemeIs("file"));
143 }
144 return this.__proto__.__proto__.getProperty.apply(this, arguments);
145 },
146
147 refresh: function calICSCalendar_refresh() {
148 this.queue.push({action: 'refresh'});
149 this.processQueue();
150 },
151
152 doRefresh: function calICSCalendar_doRefresh() {
153 var ioService = Components.classes["@mozilla.org/network/io-service;1"]
154 .getService(Components.interfaces.nsIIOService);
155
156 var channel = ioService.newChannelFromURI(this.mUri);
157 channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
158 channel.notificationCallbacks = this;
159
160 // Allow the hook to do its work, like a performing a quick check to
161 // see if the remote file really changed. Might save a lot of time
162 this.mHooks.onBeforeGet(channel);
163
164 var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
165 .createInstance(Components.interfaces.nsIStreamLoader);
166
167 // Lock other changes to the item list.
168 this.lock();
169
170 try {
171 if (isOnBranch) {
172 streamLoader.init(channel, this, this);
173 } else {
174 streamLoader.init(this);
175 channel.asyncOpen(streamLoader, this);
176 }
177 } catch(e) {
178 // File not found: a new calendar. No problem.
179 this.unlock();
180 }
181 },
182
183 calendarPromotedProps: {
184 "PRODID": true,
185 "VERSION": true
186 },
187
188 // nsIStreamLoaderObserver impl
189 // Listener for download. Parse the downloaded file
190
191 onStreamComplete: function(loader, ctxt, status, resultLength, result)
192 {
193 // No need to do anything if there was no result
194 if (!resultLength) {
195 this.unlock();
196 return;
197 }
198
199 // Allow the hook to get needed data (like an etag) of the channel
200 var cont = this.mHooks.onAfterGet();
201 if (!cont) {
202 this.unlock();
203 return;
204 }
205
206 // This conversion is needed, because the stream only knows about
207 // byte arrays, not about strings or encodings. The array of bytes
208 // need to be interpreted as utf8 and put into a javascript string.
209 var unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
210 .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
211 // ics files are always utf8
212 unicodeConverter.charset = "UTF-8";
213 var str;
214 try {
215 str = unicodeConverter.convertFromByteArray(result, result.length);
216 } catch(e) {
217 this.mObserver.onError(calIErrors.CAL_UTF8_DECODING_FAILED, e.toString());
218 this.unlock();
219 return;
220 }
221
222 // Create a new calendar, to get rid of all the old events
223 // Don't forget to remove the observer
224 this.mMemoryCalendar.removeObserver(this.mObserver);
225 this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
226 .createInstance(Components.interfaces.calICalendar);
227 this.mMemoryCalendar.uri = this.mUri;
228 this.mMemoryCalendar.superCalendar = this;
229
230 this.mObserver.onStartBatch();
231
232 // Wrap parsing in a try block. Will ignore errors. That's a good thing
233 // for non-existing or empty files, but not good for invalid files.
234 // That's why we put them in readOnly mode
235 try {
236 var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"].
237 createInstance(Components.interfaces.calIIcsParser);
238 parser.parseString(str, null);
239 var items = parser.getItems({});
240
241 for each (var item in items) {
242 this.mMemoryCalendar.adoptItem(item, null);
243 }
244 this.unmappedComponents = parser.getComponents({});
245 this.unmappedProperties = parser.getProperties({});
246 } catch(e) {
247 LOG("Parsing the file failed:"+e);
248 this.mObserver.onError(e.result, e.toString());
249 }
250 this.mObserver.onEndBatch();
251 this.mObserver.onLoad(this);
252
253 // Now that all items have been stuffed into the memory calendar
254 // we should add ourselves as observer. It is important that this
255 // happens *after* the calls to adoptItem in the above loop to prevent
256 // the views from being notified.
257 this.mMemoryCalendar.addObserver(this.mObserver);
258
259 this.unlock();
260 },
261
262 writeICS: function () {
263 this.lock();
264 try {
265 if (!this.mUri)
266 throw Components.results.NS_ERROR_FAILURE;
267 // makeBackup will call doWriteICS
268 this.makeBackup(this.doWriteICS);
269 } catch (exc) {
270 this.unlock();
271 throw exc;
272 }
273 },
274
275 doWriteICS: function () {
276 var savedthis = this;
277 var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
278 .getService(Components.interfaces.nsIAppStartup);
279 var listener =
280 {
281 serializer: null,
282 onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail)
283 {
284 var inLastWindowClosingSurvivalArea = false;
285 try {
286 // All events are returned. Now set up a channel and a
287 // streamloader to upload. onStopRequest will be called
288 // once the write has finished
289 var ioService = Components.classes
290 ["@mozilla.org/network/io-service;1"]
291 .getService(Components.interfaces.nsIIOService);
292 var channel = ioService.newChannelFromURI(savedthis.mUri);
293
294 // Allow the hook to add things to the channel, like a
295 // header that checks etags
296 savedthis.mHooks.onBeforePut(channel);
297
298 channel.notificationCallbacks = savedthis;
299 var uploadChannel = channel.QueryInterface(
300 Components.interfaces.nsIUploadChannel);
301
302 // Serialize
303 var icsStream = this.serializer.serializeToInputStream();
304
305 // Upload
306 uploadChannel.setUploadStream(icsStream,
307 "text/calendar", -1);
308
309 appStartup.enterLastWindowClosingSurvivalArea();
310 inLastWindowClosingSurvivalArea = true;
311 channel.asyncOpen(savedthis, savedthis);
312 } catch (ex) {
313 if (inLastWindowClosingSurvivalArea) {
314 appStartup.exitLastWindowClosingSurvivalArea();
315 }
316 savedthis.mObserver.onError(
317 ex.result, "The calendar could not be saved; there " +
318 "was a failure: 0x" + ex.result.toString(16));
319 savedthis.unlock();
320 }
321 },
322 onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems)
323 {
324 this.serializer.addItems(aItems, aCount);
325 }
326 };
327 listener.serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"].
328 createInstance(Components.interfaces.calIIcsSerializer);
329 for each (var comp in this.unmappedComponents) {
330 listener.serializer.addComponent(comp);
331 }
332 for each (var prop in this.unmappedProperties) {
333 listener.serializer.addProperty(prop);
334 }
335
336 // don't call this.getItems, because we are locked:
337 this.mMemoryCalendar.getItems(calICalendar.ITEM_FILTER_TYPE_ALL | calICalendar.ITEM_FILTER_COMPLETED_ALL,
338 0, null, null, listener);
339 },
340
341 // nsIStreamListener impl
342 // For after publishing. Do error checks here
343 onStartRequest: function(request, ctxt) {},
344 onDataAvailable: function(request, ctxt, inStream, sourceOffset, count) {
345 // All data must be consumed. For an upload channel, there is
346 // no meaningfull data. So it gets read and then ignored
347 var scriptableInputStream =
348 Components.classes["@mozilla.org/scriptableinputstream;1"]
349 .createInstance(Components.interfaces.nsIScriptableInputStream);
350 scriptableInputStream.init(inStream);
351 scriptableInputStream.read(-1);
352 },
353 onStopRequest: function(request, ctxt, status, errorMsg)
354 {
355 ctxt = ctxt.wrappedJSObject;
356 var channel;
357 try {
358 channel = request.QueryInterface(Components.interfaces.nsIHttpChannel);
359 LOG("calICSCalendar channel.requestSucceeded: " + channel.requestSucceeded);
360 } catch(e) {
361 }
362
363 if (channel && !channel.requestSucceeded) {
364 ctxt.mObserver.onError(channel.requestSucceeded,
365 "Publishing the calendar file failed\n" +
366 "Status code: "+channel.responseStatus+": "+channel.responseStatusText+"\n");
367 }
368
369 else if (!channel && !Components.isSuccessCode(request.status)) {
370 ctxt.mObserver.onError(request.status,
371 "Publishing the calendar file failed\n" +
372 "Status code: "+request.status.toString(16)+"\n");
373 }
374
375 // Allow the hook to grab data of the channel, like the new etag
376 ctxt.mHooks.onAfterPut(channel);
377
378 ctxt.unlock();
379 var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
380 .getService(Components.interfaces.nsIAppStartup);
381 appStartup.exitLastWindowClosingSurvivalArea();
382 },
383
384 // Always use the queue, just to reduce the amount of places where
385 // this.mMemoryCalendar.addItem() and friends are called. less
386 // copied code.
387 addItem: function (aItem, aListener) {
388 this.adoptItem(aItem.clone(), aListener);
389 },
390 adoptItem: function (aItem, aListener) {
391 if (this.readOnly)
392 throw Components.interfaces.calIErrors.CAL_IS_READONLY;
393 this.queue.push({action:'add', item:aItem, listener:aListener});
394 this.processQueue();
395 },
396
397 modifyItem: function (aNewItem, aOldItem, aListener) {
398 if (this.readOnly)
399 throw Components.interfaces.calIErrors.CAL_IS_READONLY;
400 this.queue.push({action:'modify', oldItem: aOldItem,
401 newItem: aNewItem, listener:aListener});
402 this.processQueue();
403 },
404
405 deleteItem: function (aItem, aListener) {
406 if (this.readOnly)
407 throw Components.interfaces.calIErrors.CAL_IS_READONLY;
408 this.queue.push({action:'delete', item:aItem, listener:aListener});
409 this.processQueue();
410 },
411
412 getItem: function (aId, aListener) {
413 this.queue.push({action:'get_item', id:aId, listener:aListener});
414 this.processQueue();
415 },
416
417 getItems: function (aItemFilter, aCount,
418 aRangeStart, aRangeEnd, aListener)
419 {
420 this.queue.push({action:'get_items',
421 itemFilter:aItemFilter, count:aCount,
422 rangeStart:aRangeStart, rangeEnd:aRangeEnd,
423 listener:aListener});
424 this.processQueue();
425 },
426
427 processQueue: function ()
428 {
429 if (this.isLocked())
430 return;
431 var a;
432 var writeICS = false;
433 var refreshAction = null;
434 while ((a = this.queue.shift())) {
435 switch (a.action) {
436 case 'add':
437 this.mMemoryCalendar.addItem(a.item, a.listener);
438 writeICS = true;
439 break;
440 case 'modify':
441 this.mMemoryCalendar.modifyItem(a.newItem, a.oldItem,
442 a.listener);
443 writeICS = true;
444 break;
445 case 'delete':
446 this.mMemoryCalendar.deleteItem(a.item, a.listener);
447 writeICS = true;
448 break;
449 case 'get_item':
450 this.mMemoryCalendar.getItem(a.id, a.listener);
451 break;
452 case 'get_items':
453 this.mMemoryCalendar.getItems(a.itemFilter, a.count,
454 a.rangeStart, a.rangeEnd,
455 a.listener);
456 break;
457 case 'refresh':
458 refreshAction = a;
459 break;
460 }
461 if (refreshAction) {
462 // break queue processing here and wait for refresh to finish
463 // before processing further operations
464 break;
465 }
466 }
467 if (writeICS) {
468 if (refreshAction) {
469 // reschedule the refresh for next round, after the file has been written;
470 // strictly we may not need to refresh once the file has been successfully
471 // written, but we don't know if that write will succeed.
472 this.queue.unshift(refreshAction);
473 }
474 this.writeICS();
475 }
476 else if (refreshAction) {
477 this.doRefresh();
478 }
479 },
480
481 lock: function () {
482 this.locked = true;
483 },
484
485 unlock: function () {
486 this.locked = false;
487 this.processQueue();
488 },
489
490 isLocked: function () {
491 return this.locked;
492 },
493
494 startBatch: function ()
495 {
496 this.mObserver.onStartBatch();
497 },
498 endBatch: function ()
499 {
500 this.mObserver.onEndBatch();
501 },
502
503 // nsIInterfaceRequestor impl
504 getInterface: function(iid, instance) {
505 if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
506 return new calAuthPrompt();
507 }
508 else if (iid.equals(Components.interfaces.nsIPrompt)) {
509 // use the window watcher service to get a nsIPrompt impl
510 return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
511 .getService(Components.interfaces.nsIWindowWatcher)
512 .getNewPrompter(null);
513 }
514 Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
515 return null;
516 },
517
518 /**
519 * Make a backup of the (remote) calendar
520 *
521 * This will download the remote file into the profile dir.
522 * It should be called before every upload, so every change can be
523 * restored. By default, it will keep 3 backups. It also keeps one
524 * file each day, for 3 days. That way, even if the user doesn't notice
525 * the remote calendar has become corrupted, he will still loose max 1
526 * day of work.
527 * After the back up is finished, will call aCallback.
528 *
529 * @param aCallback
530 * Function that will be calles after the backup is finished.
531 * will be called in the original context in which makeBackup
532 * was called
533 */
534 makeBackup: function(aCallback) {
535 // Uses |pseudoID|, an id of the calendar, defined below
536 function makeName(type) {
537 return 'calBackupData_'+pseudoID+'_'+type+'.ics';
538 }
539
540 // This is a bit messy. createUnique creates an empty file,
541 // but we don't use that file. All we want is a filename, to be used
542 // in the call to copyTo later. So we create a file, get the filename,
543 // and never use the file again, but write over it.
544 // Using createUnique anyway, because I don't feel like
545 // re-implementing it
546 function makeDailyFileName() {
547 var dailyBackupFile = backupDir.clone();
548 dailyBackupFile.append(makeName('day'));
549 dailyBackupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
550 dailyBackupFileName = dailyBackupFile.leafName;
551
552 // Remove the reference to the nsIFile, because we need to
553 // write over the file later, and you never know what happens
554 // if something still has a reference.
555 // Also makes it explicit that we don't need the file itself,
556 // just the name.
557 dailyBackupFile = null;
558
559 return dailyBackupFileName;
560 }
561
562 function purgeBackupsByType(files, type) {
563 // filter out backups of the type we care about.
564 var filteredFiles = files.filter(
565 function f(v) {
566 return (v.name.indexOf("calBackupData_"+pseudoID+"_"+type) != -1)
567 });
568 // Sort by lastmodifed
569 filteredFiles.sort(
570 function s(a,b) {
571 return (a.lastmodified - b.lastmodified);
572 });
573 // And delete the oldest files, and keep the desired number of
574 // old backups
575 var i;
576 for (i = 0; i < filteredFiles.length - numBackupFiles; ++i) {
577 file = backupDir.clone();
578 file.append(filteredFiles[i].name);
579
580 // This can fail because of some crappy code in nsILocalFile.
581 // That's not the end of the world. We can try to remove the
582 // file the next time around.
583 try {
584 file.remove(false);
585 } catch(ex) {}
586 }
587 return;
588 }
589
590 function purgeOldBackups() {
591 // Enumerate files in the backupdir for expiry of old backups
592 var dirEnum = backupDir.directoryEntries;
593 var files = [];
594 while (dirEnum.hasMoreElements()) {
595 var file = dirEnum.getNext().QueryInterface(CI.nsIFile);
596 if (file.isFile()) {
597 files.push({name: file.leafName, lastmodified: file.lastModifiedTime});
598 }
599 }
600
601 if (doDailyBackup)
602 purgeBackupsByType(files, 'day');
603 else
604 purgeBackupsByType(files, 'edit');
605
606 return;
607 }
608
609 function copyToOverwriting(oldFile, newParentDir, newName) {
610 try {
611 var newFile = newParentDir.clone();
612 newFile.append(newName);
613
614 if (newFile.exists()) {
615 newFile.remove(false);
616 }
617 oldFile.copyTo(newParentDir, newName);
618 } catch(e) {
619 Components.utils.reportError("Backup failed, no copy:"+e);
620 // Error in making a daily/initial backup.
621 // not fatal, so just continue
622 }
623 }
624
625 function getIntPrefSafe(prefName, defaultValue)
626 {
627 try {
628 var prefValue = backupBranch.getIntPref(prefName);
629 return prefValue;
630 }
631 catch (ex) {
632 return defaultValue;
633 }
634 }
635 var backupDays = getIntPrefSafe("days", 1);
636 var numBackupFiles = getIntPrefSafe("filenum", 3);
637
638 try {
639 var dirService = Components.classes["@mozilla.org/file/directory_service;1"]
640 .getService(CI.nsIProperties);
641 // xxx todo: would we want to migrate the backups into getCalendarDirectory()?
642 var backupDir = dirService.get("ProfD", CI.nsILocalFile);
643 backupDir.append("backupData");
644 if (!backupDir.exists()) {
645 backupDir.create(CI.nsIFile.DIRECTORY_TYPE, 0755);
646 }
647 } catch(e) {
648 // Backup dir wasn't found. Likely because we are running in
649 // xpcshell. Don't die, but continue the upload.
650 LOG("Backup failed, no backupdir:"+e);
651 aCallback.call(this);
652 return;
653 }
654
655 try {
656 var pseudoID = this.getProperty("uniquenum");
657 if (!pseudoID) {
658 pseudoID = new Date().getTime();
659 this.setProperty("uniquenum", pseudoID);
660 }
661 } catch(e) {
662 // calendarmgr not found. Likely because we are running in
663 // xpcshell. Don't die, but continue the upload.
664 LOG("Backup failed, no calendarmanager:"+e);
665 aCallback.call(this);
666 return;
667 }
668
669 var doInitialBackup = false;
670 var initialBackupFile = backupDir.clone();
671 initialBackupFile.append(makeName('initial'));
672 if (!initialBackupFile.exists())
673 doInitialBackup = true;
674
675 var doDailyBackup = false;
676 var backupTime = this.getProperty('backup-time');
677 if (!backupTime ||
678 (new Date().getTime() > backupTime + backupDays*24*60*60*1000)) {
679 // It's time do to a daily backup
680 doDailyBackup = true;
681 this.setProperty('backup-time', new Date().getTime());
682 }
683
684 var dailyBackupFileName;
685 if (doDailyBackup) {
686 dailyBackupFileName = makeDailyFileName(backupDir);
687 }
688
689 var backupFile = backupDir.clone();
690 backupFile.append(makeName('edit'));
691 backupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
692
693 purgeOldBackups();
694
695 // Now go download the remote file, and store it somewhere local.
696 var ioService = Components.classes["@mozilla.org/network/io-service;1"]
697 .getService(CI.nsIIOService);
698 var channel = ioService.newChannelFromURI(this.mUri);
699 channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
700 channel.notificationCallbacks = this;
701
702 var downloader = Components.classes["@mozilla.org/network/downloader;1"]
703 .createInstance(CI.nsIDownloader);
704
705 var savedthis = this;
706 var listener = {
707 onDownloadComplete: function(downloader, request, ctxt, status, result) {
708 if (doInitialBackup)
709 copyToOverwriting(result, backupDir, makeName('initial'));
710 if (doDailyBackup)
711 copyToOverwriting(result, backupDir, dailyBackupFileName);
712
713 aCallback.call(savedthis);
714 }
715 }
716
717 downloader.init(listener, backupFile);
718 try {
719 channel.asyncOpen(downloader, null);
720 } catch(e) {
721 // For local files, asyncOpen throws on new (calendar) files
722 // No problem, go and upload something
723 LOG("Backup failed in asyncOpen:"+e);
724 aCallback.call(this);
725 return;
726 }
727
728 return;
729 }
730 };
731
732 function calICSObserver(aCalendar) {
733 this.mCalendar = aCalendar;
734 }
735
736 calICSObserver.prototype = {
737 mCalendar: null,
738 mInBatch: false,
739
740 // calIObserver:
741 onStartBatch: function() {
742 this.mCalendar.observers.notify("onStartBatch");
743 this.mInBatch = true;
744 },
745 onEndBatch: function() {
746 this.mCalendar.observers.notify("onEndBatch");
747 this.mInBatch = false;
748 },
749 onLoad: function(calendar) {
750 this.mCalendar.observers.notify("onLoad", [calendar]);
751 },
752 onAddItem: function(aItem) {
753 this.mCalendar.observers.notify("onAddItem", [aItem]);
754 },
755 onModifyItem: function(aNewItem, aOldItem) {
756 this.mCalendar.observers.notify("onModifyItem", [aNewItem, aOldItem]);
757 },
758 onDeleteItem: function(aDeletedItem) {
759 this.mCalendar.observers.notify("onDeleteItem", [aDeletedItem]);
760 },
761 onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
762 this.mCalendar.observers.notify("onPropertyChanged", [aCalendar, aName, aValue, aOldValue]);
763 },
764 onPropertyDeleting: function(aCalendar, aName) {
765 this.mCalendar.observers.notify("onPropertyDeleting", [aCalendar, aName]);
766 },
767
768 // Unless an error number is in this array, we consider it very bad, set
769 // the calendar to readOnly, and give up.
770 acceptableErrorNums: [],
771
772 onError: function(aErrNo, aMessage) {
773 var errorIsOk = false;
774 for each (num in this.acceptableErrorNums) {
775 if (num == aErrNo) {
776 errorIsOk = true;
777 break;
778 }
779 }
780 if (!errorIsOk)
781 this.mCalendar.readOnly = true;
782 this.mCalendar.observers.notify("onError", [aErrNo, aMessage]);
783 }
784 };
785
786 /***************************
787 * Transport Abstraction Hooks
788 *
789 * Those hooks provide a way to do checks before or after publishing an
790 * ics file. The main use will be to check etags (or some other way to check
791 * for remote changes) to protect remote changes from being overwritten.
792 *
793 * Different protocols need different checks (webdav can do etag, but
794 * local files need last-modified stamps), hence different hooks for each
795 * types
796 */
797
798 // dummyHooks are for transport types that don't have hooks of their own.
799 // Also serves as poor-mans interface definition.
800 function dummyHooks() {
801 }
802
803 dummyHooks.prototype = {
804 onBeforeGet: function(aChannel) {
805 return true;
806 },
807
808 /**
809 * @return
810 * a boolean, false if the previous data should be used (the datastore
811 * didn't change, there might be no data in this GET), true in all
812 * other cases
813 */
814 onAfterGet: function() {
815 return true;
816 },
817
818 onBeforePut: function(aChannel) {
819 return true;
820 },
821
822 onAfterPut: function(aChannel) {
823 return true;
824 }
825 };
826
827 function httpHooks() {
828 this.mChannel = null;
829 }
830
831 httpHooks.prototype = {
832 onBeforeGet: function(aChannel) {
833 this.mChannel = aChannel;
834 if (this.mEtag) {
835 var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
836 // Somehow the webdav header 'If' doesn't work on apache when
837 // passing in a Not, so use the http version here.
838 httpchannel.setRequestHeader("If-None-Match", this.mEtag, false);
839 }
840
841 return true;
842 },
843
844 onAfterGet: function() {
845 var httpchannel = this.mChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
846
847 // 304: Not Modified
848 // Can use the old data, so tell the caller that it can skip parsing.
849 if (httpchannel.responseStatus == 304)
850 return false;
851
852 // 404: Not Found
853 // This is a new calendar. Shouldn't try to parse it. But it also
854 // isn't a failure, so don't throw.
855 if (httpchannel.responseStatus == 404)
856 return false;
857
858 try {
859 this.mEtag = httpchannel.getResponseHeader("ETag");
860 } catch(e) {
861 // No etag header. Now what?
862 this.mEtag = null;
863 }
864 this.mChannel = null;
865 return true;
866 },
867
868 onBeforePut: function(aChannel) {
869 if (this.mEtag) {
870 var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
871
872 // Apache doesn't work correctly with if-match on a PUT method,
873 // so use the webdav header
874 httpchannel.setRequestHeader("If", '(['+this.mEtag+'])', false);
875 }
876 return true;
877 },
878
879 onAfterPut: function(aChannel) {
880 var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
881 try {
882 this.mEtag = httpchannel.getResponseHeader("ETag");
883 } catch(e) {
884 // There was no ETag header on the response. This means that
885 // putting is not atomic. This is bad. Race conditions can happen,
886 // because there is a time in which we don't know the right
887 // etag.
888 // Try to do the best we can, by immediatly getting the etag.
889
890 // Only on branch, because webdav doesn't work on trunk: bug 332840
891 if (isOnBranch) {
892 var res = new WebDavResource(aChannel.URI);
893 var webSvc = Components.classes['@mozilla.org/webdav/service;1']
894 .getService(Components.interfaces.nsIWebDAVService);
895 // The namespace is 'DAV:', not just 'DAV'.
896 webSvc.getResourceProperties(res, 1, ['DAV: getetag'], false,
897 this, null, null);
898 } else {
899 // instead, on trunk, set mEtag to null, so it will be ignored on
900 // the next GET/PUT
901 this.mEtag = null;
902 }
903 }
904 return true;
905 },
906
907 onOperationComplete: function(aStatusCode, aResource, aOperation, aClosure) {
908 },
909
910 onOperationDetail: function(aStatusCode, aResource, aOperation, aDetail, aClosure) {
911 var props = aDetail.QueryInterface(Components.interfaces.nsIProperties);
912 try {
913 this.mEtag = props.get('DAV: getetag', Components.interfaces.nsISupportsString).toString();
914 } catch(e) {
915 // No etag header. Now what?
916 this.mEtag = null;
917 }
918 }
919 };
920
921 function WebDavResource(url) {
922 this.mResourceURL = url;
923 }
924
925 WebDavResource.prototype = {
926 mResourceURL: {},
927 get resourceURL() { return this.mResourceURL; },
928 QueryInterface: function(iid) {
929 if (iid.equals(CI.nsIWebDAVResource) ||
930 iid.equals(CI.nsISupports)) {
931 return this;
932 }
933 throw Components.interfaces.NS_ERROR_NO_INTERFACE;
934 }
935 };