server/core/js/files/client.js
Daniel Calviño Sánchez be56374c51 Fix sorting of favorite files
The sort comparator checks the "isFavorite" property of the FileInfo
objects to compare. That property is set when the file list is loaded
and the response from the server is parsed, and thus a freshly loaded
file list has the proper sorting for favorite files. However, the
property is not set in other cases, like when the FileInfo objects are
derived from FileInfoModels due to a file being marked as a favorite or
a text editor being closed, which causes the file to be sorted in the
wrong position.

There is no need to add the property in those situations, though; in all
cases the TagsPlugin adds a "tags" array property that contains an
OC.TAG_FAVORITE tag, so that tag can be checked instead of "isFavorite".
Moreover, although "isFavorite" was added by the main "_parseFileInfo"
function it did not really belong there but to the "FileInfoParser" from
the TagsPlugin; however, as that property now is not used anywhere it
was removed altogether.

A cleaner solution would have been to make the sort comparator
extensible by plugins like other behaviours of the file list and then
add the sorting logic related to favorite files to the TagsPlugin.
However, right now only the TagsPlugin would need to alter the main
sorting logic, and it seems like a corner case anyway. Even if it is
implemented as a plugin, favorite files is a core feature, so for the
time being it will be taken into account directly in the main sorting
logic; making the sort comparator extensible by plugins is defered until
there are other use cases for that.

Fixes #5410

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
2017-07-05 15:01:23 +02:00

833 lines
20 KiB
JavaScript

