Repository: cordova-plugin-file-transfer Updated Branches: refs/heads/master 6f9b59fd8 -> affb5ec0a
CB-7316 Improves current specs compatibility: * Update handling credentials routine, replace parsing with Uri object by regexp parsing due to security errors in Windows. * Add support to aborting download/upload operations for windows platform. * Add support of progress events for download/upload operations for windows platform. - progress event object for upload operation is always empty, due to Windows restrictions. * Fix multiple errors with handling invalid URLs and paths. * Fix error callbacks in download/upload methods. Now they called with proper Error objects. Project: http://git-wip-us.apache.org/repos/asf/cordova-plugin-file-transfer/repo Commit: http://git-wip-us.apache.org/repos/asf/cordova-plugin-file-transfer/commit/f92244af Tree: http://git-wip-us.apache.org/repos/asf/cordova-plugin-file-transfer/tree/f92244af Diff: http://git-wip-us.apache.org/repos/asf/cordova-plugin-file-transfer/diff/f92244af Branch: refs/heads/master Commit: f92244aff9641dd3a287e7efe7f15344cec3db1c Parents: b5d1dff Author: Vladimir Kotikov <[email protected]> Authored: Fri Aug 15 12:41:59 2014 +0400 Committer: Vladimir Kotikov <[email protected]> Committed: Mon Aug 18 10:07:49 2014 +0400 ---------------------------------------------------------------------- www/FileTransfer.js | 31 ++--- www/windows8/FileTransferProxy.js | 202 ++++++++++++++++++++++++++++----- 2 files changed, 188 insertions(+), 45 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cordova-plugin-file-transfer/blob/f92244af/www/FileTransfer.js ---------------------------------------------------------------------- diff --git a/www/FileTransfer.js b/www/FileTransfer.js index 4668a3c..ff4bbef 100644 --- a/www/FileTransfer.js +++ b/www/FileTransfer.js @@ -32,24 +32,23 @@ function newProgressEvent(result) { return pe; } +function getUrlCredentials(urlString) { + var credentialsPattern = /^http\:\/\/((.*?)\:(.*?))@.*$/g, + credentials = credentialsPattern.exec(urlString); + + return credentials && credentials[1]; +} + function getBasicAuthHeader(urlString) { var header = null; - if (window.btoa) { - // parse the url using the Location object - var url = document.createElement('a'); - url.href = urlString; - - var credentials = null; - var protocol = url.protocol + "//"; - var origin = protocol + url.host.replace(":" + url.port, ""); // Windows 8 (IE10) append :80 or :443 to url.host - - // check whether there are the username:password credentials in the url - if (url.href.indexOf(origin) !== 0) { // credentials found - var atIndex = url.href.indexOf("@"); - credentials = url.href.substring(protocol.length, atIndex); - } + // This is changed due to MS Windows doesn't support credentials in http uris + // so we detect them by regexp and strip off from result url + // Proof: http://social.msdn.microsoft.com/Forums/windowsapps/en-US/a327cf3c-f033-4a54-8b7f-03c56ba3203f/windows-foundation-uri-security-problem + + if (window.btoa) { + var credentials = getUrlCredentials(urlString); if (credentials) { var authHeader = "Authorization"; var authHeaderValue = "Basic " + window.btoa(credentials); @@ -97,6 +96,8 @@ FileTransfer.prototype.upload = function(filePath, server, successCallback, erro var httpMethod = null; var basicAuthHeader = getBasicAuthHeader(server); if (basicAuthHeader) { + server = server.replace(getUrlCredentials(server) + '@', ''); + options = options || {}; options.headers = options.headers || {}; options.headers[basicAuthHeader.name] = basicAuthHeader.value; @@ -157,6 +158,8 @@ FileTransfer.prototype.download = function(source, target, successCallback, erro var basicAuthHeader = getBasicAuthHeader(source); if (basicAuthHeader) { + source = source.replace(getUrlCredentials(source) + '@', ''); + options = options || {}; options.headers = options.headers || {}; options.headers[basicAuthHeader.name] = basicAuthHeader.value; http://git-wip-us.apache.org/repos/asf/cordova-plugin-file-transfer/blob/f92244af/www/windows8/FileTransferProxy.js ---------------------------------------------------------------------- diff --git a/www/windows8/FileTransferProxy.js b/www/windows8/FileTransferProxy.js index d72e985..ba474a4 100644 --- a/www/windows8/FileTransferProxy.js +++ b/www/windows8/FileTransferProxy.js @@ -19,19 +19,50 @@ * */ +/*jshint -W030 */ var FileTransferError = require('./FileTransferError'), + ProgressEvent = require('org.apache.cordova.file.ProgressEvent'), FileUploadResult = require('org.apache.cordova.file.FileUploadResult'), FileEntry = require('org.apache.cordova.file.FileEntry'); + +// Some private helper functions, hidden by the module +function cordovaPathToNative(path) { + // turn / into \\ + var cleanPath = path.replace(/\//g, '\\'); + // turn \\ into \ + cleanPath = cleanPath.replace(/\\\\/g, '\\'); + // strip end \\ characters + cleanPath = cleanPath.replace(/\\+$/g, ''); + return cleanPath; +} + +function nativePathToCordova(path) { + var cleanPath = path.replace(/\\/g, '/'); + return cleanPath; +} + +var fileTransferOps = []; + +function FileTransferOperation(state, promise) { + this.state = state; + this.promise = promise; +} + +FileTransferOperation.PENDING = 0; +FileTransferOperation.DONE = 1; +FileTransferOperation.CANCELLED = 2; + + module.exports = { /* exec(win, fail, 'FileTransfer', 'upload', [filePath, server, fileKey, fileName, mimeType, params, trustAllHosts, chunkedMode, headers, this._id, httpMethod]); */ - upload:function(successCallback, error, options) { - var filePath = options[0]; + upload:function(successCallback, errorCallback, options) { + var filePath = cordovaPathToNative(options[0]); var server = options[1]; var fileKey = options[2] || 'source'; var fileName = options[3]; @@ -40,9 +71,10 @@ exec(win, fail, 'FileTransfer', 'upload', var trustAllHosts = options[6]; // todo var chunkedMode = options[7]; // todo var headers = options[8] || {}; + var uploadId = options[9]; if (filePath === null || typeof filePath === 'undefined') { - error && error(FileTransferError.FILE_NOT_FOUND_ERR); + errorCallback && errorCallback(FileTransferError.FILE_NOT_FOUND_ERR); return; } @@ -50,6 +82,9 @@ exec(win, fail, 'FileTransfer', 'upload', filePath = Windows.Storage.ApplicationData.current.localFolder.path + String(filePath).substr(8).split("/").join("\\"); } + // Create internal download operation object + fileTransferOps[uploadId] = new FileTransferOperation(FileTransferOperation.PENDING, null); + Windows.Storage.StorageFile.getFileFromPathAsync(filePath).then(function (storageFile) { if(!fileName) { @@ -63,6 +98,13 @@ exec(win, fail, 'FileTransfer', 'upload', storageFile.openAsync(Windows.Storage.FileAccessMode.read).then(function (stream) { + // check if upload isn't already cancelled + var uploadOp = fileTransferOps[uploadId]; + if (uploadOp && uploadOp.state == FileTransferOperation.CANCELLED) { + // Here we should call errorCB with ABORT_ERR error + errorCallback && errorCallback(new FileTransferError(FileTransferError.ABORT_ERR, filePath, server)); + return; + } var blob = MSApp.createBlobFromRandomAccessStream(mimeType, stream); @@ -73,39 +115,49 @@ exec(win, fail, 'FileTransfer', 'upload', formData.append(key,params[key]); } - WinJS.xhr({ type: "POST", url: server, data: formData, headers: headers }).then(function (response) { - storageFile.getBasicPropertiesAsync().done(function (basicProperties) { - - Windows.Storage.FileIO.readBufferAsync(storageFile).done(function (buffer) { - var dataReader = Windows.Storage.Streams.DataReader.fromBuffer(buffer); - var fileContent = dataReader.readString(buffer.length); - dataReader.close(); - var ftResult = new FileUploadResult(); - ftResult.bytesSent = basicProperties.size; - ftResult.responseCode = response.status; - ftResult.response = fileContent; - successCallback && successCallback(ftResult); - }); + var uploadOperation; + try { + // Create XHR promise for uploading data to server + uploadOperation = WinJS.xhr({ type: "POST", url: server, data: formData, headers: headers }); + fileTransferOps[uploadId].promise = uploadOperation; + } catch (e) { + // it will fail if URL is malformed, so we handle this situation + errorCallback && errorCallback(new FileTransferError(FileTransferError.INVALID_URL_ERR, filePath, server, null, null, e)); + return; + } + uploadOperation.then(function (response) { + storageFile.getBasicPropertiesAsync().done(function(basicProperties) { + var ftResult = new FileUploadResult(basicProperties.size, response.status, response.responseText); + successCallback && successCallback(ftResult); }); - }, function () { - error && error(FileTransferError.INVALID_URL_ERR); + }, function(err) { + if ('status' in err) { + errorCallback && errorCallback(new FileTransferError(FileTransferError.CONNECTION_ERR, filePath, server, err.status, err.responseText, err)); + } else { + errorCallback && errorCallback(new FileTransferError(FileTransferError.INVALID_URL_ERR, filePath, server, null, null, err)); + } + }, function(evt) { + // progress event handler, calls successCallback with empty ProgressEvent + // We can't specify ProgressEvent data here since evt not provides any helpful information + var progressEvent = new ProgressEvent('progress'); + successCallback && successCallback(progressEvent, { keepCallback: true }); }); }); - - },function() { - error && error(FileTransferError.FILE_NOT_FOUND_ERR); + }, function(err) { + errorCallback && errorCallback(new FileTransferError(FileTransferError.FILE_NOT_FOUND_ERR, server, server, null, null, err)); }); }, - download:function(successCallback, error, options) { + download:function(successCallback, errorCallback, options) { var source = options[0]; - var target = options[1]; + var target = cordovaPathToNative(options[1]); + var downloadId = options[3]; var headers = options[4] || {}; if (target === null || typeof target === undefined) { - error && error(FileTransferError.FILE_NOT_FOUND_ERR); + errorCallback && errorCallback(FileTransferError.FILE_NOT_FOUND_ERR); return; } if (String(target).substr(0, 8) == "file:///") { @@ -114,30 +166,118 @@ exec(win, fail, 'FileTransfer', 'upload', var path = target.substr(0, String(target).lastIndexOf("\\")); var fileName = target.substr(String(target).lastIndexOf("\\") + 1); if (path === null || fileName === null) { - error && error(FileTransferError.FILE_NOT_FOUND_ERR); + errorCallback && errorCallback(FileTransferError.FILE_NOT_FOUND_ERR); return; } var download = null; + // Create internal download operation object + fileTransferOps[downloadId] = new FileTransferOperation(FileTransferOperation.PENDING, null); Windows.Storage.StorageFolder.getFolderFromPathAsync(path).then(function (storageFolder) { storageFolder.createFileAsync(fileName, Windows.Storage.CreationCollisionOption.generateUniqueName).then(function (storageFile) { - var uri = Windows.Foundation.Uri(source); - var downloader = new Windows.Networking.BackgroundTransfer.BackgroundDownloader(); + // check if download isn't already cancelled + var downloadOp = fileTransferOps[downloadId]; + if (downloadOp && downloadOp.state == FileTransferOperation.CANCELLED) { + // Here we should call errorCB with ABORT_ERR error + errorCallback && errorCallback(new FileTransferError(FileTransferError.ABORT_ERR, source, target)); + return; + } + + // if download isn't cancelled, contunue with creating and preparing download operation + var downloader = new Windows.Networking.BackgroundTransfer.BackgroundDownloader(); for (var header in headers) { downloader.setRequestHeader(header, headers[header]); } - download = downloader.createDownload(uri, storageFile); - download.startAsync().then(function () { + + // create download object. This will throw an exception if URL is malformed + try { + var uri = Windows.Foundation.Uri(source); + download = downloader.createDownload(uri, storageFile); + } catch (e) { + // so we handle this and call errorCallback + errorCallback && errorCallback(new FileTransferError(FileTransferError.INVALID_URL_ERR)); + return; + } + + var downloadOperation = download.startAsync(); + // update internal TransferOperation object with newly created promise + fileTransferOps[downloadId].promise = downloadOperation; + + downloadOperation.then(function () { + + // Update TransferOperation object with new state, delete promise property + // since it is not actual anymore + var currentDownloadOp = fileTransferOps[downloadId]; + if (currentDownloadOp) { + currentDownloadOp.state = FileTransferOperation.DONE; + currentDownloadOp.promise = null; + } + successCallback && successCallback(new FileEntry(storageFile.name, storageFile.path)); - }, function () { - error && error(FileTransferError.INVALID_URL_ERR); + }, function (error) { + + var result; + // Handle download error here. If download was cancelled, + // message property will be specified + if (error.message == 'Canceled') { + result = new FileTransferError(FileTransferError.ABORT_ERR, source, target, null, null, error); + } else { + // in the other way, try to get response property + var response = download.getResponseInformation(); + if (!response) { + result = new FileTransferError(FileTransferError.CONNECTION_ERR, source, target); + } else if (response.statusCode == 401 || response.statusCode == 404) { + result = new FileTransferError(FileTransferError.FILE_NOT_FOUND_ERR, source, target, response.statusCode, null, error); + } + } + + // Update TransferOperation object with new state, delete promise property + // since it is not actual anymore + var currentDownloadOp = fileTransferOps[downloadId]; + if (currentDownloadOp) { + currentDownloadOp.state = FileTransferOperation.CANCELLED; + currentDownloadOp.promise = null; + } + + // Cleanup, remove incompleted file + storageFile.deleteAsync().then(function () { + errorCallback && errorCallback(result); + }); + + + }, function(evt) { + + var progressEvent = new ProgressEvent('progress', { + loaded: evt.progress.bytesReceived, + total: evt.progress.totalBytesToReceive, + target: evt.resultFile + }); + progressEvent.lengthComputable = true; + + successCallback && successCallback(progressEvent, { keepCallback: true }); }); + }, function (error) { + errorCallback && errorCallback(new FileTransferError(FileTransferError.FILE_NOT_FOUND_ERR, source, target, null, null, error)); }); + }, function (error) { + errorCallback && errorCallback(new FileTransferError(FileTransferError.FILE_NOT_FOUND_ERR, source, target, null, null, error)); }); + }, + + abort: function (successCallback, error, options) { + var fileTransferOpId = options[0]; + + // Try to find transferOperation with id specified, and cancel its' promise + var currentOp = fileTransferOps[fileTransferOpId]; + if (currentOp) { + currentOp.state = FileTransferOperation.CANCELLED; + currentOp.promise && currentOp.promise.cancel(); + } } + }; require("cordova/exec/proxy").add("FileTransfer",module.exports);
