1/* 2 * vim: expandtab shiftwidth=4 softtabstop=4 3 */ 4 5/* global dav */ 6if (typeof dav == 'undefined') { dav = {}; }; 7 8dav._XML_CHAR_MAP = { 9 '<': '<', 10 '>': '>', 11 '&': '&', 12 '"': '"', 13 "'": ''' 14}; 15 16dav._escapeXml = function(s) { 17 return s.replace(/[<>&"']/g, function (ch) { 18 return dav._XML_CHAR_MAP[ch]; 19 }); 20}; 21 22dav.Client = function(options) { 23 var i; 24 for(i in options) { 25 this[i] = options[i]; 26 } 27 28}; 29 30dav.Client.prototype = { 31 32 baseUrl : null, 33 34 userName : null, 35 36 password : null, 37 38 39 xmlNamespaces : { 40 'DAV:' : 'd' 41 }, 42 43 /** 44 * Generates a propFind request. 45 * 46 * @param {string} url Url to do the propfind request on 47 * @param {Array} properties List of properties to retrieve. 48 * @param {Object} [headers] headers 49 * @return {Promise} 50 */ 51 propFind : function(url, properties, depth, headers) { 52 53 if(typeof depth == "undefined") { 54 depth = 0; 55 } 56 57 headers = headers || {}; 58 59 headers['Depth'] = depth; 60 headers['Content-Type'] = 'application/xml; charset=utf-8'; 61 62 var body = 63 '<?xml version="1.0"?>\n' + 64 '<d:propfind '; 65 var namespace; 66 for (namespace in this.xmlNamespaces) { 67 body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"'; 68 } 69 body += '>\n' + 70 ' <d:prop>\n'; 71 72 for(var ii in properties) { 73 74 var property = this.parseClarkNotation(properties[ii]); 75 if (this.xmlNamespaces[property.namespace]) { 76 body+=' <' + this.xmlNamespaces[property.namespace] + ':' + property.name + ' />\n'; 77 } else { 78 body+=' <x:' + property.name + ' xmlns:x="' + property.namespace + '" />\n'; 79 } 80 81 } 82 body+=' </d:prop>\n'; 83 body+='</d:propfind>'; 84 85 return this.request('PROPFIND', url, headers, body).then( 86 function(result) { 87 88 if (depth===0) { 89 return { 90 status: result.status, 91 body: result.body[0], 92 xhr: result.xhr 93 }; 94 } else { 95 return { 96 status: result.status, 97 body: result.body, 98 xhr: result.xhr 99 }; 100 } 101 102 }.bind(this) 103 ); 104 105 }, 106 107 /** 108 * Renders a "d:set" block for the given properties. 109 * 110 * @param {Object.<String,String>} properties 111 * @return {String} XML "<d:set>" block 112 */ 113 _renderPropSet: function(properties) { 114 var body = ' <d:set>\n' + 115 ' <d:prop>\n'; 116 117 for(var ii in properties) { 118 var property = this.parseClarkNotation(ii); 119 var propName; 120 var propValue = properties[ii]; 121 if (this.xmlNamespaces[property.namespace]) { 122 propName = this.xmlNamespaces[property.namespace] + ':' + property.name; 123 } else { 124 propName = 'x:' + property.name + ' xmlns:x="' + property.namespace + '"'; 125 } 126 127 // FIXME: hard-coded for now until we allow properties to 128 // specify whether to be escaped or not 129 if (propName !== 'd:resourcetype') { 130 propValue = dav._escapeXml(propValue); 131 } 132 body += ' <' + propName + '>' + propValue + '</' + propName + '>\n'; 133 } 134 body +=' </d:prop>\n'; 135 body +=' </d:set>\n'; 136 return body; 137 }, 138 139 /** 140 * Generates a propPatch request. 141 * 142 * @param {string} url Url to do the proppatch request on 143 * @param {Object.<String,String>} properties List of properties to store. 144 * @param {Object} [headers] headers 145 * @return {Promise} 146 */ 147 propPatch : function(url, properties, headers) { 148 headers = headers || {}; 149 150 headers['Content-Type'] = 'application/xml; charset=utf-8'; 151 152 var body = 153 '<?xml version="1.0"?>\n' + 154 '<d:propertyupdate '; 155 var namespace; 156 for (namespace in this.xmlNamespaces) { 157 body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"'; 158 } 159 body += '>\n' + this._renderPropSet(properties); 160 body += '</d:propertyupdate>'; 161 162 return this.request('PROPPATCH', url, headers, body).then( 163 function(result) { 164 return { 165 status: result.status, 166 body: result.body, 167 xhr: result.xhr 168 }; 169 }.bind(this) 170 ); 171 172 }, 173 174 /** 175 * Generates a MKCOL request. 176 * If attributes are given, it will use an extended MKCOL request. 177 * 178 * @param {string} url Url to do the proppatch request on 179 * @param {Object.<String,String>} [properties] list of properties to store. 180 * @param {Object} [headers] headers 181 * @return {Promise} 182 */ 183 mkcol : function(url, properties, headers) { 184 var body = ''; 185 headers = headers || {}; 186 headers['Content-Type'] = 'application/xml; charset=utf-8'; 187 188 if (properties) { 189 body = 190 '<?xml version="1.0"?>\n' + 191 '<d:mkcol'; 192 var namespace; 193 for (namespace in this.xmlNamespaces) { 194 body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"'; 195 } 196 body += '>\n' + this._renderPropSet(properties); 197 body +='</d:mkcol>'; 198 } 199 200 return this.request('MKCOL', url, headers, body).then( 201 function(result) { 202 return { 203 status: result.status, 204 body: result.body, 205 xhr: result.xhr 206 }; 207 }.bind(this) 208 ); 209 210 }, 211 212 /** 213 * Performs a HTTP request, and returns a Promise 214 * 215 * @param {string} method HTTP method 216 * @param {string} url Relative or absolute url 217 * @param {Object} headers HTTP headers as an object. 218 * @param {string} body HTTP request body. 219 * @return {Promise} 220 */ 221 request : function(method, url, headers, body) { 222 223 var self = this; 224 var xhr = this.xhrProvider(); 225 headers = headers || {}; 226 227 if (this.userName) { 228 headers['Authorization'] = 'Basic ' + btoa(this.userName + ':' + this.password); 229 // xhr.open(method, this.resolveUrl(url), true, this.userName, this.password); 230 } 231 xhr.open(method, this.resolveUrl(url), true); 232 var ii; 233 for(ii in headers) { 234 xhr.setRequestHeader(ii, headers[ii]); 235 } 236 237 // Work around for edge 238 if (body === undefined) { 239 xhr.send(); 240 } else { 241 xhr.send(body); 242 } 243 244 return new Promise(function(fulfill, reject) { 245 246 xhr.onreadystatechange = function() { 247 248 if (xhr.readyState !== 4) { 249 return; 250 } 251 252 var resultBody = xhr.response; 253 if (xhr.status === 207) { 254 resultBody = self.parseMultiStatus(xhr.response); 255 } 256 257 fulfill({ 258 body: resultBody, 259 status: xhr.status, 260 xhr: xhr 261 }); 262 263 }; 264 265 xhr.ontimeout = function() { 266 267 reject(new Error('Timeout exceeded')); 268 269 }; 270 271 }); 272 273 }, 274 275 /** 276 * Returns an XMLHttpRequest object. 277 * 278 * This is in its own method, so it can be easily overridden. 279 * 280 * @return {XMLHttpRequest} 281 */ 282 xhrProvider : function() { 283 284 return new XMLHttpRequest(); 285 286 }, 287 288 /** 289 * Parses a property node. 290 * 291 * Either returns a string if the node only contains text, or returns an 292 * array of non-text subnodes. 293 * 294 * @param {Object} propNode node to parse 295 * @return {string|Array} text content as string or array of subnodes, excluding text nodes 296 */ 297 _parsePropNode: function(propNode) { 298 var content = null; 299 if (propNode.childNodes && propNode.childNodes.length > 0) { 300 var subNodes = []; 301 // filter out text nodes 302 for (var j = 0; j < propNode.childNodes.length; j++) { 303 var node = propNode.childNodes[j]; 304 if (node.nodeType === 1) { 305 subNodes.push(node); 306 } 307 } 308 if (subNodes.length) { 309 content = subNodes; 310 } 311 } 312 313 return content || propNode.textContent || propNode.text || ''; 314 }, 315 316 /** 317 * Parses a multi-status response body. 318 * 319 * @param {string} xmlBody 320 * @param {Array} 321 */ 322 parseMultiStatus : function(xmlBody) { 323 324 var parser = new DOMParser(); 325 var doc = parser.parseFromString(xmlBody, "application/xml"); 326 327 var resolver = function(foo) { 328 var ii; 329 for(ii in this.xmlNamespaces) { 330 if (this.xmlNamespaces[ii] === foo) { 331 return ii; 332 } 333 } 334 }.bind(this); 335 336 var responseIterator = doc.evaluate('/d:multistatus/d:response', doc, resolver, XPathResult.ANY_TYPE, null); 337 338 var result = []; 339 var responseNode = responseIterator.iterateNext(); 340 341 while(responseNode) { 342 343 var response = { 344 href : null, 345 propStat : [] 346 }; 347 348 response.href = doc.evaluate('string(d:href)', responseNode, resolver, XPathResult.ANY_TYPE, null).stringValue; 349 350 var propStatIterator = doc.evaluate('d:propstat', responseNode, resolver, XPathResult.ANY_TYPE, null); 351 var propStatNode = propStatIterator.iterateNext(); 352 353 while(propStatNode) { 354 var propStat = { 355 status : doc.evaluate('string(d:status)', propStatNode, resolver, XPathResult.ANY_TYPE, null).stringValue, 356 properties : {}, 357 }; 358 359 var propIterator = doc.evaluate('d:prop/*', propStatNode, resolver, XPathResult.ANY_TYPE, null); 360 361 var propNode = propIterator.iterateNext(); 362 while(propNode) { 363 var content = this._parsePropNode(propNode); 364 propStat.properties['{' + propNode.namespaceURI + '}' + propNode.localName] = content; 365 propNode = propIterator.iterateNext(); 366 367 } 368 response.propStat.push(propStat); 369 propStatNode = propStatIterator.iterateNext(); 370 371 372 } 373 374 result.push(response); 375 responseNode = responseIterator.iterateNext(); 376 377 } 378 379 return result; 380 381 }, 382 383 /** 384 * Takes a relative url, and maps it to an absolute url, using the baseUrl 385 * 386 * @param {string} url 387 * @return {string} 388 */ 389 resolveUrl : function(url) { 390 391 // Note: this is rudamentary.. not sure yet if it handles every case. 392 if (/^https?:\/\//i.test(url)) { 393 // absolute 394 return url; 395 } 396 397 var baseParts = this.parseUrl(this.baseUrl); 398 if (url.charAt('/')) { 399 // Url starts with a slash 400 return baseParts.root + url; 401 } 402 403 // Url does not start with a slash, we need grab the base url right up until the last slash. 404 var newUrl = baseParts.root + '/'; 405 if (baseParts.path.lastIndexOf('/')!==-1) { 406 newUrl = newUrl = baseParts.path.subString(0, baseParts.path.lastIndexOf('/')) + '/'; 407 } 408 newUrl+=url; 409 return url; 410 411 }, 412 413 /** 414 * Parses a url and returns its individual components. 415 * 416 * @param {String} url 417 * @return {Object} 418 */ 419 parseUrl : function(url) { 420 421 var parts = url.match(/^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/); 422 var result = { 423 url : parts[0], 424 scheme : parts[1], 425 host : parts[3], 426 port : parts[4], 427 path : parts[5], 428 query : parts[6], 429 fragment : parts[7], 430 }; 431 result.root = 432 result.scheme + '://' +
433 result.host + 434 (result.port ? ':' + result.port : ''); 435 436 return result; 437 438 }, 439 440 parseClarkNotation : function(propertyName) { 441 442 var result = propertyName.match(/^{([^}]+)}(.*)$/); 443 if (!result) { 444 return; 445 } 446 447 return { 448 name : result[2], 449 namespace : result[1] 450 }; 451 452 } 453 454}; 455
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.