Skip to content
This repository
Browse code

finished CardDAV port - ready for first run and testing - and improve…

…d browser plugin with forms
  • Loading branch information...
commit 6b3b2dc8e7626179a5fd2d5d9499db6c96d312c3 1 parent 376811f
Mike de Boer authored February 01, 2013
458  lib/CardDAV/backends/redis.js
... ...
@@ -0,0 +1,458 @@
  1
+/*
  2
+ * @package jsDAV
  3
+ * @subpackage CardDAV
  4
+ * @copyright Copyright(c) 2013 Mike de Boer. <info AT mikedeboer DOT nl>
  5
+ * @author Mike de Boer <info AT mikedeboer DOT nl>
  6
+ * @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
  7
+ */
  8
+"use strict";
  9
+
  10
+var jsCardDAV_iBackend = require("./../interfaces/iBackend");
  11
+var jsCardDAV_Plugin = require("./../plugin");
  12
+var jsCardDAV_Property_SupportedAddressData = require("./../property/supportedAddressData");
  13
+
  14
+var Db = require("./../../shared/db");
  15
+var Exc = require("./../../shared/exceptions");
  16
+var Util = require("./../../shared/util");
  17
+
  18
+var Redis = require("redis");
  19
+
  20
+/**
  21
+ * Redis CardDAV backend
  22
+ *
  23
+ * This CardDAV backend uses Redis to store addressbooks
  24
+ */
  25
