MOBILE-2261 ws: Implement WS provider and FT Mock
This commit is contained in:
		
							parent
							
								
									6c19fd1b0e
								
							
						
					
					
						commit
						4089a85f94
					
				
							
								
								
									
										10
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -69,6 +69,11 @@ | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/file/-/file-4.3.3.tgz", | ||||
|       "integrity": "sha512-r273zw1gkgGTmlapyJnh31Yemt1P8u1CnTtiZGr3oC/BdlSvLppR7ONW7KbsAxA31UIDAXG+mXP3EqEP2AtVvw==" | ||||
|     }, | ||||
|     "@ionic-native/file-transfer": { | ||||
|       "version": "4.3.3", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/file-transfer/-/file-transfer-4.3.3.tgz", | ||||
|       "integrity": "sha512-NRs2Nl2+Zzk4tVXESssV9DcYKvzmgUn6e1+MwxJ+QmVoQNmeSPNRnFSQoqE+IEkCjENjK1Yg0XJFfcDBpyCp6Q==" | ||||
|     }, | ||||
|     "@ionic-native/globalization": { | ||||
|       "version": "4.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/@ionic-native/globalization/-/globalization-4.3.2.tgz", | ||||
| @ -4707,6 +4712,11 @@ | ||||
|       "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "ts-md5": { | ||||
|       "version": "1.2.2", | ||||
|       "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.2.2.tgz", | ||||
|       "integrity": "sha512-rTjX9/xTOvAcKC7TIsahiSdeyXgMtKa4wd6iClduCWeVoj41Q/HX5oYLkJzHQktW/DHbBjUatoFj0Q481N/Idw==" | ||||
|     }, | ||||
|     "tsickle": { | ||||
|       "version": "0.21.6", | ||||
|       "resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.21.6.tgz", | ||||
|  | ||||
| @ -37,6 +37,7 @@ | ||||
|     "@ionic-native/clipboard": "^4.3.2", | ||||
|     "@ionic-native/core": "4.3.0", | ||||
|     "@ionic-native/file": "^4.3.3", | ||||
|     "@ionic-native/file-transfer": "^4.3.3", | ||||
|     "@ionic-native/globalization": "^4.3.2", | ||||
|     "@ionic-native/in-app-browser": "^4.3.3", | ||||
|     "@ionic-native/keyboard": "^4.3.2", | ||||
| @ -63,6 +64,7 @@ | ||||
|     "promise.prototype.finally": "^3.0.1", | ||||
|     "rxjs": "5.4.3", | ||||
|     "sw-toolbox": "3.6.0", | ||||
|     "ts-md5": "^1.2.2", | ||||
|     "zone.js": "0.8.18" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|  | ||||
| @ -25,6 +25,7 @@ import { CoreUtilsProvider } from '../providers/utils/utils'; | ||||
| import { CoreMimetypeUtilsProvider } from '../providers/utils/mimetype'; | ||||
| import { CoreInitDelegate } from '../providers/init'; | ||||
| import { CoreFileProvider } from '../providers/file'; | ||||
| import { CoreWSProvider } from '../providers/ws'; | ||||
| 
 | ||||
| // For translate loader. AoT requires an exported function for factories.
 | ||||
| export function createTranslateLoader(http: HttpClient) { | ||||
| @ -70,7 +71,8 @@ export function createTranslateLoader(http: HttpClient) { | ||||
|         CoreUtilsProvider, | ||||
|         CoreMimetypeUtilsProvider, | ||||
|         CoreInitDelegate, | ||||
|         CoreFileProvider | ||||
|         CoreFileProvider, | ||||
|         CoreWSProvider | ||||
|     ] | ||||
| }) | ||||
| export class AppModule { | ||||
|  | ||||
| @ -24,4 +24,5 @@ export class CoreConstants { | ||||
|     public static downloadThreshold = 10485760; // 10MB.
 | ||||
|     public static dontShowError = 'CoreDontShowError'; | ||||
|     public static settingsRichTextEditor = 'CoreSettingsRichTextEditor'; | ||||
|     public static wsTimeout = 30000; | ||||
| } | ||||
|  | ||||
| @ -17,11 +17,14 @@ import { Platform } from 'ionic-angular'; | ||||
| 
 | ||||
| import { Clipboard } from '@ionic-native/clipboard'; | ||||
| import { File } from '@ionic-native/file'; | ||||
| import { FileTransfer } from '@ionic-native/file-transfer'; | ||||
| import { Globalization } from '@ionic-native/globalization'; | ||||
| import { Network } from '@ionic-native/network'; | ||||
| import { Zip } from '@ionic-native/zip'; | ||||
| 
 | ||||
| import { ClipboardMock } from './providers/clipboard'; | ||||
| import { FileMock} from './providers/file'; | ||||
| import { FileMock } from './providers/file'; | ||||
| import { FileTransferMock } from './providers/file-transfer'; | ||||
| import { GlobalizationMock } from './providers/globalization'; | ||||
| import { NetworkMock } from './providers/network'; | ||||
| import { ZipMock } from './providers/zip'; | ||||
| @ -29,6 +32,7 @@ import { InAppBrowser } from '@ionic-native/in-app-browser'; | ||||
| 
 | ||||
| import { CoreEmulatorHelper } from './providers/helper'; | ||||
| import { CoreAppProvider } from '../../providers/app'; | ||||
| import { CoreFileProvider } from '../../providers/file'; | ||||
| import { CoreTextUtilsProvider } from '../../providers/utils/text'; | ||||
| import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype'; | ||||
| import { CoreInitDelegate } from '../../providers/init'; | ||||
| @ -57,6 +61,14 @@ import { CoreInitDelegate } from '../../providers/init'; | ||||
|                 return appProvider.isMobile() ? new File() : new FileMock(appProvider, textUtils); | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             provide: FileTransfer, | ||||
|             deps: [CoreAppProvider, CoreFileProvider], | ||||
|             useFactory: (appProvider: CoreAppProvider, fileProvider: CoreFileProvider) => { | ||||
|                 // Use platform instead of CoreAppProvider to prevent circular dependencies.
 | ||||
|                 return appProvider.isMobile() ? new FileTransfer() : new FileTransferMock(appProvider, fileProvider); | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             provide: Globalization, | ||||
|             deps: [CoreAppProvider], | ||||
|  | ||||
							
								
								
									
										336
									
								
								src/core/emulator/providers/file-transfer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								src/core/emulator/providers/file-transfer.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,336 @@ | ||||
| // (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<any>} Returns a Promise that resolves to a FileEntry object. | ||||
|      */ | ||||
|     download(source: string, target: string, trustAllHosts?: boolean, options?: { [s: string]: any; }): Promise<any> { | ||||
|         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]); | ||||
|             } | ||||
| 
 | ||||
