client.js 18 KB

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