+var jsCardDAV_Backend_Redis = module.exports = jsCardDAV_iBackend.extend({
  26
+    /**
  27
+     * Redis connection
  28
+     *
  29
+     * @var redis
  30
+     */
  31
+    redis: null,
  32
+
  33
+    /**
  34
+     * The PDO table name used to store addressbooks
  35
+     */
  36
+    addressBooksTableName: null,
  37
+
  38
+    /**
  39
+     * The PDO table name used to store cards
  40
+     */
  41
+    cardsTableName: null,
  42
+
  43
+    /**
  44
+     * Sets up the object
  45
+     *
  46
+     * @param Redis redis
  47
+     * @param string addressBooksTableName
  48
+     * @param string cardsTableName
  49
+     */
  50
+    initialize: function(redis, addressBooksTableName, cardsTableName) {
  51
+        this.redis = redis;
  52
+        this.addressBooksTableName = addressBooksTableName || "addressbooks";
  53
+        this.cardsTableName = cardsTableName || "cards";
  54
+    },
  55
+
  56
+    /**
  57
+     * Returns the list of addressbooks for a specific user.
  58
+     *
  59
+     * @param string principalUri
  60
+     * @return array
  61
+     */
  62
+    getAddressBooksForUser: function(principalUri, callback) {
  63
+        var self = this;
  64
+        this.redis.hget(this.addressBooksTableName + "/pricipalUri", principalUri, function(err, res) {
  65
+            if (err)
  66
+                return callback(err);
  67
+            
  68
+            var ids;
  69
+            try {
  70
+                ids = JSON.parse(res.toString("utf8"));
  71
+            }
  72
+            catch (ex) {
  73
+                return callback(ex);
  74
+            }
  75
+            
  76
+            var commands = ids.map(function(id) {
  77
+                return ["HMGET", self.addressBooksTableName + "/" + id, "uri", "principaluri", "description", "ctag"];
  78
+            });
  79
+            
  80
+            self.redis.multi(commands).exec(function(err, res) {
  81
+                if (err)
  82
+                    return callback(err);
  83
+                
  84
+                var addressBooks = Db.fromMultiBulk(res).map(function(data, idx) {
  85
+                    var obj = {
  86
+                        id : ids[idx],
  87
+                        uri: data[0],
  88
+                        principaluri: data[1],
  89
+                        "{DAV:}displayname": data[2],
  90
+                        "{http://calendarserver.org/ns/}getctag": data[4]
  91
+                    };
  92
+                    obj["{" + jsCardDAV_Plugin.NS_CARDDAV + "}addressbook-description"] = data[3];
  93
+                    obj["{" + jsCardDAV_Plugin.NS_CARDDAV + "}supported-address-data"] = jsCardDAV_Property_SupportedAddressData.new();
  94
+                    return obj;
  95
+                });
  96
+                
  97
+                callback(null, addressBooks);
  98
+            });
  99
+        });
  100
+    },
  101
+
  102
+    /**
  103
+     * Updates an addressbook's properties
  104
+     *
  105
+     * See jsDAV_iProperties for a description of the mutations array, as
  106
+     * well as the return value.
  107
+     *
  108
+     * @param mixed addressBookId
  109
+     * @param array mutations
  110
+     * @see jsDAV_iProperties#updateProperties
  111
+     * @return bool|array
  112
+     */
  113
+    updateAddressBook: function(addressBookId, mutations, callback) {
  114
+        var updates = {};
  115
+        var newValue;
  116
+        for (var property in mutations) {
  117
+            newValue = mutations[property];
  118
+            switch (property) {
  119
+                case "{DAV:}displayname" :
  120
+                    updates.displayname = newValue;
  121
+                    break;
  122
+                case "{" + jsCardDAV_Plugin.NS_CARDDAV + "}addressbook-description" :
  123
+                    updates.description = newValue;
  124
+                    break;
  125
+                default :
  126
+                    // If any unsupported values were being updated, we must
  127
+                    // let the entire request fail.
  128
+                    return callback(null, false);
  129
+            }
  130
+        }
  131
+
  132
+        // No values are being updated?
  133
+        if (!Object.keys(updates).length)
  134
+            return callback(null, false);
  135
+
  136
+        var self = this;
  137
+        this.redis.hget(this.addressBooksTableName + "/" + addressBookId, "ctag", function(err, res) {
  138
+            if (err)
  139
+                return callback(err);
  140
+            
  141
+            var ctag = parseInt(res.toString("utf8"), 10);
  142
+            var command = [self.addressBooksTableName + "/" + addressBookId, "ctag", ++ctag];
  143
+            for (var property in updates)
  144
+                command.push(property, updates[property]);
  145
+            command.push(function(err) {
  146
+                if (err)
  147
+                    return callback(err);
  148
+                callback(null, true);
  149
+            });
  150
+            self.redis.hmset.apply(self.redis, command);
  151
+        })
  152
+    },
  153
+
  154
+    /**
  155
+     * Creates a new address book
  156
+     *
  157
+     * @param string principalUri
  158
+     * @param string url Just the 'basename' of the url.
  159
+     * @param array properties
  160
+     * @return void
  161
+     */
  162
+    createAddressBook: function(principalUri, url, properties, callback) {
  163
+        var values = {
  164
+            "displayname": null,
  165
+            "description": null,
  166
+            "principaluri": principalUri,
  167
+            "uri": url,
  168
+        };
  169
+
  170
+        var newValue;
  171
+        for (var property in properties) {
  172
+            newValue = properties[property];
  173
+            
  174
+            switch (property) {
  175
+                case "{DAV:}displayname" :
  176
+                    values.displayname = newValue;
  177
+                    break;
  178
+                case "{" + jsCardDAV_Plugin.NS_CARDDAV + "}addressbook-description" :
  179
+                    values.description = newValue;
  180
+                    break;
  181
+                default :
  182
+                    return callback(new Exc.BadRequest("Unknown property: " + property));
  183
+            }
  184
+        }
  185
+
  186
+        var self = this;
  187
+        this.redis.hget(this.addressBooksTableName + "/pricipalUri", principalUri, function(err, res) {
  188
+            if (err)
  189
+                return callback(err);
  190
+            
  191
+            var ids;
  192
+            try {
  193
+                ids = JSON.parse(res.toString("utf8"));
  194
+            }
  195
+            catch (ex) {
  196
+                ids = [];
  197
+            }
  198
+            
  199
+            self.redis.incr(self.addressBooksTableName + "/ID", function(err, id) {
  200
+                if (err)
  201
+                    return callback(err);
  202
+                
  203
+                ids.push(id);
  204
+                var commands = [
  205
+                    ["HSET", self.addressBooksTableName + "/pricipalUri", principalUri, JSON.stringify(ids)]
  206
+                ];
  207
+                var hmset = ["HMSET", self.addressBooksTableName + "/" + id, "ctag", 1];
  208
+                for (var property in values) {
  209
+                    hmset.push(property, values[property]);
  210
+                }
  211
+                commands.push(hmset);
  212
+                self.redis.multi(commands).exec(callback);
  213
+            });
  214
+        });
  215
+    },
  216
+
  217
+    /**
  218
+     * Deletes an entire addressbook and all its contents
  219
+     *
  220
+     * @param int addressBookId
  221
+     * @return void
  222
+     */
  223
+    deleteAddressBook: function(addressBookId, callback) {
  224
+        var commands = [
  225
+            ["DEL", this.addressBooksTableName + "/" + addressBookId],
  226
+            ["DEL", this.addressBooksTableName + "/" + addressBookId + "/" + this.cardsTableName]
  227
+        ];
  228
+        var self = this;
  229
+        // fetch the principalUri to be able to retrieve the array of addressbooks. 
  230
+        this.redis.hget(this.addressBooksTableName + "/" + addressBookId, "pricipaluri", function(err, res) {
  231
+            if (err)
  232
+                return callback(err);
  233
+                
  234
+            var principalUri = res.toString("utf8");
  235
+            // fetch the addressbook array for this principalUri
  236
+            self.redis.hget(self.addressBooksTableName + "/pricipalUri", principalUri, function(err, res) {
  237
+                if (err)
  238
+                    return callback(err);
  239
+                
  240
+                var ids;
  241
+                try {
  242
+                    ids = JSON.parse(res.toString("utf8"));
  243
+                }
  244
+                catch (ex) {
  245
+                    ids = [];
  246
+                }
  247
+                var idx = ids.indexOf(addressBookId);
  248
+                if (idx > -1)
  249
+                    ids.splice(idx, 1);
  250
+                
  251
+                commands.push(["HSET", self.addressBooksTableName + "/pricipalUri", principalUri, JSON.stringify(ids)]);
  252
+                // fetch the list of card IDs
  253
+                self.redis.zrange(self.addressBooksTableName + "/" + addressBookId + "/" + self.cardsTableName, 0, -1, function(err, res) {
  254
+                    if (err)
  255
+                        return callback(err);
  256
+                        
  257
+                    Db.fromMultiBulk(res).forEach(function(cardUri) {
  258
+                        commands.push(["DEL", self.cardsTableName + "/" + addressBookId + "/" + cardUri]);
  259
+                    });
  260
+                    self.redis.multi(commands).exec(callback)
  261
+                });
  262
+            });
  263
+        });
  264
+    },
  265
+
  266
+    /**
  267
+     * Returns all cards for a specific addressbook id.
  268
+     *
  269
+     * This method should return the following properties for each card:
  270
+     *   * carddata - raw vcard data
  271
+     *   * uri - Some unique url
  272
+     *   * lastmodified - A unix timestamp
  273
+     *
  274
+     * It's recommended to also return the following properties:
  275
+     *   * etag - A unique etag. This must change every time the card changes.
  276
+     *   * size - The size of the card in bytes.
  277
+     *
  278
+     * If these last two properties are provided, less time will be spent
  279
+     * calculating them. If they are specified, you can also ommit carddata.
  280
+     * This may speed up certain requests, especially with large cards.
  281
+     *
  282
+     * @param mixed addressbookId
  283
+     * @return array
  284
+     */
  285
+    getCards: function(addressbookId, callback) {
  286
+        var self = this;
  287
+        // fetch the list of card IDs
  288
+        self.redis.zrange(this.addressBooksTableName + "/" + addressbookId + "/" + this.cardsTableName, 0, -1, function(err, res) {
  289
+            if (err)
  290
+                return callback(err);
  291
+                
  292
+            var cardUris = Db.fromMultiBulk(res);
  293
+            var commands = cardUris.map(function(cardUri) {
  294
+                return ["HMGET", self.cardsTableName + "/"+ addressbookId + "/" + cardUri, "carddata", "lastmodified"];
  295
+            });
  296
+            self.redis.multi(commands).exec(function(err, res) {
  297
+                if (err)
  298
+                    return callback(err);
  299
+                
  300
+                var cards = Db.fromMultiBulk(res).map(function(data, idx) {
  301
+                    return {
  302
+                        uri: cardUris[idx],
  303
+                        carddata: data[0],
  304
+                        lastmodified: data[1]
  305
+                    };
  306
+                });
  307
+                callback(null, cards);
  308
+            });
  309
+        });
  310
+    },
  311
+
  312
+    /**
  313
+     * Returns a specfic card.
  314
+     *
  315
+     * The same set of properties must be returned as with getCards. The only
  316
+     * exception is that 'carddata' is absolutely required.
  317
+     *
  318
+     * @param mixed addressBookId
  319
+     * @param string cardUri
  320
+     * @return array
  321
+     */
  322
+    getCard: function(addressBookId, cardUri, callback) {
  323
+        this.redis.hmget(this.cardsTableName + "/" + addressBookId + "/" + cardUri, "carddata", "lastmodified", function(err, res) {
  324
+            if (err)
  325
+                return callback(err);
  326
+            
  327
+            res = Db.fromMultiBulk(res);
  328
+            if (!res || !res.length)
  329
+                return callback
  330
+            callback(null, res && res.length 
  331
+                ? {
  332
+                    uri: cardUri,
  333
+                    carddata: res[0],
  334
+                    lastmodified: res[1]
  335
+                  }
  336
+                : false
  337
+            );
  338
+        });
  339
+    },
  340
+
  341
+    /**
  342
+     * Creates a new card.
  343
+     *
  344
+     * The addressbook id will be passed as the first argument. This is the
  345
+     * same id as it is returned from the getAddressbooksForUser method.
  346
+     *
  347
+     * The cardUri is a base uri, and doesn't include the full path. The
  348
+     * cardData argument is the vcard body, and is passed as a string.
  349
+     *
  350
+     * It is possible to return an ETag from this method. This ETag is for the
  351
+     * newly created resource, and must be enclosed with double quotes (that
  352
+     * is, the string itself must contain the double quotes).
  353
+     *
  354
+     * You should only return the ETag if you store the carddata as-is. If a
  355
+     * subsequent GET request on the same card does not have the same body,
  356
+     * byte-by-byte and you did return an ETag here, clients tend to get
  357
+     * confused.
  358
+     *
  359
+     * If you don't return an ETag, you can just return null.
  360
+     *
  361
+     * @param mixed addressBookId
  362
+     * @param string cardUri
  363
+     * @param string cardData
  364
+     * @return string|null
  365
+     */
  366
+    createCard: function(addressBookId, cardUri, cardData, callback) {
  367
+        var self = this;
  368
+        var now = Date.now();
  369
+        var commands = [
  370
+            ["HMSET", this.cardsTableName + "/" + addressBookId + "/" + cardUri, "carddata", cardData, "lastmodified", now],
  371
+            ["ZADD", this.addressBooksTableName + "/" + this.cardsTableName, now, cardUri]
  372
+        ];
  373
+        this.redis.hget(this.addressBooksTableName + "/" + addressBookId, "ctag", function(err, ctag) {
  374
+            if (err)
  375
+                return callback(err);
  376
+            
  377
+            ctag = parseInt(ctag.toString("utf8"), 10);
  378
+            commands.push(["HSET", self.addressBooksTableName + "/" + addressBookId, "ctag", ++ctag]);
  379
+            self.redis.multi(commands).exec(function(err) {
  380
+                if (err)
  381
+                    return callback(err);
  382
+                callback(null, "\"" + Util.md5(cardData) + "\"");
  383
+            });
  384
+        });
  385
+    },
  386
+
  387
+    /**
  388
+     * Updates a card.
  389
+     *
  390
+     * The addressbook id will be passed as the first argument. This is the
  391
+     * same id as it is returned from the getAddressbooksForUser method.
  392
+     *
  393
+     * The cardUri is a base uri, and doesn't include the full path. The
  394
+     * cardData argument is the vcard body, and is passed as a string.
  395
+     *
  396
+     * It is possible to return an ETag from this method. This ETag should
  397
+     * match that of the updated resource, and must be enclosed with double
  398
+     * quotes (that is: the string itself must contain the actual quotes).
  399
+     *
  400
+     * You should only return the ETag if you store the carddata as-is. If a
  401
+     * subsequent GET request on the same card does not have the same body,
  402
+     * byte-by-byte and you did return an ETag here, clients tend to get
  403
+     * confused.
  404
+     *
  405
+     * If you don't return an ETag, you can just return null.
  406
+     *
  407
+     * @param mixed addressBookId
  408
+     * @param string cardUri
  409
+     * @param string cardData
  410
+     * @return string|null
  411
+     */
  412
+    updateCard: function(addressBookId, cardUri, cardData, callback) {
  413
+        var self = this;
  414
+        var now = Date.now();
  415
+        var commands = [
  416
+            ["HMSET", this.cardsTableName + "/" + addressBookId + "/" + cardUri, "carddata", cardData, "lastmodified", now]
  417
+        ];
  418
+        this.redis.hget(this.addressBooksTableName + "/" + addressBookId, "ctag", function(err, ctag) {
  419
+            if (err)
  420
+                return callback(err);
  421
+            
  422
+            ctag = parseInt(ctag.toString("utf8"), 10);
  423
+            commands.push(["HSET", self.addressBooksTableName + "/" + addressBookId, "ctag", ++ctag]);
  424
+            self.redis.multi(commands).exec(function(err) {
  425
+                if (err)
  426
+                    return callback(err);
  427
+                callback(null, "\"" + Util.md5(cardData) + "\"");
  428
+            });
  429
+        });
  430
+    },
  431
+
  432
+    /**
  433
+     * Deletes a card
  434
+     *
  435
+     * @param mixed addressBookId
  436
+     * @param string cardUri
  437
+     * @return bool
  438
+     */
  439
+    deleteCard: function(addressBookId, cardUri, callback) {
  440
+        var self = this;
  441
+        var commands = [
  442
+            ["DEL", this.cardsTableName + "/" + addressBookId + "/" + cardUri],
  443
+            ["ZREM", this.addressBooksTableName + "/" + this.cardsTableName, cardUri]
  444
+        ];
  445
+        this.redis.hget(this.addressBooksTableName + "/" + addressBookId, "ctag", function(err, ctag) {
  446
+            if (err)
  447
+                return callback(err);
  448
+            
  449
+            ctag = parseInt(ctag.toString("utf8"), 10);
  450
+            commands.push(["HSET", self.addressBooksTableName + "/" + addressBookId, "ctag", ++ctag]);
  451
+            self.redis.multi(commands).exec(function(err) {
  452
+                if (err)
  453
+                    return callback(err);
  454
+                callback(null, true);
  455
+            });
  456
+        });
  457
+    }
  458
+});
713  lib/CardDAV/plugin.js
@@ -10,14 +10,21 @@
10 10
 var jsDAV_Plugin = require("./../DAV/plugin");
