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 / calDavCalendar.js < prev    next >
Encoding:
Text File  |  2007-05-23  |  56.1 KB  |  1,528 lines

  1. /* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
  2. /* ***** BEGIN LICENSE BLOCK *****
  3.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4.  *
  5.  * The contents of this file are subject to the Mozilla Public License Version
  6.  * 1.1 (the "License"); you may not use this file except in compliance with
  7.  * the License. You may obtain a copy of the License at
  8.  * http://www.mozilla.org/MPL/
  9.  *
  10.  * Software distributed under the License is distributed on an "AS IS" basis,
  11.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12.  * for the specific language governing rights and limitations under the
  13.  * License.
  14.  *
  15.  * The Original Code is Oracle Corporation code.
  16.  *
  17.  * The Initial Developer of the Original Code is
  18.  *  Oracle Corporation
  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.  *   Mike Shaver <mike.x.shaver@oracle.com>
  26.  *   Gary van der Merwe <garyvdm@gmail.com>
  27.  *   Bruno Browning <browning@uwalumni.com>
  28.  *   Matthew Willis <lilmatt@mozilla.com>
  29.  *
  30.  * Alternatively, the contents of this file may be used under the terms of
  31.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  32.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  33.  * in which case the provisions of the GPL or the LGPL are applicable instead
  34.  * of those above. If you wish to allow use of your version of this file only
  35.  * under the terms of either the GPL or the LGPL, and not to allow others to
  36.  * use your version of this file under the terms of the MPL, indicate your
  37.  * decision by deleting the provisions above and replace them with the notice
  38.  * and other provisions required by the GPL or the LGPL. If you do not delete
  39.  * the provisions above, a recipient may use your version of this file under
  40.  * the terms of any one of the MPL, the GPL or the LGPL.
  41.  *
  42.  * ***** END LICENSE BLOCK ***** */
  43.  
  44. //
  45. // calDavCalendar.js
  46. //
  47.  
  48. // XXXdmose deal with generation goop
  49.  
  50. // XXXdmose need to re-query for add & modify to get up-to-date items
  51.  
  52. // XXXdmose deal with locking
  53.  
  54. // XXXdmose need to make and use better error reporting interface for webdav
  55. // (all uses of aStatusCode, probably)
  56.  
  57. // XXXdmose use real calendar result codes, not NS_ERROR_FAILURE for everything
  58.  
  59. const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n';
  60.  
  61. function calDavCalendar() {
  62.     this.wrappedJSObject = this;
  63.     this.mObservers = Array();
  64.     this.unmappedProperties = [];
  65.     this.mUriParams = null;
  66.     this.mEtagCache = Array();
  67. }
  68.  
  69. // some shorthand
  70. const nsIWebDAVOperationListener = 
  71.     Components.interfaces.nsIWebDAVOperationListener;
  72. const calICalendar = Components.interfaces.calICalendar;
  73. const nsISupportsCString = Components.interfaces.nsISupportsCString;
  74. const calIErrors = Components.interfaces.calIErrors;
  75.  
  76. var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
  77.                         .getService(Components.interfaces.nsIXULAppInfo);
  78. var isOnBranch = appInfo.platformVersion.indexOf("1.8") == 0;
  79.  
  80.  
  81. function makeOccurrence(item, start, end)
  82. {
  83.     var occ = item.createProxy();
  84.     occ.recurrenceId = start;
  85.     occ.startDate = start;
  86.     occ.endDate = end;
  87.  
  88.     return occ;
  89. }
  90.   
  91. // END_OF_TIME needs to be the max value a PRTime can be
  92. const START_OF_TIME = -0x7fffffffffffffff;
  93. const END_OF_TIME = 0x7fffffffffffffff;
  94.  
  95. // used by mAuthenticationStatus
  96. const kCaldavNoAuthentication = 0;
  97. const kCaldavFirstRequestSent = 1;      // Queueing subsequent requests
  98. const kCaldavFreshlyAuthenticated = 2;  // Need to process queue
  99. const kCaldavAuthenticated = 3;         // Queue being processed or empty
  100.  
  101. // used in checking calendar URI for (Cal)DAV-ness
  102. const kDavResourceTypeNone = 0;
  103. const kDavResourceTypeCollection = 1;
  104. const kDavResourceTypeCalendar = 2;
  105.  
  106. // used for etag checking
  107. const CALDAV_ADOPT_ITEM = 1;
  108. const CALDAV_MODIFY_ITEM = 2;
  109. const CALDAV_DELETE_ITEM = 3;
  110. const CALDAV_CACHE_ETAG = 4;
  111.  
  112. calDavCalendar.prototype = {
  113.     //
  114.     // nsISupports interface
  115.     // 
  116.     QueryInterface: function (aIID) {
  117.         if (!aIID.equals(Components.interfaces.nsISupports) &&
  118.             !aIID.equals(Components.interfaces.calICalendarProvider) &&
  119.             !aIID.equals(Components.interfaces.calICalendar) &&
  120.             !aIID.equals(Components.interfaces.nsIInterfaceRequestor)) {
  121.             throw Components.results.NS_ERROR_NO_INTERFACE;
  122.         }
  123.  
  124.         return this;
  125.     },
  126.  
  127.     //
  128.     // calICalendarProvider interface
  129.     //
  130.     get prefChromeOverlay() {
  131.         return null;
  132.     },
  133.  
  134.     get displayName() {
  135.         return calGetString("calendar", "caldavName");
  136.     },
  137.  
  138.     createCalendar: function caldav_createCal() {
  139.         throw NS_ERROR_NOT_IMPLEMENTED;
  140.     },
  141.  
  142.     deleteCalendar: function caldav_deleteCal(cal, listener) {
  143.         throw NS_ERROR_NOT_IMPLEMENTED;
  144.     },
  145.  
  146.     //
  147.     // calICalendar interface
  148.     //
  149.  
  150.     // attribute AUTF8String id;
  151.     mID: null,
  152.     get id() {
  153.         return this.mID;
  154.     },
  155.     set id(id) {
  156.         if (this.mID)
  157.             throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
  158.         return (this.mID = id);
  159.     },
  160.  
  161.     // attribute AUTF8String name;
  162.     get name() {
  163.         return getCalendarManager().getCalendarPref(this, "NAME");
  164.     },
  165.     set name(name) {
  166.         getCalendarManager().setCalendarPref(this, "NAME", name);
  167.     },
  168.  
  169.     // readonly attribute AUTF8String type;
  170.     get type() { return "caldav"; },
  171.  
  172.     mReadOnly: false,
  173.  
  174.     get readOnly() { 
  175.         return this.mReadOnly;
  176.     },
  177.     set readOnly(bool) {
  178.         this.mReadOnly = bool;
  179.     },
  180.  
  181.     // attribute PRUInt8 mAuthenticationStatus;
  182.     mAuthenticationStatus: 0,
  183.  
  184.     mPendingStartupRequests: Array(),
  185.  
  186.     get canRefresh() {
  187.         // refresh() is currently not implemented, but we may want to change that
  188.         return false;
  189.     },
  190.  
  191.     // mUriParams stores trailing ?parameters from the
  192.     // supplied calendar URI. Needed for (at least) Cosmo
  193.     // tickets
  194.     mUriParams: null,
  195.  
  196.     // attribute nsIURI uri;
  197.     mUri: null,
  198.     get uri() { return this.mUri; },
  199.     set uri(aUri) { this.mUri = aUri; },
  200.  
  201.     get mCalendarUri() { 
  202.         calUri = this.mUri.clone();
  203.         var parts = calUri.spec.split('?');
  204.         if (parts.length > 1) {
  205.             calUri.spec = parts.shift();
  206.             this.mUriParams = '?' + parts.join('?');
  207.         }
  208.         if (calUri.spec.charAt(calUri.spec.length-1) != '/') {
  209.             calUri.spec += "/";
  210.         }
  211.         return calUri;
  212.     },
  213.     
  214.     mMakeUri: function caldav_makeUri(aInsertString) {
  215.         var spec = this.mCalendarUri.spec + aInsertString;
  216.         if (this.mUriParams) {
  217.             return spec + this.mUriParams;
  218.         }
  219.         return spec;
  220.     },
  221.  
  222.     get mLocationPath() {
  223.         return decodeURIComponent(this.mCalendarUri.path);
  224.     },
  225.  
  226.     refresh: function() {
  227.         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  228.     },
  229.  
  230.     // attribute boolean suppressAlarms;
  231.     get suppressAlarms() { return false; },
  232.     set suppressAlarms(aSuppressAlarms) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; },
  233.  
  234.     get sendItipInvitations() { return true; },
  235.  
  236.     // void addObserver( in calIObserver observer );
  237.     addObserver: function (aObserver, aItemFilter) {
  238.         for each (obs in this.mObservers) {
  239.             if (compareObjects(obs, aObserver, Ci.calIObserver)) {
  240.                 return;
  241.             }
  242.         }
  243.  
  244.         this.mObservers.push(aObserver);
  245.     },
  246.  
  247.     // void removeObserver( in calIObserver observer );
  248.     removeObserver: function (aObserver) {
  249.         this.mObservers = this.mObservers.filter(
  250.             function(o) {return (o != aObserver);});
  251.     },
  252.  
  253.     /**
  254.      * Fetches etag from server and compares with local cached version
  255.      * before we add/adopt/modify/delete item.
  256.      *
  257.      * @param aMethod     requested method (adopt/modify/delete)
  258.      * @param aItem       item to check
  259.      * @param aListener   listener from original request
  260.      * @param aOldItem    aOldItem argument in modifyItem requests
  261.      */
  262.     fetchEtag: function caldavFE(aMethod, aItem, aListener, aOldItem) {
  263.         if (this.readOnly) {
  264.             throw calIErrors.CAL_IS_READONLY;
  265.         }
  266.  
  267.         var serverEtag = null;
  268.         var listener = new WebDavListener();
  269.  
  270.         var thisCalendar = this;
  271.  
  272.         var itemUri = this.mCalendarUri.clone();
  273.  
  274.         try {
  275.             itemUri.spec = this.mMakeUri(aItem.getProperty("X-MOZ-LOCATIONPATH"));
  276.             LOG("using X-MOZ-LOCATIONPATH: " + itemUri.spec);
  277.         } catch (ex) {
  278.             // XXX how are we REALLY supposed to figure this out?
  279.             itemUri.spec = this.mMakeUri(aItem.id + ".ics");
  280.         }
  281.  
  282.         var itemResource = new WebDavResource(itemUri);
  283.  
  284.         listener.onOperationComplete = function OOC(aStatusCode,
  285.                                                     aResource,
  286.                                                     aOperation,
  287.                                                     aClosure)
  288.         {
  289.             var mismatch = (serverEtag != thisCalendar.mEtagCache[aItem.id]);
  290.  
  291.             switch (aMethod) {
  292.                 case CALDAV_ADOPT_ITEM:
  293.                     if (serverEtag != null) {
  294.                         // The server thinks it already has an item we want to
  295.                         // create as new. This either a server error or we're
  296.                         // trying to copy an item onto itself; either way the
  297.                         // safe thing to do is abort the operation.
  298.                         LOG("CalDAV: non-null etag in adoptItem");
  299.                         thisCalendar.readOnly = true;
  300.                     } else {
  301.                         thisCalendar.performAdoptItem(aItem, aListener);
  302.                     }
  303.                     break;
  304.  
  305.                 case CALDAV_MODIFY_ITEM:
  306.                     if (mismatch) {
  307.                         LOG("CalDAV: etag mismatch in modifyItem");
  308.                         thisCalendar.promptOverwrite(aMethod, aItem,
  309.                                                      aListener, aOldItem);
  310.                     } else {
  311.                         thisCalendar.performModifyItem(aItem, aOldItem, aListener);
  312.                     }
  313.                     break;
  314.  
  315.                 case CALDAV_DELETE_ITEM:
  316.                     if (mismatch) {
  317.                         LOG("CalDAV: etag mismatch in deleteItem");
  318.                         thisCalendar.promptOverwrite(aMethod, aItem,
  319.                                                      aListener, null);
  320.                     } else {
  321.                         thisCalendar.performDeleteItem(aItem, aListener);
  322.                     }
  323.                     break;
  324.  
  325.                 default:
  326.                     thisCalendar.mEtagCache[aItem.id] = serverEtag;
  327.                     break;
  328.             }
  329.         }
  330.  
  331.         listener.onOperationDetail = function OOD(aStatusCode, aResource,
  332.                                                   aOperation, aDetail,
  333.                                                   aClosure) {
  334.             var props = aDetail.QueryInterface(Ci.nsIProperties);
  335.             serverEtag = props.get("DAV: getetag",
  336.                                    Ci.nsISupportsString).toString();
  337.         }
  338.  
  339.         var webdavSvc = Cc['@mozilla.org/webdav/service;1'].
  340.                         getService(Ci.nsIWebDAVService);
  341.         webdavSvc.getResourceProperties(itemResource, 1, ["DAV: getetag"],
  342.                                         false, listener, this, null);
  343.     },
  344.  
  345.     promptOverwrite: function caldavPO(aMethod, aItem, aListener, aOldItem) {
  346.         var promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].
  347.                             getService(Ci.nsIPromptService);
  348.  
  349.         var promptTitle = calGetString("calendar", "itemModifiedOnServerTitle");
  350.         var promptMessage = calGetString("calendar", "itemModifiedOnServer");
  351.         var buttonLabel1;
  352.  
  353.         if (aMethod == CALDAV_MODIFY_ITEM) {
  354.             promptMessage += calGetString("calendar", "modifyWillLoseData");
  355.             buttonLabel1 = calGetString("calendar", "proceedModify");
  356.         } else {
  357.             promptMessage += calGetString("calendar", "deleteWillLoseData");
  358.             buttonLabel1 = calGetString("calendar", "proceedDelete");
  359.         }
  360.         
  361.         var buttonLabel2 = calGetString("calendar", "updateFromServer");
  362.  
  363.         var flags = promptService.BUTTON_TITLE_IS_STRING *
  364.                     promptService.BUTTON_POS_0 +
  365.                     promptService.BUTTON_TITLE_IS_STRING *
  366.                     promptService.BUTTON_POS_1;
  367.  
  368.         var choice = promptService.confirmEx(null, promptTitle, promptMessage,
  369.                                              flags, buttonLabel1, buttonLabel2,
  370.                                              null, null, {});
  371.  
  372.         if (choice == 0) {
  373.             if (aMethod == CALDAV_MODIFY_ITEM) {
  374.                 this.performModifyItem(aItem, aOldItem, aListener);
  375.             } else {
  376.                 this.performDeleteItem(aItem, aListener);
  377.             }
  378.         } else {
  379.             this.getUpdatedItem(aItem, aListener);
  380.         }
  381.  
  382.     },
  383.  
  384.     mEtagCache: Array(),
  385.  
  386.     /**
  387.      * addItem() is required by the IDL, but simply calls adoptItem().
  388.      * Actually adding the item to the CalDAV store takes place in
  389.      * performAdoptItem().
  390.      *
  391.      * @param aItem       item to check
  392.      * @param aListener   listener for method completion
  393.      */
  394.     addItem: function caldavAI(aItem, aListener) {
  395.         var newItem = aItem.clone();
  396.         return this.adoptItem(newItem, aListener);
  397.     },
  398.     
  399.     /**
  400.      * Sends data regarding the requested new item off for etag checking.
  401.      *
  402.      * @param aItem       item to check
  403.      * @param aListener   listener for method completion
  404.      */
  405.     adoptItem: function caldavAI2(aItem, aListener) {
  406.         this.fetchEtag(CALDAV_ADOPT_ITEM, aItem, aListener, null);
  407.     },
  408.  
  409.     /**
  410.      * Performs the actual addition of the item to CalDAV store, after etag
  411.      * checking.
  412.      *
  413.      * @param aItem       item to check
  414.      * @param aListener   listener for method completion
  415.      */
  416.     performAdoptItem: function caldavPAI(aItem, aListener) {
  417.         if (aItem.id == null && aItem.isMutable) {
  418.             aItem.id = getUUID();
  419.         }
  420.  
  421.         if (aItem.id == null) {
  422.             if (aListener)
  423.                 aListener.onOperationComplete (this,
  424.                                                Components.results.NS_ERROR_FAILURE,
  425.                                                aListener.ADD,
  426.                                                aItem.id,
  427.                                                "Can't set ID on non-mutable item to addItem");
  428.             return;
  429.         }
  430.  
  431.         // XXX how are we REALLY supposed to figure this out?
  432.         var locationPath = aItem.id + ".ics";
  433.         var itemUri = this.mCalendarUri.clone();
  434.         itemUri.spec = this.mMakeUri(locationPath);
  435.         LOG("itemUri.spec = " + itemUri.spec);
  436.         var eventResource = new WebDavResource(itemUri);
  437.  
  438.         var listener = new WebDavListener();
  439.         var thisCalendar = this;
  440.         listener.onOperationComplete = 
  441.         function onPutComplete(aStatusCode, aResource, aOperation, aClosure) {
  442.  
  443.             // 201 = HTTP "Created"
  444.             //
  445.             if (aStatusCode == 201) {
  446.                 LOG("Item added successfully");
  447.  
  448.                 var retVal = Components.results.NS_OK;
  449.                 // CalDAV does not require that the etag returned on PUT will
  450.                 // be identical to the etag returned on later operations
  451.                 // (CalDAV 5.3.4), so we best re-query.
  452.                 thisCalendar.fetchEtag(CALDAV_CACHE_ETAG, aItem, null, null);
  453.  
  454.             } else if (aStatusCode == 200) {
  455.                 LOG("CalDAV: 200 received from server: server malfunction");
  456.                 retVal = Components.results.NS_ERROR_FAILURE;
  457.             } else {
  458.                 if (aStatusCode > 999) {
  459.                     aStatusCode = "0x" + aStatusCode.toString(16);
  460.                 }
  461.  
  462.                 // XXX real error handling
  463.                 LOG("Error adding item: " + aStatusCode);
  464.                 retVal = Components.results.NS_ERROR_FAILURE;
  465.             }
  466.  
  467.             // notify the listener
  468.             if (aListener) {
  469.                 try {
  470.                     aListener.onOperationComplete(thisCalendar,
  471.                                                   retVal,
  472.                                                   aListener.ADD,
  473.                                                   aItem.id,
  474.                                                   aItem);
  475.                 } catch (ex) {
  476.                     LOG("addItem's onOperationComplete threw an exception "
  477.                           + ex + "; ignoring");
  478.                 }
  479.             }
  480.  
  481.             // notify observers
  482.             if (Components.isSuccessCode(retVal)) {
  483.                 thisCalendar.observeAddItem(aItem);
  484.             }
  485.         }
  486.   
  487.         aItem.calendar = this;
  488.         aItem.generation = 1;
  489.         aItem.setProperty("X-MOZ-LOCATIONPATH", locationPath);
  490.         aItem.makeImmutable();
  491.  
  492.         LOG("icalString = " + aItem.icalString);
  493.  
  494.         // XXX use if not exists
  495.         // do WebDAV put
  496.         var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  497.             .getService(Components.interfaces.nsIWebDAVService);
  498.         webSvc.putFromString(eventResource, "text/calendar; charset=utf-8",
  499.                              aItem.icalString, listener, this, null);
  500.  
  501.         return;
  502.     },
  503.  
  504.     /**
  505.      * Sends info about the request to modify an item off for etag checking.
  506.      *
  507.      * @param aItem       item to check
  508.      * @param aOldItem    aOldItem argument in modifyItem requests
  509.      * @param aListener   listener from original request
  510.      */
  511.     modifyItem: function caldavMI(aItem, aOldItem, aListener) {
  512.         this.fetchEtag(CALDAV_MODIFY_ITEM, aItem, aListener, aOldItem);
  513.     },
  514.  
  515.     /**
  516.      * Modifies existing item in CalDAV store.
  517.      *
  518.      * @param aItem       item to check
  519.      * @param aOldItem    previous version of item to be modified
  520.      * @param aListener   listener from original request
  521.      */
  522.     performModifyItem: function caldavPMI(aNewItem, aOldItem, aListener) {
  523.  
  524.         if (aNewItem.id == null) {
  525.  
  526.             // XXXYYY fix to match iface spec
  527.             // this is definitely an error
  528.             if (aListener) {
  529.                 try {
  530.                     aListener.onOperationComplete(this,
  531.                                                   Components.results.NS_ERROR_FAILURE,
  532.                                                   aListener.MODIFY,
  533.                                                   aItem.id,
  534.                                                   "ID for modifyItem doesn't exist or is null");
  535.                 } catch (ex) {
  536.                     LOG("modifyItem's onOperationComplete threw an"
  537.                           + " exception " + ex + "; ignoring");
  538.                 }
  539.             }
  540.  
  541.             return;
  542.         }
  543.  
  544.         if (aNewItem.parentItem != aNewItem) {
  545.             aNewItem.parentItem.recurrenceInfo.modifyException(aNewItem);
  546.             aNewItem = aNewItem.parentItem;
  547.         }
  548.  
  549.         var eventUri = this.mCalendarUri.clone();
  550.         try {
  551.             eventUri.spec = this.mMakeUri(aNewItem.getProperty("X-MOZ-LOCATIONPATH"));
  552.             LOG("using X-MOZ-LOCATIONPATH: " + eventUri.spec);
  553.         } catch (ex) {
  554.             // XXX how are we REALLY supposed to figure this out?
  555.             eventUri.spec = this.mMakeUri(aNewItem.id + ".ics");
  556.         }
  557.  
  558.         // It seems redundant to use generation when we have etags
  559.         // but until the idl is changed we do it.
  560.         if (aOldItem.parentItem.generation != aNewItem.generation) {
  561.             if (aListener) {
  562.                 aListener.onOperationComplete (this.calendarToReturn,
  563.                                                Components.results.NS_ERROR_FAILURE,
  564.                                                aListener.MODIFY,
  565.                                                aNewItem.id,
  566.                                                "generation mismatch in modifyItem");
  567.             }
  568.             return;
  569.         }
  570.  
  571.         aNewItem.generation += 1;
  572.  
  573.         var eventResource = new WebDavResource(eventUri);
  574.  
  575.         var listener = new WebDavListener();
  576.         var thisCalendar = this;
  577.  
  578.         const icssvc = Cc["@mozilla.org/calendar/ics-service;1"].
  579.                        getService(Components.interfaces.calIICSService);
  580.         var modifiedItem = icssvc.createIcalComponent("VCALENDAR");
  581.         modifiedItem.prodid = "-//Mozilla Calendar//NONSGML Sunbird//EN";
  582.         modifiedItem.version = "2.0";
  583.         modifiedItem.addSubcomponent(aNewItem.icalComponent);
  584.         if (aNewItem.recurrenceInfo) {
  585.             var exceptions = aNewItem.recurrenceInfo.getExceptionIds({});
  586.             for each (var exc in exceptions) {
  587.                 modifiedItem.addSubcomponent(aNewItem.recurrenceInfo.getExceptionFor(exc, true).icalComponent);
  588.             }
  589.         }
  590.         var modifiedItemICS = modifiedItem.serializeToICS();
  591.  
  592.         listener.onOperationComplete = function(aStatusCode, aResource,
  593.                                                 aOperation, aClosure) {
  594.             // 201 = HTTP "Created"
  595.             // 204 = HTTP "No Content"
  596.             //
  597.             if (aStatusCode == 204 || aStatusCode == 201) {
  598.                 LOG("Item modified successfully.");
  599.                 var retVal = Components.results.NS_OK;
  600.                 // CalDAV does not require that the etag returned on PUT will
  601.                 // be identical to the etag returned on later operations
  602.                 // (CalDAV 5.3.4), so we best re-query.
  603.                 thisCalendar.fetchEtag(CALDAV_CACHE_ETAG, aNewItem, null, null);
  604.  
  605.             } else {
  606.                 if (aStatusCode > 999) {
  607.                     aStatusCode = "0x " + aStatusCode.toString(16);
  608.                 }
  609.                 LOG("Error modifying item: " + aStatusCode);
  610.  
  611.                 // XXX deal with non-existent item here, other
  612.                 // real error handling
  613.  
  614.                 // XXX aStatusCode will be 201 Created for a PUT on an item
  615.                 // that didn't exist before.
  616.  
  617.                 retVal = Components.results.NS_ERROR_FAILURE;
  618.             }
  619.  
  620.             // XXX ensure immutable version returned
  621.             // notify listener
  622.             if (aListener) {
  623.                 try {
  624.                     aListener.onOperationComplete(thisCalendar, retVal,
  625.                                                   aListener.MODIFY,
  626.                                                   aNewItem.id, aNewItem);
  627.                 } catch (ex) {
  628.                     LOG("modifyItem's onOperationComplete threw an"
  629.                           + " exception " + ex + "; ignoring");
  630.                 }
  631.             }
  632.  
  633.             // notify observers
  634.             if (Components.isSuccessCode(retVal)) {
  635.                 thisCalendar.observeModifyItem(aNewItem, aOldItem.parentItem);
  636.             }
  637.  
  638.             return;
  639.         }
  640.  
  641.         // XXX use if-exists stuff here
  642.         // XXX use etag as generation
  643.         // do WebDAV put
  644.         LOG("modifyItem: aNewItem.icalString = " + aNewItem.icalString);
  645.         var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  646.             .getService(Components.interfaces.nsIWebDAVService);
  647.         webSvc.putFromString(eventResource, "text/calendar; charset=utf-8",
  648.                              modifiedItemICS, listener, this, null);
  649.  
  650.         return;
  651.     },
  652.  
  653.     /**
  654.      * Sends data regarding requested deletion off for etag checking.
  655.      *
  656.      * @param aItem       item to check
  657.      * @param aListener   listener for method completion
  658.      */
  659.     deleteItem: function caldavDI(aItem, aListener) {
  660.         this.fetchEtag(CALDAV_DELETE_ITEM, aItem, aListener, null);
  661.     },
  662.  
  663.     /**
  664.      * Deletes item from CalDAV store.
  665.      *
  666.      * @param aItem       item to delete
  667.      * @param aListener   listener for method completion
  668.      */
  669.     performDeleteItem: function caldavPDI(aItem, aListener) {
  670.  
  671.         if (aItem.id == null) {
  672.             if (aListener)
  673.                 aListener.onOperationComplete (this,
  674.                                                Components.results.NS_ERROR_FAILURE,
  675.                                                aListener.DELETE,
  676.                                                aItem.id,
  677.                                                "ID doesn't exist for deleteItem");
  678.             return;
  679.         }
  680.  
  681.         var eventUri = this.mCalendarUri.clone();
  682.         try {
  683.             eventUri.spec = this.mMakeUri(aItem.getProperty("X-MOZ-LOCATIONPATH"));
  684.             LOG("using X-MOZ-LOCATIONPATH: " + eventUri.spec);
  685.         } catch (ex) {
  686.             // XXX how are we REALLY supposed to figure this out?
  687.             eventUri.spec = this.mMakeUri(aItem.id + ".ics");
  688.         }
  689.  
  690.         var eventResource = new WebDavResource(eventUri);
  691.  
  692.         var listener = new WebDavListener();
  693.         var thisCalendar = this;
  694.  
  695.         listener.onOperationComplete = 
  696.         function onOperationComplete(aStatusCode, aResource, aOperation,
  697.                                      aClosure) {
  698.  
  699.             // 204 = HTTP "No content"
  700.             //
  701.             if (aStatusCode == 204) {
  702.                 delete thisCalendar.mEtagCache[aItem.id];
  703.                 LOG("Item deleted successfully.");
  704.                 var retVal = Components.results.NS_OK;
  705.             } else {
  706.                 LOG("Error deleting item: " + aStatusCode);
  707.                 // XXX real error handling here
  708.                 retVal = Components.results.NS_ERROR_FAILURE;
  709.             }
  710.  
  711.             // notify the listener
  712.             if (aListener) {
  713.                 try {
  714.                     aListener.onOperationComplete(thisCalendar,
  715.                                                   Components.results.NS_OK,
  716.                                                   aListener.DELETE,
  717.                                                   aItem.id,
  718.                                                   null);
  719.                 } catch (ex) {
  720.                     LOG("deleteItem's onOperationComplete threw an"
  721.                           + " exception " + ex + "; ignoring");
  722.                 }
  723.             }
  724.  
  725.             // notify observers
  726.             if (Components.isSuccessCode(retVal)) {
  727.                 thisCalendar.observeDeleteItem(aItem);
  728.             }
  729.         }
  730.  
  731.         // XXX check generation
  732.         // do WebDAV remove
  733.         var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  734.             .getService(Components.interfaces.nsIWebDAVService);
  735.         webSvc.remove(eventResource, listener, this, null);
  736.  
  737.         return;
  738.     },
  739.  
  740.     /**
  741.      * Retrieves a specific item from the CalDAV store.
  742.      * Use when an outdated copy of the item is in hand.
  743.      *
  744.      * @param aItem       item to fetch
  745.      * @param aListener   listener for method completion
  746.      */
  747.     getUpdatedItem: function caldavGUI(aItem, aListener) {
  748.         if (!aListener) {
  749.             return;
  750.         }
  751.  
  752.         if (aItem == null) {
  753.             aListener.onOperationComplete(this,
  754.                                           Components.results.NS_ERROR_FAILURE,
  755.                                           aListener.GET,
  756.                                           null,
  757.                                           "passed in null item");
  758.             return;
  759.         }
  760.  
  761.         var itemType = "VEVENT";
  762.         if (aItem instanceof Ci.calITodo) {
  763.             itemType = "VTODO";
  764.         }
  765.  
  766.         var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
  767.         var D = new Namespace("D", "DAV:");
  768.         default xml namespace = C;
  769.  
  770.         queryXml =
  771.           <calendar-query xmlns:D="DAV:">
  772.             <D:prop>
  773.               <D:getetag/>
  774.               <calendar-data/>
  775.             </D:prop>
  776.             <filter>
  777.               <comp-filter name="VCALENDAR">
  778.                 <comp-filter name={itemType}>
  779.                   <prop-filter name="UID">
  780.                     <text-match collation="i;octet">
  781.                       {aItem.id}
  782.                     </text-match>
  783.                   </prop-filter>
  784.                 </comp-filter>
  785.               </comp-filter>
  786.             </filter>
  787.           </calendar-query>;
  788.  
  789.         this.reportInternal(xmlHeader + queryXml.toXMLString(),
  790.                             false, null, null, 1, aListener, aItem);
  791.         return;
  792.  
  793.     },
  794.  
  795.     // void getItem( in string id, in calIOperationListener aListener );
  796.     getItem: function (aId, aListener) {
  797.  
  798.         if (!aListener)
  799.             return;
  800.  
  801.         if (aId == null) {
  802.             aListener.onOperationComplete(this,
  803.                                           Components.results.NS_ERROR_FAILURE,
  804.                                           aListener.GET,
  805.                                           null,
  806.                                           "passed in empty iid");
  807.             return;
  808.         }
  809.  
  810.  
  811.         // this is our basic search-by-uid xml
  812.         // XXX get rid of vevent filter?
  813.         // XXX need a prefix in the namespace decl?
  814.         default xml namespace = "urn:ietf:params:xml:ns:caldav";
  815.         var D = new Namespace("D", "DAV:");
  816.         queryXml = 
  817.           <calendar-query xmlns:D="DAV:">
  818.             <D:prop>
  819.               <calendar-data/>
  820.             </D:prop>
  821.             <filter>
  822.               <comp-filter name="VCALENDAR">
  823.                 <comp-filter name="VEVENT">
  824.                   <prop-filter name="UID">
  825.                     <text-match caseless="no">
  826.                       {aId}
  827.                     </text-match>
  828.                   </prop-filter>
  829.                 </comp-filter>
  830.               </comp-filter>
  831.             </filter>
  832.           </calendar-query>;
  833.  
  834.         this.reportInternal(xmlHeader + queryXml.toXMLString(), 
  835.                             false, null, null, 1, aListener);
  836.         return;
  837.     },
  838.  
  839.     reportInternal: function caldavRI(aQuery, aOccurrences, aRangeStart,
  840.                                       aRangeEnd, aCount, aListener, aItem)
  841.     {
  842.         var reportListener = new WebDavListener();
  843.         var count = 0;  // maximum number of hits to return
  844.         var thisCalendar = this; // need to access from inside the callback
  845.  
  846.         reportListener.onOperationDetail = function(aStatusCode, aResource,
  847.                                                     aOperation, aDetail,
  848.                                                     aClosure) {
  849.             var rv;
  850.             var errString;
  851.  
  852.             // is this detail the entire search result, rather than a single
  853.             // detail within a set?
  854.             //
  855.             if (aResource.path == calendarDirUri.path) {
  856.                 // XXX is this even valid?  what should we do here?
  857.                 // XXX if it's an error, it might be valid?
  858.                 throw("XXX report result for calendar, not event\n");
  859.             }
  860.  
  861.             var items = null;
  862.  
  863.             // XXX need to do better than looking for just 200
  864.             if (aStatusCode == 200) {
  865.  
  866.                 // we've already called back the maximum number of hits, so 
  867.                 // we're done here.
  868.                 // 
  869.                 if (aCount && count >= aCount) {
  870.                     return;
  871.                 }
  872.                 ++count;
  873.  
  874.                 // aDetail is the response element from the multi-status
  875.                 // XXX try-catch
  876.                 var xSerializer = Components.classes
  877.                     ['@mozilla.org/xmlextras/xmlserializer;1']
  878.                     .getService(Components.interfaces.nsIDOMSerializer);
  879.                 // libical needs to see \r\n instead on \n\n in the case of "folded" lines
  880.                 var response = xSerializer.serializeToString(aDetail).replace(/\n\n/g, "\r\n");
  881.                 var responseElement = new XML(response);
  882.  
  883.                 // create calIItemBase from e4x object
  884.                 // XXX error-check that we only have one result, etc
  885.                 var C = new Namespace("urn:ietf:params:xml:ns:caldav");
  886.                 var D = new Namespace("DAV:");
  887.  
  888.                 var etag = responseElement..D::["getetag"];
  889.  
  890.                 // cause returned data to be parsed into the item
  891.                 var calData = responseElement..C::["calendar-data"];
  892.                 if (!calData.toString().length) {
  893.                   Components.utils.reportError(
  894.                     "Empty or non-existent <calendar-data> element returned" +
  895.                     " by CalDAV server for URI <" + aResource.spec +
  896.                     ">; ignoring");
  897.                   return;
  898.                 }
  899.                 LOG("item result = \n" + calData);
  900.                 this.mICSService = Cc["@mozilla.org/calendar/ics-service;1"].
  901.                                    getService(Components.interfaces.calIICSService);
  902.                 var rootComp = this.mICSService.parseICS(calData);
  903.  
  904.                 var calComp;
  905.                 if (rootComp.componentType == 'VCALENDAR') {
  906.                     calComp = rootComp;
  907.                 } else {
  908.                     calComp = rootComp.getFirstSubcomponent('VCALENDAR');
  909.                 }
  910.  
  911.                 var unexpandedItems = [];
  912.                 var uid2parent = {};
  913.                 var excItems = [];
  914.  
  915.                 while (calComp) {
  916.                     // Get unknown properties
  917.                     var prop = calComp.getFirstProperty("ANY");
  918.                     while (prop) {
  919.                         thisCalendar.unmappedProperties.push(prop);
  920.                         prop = calComp.getNextProperty("ANY");
  921.                     }
  922.  
  923.                     var subComp = calComp.getFirstSubcomponent("ANY");
  924.                     while (subComp) {
  925.                         // Place each subcomp in a try block, to hopefully get as
  926.                         // much of a bad calendar as possible
  927.                         try {
  928.                             var item = null;
  929.                             switch (subComp.componentType) {
  930.                             case "VEVENT":
  931.                                 item = Cc["@mozilla.org/calendar/event;1"].
  932.                                        createInstance(Components.interfaces.calIEvent);
  933.                                 break;
  934.                             case "VTODO":
  935.                                 item = Cc["@mozilla.org/calendar/todo;1"].
  936.                                        createInstance(Components.interfaces.calITodo);
  937.                                 break;
  938.                             case "VTIMEZONE":
  939.                                 // we should already have this, so there's no need to
  940.                                 // do anything with it here.
  941.                                 break;
  942.                             default:
  943.                                 this.unmappedComponents.push(subComp);
  944.                             }
  945.                             if (item != null) {
  946.  
  947.                                 item.icalComponent = subComp;
  948.                                 // save the location name in case we need to modify
  949.                                 // need to build using thisCalendar since aResource.spec
  950.                                 // won't contain any auth info embedded in the URI
  951.                                 var locationPath = decodeURIComponent(aResource.path)
  952.                                                    .substr(thisCalendar.mLocationPath.length);
  953.                                 item.setProperty("X-MOZ-LOCATIONPATH", locationPath);
  954.  
  955.                                 var rid = item.recurrenceId;
  956.                                 if (rid == null) {
  957.                                     unexpandedItems.push( item );
  958.                                     if (item.recurrenceInfo != null) {
  959.                                         uid2parent[item.id] = item;
  960.                                     }
  961.                                 } else {
  962.                                     item.calendar = thisCalendar;
  963.                                     // force no recurrence info so we can
  964.                                     // rebuild it cleanly below
  965.                                     item.recurrenceInfo = null;
  966.                                     excItems.push(item);
  967.                                 }
  968.                             }
  969.                         } catch (ex) { 
  970.                             this.mObserver.onError(ex.result, ex.toString());
  971.                         }
  972.                         subComp = calComp.getNextSubcomponent("ANY");
  973.                     }
  974.                     calComp = rootComp.getNextSubcomponent('VCALENDAR');
  975.                 }
  976.  
  977.                 // tag "exceptions", i.e. items with rid:
  978.                 for each (var item in excItems) {
  979.                     var parent = uid2parent[item.id];
  980.                     if (parent == null) {
  981.                         LOG( "no parent item for rid=" + item.recurrenceId );
  982.                     } else {
  983.                         item.parentItem = parent;
  984.                         item.parentItem.recurrenceInfo.modifyException(item);
  985.                     }
  986.                 }
  987.                 // if we loop over both excItems and unexpandedItems using 'item'
  988.                 // we can be confident that 'item' means something below
  989.                 for each (var item in unexpandedItems) {
  990.                     item.calendar = thisCalendar;
  991.                 }
  992.  
  993.                 thisCalendar.mEtagCache[item.id] = etag;
  994.                 if (aItem) {
  995.                     // if aItem is not null, we were called from
  996.                     // getUpdatedItem(), and the view isn't listening to any
  997.                     // changes. So in order to have the updated item displayed
  998.                     // we need to modify the item currently displayed with
  999.                     // the one just fetched
  1000.                     thisCalendar.observeModifyItem(item, aItem.parentItem);
  1001.                 }
  1002.  
  1003.                 // figure out what type of item to return
  1004.                 var iid;
  1005.                 if(aOccurrences) {
  1006.                     iid = Ci.calIItemBase;
  1007.                     if (item.recurrenceInfo) {
  1008.                         LOG("ITEM has recurrence: " + item + " (" + item.title + ")");
  1009.                         LOG("rangestart: " + aRangeStart.jsDate + " -> " + aRangeEnd.jsDate);
  1010.                         // XXX does getOcc call makeImmutable?
  1011.                         items = item.recurrenceInfo.getOccurrences(aRangeStart,
  1012.                                                                    aRangeEnd,
  1013.                                                                    0, {});
  1014.                     } else {
  1015.                         // XXX need to make occurrences immutable?
  1016.                         items = [ item ];
  1017.                     }
  1018.                     rv = Components.results.NS_OK;
  1019.                 } else if (item instanceof Ci.calIEvent) {
  1020.                     iid = Ci.calIEvent;
  1021.                     rv = Components.results.NS_OK;
  1022.                     items = [ item ];
  1023.                 } else if (item instanceof Ci.calITodo) {
  1024.                     iid = Ci.calITodo;
  1025.                     rv = Components.results.NS_OK;
  1026.                     items = [ item ];
  1027.                 } else {
  1028.                     errString = "Can't deduce item type based on query";
  1029.                     rv = Components.results.NS_ERROR_FAILURE;
  1030.                 }
  1031.  
  1032.             } else { 
  1033.                 // XXX
  1034.                 LOG("aStatusCode = " + aStatusCode);
  1035.                 errString = "XXX";
  1036.                 rv = Components.results.NS_ERROR_FAILURE;
  1037.             }
  1038.  
  1039.             // XXX  handle aCount
  1040.             if (errString) {
  1041.                 LOG("errString = " + errString);
  1042.             }
  1043.  
  1044.             try {
  1045.                 aListener.onGetResult(thisCalendar, rv, iid, null,
  1046.                                       items ? items.length : 0,
  1047.                                       errString ? errString : items);
  1048.             } catch (ex) {
  1049.                     LOG("reportInternal's onGetResult threw an"
  1050.                           + " exception " + ex + "; ignoring");
  1051.             }
  1052.  
  1053.             // We have a result, so we must be authenticated
  1054.             if (thisCalendar.mAuthenticationStatus == kCaldavFirstRequestSent) {
  1055.                 thisCalendar.mAuthenticationStatus = kCaldavFreshlyAuthenticated;
  1056.             }
  1057.  
  1058.             if (thisCalendar.mAuthenticationStatus == kCaldavFreshlyAuthenticated) {
  1059.                 thisCalendar.mAuthenticationStatus = kCaldavAuthenticated;
  1060.                 while (thisCalendar.mPendingStartupRequests.length > 0) {
  1061.                     thisCalendar.popStartupRequest();
  1062.                 }
  1063.             }
  1064.             return;
  1065.         };
  1066.  
  1067.         reportListener.onOperationComplete = function(aStatusCode, aResource,
  1068.                                                       aOperation, aClosure) {
  1069.             
  1070.             // XXX test that something reasonable happens w/notfound
  1071.  
  1072.             // parse aStatusCode
  1073.             var rv;
  1074.             var errString;
  1075.             if (aStatusCode == 200) { // XXX better error checking
  1076.                 rv = Components.results.NS_OK;
  1077.             } else {
  1078.                 rv = Components.results.NS_ERROR_FAILURE;
  1079.                 errString = "XXX something bad happened";
  1080.             }
  1081.  
  1082.             // call back the listener
  1083.             try {
  1084.                 if (aListener) {
  1085.                     aListener.onOperationComplete(thisCalendar,
  1086.                                                   Components.results.
  1087.                                                   NS_ERROR_FAILURE,
  1088.                                                   aListener.GET, null,
  1089.                                                   errString);
  1090.                 }
  1091.             } catch (ex) {
  1092.                     LOG("reportInternal's onOperationComplete threw an"
  1093.                           + " exception " + ex + "; ignoring");
  1094.             }
  1095.  
  1096.             return;
  1097.         };
  1098.  
  1099.         // convert this into a form the WebDAV service can use
  1100.         var xParser = Components.classes['@mozilla.org/xmlextras/domparser;1']
  1101.                       .getService(Components.interfaces.nsIDOMParser);
  1102.         queryDoc = xParser.parseFromString(aQuery, "application/xml");
  1103.  
  1104.         // construct the resource we want to search against
  1105.         var calendarDirUri = this.mCalendarUri.clone();
  1106.         calendarDirUri.spec = this.mMakeUri('');
  1107.         LOG("report uri = " + calendarDirUri.spec);
  1108.         var calendarDirResource = new WebDavResource(calendarDirUri);
  1109.  
  1110.         var webSvc = Components.classes['@mozilla.org/webdav/service;1']
  1111.             .getService(Components.interfaces.nsIWebDAVService);
  1112.         webSvc.report(calendarDirResource, queryDoc, true, reportListener,
  1113.                       this, null);
  1114.         return;    
  1115.  
  1116.     },
  1117.  
  1118.  
  1119.     // void getItems( in unsigned long aItemFilter, in unsigned long aCount, 
  1120.     //                in calIDateTime aRangeStart, in calIDateTime aRangeEnd,
  1121.     //                in calIOperationListener aListener );
  1122.     getItems: function (aItemFilter, aCount, aRangeStart, aRangeEnd, aListener)
  1123.     {
  1124.         if (!aListener) {
  1125.             return;
  1126.         }
  1127.  
  1128.         if (this.mAuthenticationStatus == kCaldavNoAuthentication) {
  1129.            this.mAuthenticationStatus = kCaldavFirstRequestSent;
  1130.            this.checkDavResourceType();
  1131.         }
  1132.  
  1133.         if (this.mAuthenticationStatus == kCaldavFirstRequestSent) {
  1134.             var req = new Array(aItemFilter, aCount, aRangeStart, aRangeEnd, aListener);
  1135.             this.mPendingStartupRequests.push(req);
  1136.             return;
  1137.         }
  1138.  
  1139.         // this is our basic report xml
  1140.         var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
  1141.         var D = new Namespace("D", "DAV:");
  1142.         default xml namespace = C;
  1143.  
  1144.         var queryXml = 
  1145.           <calendar-query xmlns:D={D}>
  1146.             <D:prop>
  1147.               <D:getetag/>
  1148.               <calendar-data/>
  1149.             </D:prop>
  1150.             <filter>
  1151.               <comp-filter name="VCALENDAR">
  1152.                 <comp-filter/>
  1153.               </comp-filter>
  1154.             </filter>
  1155.           </calendar-query>;
  1156.  
  1157.         // figure out 
  1158.         var compFilterNames = new Array();
  1159.         compFilterNames[calICalendar.ITEM_FILTER_TYPE_TODO] = "VTODO";
  1160.         compFilterNames[calICalendar.ITEM_FILTER_TYPE_VJOURNAL] = "VJOURNAL";
  1161.         compFilterNames[calICalendar.ITEM_FILTER_TYPE_EVENT] = "VEVENT";
  1162.  
  1163.         var filterTypes = 0;
  1164.         for (var i in compFilterNames) {
  1165.             if (aItemFilter & i) {
  1166.                 // XXX CalDAV only allows you to ask for one filter-type
  1167.                 // at once, so if the caller wants multiple filtern
  1168.                 // types, all they're going to get for now is events
  1169.                 // (since that's last in the Array).  Sorry!
  1170.                 ++filterTypes;
  1171.                 queryXml[0].C::filter.C::["comp-filter"]
  1172.                     .C::["comp-filter"] = 
  1173.                     <comp-filter name={compFilterNames[i]}/>;
  1174.             }
  1175.         }
  1176.  
  1177.         if (filterTypes < 1) {
  1178.             LOG("No item types specified");
  1179.             // XXX should we just quietly call back the completion method?
  1180.             throw NS_ERROR_FAILURE;
  1181.         }
  1182.  
  1183.         // if a time range has been specified, do the appropriate restriction.
  1184.         // XXX express "end of time" in caldav by leaving off "start", "end"
  1185.         if (aRangeStart && aRangeStart.isValid && 
  1186.             aRangeEnd && aRangeEnd.isValid) {
  1187.  
  1188.             var queryRangeStart = aRangeStart.clone();
  1189.             var queryRangeEnd = aRangeEnd.clone();
  1190.             queryRangeStart.isDate = false;
  1191.             if (queryRangeEnd.isDate) {
  1192.                 // add a day to rangeEnd since we want to match events all that day
  1193.                 // and isDate=false is converting the date to midnight
  1194.                 queryRangeEnd.day++;
  1195.                 queryRangeEnd.normalize();
  1196.                 queryRangeEnd.isDate = false;
  1197.             }
  1198.             var rangeXml = <time-range start={queryRangeStart.getInTimezone("UTC").icalString}
  1199.                                        end={queryRangeEnd.getInTimezone("UTC").icalString}/>;
  1200.  
  1201.             // append the time-range as a child of our innermost comp-filter
  1202.             queryXml[0].C::filter.C::["comp-filter"]
  1203.                 .C::["comp-filter"].appendChild(rangeXml);
  1204.         }
  1205.  
  1206.         var queryString = xmlHeader + queryXml.toXMLString();
  1207.         LOG("getItems(): querying CalDAV server for events: \n" + queryString);
  1208.  
  1209.         var occurrences = (aItemFilter &
  1210.                            calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) != 0; 
  1211.         this.reportInternal(queryString, occurrences, aRangeStart, aRangeEnd,
  1212.                             aCount, aListener, null);
  1213.     },
  1214.  
  1215.     startBatch: function ()
  1216.     {
  1217.         this.observeBatchChange(true);
  1218.     },
  1219.     endBatch: function ()
  1220.     {
  1221.         this.observeBatchChange(false);
  1222.     },
  1223.  
  1224.     // nsIInterfaceRequestor impl
  1225.     getInterface: function(iid) {
  1226.         if (iid.equals(Components.interfaces.nsIAuthPrompt)) {
  1227.             return new calAuthPrompt();
  1228.         }
  1229.         else if (iid.equals(Components.interfaces.nsIPrompt)) {
  1230.             // use the window watcher service to get a nsIPrompt impl
  1231.             return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  1232.                              .getService(Components.interfaces.nsIWindowWatcher)
  1233.                              .getNewPrompter(null);
  1234.         } else if (iid.equals(Components.interfaces.nsIProgressEventSink)) {
  1235.             return this;
  1236.         // Needed for Lightning on branch vvv
  1237.         } else if (iid.equals(Components.interfaces.nsIDocShellTreeItem)) {
  1238.             return this;
  1239.         } else if (iid.equals(Components.interfaces.nsIAuthPromptProvider)) {
  1240.             return Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
  1241.                              .getService(Components.interfaces.nsIWindowWatcher)
  1242.                              .getNewPrompter(null);
  1243.         } else if (!isOnBranch && iid.equals(Components.interfaces.nsIAuthPrompt2)) {
  1244.             return new calAuthPrompt();
  1245.         }
  1246.         throw Components.results.NS_ERROR_NO_INTERFACE;
  1247.     },
  1248.  
  1249.     //
  1250.     // Helper functions
  1251.     //
  1252.     observeBatchChange: function observeBatchChange (aNewBatchMode) {
  1253.         for each (obs in this.mObservers) {
  1254.             if (aNewBatchMode) {
  1255.                 try {
  1256.                     obs.onStartBatch();
  1257.                 } catch (ex) {
  1258.                     LOG("observer's onStartBatch threw an exception " + ex 
  1259.                           + "; ignoring");
  1260.                 }
  1261.             } else {
  1262.                 try {
  1263.                     obs.onEndBatch();
  1264.                 } catch (ex) {
  1265.                     LOG("observer's onEndBatch threw an exception " + ex 
  1266.                           + "; ignoring");
  1267.                 }
  1268.             }
  1269.         }
  1270.         return;
  1271.     },
  1272.  
  1273.     observeAddItem: 
  1274.         function observeAddItem(aItem) {
  1275.             for each (obs in this.mObservers) {
  1276.                 try {
  1277.                     obs.onAddItem(aItem);
  1278.                 } catch (ex) {
  1279.                     LOG("observer's onAddItem threw an exception " + ex 
  1280.                           + "; ignoring");
  1281.                 }
  1282.             }
  1283.             return;
  1284.         },
  1285.  
  1286.     observeModifyItem: 
  1287.         function observeModifyItem(aNewItem, aOldItem) {
  1288.             for each (obs in this.mObservers) {
  1289.                 try {
  1290.                     obs.onModifyItem(aNewItem, aOldItem);
  1291.                 } catch (ex) {
  1292.                     LOG("observer's onModifyItem threw an exception " + ex 
  1293.                           + "; ignoring");
  1294.                 }
  1295.             }
  1296.             return;
  1297.         },
  1298.  
  1299.     observeDeleteItem: 
  1300.         function observeDeleteItem(aDeletedItem) {
  1301.             for each (obs in this.mObservers) {
  1302.                 try {
  1303.                     obs.onDeleteItem(aDeletedItem);
  1304.                 } catch (ex) {
  1305.                     LOG("observer's onDeleteItem threw an exception " + ex 
  1306.                           + "; ignoring");
  1307.                 }
  1308.             }
  1309.             return;
  1310.         },
  1311.  
  1312.     popStartupRequest: function popStartupRequest() {
  1313.         var req = this.mPendingStartupRequests.pop();
  1314.         this.getItems(req[0], req[1], req[2], req[3], req[4]);
  1315.     },
  1316.  
  1317.     /**
  1318.      * Checks that the calendar URI exists and is a CalDAV calendar
  1319.      *
  1320.      */
  1321.     checkDavResourceType: function checkDavResourceType() {
  1322.         var listener = new WebDavListener();
  1323.         var resourceTypeXml = null;
  1324.         var resourceType = kDavResourceTypeNone;
  1325.         var thisCalendar = this;
  1326.         listener.onOperationComplete =
  1327.             function checkDavResourceType_oOC(aStatusCode, aResource,
  1328.                                               aOperation, aClosure) {
  1329.  
  1330.             if (resourceType == null || resourceType == kDavResourceTypeNone) {
  1331.                 thisCalendar.mReadOnly = true;
  1332.                 throw("The resource at " + thisCalendar.mUri.spec +
  1333.                       " is either not DAV or not available\n");
  1334.             }
  1335.  
  1336.             if (resourceType == kDavResourceTypeCollection) {
  1337.                 thisCalendar.mReadOnly = true;
  1338.                 throw("The resource at " + thisCalendar.mUri.spec +
  1339.                       " is a DAV collection but not a CalDAV calendar\n");
  1340.             }
  1341.  
  1342.             // we've authenticated in the process of PROPFINDing and can flush
  1343.             // the getItems request queue
  1344.             thisCalendar.mAuthenticationStatus = kCaldavFreshlyAuthenticated;
  1345.             if (thisCalendar.mPendingStartupRequests.length > 0) {
  1346.                 thisCalendar.popStartupRequest();
  1347.             }
  1348.         }
  1349.  
  1350.         listener.onOperationDetail =
  1351.             function checkDavResourceType_oOD(aStatusCode, aResource,
  1352.                                               aOperation, aDetail, aClosure) {
  1353.  
  1354.             var prop = aDetail.QueryInterface(Ci.nsIProperties);
  1355.  
  1356.             try {
  1357.                 resourceTypeXml = prop.get("DAV: resourcetype",
  1358.                                            Ci.nsISupportsString).toString();
  1359.             } catch (ex) {
  1360.                 LOG("error " + e + " fetching resource type");
  1361.             }
  1362.  
  1363.             if (resourceTypeXml.length == 0) {
  1364.                 resourceType = kDavResourceTypeNone;
  1365.             } else if (resourceTypeXml.indexOf("calendar") != -1) {
  1366.                 resourceType = kDavResourceTypeCalendar;
  1367.             } else if (resourceTypeXml.indexOf("collection") != -1) {
  1368.                 resourceType = kDavResourceTypeCollection;
  1369.             }
  1370.         }
  1371.  
  1372.         var res = new WebDavResource(this.mCalendarUri);
  1373.         var webSvc = Cc['@mozilla.org/webdav/service;1'].
  1374.                      getService(Ci.nsIWebDAVService);
  1375.         try {
  1376.             webSvc.getResourceProperties(res, 1, ["DAV: resourcetype"], false,
  1377.                                           listener, this, null);
  1378.         } catch (ex) {
  1379.             thisCalendar.mReadOnly = true;
  1380.             Components.utils.reportError(
  1381.                 "Unable to get properties of resource " + thisCalendar.mUri.spec
  1382.                 + " (not a network resource?); setting read-only");
  1383.         }
  1384.     },
  1385.  
  1386.     refresh: function calDAV_refresh() {
  1387.         // XXX-fill this in, get a report for modifications+onModifyItem
  1388.     },
  1389.     
  1390.     // stubs to keep callbacks we don't support yet from throwing errors 
  1391.     // we don't care about
  1392.     // nsIProgressEventSink
  1393.     onProgress: function onProgress(aRequest, aContext, aProgress, aProgressMax) {},
  1394.     onStatus: function onStatus(aRequest, aContext, aStatus, aStatusArg) {},
  1395.     // nsIDocShellTreeItem
  1396.     findItemWithName: function findItemWithName(name, aRequestor, aOriginalRequestor) {}
  1397. };
  1398.  
  1399. function WebDavResource(url) {
  1400.     this.mResourceURL = url;
  1401. }
  1402.  
  1403. WebDavResource.prototype = {
  1404.     mResourceURL: {},
  1405.     get resourceURL() { 
  1406.         return this.mResourceURL;}  ,
  1407.     QueryInterface: function(iid) {
  1408.         if (iid.equals(CI.nsIWebDAVResource) ||
  1409.             iid.equals(CI.nsISupports)) {
  1410.             return this;
  1411.         }
  1412.        
  1413.         throw Components.interfaces.NS_ERROR_NO_INTERFACE;
  1414.     }
  1415. };
  1416.  
  1417. function WebDavListener() {
  1418. }
  1419.  
  1420. WebDavListener.prototype = { 
  1421.  
  1422.     QueryInterface: function (aIID) {
  1423.         if (!aIID.equals(Components.interfaces.nsISupports) &&
  1424.             !aIID.equals(nsIWebDavOperationListener)) {
  1425.             throw Components.results.NS_ERROR_NO_INTERFACE;
  1426.         }
  1427.  
  1428.         return this;
  1429.     },
  1430.  
  1431.     onOperationComplete: function(aStatusCode, aResource, aOperation,
  1432.                                   aClosure) {
  1433.         // aClosure is the listener
  1434.         aClosure.onOperationComplete(this, aStatusCode, 0, null, null);
  1435.  
  1436.         LOG("WebDavListener.onOperationComplete() called");
  1437.         return;
  1438.     },
  1439.  
  1440.     onOperationDetail: function(aStatusCode, aResource, aOperation, aDetail,
  1441.                                 aClosure) {
  1442.         LOG("WebDavListener.onOperationDetail() called");
  1443.         return;
  1444.     }
  1445. }
  1446.  
  1447. /****
  1448.  **** module registration
  1449.  ****/
  1450.  
  1451. var calDavCalendarModule = {
  1452.     mCID: Components.ID("{a35fc6ea-3d92-11d9-89f9-00045ace3b8d}"),
  1453.     mContractID: "@mozilla.org/calendar/calendar;1?type=caldav",
  1454.  
  1455.     mUtilsLoaded: false,
  1456.     loadUtils: function caldavLoadUtils() {
  1457.         if (this.mUtilsLoaded)
  1458.             return;
  1459.  
  1460.         const jssslContractID = "@mozilla.org/moz/jssubscript-loader;1";
  1461.         const jssslIID = Components.interfaces.mozIJSSubScriptLoader;
  1462.  
  1463.         const iosvcContractID = "@mozilla.org/network/io-service;1";
  1464.         const iosvcIID = Components.interfaces.nsIIOService;
  1465.  
  1466.         var loader = Components.classes[jssslContractID].getService(jssslIID);
  1467.         var iosvc = Components.classes[iosvcContractID].getService(iosvcIID);
  1468.  
  1469.         // Note that unintuitively, __LOCATION__.parent == .
  1470.         // We expect to find utils in ./../js
  1471.         var appdir = __LOCATION__.parent.parent;
  1472.         appdir.append("js");
  1473.         var scriptName = "calUtils.js";
  1474.  
  1475.         var f = appdir.clone();
  1476.         f.append(scriptName);
  1477.  
  1478.         try {
  1479.             var fileurl = iosvc.newFileURI(f);
  1480.             loader.loadSubScript(fileurl.spec, this.__parent__.__parent__);
  1481.         } catch (e) {
  1482.             LOG("Error while loading " + fileurl.spec );
  1483.             throw e;
  1484.         }
  1485.  
  1486.         this.mUtilsLoaded = true;
  1487.     },
  1488.     
  1489.     registerSelf: function (compMgr, fileSpec, location, type) {
  1490.         compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
  1491.         compMgr.registerFactoryLocation(this.mCID,
  1492.                                         "Calendar CalDAV back-end",
  1493.                                         this.mContractID,
  1494.                                         fileSpec,
  1495.                                         location,
  1496.                                         type);
  1497.     },
  1498.  
  1499.     getClassObject: function (compMgr, cid, iid) {
  1500.         if (!cid.equals(this.mCID))
  1501.             throw Components.results.NS_ERROR_NO_INTERFACE;
  1502.  
  1503.         if (!iid.equals(Components.interfaces.nsIFactory))
  1504.             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1505.  
  1506.         this.loadUtils();
  1507.  
  1508.         return this.mFactory;
  1509.     },
  1510.  
  1511.     mFactory: {
  1512.         createInstance: function (outer, iid) {
  1513.             if (outer != null)
  1514.                 throw Components.results.NS_ERROR_NO_AGGREGATION;
  1515.             return (new calDavCalendar()).QueryInterface(iid);
  1516.         }
  1517.     },
  1518.  
  1519.     canUnload: function(compMgr) {
  1520.         return true;
  1521.     }
  1522. };
  1523.  
  1524.  
  1525. function NSGetModule(compMgr, fileSpec) {
  1526.     return calDavCalendarModule;
  1527. }
  1528.