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 { | ||||
|             data = this.wsProvider.convertValuesToString(data, wsPreSets.cleanUnicode); | ||||
|         } catch (e) { | ||||
|         // Convert arguments to strings before starting the cache process.
 | ||||
|         data = this.wsProvider.convertValuesToString(data, wsPreSets.cleanUnicode); | ||||
|         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,11 +893,8 @@ 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) { | ||||
|                 return true; | ||||
|             } | ||||
|         if (this.infos.functionsByName[method]) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // Let's try again with the compatibility prefix.
 | ||||
|  | ||||
| @ -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 = {}; | ||||
|         } else { | ||||
|             result = []; | ||||
|         } | ||||
|     convertValuesToString(data: any, stripUnicode?: boolean): any { | ||||
|         const result: any = Array.isArray(data) ? [] : {}; | ||||
| 
 | ||||
|         for (const 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] + ''; | ||||
|         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 { | ||||
|                     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.
 | ||||
|         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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user