11 11
 var jsDAV_Property_Href = require("./../DAV/property/href");
12 12
 var jsDAV_Property_HrefList = require("./../DAV/property/hrefList");
  13
+var jsDAV_Property_iHref = require("./../DAV/interfaces/iHref");
13 14
 var jsCardDAV_iAddressBook = require("./interfaces/iAddressBook");
14 15
 var jsCardDAV_iCard = require("./interfaces/iCard");
15 16
 var jsCardDAV_iDirectory = require("./interfaces/iDirectory");
  17
+var jsCardDAV_UserAddressBooks = require("./userAddressBooks");
  18
+var jsCardDAV_AddressBookQueryParser = require("./addressBookQueryParser");
  19
+var jsDAVACL_iPrincipal = require("./../DAVACL/interfaces/iPrincipal");
  20
+var jsVObject_Reader = require("./../VObject/reader");
16 21
 
17 22
 var Exc = require("./../shared/exceptions");
18 23
 var Util = require("./../shared/util");
19 24
 var Xml = require("./../shared/xml");
20 25
 
  26
+var Async = require("asyncjs");
  27
+
21 28
 /**
22 29
  * CardDAV plugin
23 30
  *
@@ -58,7 +65,7 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
58 65
     initialize: function(handler) {
59 66
         this.directories = [];
60 67
         
61  
-        /* Events */
  68
