commit
3e71f6f2a7
|
@ -26,7 +26,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
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 { CoreConstants } from '@core/constants';
|
||||||
import { CoreConfigConstants } from '../configconstants';
|
import { CoreConfigConstants } from '../configconstants';
|
||||||
import { Md5 } from 'ts-md5/dist/md5';
|
import { Md5 } from 'ts-md5/dist/md5';
|
||||||
|
@ -113,6 +113,17 @@ export interface CoreSiteWSPreSets {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
typeExpected?: 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;
|
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).
|
* Class that represents a site (combination of site + user).
|
||||||
* It will have all the site data and provide utility functions regarding a site.
|
* 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.
|
* the tables are created in all the sites, not just the current one.
|
||||||
*/
|
*/
|
||||||
export class CoreSite {
|
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.
|
// List of injected services. This class isn't injectable, so it cannot use DI.
|
||||||
protected appProvider: CoreAppProvider;
|
protected appProvider: CoreAppProvider;
|
||||||
protected dbProvider: CoreDbProvider;
|
protected dbProvider: CoreDbProvider;
|
||||||
|
@ -186,6 +214,8 @@ export class CoreSite {
|
||||||
protected lastAutoLogin = 0;
|
protected lastAutoLogin = 0;
|
||||||
protected offlineDisabled = false;
|
protected offlineDisabled = false;
|
||||||
protected ongoingRequests: { [cacheId: string]: Promise<any> } = {};
|
protected ongoingRequests: { [cacheId: string]: Promise<any> } = {};
|
||||||
|
protected requestQueue: RequestQueueItem[] = [];
|
||||||
|
protected requestQueueTimeout = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a site.
|
* Create a site.
|
||||||
|
@ -217,6 +247,7 @@ export class CoreSite {
|
||||||
this.wsProvider = injector.get(CoreWSProvider);
|
this.wsProvider = injector.get(CoreWSProvider);
|
||||||
|
|
||||||
this.logger = logger.getInstance('CoreWSProvider');
|
this.logger = logger.getInstance('CoreWSProvider');
|
||||||
|
this.setInfo(infos);
|
||||||
this.calculateOfflineDisabled();
|
this.calculateOfflineDisabled();
|
||||||
|
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
|
@ -349,6 +380,14 @@ export class CoreSite {
|
||||||
*/
|
*/
|
||||||
setInfo(infos: any): void {
|
setInfo(infos: any): void {
|
||||||
this.infos = infos;
|
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.
|
// The get_site_info WS call won't be cached.
|
||||||
const preSets = {
|
const preSets = {
|
||||||
getFromCache: false,
|
getFromCache: false,
|
||||||
saveToCache: false
|
saveToCache: false,
|
||||||
|
skipQueue: true
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset clean Unicode to check if it's supported again.
|
// Reset clean Unicode to check if it's supported again.
|
||||||
|
@ -467,6 +507,9 @@ export class CoreSite {
|
||||||
if (typeof preSets.saveToCache == 'undefined') {
|
if (typeof preSets.saveToCache == 'undefined') {
|
||||||
preSets.saveToCache = true;
|
preSets.saveToCache = true;
|
||||||
}
|
}
|
||||||
|
if (typeof preSets.reusePending == 'undefined') {
|
||||||
|
preSets.reusePending = true;
|
||||||
|
}
|
||||||
|
|
||||||
return this.request(method, data, preSets);
|
return this.request(method, data, preSets);
|
||||||
}
|
}
|
||||||
|
@ -564,10 +607,9 @@ export class CoreSite {
|
||||||
|
|
||||||
const originalData = data;
|
const originalData = data;
|
||||||
|
|
||||||
// Convert the values to string before starting the cache process.
|
// Convert arguments to strings before starting the cache process.
|
||||||
try {
|
data = this.wsProvider.convertValuesToString(data, wsPreSets.cleanUnicode);
|
||||||
data = this.wsProvider.convertValuesToString(data, wsPreSets.cleanUnicode);
|
if (data == null) {
|
||||||
} catch (e) {
|
|
||||||
// Empty cleaned text found.
|
// Empty cleaned text found.
|
||||||
return Promise.reject(this.utils.createFakeWSError('core.unicodenotsupportedcleanerror', true));
|
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(() => {
|
const promise = this.getFromCache(method, data, preSets, false, originalData).catch(() => {
|
||||||
// Do not pass those options to the core WS factory.
|
// 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) {
|
if (preSets.saveToCache) {
|
||||||
this.saveToCache(method, data, response, preSets);
|
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.
|
* Check if a WS is available in this site.
|
||||||
*
|
*
|
||||||
|
@ -711,11 +893,8 @@ export class CoreSite {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.infos.functions.length; i++) {
|
if (this.infos.functionsByName[method]) {
|
||||||
const func = this.infos.functions[i];
|
return true;
|
||||||
if (func.name == method) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's try again with the compatibility prefix.
|
// Let's try again with the compatibility prefix.
|
||||||
|
|
|
@ -250,8 +250,8 @@ export class CoreUserProvider {
|
||||||
this.logger.debug(`Get user with ID '${userId}'`);
|
this.logger.debug(`Get user with ID '${userId}'`);
|
||||||
wsName = 'core_user_get_users_by_field';
|
wsName = 'core_user_get_users_by_field';
|
||||||
data = {
|
data = {
|
||||||
'field': 'id',
|
field: 'id',
|
||||||
'values[0]': userId
|
values: [userId]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -284,32 +284,55 @@ export class CoreWSProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an objects values to strings where appropriate.
|
* 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 {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.
|
* @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 {
|
convertValuesToString(data: any, stripUnicode?: boolean): any {
|
||||||
let result;
|
const result: any = Array.isArray(data) ? [] : {};
|
||||||
if (!Array.isArray(data) && typeof data == 'object') {
|
|
||||||
result = {};
|
|
||||||
} else {
|
|
||||||
result = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const el in data) {
|
for (const key in data) {
|
||||||
if (typeof data[el] == 'object') {
|
let value = data[key];
|
||||||
result[el] = this.convertValuesToString(data[el], stripUnicode);
|
|
||||||
} else {
|
if (value == null) {
|
||||||
if (typeof data[el] == 'string') {
|
// Skip null or undefined value.
|
||||||
result[el] = stripUnicode ? this.textUtils.stripUnicode(data[el]) : data[el];
|
continue;
|
||||||
if (stripUnicode && data[el] != result[el] && result[el].trim().length == 0) {
|
} else if (typeof value == 'object') {
|
||||||
throw new Error();
|
// Object or array.
|
||||||
}
|
value = this.convertValuesToString(value, stripUnicode);
|
||||||
} else {
|
if (value == null) {
|
||||||
result[el] = data[el] + '';
|
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 {
|
||||||
|
value = value ? '1' : '0';
|
||||||
|
}
|
||||||
|
} else if (typeof value == 'number') {
|
||||||
|
value = String(value);
|
||||||
|
} else {
|
||||||
|
// Unknown type.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
result.push(value);
|
||||||
|
} else {
|
||||||
|
result[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -525,9 +548,9 @@ export class CoreWSProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the post request.
|
// 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.
|
// 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) {
|
||||||
|
@ -608,10 +631,6 @@ export class CoreWSProvider {
|
||||||
|
|
||||||
return Promise.reject(this.createFakeWSError('core.serverconnection', true));
|
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;
|
preSets.responseExpected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
data = this.convertValuesToString(data || {}, preSets.cleanUnicode);
|
||||||
data = this.convertValuesToString(data, preSets.cleanUnicode);
|
if (data == null) {
|
||||||
} catch (e) {
|
|
||||||
// Empty cleaned text found.
|
// Empty cleaned text found.
|
||||||
errorResponse.message = this.translate.instant('core.unicodenotsupportedcleanerror');
|
errorResponse.message = this.translate.instant('core.unicodenotsupportedcleanerror');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue