From fb3d5c7856b9fbed926091e91ec54de023859df8 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Mon, 2 Nov 2015 13:19:30 +0100 Subject: [PATCH 01/19] Add evert's davclient.js + es6-promise + IE8 workaround - Add davclient.js lib - Add es6-promise required by that lib - Wrote IE8 workaround lib/shim for davclient.js --- bower.json | 4 +- core/js/core.json | 8 +- core/js/files/ie8davclient.js | 169 ++++ core/vendor/.gitignore | 11 + core/vendor/davclient.js/LICENSE | 27 + core/vendor/davclient.js/lib/client.js | 296 ++++++ core/vendor/es6-promise/.bower.json | 40 + core/vendor/es6-promise/.npmignore | 11 + core/vendor/es6-promise/.release.json | 17 + core/vendor/es6-promise/.spmignore | 11 + core/vendor/es6-promise/LICENSE | 19 + core/vendor/es6-promise/dist/es6-promise.js | 972 ++++++++++++++++++++ lib/private/template.php | 11 +- 13 files changed, 1591 insertions(+), 5 deletions(-) create mode 100644 core/js/files/ie8davclient.js create mode 100644 core/vendor/davclient.js/LICENSE create mode 100644 core/vendor/davclient.js/lib/client.js create mode 100644 core/vendor/es6-promise/.bower.json create mode 100644 core/vendor/es6-promise/.npmignore create mode 100644 core/vendor/es6-promise/.release.json create mode 100644 core/vendor/es6-promise/.spmignore create mode 100644 core/vendor/es6-promise/LICENSE create mode 100644 core/vendor/es6-promise/dist/es6-promise.js diff --git a/bower.json b/bower.json index e8bb9a35f321..77d7d75e85c5 100644 --- a/bower.json +++ b/bower.json @@ -27,6 +27,8 @@ "strengthify": "0.4.2", "underscore": "~1.8.0", "bootstrap": "~3.3.5", - "backbone": "~1.2.1" + "backbone": "~1.2.1", + "davclient.js": "https://github.com/evert/davclient.js.git", + "es6-promise": "https://github.com/jakearchibald/es6-promise.git#~2.3.0" } } diff --git a/core/js/core.json b/core/js/core.json index a80636e84636..c7621a08d627 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -8,7 +8,9 @@ "handlebars/handlebars.js", "blueimp-md5/js/md5.js", "bootstrap/js/tooltip.js", - "backbone/backbone.js" + "backbone/backbone.js", + "es6-promise/dist/es6-promise.js", + "davclient.js/lib/client.js" ], "libraries": [ "jquery-showpassword.js", @@ -39,6 +41,8 @@ "setupchecks.js", "../search/js/search.js", "mimetype.js", - "mimetypelist.js" + "mimetypelist.js", + "files/fileinfo.js", + "files/client.js" ] } diff --git a/core/js/files/ie8davclient.js b/core/js/files/ie8davclient.js new file mode 100644 index 000000000000..9887d34e5b4b --- /dev/null +++ b/core/js/files/ie8davclient.js @@ -0,0 +1,169 @@ +/* + * 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(dav) { + + /** + * Override davclient.js methods with IE8-compatible logic + */ + dav.Client.prototype = _.extend({}, dav.Client.prototype, { + + /** + * Generates a propFind request. + * + * @param {string} url Url to do the propfind request on + * @param {Array} properties List of properties to retrieve. + * @return {Promise} + */ + propFind : function(url, properties, depth) { + + if(typeof depth == "undefined") { + depth = 0; + } + + var headers = { + Depth : depth, + 'Content-Type' : 'application/xml; charset=utf-8' + }; + + var body = + '<?xml version="1.0"?>\n' + + '<d:propfind '; + + var namespace; + for (namespace in this.xmlNamespaces) { + body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"'; + } + body += '>\n' + + ' <d:prop>\n'; + + for(var ii in properties) { + var propText = properties[ii]; + if (typeof propText !== 'string') { + // can happen on IE8 + continue; + } + var property = this.parseClarkNotation(properties[ii]); + if (this.xmlNamespaces[property.namespace]) { + body+=' <' + this.xmlNamespaces[property.namespace] + ':' + property.name + ' />\n'; + } else { + body+=' <x:' + property.name + ' xmlns:x="' + property.namespace + '" />\n'; + } + + } + body+=' </d:prop>\n'; + body+='</d:propfind>'; + + return this.request('PROPFIND', url, headers, body).then( + function(result) { + var elements = this.parseMultiStatus(result.xhr.responseXML); + var response; + if (depth===0) { + response = { + status: result.status, + body: elements[0] + }; + } else { + response = { + status: result.status, + body: elements + }; + } + return response; + + }.bind(this) + ); + + }, + + + _getElementsByTagName: function(node, name, resolver) { + var parts = name.split(':'); + var tagName = parts[1]; + var namespace = resolver(parts[0]); + if (node.getElementsByTagNameNS) { + return node.getElementsByTagNameNS(namespace, tagName); + } + return node.getElementsByTagName(name); + }, + + /** + * Parses a multi-status response body. + * + * @param {string} xmlBody + * @param {Array} + */ + parseMultiStatus : function(doc) { + + var result = []; + var resolver = function(foo) { + var ii; + for(ii in this.xmlNamespaces) { + if (this.xmlNamespaces[ii] === foo) { + return ii; + } + } + }.bind(this); + + var responses = this._getElementsByTagName(doc, 'd:response', resolver); + var i; + for (i = 0; i < responses.length; i++) { + var responseNode = responses[i]; + var response = { + href : null, + propStat : [] + }; + + var hrefNode = this._getElementsByTagName(responseNode, 'd:href', resolver)[0]; + + response.href = hrefNode.textContent || hrefNode.text; + + var propStatNodes = this._getElementsByTagName(responseNode, 'd:propstat', resolver); + var j = 0; + + for (j = 0; j < propStatNodes.length; j++) { + var propStatNode = propStatNodes[j]; + var statusNode = this._getElementsByTagName(propStatNode, 'd:status', resolver)[0]; + + var propStat = { + status : statusNode.textContent || statusNode.text, + properties : [] + }; + + var propNode = this._getElementsByTagName(propStatNode, 'd:prop', resolver)[0]; + if (!propNode) { + continue; + } + var k = 0; + for (k = 0; k < propNode.childNodes.length; k++) { + var prop = propNode.childNodes[k]; + var value = prop.textContent || prop.text; + if (prop.childNodes && prop.childNodes.length > 0 && prop.childNodes[0].nodeType === 1) { + value = prop.childNodes; + } + propStat.properties['{' + prop.namespaceURI + '}' + (prop.localName || prop.baseName)] = value; + + } + response.propStat.push(propStat); + } + + result.push(response); + } + + return result; + + } + + + }); + +})(dav); + diff --git a/core/vendor/.gitignore b/core/vendor/.gitignore index bcbb59b6f241..09b6a47c72d5 100644 --- a/core/vendor/.gitignore +++ b/core/vendor/.gitignore @@ -122,3 +122,14 @@ bootstrap/js/* # backbone backbone/backbone-min* + +# davclient.js +davclient.js/** +!davclient.js/lib/* +!davclient.js/LICENSE + +# es6-promise +es6-promise/** +!es6-promise/LICENSE +!es6-promise/dist/es6-promise.js + diff --git a/core/vendor/davclient.js/LICENSE b/core/vendor/davclient.js/LICENSE new file mode 100644 index 000000000000..fd7293e8f324 --- /dev/null +++ b/core/vendor/davclient.js/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2013-2014 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/core/vendor/davclient.js/lib/client.js b/core/vendor/davclient.js/lib/client.js new file mode 100644 index 000000000000..93678f092486 --- /dev/null +++ b/core/vendor/davclient.js/lib/client.js @@ -0,0 +1,296 @@ +if (typeof dav == 'undefined') { dav = {}; }; + +dav.Client = function(options) { + var i; + for(i in options) { + this[i] = options[i]; + } + +}; + +dav.Client.prototype = { + + baseUrl : null, + + userName : null, + + password : null, + + + xmlNamespaces : { + 'DAV:' : 'd' + }, + + /** + * Generates a propFind request. + * + * @param {string} url Url to do the propfind request on + * @param {Array} properties List of properties to retrieve. + * @return {Promise} + */ + propFind : function(url, properties, depth) { + + if(typeof depth == "undefined") { + depth = 0; + } + + var headers = { + Depth : depth, + 'Content-Type' : 'application/xml; charset=utf-8' + }; + + var body = + '<?xml version="1.0"?>\n' + + '<d:propfind '; + var namespace; + for (namespace in this.xmlNamespaces) { + body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"'; + } + body += '>\n' + + ' <d:prop>\n'; + + for(var ii in properties) { + + var property = this.parseClarkNotation(properties[ii]); + if (this.xmlNamespaces[property.namespace]) { + body+=' <' + this.xmlNamespaces[property.namespace] + ':' + property.name + ' />\n'; + } else { + body+=' <x:' + property.name + ' xmlns:x="' + property.namespace + '" />\n'; + } + + } + body+=' </d:prop>\n'; + body+='</d:propfind>'; + + return this.request('PROPFIND', url, headers, body).then( + function(result) { + + var resultBody = this.parseMultiStatus(result.body); + if (depth===0) { + return { + status: result.status, + body: resultBody[0], + xhr: result.xhr + }; + } else { + return { + status: result.status, + body: resultBody, + xhr: result.xhr + }; + } + + }.bind(this) + ); + + }, + + /** + * Performs a HTTP request, and returns a Promise + * + * @param {string} method HTTP method + * @param {string} url Relative or absolute url + * @param {Object} headers HTTP headers as an object. + * @param {string} body HTTP request body. + * @return {Promise} + */ + request : function(method, url, headers, body) { + + var xhr = this.xhrProvider(); + + if (this.userName) { + headers['Authorization'] = 'Basic ' + btoa(this.userName + ':' + this.password); + // xhr.open(method, this.resolveUrl(url), true, this.userName, this.password); + } + xhr.open(method, this.resolveUrl(url), true); + var ii; + for(ii in headers) { + xhr.setRequestHeader(ii, headers[ii]); + } + xhr.send(body); + + return new Promise(function(fulfill, reject) { + + xhr.onreadystatechange = function() { + + if (xhr.readyState !== 4) { + return; + } + + fulfill({ + body: xhr.response, + status: xhr.status, + xhr: xhr + }); + + }; + + xhr.ontimeout = function() { + + reject(new Error('Timeout exceeded')); + + }; + + }); + + }, + + /** + * Returns an XMLHttpRequest object. + * + * This is in its own method, so it can be easily overridden. + * + * @return {XMLHttpRequest} + */ + xhrProvider : function() { + + return new XMLHttpRequest(); + + }, + + + /** + * Parses a multi-status response body. + * + * @param {string} xmlBody + * @param {Array} + */ + parseMultiStatus : function(xmlBody) { + + var parser = new DOMParser(); + var doc = parser.parseFromString(xmlBody, "application/xml"); + + var resolver = function(foo) { + var ii; + for(ii in this.xmlNamespaces) { + if (this.xmlNamespaces[ii] === foo) { + return ii; + } + } + }.bind(this); + + var responseIterator = doc.evaluate('/d:multistatus/d:response', doc, resolver); + + var result = []; + var responseNode = responseIterator.iterateNext(); + + while(responseNode) { + + var response = { + href : null, + propStat : [] + }; + + response.href = doc.evaluate('string(d:href)', responseNode, resolver).stringValue; + + var propStatIterator = doc.evaluate('d:propstat', responseNode, resolver); + var propStatNode = propStatIterator.iterateNext(); + + while(propStatNode) { + + var propStat = { + status : doc.evaluate('string(d:status)', propStatNode, resolver).stringValue, + properties : [], + }; + + var propIterator = doc.evaluate('d:prop/*', propStatNode, resolver); + + var propNode = propIterator.iterateNext(); + while(propNode) { + var content = propNode.textContent; + if (!content && propNode.hasChildNodes()) { + content = propNode.childNodes; + } + + propStat.properties['{' + propNode.namespaceURI + '}' + propNode.localName] = content; + propNode = propIterator.iterateNext(); + + } + response.propStat.push(propStat); + propStatNode = propStatIterator.iterateNext(); + + + } + + result.push(response); + responseNode = responseIterator.iterateNext(); + + } + + return result; + + }, + + /** + * Takes a relative url, and maps it to an absolute url, using the baseUrl + * + * @param {string} url + * @return {string} + */ + resolveUrl : function(url) { + + // Note: this is rudamentary.. not sure yet if it handles every case. + if (/^https?:\/\//i.test(url)) { + // absolute + return url; + } + + var baseParts = this.parseUrl(this.baseUrl); + if (url.charAt('/')) { + // Url starts with a slash + return baseParts.root + url; + } + + // Url does not start with a slash, we need grab the base url right up until the last slash. + var newUrl = baseParts.root + '/'; + if (baseParts.path.lastIndexOf('/')!==-1) { + newUrl = newUrl = baseParts.path.subString(0, baseParts.path.lastIndexOf('/')) + '/'; + } + newUrl+=url; + return url; + + }, + + /** + * Parses a url and returns its individual components. + * + * @param {String} url + * @return {Object} + */ + parseUrl : function(url) { + + var parts = url.match(/^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/); + var result = { + url : parts[0], + scheme : parts[1], + host : parts[3], + port : parts[4], + path : parts[5], + query : parts[6], + fragment : parts[7], + }; + result.root = + result.scheme + '://' + + result.host + + (result.port ? ':' + result.port : ''); + + return result; + + }, + + parseClarkNotation : function(propertyName) { + + var result = propertyName.match(/^{([^}]+)}(.*)$/); + if (!result) { + return; + } + + return { + name : result[2], + namespace : result[1] + }; + + } + +}; + diff --git a/core/vendor/es6-promise/.bower.json b/core/vendor/es6-promise/.bower.json new file mode 100644 index 000000000000..f8c28b04e532 --- /dev/null +++ b/core/vendor/es6-promise/.bower.json @@ -0,0 +1,40 @@ +{ + "name": "es6-promise", + "namespace": "Promise", + "version": "2.3.0", + "description": "A polyfill for ES6-style Promises, tracking rsvp", + "authors": [ + "Stefan Penner <stefan.penner@gmail.com>" + ], + "main": "dist/es6-promise.js", + "keywords": [ + "promise" + ], + "repository": { + "type": "git", + "url": "git://github.com/jakearchibald/ES6-Promises.git" + }, + "bugs": { + "url": "https://github.com/jakearchibald/ES6-Promises/issues" + }, + "license": "MIT", + "ignore": [ + "node_modules", + "bower_components", + "test", + "tests", + "vendor", + "tasks" + ], + "homepage": "https://github.com/jakearchibald/es6-promise", + "_release": "2.3.0", + "_resolution": { + "type": "version", + "tag": "2.3.0", + "commit": "fcbab11a1a981eb2290bfff89017cb764335a2a5" + }, + "_source": "https://github.com/jakearchibald/es6-promise.git", + "_target": "~2.3.0", + "_originalSource": "https://github.com/jakearchibald/es6-promise.git", + "_direct": true +} \ No newline at end of file diff --git a/core/vendor/es6-promise/.npmignore b/core/vendor/es6-promise/.npmignore new file mode 100644 index 000000000000..7a758111e9e2 --- /dev/null +++ b/core/vendor/es6-promise/.npmignore @@ -0,0 +1,11 @@ +/node_modules/ +/tmp +/tasks +/test +/vendor +/.jshintrc +/.npmignore +/.travis.yml +/Gruntfile.js +/component.json +/index.html diff --git a/core/vendor/es6-promise/.release.json b/core/vendor/es6-promise/.release.json new file mode 100644 index 000000000000..dee8cbc5d923 --- /dev/null +++ b/core/vendor/es6-promise/.release.json @@ -0,0 +1,17 @@ +{ + "non-interactive": true, + "dry-run": false, + "verbose": false, + "force": false, + "pkgFiles": ["package.json", "bower.json"], + "increment": "patch", + "commitMessage": "Release %s", + "tagName": "%s", + "tagAnnotation": "Release %s", + "buildCommand": "npm run-script build-all", + "distRepo": "git@github.com:components/rsvp.js.git", + "distStageDir": "tmp/stage", + "distBase": "dist", + "distFiles": ["**/*", "../package.json", "../bower.json"], + "publish": false +} diff --git a/core/vendor/es6-promise/.spmignore b/core/vendor/es6-promise/.spmignore new file mode 100644 index 000000000000..7a758111e9e2 --- /dev/null +++ b/core/vendor/es6-promise/.spmignore @@ -0,0 +1,11 @@ +/node_modules/ +/tmp +/tasks +/test +/vendor +/.jshintrc +/.npmignore +/.travis.yml +/Gruntfile.js +/component.json +/index.html diff --git a/core/vendor/es6-promise/LICENSE b/core/vendor/es6-promise/LICENSE new file mode 100644 index 000000000000..954ec5992df7 --- /dev/null +++ b/core/vendor/es6-promise/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/core/vendor/es6-promise/dist/es6-promise.js b/core/vendor/es6-promise/dist/es6-promise.js new file mode 100644 index 000000000000..aff0482ee5e6 --- /dev/null +++ b/core/vendor/es6-promise/dist/es6-promise.js @@ -0,0 +1,972 @@ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE + * @version 2.3.0 + */ + +(function() { + "use strict"; + function lib$es6$promise$utils$$objectOrFunction(x) { + return typeof x === 'function' || (typeof x === 'object' && x !== null); + } + + function lib$es6$promise$utils$$isFunction(x) { + return typeof x === 'function'; + } + + function lib$es6$promise$utils$$isMaybeThenable(x) { + return typeof x === 'object' && x !== null; + } + + var lib$es6$promise$utils$$_isArray; + if (!Array.isArray) { + lib$es6$promise$utils$$_isArray = function (x) { + return Object.prototype.toString.call(x) === '[object Array]'; + }; + } else { + lib$es6$promise$utils$$_isArray = Array.isArray; + } + + var lib$es6$promise$utils$$isArray = lib$es6$promise$utils$$_isArray; + var lib$es6$promise$asap$$len = 0; + var lib$es6$promise$asap$$toString = {}.toString; + var lib$es6$promise$asap$$vertxNext; + var lib$es6$promise$asap$$customSchedulerFn; + + var lib$es6$promise$asap$$asap = function asap(callback, arg) { + lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len] = callback; + lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len + 1] = arg; + lib$es6$promise$asap$$len += 2; + if (lib$es6$promise$asap$$len === 2) { + // If len is 2, that means that we need to schedule an async flush. + // If additional callbacks are queued before the queue is flushed, they + // will be processed by this flush that we are scheduling. + if (lib$es6$promise$asap$$customSchedulerFn) { + lib$es6$promise$asap$$customSchedulerFn(lib$es6$promise$asap$$flush); + } else { + lib$es6$promise$asap$$scheduleFlush(); + } + } + } + + function lib$es6$promise$asap$$setScheduler(scheduleFn) { + lib$es6$promise$asap$$customSchedulerFn = scheduleFn; + } + + function lib$es6$promise$asap$$setAsap(asapFn) { + lib$es6$promise$asap$$asap = asapFn; + } + + var lib$es6$promise$asap$$browserWindow = (typeof window !== 'undefined') ? window : undefined; + var lib$es6$promise$asap$$browserGlobal = lib$es6$promise$asap$$browserWindow || {}; + var lib$es6$promise$asap$$BrowserMutationObserver = lib$es6$promise$asap$$browserGlobal.MutationObserver || lib$es6$promise$asap$$browserGlobal.WebKitMutationObserver; + var lib$es6$promise$asap$$isNode = typeof process !== 'undefined' && {}.toString.call(process) === '[object process]'; + + // test for web worker but not in IE10 + var lib$es6$promise$asap$$isWorker = typeof Uint8ClampedArray !== 'undefined' && + typeof importScripts !== 'undefined' && + typeof MessageChannel !== 'undefined'; + + // node + function lib$es6$promise$asap$$useNextTick() { + var nextTick = process.nextTick; + // node version 0.10.x displays a deprecation warning when nextTick is used recursively + // setImmediate should be used instead instead + var version = process.versions.node.match(/^(?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+)$/); + if (Array.isArray(version) && version[1] === '0' && version[2] === '10') { + nextTick = setImmediate; + } + return function() { + nextTick(lib$es6$promise$asap$$flush); + }; + } + + // vertx + function lib$es6$promise$asap$$useVertxTimer() { + return function() { + lib$es6$promise$asap$$vertxNext(lib$es6$promise$asap$$flush); + }; + } + + function lib$es6$promise$asap$$useMutationObserver() { + var iterations = 0; + var observer = new lib$es6$promise$asap$$BrowserMutationObserver(lib$es6$promise$asap$$flush); + var node = document.createTextNode(''); + observer.observe(node, { characterData: true }); + + return function() { + node.data = (iterations = ++iterations % 2); + }; + } + + // web worker + function lib$es6$promise$asap$$useMessageChannel() { + var channel = new MessageChannel(); + channel.port1.onmessage = lib$es6$promise$asap$$flush; + return function () { + channel.port2.postMessage(0); + }; + } + + function lib$es6$promise$asap$$useSetTimeout() { + return function() { + setTimeout(lib$es6$promise$asap$$flush, 1); + }; + } + + var lib$es6$promise$asap$$queue = new Array(1000); + function lib$es6$promise$asap$$flush() { + for (var i = 0; i < lib$es6$promise$asap$$len; i+=2) { + var callback = lib$es6$promise$asap$$queue[i]; + var arg = lib$es6$promise$asap$$queue[i+1]; + + callback(arg); + + lib$es6$promise$asap$$queue[i] = undefined; + lib$es6$promise$asap$$queue[i+1] = undefined; + } + + lib$es6$promise$asap$$len = 0; + } + + function lib$es6$promise$asap$$attemptVertex() { + try { + var r = require; + var vertx = r('vertx'); + lib$es6$promise$asap$$vertxNext = vertx.runOnLoop || vertx.runOnContext; + return lib$es6$promise$asap$$useVertxTimer(); + } catch(e) { + return lib$es6$promise$asap$$useSetTimeout(); + } + } + + var lib$es6$promise$asap$$scheduleFlush; + // Decide what async method to use to triggering processing of queued callbacks: + if (lib$es6$promise$asap$$isNode) { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useNextTick(); + } else if (lib$es6$promise$asap$$BrowserMutationObserver) { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useMutationObserver(); + } else if (lib$es6$promise$asap$$isWorker) { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useMessageChannel(); + } else if (lib$es6$promise$asap$$browserWindow === undefined && typeof require === 'function') { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$attemptVertex(); + } else { + lib$es6$promise$asap$$scheduleFlush = lib$es6$promise$asap$$useSetTimeout(); + } + + function lib$es6$promise$$internal$$noop() {} + + var lib$es6$promise$$internal$$PENDING = void 0; + var lib$es6$promise$$internal$$FULFILLED = 1; + var lib$es6$promise$$internal$$REJECTED = 2; + + var lib$es6$promise$$internal$$GET_THEN_ERROR = new lib$es6$promise$$internal$$ErrorObject(); + + function lib$es6$promise$$internal$$selfFullfillment() { + return new TypeError("You cannot resolve a promise with itself"); + } + + function lib$es6$promise$$internal$$cannotReturnOwn() { + return new TypeError('A promises callback cannot return that same promise.'); + } + + function lib$es6$promise$$internal$$getThen(promise) { + try { + return promise.then; + } catch(error) { + lib$es6$promise$$internal$$GET_THEN_ERROR.error = error; + return lib$es6$promise$$internal$$GET_THEN_ERROR; + } + } + + function lib$es6$promise$$internal$$tryThen(then, value, fulfillmentHandler, rejectionHandler) { + try { + then.call(value, fulfillmentHandler, rejectionHandler); + } catch(e) { + return e; + } + } + + function lib$es6$promise$$internal$$handleForeignThenable(promise, thenable, then) { + lib$es6$promise$asap$$asap(function(promise) { + var sealed = false; + var error = lib$es6$promise$$internal$$tryThen(then, thenable, function(value) { + if (sealed) { return; } + sealed = true; + if (thenable !== value) { + lib$es6$promise$$internal$$resolve(promise, value); + } else { + lib$es6$promise$$internal$$fulfill(promise, value); + } + }, function(reason) { + if (sealed) { return; } + sealed = true; + + lib$es6$promise$$internal$$reject(promise, reason); + }, 'Settle: ' + (promise._label || ' unknown promise')); + + if (!sealed && error) { + sealed = true; + lib$es6$promise$$internal$$reject(promise, error); + } + }, promise); + } + + function lib$es6$promise$$internal$$handleOwnThenable(promise, thenable) { + if (thenable._state === lib$es6$promise$$internal$$FULFILLED) { + lib$es6$promise$$internal$$fulfill(promise, thenable._result); + } else if (thenable._state === lib$es6$promise$$internal$$REJECTED) { + lib$es6$promise$$internal$$reject(promise, thenable._result); + } else { + lib$es6$promise$$internal$$subscribe(thenable, undefined, function(value) { + lib$es6$promise$$internal$$resolve(promise, value); + }, function(reason) { + lib$es6$promise$$internal$$reject(promise, reason); + }); + } + } + + function lib$es6$promise$$internal$$handleMaybeThenable(promise, maybeThenable) { + if (maybeThenable.constructor === promise.constructor) { + lib$es6$promise$$internal$$handleOwnThenable(promise, maybeThenable); + } else { + var then = lib$es6$promise$$internal$$getThen(maybeThenable); + + if (then === lib$es6$promise$$internal$$GET_THEN_ERROR) { + lib$es6$promise$$internal$$reject(promise, lib$es6$promise$$internal$$GET_THEN_ERROR.error); + } else if (then === undefined) { + lib$es6$promise$$internal$$fulfill(promise, maybeThenable); + } else if (lib$es6$promise$utils$$isFunction(then)) { + lib$es6$promise$$internal$$handleForeignThenable(promise, maybeThenable, then); + } else { + lib$es6$promise$$internal$$fulfill(promise, maybeThenable); + } + } + } + + function lib$es6$promise$$internal$$resolve(promise, value) { + if (promise === value) { + lib$es6$promise$$internal$$reject(promise, lib$es6$promise$$internal$$selfFullfillment()); + } else if (lib$es6$promise$utils$$objectOrFunction(value)) { + lib$es6$promise$$internal$$handleMaybeThenable(promise, value); + } else { + lib$es6$promise$$internal$$fulfill(promise, value); + } + } + + function lib$es6$promise$$internal$$publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._result); + } + + lib$es6$promise$$internal$$publish(promise); + } + + function lib$es6$promise$$internal$$fulfill(promise, value) { + if (promise._state !== lib$es6$promise$$internal$$PENDING) { return; } + + promise._result = value; + promise._state = lib$es6$promise$$internal$$FULFILLED; + + if (promise._subscribers.length !== 0) { + lib$es6$promise$asap$$asap(lib$es6$promise$$internal$$publish, promise); + } + } + + function lib$es6$promise$$internal$$reject(promise, reason) { + if (promise._state !== lib$es6$promise$$internal$$PENDING) { return; } + promise._state = lib$es6$promise$$internal$$REJECTED; + promise._result = reason; + + lib$es6$promise$asap$$asap(lib$es6$promise$$internal$$publishRejection, promise); + } + + function lib$es6$promise$$internal$$subscribe(parent, child, onFulfillment, onRejection) { + var subscribers = parent._subscribers; + var length = subscribers.length; + + parent._onerror = null; + + subscribers[length] = child; + subscribers[length + lib$es6$promise$$internal$$FULFILLED] = onFulfillment; + subscribers[length + lib$es6$promise$$internal$$REJECTED] = onRejection; + + if (length === 0 && parent._state) { + lib$es6$promise$asap$$asap(lib$es6$promise$$internal$$publish, parent); + } + } + + function lib$es6$promise$$internal$$publish(promise) { + var subscribers = promise._subscribers; + var settled = promise._state; + + if (subscribers.length === 0) { return; } + + var child, callback, detail = promise._result; + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + if (child) { + lib$es6$promise$$internal$$invokeCallback(settled, child, callback, detail); + } else { + callback(detail); + } + } + + promise._subscribers.length = 0; + } + + function lib$es6$promise$$internal$$ErrorObject() { + this.error = null; + } + + var lib$es6$promise$$internal$$TRY_CATCH_ERROR = new lib$es6$promise$$internal$$ErrorObject(); + + function lib$es6$promise$$internal$$tryCatch(callback, detail) { + try { + return callback(detail); + } catch(e) { + lib$es6$promise$$internal$$TRY_CATCH_ERROR.error = e; + return lib$es6$promise$$internal$$TRY_CATCH_ERROR; + } + } + + function lib$es6$promise$$internal$$invokeCallback(settled, promise, callback, detail) { + var hasCallback = lib$es6$promise$utils$$isFunction(callback), + value, error, succeeded, failed; + + if (hasCallback) { + value = lib$es6$promise$$internal$$tryCatch(callback, detail); + + if (value === lib$es6$promise$$internal$$TRY_CATCH_ERROR) { + failed = true; + error = value.error; + value = null; + } else { + succeeded = true; + } + + if (promise === value) { + lib$es6$promise$$internal$$reject(promise, lib$es6$promise$$internal$$cannotReturnOwn()); + return; + } + + } else { + value = detail; + succeeded = true; + } + + if (promise._state !== lib$es6$promise$$internal$$PENDING) { + // noop + } else if (hasCallback && succeeded) { + lib$es6$promise$$internal$$resolve(promise, value); + } else if (failed) { + lib$es6$promise$$internal$$reject(promise, error); + } else if (settled === lib$es6$promise$$internal$$FULFILLED) { + lib$es6$promise$$internal$$fulfill(promise, value); + } else if (settled === lib$es6$promise$$internal$$REJECTED) { + lib$es6$promise$$internal$$reject(promise, value); + } + } + + function lib$es6$promise$$internal$$initializePromise(promise, resolver) { + try { + resolver(function resolvePromise(value){ + lib$es6$promise$$internal$$resolve(promise, value); + }, function rejectPromise(reason) { + lib$es6$promise$$internal$$reject(promise, reason); + }); + } catch(e) { + lib$es6$promise$$internal$$reject(promise, e); + } + } + + function lib$es6$promise$enumerator$$Enumerator(Constructor, input) { + var enumerator = this; + + enumerator._instanceConstructor = Constructor; + enumerator.promise = new Constructor(lib$es6$promise$$internal$$noop); + + if (enumerator._validateInput(input)) { + enumerator._input = input; + enumerator.length = input.length; + enumerator._remaining = input.length; + + enumerator._init(); + + if (enumerator.length === 0) { + lib$es6$promise$$internal$$fulfill(enumerator.promise, enumerator._result); + } else { + enumerator.length = enumerator.length || 0; + enumerator._enumerate(); + if (enumerator._remaining === 0) { + lib$es6$promise$$internal$$fulfill(enumerator.promise, enumerator._result); + } + } + } else { + lib$es6$promise$$internal$$reject(enumerator.promise, enumerator._validationError()); + } + } + + lib$es6$promise$enumerator$$Enumerator.prototype._validateInput = function(input) { + return lib$es6$promise$utils$$isArray(input); + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._validationError = function() { + return new Error('Array Methods must be provided an Array'); + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._init = function() { + this._result = new Array(this.length); + }; + + var lib$es6$promise$enumerator$$default = lib$es6$promise$enumerator$$Enumerator; + + lib$es6$promise$enumerator$$Enumerator.prototype._enumerate = function() { + var enumerator = this; + + var length = enumerator.length; + var promise = enumerator.promise; + var input = enumerator._input; + + for (var i = 0; promise._state === lib$es6$promise$$internal$$PENDING && i < length; i++) { + enumerator._eachEntry(input[i], i); + } + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._eachEntry = function(entry, i) { + var enumerator = this; + var c = enumerator._instanceConstructor; + + if (lib$es6$promise$utils$$isMaybeThenable(entry)) { + if (entry.constructor === c && entry._state !== lib$es6$promise$$internal$$PENDING) { + entry._onerror = null; + enumerator._settledAt(entry._state, i, entry._result); + } else { + enumerator._willSettleAt(c.resolve(entry), i); + } + } else { + enumerator._remaining--; + enumerator._result[i] = entry; + } + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._settledAt = function(state, i, value) { + var enumerator = this; + var promise = enumerator.promise; + + if (promise._state === lib$es6$promise$$internal$$PENDING) { + enumerator._remaining--; + + if (state === lib$es6$promise$$internal$$REJECTED) { + lib$es6$promise$$internal$$reject(promise, value); + } else { + enumerator._result[i] = value; + } + } + + if (enumerator._remaining === 0) { + lib$es6$promise$$internal$$fulfill(promise, enumerator._result); + } + }; + + lib$es6$promise$enumerator$$Enumerator.prototype._willSettleAt = function(promise, i) { + var enumerator = this; + + lib$es6$promise$$internal$$subscribe(promise, undefined, function(value) { + enumerator._settledAt(lib$es6$promise$$internal$$FULFILLED, i, value); + }, function(reason) { + enumerator._settledAt(lib$es6$promise$$internal$$REJECTED, i, reason); + }); + }; + function lib$es6$promise$promise$all$$all(entries) { + return new lib$es6$promise$enumerator$$default(this, entries).promise; + } + var lib$es6$promise$promise$all$$default = lib$es6$promise$promise$all$$all; + function lib$es6$promise$promise$race$$race(entries) { + /*jshint validthis:true */ + var Constructor = this; + + var promise = new Constructor(lib$es6$promise$$internal$$noop); + + if (!lib$es6$promise$utils$$isArray(entries)) { + lib$es6$promise$$internal$$reject(promise, new TypeError('You must pass an array to race.')); + return promise; + } + + var length = entries.length; + + function onFulfillment(value) { + lib$es6$promise$$internal$$resolve(promise, value); + } + + function onRejection(reason) { + lib$es6$promise$$internal$$reject(promise, reason); + } + + for (var i = 0; promise._state === lib$es6$promise$$internal$$PENDING && i < length; i++) { + lib$es6$promise$$internal$$subscribe(Constructor.resolve(entries[i]), undefined, onFulfillment, onRejection); + } + + return promise; + } + var lib$es6$promise$promise$race$$default = lib$es6$promise$promise$race$$race; + function lib$es6$promise$promise$resolve$$resolve(object) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + var promise = new Constructor(lib$es6$promise$$internal$$noop); + lib$es6$promise$$internal$$resolve(promise, object); + return promise; + } + var lib$es6$promise$promise$resolve$$default = lib$es6$promise$promise$resolve$$resolve; + function lib$es6$promise$promise$reject$$reject(reason) { + /*jshint validthis:true */ + var Constructor = this; + var promise = new Constructor(lib$es6$promise$$internal$$noop); + lib$es6$promise$$internal$$reject(promise, reason); + return promise; + } + var lib$es6$promise$promise$reject$$default = lib$es6$promise$promise$reject$$reject; + + var lib$es6$promise$promise$$counter = 0; + + function lib$es6$promise$promise$$needsResolver() { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); + } + + function lib$es6$promise$promise$$needsNew() { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } + + var lib$es6$promise$promise$$default = lib$es6$promise$promise$$Promise; + /** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise's eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + var promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']')); + } + } + }; + }); + } + + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class Promise + @param {function} resolver + Useful for tooling. + @constructor + */ + function lib$es6$promise$promise$$Promise(resolver) { + this._id = lib$es6$promise$promise$$counter++; + this._state = undefined; + this._result = undefined; + this._subscribers = []; + + if (lib$es6$promise$$internal$$noop !== resolver) { + if (!lib$es6$promise$utils$$isFunction(resolver)) { + lib$es6$promise$promise$$needsResolver(); + } + + if (!(this instanceof lib$es6$promise$promise$$Promise)) { + lib$es6$promise$promise$$needsNew(); + } + + lib$es6$promise$$internal$$initializePromise(this, resolver); + } + } + + lib$es6$promise$promise$$Promise.all = lib$es6$promise$promise$all$$default; + lib$es6$promise$promise$$Promise.race = lib$es6$promise$promise$race$$default; + lib$es6$promise$promise$$Promise.resolve = lib$es6$promise$promise$resolve$$default; + lib$es6$promise$promise$$Promise.reject = lib$es6$promise$promise$reject$$default; + lib$es6$promise$promise$$Promise._setScheduler = lib$es6$promise$asap$$setScheduler; + lib$es6$promise$promise$$Promise._setAsap = lib$es6$promise$asap$$setAsap; + lib$es6$promise$promise$$Promise._asap = lib$es6$promise$asap$$asap; + + lib$es6$promise$promise$$Promise.prototype = { + constructor: lib$es6$promise$promise$$Promise, + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + + Chaining + -------- + + The return value of `then` is itself a promise. This second, 'downstream' + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return 'default name'; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `'default name'` + }); + + findUser().then(function (user) { + throw new Error('Found user, but still unhappy'); + }, function (reason) { + throw new Error('`findUser` rejected and we're unhappy'); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. + // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException('Upstream error'); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + + Assimilation + ------------ + + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + + If the assimliated promise rejects, then the downstream promise will also reject. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + + Simple Example + -------------- + + Synchronous Example + + ```javascript + var result; + + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + + Promise Example; + + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + + Advanced Example + -------------- + + Synchronous Example + + ```javascript + var author, books; + + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + + function foundBooks(books) { + + } + + function failure(reason) { + + } + + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + + Promise Example; + + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + + @method then + @param {Function} onFulfilled + @param {Function} onRejected + Useful for tooling. + @return {Promise} + */ + then: function(onFulfillment, onRejection) { + var parent = this; + var state = parent._state; + + if (state === lib$es6$promise$$internal$$FULFILLED && !onFulfillment || state === lib$es6$promise$$internal$$REJECTED && !onRejection) { + return this; + } + + var child = new this.constructor(lib$es6$promise$$internal$$noop); + var result = parent._result; + + if (state) { + var callback = arguments[state - 1]; + lib$es6$promise$asap$$asap(function(){ + lib$es6$promise$$internal$$invokeCallback(state, child, callback, result); + }); + } else { + lib$es6$promise$$internal$$subscribe(parent, child, onFulfillment, onRejection); + } + + return child; + }, + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + + ```js + function findAuthor(){ + throw new Error('couldn't find that author'); + } + + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + + @method catch + @param {Function} onRejection + Useful for tooling. + @return {Promise} + */ + 'catch': function(onRejection) { + return this.then(null, onRejection); + } + }; + function lib$es6$promise$polyfill$$polyfill() { + var local; + + if (typeof global !== 'undefined') { + local = global; + } else if (typeof self !== 'undefined') { + local = self; + } else { + try { + local = Function('return this')(); + } catch (e) { + throw new Error('polyfill failed because global object is unavailable in this environment'); + } + } + + var P = local.Promise; + + if (P && Object.prototype.toString.call(P.resolve()) === '[object Promise]' && !P.cast) { + return; + } + + local.Promise = lib$es6$promise$promise$$default; + } + var lib$es6$promise$polyfill$$default = lib$es6$promise$polyfill$$polyfill; + + var lib$es6$promise$umd$$ES6Promise = { + 'Promise': lib$es6$promise$promise$$default, + 'polyfill': lib$es6$promise$polyfill$$default + }; + + /* global define:true module:true window: true */ + if (typeof define === 'function' && define['amd']) { + define(function() { return lib$es6$promise$umd$$ES6Promise; }); + } else if (typeof module !== 'undefined' && module['exports']) { + module['exports'] = lib$es6$promise$umd$$ES6Promise; + } else if (typeof this !== 'undefined') { + this['ES6Promise'] = lib$es6$promise$umd$$ES6Promise; + } + + lib$es6$promise$polyfill$$default(); +}).call(this); + diff --git a/lib/private/template.php b/lib/private/template.php index 97666f0b8dc7..2c9721dc9640 100644 --- a/lib/private/template.php +++ b/lib/private/template.php @@ -143,7 +143,9 @@ public static function initTemplateEngine($renderAs) { OC_Util::addStyle("jquery.ocdialog"); OC_Util::addScript("compatibility", null, true); OC_Util::addScript("placeholders", null, true); - + OC_Util::addScript('files/fileinfo'); + OC_Util::addScript('files/client'); + // Add the stuff we need always // following logic will import all vendor libraries that are // specified in core/js/core.json @@ -158,7 +160,12 @@ public static function initTemplateEngine($renderAs) { } else { throw new \Exception('Cannot read core/js/core.json'); } - + + if (\OC::$server->getRequest()->isUserAgent([\OC\AppFramework\Http\Request::USER_AGENT_IE_8])) { + // shim for the davclient.js library + \OCP\Util::addScript('files/ie8davclient'); + } + self::$initTemplateEngineFirstRun = false; } From f120846e291bf83244831770c5f25b730fa8ba90 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Mon, 13 Jul 2015 17:31:47 +0200 Subject: [PATCH 02/19] Added OC.Files.Client Webdav-based files client --- buildjsdocs.sh | 2 +- core/js/files/client.js | 673 ++++++++++++++++++++++ core/js/files/fileinfo.js | 143 +++++ core/js/js.js | 1 - core/js/tests/specHelper.js | 1 + core/js/tests/specs/files/clientSpec.js | 712 ++++++++++++++++++++++++ tests/karma.config.js | 6 +- 7 files changed, 1533 insertions(+), 5 deletions(-) create mode 100644 core/js/files/client.js create mode 100644 core/js/files/fileinfo.js create mode 100644 core/js/tests/specs/files/clientSpec.js diff --git a/buildjsdocs.sh b/buildjsdocs.sh index 90562558f66a..57eefb29fd04 100755 --- a/buildjsdocs.sh +++ b/buildjsdocs.sh @@ -11,7 +11,7 @@ NPM="$(which npm 2>/dev/null)" PREFIX="build" OUTPUT_DIR="build/jsdocs" -JS_FILES="core/js/*.js apps/*/js/*.js" +JS_FILES="core/js/*.js core/js/**/*.js apps/*/js/*.js" if test -z "$NPM" then diff --git a/core/js/files/client.js b/core/js/files/client.js new file mode 100644 index 000000000000..9bb7bb999fd3 --- /dev/null +++ b/core/js/files/client.js @@ -0,0 +1,673 @@ +/* + * 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); + } + + if (!options.port) { + // workaround in case port is null or empty + options.port = undefined; + } + var url = ''; + var port = ''; + if (options.useHTTPS) { + url += 'https://'; + if (options.port && options.port !== 443) { + port = ':' + options.port; + } + } else { + url += 'http://'; + if (options.port && options.port !== 80) { + port = ':' + options.port; + } + } + var credentials = ''; + if (options.userName) { + credentials += encodeURIComponent(options.userName); + } + if (options.password) { + credentials += ':' + encodeURIComponent(options.password); + } + if (credentials.length > 0) { + url += credentials + '@'; + } + + url += options.host + port + this._root; + this._defaultHeaders = options.defaultHeaders || {'X-Requested-With': 'XMLHttpRequest'}; + this._baseUrl = url; + this._client = new dav.Client({ + baseUrl: this._baseUrl, + xmlNamespaces: { + 'DAV:': 'd', + 'http://owncloud.org/ns': 'oc' + } + }); + this._client.xhrProvider = _.bind(this._xhrProvider, this); + }; + + Client.NS_OWNCLOUD = 'http://owncloud.org/ns'; + Client.NS_DAV = 'DAV:'; + 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'], + /** + * Compound file id, contains fileid + server instance id + */ + [Client.NS_OWNCLOUD, 'id'], + /** + * Letter-coded permissions + */ + [Client.NS_OWNCLOUD, 'permissions'], + //[Client.NS_OWNCLOUD, 'downloadURL'], + /** + * Folder sizes + */ + [Client.NS_OWNCLOUD, 'size'], + /** + * File sizes + */ + [Client.NS_DAV, 'getcontentlength'] + ]; + + /** + * @memberof OC.Files + */ + Client.prototype = { + + /** + * Root path of the Webdav endpoint + * + * @type string + */ + _root: null, + + /** + * Client from the library + * + * @type nl.sara.webdav.Client + */ + _client: null, + + /** + * 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; + }; + 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 compound file id + * + * @param {string} compoundFileId compound file id as returned by the server + * + * @return {int} local file id, stripped of the instance id + */ + _parseFileId: function(compoundFileId) { + if (!compoundFileId || compoundFileId.length < 8) { + return null; + } + return parseInt(compoundFileId.substr(0, 8), 10); + }, + + /** + * 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 === 1 && response.propStat[0].status !== 200) { + return null; + } + + var props = response.propStat[0].properties; + + var data = { + id: this._parseFileId(props['{' + Client.NS_OWNCLOUD + '}id']), + path: OC.dirname(path) || '/', + name: OC.basename(path), + mtime: new Date(props['{' + Client.NS_DAV + '}getlastmodified']), + _props: props + }; + + var etagProp = props['{' + Client.NS_DAV + '}getetag']; + if (!_.isUndefined(etagProp)) { + data.etag = this._parseEtag(etagProp); + } + + var sizeProp = props['{' + Client.NS_DAV + '}getcontentlength']; + if (!_.isUndefined(sizeProp)) { + data.size = parseInt(sizeProp, 10); + } + + sizeProp = props['{' + Client.NS_OWNCLOUD + '}size']; + if (!_.isUndefined(sizeProp)) { + data.size = parseInt(sizeProp, 10); + } + + var contentType = props['{' + Client.NS_DAV + '}getcontenttype']; + if (!_.isUndefined(contentType)) { + data.mimetype = contentType; + } + + var resType = props['{' + Client.NS_DAV + '}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.NS_OWNCLOUD + '}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': + if (isFile) { + // also add create permissions + data.permissions |= OC.PERMISSION_CREATE; + } + 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; + } + } + } + + 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 + * + * @return {Promise} promise + */ + getFolderContents: function(path, options) { + if (!path) { + path = ''; + } + var self = this; + var deferred = $.Deferred(); + var promise = deferred.promise(); + + // TODO: headers + this._client.propFind( + this._buildUrl(path), + this._getPropfindProperties(), + 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; + }, + + /** + * Returns the file info of a given path. + * + * @param {String} path path + * @param {Array} [properties] list of webdav properties to + * retrieve + * + * @return {Promise} promise + */ + getFileInfo: function(path) { + if (!path) { + path = ''; + } + var self = this; + var deferred = $.Deferred(); + var promise = deferred.promise(); + + // TODO: headers + this._client.propFind( + this._buildUrl(path), + this._getPropfindProperties(), + 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), + this._defaultHeaders + ).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 = _.extend({}, this._defaultHeaders); + var contentType = 'text/plain'; + 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), + this._defaultHeaders + ).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 = + _.extend({ + 'Destination' : this._buildUrl(destinationPath) + }, this._defaultHeaders); + + 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; + } + + }; + + 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); + diff --git a/core/js/files/fileinfo.js b/core/js/files/fileinfo.js new file mode 100644 index 000000000000..95af07b79924 --- /dev/null +++ b/core/js/files/fileinfo.js @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC) { + + /** + * @class OC.Files.FileInfo + * @classdesc File information + * + * @param {Object} data file data, see attributes for details + * + * @since 8.2 + */ + var FileInfo = function(data) { + if (!_.isUndefined(data.id)) { + this.id = parseInt(data.id, 10); + } + + // TODO: normalize path + this.path = data.path || ''; + this.name = data.name; + + this.mtime = data.mtime; + this.etag = data.etag; + this.permissions = data.permissions; + this.size = data.size; + this.mimetype = data.mimetype || 'application/octet-stream'; + this.mountType = data.mountType; + this.icon = data.icon; + this._props = data._props; + + if (data.type) { + this.type = data.type; + } else if (this.mimetype === 'httpd/unix-directory') { + this.type = 'dir'; + } else { + this.type = 'file'; + } + + if (!this.mimetype) { + if (this.type === 'dir') { + this.mimetype = 'httpd/unix-directory'; + } else { + this.mimetype = 'application/octet-stream'; + } + } + }; + + /** + * @memberof OC.Files + */ + FileInfo.prototype = { + /** + * File id + * + * @type int + */ + id: null, + + /** + * File name + * + * @type String + */ + name: null, + + /** + * Path leading to the file, without the file name, + * and with a leading slash. + * + * @type String + */ + path: null, + + /** + * Mime type + * + * @type String + */ + mimetype: null, + + /** + * Icon URL. + * + * Can be used to override the mime type icon. + * + * @type String + */ + icon: null, + + /** + * File type. 'file' for files, 'dir' for directories. + * + * @type String + * @deprecated rely on mimetype instead + */ + type: 'file', + + /** + * Permissions. + * + * @see OC#PERMISSION_ALL for permissions + * @type int + */ + permissions: null, + + /** + * Modification time + * + * @type int + */ + mtime: null, + + /** + * Etag + * + * @type String + */ + etag: null, + + /** + * Mount type. + * + * One of null, "external-root", "shared" or "shared-root" + * + * @type string + */ + mountType: null + }; + + if (!OC.Files) { + OC.Files = {}; + } + OC.Files.FileInfo = FileInfo; +})(OC); + diff --git a/core/js/js.js b/core/js/js.js index 57c9871233b4..ce552bb8ea2c 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1428,7 +1428,6 @@ function initCore() { $('body').delegate('#app-content', 'apprendered appresized', adjustControlsWidth); } - } $(document).ready(initCore); diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js index cd387d76ce8f..f09a7054c9f9 100644 --- a/core/js/tests/specHelper.js +++ b/core/js/tests/specHelper.js @@ -86,6 +86,7 @@ window.firstDay = 0; // setup dummy webroots /* jshint camelcase: false */ window.oc_debug = true; +// FIXME: oc_webroot is supposed to be only the path!!! window.oc_webroot = location.href + '/'; window.oc_appswebroots = { "files": window.oc_webroot + '/apps/files/' diff --git a/core/js/tests/specs/files/clientSpec.js b/core/js/tests/specs/files/clientSpec.js new file mode 100644 index 000000000000..67815d93f056 --- /dev/null +++ b/core/js/tests/specs/files/clientSpec.js @@ -0,0 +1,712 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2015 Vincent Petry <pvince81@owncloud.com> +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see <http://www.gnu.org/licenses/>. +* +*/ + +describe('OC.Files.Client tests', function() { + var Client = OC.Files.Client; + var baseUrl; + var client; + + beforeEach(function() { + baseUrl = 'https://testhost:999/owncloud/remote.php/webdav/'; + + client = new Client({ + host: 'testhost', + port: 999, + root: '/owncloud/remote.php/webdav', + useHTTPS: true + }); + }); + afterEach(function() { + client = null; + }); + + /** + * Send an status response and check that the given + * promise gets its success handler called with the error + * status code + * + * @param {Promise} promise promise + * @param {int} status status to test + */ + function respondAndCheckStatus(promise, status) { + var successHandler = sinon.stub(); + var failHandler = sinon.stub(); + promise.done(successHandler); + promise.fail(failHandler); + + fakeServer.requests[0].respond( + status, + {'Content-Type': 'application/xml'}, + '' + ); + + promise.then(function() { + expect(successHandler.calledOnce).toEqual(true); + expect(successHandler.getCall(0).args[0]).toEqual(status); + + expect(failHandler.notCalled).toEqual(true); + }); + + return promise; + } + + /** + * Send an error response and check that the given + * promise gets its fail handler called with the error + * status code + * + * @param {Promise} promise promise object + * @param {int} status error status to test + */ + function respondAndCheckError(promise, status) { + var successHandler = sinon.stub(); + var failHandler = sinon.stub(); + promise.done(successHandler); + promise.fail(failHandler); + + fakeServer.requests[0].respond( + status, + {'Content-Type': 'application/xml'}, + '' + ); + + promise.then(function() { + expect(failHandler.calledOnce).toEqual(true); + expect(failHandler.calledWith(status)).toEqual(true); + + expect(successHandler.notCalled).toEqual(true); + + fulfill(); + }); + + return promise; + } + + /** + * Returns a list of request properties parsed from the given request body. + * + * @param {string} requestBody request XML + * + * @return {Array.<String>} array of request properties in the format + * "{NS:}propname" + */ + function getRequestedProperties(requestBody) { + var doc = (new window.DOMParser()).parseFromString( + requestBody, + 'application/xml' + ); + var propRoots = doc.getElementsByTagNameNS('DAV:', 'prop'); + var propsList = propRoots.item(0).childNodes; + return _.map(propsList, function(propNode) { + return '{' + propNode.namespaceURI + '}' + propNode.localName; + }); + } + + function makePropBlock(props) { + var s = '<d:prop>\n'; + + _.each(props, function(value, key) { + s += '<' + key + '>' + value + '</' + key + '>\n'; + }); + + return s + '</d:prop>\n'; + } + + function makeResponseBlock(href, props, failedProps) { + var s = '<d:response>\n'; + s += '<d:href>' + href + '</d:href>\n'; + s += '<d:propstat>\n'; + s += makePropBlock(props); + s += '<d:status>HTTP/1.1 200 OK</d:status>'; + s += '</d:propstat>\n'; + if (failedProps) { + s += '<d:propstat>\n'; + _.each(failedProps, function(prop) { + s += '<' + prop + '/>\n'; + }); + s += '<d:status>HTTP/1.1 404 Not Found</d:status>\n'; + s += '</d:propstat>\n'; + } + return s + '</d:response>\n'; + } + + describe('file listing', function() { + + var folderContentsXml = + '<?xml version="1.0" encoding="utf-8"?>' + + '<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">' + + makeResponseBlock( + '/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/', + { + 'd:getlastmodified': 'Fri, 10 Jul 2015 10:00:05 GMT', + 'd:getetag': '"56cfcabd79abb"', + 'd:resourcetype': '<d:collection/>', + 'oc:id': '00000011oc2d13a6a068', + 'oc:permissions': 'RDNVCK', + 'oc:size': 120 + }, + [ + 'd:getcontenttype', + 'd:getcontentlength' + ] + ) + + makeResponseBlock( + '/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/One.txt', + { + 'd:getlastmodified': 'Fri, 10 Jul 2015 13:38:05 GMT', + 'd:getetag': '"559fcabd79a38"', + 'd:getcontenttype': 'text/plain', + 'd:getcontentlength': 250, + 'd:resourcetype': '', + 'oc:id': '00000051oc2d13a6a068', + 'oc:permissions': 'RDNVW' + }, + [ + 'oc:size', + ] + ) + + makeResponseBlock( + '/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/sub', + { + 'd:getlastmodified': 'Fri, 10 Jul 2015 14:00:00 GMT', + 'd:getetag': '"66cfcabd79abb"', + 'd:resourcetype': '<d:collection/>', + 'oc:id': '00000015oc2d13a6a068', + 'oc:permissions': 'RDNVCK', + 'oc:size': 100 + }, + [ + 'd:getcontenttype', + 'd:getcontentlength' + ] + ) + + '</d:multistatus>'; + + it('sends PROPFIND with explicit properties to get file list', function() { + client.getFolderContents('path/to space/文件夹'); + + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('PROPFIND'); + expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9'); + expect(fakeServer.requests[0].requestHeaders.Depth).toEqual(1); + + var props = getRequestedProperties(fakeServer.requests[0].requestBody); + expect(props).toContain('{DAV:}getlastmodified'); + expect(props).toContain('{DAV:}getcontentlength'); + expect(props).toContain('{DAV:}getcontenttype'); + expect(props).toContain('{DAV:}getetag'); + expect(props).toContain('{DAV:}resourcetype'); + expect(props).toContain('{http://owncloud.org/ns}id'); + expect(props).toContain('{http://owncloud.org/ns}size'); + expect(props).toContain('{http://owncloud.org/ns}permissions'); + }); + it('sends PROPFIND to base url when empty path given', function() { + client.getFolderContents(''); + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].url).toEqual(baseUrl); + }); + it('sends PROPFIND to base url when root path given', function() { + client.getFolderContents('/'); + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].url).toEqual(baseUrl); + }); + it('parses the result list into a FileInfo array', function() { + var promise = client.getFolderContents('path/to space/文件夹'); + + expect(fakeServer.requests.length).toEqual(1); + + fakeServer.requests[0].respond( + 207, + {'Content-Type': 'application/xml'}, + folderContentsXml + ); + + promise.then(function(status, response) { + expect(status).toEqual(207); + expect(_.isArray(response)).toEqual(true); + + expect(response.length).toEqual(2); + + // file entry + var info = response[0]; + expect(info instanceof OC.Files.FileInfo).toEqual(true); + expect(info.id).toEqual(51); + expect(info.path).toEqual('/path/to space/文件夹'); + expect(info.name).toEqual('One.txt'); + expect(info.permissions).toEqual(31); + expect(info.size).toEqual(250); + expect(info.mtime.getTime()).toEqual(1436535485000); + expect(info.mimetype).toEqual('text/plain'); + expect(info.etag).toEqual('559fcabd79a38'); + + // sub entry + info = response[1]; + expect(info instanceof OC.Files.FileInfo).toEqual(true); + expect(info.id).toEqual(15); + expect(info.path).toEqual('/path/to space/文件夹'); + expect(info.name).toEqual('sub'); + expect(info.permissions).toEqual(31); + expect(info.size).toEqual(100); + expect(info.mtime.getTime()).toEqual(1436536800000); + expect(info.mimetype).toEqual('httpd/unix-directory'); + expect(info.etag).toEqual('66cfcabd79abb'); + }); + return promise.promise(); + }); + it('returns parent node in result if specified', function() { + var promise = client.getFolderContents('path/to space/文件夹', {includeParent: true}); + + expect(fakeServer.requests.length).toEqual(1); + + fakeServer.requests[0].respond( + 207, + {'Content-Type': 'application/xml'}, + folderContentsXml + ); + + promise.then(function(status, response) { + expect(status).toEqual(207); + expect(_.isArray(response)).toEqual(true); + + expect(response.length).toEqual(3); + + // root entry + var info = response[0]; + expect(info instanceof OC.Files.FileInfo).toEqual(true); + expect(info.id).toEqual(11); + expect(info.path).toEqual('/path/to space'); + expect(info.name).toEqual('文件夹'); + expect(info.permissions).toEqual(31); + expect(info.size).toEqual(120); + expect(info.mtime.getTime()).toEqual(1436522405000); + expect(info.mimetype).toEqual('httpd/unix-directory'); + expect(info.etag).toEqual('56cfcabd79abb'); + + // the two other entries follow + expect(response[1].id).toEqual(51); + expect(response[2].id).toEqual(15); + }); + + return promise; + }); + it('rejects promise when an error occurred', function() { + var promise = client.getFolderContents('path/to space/文件夹', {includeParent: true}); + return respondAndCheckError(promise, 404); + }); + it('throws exception if arguments are missing', function() { + // TODO + }); + }); + + describe('file info', function() { + var responseXml = + '<?xml version="1.0" encoding="utf-8"?>' + + '<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">' + + makeResponseBlock( + '/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/', + { + 'd:getlastmodified': 'Fri, 10 Jul 2015 10:00:05 GMT', + 'd:getetag': '"56cfcabd79abb"', + 'd:resourcetype': '<d:collection/>', + 'oc:id': '00000011oc2d13a6a068', + 'oc:permissions': 'RDNVCK', + 'oc:size': 120 + }, + [ + 'd:getcontenttype', + 'd:getcontentlength' + ] + ) + + '</d:multistatus>'; + + it('sends PROPFIND with zero depth to get single file info', function() { + client.getFileInfo('path/to space/文件夹'); + + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('PROPFIND'); + expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9'); + expect(fakeServer.requests[0].requestHeaders.Depth).toEqual(0); + + var props = getRequestedProperties(fakeServer.requests[0].requestBody); + expect(props).toContain('{DAV:}getlastmodified'); + expect(props).toContain('{DAV:}getcontentlength'); + expect(props).toContain('{DAV:}getcontenttype'); + expect(props).toContain('{DAV:}getetag'); + expect(props).toContain('{DAV:}resourcetype'); + expect(props).toContain('{http://owncloud.org/ns}id'); + expect(props).toContain('{http://owncloud.org/ns}size'); + expect(props).toContain('{http://owncloud.org/ns}permissions'); + }); + it('parses the result into a FileInfo', function() { + var promise = client.getFileInfo('path/to space/文件夹'); + + expect(fakeServer.requests.length).toEqual(1); + + fakeServer.requests[0].respond( + 207, + {'Content-Type': 'application/xml'}, + responseXml + ); + + promise.then(function(status, response) { + expect(status).toEqual(207); + expect(_.isArray(response)).toEqual(false); + + var info = response; + expect(info instanceof OC.Files.FileInfo).toEqual(true); + expect(info.id).toEqual(11); + expect(info.path).toEqual('/path/to space'); + expect(info.name).toEqual('文件夹'); + expect(info.permissions).toEqual(31); + expect(info.size).toEqual(120); + expect(info.mtime.getTime()).toEqual(1436522405000); + expect(info.mimetype).toEqual('httpd/unix-directory'); + expect(info.etag).toEqual('56cfcabd79abb'); + }); + + return promise; + }); + it('properly parses entry inside root', function() { + var responseXml = + '<?xml version="1.0" encoding="utf-8"?>' + + '<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">' + + makeResponseBlock( + '/owncloud/remote.php/webdav/in%20root', + { + 'd:getlastmodified': 'Fri, 10 Jul 2015 10:00:05 GMT', + 'd:getetag': '"56cfcabd79abb"', + 'd:resourcetype': '<d:collection/>', + 'oc:id': '00000011oc2d13a6a068', + 'oc:permissions': 'RDNVCK', + 'oc:size': 120 + }, + [ + 'd:getcontenttype', + 'd:getcontentlength' + ] + ) + + '</d:multistatus>'; + + var promise = client.getFileInfo('in root'); + + expect(fakeServer.requests.length).toEqual(1); + + fakeServer.requests[0].respond( + 207, + {'Content-Type': 'application/xml'}, + responseXml + ); + + promise.then(function(status, response) { + expect(status).toEqual(207); + expect(_.isArray(response)).toEqual(false); + + var info = response; + expect(info instanceof OC.Files.FileInfo).toEqual(true); + expect(info.id).toEqual(11); + expect(info.path).toEqual('/'); + expect(info.name).toEqual('in root'); + expect(info.permissions).toEqual(31); + expect(info.size).toEqual(120); + expect(info.mtime.getTime()).toEqual(1436522405000); + expect(info.mimetype).toEqual('httpd/unix-directory'); + expect(info.etag).toEqual('56cfcabd79abb'); + }); + + return promise; + }); + it('rejects promise when an error occurred', function() { + var promise = client.getFileInfo('path/to space/文件夹'); + return respondAndCheckError(promise, 404); + }); + it('throws exception if arguments are missing', function() { + // TODO + }); + }); + + describe('permissions', function() { + + function getFileInfoWithPermission(webdavPerm, isFile) { + var props = { + 'd:getlastmodified': 'Fri, 10 Jul 2015 13:38:05 GMT', + 'd:getetag': '"559fcabd79a38"', + 'd:getcontentlength': 250, + 'oc:id': '00000051oc2d13a6a068', + 'oc:permissions': webdavPerm, + }; + + if (isFile) { + props['d:getcontenttype'] = 'text/plain'; + } else { + props['d:resourcetype'] = '<d:collection/>'; + } + + var responseXml = + '<?xml version="1.0" encoding="utf-8"?>' + + '<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">' + + makeResponseBlock( + '/owncloud/remote.php/webdav/file.txt', + props + ) + + '</d:multistatus>'; + var promise = client.getFileInfo('file.txt'); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 207, + {'Content-Type': 'application/xml'}, + responseXml + ); + + fakeServer.restore(); + fakeServer = sinon.fakeServer.create(); + + return promise; + } + + function testPermission(permission, isFile, expectedPermissions) { + var promise = getFileInfoWithPermission(permission, isFile); + promise.then(function(result) { + expect(result.permissions).toEqual(expectedPermissions); + }); + return promise; + } + + function testMountType(permission, isFile, expectedMountType) { + var promise = getFileInfoWithPermission(permission, isFile); + promise.then(function(result) { + expect(result.mountType).toEqual(expectedMountType); + }); + return promise; + } + + it('properly parses file permissions', function() { + // permission, isFile, expectedPermissions + var testCases = [ + ['', true, OC.PERMISSION_READ], + ['C', true, OC.PERMISSION_READ | OC.PERMISSION_CREATE], + ['K', true, OC.PERMISSION_READ | OC.PERMISSION_CREATE], + ['W', true, OC.PERMISSION_READ | OC.PERMISSION_CREATE | OC.PERMISSION_UPDATE], + ['D', true, OC.PERMISSION_READ | OC.PERMISSION_DELETE], + ['R', true, OC.PERMISSION_READ | OC.PERMISSION_SHARE], + ['CKWDR', true, OC.PERMISSION_ALL] + ]; + return Promise.all( + _.map(testCases, function(testCase) { + return testPermission.apply(testCase); + }) + ); + }); + it('properly parses folder permissions', function() { + var testCases = [ + ['', false, OC.PERMISSION_READ], + ['C', false, OC.PERMISSION_READ | OC.PERMISSION_CREATE | OC.PERMISSION_UPDATE], + ['K', false, OC.PERMISSION_READ | OC.PERMISSION_CREATE | OC.PERMISSION_UPDATE], + ['W', false, OC.PERMISSION_READ | OC.PERMISSION_UPDATE], + ['D', false, OC.PERMISSION_READ | OC.PERMISSION_DELETE], + ['R', false, OC.PERMISSION_READ | OC.PERMISSION_SHARE], + ['CKWDR', false, OC.PERMISSION_ALL] + ]; + + return Promise.all( + _.map(testCases, function(testCase) { + return testPermission.apply(testCase); + }) + ); + }); + it('properly parses mount types', function() { + var testCases = [ + ['CKWDR', false, null], + ['M', false, 'external'], + ['S', false, 'shared'], + ['SM', false, 'shared'] + ]; + + return Promise.all( + _.map(testCases, function(testCase) { + return testMountType.apply(testCase); + }) + ); + }); + }); + + describe('get file contents', function() { + it('returns file contents', function() { + var promise = client.getFileContents('path/to space/文件夹/One.txt'); + + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('GET'); + expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/One.txt'); + + fakeServer.requests[0].respond( + 200, + {'Content-Type': 'text/plain'}, + 'some contents' + ); + + promise.then(function(status, response) { + expect(status).toEqual(200); + expect(response).toEqual('some contents'); + }); + + return promise; + }); + it('rejects promise when an error occurred', function() { + var promise = client.getFileContents('path/to space/文件夹/One.txt'); + return respondAndCheckError(promise, 409); + }); + it('throws exception if arguments are missing', function() { + // TODO + }); + }); + + describe('put file contents', function() { + it('sends PUT with file contents', function() { + var promise = client.putFileContents( + 'path/to space/文件夹/One.txt', + 'some contents' + ); + + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('PUT'); + expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/One.txt'); + expect(fakeServer.requests[0].requestBody).toEqual('some contents'); + expect(fakeServer.requests[0].requestHeaders['If-None-Match']).toEqual('*'); + expect(fakeServer.requests[0].requestHeaders['Content-Type']).toEqual('text/plain;charset=utf-8'); + + return respondAndCheckStatus(promise, 201); + }); + it('sends PUT with file contents with headers matching options', function() { + var promise = client.putFileContents( + 'path/to space/文件夹/One.txt', + 'some contents', + { + overwrite: false, + contentType: 'text/markdown' + } + ); + + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('PUT'); + expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/One.txt'); + expect(fakeServer.requests[0].requestBody).toEqual('some contents'); + expect(fakeServer.requests[0].requestHeaders['If-None-Match']).not.toBeDefined(); + expect(fakeServer.requests[0].requestHeaders['Content-Type']).toEqual('text/markdown;charset=utf-8'); + + return respondAndCheckStatus(promise, 201); + }); + it('rejects promise when an error occurred', function() { + var promise = client.putFileContents( + 'path/to space/文件夹/One.txt', + 'some contents' + ); + return respondAndCheckError(promise, 409); + }); + it('throws exception if arguments are missing', function() { + // TODO + }); + }); + + describe('create directory', function() { + it('sends MKCOL with specified path', function() { + var promise = client.createDirectory('path/to space/文件夹/new dir'); + + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('MKCOL'); + expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/new%20dir'); + + return respondAndCheckStatus(promise, 201); + }); + it('rejects promise when an error occurred', function() { + var promise = client.createDirectory('path/to space/文件夹/new dir'); + return respondAndCheckError(promise, 404); + }); + it('throws exception if arguments are missing', function() { + // TODO + }); + }); + + describe('deletion', function() { + it('sends DELETE with specified path', function() { + var promise = client.remove('path/to space/文件夹'); + + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('DELETE'); + expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9'); + + return respondAndCheckStatus(promise, 201); + }); + it('rejects promise when an error occurred', function() { + var promise = client.remove('path/to space/文件夹'); + return respondAndCheckError(promise, 404); + }); + it('throws exception if arguments are missing', function() { + // TODO + }); + }); + + describe('move', function() { + it('sends MOVE with specified paths with fail on overwrite by default', function() { + var promise = client.move( + 'path/to space/文件夹', + 'path/to space/anotherdir/文件夹' + ); + + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('MOVE'); + expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9'); + expect(fakeServer.requests[0].requestHeaders.Destination) + .toEqual(baseUrl + 'path/to%20space/anotherdir/%E6%96%87%E4%BB%B6%E5%A4%B9'); + expect(fakeServer.requests[0].requestHeaders.Overwrite) + .toEqual('F'); + + return respondAndCheckStatus(promise, 201); + }); + it('sends MOVE with silent overwrite mode when specified', function() { + var promise = client.move( + 'path/to space/文件夹', + 'path/to space/anotherdir/文件夹', + {allowOverwrite: true} + ); + + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('MOVE'); + expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9'); + expect(fakeServer.requests[0].requestHeaders.Destination) + .toEqual(baseUrl + 'path/to%20space/anotherdir/%E6%96%87%E4%BB%B6%E5%A4%B9'); + expect(fakeServer.requests[0].requestHeaders.Overwrite) + .not.toBeDefined(); + + return respondAndCheckStatus(promise, 201); + }); + it('rejects promise when an error occurred', function() { + var promise = client.move( + 'path/to space/文件夹', + 'path/to space/anotherdir/文件夹', + {allowOverwrite: true} + ); + return respondAndCheckError(promise, 404); + }); + it('throws exception if arguments are missing', function() { + // TODO + }); + }); +}); diff --git a/tests/karma.config.js b/tests/karma.config.js index 64a94ef230bc..dc621ae0f743 100644 --- a/tests/karma.config.js +++ b/tests/karma.config.js @@ -164,15 +164,15 @@ module.exports = function(config) { // need to test the core app as well ? if (testCore) { // core tests - files.push(corePath + 'tests/specs/*.js'); + files.push(corePath + 'tests/specs/**/*.js'); } function addApp(app) { // if only a string was specified, expand to structure if (typeof(app) === 'string') { app = { - srcFiles: 'apps/' + app + '/js/*.js', - testFiles: 'apps/' + app + '/tests/js/*.js' + srcFiles: 'apps/' + app + '/js/**/*.js', + testFiles: 'apps/' + app + '/tests/js/**/*.js' }; } From fa2be0750c50de45a2fd101eb23fa858c0e0771b Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Mon, 13 Jul 2015 17:38:13 +0200 Subject: [PATCH 03/19] Make files app use Webdav for most operations --- apps/files/ajax/delete.php | 81 -- apps/files/ajax/move.php | 59 -- apps/files/ajax/newfile.php | 103 --- apps/files/ajax/newfolder.php | 99 -- apps/files/ajax/rename.php | 58 -- apps/files/js/app.js | 3 +- apps/files/js/fileactions.js | 3 +- apps/files/js/filelist.js | 572 ++++++------ apps/files/js/files.js | 30 +- apps/files/lib/app.php | 88 +- apps/files/lib/helper.php | 3 - apps/files/tests/ajax_rename.php | 232 ----- apps/files/tests/js/favoritesfilelistspec.js | 3 +- apps/files/tests/js/fileUploadSpec.js | 2 + apps/files/tests/js/fileactionsSpec.js | 2 +- apps/files/tests/js/fileactionsmenuSpec.js | 7 +- apps/files/tests/js/filelistSpec.js | 843 ++++++++---------- apps/files/tests/js/filesSpec.js | 6 +- apps/files_sharing/js/sharedfilelist.js | 4 +- .../tests/js/sharedfilelistSpec.js | 26 +- apps/files_trashbin/js/filelist.js | 72 +- core/js/oc-dialogs.js | 2 +- 22 files changed, 828 insertions(+), 1470 deletions(-) delete mode 100644 apps/files/ajax/delete.php delete mode 100644 apps/files/ajax/move.php delete mode 100644 apps/files/ajax/newfile.php delete mode 100644 apps/files/ajax/newfolder.php delete mode 100644 apps/files/ajax/rename.php delete mode 100644 apps/files/tests/ajax_rename.php diff --git a/apps/files/ajax/delete.php b/apps/files/ajax/delete.php deleted file mode 100644 index 2d02869df14f..000000000000 --- a/apps/files/ajax/delete.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * @author Frank Karlitschek <frank@owncloud.org> - * @author Jakob Sack <mail@jakobsack.de> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -OCP\JSON::checkLoggedIn(); -OCP\JSON::callCheck(); -\OC::$server->getSession()->close(); - - -// Get data -$dir = isset($_POST['dir']) ? (string)$_POST['dir'] : ''; -$allFiles = isset($_POST["allfiles"]) ? (string)$_POST["allfiles"] : false; - -// delete all files in dir ? -if ($allFiles === 'true') { - $files = array(); - $fileList = \OC\Files\Filesystem::getDirectoryContent($dir); - foreach ($fileList as $fileInfo) { - $files[] = $fileInfo['name']; - } -} else { - $files = isset($_POST["file"]) ? (string)$_POST["file"] : (string)$_POST["files"]; - $files = json_decode($files); -} -$filesWithError = ''; - -$success = true; - -//Now delete -foreach ($files as $file) { - try { - if (\OC\Files\Filesystem::file_exists($dir . '/' . $file) && - !(\OC\Files\Filesystem::isDeletable($dir . '/' . $file) && - \OC\Files\Filesystem::unlink($dir . '/' . $file)) - ) { - $filesWithError .= $file . "\n"; - $success = false; - } - } catch (\Exception $e) { - $filesWithError .= $file . "\n"; - $success = false; - } -} - -// get array with updated storage stats (e.g. max file size) after upload -try { - $storageStats = \OCA\Files\Helper::buildFileStorageStatistics($dir); -} catch(\OCP\Files\NotFoundException $e) { - OCP\JSON::error(['data' => ['message' => 'File not found']]); - return; -} - -if ($success) { - OCP\JSON::success(array("data" => array_merge(array("dir" => $dir, "files" => $files), $storageStats))); -} else { - OCP\JSON::error(array("data" => array_merge(array("message" => "Could not delete:\n" . $filesWithError), $storageStats))); -} diff --git a/apps/files/ajax/move.php b/apps/files/ajax/move.php deleted file mode 100644 index 0961636a116b..000000000000 --- a/apps/files/ajax/move.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Frank Karlitschek <frank@owncloud.org> - * @author Georg Ehrke <georg@owncloud.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -OCP\JSON::checkLoggedIn(); -OCP\JSON::callCheck(); -\OC::$server->getSession()->close(); - -// Get data -$dir = isset($_POST['dir']) ? (string)$_POST['dir'] : ''; -$file = isset($_POST['file']) ? (string)$_POST['file'] : ''; -$target = isset($_POST['target']) ? rawurldecode((string)$_POST['target']) : ''; - -$l = \OC::$server->getL10N('files'); - -if(\OC\Files\Filesystem::file_exists($target . '/' . $file)) { - OCP\JSON::error(array("data" => array( "message" => $l->t("Could not move %s - File with this name already exists", array($file)) ))); - exit; -} - -if ($target != '' || strtolower($file) != 'shared') { - $targetFile = \OC\Files\Filesystem::normalizePath($target . '/' . $file); - $sourceFile = \OC\Files\Filesystem::normalizePath($dir . '/' . $file); - try { - if(\OC\Files\Filesystem::rename($sourceFile, $targetFile)) { - OCP\JSON::success(array("data" => array( "dir" => $dir, "files" => $file ))); - } else { - OCP\JSON::error(array("data" => array( "message" => $l->t("Could not move %s", array($file)) ))); - } - } catch (\OCP\Files\NotPermittedException $e) { - OCP\JSON::error(array("data" => array( "message" => $l->t("Permission denied") ))); - } catch (\Exception $e) { - OCP\JSON::error(array("data" => array( "message" => $e->getMessage()))); - } -}else{ - OCP\JSON::error(array("data" => array( "message" => $l->t("Could not move %s", array($file)) ))); -} diff --git a/apps/files/ajax/newfile.php b/apps/files/ajax/newfile.php deleted file mode 100644 index be09b288d4b4..000000000000 --- a/apps/files/ajax/newfile.php +++ /dev/null @@ -1,103 +0,0 @@ -<?php -/** - * @author Andreas Fischer <bantu@owncloud.com> - * @author Georg Ehrke <georg@owncloud.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -// Init owncloud -global $eventSource; - -\OCP\JSON::checkLoggedIn(); -\OCP\JSON::callCheck(); - -\OC::$server->getSession()->close(); - -// Get the params -$dir = isset( $_REQUEST['dir'] ) ? '/'.trim((string)$_REQUEST['dir'], '/\\') : ''; -$fileName = isset( $_REQUEST['filename'] ) ? trim((string)$_REQUEST['filename'], '/\\') : ''; - -$l10n = \OC::$server->getL10N('files'); - -$result = array( - 'success' => false, - 'data' => NULL -); - -try { - \OC\Files\Filesystem::getView()->verifyPath($dir, $fileName); -} catch (\OCP\Files\InvalidPathException $ex) { - $result['data'] = [ - 'message' => $ex->getMessage()]; - OCP\JSON::error($result); - return; -} - -if (!\OC\Files\Filesystem::file_exists($dir . '/')) { - $result['data'] = array('message' => (string)$l10n->t( - 'The target folder has been moved or deleted.'), - 'code' => 'targetnotfound' - ); - OCP\JSON::error($result); - exit(); -} - -$target = $dir.'/'.$fileName; - -if (\OC\Files\Filesystem::file_exists($target)) { - $result['data'] = array('message' => (string)$l10n->t( - 'The name %s is already used in the folder %s. Please choose a different name.', - array($fileName, $dir)) - ); - OCP\JSON::error($result); - exit(); -} - -$success = false; -$templateManager = OC_Helper::getFileTemplateManager(); -$mimeType = OC_Helper::getMimetypeDetector()->detectPath($target); -$content = $templateManager->getTemplate($mimeType); - -try { - if($content) { - $success = \OC\Files\Filesystem::file_put_contents($target, $content); - } else { - $success = \OC\Files\Filesystem::touch($target); - } -} catch (\Exception $e) { - $result = [ - 'success' => false, - 'data' => [ - 'message' => $e->getMessage() - ] - ]; - OCP\JSON::error($result); - exit(); -} - -if($success) { - $meta = \OC\Files\Filesystem::getFileInfo($target); - OCP\JSON::success(array('data' => \OCA\Files\Helper::formatFileInfo($meta))); - return; -} - -OCP\JSON::error(array('data' => array( 'message' => $l10n->t('Error when creating the file') ))); diff --git a/apps/files/ajax/newfolder.php b/apps/files/ajax/newfolder.php deleted file mode 100644 index a2897dd437ac..000000000000 --- a/apps/files/ajax/newfolder.php +++ /dev/null @@ -1,99 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * @author Björn Schießle <schiessle@owncloud.com> - * @author Frank Karlitschek <frank@owncloud.org> - * @author Georg Ehrke <georg@owncloud.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -// Init owncloud - - -OCP\JSON::checkLoggedIn(); -OCP\JSON::callCheck(); -\OC::$server->getSession()->close(); - -// Get the params -$dir = isset($_POST['dir']) ? (string)$_POST['dir'] : ''; -$folderName = isset($_POST['foldername']) ?(string) $_POST['foldername'] : ''; - -$l10n = \OC::$server->getL10N('files'); - -$result = array( - 'success' => false, - 'data' => NULL - ); - -try { - \OC\Files\Filesystem::getView()->verifyPath($dir, $folderName); -} catch (\OCP\Files\InvalidPathException $ex) { - $result['data'] = [ - 'message' => $ex->getMessage()]; - OCP\JSON::error($result); - return; -} - -if (!\OC\Files\Filesystem::file_exists($dir . '/')) { - $result['data'] = array('message' => (string)$l10n->t( - 'The target folder has been moved or deleted.'), - 'code' => 'targetnotfound' - ); - OCP\JSON::error($result); - exit(); -} - -$target = $dir . '/' . $folderName; - -if (\OC\Files\Filesystem::file_exists($target)) { - $result['data'] = array('message' => $l10n->t( - 'The name %s is already used in the folder %s. Please choose a different name.', - array($folderName, $dir)) - ); - OCP\JSON::error($result); - exit(); -} - -try { - if(\OC\Files\Filesystem::mkdir($target)) { - if ( $dir !== '/') { - $path = $dir.'/'.$folderName; - } else { - $path = '/'.$folderName; - } - $meta = \OC\Files\Filesystem::getFileInfo($path); - $meta['type'] = 'dir'; // missing ?! - OCP\JSON::success(array('data' => \OCA\Files\Helper::formatFileInfo($meta))); - exit(); - } -} catch (\Exception $e) { - $result = [ - 'success' => false, - 'data' => [ - 'message' => $e->getMessage() - ] - ]; - OCP\JSON::error($result); - exit(); -} - -OCP\JSON::error(array('data' => array( 'message' => $l10n->t('Error when creating the folder') ))); diff --git a/apps/files/ajax/rename.php b/apps/files/ajax/rename.php deleted file mode 100644 index a24a57b10464..000000000000 --- a/apps/files/ajax/rename.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php -/** - * @author Christopher Schäpers <kondou@ts.unde.re> - * @author Frank Karlitschek <frank@owncloud.org> - * @author Jakob Sack <mail@jakobsack.de> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -OCP\JSON::checkLoggedIn(); -OCP\JSON::callCheck(); -\OC::$server->getSession()->close(); - -$l10n = \OC::$server->getL10N('files'); - -$files = new \OCA\Files\App( - \OC\Files\Filesystem::getView(), - \OC::$server->getL10N('files') -); -try { - $result = $files->rename( - isset($_GET['dir']) ? (string)$_GET['dir'] : '', - isset($_GET['file']) ? (string)$_GET['file'] : '', - isset($_GET['newname']) ? (string)$_GET['newname'] : '' - ); -} catch (\Exception $e) { - $result = [ - 'success' => false, - 'data' => [ - 'message' => $e->getMessage() - ] - ]; -} - -if($result['success'] === true){ - OCP\JSON::success(['data' => $result['data']]); -} else { - OCP\JSON::error(['data' => $result['data']]); -} diff --git a/apps/files/js/app.js b/apps/files/js/app.js index f31770466fed..ff505d417f1e 100644 --- a/apps/files/js/app.js +++ b/apps/files/js/app.js @@ -71,7 +71,8 @@ folderDropOptions: folderDropOptions, fileActions: fileActions, allowLegacyActions: true, - scrollTo: urlParams.scrollto + scrollTo: urlParams.scrollto, + filesClient: OC.Files.getClient() } ); this.files.initialize(); diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 6a767d48a289..32385c424789 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -575,7 +575,8 @@ }, actionHandler: function (filename, context) { var dir = context.dir || context.fileList.getCurrentDirectory(); - var url = context.fileList.getDownloadUrl(filename, dir); + var isDir = context.$file.attr('data-type') === 'dir'; + var url = context.fileList.getDownloadUrl(filename, dir, isDir); var downloadFileaction = $(context.$file).find('.fileactions .action-download'); diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index d1f68d98eab0..2c97816df008 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -22,11 +22,12 @@ * * @param $el container element with existing markup for the #controls * and a table - * @param [options] map of options, see other parameters - * @param [options.scrollContainer] scrollable container, defaults to $(window) - * @param [options.dragOptions] drag options, disabled by default - * @param [options.folderDropOptions] folder drop options, disabled by default - * @param [options.detailsViewEnabled=true] whether to enable details view + * @param {Object} [options] map of options, see other parameters + * @param {Object} [options.scrollContainer] scrollable container, defaults to $(window) + * @param {Object} [options.dragOptions] drag options, disabled by default + * @param {Object} [options.folderDropOptions] folder drop options, disabled by default + * @param {boolean} [options.detailsViewEnabled=true] whether to enable details view + * @param {OC.Files.Client} [options.filesClient] files client to use */ var FileList = function($el, options) { this.initialize($el, options); @@ -73,6 +74,13 @@ */ _detailsView: null, + /** + * Files client instance + * + * @type OC.Files.Client + */ + filesClient: null, + /** * Whether the file list was initialized already. * @type boolean @@ -92,10 +100,17 @@ * Array of files in the current folder. * The entries are of file data. * - * @type Array.<Object> + * @type Array.<OC.Files.FileInfo> */ files: [], + /** + * Current directory entry + * + * @type OC.Files.FileInfo + */ + dirInfo: null, + /** * File actions handler, defaults to OCA.Files.FileActions * @type OCA.Files.FileActions @@ -149,7 +164,7 @@ * When false, clicking on a table header will call reload(). * When true, clicking on a table header will simply resort the list. */ - _clientSideSort: false, + _clientSideSort: true, /** * Current directory @@ -170,6 +185,7 @@ * @param options.dragOptions drag options, disabled by default * @param options.folderDropOptions folder drop options, disabled by default * @param options.scrollTo name of file to scroll to after the first load + * @param {OC.Files.Client} [options.filesClient] files API client * @private */ initialize: function($el, options) { @@ -185,6 +201,12 @@ if (options.folderDropOptions) { this._folderDropOptions = options.folderDropOptions; } + if (options.filesClient) { + this.filesClient = options.filesClient; + } else { + // default client if not specified + this.filesClient = OC.Files.getClient(); + } this.$el = $el; if (options.id) { @@ -209,6 +231,8 @@ this.files = []; this._selectedFiles = {}; this._selectionSummary = new OCA.Files.FileSummary(); + // dummy root dir info + this.dirInfo = new OC.Files.FileInfo({}); this.fileSummary = this._createSummary(); @@ -359,7 +383,7 @@ var highlightState = $tr.hasClass('highlighted'); $tr = self.updateRow( $tr, - _.extend({isPreviewAvailable: true}, model.toJSON()), + model.toJSON(), {updateSummary: true, silent: false, animate: true} ); $tr.toggleClass('highlighted', highlightState); @@ -618,7 +642,7 @@ }; OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, true); - OCA.Files.Files.handleDownload(this.getDownloadUrl(files, dir), disableLoadingState); + OCA.Files.Files.handleDownload(this.getDownloadUrl(files, dir, true), disableLoadingState); return false; }, @@ -894,16 +918,39 @@ self.$el.closest('#app-content').trigger(jQuery.Event('apprendered')); }); }, + + /** + * Returns the icon URL matching the given file info + * + * @param {OC.Files.FileInfo} fileInfo file info + * + * @return {string} icon URL + */ + _getIconUrl: function(fileInfo) { + var mimeType = fileInfo.mimetype || 'application/octet-stream'; + if (mimeType === 'httpd/unix-directory') { + // use default folder icon + if (fileInfo.mountType === 'shared' || fileInfo.mountType === 'shared-root') { + return OC.MimeType.getIconUrl('dir-shared'); + } else if (fileInfo.mountType === 'external-root') { + return OC.MimeType.getIconUrl('dir-external'); + } + return OC.MimeType.getIconUrl('dir'); + } + return OC.MimeType.getIconUrl(mimeType); + }, + /** * Creates a new table row element using the given file data. - * @param {OCA.Files.FileInfo} fileData file info attributes + * @param {OC.Files.FileInfo} fileData file info attributes * @param options map of attributes * @return new tr element (not appended to the table) */ _createRow: function(fileData, options) { var td, simpleSize, basename, extension, sizeColor, - icon = OC.MimeType.getIconUrl(fileData.mimetype), + icon = fileData.icon || this._getIconUrl(fileData), name = fileData.name, + // TODO: get rid of type, only use mime type type = fileData.type || 'file', mtime = parseInt(fileData.mtime, 10), mime = fileData.mimetype, @@ -943,6 +990,14 @@ } if (fileData.mountType) { + // FIXME: HACK: detect shared-root + if (fileData.mountType === 'shared' && this.dirInfo.mountType !== 'shared') { + // if parent folder isn't share, assume the displayed folder is a share root + fileData.mountType = 'shared-root'; + } else if (fileData.mountType === 'external' && this.dirInfo.mountType !== 'external') { + // if parent folder isn't external, assume the displayed folder is the external storage root + fileData.mountType = 'external-root'; + } tr.attr('data-mounttype', fileData.mountType); } @@ -953,24 +1008,16 @@ path = this.getCurrentDirectory(); } - if (type === 'dir') { - // use default folder icon - icon = icon || OC.imagePath('core', 'filetypes/folder'); - } - else { - icon = icon || OC.imagePath('core', 'filetypes/file'); - } - // filename td td = $('<td class="filename"></td>'); // linkUrl - if (type === 'dir') { + if (mime === 'httpd/unix-directory') { linkUrl = this.linkTo(path + '/' + name); } else { - linkUrl = this.getDownloadUrl(name, path); + linkUrl = this.getDownloadUrl(name, path, type === 'dir'); } if (this._allowSelection) { td.append( @@ -996,7 +1043,7 @@ basename = ''; extension = name; // split extension from filename for non dirs - } else if (type !== 'dir' && name.indexOf('.') !== -1) { + } else if (mime !== 'httpd/unix-directory' && name.indexOf('.') !== -1) { basename = name.substr(0, name.lastIndexOf('.')); extension = name.substr(name.lastIndexOf('.')); } else { @@ -1018,7 +1065,7 @@ nameSpan.tooltip({placement: 'right'}); } // dirs can show the number of uploaded files - if (type === 'dir') { + if (mime !== 'httpd/unix-directory') { linkElem.append($('<span></span>').attr({ 'class': 'uploadtext', 'currentUploads': 0 @@ -1074,7 +1121,7 @@ * Adds an entry to the files array and also into the DOM * in a sorted manner. * - * @param {OCA.Files.FileInfo} fileData map of file attributes + * @param {OC.Files.FileInfo} fileData map of file attributes * @param {Object} [options] map of attributes * @param {boolean} [options.updateSummary] true to update the summary * after adding (default), false otherwise. Defaults to true. @@ -1147,7 +1194,7 @@ * Creates a new row element based on the given attributes * and returns it. * - * @param {OCA.Files.FileInfo} fileData map of file attributes + * @param {OC.Files.FileInfo} fileData map of file attributes * @param {Object} [options] map of attributes * @param {int} [options.index] index at which to insert the element * @param {boolean} [options.updateSummary] true to update the summary @@ -1182,7 +1229,7 @@ filenameTd.draggable(this._dragOptions); } // allow dropping on folders - if (this._folderDropOptions && fileData.type === 'dir') { + if (this._folderDropOptions && mime === 'httpd/unix-directory') { filenameTd.droppable(this._folderDropOptions); } @@ -1193,7 +1240,7 @@ // display actions this.fileActions.display(filenameTd, !options.silent, this); - if (fileData.isPreviewAvailable && mime !== 'httpd/unix-directory') { + if (mime !== 'httpd/unix-directory') { var iconDiv = filenameTd.find('.thumbnail'); // lazy load / newly inserted td ? // the typeof check ensures that the default value of animate is true @@ -1343,17 +1390,7 @@ this._currentFileModel = null; this.$el.find('.select-all').prop('checked', false); this.showMask(); - if (this._reloadCall) { - this._reloadCall.abort(); - } - this._reloadCall = $.ajax({ - url: this.getAjaxUrl('list'), - data: { - dir : this.getCurrentDirectory(), - sort: this._sort, - sortdirection: this._sortDirection - } - }); + this._reloadCall = this.filesClient.getFolderContents(this.getCurrentDirectory(), {includeParent: true}); if (this._detailsView) { // close sidebar this._updateDetailsView(null); @@ -1361,24 +1398,19 @@ var callBack = this.reloadCallback.bind(this); return this._reloadCall.then(callBack, callBack); }, - reloadCallback: function(result) { + reloadCallback: function(status, result) { delete this._reloadCall; this.hideMask(); - if (!result || result.status === 'error') { - // if the error is not related to folder we're trying to load, reload the page to handle logout etc - if (result.data.error === 'authentication_error' || - result.data.error === 'token_expired' || - result.data.error === 'application_not_enabled' - ) { - OC.redirect(OC.generateUrl('apps/files')); - } - OC.Notification.showTemporary(result.data.message); + if (status === 401) { + // TODO: append current URL to be able to get back after logging in again + OC.redirect(OC.generateUrl('apps/files')); + OC.Notification.show(result); return false; } // Firewall Blocked request? - if (result.status === 403) { + if (status === 403) { // Go home this.changeDirectory('/'); OC.Notification.showTemporary(t('files', 'This operation is forbidden')); @@ -1386,32 +1418,49 @@ } // Did share service die or something else fail? - if (result.status === 500) { + if (status === 500) { // Go home this.changeDirectory('/'); - OC.Notification.showTemporary(t('files', 'This directory is unavailable, please check the logs or contact the administrator')); + OC.Notification.showTemporary( + t('files', 'This directory is unavailable, please check the logs or contact the administrator') + ); + return false; + } + + if (status === 503) { + // Go home + if (this.getCurrentDirectory() !== '/') { + this.changeDirectory('/'); + // TODO: read error message from exception + OC.Notification.showTemporary( + t('files', 'Storage not available') + ); + } return false; } - if (result.status === 404) { + if (status === 404) { // go back home this.changeDirectory('/'); return false; } // aborted ? - if (result.status === 0){ + if (status === 0){ return true; } - // TODO: should rather return upload file size through - // the files list ajax call + // TODO: parse remaining quota from PROPFIND response this.updateStorageStatistics(true); - if (result.data.permissions) { - this.setDirectoryPermissions(result.data.permissions); + // first entry is the root + this.dirInfo = result.shift(); + + if (this.dirInfo.permissions) { + this.setDirectoryPermissions(this.dirInfo.permissions); } - this.setFiles(result.data.files); + result.sort(this._sortComparator); + this.setFiles(result); return true; }, @@ -1419,12 +1468,15 @@ OCA.Files.Files.updateStorageStatistics(this.getCurrentDirectory(), force); }, + /** + * @deprecated do not use nor override + */ getAjaxUrl: function(action, params) { return OCA.Files.Files.getAjaxUrl(action, params); }, - getDownloadUrl: function(files, dir) { - return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory()); + getDownloadUrl: function(files, dir, isDir) { + return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory(), isDir); }, /** @@ -1489,8 +1541,6 @@ if (etag){ // use etag as cache buster urlSpec.c = etag; - } else { - console.warn('OCA.Files.FileList.lazyLoadPreview(): missing etag argument'); } previewURL = self.generatePreviewUrl(urlSpec); @@ -1514,6 +1564,9 @@ img.src = previewURL; }, + /** + * @deprecated + */ setDirectoryPermissions: function(permissions) { var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; this.$el.find('#permissions').val(permissions); @@ -1610,7 +1663,7 @@ * fileData should be inserted, considering the current * sorting * - * @param {OCA.Files.FileInfo} fileData file info + * @param {OC.Files.FileInfo} fileData file info */ _findInsertionIndex: function(fileData) { var index = 0; @@ -1628,6 +1681,9 @@ move: function(fileNames, targetPath) { var self = this; var dir = this.getCurrentDirectory(); + if (dir.charAt(dir.length - 1) !== '/') { + dir += '/'; + } var target = OC.basename(targetPath); if (!_.isArray(fileNames)) { fileNames = [fileNames]; @@ -1635,46 +1691,42 @@ _.each(fileNames, function(fileName) { var $tr = self.findFileEl(fileName); self.showFileBusyState($tr, true); - // TODO: improve performance by sending all file names in a single call - $.post( - OC.filePath('files', 'ajax', 'move.php'), - { - dir: dir, - file: fileName, - target: targetPath - }, - function(result) { - if (result) { - if (result.status === 'success') { - // if still viewing the same directory - if (self.getCurrentDirectory() === dir) { - // recalculate folder size - var oldFile = self.findFileEl(target); - var newFile = self.findFileEl(fileName); - var oldSize = oldFile.data('size'); - var newSize = oldSize + newFile.data('size'); - oldFile.data('size', newSize); - oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize)); - - // TODO: also update entry in FileList.files - - self.remove(fileName); - } - } else { - OC.Notification.hide(); - if (result.status === 'error' && result.data.message) { - OC.Notification.showTemporary(result.data.message); - } - else { - OC.Notification.showTemporary(t('files', 'Error moving file.')); - } - } + if (targetPath.charAt(targetPath.length - 1) !== '/') { + // make sure we move the files into the target dir, + // not overwrite it + targetPath = targetPath + '/'; + } + self.filesClient.move(dir + fileName, targetPath + fileName) + .done(function() { + // if still viewing the same directory + if (OC.joinPaths(self.getCurrentDirectory(), '/') === dir) { + // recalculate folder size + var oldFile = self.findFileEl(target); + var newFile = self.findFileEl(fileName); + var oldSize = oldFile.data('size'); + var newSize = oldSize + newFile.data('size'); + oldFile.data('size', newSize); + oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize)); + + // TODO: also update entry in FileList.files + self.remove(fileName); + } + }) + .fail(function(status) { + if (status === 412) { + // TODO: some day here we should invoke the conflict dialog + OC.Notification.showTemporary( + t('files', 'Could not move "{file}", target exists', {file: fileName}) + ); } else { - OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); + OC.Notification.showTemporary( + t('files', 'Could not move "{file}"', {file: fileName}) + ); } + }) + .always(function() { self.showFileBusyState($tr, false); - } - ); + }); }); }, @@ -1700,16 +1752,16 @@ * Triggers file rename input field for the given file name. * If the user enters a new name, the file will be renamed. * - * @param oldname file name of the file to rename + * @param oldName file name of the file to rename */ - rename: function(oldname) { + rename: function(oldName) { var self = this; var tr, td, input, form; - tr = this.findFileEl(oldname); + tr = this.findFileEl(oldName); var oldFileInfo = this.files[tr.index()]; tr.data('renaming',true); td = tr.children('td.filename'); - input = $('<input type="text" class="filename"/>').val(oldname); + input = $('<input type="text" class="filename"/>').val(oldName); form = $('<form></form>'); form.append(input); td.children('a.name').hide(); @@ -1724,11 +1776,11 @@ input.selectRange(0, len); var checkInput = function () { var filename = input.val(); - if (filename !== oldname) { + if (filename !== oldName) { // Files.isFileNameValid(filename) throws an exception itself OCA.Files.Files.isFileNameValid(filename); if (self.inList(filename)) { - throw t('files', '{new_name} already exists', {new_name: filename}); + throw t('files', '{newName} already exists', {newName: filename}); } } return true; @@ -1741,6 +1793,14 @@ td.children('a.name').show(); } + function updateInList(fileInfo) { + tr.remove(); + tr = self.add(fileInfo, {updateSummary: false, silent: true}); + self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)})); + self._updateDetailsView(fileInfo.name); + } + + // TODO: too many nested blocks, move parts into functions form.submit(function(event) { event.stopPropagation(); event.preventDefault(); @@ -1753,7 +1813,7 @@ input.tooltip('hide'); form.remove(); - if (newName !== oldname) { + if (newName !== oldName) { checkInput(); // mark as loading (temp element) self.showFileBusyState(tr, true); @@ -1765,34 +1825,46 @@ td.find('a.name span.nametext').text(basename); td.children('a.name').show(); - $.ajax({ - url: OC.filePath('files','ajax','rename.php'), - data: { - dir : tr.attr('data-path') || self.getCurrentDirectory(), - newname: newName, - file: oldname - }, - success: function(result) { - var fileInfo; - if (!result || result.status === 'error') { - OC.dialogs.alert(result.data.message, t('files', 'Could not rename file')); - fileInfo = oldFileInfo; - if (result.data.code === 'sourcenotfound') { - self.remove(result.data.newname, {updateSummary: true}); - return; - } - } - else { - fileInfo = result.data; + var path = tr.attr('data-path') || self.getCurrentDirectory(); + self.filesClient.move(path + '/' + oldName, path + '/' + newName) + .done(function() { + var fileInfo = self.files.splice(tr.index(), 1)[0]; + fileInfo.name = newName; + updateInList(fileInfo); + }) + .fail(function(status) { + // TODO: 409 means current folder does not exist, redirect ? + if (status === 404) { + // source not found, so remove it from the list + OC.Notification.showTemporary( + t( + 'files', + 'Could not rename "{fileName}", it does not exist any more', + {fileName: oldName} + ) + ); + self.remove(newName, {updateSummary: true}); + return; + } else if (status === 412) { + // target exists + OC.Notification.showTemporary( + t( + 'files', + 'The name "{targetName}" is already used in the folder "{dir}". Please choose a different name.', + { + targetName: newName, + dir: self.getCurrentDirectory() + } + ) + ); + } else { + // restore the item to its previous state + OC.Notification.showTemporary( + t('files', 'Could not rename "{fileName}"', {fileName: oldName}) + ); } - // reinsert row - self.files.splice(tr.index(), 1); - tr.remove(); - tr = self.add(fileInfo, {updateSummary: false, silent: true}); - self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)})); - self._updateDetailsView(fileInfo.name, false); - } - }); + updateInList(oldFileInfo); + }); } else { // add back the old file info when cancelled self.files.splice(tr.index(), 1); @@ -1849,32 +1921,44 @@ var promise = deferred.promise(); OCA.Files.Files.isFileNameValid(name); - name = this.getUniqueName(name); if (this.lastAction) { this.lastAction(); } - $.post( - OC.generateUrl('/apps/files/ajax/newfile.php'), - { - dir: this.getCurrentDirectory(), - filename: name - }, - function(result) { - if (result.status === 'success') { - self.add(result.data, {animate: true, scrollTo: true}); - deferred.resolve(result.status, result.data); + name = this.getUniqueName(name); + var targetPath = this.getCurrentDirectory() + '/' + name; + + self.filesClient.putFileContents( + targetPath, + '', + { + contentType: 'text/plain', + overwrite: true + } + ) + .done(function() { + // TODO: error handling / conflicts + self.filesClient.getFileInfo(targetPath) + .then(function(status, data) { + self.add(data, {animate: true, scrollTo: true}); + deferred.resolve(status, data); + }) + .fail(function(status) { + OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name})); + deferred.reject(status); + }); + }) + .fail(function(status) { + if (status === 412) { + OC.Notification.showTemporary( + t('files', 'Could not create file "{file}" because it already exists', {file: name}) + ); } else { - if (result.data && result.data.message) { - OC.Notification.showTemporary(result.data.message); - } else { - OC.Notification.showTemporary(t('core', 'Could not create file')); - } - deferred.reject(result.status, result.data); + OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name})); } - } - ); + deferred.reject(status); + }); return promise; }, @@ -1895,32 +1979,50 @@ var promise = deferred.promise(); OCA.Files.Files.isFileNameValid(name); - name = this.getUniqueName(name); if (this.lastAction) { this.lastAction(); } - $.post( - OC.generateUrl('/apps/files/ajax/newfolder.php'), - { - dir: this.getCurrentDirectory(), - foldername: name - }, - function(result) { - if (result.status === 'success') { - self.add(result.data, {animate: true, scrollTo: true}); - deferred.resolve(result.status, result.data); + name = this.getUniqueName(name); + var targetPath = this.getCurrentDirectory() + '/' + name; + + this.filesClient.createDirectory(targetPath) + .done(function(createStatus) { + self.filesClient.getFileInfo(targetPath) + .done(function(status, data) { + self.add(data, {animate: true, scrollTo: true}); + deferred.resolve(status, data); + }) + .fail(function() { + OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: name})); + deferred.reject(createStatus); + }); + }) + .fail(function(createStatus) { + // method not allowed, folder might exist already + if (createStatus === 405) { + self.filesClient.getFileInfo(targetPath) + .done(function(status, data) { + // add it to the list, for completeness + self.add(data, {animate: true, scrollTo: true}); + OC.Notification.showTemporary( + t('files', 'Could not create folder "{dir}" because it already exists', {dir: name}) + ); + // still consider a failure + deferred.reject(createStatus, data); + }) + .fail(function() { + OC.Notification.showTemporary( + t('files', 'Could not create folder "{dir}"', {dir: name}) + ); + deferred.reject(status); + }); } else { - if (result.data && result.data.message) { - OC.Notification.showTemporary(result.data.message); - } else { - OC.Notification.showTemporary(t('core', 'Could not create folder')); - } - deferred.reject(result.status); + OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: name})); + deferred.reject(createStatus); } - } - ); + }); return promise; }, @@ -1981,76 +2083,59 @@ */ do_delete:function(files, dir) { var self = this; - var params; if (files && files.substr) { files=[files]; } + if (!files) { + // delete all files in directory + files = _.pluck(this.files, 'name'); + } if (files) { this.showFileBusyState(files, true); - for (var i=0; i<files.length; i++) { - } } // Finish any existing actions if (this.lastAction) { this.lastAction(); } - params = { - dir: dir || this.getCurrentDirectory() - }; - if (files) { - params.files = JSON.stringify(files); + dir = dir || this.getCurrentDirectory(); + + function removeFromList(file) { + var fileEl = self.remove(file, {updateSummary: false}); + // FIXME: not sure why we need this after the + // element isn't even in the DOM any more + fileEl.find('.selectCheckBox').prop('checked', false); + fileEl.removeClass('selected'); + self.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}); + // TODO: this info should be returned by the ajax call! + self.updateEmptyContent(); + self.fileSummary.update(); + self.updateSelectionSummary(); + // FIXME: don't repeat this, do it once all files are done + self.updateStorageStatistics(); } - else { - // no files passed, delete all in current dir - params.allfiles = true; - // show spinner for all files - this.showFileBusyState(this.$fileList.find('tr'), true); - } - - $.post(OC.filePath('files', 'ajax', 'delete.php'), - params, - function(result) { - if (result.status === 'success') { - if (params.allfiles) { - self.setFiles([]); - } - else { - $.each(files,function(index,file) { - var fileEl = self.remove(file, {updateSummary: false}); - // FIXME: not sure why we need this after the - // element isn't even in the DOM any more - fileEl.find('.selectCheckBox').prop('checked', false); - fileEl.removeClass('selected'); - self.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}); - }); - } - // TODO: this info should be returned by the ajax call! - self.updateEmptyContent(); - self.fileSummary.update(); - self.updateSelectionSummary(); - self.updateStorageStatistics(); - // in case there was a "storage full" permanent notification - OC.Notification.hide(); + + _.each(files, function(file) { + self.filesClient.remove(dir + '/' + file) + .done(function() { + removeFromList(file); + }) + .fail(function(status) { + if (status === 404) { + // the file already did not exist, remove it from the list + removeFromList(file); } else { - if (result.status === 'error' && result.data.message) { - OC.Notification.showTemporary(result.data.message); - } - else { - OC.Notification.showTemporary(t('files', 'Error deleting file.')); - } - if (params.allfiles) { - // reload the page as we don't know what files were deleted - // and which ones remain - self.reload(); - } - else { - $.each(files,function(index,file) { - self.showFileBusyState(file, false); - }); - } + // only reset the spinner for that one file + OC.Notification.showTemporary( + t('files', 'Error deleting file "{fileName}".', {fileName: file}), + {timeout: 10} + ); + var deleteAction = self.findFileEl(file).find('.action.delete'); + deleteAction.removeClass('icon-loading-small').addClass('icon-delete'); + self.showFileBusyState(files, false); } }); + }); }, /** * Creates the file summary section @@ -2659,8 +2744,8 @@ * Compares two file infos by name, making directories appear * first. * - * @param {OCA.Files.FileInfo} fileInfo1 file info - * @param {OCA.Files.FileInfo} fileInfo2 file info + * @param {OC.Files.FileInfo} fileInfo1 file info + * @param {OC.Files.FileInfo} fileInfo2 file info * @return {int} -1 if the first file must appear before the second one, * 0 if they are identify, 1 otherwise. */ @@ -2676,8 +2761,8 @@ /** * Compares two file infos by size. * - * @param {OCA.Files.FileInfo} fileInfo1 file info - * @param {OCA.Files.FileInfo} fileInfo2 file info + * @param {OC.Files.FileInfo} fileInfo1 file info + * @param {OC.Files.FileInfo} fileInfo2 file info * @return {int} -1 if the first file must appear before the second one, * 0 if they are identify, 1 otherwise. */ @@ -2687,8 +2772,8 @@ /** * Compares two file infos by timestamp. * - * @param {OCA.Files.FileInfo} fileInfo1 file info - * @param {OCA.Files.FileInfo} fileInfo2 file info + * @param {OC.Files.FileInfo} fileInfo1 file info + * @param {OC.Files.FileInfo} fileInfo2 file info * @return {int} -1 if the first file must appear before the second one, * 0 if they are identify, 1 otherwise. */ @@ -2700,23 +2785,14 @@ /** * File info attributes. * - * @todo make this a real class in the future - * @typedef {Object} OCA.Files.FileInfo + * @typedef {Object} OC.Files.FileInfo + * + * @lends OC.Files.FileInfo + * + * @deprecated use OC.Files.FileInfo instead * - * @property {int} id file id - * @property {String} name file name - * @property {String} [path] file path, defaults to the list's current path - * @property {String} mimetype mime type - * @property {String} type "file" for files or "dir" for directories - * @property {int} permissions file permissions - * @property {int} mtime modification time in milliseconds - * @property {boolean} [isShareMountPoint] whether the file is a share mount - * point - * @property {boolean} [isPreviewAvailable] whether a preview is available - * for the given file type - * @property {String} [icon] path to the mime type icon - * @property {String} etag etag of the file */ + OCA.Files.FileInfo = OC.Files.FileInfo; OCA.Files.FileList = FileList; })(); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index ae38511ec057..6bdd14ac65d5 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -136,13 +136,27 @@ /** * Returns the download URL of the given file(s) - * @param filename string or array of file names to download - * @param dir optional directory in which the file name is, defaults to the current directory + * @param {string} filename string or array of file names to download + * @param {string} [dir] optional directory in which the file name is, defaults to the current directory + * @param {bool} [isDir=false] whether the given filename is a directory and might need a special URL */ - getDownloadUrl: function(filename, dir) { - if ($.isArray(filename)) { + getDownloadUrl: function(filename, dir, isDir) { + if (!_.isArray(filename) && !isDir) { + var pathSections = dir.split('/'); + pathSections.push(filename); + var encodedPath = ''; + _.each(pathSections, function(section) { + if (section !== '') { + encodedPath += '/' + encodeURIComponent(section); + } + }); + return OC.linkToRemoteBase('webdav') + encodedPath; + } + + if (_.isArray(filename)) { filename = JSON.stringify(filename); } + var params = { dir: dir, files: filename @@ -356,8 +370,10 @@ scanFiles.scanning=false; // TODO: move to FileList var createDragShadow = function(event) { + // FIXME: inject file list instance somehow + /* global FileList, Files */ + //select dragged file - var FileList = OCA.Files.App.fileList; var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked'); if (!isDragSelected) { //select dragged file @@ -394,7 +410,7 @@ var createDragShadow = function(event) { .css('background-image', 'url(' + OC.imagePath('core', 'filetypes/folder.png') + ')'); } else { var path = dir + '/' + elem.name; - OCA.Files.App.files.lazyLoadPreview(path, elem.mime, function(previewpath) { + Files.lazyLoadPreview(path, elem.mimetype, function(previewpath) { newtr.find('td.filename') .css('background-image', 'url(' + previewpath + ')'); }, null, null, elem.etag); @@ -441,7 +457,7 @@ var folderDropOptions = { hoverClass: "canDrop", drop: function( event, ui ) { // don't allow moving a file into a selected folder - var FileList = OCA.Files.App.fileList; + /* global FileList */ if ($(event.target).parents('tr').find('td input:first').prop('checked') === true) { return false; } diff --git a/apps/files/lib/app.php b/apps/files/lib/app.php index 5bd8c1275861..18813e224de1 100644 --- a/apps/files/lib/app.php +++ b/apps/files/lib/app.php @@ -28,108 +28,22 @@ namespace OCA\Files; class App { - /** - * @var \OC_L10N - */ - private $l10n; - /** * @var \OCP\INavigationManager */ private static $navigationManager; - /** - * @var \OC\Files\View - */ - private $view; - - public function __construct($view, $l10n) { - $this->view = $view; - $this->l10n = $l10n; - } - /** * Returns the app's navigation manager * * @return \OCP\INavigationManager */ public static function getNavigationManager() { + // TODO: move this into a service in the Application class if (self::$navigationManager === null) { self::$navigationManager = new \OC\NavigationManager(); } return self::$navigationManager; } - /** - * rename a file - * - * @param string $dir - * @param string $oldname - * @param string $newname - * @return array - */ - public function rename($dir, $oldname, $newname) { - $result = array( - 'success' => false, - 'data' => NULL - ); - - try { - // check if the new name is conform to file name restrictions - $this->view->verifyPath($dir, $newname); - } catch (\OCP\Files\InvalidPathException $ex) { - $result['data'] = array( - 'message' => $this->l10n->t($ex->getMessage()), - 'code' => 'invalidname', - ); - return $result; - } - - $normalizedOldPath = \OC\Files\Filesystem::normalizePath($dir . '/' . $oldname); - $normalizedNewPath = \OC\Files\Filesystem::normalizePath($dir . '/' . $newname); - - // rename to non-existing folder is denied - if (!$this->view->file_exists($normalizedOldPath)) { - $result['data'] = array( - 'message' => $this->l10n->t('%s could not be renamed as it has been deleted', array($oldname)), - 'code' => 'sourcenotfound', - 'oldname' => $oldname, - 'newname' => $newname, - ); - }else if (!$this->view->file_exists($dir)) { - $result['data'] = array('message' => (string)$this->l10n->t( - 'The target folder has been moved or deleted.', - array($dir)), - 'code' => 'targetnotfound' - ); - // rename to existing file is denied - } else if ($this->view->file_exists($normalizedNewPath)) { - - $result['data'] = array( - 'message' => $this->l10n->t( - "The name %s is already used in the folder %s. Please choose a different name.", - array($newname, $dir)) - ); - } else if ( - // rename to "." is denied - $newname !== '.' and - // THEN try to rename - $this->view->rename($normalizedOldPath, $normalizedNewPath) - ) { - // successful rename - $meta = $this->view->getFileInfo($normalizedNewPath); - $meta = \OCA\Files\Helper::populateTags(array($meta)); - $fileInfo = \OCA\Files\Helper::formatFileInfo(current($meta)); - $fileInfo['path'] = dirname($normalizedNewPath); - $result['success'] = true; - $result['data'] = $fileInfo; - } else { - // rename failed - $result['data'] = array( - 'message' => $this->l10n->t('%s could not be renamed', array($oldname)) - ); - } - return $result; - } - } diff --git a/apps/files/lib/helper.php b/apps/files/lib/helper.php index fb14cea731f3..9a4e8d59786a 100644 --- a/apps/files/lib/helper.php +++ b/apps/files/lib/helper.php @@ -139,9 +139,6 @@ public static function formatFileInfo(FileInfo $i) { $entry['parentId'] = $i['parent']; $entry['mtime'] = $i['mtime'] * 1000; // only pick out the needed attributes - if (\OC::$server->getPreviewManager()->isAvailable($i)) { - $entry['isPreviewAvailable'] = true; - } $entry['name'] = $i->getName(); $entry['permissions'] = $i['permissions']; $entry['mimetype'] = $i['mimetype']; diff --git a/apps/files/tests/ajax_rename.php b/apps/files/tests/ajax_rename.php deleted file mode 100644 index 859c7042b89c..000000000000 --- a/apps/files/tests/ajax_rename.php +++ /dev/null @@ -1,232 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Christopher Schäpers <kondou@ts.unde.re> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -class Test_OC_Files_App_Rename extends \Test\TestCase { - private static $user; - - /** - * @var PHPUnit_Framework_MockObject_MockObject - */ - private $viewMock; - - /** - * @var \OCA\Files\App - */ - private $files; - - protected function setUp() { - parent::setUp(); - - // mock OC_L10n - if (!self::$user) { - self::$user = uniqid(); - } - \OC_User::createUser(self::$user, 'password'); - $this->loginAsUser(self::$user); - - $l10nMock = $this->getMock('\OC_L10N', array('t'), array(), '', false); - $l10nMock->expects($this->any()) - ->method('t') - ->will($this->returnArgument(0)); - $viewMock = $this->getMock('\OC\Files\View', array('rename', 'normalizePath', 'getFileInfo', 'file_exists'), array(), '', false); - $viewMock->expects($this->any()) - ->method('normalizePath') - ->will($this->returnArgument(0)); - $viewMock->expects($this->any()) - ->method('rename') - ->will($this->returnValue(true)); - $this->viewMock = $viewMock; - $this->files = new \OCA\Files\App($viewMock, $l10nMock); - } - - protected function tearDown() { - $result = \OC_User::deleteUser(self::$user); - $this->assertTrue($result); - - $this->logout(); - parent::tearDown(); - } - - /** - * test rename of file/folder - */ - function testRenameFolder() { - $dir = '/'; - $oldname = 'oldname'; - $newname = 'newname'; - - $this->viewMock->expects($this->any()) - ->method('file_exists') - ->with($this->anything()) - ->will($this->returnValueMap(array( - array('/', true), - array('/oldname', true) - ))); - - - $this->viewMock->expects($this->any()) - ->method('getFileInfo') - ->will($this->returnValue(new \OC\Files\FileInfo( - '/new_name', - new \OC\Files\Storage\Local(array('datadir' => '/')), - '/', - array( - 'fileid' => 123, - 'type' => 'dir', - 'mimetype' => 'httpd/unix-directory', - 'mtime' => 0, - 'permissions' => 31, - 'size' => 18, - 'etag' => 'abcdef', - 'directory' => '/', - 'name' => 'new_name', - ), null))); - - $result = $this->files->rename($dir, $oldname, $newname); - - $this->assertTrue($result['success']); - $this->assertEquals(123, $result['data']['id']); - $this->assertEquals('new_name', $result['data']['name']); - $this->assertEquals(18, $result['data']['size']); - $this->assertEquals('httpd/unix-directory', $result['data']['mimetype']); - $this->assertEquals('abcdef', $result['data']['etag']); - $this->assertFalse(isset($result['data']['tags'])); - $this->assertEquals('/', $result['data']['path']); - } - - /** - * test rename of file with tag - */ - function testRenameFileWithTag() { - $taggerMock = $this->getMock('\OCP\ITags'); - $taggerMock->expects($this->any()) - ->method('getTagsForObjects') - ->with(array(123)) - ->will($this->returnValue(array(123 => array('tag1', 'tag2')))); - $tagManagerMock = $this->getMock('\OCP\ITagManager'); - $tagManagerMock->expects($this->any()) - ->method('load') - ->with('files') - ->will($this->returnValue($taggerMock)); - $oldTagManager = \OC::$server->query('TagManager'); - \OC::$server->registerService('TagManager', function ($c) use ($tagManagerMock) { - return $tagManagerMock; - }); - - $dir = '/'; - $oldname = 'oldname.txt'; - $newname = 'newname.txt'; - - $this->viewMock->expects($this->any()) - ->method('file_exists') - ->with($this->anything()) - ->will($this->returnValueMap(array( - array('/', true), - array('/oldname.txt', true) - ))); - - - $this->viewMock->expects($this->any()) - ->method('getFileInfo') - ->will($this->returnValue(new \OC\Files\FileInfo( - '/new_name.txt', - new \OC\Files\Storage\Local(array('datadir' => '/')), - '/', - array( - 'fileid' => 123, - 'type' => 'file', - 'mimetype' => 'text/plain', - 'mtime' => 0, - 'permissions' => 31, - 'size' => 18, - 'etag' => 'abcdef', - 'directory' => '/', - 'name' => 'new_name.txt', - ), null))); - - $result = $this->files->rename($dir, $oldname, $newname); - - $this->assertTrue($result['success']); - $this->assertEquals(123, $result['data']['id']); - $this->assertEquals('new_name.txt', $result['data']['name']); - $this->assertEquals(18, $result['data']['size']); - $this->assertEquals('text/plain', $result['data']['mimetype']); - $this->assertEquals('abcdef', $result['data']['etag']); - $this->assertEquals(array('tag1', 'tag2'), $result['data']['tags']); - $this->assertEquals('/', $result['data']['path']); - - \OC::$server->registerService('TagManager', function ($c) use ($oldTagManager) { - return $oldTagManager; - }); - } - - /** - * Test rename inside a folder that doesn't exist any more - */ - function testRenameInNonExistingFolder() { - $dir = '/unexist'; - $oldname = 'oldname'; - $newname = 'newname'; - - $this->viewMock->expects($this->at(0)) - ->method('file_exists') - ->with('/unexist/oldname') - ->will($this->returnValue(false)); - - $this->viewMock->expects($this->any()) - ->method('getFileInfo') - ->will($this->returnValue(array( - 'fileid' => 123, - 'type' => 'dir', - 'mimetype' => 'httpd/unix-directory', - 'size' => 18, - 'etag' => 'abcdef', - 'directory' => '/unexist', - 'name' => 'new_name', - ))); - - $result = $this->files->rename($dir, $oldname, $newname); - - $this->assertFalse($result['success']); - $this->assertEquals('sourcenotfound', $result['data']['code']); - } - - /** - * Test move to invalid name - */ - function testRenameToInvalidName() { - $dir = '/'; - $oldname = 'oldname'; - $newname = 'abc\\'; - - $result = $this->files->rename($dir, $oldname, $newname); - - $this->assertFalse($result['success']); - $this->assertEquals('File name contains at least one invalid character', $result['data']['message']); - $this->assertEquals('invalidname', $result['data']['code']); - } -} diff --git a/apps/files/tests/js/favoritesfilelistspec.js b/apps/files/tests/js/favoritesfilelistspec.js index 608ddaca18be..1c833d334e21 100644 --- a/apps/files/tests/js/favoritesfilelistspec.js +++ b/apps/files/tests/js/favoritesfilelistspec.js @@ -100,8 +100,7 @@ describe('OCA.Files.FavoritesFileList tests', function() { expect($tr.attr('data-mtime')).toEqual('11111000'); expect($tr.find('a.name').attr('href')).toEqual( OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Fsomedir&files=test.txt' + '/remote.php/webdav/somedir/test.txt' ); expect($tr.find('.nametext').text().trim()).toEqual('test.txt'); }); diff --git a/apps/files/tests/js/fileUploadSpec.js b/apps/files/tests/js/fileUploadSpec.js index a49a5d4e2e07..8a0d6b019523 100644 --- a/apps/files/tests/js/fileUploadSpec.js +++ b/apps/files/tests/js/fileUploadSpec.js @@ -19,6 +19,8 @@ * */ +/* global FileList */ + describe('OC.Upload tests', function() { var $dummyUploader; var testFile; diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index d29164c51366..a905a4d969d8 100644 --- a/apps/files/tests/js/fileactionsSpec.js +++ b/apps/files/tests/js/fileactionsSpec.js @@ -584,7 +584,7 @@ describe('OCA.Files.FileActions tests', function() { expect(busyStub.calledWith('testName.txt', true)).toEqual(true); expect(handleDownloadStub.calledOnce).toEqual(true); expect(handleDownloadStub.getCall(0).args[0]).toEqual( - OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=testName.txt' + OC.webroot + '/remote.php/webdav/subdir/testName.txt' ); busyStub.reset(); handleDownloadStub.yield(); diff --git a/apps/files/tests/js/fileactionsmenuSpec.js b/apps/files/tests/js/fileactionsmenuSpec.js index dee542458b62..747a746a6025 100644 --- a/apps/files/tests/js/fileactionsmenuSpec.js +++ b/apps/files/tests/js/fileactionsmenuSpec.js @@ -237,8 +237,8 @@ describe('OCA.Files.FileActionsMenu tests', function() { expect(redirectStub.calledOnce).toEqual(true); expect(redirectStub.getCall(0).args[0]).toContain( OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Fsubdir&files=testName.txt'); + '/remote.php/webdav/subdir/testName.txt' + ); redirectStub.restore(); }); it('takes the file\'s path into account when clicking download', function() { @@ -269,8 +269,7 @@ describe('OCA.Files.FileActionsMenu tests', function() { expect(redirectStub.calledOnce).toEqual(true); expect(redirectStub.getCall(0).args[0]).toContain( - OC.webroot + '/index.php/apps/files/ajax/download.php' + - '?dir=%2Fanotherpath%2Fthere&files=testName.txt' + OC.webroot + '/remote.php/webdav/anotherpath/there/testName.txt' ); redirectStub.restore(); }); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 05e6fcc6122b..9f7ad50bc608 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -20,8 +20,11 @@ */ describe('OCA.Files.FileList tests', function() { - var testFiles, alertStub, notificationStub, fileList, pageSizeStub; + var FileInfo = OC.Files.FileInfo; + var testFiles, testRoot, notificationStub, fileList, pageSizeStub; var bcResizeStub; + var filesClient; + var redirectStub; /** * Generate test file data @@ -38,21 +41,29 @@ describe('OCA.Files.FileList tests', function() { name += '0'; } name += i + '.txt'; - files.push({ + files.push(new FileInfo({ id: i, type: 'file', name: name, mimetype: 'text/plain', size: i * 2, etag: 'abc' - }); + })); } return files; } beforeEach(function() { - alertStub = sinon.stub(OC.dialogs, 'alert'); - notificationStub = sinon.stub(OC.Notification, 'show'); + filesClient = new OC.Files.Client({ + host: 'localhost', + port: 80, + // FIXME: uncomment after fixing the test OC.webroot + //root: OC.webroot + '/remote.php/webdav', + root: '/remote.php/webdav', + useHTTPS: false + }); + redirectStub = sinon.stub(OC, 'redirect'); + notificationStub = sinon.stub(OC.Notification, 'showTemporary'); // prevent resize algo to mess up breadcrumb order while // testing bcResizeStub = sinon.stub(OCA.Files.BreadCrumb.prototype, '_resize'); @@ -93,7 +104,17 @@ describe('OCA.Files.FileList tests', function() { '</div>' ); - testFiles = [{ + testRoot = new FileInfo({ + // root entry + id: 99, + type: 'dir', + name: '/subdir', + mimetype: 'httpd/unix-directory', + size: 1200000, + etag: 'a0b0c0d0', + permissions: OC.PERMISSION_ALL + }); + testFiles = [new FileInfo({ id: 1, type: 'file', name: 'One.txt', @@ -102,7 +123,7 @@ describe('OCA.Files.FileList tests', function() { size: 12, etag: 'abc', permissions: OC.PERMISSION_ALL - }, { + }), new FileInfo({ id: 2, type: 'file', name: 'Two.jpg', @@ -111,7 +132,7 @@ describe('OCA.Files.FileList tests', function() { size: 12049, etag: 'def', permissions: OC.PERMISSION_ALL - }, { + }), new FileInfo({ id: 3, type: 'file', name: 'Three.pdf', @@ -120,7 +141,7 @@ describe('OCA.Files.FileList tests', function() { size: 58009, etag: '123', permissions: OC.PERMISSION_ALL - }, { + }), new FileInfo({ id: 4, type: 'dir', name: 'somedir', @@ -129,9 +150,11 @@ describe('OCA.Files.FileList tests', function() { size: 250, etag: '456', permissions: OC.PERMISSION_ALL - }]; + })]; pageSizeStub = sinon.stub(OCA.Files.FileList.prototype, 'pageSize').returns(20); - fileList = new OCA.Files.FileList($('#app-content-files')); + fileList = new OCA.Files.FileList($('#app-content-files'), { + filesClient: filesClient + }); }); afterEach(function() { testFiles = undefined; @@ -141,9 +164,9 @@ describe('OCA.Files.FileList tests', function() { fileList = undefined; notificationStub.restore(); - alertStub.restore(); bcResizeStub.restore(); pageSizeStub.restore(); + redirectStub.restore(); }); describe('Getters', function() { it('Returns the current directory', function() { @@ -166,15 +189,14 @@ describe('OCA.Files.FileList tests', function() { clock.restore(); }); it('generates file element with correct attributes when calling add() with file data', function() { - var fileData = { + var fileData = new FileInfo({ id: 18, - type: 'file', name: 'testName.txt', mimetype: 'text/plain', - size: '1234', + size: 1234, etag: 'a01234c', - mtime: '123456' - }; + mtime: 123456 + }); var $tr = fileList.add(fileData); expect($tr).toBeDefined(); @@ -188,7 +210,7 @@ describe('OCA.Files.FileList tests', function() { expect($tr.attr('data-mime')).toEqual('text/plain'); expect($tr.attr('data-mtime')).toEqual('123456'); expect($tr.find('a.name').attr('href')) - .toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=testName.txt'); + .toEqual(OC.webroot + '/remote.php/webdav/subdir/testName.txt'); expect($tr.find('.nametext').text().trim()).toEqual('testName.txt'); expect($tr.find('.filesize').text()).toEqual('1 kB'); @@ -196,15 +218,14 @@ describe('OCA.Files.FileList tests', function() { expect(fileList.findFileEl('testName.txt')[0]).toEqual($tr[0]); }); it('generates dir element with correct attributes when calling add() with dir data', function() { - var fileData = { + var fileData = new FileInfo({ id: 19, - type: 'dir', name: 'testFolder', mimetype: 'httpd/unix-directory', - size: '1234', + size: 1234, etag: 'a01234c', - mtime: '123456' - }; + mtime: 123456 + }); var $tr = fileList.add(fileData); expect($tr).toBeDefined(); @@ -297,7 +318,6 @@ describe('OCA.Files.FileList tests', function() { expect($tr.index()).toEqual(4); }); it('inserts files in a sorted manner when insert option is enabled', function() { - var $tr; for (var i = 0; i < testFiles.length; i++) { fileList.add(testFiles[i]); } @@ -423,28 +443,31 @@ describe('OCA.Files.FileList tests', function() { }); }); describe('Deleting files', function() { + var deferredDelete; + var deleteStub; + + beforeEach(function() { + deferredDelete = $.Deferred(); + deleteStub = sinon.stub(filesClient, 'remove').returns(deferredDelete.promise()); + }); + afterEach(function() { + deleteStub.restore(); + }); + function doDelete() { - var request, query; // note: normally called from FileActions fileList.do_delete(['One.txt', 'Two.jpg']); - expect(fakeServer.requests.length).toEqual(1); - request = fakeServer.requests[0]; - expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/delete.php'); - - query = fakeServer.requests[0].requestBody; - expect(OC.parseQueryString(query)).toEqual({'dir': '/subdir', files: '["One.txt","Two.jpg"]'}); + expect(deleteStub.calledTwice).toEqual(true); + expect(deleteStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); + expect(deleteStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg'); } it('calls delete.php, removes the deleted entries and updates summary', function() { var $summary; fileList.setFiles(testFiles); doDelete(); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({status: 'success'}) - ); + deferredDelete.resolve(200); expect(fileList.findFileEl('One.txt').length).toEqual(0); expect(fileList.findFileEl('Two.jpg').length).toEqual(0); @@ -482,11 +505,7 @@ describe('OCA.Files.FileList tests', function() { fileList.setFiles([testFiles[0], testFiles[1]]); doDelete(); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({status: 'success'}) - ); + deferredDelete.resolve(200); expect(fileList.$fileList.find('tr').length).toEqual(0); @@ -501,21 +520,41 @@ describe('OCA.Files.FileList tests', function() { fileList.setFiles(testFiles); doDelete(); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({status: 'error', data: {message: 'WOOT'}}) - ); + deferredDelete.reject(403); // files are still in the list expect(fileList.findFileEl('One.txt').length).toEqual(1); expect(fileList.findFileEl('Two.jpg').length).toEqual(1); expect(fileList.$fileList.find('tr').length).toEqual(4); - expect(notificationStub.calledOnce).toEqual(true); + expect(notificationStub.calledTwice).toEqual(true); + }); + it('remove file from list if delete call returned 404 not found', function() { + fileList.setFiles(testFiles); + doDelete(); + + deferredDelete.reject(404); + + // files are still in the list + expect(fileList.findFileEl('One.txt').length).toEqual(0); + expect(fileList.findFileEl('Two.jpg').length).toEqual(0); + expect(fileList.$fileList.find('tr').length).toEqual(2); + + expect(notificationStub.notCalled).toEqual(true); }); }); describe('Renaming files', function() { + var deferredRename; + var renameStub; + + beforeEach(function() { + deferredRename = $.Deferred(); + renameStub = sinon.stub(filesClient, 'move').returns(deferredRename.promise()); + }); + afterEach(function() { + renameStub.restore(); + }); + function doCancelRename() { var $input; for (var i = 0; i < testFiles.length; i++) { @@ -530,10 +569,10 @@ describe('OCA.Files.FileList tests', function() { // trigger submit because triggering blur doesn't work in all browsers $input.closest('form').trigger('submit'); - expect(fakeServer.requests.length).toEqual(0); + expect(renameStub.notCalled).toEqual(true); } function doRename() { - var $input, request; + var $input; for (var i = 0; i < testFiles.length; i++) { var file = testFiles[i]; @@ -548,83 +587,61 @@ describe('OCA.Files.FileList tests', function() { // trigger submit because triggering blur doesn't work in all browsers $input.closest('form').trigger('submit'); - expect(fakeServer.requests.length).toEqual(1); - request = fakeServer.requests[0]; - expect(request.url.substr(0, request.url.indexOf('?'))).toEqual(OC.webroot + '/index.php/apps/files/ajax/rename.php'); - expect(OC.parseQueryString(request.url)).toEqual({'dir': '/some/subdir', newname: 'Tu_after_three.txt', file: 'One.txt'}); + expect(renameStub.calledOnce).toEqual(true); + expect(renameStub.getCall(0).args[0]).toEqual('/some/subdir/One.txt'); + expect(renameStub.getCall(0).args[1]).toEqual('/some/subdir/Tu_after_three.txt'); } it('Inserts renamed file entry at correct position if rename ajax call suceeded', function() { doRename(); - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'success', - data: { - name: 'Tu_after_three.txt', - type: 'file' - } - })); + deferredRename.resolve(201); // element stays renamed expect(fileList.findFileEl('One.txt').length).toEqual(0); expect(fileList.findFileEl('Tu_after_three.txt').length).toEqual(1); - expect(fileList.findFileEl('Tu_after_three.txt').index()).toEqual(2); // after Two.txt + expect(fileList.findFileEl('Tu_after_three.txt').index()).toEqual(2); // after Two.jpg - expect(alertStub.notCalled).toEqual(true); + expect(notificationStub.notCalled).toEqual(true); }); it('Reverts file entry if rename ajax call failed', function() { doRename(); - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'error', - data: { - message: 'Something went wrong' - } - })); + deferredRename.reject(403); // element was reverted expect(fileList.findFileEl('One.txt').length).toEqual(1); expect(fileList.findFileEl('One.txt').index()).toEqual(1); // after somedir expect(fileList.findFileEl('Tu_after_three.txt').length).toEqual(0); - expect(alertStub.calledOnce).toEqual(true); + expect(notificationStub.calledOnce).toEqual(true); }); it('Correctly updates file link after rename', function() { var $tr; doRename(); - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'success', - data: { - name: 'Tu_after_three.txt' - } - })); + deferredRename.resolve(201); $tr = fileList.findFileEl('Tu_after_three.txt'); - expect($tr.find('a.name').attr('href')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=Tu_after_three.txt'); + expect($tr.find('a.name').attr('href')) + .toEqual(OC.webroot + '/remote.php/webdav/some/subdir/Tu_after_three.txt'); }); it('Triggers "fileActionsReady" event after rename', function() { var handler = sinon.stub(); fileList.$fileList.on('fileActionsReady', handler); doRename(); expect(handler.notCalled).toEqual(true); - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'success', - data: { - name: 'Tu_after_three.txt' - } - })); + + deferredRename.resolve(201); + expect(handler.calledOnce).toEqual(true); expect(fileList.$fileList.find('.test').length).toEqual(0); }); it('Leaves the summary alone when reinserting renamed element', function() { var $summary = $('#filestable .summary'); doRename(); - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'success', - data: { - name: 'Tu_after_three.txt' - } - })); + + deferredRename.resolve(201); + expect($summary.find('.info').text()).toEqual('1 folder and 3 files'); }); it('Leaves the summary alone when cancel renaming', function() { @@ -668,7 +685,7 @@ describe('OCA.Files.FileList tests', function() { // trigger submit does not send server request $input.closest('form').trigger('submit'); - expect(fakeServer.requests.length).toEqual(0); + expect(renameStub.notCalled).toEqual(true); // simulate escape key $input.trigger(new $.Event('keyup', {keyCode: 27})); @@ -694,12 +711,7 @@ describe('OCA.Files.FileList tests', function() { expect(OC.TestUtil.getImageUrl(fileList.findFileEl('Tu_after_three.txt').find('.thumbnail'))) .toEqual(OC.imagePath('core', 'loading.gif')); - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'error', - data: { - message: 'Something went wrong' - } - })); + deferredRename.reject(409); expect(fileList.findFileEl('One.txt').length).toEqual(1); expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail'))) @@ -707,25 +719,27 @@ describe('OCA.Files.FileList tests', function() { }); }); describe('Moving files', function() { + var deferredMove; + var moveStub; + beforeEach(function() { + deferredMove = $.Deferred(); + moveStub = sinon.stub(filesClient, 'move').returns(deferredMove.promise()); + fileList.setFiles(testFiles); }); + afterEach(function() { + moveStub.restore(); + }); + it('Moves single file to target folder', function() { - var request; fileList.move('One.txt', '/somedir'); - expect(fakeServer.requests.length).toEqual(1); - request = fakeServer.requests[0]; - expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); - expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'One.txt', target: '/somedir'}); + expect(moveStub.calledOnce).toEqual(true); + expect(moveStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); + expect(moveStub.getCall(0).args[1]).toEqual('/somedir/One.txt'); - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'success', - data: { - name: 'One.txt', - type: 'file' - } - })); + deferredMove.resolve(201); expect(fileList.findFileEl('One.txt').length).toEqual(0); @@ -736,39 +750,28 @@ describe('OCA.Files.FileList tests', function() { expect(notificationStub.notCalled).toEqual(true); }); it('Moves list of files to target folder', function() { - var request; + var deferredMove1 = $.Deferred(); + var deferredMove2 = $.Deferred(); + moveStub.onCall(0).returns(deferredMove1.promise()); + moveStub.onCall(1).returns(deferredMove2.promise()); + fileList.move(['One.txt', 'Two.jpg'], '/somedir'); - expect(fakeServer.requests.length).toEqual(2); - request = fakeServer.requests[0]; - expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); - expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'One.txt', target: '/somedir'}); + expect(moveStub.calledTwice).toEqual(true); + expect(moveStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); + expect(moveStub.getCall(0).args[1]).toEqual('/somedir/One.txt'); + expect(moveStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg'); + expect(moveStub.getCall(1).args[1]).toEqual('/somedir/Two.jpg'); - request = fakeServer.requests[1]; - expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); - expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'Two.jpg', target: '/somedir'}); - - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'success', - data: { - name: 'One.txt', - type: 'file' - } - })); + deferredMove1.resolve(201); expect(fileList.findFileEl('One.txt').length).toEqual(0); - // folder size has increased + // folder size has increased during move expect(fileList.findFileEl('somedir').data('size')).toEqual(262); expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B'); - fakeServer.requests[1].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'success', - data: { - name: 'Two.jpg', - type: 'file' - } - })); + deferredMove2.resolve(201); expect(fileList.findFileEl('Two.jpg').length).toEqual(0); @@ -779,47 +782,31 @@ describe('OCA.Files.FileList tests', function() { expect(notificationStub.notCalled).toEqual(true); }); it('Shows notification if a file could not be moved', function() { - var request; fileList.move('One.txt', '/somedir'); - expect(fakeServer.requests.length).toEqual(1); - request = fakeServer.requests[0]; - expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); - expect(OC.parseQueryString(request.requestBody)).toEqual({dir: '/subdir', file: 'One.txt', target: '/somedir'}); + expect(moveStub.calledOnce).toEqual(true); - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'error', - data: { - message: 'Error while moving file' - } - })); + deferredMove.reject(409); expect(fileList.findFileEl('One.txt').length).toEqual(1); expect(notificationStub.calledOnce).toEqual(true); - expect(notificationStub.getCall(0).args[0]).toEqual('Error while moving file'); + expect(notificationStub.getCall(0).args[0]).toEqual('Could not move "One.txt"'); }); it('Restores thumbnail if a file could not be moved', function() { - var request; fileList.move('One.txt', '/somedir'); expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail'))) .toEqual(OC.imagePath('core', 'loading.gif')); - expect(fakeServer.requests.length).toEqual(1); - request = fakeServer.requests[0]; + expect(moveStub.calledOnce).toEqual(true); - fakeServer.requests[0].respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ - status: 'error', - data: { - message: 'Error while moving file' - } - })); + deferredMove.reject(409); expect(fileList.findFileEl('One.txt').length).toEqual(1); expect(notificationStub.calledOnce).toEqual(true); - expect(notificationStub.getCall(0).args[0]).toEqual('Error while moving file'); + expect(notificationStub.getCall(0).args[0]).toEqual('Could not move "One.txt"'); expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail'))) .toEqual(OC.imagePath('core', 'filetypes/text.svg')); @@ -878,7 +865,7 @@ describe('OCA.Files.FileList tests', function() { name: 'testFile.txt', directory: '/current dir' }; - var $tr = fileList.add(fileData); + fileList.add(fileData); expect(fileList.findFileEl('testFile.txt').length).toEqual(1); }); it('triggers "fileActionsReady" event after update', function() { @@ -1143,69 +1130,85 @@ describe('OCA.Files.FileList tests', function() { afterEach(function() { previewLoadStub.restore(); }); - it('renders default icon for file when none provided and no preview is available', function() { + it('renders default file icon when none provided and no mime type is set', function() { var fileData = { - type: 'file', name: 'testFile.txt' }; var $tr = fileList.add(fileData); var $imgDiv = $tr.find('td.filename .thumbnail'); expect(OC.TestUtil.getImageUrl($imgDiv)).toEqual(OC.webroot + '/core/img/filetypes/file.svg'); - expect(previewLoadStub.notCalled).toEqual(true); + // tries to load preview + expect(previewLoadStub.calledOnce).toEqual(true); }); - it('renders default icon for dir when none provided and no preview is available', function() { + it('renders default icon for folder when none provided', function() { var fileData = { - type: 'dir', - name: 'test dir' + name: 'test dir', + mimetype: 'httpd/unix-directory' }; + var $tr = fileList.add(fileData); var $imgDiv = $tr.find('td.filename .thumbnail'); expect(OC.TestUtil.getImageUrl($imgDiv)).toEqual(OC.webroot + '/core/img/filetypes/folder.svg'); + // no preview since it's a directory expect(previewLoadStub.notCalled).toEqual(true); }); it('renders provided icon for file when provided', function() { - var fileData = { + var fileData = new FileInfo({ type: 'file', name: 'test file', icon: OC.webroot + '/core/img/filetypes/application-pdf.svg', mimetype: 'application/pdf' - }; + }); var $tr = fileList.add(fileData); var $imgDiv = $tr.find('td.filename .thumbnail'); expect(OC.TestUtil.getImageUrl($imgDiv)).toEqual(OC.webroot + '/core/img/filetypes/application-pdf.svg'); + // try loading preview + expect(previewLoadStub.calledOnce).toEqual(true); + }); + it('renders provided icon for file when provided', function() { + var fileData = new FileInfo({ + name: 'somefile.pdf', + icon: OC.webroot + '/core/img/filetypes/application-pdf.svg' + }); + + var $tr = fileList.add(fileData); + var $imgDiv = $tr.find('td.filename .thumbnail'); + expect(OC.TestUtil.getImageUrl($imgDiv)).toEqual(OC.webroot + '/core/img/filetypes/application-pdf.svg'); + // try loading preview + expect(previewLoadStub.calledOnce).toEqual(true); + }); + it('renders provided icon for folder when provided', function() { + var fileData = new FileInfo({ + name: 'some folder', + mimetype: 'httpd/unix-directory', + icon: OC.webroot + '/core/img/filetypes/folder-alt.svg' + }); + + var $tr = fileList.add(fileData); + var $imgDiv = $tr.find('td.filename .thumbnail'); + expect(OC.TestUtil.getImageUrl($imgDiv)).toEqual(OC.webroot + '/core/img/filetypes/folder-alt.svg'); + // do not load preview for folders expect(previewLoadStub.notCalled).toEqual(true); }); - it('renders preview when no icon was provided and preview is available', function() { + it('renders preview when no icon was provided', function() { var fileData = { type: 'file', - name: 'test file', - isPreviewAvailable: true + name: 'test file' }; var $tr = fileList.add(fileData); var $td = $tr.find('td.filename'); - expect(OC.TestUtil.getImageUrl($td.find('.thumbnail'))).toEqual(OC.webroot + '/core/img/filetypes/file.svg'); + expect(OC.TestUtil.getImageUrl($td.find('.thumbnail'))) + .toEqual(OC.webroot + '/core/img/filetypes/file.svg'); expect(previewLoadStub.calledOnce).toEqual(true); // third argument is callback previewLoadStub.getCall(0).args[0].callback(OC.webroot + '/somepath.png'); expect(OC.TestUtil.getImageUrl($td.find('.thumbnail'))).toEqual(OC.webroot + '/somepath.png'); }); - it('renders default file type icon when no icon was provided and no preview is available', function() { - var fileData = { - type: 'file', - name: 'test file', - isPreviewAvailable: false - }; - var $tr = fileList.add(fileData); - var $imgDiv = $tr.find('td.filename .thumbnail'); - expect(OC.TestUtil.getImageUrl($imgDiv)).toEqual(OC.webroot + '/core/img/filetypes/file.svg'); - expect(previewLoadStub.notCalled).toEqual(true); - }); it('does not render preview for directories', function() { var fileData = { type: 'dir', mimetype: 'httpd/unix-directory', - name: 'test dir', - isPreviewAvailable: true + name: 'test dir' }; var $tr = fileList.add(fileData); var $td = $tr.find('td.filename'); @@ -1217,7 +1220,6 @@ describe('OCA.Files.FileList tests', function() { type: 'dir', mimetype: 'httpd/unix-directory', name: 'test dir', - isPreviewAvailable: true, mountType: 'external-root' }; var $tr = fileList.add(fileData); @@ -1230,7 +1232,6 @@ describe('OCA.Files.FileList tests', function() { type: 'dir', mimetype: 'httpd/unix-directory', name: 'test dir', - isPreviewAvailable: true, mountType: 'external' }; var $tr = fileList.add(fileData); @@ -1278,75 +1279,47 @@ describe('OCA.Files.FileList tests', function() { }); }); describe('loading file list', function() { + var deferredList; + var getFolderContentsStub; + beforeEach(function() { - var data = { - status: 'success', - data: { - files: testFiles, - permissions: 31 - } - }; - fakeServer.respondWith(/\/index\.php\/apps\/files\/ajax\/list.php\?dir=%2F(subdir|anothersubdir)/, [ - 200, { - "Content-Type": "application/json" - }, - JSON.stringify(data) - ]); + deferredList = $.Deferred(); + getFolderContentsStub = sinon.stub(filesClient, 'getFolderContents').returns(deferredList.promise()); + }); + afterEach(function() { + getFolderContentsStub.restore(); }); it('fetches file list from server and renders it when reload() is called', function() { fileList.reload(); - expect(fakeServer.requests.length).toEqual(1); - var url = fakeServer.requests[0].url; - var query = url.substr(url.indexOf('?') + 1); - expect(OC.parseQueryString(query)).toEqual({'dir': '/subdir', sort: 'name', sortdirection: 'asc'}); - fakeServer.respond(); + expect(getFolderContentsStub.calledOnce).toEqual(true); + expect(getFolderContentsStub.calledWith('/subdir')).toEqual(true); + deferredList.resolve(200, [testRoot].concat(testFiles)); expect($('#fileList tr').length).toEqual(4); expect(fileList.findFileEl('One.txt').length).toEqual(1); }); it('switches dir and fetches file list when calling changeDirectory()', function() { fileList.changeDirectory('/anothersubdir'); expect(fileList.getCurrentDirectory()).toEqual('/anothersubdir'); - expect(fakeServer.requests.length).toEqual(1); - var url = fakeServer.requests[0].url; - var query = url.substr(url.indexOf('?') + 1); - expect(OC.parseQueryString(query)).toEqual({'dir': '/anothersubdir', sort: 'name', sortdirection: 'asc'}); - fakeServer.respond(); + expect(getFolderContentsStub.calledOnce).toEqual(true); + expect(getFolderContentsStub.calledWith('/anothersubdir')).toEqual(true); }); it('converts backslashes to slashes when calling changeDirectory()', function() { fileList.changeDirectory('/another\\subdir'); expect(fileList.getCurrentDirectory()).toEqual('/another/subdir'); }); it('switches to root dir when current directory does not exist', function() { - fakeServer.respondWith(/\/index\.php\/apps\/files\/ajax\/list.php\?dir=%2funexist/, [ - 404, { - "Content-Type": "application/json" - }, - '' - ]); fileList.changeDirectory('/unexist'); - fakeServer.respond(); + deferredList.reject(404); expect(fileList.getCurrentDirectory()).toEqual('/'); }); it('switches to root dir when current directory is forbidden', function() { - fakeServer.respondWith(/\/index\.php\/apps\/files\/ajax\/list.php\?dir=%2funexist/, [ - 403, { - "Content-Type": "application/json" - }, - '' - ]); fileList.changeDirectory('/unexist'); - fakeServer.respond(); + deferredList.reject(403); expect(fileList.getCurrentDirectory()).toEqual('/'); }); it('switches to root dir when current directory is unavailable', function() { - fakeServer.respondWith(/\/index\.php\/apps\/files\/ajax\/list.php\?dir=%2funexist/, [ - 500, { - "Content-Type": "application/json" - }, - '' - ]); fileList.changeDirectory('/unexist'); - fakeServer.respond(); + deferredList.reject(500); expect(fileList.getCurrentDirectory()).toEqual('/'); }); it('shows mask before loading file list then hides it at the end', function() { @@ -1355,7 +1328,7 @@ describe('OCA.Files.FileList tests', function() { fileList.changeDirectory('/anothersubdir'); expect(showMaskStub.calledOnce).toEqual(true); expect(hideMaskStub.calledOnce).toEqual(false); - fakeServer.respond(); + deferredList.resolve(200, [testRoot].concat(testFiles)); expect(showMaskStub.calledOnce).toEqual(true); expect(hideMaskStub.calledOnce).toEqual(true); showMaskStub.restore(); @@ -1365,6 +1338,7 @@ describe('OCA.Files.FileList tests', function() { var handler = sinon.stub(); $('#app-content-files').on('changeDirectory', handler); fileList.changeDirectory('/somedir'); + deferredList.resolve(200, [testRoot].concat(testFiles)); expect(handler.calledOnce).toEqual(true); expect(handler.getCall(0).args[0].dir).toEqual('/somedir'); }); @@ -1375,31 +1349,27 @@ describe('OCA.Files.FileList tests', function() { it('refreshes breadcrumb after update', function() { var setDirSpy = sinon.spy(fileList.breadcrumb, 'setDirectory'); fileList.changeDirectory('/anothersubdir'); - fakeServer.respond(); + deferredList.resolve(200, [testRoot].concat(testFiles)); expect(fileList.breadcrumb.setDirectory.calledOnce).toEqual(true); expect(fileList.breadcrumb.setDirectory.calledWith('/anothersubdir')).toEqual(true); setDirSpy.restore(); + getFolderContentsStub.restore(); }); }); describe('breadcrumb events', function() { + var deferredList; + var getFolderContentsStub; + beforeEach(function() { - var data = { - status: 'success', - data: { - files: testFiles, - permissions: 31 - } - }; - fakeServer.respondWith(/\/index\.php\/apps\/files\/ajax\/list.php\?dir=%2Fsubdir/, [ - 200, { - "Content-Type": "application/json" - }, - JSON.stringify(data) - ]); + deferredList = $.Deferred(); + getFolderContentsStub = sinon.stub(filesClient, 'getFolderContents').returns(deferredList.promise()); + }); + afterEach(function() { + getFolderContentsStub.restore(); }); it('clicking on root breadcrumb changes directory to root', function() { fileList.changeDirectory('/subdir/two/three with space/four/five'); - fakeServer.respond(); + deferredList.resolve(200, [testRoot].concat(testFiles)); var changeDirStub = sinon.stub(fileList, 'changeDirectory'); fileList.breadcrumb.$el.find('.crumb:eq(0)').trigger({type: 'click', which: 1}); @@ -1409,7 +1379,7 @@ describe('OCA.Files.FileList tests', function() { }); it('clicking on breadcrumb changes directory', function() { fileList.changeDirectory('/subdir/two/three with space/four/five'); - fakeServer.respond(); + deferredList.resolve(200, [testRoot].concat(testFiles)); var changeDirStub = sinon.stub(fileList, 'changeDirectory'); fileList.breadcrumb.$el.find('.crumb:eq(3)').trigger({type: 'click', which: 1}); @@ -1418,9 +1388,10 @@ describe('OCA.Files.FileList tests', function() { changeDirStub.restore(); }); it('dropping files on breadcrumb calls move operation', function() { - var request, query, testDir = '/subdir/two/three with space/four/five'; + var testDir = '/subdir/two/three with space/four/five'; + var moveStub = sinon.stub(filesClient, 'move').returns($.Deferred().promise()); fileList.changeDirectory(testDir); - fakeServer.respond(); + deferredList.resolve(200, [testRoot].concat(testFiles)); var $crumb = fileList.breadcrumb.$el.find('.crumb:eq(3)'); // no idea what this is but is required by the handler var ui = { @@ -1436,33 +1407,18 @@ describe('OCA.Files.FileList tests', function() { // simulate drop event fileList._onDropOnBreadCrumb(new $.Event('drop', {target: $crumb}), ui); - // will trigger two calls to move.php (first one was previous list.php) - expect(fakeServer.requests.length).toEqual(3); - - request = fakeServer.requests[1]; - expect(request.method).toEqual('POST'); - expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); - query = OC.parseQueryString(request.requestBody); - expect(query).toEqual({ - target: '/subdir/two/three with space', - dir: testDir, - file: 'One.txt' - }); - - request = fakeServer.requests[2]; - expect(request.method).toEqual('POST'); - expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/move.php'); - query = OC.parseQueryString(request.requestBody); - expect(query).toEqual({ - target: '/subdir/two/three with space', - dir: testDir, - file: 'Two.jpg' - }); + expect(moveStub.callCount).toEqual(2); + expect(moveStub.getCall(0).args[0]).toEqual(testDir + '/One.txt'); + expect(moveStub.getCall(0).args[1]).toEqual('/subdir/two/three with space/One.txt'); + expect(moveStub.getCall(1).args[0]).toEqual(testDir + '/Two.jpg'); + expect(moveStub.getCall(1).args[1]).toEqual('/subdir/two/three with space/Two.jpg'); + moveStub.restore(); }); it('dropping files on same dir breadcrumb does nothing', function() { var testDir = '/subdir/two/three with space/four/five'; + var moveStub = sinon.stub(filesClient, 'move').returns($.Deferred().promise()); fileList.changeDirectory(testDir); - fakeServer.respond(); + deferredList.resolve(200, [testRoot].concat(testFiles)); var $crumb = fileList.breadcrumb.$el.find('.crumb:last'); // no idea what this is but is required by the handler var ui = { @@ -1479,21 +1435,26 @@ describe('OCA.Files.FileList tests', function() { fileList._onDropOnBreadCrumb(new $.Event('drop', {target: $crumb}), ui); // no extra server request - expect(fakeServer.requests.length).toEqual(1); + expect(moveStub.notCalled).toEqual(true); }); }); describe('Download Url', function() { it('returns correct download URL for single files', function() { - expect(fileList.getDownloadUrl('some file.txt')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=some%20file.txt'); - expect(fileList.getDownloadUrl('some file.txt', '/anotherpath/abc')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fanotherpath%2Fabc&files=some%20file.txt'); + expect(fileList.getDownloadUrl('some file.txt')) + .toEqual(OC.webroot + '/remote.php/webdav/subdir/some%20file.txt'); + expect(fileList.getDownloadUrl('some file.txt', '/anotherpath/abc')) + .toEqual(OC.webroot + '/remote.php/webdav/anotherpath/abc/some%20file.txt'); $('#dir').val('/'); - expect(fileList.getDownloadUrl('some file.txt')).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=some%20file.txt'); + expect(fileList.getDownloadUrl('some file.txt')) + .toEqual(OC.webroot + '/remote.php/webdav/some%20file.txt'); }); it('returns correct download URL for multiple files', function() { - expect(fileList.getDownloadUrl(['a b c.txt', 'd e f.txt'])).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=%5B%22a%20b%20c.txt%22%2C%22d%20e%20f.txt%22%5D'); + expect(fileList.getDownloadUrl(['a b c.txt', 'd e f.txt'])) + .toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=%5B%22a%20b%20c.txt%22%2C%22d%20e%20f.txt%22%5D'); }); it('returns the correct ajax URL', function() { - expect(fileList.getAjaxUrl('test', {a:1, b:'x y'})).toEqual(OC.webroot + '/index.php/apps/files/ajax/test.php?a=1&b=x%20y'); + expect(fileList.getAjaxUrl('test', {a:1, b:'x y'})) + .toEqual(OC.webroot + '/index.php/apps/files/ajax/test.php?a=1&b=x%20y'); }); }); describe('File selection', function() { @@ -1672,24 +1633,17 @@ describe('OCA.Files.FileList tests', function() { }); it('Selection is cleared when switching dirs', function() { $('.select-all').click(); - var data = { - status: 'success', - data: { - files: testFiles, - permissions: 31 - } - }; - fakeServer.respondWith(/\/index\.php\/apps\/files\/ajax\/list.php/, [ - 200, { - "Content-Type": "application/json" - }, - JSON.stringify(data) - ] - ); + var deferredList = $.Deferred(); + var getFolderContentsStub = sinon.stub(filesClient, 'getFolderContents').returns(deferredList.promise()); + fileList.changeDirectory('/'); - fakeServer.respond(); + + deferredList.resolve(200, [testRoot].concat(testFiles)); + expect($('.select-all').prop('checked')).toEqual(false); expect(_.pluck(fileList.getSelectedFiles(), 'name')).toEqual([]); + + getFolderContentsStub.restore(); }); it('getSelectedFiles returns the selected files even when they are on the next page', function() { var selectedFiles; @@ -1796,6 +1750,12 @@ describe('OCA.Files.FileList tests', function() { etag: '456', permissions: OC.PERMISSION_ALL }); + expect(files[0].id).toEqual(1); + expect(files[0].name).toEqual('One.txt'); + expect(files[1].id).toEqual(3); + expect(files[1].name).toEqual('Three.pdf'); + expect(files[2].id).toEqual(4); + expect(files[2].name).toEqual('somedir'); }); it('Removing a file removes it from the selection', function() { fileList.remove('Three.pdf'); @@ -1824,7 +1784,6 @@ describe('OCA.Files.FileList tests', function() { }); describe('Download', function() { it('Opens download URL when clicking "Download"', function() { - var redirectStub = sinon.stub(OC, 'redirect'); $('.selectedActions .download').click(); expect(redirectStub.calledOnce).toEqual(true); expect(redirectStub.getCall(0).args[0]).toContain(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=%5B%22One.txt%22%2C%22Three.pdf%22%2C%22somedir%22%5D'); @@ -1833,54 +1792,53 @@ describe('OCA.Files.FileList tests', function() { it('Downloads root folder when all selected in root folder', function() { $('#dir').val('/'); $('.select-all').click(); - var redirectStub = sinon.stub(OC, 'redirect'); $('.selectedActions .download').click(); expect(redirectStub.calledOnce).toEqual(true); expect(redirectStub.getCall(0).args[0]).toContain(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files='); - redirectStub.restore(); }); it('Downloads parent folder when all selected in subfolder', function() { $('.select-all').click(); - var redirectStub = sinon.stub(OC, 'redirect'); $('.selectedActions .download').click(); expect(redirectStub.calledOnce).toEqual(true); expect(redirectStub.getCall(0).args[0]).toContain(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=subdir'); - redirectStub.restore(); }); }); describe('Delete', function() { + var deleteStub, deferredDelete; + beforeEach(function() { + deferredDelete = $.Deferred(); + deleteStub = sinon.stub(filesClient, 'remove').returns(deferredDelete.promise()); + }); + afterEach(function() { + deleteStub.restore(); + }); it('Deletes selected files when "Delete" clicked', function() { - var request; $('.selectedActions .delete-selected').click(); - expect(fakeServer.requests.length).toEqual(1); - request = fakeServer.requests[0]; - expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/delete.php'); - expect(OC.parseQueryString(request.requestBody)) - .toEqual({'dir': '/subdir', files: '["One.txt","Three.pdf","somedir"]'}); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({status: 'success'}) - ); + + expect(deleteStub.callCount).toEqual(3); + expect(deleteStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); + expect(deleteStub.getCall(1).args[0]).toEqual('/subdir/Three.pdf'); + expect(deleteStub.getCall(2).args[0]).toEqual('/subdir/somedir'); + + deferredDelete.resolve(204); + expect(fileList.findFileEl('One.txt').length).toEqual(0); expect(fileList.findFileEl('Three.pdf').length).toEqual(0); expect(fileList.findFileEl('somedir').length).toEqual(0); expect(fileList.findFileEl('Two.jpg').length).toEqual(1); }); it('Deletes all files when all selected when "Delete" clicked', function() { - var request; $('.select-all').click(); $('.selectedActions .delete-selected').click(); - expect(fakeServer.requests.length).toEqual(1); - request = fakeServer.requests[0]; - expect(request.url).toEqual(OC.webroot + '/index.php/apps/files/ajax/delete.php'); - expect(OC.parseQueryString(request.requestBody)) - .toEqual({'dir': '/subdir', allfiles: 'true'}); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({status: 'success'}) - ); + + expect(deleteStub.callCount).toEqual(4); + expect(deleteStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); + expect(deleteStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg'); + expect(deleteStub.getCall(2).args[0]).toEqual('/subdir/Three.pdf'); + expect(deleteStub.getCall(3).args[0]).toEqual('/subdir/somedir'); + + deferredDelete.resolve(204); + expect(fileList.isEmpty).toEqual(true); }); }); @@ -2118,30 +2076,6 @@ describe('OCA.Files.FileList tests', function() { }); }); describe('Sorting files', function() { - it('Sorts by name by default', function() { - fileList.reload(); - expect(fakeServer.requests.length).toEqual(1); - var url = fakeServer.requests[0].url; - var query = OC.parseQueryString(url.substr(url.indexOf('?') + 1)); - expect(query.sort).toEqual('name'); - expect(query.sortdirection).toEqual('asc'); - }); - it('Reloads file list with a different sort when clicking on column header of unsorted column', function() { - fileList.$el.find('.column-size .columntitle').click(); - expect(fakeServer.requests.length).toEqual(1); - var url = fakeServer.requests[0].url; - var query = OC.parseQueryString(url.substr(url.indexOf('?') + 1)); - expect(query.sort).toEqual('size'); - expect(query.sortdirection).toEqual('desc'); - }); - it('Toggles sort direction when clicking on already sorted column', function() { - fileList.$el.find('.column-name .columntitle').click(); - expect(fakeServer.requests.length).toEqual(1); - var url = fakeServer.requests[0].url; - var query = OC.parseQueryString(url.substr(url.indexOf('?') + 1)); - expect(query.sort).toEqual('name'); - expect(query.sortdirection).toEqual('desc'); - }); it('Toggles the sort indicator when clicking on a column header', function() { var ASC_CLASS = fileList.SORT_INDICATOR_ASC_CLASS; var DESC_CLASS = fileList.SORT_INDICATOR_DESC_CLASS; @@ -2191,28 +2125,15 @@ describe('OCA.Files.FileList tests', function() { it('Uses correct sort comparator when inserting files', function() { testFiles.sort(OCA.Files.FileList.Comparators.size); testFiles.reverse(); //default is descending - // this will make it reload the testFiles with the correct sorting + fileList.setFiles(testFiles); fileList.$el.find('.column-size .columntitle').click(); - expect(fakeServer.requests.length).toEqual(1); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - data: { - files: testFiles, - permissions: 31 - } - }) - ); - var newFileData = { + var newFileData = new FileInfo({ id: 999, - type: 'file', name: 'new file.txt', mimetype: 'text/plain', size: 40001, etag: '999' - }; + }); fileList.add(newFileData); expect(fileList.findFileEl('Three.pdf').index()).toEqual(0); expect(fileList.findFileEl('new file.txt').index()).toEqual(1); @@ -2224,41 +2145,18 @@ describe('OCA.Files.FileList tests', function() { }); it('Uses correct reversed sort comparator when inserting files', function() { testFiles.sort(OCA.Files.FileList.Comparators.size); - // this will make it reload the testFiles with the correct sorting + fileList.setFiles(testFiles); fileList.$el.find('.column-size .columntitle').click(); - expect(fakeServer.requests.length).toEqual(1); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - data: { - files: testFiles, - permissions: 31 - } - }) - ); + // reverse sort fileList.$el.find('.column-size .columntitle').click(); - fakeServer.requests[1].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - data: { - files: testFiles, - permissions: 31 - } - }) - ); - var newFileData = { + var newFileData = new FileInfo({ id: 999, - type: 'file', name: 'new file.txt', mimetype: 'text/plain', size: 40001, etag: '999' - }; + }); fileList.add(newFileData); expect(fileList.findFileEl('One.txt').index()).toEqual(0); expect(fileList.findFileEl('somedir').index()).toEqual(1); @@ -2290,87 +2188,96 @@ describe('OCA.Files.FileList tests', function() { }); describe('create file', function() { var deferredCreate; + var deferredInfo; + var createStub; + var getFileInfoStub; beforeEach(function() { deferredCreate = $.Deferred(); + deferredInfo = $.Deferred(); + createStub = sinon.stub(filesClient, 'putFileContents') + .returns(deferredCreate.promise()); + getFileInfoStub = sinon.stub(filesClient, 'getFileInfo') + .returns(deferredInfo.promise()); + }); + afterEach(function() { + createStub.restore(); + getFileInfoStub.restore(); }); it('creates file with given name and adds it to the list', function() { - var deferred = fileList.createFile('test file.txt'); - var successStub = sinon.stub(); - var failureStub = sinon.stub(); + fileList.createFile('test.txt'); - deferred.done(successStub); - deferred.fail(failureStub); + expect(createStub.calledOnce).toEqual(true); + expect(createStub.getCall(0).args[0]).toEqual('/subdir/test.txt'); + expect(createStub.getCall(0).args[2]).toEqual({ + contentType: 'text/plain', + overwrite: true + }); - expect(fakeServer.requests.length).toEqual(1); - expect(fakeServer.requests[0].url).toEqual(OC.generateUrl('/apps/files/ajax/newfile.php')); + deferredCreate.resolve(200); - var query = fakeServer.requests[0].requestBody; - expect(OC.parseQueryString(query)).toEqual({ - dir: '/subdir', - filename: 'test file.txt' - }); + expect(getFileInfoStub.calledOnce).toEqual(true); + expect(getFileInfoStub.getCall(0).args[0]).toEqual('/subdir/test.txt'); - fakeServer.requests[0].respond( + deferredInfo.resolve( 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - data: { - path: '/subdir', - name: 'test file.txt', - mimetype: 'text/plain' - } + new FileInfo({ + path: '/subdir', + name: 'test.txt', + mimetype: 'text/plain' }) ); - var $tr = fileList.findFileEl('test file.txt'); + var $tr = fileList.findFileEl('test.txt'); expect($tr.length).toEqual(1); expect($tr.attr('data-mime')).toEqual('text/plain'); - - expect(successStub.calledOnce).toEqual(true); - expect(failureStub.notCalled).toEqual(true); }); // TODO: error cases // TODO: unique name cases }); - describe('create directory', function() { - it('creates directory with given name and adds it to the list', function() { - var deferred = fileList.createDirectory('test directory'); - var successStub = sinon.stub(); - var failureStub = sinon.stub(); - - deferred.done(successStub); - deferred.fail(failureStub); - - expect(fakeServer.requests.length).toEqual(1); - expect(fakeServer.requests[0].url).toEqual(OC.generateUrl('/apps/files/ajax/newfolder.php')); - var query = fakeServer.requests[0].requestBody; - expect(OC.parseQueryString(query)).toEqual({ - dir: '/subdir', - foldername: 'test directory' - }); + describe('create folder', function() { + var deferredCreate; + var deferredInfo; + var createStub; + var getFileInfoStub; + + beforeEach(function() { + deferredCreate = $.Deferred(); + deferredInfo = $.Deferred(); + createStub = sinon.stub(filesClient, 'createDirectory') + .returns(deferredCreate.promise()); + getFileInfoStub = sinon.stub(filesClient, 'getFileInfo') + .returns(deferredInfo.promise()); + }); + afterEach(function() { + createStub.restore(); + getFileInfoStub.restore(); + }); + + it('creates folder with given name and adds it to the list', function() { + fileList.createDirectory('sub dir'); + + expect(createStub.calledOnce).toEqual(true); + expect(createStub.getCall(0).args[0]).toEqual('/subdir/sub dir'); - fakeServer.requests[0].respond( + deferredCreate.resolve(200); + + expect(getFileInfoStub.calledOnce).toEqual(true); + expect(getFileInfoStub.getCall(0).args[0]).toEqual('/subdir/sub dir'); + + deferredInfo.resolve( 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'success', - data: { - path: '/subdir', - name: 'test directory', - mimetype: 'httpd/unix-directory' - } + new FileInfo({ + path: '/subdir', + name: 'sub dir', + mimetype: 'httpd/unix-directory' }) ); - var $tr = fileList.findFileEl('test directory'); + var $tr = fileList.findFileEl('sub dir'); expect($tr.length).toEqual(1); expect($tr.attr('data-mime')).toEqual('httpd/unix-directory'); - - expect(successStub.calledOnce).toEqual(true); - expect(failureStub.notCalled).toEqual(true); }); // TODO: error cases // TODO: unique name cases @@ -2481,14 +2388,14 @@ describe('OCA.Files.FileList tests', function() { expect(ev.result).not.toEqual(false); }); it('drop on a folder row inside the table triggers upload to target folder', function() { - var ev, formData; + var ev; ev = dropOn(fileList.findFileEl('somedir').find('td:eq(2)'), uploadData); expect(ev.result).not.toEqual(false); expect(uploadData.targetDir).toEqual('/subdir/somedir'); }); it('drop on a breadcrumb inside the table triggers upload to target folder', function() { - var ev, formData; + var ev; fileList.changeDirectory('a/b/c/d'); ev = dropOn(fileList.$el.find('.crumb:eq(2)'), uploadData); @@ -2497,32 +2404,50 @@ describe('OCA.Files.FileList tests', function() { }); }); }); - describe('Handeling errors', function () { - var redirectStub; + describe('Handling errors', function () { + var deferredList; + var getFolderContentsStub; - beforeEach(function () { - redirectStub = sinon.stub(OC, 'redirect'); - - fileList = new OCA.Files.FileList($('#app-content-files')); + beforeEach(function() { + deferredList = $.Deferred(); + getFolderContentsStub = + sinon.stub(filesClient, 'getFolderContents'); + getFolderContentsStub.onCall(0).returns(deferredList.promise()); + getFolderContentsStub.onCall(1).returns($.Deferred().promise()); + fileList.reload(); }); - afterEach(function () { + afterEach(function() { + getFolderContentsStub.restore(); fileList = undefined; + }); + it('redirects to files app in case of auth error', function () { + deferredList.reject(401, 'Authentication error'); - redirectStub.restore(); + expect(redirectStub.calledOnce).toEqual(true); + expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files'); + expect(getFolderContentsStub.calledOnce).toEqual(true); }); - it('reloads the page on authentication errors', function () { - fileList.reload(); - fakeServer.requests[0].respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ - status: 'error', - data: { - 'error': 'authentication_error' - } - }) - ); - expect(redirectStub.calledWith(OC.generateUrl('apps/files'))).toEqual(true); + it('redirects to root folder in case of forbidden access', function () { + deferredList.reject(403); + + expect(fileList.getCurrentDirectory()).toEqual('/'); + expect(getFolderContentsStub.calledTwice).toEqual(true); + }); + it('redirects to root folder and shows notification in case of internal server error', function () { + expect(notificationStub.notCalled).toEqual(true); + deferredList.reject(500); + + expect(fileList.getCurrentDirectory()).toEqual('/'); + expect(getFolderContentsStub.calledTwice).toEqual(true); + expect(notificationStub.calledOnce).toEqual(true); + }); + it('redirects to root folder and shows notification in case of storage not available', function () { + expect(notificationStub.notCalled).toEqual(true); + deferredList.reject(503, 'Storage not available'); + + expect(fileList.getCurrentDirectory()).toEqual('/'); + expect(getFolderContentsStub.calledTwice).toEqual(true); + expect(notificationStub.calledOnce).toEqual(true); }); }); describe('showFileBusyState', function() { diff --git a/apps/files/tests/js/filesSpec.js b/apps/files/tests/js/filesSpec.js index 30e6675c1556..b7627d59fdf6 100644 --- a/apps/files/tests/js/filesSpec.js +++ b/apps/files/tests/js/filesSpec.js @@ -76,11 +76,11 @@ describe('OCA.Files.Files tests', function() { describe('getDownloadUrl', function() { it('returns the ajax download URL when filename and dir specified', function() { var url = Files.getDownloadUrl('test file.txt', '/subdir'); - expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=test%20file.txt'); + expect(url).toEqual(OC.webroot + '/remote.php/webdav/subdir/test%20file.txt'); }); - it('returns the ajax download URL when filename and root dir specific', function() { + it('returns the webdav download URL when filename and root dir specified', function() { var url = Files.getDownloadUrl('test file.txt', '/'); - expect(url).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=test%20file.txt'); + expect(url).toEqual(OC.webroot + '/remote.php/webdav/test%20file.txt'); }); it('returns the ajax download URL when multiple files specified', function() { var url = Files.getDownloadUrl(['test file.txt', 'abc.txt'], '/subdir'); diff --git a/apps/files_sharing/js/sharedfilelist.js b/apps/files_sharing/js/sharedfilelist.js index 68bfd63ec89a..a799d4a94c2a 100644 --- a/apps/files_sharing/js/sharedfilelist.js +++ b/apps/files_sharing/js/sharedfilelist.js @@ -231,6 +231,7 @@ files = _.chain(files) // convert share data to file data .map(function(share) { + // TODO: use OC.Files.FileInfo var file = { id: share.file_source, icon: OC.MimeType.getIconUrl(share.mimetype), @@ -242,9 +243,6 @@ } else { file.type = 'file'; - if (share.isPreviewAvailable) { - file.isPreviewAvailable = true; - } } file.share = { id: share.id, diff --git a/apps/files_sharing/tests/js/sharedfilelistSpec.js b/apps/files_sharing/tests/js/sharedfilelistSpec.js index b4b6ac4954a9..fdc9de49c174 100644 --- a/apps/files_sharing/tests/js/sharedfilelistSpec.js +++ b/apps/files_sharing/tests/js/sharedfilelistSpec.js @@ -166,8 +166,7 @@ describe('OCA.Sharing.FileList tests', function() { expect($tr.attr('data-share-id')).toEqual('7'); expect($tr.find('a.name').attr('href')).toEqual( OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Flocal%20path&files=local%20name.txt' + '/remote.php/webdav/local%20path/local%20name.txt' ); expect($tr.find('.nametext').text().trim()).toEqual('local name.txt'); @@ -185,8 +184,7 @@ describe('OCA.Sharing.FileList tests', function() { expect($tr.attr('data-share-id')).toEqual('8'); expect($tr.find('a.name').attr('href')).toEqual( OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2F&files=b.txt' + '/remote.php/webdav/b.txt' ); expect($tr.find('.nametext').text().trim()).toEqual('b.txt'); }); @@ -338,8 +336,7 @@ describe('OCA.Sharing.FileList tests', function() { expect($tr.attr('data-share-id')).toEqual('7'); expect($tr.find('a.name').attr('href')).toEqual( OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Flocal%20path&files=local%20name.txt' + '/remote.php/webdav/local%20path/local%20name.txt' ); expect($tr.find('.nametext').text().trim()).toEqual('local name.txt'); }); @@ -429,9 +426,8 @@ describe('OCA.Sharing.FileList tests', function() { expect($tr.attr('data-share-owner')).not.toBeDefined(); expect($tr.attr('data-share-id')).toEqual('7'); expect($tr.find('a.name').attr('href')).toEqual( - OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Flocal%20path&files=local%20name.txt'); + OC.webroot + '/remote.php/webdav/local%20path/local%20name.txt' + ); expect($tr.find('.nametext').text().trim()).toEqual('local name.txt'); }); @@ -498,9 +494,7 @@ describe('OCA.Sharing.FileList tests', function() { expect($tr.attr('data-share-owner')).not.toBeDefined(); expect($tr.attr('data-share-id')).toEqual('7,8,9'); expect($tr.find('a.name').attr('href')).toEqual( - OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Flocal%20path&files=local%20name.txt' + OC.webroot + '/remote.php/webdav/local%20path/local%20name.txt' ); expect($tr.find('.nametext').text().trim()).toEqual('local name.txt'); }); @@ -592,9 +586,8 @@ describe('OCA.Sharing.FileList tests', function() { expect($tr.attr('data-share-owner')).not.toBeDefined(); expect($tr.attr('data-share-id')).toEqual('7'); expect($tr.find('a.name').attr('href')).toEqual( - OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Flocal%20path&files=local%20name.txt'); + OC.webroot + '/remote.php/webdav/local%20path/local%20name.txt' + ); expect($tr.find('.nametext').text().trim()).toEqual('local name.txt'); }); @@ -634,8 +627,7 @@ describe('OCA.Sharing.FileList tests', function() { expect($tr.attr('data-share-id')).toEqual('7'); expect($tr.find('a.name').attr('href')).toEqual( OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Flocal%20path&files=local%20name.txt'); + '/remote.php/webdav/local%20path/local%20name.txt'); expect($tr.find('.nametext').text().trim()).toEqual('local name.txt'); }); diff --git a/apps/files_trashbin/js/filelist.js b/apps/files_trashbin/js/filelist.js index 6b624e333a06..5812aff82f7a 100644 --- a/apps/files_trashbin/js/filelist.js +++ b/apps/files_trashbin/js/filelist.js @@ -283,7 +283,77 @@ isSelectedDeletable: function() { return true; - } + }, + + /** + * Reloads the file list using ajax call + * + * @return ajax call object + */ + reload: function() { + this._selectedFiles = {}; + this._selectionSummary.clear(); + this.$el.find('.select-all').prop('checked', false); + this.showMask(); + if (this._reloadCall) { + this._reloadCall.abort(); + } + this._reloadCall = $.ajax({ + url: this.getAjaxUrl('list'), + data: { + dir : this.getCurrentDirectory(), + sort: this._sort, + sortdirection: this._sortDirection + } + }); + var callBack = this.reloadCallback.bind(this); + return this._reloadCall.then(callBack, callBack); + }, + reloadCallback: function(result) { + delete this._reloadCall; + this.hideMask(); + + if (!result || result.status === 'error') { + // if the error is not related to folder we're trying to load, reload the page to handle logout etc + if (result.data.error === 'authentication_error' || + result.data.error === 'token_expired' || + result.data.error === 'application_not_enabled' + ) { + OC.redirect(OC.generateUrl('apps/files')); + } + OC.Notification.show(result.data.message); + return false; + } + + // Firewall Blocked request? + if (result.status === 403) { + // Go home + this.changeDirectory('/'); + OC.Notification.show(t('files', 'This operation is forbidden')); + return false; + } + + // Did share service die or something else fail? + if (result.status === 500) { + // Go home + this.changeDirectory('/'); + OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator')); + return false; + } + + if (result.status === 404) { + // go back home + this.changeDirectory('/'); + return false; + } + // aborted ? + if (result.status === 0){ + return true; + } + + this.setFiles(result.data.files); + return true; + }, }); diff --git a/core/js/oc-dialogs.js b/core/js/oc-dialogs.js index 4448b8130215..fe93d0ea6570 100644 --- a/core/js/oc-dialogs.js +++ b/core/js/oc-dialogs.js @@ -759,7 +759,7 @@ var OCdialogs = { filename: entry.name, date: OC.Util.relativeModifiedDate(entry.mtime) }); - if (entry.isPreviewAvailable) { + if (entry.type === 'file') { var urlSpec = { file: dir + '/' + entry.name }; From f20232d4bd3e78c4ea980aa03849c3f9bcf5b893 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Mon, 13 Jul 2015 17:41:20 +0200 Subject: [PATCH 04/19] Make public link share page work with Webdav and add operations The public page now uses the public.php/webdav endpoint. Also enabled more file operations like rename, move, delete and create folder from the public page, which are now all possible thanks to the public.php/webdav endpoint. --- apps/files/ajax/upload.php | 7 +- apps/files/js/files.js | 2 +- apps/files/templates/list.php | 11 --- apps/files_sharing/ajax/list.php | 96 -------------------- apps/files_sharing/js/public.js | 16 +++- apps/files_sharing/tests/js/publicAppSpec.js | 20 +++- 6 files changed, 35 insertions(+), 117 deletions(-) delete mode 100644 apps/files_sharing/ajax/list.php diff --git a/apps/files/ajax/upload.php b/apps/files/ajax/upload.php index a784642728fa..18e9cfe6117b 100644 --- a/apps/files/ajax/upload.php +++ b/apps/files/ajax/upload.php @@ -41,7 +41,6 @@ // If not, check the login. // If no token is sent along, rely on login only -$allowedPermissions = \OCP\Constants::PERMISSION_ALL; $errorCode = null; $l = \OC::$server->getL10N('files'); @@ -60,8 +59,6 @@ \OC_User::setIncognitoMode(true); - // return only read permissions for public upload - $allowedPermissions = \OCP\Constants::PERMISSION_READ; $publicDirectory = !empty($_POST['subdir']) ? (string)$_POST['subdir'] : '/'; $linkItem = OCP\Share::getShareByToken((string)$_POST['dirToken']); @@ -207,7 +204,7 @@ $data['originalname'] = $files['name'][$i]; $data['uploadMaxFilesize'] = $maxUploadFileSize; $data['maxHumanFilesize'] = $maxHumanFileSize; - $data['permissions'] = $meta['permissions'] & $allowedPermissions; + $data['permissions'] = $meta['permissions']; $data['directory'] = $returnedDir; $result[] = $data; } @@ -234,7 +231,7 @@ $data['originalname'] = $files['name'][$i]; $data['uploadMaxFilesize'] = $maxUploadFileSize; $data['maxHumanFilesize'] = $maxHumanFileSize; - $data['permissions'] = $meta['permissions'] & $allowedPermissions; + $data['permissions'] = $meta['permissions']; $data['directory'] = $returnedDir; $result[] = $data; } diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 6bdd14ac65d5..e33b83544370 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -207,7 +207,7 @@ */ lazyLoadPreview : function(path, mime, ready, width, height, etag) { console.warn('DEPRECATED: please use lazyLoadPreview() from an OCA.Files.FileList instance'); - return OCA.Files.App.fileList.lazyLoadPreview({ + return FileList.lazyLoadPreview({ path: path, mime: mime, callback: ready, diff --git a/apps/files/templates/list.php b/apps/files/templates/list.php index 7ebf80ee8b2f..04550f945b6e 100644 --- a/apps/files/templates/list.php +++ b/apps/files/templates/list.php @@ -1,16 +1,5 @@ <div id="controls"> <div class="actions creatable hidden"> - <?php /* - Only show upload button for public page - */ ?> - <?php if(isset($_['dirToken'])):?> - <div id="upload" class="button upload" - title="<?php isset($_['uploadMaxHumanFilesize']) ? p($l->t('Upload (max. %s)', array($_['uploadMaxHumanFilesize']))) : '' ?>"> - <label for="file_upload_start" class="svg icon-upload"> - <span class="hidden-visually"><?php p($l->t('Upload'))?></span> - </label> - </div> - <?php endif; ?> <div id="uploadprogresswrapper"> <div id="uploadprogressbar"></div> <button class="stop icon-close" style="display:none"> diff --git a/apps/files_sharing/ajax/list.php b/apps/files_sharing/ajax/list.php deleted file mode 100644 index c7f0bde5d4ab..000000000000 --- a/apps/files_sharing/ajax/list.php +++ /dev/null @@ -1,96 +0,0 @@ -<?php -/** - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -OCP\JSON::checkAppEnabled('files_sharing'); - -if(!isset($_GET['t'])){ - \OC_Response::setStatus(\OC_Response::STATUS_BAD_REQUEST); - \OCP\Util::writeLog('core-preview', 'No token parameter was passed', \OCP\Util::DEBUG); - exit; -} - -$token = $_GET['t']; - -$password = null; -if (isset($_POST['password'])) { - $password = $_POST['password']; -} - -$relativePath = null; -if (isset($_GET['dir'])) { - $relativePath = $_GET['dir']; -} - -$sortAttribute = isset( $_GET['sort'] ) ? $_GET['sort'] : 'name'; -$sortDirection = isset( $_GET['sortdirection'] ) ? ($_GET['sortdirection'] === 'desc') : false; - -$data = \OCA\Files_Sharing\Helper::setupFromToken($token, $relativePath, $password); - -$linkItem = $data['linkItem']; -// Load the files -$dir = $data['realPath']; - -$dir = \OC\Files\Filesystem::normalizePath($dir); -if (!\OC\Files\Filesystem::is_dir($dir . '/')) { - \OC_Response::setStatus(\OC_Response::STATUS_NOT_FOUND); - \OCP\JSON::error(array('success' => false)); - exit(); -} - -$data = array(); - -// make filelist -$files = \OCA\Files\Helper::getFiles($dir, $sortAttribute, $sortDirection); - -$formattedFiles = array(); -foreach ($files as $file) { - $entry = \OCA\Files\Helper::formatFileInfo($file); - // for now - unset($entry['directory']); - // do not disclose share owner - unset($entry['shareOwner']); - // do not disclose if something is a remote shares - unset($entry['mountType']); - unset($entry['icon']); - $entry['permissions'] = \OCP\Constants::PERMISSION_READ; - $formattedFiles[] = $entry; -} - -$data['directory'] = $relativePath; -$data['files'] = $formattedFiles; -$data['dirToken'] = $linkItem['token']; - -$permissions = $linkItem['permissions']; - -// if globally disabled -if (\OC::$server->getAppConfig()->getValue('core', 'shareapi_allow_public_upload', 'yes') === 'no') { - // only allow reading - $permissions = \OCP\Constants::PERMISSION_READ; -} - -$data['permissions'] = $permissions; - -OCP\JSON::success(array('data' => $data)); diff --git a/apps/files_sharing/js/public.js b/apps/files_sharing/js/public.js index 246b639f652c..826911299261 100644 --- a/apps/files_sharing/js/public.js +++ b/apps/files_sharing/js/public.js @@ -48,8 +48,20 @@ OCA.Sharing.PublicApp = { this._initialized = true; this.initialDir = $('#dir').val(); + var token = $('#sharingToken').val(); + // file list mode ? if ($el.find('#filestable').length) { + var filesClient = new OC.Files.Client({ + host: OC.getHost(), + port: OC.getPort(), + userName: token, + // note: password not be required, the endpoint + // will recognize previous validation from the session + root: OC.getRootPath() + '/public.php/webdav', + useHTTPS: OC.getProtocol() === 'https' + }); + this.fileList = new OCA.Files.FileList( $el, { @@ -58,7 +70,8 @@ OCA.Sharing.PublicApp = { dragOptions: dragOptions, folderDropOptions: folderDropOptions, fileActions: fileActions, - detailsViewEnabled: false + detailsViewEnabled: false, + filesClient: filesClient } ); this.files = OCA.Files.Files; @@ -88,7 +101,6 @@ OCA.Sharing.PublicApp = { // dynamically load image previews - var token = $('#sharingToken').val(); var bottomMargin = 350; var previewWidth = Math.ceil($(window).width() * window.devicePixelRatio); var previewHeight = Math.ceil(($(window).height() - bottomMargin) * window.devicePixelRatio); diff --git a/apps/files_sharing/tests/js/publicAppSpec.js b/apps/files_sharing/tests/js/publicAppSpec.js index d496b78acfac..8a644232e6cd 100644 --- a/apps/files_sharing/tests/js/publicAppSpec.js +++ b/apps/files_sharing/tests/js/publicAppSpec.js @@ -21,11 +21,14 @@ describe('OCA.Sharing.PublicApp tests', function() { var App = OCA.Sharing.PublicApp; + var hostStub, portStub, protocolStub, webrootStub; var $preview; - var fileListIn; - var fileListOut; beforeEach(function() { + protocolStub = sinon.stub(OC, 'getProtocol').returns('https'); + hostStub = sinon.stub(OC, 'getHost').returns('example.com'); + portStub = sinon.stub(OC, 'getPort').returns(8080); + webrootStub = sinon.stub(OC, 'getRootPath').returns('/owncloud'); $preview = $('<div id="preview"></div>'); $('#testArea').append($preview); $preview.append( @@ -35,6 +38,13 @@ describe('OCA.Sharing.PublicApp tests', function() { ); }); + afterEach(function() { + protocolStub.restore(); + hostStub.restore(); + portStub.restore(); + webrootStub.restore(); + }); + describe('File list', function() { // TODO: this should be moved to a separate file once the PublicFileList is extracted from public.js beforeEach(function() { @@ -78,6 +88,12 @@ describe('OCA.Sharing.PublicApp tests', function() { App._initialized = false; }); + it('Uses public webdav endpoint', function() { + expect(fakeServer.requests.length).toEqual(1); + expect(fakeServer.requests[0].method).toEqual('PROPFIND'); + expect(fakeServer.requests[0].url).toEqual('https://sh4tok@example.com:8080/owncloud/public.php/webdav/subdir'); + }); + describe('Download Url', function() { var fileList; From dc8ce87a26aaeeec655530e2960561498e61c94a Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Mon, 2 Nov 2015 14:17:49 +0100 Subject: [PATCH 05/19] Query tags/favorite through Webdav in file list --- apps/files/js/filelist.js | 32 ++++++++++++++++--- apps/files/js/tagsplugin.js | 32 +++++++++++++++++++ core/js/files/client.js | 63 +++++++++++++++++++++++++++++++------ core/js/files/fileinfo.js | 5 ++- 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 2c97816df008..83c7e147d46c 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -1376,6 +1376,13 @@ } }, + /** + * Returns list of webdav properties to request + */ + _getWebdavProperties: function() { + return this.filesClient.getPropfindProperties(); + }, + /** * Reloads the file list using ajax call * @@ -1390,7 +1397,12 @@ this._currentFileModel = null; this.$el.find('.select-all').prop('checked', false); this.showMask(); - this._reloadCall = this.filesClient.getFolderContents(this.getCurrentDirectory(), {includeParent: true}); + this._reloadCall = this.filesClient.getFolderContents( + this.getCurrentDirectory(), { + includeParent: true, + properties: this._getWebdavProperties() + } + ); if (this._detailsView) { // close sidebar this._updateDetailsView(null); @@ -1939,7 +1951,11 @@ ) .done(function() { // TODO: error handling / conflicts - self.filesClient.getFileInfo(targetPath) + self.filesClient.getFileInfo( + targetPath, { + properties: self._getWebdavProperties() + } + ) .then(function(status, data) { self.add(data, {animate: true, scrollTo: true}); deferred.resolve(status, data); @@ -1989,7 +2005,11 @@ this.filesClient.createDirectory(targetPath) .done(function(createStatus) { - self.filesClient.getFileInfo(targetPath) + self.filesClient.getFileInfo( + targetPath, { + properties: self._getWebdavProperties() + } + ) .done(function(status, data) { self.add(data, {animate: true, scrollTo: true}); deferred.resolve(status, data); @@ -2002,7 +2022,11 @@ .fail(function(createStatus) { // method not allowed, folder might exist already if (createStatus === 405) { - self.filesClient.getFileInfo(targetPath) + self.filesClient.getFileInfo( + targetPath, { + properties: self._getWebdavProperties() + } + ) .done(function(status, data) { // add it to the list, for completeness self.add(data, {animate: true, scrollTo: true}); diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index 23945d52603b..81b22e34cc25 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -161,6 +161,38 @@ fileInfo.tags = tags; return fileInfo; }; + + var NS_OC = 'http://owncloud.org/ns'; + + var oldGetWebdavProperties = fileList._getWebdavProperties; + fileList._getWebdavProperties = function() { + var props = oldGetWebdavProperties.apply(this, arguments); + props.push('{' + NS_OC + '}tags'); + props.push('{' + NS_OC + '}favorite'); + return props; + }; + + fileList.filesClient.addFileInfoParser(function(response) { + var data = {}; + var props = response.propStat[0].properties; + var tags = props['{' + NS_OC + '}tags']; + var favorite = props['{' + NS_OC + '}favorite']; + if (tags && tags.length) { + tags = _.chain(tags).filter(function(xmlvalue) { + return (xmlvalue.namespaceURI === NS_OC && xmlvalue.nodeName.split(':')[1] === 'tag'); + }).map(function(xmlvalue) { + return xmlvalue.textContent || xmlvalue.text; + }).value(); + } + if (tags) { + data.tags = tags; + } + if (favorite && parseInt(favorite, 10) !== 0) { + data.tags = data.tags || []; + data.tags.push(OC.TAG_FAVORITE); + } + return data; + }); }, attach: function(fileList) { diff --git a/core/js/files/client.js b/core/js/files/client.js index 9bb7bb999fd3..5ee90d2d52e8 100644 --- a/core/js/files/client.js +++ b/core/js/files/client.js @@ -125,10 +125,17 @@ /** * Client from the library * - * @type nl.sara.webdav.Client + * @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} @@ -273,8 +280,7 @@ id: this._parseFileId(props['{' + Client.NS_OWNCLOUD + '}id']), path: OC.dirname(path) || '/', name: OC.basename(path), - mtime: new Date(props['{' + Client.NS_DAV + '}getlastmodified']), - _props: props + mtime: new Date(props['{' + Client.NS_DAV + '}getlastmodified']) }; var etagProp = props['{' + Client.NS_DAV + '}getetag']; @@ -350,6 +356,11 @@ } } + // extend the parsed data using the custom parsers + _.each(this._fileInfoParsers, function(parserFunction) { + _.extend(data, parserFunction(response) || {}); + }); + return new FileInfo(data); }, @@ -381,7 +392,7 @@ * * @return {Array.<Object>} array of properties */ - _getPropfindProperties: function() { + getPropfindProperties: function() { if (!this._propfindProperties) { this._propfindProperties = _.map(Client._PROPFIND_PROPERTIES, function(propDef) { return '{' + propDef[0] + '}' + propDef[1]; @@ -397,6 +408,7 @@ * @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 */ @@ -404,14 +416,21 @@ 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), - this._getPropfindProperties(), + properties, 1 ).then(function(result) { if (self._isSuccessStatus(result.status)) { @@ -432,23 +451,29 @@ * Returns the file info of a given path. * * @param {String} path path - * @param {Array} [properties] list of webdav properties to - * retrieve + * @param {Array} [options.properties] list of Webdav properties to retrieve * * @return {Promise} promise */ - getFileInfo: function(path) { + 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), - this._getPropfindProperties(), + properties, 0 ).then( function(result) { @@ -633,10 +658,30 @@ } ); return promise; + }, + + /** + * Add a file info parser function + * + * @param {OC.Files.Client~parseFileInfo>} + */ + addFileInfoParser: function(parserFunction) { + this._fileInfoParsers.push(parserFunction); } }; + /** + * 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 diff --git a/core/js/files/fileinfo.js b/core/js/files/fileinfo.js index 95af07b79924..c4a9eeb3d7cb 100644 --- a/core/js/files/fileinfo.js +++ b/core/js/files/fileinfo.js @@ -34,7 +34,6 @@ this.mimetype = data.mimetype || 'application/octet-stream'; this.mountType = data.mountType; this.icon = data.icon; - this._props = data._props; if (data.type) { this.type = data.type; @@ -44,6 +43,10 @@ this.type = 'file'; } + if (data.tags) { + this.tags = data.tags; + } + if (!this.mimetype) { if (this.type === 'dir') { this.mimetype = 'httpd/unix-directory'; From 21da6271a8656517e956aa8897d8316b6a61ceac Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Mon, 2 Nov 2015 15:27:58 +0100 Subject: [PATCH 06/19] Parse share owner from Webdav to display in UI --- apps/files_sharing/js/share.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js index 63225a0d8ec6..3d105f283d85 100644 --- a/apps/files_sharing/js/share.js +++ b/apps/files_sharing/js/share.js @@ -50,7 +50,7 @@ if (fileData.shareOwner) { tr.attr('data-share-owner', fileData.shareOwner); // user should always be able to rename a mount point - if (fileData.isShareMountPoint) { + if (fileData.mountType === 'shared-root') { tr.attr('data-permissions', fileData.permissions | OC.PERMISSION_UPDATE); } } @@ -68,6 +68,26 @@ return fileInfo; }; + var NS_OC = 'http://owncloud.org/ns'; + + var oldGetWebdavProperties = fileList._getWebdavProperties; + fileList._getWebdavProperties = function() { + var props = oldGetWebdavProperties.apply(this, arguments); + props.push('{' + NS_OC + '}owner-display-name'); + return props; + }; + + fileList.filesClient.addFileInfoParser(function(response) { + var data = {}; + var props = response.propStat[0].properties; + var permissionsProp = props['{' + NS_OC + '}permissions']; + + if (permissionsProp && permissionsProp.indexOf('S') >= 0) { + data.shareOwner = props['{' + NS_OC + '}owner-display-name']; + } + return data; + }); + // use delegate to catch the case with multiple file lists fileList.$el.on('fileActionsReady', function(ev){ var fileList = ev.fileList; From 240ae46265c9bae2b1943d34dfe7b708d06c247a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <thomas.mueller@tmit.eu> Date: Tue, 17 Nov 2015 13:36:40 +0100 Subject: [PATCH 07/19] Fix js file load order --- apps/files/controller/viewcontroller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/files/controller/viewcontroller.php b/apps/files/controller/viewcontroller.php index c274680e525c..1d1a9111d192 100644 --- a/apps/files/controller/viewcontroller.php +++ b/apps/files/controller/viewcontroller.php @@ -119,6 +119,8 @@ protected function getStorageInfo() { * @throws \OCP\Files\NotFoundException */ public function index($dir = '', $view = '') { + $nav = new \OCP\Template('files', 'appnavigation', ''); + // Load the files we need \OCP\Util::addStyle('files', 'files'); \OCP\Util::addStyle('files', 'upload'); @@ -169,8 +171,6 @@ public function index($dir = '', $view = '') { // FIXME: Make non static $storageInfo = $this->getStorageInfo(); - $nav = new \OCP\Template('files', 'appnavigation', ''); - \OCA\Files\App::getNavigationManager()->add( [ 'id' => 'favorites', From ab1d786d879d519c86cd2016490d1ade5e43fd41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <thomas.mueller@tmit.eu> Date: Tue, 17 Nov 2015 13:37:13 +0100 Subject: [PATCH 08/19] Fix port issue - options.host already has the port attached --- apps/files_sharing/tests/js/publicAppSpec.js | 6 ++---- core/js/files/client.js | 19 +++---------------- core/js/tests/specs/files/clientSpec.js | 3 +-- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/apps/files_sharing/tests/js/publicAppSpec.js b/apps/files_sharing/tests/js/publicAppSpec.js index 8a644232e6cd..1ea5f7ed1bc6 100644 --- a/apps/files_sharing/tests/js/publicAppSpec.js +++ b/apps/files_sharing/tests/js/publicAppSpec.js @@ -21,13 +21,12 @@ describe('OCA.Sharing.PublicApp tests', function() { var App = OCA.Sharing.PublicApp; - var hostStub, portStub, protocolStub, webrootStub; + var hostStub, protocolStub, webrootStub; var $preview; beforeEach(function() { protocolStub = sinon.stub(OC, 'getProtocol').returns('https'); hostStub = sinon.stub(OC, 'getHost').returns('example.com'); - portStub = sinon.stub(OC, 'getPort').returns(8080); webrootStub = sinon.stub(OC, 'getRootPath').returns('/owncloud'); $preview = $('<div id="preview"></div>'); $('#testArea').append($preview); @@ -41,7 +40,6 @@ describe('OCA.Sharing.PublicApp tests', function() { afterEach(function() { protocolStub.restore(); hostStub.restore(); - portStub.restore(); webrootStub.restore(); }); @@ -91,7 +89,7 @@ describe('OCA.Sharing.PublicApp tests', function() { it('Uses public webdav endpoint', function() { expect(fakeServer.requests.length).toEqual(1); expect(fakeServer.requests[0].method).toEqual('PROPFIND'); - expect(fakeServer.requests[0].url).toEqual('https://sh4tok@example.com:8080/owncloud/public.php/webdav/subdir'); + expect(fakeServer.requests[0].url).toEqual('https://sh4tok@example.com/owncloud/public.php/webdav/subdir'); }); describe('Download Url', function() { diff --git a/core/js/files/client.js b/core/js/files/client.js index 5ee90d2d52e8..07a7132d4bed 100644 --- a/core/js/files/client.js +++ b/core/js/files/client.js @@ -31,22 +31,9 @@ this._root = this._root.substr(0, this._root.length - 1); } - if (!options.port) { - // workaround in case port is null or empty - options.port = undefined; - } - var url = ''; - var port = ''; + var url = 'http://'; if (options.useHTTPS) { - url += 'https://'; - if (options.port && options.port !== 443) { - port = ':' + options.port; - } - } else { - url += 'http://'; - if (options.port && options.port !== 80) { - port = ':' + options.port; - } + url = 'https://'; } var credentials = ''; if (options.userName) { @@ -59,7 +46,7 @@ url += credentials + '@'; } - url += options.host + port + this._root; + url += options.host + this._root; this._defaultHeaders = options.defaultHeaders || {'X-Requested-With': 'XMLHttpRequest'}; this._baseUrl = url; this._client = new dav.Client({ diff --git a/core/js/tests/specs/files/clientSpec.js b/core/js/tests/specs/files/clientSpec.js index 67815d93f056..7d039b5143af 100644 --- a/core/js/tests/specs/files/clientSpec.js +++ b/core/js/tests/specs/files/clientSpec.js @@ -25,11 +25,10 @@ describe('OC.Files.Client tests', function() { var client; beforeEach(function() { - baseUrl = 'https://testhost:999/owncloud/remote.php/webdav/'; + baseUrl = 'https://testhost/owncloud/remote.php/webdav/'; client = new Client({ host: 'testhost', - port: 999, root: '/owncloud/remote.php/webdav', useHTTPS: true }); From a1d0682ef849a452c0be4d107104e476007a6aa1 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Wed, 18 Nov 2015 15:18:00 +0100 Subject: [PATCH 09/19] Use oc:fileid property instead of oc:id --- core/js/files/client.js | 20 +++----------------- core/js/tests/specs/files/clientSpec.js | 4 ++-- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/core/js/files/client.js b/core/js/files/client.js index 07a7132d4bed..82cf3ff51218 100644 --- a/core/js/files/client.js +++ b/core/js/files/client.js @@ -79,9 +79,9 @@ */ [Client.NS_DAV, 'resourcetype'], /** - * Compound file id, contains fileid + server instance id + * File id */ - [Client.NS_OWNCLOUD, 'id'], + [Client.NS_OWNCLOUD, 'fileid'], /** * Letter-coded permissions */ @@ -210,20 +210,6 @@ return headers; }, - /** - * Parses the compound file id - * - * @param {string} compoundFileId compound file id as returned by the server - * - * @return {int} local file id, stripped of the instance id - */ - _parseFileId: function(compoundFileId) { - if (!compoundFileId || compoundFileId.length < 8) { - return null; - } - return parseInt(compoundFileId.substr(0, 8), 10); - }, - /** * Parses the etag response which is in double quotes. * @@ -264,7 +250,7 @@ var props = response.propStat[0].properties; var data = { - id: this._parseFileId(props['{' + Client.NS_OWNCLOUD + '}id']), + id: props['{' + Client.NS_OWNCLOUD + '}fileid'], path: OC.dirname(path) || '/', name: OC.basename(path), mtime: new Date(props['{' + Client.NS_DAV + '}getlastmodified']) diff --git a/core/js/tests/specs/files/clientSpec.js b/core/js/tests/specs/files/clientSpec.js index 7d039b5143af..3a3181d84263 100644 --- a/core/js/tests/specs/files/clientSpec.js +++ b/core/js/tests/specs/files/clientSpec.js @@ -213,7 +213,7 @@ describe('OC.Files.Client tests', function() { expect(props).toContain('{DAV:}getcontenttype'); expect(props).toContain('{DAV:}getetag'); expect(props).toContain('{DAV:}resourcetype'); - expect(props).toContain('{http://owncloud.org/ns}id'); + expect(props).toContain('{http://owncloud.org/ns}fileid'); expect(props).toContain('{http://owncloud.org/ns}size'); expect(props).toContain('{http://owncloud.org/ns}permissions'); }); @@ -350,7 +350,7 @@ describe('OC.Files.Client tests', function() { expect(props).toContain('{DAV:}getcontenttype'); expect(props).toContain('{DAV:}getetag'); expect(props).toContain('{DAV:}resourcetype'); - expect(props).toContain('{http://owncloud.org/ns}id'); + expect(props).toContain('{http://owncloud.org/ns}fileid'); expect(props).toContain('{http://owncloud.org/ns}size'); expect(props).toContain('{http://owncloud.org/ns}permissions'); }); From e8661a6b563129f95f7d98322006feb9da8e2c7c Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Wed, 18 Nov 2015 15:24:39 +0100 Subject: [PATCH 10/19] Update davclient.js --- core/vendor/davclient.js/lib/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vendor/davclient.js/lib/client.js b/core/vendor/davclient.js/lib/client.js index 93678f092486..39c1b0f3e933 100644 --- a/core/vendor/davclient.js/lib/client.js +++ b/core/vendor/davclient.js/lib/client.js @@ -198,7 +198,7 @@ dav.Client.prototype = { var propNode = propIterator.iterateNext(); while(propNode) { var content = propNode.textContent; - if (!content && propNode.hasChildNodes()) { + if (propNode.childNodes && propNode.childNodes.length > 0 && propNode.childNodes[0].nodeType === 1) { content = propNode.childNodes; } From 358858c9e3f06560b67b69e1d15c141debb2729a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <thomas.mueller@tmit.eu> Date: Wed, 18 Nov 2015 15:27:17 +0100 Subject: [PATCH 11/19] Fix undefined HTTP_USER_AGENT --- lib/private/appframework/http/request.php | 3 ++ tests/lib/appframework/http/RequestTest.php | 33 ++++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 96620838dfb0..8400882b88f0 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -674,6 +674,9 @@ public function getScriptName() { * @return bool true if at least one of the given agent matches, false otherwise */ public function isUserAgent(array $agent) { + if (!isset($this->server['HTTP_USER_AGENT'])) { + return false; + } foreach ($agent as $regex) { if (preg_match($regex, $this->server['HTTP_USER_AGENT'])) { return true; diff --git a/tests/lib/appframework/http/RequestTest.php b/tests/lib/appframework/http/RequestTest.php index f628a30f1da9..ab5fe1544413 100644 --- a/tests/lib/appframework/http/RequestTest.php +++ b/tests/lib/appframework/http/RequestTest.php @@ -693,19 +693,36 @@ public function testGetServerProtocolBehindLoadBalancers() { */ public function testUserAgent($testAgent, $userAgent, $matches) { $request = new Request( - [ - 'server' => [ - 'HTTP_USER_AGENT' => $testAgent, - ] - ], - $this->secureRandom, - $this->config, - $this->stream + [ + 'server' => [ + 'HTTP_USER_AGENT' => $testAgent, + ] + ], + $this->secureRandom, + $this->config, + $this->stream ); $this->assertSame($matches, $request->isUserAgent($userAgent)); } + /** + * @dataProvider userAgentProvider + * @param string $testAgent + * @param array $userAgent + * @param bool $matches + */ + public function testUndefinedUserAgent($testAgent, $userAgent, $matches) { + $request = new Request( + [], + $this->secureRandom, + $this->config, + $this->stream + ); + + $this->assertFalse($request->isUserAgent($userAgent)); + } + /** * @return array */ From 7ec83fc9fbb7428bde4cf0e1071704c941ac1f37 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Wed, 18 Nov 2015 15:42:35 +0100 Subject: [PATCH 12/19] Fix OC.FileInfo to copy all properties This makes it possible to also store custom properties passed through the data object like tags or shareOwner. --- core/js/files/fileinfo.js | 42 ++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/core/js/files/fileinfo.js b/core/js/files/fileinfo.js index c4a9eeb3d7cb..3bf68d88b15c 100644 --- a/core/js/files/fileinfo.js +++ b/core/js/files/fileinfo.js @@ -19,39 +19,31 @@ * @since 8.2 */ var FileInfo = function(data) { - if (!_.isUndefined(data.id)) { + var self = this; + _.each(data, function(value, key) { + if (!_.isFunction(value)) { + self[key] = value; + } + }); + + if (!_.isUndefined(this.id)) { this.id = parseInt(data.id, 10); } // TODO: normalize path this.path = data.path || ''; - this.name = data.name; - - this.mtime = data.mtime; - this.etag = data.etag; - this.permissions = data.permissions; - this.size = data.size; - this.mimetype = data.mimetype || 'application/octet-stream'; - this.mountType = data.mountType; - this.icon = data.icon; - - if (data.type) { - this.type = data.type; - } else if (this.mimetype === 'httpd/unix-directory') { - this.type = 'dir'; - } else { - this.type = 'file'; - } - if (data.tags) { - this.tags = data.tags; + if (this.type === 'dir') { + this.mimetype = 'httpd/unix-directory'; + } else { + this.mimetype = this.mimetype || 'application/octet-stream'; } - if (!this.mimetype) { - if (this.type === 'dir') { - this.mimetype = 'httpd/unix-directory'; + if (!this.type) { + if (this.mimetype === 'httpd/unix-directory') { + this.type = 'dir'; } else { - this.mimetype = 'application/octet-stream'; + this.type = 'file'; } } }; @@ -104,7 +96,7 @@ * @type String * @deprecated rely on mimetype instead */ - type: 'file', + type: null, /** * Permissions. From ec3166742b08eabcca2c6d2166070b4cee488bf7 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Wed, 18 Nov 2015 17:54:00 +0100 Subject: [PATCH 13/19] Properly join path sections This prevents double slashes that can mess up path comparisons in some cases. --- apps/files/js/favoritesplugin.js | 2 +- apps/files/js/fileactions.js | 5 +---- apps/files_external/js/app.js | 2 +- apps/files_sharing/js/app.js | 2 +- apps/files_trashbin/js/app.js | 5 +---- 5 files changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/files/js/favoritesplugin.js b/apps/files/js/favoritesplugin.js index 417a32ef804b..454a505c7bd6 100644 --- a/apps/files/js/favoritesplugin.js +++ b/apps/files/js/favoritesplugin.js @@ -92,7 +92,7 @@ // folder in the files app instead of opening it directly fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { OCA.Files.App.setActiveView('files', {silent: true}); - OCA.Files.App.fileList.changeDirectory(context.$file.attr('data-path') + '/' + filename, true, true); + OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true); }); fileActions.setDefault('dir', 'Open'); return fileActions; diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 32385c424789..871a2149c883 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -612,10 +612,7 @@ this.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { var dir = context.$file.attr('data-path') || context.fileList.getCurrentDirectory(); - if (dir !== '/') { - dir = dir + '/'; - } - context.fileList.changeDirectory(dir + filename); + context.fileList.changeDirectory(OC.joinPaths(dir, filename)); }); this.registerAction({ diff --git a/apps/files_external/js/app.js b/apps/files_external/js/app.js index bf853f926dcf..1bff3014bd66 100644 --- a/apps/files_external/js/app.js +++ b/apps/files_external/js/app.js @@ -54,7 +54,7 @@ OCA.External.App = { // folder in the files app instead of opening it directly fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { OCA.Files.App.setActiveView('files', {silent: true}); - OCA.Files.App.fileList.changeDirectory(context.$file.attr('data-path') + '/' + filename, true, true); + OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true); }); fileActions.setDefault('dir', 'Open'); return fileActions; diff --git a/apps/files_sharing/js/app.js b/apps/files_sharing/js/app.js index 3168e930829b..af198208de2b 100644 --- a/apps/files_sharing/js/app.js +++ b/apps/files_sharing/js/app.js @@ -142,7 +142,7 @@ OCA.Sharing.App = { // folder in the files app instead of opening it directly fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { OCA.Files.App.setActiveView('files', {silent: true}); - OCA.Files.App.fileList.changeDirectory(context.$file.attr('data-path') + '/' + filename, true, true); + OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true); }); fileActions.setDefault('dir', 'Open'); return fileActions; diff --git a/apps/files_trashbin/js/app.js b/apps/files_trashbin/js/app.js index 1f46f568bf2f..600a8ce2b03d 100644 --- a/apps/files_trashbin/js/app.js +++ b/apps/files_trashbin/js/app.js @@ -38,10 +38,7 @@ OCA.Trashbin.App = { var fileActions = new OCA.Files.FileActions(); fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { var dir = context.fileList.getCurrentDirectory(); - if (dir !== '/') { - dir = dir + '/'; - } - context.fileList.changeDirectory(dir + filename); + context.fileList.changeDirectory(OC.joinPaths(dir, filename)); }); fileActions.setDefault('dir', 'Open'); From d3383adb71d7f05642610490b747f92958c7d357 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Thu, 19 Nov 2015 11:42:17 +0100 Subject: [PATCH 14/19] Fix issue when renaming creates a separate entry --- apps/files/js/filelist.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 83c7e147d46c..672c39a8bb16 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -1806,10 +1806,8 @@ } function updateInList(fileInfo) { - tr.remove(); - tr = self.add(fileInfo, {updateSummary: false, silent: true}); - self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)})); - self._updateDetailsView(fileInfo.name); + self.updateRow(tr, fileInfo); + self._updateDetailsView(fileInfo.name, false); } // TODO: too many nested blocks, move parts into functions @@ -1838,11 +1836,10 @@ td.children('a.name').show(); var path = tr.attr('data-path') || self.getCurrentDirectory(); - self.filesClient.move(path + '/' + oldName, path + '/' + newName) + self.filesClient.move(OC.joinPaths(path, oldName), OC.joinPaths(path, newName)) .done(function() { - var fileInfo = self.files.splice(tr.index(), 1)[0]; - fileInfo.name = newName; - updateInList(fileInfo); + oldFileInfo.name = newName; + updateInList(oldFileInfo); }) .fail(function(status) { // TODO: 409 means current folder does not exist, redirect ? From ce897f80e60a4e2258da89745b8fda030f548df8 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Thu, 19 Nov 2015 12:01:55 +0100 Subject: [PATCH 15/19] Send download token as cookie to tell the UI that it started This used to be done in the ajax download code. Now that single file downloads are going through Webdav, the token handling needs to be done here too. --- apps/dav/lib/connector/sabre/filesplugin.php | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/dav/lib/connector/sabre/filesplugin.php b/apps/dav/lib/connector/sabre/filesplugin.php index d68397dcaa32..e85a67a87593 100644 --- a/apps/dav/lib/connector/sabre/filesplugin.php +++ b/apps/dav/lib/connector/sabre/filesplugin.php @@ -116,6 +116,7 @@ public function initialize(\Sabre\DAV\Server $server) { $this->server->on('afterBind', array($this, 'sendFileIdHeader')); $this->server->on('afterWriteContent', array($this, 'sendFileIdHeader')); $this->server->on('afterMethod:GET', [$this,'httpGet']); + $this->server->on('afterMethod:GET', array($this, 'handleDownloadToken')); $this->server->on('afterResponse', function($request, ResponseInterface $response) { $body = $response->getBody(); if (is_resource($body)) { @@ -148,6 +149,32 @@ function checkMove($source, $destination) { } } + /** + * This sets a cookie to be able to recognize the start of the download + * the content must not be longer than 32 characters and must only contain + * alphanumeric characters + * + * @param RequestInterface $request + * @param ResponseInterface $response + */ + function handleDownloadToken(RequestInterface $request, ResponseInterface $response) { + $queryParams = $request->getQueryParameters(); + + /** + * this sets a cookie to be able to recognize the start of the download + * the content must not be longer than 32 characters and must only contain + * alphanumeric characters + */ + if (isset($queryParams['downloadStartSecret'])) { + $token = $queryParams['downloadStartSecret']; + if (!isset($token[32]) + && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) { + // FIXME: use $response->setHeader() instead + setcookie('ocDownloadStarted', $token, time() + 20, '/'); + } + } + } + /** * Plugin that adds a 'Content-Disposition: attachment' header to all files * delivered by SabreDAV. From fd40b0663972b1bd1fe5e481fcf595289c8a33f8 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Thu, 19 Nov 2015 15:22:52 +0100 Subject: [PATCH 16/19] Patch for davclient.js for Firefox --- core/vendor/davclient.js/lib/client.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/vendor/davclient.js/lib/client.js b/core/vendor/davclient.js/lib/client.js index 39c1b0f3e933..121b5dcab5f7 100644 --- a/core/vendor/davclient.js/lib/client.js +++ b/core/vendor/davclient.js/lib/client.js @@ -169,7 +169,7 @@ dav.Client.prototype = { } }.bind(this); - var responseIterator = doc.evaluate('/d:multistatus/d:response', doc, resolver); + var responseIterator = doc.evaluate('/d:multistatus/d:response', doc, resolver, XPathResult.ANY_TYPE, null); var result = []; var responseNode = responseIterator.iterateNext(); @@ -181,19 +181,19 @@ dav.Client.prototype = { propStat : [] }; - response.href = doc.evaluate('string(d:href)', responseNode, resolver).stringValue; + response.href = doc.evaluate('string(d:href)', responseNode, resolver, XPathResult.ANY_TYPE, null).stringValue; - var propStatIterator = doc.evaluate('d:propstat', responseNode, resolver); + var propStatIterator = doc.evaluate('d:propstat', responseNode, resolver, XPathResult.ANY_TYPE, null); var propStatNode = propStatIterator.iterateNext(); while(propStatNode) { var propStat = { - status : doc.evaluate('string(d:status)', propStatNode, resolver).stringValue, + status : doc.evaluate('string(d:status)', propStatNode, resolver, XPathResult.ANY_TYPE, null).stringValue, properties : [], }; - var propIterator = doc.evaluate('d:prop/*', propStatNode, resolver); + var propIterator = doc.evaluate('d:prop/*', propStatNode, resolver, XPathResult.ANY_TYPE, null); var propNode = propIterator.iterateNext(); while(propNode) { From a05e40932c093609892c12bb5e24a882661075da Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Thu, 19 Nov 2015 17:20:06 +0100 Subject: [PATCH 17/19] Now using IE8 workaround of davclient.js for all IE versions --- core/js/files/{ie8davclient.js => iedavclient.js} | 2 +- lib/private/template.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename core/js/files/{ie8davclient.js => iedavclient.js} (98%) diff --git a/core/js/files/ie8davclient.js b/core/js/files/iedavclient.js similarity index 98% rename from core/js/files/ie8davclient.js rename to core/js/files/iedavclient.js index 9887d34e5b4b..bc6bce2f9aeb 100644 --- a/core/js/files/ie8davclient.js +++ b/core/js/files/iedavclient.js @@ -12,7 +12,7 @@ (function(dav) { /** - * Override davclient.js methods with IE8-compatible logic + * Override davclient.js methods with IE-compatible logic */ dav.Client.prototype = _.extend({}, dav.Client.prototype, { diff --git a/lib/private/template.php b/lib/private/template.php index 2c9721dc9640..1476a964ef36 100644 --- a/lib/private/template.php +++ b/lib/private/template.php @@ -161,9 +161,9 @@ public static function initTemplateEngine($renderAs) { throw new \Exception('Cannot read core/js/core.json'); } - if (\OC::$server->getRequest()->isUserAgent([\OC\AppFramework\Http\Request::USER_AGENT_IE_8])) { + if (\OC::$server->getRequest()->isUserAgent([\OC\AppFramework\Http\Request::USER_AGENT_IE])) { // shim for the davclient.js library - \OCP\Util::addScript('files/ie8davclient'); + \OCP\Util::addScript('files/iedavclient'); } self::$initTemplateEngineFirstRun = false; From 2d7c9f0ba9f842c4208968a4cff7cb29bdac5388 Mon Sep 17 00:00:00 2001 From: Robin Appelman <icewind@owncloud.com> Date: Thu, 19 Nov 2015 17:33:17 +0100 Subject: [PATCH 18/19] also match ie11 with Request::USER_AGENT_IE --- lib/private/appframework/http/request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/appframework/http/request.php b/lib/private/appframework/http/request.php index 8400882b88f0..ea42c9a89672 100644 --- a/lib/private/appframework/http/request.php +++ b/lib/private/appframework/http/request.php @@ -42,7 +42,7 @@ */ class Request implements \ArrayAccess, \Countable, IRequest { - const USER_AGENT_IE = '/MSIE/'; + const USER_AGENT_IE = '/(MSIE)|(Trident)/'; const USER_AGENT_IE_8 = '/MSIE 8.0/'; // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#'; From 418fefc93c3332c77ec617ef108138efb6a34544 Mon Sep 17 00:00:00 2001 From: Vincent Petry <pvince81@owncloud.com> Date: Fri, 20 Nov 2015 17:12:50 +0100 Subject: [PATCH 19/19] Update app version for Webdav impl of files GUI This will make sure the cached JS gets properly updated. Also, since this is a bigger change it also qualifies for a version increase :-) --- apps/files/appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index ba8bb62494e5..4ab226f39685 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -8,7 +8,7 @@ <shipped>true</shipped> <standalone/> <default_enable/> - <version>1.3.0</version> + <version>1.4.0</version> <types> <filesystem/> </types>