+        // Events
62 69
         handler.addEventListener("beforeGetProperties", this.beforeGetProperties.bind(this));
63 70
         handler.addEventListener("afterGetProperties",  this.afterGetProperties.bind(this));
64 71
         handler.addEventListener("updateProperties", this.updateProperties.bind(this));
@@ -68,14 +75,14 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
68 75
         handler.addEventListener("beforeWriteContent", this.beforeWriteContent.bind(this));
69 76
         handler.addEventListener("beforeCreateFile", this.beforeCreateFile.bind(this));
70 77
 
71  
-        /* Namespaces */
  78
+        // Namespaces
72 79
         Xml.xmlNamespaces[this.NS_CARDDAV] = "card";
73 80
 
74  
-        /* Mapping Interfaces to {DAV:}resourcetype values */
  81
+        // Mapping Interfaces to {DAV:}resourcetype values
75 82
         handler.resourceTypeMapping["{" + this.NS_CARDDAV + "}addressbook"] = jsCardDAV_iAddressBook;
76 83
         handler.resourceTypeMapping["{" + this.NS_CARDDAV + "}directory"] = jsCardDAV_iDirectory;
77 84
 
78  
-        /* Adding properties that may never be changed */
  85
+        // Adding properties that may never be changed
79 86
         handler.protectedProperties.push(
80 87
             "{" + this.NS_CARDDAV + "}supported-address-data",
81 88
             "{" + this.NS_CARDDAV + "}max-resource-size",
@@ -110,14 +117,15 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
110 117
      * @return array
111 118
      */
112 119
     getSupportedReportSet: function(uri, callback) {
  120
+        var self = this;
113 121
         this.handler.getNodeForPath(uri, function(err, node) {
114 122
             if (err)
115 123
                 return callback(err);
116 124
             
117 125
             if (node.hasFeature(jsCardDAV_iAddressBook) || node.hasFeature(jsCardDAV_iCard)) {
118 126
                 return callback(null, [
119  
-                     "{" + this.NS_CARDDAV + "}addressbook-multiget",
120  
-                     "{" + this.NS_CARDDAV + "}addressbook-query"
  127
+                     "{" + self.NS_CARDDAV + "}addressbook-multiget",
  128
+                     "{" + self.NS_CARDDAV + "}addressbook-query"
121 129
                 ]);
122 130
             }
123 131
             return callback(null, []);
@@ -134,64 +142,73 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
134 142
      * @return void
135 143
      */
136 144
     beforeGetProperties: function(e, path, node, requestedProperties, returnedProperties) {
137  
-
138  
-        if (node instanceof DAVACL\IPrincipal) {
139  
-
  145
+        var self = this;
  146
+        if (node.hasFeature(jsDAVACL_iPrincipal)) {
140 147
             // calendar-home-set property
141  
-            addHome = "{" . self::NS_CARDDAV . "}addressbook-home-set";
142  
-            if (in_array(addHome,requestedProperties)) {
143  
-                principalId = node.getName();
144  
-                addressbookHomePath = self::ADDRESSBOOK_ROOT . "/" . principalId . "/";
145  
-                unset(requestedProperties[array_search(addHome, requestedProperties)]);
146  
-                returnedProperties[200][addHome] = new DAV\Property\Href(addressbookHomePath);
  148
+            var addHome = "{" + this.NS_CARDDAV + "}addressbook-home-set";
  149
+            var addHomeIdx = requestedProperties.indexOf(addHome);
  150
+            if (addHomeIdx > -1) {
  151
+                var principalId = node.getName();
  152
+                var addressbookHomePath = this.ADDRESSBOOK_ROOT + "/" + principalId + "/";
  153
+                requestedProperties.splice(addHomeIdx, 1);
  154
+                returnedProperties["200"][addHome] = new jsDAV_Property_Href.new(addressbookHomePath);
147 155
             }
148 156
 
149  
-            directories = "{" . self::NS_CARDDAV . "}directory-gateway";
150  
-            if (this.directories && in_array(directories, requestedProperties)) {
151  
-                unset(requestedProperties[array_search(directories, requestedProperties)]);
152  
-                returnedProperties[200][directories] = new DAV\Property\HrefList(this.directories);
  157
+            var directories = "{" + this.NS_CARDDAV + "}directory-gateway";
  158
+            var dirIdx = requestedProperties.indexOf(directories);
  159
+            if (this.directories && dirIdx > -1) {
  160
+                requestedProperties.splice(dirIdx, 1);
  161
+                returnedProperties["200"][directories] = jsDAV_Property_HrefList.new(this.directories);
153 162
             }
154 163
 
155 164
         }
156 165
 
157  
-        if (node instanceof ICard) {
158  
-
  166
+        if (node.hasFeature(jsCardDAV_iCard)) {
159 167
             // The address-data property is not supposed to be a 'real'
160 168
             // property, but in large chunks of the spec it does act as such.
161 169
             // Therefore we simply expose it as a property.
162  
-            addressDataProp = "{" . self::NS_CARDDAV . "}address-data";
163  
-            if (in_array(addressDataProp, requestedProperties)) {
164  
-                unset(requestedProperties[addressDataProp]);
165  
-                val = node.get();
166  
-                if (is_resource(val))
167  
-                    val = stream_get_contents(val);
168  
-
169  
-                returnedProperties[200][addressDataProp] = val;
170  
-
  170
+            var addressDataProp = "{" + this.NS_CARDDAV + "}address-data";
  171
+            var addressIdx = requestedProperties.indexOf(addressDataProp)
  172
+            if (addressIdx > -1) {
  173
+                requestedProperties.splice(addressIdx, 1);
  174
+                node.get(function(err, val) {
  175
+                    if (err)
  176
+                        return e.next(err);
  177
+                    returnedProperties["200"][addressDataProp] = val.toString("utf8");
  178
+                    afterICard();
  179
+                });
171 180
             }
  181
+            else
  182
+                afterICard();
172 183
         }
173  
-
174  
-        if (node instanceof UserAddressBooks) {
175  
-
176  
-            meCardProp = "{http://calendarserver.org/ns/}me-card";
177  
-            if (in_array(meCardProp, requestedProperties)) {
178  
-
179  
-                props = this.server.getProperties(node.getOwner(), array("{http://sabredav.org/ns}vcard-url"));
180  
-                if (isset(props["{http://sabredav.org/ns}vcard-url"])) {
181  
-
182  
-                    returnedProperties[200][meCardProp] = new DAV\Property\Href(
183  
-                        props["{http://sabredav.org/ns}vcard-url"]
184  
-                    );
185  
-                    pos = array_search(meCardProp, requestedProperties);
186  
-                    unset(requestedProperties[pos]);
187  
-
  184
+        else
  185
+            afterICard();
  186
+
  187
+        function afterICard() {
  188
+            if (node.hasFeature(jsCardDAV_UserAddressBooks)) {
  189
+                var meCardProp = "{http://calendarserver.org/ns/}me-card";
  190
+                var propIdx = requestedProperties.indexOf(meCardProp);
  191
+                if (propIdx > -1) {
  192
+                    self.handler.getProperties(node.getOwner(), ["{http://sabredav.org/ns}vcard-url"], function(err, props) {
  193
+                        if (err)
  194
+                            return e.next(err);
  195
+                            
  196
+                        if (props["{http://sabredav.org/ns}vcard-url"]) {
  197
+                            returnedProperties["200"][meCardProp] = jsDAV_Property_Href.new(
  198
+                                props["{http://sabredav.org/ns}vcard-url"]
  199
+                            );
  200
+                            requestedProperties.splice(propIdx, 1);
  201
+                        }
  202
+                        e.next();
  203
+                    });
188 204
                 }
189  
-
  205
+                else
  206
+                    e.next();
190 207
             }
191  
-
  208
+            else
  209
+                e.next();
192 210
         }
193  
-
194  
-    }
  211
+    },
195 212
 
196 213
     /**
197 214
      * This event is triggered when a PROPPATCH method is executed
@@ -201,72 +218,67 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
201 218
      * @param DAV\INode node
202 219
      * @return bool
203 220
      */
204  
-    updateProperties: function(&mutations, &result, DAV\INode node) {
  221
+    updateProperties: function(e, mutations, result, node) {
  222
+        if (!node.hasFeature(jsCardDAV_UserAddressBooks))
  223
+            return e.next();
205 224
 
206  
-        if (!node instanceof UserAddressBooks) {
207  
-            return true;
208  
-        }
209  
-
210  
-        meCard = "{http://calendarserver.org/ns/}me-card";
  225
+        var meCard = "{http://calendarserver.org/ns/}me-card";
211 226
 
212 227
         // The only property we care about
213  
-        if (!isset(mutations[meCard]))
214  
-            return true;
  228
+        if (!mutations[meCard])
  229
+            return e.next();
215 230
 
216  
-        value = mutations[meCard];
217  
-        unset(mutations[meCard]);
  231
+        var value = mutations[meCard];
  232
+        delete mutations[meCard];
218 233
 
219  
-        if (value instanceof DAV\Property\IHref) {
220  
-            value = value.getHref();
221  
-            value = this.server.calculateUri(value);
222  
-        } elseif (!is_null(value)) {
223  
-            result[400][meCard] = null;
224  
-            return false;
  234
+        if (value.hasFeature(jsDAV_Property_iHref)) {
  235
+            value = this.handler.calculateUri(value.getHref());
225 236
         }
226  
-
227  
-        innerResult = this.server.updateProperties(
228  
-            node.getOwner(),
229  
-            array(
230  
-                "{http://sabredav.org/ns}vcard-url": value,
231  
-            )
232  
-        );
233  
-
234  
-        closureResult = false;
235  
-        foreach(innerResult as status: props) {
236  
-            if (is_array(props) && array_key_exists("{http://sabredav.org/ns}vcard-url", props)) {
237  
-                result[status][meCard] = null;
238  
-                closureResult = (status>=200 && status<300);
239  
-            }
240  
-
  237
+        else if (!value) {
  238
+            result["400"][meCard] = null;
  239
+            return e.stop();
241 240
         }
242 241
 
243  
-        return result;
244  
-
245  
-    }
  242
+        this.server.updateProperties(node.getOwner(), {"{http://sabredav.org/ns}vcard-url": value}, function(err, innerResult) {
  243
+            if (err)
  244
+                return e.next(err);
  245
+            
  246
+            var closureResult = false;
  247
+            var props;
  248
+            for (var status in innerResult) {
  249
+                props = innerResult[status];
  250
+                if (props["{http://ajax.org/2005/aml}vcard-url"]) {
  251
+                    result[status][meCard] = null;
  252
+                    status = parseInt(status);
  253
+                    closureResult = (status >= 200 && status < 300);
  254
+                }
  255
+            }
  256
+    
  257
+            if (!closureResult)
  258
+                return e.stop();
  259
+            e.next();
  260
+        });
  261
+    },
246 262
 
247 263
     /**
248 264
      * This functions handles REPORT requests specific to CardDAV
249 265
      *
250 266
      * @param string reportName
251  
-     * @param \DOMNode dom
  267
+     * @param DOMNode dom
252 268
      * @return bool
253 269
      */
254  
-    report: function(reportName,dom) {
255  
-
  270
+    report: function(e, reportName, dom) {
256 271
         switch(reportName) {
257  
-            case "{".self::NS_CARDDAV."}addressbook-multiget" :
258  
-                this.addressbookMultiGetReport(dom);
259  
-                return false;
260  
-            case "{".self::NS_CARDDAV."}addressbook-query" :
261  
-                this.addressBookQueryReport(dom);
262  
-                return false;
  272
+            case "{" + this.NS_CARDDAV + "}addressbook-multiget" :
  273
+                this.addressbookMultiGetReport(e, dom);
  274
+                break;
  275
+            case "{" + this.NS_CARDDAV + "}addressbook-query" :
  276
+                this.addressBookQueryReport(e, dom);
  277
+                break;
263 278
             default :
264  
-                return;
265  
-
  279
+                return e.next();
266 280
         }
267  
-
268  
-
269  
-    }
  281
+    },
270 282
 
271 283
     /**
272 284
      * This function handles the addressbook-multiget REPORT.
@@ -274,31 +286,42 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
274 286
      * This report is used by the client to fetch the content of a series
275 287
      * of urls. Effectively avoiding a lot of redundant requests.
276 288
      *
277  
-     * @param \DOMNode dom
  289
+     * @param DOMNode dom
278 290
      * @return void
279 291
      */
280  
-    addressbookMultiGetReport: function(dom) {
281  
-
282  
-        properties = array_keys(DAV\XMLUtil::parseProperties(dom.firstChild));
283  
-
284  
-        hrefElems = dom.getElementsByTagNameNS("urn:DAV","href");
285  
-        propertyList = array();
286  
-
287  
-        foreach(hrefElems as elem) {
288  
-
289  
-            uri = this.server.calculateUri(elem.nodeValue);
290  
-            list(propertyList[]) = this.server.getPropertiesForPath(uri,properties);
291  
-
292  
-        }
293  
-
294  
-        prefer = this.server.getHTTPPRefer();
295  
-
296  
-        this.server.httpResponse.sendStatus(207);
297  
-        this.server.httpResponse.setHeader("Content-Type","application/xml; charset=utf-8");
298  
-        this.server.httpResponse.setHeader("Vary","Brief,Prefer");
299  
-        this.server.httpResponse.sendBody(this.server.generateMultiStatus(propertyList, prefer["return-minimal"]));
300  
-
301  
-    }
  292
+    addressbookMultiGetReport: function(e, dom) {
  293
+        var properties = Object.keys(Xml.parseProperties(dom.firstChild));
  294
+
  295
+        var hrefElems = dom.getElementsByTagName("d:href");
  296
+        var propertyList = {};
  297
+        var self = this;
  298
+
  299
+        Async.list(hrefElems)
  300
+            .each(function(elem, next) {
  301
+                var uri = self.handler.calculateUri(elem.nodeValue);
  302
+                //propertyList[uri]
  303
+                self.handler.getPropertiesForPath(uri, properties, 0, function(err, props) {
  304
+                    if (err)
  305
+                        return next(err);
  306
+                    
  307
+                    Util.extend(propertyList, props);
  308
+                    next();
  309
+                });
  310
+            })
  311
+            .end(function(err) {
  312
+                if (err)
  313
+                    return e.next(err);
  314
+                
  315
+                var prefer = self.handler.getHTTPPRefer();
  316
+        
  317
+                e.stop();
  318
+                self.handler.httpResponse.writeHead(207, {
  319
+                    "content-type": "application/xml; charset=utf-8",
  320
+                    "vary": "Brief,Prefer"
  321
+                });
  322
+                self.handler.httpResponse.end(self.handler.generateMultiStatus(propertyList, prefer["return-minimal"]));
  323
+            });
  324
+    },
302 325
 
303 326
     /**
304 327
      * This method is triggered before a file gets updated with new content.
@@ -311,14 +334,19 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
311 334
      * @param resource data
312 335
      * @return void
313 336
      */
314  
-    beforeWriteContent: function(path, DAV\IFile node, &data) {
315  
-
316  
-        if (!node instanceof ICard)
317  
-            return;
318  
-
319  
-        this.validateVCard(data);
  337
+    beforeWriteContent: function(e, path, node, data) {
  338
+        if (!node.hasFeature(jsCardDAV_iCard))
  339
+            return e.next();
320 340
 
321  
-    }
  341
+        try {
  342
+            this.validateVCard(data);
  343
+        }
  344
+        catch (ex) {
  345
+            return e.next(ex);
  346
+        }
  347
+        
  348
+        e.next();
  349
+    },
322 350
 
323 351
     /**
324 352
      * This method is triggered before a new file is created.
@@ -328,17 +356,22 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
328 356
      *
329 357
      * @param string path
330 358
      * @param resource data
331  
-     * @param DAV\ICollection parentNode
  359
+     * @param jsDAV_iCollection parentNode
332 360
      * @return void
333 361
      */
334  
-    beforeCreateFile: function(path, &data, DAV\ICollection parentNode) {
  362
+    beforeCreateFile: function(e, path, data, parentNode) {
  363
+        if (!parentNode.hasFeature(jsCardDAV_iAddressBook))
  364
+            return e.next();
335 365
 
336  
-        if (!parentNode instanceof IAddressBook)
337  
-            return;
338  
-
339  
-        this.validateVCard(data);
340  
-
341  
-    }
  366
+        try {
  367
+            this.validateVCard(data);
  368
+        }
  369
+        catch (ex) {
  370
+            return e.next(ex);
  371
+        }
  372
+        
  373
+        e.next();
  374
+    },
342 375
 
343 376
     /**
344 377
      * Checks if the submitted iCalendar data is in fact, valid.
@@ -348,36 +381,25 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
348 381
      * @param resource|string data
349 382
      * @return void
350 383
      */
351  
-    protected function validateVCard(&data) {
352  
-
  384
+    validateVCard: function(data) {
353 385
         // If it's a stream, we convert it to a string first.
354  
-        if (is_resource(data)) {
355  
-            data = stream_get_contents(data);
356  
-        }
357  
-
358  
-        // Converting the data to unicode, if needed.
359  
-        data = DAV\StringUtil::ensureUTF8(data);
  386
+        if (Buffer.isBuffer(data))
  387
+            data = data.toString("utf8");
360 388
 
  389
+        var vobj;
361 390
         try {
362  
-
363  
-            vobj = VObject\Reader::read(data);
364  
-
365  
-        } catch (VObject\ParseException e) {
366  
-
367  
-            throw new DAV\Exception\UnsupportedMediaType("This resource only supports valid vcard data. Parse error: " . e.getMessage());
368  
-
369  
-        }
370  
-
371  
-        if (vobj.name !== "VCARD") {
372  
-            throw new DAV\Exception\UnsupportedMediaType("This collection can only support vcard objects.");
  391
+            vobj = jsVObject_Reader.read(data);
373 392
         }
374  
-
375  
-        if (!isset(vobj.UID)) {
376  
-            throw new DAV\Exception\BadRequest("Every vcard must have a UID.");
  393
+        catch (ex) {
  394
+            throw new Exc.UnsupportedMediaType("This resource only supports valid vcard data. Parse error: " + ex.message);
377 395
         }
378 396
 
379  
-    }
  397
+        if (vobj.name != "VCARD")
  398
+            throw new Exc.UnsupportedMediaType("This collection can only support vcard objects.");
380 399
 
  400
+        if (!vobj.UID)
  401
+            throw new Exc.BadRequest("Every vcard must have a UID.");
  402
+    },
381 403
 
382 404
     /**
383 405
      * This function handles the addressbook-query REPORT
@@ -385,69 +407,97 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
385 407
      * This report is used by the client to filter an addressbook based on a
386 408
      * complex query.
387 409
      *
388  
-     * @param \DOMNode dom
  410
+     * @param DOMNode dom
389 411
      * @return void
390 412
      */
391  
-    protected function addressbookQueryReport(dom) {
392  
-
393  
-        query = new AddressBookQueryParser(dom);
394  
-        query.parse();
395  
-
396  
-        depth = this.server.getHTTPDepth(0);
397  
-
398  
-        if (depth==0) {
399  
-            candidateNodes = array(
400  
-                this.server.tree.getNodeForPath(this.server.getRequestUri())
401  
-            );
402  
-        } else {
403  
-            candidateNodes = this.server.tree.getChildren(this.server.getRequestUri());
  413
+    addressbookQueryReport: function(e, dom) {
  414
+        var query = new jsCardDAV_AddressBookQueryParser(dom);
  415
+        try {
  416
+            query.parse();
404 417
         }
405  
-
406  
-        validNodes = array();
407  
-        foreach(candidateNodes as node) {
408  
-
409  
-            if (!node instanceof ICard)
410  
-                continue;
411  
-
412  
-            blob = node.get();
413  
-            if (is_resource(blob)) {
414  
-                blob = stream_get_contents(blob);
415  
-            }
416  
-
417  
-            if (!this.validateFilters(blob, query.filters, query.test)) {
418  
-                continue;
419  
-            }
420  
-
421  
-            validNodes[] = node;
422  
-
423  
-            if (query.limit && query.limit <= count(validNodes)) {
424  
-                // We hit the maximum number of items, we can stop now.
425  
-                break;
426  
-            }
427  
-
  418
+        catch(ex) {
  419
+            console.log("QUERY PARSE ERROR:",ex);
  420
+            return e.next(ex);
428 421
         }
429 422
 
430  
-        result = array();
431  
-        foreach(validNodes as validNode) {
432  
-
433  
-            if (depth==0) {
434  
-                href = this.server.getRequestUri();
435  
-            } else {
436  
-                href = this.server.getRequestUri() . "/" . validNode.getName();
437  
-            }
438  
-
439  
-            list(result[]) = this.server.getPropertiesForPath(href, query.requestedProperties, 0);
  423
+        var depth = this.handler.getHTTPDepth(0);
440 424
 
  425
+        if (depth === 0) {
  426
+            this.handler.getNodeForPath(this.handler.getRequestUri(), function(err, node) {
  427
+                if (err)
  428
+                    return e.next(err);
  429
+                afterCandidates([node]);
  430
+            })
  431
+        }
  432
+        else {
  433
+            this.handler.server.tree.getChildren(this.handler.getRequestUri(), function(err, children) {
  434
+                if (err)
  435
+                    return e.next(err);
  436
+                afterCandidates(children);
  437
+            });
441 438
         }
442 439
 
443  
-        prefer = this.server.getHTTPPRefer();
444  
-
445  
-        this.server.httpResponse.sendStatus(207);
446  
-        this.server.httpResponse.setHeader("Content-Type","application/xml; charset=utf-8");
447  
-        this.server.httpResponse.setHeader("Vary","Brief,Prefer");
448  
-        this.server.httpResponse.sendBody(this.server.generateMultiStatus(result, prefer["return-minimal"]));
449  
-
450  
-    }
  440
+        var self = this;
  441
+        function afterCandidates(candidateNodes) {
  442
+            var validNodes = [];
  443
+            
  444
+            Async.list(candidateNodes)
  445
+                .each(function(node, next) {
  446
+                    if (!node.hasFeature(jsCardDAV_iCard))
  447
+                        return next();
  448
+        
  449
+                    node.get(function(err, blob) {
  450
+                        if (err)
  451
+                            return next(err);
  452
+                        
  453
+                        if (!self.validateFilters(blob.toString("utf8"), query.filters, query.test))
  454
+                            return next();
  455
+            
  456
+                        validNodes.push(node);
  457
+            
  458
+                        if (query.limit && query.limit <= validNodes.length) {
  459
+                            // We hit the maximum number of items, we can stop now.
  460
+                            return next(Async.STOP);
  461
+                        }
  462
+                        
  463
+                        next();
  464
+                    });
  465
+                })
  466
+                .end(function(err) {
  467
+                    if (err)
  468
+                        return e.next(err);
  469
+                        
  470
+                    var result = {};
  471
+                    Async.list(validNodes)
  472
+                        .each(function(validNode, next) {
  473
+                            var href = self.handler.getRequestUri();
  474
+                            if (depth !== 0)
  475
+                                href = href + "/" + validNode.getName();
  476
+                
  477
+                            self.handler.getPropertiesForPath(href, query.requestedProperties, 0, function(err, props) {
  478
+                                if (err)
  479
+                                    return next(err);
  480
+                                    
  481
+                                Util.extend(result, props);
  482
+                                next();
  483
+                            });
  484
+                        })
  485
+                        .end(function(err) {
  486
+                            if (err)
  487
+                                return e.next(err);
  488
+                            
  489
+                            e.stop();
  490
+                            var prefer = self.handler.getHTTPPRefer();
  491
+            
  492
+                            self.handler.httpResponse.writeHead(207, {
  493
+                                "content-type": "application/xml; charset=utf-8",
  494
+                                "vary": "Brief,Prefer"
  495
+                            });
  496
+                            self.handler.httpResponse.end(self.handler.generateMultiStatus(result, prefer["return-minimal"]));
  497
+                        });
  498
+                });
  499
+        }
  500
+    },
451 501
 
452 502
     /**
453 503
      * Validates if a vcard makes it throught a list of filters.
@@ -457,63 +507,64 @@ var jsCardDAV_Plugin = module.exports = jsDAV_Plugin.extend({
457 507
      * @param string test anyof or allof (which means OR or AND)
458 508
      * @return bool
459 509
      */
460  
-    validateFilters: function(vcardData, array filters, test) {
461  
-
462  
-        vcard = VObject\Reader::read(vcardData);
  510
+    validateFilters: function(vcardData, filters, test) {
  511
+        var vcard;
  512
+        try {
  513
+            vcard = jsVObject_Reader.read(vcardData);
  514
+        }
  515
+        catch (ex) {
  516
+            return false;
  517
+        }
463 518
 
464  
-        if (!filters) return true;
  519
+        if (!filters)
  520
+            return true;
465 521
 
466  
-        foreach(filters as filter) {
  522
+        var filter, isDefined, success, vProperties, results, texts;
  523
+        for (var i = 0, l = filters.length; i < l; ++i) {
  524
+            filter = filters[i];
467 525
 
468  
-            isDefined = isset(vcard.{filter["name"]});
  526
+            isDefined = vcard.get(filter.name);
469 527
             if (filter["is-not-defined"]) {
470  
-                if (isDefined) {
  528
+                if (isDefined)
471 529
                     success = false;
472  
-                } else {
  530
+                else
473 531
                     success = true;
474  
-                }
475  
-            } elseif ((!filter["param-filters"] && !filter["text-matches"]) || !isDefined) {
476  
-
  532
+            }
  533
+            else if ((!filter["param-filters"] && !filter["text-matches"]) || !isDefined) {
477 534
                 // We only need to check for existence
478 535
                 success = isDefined;
  536
+            }
  537
+            else {
  538
+                vProperties = vcard.select(filter.name);
479 539
 
480  
-            } else {
481  
-
482  
-                vProperties = vcard.select(filter["name"]);
483  
-
484  
-                results = array();
485  
-                if (filter["param-filters"]) {
486  
-                    results[] = this.validateParamFilters(vProperties, filter["param-filters"], filter["test"]);
487  
-                }
  540
+                results = [];
  541
+                if (filter["param-filters"])
  542
+                    results.push(this.validateParamFilters(vProperties, filter["param-filters"], filter.test));
488 543
                 if (filter["text-matches"]) {
489  
-                    texts = array();
490  
-                    foreach(vProperties as vProperty)
491  
-                        texts[] = vProperty.value;
492  
-
493  
-                    results[] = this.validateTextMatches(texts, filter["text-matches"], filter["test"]);
  544
+                    texts = vProperties.map(function(vProperty) {
  545
+                        return vProperty.value;
  546
+                    });
  547
+                    
  548
+                    results.push(this.validateTextMatches(texts, filter["text-matches"], filter.test));
494 549
                 }
495 550
 
496  
-                if (count(results)===1) {
  551
+                if (results.length === 1) {