// (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 { static FILE_NOT_FOUND_ERR = 1; static INVALID_URL_ERR = 2; static CONNECTION_ERR = 3; static ABORT_ERR = 4; static NOT_MODIFIED_ERR = 5; // tslint:disable-next-line: variable-name 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; protected 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): void => { // Use XMLHttpRequest instead of HttpClient to support onprogress and abort. const basicAuthHeader = this.getBasicAuthHeader(source), xhr = new XMLHttpRequest(), isDesktop = this.appProvider.isDesktop(); let 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 (const name in headers) { xhr.setRequestHeader(name, headers[name]); } ( xhr).onprogress = (xhr, ev): void => { if (this.progressListener) { this.progressListener(ev); } }; xhr.onerror = (err): void => { reject(new FileTransferErrorMock(-1, source, target, xhr.status, xhr.statusText, null)); }; xhr.onload = (): void => { // Finished dowloading the file. let response = xhr.response || xhr.responseText; const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0); if (status < 200 || status >= 300) { // Request failed. Try to get the error message. this.parseResponse(response).then((response) => { reject(new FileTransferErrorMock(-1, source, target, xhr.status, response || xhr.statusText, null)); }); return; } 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; // 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(), result = {}; if (headersString) { const headers = headersString.split('\n'); for (const 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; } /** * Same as Javascript's JSON.parse, but it will handle errors. * * @param {string} json JSON text. * @return {any} JSON parsed as object or what it gets. */ protected parseJSON(json: string): any { try { return JSON.parse(json); } catch (ex) { // Error. } return json; } /** * Parse a response, converting it into text and the into an object if needed. * * @param {any} response The response to parse. * @return {Promise} Promise resolved with the parsed response. */ protected parseResponse(response: any): Promise { return new Promise((resolve, reject): void => { if (!response) { resolve(''); } else if (response.toString && response.toString() == '[object Blob]') { // Convert the Blob into text. const reader = new FileReader(); reader.onloadend = (): void => { resolve(reader.result); }; reader.readAsText(response); } else if (response.toString && response.toString() == '[object ArrayBuffer]') { // Convert the ArrayBuffer into text. resolve(String.fromCharCode.apply(null, new Uint8Array(response))); } else { resolve(response); } }).then((response: any) => { return this.parseJSON(response); }); } /** * 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): void => { const basicAuthHeader = this.getBasicAuthHeader(url); let fileKey = null, fileName = null, params = null, headers = null, httpMethod = null; 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; 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 + '"' : ''); } // 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. const xhr = new XMLHttpRequest(); xhr.open(httpMethod || 'POST', url); for (const name in headers) { // Filter "unsafe" headers. if (name != 'Connection') { xhr.setRequestHeader(name, headers[name]); } } xhr.onprogress = (ev: ProgressEvent): any => { if (this.progressListener) { this.progressListener(ev); } }; this.xhr = xhr; this.source = fileUrl; this.target = url; this.reject = reject; xhr.onerror = (): void => { reject(new FileTransferErrorMock(-1, fileUrl, url, xhr.status, xhr.statusText, null)); }; xhr.onload = (): void => { // Finished uploading the file. resolve({ bytesSent: file.size, responseCode: xhr.status, response: xhr.response, headers: this.getHeadersAsObject(xhr) }); }; // Create a form data to send params and the file. const fd = new FormData(); for (const name in params) { fd.append(name, params[name]); } fd.append('file', file); xhr.send(fd); }).catch(reject); }); } }