1 /* FBURL provider extension, retrieving a free/busy URL from LDAP and 2 * presenting the resource's content to the free/busy mechanism. 3 * 4 * Copyright (C) 2014, 2016 Paul Boddie <paul@boddie.org.uk> 5 * 6 * This Source Code Form is subject to the terms of the Mozilla Public 7 * License, v. 2.0. If a copy of the MPL was not distributed with this 8 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 10 var EXPORTED_SYMBOLS = [ "fburl" ]; 11 12 const Cc = Components.classes; 13 const Ci = Components.interfaces; 14 const Cu = Components.utils; 15 16 Cu.import("resource://calendar/modules/calUtils.jsm"); 17 Cu.import("resource://calendar/modules/calIteratorUtils.jsm"); 18 Cu.import("resource:///modules/mailServices.js"); 19 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 20 21 if ("undefined" == typeof(fburl)) { 22 var fburl = {}; 23 }; 24 25 const calIFreeBusyInterval = Ci.calIFreeBusyInterval; 26 const calIDateTime = Ci.calIDateTime; 27 28 /* Initialise the provider. 29 */ 30 fburl.fbUrlProvider = function(cal) { 31 this.cal = cal; 32 this._query = null; 33 this._context = null; 34 this._result = null; 35 this._searched = false; 36 this._error = null; 37 this._listener = null; 38 this._url = null; 39 this._calId = null; 40 this._periods = null; 41 }; 42 43 /* The free/busy provider for FBURL-specified resources. 44 * This implements the calIFreeBusyProvider interface, returning free/busy 45 * intervals when getFreeBusyIntervals is called. 46 */ 47 fburl.fbUrlProvider.prototype = { 48 classID: Components.ID("{11291d94-b457-4322-bfba-ae9df4b6a3c1}"), 49 50 /* Called by the calFreeBusyService, this function performs a search in 51 * LDAP address books and returns any intervals found. 52 */ 53 getFreeBusyIntervals: function (aCalId, aRangeStart, aRangeEnd, aBusyTypes, aListener) { 54 55 this._listener = aListener; 56 this._calId = aCalId; 57 this._periods = null; 58 this.startSearch(aCalId); 59 }, 60 61 /* Access the address book manager and obtain all LDAP directory address 62 * books. 63 */ 64 getLDAPAddressBooks: function () { 65 66 var abManager = Cc["@mozilla.org/abmanager;1"].getService(Ci.nsIAbManager); 67 var allAddressBooks = abManager.directories; 68 var books = []; 69 70 while (allAddressBooks.hasMoreElements()) { 71 var addressBook = allAddressBooks.getNext().QueryInterface(Ci.nsIAbDirectory); 72 if (addressBook instanceof Ci.nsIAbLDAPDirectory) { 73 books.push(addressBook); 74 } 75 } 76 77 return books; 78 }, 79 80 /* Initiate a search, obtaining LDAP address books and querying each one for 81 * an identity having the given primary e-mail address. 82 */ 83 startSearch: function (aCalId) { 84 85 var service = Cc["@mozilla.org/network/ldap-service;1"].getService(Ci.nsILDAPService); 86 var books = this.getLDAPAddressBooks(); 87 88 this._result = null; 89 aCalId = aCalId.replace(/^mailto:/i, ""); 90 91 for each (var book in books) { 92 var ldap = book.QueryInterface(Ci.nsIAbLDAPDirectory); 93 var attrmap = Cc["@mozilla.org/addressbook/ldap-attribute-map;1"].createInstance(Ci.nsIAbLDAPAttributeMap); 94 var args = Cc["@mozilla.org/addressbook/directory/query-arguments;1"].createInstance(Ci.nsIAbDirectoryQueryArguments); 95 96 attrmap.setAttributeList("PrimaryEmail", "mail", true); 97 attrmap.setAttributeList("FBURL", "calFbUrl", true); 98 args.filter = service.createFilter(aCalId.length * 2, "(%a=%v)", "", "", "mail", aCalId); 99 args.typeSpecificArg = attrmap; 100 args.querySubDirectories = true; 101 102 this._query = Cc["@mozilla.org/addressbook/ldap-directory-query;1"].createInstance(Ci.nsIAbDirectoryQuery); 103 this._context = this._query.doQuery(ldap, args, this, ldap.maxHits, 0); 104 this._searched = true; 105 106 // NOTE: Do only one query for now. 107 108 break; 109 } 110 }, 111 112 /* Stop any currently-initiated search. 113 */ 114 stopSearch: function stopSearch() { 115 if (this._context) { 116 this._query.stopQuery(this._context); 117 this._context = null; 118 this._listener = null; 119 } 120 }, 121 122 /* Search finished callback. 123 */ 124 onSearchFinished: function (aResult, aErrorMsg) { 125 this._context = null; 126 this._error = aErrorMsg; 127 }, 128 129 /* Search success callback, indicating any found address book card. The 130 * supplied card should provide the FBURL property which is then used to 131 * retrieve the referenced free/busy resource. 132 * 133 * Upon retrieval, the resource is parsed using the calICSService and the 134 * free/busy periods obtained. Finally, each period is encapsulated in an 135 * interval object and the collection of such objects presented to the 136 * originally-specified result callback. 137 */ 138 onSearchFoundCard: function (aCard) { 139 140 this._result = aCard; 141 142 // Obtain the URL from the card. 143 144 this._url = aCard.getProperty("FBURL", ""); 145 146 // Retrieve the resource and parse it. 147 148 var parser = Cc["@mozilla.org/calendar/ics-service;1"].getService(Ci.calIICSService); 149 //var parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser); 150 var service = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); 151 var stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); 152 var channel = service.newChannelFromURI(service.newURI(this._url, null, null)); 153 154 stream.init(channel.open()); 155 var resource = parser.parseICS(stream.read(-1), null); 156 157 // Obtain the free/busy periods. 158 159 this._periods = []; 160 var fbTypeMap = {}; 161 162 fbTypeMap["FREE"] = calIFreeBusyInterval.FREE; 163 fbTypeMap["BUSY"] = calIFreeBusyInterval.BUSY; 164 fbTypeMap["BUSY-UNAVAILABLE"] = calIFreeBusyInterval.BUSY_UNAVAILABLE; 165 fbTypeMap["BUSY-TENTATIVE"] = calIFreeBusyInterval.BUSY_TENTATIVE; 166 167 // Iterate over components in the response. 168 169 for (var comp in cal.ical.calendarComponentIterator(resource)) { 170 171 // Iterate over free/busy properties. 172 173 for (var fbProp in cal.ical.propertyIterator(comp, "FREEBUSY")) { 174 175 // Assign the stated type or busy otherwise. 176 177 var fbType = fbProp.getParameter("FBTYPE"); 178 179 if (fbType) { 180 fbType = fbTypeMap[fbType]; 181 } else { 182 fbType = calIFreeBusyInterval.BUSY; 183 } 184 185 var parts = fbProp.value.split("/"); 186 var begin = cal.createDateTime(parts[0]); 187 var end; 188 189 // Support durations. 190 191 if (parts[1].charAt(0) == "P") { 192 end = begin.clone(); 193 end.addDuration(cal.createDuration(parts[1])); 194 195 // Support plain datetimes. 196 197 } else { 198 end = cal.createDateTime(parts[1]); 199 } 200 201 interval = new cal.FreeBusyInterval(this._calId, fbType, begin, end); 202 this._periods.push(interval); 203 } 204 } 205 206 this._listener.onResult(null, this._periods); 207 }, 208 209 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsIAbDirSearchListener]) 210 }; 211 212 /* Provide a way of adding the provider to the free/busy service. 213 */ 214 fburl.initProvider = function() { 215 cal.getFreeBusyService().addProvider(new fburl.fbUrlProvider(cal)); 216 } 217 218 fburl.test = function(aCalId) { 219 var fbp = new fburl.fbUrlProvider(cal); 220 fbp.startSearch(aCalId); 221 return fbp; 222 } 223 224 const NSGetFactory = XPCOMUtils.generateNSGetFactory([fburl.fbUrlProvider]);