/*
* Copyright (c) 2015
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
/* global dav */
(function(OC, FileInfo) {
/**
* @class OC.Files.Client
* @classdesc Client to access files on the server
*
* @param {Object} options
* @param {String} options.host host name
* @param {int} [options.port] port
* @param {boolean} [options.useHTTPS] whether to use https
* @param {String} [options.root] root path
* @param {String} [options.userName] user name
* @param {String} [options.password] password
*
* @since 8.2
*/
var Client = function(options) {
this._root = options.root;
if (this._root.charAt(this._root.length - 1) === '/') {
this._root = this._root.substr(0, this._root.length - 1);
}
var url = Client.PROTOCOL_HTTP + '://';
if (options.useHTTPS) {
url = Client.PROTOCOL_HTTPS + '://';
}
url += options.host + this._root;
this._defaultHeaders = options.defaultHeaders || {
'X-Requested-With': 'XMLHttpRequest',
'requesttoken': OC.requestToken
};
this._baseUrl = url;
var clientOptions = {
baseUrl: this._baseUrl,
xmlNamespaces: {
'DAV:': 'd',
'http://owncloud.org/ns': 'oc',
'http://nextcloud.org/ns': 'nc'
}
};
if (options.userName) {
clientOptions.userName = options.userName;
}
if (options.password) {
clientOptions.password = options.password;
}
this._client = new dav.Client(clientOptions);
this._client.xhrProvider = _.bind(this._xhrProvider, this);
};
Client.NS_OWNCLOUD = 'http://owncloud.org/ns';
Client.NS_NEXTCLOUD = 'http://nextcloud.org/ns';
Client.NS_DAV = 'DAV:';
Client.PROPERTY_GETLASTMODIFIED = '{' + Client.NS_DAV + '}getlastmodified';
Client.PROPERTY_GETETAG = '{' + Client.NS_DAV + '}getetag';
Client.PROPERTY_GETCONTENTTYPE = '{' + Client.NS_DAV + '}getcontenttype';
Client.PROPERTY_RESOURCETYPE = '{' + Client.NS_DAV + '}resourcetype';
Client.PROPERTY_INTERNAL_FILEID = '{' + Client.NS_OWNCLOUD + '}fileid';
Client.PROPERTY_PERMISSIONS = '{' + Client.NS_OWNCLOUD + '}permissions';
Client.PROPERTY_SIZE = '{' + Client.NS_OWNCLOUD + '}size';
Client.PROPERTY_GETCONTENTLENGTH = '{' + Client.NS_DAV + '}getcontentlength';
Client.PROTOCOL_HTTP = 'http';
Client.PROTOCOL_HTTPS = 'https';
Client._PROPFIND_PROPERTIES = [
/**
* Modified time
*/
[Client.NS_DAV, 'getlastmodified'],
/**
* Etag
*/
[Client.NS_DAV, 'getetag'],
/**
* Mime type
*/
[Client.NS_DAV, 'getcontenttype'],
/**
* Resource type "collection" for folders, empty otherwise
*/
[Client.NS_DAV, 'resourcetype'],
/**
* File id
*/
[Client.NS_OWNCLOUD, 'fileid'],
/**
* Letter-coded permissions
*/
[Client.NS_OWNCLOUD, 'permissions'],
//[Client.NS_OWNCLOUD, 'downloadURL'],
/**
* Folder sizes
*/
[Client.NS_OWNCLOUD, 'size'],
/**
* File sizes
*/
[Client.NS_DAV, 'getcontentlength'],
/**
* Preview availability
*/
[Client.NS_NEXTCLOUD, 'has-preview'],
/**
* Mount type
*/
[Client.NS_NEXTCLOUD, 'mount-type'],
];
/**
* @memberof OC.Files
*/
Client.prototype = {
/**
* Root path of the Webdav endpoint
*
* @type string
*/
_root: null,
/**
* Client from the library
*
* @type dav.Client
*/
_client: null,
/**
* Array of file info parsing functions.
*
* @type Array<OC.Files.Client~parseFileInfo>
*/
_fileInfoParsers: [],
/**
* Returns the configured XHR provider for davclient
* @return {XMLHttpRequest}
*/
_xhrProvider: function() {
var headers = this._defaultHeaders;
var xhr = new XMLHttpRequest();
var oldOpen = xhr.open;
// override open() method to add headers
xhr.open = function() {
var result = oldOpen.apply(this, arguments);
_.each(headers, function(value, key) {
xhr.setRequestHeader(key, value);
});
return result;
};
OC.registerXHRForErrorProcessing(xhr);
return xhr;
},
/**
* Prepends the base url to the given path sections
*
* @param {...String} path sections
*
* @return {String} base url + joined path, any leading or trailing slash
* will be kept
*/
_buildUrl: function() {
var path = this._buildPath.apply(this, arguments);
if (path.charAt([path.length - 1]) === '/') {
path = path.substr(0, path.length - 1);
}
if (path.charAt(0) === '/') {
path = path.substr(1);
}
return this._baseUrl + '/' + path;
},
/**
* Append the path to the root and also encode path
* sections
*
* @param {...String} path sections
*
* @return {String} joined path, any leading or trailing slash
* will be kept
*/
_buildPath: function() {
var path = OC.joinPaths.apply(this, arguments);
var sections = path.split('/');
var i;
for (i = 0; i < sections.length; i++) {
sections[i] = encodeURIComponent(sections[i]);
}
path = sections.join('/');
return path;
},
/**
* Parse headers string into a map
*
* @param {string} headersString headers list as string
*
* @return {Object.<String,Array>} map of header name to header contents
*/
_parseHeaders: function(headersString) {
var headerRows = headersString.split('\n');
var headers = {};
for (var i = 0; i < headerRows.length; i++) {
var sepPos = headerRows[i].indexOf(':');
if (sepPos < 0) {
continue;
}
var headerName = headerRows[i].substr(0, sepPos);
var headerValue = headerRows[i].substr(sepPos + 2);
if (!headers[headerName]) {
// make it an array
headers[headerName] = [];
}
headers[headerName].push(headerValue);
}
return headers;
},
/**
* Parses the etag response which is in double quotes.
*
* @param {string} etag etag value in double quotes
*
* @return {string} etag without double quotes
*/
_parseEtag: function(etag) {
if (etag.charAt(0) === '"') {
return etag.split('"')[1];
}
return etag;
},
/**
* Parse Webdav result
*
* @param {Object} response XML object
*
* @return {Array.<FileInfo>} array of file info
*/
_parseFileInfo: function(response) {
var path = response.href;
if (path.substr(0, this._root.length) === this._root) {
path = path.substr(this._root.length);
}
if (path.charAt(path.length - 1) === '/') {
path = path.substr(0, path.length - 1);
}
path = decodeURIComponent(path);
if (response.propStat.length === 0 || response.propStat[0].status !== 'HTTP/1.1 200 OK') {
return null;
}
var props = response.propStat[0].properties;
var data = {
id: props[Client.PROPERTY_INTERNAL_FILEID],
path: OC.dirname(path) || '/',
name: OC.basename(path),
mtime: (new Date(props[Client.PROPERTY_GETLASTMODIFIED])).getTime()
};
var etagProp = props[Client.PROPERTY_GETETAG];
if (!_.isUndefined(etagProp)) {
data.etag = this._parseEtag(etagProp);
}
var sizeProp = props[Client.PROPERTY_GETCONTENTLENGTH];
if (!_.isUndefined(sizeProp)) {
data.size = parseInt(sizeProp, 10);
}
sizeProp = props[Client.PROPERTY_SIZE];
if (!_.isUndefined(sizeProp)) {
data.size = parseInt(sizeProp, 10);
}
var hasPreviewProp = props['{' + Client.NS_NEXTCLOUD + '}has-preview'];
if (!_.isUndefined(hasPreviewProp)) {
data.hasPreview = hasPreviewProp === 'true';
} else {
data.hasPreview = true;
}
var contentType = props[Client.PROPERTY_GETCONTENTTYPE];
if (!_.isUndefined(contentType)) {
data.mimetype = contentType;
}
var resType = props[Client.PROPERTY_RESOURCETYPE];
var isFile = true;
if (!data.mimetype && resType) {
var xmlvalue = resType[0];
if (xmlvalue.namespaceURI === Client.NS_DAV && xmlvalue.nodeName.split(':')[1] === 'collection') {
data.mimetype = 'httpd/unix-directory';
isFile = false;
}
}
data.permissions = OC.PERMISSION_READ;
var permissionProp = props[Client.PROPERTY_PERMISSIONS];
if (!_.isUndefined(permissionProp)) {
var permString = permissionProp || '';
data.mountType = null;
for (var i = 0; i < permString.length; i++) {
var c = permString.charAt(i);
switch (c) {
// FIXME: twisted permissions
case 'C':
case 'K':
data.permissions |= OC.PERMISSION_CREATE;
if (!isFile) {
data.permissions |= OC.PERMISSION_UPDATE;
}
break;
case 'W':
data.permissions |= OC.PERMISSION_UPDATE;
break;
case 'D':
data.permissions |= OC.PERMISSION_DELETE;
break;
case 'R':
data.permissions |= OC.PERMISSION_SHARE;
break;
case 'M':
if (!data.mountType) {
// TODO: how to identify external-root ?
data.mountType = 'external';
}
break;
case 'S':
// TODO: how to identify shared-root ?
data.mountType = 'shared';
break;
}
}
}
var mounTypeProp = props['{' + Client.NS_NEXTCLOUD + '}mount-type'];
if (!_.isUndefined(mounTypeProp)) {
data.mountType = mounTypeProp;
}
// extend the parsed data using the custom parsers
_.each(this._fileInfoParsers, function(parserFunction) {
_.extend(data, parserFunction(response) || {});
});
return new FileInfo(data);
},
/**
* Parse Webdav multistatus
*
* @param {Array} responses
*/
_parseResult: function(responses) {
var self = this;
return _.map(responses, function(response) {
return self._parseFileInfo(response);
});
},
/**
* Returns whether the given status code means success
*
* @param {int} status status code
*
* @return true if status code is between 200 and 299 included
*/
_isSuccessStatus: function(status) {
return status >= 200 && status <= 299;
},
/**
* Returns the default PROPFIND properties to use during a call.
*
* @return {Array.<Object>} array of properties
*/
getPropfindProperties: function() {
if (!this._propfindProperties) {
this._propfindProperties = _.map(Client._PROPFIND_PROPERTIES, function(propDef) {
return '{' + propDef[0] + '}' + propDef[1];
});
}
return this._propfindProperties;
},
/**
* Lists the contents of a directory
*
* @param {String} path path to retrieve
* @param {Object} [options] options
* @param {boolean} [options.includeParent=false] set to true to keep
* the parent folder in the result list
* @param {Array} [options.properties] list of Webdav properties to retrieve
*
* @return {Promise} promise
*/
getFolderContents: function(path, options) {
if (!path) {
path = '';
}
options = options || {};
var self = this;
var deferred = $.Deferred();
var promise = deferred.promise();
var properties;
if (_.isUndefined(options.properties)) {
properties = this.getPropfindProperties();
} else {
properties = options.properties;
}
this._client.propFind(
this._buildUrl(path),
properties,
1
).then(function(result) {
if (self._isSuccessStatus(result.status)) {
var results = self._parseResult(result.body);
if (!options || !options.includeParent) {
// remove root dir, the first entry
results.shift();
}
deferred.resolve(result.status, results);
} else {
deferred.reject(result.status);
}
});
return promise;
},
/**
* Fetches a flat list of files filtered by a given filter criteria.
* (currently only system tags is supported)
*
* @param {Object} filter filter criteria
* @param {Object} [filter.systemTagIds] list of system tag ids to filter by
* @param {bool} [filter.favorite] set it to filter by favorites
* @param {Object} [options] options
* @param {Array} [options.properties] list of Webdav properties to retrieve
*
* @return {Promise} promise
*/
getFilteredFiles: function(filter, options) {
options = options || {};
var self = this;
var deferred = $.Deferred();
var promise = deferred.promise();
var properties;
if (_.isUndefined(options.properties)) {
properties = this.getPropfindProperties();
} else {
properties = options.properties;
}
if (!filter || (!filter.systemTagIds && _.isUndefined(filter.favorite))) {
throw 'Missing filter argument';
}
// root element with namespaces
var body = '<oc:filter-files ';
var namespace;
for (namespace in this._client.xmlNamespaces) {
body += ' xmlns:' + this._client.xmlNamespaces[namespace] + '="' + namespace + '"';
}
body += '>\n';
// properties query
body += ' <' + this._client.xmlNamespaces['DAV:'] + ':prop>\n';
_.each(properties, function(prop) {
var property = self._client.parseClarkNotation(prop);
body += ' <' + self._client.xmlNamespaces[property.namespace] + ':' + property.name + ' />\n';
});
body += ' </' + this._client.xmlNamespaces['DAV:'] + ':prop>\n';
// rules block
body += ' <oc:filter-rules>\n';
_.each(filter.systemTagIds, function(systemTagIds) {
body += ' <oc:systemtag>' + escapeHTML(systemTagIds) + '</oc:systemtag>\n';
});
if (filter.favorite) {
body += ' <oc:favorite>' + (filter.favorite ? '1': '0') + '</oc:favorite>\n';
}
body += ' </oc:filter-rules>\n';
// end of root
body += '</oc:filter-files>\n';
this._client.request(
'REPORT',
this._buildUrl(),
{},
body
).then(function(result) {
if (self._isSuccessStatus(result.status)) {
var results = self._parseResult(result.body);
deferred.resolve(result.status, results);
} else {
deferred.reject(result.status);
}
});
return promise;
},
/**
* Returns the file info of a given path.
*
* @param {String} path path
* @param {Array} [options.properties] list of Webdav properties to retrieve
*
* @return {Promise} promise
*/
getFileInfo: function(path, options) {
if (!path) {
path = '';
}
options = options || {};
var self = this;
var deferred = $.Deferred();
var promise = deferred.promise();
var properties;
if (_.isUndefined(options.properties)) {
properties = this.getPropfindProperties();
} else {
properties = options.properties;
}
// TODO: headers
this._client.propFind(
this._buildUrl(path),
properties,
0
).then(
function(result) {
if (self._isSuccessStatus(result.status)) {
deferred.resolve(result.status, self._parseResult([result.body])[0]);
} else {
deferred.reject(result.status);
}
}
);
return promise;
},
/**
* Returns the contents of the given file.
*
* @param {String} path path to file
*
* @return {Promise}
*/
getFileContents: function(path) {
if (!path) {
throw 'Missing argument "path"';
}
var self = this;
var deferred = $.Deferred();
var promise = deferred.promise();
this._client.request(
'GET',
this._buildUrl(path)
).then(
function(result) {
if (self._isSuccessStatus(result.status)) {
deferred.resolve(result.status, result.body);
} else {
deferred.reject(result.status);
}
}
);
return promise;
},
/**
* Puts the given data into the given file.
*
* @param {String} path path to file
* @param {String} body file body
* @param {Object} [options]
* @param {String} [options.contentType='text/plain'] content type
* @param {bool} [options.overwrite=true] whether to overwrite an existing file
*
* @return {Promise}
*/
putFileContents: function(path, body, options) {
if (!path) {
throw 'Missing argument "path"';
}
var self = this;
var deferred = $.Deferred();
var promise = deferred.promise();
options = options || {};
var headers = {};
var contentType = 'text/plain;charset=utf-8';
if (options.contentType) {
contentType = options.contentType;
}
headers['Content-Type'] = contentType;
if (_.isUndefined(options.overwrite) || options.overwrite) {
// will trigger 412 precondition failed if a file already exists
headers['If-None-Match'] = '*';
}
this._client.request(
'PUT',
this._buildUrl(path),
headers,
body || ''
).then(
function(result) {
if (self._isSuccessStatus(result.status)) {
deferred.resolve(result.status);
} else {
deferred.reject(result.status);
}
}
);
return promise;
},
_simpleCall: function(method, path) {
if (!path) {
throw 'Missing argument "path"';
}
var self = this;
var deferred = $.Deferred();
var promise = deferred.promise();
this._client.request(
method,
this._buildUrl(path)
).then(
function(result) {
if (self._isSuccessStatus(result.status)) {
deferred.resolve(result.status);
} else {
deferred.reject(result.status);
}
}
);
return promise;
},
/**
* Creates a directory
*
* @param {String} path path to create
*
* @return {Promise}
*/
createDirectory: function(path) {
return this._simpleCall('MKCOL', path);
},
/**
* Deletes a file or directory
*
* @param {String} path path to delete
*
* @return {Promise}
*/
remove: function(path) {
return this._simpleCall('DELETE', path);
},
/**
* Moves path to another path
*
* @param {String} path path to move
* @param {String} destinationPath destination path
* @param {boolean} [allowOverwrite=false] true to allow overwriting,
* false otherwise
*
* @return {Promise} promise
*/
move: function(path, destinationPath, allowOverwrite) {
if (!path) {
throw 'Missing argument "path"';
}
if (!destinationPath) {
throw 'Missing argument "destinationPath"';
}
var self = this;
var deferred = $.Deferred();
var promise = deferred.promise();
var headers = {
'Destination' : this._buildUrl(destinationPath)
};
if (!allowOverwrite) {
headers.Overwrite = 'F';
}
this._client.request(
'MOVE',
this._buildUrl(path),
headers
).then(
function(response) {
if (self._isSuccessStatus(response.status)) {
deferred.resolve(response.status);
} else {
deferred.reject(response.status);
}
}
);
return promise;
},
/**
* Add a file info parser function
*
* @param {OC.Files.Client~parseFileInfo>}
*/
addFileInfoParser: function(parserFunction) {
this._fileInfoParsers.push(parserFunction);
},
/**
* Returns the dav.Client instance used internally
*
* @since 11.0.0
* @return {dav.Client}
*/
getClient: function() {
return this._client;
},
/**
* Returns the user name
*
* @since 11.0.0
* @return {String} userName
*/
getUserName: function() {
return this._client.userName;
},
/**
* Returns the password
*
* @since 11.0.0
* @return {String} password
*/
getPassword: function() {
return this._client.password;
},
/**
* Returns the base URL
*
* @since 11.0.0
* @return {String} base URL
*/
getBaseUrl: function() {
return this._client.baseUrl;
}
};
/**
* File info parser function
*
* This function receives a list of Webdav properties as input and
* should return a hash array of parsed properties, if applicable.
*
* @callback OC.Files.Client~parseFileInfo
* @param {Object} XML Webdav properties
* @return {Array} array of parsed property values
*/
if (!OC.Files) {
/**
* @namespace OC.Files
*
* @since 8.2
*/
OC.Files = {};
}
/**
* Returns the default instance of the files client
*
* @return {OC.Files.Client} default client
*
* @since 8.2
*/
OC.Files.getClient = function() {
if (OC.Files._defaultClient) {
return OC.Files._defaultClient;
}
var client = new OC.Files.Client({
host: OC.getHost(),
port: OC.getPort(),
root: OC.linkToRemoteBase('webdav'),
useHTTPS: OC.getProtocol() === 'https'
});
OC.Files._defaultClient = client;
return client;
};
OC.Files.Client = Client;
})(OC, OC.Files.FileInfo);