// (C) Copyright 2015 Martin Dougiamas // // Licensed 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. import { Injectable } from '@angular/core'; import { FileTransfer, FileTransferObject, FileUploadResult, FileTransferError } from '@ionic-native/file-transfer'; import { CoreAppProvider } from '../../../providers/app'; import { CoreFileProvider } from '../../../providers/file'; /** * Mock the File Transfer Error. */ export class FileTransferErrorMock implements FileTransferError { public static FILE_NOT_FOUND_ERR = 1; public static INVALID_URL_ERR = 2; public static CONNECTION_ERR = 3; public static ABORT_ERR = 4; public static NOT_MODIFIED_ERR = 5; constructor(public code: number, public source: string, public target: string, public http_status: number, public body: string, public exception: string) { } }; /** * Emulates the Cordova FileTransfer plugin in desktop apps and in browser. */ @Injectable() export class FileTransferMock extends FileTransfer { constructor(private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider) { super(); } /** * Creates a new FileTransferObjectMock object. * * @return {FileTransferObjectMock} */ create(): FileTransferObjectMock { return new FileTransferObjectMock(this.appProvider, this.fileProvider); } } /** * Emulates the FileTransferObject class in desktop apps and in browser. */ export class FileTransferObjectMock extends FileTransferObject { progressListener: (event: ProgressEvent) => any; source: string; target: string; xhr: XMLHttpRequest; private reject: Function; constructor(private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider) { super(); } /** * Aborts an in-progress transfer. The onerror callback is passed a FileTransferError * object which has an error code of FileTransferError.ABORT_ERR. */ abort(): void { if (this.xhr) { this.xhr.abort(); this.reject(new FileTransferErrorMock(FileTransferErrorMock.ABORT_ERR, this.source, this.target, null, null, null)); } } /** * Downloads a file from server. * * @param {string} source URL of the server to download the file, as encoded by encodeURI(). * @param {string} target Filesystem url representing the file on the device. * @param {boolean} [trustAllHosts] If set to true, it accepts all security certificates. * @param {object} [options] Optional parameters, currently only supports headers. * @returns {Promise} Returns a Promise that resolves to a FileEntry object. */ download(source: string, target: string, trustAllHosts?: boolean, options?: { [s: string]: any; }): Promise { return new Promise((resolve, reject) => { // Use XMLHttpRequest instead of HttpClient to support onprogress and abort. let basicAuthHeader = this.getBasicAuthHeader(source), xhr = new XMLHttpRequest(), isDesktop = this.appProvider.isDesktop(), headers = null; this.xhr = xhr; this.source = source; this.target = target; this.reject = reject; if (basicAuthHeader) { source = source.replace(this.getUrlCredentials(source) + '@', ''); options = options || {}; options.headers = options.headers || {}; options.headers[basicAuthHeader.name] = basicAuthHeader.value; } if (options) { headers = options.headers || null; } // Prepare the request. xhr.open('GET', source, true); xhr.responseType = isDesktop ? 'arraybuffer' : 'blob'; for (let name in headers) { xhr.setRequestHeader(name, headers[name]); } (xhr).onprogress = (xhr, ev) => { if (this.progressListener) { this.progressListener(ev); } }; xhr.onerror = (err) => { reject(new FileTransferError(-1, source, target, xhr.status, xhr.statusText)); }; xhr.onload = () => { // Finished dowloading the file. let response = xhr.response; if (!response) { reject(); } else { const basePath = this.fileProvider.getBasePathInstant(); target = target.replace(basePath, ''); // Remove basePath from the target. target = target.replace(/%20/g, ' '); // Replace all %20 with spaces. if (isDesktop) { // In desktop we need to convert the arraybuffer into a Buffer. response = Buffer.from( new Uint8Array(response)); } this.fileProvider.writeFile(target, response).then(resolve, reject); } }; xhr.send(); }); } /** * Given a URL, check if it has a credentials in it and, if so, return them in a header object. * This code is extracted from Cordova FileTransfer plugin. * * @param {string} urlString The URL to get the credentials from. * @return {any} The header with the credentials, null if no credentials. */ protected getBasicAuthHeader(urlString: string): any { let 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. if (window.btoa) { const credentials = this.getUrlCredentials(urlString); if (credentials) { const authHeader = 'Authorization', authHeaderValue = 'Basic ' + window.btoa(credentials); header = { name: authHeader, value: authHeaderValue }; } } return header; } /** * Given an instance of XMLHttpRequest, get the response headers as an object. * * @param {XMLHttpRequest} xhr XMLHttpRequest instance. * @return {{[s: string]: any}} Object with the headers. */ protected getHeadersAsObject(xhr: XMLHttpRequest) : { [s: string]: any } { const headersString = xhr.getAllResponseHeaders(); let result = {}; if (headersString) { const headers = headersString.split('\n'); for (let i in headers) { const headerString = headers[i], separatorPos = headerString.indexOf(':'); if (separatorPos != -1) { result[headerString.substr(0, separatorPos)] = headerString.substr(separatorPos + 1).trim(); } } } return result; } /** * Get the credentials from a URL. * This code is extracted from Cordova FileTransfer plugin. * * @param {string} urlString The URL to get the credentials from. * @return {string} Retrieved credentials. */ protected getUrlCredentials(urlString: string) : string { const credentialsPattern = /^https?\:\/\/(?:(?:(([^:@\/]*)(?::([^@\/]*))?)?@)?([^:\/?#]*)(?::(\d*))?).*$/, credentials = credentialsPattern.exec(urlString); return credentials && credentials[1]; } /** * Registers a listener that gets called whenever a new chunk of data is transferred. * * @param {Function} listener Listener that takes a progress event. */ onProgress(listener: (event: ProgressEvent) => any): void { this.progressListener = listener; } /** * Sends a file to a server. * * @param {string} fileUrl Filesystem URL representing the file on the device or a data URI. * @param {string} url URL of the server to receive the file, as encoded by encodeURI(). * @param {FileUploadOptions} [options] Optional parameters. * @param {boolean} [trustAllHosts] If set to true, it accepts all security certificates. * @returns {Promise} Promise that resolves to a FileUploadResult and rejects with FileTransferError. */ upload(fileUrl: string, url: string, options?: FileUploadOptions, trustAllHosts?: boolean): Promise { return new Promise((resolve, reject) => { let fileKey = null, fileName = null, mimeType = null, params = null, headers = null, httpMethod = null, basicAuthHeader = this.getBasicAuthHeader(url); if (basicAuthHeader) { url = url.replace(this.getUrlCredentials(url) + '@', ''); options = options || {}; options.headers = options.headers || {}; options.headers[basicAuthHeader.name] = basicAuthHeader.value; } if (options) { fileKey = options.fileKey; fileName = options.fileName; mimeType = options.mimeType; headers = options.headers; httpMethod = options.httpMethod || 'POST'; if (httpMethod.toUpperCase() == "PUT"){ httpMethod = 'PUT'; } else { httpMethod = 'POST'; } if (options.params) { params = options.params; } else { params = {}; } } // Add fileKey and fileName to the headers. headers = headers || {}; if (!headers['Content-Disposition']) { headers['Content-Disposition'] = 'form-data;' + (fileKey ? ' name="' + fileKey + '";' : '') + (fileName ? ' filename="' + fileName + '"' : '') } // For some reason, adding a Content-Type header with the mimeType makes the request fail (it doesn't detect // the token in the params). Don't include this header, and delete it if it's supplied. delete headers['Content-Type']; // Get the file to upload. this.fileProvider.getFile(fileUrl).then((fileEntry) => { return this.fileProvider.getFileObjectFromFileEntry(fileEntry); }).then((file) => { // Use XMLHttpRequest instead of HttpClient to support onprogress and abort. let xhr = new XMLHttpRequest(); xhr.open(httpMethod || 'POST', url); for (let name in headers) { // Filter "unsafe" headers. if (name != 'Connection') { xhr.setRequestHeader(name, headers[name]); } } (xhr).onprogress = (xhr, ev) => { if (this.progressListener) { this.progressListener(ev); } }; this.xhr = xhr; this.source = fileUrl; this.target = url; this.reject = reject; xhr.onerror = () => { reject(new FileTransferError(-1, fileUrl, url, xhr.status, xhr.statusText)); }; xhr.onload = () => { // Finished uploading the file. let result: FileUploadResult = { bytesSent: file.size, responseCode: xhr.status, response: xhr.response, headers: this.getHeadersAsObject(xhr) }; resolve(result); }; // Create a form data to send params and the file. let fd = new FormData(); for (var name in params) { fd.append(name, params[name]); } fd.append('file', file); xhr.send(fd); }).catch(reject); }); } }