commit
3e71f6f2a7
|
@ -26,7 +26,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
|||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils';
|
||||
import { CoreConstants } from '@core/constants';
|
||||
import { CoreConfigConstants } from '../configconstants';
|
||||
import { Md5 } from 'ts-md5/dist/md5';
|
||||
|
@ -113,6 +113,17 @@ export interface CoreSiteWSPreSets {
|
|||
* @type {string}
|
||||
*/
|
||||
typeExpected?: string;
|
||||
|
||||
/**
|
||||
* Wehther a pending request in the queue matching the same function and arguments can be reused instead of adding
|
||||
* a new request to the queue. Defaults to true for read requests.
|
||||
*/
|
||||
reusePending?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the request will be be sent immediately as a single request. Defaults to false.
|
||||
*/
|
||||
skipQueue?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -144,6 +155,18 @@ export interface LocalMobileResponse {
|
|||
coreSupported?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Info of a request waiting in the queue.
|
||||
*/
|
||||
interface RequestQueueItem {
|
||||
cacheId: string;
|
||||
method: string;
|
||||
data: any;
|
||||
preSets: CoreSiteWSPreSets;
|
||||
wsPreSets: CoreWSPreSets;
|
||||
deferred: PromiseDefer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that represents a site (combination of site + user).
|
||||
* It will have all the site data and provide utility functions regarding a site.
|
||||
|
@ -151,6 +174,11 @@ export interface LocalMobileResponse {
|
|||
* the tables are created in all the sites, not just the current one.
|
||||
*/
|
||||
export class CoreSite {
|
||||
static REQUEST_QUEUE_DELAY = 50; // Maximum number of miliseconds to wait before processing the queue.
|
||||
static REQUEST_QUEUE_LIMIT = 10; // Maximum number of requests allowed in the queue.
|
||||
// @todo Set REQUEST_QUEUE_FORCE_WS to false before the release.
|
||||
static REQUEST_QUEUE_FORCE_WS = true; // Use "tool_mobile_call_external_functions" even for calling a single function.
|
||||
|
||||
// List of injected services. This class isn't injectable, so it cannot use DI.
|
||||
protected appProvider: CoreAppProvider;
|
||||
protected dbProvider: CoreDbProvider;
|
||||
|
@ -186,6 +214,8 @@ export class CoreSite {
|
|||
protected lastAutoLogin = 0;
|
||||
protected offlineDisabled = false;
|
||||
protected ongoingRequests: { [cacheId: string]: Promise<any> } = {};
|
||||
protected requestQueue: RequestQueueItem[] = [];
|
||||
protected requestQueueTimeout = null;
|
||||
|
||||
/**
|
||||
* Create a site.
|
||||
|
@ -217,6 +247,7 @@ export class CoreSite {
|
|||
this.wsProvider = injector.get(CoreWSProvider);
|
||||
|
||||
this.logger = logger.getInstance('CoreWSProvider');
|
||||
this.setInfo(infos);
|
||||
this.calculateOfflineDisabled();
|
||||
|
||||
if (this.id) {
|
||||
|
@ -349,6 +380,14 @@ export class CoreSite {
|
|||
*/
|
||||
setInfo(infos: any): void {
|
||||
this.infos = infos;
|
||||
|
||||
// Index function by name to speed up wsAvailable method.
|
||||
if (infos && infos.functions) {
|
||||
infos.functionsByName = {};
|
||||
infos.functions.forEach((func) => {
|
||||
infos.functionsByName[func.name] = func;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -442,7 +481,8 @@ export class CoreSite {
|
|||
// The get_site_info WS call won't be cached.
|
||||
const preSets = {
|
||||
getFromCache: false,
|
||||
saveToCache: false
|
||||
saveToCache: false,
|
||||
skipQueue: true
|
||||
};
|
||||
|
||||
// Reset clean Unicode to check if it's supported again.
|
||||
|
@ -467,6 +507,9 @@ export class CoreSite {
|
|||
if (typeof preSets.saveToCache == 'undefined') {
|
||||
preSets.saveToCache = true;
|
||||
}
|
||||
if (typeof preSets.reusePending == 'undefined') {
|
||||
preSets.reusePending = true;
|
||||
}
|
||||
|
||||
return this.request(method, data, preSets);
|
||||
}
|
||||
|
@ -564,10 +607,9 @@ export class CoreSite {
|
|||
|
||||
const originalData = data;
|
||||
|
||||
// Convert the values to string before starting the cache process.
|
||||
try {
|
||||
// Convert arguments to strings before starting the cache process.
|
||||
data = this.wsProvider.convertValuesToString(data, wsPreSets.cleanUnicode);
|
||||
} catch (e) {
|
||||
if (data == null) {
|
||||
// Empty cleaned text found.
|
||||
return Promise.reject(this.utils.createFakeWSError('core.unicodenotsupportedcleanerror', true));
|
||||
}
|
||||
|
@ -584,7 +626,7 @@ export class CoreSite {
|
|||
|
||||
const promise = this.getFromCache(method, data, preSets, false, originalData).catch(() => {
|
||||
// Do not pass those options to the core WS factory.
|
||||
return this.wsProvider.call(method, data, wsPreSets).then((response) => {
|
||||
return this.callOrEnqueueRequest(method, data, preSets, wsPreSets).then((response) => {
|
||||
if (preSets.saveToCache) {
|
||||
this.saveToCache(method, data, response, preSets);
|
||||
}
|
||||
|
@ -699,6 +741,146 @@ export class CoreSite {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a request to the queue or calls it immediately when not using the queue.
|
||||
*
|
||||
* @param {string} method The WebService method to be called.
|
||||
* @param {any} data Arguments to pass to the method.
|
||||
* @param {CoreSiteWSPreSets} preSets Extra options related to the site.
|
||||
* @param {CoreWSPreSets} wsPreSets Extra options related to the WS call.
|
||||
* @returns {Promise<any>} Promise resolved with the response when the WS is called.
|
||||
*/
|
||||
protected callOrEnqueueRequest(method: string, data: any, preSets: CoreSiteWSPreSets, wsPreSets: CoreWSPreSets): Promise<any> {
|
||||
if (preSets.skipQueue || !this.wsAvailable('tool_mobile_call_external_functions')) {
|
||||
return this.wsProvider.call(method, data, wsPreSets);
|
||||
}
|
||||
|
||||
const cacheId = this.getCacheId(method, data);
|
||||
|
||||
// Check if there is an identical request waiting in the queue (read requests only by default).
|
||||
if (preSets.reusePending) {
|
||||
const request = this.requestQueue.find((request) => request.cacheId == cacheId);
|
||||
if (request) {
|
||||
return request.deferred.promise;
|
||||
}
|
||||
}
|
||||
|
||||
const request: RequestQueueItem = {
|
||||
cacheId,
|
||||
method,
|
||||
data,
|
||||
preSets,
|
||||
wsPreSets,
|
||||
deferred: {}
|
||||
};
|
||||
|
||||
request.deferred.promise = new Promise((resolve, reject): void => {
|
||||
request.deferred.resolve = resolve;
|
||||
request.deferred.reject = reject;
|
||||
});
|
||||
|
||||
this.requestQueue.push(request);
|
||||
|
||||
if (this.requestQueue.length >= CoreSite.REQUEST_QUEUE_LIMIT) {
|
||||
this.processRequestQueue();
|
||||
} else if (!this.requestQueueTimeout) {
|
||||
this.requestQueueTimeout = setTimeout(this.processRequestQueue.bind(this), CoreSite.REQUEST_QUEUE_DELAY);
|
||||
}
|
||||
|
||||
return request.deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the enqueued web service requests.
|
||||
*/
|
||||
protected processRequestQueue(): void {
|
||||
this.logger.debug(`Processing request queue (${this.requestQueue.length} requests)`);
|
||||
|
||||
// Clear timeout if set.
|
||||
if (this.requestQueueTimeout) {
|
||||
clearTimeout(this.requestQueueTimeout);
|
||||
this.requestQueueTimeout = null;
|
||||
}
|
||||
|
||||
// Extract all requests from the queue.
|
||||
const requests = this.requestQueue;
|
||||
this.requestQueue = [];
|
||||
|
||||
if (requests.length == 1 && !CoreSite.REQUEST_QUEUE_FORCE_WS) {
|
||||
// Only one request, do a regular web service call.
|
||||
this.wsProvider.call(requests[0].method, requests[0].data, requests[0].wsPreSets).then((data) => {
|
||||
requests[0].deferred.resolve(data);
|
||||
}).catch((error) => {
|
||||
requests[0].deferred.reject(error);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
requests: requests.map((request) => {
|
||||
const args = {};
|
||||
const settings = {};
|
||||
|
||||
// Separate WS settings from function arguments.
|
||||
Object.keys(request.data).forEach((key) => {
|
||||
let value = request.data[key];
|
||||
const match = /^moodlews(setting.*)$/.exec(key);
|
||||
if (match) {
|
||||
if (match[1] == 'settingfilter' || match[1] == 'settingfileurl') {
|
||||
// Undo special treatment of these settings in CoreWSProvider.convertValuesToString.
|
||||
value = (value == 'true' ? '1' : '0');
|
||||
}
|
||||
settings[match[1]] = value;
|
||||
} else {
|
||||
args[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
function: request.method,
|
||||
arguments: JSON.stringify(args),
|
||||
...settings
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const wsPresets: CoreWSPreSets = {
|
||||
siteUrl: this.siteUrl,
|
||||
wsToken: this.token,
|
||||
};
|
||||
|
||||
this.wsProvider.call('tool_mobile_call_external_functions', data, wsPresets).then((data) => {
|
||||
if (!data || !data.responses) {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
requests.forEach((request, i) => {
|
||||
const response = data.responses[i];
|
||||
|
||||
if (!response) {
|
||||
// Request not executed, enqueue again.
|
||||
this.callOrEnqueueRequest(request.method, request.data, request.preSets, request.wsPreSets);
|
||||
} else if (response.error) {
|
||||
request.deferred.reject(this.textUtils.parseJSON(response.exception));
|
||||
} else {
|
||||
let responseData = this.textUtils.parseJSON(response.data);
|
||||
// Match the behaviour of CoreWSProvider.call when no response is expected.
|
||||
if (!responseData && (typeof wsPresets.responseExpected == 'undefined' || wsPresets.responseExpected)) {
|
||||
responseData = {};
|
||||
}
|
||||
request.deferred.resolve(responseData);
|
||||
}
|
||||
});
|
||||
|
||||
}).catch((error) => {
|
||||
// Error not specific to a single request, reject all promises.
|
||||
requests.forEach((request) => {
|
||||
request.deferred.reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a WS is available in this site.
|
||||
*
|
||||
|
@ -711,12 +893,9 @@ export class CoreSite {
|
|||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.infos.functions.length; i++) {
|
||||
const func = this.infos.functions[i];
|
||||
if (func.name == method) {
|
||||
if (this.infos.functionsByName[method]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's try again with the compatibility prefix.
|
||||
if (checkPrefix) {
|
||||
|
|
|
@ -250,8 +250,8 @@ export class CoreUserProvider {
|
|||
this.logger.debug(`Get user with ID '${userId}'`);
|
||||
wsName = 'core_user_get_users_by_field';
|
||||
data = {
|
||||
'field': 'id',
|
||||
'values[0]': userId
|
||||
field: 'id',
|
||||
values: [userId]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -284,32 +284,55 @@ export class CoreWSProvider {
|
|||
|
||||
/**
|
||||
* Converts an objects values to strings where appropriate.
|
||||
* Arrays (associative or otherwise) will be maintained.
|
||||
* Arrays (associative or otherwise) will be maintained, null values will be removed.
|
||||
*
|
||||
* @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.
|
||||
* @return {object} The cleaned object or null if some strings becomes empty after stripping Unicode.
|
||||
*/
|
||||
convertValuesToString(data: object, stripUnicode?: boolean): object {
|
||||
let result;
|
||||
if (!Array.isArray(data) && typeof data == 'object') {
|
||||
result = {};
|
||||
convertValuesToString(data: any, stripUnicode?: boolean): any {
|
||||
const result: any = Array.isArray(data) ? [] : {};
|
||||
|
||||
for (const key in data) {
|
||||
let value = data[key];
|
||||
|
||||
if (value == null) {
|
||||
// Skip null or undefined value.
|
||||
continue;
|
||||
} else if (typeof value == 'object') {
|
||||
// Object or array.
|
||||
value = this.convertValuesToString(value, stripUnicode);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
} else if (typeof value == 'string') {
|
||||
if (stripUnicode) {
|
||||
const stripped = this.textUtils.stripUnicode(value);
|
||||
if (stripped != value && stripped.trim().length == 0) {
|
||||
return null;
|
||||
}
|
||||
value = stripped;
|
||||
}
|
||||
} else if (typeof value == 'boolean') {
|
||||
/* Moodle does not allow "true" or "false" in WS parameters, only in POST parameters.
|
||||
We've been using "true" and "false" for WS settings "filter" and "fileurl",
|
||||
we keep it this way to avoid changing cache keys. */
|
||||
if (key == 'moodlewssettingfilter' || key == 'moodlewssettingfileurl') {
|
||||
value = value ? 'true' : 'false';
|
||||
} else {
|
||||
result = [];
|
||||
value = value ? '1' : '0';
|
||||
}
|
||||
} else if (typeof value == 'number') {
|
||||
value = String(value);
|
||||
} else {
|
||||
// Unknown type.
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const el in data) {
|
||||
if (typeof data[el] == 'object') {
|
||||
result[el] = this.convertValuesToString(data[el], stripUnicode);
|
||||
if (Array.isArray(result)) {
|
||||
result.push(value);
|
||||
} 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] + '';
|
||||
}
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -525,9 +548,9 @@ export class CoreWSProvider {
|
|||
}
|
||||
|
||||
// Perform the post request.
|
||||
let promise = this.http.post(siteUrl, ajaxData, options).timeout(CoreConstants.WS_TIMEOUT).toPromise();
|
||||
const promise = this.http.post(siteUrl, ajaxData, options).timeout(CoreConstants.WS_TIMEOUT).toPromise();
|
||||
|
||||
promise = promise.then((data: any) => {
|
||||
return promise.then((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) {
|
||||
|
@ -608,10 +631,6 @@ export class CoreWSProvider {
|
|||
|
||||
return Promise.reject(this.createFakeWSError('core.serverconnection', true));
|
||||
});
|
||||
|
||||
promise = this.setPromiseHttp(promise, 'post', preSets.siteUrl, ajaxData);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -692,9 +711,8 @@ export class CoreWSProvider {
|
|||
preSets.responseExpected = true;
|
||||
}
|
||||
|
||||
try {
|
||||
data = this.convertValuesToString(data, preSets.cleanUnicode);
|
||||
} catch (e) {
|
||||
data = this.convertValuesToString(data || {}, preSets.cleanUnicode);
|
||||
if (data == null) {
|
||||
// Empty cleaned text found.
|
||||
errorResponse.message = this.translate.instant('core.unicodenotsupportedcleanerror');
|
||||
|
||||
|
|
Loading…
Reference in New Issue