|             (<any>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(<any> 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<FileUploadResult>} Promise that resolves to a FileUploadResult and rejects with FileTransferError. | ||||
|      */ | ||||
|     upload(fileUrl: string, url: string, options?: FileUploadOptions, trustAllHosts?: boolean): Promise<FileUploadResult> { | ||||
|         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]); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 (<any>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); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @ -17,6 +17,7 @@ import { CoreFileProvider } from '../../../providers/file'; | ||||
| import { CoreUtilsProvider } from '../../../providers/utils/utils'; | ||||
| import { File } from '@ionic-native/file'; | ||||
| import { CoreInitDelegate, CoreInitHandler } from '../../../providers/init'; | ||||
| import { FileTransferErrorMock } from './file-transfer'; | ||||
| 
 | ||||
| /** | ||||
|  * Emulates the Cordova Zip plugin in desktop apps and in browser. | ||||
| @ -43,6 +44,7 @@ export class CoreEmulatorHelper implements CoreInitHandler { | ||||
|         promises.push((<any>this.file).load().then((basePath: string) => { | ||||
|             this.fileProvider.setHTMLBasePath(basePath); | ||||
|         })); | ||||
|         (<any>window).FileTransferError = FileTransferErrorMock; | ||||
| 
 | ||||
|         return this.utils.allPromises(promises); | ||||
|     } | ||||
|  | ||||
							
								
								
									
										719
									
								
								src/providers/ws.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										719
									
								
								src/providers/ws.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,719 @@ | ||||
| // (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 { HttpClient } from '@angular/common/http'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { FileTransfer, FileUploadOptions } from '@ionic-native/file-transfer'; | ||||
| import { CoreAppProvider } from './app'; | ||||
| import { CoreFileProvider } from './file'; | ||||
| import { CoreLoggerProvider } from './logger'; | ||||
| import { CoreMimetypeUtilsProvider } from './utils/mimetype'; | ||||
| import { CoreTextUtilsProvider } from './utils/text'; | ||||
| import { CoreUtilsProvider } from './utils/utils'; | ||||
| import { CoreConstants } from '../core/constants'; | ||||
| import { Md5 } from 'ts-md5/dist/md5'; | ||||
| 
 | ||||
| /** | ||||
|  * Interface of the presets accepted by the WS call. | ||||
|  */ | ||||
| export interface CoreWSPreSets { | ||||
|     siteUrl: string; // The site URL.
 | ||||
|     wsToken: string; // The Webservice token.
 | ||||
|     responseExpected?: boolean; // Defaults to true. Set to false when the expected response is null.
 | ||||
|     typeExpected?: string; // Defaults to 'object'. Use it when you expect a type that's not an object|array.
 | ||||
|     cleanUnicode?: boolean; // Defaults to false. Clean multibyte Unicode chars from data.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Interface of the presets accepted by AJAX WS calls. | ||||
|  */ | ||||
| export interface CoreWSAjaxPreSets { | ||||
|     siteUrl: string; // The site URL.
 | ||||
|     responseExpected?: boolean; // Defaults to true. Set to false when the expected response is null.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Interface for WS Errors. | ||||
|  */ | ||||
| export interface CoreWSError { | ||||
|     message: string; // The error message.
 | ||||
|     exception?: string; // Name of the exception. Undefined for local errors (fake WS errors).
 | ||||
|     errorcode?: string; // The error code. Undefined for local errors (fake WS errors).
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Interface for file upload options. | ||||
|  */ | ||||
| export interface CoreWSFileUploadOptions extends FileUploadOptions { | ||||
|     fileArea?: string; // The file area where to put the file. By default, 'draft'.
 | ||||
|     itemId?: number; // Item ID of the area where to put the file. By default, 0.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * This service allows performing WS calls and download/upload files. | ||||
|  */ | ||||
| @Injectable() | ||||
| export class CoreWSProvider { | ||||
|     logger; | ||||
|     mimeTypeCache = {}; // A "cache" to store file mimetypes to prevent performing too many HEAD requests.
 | ||||
|     ongoingCalls = {}; | ||||
|     retryCalls = []; | ||||
|     retryTimeout = 0; | ||||
| 
 | ||||
|     constructor(private http: HttpClient, private translate: TranslateService, private appProvider: CoreAppProvider, | ||||
|             private textUtils: CoreTextUtilsProvider, logger: CoreLoggerProvider, private utils: CoreUtilsProvider, | ||||
|             private fileProvider: CoreFileProvider, private fileTransfer: FileTransfer, private mimeUtils: CoreMimetypeUtilsProvider) { | ||||
|         this.logger = logger.getInstance('CoreWSProvider'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds the call data to an special queue to be processed when retrying. | ||||
|      * | ||||
|      * @param {string} method The WebService method to be called. | ||||
|      * @param {string} siteUrl Complete site url to perform the call. | ||||
|      * @param {any} ajaxData Arguments to pass to the method. | ||||
|      * @param {CoreWSPreSets} preSets Extra settings and information. | ||||
|      * @return {Promise} Deferrend promise resolved with the response data in success and rejected with the error message if it fails. | ||||
|      */ | ||||
|     protected addToRetryQueue(method: string, siteUrl: string, ajaxData: any, preSets: CoreWSPreSets) { | ||||
|         let call = { | ||||
|             method: method, | ||||
|             siteUrl: siteUrl, | ||||
|             ajaxData: ajaxData, | ||||
|             preSets: preSets, | ||||
|             deferred: this.utils.promiseDefer(), | ||||
|         }; | ||||
| 
 | ||||
|         this.retryCalls.push(call); | ||||
|         return call.deferred.promise; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A wrapper function for a moodle WebService call. | ||||
|      * | ||||
|      * @param {string} method The WebService method to be called. | ||||
|      * @param {any} data Arguments to pass to the method. | ||||
|      * @param {CoreWSPreSets} preSets Extra settings and information. | ||||
|      * @return {Promise} Promise resolved with the response data in success and rejected with the error message if it fails. | ||||
|      */ | ||||
|     call(method: string, data: any, preSets: CoreWSPreSets) { | ||||
| 
 | ||||
|         let siteUrl; | ||||
| 
 | ||||
|         if (!preSets) { | ||||
|             return Promise.reject(this.createFakeWSError('mm.core.unexpectederror', true)); | ||||
|         } else if (!this.appProvider.isOnline()) { | ||||
|             return Promise.reject(this.createFakeWSError('mm.core.networkerrormsg', true)); | ||||
|         } | ||||
| 
 | ||||
|         preSets.typeExpected = preSets.typeExpected || 'object'; | ||||
|         if (typeof preSets.responseExpected == 'undefined') { | ||||
|             preSets.responseExpected = true; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             data = this.convertValuesToString(data, preSets.cleanUnicode); | ||||
|         } catch (e) { | ||||
|             // Empty cleaned text found.
 | ||||
|             return Promise.reject(this.createFakeWSError('mm.core.unicodenotsupportedcleanerror', true)); | ||||
|         } | ||||
| 
 | ||||
|         data.wsfunction = method; | ||||
|         data.wstoken = preSets.wsToken; | ||||
|         siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json'; | ||||
| 
 | ||||
|         let promise = this.getPromiseHttp('post', preSets.siteUrl, data); | ||||
| 
 | ||||
|         if (!promise) { | ||||
|             // There are some ongoing retry calls, wait for timeout.
 | ||||
|             if (this.retryCalls.length > 0) { | ||||
|                 this.logger.warn('Calls locked, trying later...'); | ||||
|                 promise = this.addToRetryQueue(method, siteUrl, data, preSets); | ||||
|             } else { | ||||
|                 promise = this.performPost(method, siteUrl, data, preSets); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return promise; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Call a Moodle WS using the AJAX API. Please use it if the WS layer is not an option. | ||||
|      * | ||||
|      * @param {string} method The WebService method to be called. | ||||
|      * @param {any} data Arguments to pass to the method. | ||||
|      * @param {CoreWSAjaxPreSets} preSets Extra settings and information. Only some | ||||
|      * @return {Promise}       Promise resolved with the response data in success and rejected with an object containing: | ||||
|      *                                 - error: Error message. | ||||
|      *                                 - errorcode: Error code returned by the site (if any). | ||||
|      *                                 - available: 0 if unknown, 1 if available, -1 if not available. | ||||
|      */ | ||||
|     callAjax(method: string, data: any, preSets: CoreWSAjaxPreSets) { | ||||
|         let siteUrl, | ||||
|             ajaxData; | ||||
| 
 | ||||
|         if (typeof preSets.siteUrl == 'undefined') { | ||||
|             return rejectWithError(this.translate.instant('mm.core.unexpectederror')); | ||||
|         } else if (!this.appProvider.isOnline()) { | ||||
|             return rejectWithError(this.translate.instant('mm.core.networkerrormsg')); | ||||
|         } | ||||
| 
 | ||||
|         if (typeof preSets.responseExpected == 'undefined') { | ||||
|             preSets.responseExpected = true; | ||||
|         } | ||||
| 
 | ||||
|         ajaxData = [{ | ||||
|             index: 0, | ||||
|             methodname: method, | ||||
|             args: this.convertValuesToString(data) | ||||
|         }]; | ||||
| 
 | ||||
|         siteUrl = preSets.siteUrl + '/lib/ajax/service.php'; | ||||
| 
 | ||||
|         return new Promise((resolve, reject) => { | ||||
|             this.http.post(siteUrl, JSON.stringify(ajaxData)).timeout(CoreConstants.wsTimeout).subscribe((data: any) => { | ||||
|                 // Some moodle web services return null. If the responseExpected value is set then so long as no data
 | ||||
|                 // is returned, we create a blank object.
 | ||||
|                 if (!data && !preSets.responseExpected) { | ||||
|                     data = [{}]; | ||||
|                 } | ||||
| 
 | ||||
|                 // Check if error. Ajax layer should always return an object (if error) or an array (if success).
 | ||||
|                 if (!data || typeof data != 'object') { | ||||
|                     return rejectWithError(this.translate.instant('mm.core.serverconnection')); | ||||
|                 } else if (data.error) { | ||||
|                     return rejectWithError(data.error, data.errorcode); | ||||
|                 } | ||||
| 
 | ||||
|                 // Get the first response since only one request was done.
 | ||||
|                 data = data[0]; | ||||
| 
 | ||||
|                 if (data.error) { | ||||
|                     return rejectWithError(data.exception.message, data.exception.errorcode); | ||||
|                 } | ||||
| 
 | ||||
|                 return data.data; | ||||
|             }, (data) => { | ||||
|                 let available = data.status == 404 ? -1 : 0; | ||||
|                 return rejectWithError(this.translate.instant('mm.core.serverconnection'), '', available); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         // Convenience function to return an error.
 | ||||
|         function rejectWithError(message: string, code?: string, available?: number) { | ||||
|             if (typeof available == 'undefined') { | ||||
|                 if (code) { | ||||
|                     available = code == 'invalidrecord' ? -1 : 1; | ||||
|                 } else { | ||||
|                     available = 0; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return Promise.reject({ | ||||
|                 error: message, | ||||
|                 errorcode: code, | ||||
|                 available: available | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Converts an objects values to strings where appropriate. | ||||
|      * Arrays (associative or otherwise) will be maintained. | ||||
|      * | ||||
|      * @param {object} data The data that needs all the non-object values set to strings. | ||||
|      * @param {boolean} [stripUnicode] If Unicode long chars need to be stripped. | ||||
|      * @return {object} The cleaned object, with multilevel array and objects preserved. | ||||
|      */ | ||||
|     protected convertValuesToString(data: object, stripUnicode?: boolean) : object { | ||||
|         let result; | ||||
|         if (!Array.isArray(data) && typeof data == 'object') { | ||||
|             result = {}; | ||||
|         } else { | ||||
|             result = []; | ||||
|         } | ||||
| 
 | ||||
|         for (let el in data) { | ||||
|             if (typeof data[el] == 'object') { | ||||
|                 result[el] = this.convertValuesToString(data[el], stripUnicode); | ||||
|             } else { | ||||
|                 if (typeof data[el] == 'string') { | ||||
|                     result[el] = stripUnicode ? this.textUtils.stripUnicode(data[el]) : data[el]; | ||||
|                     if (stripUnicode && data[el] != result[el] && result[el].trim().length == 0) { | ||||
|                         throw new Error(); | ||||
|                     } | ||||
|                 } else { | ||||
|                     result[el] = data[el] + ''; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a "fake" WS error for local errors. | ||||
|      * | ||||
|      * @param {string} message The message to include in the error. | ||||
|      * @param {boolean} [needsTranslate] If the message needs to be translated. | ||||
|      * @return {CoreWSError} Fake WS error. | ||||
|      */ | ||||
|     createFakeWSError(message: string, needsTranslate?: boolean) : CoreWSError { | ||||
|         if (needsTranslate) { | ||||
|             message = this.translate.instant(message); | ||||
|         } | ||||
|         return { | ||||
|             message: message | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Downloads a file from Moodle using Cordova File API. | ||||
|      * | ||||
|      * @param {string} url Download url. | ||||
|      * @param {string} path Local path to store the file. | ||||
|      * @param {boolean} addExtension True if extension need to be added to the final path. | ||||
|      * @return {Promise<FileEntry>} Promise resolved with the downloaded file. | ||||
|      */ | ||||
|     downloadFile(url: string, path: string, addExtension?: boolean) : Promise<FileEntry> { | ||||
|         this.logger.debug('Downloading file', url, path, addExtension); | ||||
| 
 | ||||
|         if (!this.appProvider.isOnline()) { | ||||
|             return Promise.reject(this.translate.instant('mm.core.networkerrormsg')); | ||||
|         } | ||||
| 
 | ||||
|         // Use a tmp path to download the file and then move it to final location. This is because if the download fails,
 | ||||
|         // the local file is deleted.
 | ||||
|         let tmpPath = path + '.tmp'; | ||||
| 
 | ||||
|         // Create the tmp file as an empty file.
 | ||||
|         return this.fileProvider.createFile(tmpPath).then((fileEntry) => { | ||||
|             let transfer = this.fileTransfer.create(); | ||||
|             return transfer.download(url, fileEntry.toURL(), true).then(() => { | ||||
|                 let promise; | ||||
| 
 | ||||
|                 if (addExtension) { | ||||
|                     let ext = this.mimeUtils.getFileExtension(path); | ||||
| 
 | ||||
|                     // Google Drive extensions will be considered invalid since Moodle usually converts them.
 | ||||
|                     if (!ext || ext == 'gdoc' || ext == 'gsheet' || ext == 'gslides' || ext == 'gdraw') { | ||||
|                         // Not valid, get the file's mimetype.
 | ||||
|                         promise = this.getRemoteFileMimeType(url).then((mime) => { | ||||
|                             if (mime) { | ||||
|                                 let remoteExt = this.mimeUtils.getExtension(mime, url); | ||||
|                                 // If the file is from Google Drive, ignore mimetype application/json (sometimes pluginfile
 | ||||
|                                 // returns an invalid mimetype for files).
 | ||||
|                                 if (remoteExt && (!ext || mime != 'application/json')) { | ||||
|                                     if (ext) { | ||||
|                                         // Remove existing extension since we will use another one.
 | ||||
|                                         path = this.mimeUtils.removeExtension(path); | ||||
|                                     } | ||||
|                                     path += '.' + remoteExt; | ||||
|                                     return remoteExt; | ||||
|                                 } | ||||
|                             } | ||||
|                             return ext; | ||||
|                         }); | ||||
|                     } else { | ||||
|                         promise = Promise.resolve(ext); | ||||
|                     } | ||||
|                 } else { | ||||
|                     promise = Promise.resolve(''); | ||||
|                 } | ||||
| 
 | ||||
|                 return promise.then((extension) => { | ||||
|                     return this.fileProvider.moveFile(tmpPath, path).then((movedEntry) => { | ||||
|                         // Save the extension.
 | ||||
|                         movedEntry.extension = extension; | ||||
|                         movedEntry.path = path; | ||||
|                         this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`); | ||||
|                         return movedEntry; | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }).catch((err) => { | ||||
|             this.logger.error(`Error downloading ${url} to ${path}`, JSON.stringify(err)); | ||||
|             return Promise.reject(err); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a promise from the cache. | ||||
|      * | ||||
|      * @param {string} method Method of the HTTP request. | ||||
|      * @param {string} url Base URL of the HTTP request. | ||||
|      * @param {any} [params] Params of the HTTP request. | ||||
|      */ | ||||
|     protected getPromiseHttp(method: string, url: string, params?: any) : any { | ||||
|         let queueItemId = this.getQueueItemId(method, url, params); | ||||
|         if (typeof this.ongoingCalls[queueItemId] != 'undefined') { | ||||
|             return this.ongoingCalls[queueItemId]; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform a HEAD request to get the mimetype of a remote file. | ||||
|      * | ||||
|      * @param {string} url File URL. | ||||
|      * @param {boolean} [ignoreCache] True to ignore cache, false otherwise. | ||||
|      * @return {Promise<string>} Promise resolved with the mimetype or '' if failure. | ||||
|      */ | ||||
|     getRemoteFileMimeType(url: string, ignoreCache?: boolean) : Promise<string> { | ||||
|         if (this.mimeTypeCache[url] && !ignoreCache) { | ||||
|             return Promise.resolve(this.mimeTypeCache[url]); | ||||
|         } | ||||
| 
 | ||||
|         return this.performHead(url).then((data) => { | ||||
|             let mimeType = data.headers('Content-Type'); | ||||
|             if (mimeType) { | ||||
|                 // Remove "parameters" like charset.
 | ||||
|                 mimeType = mimeType.split(';')[0]; | ||||
|             } | ||||
|             this.mimeTypeCache[url] = mimeType; | ||||
| 
 | ||||
|             return mimeType || ''; | ||||
|         }).catch(() => { | ||||
|             // Error, resolve with empty mimetype.
 | ||||
|             return ''; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform a HEAD request to get the size of a remote file. | ||||
|      * | ||||
|      * @param {string} url File URL. | ||||
|      * @return {Promise}   Promise resolved with the size or -1 if failure. | ||||
|      */ | ||||
|     getRemoteFileSize(url: string) : Promise<number> { | ||||
|         return this.performHead(url).then((data) => { | ||||
|             let size = parseInt(data.headers('Content-Length'), 10); | ||||
| 
 | ||||
|             if (size) { | ||||
|                 return size; | ||||
|             } | ||||
|             return -1; | ||||
|         }).catch(() => { | ||||
|             // Error, return -1.
 | ||||
|             return -1; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the unique queue item id of the cache for a HTTP request. | ||||
|      * | ||||
|      * @param {string} method Method of the HTTP request. | ||||
|      * @param {string} url Base URL of the HTTP request. | ||||
|      * @param {object} [params] Params of the HTTP request. | ||||
|      */ | ||||
|     protected getQueueItemId(method: string, url: string, params?: any) { | ||||
|         if (params) { | ||||
|             url += '###' + this.utils.serialize(params); | ||||
|         } | ||||
|         return method + '#' + Md5.hashAsciiStr(url); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform a HEAD request and save the promise while waiting to be resolved. | ||||
|      * | ||||
|      * @param {string} url URL to perform the request. | ||||
|      * @return {Promise<any>} Promise resolved with the response. | ||||
|      */ | ||||
|     performHead(url: string) : Promise<any> { | ||||
|         let promise = this.getPromiseHttp('head', url); | ||||
| 
 | ||||
|         if (!promise) { | ||||
|             promise = new Promise((resolve, reject) => { | ||||
|                 this.http.head(url).timeout(CoreConstants.wsTimeout).subscribe(resolve, reject); | ||||
|             }); | ||||
| 
 | ||||
|             this.setPromiseHttp(promise, 'head', url); | ||||
|         } | ||||
| 
 | ||||
|         return promise; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the post call and save the promise while waiting to be resolved. | ||||
|      * | ||||
|      * @param {string} method The WebService method to be called. | ||||
|      * @param {string} siteUrl Complete site url to perform the call. | ||||
|      * @param {any} ajaxData Arguments to pass to the method. | ||||
|      * @param {CoreWSPreSets} preSets Extra settings and information. | ||||
|      * @return {Promise<any>} Promise resolved with the response data in success and rejected with CoreWSError if it fails. | ||||
|      */ | ||||
|     performPost(method: string, siteUrl: string, ajaxData: any, preSets: CoreWSPreSets) : Promise<any> { | ||||
|         // Create the promise for the request.
 | ||||
|         let promise = new Promise((resolve, reject) => { | ||||
| 
 | ||||
|             this.http.post(siteUrl, ajaxData).timeout(CoreConstants.wsTimeout).subscribe((data: any) => { | ||||
|                 // Some moodle web services return null.
 | ||||
|                 // If the responseExpected value is set to false, we create a blank object if the response is null.
 | ||||
|                 if (!data && !preSets.responseExpected) { | ||||
|                     data = {}; | ||||
|                 } | ||||
| 
 | ||||
|                 if (!data) { | ||||
|                     return Promise.reject(this.createFakeWSError('mm.core.serverconnection', true)); | ||||
|                 } else if (typeof data != preSets.typeExpected) { | ||||
|                     this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); | ||||
|                     return Promise.reject(this.createFakeWSError('mm.core.errorinvalidresponse', true)); | ||||
|                 } | ||||
| 
 | ||||
|                 if (typeof data.exception !== 'undefined') { | ||||
|                     return Promise.reject(data); | ||||
|                 } | ||||
| 
 | ||||
|                 if (typeof data.debuginfo != 'undefined') { | ||||
|                     return Promise.reject(this.createFakeWSError('Error. ' + data.message)); | ||||
|                 } | ||||
| 
 | ||||
|                 return data; | ||||
|             }, (error) => { | ||||
|                 // If server has heavy load, retry after some seconds.
 | ||||
|                 if (error.status == 429) { | ||||
|                     let retryPromise = this.addToRetryQueue(method, siteUrl, ajaxData, preSets); | ||||
| 
 | ||||
|                     // Only process the queue one time.
 | ||||
|                     if (this.retryTimeout == 0) { | ||||
|                         this.retryTimeout = parseInt(error.headers('Retry-After'), 10) || 5; | ||||
|                         this.logger.warn(`${error.statusText}. Retrying in ${this.retryTimeout} seconds. ` + | ||||
|                                         `${this.retryCalls.length} calls left.`); | ||||
| 
 | ||||
|                         setTimeout(() => { | ||||
|                             this.logger.warn(`Retrying now with ${this.retryCalls.length} calls to process.`); | ||||
|                             // Finish timeout.
 | ||||
|                             this.retryTimeout = 0; | ||||
|                             this.processRetryQueue(); | ||||
|                         }, this.retryTimeout * 1000); | ||||
|                     } else { | ||||
|                         this.logger.warn('Calls locked, trying later...'); | ||||
|                     } | ||||
| 
 | ||||
|                     return retryPromise; | ||||
|                 } | ||||
| 
 | ||||
|                 return Promise.reject(this.createFakeWSError('mm.core.serverconnection', true)); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         this.setPromiseHttp(promise, 'post', preSets.siteUrl, ajaxData); | ||||
| 
 | ||||
|         return promise; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry all requests in the queue. | ||||
|      * This function uses recursion in order to add a delay between requests to reduce stress. | ||||
|      */ | ||||
|     protected processRetryQueue() : void { | ||||
|         if (this.retryCalls.length > 0 && this.retryTimeout == 0) { | ||||
|             let call = this.retryCalls.shift(); | ||||
|             // Add a delay between calls.
 | ||||
|             setTimeout(() => { | ||||
|                 call.deferred.resolve(this.performPost(call.method, call.siteUrl, call.ajaxData, call.preSets)); | ||||
|                 this.processRetryQueue(); | ||||
|             }, 200); | ||||
|         } else { | ||||
|             this.logger.warn(`Retry queue has stopped with ${this.retryCalls.length} calls and ${this.retryTimeout} timeout secs.`); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Save promise on the cache. | ||||
|      * | ||||
|      * @param {Promise<any>} promise Promise to be saved. | ||||
|      * @param {string} method Method of the HTTP request. | ||||
|      * @param {string} url Base URL of the HTTP request. | ||||
|      * @param {any} [params] Params of the HTTP request. | ||||
|      */ | ||||
|     protected setPromiseHttp(promise: Promise<any>, method: string, url: string, params?: any) : void { | ||||
|         let timeout, | ||||
|             queueItemId = this.getQueueItemId(method, url, params); | ||||
| 
 | ||||
|         this.ongoingCalls[queueItemId] = promise; | ||||
| 
 | ||||
|         // HTTP not finished, but we should delete the promise after timeout.
 | ||||
|         timeout = setTimeout(() => { | ||||
|             delete this.ongoingCalls[queueItemId]; | ||||
|         }, CoreConstants.wsTimeout); | ||||
| 
 | ||||
|         // HTTP finished, delete from ongoing.
 | ||||
|         this.ongoingCalls[queueItemId].finally(() => { | ||||
|             delete this.ongoingCalls[queueItemId]; | ||||
| 
 | ||||
|             clearTimeout(timeout); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A wrapper function for a synchronous Moodle WebService call. | ||||
|      * Warning: This function should only be used if synchronous is a must. It's recommended to use call. | ||||
|      * | ||||
|      * @param {string} method The WebService method to be called. | ||||
|      * @param {any} data Arguments to pass to the method. | ||||
|      * @param {CoreWSPreSets} preSets Extra settings and information. | ||||
|      * @return {Promise} Promise resolved with the response data in success and rejected with the error message if it fails. | ||||
|      * @return {any} Request response. If the request fails, returns an object with 'error'=true and 'message' properties. | ||||
|      */ | ||||
|     syncCall(method: string, data: any, preSets: CoreWSPreSets) : any { | ||||
|         let siteUrl, | ||||
|             xhr, | ||||
|             errorResponse = { | ||||
|                 error: true, | ||||
|                 message: '' | ||||
|             }; | ||||
| 
 | ||||
|         if (!preSets) { | ||||
|             errorResponse.message = this.translate.instant('mm.core.unexpectederror'); | ||||
|             return errorResponse; | ||||
|         } else if (!this.appProvider.isOnline()) { | ||||
|             errorResponse.message = this.translate.instant('mm.core.networkerrormsg'); | ||||
|             return errorResponse; | ||||
|         } | ||||
| 
 | ||||
|         preSets.typeExpected = preSets.typeExpected || 'object'; | ||||
|         if (typeof preSets.responseExpected == 'undefined') { | ||||
|             preSets.responseExpected = true; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             data = this.convertValuesToString(data, preSets.cleanUnicode); | ||||
|         } catch (e) { | ||||
|             // Empty cleaned text found.
 | ||||
|             errorResponse.message = this.translate.instant('mm.core.unicodenotsupportedcleanerror'); | ||||
|             return errorResponse; | ||||
|         } | ||||
| 
 | ||||
|         data.wsfunction = method; | ||||
|         data.wstoken = preSets.wsToken; | ||||
|         siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json'; | ||||
| 
 | ||||
|         // Serialize data.
 | ||||
|         data = this.utils.serialize(data); | ||||
| 
 | ||||
|         // Perform sync request using XMLHttpRequest.
 | ||||
|         xhr = new (<any>window).XMLHttpRequest(); | ||||
|         xhr.open('post', siteUrl, false); | ||||
|         xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8'); | ||||
| 
 | ||||
|         xhr.send(data); | ||||
| 
 | ||||
|         // Get response.
 | ||||
|         data = ('response' in xhr) ? xhr.response : xhr.responseText; | ||||
| 
 | ||||
|         // Check status.
 | ||||
|         xhr.status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0); | ||||
|         if (xhr.status < 200 || xhr.status >= 300) { | ||||
|             // Request failed.
 | ||||
|             errorResponse.message = data; | ||||
|             return errorResponse; | ||||
|         } | ||||
| 
 | ||||
|         // Treat response.
 | ||||
|         try { | ||||
|             data = JSON.parse(data); | ||||
|         } catch(ex) {} | ||||
| 
 | ||||
|         // Some moodle web services return null.
 | ||||
|         // If the responseExpected value is set then so long as no data is returned, we create a blank object.
 | ||||
|         if ((!data || !data.data) && !preSets.responseExpected) { | ||||
|             data = {}; | ||||
|         } | ||||
| 
 | ||||
|         if (!data) { | ||||
|             errorResponse.message = this.translate.instant('mm.core.serverconnection'); | ||||
|         } else if (typeof data != preSets.typeExpected) { | ||||
|             this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"'); | ||||
|             errorResponse.message = this.translate.instant('mm.core.errorinvalidresponse'); | ||||
|         } | ||||
| 
 | ||||
|         if (typeof data.exception != 'undefined' || typeof data.debuginfo != 'undefined') { | ||||
|             errorResponse.message = data.message; | ||||
|         } | ||||
| 
 | ||||
|         if (errorResponse.message !== '') { | ||||
|             return errorResponse; | ||||
|         } | ||||
| 
 | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
|     /* | ||||
|      * Uploads a file. | ||||
|      * | ||||
|      * @param {string} filePath File path. | ||||
|      * @param {CoreWSFileUploadOptions} options File upload options. | ||||
|      * @param {CoreWSPreSets} preSets Must contain siteUrl and wsToken. | ||||
|      * @return {Promise<any>} Promise resolved when uploaded. | ||||
|      */ | ||||
|     uploadFile(filePath: string, options: CoreWSFileUploadOptions, preSets: CoreWSPreSets) : Promise<any> { | ||||
|         this.logger.debug(`Trying to upload file: ${filePath}`); | ||||
| 
 | ||||
|         if (!filePath || !options || !preSets) { | ||||
|             return Promise.reject(null); | ||||
|         } | ||||
| 
 | ||||
|         if (!this.appProvider.isOnline()) { | ||||
|             return Promise.reject(this.translate.instant('mm.core.networkerrormsg')); | ||||
|         } | ||||
| 
 | ||||
|         let uploadUrl = preSets.siteUrl + '/webservice/upload.php', | ||||
|             transfer = this.fileTransfer.create(); | ||||
| 
 | ||||
|         options.httpMethod = 'POST'; | ||||
|         options.params = { | ||||
|             token: preSets.wsToken, | ||||
|             filearea: options.fileArea || 'draft', | ||||
|             itemid: options.itemId || 0 | ||||
|         }; | ||||
|         options.chunkedMode = false; | ||||
|         options.headers = { | ||||
|             Connection: "close" | ||||
|         }; | ||||
| 
 | ||||
|         return transfer.upload(filePath, uploadUrl, options, true).then((success) => { | ||||
|             let data: any = success.response; | ||||
|             try { | ||||
|                 data = JSON.parse(data); | ||||
|             } catch(err) { | ||||
|                 this.logger.error('Error parsing response from upload:', err, data); | ||||
|                 return Promise.reject(this.translate.instant('mm.core.errorinvalidresponse')); | ||||
|             } | ||||
| 
 | ||||
|             if (!data) { | ||||
|                 return Promise.reject(this.translate.instant('mm.core.serverconnection')); | ||||
|             } else if (typeof data != 'object') { | ||||
|                 this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"'); | ||||
|                 return Promise.reject(this.translate.instant('mm.core.errorinvalidresponse')); | ||||
|             } | ||||
| 
 | ||||
|             if (typeof data.exception !== 'undefined') { | ||||
|                 return Promise.reject(data.message); | ||||
|             } else if (data && typeof data.error !== 'undefined') { | ||||
|                 return Promise.reject(data.error); | ||||
|             } else if (data[0] && typeof data[0].error !== 'undefined') { | ||||
|                 return Promise.reject(data[0].error); | ||||
|             } | ||||
| 
 | ||||
|             // We uploaded only 1 file, so we only return the first file returned.
 | ||||
|             this.logger.debug('Successfully uploaded file', filePath); | ||||
|             return data[0]; | ||||
|         }, (error) => { | ||||
|             this.logger.error('Error while uploading file', filePath, error.exception); | ||||
|             return Promise.reject(this.translate.instant('mm.core.errorinvalidresponse')); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user