From fb3d5c7856b9fbed926091e91ec54de023859df8 Mon Sep 17 00:00:00 2001 From: Vincent Petry 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 = + '\n' + + '\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+=' \n'; + } + + } + body+=' \n'; + body+=''; + + 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 = + '\n' + + '\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+=' \n'; + } + + } + body+=' \n'; + body+=''; + + 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 " + ], + "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 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.} 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.} 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.} 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 +* +* 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 . +* +*/ + +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.} 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 = '\n'; + + _.each(props, function(value, key) { + s += '<' + key + '>' + value + '\n'; + }); + + return s + '\n'; + } + + function makeResponseBlock(href, props, failedProps) { + var s = '\n'; + s += '' + href + '\n'; + s += '\n'; + s += makePropBlock(props); + s += 'HTTP/1.1 200 OK'; + s += '\n'; + if (failedProps) { + s += '\n'; + _.each(failedProps, function(prop) { + s += '<' + prop + '/>\n'; + }); + s += 'HTTP/1.1 404 Not Found\n'; + s += '\n'; + } + return s + '\n'; + } + + describe('file listing', function() { + + var folderContentsXml = + '' + + '' + + 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': '', + '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': '', + 'oc:id': '00000015oc2d13a6a068', + 'oc:permissions': 'RDNVCK', + 'oc:size': 100 + }, + [ + 'd:getcontenttype', + 'd:getcontentlength' + ] + ) + + ''; + + 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 = + '' + + '' + + 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': '', + 'oc:id': '00000011oc2d13a6a068', + 'oc:permissions': 'RDNVCK', + 'oc:size': 120 + }, + [ + 'd:getcontenttype', + 'd:getcontentlength' + ] + ) + + ''; + + 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 = + '' + + '' + + makeResponseBlock( + '/owncloud/remote.php/webdav/in%20root', + { + 'd:getlastmodified': 'Fri, 10 Jul 2015 10:00:05 GMT', + 'd:getetag': '"56cfcabd79abb"', + 'd:resourcetype': '', + 'oc:id': '00000011oc2d13a6a068', + 'oc:permissions': 'RDNVCK', + 'oc:size': 120 + }, + [ + 'd:getcontenttype', + 'd:getcontentlength' + ] + ) + + ''; + + 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'] = ''; + } + + var responseXml = + '' + + '' + + makeResponseBlock( + '/owncloud/remote.php/webdav/file.txt', + props + ) + + ''; + 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 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 @@ - - * @author Frank Karlitschek - * @author Jakob Sack - * @author Joas Schilling - * @author Jörn Friedrich Dreyer - * @author Lukas Reschke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @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 - * - */ -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 @@ - - * @author Frank Karlitschek - * @author Georg Ehrke - * @author Jörn Friedrich Dreyer - * @author Lukas Reschke - * @author Robin Appelman - * @author Vincent Petry - * - * @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 - * - */ -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 @@ - - * @author Georg Ehrke - * @author Jörn Friedrich Dreyer - * @author Lukas Reschke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @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 - * - */ -// 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 @@ - - * @author Björn Schießle - * @author Frank Karlitschek - * @author Georg Ehrke - * @author Jörn Friedrich Dreyer - * @author Lukas Reschke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @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 - * - */ -// 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 @@ - - * @author Frank Karlitschek - * @author Jakob Sack - * @author Jörn Friedrich Dreyer - * @author Lukas Reschke - * @author Morris Jobke - * @author Robin Appelman - * @author Vincent Petry - * - * @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 - * - */ - -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. + * @type Array. */ 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 = $(''); // 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($('').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 = $('').val(oldname); + input = $('').val(oldName); 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; iview = $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 @@ - - * @author Christopher Schäpers - * @author Joas Schilling - * @author Morris Jobke - * @author Robin Appelman - * @author Thomas Müller - * @author Vincent Petry - * - * @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 - * - */ - -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() { '' ); - 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 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 @@