diff --git a/doc/index.md b/doc/index.md index 81710b1..4c2236b 100644 --- a/doc/index.md +++ b/doc/index.md @@ -39,6 +39,7 @@ Although in the global scope, they are not available until after the `deviceread - Amazon Fire OS - Android - BlackBerry 10 +- Browser - Firefox OS** - iOS - Windows Phone 7 and 8* diff --git a/plugin.xml b/plugin.xml index d9ee6eb..cb85a32 100644 --- a/plugin.xml +++ b/plugin.xml @@ -161,4 +161,11 @@ + + + + + + + diff --git a/tests/tests.js b/tests/tests.js index f64a4b0..ffbed50 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -19,15 +19,17 @@ * */ -/* global exports, cordova */ -/* global describe, it, expect, beforeEach, afterEach, jasmine, pending, spyOn */ +/*global exports, cordova, FileTransfer, FileTransferError, + FileUploadOptions, LocalFileSystem, requestFileSystem, TEMPORARY */ -/* global FileTransfer, FileTransferError, FileUploadOptions, LocalFileSystem */ +/*global describe, it, expect, beforeEach, afterEach, spyOn, + jasmine, pending*/ exports.defineAutoTests = function () { // constants var GRACE_TIME_DELTA = 300; // in milliseconds + var DEFAULT_FILESYSTEM_SIZE = 1024*50; //filesystem size in bytes var UNKNOWN_HOST = "http://foobar.apache.org"; var HEADERS_ECHO = "http://whatheaders.com"; // NOTE: this site is very useful! @@ -42,6 +44,9 @@ exports.defineAutoTests = function () { return (cordova.platformId === "windows") || (navigator.appVersion.indexOf("MSAppHost/1.0") !== -1); }; + var isBrowser = cordova.platformId === 'browser'; + var isIE = isBrowser && navigator.userAgent.indexOf('Trident') >= 0; + describe('FileTransferError', function () { it('should exist', function () { @@ -140,7 +145,14 @@ exports.defineAutoTests = function () { throw new Error('aborted creating test file \'' + name + '\': ' + evt); }; - writer.write(content + "\n"); + if (cordova.platformId === 'browser') { + // var builder = new BlobBuilder(); + // builder.append(content + '\n'); + var blob = new Blob([content + '\n'], { type: 'text/plain' }); + writer.write(blob); + } else { + writer.write(content + "\n"); + } }, unexpectedCallbacks.fileOperationFail); }, @@ -157,7 +169,12 @@ exports.defineAutoTests = function () { expect(event.total).not.toBeLessThan(event.loaded); expect(event.lengthComputable).toBe(true, 'lengthComputable'); } else { - expect(event.total).toBe(0); + // In IE, when lengthComputable === false, event.total somehow is equal to 2^64 + if (isIE) { + expect(event.total).toBe(Math.pow(2, 64)); + } else { + expect(event.total).toBe(0); + } } }; @@ -176,7 +193,7 @@ exports.defineAutoTests = function () { // signifies a completed async call, each async call needs its own done(), and // therefore its own beforeEach beforeEach(function (done) { - window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, + window.requestFileSystem(LocalFileSystem.PERSISTENT, DEFAULT_FILESYSTEM_SIZE, function (fileSystem) { persistentRoot = fileSystem.root; done(); @@ -188,7 +205,7 @@ exports.defineAutoTests = function () { }); beforeEach(function (done) { - window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, + window.requestFileSystem(LocalFileSystem.TEMPORARY, DEFAULT_FILESYSTEM_SIZE, function (fileSystem) { tempRoot = fileSystem.root; done(); @@ -210,7 +227,7 @@ exports.defineAutoTests = function () { } // but run the implementations of the expected callbacks - for (callback in expectedCallbacks) { + for (callback in expectedCallbacks) { //jshint ignore: line if (expectedCallbacks.hasOwnProperty(callback)) { spyOn(expectedCallbacks, callback).and.callThrough(); } @@ -361,10 +378,8 @@ exports.defineAutoTests = function () { var fileURL = window.location.protocol + '//' + window.location.pathname.replace(/ /g, '%20'); - if (!/^file/.exec(fileURL) && cordova.platformId !== 'blackberry10') { - if (cordova.platformId !== 'windowsphone') - expect(fileURL).toMatch(/^file:/); - else + if (!/^file:/.exec(fileURL) && cordova.platformId !== 'blackberry10') { + if (cordova.platformId === 'windowsphone') expect(fileURL).toMatch(/^x-wmapp0:/); done(); return; @@ -553,6 +568,11 @@ exports.defineAutoTests = function () { it("filetransfer.spec.30 downloaded file entries should have a toNativeURL method", function (done) { + if (cordova.platformId === 'browser') { + pending(); + return; + } + var fileURL = SERVER + "/robots.txt"; var downloadWin = function (entry) { diff --git a/www/browser/FileTransfer.js b/www/browser/FileTransfer.js new file mode 100644 index 0000000..e142222 --- /dev/null +++ b/www/browser/FileTransfer.js @@ -0,0 +1,325 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/*global module, require*/ + +var argscheck = require('cordova/argscheck'), + FileTransferError = require('./FileTransferError'); + +function getParentPath(filePath) { + var pos = filePath.lastIndexOf('/'); + return filePath.substring(0, pos + 1); +} + +function getFileName(filePath) { + var pos = filePath.lastIndexOf('/'); + return filePath.substring(pos + 1); +} + +function getUrlCredentials(urlString) { + var credentialsPattern = /^https?\:\/\/(?:(?:(([^:@\/]*)(?::([^@\/]*))?)?@)?([^:\/?#]*)(?::(\d*))?).*$/, + credentials = credentialsPattern.exec(urlString); + + return credentials && credentials[1]; +} + +function getBasicAuthHeader(urlString) { + var header = null; + + + // 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); + + header = { + name : authHeader, + value : authHeaderValue + }; + } + } + + return header; +} + +function checkURL(url) { + return url.indexOf(' ') === -1 ? true : false; +} + +var idCounter = 0; + +var transfers = {}; + +/** + * FileTransfer uploads a file to a remote server. + * @constructor + */ +var FileTransfer = function() { + this._id = ++idCounter; + this.onprogress = null; // optional callback +}; + +/** + * Given an absolute file path, uploads a file on the device to a remote server + * using a multipart HTTP request. + * @param filePath {String} Full path of the file on the device + * @param server {String} URL of the server to receive the file + * @param successCallback (Function} Callback to be invoked when upload has completed + * @param errorCallback {Function} Callback to be invoked upon error + * @param options {FileUploadOptions} Optional parameters such as file name and mimetype + * @param trustAllHosts {Boolean} Optional trust all hosts (e.g. for self-signed certs), defaults to false + */ +FileTransfer.prototype.upload = function(filePath, server, successCallback, errorCallback, options) { + // check for arguments + argscheck.checkArgs('ssFFO*', 'FileTransfer.upload', arguments); + + // Check if target URL doesn't contain spaces. If contains, it should be escaped first + // (see https://github.com/apache/cordova-plugin-file-transfer/blob/master/doc/index.md#upload) + if (!checkURL(server)) { + errorCallback && errorCallback(new FileTransferError(FileTransferError.INVALID_URL_ERR, filePath, server)); + return; + } + + options = options || {}; + + var fileKey = options.fileKey || "file"; + var fileName = options.fileName || "image.jpg"; + var mimeType = options.mimeType || "image/jpeg"; + var params = options.params || {}; + // var chunkedMode = !!options.chunkedMode; // Not supported + var headers = options.headers || {}; + var httpMethod = options.httpMethod && options.httpMethod.toUpperCase() === "PUT" ? "PUT" : "POST"; + + var basicAuthHeader = getBasicAuthHeader(server); + if (basicAuthHeader) { + server = server.replace(getUrlCredentials(server) + '@', ''); + headers[basicAuthHeader.name] = basicAuthHeader.value; + } + + var that = this; + var xhr = transfers[this._id] = new XMLHttpRequest(); + + var fail = errorCallback && function(code, status, response) { + transfers[this._id] && delete transfers[this._id]; + var error = new FileTransferError(code, filePath, server, status, response); + errorCallback && errorCallback(error); + }; + + window.resolveLocalFileSystemURL(filePath, function(entry) { + entry.file(function(file) { + var reader = new FileReader(); + reader.onloadend = function() { + var blob = new Blob([this.result], {type: mimeType}); + + // Prepare form data to send to server + var fd = new FormData(); + fd.append(fileKey, blob, fileName); + for (var prop in params) { + if (params.hasOwnProperty(prop)) { + fd.append(prop, params[prop]); + } + } + + xhr.open(httpMethod, server); + + // Fill XHR headers + for (var header in headers) { + if (headers.hasOwnProperty(header)) { + xhr.setRequestHeader(header, headers[header]); + } + } + + xhr.onload = function() { + if (this.status === 200) { + var result = new FileUploadResult(); // jshint ignore:line + result.bytesSent = blob.size; + result.responseCode = this.status; + result.response = this.response; + delete transfers[that._id]; + successCallback(result); + } else if (this.status === 404) { + fail(FileTransferError.INVALID_URL_ERR, this.status, this.response); + } else { + fail(FileTransferError.CONNECTION_ERR, this.status, this.response); + } + }; + + xhr.ontimeout = function() { + fail(FileTransferError.CONNECTION_ERR, this.status, this.response); + }; + + xhr.onerror = function() { + fail(FileTransferError.CONNECTION_ERR, this.status, this.response); + }; + + xhr.onabort = function () { + fail(FileTransferError.ABORT_ERR, this.status, this.response); + }; + + xhr.upload.onprogress = function (e) { + that.onprogress && that.onprogress(e); + }; + + xhr.send(fd); + // Special case when transfer already aborted, but XHR isn't sent. + // In this case XHR won't fire an abort event, so we need to check if transfers record + // isn't deleted by filetransfer.abort and if so, call XHR's abort method again + if (!transfers[that._id]) { + xhr.abort(); + } + }; + reader.readAsArrayBuffer(file); + }, function() { + fail(FileTransferError.FILE_NOT_FOUND_ERR); + }); + }, function() { + fail(FileTransferError.FILE_NOT_FOUND_ERR); + }); +}; + +/** + * Downloads a file form a given URL and saves it to the specified directory. + * @param source {String} URL of the server to receive the file + * @param target {String} Full path of the file on the device + * @param successCallback (Function} Callback to be invoked when upload has completed + * @param errorCallback {Function} Callback to be invoked upon error + * @param trustAllHosts {Boolean} Optional trust all hosts (e.g. for self-signed certs), defaults to false + * @param options {FileDownloadOptions} Optional parameters such as headers + */ +FileTransfer.prototype.download = function(source, target, successCallback, errorCallback, trustAllHosts, options) { + argscheck.checkArgs('ssFF*', 'FileTransfer.download', arguments); + + // Check if target URL doesn't contain spaces. If contains, it should be escaped first + // (see https://github.com/apache/cordova-plugin-file-transfer/blob/master/doc/index.md#download) + if (!checkURL(source)) { + errorCallback && errorCallback(new FileTransferError(FileTransferError.INVALID_URL_ERR, source, target)); + return; + } + + options = options || {}; + + var headers = options.headers || {}; + + var basicAuthHeader = getBasicAuthHeader(source); + if (basicAuthHeader) { + source = source.replace(getUrlCredentials(source) + '@', ''); + headers[basicAuthHeader.name] = basicAuthHeader.value; + } + + var that = this; + var xhr = transfers[this._id] = new XMLHttpRequest(); + + var fail = errorCallback && function(code, status, response) { + transfers[that._id] && delete transfers[that._id]; + // In XHR GET reqests we're setting response type to Blob + // but in case of error we need to raise event with plain text response + if (response instanceof Blob) { + var reader = new FileReader(); + reader.readAsText(response); + reader.onloadend = function(e) { + var error = new FileTransferError(code, source, target, status, e.target.result); + errorCallback(error); + }; + } else { + var error = new FileTransferError(code, source, target, status, response); + errorCallback(error); + } + }; + + xhr.onload = function (e) { + + var fileNotFound = function () { + fail(FileTransferError.FILE_NOT_FOUND_ERR); + }; + + var req = e.target; + // req.status === 0 is special case for local files with file:// URI scheme + if ((req.status === 200 || req.status === 0) && req.response) { + window.resolveLocalFileSystemURL(getParentPath(target), function (dir) { + dir.getFile(getFileName(target), {create: true}, function writeFile(entry) { + entry.createWriter(function (fileWriter) { + fileWriter.onwriteend = function (evt) { + if (!evt.target.error) { + entry.filesystemName = entry.filesystem.name; + delete transfers[that._id]; + successCallback && successCallback(entry); + } else { + fail(FileTransferError.FILE_NOT_FOUND_ERR); + } + }; + fileWriter.onerror = function () { + fail(FileTransferError.FILE_NOT_FOUND_ERR); + }; + fileWriter.write(req.response); + }, fileNotFound); + }, fileNotFound); + }, fileNotFound); + } else if (req.status === 404) { + fail(FileTransferError.INVALID_URL_ERR, req.status, req.response); + } else { + fail(FileTransferError.CONNECTION_ERR, req.status, req.response); + } + }; + + xhr.onprogress = function (e) { + that.onprogress && that.onprogress(e); + }; + + xhr.onerror = function () { + fail(FileTransferError.CONNECTION_ERR, this.status, this.response); + }; + + xhr.onabort = function () { + fail(FileTransferError.ABORT_ERR, this.status, this.response); + }; + + xhr.open("GET", source, true); + + for (var header in headers) { + if (headers.hasOwnProperty(header)) { + xhr.setRequestHeader(header, headers[header]); + } + } + + xhr.responseType = "blob"; + + xhr.send(); +}; + +/** + * Aborts the ongoing file transfer on this object. The original error + * callback for the file transfer will be called if necessary. + */ +FileTransfer.prototype.abort = function() { + if (this instanceof FileTransfer) { + if (transfers[this._id]) { + transfers[this._id].abort(); + delete transfers[this._id]; + } + } +}; + +module.exports = FileTransfer;