diff --git a/src/app/classes/delegate.ts b/src/app/classes/delegate.ts index a3c9665fa..48a86308c 100644 --- a/src/app/classes/delegate.ts +++ b/src/app/classes/delegate.ts @@ -40,18 +40,18 @@ export class CoreDelegate { /** * Default handler */ - protected defaultHandler: CoreDelegateHandler; + protected defaultHandler?: CoreDelegateHandler; /** * Time when last updateHandler functions started. */ - protected lastUpdateHandlersStart: number; + protected lastUpdateHandlersStart = 0; /** * Feature prefix to check is feature is enabled or disabled in site. * This check is only made if not false. Override on the subclass or override isFeatureDisabled function. */ - protected featurePrefix: string; + protected featurePrefix?: string; /** * Name of the property to be used to index the handlers. By default, the handler's name will be used. @@ -78,7 +78,7 @@ export class CoreDelegate { /** * Function to resolve the handlers init promise. */ - protected handlersInitResolve: () => void; + protected handlersInitResolve!: () => void; /** * Constructor of the Delegate. @@ -110,7 +110,7 @@ export class CoreDelegate { * @param params Parameters to pass to the function. * @return Function returned value or default value. */ - protected executeFunctionOnEnabled(handlerName: string, fnName: string, params?: unknown[]): T { + protected executeFunctionOnEnabled(handlerName: string, fnName: string, params?: unknown[]): T | undefined { return this.execute(this.enabledHandlers[handlerName], fnName, params); } @@ -123,7 +123,7 @@ export class CoreDelegate { * @param params Parameters to pass to the function. * @return Function returned value or default value. */ - protected executeFunction(handlerName: string, fnName: string, params?: unknown[]): T { + protected executeFunction(handlerName: string, fnName: string, params?: unknown[]): T | undefined { return this.execute(this.handlers[handlerName], fnName, params); } @@ -136,7 +136,7 @@ export class CoreDelegate { * @param params Parameters to pass to the function. * @return Function returned value or default value. */ - private execute(handler: CoreDelegateHandler, fnName: string, params?: unknown[]): T { + private execute(handler: CoreDelegateHandler, fnName: string, params?: unknown[]): T | undefined { if (handler && handler[fnName]) { return handler[fnName].apply(handler, params); } else if (this.defaultHandler && this.defaultHandler[fnName]) { @@ -252,7 +252,7 @@ export class CoreDelegate { this.updatePromises[siteId] = {}; } - if (!CoreSites.instance.isLoggedIn() || this.isFeatureDisabled(handler, currentSite)) { + if (!CoreSites.instance.isLoggedIn() || this.isFeatureDisabled(handler, currentSite!)) { promise = Promise.resolve(false); } else { promise = Promise.resolve(handler.isEnabled()).catch(() => false); @@ -270,6 +270,8 @@ export class CoreDelegate { delete this.enabledHandlers[key]; } } + + return; }).finally(() => { // Update finished, delete the promise. delete this.updatePromises[siteId][handler.name]; @@ -295,7 +297,7 @@ export class CoreDelegate { * @return Resolved when done. */ protected async updateHandlers(): Promise { - const promises = []; + const promises: Promise[] = []; const now = Date.now(); this.logger.debug('Updating handlers for current site.'); diff --git a/src/app/classes/errors/ajaxwserror.ts b/src/app/classes/errors/ajaxwserror.ts index c2e5df5c3..338a0706a 100644 --- a/src/app/classes/errors/ajaxwserror.ts +++ b/src/app/classes/errors/ajaxwserror.ts @@ -28,7 +28,7 @@ export class CoreAjaxWSError extends CoreError { backtrace?: string; // Backtrace. Only if debug mode is enabled. available?: number; // Whether the AJAX call is available. 0 if unknown, 1 if available, -1 if not available. - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(error: any, available?: number) { super(error.message); diff --git a/src/app/classes/errors/wserror.ts b/src/app/classes/errors/wserror.ts index 27e3df43f..a3b4c9c0f 100644 --- a/src/app/classes/errors/wserror.ts +++ b/src/app/classes/errors/wserror.ts @@ -27,6 +27,7 @@ export class CoreWSError extends CoreError { debuginfo?: string; // Debug info. Only if debug mode is enabled. backtrace?: string; // Backtrace. Only if debug mode is enabled. + // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(error: any) { super(error.message); diff --git a/src/app/classes/interceptor.ts b/src/app/classes/interceptor.ts index c7fcaa782..34c8e4f4d 100644 --- a/src/app/classes/interceptor.ts +++ b/src/app/classes/interceptor.ts @@ -30,7 +30,7 @@ export class CoreInterceptor implements HttpInterceptor { * @param addNull Add null values to the serialized as empty parameters. * @return Serialization of the object. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any static serialize(obj: any, addNull?: boolean): string { let query = ''; @@ -61,7 +61,7 @@ export class CoreInterceptor implements HttpInterceptor { return query.length ? query.substr(0, query.length - 1) : query; } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any intercept(req: HttpRequest, next: HttpHandler): Observable { // Add the header and serialize the body if needed. const newReq = req.clone({ diff --git a/src/app/classes/ion-loading.ts b/src/app/classes/ion-loading.ts index 370b9fa25..450e0aced 100644 --- a/src/app/classes/ion-loading.ts +++ b/src/app/classes/ion-loading.ts @@ -25,7 +25,7 @@ export class CoreIonLoadingElement { constructor(public loading: HTMLIonLoadingElement) { } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any async dismiss(data?: any, role?: string): Promise { if (!this.isPresented || this.isDismissed) { this.isDismissed = true; diff --git a/src/app/classes/queue-runner.ts b/src/app/classes/queue-runner.ts index 3b05b1369..fb70f70b9 100644 --- a/src/app/classes/queue-runner.ts +++ b/src/app/classes/queue-runner.ts @@ -91,7 +91,7 @@ export class CoreQueueRunner { return; } - const item = this.orderedQueue.shift(); + const item = this.orderedQueue.shift()!; this.numberRunning++; try { diff --git a/src/app/classes/site.ts b/src/app/classes/site.ts index c02a3259f..7ff9b058d 100644 --- a/src/app/classes/site.ts +++ b/src/app/classes/site.ts @@ -32,12 +32,15 @@ import { CoreError } from '@classes/errors/error'; import { CoreWSError } from '@classes/errors/wserror'; import { CoreLogger } from '@singletons/logger'; import { Translate } from '@singletons/core.singletons'; +import { CoreIonLoadingElement } from './ion-loading'; /** * Class that represents a site (combination of site + user). * It will have all the site data and provide utility functions regarding a site. * To add tables to the site's database, please use CoreSitesProvider.registerSiteSchema. This will make sure that * the tables are created in all the sites, not just the current one. + * + * @todo: Refactor this class to improve "temporary" sites support (not fully authenticated). */ export class CoreSite { @@ -78,17 +81,17 @@ export class CoreSite { // Rest of variables. protected logger: CoreLogger; - protected db: SQLiteDB; + protected db?: SQLiteDB; protected cleanUnicode = false; protected lastAutoLogin = 0; protected offlineDisabled = false; // eslint-disable-next-line @typescript-eslint/no-explicit-any protected ongoingRequests: { [cacheId: string]: Promise } = {}; protected requestQueue: RequestQueueItem[] = []; - protected requestQueueTimeout: number = null; - protected tokenPluginFileWorks: boolean; - protected tokenPluginFileWorksPromise: Promise; - protected oauthId: number; + protected requestQueueTimeout: number | null = null; + protected tokenPluginFileWorks?: boolean; + protected tokenPluginFileWorksPromise?: Promise; + protected oauthId?: number; /** * Create a site. @@ -101,9 +104,16 @@ export class CoreSite { * @param config Site public config. * @param loggedOut Whether user is logged out. */ - constructor(public id: string, public siteUrl: string, public token?: string, public infos?: CoreSiteInfo, - public privateToken?: string, public config?: CoreSiteConfig, public loggedOut?: boolean) { - this.logger = CoreLogger.getInstance('CoreWSProvider'); + constructor( + public id: string | undefined, + public siteUrl: string, + public token?: string, + public infos?: CoreSiteInfo, + public privateToken?: string, + public config?: CoreSiteConfig, + public loggedOut?: boolean, + ) { + this.logger = CoreLogger.getInstance('CoreSite'); this.setInfo(infos); this.calculateOfflineDisabled(); @@ -125,6 +135,11 @@ export class CoreSite { * @return Site ID. */ getId(): string { + if (!this.id) { + // Shouldn't happen for authenticated sites. + throw new CoreError('This site doesn\'t have an ID'); + } + return this.id; } @@ -143,6 +158,11 @@ export class CoreSite { * @return Site token. */ getToken(): string { + if (!this.token) { + // Shouldn't happen for authenticated sites. + throw new CoreError('This site doesn\'t have a token'); + } + return this.token; } @@ -151,7 +171,7 @@ export class CoreSite { * * @return Site info. */ - getInfo(): CoreSiteInfo { + getInfo(): CoreSiteInfo | undefined { return this.infos; } @@ -160,7 +180,7 @@ export class CoreSite { * * @return Site private token. */ - getPrivateToken(): string { + getPrivateToken(): string | undefined { return this.privateToken; } @@ -170,6 +190,11 @@ export class CoreSite { * @return Site DB. */ getDb(): SQLiteDB { + if (!this.db) { + // Shouldn't happen for authenticated sites. + throw new CoreError('Site DB doesn\'t exist'); + } + return this.db; } @@ -179,9 +204,12 @@ export class CoreSite { * @return User's ID. */ getUserId(): number { - if (typeof this.infos != 'undefined' && typeof this.infos.userid != 'undefined') { - return this.infos.userid; + if (!this.infos) { + // Shouldn't happen for authenticated sites. + throw new CoreError('Site info could not be fetched.'); } + + return this.infos.userid; } /** @@ -190,7 +218,7 @@ export class CoreSite { * @return Site Home ID. */ getSiteHomeId(): number { - return this.infos && this.infos.siteid || 1; + return this.infos?.siteid || 1; } /** @@ -203,7 +231,7 @@ export class CoreSite { // Overridden by config. return CoreConfigConstants.sitename; } else { - return this.infos && this.infos.sitename || ''; + return this.infos?.sitename || ''; } } @@ -249,7 +277,7 @@ export class CoreSite { * * @return OAuth ID. */ - getOAuthId(): number { + getOAuthId(): number | undefined { return this.oauthId; } @@ -258,14 +286,14 @@ export class CoreSite { * * @param New info. */ - setInfo(infos: CoreSiteInfo): void { + setInfo(infos?: CoreSiteInfo): void { this.infos = infos; // Index function by name to speed up wsAvailable method. - if (infos && infos.functions) { + if (infos?.functions) { infos.functionsByName = {}; infos.functions.forEach((func) => { - infos.functionsByName[func.name] = func; + infos.functionsByName![func.name] = func; }); } } @@ -298,7 +326,7 @@ export class CoreSite { * * @param oauth OAuth ID. */ - setOAuthId(oauthId: number): void { + setOAuthId(oauthId: number | undefined): void { this.oauthId = oauthId; } @@ -317,9 +345,9 @@ export class CoreSite { * @return Whether can access my files. */ canAccessMyFiles(): boolean { - const infos = this.getInfo(); + const info = this.getInfo(); - return infos && (typeof infos.usercanmanageownfiles === 'undefined' || infos.usercanmanageownfiles); + return !!(info && (typeof info.usercanmanageownfiles === 'undefined' || info.usercanmanageownfiles)); } /** @@ -328,9 +356,9 @@ export class CoreSite { * @return Whether can download files. */ canDownloadFiles(): boolean { - const infos = this.getInfo(); + const info = this.getInfo(); - return infos && infos.downloadfiles > 0; + return !!info?.downloadfiles && info?.downloadfiles > 0; } /** @@ -340,22 +368,20 @@ export class CoreSite { * @param whenUndefined The value to return when the parameter is undefined. * @return Whether can use advanced feature. */ - canUseAdvancedFeature(feature: string, whenUndefined: boolean = true): boolean { - const infos = this.getInfo(); - let canUse = true; + canUseAdvancedFeature(featureName: string, whenUndefined: boolean = true): boolean { + const info = this.getInfo(); - if (typeof infos.advancedfeatures === 'undefined') { - canUse = whenUndefined; - } else { - for (const i in infos.advancedfeatures) { - const item = infos.advancedfeatures[i]; - if (item.name === feature && item.value === 0) { - canUse = false; - } - } + if (typeof info?.advancedfeatures === 'undefined') { + return whenUndefined; } - return canUse; + const feature = info.advancedfeatures.find((item) => item.name === featureName); + + if (!feature) { + return whenUndefined; + } + + return feature.value !== 0; } /** @@ -364,9 +390,9 @@ export class CoreSite { * @return Whether can upload files. */ canUploadFiles(): boolean { - const infos = this.getInfo(); + const info = this.getInfo(); - return infos && infos.uploadfiles > 0; + return !!info?.uploadfiles && info?.uploadfiles > 0; } /** @@ -396,7 +422,7 @@ export class CoreSite { * @param preSets Extra options. * @return Promise resolved with the response, rejected with CoreWSError if it fails. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any read(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { preSets = preSets || {}; if (typeof preSets.getFromCache == 'undefined') { @@ -420,7 +446,7 @@ export class CoreSite { * @param preSets Extra options. * @return Promise resolved with the response, rejected with CoreWSError if it fails. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any write(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { preSets = preSets || {}; if (typeof preSets.getFromCache == 'undefined') { @@ -455,13 +481,13 @@ export class CoreSite { * This method is smart which means that it will try to map the method to a compatibility one if need be, usually this * means that it will fallback on the 'local_mobile_' prefixed function if it is available and the non-prefixed is not. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any - request(method: string, data: any, preSets: CoreSiteWSPreSets, retrying?: boolean): Promise { - const initialToken = this.token; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async request(method: string, data: any, preSets: CoreSiteWSPreSets, retrying?: boolean): Promise { + const initialToken = this.token || ''; data = data || {}; if (!CoreApp.instance.isOnline() && this.offlineDisabled) { - return Promise.reject(new CoreError(Translate.instance.instant('core.errorofflinedisabled'))); + throw new CoreError(Translate.instance.instant('core.errorofflinedisabled')); } // Check if the method is available, use a prefixed version if possible. @@ -474,12 +500,12 @@ export class CoreSite { } else { this.logger.error(`WS function '${method}' is not available, even in compatibility mode.`); - return Promise.reject(new CoreError(Translate.instance.instant('core.wsfunctionnotavailable'))); + throw new CoreError(Translate.instance.instant('core.wsfunctionnotavailable')); } } const wsPreSets: CoreWSPreSets = { - wsToken: this.token, + wsToken: this.token || '', siteUrl: this.siteUrl, cleanUnicode: this.cleanUnicode, typeExpected: preSets.typeExpected, @@ -509,24 +535,25 @@ export class CoreSite { data = CoreWS.instance.convertValuesToString(data, wsPreSets.cleanUnicode); if (data == null) { // Empty cleaned text found. - return Promise.reject(new CoreError(Translate.instance.instant('core.unicodenotsupportedcleanerror'))); + throw new CoreError(Translate.instance.instant('core.unicodenotsupportedcleanerror')); } const cacheId = this.getCacheId(method, data); // Check for an ongoing identical request if we're not ignoring cache. if (preSets.getFromCache && this.ongoingRequests[cacheId]) { - return this.ongoingRequests[cacheId].then((response) => - // Clone the data, this may prevent errors if in the callback the object is modified. - CoreUtils.instance.clone(response), - ); + const response = await this.ongoingRequests[cacheId]; + + // Clone the data, this may prevent errors if in the callback the object is modified. + return CoreUtils.instance.clone(response); } const promise = this.getFromCache(method, data, preSets, false).catch(() => { if (preSets.forceOffline) { // Don't call the WS, just fail. - return Promise.reject(new CoreError(Translate.instance.instant('core.cannotconnect', - { $a: CoreSite.MINIMUM_MOODLE_VERSION }))); + throw new CoreError( + Translate.instance.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), + ); } // Call the WS. @@ -558,7 +585,7 @@ export class CoreSite { CoreEvents.instance.trigger(CoreEventsProvider.USER_DELETED, { params: data }, this.id); error.message = Translate.instance.instant('core.userdeleted'); - return Promise.reject(new CoreWSError(error)); + throw new CoreWSError(error); } else if (error.errorcode === 'forcepasswordchangenotice') { // Password Change Forced, trigger event. Try to get data from cache, the event will handle the error. CoreEvents.instance.trigger(CoreEventsProvider.PASSWORD_CHANGE_FORCED, {}, this.id); @@ -572,7 +599,7 @@ export class CoreSite { CoreEvents.instance.trigger(CoreEventsProvider.SITE_POLICY_NOT_AGREED, {}, this.id); error.message = Translate.instance.instant('core.login.sitepolicynotagreederror'); - return Promise.reject(new CoreWSError(error)); + throw new CoreWSError(error); } else if (error.errorcode === 'dmlwriteexception' && CoreTextUtils.instance.hasUnicodeData(data)) { if (!this.cleanUnicode) { // Try again cleaning unicode. @@ -583,7 +610,7 @@ export class CoreSite { // This should not happen. error.message = Translate.instance.instant('core.unicodenotsupported'); - return Promise.reject(new CoreWSError(error)); + throw new CoreWSError(error); } else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission' || error.errorcode === 'notingroup') { // Translate error messages with missing strings. @@ -596,16 +623,16 @@ export class CoreSite { // Save the error instead of deleting the cache entry so the same content is displayed in offline. this.saveToCache(method, data, error, preSets); - return Promise.reject(new CoreWSError(error)); + throw new CoreWSError(error); } else if (preSets.cacheErrors && preSets.cacheErrors.indexOf(error.errorcode) != -1) { // Save the error instead of deleting the cache entry so the same content is displayed in offline. this.saveToCache(method, data, error, preSets); - return Promise.reject(new CoreWSError(error)); + throw new CoreWSError(error); } else if (typeof preSets.emergencyCache !== 'undefined' && !preSets.emergencyCache) { this.logger.debug(`WS call '${method}' failed. Emergency cache is forbidden, rejecting.`); - return Promise.reject(new CoreWSError(error)); + throw new CoreWSError(error); } if (preSets.deleteCacheIfWSError && CoreUtils.instance.isWebServiceError(error)) { @@ -614,7 +641,7 @@ export class CoreSite { // Ignore errors. }); - return Promise.reject(new CoreWSError(error)); + throw new CoreWSError(error); } this.logger.debug(`WS call '${method}' failed. Trying to use the emergency cache.`); @@ -623,11 +650,11 @@ export class CoreSite { return this.getFromCache(method, data, preSets, true).catch(() => Promise.reject(new CoreWSError(error))); }); - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any }).then((response: any) => { // Check if the response is an error, this happens if the error was stored in the cache. if (response && (typeof response.exception != 'undefined' || typeof response.errorcode != 'undefined')) { - return Promise.reject(new CoreWSError(response)); + throw new CoreWSError(response); } return response; @@ -636,15 +663,17 @@ export class CoreSite { this.ongoingRequests[cacheId] = promise; // Clear ongoing request after setting the promise (just in case it's already resolved). - return promise.finally(() => { + try { + const response = await promise; + + // We pass back a clone of the original object, this may prevent errors if in the callback the object is modified. + return CoreUtils.instance.clone(response); + } finally { // Make sure we don't clear the promise of a newer request that ignores the cache. if (this.ongoingRequests[cacheId] === promise) { delete this.ongoingRequests[cacheId]; } - }).then((response) => - // We pass back a clone of the original object, this may prevent errors if in the callback the object is modified. - CoreUtils.instance.clone(response), - ); + } } /** @@ -658,8 +687,7 @@ export class CoreSite { */ protected callOrEnqueueRequest( method: string, - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any - data: any, + data: any, // eslint-disable-line @typescript-eslint/no-explicit-any preSets: CoreSiteWSPreSets, wsPreSets: CoreWSPreSets, ): Promise { @@ -677,13 +705,13 @@ export class CoreSite { } } - const request: RequestQueueItem = { + const request: RequestQueueItem = { cacheId, method, data, preSets, wsPreSets, - deferred: CoreUtils.instance.promiseDefer(), + deferred: CoreUtils.instance.promiseDefer(), }; return this.enqueueRequest(request); @@ -695,8 +723,7 @@ export class CoreSite { * @param request The request to enqueue. * @return Promise resolved with the response when the WS is called. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any - protected enqueueRequest(request: RequestQueueItem): Promise { + protected enqueueRequest(request: RequestQueueItem): Promise { this.requestQueue.push(request); if (this.requestQueue.length >= CoreSite.REQUEST_QUEUE_LIMIT) { @@ -711,7 +738,7 @@ export class CoreSite { /** * Call the enqueued web service requests. */ - protected processRequestQueue(): void { + protected async processRequestQueue(): Promise { this.logger.debug(`Processing request queue (${this.requestQueue.length} requests)`); // Clear timeout if set. @@ -726,16 +753,18 @@ export class CoreSite { if (requests.length == 1 && !CoreSite.REQUEST_QUEUE_FORCE_WS) { // Only one request, do a regular web service call. - CoreWS.instance.call(requests[0].method, requests[0].data, requests[0].wsPreSets).then((data) => { + try { + const data = await CoreWS.instance.call(requests[0].method, requests[0].data, requests[0].wsPreSets); + requests[0].deferred.resolve(data); - }).catch((error) => { + } catch (error) { requests[0].deferred.reject(error); - }); + } return; } - const data = { + const requestsData = { requests: requests.map((request) => { const args = {}; const settings = {}; @@ -765,13 +794,18 @@ export class CoreSite { const wsPresets: CoreWSPreSets = { siteUrl: this.siteUrl, - wsToken: this.token, + wsToken: this.token || '', }; - CoreWS.instance.call('tool_mobile_call_external_functions', data, wsPresets) - .then((data) => { + try { + const data = await CoreWS.instance.call( + 'tool_mobile_call_external_functions', + requestsData, + wsPresets, + ); + if (!data || !data.responses) { - return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); + throw new CoreError(Translate.instance.instant('core.errorinvalidresponse')); } requests.forEach((request, i) => { @@ -781,9 +815,9 @@ export class CoreSite { // Request not executed, enqueue again. this.enqueueRequest(request); } else if (response.error) { - request.deferred.reject(CoreTextUtils.instance.parseJSON(response.exception)); + request.deferred.reject(CoreTextUtils.instance.parseJSON(response.exception || '')); } else { - let responseData = CoreTextUtils.instance.parseJSON(response.data); + let responseData = response.data ? CoreTextUtils.instance.parseJSON(response.data) : {}; // Match the behaviour of CoreWSProvider.call when no response is expected. const responseExpected = typeof wsPresets.responseExpected == 'undefined' || wsPresets.responseExpected; if (!responseExpected && (responseData == null || responseData === '')) { @@ -792,12 +826,12 @@ export class CoreSite { request.deferred.resolve(responseData); } }); - }).catch((error) => { + } catch (error) { // Error not specific to a single request, reject all promises. requests.forEach((request) => { request.deferred.reject(error); }); - }); + } } /** @@ -812,7 +846,7 @@ export class CoreSite { return false; } - if (this.infos.functionsByName[method]) { + if (this.infos?.functionsByName?.[method]) { return true; } @@ -831,7 +865,7 @@ export class CoreSite { * @param data Arguments to pass to the method. * @return Cache ID. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any protected getCacheId(method: string, data: any): string { return Md5.hashAsciiStr(method + ':' + CoreUtils.instance.sortAndStringify(data)); } @@ -846,66 +880,70 @@ export class CoreSite { * @param originalData Arguments to pass to the method before being converted to strings. * @return Promise resolved with the WS response. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any - protected getFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, emergency?: boolean): Promise { + protected async getFromCache( + method: string, + data: any, // eslint-disable-line @typescript-eslint/no-explicit-any + preSets: CoreSiteWSPreSets, + emergency?: boolean, + ): Promise { if (!this.db || !preSets.getFromCache) { - return Promise.reject(new CoreError('Get from cache is disabled.')); + throw new CoreError('Get from cache is disabled.'); } const id = this.getCacheId(method, data); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let promise: Promise; + let entry: CoreSiteWSCacheRecord | undefined; if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) { - promise = this.db.getRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey }).then((entries) => { - if (!entries.length) { - // Cache key not found, get by params sent. - return this.db.getRecord(CoreSite.WS_CACHE_TABLE, { id }); - } else if (entries.length > 1) { + const entries = await this.db.getRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey }); + + if (!entries.length) { + // Cache key not found, get by params sent. + entry = await this.db!.getRecord(CoreSite.WS_CACHE_TABLE, { id }); + } else { + if (entries.length > 1) { // More than one entry found. Search the one with same ID as this call. - for (let i = 0, len = entries.length; i < len; i++) { - const entry = entries[i]; - if (entry.id == id) { - return entry; - } - } + entry = entries.find((entry) => entry.id == id); } - return entries[0]; - }); + if (!entry) { + entry = entries[0]; + } + } } else { - promise = this.db.getRecord(CoreSite.WS_CACHE_TABLE, { id }); + entry = await this.db!.getRecord(CoreSite.WS_CACHE_TABLE, { id }); } - return promise.then((entry) => { - const now = Date.now(); - let expirationTime: number; + if (typeof entry == 'undefined') { + throw new CoreError('Cache entry not valid.'); + } - preSets.omitExpires = preSets.omitExpires || preSets.forceOffline || !CoreApp.instance.isOnline(); + const now = Date.now(); + let expirationTime: number | undefined; - if (!preSets.omitExpires) { - expirationTime = entry.expirationTime + this.getExpirationDelay(preSets.updateFrequency); + preSets.omitExpires = preSets.omitExpires || preSets.forceOffline || !CoreApp.instance.isOnline(); - if (now > expirationTime) { - this.logger.debug('Cached element found, but it is expired'); + if (!preSets.omitExpires) { + expirationTime = entry.expirationTime + this.getExpirationDelay(preSets.updateFrequency); - return Promise.reject(new CoreError('Cache entry is expired.')); - } + if (now > expirationTime!) { + this.logger.debug('Cached element found, but it is expired'); + + throw new CoreError('Cache entry is expired.'); + } + } + + if (typeof entry.data != 'undefined') { + if (!expirationTime) { + this.logger.info(`Cached element found, id: ${id}. Expiration time ignored.`); + } else { + const expires = (expirationTime - now) / 1000; + this.logger.info(`Cached element found, id: ${id}. Expires in expires in ${expires} seconds`); } - if (typeof entry != 'undefined' && typeof entry.data != 'undefined') { - if (!expirationTime) { - this.logger.info(`Cached element found, id: ${id}. Expiration time ignored.`); - } else { - const expires = (expirationTime - now) / 1000; - this.logger.info(`Cached element found, id: ${id}. Expires in expires in ${expires} seconds`); - } + return CoreTextUtils.instance.parseJSON(entry.data, {}); + } - return CoreTextUtils.instance.parseJSON(entry.data, {}); - } - - return Promise.reject(new CoreError('Cache entry not valid.')); - }); + throw new CoreError('Cache entry not valid.'); } /** @@ -923,8 +961,10 @@ export class CoreSite { extraClause = ' AND componentId = ?'; } - const size = await this.db.getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE + - ' WHERE component = ?' + extraClause, params); + const size = await this.getDb().getFieldSql( + 'SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE + ' WHERE component = ?' + extraClause, + params, + ); return size; } @@ -938,7 +978,7 @@ export class CoreSite { * @param preSets Extra options. * @return Promise resolved when the response is saved. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any protected async saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets): Promise { if (!this.db) { throw new CoreError('Site DB not initialized.'); @@ -981,7 +1021,7 @@ export class CoreSite { * @param allCacheKey True to delete all entries with the cache key, false to delete only by ID. * @return Promise resolved when the entries are deleted. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise { if (!this.db) { throw new CoreError('Site DB not initialized.'); @@ -1042,7 +1082,7 @@ export class CoreSite { return CoreWS.instance.uploadFile(filePath, options, { siteUrl: this.siteUrl, - wsToken: this.token, + wsToken: this.token || '', }, onProgress); } @@ -1144,7 +1184,7 @@ export class CoreSite { const accessKey = this.tokenPluginFileWorks || typeof this.tokenPluginFileWorks == 'undefined' ? this.infos && this.infos.userprivateaccesskey : undefined; - return CoreUrlUtils.instance.fixPluginfileURL(url, this.token, this.siteUrl, accessKey); + return CoreUrlUtils.instance.fixPluginfileURL(url, this.token || '', this.siteUrl, accessKey); } /** @@ -1162,7 +1202,7 @@ export class CoreSite { * @return Promise to be resolved when the DB is deleted. */ async deleteFolder(): Promise { - if (!CoreFile.instance.isAvailable()) { + if (!CoreFile.instance.isAvailable() || !this.id) { return; } @@ -1178,7 +1218,7 @@ export class CoreSite { * @return Promise resolved with the site space usage (size). */ getSpaceUsage(): Promise { - if (CoreFile.instance.isAvailable()) { + if (CoreFile.instance.isAvailable() && this.id) { const siteFolderPath = CoreFile.instance.getSiteFolder(this.id); return CoreFile.instance.getDirectorySize(siteFolderPath).catch(() => 0); @@ -1195,7 +1235,7 @@ export class CoreSite { * @return Promise resolved with the total size of all data in the cache table (bytes) */ async getCacheUsage(): Promise { - const size = await this.db.getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE); + const size = await this.getDb().getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE); return size; } @@ -1219,7 +1259,7 @@ export class CoreSite { * @return Promise resolved with the Moodle docs URL. */ getDocsUrl(page?: string): Promise { - const release = this.infos.release ? this.infos.release : undefined; + const release = this.infos?.release ? this.infos.release : undefined; return CoreUrlUtils.instance.getDocsUrl(release, page); } @@ -1369,37 +1409,43 @@ export class CoreSite { * * @return Promise resolved with public config. Rejected with an object if error, see CoreWSProvider.callAjax. */ - getPublicConfig(): Promise { + async getPublicConfig(): Promise { const preSets: CoreWSAjaxPreSets = { siteUrl: this.siteUrl, }; - return CoreWS.instance.callAjax('tool_mobile_get_public_config', {}, preSets).catch((error) => { + let config: CoreSitePublicConfigResponse | undefined; + + try { + config = await CoreWS.instance.callAjax('tool_mobile_get_public_config', {}, preSets); + } catch (error) { if ((!this.getInfo() || this.isVersionGreaterEqualThan('3.8')) && error && error.errorcode == 'codingerror') { // This error probably means that there is a redirect in the site. Try to use a GET request. preSets.noLogin = true; preSets.useGet = true; - return CoreWS.instance.callAjax('tool_mobile_get_public_config', {}, preSets).catch((error2) => { + try { + config = await CoreWS.instance.callAjax('tool_mobile_get_public_config', {}, preSets); + } catch (error2) { if (this.getInfo() && this.isVersionGreaterEqualThan('3.8')) { // GET is supported, return the second error. - return Promise.reject(error2); + throw error2; } else { // GET not supported or we don't know if it's supported. Return first error. - return Promise.reject(error); + throw error; } - }); + } } - return Promise.reject(error); - }).then((config: CoreSitePublicConfigResponse) => { - // Use the wwwroot returned by the server. - if (config.httpswwwroot) { - this.siteUrl = config.httpswwwroot; - } + throw error; + } - return config; - }); + // Use the wwwroot returned by the server. + if (config!.httpswwwroot) { + this.siteUrl = config!.httpswwwroot; + } + + return config!; } /** @@ -1446,8 +1492,11 @@ export class CoreSite { * @param alertMessage If defined, an alert will be shown before opening the inappbrowser. * @return Promise resolved when done. */ - async openInAppWithAutoLoginIfSameSite(url: string, options?: InAppBrowserOptions, alertMessage?: string): - Promise { + async openInAppWithAutoLoginIfSameSite( + url: string, + options?: InAppBrowserOptions, + alertMessage?: string, + ): Promise { const iabInstance = await this.openWithAutoLoginIfSameSite(true, url, options, alertMessage); return iabInstance; @@ -1462,34 +1511,33 @@ export class CoreSite { * @param alertMessage If defined, an alert will be shown before opening the browser/inappbrowser. * @return Promise resolved when done. Resolve param is returned only if inApp=true. */ - openWithAutoLogin(inApp: boolean, url: string, options?: InAppBrowserOptions, alertMessage?: string): - Promise { + async openWithAutoLogin( + inApp: boolean, + url: string, + options?: InAppBrowserOptions, + alertMessage?: string, + ): Promise { // Get the URL to open. - return this.getAutoLoginUrl(url).then((url) => { - if (!alertMessage) { - // Just open the URL. - if (inApp) { - return CoreUtils.instance.openInApp(url, options); - } else { - return CoreUtils.instance.openInBrowser(url); - } - } + url = await this.getAutoLoginUrl(url); + if (alertMessage) { // Show an alert first. - return CoreDomUtils.instance.showAlert(Translate.instance.instant('core.notice'), alertMessage, undefined, 3000) - .then(() => new Promise(() => { - // @todo - // const subscription = alert.didDismiss.subscribe(() => { - // subscription && subscription.unsubscribe(); + const alert = await CoreDomUtils.instance.showAlert( + Translate.instance.instant('core.notice'), + alertMessage, + undefined, + 3000, + ); - // if (inApp) { - // resolve(CoreUtils.instance.openInApp(url, options)); - // } else { - // resolve(CoreUtils.instance.openInBrowser(url)); - // } - // }); - })); - }); + await alert.onDidDismiss(); + } + + // Open the URL. + if (inApp) { + return CoreUtils.instance.openInApp(url, options); + } else { + return CoreUtils.instance.openInBrowser(url); + } } /** @@ -1501,8 +1549,12 @@ export class CoreSite { * @param alertMessage If defined, an alert will be shown before opening the browser/inappbrowser. * @return Promise resolved when done. Resolve param is returned only if inApp=true. */ - openWithAutoLoginIfSameSite(inApp: boolean, url: string, options?: InAppBrowserOptions, alertMessage?: string): - Promise { + async openWithAutoLoginIfSameSite( + inApp: boolean, + url: string, + options?: InAppBrowserOptions, + alertMessage?: string, + ): Promise { if (this.containsUrl(url)) { return this.openWithAutoLogin(inApp, url, options, alertMessage); } else { @@ -1522,6 +1574,8 @@ export class CoreSite { * @param ignoreCache True if it should ignore cached data. * @return Promise resolved with site config. */ + getConfig(name?: undefined, ignoreCache?: boolean): Promise; + getConfig(name: string, ignoreCache?: boolean): Promise; getConfig(name?: string, ignoreCache?: boolean): Promise { const preSets: CoreSiteWSPreSets = { cacheKey: this.getConfigCacheKey(), @@ -1578,7 +1632,9 @@ export class CoreSite { * @param name Name of the setting to get. If not set, all settings will be returned. * @return Site config or a specific setting. */ - getStoredConfig(name?: string): string | CoreSiteConfig { + getStoredConfig(): CoreSiteConfig | undefined; + getStoredConfig(name: string): string | undefined; + getStoredConfig(name?: string): string | CoreSiteConfig | undefined { if (!this.config) { return; } @@ -1597,7 +1653,7 @@ export class CoreSite { * @return Whether it's disabled. */ isFeatureDisabled(name: string): boolean { - const disabledFeatures = this.getStoredConfig('tool_mobile_disabledfeatures'); + const disabledFeatures = this.getStoredConfig('tool_mobile_disabledfeatures'); if (!disabledFeatures) { return false; } @@ -1645,7 +1701,13 @@ export class CoreSite { * it's the last released major version. */ isVersionGreaterEqualThan(versions: string | string[]): boolean { - const siteVersion = parseInt(this.getInfo().version, 10); + const info = this.getInfo(); + + if (!info || !info.version) { + return false; + } + + const siteVersion = Number(info.version); if (Array.isArray(versions)) { if (!versions.length) { @@ -1679,26 +1741,27 @@ export class CoreSite { * @param showModal Whether to show a loading modal. * @return Promise resolved with the converted URL. */ - getAutoLoginUrl(url: string, showModal: boolean = true): Promise { + async getAutoLoginUrl(url: string, showModal: boolean = true): Promise { if (!this.privateToken || !this.wsAvailable('tool_mobile_get_autologin_key') || (this.lastAutoLogin && CoreTimeUtils.instance.timestamp() - this.lastAutoLogin < CoreConstants.SECONDS_MINUTE * 6)) { // No private token, WS not available or last auto-login was less than 6 minutes ago. Don't change the URL. - - return Promise.resolve(url); + return url; } const userId = this.getUserId(); const params = { privatetoken: this.privateToken, }; - let modal; + let modal: CoreIonLoadingElement | undefined; if (showModal) { - modal = CoreDomUtils.instance.showModalLoading(); + modal = await CoreDomUtils.instance.showModalLoading(); } - // Use write to not use cache. - return this.write('tool_mobile_get_autologin_key', params).then((data) => { + try { + // Use write to not use cache. + const data = await this.write('tool_mobile_get_autologin_key', params); + if (!data.autologinurl || !data.key) { // Not valid data, return the same URL. return url; @@ -1707,13 +1770,12 @@ export class CoreSite { this.lastAutoLogin = CoreTimeUtils.instance.timestamp(); return data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + encodeURIComponent(url); - }).catch(() => - + } catch (error) { // Couldn't get autologin key, return the same URL. - url, - ).finally(() => { - modal && modal.dismiss(); - }); + return url; + } finally { + modal?.dismiss(); + } } /** @@ -1733,7 +1795,7 @@ export class CoreSite { if (typeof this.MOODLE_RELEASES[data.major] == 'undefined') { // Major version not found. Use the last one. - data.major = Object.keys(this.MOODLE_RELEASES).pop(); + data.major = Object.keys(this.MOODLE_RELEASES).pop()!; } return this.MOODLE_RELEASES[data.major] + data.minor; @@ -1790,7 +1852,7 @@ export class CoreSite { * @return Promise resolved when done. */ async deleteSiteConfig(name: string): Promise { - await this.db.deleteRecords(CoreSite.CONFIG_TABLE, { name }); + await this.getDb().deleteRecords(CoreSite.CONFIG_TABLE, { name }); } /** @@ -1800,15 +1862,18 @@ export class CoreSite { * @param defaultValue Default value to use if the entry is not found. * @return Resolves upon success along with the config data. Reject on failure. */ - getLocalSiteConfig(name: string, defaultValue?: T): Promise { - return this.db.getRecord(CoreSite.CONFIG_TABLE, { name }).then((entry) => entry.value) - .catch((error) => { + async getLocalSiteConfig(name: string, defaultValue?: T): Promise { + try { + const entry = await this.getDb().getRecord(CoreSite.CONFIG_TABLE, { name }); + + return entry.value; + } catch (error) { if (typeof defaultValue != 'undefined') { return defaultValue; } - return Promise.reject(error); - }); + throw error; + } } /** @@ -1819,7 +1884,7 @@ export class CoreSite { * @return Promise resolved when done. */ async setLocalSiteConfig(name: string, value: number | string): Promise { - await this.db.insertRecord(CoreSite.CONFIG_TABLE, { name, value }); + await this.getDb().insertRecord(CoreSite.CONFIG_TABLE, { name, value }); } /** @@ -1829,6 +1894,7 @@ export class CoreSite { * @return Expiration delay. */ getExpirationDelay(updateFrequency?: number): number { + updateFrequency = updateFrequency || CoreSite.FREQUENCY_USUALLY; let expirationDelay = this.UPDATE_FREQUENCIES[updateFrequency] || this.UPDATE_FREQUENCIES[CoreSite.FREQUENCY_USUALLY]; if (CoreApp.instance.isNetworkAccessLimited()) { @@ -2010,15 +2076,14 @@ export type LocalMobileResponse = { /** * Info of a request waiting in the queue. */ -type RequestQueueItem = { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type RequestQueueItem = { cacheId: string; method: string; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any - data: any; + data: any; // eslint-disable-line @typescript-eslint/no-explicit-any preSets: CoreSiteWSPreSets; wsPreSets: CoreWSPreSets; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any - deferred: PromiseDefer; + deferred: PromiseDefer; }; /** @@ -2166,3 +2231,12 @@ export type CoreSiteConfigDBRecord = { name: string; value: string | number; }; + +export type CoreSiteWSCacheRecord = { + id: string; + data: string; + expirationTime: number; + key?: string; + component?: string; + componentId?: number; +}; diff --git a/src/app/classes/sqlitedb.ts b/src/app/classes/sqlitedb.ts index a46729325..14fff4066 100644 --- a/src/app/classes/sqlitedb.ts +++ b/src/app/classes/sqlitedb.ts @@ -472,7 +472,7 @@ export class SQLiteDB { * @return List of params. */ protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] { - return Object.keys(data).map((key) => data[key]); + return Object.keys(data).map((key) => data[key]!); } /** @@ -1087,7 +1087,7 @@ export class SQLiteDB { } export type SQLiteDBRecordValues = { - [key in string ]: SQLiteDBRecordValue; + [key in string ]: SQLiteDBRecordValue | undefined; }; export type SQLiteDBQueryParams = { diff --git a/src/app/components/icon/icon.ts b/src/app/components/icon/icon.ts index eea96b488..830ed8375 100644 --- a/src/app/components/icon/icon.ts +++ b/src/app/components/icon/icon.ts @@ -38,7 +38,7 @@ export class CoreIconComponent implements OnChanges, OnDestroy { @Input() ios?: string; // FontAwesome params. - @Input('fixed-width') fixedWidth: boolean; + @Input('fixed-width') fixedWidth?: boolean; // eslint-disable-line @angular-eslint/no-input-rename @Input() label?: string; @Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false. @@ -48,7 +48,7 @@ export class CoreIconComponent implements OnChanges, OnDestroy { constructor(el: ElementRef) { this.element = el.nativeElement; - this.newElement = this.element + this.newElement = this.element; } /** diff --git a/src/app/components/loading/loading.ts b/src/app/components/loading/loading.ts index baf58d6cf..6ce15ee24 100644 --- a/src/app/components/loading/loading.ts +++ b/src/app/components/loading/loading.ts @@ -48,9 +48,9 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { @Input() hideUntil: unknown; // Determine when should the contents be shown. @Input() message?: string; // Message to show while loading. - @ViewChild('content') content: ElementRef; + @ViewChild('content') content?: ElementRef; - protected uniqueId: string; + protected uniqueId!: string; protected element: HTMLElement; // Current element. constructor(element: ElementRef) { diff --git a/src/app/components/show-password/show-password.ts b/src/app/components/show-password/show-password.ts index e2d79fa4d..12dafefe4 100644 --- a/src/app/components/show-password/show-password.ts +++ b/src/app/components/show-password/show-password.ts @@ -40,16 +40,16 @@ import { CoreUtils } from '@services/utils/utils'; }) export class CoreShowPasswordComponent implements OnInit, AfterViewInit { - @Input() name: string; // Name of the input affected. + @Input() name?: string; // Name of the input affected. @Input() initialShown?: boolean | string; // Whether the password should be shown at start. - @ContentChild(IonInput) ionInput: IonInput; + @ContentChild(IonInput) ionInput?: IonInput; - shown: boolean; // Whether the password is shown. - label: string; // Label for the button to show/hide. - iconName: string; // Name of the icon of the button to show/hide. + shown!: boolean; // Whether the password is shown. + label?: string; // Label for the button to show/hide. + iconName?: string; // Name of the icon of the button to show/hide. selector = ''; // Selector to identify the input. - protected input: HTMLInputElement; // Input affected. + protected input?: HTMLInputElement | null; // Input affected. protected element: HTMLElement; // Current element. constructor(element: ElementRef) { @@ -84,7 +84,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit { } // Search the input. - this.input = this.element.querySelector(this.selector); + this.input = this.element.querySelector(this.selector); if (this.input) { // Input found. Set the right type. @@ -128,7 +128,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit { if (isFocused && CoreApp.instance.isAndroid()) { // In Android, the keyboard is closed when the input type changes. Focus it again. setTimeout(() => { - CoreDomUtils.instance.focusElement(this.input); + CoreDomUtils.instance.focusElement(this.input!); }, 400); } } diff --git a/src/app/core/constants.ts b/src/app/core/constants.ts index def5471dd..41c7c1b3f 100644 --- a/src/app/core/constants.ts +++ b/src/app/core/constants.ts @@ -39,7 +39,7 @@ export class CoreConstants { static readonly DOWNLOAD_THRESHOLD = 10485760; // 10MB. static readonly MINIMUM_FREE_SPACE = 10485760; // 10MB. static readonly IOS_FREE_SPACE_THRESHOLD = 524288000; // 500MB. - static readonly DONT_SHOW_ERROR = 'CoreDontShowError'; + static readonly DONT_SHOW_ERROR = 'CoreDontShowError'; // @deprecated since 3.9.5. Use CoreSilentError instead. static readonly NO_SITE_ID = 'NoSite'; // Settings constants. diff --git a/src/app/core/emulator/classes/sqlitedb.ts b/src/app/core/emulator/classes/sqlitedb.ts index b327029fd..055944f71 100644 --- a/src/app/core/emulator/classes/sqlitedb.ts +++ b/src/app/core/emulator/classes/sqlitedb.ts @@ -35,6 +35,7 @@ export class SQLiteDBMock extends SQLiteDB { * * @return Promise resolved when done. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any close(): Promise { // WebSQL databases aren't closed. return Promise.resolve(); @@ -45,6 +46,7 @@ export class SQLiteDBMock extends SQLiteDB { * * @return Promise resolved when done. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any async emptyDatabase(): Promise { await this.ready(); @@ -89,6 +91,7 @@ export class SQLiteDBMock extends SQLiteDB { * @param params Query parameters. * @return Promise resolved with the result. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any async execute(sql: string, params?: any[]): Promise { await this.ready(); @@ -115,6 +118,7 @@ export class SQLiteDBMock extends SQLiteDB { * @param sqlStatements SQL statements to execute. * @return Promise resolved with the result. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any async executeBatch(sqlStatements: any[]): Promise { await this.ready(); @@ -148,6 +152,7 @@ export class SQLiteDBMock extends SQLiteDB { })); }); + // eslint-disable-next-line promise/catch-or-return Promise.all(promises).then(resolve, reject); }); }); @@ -158,6 +163,7 @@ export class SQLiteDBMock extends SQLiteDB { */ init(): void { // This DB is for desktop apps, so use a big size to be sure it isn't filled. + // eslint-disable-next-line @typescript-eslint/no-explicit-any this.db = ( window).openDatabase(this.name, '1.0', this.name, 500 * 1024 * 1024); this.promise = Promise.resolve(); } diff --git a/src/app/core/login/pages/credentials/credentials.page.ts b/src/app/core/login/pages/credentials/credentials.page.ts index 57fa2913e..3bcc2f00e 100644 --- a/src/app/core/login/pages/credentials/credentials.page.ts +++ b/src/app/core/login/pages/credentials/credentials.page.ts @@ -36,27 +36,27 @@ import { CoreEvents, CoreEventsProvider } from '@/app/services/events'; }) export class CoreLoginCredentialsPage implements OnInit, OnDestroy { - @ViewChild('credentialsForm') formElement: ElementRef; + @ViewChild('credentialsForm') formElement?: ElementRef; - credForm: FormGroup; - siteUrl: string; + credForm!: FormGroup; + siteUrl!: string; siteChecked = false; - siteName: string; - logoUrl: string; - authInstructions: string; - canSignup: boolean; - identityProviders: CoreSiteIdentityProvider[]; + siteName?: string; + logoUrl?: string; + authInstructions?: string; + canSignup?: boolean; + identityProviders?: CoreSiteIdentityProvider[]; pageLoaded = false; isBrowserSSO = false; isFixedUrlSet = false; showForgottenPassword = true; showScanQR: boolean; - protected siteConfig: CoreSitePublicConfigResponse; + protected siteConfig?: CoreSitePublicConfigResponse; protected eventThrown = false; protected viewLeft = false; - protected siteId: string; - protected urlToOpen: string; + protected siteId?: string; + protected urlToOpen?: string; constructor( protected fb: FormBuilder, @@ -82,8 +82,8 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { ngOnInit(): void { this.route.queryParams.subscribe(params => { this.siteUrl = params['siteUrl']; - this.siteName = params['siteName'] || null; - this.logoUrl = !CoreConfigConstants.forceLoginLogo && params['logoUrl'] || null; + this.siteName = params['siteName'] || undefined; + this.logoUrl = !CoreConfigConstants.forceLoginLogo && params['logoUrl'] || undefined; this.siteConfig = params['siteConfig']; this.urlToOpen = params['urlToOpen']; @@ -138,7 +138,11 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { // Check that there's no SSO authentication ongoing and the view hasn't changed. if (!CoreApp.instance.isSSOAuthenticationOngoing() && !this.viewLeft) { CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin( - result.siteUrl, result.code, result.service, result.config?.launchurl); + result.siteUrl, + result.code, + result.service, + result.config?.launchurl, + ); } } else { this.isBrowserSSO = false; @@ -171,7 +175,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { CoreEvents.instance.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, { config: this.siteConfig }); } } else { - this.authInstructions = null; + this.authInstructions = undefined; this.canSignup = false; this.identityProviders = []; } @@ -261,7 +265,11 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { */ forgottenPassword(): void { CoreLoginHelper.instance.forgottenPasswordClicked( - this.navCtrl, this.siteUrl, this.credForm.value.username, this.siteConfig); + this.navCtrl, + this.siteUrl, + this.credForm.value.username, + this.siteConfig, + ); } /** @@ -270,7 +278,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { * @param provider The provider that was clicked. */ oauthClicked(provider: CoreSiteIdentityProvider): void { - if (!CoreLoginHelper.instance.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig.launchurl)) { + if (!CoreLoginHelper.instance.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig?.launchurl)) { CoreDomUtils.instance.showErrorModal('Invalid data.'); } } @@ -289,8 +297,10 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { // Show some instructions first. CoreDomUtils.instance.showAlertWithOptions({ header: Translate.instance.instant('core.login.faqwhereisqrcode'), - message: Translate.instance.instant('core.login.faqwhereisqrcodeanswer', - { $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }), + message: Translate.instance.instant( + 'core.login.faqwhereisqrcodeanswer', + { $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }, + ), buttons: [ { text: Translate.instance.instant('core.cancel'), diff --git a/src/app/core/login/pages/init/init.page.ts b/src/app/core/login/pages/init/init.page.ts index 006ec48c6..762be4743 100644 --- a/src/app/core/login/pages/init/init.page.ts +++ b/src/app/core/login/pages/init/init.page.ts @@ -34,44 +34,44 @@ export class CoreLoginInitPage implements OnInit { /** * Initialize the component. */ - ngOnInit(): void { + async ngOnInit(): Promise { // Wait for the app to be ready. - CoreInit.instance.ready().then(() => { - // Check if there was a pending redirect. - const redirectData = CoreApp.instance.getRedirect(); - if (redirectData.siteId) { - // Unset redirect data. - CoreApp.instance.storeRedirect('', '', {}); + await CoreInit.instance.ready(); - // Only accept the redirect if it was stored less than 20 seconds ago. - if (Date.now() - redirectData.timemodified < 20000) { - // if (redirectData.siteId != CoreConstants.NO_SITE_ID) { - // // The redirect is pointing to a site, load it. - // return this.sitesProvider.loadSite(redirectData.siteId, redirectData.page, redirectData.params) - // .then((loggedIn) => { + // Check if there was a pending redirect. + const redirectData = CoreApp.instance.getRedirect(); + if (redirectData.siteId) { + // Unset redirect data. + CoreApp.instance.storeRedirect('', '', {}); - // if (loggedIn) { - // return this.loginHelper.goToSiteInitialPage(this.navCtrl, redirectData.page, redirectData.params, - // { animate: false }); - // } - // }).catch(() => { - // // Site doesn't exist. - // return this.loadPage(); - // }); - // } else { - // // No site to load, open the page. - // return this.loginHelper.goToNoSitePage(this.navCtrl, redirectData.page, redirectData.params); - // } - } + // Only accept the redirect if it was stored less than 20 seconds ago. + if (redirectData.timemodified && Date.now() - redirectData.timemodified < 20000) { + // if (redirectData.siteId != CoreConstants.NO_SITE_ID) { + // // The redirect is pointing to a site, load it. + // return this.sitesProvider.loadSite(redirectData.siteId, redirectData.page, redirectData.params) + // .then((loggedIn) => { + + // if (loggedIn) { + // return this.loginHelper.goToSiteInitialPage(this.navCtrl, redirectData.page, redirectData.params, + // { animate: false }); + // } + // }).catch(() => { + // // Site doesn't exist. + // return this.loadPage(); + // }); + // } else { + // // No site to load, open the page. + // return this.loginHelper.goToNoSitePage(this.navCtrl, redirectData.page, redirectData.params); + // } } + } - return this.loadPage(); - }).then(() => { - // If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen. - setTimeout(() => { - SplashScreen.instance.hide(); - }, 100); - }); + await this.loadPage(); + + // If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen. + setTimeout(() => { + SplashScreen.instance.hide(); + }, 100); } /** diff --git a/src/app/core/login/pages/site/site.page.ts b/src/app/core/login/pages/site/site.page.ts index 465181352..432c52a12 100644 --- a/src/app/core/login/pages/site/site.page.ts +++ b/src/app/core/login/pages/site/site.page.ts @@ -40,11 +40,11 @@ import { NavController } from '@ionic/angular'; }) export class CoreLoginSitePage implements OnInit { - @ViewChild('siteFormEl') formElement: ElementRef; + @ViewChild('siteFormEl') formElement?: ElementRef; siteForm: FormGroup; - fixedSites: CoreLoginSiteInfoExtended[]; - filteredSites: CoreLoginSiteInfoExtended[]; + fixedSites?: CoreLoginSiteInfoExtended[]; + filteredSites?: CoreLoginSiteInfoExtended[]; siteSelector = 'sitefinder'; showKeyboard = false; filter = ''; @@ -53,7 +53,7 @@ export class CoreLoginSitePage implements OnInit { loadingSites = false; searchFunction: (search: string) => void; showScanQR: boolean; - enteredSiteUrl: CoreLoginSiteInfoExtended; + enteredSiteUrl?: CoreLoginSiteInfoExtended; siteFinderSettings: SiteFinderSettings; constructor( @@ -95,10 +95,10 @@ export class CoreLoginSitePage implements OnInit { if (search.length >= 3) { // Update the sites list. - this.sites = await CoreSites.instance.findSites(search); + const sites = await CoreSites.instance.findSites(search); // Add UI tweaks. - this.sites = this.extendCoreLoginSiteInfo(this.sites); + this.sites = this.extendCoreLoginSiteInfo( sites); this.hasSites = !!this.sites.length; } else { @@ -201,7 +201,7 @@ export class CoreLoginSitePage implements OnInit { let valid = value.length >= 3 && CoreUrl.isValidMoodleUrl(value); if (!valid) { - const demo = !!this.getDemoSiteData(value); + const demo = !!CoreSites.instance.getDemoSiteData(value); if (demo) { valid = true; @@ -212,19 +212,6 @@ export class CoreLoginSitePage implements OnInit { }; } - /** - * Get the demo data for a certain "name" if it is a demo site. - * - * @param name Name of the site to check. - * @return Site data if it's a demo site, undefined otherwise. - */ - getDemoSiteData(name: string): CoreSitesDemoSiteData { - const demoSites = CoreConfigConstants.demo_sites; - if (typeof demoSites != 'undefined' && typeof demoSites[name] != 'undefined') { - return demoSites[name]; - } - } - /** * Show a help modal. */ @@ -361,7 +348,11 @@ export class CoreLoginSitePage implements OnInit { if (CoreLoginHelper.instance.isSSOLoginNeeded(response.code)) { // SSO. User needs to authenticate in a browser. CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin( - response.siteUrl, response.code, response.service, response.config && response.config.launchurl); + response.siteUrl, + response.code, + response.service, + response.config?.launchurl, + ); } else { const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config }; if (foundSite) { @@ -382,7 +373,7 @@ export class CoreLoginSitePage implements OnInit { * @param url The URL the user was trying to connect to. * @param error Error to display. */ - protected showLoginIssue(url: string, error: CoreError): void { + protected showLoginIssue(url: string | null, error: CoreError): void { let errorMessage = CoreDomUtils.instance.getErrorMessage(error); if (errorMessage == Translate.instance.instant('core.cannotconnecttrouble')) { @@ -452,10 +443,12 @@ export class CoreLoginSitePage implements OnInit { this.enteredSiteUrl = { url: search, name: 'connect', + title: '', + location: '', noProtocolUrl: CoreUrl.removeProtocol(search), }; } else { - this.enteredSiteUrl = null; + this.enteredSiteUrl = undefined; } this.searchFunction(search.trim()); @@ -468,8 +461,10 @@ export class CoreLoginSitePage implements OnInit { // Show some instructions first. CoreDomUtils.instance.showAlertWithOptions({ header: Translate.instance.instant('core.login.faqwhereisqrcode'), - message: Translate.instance.instant('core.login.faqwhereisqrcodeanswer', - { $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }), + message: Translate.instance.instant( + 'core.login.faqwhereisqrcodeanswer', + { $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }, + ), buttons: [ { text: Translate.instance.instant('core.cancel'), @@ -505,9 +500,9 @@ export class CoreLoginSitePage implements OnInit { * Extended data for UI implementation. */ type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & { - noProtocolUrl?: string; // Url wihtout protocol. - location?: string; // City + country. - title?: string; // Name + alias. + noProtocolUrl: string; // Url wihtout protocol. + location: string; // City + country. + title: string; // Name + alias. }; type SiteFinderSettings = { diff --git a/src/app/core/login/services/helper.ts b/src/app/core/login/services/helper.ts index a4256866e..cd3f3dbdf 100644 --- a/src/app/core/login/services/helper.ts +++ b/src/app/core/login/services/helper.ts @@ -50,7 +50,7 @@ export class CoreLoginHelperProvider { protected logger: CoreLogger; protected isSSOConfirmShown = false; protected isOpenEditAlertShown = false; - protected pageToLoad: {page: string; params: Params; time: number}; // Page to load once main menu is opened. + protected pageToLoad?: {page: string; params: Params; time: number}; // Page to load once main menu is opened. protected isOpeningReconnect = false; waitingForBrowser = false; @@ -208,7 +208,7 @@ export class CoreLoginHelperProvider { const categories: Record = {}; profileFields.forEach((field) => { - if (!field.signup) { + if (!field.signup || !field.categoryid) { // Not a signup field, ignore it. return; } @@ -216,7 +216,7 @@ export class CoreLoginHelperProvider { if (!categories[field.categoryid]) { categories[field.categoryid] = { id: field.categoryid, - name: field.categoryname, + name: field.categoryname || '', fields: [], }; } @@ -233,8 +233,8 @@ export class CoreLoginHelperProvider { * @param config Site public config. * @return Disabled features. */ - getDisabledFeatures(config: CoreSitePublicConfigResponse): string { - const disabledFeatures = config && config.tool_mobile_disabledfeatures; + getDisabledFeatures(config?: CoreSitePublicConfigResponse): string { + const disabledFeatures = config?.tool_mobile_disabledfeatures; if (!disabledFeatures) { return ''; } @@ -302,8 +302,8 @@ export class CoreLoginHelperProvider { * @param config Site public config. * @return Logo URL. */ - getLogoUrl(config: CoreSitePublicConfigResponse): string { - return !CoreConfigConstants.forceLoginLogo && config ? (config.logourl || config.compactlogourl) : null; + getLogoUrl(config: CoreSitePublicConfigResponse): string | undefined { + return !CoreConfigConstants.forceLoginLogo && config ? (config.logourl || config.compactlogourl) : undefined; } /** @@ -314,7 +314,7 @@ export class CoreLoginHelperProvider { */ getLogoutLabel(site?: CoreSite): string { site = site || CoreSites.instance.getCurrentSite(); - const config = site.getStoredConfig(); + const config = site?.getStoredConfig(); return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'changesite'); } @@ -325,7 +325,7 @@ export class CoreLoginHelperProvider { * @param params Params. * @return OAuth ID. */ - getOAuthIdFromParams(params: CoreUrlParams): number { + getOAuthIdFromParams(params: CoreUrlParams): number | undefined { return params && typeof params.oauthsso != 'undefined' ? Number(params.oauthsso) : undefined; } @@ -338,15 +338,18 @@ export class CoreLoginHelperProvider { async getSitePolicy(siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); - let sitePolicy: string; + let sitePolicy: string | undefined; try { // Try to get the latest config, maybe the site policy was just added or has changed. sitePolicy = await site.getConfig('sitepolicy', true); } catch (error) { // Cannot get config, try to get the site policy using auth_email_get_signup_settings. - const settings = await CoreWS.instance.callAjax('auth_email_get_signup_settings', {}, - { siteUrl: site.getURL() }); + const settings = await CoreWS.instance.callAjax( + 'auth_email_get_signup_settings', + {}, + { siteUrl: site.getURL() }, + ); sitePolicy = settings.sitepolicy; } @@ -374,7 +377,10 @@ export class CoreLoginHelperProvider { * @param disabledFeatures List of disabled features already treated. If not provided it will be calculated. * @return Valid identity providers. */ - getValidIdentityProviders(siteConfig: CoreSitePublicConfigResponse, disabledFeatures?: string): CoreSiteIdentityProvider[] { + getValidIdentityProviders(siteConfig?: CoreSitePublicConfigResponse, disabledFeatures?: string): CoreSiteIdentityProvider[] { + if (!siteConfig) { + return []; + } if (this.isFeatureDisabled('NoDelegate_IdentityProviders', siteConfig, disabledFeatures)) { // Identity providers are disabled, return an empty list. return []; @@ -460,8 +466,8 @@ export class CoreLoginHelperProvider { * @return Whether there are several fixed URLs. */ hasSeveralFixedSites(): boolean { - return CoreConfigConstants.siteurl && Array.isArray(CoreConfigConstants.siteurl) && - CoreConfigConstants.siteurl.length > 1; + return !!(CoreConfigConstants.siteurl && Array.isArray(CoreConfigConstants.siteurl) && + CoreConfigConstants.siteurl.length > 1); } /** @@ -923,8 +929,14 @@ export class CoreLoginHelperProvider { try { this.waitingForBrowser = true; - this.openBrowserForSSOLogin(result.siteUrl, result.code, result.service, result.config?.launchurl, - data.pageName, data.params); + this.openBrowserForSSOLogin( + result.siteUrl, + result.code, + result.service, + result.config?.launchurl, + data.pageName, + data.params, + ); } catch (error) { // User cancelled, logout him. CoreSites.instance.logout(); @@ -956,8 +968,13 @@ export class CoreLoginHelperProvider { this.waitingForBrowser = true; CoreSites.instance.unsetCurrentSite(); // Unset current site to make authentication work fine. - this.openBrowserForOAuthLogin(siteUrl, providerToUse, result.config.launchurl, data.pageName, - data.params); + this.openBrowserForOAuthLogin( + siteUrl, + providerToUse, + result.config?.launchurl, + data.pageName, + data.params, + ); } catch (error) { // User cancelled, logout him. CoreSites.instance.logout(); @@ -1067,10 +1084,13 @@ export class CoreLoginHelperProvider { try { const result = await CoreWS.instance.callAjax( - 'core_auth_resend_confirmation_email', data, preSets); + 'core_auth_resend_confirmation_email', + data, + preSets, + ); if (!result.status) { - throw new CoreWSError(result.warnings[0]); + throw new CoreWSError(result.warnings?.[0]); } const message = Translate.instance.instant('core.login.emailconfirmsentsuccess'); @@ -1096,6 +1116,8 @@ export class CoreLoginHelperProvider { try { // This call will always fail because we aren't sending parameters. await CoreWS.instance.callAjax('core_auth_resend_confirmation_email', {}, { siteUrl }); + + return true; // We should never reach here. } catch (error) { // If the WS responds with an invalid parameter error it means the WS is avaiable. return error?.errorcode === 'invalidparameter'; @@ -1134,13 +1156,13 @@ export class CoreLoginHelperProvider { */ treatUserTokenError(siteUrl: string, error: CoreWSError, username?: string, password?: string): void { if (error.errorcode == 'forcepasswordchangenotice') { - this.openChangePassword(siteUrl, CoreTextUtils.instance.getErrorMessageFromError(error)); + this.openChangePassword(siteUrl, CoreTextUtils.instance.getErrorMessageFromError(error)!); } else if (error.errorcode == 'usernotconfirmed') { this.showNotConfirmedModal(siteUrl, undefined, username, password); } else if (error.errorcode == 'connecttomoodleapp') { - this.showMoodleAppNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)); + this.showMoodleAppNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)!); } else if (error.errorcode == 'connecttoworkplaceapp') { - this.showWorkplaceNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)); + this.showWorkplaceNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)!); } else { CoreDomUtils.instance.showErrorModal(error); } diff --git a/src/app/directives/format-text.ts b/src/app/directives/format-text.ts index e4c74ebaa..7c679563e 100644 --- a/src/app/directives/format-text.ts +++ b/src/app/directives/format-text.ts @@ -15,7 +15,7 @@ import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core'; import { NavController, IonContent } from '@ionic/angular'; -import { CoreEventLoadingChangedData, CoreEvents, CoreEventsProvider } from '@services/events'; +import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreIframeUtils, CoreIframeUtilsProvider } from '@services/utils/iframe'; @@ -38,7 +38,7 @@ import { Translate } from '@singletons/core.singletons'; }) export class CoreFormatTextDirective implements OnChanges { - @Input() text: string; // The text to format. + @Input() text?: string; // The text to format. @Input() siteId?: string; // Site ID to use. @Input() component?: string; // Component for CoreExternalContentDirective. @Input() componentId?: string | number; // Component ID to use in conjunction with the component. @@ -56,11 +56,11 @@ export class CoreFormatTextDirective implements OnChanges { @Input() contextInstanceId?: number; // The instance ID related to the context. @Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters. @Input() wsNotFiltered?: boolean | string; // If true it means the WS didn't filter the text for some reason. - @Output() afterRender?: EventEmitter; // Called when the data is rendered. + @Output() afterRender: EventEmitter; // Called when the data is rendered. protected element: HTMLElement; - protected showMoreDisplayed: boolean; - protected loadingChangedListener; + protected showMoreDisplayed = false; + protected loadingChangedListener?: CoreEventObserver; constructor( element: ElementRef, @@ -116,9 +116,9 @@ export class CoreFormatTextDirective implements OnChanges { const container = document.createElement('span'); const originalWidth = img.attributes.getNamedItem('width'); - const forcedWidth = parseInt(originalWidth && originalWidth.value); + const forcedWidth = Number(originalWidth?.value); if (!isNaN(forcedWidth)) { - if (originalWidth.value.indexOf('%') < 0) { + if (originalWidth!.value.indexOf('%') < 0) { img.style.width = forcedWidth + 'px'; } else { img.style.width = forcedWidth + '%'; @@ -160,7 +160,7 @@ export class CoreFormatTextDirective implements OnChanges { return; } - let imgWidth = parseInt(img.getAttribute('width')); + let imgWidth = Number(img.getAttribute('width')); if (!imgWidth) { // No width attribute, use real size. imgWidth = img.naturalWidth; @@ -185,7 +185,7 @@ export class CoreFormatTextDirective implements OnChanges { CoreDomUtils.instance.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId, true); }); - img.parentNode.appendChild(anchor); + img.parentNode?.appendChild(anchor); }); } @@ -194,10 +194,13 @@ export class CoreFormatTextDirective implements OnChanges { */ protected calculateHeight(): void { // @todo: Work on calculate this height better. + if (!this.maxHeight) { + return; + } // Remove max-height (if any) to calculate the real height. const initialMaxHeight = this.element.style.maxHeight; - this.element.style.maxHeight = null; + this.element.style.maxHeight = ''; const height = this.getElementHeight(this.element); @@ -245,6 +248,9 @@ export class CoreFormatTextDirective implements OnChanges { // Ignore it if the event was prevented by some other listener. return; } + if (!this.text) { + return; + } const expandInFullview = CoreUtils.instance.isTrueOrOne(this.fullOnClick) || false; @@ -265,14 +271,18 @@ export class CoreFormatTextDirective implements OnChanges { // Open a new state with the contents. const filter = typeof this.filter != 'undefined' ? CoreUtils.instance.isTrueOrOne(this.filter) : undefined; - CoreTextUtils.instance.viewText(this.fullTitle || Translate.instance.instant('core.description'), this.text, { - component: this.component, - componentId: this.componentId, - filter: filter, - contextLevel: this.contextLevel, - instanceId: this.contextInstanceId, - courseId: this.courseId, - }); + CoreTextUtils.instance.viewText( + this.fullTitle || Translate.instance.instant('core.description'), + this.text, + { + component: this.component, + componentId: this.componentId, + filter: filter, + contextLevel: this.contextLevel, + instanceId: this.contextInstanceId, + courseId: this.courseId, + }, + ); } } @@ -357,29 +367,17 @@ export class CoreFormatTextDirective implements OnChanges { * @return Promise resolved with a div element containing the code. */ protected async formatContents(): Promise { - - const result: FormatContentsResult = { - div: null, - filters: [], - options: {}, - siteId: this.siteId, - }; - // Retrieve the site since it might be needed later. const site = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSite(this.siteId)); - if (site) { - result.siteId = site.getId(); - } - - if (this.contextLevel == 'course' && this.contextInstanceId <= 0) { + if (site && this.contextLevel == 'course' && this.contextInstanceId !== undefined && this.contextInstanceId <= 0) { this.contextInstanceId = site.getSiteHomeId(); } const filter = typeof this.filter == 'undefined' ? !!(this.contextLevel && typeof this.contextInstanceId != 'undefined') : CoreUtils.instance.isTrueOrOne(this.filter); - result.options = { + const options = { clean: CoreUtils.instance.isTrueOrOne(this.clean), singleLine: CoreUtils.instance.isTrueOrOne(this.singleLine), highlight: this.highlight, @@ -391,10 +389,10 @@ export class CoreFormatTextDirective implements OnChanges { if (filter) { // @todo - formatted = this.text; + formatted = this.text!; } else { // @todo - formatted = this.text; + formatted = this.text!; } formatted = this.treatWindowOpen(formatted); @@ -405,9 +403,12 @@ export class CoreFormatTextDirective implements OnChanges { this.treatHTMLElements(div, site); - result.div = div; - - return result; + return { + div, + filters: [], + options, + siteId: site?.getId(), + }; } /** @@ -418,7 +419,7 @@ export class CoreFormatTextDirective implements OnChanges { * @return Promise resolved when done. */ protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise { - const canTreatVimeo = site && site.isVersionGreaterEqualThan(['3.3.4', '3.4']); + const canTreatVimeo = site?.isVersionGreaterEqualThan(['3.3.4', '3.4']) || false; const navCtrl = this.navCtrl; // @todo this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; const images = Array.from(div.querySelectorAll('img')); @@ -537,7 +538,8 @@ export class CoreFormatTextDirective implements OnChanges { if (!width) { // All elements inside are floating or inline. Change display mode to allow calculate the width. - const parentWidth = CoreDomUtils.instance.getElementWidth(element.parentElement, true, false, false, true); + const parentWidth = element.parentElement ? + CoreDomUtils.instance.getElementWidth(element.parentElement, true, false, false, true) : 0; const previousDisplay = getComputedStyle(element, null).display; element.style.display = 'inline-block'; @@ -578,7 +580,7 @@ export class CoreFormatTextDirective implements OnChanges { this.element.classList.remove('core-expand-in-fullview'); this.element.classList.remove('core-text-formatted'); this.element.classList.remove('core-shortened'); - this.element.style.maxHeight = null; + this.element.style.maxHeight = ''; this.showMoreDisplayed = false; } @@ -595,7 +597,7 @@ export class CoreFormatTextDirective implements OnChanges { const tracks = Array.from(element.querySelectorAll('track')); sources.forEach((source) => { - source.setAttribute('target-src', source.getAttribute('src')); + source.setAttribute('target-src', source.getAttribute('src') || ''); source.removeAttribute('src'); this.addExternalContent(source); }); @@ -618,8 +620,12 @@ export class CoreFormatTextDirective implements OnChanges { * @param canTreatVimeo Whether Vimeo videos can be treated in the site. * @param navCtrl NavController to use. */ - protected async treatIframe(iframe: HTMLIFrameElement, site: CoreSite, canTreatVimeo: boolean, navCtrl: NavController): - Promise { + protected async treatIframe( + iframe: HTMLIFrameElement, + site: CoreSite | undefined, + canTreatVimeo: boolean, + navCtrl: NavController, + ): Promise { const src = iframe.src; const currentSite = CoreSites.instance.getCurrentSite(); @@ -636,7 +642,7 @@ export class CoreFormatTextDirective implements OnChanges { return; } - if (src && canTreatVimeo) { + if (site && src && canTreatVimeo) { // Check if it's a Vimeo video. If it is, use the wsplayer script instead to make restricted videos work. const matches = iframe.src.match(/https?:\/\/player\.vimeo\.com\/video\/([0-9]+)/); if (matches && matches[1]) { @@ -679,7 +685,7 @@ export class CoreFormatTextDirective implements OnChanges { } // Do the iframe responsive. - if (iframe.parentElement.classList.contains('embed-responsive')) { + if (iframe.parentElement?.classList.contains('embed-responsive')) { iframe.addEventListener('load', () => { if (iframe.contentDocument) { const css = document.createElement('style'); @@ -723,5 +729,5 @@ type FormatContentsResult = { div: HTMLElement; filters: any[]; options: any; - siteId: string; + siteId?: string; }; diff --git a/src/app/directives/supress-events.ts b/src/app/directives/supress-events.ts index 0b3c363a2..401578d8b 100644 --- a/src/app/directives/supress-events.ts +++ b/src/app/directives/supress-events.ts @@ -38,7 +38,7 @@ import { Directive, ElementRef, OnInit, Input, Output, EventEmitter } from '@ang }) export class CoreSupressEventsDirective implements OnInit { - @Input('core-suppress-events') suppressEvents: string | string[]; + @Input('core-suppress-events') suppressEvents?: string | string[]; @Output() onClick = new EventEmitter(); // eslint-disable-line @angular-eslint/no-output-on-prefix protected element: HTMLElement; diff --git a/src/app/services/app.ts b/src/app/services/app.ts index 1b3bd700a..5f983adcd 100644 --- a/src/app/services/app.ts +++ b/src/app/services/app.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Injectable, NgZone, ApplicationRef } from '@angular/core'; +import { Params } from '@angular/router'; import { Connection } from '@ionic-native/network/ngx'; import { CoreDB } from '@services/db'; @@ -224,7 +225,7 @@ export class CoreAppProvider { * @param storesConfig Config params to send the user to the right place. * @return Store URL. */ - getAppStoreUrl(storesConfig: CoreStoreConfig): string | null { + getAppStoreUrl(storesConfig: CoreStoreConfig): string | undefined { if (this.isMac() && storesConfig.mac) { return 'itms-apps://itunes.apple.com/app/' + storesConfig.mac; } @@ -253,7 +254,7 @@ export class CoreAppProvider { return storesConfig.mobile; } - return storesConfig.default || null; + return storesConfig.default; } /** @@ -563,11 +564,11 @@ export class CoreAppProvider { * * @return Object with siteid, state, params and timemodified. */ - getRedirect = Record>(): CoreRedirectData { + getRedirect(): CoreRedirectData { if (localStorage?.getItem) { try { const paramsJson = localStorage.getItem('CoreRedirectParams'); - const data: CoreRedirectData = { + const data: CoreRedirectData = { siteId: localStorage.getItem('CoreRedirectSiteId') || undefined, page: localStorage.getItem('CoreRedirectState') || undefined, timemodified: parseInt(localStorage.getItem('CoreRedirectTime') || '0', 10), @@ -593,7 +594,7 @@ export class CoreAppProvider { * @param page Page to go. * @param params Page params. */ - storeRedirect(siteId: string, page: string, params: Record): void { + storeRedirect(siteId: string, page: string, params: Params): void { if (localStorage && localStorage.setItem) { try { localStorage.setItem('CoreRedirectSiteId', siteId); @@ -697,7 +698,7 @@ export class CoreApp extends makeSingleton(CoreAppProvider) {} /** * Data stored for a redirect to another page/site. */ -export type CoreRedirectData> = { +export type CoreRedirectData = { /** * ID of the site to load. */ diff --git a/src/app/services/cron.ts b/src/app/services/cron.ts index 53c23b38a..a4e6cdf38 100644 --- a/src/app/services/cron.ts +++ b/src/app/services/cron.ts @@ -97,18 +97,17 @@ export class CoreCronDelegate { * @param siteId Site ID. If not defined, all sites. * @return Promise resolved if handler is executed successfully, rejected otherwise. */ - protected checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise { + protected async checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise { if (!this.handlers[name] || !this.handlers[name].execute) { // Invalid handler. const message = `Cannot execute handler because is invalid: ${name}`; this.logger.debug(message); - return Promise.reject(new CoreError(message)); + throw new CoreError(message); } const usesNetwork = this.handlerUsesNetwork(name); const isSync = !force && this.isHandlerSync(name); - let promise; if (usesNetwork && !CoreApp.instance.isOnline()) { // Offline, stop executing. @@ -116,47 +115,46 @@ export class CoreCronDelegate { this.logger.debug(message); this.stopHandler(name); - return Promise.reject(new CoreError(message)); + throw new CoreError(message); } if (isSync) { // Check network connection. - promise = CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false) - .then((syncOnlyOnWifi) => !syncOnlyOnWifi || CoreApp.instance.isWifi()); - } else { - promise = Promise.resolve(true); - } + const syncOnlyOnWifi = await CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false); - return promise.then((execute: boolean) => { - if (!execute) { + if (syncOnlyOnWifi && !CoreApp.instance.isWifi()) { // Cannot execute in this network connection, retry soon. const message = `Cannot execute handler because device is using limited connection: ${name}`; this.logger.debug(message); this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL); - return Promise.reject(new CoreError(message)); + throw new CoreError(message); } + } + + // Add the execution to the queue. + this.queuePromise = CoreUtils.instance.ignoreErrors(this.queuePromise).then(async () => { + try { + await this.executeHandler(name, force, siteId); - // Add the execution to the queue. - this.queuePromise = this.queuePromise.catch(() => { - // Ignore errors in previous handlers. - }).then(() => this.executeHandler(name, force, siteId).then(() => { this.logger.debug(`Execution of handler '${name}' was a success.`); - return this.setHandlerLastExecutionTime(name, Date.now()).then(() => { - this.scheduleNextExecution(name); - }); - }, (error) => { + await CoreUtils.instance.ignoreErrors(this.setHandlerLastExecutionTime(name, Date.now())); + + this.scheduleNextExecution(name); + + return; + } catch (error) { // Handler call failed. Retry soon. const message = `Execution of handler '${name}' failed.`; this.logger.error(message, error); this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL); - return Promise.reject(new CoreError(message)); - })); - - return this.queuePromise; + throw new CoreError(message); + } }); + + return this.queuePromise; } /** @@ -172,7 +170,7 @@ export class CoreCronDelegate { this.logger.debug('Executing handler: ' + name); // Wrap the call in Promise.resolve to make sure it's a promise. - Promise.resolve(this.handlers[name].execute(siteId, force)).then(resolve).catch(reject).finally(() => { + Promise.resolve(this.handlers[name].execute!(siteId, force)).then(resolve).catch(reject).finally(() => { clearTimeout(cancelTimeout); }); @@ -192,7 +190,7 @@ export class CoreCronDelegate { * @return Promise resolved if all handlers are executed successfully, rejected otherwise. */ async forceSyncExecution(siteId?: string): Promise { - const promises = []; + const promises: Promise[] = []; for (const name in this.handlers) { if (this.isHandlerManualSync(name)) { @@ -208,11 +206,11 @@ export class CoreCronDelegate { * Force execution of a cron tasks without waiting for the scheduled time. * Please notice that some tasks may not be executed depending on the network connection and sync settings. * - * @param name If provided, the name of the handler. + * @param name Name of the handler. * @param siteId Site ID. If not defined, all sites. * @return Promise resolved if handler has been executed successfully, rejected otherwise. */ - forceCronHandlerExecution(name?: string, siteId?: string): Promise { + forceCronHandlerExecution(name: string, siteId?: string): Promise { const handler = this.handlers[name]; // Mark the handler as running (it might be running already). @@ -240,7 +238,7 @@ export class CoreCronDelegate { // Don't allow intervals lower than the minimum. const minInterval = CoreApp.instance.isDesktop() ? CoreCronDelegate.DESKTOP_MIN_INTERVAL : CoreCronDelegate.MIN_INTERVAL; - const handlerInterval = this.handlers[name].getInterval(); + const handlerInterval = this.handlers[name].getInterval!(); if (!handlerInterval) { return CoreCronDelegate.DEFAULT_INTERVAL; @@ -288,12 +286,12 @@ export class CoreCronDelegate { * @return True if handler uses network or not defined, false otherwise. */ protected handlerUsesNetwork(name: string): boolean { - if (!this.handlers[name] || !this.handlers[name].usesNetwork) { + if (!this.handlers[name] || this.handlers[name].usesNetwork) { // Invalid, return default. return true; } - return this.handlers[name].usesNetwork(); + return this.handlers[name].usesNetwork!(); } /** @@ -338,7 +336,7 @@ export class CoreCronDelegate { return this.isHandlerSync(name); } - return this.handlers[name].canManualSync(); + return this.handlers[name].canManualSync!(); } /** @@ -353,7 +351,7 @@ export class CoreCronDelegate { return true; } - return this.handlers[name].isSync(); + return this.handlers[name].isSync!(); } /** @@ -385,10 +383,10 @@ export class CoreCronDelegate { * Schedule a next execution for a handler. * * @param name Name of the handler. - * @param time Time to the next execution. If not supplied it will be calculated using the last execution and - * the handler's interval. This param should be used only if it's really necessary. + * @param timeToNextExecution Time (in milliseconds). If not supplied it will be calculated. + * @return Promise resolved when done. */ - protected scheduleNextExecution(name: string, time?: number): void { + protected async scheduleNextExecution(name: string, timeToNextExecution?: number): Promise { if (!this.handlers[name]) { // Invalid handler. return; @@ -398,33 +396,24 @@ export class CoreCronDelegate { return; } - let promise; - - if (time) { - promise = Promise.resolve(time); - } else { + if (!timeToNextExecution) { // Get last execution time to check when do we need to execute it. - promise = this.getHandlerLastExecutionTime(name).then((lastExecution) => { - const interval = this.getHandlerInterval(name); - const nextExecution = lastExecution + interval; + const lastExecution = await this.getHandlerLastExecutionTime(name); - return nextExecution - Date.now(); - }); + const interval = this.getHandlerInterval(name); + + timeToNextExecution = lastExecution + interval - Date.now(); } - promise.then((nextExecution) => { - this.logger.debug(`Scheduling next execution of handler '${name}' in '${nextExecution}' ms`); - if (nextExecution < 0) { - nextExecution = 0; // Big negative numbers aren't executed immediately. - } + this.logger.debug(`Scheduling next execution of handler '${name}' in '${timeToNextExecution}' ms`); + if (timeToNextExecution < 0) { + timeToNextExecution = 0; // Big negative numbers aren't executed immediately. + } - this.handlers[name].timeout = window.setTimeout(() => { - delete this.handlers[name].timeout; - this.checkAndExecuteHandler(name).catch(() => { - // Ignore errors. - }); - }, nextExecution); - }); + this.handlers[name].timeout = window.setTimeout(() => { + delete this.handlers[name].timeout; + CoreUtils.instance.ignoreErrors(this.checkAndExecuteHandler(name)); + }, timeToNextExecution); } /** diff --git a/src/app/services/file-helper.ts b/src/app/services/file-helper.ts index 31ac0aa8c..ae7842ebd 100644 --- a/src/app/services/file-helper.ts +++ b/src/app/services/file-helper.ts @@ -44,11 +44,17 @@ export class CoreFileHelperProvider { * @param siteId The site ID. If not defined, current site. * @return Resolved on success. */ - async downloadAndOpenFile(file: CoreWSExternalFile, component: string, componentId: string | number, state?: string, - onProgress?: CoreFileHelperOnProgress, siteId?: string): Promise { + async downloadAndOpenFile( + file: CoreWSExternalFile, + component: string, + componentId: string | number, + state?: string, + onProgress?: CoreFileHelperOnProgress, + siteId?: string, + ): Promise { siteId = siteId || CoreSites.instance.getCurrentSiteId(); - const fileUrl = this.getFileUrl(file); + const fileUrl = file.fileurl; const timemodified = this.getFileTimemodified(file); if (!this.isOpenableInApp(file)) { @@ -111,70 +117,76 @@ export class CoreFileHelperProvider { * @param siteId The site ID. If not defined, current site. * @return Resolved with the URL to use on success. */ - protected downloadFileIfNeeded(file: CoreWSExternalFile, fileUrl: string, component?: string, componentId?: string | number, - timemodified?: number, state?: string, onProgress?: CoreFileHelperOnProgress, siteId?: string): Promise { + protected async downloadFileIfNeeded( + file: CoreWSExternalFile, + fileUrl: string, + component?: string, + componentId?: string | number, + timemodified?: number, + state?: string, + onProgress?: CoreFileHelperOnProgress, + siteId?: string, + ): Promise { siteId = siteId || CoreSites.instance.getCurrentSiteId(); - return CoreSites.instance.getSite(siteId).then((site) => site.checkAndFixPluginfileURL(fileUrl)).then((fixedUrl) => { - if (CoreFile.instance.isAvailable()) { - let promise; - if (state) { - promise = Promise.resolve(state); - } else { - // Calculate the state. - promise = CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified); + const site = await CoreSites.instance.getSite(siteId); + const fixedUrl = await site.checkAndFixPluginfileURL(fileUrl); + + if (!CoreFile.instance.isAvailable()) { + // Use the online URL. + return fixedUrl; + } + + if (!state) { + // Calculate the state. + state = await CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified); + } + + // The file system is available. + const isWifi = CoreApp.instance.isWifi(); + const isOnline = CoreApp.instance.isOnline(); + + if (state == CoreConstants.DOWNLOADED) { + // File is downloaded, get the local file URL. + return CoreFilepool.instance.getUrlByUrl(siteId, fileUrl, component, componentId, timemodified, false, false, file); + } else { + if (!isOnline && !this.isStateDownloaded(state)) { + // Not downloaded and user is offline, reject. + throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + } + + if (onProgress) { + // This call can take a while. Send a fake event to notify that we're doing some calculations. + onProgress({ calculating: true }); + } + + try { + await CoreFilepool.instance.shouldDownloadBeforeOpen(fixedUrl, file.filesize || 0); + } catch (error) { + // Start the download if in wifi, but return the URL right away so the file is opened. + if (isWifi) { + this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); } - return promise.then((state) => { - // The file system is available. - const isWifi = CoreApp.instance.isWifi(); - const isOnline = CoreApp.instance.isOnline(); + if (!this.isStateDownloaded(state) || isOnline) { + // Not downloaded or online, return the online URL. + return fixedUrl; + } else { + // Outdated but offline, so we return the local URL. + return CoreFilepool.instance.getUrlByUrl( + siteId, fileUrl, component, componentId, timemodified, false, false, file); + } + } - if (state == CoreConstants.DOWNLOADED) { - // File is downloaded, get the local file URL. - return CoreFilepool.instance.getUrlByUrl( - siteId, fileUrl, component, componentId, timemodified, false, false, file); - } else { - if (!isOnline && !this.isStateDownloaded(state)) { - // Not downloaded and user is offline, reject. - return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); - } - - if (onProgress) { - // This call can take a while. Send a fake event to notify that we're doing some calculations. - onProgress({ calculating: true }); - } - - return CoreFilepool.instance.shouldDownloadBeforeOpen(fixedUrl, file.filesize).then(() => { - if (state == CoreConstants.DOWNLOADING) { - // It's already downloading, stop. - return; - } - - // Download and then return the local URL. - return this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); - }, () => { - // Start the download if in wifi, but return the URL right away so the file is opened. - if (isWifi) { - this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); - } - - if (!this.isStateDownloaded(state) || isOnline) { - // Not downloaded or online, return the online URL. - return fixedUrl; - } else { - // Outdated but offline, so we return the local URL. - return CoreFilepool.instance.getUrlByUrl( - siteId, fileUrl, component, componentId, timemodified, false, false, file); - } - }); - } - }); - } else { - // Use the online URL. + // Download the file first. + if (state == CoreConstants.DOWNLOADING) { + // It's already downloading, stop. return fixedUrl; } - }); + + // Download and then return the local URL. + return this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); + } } /** @@ -189,29 +201,37 @@ export class CoreFileHelperProvider { * @param siteId The site ID. If not defined, current site. * @return Resolved with internal URL on success, rejected otherwise. */ - downloadFile(fileUrl: string, component?: string, componentId?: string | number, timemodified?: number, - onProgress?: (event: ProgressEvent) => void, file?: CoreWSExternalFile, siteId?: string): Promise { + async downloadFile( + fileUrl: string, + component?: string, + componentId?: string | number, + timemodified?: number, + onProgress?: (event: ProgressEvent) => void, + file?: CoreWSExternalFile, + siteId?: string, + ): Promise { siteId = siteId || CoreSites.instance.getCurrentSiteId(); // Get the site and check if it can download files. - return CoreSites.instance.getSite(siteId).then((site) => { - if (!site.canDownloadFiles()) { - return Promise.reject(new CoreError(Translate.instance.instant('core.cannotdownloadfiles'))); + const site = await CoreSites.instance.getSite(siteId); + + if (!site.canDownloadFiles()) { + throw new CoreError(Translate.instance.instant('core.cannotdownloadfiles')); + } + + try { + return await CoreFilepool.instance.downloadUrl(siteId, fileUrl, false, component, componentId, timemodified, + onProgress, undefined, file); + } catch (error) { + // Download failed, check the state again to see if the file was downloaded before. + const state = await CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified); + + if (this.isStateDownloaded(state)) { + return CoreFilepool.instance.getInternalUrlByUrl(siteId, fileUrl); + } else { + throw error; } - - return CoreFilepool.instance.downloadUrl(siteId, fileUrl, false, component, componentId, - timemodified, onProgress, undefined, file).catch((error) => - - // Download failed, check the state again to see if the file was downloaded before. - CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified).then((state) => { - if (this.isStateDownloaded(state)) { - return CoreFilepool.instance.getInternalUrlByUrl(siteId, fileUrl); - } else { - return Promise.reject(error); - } - }), - ); - }); + } } /** @@ -220,7 +240,7 @@ export class CoreFileHelperProvider { * @param file The file. * @deprecated since 3.9.5. Get directly the fileurl instead. */ - getFileUrl(file: CoreWSExternalFile): string { + getFileUrl(file: CoreWSExternalFile): string | undefined { return file.fileurl; } @@ -337,11 +357,15 @@ export class CoreFileHelperProvider { * @return bool. */ isOpenableInApp(file: {filename?: string; name?: string}): boolean { - const re = /(?:\.([^.]+))?$/; + const regex = /(?:\.([^.]+))?$/; + const regexResult = regex.exec(file.filename || file.name || ''); - const ext = re.exec(file.filename || file.name)[1]; + if (!regexResult || !regexResult[1]) { + // Couldn't find the extension. Assume it's openable. + return true; + } - return !this.isFileTypeExcludedInApp(ext); + return !this.isFileTypeExcludedInApp(regexResult[1]); } /** @@ -365,7 +389,7 @@ export class CoreFileHelperProvider { */ isFileTypeExcludedInApp(fileType: string): boolean { const currentSite = CoreSites.instance.getCurrentSite(); - const fileTypeExcludeList = currentSite && currentSite.getStoredConfig('tool_mobile_filetypeexclusionlist'); + const fileTypeExcludeList = currentSite?.getStoredConfig('tool_mobile_filetypeexclusionlist'); if (!fileTypeExcludeList) { return false; diff --git a/src/app/services/file-session.ts b/src/app/services/file-session.ts index b03cd24a6..9cbdeadc9 100644 --- a/src/app/services/file-session.ts +++ b/src/app/services/file-session.ts @@ -85,7 +85,7 @@ export class CoreFileSessionProvider { * @param id File area identifier. * @param siteId Site ID. If not defined, current site. */ - protected initFileArea(component: string, id: string | number, siteId?: string): void { + protected initFileArea(component: string, id: string | number, siteId: string): void { if (!this.files[siteId]) { this.files[siteId] = {}; } diff --git a/src/app/services/file.ts b/src/app/services/file.ts index c674764a7..7ad27e5cf 100644 --- a/src/app/services/file.ts +++ b/src/app/services/file.ts @@ -117,25 +117,25 @@ export class CoreFileProvider { * * @return Promise to be resolved when the initialization is finished. */ - init(): Promise { + async init(): Promise { if (this.initialized) { - return Promise.resolve(); + return; } - return Platform.instance.ready().then(() => { - if (CoreApp.instance.isAndroid()) { - this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath; - } else if (CoreApp.instance.isIOS()) { - this.basePath = File.instance.documentsDirectory || this.basePath; - } else if (!this.isAvailable() || this.basePath === '') { - this.logger.error('Error getting device OS.'); + await Platform.instance.ready(); - return Promise.reject(new CoreError('Error getting device OS to initialize file system.')); - } + if (CoreApp.instance.isAndroid()) { + this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath; + } else if (CoreApp.instance.isIOS()) { + this.basePath = File.instance.documentsDirectory || this.basePath; + } else if (!this.isAvailable() || this.basePath === '') { + this.logger.error('Error getting device OS.'); - this.initialized = true; - this.logger.debug('FS initialized: ' + this.basePath); - }); + return Promise.reject(new CoreError('Error getting device OS to initialize file system.')); + } + + this.initialized = true; + this.logger.debug('FS initialized: ' + this.basePath); } /** @@ -194,8 +194,12 @@ export class CoreFileProvider { * @param base Base path to create the dir/file in. If not set, use basePath. * @return Promise to be resolved when the dir/file is created. */ - protected async create(isDirectory: boolean, path: string, failIfExists?: boolean, base?: string): - Promise { + protected async create( + isDirectory: boolean, + path: string, + failIfExists?: boolean, + base?: string, + ): Promise { await this.init(); // Remove basePath if it's in the path. @@ -340,17 +344,19 @@ export class CoreFileProvider { * @return Promise to be resolved when the size is calculated. */ protected getSize(entry: DirectoryEntry | FileEntry): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if (this.isDirectoryEntry(entry)) { const directoryReader = entry.createReader(); - directoryReader.readEntries((entries: (DirectoryEntry | FileEntry)[]) => { - const promises = []; + directoryReader.readEntries(async (entries: (DirectoryEntry | FileEntry)[]) => { + const promises: Promise[] = []; for (let i = 0; i < entries.length; i++) { promises.push(this.getSize(entries[i])); } - Promise.all(promises).then((sizes) => { + try { + const sizes = await Promise.all(promises); + let directorySize = 0; for (let i = 0; i < sizes.length; i++) { const fileSize = Number(sizes[i]); @@ -362,7 +368,9 @@ export class CoreFileProvider { directorySize += fileSize; } resolve(directorySize); - }, reject); + } catch (error) { + reject(error); + } }, reject); } else { entry.file((file) => { @@ -469,7 +477,7 @@ export class CoreFileProvider { const parsed = CoreTextUtils.instance.parseJSON(text, null); if (parsed == null && text != null) { - return Promise.reject(new CoreError('Error parsing JSON file: ' + path)); + throw new CoreError('Error parsing JSON file: ' + path); } return parsed; @@ -494,7 +502,7 @@ export class CoreFileProvider { const reader = new FileReader(); reader.onloadend = (event): void => { - if (event.target.result !== undefined && event.target.result !== null) { + if (event.target?.result !== undefined && event.target.result !== null) { if (format == CoreFileProvider.FORMATJSON) { // Convert to object. const parsed = CoreTextUtils.instance.parseJSON( event.target.result, null); @@ -507,7 +515,7 @@ export class CoreFileProvider { } else { resolve(event.target.result); } - } else if (event.target.error !== undefined && event.target.error !== null) { + } else if (event.target?.error !== undefined && event.target.error !== null) { reject(event.target.error); } else { reject({ code: null, message: 'READER_ONLOADEND_ERR' }); @@ -550,25 +558,27 @@ export class CoreFileProvider { * @param append Whether to append the data to the end of the file. * @return Promise to be resolved when the file is written. */ - writeFile(path: string, data: string | Blob, append?: boolean): Promise { - return this.init().then(() => { - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); - this.logger.debug('Write file: ' + path); + async writeFile(path: string, data: string | Blob, append?: boolean): Promise { + await this.init(); - // Create file (and parent folders) to prevent errors. - return this.createFile(path).then((fileEntry) => { - if (this.isHTMLAPI && !CoreApp.instance.isDesktop() && - (typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) { - // We need to write Blobs. - const type = CoreMimetypeUtils.instance.getMimeType(CoreMimetypeUtils.instance.getFileExtension(path)); - data = new Blob([data], { type: type || 'text/plain' }); - } + // Remove basePath if it's in the path. + path = this.removeStartingSlash(path.replace(this.basePath, '')); + this.logger.debug('Write file: ' + path); - return File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append }) - .then(() => fileEntry); - }); - }); + // Create file (and parent folders) to prevent errors. + const fileEntry = await this.createFile(path); + + if (this.isHTMLAPI && !CoreApp.instance.isDesktop() && + (typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) { + // We need to write Blobs. + const extension = CoreMimetypeUtils.instance.getFileExtension(path); + const type = extension ? CoreMimetypeUtils.instance.getMimeType(extension) : ''; + data = new Blob([data], { type: type || 'text/plain' }); + } + + await File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append }); + + return fileEntry; } /** @@ -583,8 +593,13 @@ export class CoreFileProvider { * @param append Whether to append the data to the end of the file. * @return Promise resolved when done. */ - async writeFileDataInFile(file: Blob, path: string, onProgress?: CoreFileProgressFunction, offset: number = 0, - append?: boolean): Promise { + async writeFileDataInFile( + file: Blob, + path: string, + onProgress?: CoreFileProgressFunction, + offset: number = 0, + append?: boolean, + ): Promise { offset = offset || 0; try { @@ -675,16 +690,18 @@ export class CoreFileProvider { * * @return Promise to be resolved when the base path is retrieved. */ - getBasePathToDownload(): Promise { - return this.init().then(() => { - if (CoreApp.instance.isIOS()) { - // In iOS we want the internal URL (cdvfile://localhost/persistent/...). - return File.instance.resolveDirectoryUrl(this.basePath).then((dirEntry) => dirEntry.toInternalURL()); - } else { - // In the other platforms we use the basePath as it is (file://...). - return this.basePath; - } - }); + async getBasePathToDownload(): Promise { + await this.init(); + + if (CoreApp.instance.isIOS()) { + // In iOS we want the internal URL (cdvfile://localhost/persistent/...). + const dirEntry = await File.instance.resolveDirectoryUrl(this.basePath); + + return dirEntry.toInternalURL(); + } else { + // In the other platforms we use the basePath as it is (file://...). + return this.basePath; + } } /** @@ -773,18 +790,22 @@ export class CoreFileProvider { * try to create it (slower). * @return Promise resolved when the entry is copied. */ - protected async copyOrMoveFileOrDir(from: string, to: string, isDir?: boolean, copy?: boolean, destDirExists?: boolean): - Promise { + protected async copyOrMoveFileOrDir( + from: string, + to: string, + isDir?: boolean, + copy?: boolean, + destDirExists?: boolean, + ): Promise { const fileIsInAppFolder = this.isPathInAppFolder(from); if (!fileIsInAppFolder) { return this.copyOrMoveExternalFile(from, to, copy); } - const moveCopyFn: (path: string, dirName: string, newPath: string, newDirName: string) => - Promise = copy ? - (isDir ? File.instance.copyDir.bind(File.instance) : File.instance.copyFile.bind(File.instance)) : - (isDir ? File.instance.moveDir.bind(File.instance) : File.instance.moveFile.bind(File.instance)); + const moveCopyFn: MoveCopyFunction = copy ? + (isDir ? File.instance.copyDir.bind(File.instance) : File.instance.copyFile.bind(File.instance)) : + (isDir ? File.instance.moveDir.bind(File.instance) : File.instance.moveFile.bind(File.instance)); await this.init(); @@ -880,6 +901,8 @@ export class CoreFileProvider { if (path.indexOf(this.basePath) > -1) { return path.replace(this.basePath, ''); } + + return path; } /** @@ -892,33 +915,31 @@ export class CoreFileProvider { * @param recreateDir Delete the dest directory before unzipping. Defaults to true. * @return Promise resolved when the file is unzipped. */ - unzipFile(path: string, destFolder?: string, onProgress?: (progress: ProgressEvent) => void, recreateDir: boolean = true): - Promise { + async unzipFile( + path: string, + destFolder?: string, + onProgress?: (progress: ProgressEvent) => void, + recreateDir: boolean = true, + ): Promise { // Get the source file. - let fileEntry: FileEntry; + const fileEntry = await this.getFile(path); - return this.getFile(path).then((fe) => { - fileEntry = fe; + if (destFolder && recreateDir) { + // Make sure the dest dir doesn't exist already. + await CoreUtils.instance.ignoreErrors(this.removeDir(destFolder)); - if (destFolder && recreateDir) { - // Make sure the dest dir doesn't exist already. - return this.removeDir(destFolder).catch(() => { - // Ignore errors. - }).then(() => - // Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail. - this.createDir(destFolder), - ); - } - }).then(() => { - // If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath). - destFolder = this.addBasePathIfNeeded(destFolder || CoreMimetypeUtils.instance.removeExtension(path)); + // Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail. + await this.createDir(destFolder); + } - return Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress); - }).then((result) => { - if (result == -1) { - return Promise.reject(new CoreError('Unzip failed.')); - } - }); + // If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath). + destFolder = this.addBasePathIfNeeded(destFolder || CoreMimetypeUtils.instance.removeExtension(path)); + + const result = await Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress); + + if (result == -1) { + throw new CoreError('Unzip failed.'); + } } /** @@ -999,22 +1020,22 @@ export class CoreFileProvider { * @param copy True to copy, false to move. * @return Promise resolved when the entry is copied/moved. */ - protected copyOrMoveExternalFile(from: string, to: string, copy?: boolean): Promise { + protected async copyOrMoveExternalFile(from: string, to: string, copy?: boolean): Promise { // Get the file to copy/move. - return this.getExternalFile(from).then((fileEntry) => { - // Create the destination dir if it doesn't exist. - const dirAndFile = this.getFileAndDirectoryFromPath(to); + const fileEntry = await this.getExternalFile(from); - return this.createDir(dirAndFile.directory).then((dirEntry) => - // Now copy/move the file. - new Promise((resolve, reject): void => { - if (copy) { - fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); - } else { - fileEntry.moveTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); - } - }), - ); + // Create the destination dir if it doesn't exist. + const dirAndFile = this.getFileAndDirectoryFromPath(to); + + const dirEntry = await this.createDir(dirAndFile.directory); + + // Now copy/move the file. + return new Promise((resolve, reject): void => { + if (copy) { + fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); + } else { + fileEntry.moveTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); + } }); } @@ -1048,9 +1069,11 @@ export class CoreFileProvider { * @param defaultExt Default extension to use if no extension found in the file. * @return Promise resolved with the unique file name. */ - getUniqueNameInFolder(dirPath: string, fileName: string, defaultExt?: string): Promise { + async getUniqueNameInFolder(dirPath: string, fileName: string, defaultExt?: string): Promise { // Get existing files in the folder. - return this.getDirectoryContents(dirPath).then((entries) => { + try { + const entries = await this.getDirectoryContents(dirPath); + const files = {}; let num = 1; let fileNameWithoutExtension = CoreMimetypeUtils.instance.removeExtension(fileName); @@ -1058,7 +1081,8 @@ export class CoreFileProvider { // Clean the file name. fileNameWithoutExtension = CoreTextUtils.instance.removeSpecialCharactersForFiles( - CoreTextUtils.instance.decodeURIComponent(fileNameWithoutExtension)); + CoreTextUtils.instance.decodeURIComponent(fileNameWithoutExtension), + ); // Index the files by name. entries.forEach((entry) => { @@ -1086,10 +1110,10 @@ export class CoreFileProvider { // Ask the user what he wants to do. return newName; } - }).catch(() => + } catch (error) { // Folder doesn't exist, name is unique. Clean it and return it. - CoreTextUtils.instance.removeSpecialCharactersForFiles(CoreTextUtils.instance.decodeURIComponent(fileName)), - ); + return CoreTextUtils.instance.removeSpecialCharactersForFiles(CoreTextUtils.instance.decodeURIComponent(fileName)); + } } /** @@ -1119,7 +1143,7 @@ export class CoreFileProvider { } const filesMap: {[fullPath: string]: FileEntry} = {}; - const promises = []; + const promises: Promise[] = []; // Index the received files by fullPath and ignore the invalid ones. files.forEach((file) => { @@ -1219,3 +1243,5 @@ export class CoreFileProvider { } export class CoreFile extends makeSingleton(CoreFileProvider) {} + +type MoveCopyFunction = (path: string, dirName: string, newPath: string, newDirName: string) => Promise; diff --git a/src/app/services/filepool.ts b/src/app/services/filepool.ts index b910c9334..dd602a123 100644 --- a/src/app/services/filepool.ts +++ b/src/app/services/filepool.ts @@ -239,7 +239,7 @@ export class CoreFilepoolProvider { protected logger: CoreLogger; protected appDB: SQLiteDB; protected dbReady: Promise; // Promise resolved when the app DB is initialized. - protected queueState: string; + protected queueState = CoreFilepoolProvider.QUEUE_PAUSED; protected urlAttributes: RegExp[] = [ new RegExp('(\\?|&)token=([A-Za-z0-9]*)'), new RegExp('(\\?|&)forcedownload=[0-1]'), @@ -264,16 +264,23 @@ export class CoreFilepoolProvider { CoreSites.instance.registerSiteSchema(this.siteSchema); - CoreInit.instance.ready().then(() => { - // Waiting for the app to be ready to start processing the queue. - this.checkQueueProcessing(); + this.init(); + } - // Start queue when device goes online. - Network.instance.onConnect().subscribe(() => { - // Execute the callback in the Angular zone, so change detection doesn't stop working. - NgZone.instance.run(() => { - this.checkQueueProcessing(); - }); + /** + * Init some properties. + */ + protected async init(): Promise { + // Waiting for the app to be ready to start processing the queue. + await CoreInit.instance.ready(); + + this.checkQueueProcessing(); + + // Start queue when device goes online. + Network.instance.onConnect().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + NgZone.instance.run(() => { + this.checkQueueProcessing(); }); }); } @@ -358,7 +365,7 @@ export class CoreFilepoolProvider { * @param data Additional information to store about the file (timemodified, url, ...). See FILES_TABLE schema. * @return Promise resolved on success. */ - protected async addFileToPool(siteId: string, fileId: string, data: CoreFilepoolFileEntry): Promise { + protected async addFileToPool(siteId: string, fileId: string, data: Omit): Promise { const record = { fileId, ...data, @@ -410,9 +417,18 @@ export class CoreFilepoolProvider { * @param link The link to add for the file. * @return Promise resolved when the file is downloaded. */ - protected async addToQueue(siteId: string, fileId: string, url: string, priority: number, revision: number, - timemodified: number, filePath: string, onProgress?: CoreFilepoolOnProgressCallback, - options: CoreFilepoolFileOptions = {}, link?: CoreFilepoolComponentLink): Promise { + protected async addToQueue( + siteId: string, + fileId: string, + url: string, + priority: number, + revision: number, + timemodified: number, + filePath?: string, + onProgress?: CoreFilepoolOnProgressCallback, + options: CoreFilepoolFileOptions = {}, + link?: CoreFilepoolComponentLink, + ): Promise { await this.dbReady; this.logger.debug(`Adding ${fileId} to the queue`); @@ -454,9 +470,19 @@ export class CoreFilepoolProvider { * @param alreadyFixed Whether the URL has already been fixed. * @return Resolved on success. */ - async addToQueueByUrl(siteId: string, fileUrl: string, component?: string, componentId?: string | number, - timemodified: number = 0, filePath?: string, onProgress?: CoreFilepoolOnProgressCallback, priority: number = 0, - options: CoreFilepoolFileOptions = {}, revision?: number, alreadyFixed?: boolean): Promise { + async addToQueueByUrl( + siteId: string, + fileUrl: string, + component?: string, + componentId?: string | number, + timemodified: number = 0, + filePath?: string, + onProgress?: CoreFilepoolOnProgressCallback, + priority: number = 0, + options: CoreFilepoolFileOptions = {}, + revision?: number, + alreadyFixed?: boolean, + ): Promise { await this.dbReady; if (!CoreFile.instance.isAvailable()) { @@ -468,16 +494,14 @@ export class CoreFilepoolProvider { throw new CoreError('Site doesn\'t allow downloading files.'); } - let file: CoreWSExternalFile; - if (alreadyFixed) { - // Already fixed, if we reached here it means it can be downloaded. - file = { fileurl: fileUrl }; - } else { - file = await this.fixPluginfileURL(siteId, fileUrl); + if (!alreadyFixed) { + // Fix the URL and use the fixed data. + const file = await this.fixPluginfileURL(siteId, fileUrl); + + fileUrl = file.fileurl; + timemodified = file.timemodified || timemodified; } - fileUrl = file.fileurl; - timemodified = file.timemodified || timemodified; revision = revision || this.getRevisionFromUrl(fileUrl); const fileId = this.getFileIdByUrl(fileUrl); @@ -489,71 +513,68 @@ export class CoreFilepoolProvider { // Retrieve the queue deferred now if it exists. // This is to prevent errors if file is removed from queue while we're checking if the file is in queue. const queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress); + let entry: CoreFilepoolQueueEntry; - return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => { - const newData: CoreFilepoolQueueDBEntry = {}; - let foundLink = false; - - if (entry) { - // We already have the file in queue, we update the priority and links. - if (entry.priority < priority) { - newData.priority = priority; - } - if (revision && entry.revision !== revision) { - newData.revision = revision; - } - if (timemodified && entry.timemodified !== timemodified) { - newData.timemodified = timemodified; - } - if (filePath && entry.path !== filePath) { - newData.path = filePath; - } - if (entry.isexternalfile !== options.isexternalfile && (entry.isexternalfile || options.isexternalfile)) { - newData.isexternalfile = options.isexternalfile; - } - if (entry.repositorytype !== options.repositorytype && (entry.repositorytype || options.repositorytype)) { - newData.repositorytype = options.repositorytype; - } - - if (link) { - // We need to add the new link if it does not exist yet. - if (entry.linksUnserialized && entry.linksUnserialized.length) { - foundLink = entry.linksUnserialized.some((fileLink) => - fileLink.component == link.component && fileLink.componentId == link.componentId); - } - - if (!foundLink) { - const links = entry.linksUnserialized || []; - links.push(link); - newData.links = JSON.stringify(links); - } - } - - if (Object.keys(newData).length) { - // Update only when required. - this.logger.debug(`Updating file ${fileId} which is already in queue`); - - return this.appDB.updateRecords(CoreFilepoolProvider.QUEUE_TABLE, newData, primaryKey).then(() => - this.getQueuePromise(siteId, fileId, true, onProgress)); - } - - this.logger.debug(`File ${fileId} already in queue and does not require update`); - if (queueDeferred) { - // If we were able to retrieve the queue deferred before, we use that one. - return queueDeferred.promise; - } else { - // Create a new deferred and return its promise. - return this.getQueuePromise(siteId, fileId, true, onProgress); - } - } else { - return this.addToQueue( - siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link); - } - }, () => + try { + entry = await this.hasFileInQueue(siteId, fileId); + } catch (error) { // Unsure why we could not get the record, let's add to the queue anyway. - this.addToQueue( - siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link), - ); + return this.addToQueue(siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link); + } + + const newData: Partial = {}; + let foundLink = false; + + // We already have the file in queue, we update the priority and links. + if (!entry.priority || entry.priority < priority) { + newData.priority = priority; + } + if (revision && entry.revision !== revision) { + newData.revision = revision; + } + if (timemodified && entry.timemodified !== timemodified) { + newData.timemodified = timemodified; + } + if (filePath && entry.path !== filePath) { + newData.path = filePath; + } + if (entry.isexternalfile !== options.isexternalfile && (entry.isexternalfile || options.isexternalfile)) { + newData.isexternalfile = options.isexternalfile; + } + if (entry.repositorytype !== options.repositorytype && (entry.repositorytype || options.repositorytype)) { + newData.repositorytype = options.repositorytype; + } + + if (link) { + // We need to add the new link if it does not exist yet. + if (entry.linksUnserialized && entry.linksUnserialized.length) { + foundLink = entry.linksUnserialized.some((fileLink) => + fileLink.component == link.component && fileLink.componentId == link.componentId); + } + + if (!foundLink) { + const links = entry.linksUnserialized || []; + links.push(link); + newData.links = JSON.stringify(links); + } + } + + if (Object.keys(newData).length) { + // Update only when required. + this.logger.debug(`Updating file ${fileId} which is already in queue`); + + return this.appDB.updateRecords(CoreFilepoolProvider.QUEUE_TABLE, newData, primaryKey).then(() => + this.getQueuePromise(siteId, fileId, true, onProgress)); + } + + this.logger.debug(`File ${fileId} already in queue and does not require update`); + if (queueDeferred) { + // If we were able to retrieve the queue deferred before, we use that one. + return queueDeferred.promise; + } else { + // Create a new deferred and return its promise. + return this.getQueuePromise(siteId, fileId, true, onProgress); + } } /** @@ -571,13 +592,32 @@ export class CoreFilepoolProvider { * @param revision File revision. If not defined, it will be calculated using the URL. * @return Promise resolved when the file is downloaded. */ - protected async addToQueueIfNeeded(siteId: string, fileUrl: string, component: string, componentId?: string | number, - timemodified: number = 0, checkSize: boolean = true, downloadUnknown?: boolean, options: CoreFilepoolFileOptions = {}, - revision?: number): Promise { + protected async addToQueueIfNeeded( + siteId: string, + fileUrl: string, + component?: string, + componentId?: string | number, + timemodified: number = 0, + checkSize: boolean = true, + downloadUnknown?: boolean, + options: CoreFilepoolFileOptions = {}, + revision?: number, + ): Promise { if (!checkSize) { // No need to check size, just add it to the queue. - await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, - revision, true); + await this.addToQueueByUrl( + siteId, + fileUrl, + component, + componentId, + timemodified, + undefined, + undefined, + 0, + options, + revision, + true, + ); } let size: number; @@ -603,14 +643,20 @@ export class CoreFilepoolProvider { } // Check if the file should be downloaded. - if (sizeUnknown) { - if (downloadUnknown && isWifi) { - await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, - 0, options, revision, true); - } - } else if (this.shouldDownload(size)) { - await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, - options, revision, true); + if ((sizeUnknown && downloadUnknown && isWifi) || (!sizeUnknown && this.shouldDownload(size))) { + await this.addToQueueByUrl( + siteId, + fileUrl, + component, + componentId, + timemodified, + undefined, + undefined, + 0, + options, + revision, + true, + ); } } @@ -699,12 +745,10 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Link, null if nothing to link. */ - protected createComponentLink(component: string, componentId?: string | number): CoreFilepoolComponentLink | null { + protected createComponentLink(component?: string, componentId?: string | number): CoreFilepoolComponentLink | undefined { if (typeof component != 'undefined' && component != null) { return { component, componentId: this.fixComponentId(componentId) }; } - - return null; } /** @@ -714,7 +758,7 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Links. */ - protected createComponentLinks(component: string, componentId?: string | number): CoreFilepoolComponentLink[] { + protected createComponentLinks(component?: string, componentId?: string | number): CoreFilepoolComponentLink[] { const link = this.createComponentLink(component, componentId); return link ? [link] : []; @@ -772,12 +816,18 @@ export class CoreFilepoolProvider { * @param poolFileObject When set, the object will be updated, a new entry will not be created. * @return Resolved with internal URL on success, rejected otherwise. */ - protected async downloadForPoolByUrl(siteId: string, fileUrl: string, options: CoreFilepoolFileOptions = {}, filePath?: string, - onProgress?: CoreFilepoolOnProgressCallback, poolFileObject?: CoreFilepoolFileEntry): Promise { + protected async downloadForPoolByUrl( + siteId: string, + fileUrl: string, + options: CoreFilepoolFileOptions = {}, + filePath?: string, + onProgress?: CoreFilepoolOnProgressCallback, + poolFileObject?: CoreFilepoolFileEntry, + ): Promise { const fileId = this.getFileIdByUrl(fileUrl); const extension = CoreMimetypeUtils.instance.guessExtensionFromUrl(fileUrl); const addExtension = typeof filePath == 'undefined'; - filePath = filePath || (await this.getFilePath(siteId, fileId, extension)); + const path = filePath || (await this.getFilePath(siteId, fileId, extension)); if (poolFileObject && poolFileObject.fileId !== fileId) { this.logger.error('Invalid object to update passed'); @@ -785,7 +835,7 @@ export class CoreFilepoolProvider { throw new CoreError('Invalid object to update passed.'); } - const downloadId = this.getFileDownloadId(fileUrl, filePath); + const downloadId = this.getFileDownloadId(fileUrl, path); if (this.filePromises[siteId] && this.filePromises[siteId][downloadId]) { // There's already a download ongoing for this file in this location, return the promise. @@ -799,22 +849,21 @@ export class CoreFilepoolProvider { throw new CoreError('Site doesn\'t allow downloading files.'); } - const entry = await CoreWS.instance.downloadFile(fileUrl, filePath, addExtension, onProgress); + const entry = await CoreWS.instance.downloadFile(fileUrl, path, addExtension, onProgress); const fileEntry = entry; await CorePluginFile.instance.treatDownloadedFile(fileUrl, fileEntry, siteId, onProgress); - const data: CoreFilepoolFileEntry = poolFileObject || {}; - data.downloadTime = Date.now(); - data.stale = 0; - data.url = fileUrl; - data.revision = options.revision; - data.timemodified = options.timemodified; - data.isexternalfile = options.isexternalfile ? 1 : 0; - data.repositorytype = options.repositorytype; - data.path = fileEntry.path; - data.extension = fileEntry.extension; - - await this.addFileToPool(siteId, fileId, data); + await this.addFileToPool(siteId, fileId, { + downloadTime: Date.now(), + stale: 0, + url: fileUrl, + revision: options.revision, + timemodified: options.timemodified, + isexternalfile: options.isexternalfile ? 1 : 0, + repositorytype: options.repositorytype, + path: fileEntry.path, + extension: fileEntry.extension, + }); return fileEntry.toURL(); }).finally(() => { @@ -838,9 +887,16 @@ export class CoreFilepoolProvider { * the files directly inside the filepool folder. * @return Resolved on success. */ - downloadOrPrefetchFiles(siteId: string, files: CoreWSExternalFile[], prefetch: boolean, ignoreStale?: boolean, - component?: string, componentId?: string | number, dirPath?: string): Promise { - const promises = []; + downloadOrPrefetchFiles( + siteId: string, + files: CoreWSExternalFile[], + prefetch: boolean, + ignoreStale?: boolean, + component?: string, + componentId?: string | number, + dirPath?: string, + ): Promise { + const promises: Promise[] = []; // Download files. files.forEach((file) => { @@ -850,23 +906,31 @@ export class CoreFilepoolProvider { isexternalfile: file.isexternalfile, repositorytype: file.repositorytype, }; - let path: string; + let path: string | undefined; if (dirPath) { // Calculate the path to the file. path = file.filename; - if (file.filepath !== '/') { + if (file.filepath && file.filepath !== '/') { path = file.filepath.substr(1) + path; } - path = CoreTextUtils.instance.concatenatePaths(dirPath, path); + path = CoreTextUtils.instance.concatenatePaths(dirPath, path!); } if (prefetch) { - promises.push(this.addToQueueByUrl( - siteId, url, component, componentId, timemodified, path, undefined, 0, options)); + promises.push(this.addToQueueByUrl(siteId, url, component, componentId, timemodified, path, undefined, 0, options)); } else { promises.push(this.downloadUrl( - siteId, url, ignoreStale, component, componentId, timemodified, undefined, path, options)); + siteId, + url, + ignoreStale, + component, + componentId, + timemodified, + undefined, + path, + options, + )); } }); @@ -887,9 +951,16 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Promise resolved when the package is downloaded. */ - protected downloadOrPrefetchPackage(siteId: string, fileList: CoreWSExternalFile[], prefetch?: boolean, component?: string, - componentId?: string | number, extra?: string, dirPath?: string, onProgress?: CoreFilepoolOnProgressCallback): - Promise { + protected downloadOrPrefetchPackage( + siteId: string, + fileList: CoreWSExternalFile[], + prefetch: boolean, + component: string, + componentId?: string | number, + extra?: string, + dirPath?: string, + onProgress?: CoreFilepoolOnProgressCallback, + ): Promise { const packageId = this.getPackageId(component, componentId); if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId]) { @@ -901,7 +972,7 @@ export class CoreFilepoolProvider { // Set package as downloading. const promise = this.storePackageStatus(siteId, CoreConstants.DOWNLOADING, component, componentId).then(async () => { - const promises = []; + const promises: Promise[] = []; let packageLoaded = 0; fileList.forEach((file) => { @@ -910,10 +981,10 @@ export class CoreFilepoolProvider { isexternalfile: file.isexternalfile, repositorytype: file.repositorytype, }; - let path: string; + let path: string | undefined; let promise: Promise; let fileLoaded = 0; - let onFileProgress: (progress: ProgressEvent) => void; + let onFileProgress: ((progress: ProgressEvent) => void) | undefined; if (onProgress) { // There's a onProgress event, create a function to receive file download progress events. @@ -934,18 +1005,36 @@ export class CoreFilepoolProvider { if (dirPath) { // Calculate the path to the file. path = file.filename; - if (file.filepath !== '/') { + if (file.filepath && file.filepath !== '/') { path = file.filepath.substr(1) + path; } - path = CoreTextUtils.instance.concatenatePaths(dirPath, path); + path = CoreTextUtils.instance.concatenatePaths(dirPath, path!); } if (prefetch) { promise = this.addToQueueByUrl( - siteId, fileUrl, component, componentId, file.timemodified, path, undefined, 0, options); + siteId, + fileUrl, + component, + componentId, + file.timemodified, + path, + undefined, + 0, + options, + ); } else { promise = this.downloadUrl( - siteId, fileUrl, false, component, componentId, file.timemodified, onFileProgress, path, options); + siteId, + fileUrl, + false, + component, + componentId, + file.timemodified, + onFileProgress, + path, + options, + ); } // Using undefined for success & fail will pass the success/failure to the parent promise. @@ -956,6 +1045,8 @@ export class CoreFilepoolProvider { await Promise.all(promises); // Success prefetching, store package as downloaded. await this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra); + + return; } catch (error) { // Error downloading, go back to previous status and reject the promise. await this.setPackagePreviousStatus(siteId, component, componentId); @@ -985,8 +1076,15 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Promise resolved when all files are downloaded. */ - downloadPackage(siteId: string, fileList: CoreWSExternalFile[], component: string, componentId?: string | number, - extra?: string, dirPath?: string, onProgress?: CoreFilepoolOnProgressCallback): Promise { + downloadPackage( + siteId: string, + fileList: CoreWSExternalFile[], + component: string, + componentId?: string | number, + extra?: string, + dirPath?: string, + onProgress?: CoreFilepoolOnProgressCallback, + ): Promise { return this.downloadOrPrefetchPackage(siteId, fileList, false, component, componentId, extra, dirPath, onProgress); } @@ -1010,10 +1108,18 @@ export class CoreFilepoolProvider { * not force a file to be re-downloaded if it is already part of the pool. You should mark a file as stale using * invalidateFileByUrl to trigger a download. */ - async downloadUrl(siteId: string, fileUrl: string, ignoreStale?: boolean, component?: string, componentId?: string | number, - timemodified: number = 0, onProgress?: CoreFilepoolOnProgressCallback, filePath?: string, - options: CoreFilepoolFileOptions = {}, revision?: number): Promise { - let promise: Promise; + async downloadUrl( + siteId: string, + fileUrl: string, + ignoreStale?: boolean, + component?: string, + componentId?: string | number, + timemodified: number = 0, + onProgress?: CoreFilepoolOnProgressCallback, + filePath?: string, + options: CoreFilepoolFileOptions = {}, + revision?: number, + ): Promise { let alreadyDownloaded = true; if (!CoreFile.instance.isAvailable()) { @@ -1031,47 +1137,9 @@ export class CoreFilepoolProvider { const links = this.createComponentLinks(component, componentId); - return this.hasFileInPool(siteId, fileId).then((fileObject) => { - if (typeof fileObject === 'undefined') { - // We do not have the file, download and add to pool. - this.notifyFileDownloading(siteId, fileId, links); - alreadyDownloaded = false; - - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress); - } else if (this.isFileOutdated(fileObject, options.revision, options.timemodified) && - CoreApp.instance.isOnline() && !ignoreStale) { - // The file is outdated, force the download and update it. - this.notifyFileDownloading(siteId, fileId, links); - alreadyDownloaded = false; - - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, fileObject); - } - - // Everything is fine, return the file on disk. - if (filePath) { - promise = this.getInternalUrlByPath(filePath); - } else { - promise = this.getInternalUrlById(siteId, fileId); - } - - return promise.then((url) => url, () => { - // The file was not found in the pool, weird. - this.notifyFileDownloading(siteId, fileId, links); - alreadyDownloaded = false; - - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, fileObject); - }); - }, () => { - // The file is not in the pool just yet. - this.notifyFileDownloading(siteId, fileId, links); - alreadyDownloaded = false; - - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress); - }).then((url) => { + const finishSuccessfulDownload = (url: string): string => { if (typeof component != 'undefined') { - this.addFileLink(siteId, fileId, component, componentId).catch(() => { - // Ignore errors. - }); + CoreUtils.instance.ignoreErrors(this.addFileLink(siteId, fileId, component, componentId)); } if (!alreadyDownloaded) { @@ -1079,11 +1147,43 @@ export class CoreFilepoolProvider { } return url; - }, (err) => { - this.notifyFileDownloadError(siteId, fileId, links); + }; - return Promise.reject(err); - }); + try { + const fileObject = await this.hasFileInPool(siteId, fileId); + let url: string; + + if (!fileObject || + this.isFileOutdated(fileObject, options.revision, options.timemodified) && + CoreApp.instance.isOnline() && + !ignoreStale + ) { + throw new CoreError('Needs to be downloaded'); + } + + // File downloaded and not outdated, return the file from disk. + if (filePath) { + url = await this.getInternalUrlByPath(filePath); + } else { + url = await this.getInternalUrlById(siteId, fileId); + } + + return finishSuccessfulDownload(url); + } catch (error) { + // The file is not downloaded or it's outdated. + this.notifyFileDownloading(siteId, fileId, links); + alreadyDownloaded = false; + + try { + const url = await this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress); + + return finishSuccessfulDownload(url); + } catch (error) { + this.notifyFileDownloadError(siteId, fileId, links); + + throw error; + } + } } /** @@ -1093,15 +1193,14 @@ export class CoreFilepoolProvider { * @return List of file urls. */ extractDownloadableFilesFromHtml(html: string): string[] { - let urls = []; + let urls: string[] = []; const element = CoreDomUtils.instance.convertToElement(html); - const elements: (HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | - HTMLTrackElement)[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track')); + const elements: AnchorOrMediaElement[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track')); for (let i = 0; i < elements.length; i++) { const element = elements[i]; - let url = 'href' in element ? element.href : element.src; + const url = 'href' in element ? element.href : element.src; if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) { urls.push(url); @@ -1109,9 +1208,9 @@ export class CoreFilepoolProvider { // Treat video poster. if (element.tagName == 'VIDEO' && element.getAttribute('poster')) { - url = element.getAttribute('poster'); - if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) { - urls.push(url); + const poster = element.getAttribute('poster'); + if (poster && CoreUrlUtils.instance.isDownloadableUrl(poster) && urls.indexOf(poster) == -1) { + urls.push(poster); } } } @@ -1186,20 +1285,20 @@ export class CoreFilepoolProvider { * @param componentId The component ID. * @return The normalised component ID. -1 when undefined was passed. */ - protected fixComponentId(componentId: string | number): string | number { + protected fixComponentId(componentId?: string | number): string | number { if (typeof componentId == 'number') { return componentId; } + if (typeof componentId == 'undefined' || componentId === null) { + return -1; + } + // Try to convert it to a number. const id = parseInt(componentId, 10); if (isNaN(id)) { // Not a number. - if (typeof componentId == 'undefined' || componentId === null) { - return -1; - } else { - return componentId; - } + return componentId; } return id; @@ -1230,8 +1329,11 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved with the files. */ - protected async getComponentFiles(db: SQLiteDB, component: string, componentId?: string | number): - Promise { + protected async getComponentFiles( + db: SQLiteDB, + component: string, + componentId?: string | number, + ): Promise { const conditions = { component, componentId: this.fixComponentId(componentId), @@ -1420,25 +1522,24 @@ export class CoreFilepoolProvider { async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise { const db = await CoreSites.instance.getSiteDb(siteId); const items = await this.getComponentFiles(db, component, componentId); - const files = []; + const files: CoreFilepoolFileEntry[] = []; + + await Promise.all(items.map(async (item) => { + try { + const fileEntry = await db.getRecord( + CoreFilepoolProvider.FILES_TABLE, + { fileId: item.fileId }, + ); - const promises = items.map((item) => - db.getRecord(CoreFilepoolProvider.FILES_TABLE, { fileId: item.fileId }).then((fileEntry) => { if (!fileEntry) { return; } - files.push({ - url: fileEntry.url, - path: fileEntry.path, - extension: fileEntry.extension, - revision: fileEntry.revision, - timemodified: fileEntry.timemodified, - }); - }).catch(() => { - // File not found, ignore error. - })); - await Promise.all(promises); + files.push(fileEntry); + } catch (error) { + // File not found, ignore error. + } + })); return files; } @@ -1451,21 +1552,22 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved with the size on success. */ - getFilesSizeByComponent(siteId: string, component: string, componentId?: string | number): Promise { - return this.getFilesByComponent(siteId, component, componentId).then((files) => { - const promises = []; - let size = 0; + async getFilesSizeByComponent(siteId: string, component: string, componentId?: string | number): Promise { + const files = await this.getFilesByComponent(siteId, component, componentId); - files.forEach((file) => { - promises.push(CoreFile.instance.getFileSize(file.path).then((fs) => { - size += fs; - }).catch(() => { - // Ignore failures, maybe some file was deleted. - })); - }); + let size = 0; - return Promise.all(promises).then(() => size); - }); + await Promise.all(files.map(async (file) => { + try { + const fileSize = await CoreFile.instance.getFileSize(file.path); + + size += fileSize; + } catch (error) { + // Ignore failures, maybe some file was deleted. + } + })); + + return size; } /** @@ -1478,8 +1580,13 @@ export class CoreFilepoolProvider { * @param revision File revision. If not defined, it will be calculated using the URL. * @return Promise resolved with the file state. */ - async getFileStateByUrl(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string, revision?: number): - Promise { + async getFileStateByUrl( + siteId: string, + fileUrl: string, + timemodified: number = 0, + filePath?: string, + revision?: number, + ): Promise { let file: CoreWSExternalFile; try { @@ -1545,65 +1652,73 @@ export class CoreFilepoolProvider { * This handles the queue and validity of the file. If there is a local file and it's valid, return the local URL. * If the file isn't downloaded or it's outdated, return the online URL and add it to the queue to be downloaded later. */ - protected async getFileUrlByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, - mode: string = 'url', timemodified: number = 0, checkSize: boolean = true, downloadUnknown?: boolean, - options: CoreFilepoolFileOptions = {}, revision?: number): Promise { + protected async getFileUrlByUrl( + siteId: string, + fileUrl: string, + component?: string, + componentId?: string | number, + mode: string = 'url', + timemodified: number = 0, + checkSize: boolean = true, + downloadUnknown?: boolean, + options: CoreFilepoolFileOptions = {}, + revision?: number, + ): Promise { const addToQueue = (fileUrl: string): void => { // Add the file to queue if needed and ignore errors. - this.addToQueueIfNeeded(siteId, fileUrl, component, componentId, timemodified, checkSize, - downloadUnknown, options, revision).catch(() => { - // Ignore errors. - }); + CoreUtils.instance.ignoreErrors(this.addToQueueIfNeeded( + siteId, + fileUrl, + component, + componentId, + timemodified, + checkSize, + downloadUnknown, + options, + revision, + )); }; const file = await this.fixPluginfileURL(siteId, fileUrl, timemodified); + fileUrl = file.fileurl; timemodified = file.timemodified || timemodified; revision = revision || this.getRevisionFromUrl(fileUrl); const fileId = this.getFileIdByUrl(fileUrl); - return this.hasFileInPool(siteId, fileId).then(async (entry) => { - if (typeof entry === 'undefined') { - // We do not have the file, add it to the queue, and return real URL. - addToQueue(fileUrl); + try { + const entry = await this.hasFileInPool(siteId, fileId); - return fileUrl; + if (typeof entry === 'undefined') { + throw new CoreError('File not downloaded.'); } if (this.isFileOutdated(entry, revision, timemodified) && CoreApp.instance.isOnline()) { - // The file is outdated, we add to the queue and return real URL. - addToQueue(fileUrl); - - return fileUrl; + throw new CoreError('File is outdated'); } - - try { - // We found the file entry, now look for the file on disk. - if (mode === 'src') { - return this.getInternalSrcById(siteId, fileId); - } else { - return this.getInternalUrlById(siteId, fileId); - } - } catch (error) { - // The file is not on disk. - // We could not retrieve the file, delete the entries associated with that ID. - this.logger.debug('File ' + fileId + ' not found on disk'); - this.removeFileById(siteId, fileId); - addToQueue(fileUrl); - - if (CoreApp.instance.isOnline()) { - // We still have a chance to serve the right content. - return fileUrl; - } - - throw new CoreError('File not found.'); - } - }, () => { - // We do not have the file in store yet. Add to queue and return the fixed URL. + } catch (error) { + // The file is not downloaded or it's outdated. Add to queue and return the fixed URL. addToQueue(fileUrl); return fileUrl; - }); + } + + try { + // We found the file entry, now look for the file on disk. + if (mode === 'src') { + return await this.getInternalSrcById(siteId, fileId); + } else { + return await this.getInternalUrlById(siteId, fileId); + } + } catch (error) { + // The file is not on disk. + // We could not retrieve the file, delete the entries associated with that ID. + this.logger.debug('File ' + fileId + ' not found on disk'); + this.removeFileById(siteId, fileId); + addToQueue(fileUrl); + + return fileUrl; + } } /** @@ -1770,7 +1885,7 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Download promise or undefined. */ - getPackageDownloadPromise(siteId: string, component: string, componentId?: string | number): Promise { + getPackageDownloadPromise(siteId: string, component: string, componentId?: string | number): Promise | undefined { const packageId = this.getPackageId(component, componentId); if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId]) { return this.packagesPromises[siteId][packageId]; @@ -1785,7 +1900,7 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved with the extra data. */ - getPackageExtra(siteId: string, component: string, componentId?: string | number): Promise { + getPackageExtra(siteId: string, component: string, componentId?: string | number): Promise { return this.getPackageData(siteId, component, componentId).then((entry) => entry.extra); } @@ -1842,7 +1957,7 @@ export class CoreFilepoolProvider { * @param url URL to get the args. * @return The args found, undefined if not a pluginfile. */ - protected getPluginFileArgs(url: string): string[] { + protected getPluginFileArgs(url: string): string[] | undefined { if (!CoreUrlUtils.instance.isPluginFileUrl(url)) { // Not pluginfile, return. return; @@ -1868,8 +1983,12 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Deferred. */ - protected getQueueDeferred(siteId: string, fileId: string, create: boolean = true, onProgress?: CoreFilepoolOnProgressCallback): - CoreFilepoolPromiseDefer { + protected getQueueDeferred( + siteId: string, + fileId: string, + create: boolean = true, + onProgress?: CoreFilepoolOnProgressCallback, + ): CoreFilepoolPromiseDefer | undefined { if (!this.queueDeferreds[siteId]) { if (!create) { return; @@ -1897,11 +2016,10 @@ export class CoreFilepoolProvider { * @param fileId The file ID. * @return On progress function, undefined if not found. */ - protected getQueueOnProgress(siteId: string, fileId: string): CoreFilepoolOnProgressCallback { + protected getQueueOnProgress(siteId: string, fileId: string): CoreFilepoolOnProgressCallback | undefined { const deferred = this.getQueueDeferred(siteId, fileId, false); - if (deferred) { - return deferred.onProgress; - } + + return deferred?.onProgress; } /** @@ -1913,9 +2031,15 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Promise. */ - protected getQueuePromise(siteId: string, fileId: string, create: boolean = true, onProgress?: CoreFilepoolOnProgressCallback): - Promise { - return this.getQueueDeferred(siteId, fileId, create, onProgress).promise; + protected getQueuePromise( + siteId: string, + fileId: string, + create: boolean = true, + onProgress?: CoreFilepoolOnProgressCallback, + ): Promise | undefined { + const deferred = this.getQueueDeferred(siteId, fileId, create, onProgress); + + return deferred?.promise; } /** @@ -1984,11 +2108,29 @@ export class CoreFilepoolProvider { * This will return a URL pointing to the content of the requested URL. * The URL returned is compatible to use with IMG tags. */ - getSrcByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, timemodified: number = 0, - checkSize: boolean = true, downloadUnknown?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): - Promise { - return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'src', - timemodified, checkSize, downloadUnknown, options, revision); + getSrcByUrl( + siteId: string, + fileUrl: string, + component?: string, + componentId?: string | number, + timemodified: number = 0, + checkSize: boolean = true, + downloadUnknown?: boolean, + options: CoreFilepoolFileOptions = {}, + revision?: number, + ): Promise { + return this.getFileUrlByUrl( + siteId, + fileUrl, + component, + componentId, + 'src', + timemodified, + checkSize, + downloadUnknown, + options, + revision, + ); } /** @@ -2001,7 +2143,7 @@ export class CoreFilepoolProvider { let timemodified = 0; files.forEach((file) => { - if (file.timemodified > timemodified) { + if (file.timemodified && file.timemodified > timemodified) { timemodified = file.timemodified; } }); @@ -2028,11 +2170,29 @@ export class CoreFilepoolProvider { * This will return a URL pointing to the content of the requested URL. * The URL returned is compatible to use with a local browser. */ - getUrlByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, timemodified: number = 0, - checkSize: boolean = true, downloadUnknown?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): - Promise { - return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'url', - timemodified, checkSize, downloadUnknown, options, revision); + getUrlByUrl( + siteId: string, + fileUrl: string, + component?: string, + componentId?: string | number, + timemodified: number = 0, + checkSize: boolean = true, + downloadUnknown?: boolean, + options: CoreFilepoolFileOptions = {}, + revision?: number, + ): Promise { + return this.getFileUrlByUrl( + siteId, + fileUrl, + component, + componentId, + 'url', + timemodified, + checkSize, + downloadUnknown, + options, + revision, + ); } /** @@ -2071,7 +2231,7 @@ export class CoreFilepoolProvider { // If there are hashes in the URL, extract them. const index = filename.indexOf('#'); - let hashes: string[]; + let hashes: string[] | undefined; if (index != -1) { hashes = filename.split('#'); @@ -2127,7 +2287,7 @@ export class CoreFilepoolProvider { throw new CoreError('File not found in queue.'); } // Convert the links to an object. - entry.linksUnserialized = CoreTextUtils.instance.parseJSON(entry.links, []); + entry.linksUnserialized = CoreTextUtils.instance.parseJSON(entry.links || '[]', []); return entry; } @@ -2143,7 +2303,7 @@ export class CoreFilepoolProvider { async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise { const db = await CoreSites.instance.getSiteDb(siteId); - const where = onlyUnknown ? CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : null; + const where = onlyUnknown ? CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : undefined; await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, where); } @@ -2179,8 +2339,12 @@ export class CoreFilepoolProvider { * It is advised to set it to true to reduce the performance and data usage of the app. * @return Resolved when done. */ - async invalidateFilesByComponent(siteId: string, component: string, componentId?: string | number, onlyUnknown: boolean = true): - Promise { + async invalidateFilesByComponent( + siteId: string, + component: string, + componentId?: string | number, + onlyUnknown: boolean = true, + ): Promise { const db = await CoreSites.instance.getSiteDb(siteId); const items = await this.getComponentFiles(db, component, componentId); @@ -2223,8 +2387,13 @@ export class CoreFilepoolProvider { * @param revision File revision. If not defined, it will be calculated using the URL. * @return Promise resolved with a boolean: whether a file is downloadable. */ - async isFileDownloadable(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string, revision?: number): - Promise { + async isFileDownloadable( + siteId: string, + fileUrl: string, + timemodified: number = 0, + filePath?: string, + revision?: number, + ): Promise { const state = await this.getFileStateByUrl(siteId, fileUrl, timemodified, filePath, revision); return state != CoreConstants.NOT_DOWNLOADABLE; @@ -2253,7 +2422,9 @@ export class CoreFilepoolProvider { * @param Whether the file is outdated. */ protected isFileOutdated(entry: CoreFilepoolFileEntry, revision?: number, timemodified?: number): boolean { - return !!entry.stale || revision > entry.revision || timemodified > entry.timemodified; + return !!entry.stale || + (revision !== undefined && (entry.revision === undefined || revision > entry.revision)) || + (timemodified !== undefined && (entry.timemodified === undefined || timemodified > entry.timemodified)); } /** @@ -2273,8 +2444,11 @@ export class CoreFilepoolProvider { * @param eventData The file event data. * @param links The links to the components. */ - protected notifyFileActionToComponents(siteId: string, eventData: CoreFilepoolFileEventData, - links: CoreFilepoolComponentLink[]): void { + protected notifyFileActionToComponents( + siteId: string, + eventData: CoreFilepoolFileEventData, + links: CoreFilepoolComponentLink[], + ): void { links.forEach((link) => { const data: CoreFilepoolComponentFileEventData = Object.assign({ component: link.component, @@ -2385,8 +2559,15 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Promise resolved when all files are downloaded. */ - prefetchPackage(siteId: string, fileList: CoreWSExternalFile[], component: string, componentId?: string | number, - extra?: string, dirPath?: string, onProgress?: CoreFilepoolOnProgressCallback): Promise { + prefetchPackage( + siteId: string, + fileList: CoreWSExternalFile[], + component: string, + componentId?: string | number, + extra?: string, + dirPath?: string, + onProgress?: CoreFilepoolOnProgressCallback, + ): Promise { return this.downloadOrPrefetchPackage(siteId, fileList, true, component, componentId, extra, dirPath, onProgress); } @@ -2397,24 +2578,17 @@ export class CoreFilepoolProvider { * This loops over itself to keep on processing the queue in the background. * The queue process is site agnostic. */ - protected processQueue(): void { - let promise: Promise; + protected async processQueue(): Promise { + try { + if (this.queueState !== CoreFilepoolProvider.QUEUE_RUNNING) { + // Silently ignore, the queue is on pause. + throw CoreFilepoolProvider.ERR_QUEUE_ON_PAUSE; + } else if (!CoreFile.instance.isAvailable() || !CoreApp.instance.isOnline()) { + throw CoreFilepoolProvider.ERR_FS_OR_NETWORK_UNAVAILABLE; + } - if (this.queueState !== CoreFilepoolProvider.QUEUE_RUNNING) { - // Silently ignore, the queue is on pause. - promise = Promise.reject(CoreFilepoolProvider.ERR_QUEUE_ON_PAUSE); - } else if (!CoreFile.instance.isAvailable() || !CoreApp.instance.isOnline()) { - promise = Promise.reject(CoreFilepoolProvider.ERR_FS_OR_NETWORK_UNAVAILABLE); - } else { - promise = this.processImportantQueueItem(); - } - - promise.then(() => { - // All good, we schedule next execution. - setTimeout(() => { - this.processQueue(); - }, CoreFilepoolProvider.QUEUE_PROCESS_INTERVAL); - }, (error) => { + await this.processImportantQueueItem(); + } catch (error) { // We had an error, in which case we pause the processing. if (error === CoreFilepoolProvider.ERR_FS_OR_NETWORK_UNAVAILABLE) { this.logger.debug('Filesysem or network unavailable, pausing queue processing.'); @@ -2423,7 +2597,14 @@ export class CoreFilepoolProvider { } this.queueState = CoreFilepoolProvider.QUEUE_PAUSED; - }); + + return; + } + + // All good, we schedule next execution. + setTimeout(() => { + this.processQueue(); + }, CoreFilepoolProvider.QUEUE_PROCESS_INTERVAL); } /** @@ -2437,8 +2618,14 @@ export class CoreFilepoolProvider { let items: CoreFilepoolQueueEntry[]; try { - items = await this.appDB.getRecords(CoreFilepoolProvider.QUEUE_TABLE, undefined, - 'priority DESC, added ASC', undefined, 0, 1); + items = await this.appDB.getRecords( + CoreFilepoolProvider.QUEUE_TABLE, + undefined, + 'priority DESC, added ASC', + undefined, + 0, + 1, + ); } catch (err) { throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; } @@ -2475,7 +2662,7 @@ export class CoreFilepoolProvider { this.logger.debug('Processing queue item: ' + siteId + ', ' + fileId); - let entry: CoreFilepoolFileEntry; + let entry: CoreFilepoolFileEntry | undefined; // Check if the file is already in pool. try { @@ -2502,21 +2689,19 @@ export class CoreFilepoolProvider { // The file does not exist, or is stale, ... download it. const onProgress = this.getQueueOnProgress(siteId, fileId); - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, entry).then(() => { + try { + await this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, entry); + // Success, we add links and remove from queue. - this.addFileLinks(siteId, fileId, links).catch(() => { - // Ignore errors. - }); + CoreUtils.instance.ignoreErrors(this.addFileLinks(siteId, fileId, links)); this.treatQueueDeferred(siteId, fileId, true); this.notifyFileDownloaded(siteId, fileId, links); // Wait for the item to be removed from queue before resolving the promise. // If the item could not be removed from queue we still resolve the promise. - return this.removeFromQueue(siteId, fileId).catch(() => { - // Ignore errors. - }); - }, (errorObject) => { + await CoreUtils.instance.ignoreErrors(this.removeFromQueue(siteId, fileId)); + } catch (errorObject) { // Whoops, we have an error... let dropFromQueue = false; @@ -2544,7 +2729,7 @@ export class CoreFilepoolProvider { dropFromQueue = true; } - let errorMessage = null; + let errorMessage: string | undefined; // Some Android devices restrict the amount of usable storage using quotas. // If this quota would be exceeded by the download, it throws an exception. // We catch this exception here, and report a meaningful error message to the user. @@ -2555,20 +2740,18 @@ export class CoreFilepoolProvider { if (dropFromQueue) { this.logger.debug('Item dropped from queue due to error: ' + fileUrl, errorObject); - return this.removeFromQueue(siteId, fileId).catch(() => { - // Consider this as a silent error, never reject the promise here. - }).then(() => { - this.treatQueueDeferred(siteId, fileId, false, errorMessage); - this.notifyFileDownloadError(siteId, fileId, links); - }); + await CoreUtils.instance.ignoreErrors(this.removeFromQueue(siteId, fileId)); + + this.treatQueueDeferred(siteId, fileId, false, errorMessage); + this.notifyFileDownloadError(siteId, fileId, links); } else { // We considered the file as legit but did not get it, failure. this.treatQueueDeferred(siteId, fileId, false, errorMessage); this.notifyFileDownloadError(siteId, fileId, links); - return Promise.reject(errorObject); + throw errorObject; } - }); + } } /** @@ -2596,12 +2779,12 @@ export class CoreFilepoolProvider { // Get the path to the file first since it relies on the file object stored in the pool. // Don't use getFilePath to prevent performing 2 DB requests. let path = this.getFilepoolFolderPath(siteId) + '/' + fileId; - let fileUrl: string; + let fileUrl: string | undefined; try { const entry = await this.hasFileInPool(siteId, fileId); - fileUrl = entry.url; + fileUrl = entry.url; if (entry.extension) { path += '.' + entry.extension; } @@ -2615,7 +2798,7 @@ export class CoreFilepoolProvider { // Get links to components to notify them after remove. const links = await this.getFileLinks(siteId, fileId); - const promises = []; + const promises: Promise[] = []; // Remove entry from filepool store. promises.push(db.deleteRecords(CoreFilepoolProvider.FILES_TABLE, conditions)); @@ -2629,7 +2812,7 @@ export class CoreFilepoolProvider { if (error && error.code == 1) { // Not found, ignore error since maybe it was deleted already. } else { - return Promise.reject(error); + throw error; } })); } @@ -2638,10 +2821,8 @@ export class CoreFilepoolProvider { this.notifyFileDeleted(siteId, fileId, links); - try { - await CorePluginFile.instance.fileDeleted(fileUrl, path, siteId); - } catch (error) { - // Ignore errors. + if (fileUrl) { + await CoreUtils.instance.ignoreErrors(CorePluginFile.instance.fileDeleted(fileUrl, path, siteId)); } } @@ -2720,7 +2901,7 @@ export class CoreFilepoolProvider { await site.getDb().updateRecords(CoreFilepoolProvider.PACKAGES_TABLE, newData, { id: packageId }); // Success updating, trigger event. - this.triggerPackageStatusChanged(site.id, newData.status, component, componentId); + this.triggerPackageStatusChanged(site.id!, newData.status, component, componentId); return newData.status; } @@ -2778,22 +2959,27 @@ export class CoreFilepoolProvider { * @param extra Extra data to store for the package. If you want to store more than 1 value, use JSON.stringify. * @return Promise resolved when status is stored. */ - async storePackageStatus(siteId: string, status: string, component: string, componentId?: string | number, extra?: string): - Promise { + async storePackageStatus( + siteId: string, + status: string, + component: string, + componentId?: string | number, + extra?: string, + ): Promise { this.logger.debug(`Set status '${status}' for package ${component} ${componentId}`); componentId = this.fixComponentId(componentId); const site = await CoreSites.instance.getSite(siteId); const packageId = this.getPackageId(component, componentId); - let downloadTime: number; - let previousDownloadTime: number; + let downloadTime: number | undefined; + let previousDownloadTime: number | undefined; if (status == CoreConstants.DOWNLOADING) { // Set download time if package is now downloading. downloadTime = CoreTimeUtils.instance.timestamp(); } - let previousStatus: string; + let previousStatus: string | undefined; // Search current status to set it as previous status. try { const entry = site.getDb().getRecord(CoreFilepoolProvider.PACKAGES_TABLE, { id: packageId }); @@ -2849,40 +3035,56 @@ export class CoreFilepoolProvider { * @param revision Revision to use in all files. If not defined, it will be calculated using the URL of each file. * @return Promise resolved with the CSS code. */ - treatCSSCode(siteId: string, fileUrl: string, cssCode: string, component?: string, componentId?: string | number, - revision?: number): Promise { + async treatCSSCode( + siteId: string, + fileUrl: string, + cssCode: string, + component?: string, + componentId?: string | number, + revision?: number, + ): Promise { const urls = CoreDomUtils.instance.extractUrlsFromCSS(cssCode); - const promises = []; - let filePath: string; let updated = false; // Get the path of the CSS file. - promises.push(this.getFilePathByUrl(siteId, fileUrl).then((path) => { - filePath = path; + const filePath = await this.getFilePathByUrl(siteId, fileUrl); + + // Download all files in the CSS. + await Promise.all(urls.map(async (url) => { + // Download the file only if it's an online URL. + if (CoreUrlUtils.instance.isLocalFileUrl(url)) { + return; + } + + try { + const fileUrl = await this.downloadUrl( + siteId, + url, + false, + component, + componentId, + 0, + undefined, + undefined, + undefined, + revision, + ); + + if (fileUrl != url) { + cssCode = cssCode.replace(new RegExp(CoreTextUtils.instance.escapeForRegex(url), 'g'), fileUrl); + updated = true; + } + } catch (error) { + this.logger.warn('Error treating file ', url, error); + } })); - urls.forEach((url) => { - // Download the file only if it's an online URL. - if (!CoreUrlUtils.instance.isLocalFileUrl(url)) { - promises.push(this.downloadUrl(siteId, url, false, component, componentId, 0, undefined, undefined, undefined, - revision).then((fileUrl) => { - if (fileUrl != url) { - cssCode = cssCode.replace(new RegExp(CoreTextUtils.instance.escapeForRegex(url), 'g'), fileUrl); - updated = true; - } - }).catch((error) => { - // It shouldn't happen. Ignore errors. - this.logger.warn('Error treating file ', url, error); - })); - } - }); + // All files downloaded. Store the result if it has changed. + if (updated) { + await CoreFile.instance.writeFile(filePath, cssCode); + } - return Promise.all(promises).then(() => { - // All files downloaded. Store the result if it has changed. - if (updated) { - return CoreFile.instance.writeFile(filePath, cssCode); - } - }).then(() => cssCode); + return cssCode; } /** @@ -2912,7 +3114,7 @@ export class CoreFilepoolProvider { * @param component Package's component. * @param componentId An ID to use in conjunction with the component. */ - protected triggerPackageStatusChanged(siteId: string, status: string, component: string, componentId?: string | number): void { + protected triggerPackageStatusChanged(siteId: string, status: string, component?: string, componentId?: string | number): void { const data = { component, componentId: this.fixComponentId(componentId), @@ -2938,8 +3140,11 @@ export class CoreFilepoolProvider { const site = await CoreSites.instance.getSite(siteId); const packageId = this.getPackageId(component, componentId); - await site.getDb().updateRecords(CoreFilepoolProvider.PACKAGES_TABLE, - { downloadTime: CoreTimeUtils.instance.timestamp() }, { id: packageId }); + await site.getDb().updateRecords( + CoreFilepoolProvider.PACKAGES_TABLE, + { downloadTime: CoreTimeUtils.instance.timestamp() }, + { id: packageId }, + ); } } @@ -2963,32 +3168,32 @@ export type CoreFilepoolFileEntry = CoreFilepoolFileOptions & { /** * The fileId to identify the file. */ - fileId?: string; + fileId: string; /** * File's URL. */ - url?: string; + url: string; /** * 1 if file is stale (needs to be updated), 0 otherwise. */ - stale?: number; + stale: number; /** * Timestamp when this file was downloaded. */ - downloadTime?: number; + downloadTime: number; /** * File's path. */ - path?: string; + path: string; /** * File's extension. */ - extension?: string; + extension: string; }; /** @@ -2998,27 +3203,27 @@ export type CoreFilepoolQueueDBEntry = CoreFilepoolFileOptions & { /** * The site the file belongs to. */ - siteId?: string; + siteId: string; /** * The fileId to identify the file. */ - fileId?: string; + fileId: string; /** * Timestamp when the file was added to the queue. */ - added?: number; + added: number; /** * The priority of the file. */ - priority?: number; + priority: number; /** * File's URL. */ - url?: string; + url: string; /** * File's path. @@ -3028,7 +3233,7 @@ export type CoreFilepoolQueueDBEntry = CoreFilepoolFileOptions & { /** * File links (to link the file to components and componentIds). Serialized to store on DB. */ - links?: string; + links: string; }; /** @@ -3148,7 +3353,7 @@ export type CoreFilepoolComponentFileEventData = CoreFilepoolFileEventData & { /** * The component ID. */ - componentId: string | number; + componentId?: string | number; }; /** @@ -3171,3 +3376,6 @@ type CoreFilepoolLinksRecord = { component: string; // Component name. componentId: number | string; // Component Id. }; + +type AnchorOrMediaElement = + HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | HTMLTrackElement; diff --git a/src/app/services/local-notifications.ts b/src/app/services/local-notifications.ts index 385837a89..9c2175d44 100644 --- a/src/app/services/local-notifications.ts +++ b/src/app/services/local-notifications.ts @@ -106,7 +106,7 @@ export class CoreLocalNotificationsProvider { protected cancelSubscription?: Subscription; protected addSubscription?: Subscription; protected updateSubscription?: Subscription; - protected queueRunner?: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477). + protected queueRunner: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477). constructor() { this.logger = CoreLogger.getInstance('CoreLocalNotificationsProvider'); @@ -116,46 +116,53 @@ export class CoreLocalNotificationsProvider { // Ignore errors. }); - Platform.instance.ready().then(() => { - // Listen to events. - this.triggerSubscription = LocalNotifications.instance.on('trigger').subscribe((notification: ILocalNotification) => { - this.trigger(notification); + this.init(); + } - this.handleEvent('trigger', notification); - }); + /** + * Init some properties. + */ + protected async init(): Promise { + await Platform.instance.ready(); - this.clickSubscription = LocalNotifications.instance.on('click').subscribe((notification: ILocalNotification) => { - this.handleEvent('click', notification); - }); + // Listen to events. + this.triggerSubscription = LocalNotifications.instance.on('trigger').subscribe((notification: ILocalNotification) => { + this.trigger(notification); - this.clearSubscription = LocalNotifications.instance.on('clear').subscribe((notification: ILocalNotification) => { - this.handleEvent('clear', notification); - }); + this.handleEvent('trigger', notification); + }); - this.cancelSubscription = LocalNotifications.instance.on('cancel').subscribe((notification: ILocalNotification) => { - this.handleEvent('cancel', notification); - }); + this.clickSubscription = LocalNotifications.instance.on('click').subscribe((notification: ILocalNotification) => { + this.handleEvent('click', notification); + }); - this.addSubscription = LocalNotifications.instance.on('schedule').subscribe((notification: ILocalNotification) => { - this.handleEvent('schedule', notification); - }); + this.clearSubscription = LocalNotifications.instance.on('clear').subscribe((notification: ILocalNotification) => { + this.handleEvent('clear', notification); + }); - this.updateSubscription = LocalNotifications.instance.on('update').subscribe((notification: ILocalNotification) => { - this.handleEvent('update', notification); - }); + this.cancelSubscription = LocalNotifications.instance.on('cancel').subscribe((notification: ILocalNotification) => { + this.handleEvent('cancel', notification); + }); - // Create the default channel for local notifications. + this.addSubscription = LocalNotifications.instance.on('schedule').subscribe((notification: ILocalNotification) => { + this.handleEvent('schedule', notification); + }); + + this.updateSubscription = LocalNotifications.instance.on('update').subscribe((notification: ILocalNotification) => { + this.handleEvent('update', notification); + }); + + // Create the default channel for local notifications. + this.createDefaultChannel(); + + Translate.instance.onLangChange.subscribe(() => { + // Update the channel name. this.createDefaultChannel(); - - Translate.instance.onLangChange.subscribe(() => { - // Update the channel name. - this.createDefaultChannel(); - }); }); CoreEvents.instance.on(CoreEventsProvider.SITE_DELETED, (site: CoreSite) => { if (site) { - this.cancelSiteNotifications(site.id); + this.cancelSiteNotifications(site.id!); } }); } @@ -193,13 +200,13 @@ export class CoreLocalNotificationsProvider { const scheduled = await this.getAllScheduled(); - const ids = []; + const ids: number[] = []; const queueId = 'cancelSiteNotifications-' + siteId; scheduled.forEach((notif) => { notif.data = this.parseNotificationData(notif.data); - if (typeof notif.data == 'object' && notif.data.siteId === siteId) { + if (notif.id && typeof notif.data == 'object' && notif.data.siteId === siteId) { ids.push(notif.id); } }); @@ -355,10 +362,9 @@ export class CoreLocalNotificationsProvider { * @return Whether local notifications plugin is installed. */ isAvailable(): boolean { - const win = window; + const win = window; // eslint-disable-line @typescript-eslint/no-explicit-any - return CoreApp.instance.isDesktop() || !!(win.cordova && win.cordova.plugins && win.cordova.plugins.notification && - win.cordova.plugins.notification.local); + return CoreApp.instance.isDesktop() || !!win.cordova?.plugins?.notification?.local; } /** @@ -388,11 +394,11 @@ export class CoreLocalNotificationsProvider { if (useQueue) { const queueId = 'isTriggered-' + notification.id; - return this.queueRunner.run(queueId, () => LocalNotifications.instance.isTriggered(notification.id), { + return this.queueRunner.run(queueId, () => LocalNotifications.instance.isTriggered(notification.id!), { allowRepeated: true, }); } else { - return LocalNotifications.instance.isTriggered(notification.id); + return LocalNotifications.instance.isTriggered(notification.id || 0); } } } @@ -446,9 +452,8 @@ export class CoreLocalNotificationsProvider { /** * Process the next request in queue. */ - protected processNextRequest(): void { + protected async processNextRequest(): Promise { const nextKey = Object.keys(this.codeRequestsQueue)[0]; - let promise: Promise; if (typeof nextKey == 'undefined') { // No more requests in queue, stop. @@ -457,27 +462,27 @@ export class CoreLocalNotificationsProvider { const request = this.codeRequestsQueue[nextKey]; - // Check if request is valid. - if (typeof request == 'object' && typeof request.table != 'undefined' && typeof request.id != 'undefined') { - // Get the code and resolve/reject all the promises of this request. - promise = this.getCode(request.table, request.id).then((code) => { - request.deferreds.forEach((p) => { - p.resolve(code); - }); - }).catch((error) => { - request.deferreds.forEach((p) => { - p.reject(error); - }); - }); - } else { - promise = Promise.resolve(); - } + try { + // Check if request is valid. + if (typeof request != 'object' || request.table === undefined || request.id === undefined) { + return; + } - // Once this item is treated, remove it and process next. - promise.finally(() => { + // Get the code and resolve/reject all the promises of this request. + const code = await this.getCode(request.table, request.id); + + request.deferreds.forEach((p) => { + p.resolve(code); + }); + } catch (error) { + request.deferreds.forEach((p) => { + p.reject(error); + }); + } finally { + // Once this item is treated, remove it and process next. delete this.codeRequestsQueue[nextKey]; this.processNextRequest(); - }); + } } /** @@ -596,7 +601,7 @@ export class CoreLocalNotificationsProvider { */ async schedule(notification: ILocalNotification, component: string, siteId: string, alreadyUnique?: boolean): Promise { if (!alreadyUnique) { - notification.id = await this.getUniqueNotificationId(notification.id, component, siteId); + notification.id = await this.getUniqueNotificationId(notification.id || 0, component, siteId); } notification.data = notification.data || {}; @@ -663,7 +668,7 @@ export class CoreLocalNotificationsProvider { } if (!soundEnabled) { - notification.sound = null; + notification.sound = undefined; } else { delete notification.sound; // Use default value. } @@ -671,7 +676,7 @@ export class CoreLocalNotificationsProvider { notification.foreground = true; // Remove from triggered, since the notification could be in there with a different time. - this.removeTriggered(notification.id); + this.removeTriggered(notification.id || 0); LocalNotifications.instance.schedule(notification); } }); diff --git a/src/app/services/plugin-file-delegate.ts b/src/app/services/plugin-file-delegate.ts index 8621d13b8..572cc0913 100644 --- a/src/app/services/plugin-file-delegate.ts +++ b/src/app/services/plugin-file-delegate.ts @@ -206,7 +206,7 @@ export class CorePluginFileDelegate extends CoreDelegate { } } - return downloadableFile.filesize; + return downloadableFile.filesize || 0; } /** @@ -215,7 +215,7 @@ export class CorePluginFileDelegate extends CoreDelegate { * @param file File data. * @return Handler. */ - protected getHandlerForFile(file: CoreWSExternalFile): CorePluginFileHandler { + protected getHandlerForFile(file: CoreWSExternalFile): CorePluginFileHandler | undefined { for (const component in this.enabledHandlers) { const handler = this.enabledHandlers[component]; diff --git a/src/app/services/sites.ts b/src/app/services/sites.ts index 004be6ac3..923803668 100644 --- a/src/app/services/sites.ts +++ b/src/app/services/sites.ts @@ -130,7 +130,7 @@ export class CoreSitesProvider { // Move the records from the old table. const sites = await db.getAllRecords(oldTable); - const promises = []; + const promises: Promise[] = []; sites.forEach((site) => { promises.push(db.insertRecord(newTable, site)); @@ -153,12 +153,12 @@ export class CoreSitesProvider { protected readonly VALID_VERSION = 1; protected readonly INVALID_VERSION = -1; - protected isWPApp: boolean; + protected isWPApp = false; protected logger: CoreLogger; protected services = {}; protected sessionRestored = false; - protected currentSite: CoreSite; + protected currentSite?: CoreSite; protected sites: { [s: string]: CoreSite } = {}; protected appDB: SQLiteDB; protected dbReady: Promise; // Promise resolved when the app DB is initialized. @@ -249,7 +249,8 @@ export class CoreSitesProvider { await db.execute( 'INSERT INTO ' + newTable + ' ' + 'SELECT id, data, key, expirationTime, NULL as component, NULL as componentId ' + - 'FROM ' + oldTable); + 'FROM ' + oldTable, + ); try { await db.dropTable(oldTable); @@ -276,7 +277,7 @@ export class CoreSitesProvider { * @param name Name of the site to check. * @return Site data if it's a demo site, undefined otherwise. */ - getDemoSiteData(name: string): CoreSitesDemoSiteData { + getDemoSiteData(name: string): CoreSitesDemoSiteData | undefined { const demoSites = CoreConfigConstants.demo_sites; name = name.toLowerCase(); @@ -293,39 +294,43 @@ export class CoreSitesProvider { * @param protocol Protocol to use first. * @return A promise resolved when the site is checked. */ - checkSite(siteUrl: string, protocol: string = 'https://'): Promise { + async checkSite(siteUrl: string, protocol: string = 'https://'): Promise { // The formatURL function adds the protocol if is missing. siteUrl = CoreUrlUtils.instance.formatURL(siteUrl); if (!CoreUrlUtils.instance.isHttpURL(siteUrl)) { - return Promise.reject(new CoreError(Translate.instance.instant('core.login.invalidsite'))); + throw new CoreError(Translate.instance.instant('core.login.invalidsite')); } else if (!CoreApp.instance.isOnline()) { - return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); - } else { - return this.checkSiteWithProtocol(siteUrl, protocol).catch((error: CoreSiteError) => { - // Do not continue checking if a critical error happened. - if (error.critical) { - return Promise.reject(error); + throw new CoreError(Translate.instance.instant('core.networkerrormsg')); + } + + try { + return await this.checkSiteWithProtocol(siteUrl, protocol); + } catch (error) { + // Do not continue checking if a critical error happened. + if (error.critical) { + throw error; + } + + // Retry with the other protocol. + protocol = protocol == 'https://' ? 'http://' : 'https://'; + + try { + return await this.checkSiteWithProtocol(siteUrl, protocol); + } catch (secondError) { + if (secondError.critical) { + throw secondError; } - // Retry with the other protocol. - protocol = protocol == 'https://' ? 'http://' : 'https://'; - - return this.checkSiteWithProtocol(siteUrl, protocol).catch((secondError: CoreSiteError) => { - if (secondError.critical) { - return Promise.reject(secondError); - } - - // Site doesn't exist. Return the error message. - if (CoreTextUtils.instance.getErrorMessageFromError(error)) { - return Promise.reject(error); - } else if (CoreTextUtils.instance.getErrorMessageFromError(secondError)) { - return Promise.reject(secondError); - } else { - return Translate.instance.instant('core.cannotconnecttrouble'); - } - }); - }); + // Site doesn't exist. Return the error message. + if (CoreTextUtils.instance.getErrorMessageFromError(error)) { + throw error; + } else if (CoreTextUtils.instance.getErrorMessageFromError(secondError)) { + throw secondError; + } else { + throw new CoreError(Translate.instance.instant('core.cannotconnecttrouble')); + } + } } } @@ -336,121 +341,123 @@ export class CoreSitesProvider { * @param protocol Protocol to use. * @return A promise resolved when the site is checked. */ - checkSiteWithProtocol(siteUrl: string, protocol: string): Promise { - let publicConfig: CoreSitePublicConfigResponse; + async checkSiteWithProtocol(siteUrl: string, protocol: string): Promise { + let publicConfig: CoreSitePublicConfigResponse | undefined; // Now, replace the siteUrl with the protocol. siteUrl = siteUrl.replace(/^https?:\/\//i, protocol); - return this.siteExists(siteUrl).catch((error: CoreSiteError) => { + try { + await this.siteExists(siteUrl); + } catch (error) { // Do not continue checking if WS are not enabled. if (error.errorcode == 'enablewsdescription') { error.critical = true; - return Promise.reject(error); + throw error; } // Site doesn't exist. Try to add or remove 'www'. const treatedUrl = CoreUrlUtils.instance.addOrRemoveWWW(siteUrl); - return this.siteExists(treatedUrl).then(() => { + try { + await this.siteExists(treatedUrl); + // Success, use this new URL as site url. siteUrl = treatedUrl; - }).catch((secondError: CoreSiteError) => { + } catch (secondError) { // Do not continue checking if WS are not enabled. if (secondError.errorcode == 'enablewsdescription') { secondError.critical = true; - return Promise.reject(secondError); + throw secondError; } // Return the error. if (CoreTextUtils.instance.getErrorMessageFromError(error)) { - return Promise.reject(error); + throw error; } else { - return Promise.reject(secondError); + throw secondError; } + } + } + + // Site exists. Create a temporary site to check if local_mobile is installed. + const temporarySite = new CoreSite(undefined, siteUrl); + let data: LocalMobileResponse; + + try { + data = await temporarySite.checkLocalMobilePlugin(); + } catch (error) { + // Local mobile check returned an error. This only happens if the plugin is installed and it returns an error. + throw new CoreSiteError({ + message: error.message, + critical: true, }); - }).then(() => { - // Create a temporary site to check if local_mobile is installed. - const temporarySite = new CoreSite(undefined, siteUrl); + } - return temporarySite.checkLocalMobilePlugin().then((data) => { - data.service = data.service || CoreConfigConstants.wsservice; - this.services[siteUrl] = data.service; // No need to store it in DB. + data.service = data.service || CoreConfigConstants.wsservice; + this.services[siteUrl] = data.service; // No need to store it in DB. - if (data.coreSupported || - (data.code != CoreConstants.LOGIN_SSO_CODE && data.code != CoreConstants.LOGIN_SSO_INAPP_CODE)) { - // SSO using local_mobile not needed, try to get the site public config. - return temporarySite.getPublicConfig().then((config) => { - publicConfig = config; + if (data.coreSupported || (data.code != CoreConstants.LOGIN_SSO_CODE && data.code != CoreConstants.LOGIN_SSO_INAPP_CODE)) { + // SSO using local_mobile not needed, try to get the site public config. + try { + const config = await temporarySite.getPublicConfig(); - // Check that the user can authenticate. - if (!config.enablewebservices) { - return Promise.reject(new CoreSiteError({ - message: Translate.instance.instant('core.login.webservicesnotenabled'), - })); - } else if (!config.enablemobilewebservice) { - return Promise.reject(new CoreSiteError({ - message: Translate.instance.instant('core.login.mobileservicesnotenabled'), - })); - } else if (config.maintenanceenabled) { - let message = Translate.instance.instant('core.sitemaintenance'); - if (config.maintenancemessage) { - message += config.maintenancemessage; - } + publicConfig = config; - return Promise.reject(new CoreSiteError({ - message, - })); - } + // Check that the user can authenticate. + if (!config.enablewebservices) { + throw new CoreSiteError({ + message: Translate.instance.instant('core.login.webservicesnotenabled'), + }); + } else if (!config.enablemobilewebservice) { + throw new CoreSiteError({ + message: Translate.instance.instant('core.login.mobileservicesnotenabled'), + }); + } else if (config.maintenanceenabled) { + let message = Translate.instance.instant('core.sitemaintenance'); + if (config.maintenancemessage) { + message += config.maintenancemessage; + } - // Everything ok. - if (data.code === 0) { - data.code = config.typeoflogin; - } - - return data; - }, async (error) => { - // Error, check if not supported. - if (error.available === 1) { - // Service supported but an error happened. Return error. - if (error.errorcode == 'codingerror') { - // This could be caused by a redirect. Check if it's the case. - const redirect = await CoreUtils.instance.checkRedirect(siteUrl); - - if (redirect) { - error.error = Translate.instance.instant('core.login.sitehasredirect'); - } else { - // We can't be sure if there is a redirect or not. Display cannot connect error. - error.error = Translate.instance.instant('core.cannotconnecttrouble'); - } - } - - return Promise.reject(new CoreSiteError({ - message: error.error, - errorcode: error.errorcode, - critical: true, - })); - } - - return data; + throw new CoreSiteError({ + message, }); } - return data; - }, (error: CoreError) => - // Local mobile check returned an error. This only happens if the plugin is installed and it returns an error. - Promise.reject(new CoreSiteError({ - message: error.message, - critical: true, - })), - ).then((data: LocalMobileResponse) => { - siteUrl = temporarySite.getURL(); + // Everything ok. + if (data.code === 0) { + data.code = config.typeoflogin; + } + } catch (error) { + // Error, check if not supported. + if (error.available === 1) { + // Service supported but an error happened. Return error. + if (error.errorcode == 'codingerror') { + // This could be caused by a redirect. Check if it's the case. + const redirect = await CoreUtils.instance.checkRedirect(siteUrl); - return { siteUrl, code: data.code, warning: data.warning, service: data.service, config: publicConfig }; - }); - }); + if (redirect) { + error.error = Translate.instance.instant('core.login.sitehasredirect'); + } else { + // We can't be sure if there is a redirect or not. Display cannot connect error. + error.error = Translate.instance.instant('core.cannotconnecttrouble'); + } + } + + throw new CoreSiteError({ + message: error.error, + errorcode: error.errorcode, + critical: true, + }); + } + } + } + + siteUrl = temporarySite.getURL(); + + return { siteUrl, code: data.code, warning: data.warning, service: data.service, config: publicConfig }; } /** @@ -482,7 +489,7 @@ export class CoreSitesProvider { if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) { throw new CoreSiteError({ errorcode: data.errorcode, - message: data.error, + message: data.error!, }); } @@ -506,10 +513,15 @@ export class CoreSitesProvider { * @param retry Whether we are retrying with a prefixed URL. * @return A promise resolved when the token is retrieved. */ - getUserToken(siteUrl: string, username: string, password: string, service?: string, retry?: boolean): - Promise { + async getUserToken( + siteUrl: string, + username: string, + password: string, + service?: string, + retry?: boolean, + ): Promise { if (!CoreApp.instance.isOnline()) { - return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); + throw new CoreError(Translate.instance.instant('core.networkerrormsg')); } if (!service) { @@ -522,47 +534,46 @@ export class CoreSitesProvider { service, }; const loginUrl = siteUrl + '/login/token.php'; - const promise = Http.instance.post(loginUrl, params).pipe(timeout(CoreWS.instance.getRequestTimeout())).toPromise(); + let data: CoreSitesLoginTokenResponse; - return promise.then((data: CoreSitesLoginTokenResponse) => { - if (typeof data == 'undefined') { - return Promise.reject(new CoreError(Translate.instance.instant('core.cannotconnecttrouble'))); + try { + data = await Http.instance.post(loginUrl, params).pipe(timeout(CoreWS.instance.getRequestTimeout())).toPromise(); + } catch (error) { + throw new CoreError(Translate.instance.instant('core.cannotconnecttrouble')); + } + + if (typeof data == 'undefined') { + throw new CoreError(Translate.instance.instant('core.cannotconnecttrouble')); + } else { + if (typeof data.token != 'undefined') { + return { token: data.token, siteUrl, privateToken: data.privatetoken }; } else { - if (typeof data.token != 'undefined') { - return { token: data.token, siteUrl, privateToken: data.privatetoken }; - } else { - if (typeof data.error != 'undefined') { - // We only allow one retry (to avoid loops). - if (!retry && data.errorcode == 'requirecorrectaccess') { - siteUrl = CoreUrlUtils.instance.addOrRemoveWWW(siteUrl); + if (typeof data.error != 'undefined') { + // We only allow one retry (to avoid loops). + if (!retry && data.errorcode == 'requirecorrectaccess') { + siteUrl = CoreUrlUtils.instance.addOrRemoveWWW(siteUrl); - return this.getUserToken(siteUrl, username, password, service, true); - } else if (data.errorcode == 'missingparam') { - // It seems the server didn't receive all required params, it could be due to a redirect. - return CoreUtils.instance.checkRedirect(loginUrl).then((redirect) => { - if (redirect) { - return Promise.reject(new CoreSiteError({ - message: Translate.instance.instant('core.login.sitehasredirect'), - })); - } else { - return Promise.reject(new CoreSiteError({ - message: data.error, - errorcode: data.errorcode, - })); - } + return this.getUserToken(siteUrl, username, password, service, true); + } else if (data.errorcode == 'missingparam') { + // It seems the server didn't receive all required params, it could be due to a redirect. + const redirect = await CoreUtils.instance.checkRedirect(loginUrl); + + if (redirect) { + throw new CoreSiteError({ + message: Translate.instance.instant('core.login.sitehasredirect'), }); - } else { - return Promise.reject(new CoreSiteError({ - message: data.error, - errorcode: data.errorcode, - })); } - } else { - return Promise.reject(new CoreError(Translate.instance.instant('core.login.invalidaccount'))); } + + throw new CoreSiteError({ + message: data.error, + errorcode: data.errorcode, + }); } + + throw new CoreError(Translate.instance.instant('core.login.invalidaccount')); } - }, () => Promise.reject(new CoreError(Translate.instance.instant('core.cannotconnecttrouble')))); + } } /** @@ -575,7 +586,13 @@ export class CoreSitesProvider { * @param oauthId OAuth ID. Only if the authentication was using an OAuth method. * @return A promise resolved with siteId when the site is added and the user is authenticated. */ - newSite(siteUrl: string, token: string, privateToken: string = '', login: boolean = true, oauthId?: number): Promise { + async newSite( + siteUrl: string, + token: string, + privateToken: string = '', + login: boolean = true, + oauthId?: number, + ): Promise { if (typeof login != 'boolean') { login = true; } @@ -584,74 +601,77 @@ export class CoreSitesProvider { let candidateSite = new CoreSite(undefined, siteUrl, token, undefined, privateToken, undefined, undefined); let isNewSite = true; - return candidateSite.fetchSiteInfo().then((info) => { + try { + const info = await candidateSite.fetchSiteInfo(); + const result = this.isValidMoodleVersion(info); - if (result == this.VALID_VERSION) { - const siteId = this.createSiteID(info.siteurl, info.username); - - // Check if the site already exists. - return this.getSite(siteId).catch(() => { - // Not exists. - }).then((site) => { - if (site) { - // Site already exists, update its data and use it. - isNewSite = false; - candidateSite = site; - candidateSite.setToken(token); - candidateSite.setPrivateToken(privateToken); - candidateSite.setInfo(info); - candidateSite.setOAuthId(oauthId); - candidateSite.setLoggedOut(false); - } else { - // New site, set site ID and info. - isNewSite = true; - candidateSite.setId(siteId); - candidateSite.setInfo(info); - candidateSite.setOAuthId(oauthId); - - // Create database tables before login and before any WS call. - return this.migrateSiteSchemas(candidateSite); - } - }).then(() => - - // Try to get the site config. - this.getSiteConfig(candidateSite).catch((error) => { - // Ignore errors if it's not a new site, we'll use the config already stored. - if (isNewSite) { - return Promise.reject(error); - } - }).then((config) => { - if (typeof config != 'undefined') { - candidateSite.setConfig(config); - } - - // Add site to sites list. - this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId); - this.sites[siteId] = candidateSite; - - if (login) { - // Turn candidate site into current site. - this.currentSite = candidateSite; - // Store session. - this.login(siteId); - } - - CoreEvents.instance.trigger(CoreEventsProvider.SITE_ADDED, info, siteId); - - return siteId; - }), - ); + if (result != this.VALID_VERSION) { + return this.treatInvalidAppVersion(result, siteUrl); } - return this.treatInvalidAppVersion(result, siteUrl); - }).catch((error) => { + const siteId = this.createSiteID(info.siteurl, info.username); + + // Check if the site already exists. + const site = await CoreUtils.instance.ignoreErrors(this.getSite(siteId)); + + if (site) { + // Site already exists, update its data and use it. + isNewSite = false; + candidateSite = site; + candidateSite.setToken(token); + candidateSite.setPrivateToken(privateToken); + candidateSite.setInfo(info); + candidateSite.setOAuthId(oauthId); + candidateSite.setLoggedOut(false); + } else { + // New site, set site ID and info. + isNewSite = true; + candidateSite.setId(siteId); + candidateSite.setInfo(info); + candidateSite.setOAuthId(oauthId); + + // Create database tables before login and before any WS call. + await this.migrateSiteSchemas(candidateSite); + } + + // Try to get the site config. + let config: CoreSiteConfig | undefined; + + try { + config = await this.getSiteConfig(candidateSite); + } catch (error) { + // Ignore errors if it's not a new site, we'll use the config already stored. + if (isNewSite) { + throw error; + } + } + + if (typeof config != 'undefined') { + candidateSite.setConfig(config); + } + + // Add site to sites list. + this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId); + this.sites[siteId] = candidateSite; + + if (login) { + // Turn candidate site into current site. + this.currentSite = candidateSite; + // Store session. + this.login(siteId); + } + + CoreEvents.instance.trigger(CoreEventsProvider.SITE_ADDED, info, siteId); + + return siteId; + } catch (error) { // Error invaliddevice is returned by Workplace server meaning the same as connecttoworkplaceapp. if (error && error.errorcode == 'invaliddevice') { return this.treatInvalidAppVersion(this.WORKPLACE_APP, siteUrl); } - return Promise.reject(error); - }); + throw error; + } } /** @@ -663,8 +683,8 @@ export class CoreSitesProvider { * @return A promise rejected with the error info. */ protected async treatInvalidAppVersion(result: number, siteUrl: string, siteId?: string): Promise { - let errorCode; - let errorKey; + let errorCode: string | undefined; + let errorKey: string | undefined; let translateParams; switch (result) { @@ -816,8 +836,15 @@ export class CoreSitesProvider { * @param oauthId OAuth ID. Only if the authentication was using an OAuth method. * @return Promise resolved when done. */ - async addSite(id: string, siteUrl: string, token: string, info: CoreSiteInfoResponse, privateToken: string = '', - config?: CoreSiteConfig, oauthId?: number): Promise { + async addSite( + id: string, + siteUrl: string, + token: string, + info: CoreSiteInfoResponse, + privateToken: string = '', + config?: CoreSiteConfig, + oauthId?: number, + ): Promise { await this.dbReady; const entry = { @@ -850,47 +877,55 @@ export class CoreSitesProvider { * @param siteId ID of the site to check. Current site id will be used otherwise. * @return Resolved with if meets the requirements, rejected otherwise. */ - async checkRequiredMinimumVersion(config: CoreSitePublicConfigResponse, siteId?: string): Promise { - if (config && config.tool_mobile_minimumversion) { - const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion); - const appVersion = this.convertVersionName(CoreConfigConstants.versionname); + async checkRequiredMinimumVersion(config?: CoreSitePublicConfigResponse, siteId?: string): Promise { + if (!config || !config.tool_mobile_minimumversion) { + return; + } - if (requiredVersion > appVersion) { - const storesConfig: CoreStoreConfig = { - android: config.tool_mobile_androidappid || null, - ios: config.tool_mobile_iosappid || null, - desktop: config.tool_mobile_setuplink || 'https://download.moodle.org/desktop/', - mobile: config.tool_mobile_setuplink || 'https://download.moodle.org/mobile/', - default: config.tool_mobile_setuplink, - }; + const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion); + const appVersion = this.convertVersionName(CoreConfigConstants.versionname); - const downloadUrl = CoreApp.instance.getAppStoreUrl(storesConfig); + if (requiredVersion > appVersion) { + const storesConfig: CoreStoreConfig = { + android: config.tool_mobile_androidappid, + ios: config.tool_mobile_iosappid, + desktop: config.tool_mobile_setuplink || 'https://download.moodle.org/desktop/', + mobile: config.tool_mobile_setuplink || 'https://download.moodle.org/mobile/', + default: config.tool_mobile_setuplink, + }; - siteId = siteId || this.getCurrentSiteId(); + siteId = siteId || this.getCurrentSiteId(); + const downloadUrl = CoreApp.instance.getAppStoreUrl(storesConfig); + + if (downloadUrl != null) { // Do not block interface. CoreDomUtils.instance.showConfirm( Translate.instance.instant('core.updaterequireddesc', { $a: config.tool_mobile_minimumversion }), Translate.instance.instant('core.updaterequired'), Translate.instance.instant('core.download'), - Translate.instance.instant(siteId ? 'core.mainmenu.logout' : 'core.cancel')).then(() => { - CoreUtils.instance.openInBrowser(downloadUrl); - }).catch(() => { + Translate.instance.instant(siteId ? 'core.mainmenu.logout' : 'core.cancel'), + ).then(() => CoreUtils.instance.openInBrowser(downloadUrl)).catch(() => { // Do nothing. }); + } else { + CoreDomUtils.instance.showAlert( + Translate.instance.instant('core.updaterequired'), + Translate.instance.instant('core.updaterequireddesc', { $a: config.tool_mobile_minimumversion }), + ); + } - if (siteId) { - // Logout if it's the currentSite. - if (siteId == this.getCurrentSiteId()) { - await this.logout(); - } - - // Always expire the token. - await this.setSiteLoggedOut(siteId, true); + if (siteId) { + // Logout if it's the currentSite. + if (siteId == this.getCurrentSiteId()) { + await this.logout(); } - throw new CoreError('Current app version is lower than required version.'); + // Always expire the token. + await this.setSiteLoggedOut(siteId, true); } + + throw new CoreError('Current app version is lower than required version.'); } } @@ -952,7 +987,7 @@ export class CoreSitesProvider { return false; } catch (error) { - let config: CoreSitePublicConfigResponse; + let config: CoreSitePublicConfigResponse | undefined; try { config = await site.getPublicConfig(); @@ -979,7 +1014,7 @@ export class CoreSitesProvider { * * @return Current site. */ - getCurrentSite(): CoreSite { + getCurrentSite(): CoreSite | undefined { return this.currentSite; } @@ -1015,11 +1050,7 @@ export class CoreSitesProvider { * @return Current site User ID. */ getCurrentSiteUserId(): number { - if (this.currentSite) { - return this.currentSite.getUserId(); - } else { - return 0; - } + return this.currentSite?.getUserId() || 0; } /** @@ -1150,7 +1181,7 @@ export class CoreSitesProvider { * @param siteId The site ID. If not defined, current site (if available). * @return Promise resolved with the database. */ - getSiteDb(siteId: string): Promise { + getSiteDb(siteId?: string): Promise { return this.getSite(siteId).then((site) => site.getDb()); } @@ -1175,7 +1206,7 @@ export class CoreSitesProvider { const sites = await this.appDB.getAllRecords(SITES_TABLE); - const formattedSites = []; + const formattedSites: CoreSiteBasicInfo[] = []; sites.forEach((site) => { if (!ids || ids.indexOf(site.id) > -1) { // Parse info. @@ -1184,7 +1215,7 @@ export class CoreSitesProvider { id: site.id, siteUrl: site.siteUrl, fullName: siteInfo?.fullname, - siteName: CoreConfigConstants.sitename ? CoreConfigConstants.sitename : siteInfo?.sitename, + siteName: CoreConfigConstants.sitename ?? siteInfo?.sitename, avatar: siteInfo?.userpictureurl, siteHomeId: siteInfo?.siteid || 1, }; @@ -1206,19 +1237,23 @@ export class CoreSitesProvider { // Sort sites by url and ful lname. sites.sort((a, b) => { // First compare by site url without the protocol. - let compareA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); - let compareB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); - const compare = compareA.localeCompare(compareB); + const urlA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); + const urlB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); + const compare = urlA.localeCompare(urlB); if (compare !== 0) { return compare; } // If site url is the same, use fullname instead. - compareA = a.fullName.toLowerCase().trim(); - compareB = b.fullName.toLowerCase().trim(); + const fullNameA = a.fullName?.toLowerCase().trim(); + const fullNameB = b.fullName?.toLowerCase().trim(); - return compareA.localeCompare(compareB); + if (!fullNameA || !fullNameB) { + return 0; + } + + return fullNameA.localeCompare(fullNameB); }); return sites; @@ -1279,10 +1314,10 @@ export class CoreSitesProvider { await this.dbReady; let siteId; - const promises = []; + const promises: Promise[] = []; if (this.currentSite) { - const siteConfig = this.currentSite.getStoredConfig(); + const siteConfig = this.currentSite.getStoredConfig(); siteId = this.currentSite.getId(); this.currentSite = undefined; @@ -1418,7 +1453,7 @@ export class CoreSitesProvider { } // Try to get the site config. - let config; + let config: CoreSiteConfig | undefined; try { config = await this.getSiteConfig(site); @@ -1426,10 +1461,9 @@ export class CoreSitesProvider { // Error getting config, keep the current one. } - const newValues = { + const newValues: Record = { info: JSON.stringify(info), loggedOut: site.isLoggedOut() ? 1 : 0, - config: undefined, }; if (typeof config != 'undefined') { @@ -1475,7 +1509,7 @@ export class CoreSitesProvider { // If prioritize is true, check current site first. if (prioritize && this.currentSite && this.currentSite.containsUrl(url)) { - if (!username || this.currentSite.getInfo().username == username) { + if (!username || this.currentSite?.getInfo()?.username == username) { return [this.currentSite.getId()]; } } @@ -1498,8 +1532,8 @@ export class CoreSitesProvider { try { const siteEntries = await this.appDB.getAllRecords(SITES_TABLE); - const ids = []; - const promises = []; + const ids: string[] = []; + const promises: Promise[] = []; siteEntries.forEach((site) => { if (!this.sites[site.id]) { @@ -1507,7 +1541,7 @@ export class CoreSitesProvider { } if (this.sites[site.id].containsUrl(url)) { - if (!username || this.sites[site.id].getInfo().username == username) { + if (!username || this.sites[site.id].getInfo()?.username == username) { ids.push(site.id); } } @@ -1553,15 +1587,13 @@ export class CoreSitesProvider { * @param site The site to get the config. * @return Promise resolved with config if available. */ - protected async getSiteConfig(site: CoreSite): Promise { + protected async getSiteConfig(site: CoreSite): Promise { if (!site.wsAvailable('tool_mobile_get_config')) { // WS not available, cannot get config. return; } - const config = await site.getConfig(undefined, true); - - return config; + return await site.getConfig(undefined, true); } /** @@ -1611,7 +1643,7 @@ export class CoreSitesProvider { wsAvailableInCurrentSite(method: string, checkPrefix: boolean = true): boolean { const site = this.getCurrentSite(); - return site && site.wsAvailable(method, checkPrefix); + return site ? site.wsAvailable(method, checkPrefix) : false; } /** @@ -1659,6 +1691,10 @@ export class CoreSitesProvider { * @return Promise resolved when done. */ migrateSiteSchemas(site: CoreSite): Promise { + if (!site.id) { + return Promise.resolve(); + } + if (this.siteSchemasMigration[site.id]) { return this.siteSchemasMigration[site.id]; } @@ -1672,7 +1708,7 @@ export class CoreSitesProvider { this.siteSchemasMigration[site.id] = promise; return promise.finally(() => { - delete this.siteSchemasMigration[site.id]; + delete this.siteSchemasMigration[site.id!]; }); } @@ -1694,7 +1730,7 @@ export class CoreSitesProvider { versions[record.name] = record.version; }); - const promises = []; + const promises: Promise[] = []; for (const name in schemas) { const schema = schemas[name]; const oldVersion = versions[name] || 0; @@ -1720,6 +1756,10 @@ export class CoreSitesProvider { * @return Promise resolved when done. */ protected async applySiteSchema(site: CoreSite, schema: CoreRegisteredSiteSchema, oldVersion: number): Promise { + if (!site.id) { + return; + } + const db = site.getDb(); if (schema.tables) { @@ -1741,31 +1781,31 @@ export class CoreSitesProvider { * @return Promise resolved with site to use and the list of sites that have * the URL. Site will be undefined if it isn't the root URL of any stored site. */ - isStoredRootURL(url: string, username?: string): Promise<{site: CoreSite; siteIds: string[]}> { + async isStoredRootURL(url: string, username?: string): Promise<{site?: CoreSite; siteIds: string[]}> { // Check if the site is stored. - return this.getSiteIdsFromUrl(url, true, username).then((siteIds) => { - const result = { - siteIds, - site: undefined, - }; + const siteIds = await this.getSiteIdsFromUrl(url, true, username); - if (siteIds.length > 0) { - // If more than one site is returned it usually means there are different users stored. Use any of them. - return this.getSite(siteIds[0]).then((site) => { - const siteUrl = CoreTextUtils.instance.removeEndingSlash( - CoreUrlUtils.instance.removeProtocolAndWWW(site.getURL())); - const treatedUrl = CoreTextUtils.instance.removeEndingSlash(CoreUrlUtils.instance.removeProtocolAndWWW(url)); - - if (siteUrl == treatedUrl) { - result.site = site; - } - - return result; - }); - } + const result: {site?: CoreSite; siteIds: string[]} = { + siteIds, + }; + if (!siteIds.length) { return result; - }); + } + + // If more than one site is returned it usually means there are different users stored. Use any of them. + const site = await this.getSite(siteIds[0]); + + const siteUrl = CoreTextUtils.instance.removeEndingSlash( + CoreUrlUtils.instance.removeProtocolAndWWW(site.getURL()), + ); + const treatedUrl = CoreTextUtils.instance.removeEndingSlash(CoreUrlUtils.instance.removeProtocolAndWWW(url)); + + if (siteUrl == treatedUrl) { + result.site = site; + } + + return result; } /** @@ -1775,12 +1815,12 @@ export class CoreSitesProvider { * @return Name of the site schemas. */ getSiteTableSchemasToClear(site: CoreSite): string[] { - let reset = []; + let reset: string[] = []; for (const name in this.siteSchemas) { const schema = this.siteSchemas[name]; if (schema.canBeCleared && (!schema.siteId || site.getId() == schema.siteId)) { - reset = reset.concat(this.siteSchemas[name].canBeCleared); + reset = reset.concat(schema.canBeCleared); } } @@ -1900,17 +1940,17 @@ export type CoreSiteBasicInfo = { /** * User's full name. */ - fullName: string; + fullName?: string; /** * Site's name. */ - siteName: string; + siteName?: string; /** * User's avatar. */ - avatar: string; + avatar?: string; /** * Badge to display in the site. diff --git a/src/app/services/utils/dom.ts b/src/app/services/utils/dom.ts index 97b4e3472..b2e617bb6 100644 --- a/src/app/services/utils/dom.ts +++ b/src/app/services/utils/dom.ts @@ -30,6 +30,7 @@ import { CoreConstants } from '@core/constants'; import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreError } from '@classes/errors/error'; +import { CoreSilentError } from '@classes/errors/silenterror'; import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; @@ -40,14 +41,15 @@ import { CoreLogger } from '@singletons/logger'; @Injectable() export class CoreDomUtilsProvider { + protected readonly INSTANCE_ID_ATTR_NAME = 'core-instance-id'; + // List of input types that support keyboard. protected readonly INPUT_SUPPORT_KEYBOARD: string[] = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password', 'search', 'tel', 'text', 'time', 'url', 'week']; - protected readonly INSTANCE_ID_ATTR_NAME: string = 'core-instance-id'; protected template: HTMLTemplateElement = document.createElement('template'); // A template element to convert HTML to element. - protected matchesFn: string; // Name of the "matches" function to use when simulating a closest call. + protected matchesFunctionName?: string; // Name of the "matches" function to use when simulating a closest call. // eslint-disable-next-line @typescript-eslint/no-explicit-any protected instances: {[id: string]: any} = {}; // Store component/directive instances by id. protected lastInstanceId = 0; @@ -58,10 +60,17 @@ export class CoreDomUtilsProvider { constructor(protected domSanitizer: DomSanitizer) { this.logger = CoreLogger.getInstance('CoreDomUtilsProvider'); + this.init(); + } + + /** + * Init some properties. + */ + protected async init(): Promise { // Check if debug messages should be displayed. - CoreConfig.instance.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false).then((debugDisplay) => { - this.debugDisplay = !!debugDisplay; - }); + const debugDisplay = await CoreConfig.instance.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, 0); + + this.debugDisplay = debugDisplay != 0; } /** @@ -73,17 +82,21 @@ export class CoreDomUtilsProvider { * @param selector Selector to search. * @return Closest ancestor. */ - closest(element: Element, selector: string): Element { + closest(element: Element | undefined | null, selector: string): Element | null { + if (!element) { + return null; + } + // Try to use closest if the browser supports it. if (typeof element.closest == 'function') { return element.closest(selector); } - if (!this.matchesFn) { + if (!this.matchesFunctionName) { // Find the matches function supported by the browser. ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector'].some((fn) => { if (typeof document.body[fn] == 'function') { - this.matchesFn = fn; + this.matchesFunctionName = fn; return true; } @@ -91,18 +104,22 @@ export class CoreDomUtilsProvider { return false; }); - if (!this.matchesFn) { - return; + if (!this.matchesFunctionName) { + return null; } } // Traverse parents. - while (element) { - if (element[this.matchesFn](selector)) { - return element; + let elementToTreat: Element | null = element; + + while (elementToTreat) { + if (elementToTreat[this.matchesFunctionName](selector)) { + return elementToTreat; } - element = element.parentElement; + elementToTreat = elementToTreat.parentElement; } + + return null; } /** @@ -116,7 +133,7 @@ export class CoreDomUtilsProvider { * @param alwaysConfirm True to show a confirm even if the size isn't high, false otherwise. * @return Promise resolved when the user confirms or if no confirm needed. */ - confirmDownloadSize( + async confirmDownloadSize( size: {size: number; total: boolean}, message?: string, unknownMessage?: string, @@ -126,73 +143,88 @@ export class CoreDomUtilsProvider { ): Promise { const readableSize = CoreTextUtils.instance.bytesToSize(size.size, 2); - const getAvailableBytes = new Promise((resolve): void => { + const getAvailableBytes = async (): Promise => { if (CoreApp.instance.isDesktop()) { // Free space calculation is not supported on desktop. - resolve(null); - } else { - CoreFile.instance.calculateFreeSpace().then((availableBytes) => { - if (CoreApp.instance.isAndroid()) { - return availableBytes; - } else { - // Space calculation is not accurate on iOS, but it gets more accurate when space is lower. - // We'll only use it when space is <500MB, or we're downloading more than twice the reported space. - if (availableBytes < CoreConstants.IOS_FREE_SPACE_THRESHOLD || size.size > availableBytes / 2) { - return availableBytes; - } else { - return null; - } - } - }).then((availableBytes) => { - resolve(availableBytes); - }); + return null; } - }); - const getAvailableSpace = getAvailableBytes.then((availableBytes: number) => { + const availableBytes = await CoreFile.instance.calculateFreeSpace(); + + if (CoreApp.instance.isAndroid()) { + return availableBytes; + } else { + // Space calculation is not accurate on iOS, but it gets more accurate when space is lower. + // We'll only use it when space is <500MB, or we're downloading more than twice the reported space. + if (availableBytes < CoreConstants.IOS_FREE_SPACE_THRESHOLD || size.size > availableBytes / 2) { + return availableBytes; + } else { + return null; + } + } + }; + + const getAvailableSpace = (availableBytes: number | null): string => { if (availableBytes === null) { return ''; } else { const availableSize = CoreTextUtils.instance.bytesToSize(availableBytes, 2); + if (CoreApp.instance.isAndroid() && size.size > availableBytes - CoreConstants.MINIMUM_FREE_SPACE) { - return Promise.reject(new CoreError(Translate.instance.instant('core.course.insufficientavailablespace', - { size: readableSize }))); + throw new CoreError( + Translate.instance.instant( + 'core.course.insufficientavailablespace', + { size: readableSize }, + ), + ); } return Translate.instance.instant('core.course.availablespace', { available: availableSize }); } - }); + }; - return getAvailableSpace.then((availableSpace) => { - wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold; - limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold; + const availableBytes = await getAvailableBytes(); - let wifiPrefix = ''; - if (CoreApp.instance.isNetworkAccessLimited()) { - wifiPrefix = Translate.instance.instant('core.course.confirmlimiteddownload'); - } + const availableSpace = getAvailableSpace(availableBytes); - if (size.size < 0 || (size.size == 0 && !size.total)) { - // Seems size was unable to be calculated. Show a warning. - unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize'; + wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold; + limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold; - return this.showConfirm(wifiPrefix + Translate.instance.instant( - unknownMessage, { availableSpace: availableSpace })); - } else if (!size.total) { - // Filesize is only partial. + let wifiPrefix = ''; + if (CoreApp.instance.isNetworkAccessLimited()) { + wifiPrefix = Translate.instance.instant('core.course.confirmlimiteddownload'); + } - return this.showConfirm(wifiPrefix + Translate.instance.instant('core.course.confirmpartialdownloadsize', - { size: readableSize, availableSpace: availableSpace })); - } else if (alwaysConfirm || size.size >= wifiThreshold || + if (size.size < 0 || (size.size == 0 && !size.total)) { + // Seems size was unable to be calculated. Show a warning. + unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize'; + + return this.showConfirm( + wifiPrefix + Translate.instance.instant( + unknownMessage, + { availableSpace: availableSpace }, + ), + ); + } else if (!size.total) { + // Filesize is only partial. + + return this.showConfirm( + wifiPrefix + Translate.instance.instant( + 'core.course.confirmpartialdownloadsize', + { size: readableSize, availableSpace: availableSpace }, + ), + ); + } else if (alwaysConfirm || size.size >= wifiThreshold || (CoreApp.instance.isNetworkAccessLimited() && size.size >= limitedThreshold)) { - message = message || (size.size === 0 ? 'core.course.confirmdownloadzerosize' : 'core.course.confirmdownload'); + message = message || (size.size === 0 ? 'core.course.confirmdownloadzerosize' : 'core.course.confirmdownload'); - return this.showConfirm(wifiPrefix + Translate.instance.instant(message, - { size: readableSize, availableSpace: availableSpace })); - } - - return Promise.resolve(); - }); + return this.showConfirm( + wifiPrefix + Translate.instance.instant( + message, + { size: readableSize, availableSpace: availableSpace }, + ), + ); + } } /** @@ -255,11 +287,10 @@ export class CoreDomUtilsProvider { this.logger.error('The function extractDownloadableFilesFromHtml has been moved to CoreFilepoolProvider.' + ' Please use that function instead of this one.'); - const urls = []; + const urls: string[] = []; const element = this.convertToElement(html); - const elements: (HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | - HTMLTrackElement)[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track')); + const elements: AnchorOrMediaElement[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track')); for (let i = 0; i < elements.length; i++) { const element = elements[i]; @@ -271,7 +302,7 @@ export class CoreDomUtilsProvider { // Treat video poster. if (element.tagName == 'VIDEO' && element.getAttribute('poster')) { - url = element.getAttribute('poster'); + url = element.getAttribute('poster') || ''; if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) { urls.push(url); } @@ -305,7 +336,7 @@ export class CoreDomUtilsProvider { */ extractUrlsFromCSS(code: string): string[] { // First of all, search all the url(...) occurrences that don't include "data:". - const urls = []; + const urls: string[] = []; const matches = code.match(/url\(\s*["']?(?!data:)([^)]+)\)/igm); if (!matches) { @@ -394,7 +425,7 @@ export class CoreDomUtilsProvider { * @param selector Selector to search. * @return Selection contents. Undefined if not found. */ - getContentsOfElement(element: HTMLElement, selector: string): string { + getContentsOfElement(element: HTMLElement, selector: string): string | undefined { if (element) { const selected = element.querySelector(selector); if (selected) { @@ -447,7 +478,7 @@ export class CoreDomUtilsProvider { * @param attribute Attribute to get. * @return Attribute value. */ - getHTMLElementAttribute(html: string, attribute: string): string { + getHTMLElementAttribute(html: string, attribute: string): string | null { return this.convertToElement(html).children[0].getAttribute(attribute); } @@ -584,8 +615,8 @@ export class CoreDomUtilsProvider { * @param positionParentClass Parent Class where to stop calculating the position. Default inner-scroll. * @return positionLeft, positionTop of the element relative to. */ - getElementXY(container: HTMLElement, selector?: string, positionParentClass?: string): number[] { - let element: HTMLElement = (selector ? container.querySelector(selector) : container); + getElementXY(container: HTMLElement, selector?: string, positionParentClass?: string): number[] | null { + let element: HTMLElement | null = (selector ? container.querySelector(selector) : container); let positionTop = 0; let positionLeft = 0; @@ -645,9 +676,9 @@ export class CoreDomUtilsProvider { * @param needsTranslate Whether the error needs to be translated. * @return Error message, null if no error should be displayed. */ - getErrorMessage(error: CoreError | CoreTextErrorObject | string, needsTranslate?: boolean): string { + getErrorMessage(error: CoreError | CoreTextErrorObject | string, needsTranslate?: boolean): string | null { let extraInfo = ''; - let errorMessage: string; + let errorMessage: string | undefined; if (typeof error == 'object') { if (this.debugDisplay) { @@ -657,19 +688,21 @@ export class CoreDomUtilsProvider { } if ('backtrace' in error && error.backtrace) { extraInfo += '

' + CoreTextUtils.instance.replaceNewLines( - CoreTextUtils.instance.escapeHTML(error.backtrace, false), '
'); + CoreTextUtils.instance.escapeHTML(error.backtrace, false), + '
', + ); } // eslint-disable-next-line no-console console.error(error); } - // We received an object instead of a string. Search for common properties. - if (this.isCanceledError(error)) { - // It's a canceled error, don't display an error. + if (this.isSilentError(error)) { + // It's a silent error, don't display an error. return null; } + // We received an object instead of a string. Search for common properties. errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error); if (!errorMessage) { // No common properties found, just stringify it. @@ -712,7 +745,7 @@ export class CoreDomUtilsProvider { getInstanceByElement(element: Element): any { const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME); - return this.instances[id]; + return id && this.instances[id]; } /** @@ -725,6 +758,16 @@ export class CoreDomUtilsProvider { return error instanceof CoreCanceledError; } + /** + * Check whether an error is an error caused because the user canceled a showConfirm. + * + * @param error Error to check. + * @return Whether it's a canceled error. + */ + isSilentError(error: CoreError | CoreTextErrorObject | string): boolean { + return error instanceof CoreSilentError; + } + /** * Wait an element to exists using the findFunction. * @@ -898,7 +941,7 @@ export class CoreDomUtilsProvider { */ removeInstanceByElement(element: Element): void { const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME); - delete this.instances[id]; + id && delete this.instances[id]; } /** @@ -946,7 +989,8 @@ export class CoreDomUtilsProvider { // Treat elements with src (img, audio, video, ...). const media = Array.from(element.querySelectorAll('img, video, audio, source, track')); media.forEach((media: HTMLElement) => { - let newSrc = paths[CoreTextUtils.instance.decodeURIComponent(media.getAttribute('src'))]; + const currentSrc = media.getAttribute('src'); + const newSrc = currentSrc ? paths[CoreTextUtils.instance.decodeURIComponent(currentSrc)] : undefined; if (typeof newSrc != 'undefined') { media.setAttribute('src', newSrc); @@ -954,9 +998,10 @@ export class CoreDomUtilsProvider { // Treat video posters. if (media.tagName == 'VIDEO' && media.getAttribute('poster')) { - newSrc = paths[CoreTextUtils.instance.decodeURIComponent(media.getAttribute('poster'))]; - if (typeof newSrc !== 'undefined') { - media.setAttribute('poster', newSrc); + const currentPoster = media.getAttribute('poster'); + const newPoster = paths[CoreTextUtils.instance.decodeURIComponent(currentPoster!)]; + if (typeof newPoster !== 'undefined') { + media.setAttribute('poster', newPoster); } } }); @@ -964,14 +1009,14 @@ export class CoreDomUtilsProvider { // Now treat links. const anchors = Array.from(element.querySelectorAll('a')); anchors.forEach((anchor: HTMLElement) => { - const href = CoreTextUtils.instance.decodeURIComponent(anchor.getAttribute('href')); - const newUrl = paths[href]; + const currentHref = anchor.getAttribute('href'); + const newHref = currentHref ? paths[CoreTextUtils.instance.decodeURIComponent(currentHref)] : undefined; - if (typeof newUrl != 'undefined') { - anchor.setAttribute('href', newUrl); + if (typeof newHref != 'undefined') { + anchor.setAttribute('href', newHref); if (typeof anchorFn == 'function') { - anchorFn(anchor, href); + anchorFn(anchor, newHref); } } }); @@ -990,7 +1035,7 @@ export class CoreDomUtilsProvider { * @deprecated since 3.9.5. Use directly the IonContent class. */ scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise { - return content?.scrollByPoint(x, y, duration); + return content?.scrollByPoint(x, y, duration || 0); } /** @@ -1080,7 +1125,7 @@ export class CoreDomUtilsProvider { return false; } - content?.scrollByPoint(position[0], position[1], duration); + content?.scrollByPoint(position[0], position[1], duration || 0); return true; } @@ -1108,7 +1153,7 @@ export class CoreDomUtilsProvider { return false; } - content?.scrollByPoint(position[0], position[1], duration); + content?.scrollByPoint(position[0], position[1], duration || 0); return true; } catch (error) { @@ -1186,31 +1231,36 @@ export class CoreDomUtilsProvider { const alert = await AlertController.instance.create(options); + // eslint-disable-next-line promise/catch-or-return alert.present().then(() => { if (hasHTMLTags) { // Treat all anchors so they don't override the app. - const alertMessageEl: HTMLElement = alert.querySelector('.alert-message'); - this.treatAnchors(alertMessageEl); + const alertMessageEl: HTMLElement | null = alert.querySelector('.alert-message'); + alertMessageEl && this.treatAnchors(alertMessageEl); } + + return; }); // Store the alert and remove it when dismissed. this.displayedAlerts[alertId] = alert; // // Set the callbacks to trigger an observable event. + // eslint-disable-next-line promise/catch-or-return, promise/always-return alert.onDidDismiss().then(() => { delete this.displayedAlerts[alertId]; }); - if (autocloseTime > 0) { + if (autocloseTime && autocloseTime > 0) { setTimeout(async () => { await alert.dismiss(); if (options.buttons) { // Execute dismiss function if any. - const cancelButton = options.buttons.find((button) => typeof button != 'string' && - typeof button.handler != 'undefined' && button.role == 'cancel'); - cancelButton?.handler(null); + const cancelButton = options.buttons.find( + (button) => typeof button != 'string' && typeof button.handler != 'undefined' && button.role == 'cancel', + ); + cancelButton.handler?.(null); } }, autocloseTime); } @@ -1248,8 +1298,13 @@ export class CoreDomUtilsProvider { translateArgs: Record = {}, options?: AlertOptions, ): Promise { - return this.showConfirm(Translate.instance.instant(translateMessage, translateArgs), undefined, - Translate.instance.instant('core.delete'), undefined, options); + return this.showConfirm( + Translate.instance.instant(translateMessage, translateArgs), + undefined, + Translate.instance.instant('core.delete'), + undefined, + options, + ); } /** @@ -1306,7 +1361,7 @@ export class CoreDomUtilsProvider { ): Promise { if (this.isCanceledError(error)) { // It's a canceled error, don't display an error. - return null; + return Promise.resolve(null); } const message = this.getErrorMessage(error, needsTranslate); @@ -1339,7 +1394,7 @@ export class CoreDomUtilsProvider { return null; } - let errorMessage = error; + let errorMessage = error || undefined; if (error && typeof error != 'string') { errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error); @@ -1428,8 +1483,8 @@ export class CoreDomUtilsProvider { const isDevice = CoreApp.instance.isAndroid() || CoreApp.instance.isIOS(); if (!isDevice) { // Treat all anchors so they don't override the app. - const alertMessageEl: HTMLElement = alert.querySelector('.alert-message'); - this.treatAnchors(alertMessageEl); + const alertMessageEl: HTMLElement | null = alert.querySelector('.alert-message'); + alertMessageEl && this.treatAnchors(alertMessageEl); } } @@ -1448,8 +1503,7 @@ export class CoreDomUtilsProvider { header?: string, placeholder?: string, type: TextFieldTypes | 'checkbox' | 'radio' | 'textarea' = 'password', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise { + ): Promise { // eslint-disable-line @typescript-eslint/no-explicit-any return new Promise((resolve, reject) => { placeholder = placeholder ?? Translate.instance.instant('core.login.password'); @@ -1537,7 +1591,7 @@ export class CoreDomUtilsProvider { * @param instance The instance to store. * @return ID to identify the instance. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any storeInstanceByElement(element: Element, instance: any): string { const id = String(this.lastInstanceId++); @@ -1607,7 +1661,7 @@ export class CoreDomUtilsProvider { * @param componentId An ID to use in conjunction with the component. * @param fullScreen Whether the modal should be full screen. */ - viewImage(image: string, title?: string, component?: string, componentId?: string | number, fullScreen?: boolean): void { + viewImage(image: string, title?: string | null, component?: string, componentId?: string | number, fullScreen?: boolean): void { // @todo } @@ -1619,7 +1673,7 @@ export class CoreDomUtilsProvider { */ waitForImages(element: HTMLElement): Promise { const imgs = Array.from(element.querySelectorAll('img')); - const promises = []; + const promises: Promise[] = []; let hasImgToLoad = false; imgs.forEach((img) => { @@ -1651,7 +1705,7 @@ export class CoreDomUtilsProvider { */ wrapElement(el: HTMLElement, wrapper: HTMLElement): void { // Insert the wrapper before the element. - el.parentNode.insertBefore(wrapper, el); + el.parentNode?.insertBefore(wrapper, el); // Now move the element into the wrapper. wrapper.appendChild(el); } @@ -1680,7 +1734,7 @@ export class CoreDomUtilsProvider { * @param online Whether the action was done in offline or not. * @param siteId The site affected. If not provided, no site affected. */ - triggerFormSubmittedEvent(formRef: ElementRef, online?: boolean, siteId?: string): void { + triggerFormSubmittedEvent(formRef: ElementRef | undefined, online?: boolean, siteId?: string): void { if (!formRef) { return; } @@ -1695,3 +1749,6 @@ export class CoreDomUtilsProvider { } export class CoreDomUtils extends makeSingleton(CoreDomUtilsProvider) {} + +type AnchorOrMediaElement = + HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | HTMLTrackElement; diff --git a/src/app/services/utils/iframe.ts b/src/app/services/utils/iframe.ts index ed8f61b72..eae0e490b 100644 --- a/src/app/services/utils/iframe.ts +++ b/src/app/services/utils/iframe.ts @@ -423,7 +423,7 @@ export class CoreIframeUtilsProvider { if (!CoreSites.instance.isLoggedIn()) { CoreUtils.instance.openInBrowser(link.href); } else { - await CoreSites.instance.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(link.href); + await CoreSites.instance.getCurrentSite()!.openInBrowserWithAutoLoginIfSameSite(link.href); } } else if (link.target == '_parent' || link.target == '_top' || link.target == '_blank') { // Opening links with _parent, _top or _blank can break the app. We'll open it in InAppBrowser. diff --git a/src/app/services/utils/mimetype.ts b/src/app/services/utils/mimetype.ts index d6b0e0767..63414cbd8 100644 --- a/src/app/services/utils/mimetype.ts +++ b/src/app/services/utils/mimetype.ts @@ -401,7 +401,7 @@ export class CoreMimetypeUtilsProvider { * @param capitalise If true, capitalises first character of result. * @return Type description. */ - getMimetypeDescription(obj: FileEntry | { filename: string; mimetype: string } | string, capitalise?: boolean): string { + getMimetypeDescription(obj: FileEntry | CoreWSExternalFile | string, capitalise?: boolean): string { const langPrefix = 'assets.mimetypes.'; let filename: string | undefined = ''; let mimetype: string | undefined = ''; diff --git a/src/app/services/utils/text.ts b/src/app/services/utils/text.ts index dd202be38..2a3984044 100644 --- a/src/app/services/utils/text.ts +++ b/src/app/services/utils/text.ts @@ -417,7 +417,7 @@ export class CoreTextUtilsProvider { * @param doubleEncode If false, it will not convert existing html entities. Defaults to true. * @return Escaped text. */ - escapeHTML(text: string | number, doubleEncode: boolean = true): string { + escapeHTML(text?: string | number | null, doubleEncode: boolean = true): string { if (typeof text == 'undefined' || text === null || (typeof text == 'number' && isNaN(text))) { return ''; } else if (typeof text != 'string') { @@ -670,7 +670,7 @@ export class CoreTextUtilsProvider { * @param text Text to treat. * @return Treated text. */ - removeEndingSlash(text: string): string { + removeEndingSlash(text?: string): string { if (!text) { return ''; } diff --git a/src/app/services/ws.ts b/src/app/services/ws.ts index a97eccea7..b1952f63b 100644 --- a/src/app/services/ws.ts +++ b/src/app/services/ws.ts @@ -44,7 +44,7 @@ import { CoreAjaxWSError } from '@classes/errors/ajaxwserror'; export class CoreWSProvider { protected logger: CoreLogger; - protected mimeTypeCache: {[url: string]: string} = {}; // A "cache" to store file mimetypes to decrease HEAD requests. + protected mimeTypeCache: {[url: string]: string | null} = {}; // A "cache" to store file mimetypes to decrease HEAD requests. // eslint-disable-next-line @typescript-eslint/no-explicit-any protected ongoingCalls: {[queueItemId: string]: Promise} = {}; protected retryCalls: RetryCall[] = []; @@ -53,11 +53,18 @@ export class CoreWSProvider { constructor() { this.logger = CoreLogger.getInstance('CoreWSProvider'); - Platform.instance.ready().then(() => { - if (CoreApp.instance.isIOS()) { - NativeHttp.instance.setHeader('*', 'User-Agent', navigator.userAgent); - } - }); + this.init(); + } + + /** + * Initialize some data. + */ + protected async init(): Promise { + await Platform.instance.ready(); + + if (CoreApp.instance.isIOS()) { + NativeHttp.instance.setHeader('*', 'User-Agent', navigator.userAgent); + } } /** @@ -67,8 +74,7 @@ export class CoreWSProvider { * @param siteUrl Complete site url to perform the call. * @param ajaxData Arguments to pass to the method. * @param preSets Extra settings and information. - * @return Deferred promise resolved with the response data in success and rejected with the error message - * if it fails. + * @return Deferred promise resolved with the response data in success and rejected with the error if it fails. */ protected addToRetryQueue(method: string, siteUrl: string, data: unknown, preSets: CoreWSPreSets): Promise { const call = { @@ -94,9 +100,9 @@ export class CoreWSProvider { */ call(method: string, data: unknown, preSets: CoreWSPreSets): Promise { if (!preSets) { - return Promise.reject(new CoreError(Translate.instance.instant('core.unexpectederror'))); + throw new CoreError(Translate.instance.instant('core.unexpectederror')); } else if (!CoreApp.instance.isOnline()) { - return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); + throw new CoreError(Translate.instance.instant('core.networkerrormsg')); } preSets.typeExpected = preSets.typeExpected || 'object'; @@ -126,10 +132,7 @@ export class CoreWSProvider { * @param method The WebService method to be called. * @param data Arguments to pass to the method. * @param preSets Extra settings and information. Only some - * @return Promise resolved with the response data in success and rejected with an object containing: - * - error: Error message. - * - errorcode: Error code returned by the site (if any). - * - available: 0 if unknown, 1 if available, -1 if not available. + * @return Promise resolved with the response data in success and rejected with CoreAjaxError. */ callAjax(method: string, data: Record, preSets: CoreWSAjaxPreSets): Promise { const cacheParams = { @@ -155,7 +158,7 @@ export class CoreWSProvider { * @param stripUnicode If Unicode long chars need to be stripped. * @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 + // eslint-disable-next-line @typescript-eslint/no-explicit-any convertValuesToString(data: any, stripUnicode?: boolean): any { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = Array.isArray(data) ? [] : {}; @@ -232,8 +235,12 @@ export class CoreWSProvider { * @param onProgress Function to call on progress. * @return Promise resolved with the downloaded file. */ - async downloadFile(url: string, path: string, addExtension?: boolean, onProgress?: (event: ProgressEvent) => void): - Promise { + async downloadFile( + url: string, + path: string, + addExtension?: boolean, + onProgress?: (event: ProgressEvent) => void, + ): Promise { this.logger.debug('Downloading file', url, path, addExtension); if (!CoreApp.instance.isOnline()) { @@ -249,7 +256,7 @@ export class CoreWSProvider { const fileEntry = await CoreFile.instance.createFile(tmpPath); const transfer = FileTransfer.instance.create(); - transfer.onProgress(onProgress); + onProgress && transfer.onProgress(onProgress); // Download the file in the tmp file. await transfer.download(url, fileEntry.toURL(), true); @@ -257,7 +264,7 @@ export class CoreWSProvider { let extension = ''; if (addExtension) { - extension = CoreMimetypeUtils.instance.getFileExtension(path); + extension = CoreMimetypeUtils.instance.getFileExtension(path) || ''; // Google Drive extensions will be considered invalid since Moodle usually converts them. if (!extension || CoreArray.contains(['gdoc', 'gsheet', 'gslides', 'gdraw', 'php'], extension)) { @@ -281,14 +288,15 @@ export class CoreWSProvider { } // Move the file to the final location. - const movedEntry: CoreWSDownloadedFileEntry = await CoreFile.instance.moveFile(tmpPath, path); + const movedEntry = 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; + // Also return the extension and path. + return Object.assign(movedEntry, { + extension: extension, + path: path, + }); } catch (error) { this.logger.error(`Error downloading ${url} to ${path}`, error); @@ -303,7 +311,7 @@ export class CoreWSProvider { * @param url Base URL of the HTTP request. * @param params Params of the HTTP request. */ - protected getPromiseHttp(method: string, url: string, params?: Record): Promise { + protected getPromiseHttp(method: string, url: string, params?: Record): Promise | undefined { const queueItemId = this.getQueueItemId(method, url, params); if (typeof this.ongoingCalls[queueItemId] != 'undefined') { return this.ongoingCalls[queueItemId]; @@ -317,12 +325,14 @@ export class CoreWSProvider { * @param ignoreCache True to ignore cache, false otherwise. * @return Promise resolved with the mimetype or '' if failure. */ - getRemoteFileMimeType(url: string, ignoreCache?: boolean): Promise { + async getRemoteFileMimeType(url: string, ignoreCache?: boolean): Promise { if (this.mimeTypeCache[url] && !ignoreCache) { - return Promise.resolve(this.mimeTypeCache[url]); + return this.mimeTypeCache[url]!; } - return this.performHead(url).then((response) => { + try { + const response = await this.performHead(url); + let mimeType = response.headers.get('Content-Type'); if (mimeType) { // Remove "parameters" like charset. @@ -331,10 +341,10 @@ export class CoreWSProvider { this.mimeTypeCache[url] = mimeType; return mimeType || ''; - }).catch(() => + } catch (error) { // Error, resolve with empty mimetype. - '', - ); + return ''; + } } /** @@ -345,17 +355,15 @@ export class CoreWSProvider { */ getRemoteFileSize(url: string): Promise { return this.performHead(url).then((response) => { - const size = parseInt(response.headers.get('Content-Length'), 10); + const contentLength = response.headers.get('Content-Length'); + const size = contentLength ? parseInt(contentLength, 10) : 0; if (size) { return size; } return -1; - }).catch(() => - // Error, return -1. - -1, - ); + }).catch(() => -1); } /** @@ -389,19 +397,16 @@ export class CoreWSProvider { * @param method The WebService method to be called. * @param data Arguments to pass to the method. * @param preSets Extra settings and information. Only some - * @return Promise resolved with the response data in success and rejected with an object containing: - * - error: Error message. - * - errorcode: Error code returned by the site (if any). - * - available: 0 if unknown, 1 if available, -1 if not available. + * @return Promise resolved with the response data in success and rejected with CoreAjaxError. */ protected performAjax(method: string, data: Record, preSets: CoreWSAjaxPreSets): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any let promise: Promise>; if (typeof preSets.siteUrl == 'undefined') { - return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.unexpectederror'))); + throw new CoreAjaxError(Translate.instance.instant('core.unexpectederror')); } else if (!CoreApp.instance.isOnline()) { - return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.networkerrormsg'))); + throw new CoreAjaxError(Translate.instance.instant('core.networkerrormsg')); } if (typeof preSets.responseExpected == 'undefined') { @@ -446,23 +451,23 @@ export class CoreWSProvider { // Check if error. Ajax layer should always return an object (if error) or an array (if success). if (!data || typeof data != 'object') { - return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection'))); + throw new CoreAjaxError(Translate.instance.instant('core.serverconnection')); } else if (data.error) { - return Promise.reject(new CoreAjaxWSError(data)); + throw new CoreAjaxWSError(data); } // Get the first response since only one request was done. data = data[0]; if (data.error) { - return Promise.reject(new CoreAjaxWSError(data.exception)); + throw new CoreAjaxWSError(data.exception); } return data.data; }, (data) => { const available = data.status == 404 ? -1 : 0; - return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection'), available)); + throw new CoreAjaxError(Translate.instance.instant('core.serverconnection'), available); }); } @@ -522,7 +527,7 @@ export class CoreWSProvider { } if (!data) { - return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection'))); + throw new CoreError(Translate.instance.instant('core.serverconnection')); } else if (typeof data != preSets.typeExpected) { // If responseType is text an string will be returned, parse before returning. if (typeof data == 'string') { @@ -531,7 +536,7 @@ export class CoreWSProvider { if (isNaN(data)) { this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`); - return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); + throw new CoreError(Translate.instance.instant('core.errorinvalidresponse')); } } else if (preSets.typeExpected == 'boolean') { if (data === 'true') { @@ -541,17 +546,17 @@ export class CoreWSProvider { } else { this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`); - return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); + throw new CoreError(Translate.instance.instant('core.errorinvalidresponse')); } } else { this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); - return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); + throw new CoreError(Translate.instance.instant('core.errorinvalidresponse')); } } else { this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); - return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); + throw new CoreError(Translate.instance.instant('core.errorinvalidresponse')); } } @@ -561,11 +566,11 @@ export class CoreWSProvider { this.logger.error('Error calling WS', method, data); } - return Promise.reject(new CoreWSError(data)); + throw new CoreWSError(data); } if (typeof data.debuginfo != 'undefined') { - return Promise.reject(new CoreError('Error. ' + data.message)); + throw new CoreError('Error. ' + data.message); } return data; @@ -593,7 +598,7 @@ export class CoreWSProvider { return retryPromise; } - return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection'))); + throw new CoreError(Translate.instance.instant('core.serverconnection')); }); } @@ -606,7 +611,7 @@ export class CoreWSProvider { const call = this.retryCalls.shift(); // Add a delay between calls. setTimeout(() => { - call.deferred.resolve(this.performPost(call.method, call.siteUrl, call.data, call.preSets)); + call!.deferred.resolve(this.performPost(call!.method, call!.siteUrl, call!.data, call!.preSets)); this.processRetryQueue(); }, 200); } else { @@ -623,8 +628,12 @@ export class CoreWSProvider { * @param params Params of the HTTP request. * @return The promise saved. */ - protected setPromiseHttp(promise: Promise, method: string, url: string, params?: Record): - Promise { + protected setPromiseHttp( + promise: Promise, + method: string, + url: string, + params?: Record, + ): Promise { const queueItemId = this.getQueueItemId(method, url, params); this.ongoingCalls[queueItemId] = promise; @@ -652,7 +661,7 @@ export class CoreWSProvider { * @return Promise resolved with the response data in success and rejected with the error message if it fails. * @return Request response. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + // eslint-disable-next-line @typescript-eslint/no-explicit-any syncCall(method: string, data: any, preSets: CoreWSPreSets): T { if (!preSets) { throw new CoreError(Translate.instance.instant('core.unexpectederror')); @@ -728,22 +737,26 @@ export class CoreWSProvider { * @param onProgress Function to call on progress. * @return Promise resolved when uploaded. */ - uploadFile(filePath: string, options: CoreWSFileUploadOptions, preSets: CoreWSPreSets, - onProgress?: (event: ProgressEvent) => void): Promise { + async uploadFile( + filePath: string, + options: CoreWSFileUploadOptions, + preSets: CoreWSPreSets, + onProgress?: (event: ProgressEvent) => void, + ): Promise { this.logger.debug(`Trying to upload file: ${filePath}`); if (!filePath || !options || !preSets) { - return Promise.reject(new CoreError('Invalid options passed to upload file.')); + throw new CoreError('Invalid options passed to upload file.'); } if (!CoreApp.instance.isOnline()) { - return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); + throw new CoreError(Translate.instance.instant('core.networkerrormsg')); } const uploadUrl = preSets.siteUrl + '/webservice/upload.php'; const transfer = FileTransfer.instance.create(); - transfer.onProgress(onProgress); + onProgress && transfer.onProgress(onProgress); options.httpMethod = 'POST'; options.params = { @@ -755,45 +768,51 @@ export class CoreWSProvider { options.headers = {}; options['Connection'] = 'close'; - return transfer.upload(filePath, uploadUrl, options, true).then((success) => { - const data = CoreTextUtils.instance.parseJSON(success.response, null, - this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response)); + try { + const success = await transfer.upload(filePath, uploadUrl, options, true); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = CoreTextUtils.instance.parseJSON( + success.response, + null, + this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response), + ); if (data === null) { - return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); + throw new CoreError(Translate.instance.instant('core.errorinvalidresponse')); } if (!data) { - return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection'))); + throw new CoreError(Translate.instance.instant('core.serverconnection')); } else if (typeof data != 'object') { this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"'); - return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); + throw new CoreError(Translate.instance.instant('core.errorinvalidresponse')); } if (typeof data.exception !== 'undefined') { - return Promise.reject(new CoreWSError(data)); + throw new CoreWSError(data); } else if (typeof data.error !== 'undefined') { - return Promise.reject(new CoreWSError({ + throw new CoreWSError({ errorcode: data.errortype, message: data.error, - })); + }); } else if (data[0] && typeof data[0].error !== 'undefined') { - return Promise.reject(new CoreWSError({ + throw new CoreWSError({ errorcode: data[0].errortype, message: data[0].error, - })); + }); } // We uploaded only 1 file, so we only return the first file returned. this.logger.debug('Successfully uploaded file', filePath); return data[0]; - }).catch((error) => { + } catch (error) { this.logger.error('Error while uploading file', filePath, error); - return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); - }); + throw new CoreError(Translate.instance.instant('core.errorinvalidresponse')); + } } /** @@ -842,7 +861,7 @@ export class CoreWSProvider { return new HttpResponse({ body: content, - headers: null, + headers: undefined, status: 200, statusText: 'OK', url, @@ -890,7 +909,7 @@ export class CoreWSProvider { break; default: - return Promise.reject(new CoreError('Method not implemented yet.')); + throw new CoreError('Method not implemented yet.'); } if (angularOptions.timeout) { @@ -966,6 +985,11 @@ export type CoreWSExternalWarning = { * Structure of files returned by WS. */ export type CoreWSExternalFile = { + /** + * Downloadable file url. + */ + fileurl: string; + /** * File name. */ @@ -981,11 +1005,6 @@ export type CoreWSExternalFile = { */ filesize?: number; - /** - * Downloadable file url. - */ - fileurl?: string; - /** * Time modified. */ @@ -1108,7 +1127,7 @@ export type HttpRequestOptions = { /** * Timeout for the request in seconds. If undefined, the default value will be used. If null, no timeout. */ - timeout?: number | null; + timeout?: number; /** * Serializer to use. Defaults to 'urlencoded'. Only for mobile environments. @@ -1162,6 +1181,6 @@ type RetryCall = { * Downloaded file entry. It includes some calculated data. */ export type CoreWSDownloadedFileEntry = FileEntry & { - extension?: string; // File extension. - path?: string; // File path. + extension: string; // File extension. + path: string; // File path. }; diff --git a/src/app/singletons/array.ts b/src/app/singletons/array.ts index 1e3da6b62..7f31bafc7 100644 --- a/src/app/singletons/array.ts +++ b/src/app/singletons/array.ts @@ -41,7 +41,7 @@ export class CoreArray { return (arr as any).flat(); // eslint-disable-line @typescript-eslint/no-explicit-any } - return [].concat(...arr); + return ( []).concat(...arr); } /** diff --git a/src/app/singletons/locutus.ts b/src/app/singletons/locutus.ts index 8f11cc7a1..c39484399 100644 --- a/src/app/singletons/locutus.ts +++ b/src/app/singletons/locutus.ts @@ -5,7 +5,7 @@ */ function initCache () { - const store = [] + const store: any[] = [] // cache only first element, second is length to jump ahead for the parser const cache = function cache (value) { store.push(value[0]) @@ -316,7 +316,7 @@ function expectArrayItems (str, expectedItems = 0, cache) { let hasStringKeys = false let item let totalOffset = 0 - let items = [] + let items: any[] = [] cache([items]) for (let i = 0; i < expectedItems; i++) { diff --git a/src/app/singletons/url.ts b/src/app/singletons/url.ts index d17af5bc6..3cb945572 100644 --- a/src/app/singletons/url.ts +++ b/src/app/singletons/url.ts @@ -141,7 +141,7 @@ export class CoreUrl { // If nothing else worked, parse the domain. const urlParts = CoreUrl.parse(url); - return urlParts && urlParts.domain ? urlParts.domain : null; + return urlParts?.domain ? urlParts.domain : null; } /** @@ -196,8 +196,8 @@ export class CoreUrl { const partsA = CoreUrl.parse(urlA); const partsB = CoreUrl.parse(urlB); - return partsA.domain == partsB.domain && - CoreTextUtils.instance.removeEndingSlash(partsA.path) == CoreTextUtils.instance.removeEndingSlash(partsB.path); + return partsA?.domain == partsB?.domain && + CoreTextUtils.instance.removeEndingSlash(partsA?.path) == CoreTextUtils.instance.removeEndingSlash(partsB?.path); } } diff --git a/src/app/singletons/window.ts b/src/app/singletons/window.ts index 59b6542e6..c3a5762e2 100644 --- a/src/app/singletons/window.ts +++ b/src/app/singletons/window.ts @@ -60,7 +60,7 @@ export class CoreWindow { await CoreUtils.instance.openFile(url); } else { - let treated: boolean; + let treated = false; // eslint-disable-next-line @typescript-eslint/no-unused-vars options = options || {}; @@ -76,7 +76,7 @@ export class CoreWindow { // Not logged in, cannot auto-login. CoreUtils.instance.openInBrowser(url); } else { - await CoreSites.instance.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); + await CoreSites.instance.getCurrentSite()!.openInBrowserWithAutoLoginIfSameSite(url); } } }