home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2007 September / PCWSEP07.iso / Software / Linux / Linux Mint 3.0 Light / LinuxMint-3.0-Light.iso / casper / filesystem.squashfs / usr / lib / sunbird / components / calICSCalendar.js < prev    next >
Encoding:
Text File  |  2007-05-23  |  36.3 KB  |  1,031 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 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.wrappedJSObject = this;
  61.     this.initICSCalendar();
  62.  
  63.     this.unmappedComponents = [];
  64.     this.unmappedProperties = [];
  65.     this.queue = new Array();
  66. }
  67.  
  68. calICSCalendar.prototype = {
  69.     mICSService: null,
  70.     mObserver: null,
  71.     locked: false,
  72.  
  73.     QueryInterface: function (aIID) {
  74.         if (!aIID.equals(Components.interfaces.nsISupports) &&
  75.             !aIID.equals(Components.interfaces.calICalendarProvider) &&
  76.             !aIID.equals(Components.interfaces.calICalendar) &&
  77.             !aIID.equals(Components.interfaces.nsIStreamListener) &&
  78.             !aIID.equals(Components.interfaces.nsIStreamLoaderObserver) &&
  79.             !aIID.equals(Components.interfaces.nsIInterfaceRequestor)) {
  80.             throw Components.results.NS_ERROR_NO_INTERFACE;
  81.         }
  82.  
  83.         return this;
  84.     },
  85.     
  86.     initICSCalendar: function() {
  87.         this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
  88.                                          .createInstance(Components.interfaces.calICalendar);
  89.         this.mICSService = Components.classes["@mozilla.org/calendar/ics-service;1"]
  90.                                      .getService(Components.interfaces.calIICSService);
  91.  
  92.         this.mObserver = new calICSObserver(this);
  93.         this.mMemoryCalendar.addObserver(this.mObserver);
  94.         this.mMemoryCalendar.wrappedJSObject.calendarToReturn = this;
  95.     },
  96.  
  97.     //
  98.     // calICalendarProvider interface
  99.     //
  100.     get prefChromeOverlay() {
  101.         return null;
  102.     },
  103.  
  104.     get displayName() {
  105.         return calGetString("calendar", "icsName");
  106.     },
  107.  
  108.     createCalendar: function ics_createCal() {
  109.         throw NS_ERROR_NOT_IMPLEMENTED;
  110.     },
  111.  
  112.     deleteCalendar: function ics_deleteCal(cal, listener) {
  113.         throw NS_ERROR_NOT_IMPLEMENTED;
  114.     },
  115.  
  116.     //
  117.     // calICalendar interface
  118.     //
  119.     // attribute AUTF8String id;
  120.     mID: null,
  121.     get id() {
  122.         return this.mID;
  123.     },
  124.     set id(id) {
  125.         if (this.mID)
  126.             throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
  127.         return (this.mID = id);
  128.     },
  129.  
  130.     get name() {
  131.         return getCalendarManager().getCalendarPref(this, "NAME");
  132.     },
  133.     set name(name) {
  134.         getCalendarManager().setCalendarPref(this, "NAME", name);
  135.     },
  136.  
  137.     get type() { return "ics"; },
  138.  
  139.     mReadOnly: false,
  140.  
  141.     get readOnly() { 
  142.         return this.mReadOnly;
  143.     },
  144.     set readOnly(bool) {
  145.         this.mReadOnly = bool;
  146.     },
  147.  
  148.     get canRefresh() {
  149.         return true;
  150.     },
  151.  
  152.     mUri: null,
  153.     get uri() { return this.mUri },
  154.     set uri(aUri) {
  155.         this.mUri = aUri;
  156.         this.mMemoryCalendar.uri = this.mUri;
  157.  
  158.         // Use the ioservice, to create a channel, which makes finding the
  159.         // right hooks to use easier.
  160.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  161.                                   .getService(Components.interfaces.nsIIOService);
  162.         var channel = ioService.newChannelFromURI(this.mUri);
  163.  
  164.         if (channel instanceof Components.interfaces.nsIHttpChannel) {
  165.             this.mHooks = new httpHooks();
  166.         } else {
  167.             this.mHooks = new dummyHooks();
  168.         }
  169.  
  170.         this.refresh();
  171.     },
  172.  
  173.     refresh: function() {
  174.         // Lock other changes to the item list.
  175.         this.lock();
  176.         // set to prevent writing after loading, without any changes
  177.         this.loading = true;
  178.  
  179.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  180.                                   .getService(Components.interfaces.nsIIOService);
  181.  
  182.         var channel = ioService.newChannelFromURI(this.mUri);
  183.         channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
  184.         channel.notificationCallbacks = this;
  185.  
  186.         // Allow the hook to do its work, like a performing a quick check to
  187.         // see if the remote file really changed. Might save a lot of time
  188.         this.mHooks.onBeforeGet(channel);
  189.  
  190.         var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
  191.                                      .createInstance(Components.interfaces.nsIStreamLoader);
  192.         try {
  193.             if (isOnBranch) {
  194.                 streamLoader.init(channel, this, this);
  195.             } else {
  196.                 streamLoader.init(this);
  197.                 channel.asyncOpen(streamLoader, this);
  198.             }
  199.         } catch(e) {
  200.             // File not found: a new calendar. No problem.
  201.             this.unlock();
  202.         }
  203.     },
  204.  
  205.     calendarPromotedProps: {
  206.         "PRODID": true,
  207.         "VERSION": true
  208.     },
  209.  
  210.     // nsIStreamLoaderObserver impl
  211.     // Listener for download. Parse the downloaded file
  212.  
  213.     onStreamComplete: function(loader, ctxt, status, resultLength, result)
  214.     {
  215.         // No need to do anything if there was no result
  216.         if (!resultLength) {
  217.             this.unlock();
  218.             return;
  219.         }
  220.         
  221.         // Allow the hook to get needed data (like an etag) of the channel
  222.         var cont = this.mHooks.onAfterGet();
  223.         if (!cont) {
  224.             this.unlock();
  225.             return;
  226.         }
  227.  
  228.         // This conversion is needed, because the stream only knows about
  229.         // byte arrays, not about strings or encodings. The array of bytes
  230.         // need to be interpreted as utf8 and put into a javascript string.
  231.         var unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
  232.                                          .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
  233.         // ics files are always utf8
  234.         unicodeConverter.charset = "UTF-8";
  235.         var str;
  236.         try {
  237.             str = unicodeConverter.convertFromByteArray(result, result.length);
  238.         } catch(e) {
  239.             this.mObserver.onError(calIErrors.CAL_UTF8_DECODING_FAILED, e.toString());
  240.             this.unlock();
  241.             return;
  242.         }
  243.  
  244.         // Create a new calendar, to get rid of all the old events
  245.         this.mMemoryCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=memory"]
  246.                                          .createInstance(Components.interfaces.calICalendar);
  247.         this.mMemoryCalendar.uri = this.mUri;
  248.         this.mMemoryCalendar.wrappedJSObject.calendarToReturn = this;
  249.  
  250.         this.mObserver.onStartBatch();
  251.  
  252.         // Wrap parsing in a try block. Will ignore errors. That's a good thing
  253.         // for non-existing or empty files, but not good for invalid files.
  254.         // That's why we put them in readOnly mode
  255.         try {
  256.             var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"].
  257.                                     createInstance(Components.interfaces.calIIcsParser);
  258.             parser.parseString(str);
  259.             var items = parser.getItems({});
  260.             
  261.             for each (var item in items) {
  262.                 this.mMemoryCalendar.adoptItem(item, null);
  263.             }
  264.             this.unmappedComponents = parser.getComponents({});
  265.             this.unmappedProperties = parser.getProperties({});
  266.         } catch(e) {
  267.             LOG("Parsing the file failed:"+e);
  268.             this.mObserver.onError(e.result, e.toString());
  269.         }
  270.         this.mObserver.onEndBatch();
  271.         this.mObserver.onLoad();
  272.         
  273.         // Now that all items have been stuffed into the memory calendar
  274.         // we should add ourselves as observer. It is important that this
  275.         // happens *after* the calls to adoptItem in the above loop to prevent
  276.         // the views from being notified.
  277.         this.mMemoryCalendar.addObserver(this.mObserver);
  278.         
  279.         this.unlock();
  280.     },
  281.  
  282.     writeICS: function () {
  283.         this.lock();
  284.  
  285.         if (!this.mUri)
  286.             throw Components.results.NS_ERROR_FAILURE;
  287.  
  288.         // makeBackup will call doWriteICS
  289.         this.makeBackup(this.doWriteICS);
  290.     },
  291.  
  292.     doWriteICS: function () {
  293.         var savedthis = this;
  294.         var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
  295.                                    .getService(Components.interfaces.nsIAppStartup);
  296.         var listener =
  297.         {
  298.             serializer: null,
  299.             onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail)
  300.             {
  301.                 var inLastWindowClosingSurvivalArea = false;
  302.                 try  {
  303.                     // All events are returned. Now set up a channel and a
  304.                     // streamloader to upload.  onStopRequest will be called
  305.                     // once the write has finished
  306.                     var ioService = Components.classes
  307.                         ["@mozilla.org/network/io-service;1"]
  308.                         .getService(Components.interfaces.nsIIOService);
  309.                     var channel = ioService.newChannelFromURI(savedthis.mUri);
  310.  
  311.                     // Allow the hook to add things to the channel, like a
  312.                     // header that checks etags
  313.                     savedthis.mHooks.onBeforePut(channel);
  314.  
  315.                     channel.notificationCallbacks = savedthis;
  316.                     var uploadChannel = channel.QueryInterface(
  317.                         Components.interfaces.nsIUploadChannel);
  318.  
  319.                     // Create a pipe to convert the output stream from the
  320.                     // serializer into an input stream for the upload channel
  321.                     var pipe = Components.classes["@mozilla.org/pipe;1"].
  322.                                   createInstance(Components.interfaces.nsIPipe);
  323.                     const PR_UINT32_MAX = 4294967295; // signals "infinite-length"
  324.                     pipe.init(true, true, 0, PR_UINT32_MAX, null);
  325.  
  326.                     // Serialize
  327.                     var icsStream = this.serializer.serializeToStream(pipe.outputStream);
  328.  
  329.                     // Upload
  330.                     uploadChannel.setUploadStream(pipe.inputStream,
  331.                                                   "text/calendar", -1);
  332.  
  333.                     appStartup.enterLastWindowClosingSurvivalArea();
  334.                     inLastWindowClosingSurvivalArea = true;
  335.                     channel.asyncOpen(savedthis, savedthis);
  336.                 } catch (ex) {
  337.                     if (inLastWindowClosingSurvivalArea) {
  338.                         appStartup.exitLastWindowClosingSurvivalArea();
  339.                     }
  340.                     savedthis.mObserver.onError(
  341.                         ex.result, "The calendar could not be saved; there " +
  342.                         "was a failure: 0x" + ex.result.toString(16));
  343.                 }
  344.  
  345.                 return;
  346.             },
  347.             onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems)
  348.             {
  349.                 this.serializer.addItems(aItems, aCount);
  350.             }
  351.         };
  352.         listener.serializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"].
  353.                                          createInstance(Components.interfaces.calIIcsSerializer);
  354.         for each (var comp in this.unmappedComponents) {
  355.             listener.serializer.addComponent(comp);
  356.         }
  357.         for each (var prop in this.unmappedProperties) {
  358.             listener.serializer.addProperty(prop);
  359.         }
  360.  
  361.         this.getItems(calICalendar.ITEM_FILTER_TYPE_ALL | calICalendar.ITEM_FILTER_COMPLETED_ALL,
  362.                       0, null, null, listener);
  363.     },
  364.  
  365.     // nsIStreamListener impl
  366.     // For after publishing. Do error checks here
  367.     onStartRequest: function(request, ctxt) {},
  368.     onDataAvailable: function(request, ctxt, inStream, sourceOffset, count) {},
  369.     onStopRequest: function(request, ctxt, status, errorMsg)
  370.     {
  371.         ctxt = ctxt.wrappedJSObject;
  372.         var channel;
  373.         try {
  374.             channel = request.QueryInterface(Components.interfaces.nsIHttpChannel);
  375.             LOG(channel.requestSucceeded);
  376.         } catch(e) {
  377.         }
  378.  
  379.         if (channel && !channel.requestSucceeded) {
  380.             ctxt.mObserver.onError(channel.requestSucceeded,
  381.                                    "Publishing the calendar file failed\n" +
  382.                                        "Status code: "+channel.responseStatus+": "+channel.responseStatusText+"\n");
  383.         }
  384.  
  385.         else if (!channel && !Components.isSuccessCode(request.status)) {
  386.             ctxt.mObserver.onError(request.status,
  387.                                    "Publishing the calendar file failed\n" +
  388.                                        "Status code: "+request.status.toString(16)+"\n");
  389.         }
  390.  
  391.         // Allow the hook to grab data of the channel, like the new etag
  392.         ctxt.mHooks.onAfterPut(channel);
  393.  
  394.         ctxt.unlock();
  395.         var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
  396.                                    .getService(Components.interfaces.nsIAppStartup);
  397.         appStartup.exitLastWindowClosingSurvivalArea();
  398.     },
  399.  
  400.     addObserver: function (aObserver) {
  401.         this.mObserver.addObserver(aObserver);
  402.     },
  403.     removeObserver: function (aObserver) {
  404.         this.mObserver.removeObserver(aObserver);
  405.     },
  406.  
  407.     get sendItipInvitations() { return true; },
  408.  
  409.     // Always use the queue, just to reduce the amount of places where
  410.     // this.mMemoryCalendar.addItem() and friends are called. less
  411.     // copied code.
  412.     addItem: function (aItem, aListener) {
  413.         if (this.readOnly) 
  414.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  415.         this.queue.push({action:'add', item:aItem, listener:aListener});
  416.         this.processQueue();
  417.     },
  418.  
  419.     modifyItem: function (aNewItem, aOldItem, aListener) {
  420.         if (this.readOnly) 
  421.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  422.         this.queue.push({action:'modify', oldItem: aOldItem,
  423.                          newItem: aNewItem, listener:aListener});
  424.         this.processQueue();
  425.     },
  426.  
  427.     deleteItem: function (aItem, aListener) {
  428.         if (this.readOnly) 
  429.             throw Components.interfaces.calIErrors.CAL_IS_READONLY;
  430.         this.queue.push({action:'delete', item:aItem, listener:aListener});
  431.         this.processQueue();
  432.     },
  433.  
  434.     getItem: function (aId, aListener) {
  435.         return this.mMemoryCalendar.getItem(aId, aListener);
  436.     },
  437.  
  438.     getItems: function (aItemFilter, aCount,
  439.                         aRangeStart, aRangeEnd, aListener)
  440.     {
  441.         return this.mMemoryCalendar.getItems(aItemFilter, aCount,
  442.                                              aRangeStart, aRangeEnd,
  443.                                              aListener);
  444.     },
  445.  
  446.     processQueue: function ()
  447.     {
  448.         if (this.isLocked())
  449.             return;
  450.         var a;
  451.         var hasItems = this.queue.length;
  452.         while ((a = this.queue.shift())) {
  453.             switch (a.action) {
  454.                 case 'add':
  455.                     this.mMemoryCalendar.addItem(a.item, a.listener);
  456.                     break;
  457.                 case 'modify':
  458.                     this.mMemoryCalendar.modifyItem(a.newItem, a.oldItem,
  459.                                                     a.listener);
  460.                     break;
  461.                 case 'delete':
  462.                     this.mMemoryCalendar.deleteItem(a.item, a.listener);
  463.                     break;
  464.             }
  465.         }
  466.         if (hasItems)
  467.             this.writeICS();
  468.     },
  469.  
  470.     lock: function () {
  471.         this.locked = true;
  472.     },
  473.  
  474.     unlock: function () {
  475.         this.locked = false;
  476.         this.processQueue();
  477.     },
  478.     
  479.     isLocked: function () {
  480.         return this.locked;
  481.     },
  482.  
  483.     startBatch: function ()
  484.     {
  485.         this.mObserver.onStartBatch();
  486.     },
  487.     endBatch: function ()
  488.     {
  489.         this.mObserver.onEndBatch();
  490.     },
  491.  
  492.     // nsIInterfaceRequestor impl
  493.     getInterface: function(iid, instance) {
  494.         if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
  495.             return new calAuthPrompt();
  496.         }
  497.         else if (iid.equals(Components.interfaces.nsIPrompt)) {
  498.             // use the window watcher service to get a nsIPrompt impl
  499.             return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  500.                              .getService(Components.interfaces.nsIWindowWatcher)
  501.                              .getNewPrompter(null);
  502.         }
  503.         Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
  504.         return null;
  505.     },
  506.  
  507.     /**
  508.      * Make a backup of the (remote) calendar
  509.      *
  510.      * This will download the remote file into the profile dir.
  511.      * It should be called before every upload, so every change can be
  512.      * restored. By default, it will keep 3 backups. It also keeps one
  513.      * file each day, for 3 days. That way, even if the user doesn't notice
  514.      * the remote calendar has become corrupted, he will still loose max 1
  515.      * day of work.
  516.      * After the back up is finished, will call aCallback.
  517.      *
  518.      * @param aCallback
  519.      *           Function that will be calles after the backup is finished.
  520.      *           will be called in the original context in which makeBackup
  521.      *           was called
  522.      */
  523.     makeBackup: function(aCallback) {
  524.         // Uses |pseudoID|, an id of the calendar, defined below
  525.         function makeName(type) {
  526.             return 'calBackupData_'+pseudoID+'_'+type+'.ics';
  527.         }
  528.         
  529.         // This is a bit messy. createUnique creates an empty file,
  530.         // but we don't use that file. All we want is a filename, to be used
  531.         // in the call to copyTo later. So we create a file, get the filename,
  532.         // and never use the file again, but write over it.
  533.         // Using createUnique anyway, because I don't feel like 
  534.         // re-implementing it
  535.         function makeDailyFileName() {
  536.             var dailyBackupFile = backupDir.clone();
  537.             dailyBackupFile.append(makeName('day'));
  538.             dailyBackupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
  539.             dailyBackupFileName = dailyBackupFile.leafName;
  540.  
  541.             // Remove the reference to the nsIFile, because we need to
  542.             // write over the file later, and you never know what happens
  543.             // if something still has a reference.
  544.             // Also makes it explicit that we don't need the file itself,
  545.             // just the name.
  546.             dailyBackupFile = null;
  547.  
  548.             return dailyBackupFileName;
  549.         }
  550.  
  551.         function purgeBackupsByType(files, type) {
  552.             // filter out backups of the type we care about.
  553.             var filteredFiles = files.filter(
  554.                 function f(v) { 
  555.                     return (v.name.indexOf("calBackupData_"+pseudoID+"_"+type) != -1)
  556.                 });
  557.             // Sort by lastmodifed
  558.             filteredFiles.sort(
  559.                 function s(a,b) {
  560.                     return (a.lastmodified - b.lastmodified);
  561.                 });
  562.             // And delete the oldest files, and keep the desired number of
  563.             // old backups
  564.             var i;
  565.             for (i = 0; i < filteredFiles.length - numBackupFiles; ++i) {
  566.                 file = backupDir.clone();
  567.                 file.append(filteredFiles[i].name);
  568.  
  569.                 // This can fail because of some crappy code in nsILocalFile.
  570.                 // That's not the end of the world.  We can try to remove the
  571.                 // file the next time around.
  572.                 try {
  573.                     file.remove(false);
  574.                 } catch(ex) {}
  575.             }
  576.             return;
  577.         }
  578.  
  579.         function purgeOldBackups() {
  580.             // Enumerate files in the backupdir for expiry of old backups
  581.             var dirEnum = backupDir.directoryEntries;
  582.             var files = [];
  583.             while (dirEnum.hasMoreElements()) {
  584.                 var file = dirEnum.getNext().QueryInterface(CI.nsIFile);
  585.                 if (file.isFile()) {
  586.                     files.push({name: file.leafName, lastmodified: file.lastModifiedTime});
  587.                 }
  588.             }
  589.  
  590.             if (doDailyBackup)
  591.                 purgeBackupsByType(files, 'day');
  592.             else
  593.                 purgeBackupsByType(files, 'edit');
  594.  
  595.             return;
  596.         }
  597.         
  598.         function copyToOverwriting(oldFile, newParentDir, newName) {
  599.             try {
  600.                 var newFile = newParentDir.clone();
  601.                 newFile.append(newName);
  602.             
  603.                 if (newFile.exists()) {
  604.                     newFile.remove(false);
  605.                 }
  606.                 oldFile.copyTo(newParentDir, newName);
  607.             } catch(e) {
  608.                 Components.utils.reportError("Backup failed, no copy:"+e);
  609.                 // Error in making a daily/initial backup.
  610.                 // not fatal, so just continue
  611.             }
  612.         }
  613.  
  614.         function getIntPrefSafe(prefName, defaultValue)
  615.         {
  616.             try {
  617.                 var prefValue = backupBranch.getIntPref(prefName);
  618.                 return prefValue;
  619.             }
  620.             catch (ex) {
  621.                 return defaultValue;
  622.             }
  623.         }
  624.         var backupDays = getIntPrefSafe("days", 1);
  625.         var numBackupFiles = getIntPrefSafe("filenum", 3);
  626.  
  627.         try {
  628.             var dirService = Components.classes["@mozilla.org/file/directory_service;1"]
  629.                                        .getService(CI.nsIProperties);
  630.             var backupDir = dirService.get("ProfD", CI.nsILocalFile);
  631.             backupDir.append("backupData");
  632.             if (!backupDir.exists()) {
  633.                 backupDir.create(CI.nsIFile.DIRECTORY_TYPE, 0755);
  634.             }
  635.         } catch(e) {
  636.             // Backup dir wasn't found. Likely because we are running in
  637.             // xpcshell. Don't die, but continue the upload.
  638.             LOG("Backup failed, no backupdir:"+e);
  639.             aCallback.call(this);
  640.             return;
  641.         }
  642.  
  643.         try {
  644.             var pseudoID = getCalendarManager().getCalendarPref(this, "UNIQUENUM");
  645.             if (!pseudoID) {
  646.                 pseudoID = new Date().getTime();
  647.                 getCalendarManager().setCalendarPref(this, "UNIQUENUM", pseudoID);
  648.             }
  649.         } catch(e) {
  650.             // calendarmgr not found. Likely because we are running in
  651.             // xpcshell. Don't die, but continue the upload.
  652.             LOG("Backup failed, no calendarmanager:"+e);
  653.             aCallback.call(this);
  654.             return;
  655.         }
  656.  
  657.         var doInitialBackup = false;
  658.         var initialBackupFile = backupDir.clone();
  659.         initialBackupFile.append(makeName('initial'));
  660.         if (!initialBackupFile.exists())
  661.             doInitialBackup = true;
  662.  
  663.         var doDailyBackup = false;
  664.         var backupTime = new Number(getCalendarManager().
  665.                                        getCalendarPref(this, 'backup-time'));
  666.         if (!backupTime ||
  667.             (new Date().getTime() > backupTime + backupDays*24*60*60*1000)) {
  668.             // It's time do to a daily backup
  669.             doDailyBackup = true;
  670.             getCalendarManager().setCalendarPref(this, 'backup-time', 
  671.                                                  new Date().getTime());
  672.         }
  673.  
  674.         var dailyBackupFileName;
  675.         if (doDailyBackup) {
  676.             dailyBackupFileName = makeDailyFileName(backupDir);
  677.         }
  678.  
  679.         var backupFile = backupDir.clone();
  680.         backupFile.append(makeName('edit'));
  681.         backupFile.createUnique(CI.nsIFile.NORMAL_FILE_TYPE, 0600);
  682.         
  683.         purgeOldBackups();
  684.  
  685.         // Now go download the remote file, and store it somewhere local.
  686.         var ioService = Components.classes["@mozilla.org/network/io-service;1"]
  687.                                   .getService(CI.nsIIOService);
  688.         var channel = ioService.newChannelFromURI(this.mUri);
  689.         channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
  690.         channel.notificationCallbacks = this;
  691.  
  692.         var downloader = Components.classes["@mozilla.org/network/downloader;1"]
  693.                                    .createInstance(CI.nsIDownloader);
  694.  
  695.         var savedthis = this;
  696.         var listener = {
  697.             onDownloadComplete: function(downloader, request, ctxt, status, result) {
  698.                 if (doInitialBackup)
  699.                     copyToOverwriting(result, backupDir, makeName('initial'));
  700.                 if (doDailyBackup)
  701.                     copyToOverwriting(result, backupDir, dailyBackupFileName);
  702.  
  703.                 aCallback.call(savedthis);
  704.             },
  705.         }
  706.  
  707.         downloader.init(listener, backupFile);
  708.         try {
  709.             channel.asyncOpen(downloader, null);
  710.         } catch(e) {
  711.             // For local files, asyncOpen throws on new (calendar) files
  712.             // No problem, go and upload something
  713.             LOG("Backup failed in asyncOpen:"+e);
  714.             aCallback.call(this);
  715.             return;
  716.         }
  717.  
  718.         return;
  719.     }
  720. };
  721.  
  722. function calICSObserver(aCalendar) {
  723.     this.mCalendar = aCalendar;
  724.     this.mObservers = new Array();
  725. }
  726.  
  727. calICSObserver.prototype = {
  728.     mCalendar: null,
  729.     mInBatch: false,
  730.  
  731.     // calIObserver:
  732.     onStartBatch: function() {
  733.         for (var i = 0; i < this.mObservers.length; i++)
  734.             this.mObservers[i].onStartBatch();
  735.         this.mInBatch = true;
  736.     },
  737.     onEndBatch: function() {
  738.         for (var i = 0; i < this.mObservers.length; i++)
  739.             this.mObservers[i].onEndBatch();
  740.  
  741.         this.mInBatch = false;
  742.     },
  743.     onLoad: function() {
  744.         for (var i = 0; i < this.mObservers.length; i++)
  745.             this.mObservers[i].onLoad();
  746.     },
  747.     onAddItem: function(aItem) {
  748.         for (var i = 0; i < this.mObservers.length; i++)
  749.             this.mObservers[i].onAddItem(aItem);
  750.     },
  751.     onModifyItem: function(aNewItem, aOldItem) {
  752.         for (var i = 0; i < this.mObservers.length; i++)
  753.             this.mObservers[i].onModifyItem(aNewItem, aOldItem);
  754.     },
  755.     onDeleteItem: function(aDeletedItem) {
  756.         for (var i = 0; i < this.mObservers.length; i++)
  757.             this.mObservers[i].onDeleteItem(aDeletedItem);
  758.     },
  759.  
  760.     // Unless an error number is in this array, we consider it very bad, set
  761.     // the calendar to readOnly, and give up.
  762.     acceptableErrorNums: [],
  763.  
  764.     onError: function(aErrNo, aMessage) {
  765.         var errorIsOk = false;
  766.         for each (num in this.acceptableErrorNums) {
  767.             if (num == aErrNo) {
  768.                 errorIsOk = true;
  769.                 break;
  770.             }
  771.         }
  772.         if (!errorIsOk)
  773.             this.mCalendar.readOnly = true;
  774.         for (var i = 0; i < this.mObservers.length; i++)
  775.             this.mObservers[i].onError(aErrNo, aMessage);
  776.     },
  777.  
  778.     // This observer functions as proxy for all the other observers
  779.     // So need addObserver and removeObserver here
  780.     addObserver: function (aObserver) {
  781.         for each (obs in this.mObservers) {
  782.             if (obs == aObserver)
  783.                 return;
  784.         }
  785.  
  786.         this.mObservers.push(aObserver);
  787.     },
  788.  
  789.     removeObserver: function (aObserver) {
  790.         var newObservers = Array();
  791.         for each (obs in this.mObservers) {
  792.             if (obs != aObserver)
  793.                 newObservers.push(obs);
  794.         }
  795.         this.mObservers = newObservers;
  796.     }
  797. };
  798.  
  799. /***************************
  800.  * Transport Abstraction Hooks
  801.  *
  802.  * Those hooks provide a way to do checks before or after publishing an
  803.  * ics file. The main use will be to check etags (or some other way to check
  804.  * for remote changes) to protect remote changes from being overwritten.
  805.  *
  806.  * Different protocols need different checks (webdav can do etag, but
  807.  * local files need last-modified stamps), hence different hooks for each
  808.  * types
  809.  */
  810.  
  811. // dummyHooks are for transport types that don't have hooks of their own.
  812. // Also serves as poor-mans interface definition.
  813. function dummyHooks() {
  814. }
  815.  
  816. dummyHooks.prototype = {
  817.     onBeforeGet: function(aChannel) {
  818.         return true;
  819.     },
  820.     
  821.     /**
  822.      * @return
  823.      *     a boolean, false if the previous data should be used (the datastore
  824.      *     didn't change, there might be no data in this GET), true in all
  825.      *     other cases
  826.      */
  827.     onAfterGet: function() {
  828.         return true;
  829.     },
  830.  
  831.     onBeforePut: function(aChannel) {
  832.         return true;
  833.     },
  834.     
  835.     onAfterPut: function(aChannel) {
  836.         return true;
  837.     },
  838. }
  839.  
  840. function httpHooks() {
  841.     this.mChannel = null;
  842. }
  843.  
  844. httpHooks.prototype = {
  845.     onBeforeGet: function(aChannel) {
  846.         this.mChannel = aChannel;
  847.         if (this.mEtag) {
  848.             var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  849.             // Somehow the webdav header 'If' doesn't work on apache when
  850.             // passing in a Not, so use the http version here.
  851.             httpchannel.setRequestHeader("If-None-Match", this.mEtag, false);
  852.         }
  853.  
  854.         return true;
  855.     },
  856.     
  857.     onAfterGet: function() {
  858.         var httpchannel = this.mChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  859.  
  860.         // 304: Not Modified
  861.         // Can use the old data, so tell the caller that it can skip parsing.
  862.         if (httpchannel.responseStatus == 304)
  863.             return false;
  864.  
  865.         // 404: Not Found
  866.         // This is a new calendar. Shouldn't try to parse it. But it also
  867.         // isn't a failure, so don't throw.
  868.         if (httpchannel.responseStatus == 404)
  869.             return false;
  870.  
  871.         try {
  872.             this.mEtag = httpchannel.getResponseHeader("ETag");
  873.         } catch(e) {
  874.             // No etag header. Now what?
  875.             this.mEtag = null;
  876.         }
  877.         this.mChannel = null;
  878.         return true;
  879.     },
  880.  
  881.     onBeforePut: function(aChannel) {
  882.         if (this.mEtag) {
  883.             var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  884.  
  885.             // Apache doesn't work correctly with if-match on a PUT method,
  886.             // so use the webdav header
  887.             httpchannel.setRequestHeader("If", '(['+this.mEtag+'])', false);
  888.         }
  889.         return true;
  890.     },
  891.     
  892.     onAfterPut: function(aChannel) {
  893.         var httpchannel = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
  894.         try {
  895.             this.mEtag = httpchannel.getResponseHeader("ETag");
  896.         } catch(e) {
  897.             // There was no ETag header on the response. This means that
  898.             // putting is not atomic. This is bad. Race conditions can happen,
  899.             // because there is a time in which we don't know the right
  900.             // etag.
  901.             // Try to do the best we can, by immediatly getting the etag.
  902.             
  903.             // Only on branch, because webdav doesn't work on trunk: bug 332840
  904.             if (isOnBranch) {
  905.                 var res = new WebDavResource(aChannel.URI);
  906.                 var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  907.                                        .getService(Components.interfaces.nsIWebDAVService);
  908.                 // The namespace is 'DAV:', not just 'DAV'.
  909.                 webSvc.getResourceProperties(res, 1, ['DAV: getetag'], false,
  910.                                              this, null, null);
  911.             } else {
  912.                 // instead, on trunk, set mEtag to null, so it will be ignored on 
  913.                 // the next GET/PUT
  914.                 this.mEtag = null;
  915.             }
  916.         }
  917.         return true;
  918.     },
  919.  
  920.     onOperationComplete: function(aStatusCode, aResource, aOperation, aClosure) {
  921.     },
  922.  
  923.     onOperationDetail: function(aStatusCode, aResource, aOperation, aDetail, aClosure) {
  924.         var props = aDetail.QueryInterface(Components.interfaces.nsIProperties);
  925.         try {
  926.             this.mEtag = props.get('DAV: getetag', Components.interfaces.nsISupportsString).toString();
  927.         } catch(e) {
  928.             // No etag header. Now what?
  929.             this.mEtag = null;
  930.         }
  931.     },
  932. }
  933.  
  934. function WebDavResource(url) {
  935.     this.mResourceURL = url;
  936. }
  937.  
  938. WebDavResource.prototype = {
  939.     mResourceURL: {},
  940.     get resourceURL() { return this.mResourceURL; },
  941.     QueryInterface: function(iid) {
  942.         if (iid.equals(CI.nsIWebDAVResource) ||
  943.             iid.equals(CI.nsISupports)) {
  944.             return this;
  945.         }
  946.         throw Components.interfaces.NS_ERROR_NO_INTERFACE;
  947.     }
  948. };
  949.  
  950. /****
  951.  **** module registration
  952.  ****/
  953.  
  954. var calICSCalendarModule = {
  955.  
  956.     mCID: Components.ID("{f8438bff-a3c9-4ed5-b23f-2663b5469abf}"),
  957.     mContractID: "@mozilla.org/calendar/calendar;1?type=ics",
  958.  
  959.     mUtilsLoaded: false,
  960.     loadUtils: function icsLoadUtils() {
  961.         if (this.mUtilsLoaded)
  962.             return;
  963.  
  964.         const jssslContractID = "@mozilla.org/moz/jssubscript-loader;1";
  965.         const jssslIID = Components.interfaces.mozIJSSubScriptLoader;
  966.  
  967.         const iosvcContractID = "@mozilla.org/network/io-service;1";
  968.         const iosvcIID = Components.interfaces.nsIIOService;
  969.  
  970.         var loader = Components.classes[jssslContractID].getService(jssslIID);
  971.         var iosvc = Components.classes[iosvcContractID].getService(iosvcIID);
  972.  
  973.         // Note that unintuitively, __LOCATION__.parent == .
  974.         // We expect to find utils in ./../js
  975.         var appdir = __LOCATION__.parent.parent;
  976.         appdir.append("js");
  977.         var scriptName = "calUtils.js";
  978.  
  979.         var f = appdir.clone();
  980.         f.append(scriptName);
  981.  
  982.         try {
  983.             var fileurl = iosvc.newFileURI(f);
  984.             loader.loadSubScript(fileurl.spec, this.__parent__.__parent__);
  985.         } catch (e) {
  986.             dump("Error while loading " + fileurl.spec + "\n");
  987.             throw e;
  988.         }
  989.  
  990.         this.mUtilsLoaded = true;
  991.     },
  992.     
  993.     registerSelf: function (compMgr, fileSpec, location, type) {
  994.         compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
  995.         compMgr.registerFactoryLocation(this.mCID,
  996.                                         "Calendar ICS provider",
  997.                                         this.mContractID,
  998.                                         fileSpec,
  999.                                         location,
  1000.                                         type);
  1001.     },
  1002.  
  1003.     getClassObject: function (compMgr, cid, iid) {
  1004.         if (!cid.equals(this.mCID))
  1005.             throw Components.results.NS_ERROR_NO_INTERFACE;
  1006.  
  1007.         if (!iid.equals(Components.interfaces.nsIFactory))
  1008.             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1009.  
  1010.         this.loadUtils();
  1011.  
  1012.         return this.mFactory;
  1013.     },
  1014.  
  1015.     mFactory: {
  1016.         createInstance: function (outer, iid) {
  1017.             if (outer != null)
  1018.                 throw Components.results.NS_ERROR_NO_AGGREGATION;
  1019.             return (new calICSCalendar()).QueryInterface(iid);
  1020.         }
  1021.     },
  1022.  
  1023.     canUnload: function(compMgr) {
  1024.         return true;
  1025.     }
  1026. };
  1027.  
  1028. function NSGetModule(compMgr, fileSpec) {
  1029.     return calICSCalendarModule;
  1030. }
  1031.