MOBILE-3565 core: Fix some ESLint of CoreWSProvider

main
Dani Palou 2020-10-14 08:33:50 +02:00
parent 5cb0c6fe0c
commit e7de01acf6
1 changed files with 235 additions and 237 deletions

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpResponse } from '@angular/common/http'; import { HttpResponse, HttpParams } from '@angular/common/http';
import { FileUploadOptions } from '@ionic-native/file-transfer/ngx'; import { FileUploadOptions } from '@ionic-native/file-transfer/ngx';
import { Md5 } from 'ts-md5/dist/md5'; import { Md5 } from 'ts-md5/dist/md5';
@ -25,20 +25,28 @@ import { CoreApp } from '@services/app';
import { CoreFile, CoreFileProvider } from '@services/file'; import { CoreFile, CoreFileProvider } from '@services/file';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { CoreError } from '@classes/errors/error';
import { CoreInterceptor } from '@classes/interceptor'; import { CoreInterceptor } from '@classes/interceptor';
import { makeSingleton, Translate, FileTransfer, Http, Platform } from '@singletons/core.singletons'; import { makeSingleton, Translate, FileTransfer, Http, Platform, NativeHttp } from '@singletons/core.singletons';
import { CoreArray } from '@singletons/array';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreWSError } from '@classes/errors/wserror';
import { CoreAjaxError } from '@classes/errors/ajaxerror';
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
/** /**
* This service allows performing WS calls and download/upload files. * This service allows performing WS calls and download/upload files.
*/ */
@Injectable() @Injectable()
export class CoreWSProvider { export class CoreWSProvider {
protected logger: CoreLogger; protected logger: CoreLogger;
protected mimeTypeCache = {}; // A "cache" to store file mimetypes to prevent performing too many HEAD requests. protected mimeTypeCache: {[url: string]: string} = {}; // A "cache" to store file mimetypes to decrease HEAD requests.
protected ongoingCalls = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected retryCalls = []; protected ongoingCalls: {[queueItemId: string]: Promise<any>} = {};
protected retryCalls: RetryCall[] = [];
protected retryTimeout = 0; protected retryTimeout = 0;
constructor() { constructor() {
@ -46,7 +54,7 @@ export class CoreWSProvider {
Platform.instance.ready().then(() => { Platform.instance.ready().then(() => {
if (CoreApp.instance.isIOS()) { if (CoreApp.instance.isIOS()) {
(<any> cordova).plugin.http.setHeader('User-Agent', navigator.userAgent); NativeHttp.instance.setHeader('*', 'User-Agent', navigator.userAgent);
} }
}); });
} }
@ -61,20 +69,15 @@ export class CoreWSProvider {
* @return Deferred promise resolved with the response data in success and rejected with the error message * @return Deferred promise resolved with the response data in success and rejected with the error message
* if it fails. * if it fails.
*/ */
protected addToRetryQueue(method: string, siteUrl: string, ajaxData: any, preSets: CoreWSPreSets): Promise<any> { protected addToRetryQueue<T = unknown>(method: string, siteUrl: string, data: unknown, preSets: CoreWSPreSets): Promise<T> {
const call: any = { const call = {
method, method,
siteUrl, siteUrl,
ajaxData, data,
preSets, preSets,
deferred: {} deferred: CoreUtils.instance.promiseDefer<T>(),
}; };
call.deferred.promise = new Promise((resolve, reject): void => {
call.deferred.resolve = resolve;
call.deferred.reject = reject;
});
this.retryCalls.push(call); this.retryCalls.push(call);
return call.deferred.promise; return call.deferred.promise;
@ -88,14 +91,11 @@ export class CoreWSProvider {
* @param preSets Extra settings and information. * @param preSets Extra settings and information.
* @return Promise resolved with the response data in success and rejected if it fails. * @return Promise resolved with the response data in success and rejected if it fails.
*/ */
call(method: string, data: any, preSets: CoreWSPreSets): Promise<any> { call<T = unknown>(method: string, data: unknown, preSets: CoreWSPreSets): Promise<T> {
let siteUrl;
if (!preSets) { if (!preSets) {
return Promise.reject(this.createFakeWSError('core.unexpectederror', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.unexpectederror')));
} else if (!CoreApp.instance.isOnline()) { } else if (!CoreApp.instance.isOnline()) {
return Promise.reject(this.createFakeWSError('core.networkerrormsg', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg')));
} }
preSets.typeExpected = preSets.typeExpected || 'object'; preSets.typeExpected = preSets.typeExpected || 'object';
@ -103,18 +103,18 @@ export class CoreWSProvider {
preSets.responseExpected = true; preSets.responseExpected = true;
} }
data = Object.assign({}, data); // Create a new object so the changes don't affect the original data. const dataToSend = Object.assign({}, data); // Create a new object so the changes don't affect the original data.
data.wsfunction = method; dataToSend['wsfunction'] = method;
data.wstoken = preSets.wsToken; dataToSend['wstoken'] = preSets.wsToken;
siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json'; const siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json';
// There are some ongoing retry calls, wait for timeout. // There are some ongoing retry calls, wait for timeout.
if (this.retryCalls.length > 0) { if (this.retryCalls.length > 0) {
this.logger.warn('Calls locked, trying later...'); this.logger.warn('Calls locked, trying later...');
return this.addToRetryQueue(method, siteUrl, data, preSets); return this.addToRetryQueue<T>(method, siteUrl, data, preSets);
} else { } else {
return this.performPost(method, siteUrl, data, preSets); return this.performPost<T>(method, siteUrl, data, preSets);
} }
} }
@ -130,17 +130,17 @@ export class CoreWSProvider {
* - errorcode: Error code returned by the site (if any). * - errorcode: Error code returned by the site (if any).
* - available: 0 if unknown, 1 if available, -1 if not available. * - available: 0 if unknown, 1 if available, -1 if not available.
*/ */
callAjax(method: string, data: any, preSets: CoreWSAjaxPreSets): Promise<any> { callAjax<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSAjaxPreSets): Promise<T> {
const cacheParams = { const cacheParams = {
methodname: method, methodname: method,
args: data, args: data,
}; };
let promise = this.getPromiseHttp('ajax', preSets.siteUrl, cacheParams); let promise = this.getPromiseHttp<T>('ajax', preSets.siteUrl, cacheParams);
if (!promise) { if (!promise) {
promise = this.performAjax(method, data, preSets); promise = this.performAjax<T>(method, data, preSets);
promise = this.setPromiseHttp(promise, 'ajax', preSets.siteUrl, cacheParams); promise = this.setPromiseHttp<T>(promise, 'ajax', preSets.siteUrl, cacheParams);
} }
return promise; return promise;
@ -154,7 +154,9 @@ export class CoreWSProvider {
* @param stripUnicode If Unicode long chars need to be stripped. * @param stripUnicode If Unicode long chars need to be stripped.
* @return The cleaned object or null if some strings becomes empty after stripping Unicode. * @return The cleaned object or null if some strings becomes empty after stripping Unicode.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
convertValuesToString(data: any, stripUnicode?: boolean): any { convertValuesToString(data: any, stripUnicode?: boolean): any {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = Array.isArray(data) ? [] : {}; const result: any = Array.isArray(data) ? [] : {};
for (const key in data) { for (const key in data) {
@ -210,15 +212,14 @@ export class CoreWSProvider {
* @param needsTranslate If the message needs to be translated. * @param needsTranslate If the message needs to be translated.
* @param translateParams Translation params, if needed. * @param translateParams Translation params, if needed.
* @return Fake WS error. * @return Fake WS error.
* @deprecated since 3.9.5. Just create the error directly.
*/ */
createFakeWSError(message: string, needsTranslate?: boolean, translateParams?: {}): CoreWSError { createFakeWSError(message: string, needsTranslate?: boolean, translateParams?: {[name: string]: string}): CoreError {
if (needsTranslate) { if (needsTranslate) {
message = Translate.instance.instant(message, translateParams); message = Translate.instance.instant(message, translateParams);
} }
return { return new CoreError(message);
message,
};
} }
/** /**
@ -230,71 +231,68 @@ export class CoreWSProvider {
* @param onProgress Function to call on progress. * @param onProgress Function to call on progress.
* @return Promise resolved with the downloaded file. * @return Promise resolved with the downloaded file.
*/ */
downloadFile(url: string, path: string, addExtension?: boolean, onProgress?: (event: ProgressEvent) => any): Promise<any> { async downloadFile(url: string, path: string, addExtension?: boolean, onProgress?: (event: ProgressEvent) => void):
Promise<CoreWSDownloadedFileEntry> {
this.logger.debug('Downloading file', url, path, addExtension); this.logger.debug('Downloading file', url, path, addExtension);
if (!CoreApp.instance.isOnline()) { if (!CoreApp.instance.isOnline()) {
return Promise.reject(Translate.instance.instant('core.networkerrormsg')); throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
} }
// Use a tmp path to download the file and then move it to final location. // 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. // This is because if the download fails, the local file is deleted.
const tmpPath = path + '.tmp'; const tmpPath = path + '.tmp';
// Create the tmp file as an empty file. try {
return CoreFile.instance.createFile(tmpPath).then((fileEntry) => { // Create the tmp file as an empty file.
const fileEntry = await CoreFile.instance.createFile(tmpPath);
const transfer = FileTransfer.instance.create(); const transfer = FileTransfer.instance.create();
transfer.onProgress(onProgress); transfer.onProgress(onProgress);
return transfer.download(url, fileEntry.toURL(), true).then(() => { // Download the file in the tmp file.
let promise; await transfer.download(url, fileEntry.toURL(), true);
if (addExtension) { let extension = '';
const ext = CoreMimetypeUtils.instance.getFileExtension(path);
// Google Drive extensions will be considered invalid since Moodle usually converts them. if (addExtension) {
if (!ext || ext == 'gdoc' || ext == 'gsheet' || ext == 'gslides' || ext == 'gdraw' || ext == 'php') { extension = CoreMimetypeUtils.instance.getFileExtension(path);
// Not valid, get the file's mimetype.
promise = this.getRemoteFileMimeType(url).then((mime) => {
if (mime) {
const remoteExt = CoreMimetypeUtils.instance.getExtension(mime, url);
// If the file is from Google Drive, ignore mimetype application/json.
if (remoteExt && (!ext || mime != 'application/json')) {
if (ext) {
// Remove existing extension since we will use another one.
path = CoreMimetypeUtils.instance.removeExtension(path);
}
path += '.' + remoteExt;
return remoteExt; // Google Drive extensions will be considered invalid since Moodle usually converts them.
} if (!extension || CoreArray.contains(['gdoc', 'gsheet', 'gslides', 'gdraw', 'php'], extension)) {
// Not valid, get the file's mimetype.
const mimetype = await this.getRemoteFileMimeType(url);
if (mimetype) {
const remoteExtension = CoreMimetypeUtils.instance.getExtension(mimetype, url);
// If the file is from Google Drive, ignore mimetype application/json.
if (remoteExtension && (!extension || mimetype != 'application/json')) {
if (extension) {
// Remove existing extension since we will use another one.
path = CoreMimetypeUtils.instance.removeExtension(path);
} }
path += '.' + remoteExtension;
return ext; extension = remoteExtension;
}); }
} else {
promise = Promise.resolve(ext);
} }
} else {
promise = Promise.resolve('');
} }
}
return promise.then((extension) => { // Move the file to the final location.
return CoreFile.instance.moveFile(tmpPath, path).then((movedEntry) => { const movedEntry: CoreWSDownloadedFileEntry = await CoreFile.instance.moveFile(tmpPath, path);
// Save the extension.
movedEntry.extension = extension;
movedEntry.path = path;
this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`);
return movedEntry; // Save the extension.
}); movedEntry.extension = extension;
}); movedEntry.path = path;
}); this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`);
}).catch((err) => {
this.logger.error(`Error downloading ${url} to ${path}`, err);
return Promise.reject(err); return movedEntry;
}); } catch (error) {
this.logger.error(`Error downloading ${url} to ${path}`, error);
throw error;
}
} }
/** /**
@ -304,13 +302,11 @@ export class CoreWSProvider {
* @param url Base URL of the HTTP request. * @param url Base URL of the HTTP request.
* @param params Params of the HTTP request. * @param params Params of the HTTP request.
*/ */
protected getPromiseHttp(method: string, url: string, params?: any): any { protected getPromiseHttp<T = unknown>(method: string, url: string, params?: Record<string, unknown>): Promise<T> {
const queueItemId = this.getQueueItemId(method, url, params); const queueItemId = this.getQueueItemId(method, url, params);
if (typeof this.ongoingCalls[queueItemId] != 'undefined') { if (typeof this.ongoingCalls[queueItemId] != 'undefined') {
return this.ongoingCalls[queueItemId]; return this.ongoingCalls[queueItemId];
} }
return false;
} }
/** /**
@ -334,10 +330,10 @@ export class CoreWSProvider {
this.mimeTypeCache[url] = mimeType; this.mimeTypeCache[url] = mimeType;
return mimeType || ''; return mimeType || '';
}).catch(() => { }).catch(() =>
// Error, resolve with empty mimetype. // Error, resolve with empty mimetype.
return ''; '',
}); );
} }
/** /**
@ -355,10 +351,10 @@ export class CoreWSProvider {
} }
return -1; return -1;
}).catch(() => { }).catch(() =>
// Error, return -1. // Error, return -1.
return -1; -1,
}); );
} }
/** /**
@ -378,7 +374,7 @@ export class CoreWSProvider {
* @param params Params of the HTTP request. * @param params Params of the HTTP request.
* @return Queue item ID. * @return Queue item ID.
*/ */
protected getQueueItemId(method: string, url: string, params?: any): string { protected getQueueItemId(method: string, url: string, params?: Record<string, unknown>): string {
if (params) { if (params) {
url += '###' + CoreInterceptor.serialize(params); url += '###' + CoreInterceptor.serialize(params);
} }
@ -397,14 +393,14 @@ export class CoreWSProvider {
* - errorcode: Error code returned by the site (if any). * - errorcode: Error code returned by the site (if any).
* - available: 0 if unknown, 1 if available, -1 if not available. * - available: 0 if unknown, 1 if available, -1 if not available.
*/ */
protected performAjax(method: string, data: any, preSets: CoreWSAjaxPreSets): Promise<any> { protected performAjax<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSAjaxPreSets): Promise<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let promise; let promise: Promise<HttpResponse<any>>;
if (typeof preSets.siteUrl == 'undefined') { if (typeof preSets.siteUrl == 'undefined') {
return rejectWithError(this.createFakeWSError('core.unexpectederror', true)); return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.unexpectederror')));
} else if (!CoreApp.instance.isOnline()) { } else if (!CoreApp.instance.isOnline()) {
return rejectWithError(this.createFakeWSError('core.networkerrormsg', true)); return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.networkerrormsg')));
} }
if (typeof preSets.responseExpected == 'undefined') { if (typeof preSets.responseExpected == 'undefined') {
@ -415,7 +411,7 @@ export class CoreWSProvider {
const ajaxData = [{ const ajaxData = [{
index: 0, index: 0,
methodname: method, methodname: method,
args: this.convertValuesToString(data) args: this.convertValuesToString(data),
}]; }];
// The info= parameter has no function. It is just to help with debugging. // The info= parameter has no function. It is just to help with debugging.
@ -426,18 +422,19 @@ export class CoreWSProvider {
// Send params using GET. // Send params using GET.
siteUrl += '&args=' + encodeURIComponent(JSON.stringify(ajaxData)); siteUrl += '&args=' + encodeURIComponent(JSON.stringify(ajaxData));
promise = this.sendHTTPRequest(siteUrl, { promise = this.sendHTTPRequest<T>(siteUrl, {
method: 'get', method: 'get',
}); });
} else { } else {
promise = this.sendHTTPRequest(siteUrl, { promise = this.sendHTTPRequest<T>(siteUrl, {
method: 'post', method: 'post',
data: ajaxData, // eslint-disable-next-line @typescript-eslint/no-explicit-any
data: <any> ajaxData,
serializer: 'json', serializer: 'json',
}); });
} }
return promise.then((response: HttpResponse<any>) => { return promise.then((response) => {
let data = response.body; let data = response.body;
// Some moodle web services return null. // Some moodle web services return null.
@ -448,39 +445,24 @@ export class CoreWSProvider {
// Check if error. Ajax layer should always return an object (if error) or an array (if success). // Check if error. Ajax layer should always return an object (if error) or an array (if success).
if (!data || typeof data != 'object') { if (!data || typeof data != 'object') {
return rejectWithError(this.createFakeWSError('core.serverconnection', true)); return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection')));
} else if (data.error) { } else if (data.error) {
return rejectWithError(data); return Promise.reject(new CoreAjaxWSError(data));
} }
// Get the first response since only one request was done. // Get the first response since only one request was done.
data = data[0]; data = data[0];
if (data.error) { if (data.error) {
return rejectWithError(data.exception); return Promise.reject(new CoreAjaxWSError(data.exception));
} }
return data.data; return data.data;
}, (data) => { }, (data) => {
const available = data.status == 404 ? -1 : 0; const available = data.status == 404 ? -1 : 0;
return rejectWithError(this.createFakeWSError('core.serverconnection', true), available); return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection'), available));
}); });
// Convenience function to return an error.
function rejectWithError(exception: any, available?: number): Promise<never> {
if (typeof available == 'undefined') {
if (exception.errorcode) {
available = exception.errorcode == 'invalidrecord' ? -1 : 1;
} else {
available = 0;
}
}
exception.available = available;
return Promise.reject(exception);
}
} }
/** /**
@ -489,16 +471,16 @@ export class CoreWSProvider {
* @param url URL to perform the request. * @param url URL to perform the request.
* @return Promise resolved with the response. * @return Promise resolved with the response.
*/ */
performHead(url: string): Promise<HttpResponse<any>> { performHead<T = unknown>(url: string): Promise<HttpResponse<T>> {
let promise = this.getPromiseHttp('head', url); let promise = this.getPromiseHttp<HttpResponse<T>>('head', url);
if (!promise) { if (!promise) {
promise = this.sendHTTPRequest(url, { promise = this.sendHTTPRequest<T>(url, {
method: 'head', method: 'head',
responseType: 'text', responseType: 'text',
}); });
promise = this.setPromiseHttp(promise, 'head', url); promise = this.setPromiseHttp<HttpResponse<T>>(promise, 'head', url);
} }
return promise; return promise;
@ -513,12 +495,12 @@ export class CoreWSProvider {
* @param preSets Extra settings and information. * @param preSets Extra settings and information.
* @return Promise resolved with the response data in success and rejected with CoreWSError if it fails. * @return 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> { performPost<T = unknown>(method: string, siteUrl: string, ajaxData: unknown, preSets: CoreWSPreSets): Promise<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: any = {}; const options: any = {};
// This is done because some returned values like 0 are treated as null if responseType is json. // This is done because some returned values like 0 are treated as null if responseType is json.
if (preSets.typeExpected == 'number' || preSets.typeExpected == 'boolean' || preSets.typeExpected == 'string') { if (preSets.typeExpected == 'number' || preSets.typeExpected == 'boolean' || preSets.typeExpected == 'string') {
// Avalaible values are: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
options.responseType = 'text'; options.responseType = 'text';
} }
@ -530,8 +512,8 @@ export class CoreWSProvider {
// Perform the post request. // Perform the post request.
const promise = Http.instance.post(requestUrl, ajaxData, options).pipe(timeout(this.getRequestTimeout())).toPromise(); const promise = Http.instance.post(requestUrl, ajaxData, options).pipe(timeout(this.getRequestTimeout())).toPromise();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return promise.then((data: any) => { return promise.then((data: any) => {
// Some moodle web services return null. // 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 the responseExpected value is set to false, we create a blank object if the response is null.
if (!data && !preSets.responseExpected) { if (!data && !preSets.responseExpected) {
@ -539,7 +521,7 @@ export class CoreWSProvider {
} }
if (!data) { if (!data) {
return Promise.reject(this.createFakeWSError('core.serverconnection', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection')));
} else if (typeof data != preSets.typeExpected) { } else if (typeof data != preSets.typeExpected) {
// If responseType is text an string will be returned, parse before returning. // If responseType is text an string will be returned, parse before returning.
if (typeof data == 'string') { if (typeof data == 'string') {
@ -548,7 +530,7 @@ export class CoreWSProvider {
if (isNaN(data)) { if (isNaN(data)) {
this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`); this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`);
return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
} else if (preSets.typeExpected == 'boolean') { } else if (preSets.typeExpected == 'boolean') {
if (data === 'true') { if (data === 'true') {
@ -558,17 +540,17 @@ export class CoreWSProvider {
} else { } else {
this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`); this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`);
return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
} else { } else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
} else { } else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
} }
@ -578,18 +560,18 @@ export class CoreWSProvider {
this.logger.error('Error calling WS', method, data); this.logger.error('Error calling WS', method, data);
} }
return Promise.reject(data); return Promise.reject(new CoreWSError(data));
} }
if (typeof data.debuginfo != 'undefined') { if (typeof data.debuginfo != 'undefined') {
return Promise.reject(this.createFakeWSError('Error. ' + data.message)); return Promise.reject(new CoreError('Error. ' + data.message));
} }
return data; return data;
}, (error) => { }, (error) => {
// If server has heavy load, retry after some seconds. // If server has heavy load, retry after some seconds.
if (error.status == 429) { if (error.status == 429) {
const retryPromise = this.addToRetryQueue(method, siteUrl, ajaxData, preSets); const retryPromise = this.addToRetryQueue<T>(method, siteUrl, ajaxData, preSets);
// Only process the queue one time. // Only process the queue one time.
if (this.retryTimeout == 0) { if (this.retryTimeout == 0) {
@ -610,7 +592,7 @@ export class CoreWSProvider {
return retryPromise; return retryPromise;
} }
return Promise.reject(this.createFakeWSError('core.serverconnection', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection')));
}); });
} }
@ -623,7 +605,7 @@ export class CoreWSProvider {
const call = this.retryCalls.shift(); const call = this.retryCalls.shift();
// Add a delay between calls. // Add a delay between calls.
setTimeout(() => { setTimeout(() => {
call.deferred.resolve(this.performPost(call.method, call.siteUrl, call.ajaxData, call.preSets)); call.deferred.resolve(this.performPost(call.method, call.siteUrl, call.data, call.preSets));
this.processRetryQueue(); this.processRetryQueue();
}, 200); }, 200);
} else { } else {
@ -640,14 +622,14 @@ export class CoreWSProvider {
* @param params Params of the HTTP request. * @param params Params of the HTTP request.
* @return The promise saved. * @return The promise saved.
*/ */
protected setPromiseHttp(promise: Promise<any>, method: string, url: string, params?: any): Promise<any> { protected setPromiseHttp<T = unknown>(promise: Promise<T>, method: string, url: string, params?: Record<string, unknown>):
Promise<T> {
const queueItemId = this.getQueueItemId(method, url, params); const queueItemId = this.getQueueItemId(method, url, params);
let timeout;
this.ongoingCalls[queueItemId] = promise; this.ongoingCalls[queueItemId] = promise;
// HTTP not finished, but we should delete the promise after timeout. // HTTP not finished, but we should delete the promise after timeout.
timeout = setTimeout(() => { const timeout = setTimeout(() => {
delete this.ongoingCalls[queueItemId]; delete this.ongoingCalls[queueItemId];
}, this.getRequestTimeout()); }, this.getRequestTimeout());
@ -667,22 +649,14 @@ export class CoreWSProvider {
* @param data Arguments to pass to the method. * @param data Arguments to pass to the method.
* @param preSets Extra settings and information. * @param preSets Extra settings and information.
* @return Promise resolved with the response data in success and rejected with the error message if it fails. * @return Promise resolved with the response data in success and rejected with the error message if it fails.
* @return Request response. If the request fails, returns an object with 'error'=true and 'message' properties. * @return Request response.
*/ */
syncCall(method: string, data: any, preSets: CoreWSPreSets): any { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
const errorResponse = { syncCall<T = unknown>(method: string, data: any, preSets: CoreWSPreSets): T {
error: true,
message: '',
};
if (!preSets) { if (!preSets) {
errorResponse.message = Translate.instance.instant('core.unexpectederror'); throw new CoreError(Translate.instance.instant('core.unexpectederror'));
return errorResponse;
} else if (!CoreApp.instance.isOnline()) { } else if (!CoreApp.instance.isOnline()) {
errorResponse.message = Translate.instance.instant('core.networkerrormsg'); throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
return errorResponse;
} }
preSets.typeExpected = preSets.typeExpected || 'object'; preSets.typeExpected = preSets.typeExpected || 'object';
@ -693,9 +667,7 @@ export class CoreWSProvider {
data = this.convertValuesToString(data || {}, preSets.cleanUnicode); data = this.convertValuesToString(data || {}, preSets.cleanUnicode);
if (data == null) { if (data == null) {
// Empty cleaned text found. // Empty cleaned text found.
errorResponse.message = Translate.instance.instant('core.unicodenotsupportedcleanerror'); throw new CoreError(Translate.instance.instant('core.unicodenotsupportedcleanerror'));
return errorResponse;
} }
data.wsfunction = method; data.wsfunction = method;
@ -706,22 +678,21 @@ export class CoreWSProvider {
data = CoreInterceptor.serialize(data); data = CoreInterceptor.serialize(data);
// Perform sync request using XMLHttpRequest. // Perform sync request using XMLHttpRequest.
const xhr = new (<any> window).XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('post', siteUrl, false); xhr.open('post', siteUrl, false);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8'); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
xhr.send(data); xhr.send(data);
// Get response. // Get response.
data = ('response' in xhr) ? xhr.response : xhr.responseText; // eslint-disable-next-line @typescript-eslint/no-explicit-any
data = ('response' in xhr) ? xhr.response : (<any> xhr).responseText;
// Check status. // Check status.
const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0); const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0);
if (status < 200 || status >= 300) { if (status < 200 || status >= 300) {
// Request failed. // Request failed.
errorResponse.message = data; throw new CoreError(data);
return errorResponse;
} }
// Treat response. // Treat response.
@ -734,18 +705,14 @@ export class CoreWSProvider {
} }
if (!data) { if (!data) {
errorResponse.message = Translate.instance.instant('core.serverconnection'); throw new CoreError(Translate.instance.instant('core.serverconnection'));
} else if (typeof data != preSets.typeExpected) { } else if (typeof data != preSets.typeExpected) {
this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"'); this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"');
errorResponse.message = Translate.instance.instant('core.errorinvalidresponse'); throw new CoreError(Translate.instance.instant('core.errorinvalidresponse'));
} }
if (typeof data.exception != 'undefined' || typeof data.debuginfo != 'undefined') { if (typeof data.exception != 'undefined' || typeof data.debuginfo != 'undefined') {
errorResponse.message = data.message; throw new CoreWSError(data);
}
if (errorResponse.message !== '') {
return errorResponse;
} }
return data; return data;
@ -760,16 +727,16 @@ export class CoreWSProvider {
* @param onProgress Function to call on progress. * @param onProgress Function to call on progress.
* @return Promise resolved when uploaded. * @return Promise resolved when uploaded.
*/ */
uploadFile(filePath: string, options: CoreWSFileUploadOptions, preSets: CoreWSPreSets, uploadFile<T = unknown>(filePath: string, options: CoreWSFileUploadOptions, preSets: CoreWSPreSets,
onProgress?: (event: ProgressEvent) => any): Promise<any> { onProgress?: (event: ProgressEvent) => void): Promise<T> {
this.logger.debug(`Trying to upload file: ${filePath}`); this.logger.debug(`Trying to upload file: ${filePath}`);
if (!filePath || !options || !preSets) { if (!filePath || !options || !preSets) {
return Promise.reject(null); return Promise.reject(new CoreError('Invalid options passed to upload file.'));
} }
if (!CoreApp.instance.isOnline()) { if (!CoreApp.instance.isOnline()) {
return Promise.reject(Translate.instance.instant('core.networkerrormsg')); return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg')));
} }
const uploadUrl = preSets.siteUrl + '/webservice/upload.php'; const uploadUrl = preSets.siteUrl + '/webservice/upload.php';
@ -781,34 +748,40 @@ export class CoreWSProvider {
options.params = { options.params = {
token: preSets.wsToken, token: preSets.wsToken,
filearea: options.fileArea || 'draft', filearea: options.fileArea || 'draft',
itemid: options.itemId || 0 itemid: options.itemId || 0,
}; };
options.chunkedMode = false; options.chunkedMode = false;
options.headers = { options.headers = {};
Connection: 'close' options['Connection'] = 'close';
};
return transfer.upload(filePath, uploadUrl, options, true).then((success) => { return transfer.upload(filePath, uploadUrl, options, true).then((success) => {
const data = CoreTextUtils.instance.parseJSON(success.response, null, const data = CoreTextUtils.instance.parseJSON(success.response, null,
this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response)); this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response));
if (data === null) { if (data === null) {
return Promise.reject(Translate.instance.instant('core.errorinvalidresponse')); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
if (!data) { if (!data) {
return Promise.reject(Translate.instance.instant('core.serverconnection')); return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection')));
} else if (typeof data != 'object') { } else if (typeof data != 'object') {
this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"'); this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"');
return Promise.reject(Translate.instance.instant('core.errorinvalidresponse')); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
if (typeof data.exception !== 'undefined') { if (typeof data.exception !== 'undefined') {
return Promise.reject(data.message); return Promise.reject(new CoreWSError(data));
} else if (data && typeof data.error !== 'undefined') { } else if (typeof data.error !== 'undefined') {
return Promise.reject(data.error); return Promise.reject(new CoreWSError({
errorcode: data.errortype,
message: data.error,
}));
} else if (data[0] && typeof data[0].error !== 'undefined') { } else if (data[0] && typeof data[0].error !== 'undefined') {
return Promise.reject(data[0].error); return Promise.reject(new CoreWSError({
errorcode: data[0].errortype,
message: data[0].error,
}));
} }
// We uploaded only 1 file, so we only return the first file returned. // We uploaded only 1 file, so we only return the first file returned.
@ -818,7 +791,7 @@ export class CoreWSProvider {
}).catch((error) => { }).catch((error) => {
this.logger.error('Error while uploading file', filePath, error); this.logger.error('Error while uploading file', filePath, error);
return Promise.reject(Translate.instance.instant('core.errorinvalidresponse')); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
}); });
} }
@ -835,7 +808,7 @@ export class CoreWSProvider {
responseType: 'text', responseType: 'text',
}; };
const response = await this.sendHTTPRequest(url, options); const response = await this.sendHTTPRequest<string>(url, options);
const content = response.body; const content = response.body;
@ -853,8 +826,7 @@ export class CoreWSProvider {
* @param options Options for the request. * @param options Options for the request.
* @return Promise resolved with the response. * @return Promise resolved with the response.
*/ */
async sendHTTPRequest(url: string, options: HttpRequestOptions): Promise<HttpResponse<any>> { async sendHTTPRequest<T = unknown>(url: string, options: HttpRequestOptions): Promise<HttpResponse<T>> {
// Set default values. // Set default values.
options.responseType = options.responseType || 'json'; options.responseType = options.responseType || 'json';
options.timeout = typeof options.timeout == 'undefined' ? this.getRequestTimeout() : options.timeout; options.timeout = typeof options.timeout == 'undefined' ? this.getRequestTimeout() : options.timeout;
@ -867,8 +839,8 @@ export class CoreWSProvider {
const content = await CoreFile.instance.readFile(url, format); const content = await CoreFile.instance.readFile(url, format);
return new HttpResponse({ return new HttpResponse<T>({
body: content, body: <T> content,
headers: null, headers: null,
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
@ -876,81 +848,78 @@ export class CoreWSProvider {
}); });
} }
return new Promise<HttpResponse<any>>((resolve, reject): void => { return NativeHttp.instance.sendRequest(url, options).then((response) => new CoreNativeToAngularHttpResponse(response));
// We cannot use Ionic Native plugin because it doesn't have the sendRequest method.
(<any> cordova).plugin.http.sendRequest(url, options, (response) => {
resolve(new CoreNativeToAngularHttpResponse(response));
}, reject);
});
} else { } else {
let observable: Observable<any>; // eslint-disable-next-line @typescript-eslint/no-explicit-any
let observable: Observable<HttpResponse<any>>;
const angularOptions = <AngularHttpRequestOptions> options;
// Use Angular's library. // Use Angular's library.
switch (options.method) { switch (angularOptions.method) {
case 'get': case 'get':
observable = Http.instance.get(url, { observable = Http.instance.get(url, {
headers: options.headers, headers: angularOptions.headers,
params: options.params, params: angularOptions.params,
observe: 'response', observe: 'response',
responseType: <any> options.responseType, // eslint-disable-next-line @typescript-eslint/no-explicit-any
responseType: <any> angularOptions.responseType,
}); });
break; break;
case 'post': case 'post':
if (options.serializer == 'json') { if (angularOptions.serializer == 'json') {
options.data = JSON.stringify(options.data); angularOptions.data = JSON.stringify(angularOptions.data);
} }
observable = Http.instance.post(url, options.data, { observable = Http.instance.post(url, angularOptions.data, {
headers: options.headers, headers: angularOptions.headers,
observe: 'response', observe: 'response',
responseType: <any> options.responseType, // eslint-disable-next-line @typescript-eslint/no-explicit-any
responseType: <any> angularOptions.responseType,
}); });
break; break;
case 'head': case 'head':
observable = Http.instance.head(url, { observable = Http.instance.head(url, {
headers: options.headers, headers: angularOptions.headers,
observe: 'response', observe: 'response',
responseType: <any> options.responseType // eslint-disable-next-line @typescript-eslint/no-explicit-any
responseType: <any> angularOptions.responseType,
}); });
break; break;
default: default:
return Promise.reject('Method not implemented yet.'); return Promise.reject(new CoreError('Method not implemented yet.'));
} }
if (options.timeout) { if (angularOptions.timeout) {
observable = observable.pipe(timeout(options.timeout)); observable = observable.pipe(timeout(angularOptions.timeout));
} }
return observable.toPromise(); return observable.toPromise();
} }
} }
/**
* Check if a URL works (it returns a 2XX status).
*
* @param url URL to check.
* @return Promise resolved with boolean: whether it works.
*/
async urlWorks(url: string): Promise<boolean> {
try {
const result = await this.performHead(url);
return result.status >= 200 && result.status < 300;
} catch (error) {
return false;
}
}
} }
export class CoreWS extends makeSingleton(CoreWSProvider) {} export class CoreWS extends makeSingleton(CoreWSProvider) {}
/**
* Error returned by a WS call.
*/
export interface CoreWSError {
/**
* The error message.
*/
message: string;
/**
* Name of the exception. Undefined for local errors (fake WS errors).
*/
exception?: string;
/**
* The error code. Undefined for local errors (fake WS errors).
*/
errorcode?: string;
}
/** /**
* File upload options. * File upload options.
*/ */
@ -1084,7 +1053,7 @@ export type CoreWSPreSets = {
* Defaults to false. Clean multibyte Unicode chars from data. * Defaults to false. Clean multibyte Unicode chars from data.
*/ */
cleanUnicode?: boolean; cleanUnicode?: boolean;
} };
/** /**
* PreSets accepted by AJAX WS calls. * PreSets accepted by AJAX WS calls.
@ -1109,7 +1078,7 @@ export type CoreWSAjaxPreSets = {
* Whether to send the parameters via GET. Only if noLogin is true. * Whether to send the parameters via GET. Only if noLogin is true.
*/ */
useGet?: boolean; useGet?: boolean;
} };
/** /**
* Options for HTTP requests. * Options for HTTP requests.
@ -1118,17 +1087,17 @@ export type HttpRequestOptions = {
/** /**
* The HTTP method. * The HTTP method.
*/ */
method: string; method: 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' | 'options' | 'upload' | 'download';
/** /**
* Payload to send to the server. Only applicable on post, put or patch methods. * Payload to send to the server. Only applicable on post, put or patch methods.
*/ */
data?: any; data?: Record<string, unknown>;
/** /**
* Query params to be appended to the URL (only applicable on get, head, delete, upload or download methods). * Query params to be appended to the URL (only applicable on get, head, delete, upload or download methods).
*/ */
params?: any; params?: Record<string, string | number>;
/** /**
* Response type. Defaults to json. * Response type. Defaults to json.
@ -1143,7 +1112,7 @@ export type HttpRequestOptions = {
/** /**
* Serializer to use. Defaults to 'urlencoded'. Only for mobile environments. * Serializer to use. Defaults to 'urlencoded'. Only for mobile environments.
*/ */
serializer?: string; serializer?: 'json' | 'urlencoded' | 'utf8' | 'multipart';
/** /**
* Whether to follow redirects. Defaults to true. Only for mobile environments. * Whether to follow redirects. Defaults to true. Only for mobile environments.
@ -1153,16 +1122,45 @@ export type HttpRequestOptions = {
/** /**
* Headers. Only for mobile environments. * Headers. Only for mobile environments.
*/ */
headers?: {[name: string]: string}; headers?: Record<string, string>;
/** /**
* File paths to use for upload or download. Only for mobile environments. * File paths to use for upload or download. Only for mobile environments.
*/ */
filePath?: string; filePath?: string | string[];
/** /**
* Name to use during upload. Only for mobile environments. * Name to use during upload. Only for mobile environments.
*/ */
name?: string; name?: string | string[];
}; };
/**
* Options for JSON HTTP requests using Angular Http.
*/
type AngularHttpRequestOptions = Omit<HttpRequestOptions, 'data'|'params'> & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: Record<string, any> | string;
params?: HttpParams | {
[param: string]: string | string[];
};
};
/**
* Data needed to retry a WS call.
*/
type RetryCall = {
method: string;
siteUrl: string;
data: unknown;
preSets: CoreWSPreSets;
deferred: PromiseDefer<unknown>;
};
/**
* Downloaded file entry. It includes some calculated data.
*/
export type CoreWSDownloadedFileEntry = FileEntry & {
extension?: string; // File extension.
path?: string; // File path.
};