You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

client.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. /*
  2. * Copyright (c) 2015
  3. *
  4. * This file is licensed under the Affero General Public License version 3
  5. * or later.
  6. *
  7. * See the COPYING-README file.
  8. *
  9. */
  10. /* global dav */
  11. (function(OC, FileInfo) {
  12. /**
  13. * @class OC.Files.Client
  14. * @classdesc Client to access files on the server
  15. *
  16. * @param {Object} options
  17. * @param {String} options.host host name
  18. * @param {int} [options.port] port
  19. * @param {boolean} [options.useHTTPS] whether to use https
  20. * @param {String} [options.root] root path
  21. * @param {String} [options.userName] user name
  22. * @param {String} [options.password] password
  23. *
  24. * @since 8.2
  25. */
  26. var Client = function(options) {
  27. this._root = options.root;
  28. if (this._root.charAt(this._root.length - 1) === '/') {
  29. this._root = this._root.substr(0, this._root.length - 1);
  30. }
  31. var url = 'http://';
  32. if (options.useHTTPS) {
  33. url = 'https://';
  34. }
  35. url += options.host + this._root;
  36. this._defaultHeaders = options.defaultHeaders || {
  37. 'X-Requested-With': 'XMLHttpRequest',
  38. 'requesttoken': OC.requestToken
  39. };
  40. this._baseUrl = url;
  41. var clientOptions = {
  42. baseUrl: this._baseUrl,
  43. xmlNamespaces: {
  44. 'DAV:': 'd',
  45. 'http://owncloud.org/ns': 'oc'
  46. }
  47. };
  48. if (options.userName) {
  49. clientOptions.userName = options.userName;
  50. }
  51. if (options.password) {
  52. clientOptions.password = options.password;
  53. }
  54. this._client = new dav.Client(clientOptions);
  55. this._client.xhrProvider = _.bind(this._xhrProvider, this);
  56. };
  57. Client.NS_OWNCLOUD = 'http://owncloud.org/ns';
  58. Client.NS_DAV = 'DAV:';
  59. Client._PROPFIND_PROPERTIES = [
  60. /**
  61. * Modified time
  62. */
  63. [Client.NS_DAV, 'getlastmodified'],
  64. /**
  65. * Etag
  66. */
  67. [Client.NS_DAV, 'getetag'],
  68. /**
  69. * Mime type
  70. */
  71. [Client.NS_DAV, 'getcontenttype'],
  72. /**
  73. * Resource type "collection" for folders, empty otherwise
  74. */
  75. [Client.NS_DAV, 'resourcetype'],
  76. /**
  77. * File id
  78. */
  79. [Client.NS_OWNCLOUD, 'fileid'],
  80. /**
  81. * Letter-coded permissions
  82. */
  83. [Client.NS_OWNCLOUD, 'permissions'],
  84. //[Client.NS_OWNCLOUD, 'downloadURL'],
  85. /**
  86. * Folder sizes
  87. */
  88. [Client.NS_OWNCLOUD, 'size'],
  89. /**
  90. * File sizes
  91. */
  92. [Client.NS_DAV, 'getcontentlength']
  93. ];
  94. /**
  95. * @memberof OC.Files
  96. */
  97. Client.prototype = {
  98. /**
  99. * Root path of the Webdav endpoint
  100. *
  101. * @type string
  102. */
  103. _root: null,
  104. /**
  105. * Client from the library
  106. *
  107. * @type dav.Client
  108. */
  109. _client: null,
  110. /**
  111. * Array of file info parsing functions.
  112. *
  113. * @type Array<OC.Files.Client~parseFileInfo>
  114. */
  115. _fileInfoParsers: [],
  116. /**
  117. * Returns the configured XHR provider for davclient
  118. * @return {XMLHttpRequest}
  119. */
  120. _xhrProvider: function() {
  121. var headers = this._defaultHeaders;
  122. var xhr = new XMLHttpRequest();
  123. var oldOpen = xhr.open;
  124. // override open() method to add headers
  125. xhr.open = function() {
  126. var result = oldOpen.apply(this, arguments);
  127. _.each(headers, function(value, key) {
  128. xhr.setRequestHeader(key, value);
  129. });
  130. return result;
  131. };
  132. OC.registerXHRForErrorProcessing(xhr);
  133. return xhr;
  134. },
  135. /**
  136. * Prepends the base url to the given path sections
  137. *
  138. * @param {...String} path sections
  139. *
  140. * @return {String} base url + joined path, any leading or trailing slash
  141. * will be kept
  142. */
  143. _buildUrl: function() {
  144. var path = this._buildPath.apply(this, arguments);
  145. if (path.charAt([path.length - 1]) === '/') {
  146. path = path.substr(0, path.length - 1);
  147. }
  148. if (path.charAt(0) === '/') {
  149. path = path.substr(1);
  150. }
  151. return this._baseUrl + '/' + path;
  152. },
  153. /**
  154. * Append the path to the root and also encode path
  155. * sections
  156. *
  157. * @param {...String} path sections
  158. *
  159. * @return {String} joined path, any leading or trailing slash
  160. * will be kept
  161. */
  162. _buildPath: function() {
  163. var path = OC.joinPaths.apply(this, arguments);
  164. var sections = path.split('/');
  165. var i;
  166. for (i = 0; i < sections.length; i++) {
  167. sections[i] = encodeURIComponent(sections[i]);
  168. }
  169. path = sections.join('/');
  170. return path;
  171. },
  172. /**
  173. * Parse headers string into a map
  174. *
  175. * @param {string} headersString headers list as string
  176. *
  177. * @return {Object.<String,Array>} map of header name to header contents
  178. */
  179. _parseHeaders: function(headersString) {
  180. var headerRows = headersString.split('\n');
  181. var headers = {};
  182. for (var i = 0; i < headerRows.length; i++) {
  183. var sepPos = headerRows[i].indexOf(':');
  184. if (sepPos < 0) {
  185. continue;
  186. }
  187. var headerName = headerRows[i].substr(0, sepPos);
  188. var headerValue = headerRows[i].substr(sepPos + 2);
  189. if (!headers[headerName]) {
  190. // make it an array
  191. headers[headerName] = [];
  192. }
  193. headers[headerName].push(headerValue);
  194. }
  195. return headers;
  196. },
  197. /**
  198. * Parses the etag response which is in double quotes.
  199. *
  200. * @param {string} etag etag value in double quotes
  201. *
  202. * @return {string} etag without double quotes
  203. */
  204. _parseEtag: function(etag) {
  205. if (etag.charAt(0) === '"') {
  206. return etag.split('"')[1];
  207. }
  208. return etag;
  209. },
  210. /**
  211. * Parse Webdav result
  212. *
  213. * @param {Object} response XML object
  214. *
  215. * @return {Array.<FileInfo>} array of file info
  216. */
  217. _parseFileInfo: function(response) {
  218. var path = response.href;
  219. if (path.substr(0, this._root.length) === this._root) {
  220. path = path.substr(this._root.length);
  221. }
  222. if (path.charAt(path.length - 1) === '/') {
  223. path = path.substr(0, path.length - 1);
  224. }
  225. path = decodeURIComponent(path);
  226. if (response.propStat.length === 0 || response.propStat[0].status !== 'HTTP/1.1 200 OK') {
  227. return null;
  228. }
  229. var props = response.propStat[0].properties;
  230. var data = {
  231. id: props['{' + Client.NS_OWNCLOUD + '}fileid'],
  232. path: OC.dirname(path) || '/',
  233. name: OC.basename(path),
  234. mtime: (new Date(props['{' + Client.NS_DAV + '}getlastmodified'])).getTime()
  235. };
  236. var etagProp = props['{' + Client.NS_DAV + '}getetag'];
  237. if (!_.isUndefined(etagProp)) {
  238. data.etag = this._parseEtag(etagProp);
  239. }
  240. var sizeProp = props['{' + Client.NS_DAV + '}getcontentlength'];
  241. if (!_.isUndefined(sizeProp)) {
  242. data.size = parseInt(sizeProp, 10);
  243. }
  244. sizeProp = props['{' + Client.NS_OWNCLOUD + '}size'];
  245. if (!_.isUndefined(sizeProp)) {
  246. data.size = parseInt(sizeProp, 10);
  247. }
  248. var contentType = props['{' + Client.NS_DAV + '}getcontenttype'];
  249. if (!_.isUndefined(contentType)) {
  250. data.mimetype = contentType;
  251. }
  252. var resType = props['{' + Client.NS_DAV + '}resourcetype'];
  253. var isFile = true;
  254. if (!data.mimetype && resType) {
  255. var xmlvalue = resType[0];
  256. if (xmlvalue.namespaceURI === Client.NS_DAV && xmlvalue.nodeName.split(':')[1] === 'collection') {
  257. data.mimetype = 'httpd/unix-directory';
  258. isFile = false;
  259. }
  260. }
  261. data.permissions = OC.PERMISSION_READ;
  262. var permissionProp = props['{' + Client.NS_OWNCLOUD + '}permissions'];
  263. if (!_.isUndefined(permissionProp)) {
  264. var permString = permissionProp || '';
  265. data.mountType = null;
  266. for (var i = 0; i < permString.length; i++) {
  267. var c = permString.charAt(i);
  268. switch (c) {
  269. // FIXME: twisted permissions
  270. case 'C':
  271. case 'K':
  272. data.permissions |= OC.PERMISSION_CREATE;
  273. if (!isFile) {
  274. data.permissions |= OC.PERMISSION_UPDATE;
  275. }
  276. break;
  277. case 'W':
  278. data.permissions |= OC.PERMISSION_UPDATE;
  279. break;
  280. case 'D':
  281. data.permissions |= OC.PERMISSION_DELETE;
  282. break;
  283. case 'R':
  284. data.permissions |= OC.PERMISSION_SHARE;
  285. break;
  286. case 'M':
  287. if (!data.mountType) {
  288. // TODO: how to identify external-root ?
  289. data.mountType = 'external';
  290. }
  291. break;
  292. case 'S':
  293. // TODO: how to identify shared-root ?
  294. data.mountType = 'shared';
  295. break;
  296. }
  297. }
  298. }
  299. // extend the parsed data using the custom parsers
  300. _.each(this._fileInfoParsers, function(parserFunction) {
  301. _.extend(data, parserFunction(response) || {});
  302. });
  303. return new FileInfo(data);
  304. },
  305. /**
  306. * Parse Webdav multistatus
  307. *
  308. * @param {Array} responses
  309. */
  310. _parseResult: function(responses) {
  311. var self = this;
  312. return _.map(responses, function(response) {
  313. return self._parseFileInfo(response);
  314. });
  315. },
  316. /**
  317. * Returns whether the given status code means success
  318. *
  319. * @param {int} status status code
  320. *
  321. * @return true if status code is between 200 and 299 included
  322. */
  323. _isSuccessStatus: function(status) {
  324. return status >= 200 && status <= 299;
  325. },
  326. /**
  327. * Returns the default PROPFIND properties to use during a call.
  328. *
  329. * @return {Array.<Object>} array of properties
  330. */
  331. getPropfindProperties: function() {
  332. if (!this._propfindProperties) {
  333. this._propfindProperties = _.map(Client._PROPFIND_PROPERTIES, function(propDef) {
  334. return '{' + propDef[0] + '}' + propDef[1];
  335. });
  336. }
  337. return this._propfindProperties;
  338. },
  339. /**
  340. * Lists the contents of a directory
  341. *
  342. * @param {String} path path to retrieve
  343. * @param {Object} [options] options
  344. * @param {boolean} [options.includeParent=false] set to true to keep
  345. * the parent folder in the result list
  346. * @param {Array} [options.properties] list of Webdav properties to retrieve
  347. *
  348. * @return {Promise} promise
  349. */
  350. getFolderContents: function(path, options) {
  351. if (!path) {
  352. path = '';
  353. }
  354. options = options || {};
  355. var self = this;
  356. var deferred = $.Deferred();
  357. var promise = deferred.promise();
  358. var properties;
  359. if (_.isUndefined(options.properties)) {
  360. properties = this.getPropfindProperties();
  361. } else {
  362. properties = options.properties;
  363. }
  364. this._client.propFind(
  365. this._buildUrl(path),
  366. properties,
  367. 1
  368. ).then(function(result) {
  369. if (self._isSuccessStatus(result.status)) {
  370. var results = self._parseResult(result.body);
  371. if (!options || !options.includeParent) {
  372. // remove root dir, the first entry
  373. results.shift();
  374. }
  375. deferred.resolve(result.status, results);
  376. } else {
  377. deferred.reject(result.status);
  378. }
  379. });
  380. return promise;
  381. },
  382. /**
  383. * Fetches a flat list of files filtered by a given filter criteria.
  384. * (currently only system tags is supported)
  385. *
  386. * @param {Object} filter filter criteria
  387. * @param {Object} [filter.systemTagIds] list of system tag ids to filter by
  388. * @param {Object} [options] options
  389. * @param {Array} [options.properties] list of Webdav properties to retrieve
  390. *
  391. * @return {Promise} promise
  392. */
  393. getFilteredFiles: function(filter, options) {
  394. options = options || {};
  395. var self = this;
  396. var deferred = $.Deferred();
  397. var promise = deferred.promise();
  398. var properties;
  399. if (_.isUndefined(options.properties)) {
  400. properties = this.getPropfindProperties();
  401. } else {
  402. properties = options.properties;
  403. }
  404. if (!filter || !filter.systemTagIds || !filter.systemTagIds.length) {
  405. throw 'Missing filter argument';
  406. }
  407. // root element with namespaces
  408. var body = '<oc:filter-files ';
  409. var namespace;
  410. for (namespace in this._client.xmlNamespaces) {
  411. body += ' xmlns:' + this._client.xmlNamespaces[namespace] + '="' + namespace + '"';
  412. }
  413. body += '>\n';
  414. // properties query
  415. body += ' <' + this._client.xmlNamespaces['DAV:'] + ':prop>\n';
  416. _.each(properties, function(prop) {
  417. var property = self._client.parseClarkNotation(prop);
  418. body += ' <' + self._client.xmlNamespaces[property.namespace] + ':' + property.name + ' />\n';
  419. });
  420. body += ' </' + this._client.xmlNamespaces['DAV:'] + ':prop>\n';
  421. // rules block
  422. body += ' <oc:filter-rules>\n';
  423. _.each(filter.systemTagIds, function(systemTagIds) {
  424. body += ' <oc:systemtag>' + escapeHTML(systemTagIds) + '</oc:systemtag>\n';
  425. });
  426. body += ' </oc:filter-rules>\n';
  427. // end of root
  428. body += '</oc:filter-files>\n';
  429. this._client.request(
  430. 'REPORT',
  431. this._buildUrl(),
  432. {},
  433. body
  434. ).then(function(result) {
  435. if (self._isSuccessStatus(result.status)) {
  436. var results = self._parseResult(result.body);
  437. deferred.resolve(result.status, results);
  438. } else {
  439. deferred.reject(result.status);
  440. }
  441. });
  442. return promise;
  443. },
  444. /**
  445. * Returns the file info of a given path.
  446. *
  447. * @param {String} path path
  448. * @param {Array} [options.properties] list of Webdav properties to retrieve
  449. *
  450. * @return {Promise} promise
  451. */
  452. getFileInfo: function(path, options) {
  453. if (!path) {
  454. path = '';
  455. }
  456. options = options || {};
  457. var self = this;
  458. var deferred = $.Deferred();
  459. var promise = deferred.promise();
  460. var properties;
  461. if (_.isUndefined(options.properties)) {
  462. properties = this.getPropfindProperties();
  463. } else {
  464. properties = options.properties;
  465. }
  466. // TODO: headers
  467. this._client.propFind(
  468. this._buildUrl(path),
  469. properties,
  470. 0
  471. ).then(
  472. function(result) {
  473. if (self._isSuccessStatus(result.status)) {
  474. deferred.resolve(result.status, self._parseResult([result.body])[0]);
  475. } else {
  476. deferred.reject(result.status);
  477. }
  478. }
  479. );
  480. return promise;
  481. },
  482. /**
  483. * Returns the contents of the given file.
  484. *
  485. * @param {String} path path to file
  486. *
  487. * @return {Promise}
  488. */
  489. getFileContents: function(path) {
  490. if (!path) {
  491. throw 'Missing argument "path"';
  492. }
  493. var self = this;
  494. var deferred = $.Deferred();
  495. var promise = deferred.promise();
  496. this._client.request(
  497. 'GET',
  498. this._buildUrl(path)
  499. ).then(
  500. function(result) {
  501. if (self._isSuccessStatus(result.status)) {
  502. deferred.resolve(result.status, result.body);
  503. } else {
  504. deferred.reject(result.status);
  505. }
  506. }
  507. );
  508. return promise;
  509. },
  510. /**
  511. * Puts the given data into the given file.
  512. *
  513. * @param {String} path path to file
  514. * @param {String} body file body
  515. * @param {Object} [options]
  516. * @param {String} [options.contentType='text/plain'] content type
  517. * @param {bool} [options.overwrite=true] whether to overwrite an existing file
  518. *
  519. * @return {Promise}
  520. */
  521. putFileContents: function(path, body, options) {
  522. if (!path) {
  523. throw 'Missing argument "path"';
  524. }
  525. var self = this;
  526. var deferred = $.Deferred();
  527. var promise = deferred.promise();
  528. options = options || {};
  529. var headers = {};
  530. var contentType = 'text/plain;charset=utf-8';
  531. if (options.contentType) {
  532. contentType = options.contentType;
  533. }
  534. headers['Content-Type'] = contentType;
  535. if (_.isUndefined(options.overwrite) || options.overwrite) {
  536. // will trigger 412 precondition failed if a file already exists
  537. headers['If-None-Match'] = '*';
  538. }
  539. this._client.request(
  540. 'PUT',
  541. this._buildUrl(path),
  542. headers,
  543. body || ''
  544. ).then(
  545. function(result) {
  546. if (self._isSuccessStatus(result.status)) {
  547. deferred.resolve(result.status);
  548. } else {
  549. deferred.reject(result.status);
  550. }
  551. }
  552. );
  553. return promise;
  554. },
  555. _simpleCall: function(method, path) {
  556. if (!path) {
  557. throw 'Missing argument "path"';
  558. }
  559. var self = this;
  560. var deferred = $.Deferred();
  561. var promise = deferred.promise();
  562. this._client.request(
  563. method,
  564. this._buildUrl(path)
  565. ).then(
  566. function(result) {
  567. if (self._isSuccessStatus(result.status)) {
  568. deferred.resolve(result.status);
  569. } else {
  570. deferred.reject(result.status);
  571. }
  572. }
  573. );
  574. return promise;
  575. },
  576. /**
  577. * Creates a directory
  578. *
  579. * @param {String} path path to create
  580. *
  581. * @return {Promise}
  582. */
  583. createDirectory: function(path) {
  584. return this._simpleCall('MKCOL', path);
  585. },
  586. /**
  587. * Deletes a file or directory
  588. *
  589. * @param {String} path path to delete
  590. *
  591. * @return {Promise}
  592. */
  593. remove: function(path) {
  594. return this._simpleCall('DELETE', path);
  595. },
  596. /**
  597. * Moves path to another path
  598. *
  599. * @param {String} path path to move
  600. * @param {String} destinationPath destination path
  601. * @param {boolean} [allowOverwrite=false] true to allow overwriting,
  602. * false otherwise
  603. *
  604. * @return {Promise} promise
  605. */
  606. move: function(path, destinationPath, allowOverwrite) {
  607. if (!path) {
  608. throw 'Missing argument "path"';
  609. }
  610. if (!destinationPath) {
  611. throw 'Missing argument "destinationPath"';
  612. }
  613. var self = this;
  614. var deferred = $.Deferred();
  615. var promise = deferred.promise();
  616. var headers = {
  617. 'Destination' : this._buildUrl(destinationPath)
  618. };
  619. if (!allowOverwrite) {
  620. headers['Overwrite'] = 'F';
  621. }
  622. this._client.request(
  623. 'MOVE',
  624. this._buildUrl(path),
  625. headers
  626. ).then(
  627. function(response) {
  628. if (self._isSuccessStatus(response.status)) {
  629. deferred.resolve(response.status);
  630. } else {
  631. deferred.reject(response.status);
  632. }
  633. }
  634. );
  635. return promise;
  636. },
  637. /**
  638. * Add a file info parser function
  639. *
  640. * @param {OC.Files.Client~parseFileInfo>}
  641. */
  642. addFileInfoParser: function(parserFunction) {
  643. this._fileInfoParsers.push(parserFunction);
  644. }
  645. };
  646. /**
  647. * File info parser function
  648. *
  649. * This function receives a list of Webdav properties as input and
  650. * should return a hash array of parsed properties, if applicable.
  651. *
  652. * @callback OC.Files.Client~parseFileInfo
  653. * @param {Object} XML Webdav properties
  654. * @return {Array} array of parsed property values
  655. */
  656. if (!OC.Files) {
  657. /**
  658. * @namespace OC.Files
  659. *
  660. * @since 8.2
  661. */
  662. OC.Files = {};
  663. }
  664. /**
  665. * Returns the default instance of the files client
  666. *
  667. * @return {OC.Files.Client} default client
  668. *
  669. * @since 8.2
  670. */
  671. OC.Files.getClient = function() {
  672. if (OC.Files._defaultClient) {
  673. return OC.Files._defaultClient;
  674. }
  675. var client = new OC.Files.Client({
  676. host: OC.getHost(),
  677. port: OC.getPort(),
  678. root: OC.linkToRemoteBase('webdav'),
  679. useHTTPS: OC.getProtocol() === 'https'
  680. });
  681. OC.Files._defaultClient = client;
  682. return client;
  683. };
  684. OC.Files.Client = Client;
  685. })(OC, OC.Files.FileInfo);