home *** CD-ROM | disk | FTP | other *** search
/ PC Professionell 2006 June / PCpro_2006_06.ISO / files / firefox / calendar_windows_latest.xpi / components / calICSCalendar.js < prev    next >
Encoding:
Text File  |  2006-01-21  |  29.5 KB  |  803 lines

  1. /* -*- Mode: java; 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.  *  Michiel van Leeuwen <mvl@exedo.nl>
  19.  * Portions created by the Initial Developer are Copyright (C) 2004
  20.  * the Initial Developer. All Rights Reserved.
  21.  *
  22.  * Contributor(s):
  23.  *   Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
  24.  *   Dan Mosedale <dan.mosedale@oracle.com>
  25.  *   Joey Minta <jminta@gmail.com>
  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 calCalendarManagerContractID = "@mozilla.org/calendar/manager;1";
  54. const calICalendarManager = Components.interfaces.calICalendarManager;
  55.  
  56. var activeCalendarManager = null;
  57. function getCalendarManager()
  58. {
  59.     if (!activeCalendarManager) {
  60.         activeCalendarManager = 
  61.             Components.classes[calCalendarManagerContractID].getService(calICalendarManager);
  62.     }
  63.     return activeCalendarManager;
  64. }
  65.  
  66.  
  67. function calICSCalendar () {
  68.     this.wrappedJSObject = this;
  69.     this.initICSCalendar();
  70.  
  71.     this.unmappedComponents = [];
  72.     this.unmappedProperties = [];
  73.     this.queue = new Array();
  74. }
  75.  
  76. calICSCalendar.prototype = {
  77.     mICSService: null,
  78.     mObserver: null,
  79.     locked: false,
  80.  
  81.     QueryInterface: function (aIID) {
  82.         if (!aIID.equals(Components.interfaces.nsISupports) &&
  83.             !aIID.equals(Components.interfaces.calICalendar) &&
  84.             !aIID.equals(Components.interfaces.nsIStreamListener) &&
  85.             !aIID.equals(Components.interfaces.nsIStreamLoaderObserver) &&
  86.             !aIID.equals(Components.interfaces.nsIInterfaceRequestor)) {
  87.             throw Components.results.NS_ERROR_NO_INTERFACE;
  88.         }
  89.  
  90.         return this;
  91.     },
  92.     
  93.     initICSCalendar: function() {
  94.         this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
  95.                                          .createInstance(Components.interfaces.calICalendar);
  96.         this.mICSService = Components.classes["@mozilla.org/calendar/ics-service;1"]
  97.                                      .getService(Components.interfaces.calIICSService);
  98.  
  99.         this.mObserver = new calICSObserver(this);
  100.         this.mMemoryCalendar.addObserver(this.mObserver);
  101.         this.mMemoryCalendar.wrappedJSObject.calendarToReturn = this;
  102.     },
  103.  
  104.     get name() {
  105.         return getCalendarManager().getCalendarPref(this, "NAME");
  106.     },
  107.     set name(name) {
  108.         getCalendarManager().setCalendarPref(this, "NAME", name);
  109.     },
  110.  
  111.     get type() { return "ics"; },
  112.  
  113.     mReadOnly: false,
  114.  
  115.     get readOnly() { 
  116.         return this.mReadOnly;
  117.     },
  118.     set readOnly(bool) {
  119.         this.mReadOnly = bool;
  120.     },
  121.  
  122.     mUri: null,
  123.     get uri() { return this.mUri },
  124.     set uri(aUri) {
  125.         this.mUri = aUri;
  126.         this.mMemoryCalendar.uri = this.mUri;
  127.  
  128.         this.refresh();
  129.     },
  130.  
  131.     refresh: function() {
  132.         // Lock other changes to the item list.
  133.         this.locked = true;
  134.         // set to prevent writing after loading, without any changes
  135.         this.loading = true;
  136.  
  137.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  138.                                   .getService(Components.interfaces.nsIIOService);
  139.  
  140.         var channel = ioService.newChannelFromURI(fixupUri(this.mUri));
  141.         channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
  142.         channel.notificationCallbacks = this;
  143.  
  144.         var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
  145.                                      .createInstance(Components.interfaces.nsIStreamLoader);
  146.         try {
  147.             streamLoader.init(channel, this, this);
  148.         } catch(e) {
  149.             // File not found: a new calendar. No problem.
  150.             this.mObserver.onEndBatch();
  151.             this.mObserver.onLoad();
  152.             this.locked = false;
  153.             this.processQueue();
  154.         }
  155.     },
  156.  
  157.     calendarPromotedProps: {
  158.         "PRODID": true,
  159.         "VERSION": true
  160.     },
  161.  
  162.     // nsIStreamLoaderObserver impl
  163.     // Listener for download. Parse the downloaded file
  164.  
  165.     onStreamComplete: function(loader, ctxt, status, resultLength, result)
  166.     {
  167.         // Create a new calendar, to get rid of all the old events
  168.         this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
  169.                                          .createInstance(Components.interfaces.calICalendar);
  170.         this.mMemoryCalendar.uri = this.mUri;
  171.         this.mMemoryCalendar.wrappedJSObject.calendarToReturn = this;
  172.         this.mMemoryCalendar.addObserver(this.mObserver);
  173.  
  174.         this.mObserver.onStartBatch();
  175.  
  176.         // This conversion is needed, because the stream only knows about
  177.         // byte arrays, not about strings or encodings. The array of bytes
  178.         // need to be interpreted as utf8 and put into a javascript string.
  179.         var unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
  180.                                          .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
  181.         // ics files are always utf8
  182.         unicodeConverter.charset = "UTF-8";
  183.         var str;
  184.         try {
  185.             str = unicodeConverter.convertFromByteArray(result, result.length);
  186.         } catch(e) {
  187.             this.mObserver.onError(e.result, e.toString());
  188.         }
  189.  
  190.         // Wrap parsing in a try block. Will ignore errors. That's a good thing
  191.         // for non-existing or empty files, but not good for invalid files.
  192.         // That's why we put them in readOnly mode
  193.         try {
  194.             var rootComp = this.mICSService.parseICS(str);
  195.  
  196.             var calComp;
  197.             // libical returns the vcalendar component if there is just
  198.             // one vcalendar. If there are multiple vcalendars, it returns
  199.             // an xroot component, with those vcalendar childs. We need to
  200.             // handle both.
  201.             if (rootComp.componentType == 'VCALENDAR') {
  202.                 calComp = rootComp;
  203.             } else {
  204.                 calComp = rootComp.getFirstSubcomponent('VCALENDAR');
  205.             }
  206.  
  207.             while (calComp) {
  208.                 // Get unknown properties
  209.                 var prop = calComp.getFirstProperty("ANY");
  210.                 while (prop) {
  211.                     if (!this.calendarPromotedProps[prop.propertyName]) {
  212.                         this.unmappedProperties.push(prop);
  213.                         dump(prop.propertyName+"\n");
  214.                     }
  215.                     prop = calComp.getNextProperty("ANY");
  216.                 }
  217.  
  218.                 var subComp = calComp.getFirstSubcomponent("ANY");
  219.                 while (subComp) {
  220.                     // Place each subcomp in a try block, to hopefully get as
  221.                     // much of a bad calendar as possible
  222.                     try {
  223.                         switch (subComp.componentType) {
  224.                         case "VEVENT":
  225.                             var event = Components.classes["@mozilla.org/calendar/event;1"]
  226.                                                   .createInstance(Components.interfaces.calIEvent);
  227.                             event.icalComponent = subComp;
  228.                             this.mMemoryCalendar.adoptItem(event, null);
  229.                             break;
  230.                         case "VTODO":
  231.                             var todo = Components.classes["@mozilla.org/calendar/todo;1"]
  232.                                                  .createInstance(Components.interfaces.calITodo);
  233.                             todo.icalComponent = subComp;
  234.                             this.mMemoryCalendar.adoptItem(todo, null);
  235.                             break;
  236.  
  237.                         case "VTIMEZONE":
  238.                             // this should already be attached to the relevant
  239.                             // events in the calendar, so there's no need to
  240.                             // do anything with it here.
  241.                             break;
  242.  
  243.                         default:
  244.                             this.unmappedComponents.push(subComp);
  245.                             dump(subComp.componentType+"\n");
  246.                         }
  247.                     }
  248.                     catch(ex) { 
  249.                         this.mObserver.onError(ex.result, ex.toString());
  250.                     }
  251.                     subComp = calComp.getNextSubcomponent("ANY");
  252.                 }
  253.                 calComp = rootComp.getNextSubcomponent('VCALENDAR');
  254.             }
  255.         } catch(e) {
  256.             dump("Parsing the file failed:"+e+"\n");
  257.             this.mObserver.onError(e.result, e.toString());
  258.         }
  259.  
  260.         this.mObserver.onEndBatch();
  261.         this.mObserver.onLoad();
  262.         this.locked = false;
  263.         this.processQueue();
  264.     },
  265.  
  266.     writeICS: function () {
  267.         this.locked = true;
  268.  
  269.         if (!this.mUri)
  270.             throw Components.results.NS_ERROR_FAILURE;
  271.  
  272.         // makeBackup will call doWriteICS
  273.         this.makeBackup(this.doWriteICS);
  274.     },
  275.  
  276.     doWriteICS: function () {
  277.         var savedthis = this;
  278.         var listener =
  279.         {
  280.             onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail)
  281.             {
  282.                 try  {
  283.                     // All events are returned. Now set up a channel and a
  284.                     // streamloader to upload.  onStopRequest will be called
  285.                     // once the write has finished
  286.                     var ioService = Components.classes
  287.                         ["@mozilla.org/network/io-service;1"]
  288.                         .getService(Components.interfaces.nsIIOService);
  289.                     var channel = ioService.newChannelFromURI(
  290.                         fixupUri(savedthis.mUri));
  291.                     channel.notificationCallbacks = savedthis;
  292.                     var uploadChannel = channel.QueryInterface(
  293.                         Components.interfaces.nsIUploadChannel);
  294.  
  295.                     // do the actual serialization
  296.                     var icsStream = calComp.serializeToICSStream();
  297.  
  298.                     uploadChannel.setUploadStream(icsStream, "text/calendar",
  299.                                                   -1);
  300.  
  301.                     channel.asyncOpen(savedthis, savedthis);
  302.                 } catch (ex) {
  303.                     savedthis.mObserver.onError(
  304.                         ex.result, "The calendar could not be saved; there " +
  305.                         "was a failure: 0x" + ex.result.toString(16));
  306.                 }
  307.  
  308.                 return;
  309.             },
  310.             onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems)
  311.             {
  312.                 for (var i=0; i<aCount; i++) {
  313.                     calComp.addSubcomponent(aItems[i].icalComponent);
  314.                 }
  315.             }
  316.         };
  317.  
  318.         var calComp = this.mICSService.createIcalComponent("VCALENDAR");
  319.         calComp.version = "2.0";
  320.         calComp.prodid = "-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN";
  321.  
  322.         var i;
  323.         for (i in this.unmappedComponents) {
  324.              dump("Adding a "+this.unmappedComponents[i].componentType+"\n");
  325.              calComp.addSubcomponent(this.unmappedComponents[i]);
  326.         }
  327.         for (i in this.unmappedProperties) {
  328.              dump("Adding "+this.unmappedProperties[i].propertyName+"\n");
  329.              calComp.addProperty(this.unmappedProperties[i]);
  330.         }
  331.  
  332.         this.getItems(calICalendar.ITEM_FILTER_TYPE_ALL | calICalendar.ITEM_FILTER_COMPLETED_ALL,
  333.                       0, null, null, listener);
  334.     },
  335.  
  336.     // nsIStreamListener impl
  337.     // For after publishing. Do error checks here
  338.     onStartRequest: function(request, ctxt) {},
  339.     onDataAvailable: function(request, ctxt, inStream, sourceOffset, count) {},
  340.     onStopRequest: function(request, ctxt, status, errorMsg)
  341.     {
  342.         ctxt = ctxt.wrappedJSObject;
  343.         var channel;
  344.         try {
  345.             channel = request.QueryInterface(Components.interfaces.nsIHttpChannel);
  346.             dump(channel.requestSucceeded+"\n");
  347.         } catch(e) {
  348.         }
  349.  
  350.         if (channel && !channel.requestSucceeded) {
  351.             ctxt.mObserver.onError(channel.requestSucceeded,
  352.                                    "Publishing the calendar file failed\n" +
  353.                                        "Status code: "+channel.responseStatus+": "+channel.responseStatusText+"\n");
  354.         }
  355.  
  356.         else if (!channel && !Components.isSuccessCode(request.status)) {
  357.             ctxt.mObserver.onError(request.status,
  358.                                    "Publishing the calendar file failed\n" +
  359.                                        "Status code: "+request.status.toString(16)+"\n");
  360.         }
  361.         ctxt.locked = false;
  362.         ctxt.processQueue();
  363.     },
  364.  
  365.     addObserver: function (aObserver) {
  366.         this.mObserver.addObserver(aObserver);
  367.     },
  368.     removeObserver: function (aObserver) {
  369.         this.mObserver.removeObserver(aObserver);
  370.     },
  371.  
  372.     // Always use the queue, just to reduce the amount of places where
  373.     // this.mMemoryCalendar.addItem() and friends are called. less
  374.     // copied code.
  375.     addItem: function (aItem, aListener) {
  376.         if (this.readOnly) 
  377.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  378.         this.queue.push({action:'add', item:aItem, listener:aListener});
  379.         this.processQueue();
  380.     },
  381.  
  382.     modifyItem: function (aNewItem, aOldItem, aListener) {
  383.         if (this.readOnly) 
  384.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  385.         this.queue.push({action:'modify', oldItem: aOldItem,
  386.                          newItem: aNewItem, listener:aListener});
  387.         this.processQueue();
  388.     },
  389.  
  390.     deleteItem: function (aItem, aListener) {
  391.         if (this.readOnly) 
  392.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  393.         this.queue.push({action:'delete', item:aItem, listener:aListener});
  394.         this.processQueue();
  395.     },
  396.  
  397.     getItem: function (aId, aListener) {
  398.         return this.mMemoryCalendar.getItem(aId, aListener);
  399.     },
  400.  
  401.     getItems: function (aItemFilter, aCount,
  402.                         aRangeStart, aRangeEnd, aListener)
  403.     {
  404.         return this.mMemoryCalendar.getItems(aItemFilter, aCount,
  405.                                              aRangeStart, aRangeEnd,
  406.                                              aListener);
  407.     },
  408.  
  409.     processQueue: function ()
  410.     {
  411.         if (this.locked)
  412.             return;
  413.         var a;
  414.         var hasItems = this.queue.length;
  415.         while ((a = this.queue.shift())) {
  416.             switch (a.action) {
  417.                 case 'add':
  418.                     this.mMemoryCalendar.addItem(a.item, a.listener);
  419.                     break;
  420.                 case 'modify':
  421.                     this.mMemoryCalendar.modifyItem(a.newItem, a.oldItem,
  422.                                                     a.listener);
  423.                     break;
  424.                 case 'delete':
  425.                     this.mMemoryCalendar.deleteItem(a.item, a.listener);
  426.                     break;
  427.             }
  428.         }
  429.         if (hasItems)
  430.             this.writeICS();
  431.     },
  432.  
  433.     startBatch: function ()
  434.     {
  435.         this.mObserver.onStartBatch();
  436.     },
  437.     endBatch: function ()
  438.     {
  439.         this.mObserver.onEndBatch();
  440.     },
  441.  
  442.     // nsIInterfaceRequestor impl
  443.     getInterface: function(iid, instance) {
  444.         if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
  445.             // use the window watcher service to get a nsIAuthPrompt impl
  446.             return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  447.                              .getService(Components.interfaces.nsIWindowWatcher)
  448.                              .getNewAuthPrompter(null);
  449.         }
  450.         else if (iid.equals(Components.interfaces.nsIPrompt)) {
  451.             // use the window watcher service to get a nsIPrompt impl
  452.             return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  453.                              .getService(Components.interfaces.nsIWindowWatcher)
  454.                              .getNewPrompter(null);
  455.         }
  456.         Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
  457.         return null;
  458.     },
  459.  
  460.     /**
  461.      * Make a backup of the (remote) calendar
  462.      *
  463.      * This will download the remote file into the profile dir.
  464.      * It should be called before every upload, so every change can be
  465.      * restored. By default, it will keep 3 backups. It also keeps one
  466.      * file each day, for 3 days. That way, even if the user doesn't notice
  467.      * the remote calendar has become corrupted, he will still loose max 1
  468.      * day of work.
  469.      * After the back up is finished, will call aCallback.
  470.      *
  471.      * @param aCallback
  472.      *           Function that will be calles after the backup is finished.
  473.      *           will be called in the original context in which makeBackup
  474.      *           was called
  475.      */
  476.     makeBackup: function(aCallback) {
  477.         // Uses |pseudoID|, an id of the calendar, defined below
  478.         function makeName(type) {
  479.             return 'calBackupData_'+pseudoID+'_'+type+'.ics';
  480.         }
  481.         
  482.         // This is a bit messy. createUnique creates an empty file,
  483.         // but we don't use that file. All we want is a filename, to be used
  484.         // in the call to copyTo later. So we create a file, get the filename,
  485.         // and never use the file again, but write over it.
  486.         // Using createUnique anyway, because I don't feel like 
  487.         // re-implementing it
  488.         function makeDailyFileName() {
  489.             var dailyBackupFile = backupDir.clone();
  490.             dailyBackupFile.append(makeName('day'));
  491.             dailyBackupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
  492.             dailyBackupFileName = dailyBackupFile.leafName;
  493.  
  494.             // Remove the reference to the nsIFile, because we need to
  495.             // write over the file later, and you never know what happens
  496.             // if something still has a reference.
  497.             // Also makes it explicit that we don't need the file itself,
  498.             // just the name.
  499.             dailyBackupFile = null;
  500.  
  501.             return dailyBackupFileName;
  502.         }
  503.  
  504.         function purgeBackupsByType(files, type) {
  505.             // filter out backups of the type we care about.
  506.             var filteredFiles = files.filter(
  507.                 function f(v) { 
  508.                     return (v.name.indexOf("calBackupData_"+pseudoID+"_"+type) != -1)
  509.                 });
  510.             // Sort by lastmodifed
  511.             filteredFiles.sort(
  512.                 function s(a,b) {
  513.                     return (a.lastmodified - b.lastmodified);
  514.                 });
  515.             // And delete the oldest files, and keep the desired number of
  516.             // old backups
  517.             var i;
  518.             for (i = 0; i < filteredFiles.length - numBackupFiles; ++i) {
  519.                 file = backupDir.clone();
  520.                 file.append(filteredFiles[i].name);
  521.                 file.remove(false);
  522.             }
  523.             return;
  524.         }
  525.  
  526.         function purgeOldBackups() {
  527.             // Enumerate files in the backupdir for expiry of old backups
  528.             var dirEnum = backupDir.directoryEntries;
  529.             var files = [];
  530.             while (dirEnum.hasMoreElements()) {
  531.                 var file = dirEnum.getNext().QueryInterface(CI.nsIFile);
  532.                 if (file.isFile()) {
  533.                     files.push({name: file.leafName, lastmodified: file.lastModifiedTime});
  534.                 }
  535.             }
  536.  
  537.             if (doDailyBackup)
  538.                 purgeBackupsByType(files, 'day');
  539.             else
  540.                 purgeBackupsByType(files, 'edit');
  541.  
  542.             return;
  543.         }
  544.         
  545.         function copyToOverwriting(oldFile, newParentDir, newName) {
  546.             try {
  547.                 var newFile = newParentDir.clone();
  548.                 newFile.append(newName);
  549.             
  550.                 if (newFile.exists()) {
  551.                     newFile.remove(false);
  552.                 }
  553.                 oldFile.copyTo(newParentDir, newName);
  554.             } catch(e) {
  555.                 Components.utils.reportError("Backup failed, no copy:"+e);
  556.                 // Error in making a daily/initial backup.
  557.                 // not fatal, so just continue
  558.             }
  559.         }
  560.  
  561.         function getIntPrefSafe(prefName, defaultValue)
  562.         {
  563.             try {
  564.                 var prefValue = backupBranch.getIntPref(prefName);
  565.                 return prefValue;
  566.             }
  567.             catch (ex) {
  568.                 return defaultValue;
  569.             }
  570.         }
  571.         var backupDays = getIntPrefSafe("days", 1);
  572.         var numBackupFiles = getIntPrefSafe("filenum", 3);
  573.  
  574.         try {
  575.             var dirService = Components.classes["@mozilla.org/file/directory_service;1"]
  576.                                        .getService(CI.nsIProperties);
  577.             var backupDir = dirService.get("ProfD", CI.nsILocalFile);
  578.             backupDir.append("backupData");
  579.             if (!backupDir.exists()) {
  580.                 backupDir.create(CI.nsIFile.DIRECTORY_TYPE, 0755);
  581.             }
  582.         } catch(e) {
  583.             // Backup dir wasn't found. Likely because we are running in
  584.             // xpcshell. Don't die, but continue the upload.
  585.             dump("Backup failed, no backupdir:"+e+"\n");
  586.             aCallback.call(this);
  587.             return;
  588.         }
  589.  
  590.         try {
  591.             var pseudoID = getCalendarManager().getCalendarPref(this, "UNIQUENUM");
  592.             if (!pseudoID) {
  593.                 pseudoID = new Date().getTime();
  594.                 getCalendarManager().setCalendarPref(this, "UNIQUENUM", pseudoID);
  595.             }
  596.         } catch(e) {
  597.             // calendarmgr not found. Likely because we are running in
  598.             // xpcshell. Don't die, but continue the upload.
  599.             dump("Backup failed, no calendarmanager:"+e+"\n");
  600.             aCallback.call(this);
  601.             return;
  602.         }
  603.  
  604.         var doInitialBackup = false;
  605.         var initialBackupFile = backupDir.clone();
  606.         initialBackupFile.append(makeName('initial'));
  607.         if (!initialBackupFile.exists())
  608.             doInitialBackup = true;
  609.  
  610.         var doDailyBackup = false;
  611.         var backupTime = new Number(getCalendarManager().
  612.                                        getCalendarPref(this, 'backup-time'));
  613.         if (!backupTime ||
  614.             (new Date().getTime() > backupTime + backupDays*24*60*60*1000)) {
  615.             // It's time do to a daily backup
  616.             doDailyBackup = true;
  617.             getCalendarManager().setCalendarPref(this, 'backup-time', 
  618.                                                  new Date().getTime());
  619.         }
  620.  
  621.         var dailyBackupFileName;
  622.         if (doDailyBackup) {
  623.             dailyBackupFileName = makeDailyFileName(backupDir);
  624.         }
  625.  
  626.         var backupFile = backupDir.clone();
  627.         backupFile.append(makeName('edit'));
  628.         backupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
  629.         
  630.         purgeOldBackups();
  631.  
  632.         // Now go download the remote file, and store it somewhere local.
  633.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  634.                                   .getService(CI.nsIIOService);
  635.         var channel = ioService.newChannelFromURI(fixupUri(this.mUri));
  636.         channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
  637.         channel.notificationCallbacks = this;
  638.  
  639.         var downloader = Components.classes["@mozilla.org/network/downloader;1"]
  640.                                    .createInstance(CI.nsIDownloader);
  641.  
  642.         var savedthis = this;
  643.         var listener = {
  644.             onDownloadComplete: function(downloader, request, ctxt, status, result) {
  645.                 if (doInitialBackup)
  646.                     copyToOverwriting(result, backupDir, makeName('initial'));
  647.                 if (doDailyBackup)
  648.                     copyToOverwriting(result, backupDir, dailyBackupFileName);
  649.  
  650.                 aCallback.call(savedthis);
  651.             },
  652.         }
  653.  
  654.         downloader.init(listener, backupFile);
  655.         try {
  656.             channel.asyncOpen(downloader, null);
  657.         } catch(e) {
  658.             // For local files, asyncOpen throws on new (calendar) files
  659.             // No problem, go and upload something
  660.             dump("Backup failed in asyncOpen:"+e+"\n");
  661.             aCallback.call(this);
  662.             return;
  663.         }
  664.  
  665.         return;
  666.     }
  667. };
  668.  
  669. function fixupUri(aUri) {
  670.     var uri = aUri;
  671.     if (uri.scheme == 'webcal')
  672.         uri.scheme = 'http';
  673.     if (uri.scheme == 'webcals')
  674.         uri.scheme = 'https';
  675.     return uri;
  676. }
  677.  
  678. function calICSObserver(aCalendar) {
  679.     this.mCalendar = aCalendar;
  680.     this.mObservers = new Array();
  681. }
  682.  
  683. calICSObserver.prototype = {
  684.     mCalendar: null,
  685.     mInBatch: false,
  686.  
  687.     onStartBatch: function() {
  688.         for (var i = 0; i < this.mObservers.length; i++)
  689.             this.mObservers[i].onStartBatch();
  690.         this.mInBatch = true;
  691.     },
  692.     onEndBatch: function() {
  693.         for (var i = 0; i < this.mObservers.length; i++)
  694.             this.mObservers[i].onEndBatch();
  695.  
  696.         this.mInBatch = false;
  697.     },
  698.     onLoad: function() {
  699.         for (var i = 0; i < this.mObservers.length; i++)
  700.             this.mObservers[i].onLoad();
  701.     },
  702.     onAddItem: function(aItem) {
  703.         for (var i = 0; i < this.mObservers.length; i++)
  704.             this.mObservers[i].onAddItem(aItem);
  705.     },
  706.     onModifyItem: function(aNewItem, aOldItem) {
  707.         for (var i = 0; i < this.mObservers.length; i++)
  708.             this.mObservers[i].onModifyItem(aNewItem, aOldItem);
  709.     },
  710.     onDeleteItem: function(aDeletedItem) {
  711.         for (var i = 0; i < this.mObservers.length; i++)
  712.             this.mObservers[i].onDeleteItem(aDeletedItem);
  713.     },
  714.     onAlarm: function(aAlarmItem) {
  715.         for (var i = 0; i < this.mObservers.length; i++)
  716.             this.mObservers[i].onAlarm(aAlarmItem);
  717.     },
  718.  
  719.     // Unless an error number is in this array, we consider it very bad, set
  720.     // the calendar to readOnly, and give up.
  721.     acceptableErrorNums: [],
  722.  
  723.     onError: function(aErrNo, aMessage) {
  724.         var errorIsOk = false;
  725.         for each (num in this.acceptableErrorNums) {
  726.             if (num == aErrNo) {
  727.                 errorIsOk = true;
  728.                 break;
  729.             }
  730.         }
  731.         if (!errorIsOk)
  732.             this.mCalendar.readOnly = true;
  733.         for (var i = 0; i < this.mObservers.length; i++)
  734.             this.mObservers[i].onError(aErrNo, aMessage);
  735.     },
  736.  
  737.     // This observer functions as proxy for all the other observers
  738.     // So need addObserver and removeObserver here
  739.     addObserver: function (aObserver) {
  740.         for each (obs in this.mObservers) {
  741.             if (obs == aObserver)
  742.                 return;
  743.         }
  744.  
  745.         this.mObservers.push(aObserver);
  746.     },
  747.  
  748.     removeObserver: function (aObserver) {
  749.         var newObservers = Array();
  750.         for each (obs in this.mObservers) {
  751.             if (obs != aObserver)
  752.                 newObservers.push(obs);
  753.         }
  754.         this.mObservers = newObservers;
  755.     }
  756. };
  757.  
  758. /****
  759.  **** module registration
  760.  ****/
  761.  
  762. var calICSCalendarModule = {
  763.  
  764.     mCID: Components.ID("{f8438bff-a3c9-4ed5-b23f-2663b5469abf}"),
  765.     mContractID: "@mozilla.org/calendar/calendar;1?type=ics",
  766.     
  767.     registerSelf: function (compMgr, fileSpec, location, type) {
  768.         compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
  769.         compMgr.registerFactoryLocation(this.mCID,
  770.                                         "Calendar ICS provider",
  771.                                         this.mContractID,
  772.                                         fileSpec,
  773.                                         location,
  774.                                         type);
  775.     },
  776.  
  777.     getClassObject: function (compMgr, cid, iid) {
  778.         if (!cid.equals(this.mCID))
  779.             throw Components.results.NS_ERROR_NO_INTERFACE;
  780.  
  781.         if (!iid.equals(Components.interfaces.nsIFactory))
  782.             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  783.  
  784.         return this.mFactory;
  785.     },
  786.  
  787.     mFactory: {
  788.         createInstance: function (outer, iid) {
  789.             if (outer != null)
  790.                 throw Components.results.NS_ERROR_NO_AGGREGATION;
  791.             return (new calICSCalendar()).QueryInterface(iid);
  792.         }
  793.     },
  794.  
  795.     canUnload: function(compMgr) {
  796.         return true;
  797.     }
  798. };
  799.  
  800. function NSGetModule(compMgr, fileSpec) {
  801.     return calICSCalendarModule;
  802. }
  803.