MOBILE-2261 ws: Implement WS provider and FT Mock
parent
6c19fd1b0e
commit
4089a85f94
|
@ -69,6 +69,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@ionic-native/file/-/file-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic-native/file/-/file-4.3.3.tgz",
|
||||||
"integrity": "sha512-r273zw1gkgGTmlapyJnh31Yemt1P8u1CnTtiZGr3oC/BdlSvLppR7ONW7KbsAxA31UIDAXG+mXP3EqEP2AtVvw=="
|
"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": {
|
"@ionic-native/globalization": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic-native/globalization/-/globalization-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic-native/globalization/-/globalization-4.3.2.tgz",
|
||||||
|
@ -4707,6 +4712,11 @@
|
||||||
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
|
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
|
||||||
"dev": true
|
"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": {
|
"tsickle": {
|
||||||
"version": "0.21.6",
|
"version": "0.21.6",
|
||||||
"resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.21.6.tgz",
|
"resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.21.6.tgz",
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
"@ionic-native/clipboard": "^4.3.2",
|
"@ionic-native/clipboard": "^4.3.2",
|
||||||
"@ionic-native/core": "4.3.0",
|
"@ionic-native/core": "4.3.0",
|
||||||
"@ionic-native/file": "^4.3.3",
|
"@ionic-native/file": "^4.3.3",
|
||||||
|
"@ionic-native/file-transfer": "^4.3.3",
|
||||||
"@ionic-native/globalization": "^4.3.2",
|
"@ionic-native/globalization": "^4.3.2",
|
||||||
"@ionic-native/in-app-browser": "^4.3.3",
|
"@ionic-native/in-app-browser": "^4.3.3",
|
||||||
"@ionic-native/keyboard": "^4.3.2",
|
"@ionic-native/keyboard": "^4.3.2",
|
||||||
|
@ -63,6 +64,7 @@
|
||||||
"promise.prototype.finally": "^3.0.1",
|
"promise.prototype.finally": "^3.0.1",
|
||||||
"rxjs": "5.4.3",
|
"rxjs": "5.4.3",
|
||||||
"sw-toolbox": "3.6.0",
|
"sw-toolbox": "3.6.0",
|
||||||
|
"ts-md5": "^1.2.2",
|
||||||
"zone.js": "0.8.18"
|
"zone.js": "0.8.18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { CoreUtilsProvider } from '../providers/utils/utils';
|
||||||
import { CoreMimetypeUtilsProvider } from '../providers/utils/mimetype';
|
import { CoreMimetypeUtilsProvider } from '../providers/utils/mimetype';
|
||||||
import { CoreInitDelegate } from '../providers/init';
|
import { CoreInitDelegate } from '../providers/init';
|
||||||
import { CoreFileProvider } from '../providers/file';
|
import { CoreFileProvider } from '../providers/file';
|
||||||
|
import { CoreWSProvider } from '../providers/ws';
|
||||||
|
|
||||||
// For translate loader. AoT requires an exported function for factories.
|
// For translate loader. AoT requires an exported function for factories.
|
||||||
export function createTranslateLoader(http: HttpClient) {
|
export function createTranslateLoader(http: HttpClient) {
|
||||||
|
@ -70,7 +71,8 @@ export function createTranslateLoader(http: HttpClient) {
|
||||||
CoreUtilsProvider,
|
CoreUtilsProvider,
|
||||||
CoreMimetypeUtilsProvider,
|
CoreMimetypeUtilsProvider,
|
||||||
CoreInitDelegate,
|
CoreInitDelegate,
|
||||||
CoreFileProvider
|
CoreFileProvider,
|
||||||
|
CoreWSProvider
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
|
|
|
@ -24,4 +24,5 @@ export class CoreConstants {
|
||||||
public static downloadThreshold = 10485760; // 10MB.
|
public static downloadThreshold = 10485760; // 10MB.
|
||||||
public static dontShowError = 'CoreDontShowError';
|
public static dontShowError = 'CoreDontShowError';
|
||||||
public static settingsRichTextEditor = 'CoreSettingsRichTextEditor';
|
public static settingsRichTextEditor = 'CoreSettingsRichTextEditor';
|
||||||
|
public static wsTimeout = 30000;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,14 @@ import { Platform } from 'ionic-angular';
|
||||||
|
|
||||||
import { Clipboard } from '@ionic-native/clipboard';
|
import { Clipboard } from '@ionic-native/clipboard';
|
||||||
import { File } from '@ionic-native/file';
|
import { File } from '@ionic-native/file';
|
||||||
|
import { FileTransfer } from '@ionic-native/file-transfer';
|
||||||
import { Globalization } from '@ionic-native/globalization';
|
import { Globalization } from '@ionic-native/globalization';
|
||||||
import { Network } from '@ionic-native/network';
|
import { Network } from '@ionic-native/network';
|
||||||
import { Zip } from '@ionic-native/zip';
|
import { Zip } from '@ionic-native/zip';
|
||||||
|
|
||||||
import { ClipboardMock } from './providers/clipboard';
|
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 { GlobalizationMock } from './providers/globalization';
|
||||||
import { NetworkMock } from './providers/network';
|
import { NetworkMock } from './providers/network';
|
||||||
import { ZipMock } from './providers/zip';
|
import { ZipMock } from './providers/zip';
|
||||||
|
@ -29,6 +32,7 @@ import { InAppBrowser } from '@ionic-native/in-app-browser';
|
||||||
|
|
||||||
import { CoreEmulatorHelper } from './providers/helper';
|
import { CoreEmulatorHelper } from './providers/helper';
|
||||||
import { CoreAppProvider } from '../../providers/app';
|
import { CoreAppProvider } from '../../providers/app';
|
||||||
|
import { CoreFileProvider } from '../../providers/file';
|
||||||
import { CoreTextUtilsProvider } from '../../providers/utils/text';
|
import { CoreTextUtilsProvider } from '../../providers/utils/text';
|
||||||
import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype';
|
import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype';
|
||||||
import { CoreInitDelegate } from '../../providers/init';
|
import { CoreInitDelegate } from '../../providers/init';
|
||||||
|
@ -57,6 +61,14 @@ import { CoreInitDelegate } from '../../providers/init';
|
||||||
return appProvider.isMobile() ? new File() : new FileMock(appProvider, textUtils);
|
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,
|
provide: Globalization,
|
||||||
deps: [CoreAppProvider],
|
deps: [CoreAppProvider],
|
||||||
|
|
|
@ -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 { CoreUtilsProvider } from '../../../providers/utils/utils';
|
||||||
import { File } from '@ionic-native/file';
|
import { File } from '@ionic-native/file';
|
||||||
import { CoreInitDelegate, CoreInitHandler } from '../../../providers/init';
|
import { CoreInitDelegate, CoreInitHandler } from '../../../providers/init';
|
||||||
|
import { FileTransferErrorMock } from './file-transfer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emulates the Cordova Zip plugin in desktop apps and in browser.
|
* 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) => {
|
promises.push((<any>this.file).load().then((basePath: string) => {
|
||||||
this.fileProvider.setHTMLBasePath(basePath);
|
this.fileProvider.setHTMLBasePath(basePath);
|
||||||
}));
|
}));
|
||||||
|
(<any>window).FileTransferError = FileTransferErrorMock;
|
||||||
|
|
||||||
return this.utils.allPromises(promises);
|
return this.utils.allPromises(promises);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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…
Reference in New Issue