From 9cc98e267e06e4b5d7ca9b6e0927ad48d4637144 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:26:21 +0200 Subject: [PATCH 01/17] MOBILE-3565 core: Install WebView ionic-native package --- package-lock.json | 20 ++++++++++++++++++++ package.json | 2 ++ src/app/core/emulator/emulator.module.ts | 4 ++++ tsconfig.app.json | 3 ++- tsconfig.json | 3 ++- 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a336a9a04..4c2bf21f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2224,6 +2224,21 @@ "@types/cordova": "^0.0.34" } }, + "@ionic-native/ionic-webview": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@ionic-native/ionic-webview/-/ionic-webview-5.28.0.tgz", + "integrity": "sha512-Ex/IH/LIa+4X4yGFgo4/W00IWaVsF6KkZuwIG2s3zZQEgXU3tvcgxAOEzkNCbcDC5dXcFH0z/41twZ+YC6gu+A==", + "requires": { + "@types/cordova": "^0.0.34" + }, + "dependencies": { + "@types/cordova": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", + "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=" + } + } + }, "@ionic-native/keyboard": { "version": "5.28.0", "resolved": "https://registry.npmjs.org/@ionic-native/keyboard/-/keyboard-5.28.0.tgz", @@ -3053,6 +3068,11 @@ "cordova-plugin-file-transfer": "*" } }, + "@types/dom-mediacapture-record": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.7.tgz", + "integrity": "sha512-ddDIRTO1ajtbxaNo2o7fPJggpN54PZf1ZUJKOjto2ENMJE/9GKUvaw3ZRuQzlS/p0E+PnIcssxfoqYJ4yiXSBw==" + }, "@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", diff --git a/package.json b/package.json index f92313d7e..14fd1b867 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@ionic-native/globalization": "^5.28.0", "@ionic-native/http": "^5.28.0", "@ionic-native/in-app-browser": "^5.28.0", + "@ionic-native/ionic-webview": "^5.28.0", "@ionic-native/keyboard": "^5.28.0", "@ionic-native/local-notifications": "^5.28.0", "@ionic-native/network": "^5.28.0", @@ -60,6 +61,7 @@ "@ngx-translate/http-loader": "^6.0.0", "@types/cordova": "0.0.34", "@types/cordova-plugin-file-transfer": "^1.6.2", + "@types/dom-mediacapture-record": "^1.0.7", "com-darryncampbell-cordova-plugin-intent": "^2.0.0", "cordova": "^10.0.0", "cordova-android": "^8.1.0", diff --git a/src/app/core/emulator/emulator.module.ts b/src/app/core/emulator/emulator.module.ts index 14a401ab9..2b73dbc3f 100644 --- a/src/app/core/emulator/emulator.module.ts +++ b/src/app/core/emulator/emulator.module.ts @@ -23,7 +23,9 @@ import { FileOpener } from '@ionic-native/file-opener/ngx'; import { FileTransfer } from '@ionic-native/file-transfer/ngx'; import { Geolocation } from '@ionic-native/geolocation/ngx'; import { Globalization } from '@ionic-native/globalization/ngx'; +import { HTTP } from '@ionic-native/http/ngx'; import { InAppBrowser } from '@ionic-native/in-app-browser/ngx'; +import { WebView } from '@ionic-native/ionic-webview/ngx'; import { Keyboard } from '@ionic-native/keyboard/ngx'; import { LocalNotifications } from '@ionic-native/local-notifications/ngx'; import { Network } from '@ionic-native/network/ngx'; @@ -58,6 +60,7 @@ import { Zip } from '@ionic-native/zip/ngx'; FileTransfer, Geolocation, Globalization, + HTTP, InAppBrowser, Keyboard, LocalNotifications, @@ -68,6 +71,7 @@ import { Zip } from '@ionic-native/zip/ngx'; SQLite, StatusBar, WebIntent, + WebView, Zip, ], }) diff --git a/tsconfig.app.json b/tsconfig.app.json index a323c7afd..b31726606 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -6,7 +6,8 @@ "cordova-plugin-file-transfer", "cordova-plugin-inappbrowser", "cordova", - "node" + "node", + "dom-mediacapture-record" ], "paths": { "@/*": ["*"], diff --git a/tsconfig.json b/tsconfig.json index e1026f019..a3bacffb3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,8 @@ "cordova-plugin-inappbrowser", "cordova", "jest", - "node" + "node", + "dom-mediacapture-record" ], "paths": { "@/*": ["*"], From 2dbd1a6d98b9bc1d4412827e927fbc2aa7064dba Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:27:02 +0200 Subject: [PATCH 02/17] MOBILE-3565 errors: Create several error classes --- src/app/classes/errors/ajaxerror.ts | 30 ++++++++++++++ src/app/classes/errors/ajaxwserror.ts | 53 +++++++++++++++++++++++++ src/app/classes/errors/cancelederror.ts | 20 ++++++++++ src/app/classes/{ => errors}/error.ts | 0 src/app/classes/errors/silenterror.ts | 20 ++++++++++ src/app/classes/errors/siteerror.ts | 41 +++++++++++++++++++ src/app/classes/errors/wserror.ts | 42 ++++++++++++++++++++ src/app/services/geolocation.ts | 2 +- src/app/services/utils/text.ts | 8 +++- src/app/services/utils/utils.ts | 4 +- 10 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 src/app/classes/errors/ajaxerror.ts create mode 100644 src/app/classes/errors/ajaxwserror.ts create mode 100644 src/app/classes/errors/cancelederror.ts rename src/app/classes/{ => errors}/error.ts (100%) create mode 100644 src/app/classes/errors/silenterror.ts create mode 100644 src/app/classes/errors/siteerror.ts create mode 100644 src/app/classes/errors/wserror.ts diff --git a/src/app/classes/errors/ajaxerror.ts b/src/app/classes/errors/ajaxerror.ts new file mode 100644 index 000000000..7cf6b1363 --- /dev/null +++ b/src/app/classes/errors/ajaxerror.ts @@ -0,0 +1,30 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreError } from '@classes/errors/error'; + +/** + * Generic error returned by an Ajax call. + */ +export class CoreAjaxError extends CoreError { + + available?: number; // Whether the AJAX call is available. 0 if unknown, 1 if available, -1 if not available. + + constructor(message: string, available?: number) { + super(message); + + this.available = typeof available == 'undefined' ? 0 : available; + } + +} diff --git a/src/app/classes/errors/ajaxwserror.ts b/src/app/classes/errors/ajaxwserror.ts new file mode 100644 index 000000000..c2e5df5c3 --- /dev/null +++ b/src/app/classes/errors/ajaxwserror.ts @@ -0,0 +1,53 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreError } from '@classes/errors/error'; + +/** + * Error returned by WS. + */ +export class CoreAjaxWSError extends CoreError { + + exception?: string; // Name of the Moodle exception. + errorcode?: string; + warningcode?: string; + link?: string; // Link to the site. + moreinfourl?: string; // Link to a page with more info. + debuginfo?: string; // Debug info. Only if debug mode is enabled. + 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 + constructor(error: any, available?: number) { + super(error.message); + + this.exception = error.exception; + this.errorcode = error.errorcode; + this.warningcode = error.warningcode; + this.link = error.link; + this.moreinfourl = error.moreinfourl; + this.debuginfo = error.debuginfo; + this.backtrace = error.backtrace; + + this.available = available; + if (typeof this.available == 'undefined') { + if (this.errorcode) { + this.available = this.errorcode == 'invalidrecord' ? -1 : 1; + } else { + this.available = 0; + } + } + } + +} diff --git a/src/app/classes/errors/cancelederror.ts b/src/app/classes/errors/cancelederror.ts new file mode 100644 index 000000000..1be8af272 --- /dev/null +++ b/src/app/classes/errors/cancelederror.ts @@ -0,0 +1,20 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreSilentError } from '@classes/errors/silenterror'; + +/** + * User canceled an action. + */ +export class CoreCanceledError extends CoreSilentError { } diff --git a/src/app/classes/error.ts b/src/app/classes/errors/error.ts similarity index 100% rename from src/app/classes/error.ts rename to src/app/classes/errors/error.ts diff --git a/src/app/classes/errors/silenterror.ts b/src/app/classes/errors/silenterror.ts new file mode 100644 index 000000000..13a72abcc --- /dev/null +++ b/src/app/classes/errors/silenterror.ts @@ -0,0 +1,20 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreError } from '@classes/errors/error'; + +/** + * Error that won't be displayed to the user. + */ +export class CoreSilentError extends CoreError { } diff --git a/src/app/classes/errors/siteerror.ts b/src/app/classes/errors/siteerror.ts new file mode 100644 index 000000000..a459a0257 --- /dev/null +++ b/src/app/classes/errors/siteerror.ts @@ -0,0 +1,41 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreError } from '@classes/errors/error'; + +/** + * Error returned when performing operations regarding a site (check if it exists, authenticate user, etc.). + */ +export class CoreSiteError extends CoreError { + + errorcode?: string; + critical?: boolean; + loggedOut?: boolean; + + constructor(protected error: SiteError) { + super(error.message); + + this.errorcode = error.errorcode; + this.critical = error.critical; + this.loggedOut = error.loggedOut; + } + +} + +export type SiteError = { + message: string; + errorcode?: string; + critical?: boolean; // Whether the error is important enough to abort the operation. + loggedOut?: boolean; // Whether site has been marked as logged out. +}; diff --git a/src/app/classes/errors/wserror.ts b/src/app/classes/errors/wserror.ts new file mode 100644 index 000000000..27e3df43f --- /dev/null +++ b/src/app/classes/errors/wserror.ts @@ -0,0 +1,42 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreError } from '@classes/errors/error'; + +/** + * Error returned by WS. + */ +export class CoreWSError extends CoreError { + + exception?: string; // Name of the Moodle exception. + errorcode?: string; + warningcode?: string; + link?: string; // Link to the site. + moreinfourl?: string; // Link to a page with more info. + debuginfo?: string; // Debug info. Only if debug mode is enabled. + backtrace?: string; // Backtrace. Only if debug mode is enabled. + + constructor(error: any) { + super(error.message); + + this.exception = error.exception; + this.errorcode = error.errorcode; + this.warningcode = error.warningcode; + this.link = error.link; + this.moreinfourl = error.moreinfourl; + this.debuginfo = error.debuginfo; + this.backtrace = error.backtrace; + } + +} diff --git a/src/app/services/geolocation.ts b/src/app/services/geolocation.ts index 7fe1db82a..0423c0cfc 100644 --- a/src/app/services/geolocation.ts +++ b/src/app/services/geolocation.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { Coordinates } from '@ionic-native/geolocation'; import { CoreApp } from '@services/app'; -import { CoreError } from '@classes/error'; +import { CoreError } from '@classes/errors/error'; import { Geolocation, Diagnostic, makeSingleton } from '@singletons/core.singletons'; @Injectable() diff --git a/src/app/services/utils/text.ts b/src/app/services/utils/text.ts index 30ce8fb26..86a7c3fb0 100644 --- a/src/app/services/utils/text.ts +++ b/src/app/services/utils/text.ts @@ -17,6 +17,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { CoreApp } from '@services/app'; import { CoreLang } from '@services/lang'; +import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons/core.singletons'; import { CoreWSExternalFile } from '@services/ws'; import { Locutus } from '@singletons/locutus'; @@ -29,6 +30,8 @@ export type CoreTextErrorObject = { error?: string; content?: string; body?: string; + debuginfo?: string; + backtrace?: string; }; /* @@ -526,10 +529,13 @@ export class CoreTextUtilsProvider { * @param error Error object. * @return Error message, undefined if not found. */ - getErrorMessageFromError(error: string | CoreTextErrorObject): string { + getErrorMessageFromError(error: string | CoreError | CoreTextErrorObject): string { if (typeof error == 'string') { return error; } + if (error instanceof CoreError) { + return error.message; + } return error && (error.message || error.error || error.content || error.body); } diff --git a/src/app/services/utils/utils.ts b/src/app/services/utils/utils.ts index c61f367b4..83df2f679 100644 --- a/src/app/services/utils/utils.ts +++ b/src/app/services/utils/utils.ts @@ -21,10 +21,11 @@ import { CoreApp } from '@services/app'; import { CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreFile } from '@services/file'; import { CoreLang } from '@services/lang'; -import { CoreWS, CoreWSError, CoreWSExternalFile } from '@services/ws'; +import { CoreWS, CoreWSExternalFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreWSError } from '@classes/errors/wserror'; import { makeSingleton, Clipboard, InAppBrowser, Platform, FileOpener, WebIntent, QRScanner, Translate, } from '@singletons/core.singletons'; @@ -325,6 +326,7 @@ export class CoreUtilsProvider { * @param message The message to include in the error. * @param needsTranslate If the message needs to be translated. * @return Fake WS error. + * @deprecated since 3.9.5. Just create the error directly. */ createFakeWSError(message: string, needsTranslate?: boolean): CoreWSError { return CoreWS.instance.createFakeWSError(message, needsTranslate); From 02a67d88d2d8921d9f5d991bb636a8d36ca65708 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:28:02 +0200 Subject: [PATCH 03/17] MOBILE-3565 core: Fix some ESLint of CoreSite --- src/app/classes/site.ts | 646 +++++++++++++++++++++++----------------- 1 file changed, 371 insertions(+), 275 deletions(-) diff --git a/src/app/classes/site.ts b/src/app/classes/site.ts index 451107b40..9651d195f 100644 --- a/src/app/classes/site.ts +++ b/src/app/classes/site.ts @@ -12,17 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injector } from '@angular/core'; - -import { InAppBrowserObject } from '@ionic-native/in-app-browser'; +import { InAppBrowserObject, InAppBrowserOptions } from '@ionic-native/in-app-browser'; import { Md5 } from 'ts-md5/dist/md5'; -import { SQLiteDB } from '@classes/sqlitedb'; import { CoreApp } from '@services/app'; import { CoreDB } from '@services/db'; import { CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreFile } from '@services/file'; -import { CoreWS, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets } from '@services/ws'; +import { CoreWS, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets, CoreWSExternalWarning } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; @@ -30,6 +27,9 @@ import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { CoreConstants } from '@core/constants'; import CoreConfigConstants from '@app/config.json'; +import { SQLiteDB } from '@classes/sqlitedb'; +import { CoreError } from '@classes/errors/error'; +import { CoreWSError } from '@classes/errors/wserror'; import { CoreLogger } from '@singletons/logger'; import { Translate } from '@singletons/core.singletons'; @@ -40,39 +40,40 @@ import { Translate } from '@singletons/core.singletons'; * the tables are created in all the sites, not just the current one. */ export class CoreSite { - static REQUEST_QUEUE_DELAY = 50; // Maximum number of miliseconds to wait before processing the queue. - static REQUEST_QUEUE_LIMIT = 10; // Maximum number of requests allowed in the queue. - static REQUEST_QUEUE_FORCE_WS = false; // Use "tool_mobile_call_external_functions" even for calling a single function. + + static readonly REQUEST_QUEUE_DELAY = 50; // Maximum number of miliseconds to wait before processing the queue. + static readonly REQUEST_QUEUE_LIMIT = 10; // Maximum number of requests allowed in the queue. + static readonly REQUEST_QUEUE_FORCE_WS = false; // Use "tool_mobile_call_external_functions" even for calling a single function. // Constants for cache update frequency. - static FREQUENCY_USUALLY = 0; - static FREQUENCY_OFTEN = 1; - static FREQUENCY_SOMETIMES = 2; - static FREQUENCY_RARELY = 3; + static readonly FREQUENCY_USUALLY = 0; + static readonly FREQUENCY_OFTEN = 1; + static readonly FREQUENCY_SOMETIMES = 2; + static readonly FREQUENCY_RARELY = 3; // Variables for the database. - static WS_CACHE_TABLE = 'wscache_2'; - static CONFIG_TABLE = 'core_site_config'; + static readonly WS_CACHE_TABLE = 'wscache_2'; + static readonly CONFIG_TABLE = 'core_site_config'; - static MINIMUM_MOODLE_VERSION = '3.1'; + static readonly MINIMUM_MOODLE_VERSION = '3.1'; // Versions of Moodle releases. - protected MOODLE_RELEASES = { + protected readonly MOODLE_RELEASES = { 3.1: 2016052300, 3.2: 2016120500, 3.3: 2017051503, 3.4: 2017111300, 3.5: 2018051700, 3.6: 2018120300, - 3.7: 2019052000 + 3.7: 2019052000, }; // Possible cache update frequencies. - protected UPDATE_FREQUENCIES = [ + protected readonly UPDATE_FREQUENCIES = [ CoreConfigConstants.cache_update_frequency_usually || 420000, CoreConfigConstants.cache_update_frequency_often || 1200000, CoreConfigConstants.cache_update_frequency_sometimes || 3600000, - CoreConfigConstants.cache_update_frequency_rarely || 43200000 + CoreConfigConstants.cache_update_frequency_rarely || 43200000, ]; // Rest of variables. @@ -81,9 +82,10 @@ export class CoreSite { 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 = null; + protected requestQueueTimeout: number = null; protected tokenPluginFileWorks: boolean; protected tokenPluginFileWorksPromise: Promise; protected oauthId: number; @@ -99,8 +101,8 @@ 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?: any, - public privateToken?: string, public config?: any, public loggedOut?: boolean) { + 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'); this.setInfo(infos); this.calculateOfflineDisabled(); @@ -149,7 +151,7 @@ export class CoreSite { * * @return Site info. */ - getInfo(): any { + getInfo(): CoreSiteInfo { return this.infos; } @@ -256,7 +258,7 @@ export class CoreSite { * * @param New info. */ - setInfo(infos: any): void { + setInfo(infos: CoreSiteInfo): void { this.infos = infos; // Index function by name to speed up wsAvailable method. @@ -273,7 +275,7 @@ export class CoreSite { * * @param config Config. */ - setConfig(config: any): void { + setConfig(config: CoreSiteConfig): void { if (config) { config.tool_mobile_disabledfeatures = CoreTextUtils.instance.treatDisabledFeatures(config.tool_mobile_disabledfeatures); } @@ -328,7 +330,7 @@ export class CoreSite { canDownloadFiles(): boolean { const infos = this.getInfo(); - return infos && infos.downloadfiles; + return infos && infos.downloadfiles > 0; } /** @@ -347,11 +349,10 @@ export class CoreSite { } else { for (const i in infos.advancedfeatures) { const item = infos.advancedfeatures[i]; - if (item.name === feature && parseInt(item.value, 10) === 0) { + if (item.name === feature && item.value === 0) { canUse = false; } } - } return canUse; @@ -365,7 +366,7 @@ export class CoreSite { canUploadFiles(): boolean { const infos = this.getInfo(); - return infos && infos.uploadfiles; + return infos && infos.uploadfiles > 0; } /** @@ -373,12 +374,12 @@ export class CoreSite { * * @return A promise to be resolved when the site info is retrieved. */ - fetchSiteInfo(): Promise { + fetchSiteInfo(): Promise { // The get_site_info WS call won't be cached. const preSets = { getFromCache: false, saveToCache: false, - skipQueue: true + skipQueue: true, }; // Reset clean Unicode to check if it's supported again. @@ -395,7 +396,8 @@ export class CoreSite { * @param preSets Extra options. * @return Promise resolved with the response, rejected with CoreWSError if it fails. */ - read(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + read(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { preSets = preSets || {}; if (typeof preSets.getFromCache == 'undefined') { preSets.getFromCache = true; @@ -418,7 +420,8 @@ export class CoreSite { * @param preSets Extra options. * @return Promise resolved with the response, rejected with CoreWSError if it fails. */ - write(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + write(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise { preSets = preSets || {}; if (typeof preSets.getFromCache == 'undefined') { preSets.getFromCache = false; @@ -452,12 +455,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. */ - request(method: string, data: any, preSets: CoreSiteWSPreSets, retrying?: boolean): Promise { + // 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; data = data || {}; if (!CoreApp.instance.isOnline() && this.offlineDisabled) { - return Promise.reject(CoreWS.instance.createFakeWSError('core.errorofflinedisabled', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.errorofflinedisabled'))); } // Check if the method is available, use a prefixed version if possible. @@ -470,7 +474,7 @@ export class CoreSite { } else { this.logger.error(`WS function '${method}' is not available, even in compatibility mode.`); - return Promise.reject(CoreUtils.instance.createFakeWSError('core.wsfunctionnotavailable', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.wsfunctionnotavailable'))); } } @@ -479,7 +483,7 @@ export class CoreSite { siteUrl: this.siteUrl, cleanUnicode: this.cleanUnicode, typeExpected: preSets.typeExpected, - responseExpected: preSets.responseExpected + responseExpected: preSets.responseExpected, }; if (wsPreSets.cleanUnicode && CoreTextUtils.instance.hasUnicodeData(data)) { @@ -501,34 +505,32 @@ export class CoreSite { data.moodlewssettingfilter = preSets.filter === false ? false : true; data.moodlewssettingfileurl = preSets.rewriteurls === false ? false : true; - const originalData = data; - // Convert arguments to strings before starting the cache process. data = CoreWS.instance.convertValuesToString(data, wsPreSets.cleanUnicode); if (data == null) { // Empty cleaned text found. - return Promise.reject(CoreUtils.instance.createFakeWSError('core.unicodenotsupportedcleanerror', true)); + return Promise.reject(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) => { + return this.ongoingRequests[cacheId].then((response) => // Clone the data, this may prevent errors if in the callback the object is modified. - return CoreUtils.instance.clone(response); - }); + CoreUtils.instance.clone(response), + ); } - const promise = this.getFromCache(method, data, preSets, false, originalData).catch(() => { + const promise = this.getFromCache(method, data, preSets, false).catch(() => { if (preSets.forceOffline) { // Don't call the WS, just fail. - return Promise.reject(CoreWS.instance.createFakeWSError('core.cannotconnect', true, - {$a: CoreSite.MINIMUM_MOODLE_VERSION})); + return Promise.reject(new CoreError(Translate.instance.instant('core.cannotconnect', + { $a: CoreSite.MINIMUM_MOODLE_VERSION }))); } // Call the WS. - return this.callOrEnqueueRequest(method, data, preSets, wsPreSets).then((response) => { + return this.callOrEnqueueRequest(method, data, preSets, wsPreSets).then((response) => { if (preSets.saveToCache) { this.saveToCache(method, data, response, preSets); } @@ -541,12 +543,10 @@ export class CoreSite { // Token has changed, retry with the new token. preSets.getFromCache = false; // Don't check cache now. Also, it will skip ongoingRequests. - return this.request(method, data, preSets, true); + return this.request(method, data, preSets, true); } else if (CoreApp.instance.isSSOAuthenticationOngoing()) { // There's an SSO authentication ongoing, wait for it to finish and try again. - return CoreApp.instance.waitForSSOAuthentication().then(() => { - return this.request(method, data, preSets, true); - }); + return CoreApp.instance.waitForSSOAuthentication().then(() => this.request(method, data, preSets, true)); } // Session expired, trigger event. @@ -558,34 +558,32 @@ export class CoreSite { CoreEvents.instance.trigger(CoreEventsProvider.USER_DELETED, { params: data }, this.id); error.message = Translate.instance.instant('core.userdeleted'); - return Promise.reject(error); + return Promise.reject(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); error.message = Translate.instance.instant('core.forcepasswordchangenotice'); - } else if (error.errorcode === 'usernotfullysetup') { // User not fully setup, trigger event. Try to get data from cache, the event will handle the error. CoreEvents.instance.trigger(CoreEventsProvider.USER_NOT_FULLY_SETUP, {}, this.id); error.message = Translate.instance.instant('core.usernotfullysetup'); - } else if (error.errorcode === 'sitepolicynotagreed') { // Site policy not agreed, trigger event. CoreEvents.instance.trigger(CoreEventsProvider.SITE_POLICY_NOT_AGREED, {}, this.id); error.message = Translate.instance.instant('core.login.sitepolicynotagreederror'); - return Promise.reject(error); + return Promise.reject(new CoreWSError(error)); } else if (error.errorcode === 'dmlwriteexception' && CoreTextUtils.instance.hasUnicodeData(data)) { if (!this.cleanUnicode) { // Try again cleaning unicode. this.cleanUnicode = true; - return this.request(method, data, preSets); + return this.request(method, data, preSets); } // This should not happen. error.message = Translate.instance.instant('core.unicodenotsupported'); - return Promise.reject(error); + return Promise.reject(new CoreWSError(error)); } else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission' || error.errorcode === 'notingroup') { // Translate error messages with missing strings. @@ -598,16 +596,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(error); + return Promise.reject(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(error); + return Promise.reject(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(error); + return Promise.reject(new CoreWSError(error)); } if (preSets.deleteCacheIfWSError && CoreUtils.instance.isWebServiceError(error)) { @@ -616,21 +614,20 @@ export class CoreSite { // Ignore errors. }); - return Promise.reject(error); + return Promise.reject(new CoreWSError(error)); } this.logger.debug(`WS call '${method}' failed. Trying to use the emergency cache.`); preSets.omitExpires = true; preSets.getFromCache = true; - return this.getFromCache(method, data, preSets, true, originalData).catch(() => { - return Promise.reject(error); - }); + return this.getFromCache(method, data, preSets, true).catch(() => Promise.reject(new CoreWSError(error))); }); - }).then((response) => { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @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(response); + return Promise.reject(new CoreWSError(response)); } return response; @@ -644,10 +641,10 @@ export class CoreSite { if (this.ongoingRequests[cacheId] === promise) { delete this.ongoingRequests[cacheId]; } - }).then((response) => { + }).then((response) => // 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); - }); + CoreUtils.instance.clone(response), + ); } /** @@ -659,9 +656,15 @@ export class CoreSite { * @param wsPreSets Extra options related to the WS call. * @return Promise resolved with the response when the WS is called. */ - protected callOrEnqueueRequest(method: string, data: any, preSets: CoreSiteWSPreSets, wsPreSets: CoreWSPreSets): Promise { + protected callOrEnqueueRequest( + method: string, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + data: any, + preSets: CoreSiteWSPreSets, + wsPreSets: CoreWSPreSets, + ): Promise { if (preSets.skipQueue || !this.wsAvailable('tool_mobile_call_external_functions')) { - return CoreWS.instance.call(method, data, wsPreSets); + return CoreWS.instance.call(method, data, wsPreSets); } const cacheId = this.getCacheId(method, data); @@ -680,14 +683,9 @@ export class CoreSite { data, preSets, wsPreSets, - deferred: CoreUtils.instance.promiseDefer(), + deferred: CoreUtils.instance.promiseDefer(), }; - request.deferred.promise = new Promise((resolve, reject): void => { - request.deferred.resolve = resolve; - request.deferred.reject = reject; - }); - return this.enqueueRequest(request); } @@ -697,14 +695,14 @@ 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 { - this.requestQueue.push(request); if (this.requestQueue.length >= CoreSite.REQUEST_QUEUE_LIMIT) { this.processRequestQueue(); } else if (!this.requestQueueTimeout) { - this.requestQueueTimeout = setTimeout(this.processRequestQueue.bind(this), CoreSite.REQUEST_QUEUE_DELAY); + this.requestQueueTimeout = window.setTimeout(this.processRequestQueue.bind(this), CoreSite.REQUEST_QUEUE_DELAY); } return request.deferred.promise; @@ -760,9 +758,9 @@ export class CoreSite { return { function: request.method, arguments: JSON.stringify(args), - ...settings + ...settings, }; - }) + }), }; const wsPresets: CoreWSPreSets = { @@ -770,9 +768,10 @@ export class CoreSite { wsToken: this.token, }; - CoreWS.instance.call('tool_mobile_call_external_functions', data, wsPresets).then((data) => { + CoreWS.instance.call('tool_mobile_call_external_functions', data, wsPresets) + .then((data) => { if (!data || !data.responses) { - return Promise.reject(null); + return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); } requests.forEach((request, i) => { @@ -793,7 +792,6 @@ export class CoreSite { request.deferred.resolve(responseData); } }); - }).catch((error) => { // Error not specific to a single request, reject all promises. requests.forEach((request) => { @@ -833,21 +831,11 @@ 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 protected getCacheId(method: string, data: any): string { return Md5.hashAsciiStr(method + ':' + CoreUtils.instance.sortAndStringify(data)); } - /** - * Get the cache ID used in Ionic 1 version of the app. - * - * @param method The WebService method. - * @param data Arguments to pass to the method. - * @return Cache ID. - */ - protected getCacheOldId(method: string, data: any): string { - return Md5.hashAsciiStr(method + ':' + JSON.stringify(data)); - } - /** * Get a WS response from cache. * @@ -858,14 +846,15 @@ export class CoreSite { * @param originalData Arguments to pass to the method before being converted to strings. * @return Promise resolved with the WS response. */ - protected getFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, emergency?: boolean, originalData?: any) - : Promise { + // 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 { if (!this.db || !preSets.getFromCache) { - return Promise.reject(null); + return Promise.reject(new CoreError('Get from cache is disabled.')); } const id = this.getCacheId(method, data); - let promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let promise: Promise; if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) { promise = this.db.getRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey }).then((entries) => { @@ -885,22 +874,12 @@ export class CoreSite { return entries[0]; }); } else { - promise = this.db.getRecord(CoreSite.WS_CACHE_TABLE, { id }).catch(() => { - // Entry not found, try to get it using the old ID. - const oldId = this.getCacheOldId(method, originalData || {}); - - return this.db.getRecord(CoreSite.WS_CACHE_TABLE, { id: oldId }).then((entry) => { - // Update the entry ID to use the new one. - this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { id }, {id: oldId}); - - return entry; - }); - }); + promise = this.db.getRecord(CoreSite.WS_CACHE_TABLE, { id }); } return promise.then((entry) => { const now = Date.now(); - let expirationTime; + let expirationTime: number; preSets.omitExpires = preSets.omitExpires || preSets.forceOffline || !CoreApp.instance.isOnline(); @@ -910,7 +889,7 @@ export class CoreSite { if (now > expirationTime) { this.logger.debug('Cached element found, but it is expired'); - return Promise.reject(null); + return Promise.reject(new CoreError('Cache entry is expired.')); } } @@ -925,7 +904,7 @@ export class CoreSite { return CoreTextUtils.instance.parseJSON(entry.data, {}); } - return Promise.reject(null); + return Promise.reject(new CoreError('Cache entry not valid.')); }); } @@ -937,7 +916,7 @@ export class CoreSite { * @return Promise resolved when we have calculated the size */ getComponentCacheSize(component: string, componentId?: number): Promise { - const params: any[] = [component]; + const params: Array = [component]; let extraClause = ''; if (componentId !== undefined && componentId !== null) { params.push(componentId); @@ -957,45 +936,38 @@ export class CoreSite { * @param preSets Extra options. * @return Promise resolved when the response is saved. */ - protected saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets): Promise { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + protected async saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets): Promise { if (!this.db) { - return Promise.reject(null); + throw new CoreError('Site DB not initialized.'); } - let promise; - if (preSets.uniqueCacheKey) { // Cache key must be unique, delete all entries with same cache key. - promise = this.deleteFromCache(method, data, preSets, true).catch(() => { - // Ignore errors. - }); - } else { - promise = Promise.resolve(); + await CoreUtils.instance.ignoreErrors(this.deleteFromCache(method, data, preSets, true)); } - return promise.then(() => { - // Since 3.7, the expiration time contains the time the entry is modified instead of the expiration time. - // We decided to reuse this field to prevent modifying the database table. - const id = this.getCacheId(method, data); - const entry: any = { - id, - data: JSON.stringify(response), - expirationTime: Date.now() - }; + // Since 3.7, the expiration time contains the time the entry is modified instead of the expiration time. + // We decided to reuse this field to prevent modifying the database table. + const id = this.getCacheId(method, data); + const entry = { + id, + data: JSON.stringify(response), + expirationTime: Date.now(), + }; - if (preSets.cacheKey) { - entry.key = preSets.cacheKey; + if (preSets.cacheKey) { + entry['key'] = preSets.cacheKey; + } + + if (preSets.component) { + entry['component'] = preSets.component; + if (preSets.componentId) { + entry['componentId'] = preSets.componentId; } + } - if (preSets.component) { - entry.component = preSets.component; - if (preSets.componentId) { - entry.componentId = preSets.componentId; - } - } - - return this.db.insertRecord(CoreSite.WS_CACHE_TABLE, entry); - }); + await this.db.insertRecord(CoreSite.WS_CACHE_TABLE, entry); } /** @@ -1007,18 +979,19 @@ 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. */ - protected deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise { if (!this.db) { - return Promise.reject(null); + throw new CoreError('Site DB not initialized.'); } const id = this.getCacheId(method, data); if (allCacheKey) { - return this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey }); + await this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey }); + } else { + await this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { id }); } - - return this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, { id }); } /** @@ -1035,14 +1008,14 @@ export class CoreSite { } if (!this.db) { - throw new Error('Site DB not initialized'); + throw new CoreError('Site DB not initialized'); } const params = { component, - } as any; + }; if (componentId) { - params.componentId = componentId; + params['componentId'] = componentId; } return this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, params); @@ -1056,14 +1029,18 @@ export class CoreSite { * @param onProgress Function to call on progress. * @return Promise resolved when uploaded. */ - uploadFile(filePath: string, options: CoreWSFileUploadOptions, onProgress?: (event: ProgressEvent) => any): Promise { + uploadFile( + filePath: string, + options: CoreWSFileUploadOptions, + onProgress?: (event: ProgressEvent) => void, + ): Promise { if (!options.fileArea) { options.fileArea = 'draft'; } - return CoreWS.instance.uploadFile(filePath, options, { + return CoreWS.instance.uploadFile(filePath, options, { siteUrl: this.siteUrl, - wsToken: this.token + wsToken: this.token, }, onProgress); } @@ -1072,16 +1049,18 @@ export class CoreSite { * * @return Promise resolved when the cache entries are invalidated. */ - invalidateWsCache(): Promise { + async invalidateWsCache(): Promise { if (!this.db) { - return Promise.reject(null); + throw new CoreError('Site DB not initialized'); } this.logger.debug('Invalidate all the cache for site: ' + this.id); - return this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { expirationTime: 0 }).finally(() => { + try { + await this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { expirationTime: 0 }); + } finally { CoreEvents.instance.trigger(CoreEventsProvider.WS_CACHE_INVALIDATED, {}, this.getId()); - }); + } } /** @@ -1090,17 +1069,17 @@ export class CoreSite { * @param key Key to search. * @return Promise resolved when the cache entries are invalidated. */ - invalidateWsCacheForKey(key: string): Promise { + async invalidateWsCacheForKey(key: string): Promise { if (!this.db) { - return Promise.reject(null); + throw new CoreError('Site DB not initialized'); } if (!key) { - return Promise.resolve(); + return; } this.logger.debug('Invalidate cache for key: ' + key); - return this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { expirationTime: 0 }, { key }); + await this.db.updateRecords(CoreSite.WS_CACHE_TABLE, { expirationTime: 0 }, { key }); } /** @@ -1109,22 +1088,16 @@ export class CoreSite { * @param keys Keys to search. * @return Promise resolved when the cache entries are invalidated. */ - invalidateMultipleWsCacheForKey(keys: string[]): Promise { + async invalidateMultipleWsCacheForKey(keys: string[]): Promise { if (!this.db) { - return Promise.reject(null); + throw new CoreError('Site DB not initialized'); } if (!keys || !keys.length) { - return Promise.resolve(); + return; } - const promises = []; - this.logger.debug('Invalidating multiple cache keys'); - keys.forEach((key) => { - promises.push(this.invalidateWsCacheForKey(key)); - }); - - return Promise.all(promises); + await Promise.all(keys.map((key) => this.invalidateWsCacheForKey(key))); } /** @@ -1133,19 +1106,19 @@ export class CoreSite { * @param key Key to search. * @return Promise resolved when the cache entries are invalidated. */ - invalidateWsCacheForKeyStartingWith(key: string): Promise { + async invalidateWsCacheForKeyStartingWith(key: string): Promise { if (!this.db) { - return Promise.reject(null); + throw new CoreError('Site DB not initialized'); } if (!key) { - return Promise.resolve(); + return; } this.logger.debug('Invalidate cache for key starting with: ' + key); const sql = 'UPDATE ' + CoreSite.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?'; - return this.db.execute(sql, [key + '%']); + await this.db.execute(sql, [key + '%']); } /** @@ -1155,9 +1128,7 @@ export class CoreSite { * @return Promise resolved with the fixed URL. */ checkAndFixPluginfileURL(url: string): Promise { - return this.checkTokenPluginFile(url).then(() => { - return this.fixPluginfileURL(url); - }); + return this.checkTokenPluginFile(url).then(() => this.fixPluginfileURL(url)); } /** @@ -1169,7 +1140,7 @@ export class CoreSite { */ fixPluginfileURL(url: string): string { const accessKey = this.tokenPluginFileWorks || typeof this.tokenPluginFileWorks == 'undefined' ? - this.infos && this.infos.userprivateaccesskey : undefined; + this.infos && this.infos.userprivateaccesskey : undefined; return CoreUrlUtils.instance.fixPluginfileURL(url, this.token, this.siteUrl, accessKey); } @@ -1179,8 +1150,8 @@ export class CoreSite { * * @return Promise to be resolved when the DB is deleted. */ - deleteDB(): Promise { - return CoreDB.instance.deleteDB('Site-' + this.id); + async deleteDB(): Promise { + await CoreDB.instance.deleteDB('Site-' + this.id); } /** @@ -1188,16 +1159,15 @@ export class CoreSite { * * @return Promise to be resolved when the DB is deleted. */ - deleteFolder(): Promise { - if (CoreFile.instance.isAvailable()) { - const siteFolder = CoreFile.instance.getSiteFolder(this.id); - - return CoreFile.instance.removeDir(siteFolder).catch(() => { - // Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists. - }); - } else { - return Promise.resolve(); + async deleteFolder(): Promise { + if (!CoreFile.instance.isAvailable()) { + return; } + + const siteFolder = CoreFile.instance.getSiteFolder(this.id); + + // Ignore any errors, removeDir fails if folder doesn't exists. + await CoreUtils.instance.ignoreErrors(CoreFile.instance.removeDir(siteFolder)); } /** @@ -1209,9 +1179,7 @@ export class CoreSite { if (CoreFile.instance.isAvailable()) { const siteFolderPath = CoreFile.instance.getSiteFolder(this.id); - return CoreFile.instance.getDirectorySize(siteFolderPath).catch(() => { - return 0; - }); + return CoreFile.instance.getDirectorySize(siteFolderPath).catch(() => 0); } else { return Promise.resolve(0); } @@ -1260,7 +1228,7 @@ export class CoreSite { * @param anchor Anchor text if needed. * @return URL with params. */ - createSiteUrl(path: string, params?: {[key: string]: any}, anchor?: string): string { + createSiteUrl(path: string, params?: {[key: string]: unknown}, anchor?: string): string { return CoreUrlUtils.instance.addParamsToUrl(this.siteUrl + path, params, anchor); } @@ -1303,7 +1271,7 @@ export class CoreSite { return this.checkLocalMobilePlugin(true); } else { - throw data.error; + throw new CoreWSError(data); } } else if (typeof data == 'undefined' || typeof data.code == 'undefined') { // The local_mobile returned something we didn't expect. Let's assume it's not installed. @@ -1315,18 +1283,18 @@ export class CoreSite { switch (code) { case 1: // Site in maintenance mode. - throw Translate.instance.instant('core.login.siteinmaintenance'); + throw new CoreError(Translate.instance.instant('core.login.siteinmaintenance')); case 2: // Web services not enabled. - throw Translate.instance.instant('core.login.webservicesnotenabled'); + throw new CoreError(Translate.instance.instant('core.login.webservicesnotenabled')); case 3: // Extended service not enabled, but the official is enabled. return { code: 0 }; case 4: // Neither extended or official services enabled. - throw Translate.instance.instant('core.login.mobileservicesnotenabled'); + throw new CoreError(Translate.instance.instant('core.login.mobileservicesnotenabled')); default: - throw Translate.instance.instant('core.unexpectederror'); + throw new CoreError(Translate.instance.instant('core.unexpectederror')); } } else { return { code, service, coreSupported: !!data.coresupported }; @@ -1359,22 +1327,20 @@ export class CoreSite { * * @return Promise resolved it local_mobile was added, rejected otherwise. */ - checkIfLocalMobileInstalledAndNotUsed(): Promise { + async checkIfLocalMobileInstalledAndNotUsed(): Promise { const appUsesLocalMobile = this.checkIfAppUsesLocalMobile(); if (appUsesLocalMobile) { // App already uses local_mobile, it wasn't added. - return Promise.reject(null); + throw new CoreError('Already used.'); } - return this.checkLocalMobilePlugin().then((data: LocalMobileResponse): any => { - if (typeof data.service == 'undefined') { - // The local_mobile NOT installed. Reject. - return Promise.reject(null); - } + const data = await this.checkLocalMobilePlugin(); - return data; - }); + if (typeof data.service == 'undefined') { + // The local_mobile NOT installed. Reject. + throw new CoreError('Not installed.'); + } } /** @@ -1399,13 +1365,12 @@ export class CoreSite { * * @return Promise resolved with public config. Rejected with an object if error, see CoreWSProvider.callAjax. */ - getPublicConfig(): Promise { + getPublicConfig(): Promise { const preSets: CoreWSAjaxPreSets = { - siteUrl: this.siteUrl + siteUrl: this.siteUrl, }; return 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; @@ -1423,7 +1388,7 @@ export class CoreSite { } return Promise.reject(error); - }).then((config) => { + }).then((config: CoreSitePublicConfigResponse) => { // Use the wwwroot returned by the server. if (config.httpswwwroot) { this.siteUrl = config.httpswwwroot; @@ -1440,8 +1405,8 @@ export class CoreSite { * @param alertMessage If defined, an alert will be shown before opening the browser. * @return Promise resolved when done, rejected otherwise. */ - openInBrowserWithAutoLogin(url: string, alertMessage?: string): Promise { - return this.openWithAutoLogin(false, url, undefined, alertMessage); + async openInBrowserWithAutoLogin(url: string, alertMessage?: string): Promise { + await this.openWithAutoLogin(false, url, undefined, alertMessage); } /** @@ -1451,8 +1416,8 @@ export class CoreSite { * @param alertMessage If defined, an alert will be shown before opening the browser. * @return Promise resolved when done, rejected otherwise. */ - openInBrowserWithAutoLoginIfSameSite(url: string, alertMessage?: string): Promise { - return this.openWithAutoLoginIfSameSite(false, url, undefined, alertMessage); + async openInBrowserWithAutoLoginIfSameSite(url: string, alertMessage?: string): Promise { + await this.openWithAutoLoginIfSameSite(false, url, undefined, alertMessage); } /** @@ -1463,8 +1428,10 @@ export class CoreSite { * @param alertMessage If defined, an alert will be shown before opening the inappbrowser. * @return Promise resolved when done. */ - openInAppWithAutoLogin(url: string, options?: any, alertMessage?: string): Promise { - return this.openWithAutoLogin(true, url, options, alertMessage); + async openInAppWithAutoLogin(url: string, options?: InAppBrowserOptions, alertMessage?: string): Promise { + const iabInstance = await this.openWithAutoLogin(true, url, options, alertMessage); + + return iabInstance; } /** @@ -1475,8 +1442,11 @@ export class CoreSite { * @param alertMessage If defined, an alert will be shown before opening the inappbrowser. * @return Promise resolved when done. */ - openInAppWithAutoLoginIfSameSite(url: string, options?: any, alertMessage?: string): Promise { - return this.openWithAutoLoginIfSameSite(true, url, options, alertMessage); + async openInAppWithAutoLoginIfSameSite(url: string, options?: InAppBrowserOptions, alertMessage?: string): + Promise { + const iabInstance = await this.openWithAutoLoginIfSameSite(true, url, options, alertMessage); + + return iabInstance; } /** @@ -1488,7 +1458,8 @@ 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?: any, alertMessage?: string): Promise { + openWithAutoLogin(inApp: boolean, url: string, options?: InAppBrowserOptions, alertMessage?: string): + Promise { // Get the URL to open. return this.getAutoLoginUrl(url).then((url) => { if (!alertMessage) { @@ -1502,20 +1473,18 @@ export class CoreSite { // Show an alert first. return CoreDomUtils.instance.showAlert(Translate.instance.instant('core.notice'), alertMessage, undefined, 3000) - .then((alert) => { + .then(() => new Promise(() => { + // @todo + // const subscription = alert.didDismiss.subscribe(() => { + // subscription && subscription.unsubscribe(); - return new Promise((resolve, reject): void => { - const subscription = alert.didDismiss.subscribe(() => { - subscription && subscription.unsubscribe(); - - if (inApp) { - resolve(CoreUtils.instance.openInApp(url, options)); - } else { - resolve(CoreUtils.instance.openInBrowser(url)); - } - }); - }); - }); + // if (inApp) { + // resolve(CoreUtils.instance.openInApp(url, options)); + // } else { + // resolve(CoreUtils.instance.openInBrowser(url)); + // } + // }); + })); }); } @@ -1528,18 +1497,16 @@ 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?: any, alertMessage?: string) - : Promise { + openWithAutoLoginIfSameSite(inApp: boolean, url: string, options?: InAppBrowserOptions, alertMessage?: string): + Promise { if (this.containsUrl(url)) { return this.openWithAutoLogin(inApp, url, options, alertMessage); } else { if (inApp) { - CoreUtils.instance.openInApp(url, options); + return Promise.resolve(CoreUtils.instance.openInApp(url, options)); } else { CoreUtils.instance.openInBrowser(url); } - - return Promise.resolve(null); } } @@ -1551,9 +1518,9 @@ export class CoreSite { * @param ignoreCache True if it should ignore cached data. * @return Promise resolved with site config. */ - getConfig(name?: string, ignoreCache?: boolean): Promise { + getConfig(name?: string, ignoreCache?: boolean): Promise { const preSets: CoreSiteWSPreSets = { - cacheKey: this.getConfigCacheKey() + cacheKey: this.getConfigCacheKey(), }; if (ignoreCache) { @@ -1561,7 +1528,7 @@ export class CoreSite { preSets.emergencyCache = false; } - return this.read('tool_mobile_get_config', {}, preSets).then((config) => { + return this.read('tool_mobile_get_config', {}, preSets).then((config: CoreSiteConfigResponse) => { if (name) { // Return the requested setting. for (const x in config.settings) { @@ -1570,7 +1537,7 @@ export class CoreSite { } } - return Promise.reject(null); + throw new CoreError('Site config not found: ' + name); } else { // Return all settings in the same array. const settings = {}; @@ -1588,8 +1555,8 @@ export class CoreSite { * * @return Promise resolved when the data is invalidated. */ - invalidateConfig(): Promise { - return this.invalidateWsCacheForKey(this.getConfigCacheKey()); + async invalidateConfig(): Promise { + await this.invalidateWsCacheForKey(this.getConfigCacheKey()); } /** @@ -1607,7 +1574,7 @@ 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): any { + getStoredConfig(name?: string): string | CoreSiteConfig { if (!this.config) { return; } @@ -1626,7 +1593,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; } @@ -1709,7 +1676,6 @@ export class CoreSite { * @return Promise resolved with the converted URL. */ 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. @@ -1728,8 +1694,7 @@ export class CoreSite { } // Use write to not use cache. - return this.write('tool_mobile_get_autologin_key', params).then((data) => { - + return this.write('tool_mobile_get_autologin_key', params).then((data) => { if (!data.autologinurl || !data.key) { // Not valid data, return the same URL. return url; @@ -1738,11 +1703,11 @@ export class CoreSite { this.lastAutoLogin = CoreTimeUtils.instance.timestamp(); return data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + encodeURIComponent(url); - }).catch(() => { + }).catch(() => // Couldn't get autologin key, return the same URL. - return url; - }).finally(() => { + url, + ).finally(() => { modal && modal.dismiss(); }); } @@ -1764,7 +1729,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).slice(-1); + data.major = Object.keys(this.MOODLE_RELEASES).pop(); } return this.MOODLE_RELEASES[data.major] + data.minor; @@ -1776,7 +1741,7 @@ export class CoreSite { * @param version Release version (e.g. '3.1.0'). * @return Object with major and minor. Returns false if invalid version. */ - protected getMajorAndMinor(version: string): any { + protected getMajorAndMinor(version: string): {major: string; minor: number} | false { const match = version.match(/(\d)+(?:\.(\d)+)?(?:\.(\d)+)?/); if (!match || !match[1]) { // Invalid version. @@ -1785,7 +1750,7 @@ export class CoreSite { return { major: match[1] + '.' + (match[2] || '0'), - minor: parseInt(match[3], 10) || 0 + minor: parseInt(match[3], 10) || 0, }; } @@ -1796,16 +1761,15 @@ export class CoreSite { * @return Next major version number. */ protected getNextMajorVersionNumber(version: string): number { - const data = this.getMajorAndMinor(version), - releases = Object.keys(this.MOODLE_RELEASES); - let position; + const data = this.getMajorAndMinor(version); + const releases = Object.keys(this.MOODLE_RELEASES); if (!data) { // Invalid version. return 0; } - position = releases.indexOf(data.major); + const position = releases.indexOf(data.major); if (position == -1 || position == releases.length - 1) { // Major version not found or it's the last one. Use the last one. @@ -1821,8 +1785,8 @@ export class CoreSite { * @param name The config name. * @return Promise resolved when done. */ - deleteSiteConfig(name: string): Promise { - return this.db.deleteRecords(CoreSite.CONFIG_TABLE, { name }); + async deleteSiteConfig(name: string): Promise { + await this.db.deleteRecords(CoreSite.CONFIG_TABLE, { name }); } /** @@ -1832,10 +1796,8 @@ 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?: any): Promise { - return this.db.getRecord(CoreSite.CONFIG_TABLE, { name }).then((entry) => { - return entry.value; - }).catch((error) => { + getLocalSiteConfig(name: string, defaultValue?: T): Promise { + return this.db.getRecord(CoreSite.CONFIG_TABLE, { name }).then((entry) => entry.value).catch((error) => { if (typeof defaultValue != 'undefined') { return defaultValue; } @@ -1851,8 +1813,8 @@ export class CoreSite { * @param value The config value. Can only store number or strings. * @return Promise resolved when done. */ - setLocalSiteConfig(name: string, value: number | string): Promise { - return this.db.insertRecord(CoreSite.CONFIG_TABLE, { name, value }); + async setLocalSiteConfig(name: string, value: number | string): Promise { + await this.db.insertRecord(CoreSite.CONFIG_TABLE, { name, value }); } /** @@ -1895,12 +1857,7 @@ export class CoreSite { url = this.fixPluginfileURL(url); - this.tokenPluginFileWorksPromise = CoreWS.instance.performHead(url).then((result) => { - return result.status >= 200 && result.status < 300; - }).catch((error) => { - // Error performing head request. - return false; - }).then((result) => { + this.tokenPluginFileWorksPromise = CoreWS.instance.urlWorks(url).then((result) => { this.tokenPluginFileWorks = result; return result; @@ -1908,6 +1865,7 @@ export class CoreSite { return this.tokenPluginFileWorksPromise; } + } /** @@ -2050,8 +2008,146 @@ export type LocalMobileResponse = { type RequestQueueItem = { cacheId: string; method: string; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any data: any; preSets: CoreSiteWSPreSets; wsPreSets: CoreWSPreSets; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any deferred: PromiseDefer; }; + +/** + * Result of WS core_webservice_get_site_info. + */ +export type CoreSiteInfoResponse = { + sitename: string; // Site name. + username: string; // Username. + firstname: string; // First name. + lastname: string; // Last name. + fullname: string; // User full name. + lang: string; // Current language. + userid: number; // User id. + siteurl: string; // Site url. + userpictureurl: string; // The user profile picture. + functions: { + name: string; // Function name. + version: string; // The version number of the component to which the function belongs. + }[]; + downloadfiles?: number; // 1 if users are allowed to download files, 0 if not. + uploadfiles?: number; // 1 if users are allowed to upload files, 0 if not. + release?: string; // Moodle release number. + version?: string; // Moodle version number. + mobilecssurl?: string; // Mobile custom CSS theme. + advancedfeatures?: { // Advanced features availability. + name: string; // Feature name. + value: number; // Feature value. Usually 1 means enabled. + }[]; + usercanmanageownfiles?: boolean; // True if the user can manage his own files. + userquota?: number; // User quota (bytes). 0 means user can ignore the quota. + usermaxuploadfilesize?: number; // User max upload file size (bytes). -1 means the user can ignore the upload file size. + userhomepage?: number; // The default home page for the user: 0 for the site home, 1 for dashboard. + userprivateaccesskey?: string; // Private user access key for fetching files. + siteid?: number; // Site course ID. + sitecalendartype?: string; // Calendar type set in the site. + usercalendartype?: string; // Calendar typed used by the user. + userissiteadmin?: boolean; // Whether the user is a site admin or not. + theme?: string; // Current theme for the user. +}; + +/** + * Site info, including some calculated data. + */ +export type CoreSiteInfo = CoreSiteInfoResponse & { + functionsByName?: { + [name: string]: { + name: string; // Function name. + version: string; // The version number of the component to which the function belongs. + }; + }; +}; + +/** + * Result of WS tool_mobile_get_config. + */ +export type CoreSiteConfigResponse = { + settings: { // Settings. + name: string; // The name of the setting. + value: string; // The value of the setting. + }[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Site config indexed by name. + */ +export type CoreSiteConfig = {[name: string]: string}; + +/** + * Result of WS tool_mobile_get_public_config. + */ +export type CoreSitePublicConfigResponse = { + wwwroot: string; // Site URL. + httpswwwroot: string; // Site https URL (if httpslogin is enabled). + sitename: string; // Site name. + guestlogin: number; // Whether guest login is enabled. + rememberusername: number; // Values: 0 for No, 1 for Yes, 2 for optional. + authloginviaemail: number; // Whether log in via email is enabled. + registerauth: string; // Authentication method for user registration. + forgottenpasswordurl: string; // Forgotten password URL. + authinstructions: string; // Authentication instructions. + authnoneenabled: number; // Whether auth none is enabled. + enablewebservices: number; // Whether Web Services are enabled. + enablemobilewebservice: number; // Whether the Mobile service is enabled. + maintenanceenabled: number; // Whether site maintenance is enabled. + maintenancemessage: string; // Maintenance message. + logourl?: string; // The site logo URL. + compactlogourl?: string; // The site compact logo URL. + typeoflogin: number; // The type of login. 1 for app, 2 for browser, 3 for embedded. + launchurl?: string; // SSO login launch URL. + mobilecssurl?: string; // Mobile custom CSS theme. + // eslint-disable-next-line @typescript-eslint/naming-convention + tool_mobile_disabledfeatures?: string; // Disabled features in the app. + identityproviders?: { // Identity providers. + name: string; // The identity provider name. + iconurl: string; // The icon URL for the provider. + url: string; // The URL of the provider. + }[]; + country?: string; // Default site country. + agedigitalconsentverification?: boolean; // Whether age digital consent verification is enabled. + supportname?: string; // Site support contact name (only if age verification is enabled). + supportemail?: string; // Site support contact email (only if age verification is enabled). + autolang?: number; // Whether to detect default language from browser setting. + lang?: string; // Default language for the site. + langmenu?: number; // Whether the language menu should be displayed. + langlist?: string; // Languages on language menu. + locale?: string; // Sitewide locale. + // eslint-disable-next-line @typescript-eslint/naming-convention + tool_mobile_minimumversion?: string; // Minimum required version to access. + // eslint-disable-next-line @typescript-eslint/naming-convention + tool_mobile_iosappid?: string; // IOS app's unique identifier. + // eslint-disable-next-line @typescript-eslint/naming-convention + tool_mobile_androidappid?: string; // Android app's unique identifier. + // eslint-disable-next-line @typescript-eslint/naming-convention + tool_mobile_setuplink?: string; // App download page. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS tool_mobile_get_autologin_key. + */ +export type CoreSiteAutologinKeyResult = { + key: string; // Auto-login key for a single usage with time expiration. + autologinurl: string; // Auto-login URL. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS tool_mobile_call_external_functions. + */ +export type CoreSiteCallExternalFunctionsResult = { + responses: { + error: boolean; // Whether an exception was thrown. + data?: string; // JSON-encoded response data. + exception?: string; // JSON-encoed exception info. + }[]; +}; From 7a1342885b514d4d3daf4e5ed133f0bd814ddbf9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:29:01 +0200 Subject: [PATCH 04/17] MOBILE-3565 core: Fix some ESLint of CoreAppProvider --- src/app/services/app.ts | 150 +++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 79 deletions(-) diff --git a/src/app/services/app.ts b/src/app/services/app.ts index 9b3858bc0..496761fe9 100644 --- a/src/app/services/app.ts +++ b/src/app/services/app.ts @@ -17,12 +17,16 @@ import { Connection } from '@ionic-native/network/ngx'; import { CoreDB } from '@services/db'; import { CoreEvents, CoreEventsProvider } from '@services/events'; +import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; import CoreConfigConstants from '@app/config.json'; import { makeSingleton, Keyboard, Network, StatusBar, Platform } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; +const DBNAME = 'MoodleMobile'; +const SCHEMA_VERSIONS_TABLE = 'schema_versions'; + /** * Factory to provide some global functionalities, like access to the global app database. * @@ -38,23 +42,22 @@ import { CoreLogger } from '@singletons/logger'; */ @Injectable() export class CoreAppProvider { - protected DBNAME = 'MoodleMobile'; + protected db: SQLiteDB; protected logger: CoreLogger; - protected ssoAuthenticationPromise: Promise; + protected ssoAuthenticationDeferred: PromiseDefer; protected isKeyboardShown = false; - protected _isKeyboardOpening = false; - protected _isKeyboardClosing = false; - protected backActions = []; + protected keyboardOpening = false; + protected keyboardClosing = false; + protected backActions: {callback: () => boolean; priority: number}[] = []; protected mainMenuId = 0; protected mainMenuOpen: number; protected forceOffline = false; // Variables for DB. - protected createVersionsTableReady: Promise; - protected SCHEMA_VERSIONS_TABLE = 'schema_versions'; + protected createVersionsTableReady: Promise; protected versionsTableSchema: SQLiteDBTableSchema = { - name: this.SCHEMA_VERSIONS_TABLE, + name: SCHEMA_VERSIONS_TABLE, columns: [ { name: 'name', @@ -68,11 +71,9 @@ export class CoreAppProvider { ], }; - constructor(appRef: ApplicationRef, - zone: NgZone) { - + constructor(appRef: ApplicationRef, zone: NgZone) { this.logger = CoreLogger.getInstance('CoreAppProvider'); - this.db = CoreDB.instance.getDB(this.DBNAME); + this.db = CoreDB.instance.getDB(DBNAME); // Create the schema versions table. this.createVersionsTableReady = this.db.createTableFromSchema(this.versionsTableSchema); @@ -87,7 +88,7 @@ export class CoreAppProvider { CoreEvents.instance.trigger(CoreEventsProvider.KEYBOARD_CHANGE, data.keyboardHeight); }); }); - Keyboard.instance.onKeyboardHide().subscribe((data) => { + Keyboard.instance.onKeyboardHide().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { document.body.classList.remove('keyboard-is-open'); @@ -95,18 +96,18 @@ export class CoreAppProvider { CoreEvents.instance.trigger(CoreEventsProvider.KEYBOARD_CHANGE, 0); }); }); - Keyboard.instance.onKeyboardWillShow().subscribe((data) => { + Keyboard.instance.onKeyboardWillShow().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this._isKeyboardOpening = true; - this._isKeyboardClosing = false; + this.keyboardOpening = true; + this.keyboardClosing = false; }); }); - Keyboard.instance.onKeyboardWillHide().subscribe((data) => { + Keyboard.instance.onKeyboardWillHide().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this._isKeyboardOpening = false; - this._isKeyboardClosing = true; + this.keyboardOpening = false; + this.keyboardClosing = true; }); }); @@ -116,8 +117,8 @@ export class CoreAppProvider { // Export the app provider and appRef to control the application in Behat tests. if (CoreAppProvider.isAutomated()) { - ( window).appProvider = this; - ( window).appRef = appRef; + ( window).appProvider = this; + ( window).appRef = appRef; } } @@ -145,7 +146,7 @@ export class CoreAppProvider { * @return Whether the function is supported. */ canRecordMedia(): boolean { - return !!( window).MediaRecorder; + return !!window.MediaRecorder; } /** @@ -163,7 +164,7 @@ export class CoreAppProvider { * @param schema The schema to create. * @return Promise resolved when done. */ - async createTablesFromSchema(schema: CoreAppSchema): Promise { + async createTablesFromSchema(schema: CoreAppSchema): Promise { this.logger.debug(`Apply schema to app DB: ${schema.name}`); let oldVersion; @@ -173,7 +174,7 @@ export class CoreAppProvider { await this.createVersionsTableReady; // Fetch installed version of the schema. - const entry = await this.db.getRecord(this.SCHEMA_VERSIONS_TABLE, {name: schema.name}); + const entry = await this.db.getRecord(SCHEMA_VERSIONS_TABLE, { name: schema.name }); oldVersion = entry.version; } catch (error) { // No installed version yet. @@ -195,7 +196,7 @@ export class CoreAppProvider { } // Set installed version. - await this.db.insertRecord(this.SCHEMA_VERSIONS_TABLE, {name: schema.name, version: schema.version}); + await this.db.insertRecord(SCHEMA_VERSIONS_TABLE, { name: schema.name, version: schema.version }); } /** @@ -222,7 +223,7 @@ export class CoreAppProvider { * @param storesConfig Config params to send the user to the right place. * @return Store URL. */ - getAppStoreUrl(storesConfig: CoreStoreConfig): string { + getAppStoreUrl(storesConfig: CoreStoreConfig): string { if (this.isMac() && storesConfig.mac) { return 'itms-apps://itunes.apple.com/app/' + storesConfig.mac; } @@ -260,9 +261,7 @@ export class CoreAppProvider { * @return Whether the app is running in a 64 bits desktop environment (not browser). */ is64Bits(): boolean { - const process = ( window).process; - - return this.isDesktop() && process.arch == 'x64'; + return this.isDesktop() && window.process.arch == 'x64'; } /** @@ -280,9 +279,8 @@ export class CoreAppProvider { * @return Whether the app is running in a desktop environment (not browser). */ isDesktop(): boolean { - const process = ( window).process; - - return !!(process && process.versions && typeof process.versions.electron != 'undefined'); + // @todo + return false; } /** @@ -300,7 +298,7 @@ export class CoreAppProvider { * @return Whether keyboard is closing (animating). */ isKeyboardClosing(): boolean { - return this._isKeyboardClosing; + return this.keyboardClosing; } /** @@ -309,7 +307,7 @@ export class CoreAppProvider { * @return Whether keyboard is opening (animating). */ isKeyboardOpening(): boolean { - return this._isKeyboardOpening; + return this.keyboardOpening; } /** @@ -462,8 +460,8 @@ export class CoreAppProvider { */ protected setKeyboardShown(shown: boolean): void { this.isKeyboardShown = shown; - this._isKeyboardOpening = false; - this._isKeyboardClosing = false; + this.keyboardOpening = false; + this.keyboardClosing = false; } /** @@ -487,23 +485,15 @@ export class CoreAppProvider { * NOT when the browser is opened. */ startSSOAuthentication(): void { - let cancelTimeout; - let resolvePromise; + this.ssoAuthenticationDeferred = CoreUtils.instance.promiseDefer(); - this.ssoAuthenticationPromise = new Promise((resolve, reject): void => { - resolvePromise = resolve; - - // Resolve it automatically after 10 seconds (it should never take that long). - cancelTimeout = setTimeout(() => { - this.finishSSOAuthentication(); - }, 10000); - }); - - // Store the resolve function in the promise itself. - ( this.ssoAuthenticationPromise).resolve = resolvePromise; + // Resolve it automatically after 10 seconds (it should never take that long). + const cancelTimeout = setTimeout(() => { + this.finishSSOAuthentication(); + }, 10000); // If the promise is resolved because finishSSOAuthentication is called, stop the cancel promise. - this.ssoAuthenticationPromise.then(() => { + this.ssoAuthenticationDeferred.promise.then(() => { clearTimeout(cancelTimeout); }); } @@ -512,9 +502,9 @@ export class CoreAppProvider { * Finish an SSO authentication process. */ finishSSOAuthentication(): void { - if (this.ssoAuthenticationPromise) { - ( this.ssoAuthenticationPromise).resolve && ( this.ssoAuthenticationPromise).resolve(); - this.ssoAuthenticationPromise = undefined; + if (this.ssoAuthenticationDeferred) { + this.ssoAuthenticationDeferred.resolve(); + this.ssoAuthenticationDeferred = undefined; } } @@ -524,7 +514,7 @@ export class CoreAppProvider { * @return Whether there's a SSO authentication ongoing. */ isSSOAuthenticationOngoing(): boolean { - return !!this.ssoAuthenticationPromise; + return !!this.ssoAuthenticationDeferred; } /** @@ -532,8 +522,8 @@ export class CoreAppProvider { * * @return Promise resolved once SSO authentication finishes. */ - waitForSSOAuthentication(): Promise { - return this.ssoAuthenticationPromise || Promise.resolve(); + async waitForSSOAuthentication(): Promise { + await this.ssoAuthenticationDeferred && this.ssoAuthenticationDeferred.promise; } /** @@ -542,27 +532,24 @@ export class CoreAppProvider { * @param timeout Maximum time to wait, use null to wait forever. */ async waitForResume(timeout: number | null = null): Promise { - let resolve: (value?: any) => void; - let resumeSubscription: any; - let timeoutId: NodeJS.Timer | false; + let deferred = CoreUtils.instance.promiseDefer(); - const promise = new Promise((r): any => resolve = r); - const stopWaiting = (): any => { - if (!resolve) { + const stopWaiting = () => { + if (!deferred) { return; } - resolve(); + deferred.resolve(); resumeSubscription.unsubscribe(); timeoutId && clearTimeout(timeoutId); - resolve = null; + deferred = null; }; - resumeSubscription = Platform.instance.resume.subscribe(stopWaiting); - timeoutId = timeout ? setTimeout(stopWaiting, timeout) : false; + const resumeSubscription = Platform.instance.resume.subscribe(stopWaiting); + const timeoutId = timeout ? setTimeout(stopWaiting, timeout) : false; - await promise; + await deferred.promise; } /** @@ -626,23 +613,19 @@ export class CoreAppProvider { * button is pressed. This method decides which of the registered back button * actions has the highest priority and should be called. * - * @param fn Called when the back button is pressed, - * if this registered action has the highest priority. + * @param callback Called when the back button is pressed, if this registered action has the highest priority. * @param priority Set the priority for this action. All actions sorted by priority will be executed since one of * them returns true. - * * Priorities higher or equal than 1000 will go before closing modals - * * Priorities lower than 500 will only be executed if you are in the first state of the app (before exit). - * @return A function that, when called, will unregister - * the back button action. + * - Priorities higher or equal than 1000 will go before closing modals + * - Priorities lower than 500 will only be executed if you are in the first state of the app (before exit). + * @return A function that, when called, will unregister the back button action. */ - registerBackButtonAction(fn: any, priority: number = 0): any { - const action = { fn, priority }; + registerBackButtonAction(callback: () => boolean, priority: number = 0): () => boolean { + const action = { callback, priority }; this.backActions.push(action); - this.backActions.sort((a, b) => { - return b.priority - a.priority; - }); + this.backActions.sort((a, b) => b.priority - a.priority); return (): boolean => { const index = this.backActions.indexOf(action); @@ -700,6 +683,7 @@ export class CoreAppProvider { setForceOffline(value: boolean): void { this.forceOffline = !!value; } + } export class CoreApp extends makeSingleton(CoreAppProvider) {} @@ -802,5 +786,13 @@ export type CoreAppSchema = { * @param oldVersion Old version of the schema or 0 if not installed. * @return Promise resolved when done. */ - migrate?(db: SQLiteDB, oldVersion: number): Promise; + migrate?(db: SQLiteDB, oldVersion: number): Promise; +}; + +/** + * Extended window type for automated tests. + */ +export type WindowForAutomatedTests = Window & { + appProvider?: CoreAppProvider; + appRef?: ApplicationRef; }; From a2275ab52eb122c712c20ee20de54b5e55adc254 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:29:32 +0200 Subject: [PATCH 05/17] MOBILE-3565 core: Fix some ESLint of CoreConfigProvider --- src/app/services/config.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/app/services/config.ts b/src/app/services/config.ts index 5ea413e6a..46faa027b 100644 --- a/src/app/services/config.ts +++ b/src/app/services/config.ts @@ -18,36 +18,38 @@ import { CoreApp, CoreAppSchema } from '@services/app'; import { SQLiteDB } from '@classes/sqlitedb'; import { makeSingleton } from '@singletons/core.singletons'; +const TABLE_NAME = 'core_config'; + /** * Factory to provide access to dynamic and permanent config and settings. * It should not be abused into a temporary storage. */ @Injectable() export class CoreConfigProvider { + protected appDB: SQLiteDB; - protected TABLE_NAME = 'core_config'; protected tableSchema: CoreAppSchema = { name: 'CoreConfigProvider', version: 1, tables: [ { - name: this.TABLE_NAME, + name: TABLE_NAME, columns: [ { name: 'name', type: 'TEXT', unique: true, - notNull: true + notNull: true, }, { - name: 'value' + name: 'value', }, ], }, ], }; - protected dbReady: Promise; // Promise resolved when the app DB is initialized. + protected dbReady: Promise; // Promise resolved when the app DB is initialized. constructor() { this.appDB = CoreApp.instance.getDB(); @@ -62,10 +64,10 @@ export class CoreConfigProvider { * @param name The config name. * @return Promise resolved when done. */ - async delete(name: string): Promise { + async delete(name: string): Promise { await this.dbReady; - return this.appDB.deleteRecords(this.TABLE_NAME, { name }); + await this.appDB.deleteRecords(TABLE_NAME, { name }); } /** @@ -75,11 +77,11 @@ export class CoreConfigProvider { * @param defaultValue Default value to use if the entry is not found. * @return Resolves upon success along with the config data. Reject on failure. */ - async get(name: string, defaultValue?: any): Promise { + async get(name: string, defaultValue?: T): Promise { await this.dbReady; try { - const entry = await this.appDB.getRecord(this.TABLE_NAME, { name }); + const entry = await this.appDB.getRecord(TABLE_NAME, { name }); return entry.value; } catch (error) { @@ -98,11 +100,12 @@ export class CoreConfigProvider { * @param value The config value. Can only store number or strings. * @return Promise resolved when done. */ - async set(name: string, value: number | string): Promise { + async set(name: string, value: number | string): Promise { await this.dbReady; - return this.appDB.insertRecord(this.TABLE_NAME, { name, value }); + await this.appDB.insertRecord(TABLE_NAME, { name, value }); } + } export class CoreConfig extends makeSingleton(CoreConfigProvider) {} From 8d42aaf964551ebeeba8716d42024813e56228af Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:29:45 +0200 Subject: [PATCH 06/17] MOBILE-3565 core: Fix some ESLint of CoreCronProvider --- src/app/services/cron.ts | 215 ++++++++++++++++++++------------------- 1 file changed, 113 insertions(+), 102 deletions(-) diff --git a/src/app/services/cron.ts b/src/app/services/cron.ts index 903949234..ab6b2c9c4 100644 --- a/src/app/services/cron.ts +++ b/src/app/services/cron.ts @@ -19,105 +19,50 @@ import { CoreConfig } from '@services/config'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@core/constants'; import { SQLiteDB } from '@classes/sqlitedb'; +import { CoreError } from '@classes/errors/error'; import { makeSingleton, Network } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; -/** - * Interface that all cron handlers must implement. - */ -export interface CoreCronHandler { - /** - * A name to identify the handler. - */ - name: string; - - /** - * Whether the handler is running. Used internally by the provider, there's no need to set it. - */ - running?: boolean; - - /** - * Timeout ID for the handler scheduling. Used internally by the provider, there's no need to set it. - */ - timeout?: number; - - /** - * Returns handler's interval in milliseconds. Defaults to CoreCronDelegate.DEFAULT_INTERVAL. - * - * @return Interval time (in milliseconds). - */ - getInterval?(): number; - - /** - * Check whether the process uses network or not. True if not defined. - * - * @return Whether the process uses network or not - */ - usesNetwork?(): boolean; - - /** - * Check whether it's a synchronization process or not. True if not defined. - * - * @return Whether it's a synchronization process or not. - */ - isSync?(): boolean; - - /** - * Check whether the sync can be executed manually. Call isSync if not defined. - * - * @return Whether the sync can be executed manually. - */ - canManualSync?(): boolean; - - /** - * Execute the process. - * - * @param siteId ID of the site affected. If not defined, all sites. - * @param force Determines if it's a forced execution. - * @return Promise resolved when done. If the promise is rejected, this function will be called again often, - * it shouldn't be abused. - */ - execute?(siteId?: string, force?: boolean): Promise; -} +const CRON_TABLE = 'cron'; /* * Service to handle cron processes. The registered processes will be executed every certain time. */ @Injectable() export class CoreCronDelegate { + // Constants. - static DEFAULT_INTERVAL = 3600000; // Default interval is 1 hour. - static MIN_INTERVAL = 300000; // Minimum interval is 5 minutes. - static DESKTOP_MIN_INTERVAL = 60000; // Minimum interval in desktop is 1 minute. - static MAX_TIME_PROCESS = 120000; // Max time a process can block the queue. Defaults to 2 minutes. + static readonly DEFAULT_INTERVAL = 3600000; // Default interval is 1 hour. + static readonly MIN_INTERVAL = 300000; // Minimum interval is 5 minutes. + static readonly DESKTOP_MIN_INTERVAL = 60000; // Minimum interval in desktop is 1 minute. + static readonly MAX_TIME_PROCESS = 120000; // Max time a process can block the queue. Defaults to 2 minutes. // Variables for database. - protected CRON_TABLE = 'cron'; protected tableSchema: CoreAppSchema = { name: 'CoreCronDelegate', version: 1, tables: [ { - name: this.CRON_TABLE, + name: CRON_TABLE, columns: [ { name: 'id', type: 'TEXT', - primaryKey: true + primaryKey: true, }, { name: 'value', - type: 'INTEGER' + type: 'INTEGER', }, ], }, ], }; - protected logger; + protected logger: CoreLogger; protected appDB: SQLiteDB; - protected dbReady: Promise; // Promise resolved when the app DB is initialized. + protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected handlers: { [s: string]: CoreCronHandler } = {}; protected queuePromise = Promise.resolve(); @@ -139,7 +84,7 @@ export class CoreCronDelegate { // Export the sync provider so Behat tests can trigger cron tasks without waiting. if (CoreAppProvider.isAutomated()) { - ( window).cronProvider = this; + ( window).cronProvider = this; } } @@ -152,12 +97,13 @@ 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 checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise { if (!this.handlers[name] || !this.handlers[name].execute) { // Invalid handler. - this.logger.debug('Cannot execute handler because is invalid: ' + name); + const message = `Cannot execute handler because is invalid: ${name}`; + this.logger.debug(message); - return Promise.reject(null); + return Promise.reject(new CoreError(message)); } const usesNetwork = this.handlerUsesNetwork(name); @@ -166,17 +112,17 @@ export class CoreCronDelegate { if (usesNetwork && !CoreApp.instance.isOnline()) { // Offline, stop executing. - this.logger.debug('Cannot execute handler because device is offline: ' + name); + const message = `Cannot execute handler because device is offline: ${name}`; + this.logger.debug(message); this.stopHandler(name); - return Promise.reject(null); + return Promise.reject(new CoreError(message)); } if (isSync) { // Check network connection. - promise = CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false).then((syncOnlyOnWifi) => { - return !syncOnlyOnWifi || CoreApp.instance.isWifi(); - }); + promise = CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false) + .then((syncOnlyOnWifi) => !syncOnlyOnWifi || CoreApp.instance.isWifi()); } else { promise = Promise.resolve(true); } @@ -184,30 +130,30 @@ export class CoreCronDelegate { return promise.then((execute: boolean) => { if (!execute) { // Cannot execute in this network connection, retry soon. - this.logger.debug('Cannot execute handler because device is using limited connection: ' + name); + 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(null); + return Promise.reject(new CoreError(message)); } // Add the execution to the queue. this.queuePromise = this.queuePromise.catch(() => { // Ignore errors in previous handlers. - }).then(() => { - return this.executeHandler(name, force, siteId).then(() => { - this.logger.debug(`Execution of handler '${name}' was a success.`); + }).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) => { - // Handler call failed. Retry soon. - this.logger.error(`Execution of handler '${name}' failed.`, error); - this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL); - - return Promise.reject(null); + return this.setHandlerLastExecutionTime(name, Date.now()).then(() => { + this.scheduleNextExecution(name); }); - }); + }, (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; }); @@ -221,10 +167,8 @@ export class CoreCronDelegate { * @param siteId Site ID. If not defined, all sites. * @return Promise resolved when the handler finishes or reaches max time, rejected if it fails. */ - protected executeHandler(name: string, force?: boolean, siteId?: string): Promise { + protected executeHandler(name: string, force?: boolean, siteId?: string): Promise { return new Promise((resolve, reject): void => { - let cancelTimeout; - this.logger.debug('Executing handler: ' + name); // Wrap the call in Promise.resolve to make sure it's a promise. @@ -232,7 +176,7 @@ export class CoreCronDelegate { clearTimeout(cancelTimeout); }); - cancelTimeout = setTimeout(() => { + const cancelTimeout = setTimeout(() => { // The handler took too long. Resolve because we don't want to retry soon. this.logger.debug(`Resolving execution of handler '${name}' because it took too long.`); resolve(); @@ -247,7 +191,7 @@ export class CoreCronDelegate { * @param siteId Site ID. If not defined, all sites. * @return Promise resolved if all handlers are executed successfully, rejected otherwise. */ - forceSyncExecution(siteId?: string): Promise { + async forceSyncExecution(siteId?: string): Promise { const promises = []; for (const name in this.handlers) { @@ -257,7 +201,7 @@ export class CoreCronDelegate { } } - return CoreUtils.instance.allPromises(promises); + await CoreUtils.instance.allPromises(promises); } /** @@ -268,7 +212,7 @@ export class CoreCronDelegate { * @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). @@ -327,7 +271,7 @@ export class CoreCronDelegate { const id = this.getHandlerLastExecutionId(name); try { - const entry = await this.appDB.getRecord(this.CRON_TABLE, { id }); + const entry = await this.appDB.getRecord(CRON_TABLE, { id }); const time = parseInt(entry.value, 10); return isNaN(time) ? 0 : time; @@ -489,16 +433,16 @@ export class CoreCronDelegate { * @param time Time to set. * @return Promise resolved when the execution time is saved. */ - protected async setHandlerLastExecutionTime(name: string, time: number): Promise { + protected async setHandlerLastExecutionTime(name: string, time: number): Promise { await this.dbReady; const id = this.getHandlerLastExecutionId(name); const entry = { id, - value: time + value: time, }; - return this.appDB.insertRecord(this.CRON_TABLE, entry); + await this.appDB.insertRecord(CRON_TABLE, entry); } /** @@ -559,6 +503,73 @@ export class CoreCronDelegate { clearTimeout(this.handlers[name].timeout); delete this.handlers[name].timeout; } + } export class CoreCron extends makeSingleton(CoreCronDelegate) {} + + +/** + * Interface that all cron handlers must implement. + */ +export interface CoreCronHandler { + /** + * A name to identify the handler. + */ + name: string; + + /** + * Whether the handler is running. Used internally by the provider, there's no need to set it. + */ + running?: boolean; + + /** + * Timeout ID for the handler scheduling. Used internally by the provider, there's no need to set it. + */ + timeout?: number; + + /** + * Returns handler's interval in milliseconds. Defaults to CoreCronDelegate.DEFAULT_INTERVAL. + * + * @return Interval time (in milliseconds). + */ + getInterval?(): number; + + /** + * Check whether the process uses network or not. True if not defined. + * + * @return Whether the process uses network or not + */ + usesNetwork?(): boolean; + + /** + * Check whether it's a synchronization process or not. True if not defined. + * + * @return Whether it's a synchronization process or not. + */ + isSync?(): boolean; + + /** + * Check whether the sync can be executed manually. Call isSync if not defined. + * + * @return Whether the sync can be executed manually. + */ + canManualSync?(): boolean; + + /** + * Execute the process. + * + * @param siteId ID of the site affected. If not defined, all sites. + * @param force Determines if it's a forced execution. + * @return Promise resolved when done. If the promise is rejected, this function will be called again often, + * it shouldn't be abused. + */ + execute?(siteId?: string, force?: boolean): Promise; +} + +/** + * Extended window type for automated tests. + */ +export type WindowForAutomatedTests = Window & { + cronProvider?: CoreCronDelegate; +}; From 46323cdc80def17230333345cfb8bec91ba2f5ab Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:29:58 +0200 Subject: [PATCH 07/17] MOBILE-3565 core: Fix some ESLint of CoreDbProvider --- src/app/services/db.ts | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/app/services/db.ts b/src/app/services/db.ts index 1244ed1fa..ca5aedb1e 100644 --- a/src/app/services/db.ts +++ b/src/app/services/db.ts @@ -24,9 +24,7 @@ import { makeSingleton, SQLite, Platform } from '@singletons/core.singletons'; @Injectable() export class CoreDbProvider { - protected dbInstances = {}; - - constructor() { } + protected dbInstances: {[name: string]: SQLiteDB} = {}; /** * Get or create a database object. @@ -55,31 +53,31 @@ export class CoreDbProvider { * @param name DB name. * @return Promise resolved when the DB is deleted. */ - deleteDB(name: string): Promise { - let promise; - + async deleteDB(name: string): Promise { if (typeof this.dbInstances[name] != 'undefined') { // Close the database first. - promise = this.dbInstances[name].close(); - } else { - promise = Promise.resolve(); - } + await this.dbInstances[name].close(); - return promise.then(() => { const db = this.dbInstances[name]; delete this.dbInstances[name]; - if (Platform.instance.is('cordova')) { - return SQLite.instance.deleteDatabase({ - name, - location: 'default' - }); - } else { + if (db instanceof SQLiteDBMock) { // In WebSQL we cannot delete the database, just empty it. return db.emptyDatabase(); + } else { + return SQLite.instance.deleteDatabase({ + name, + location: 'default', + }); } - }); + } else if (Platform.instance.is('cordova')) { + return SQLite.instance.deleteDatabase({ + name, + location: 'default', + }); + } } + } export class CoreDB extends makeSingleton(CoreDbProvider) {} From 529c35780c9cc7487f6b61e0b9577817e8b452e3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:30:07 +0200 Subject: [PATCH 08/17] MOBILE-3565 core: Fix some ESLint of CoreEventsProvider --- src/app/services/events.ts | 111 +++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 59 deletions(-) diff --git a/src/app/services/events.ts b/src/app/services/events.ts index 8e4afa295..528aec327 100644 --- a/src/app/services/events.ts +++ b/src/app/services/events.ts @@ -33,46 +33,47 @@ export interface CoreEventObserver { */ @Injectable() export class CoreEventsProvider { - static SESSION_EXPIRED = 'session_expired'; - static PASSWORD_CHANGE_FORCED = 'password_change_forced'; - static USER_NOT_FULLY_SETUP = 'user_not_fully_setup'; - static SITE_POLICY_NOT_AGREED = 'site_policy_not_agreed'; - static LOGIN = 'login'; - static LOGOUT = 'logout'; - static LANGUAGE_CHANGED = 'language_changed'; - static NOTIFICATION_SOUND_CHANGED = 'notification_sound_changed'; - static SITE_ADDED = 'site_added'; - static SITE_UPDATED = 'site_updated'; - static SITE_DELETED = 'site_deleted'; - static COMPLETION_MODULE_VIEWED = 'completion_module_viewed'; - static USER_DELETED = 'user_deleted'; - static PACKAGE_STATUS_CHANGED = 'package_status_changed'; - static COURSE_STATUS_CHANGED = 'course_status_changed'; - static SECTION_STATUS_CHANGED = 'section_status_changed'; - static COMPONENT_FILE_ACTION = 'component_file_action'; - static SITE_PLUGINS_LOADED = 'site_plugins_loaded'; - static SITE_PLUGINS_COURSE_RESTRICT_UPDATED = 'site_plugins_course_restrict_updated'; - static LOGIN_SITE_CHECKED = 'login_site_checked'; - static LOGIN_SITE_UNCHECKED = 'login_site_unchecked'; - static IAB_LOAD_START = 'inappbrowser_load_start'; - static IAB_EXIT = 'inappbrowser_exit'; - static APP_LAUNCHED_URL = 'app_launched_url'; // App opened with a certain URL (custom URL scheme). - static FILE_SHARED = 'file_shared'; - static KEYBOARD_CHANGE = 'keyboard_change'; - static CORE_LOADING_CHANGED = 'core_loading_changed'; - static ORIENTATION_CHANGE = 'orientation_change'; - static LOAD_PAGE_MAIN_MENU = 'load_page_main_menu'; - static SEND_ON_ENTER_CHANGED = 'send_on_enter_changed'; - static MAIN_MENU_OPEN = 'main_menu_open'; - static SELECT_COURSE_TAB = 'select_course_tab'; - static WS_CACHE_INVALIDATED = 'ws_cache_invalidated'; - static SITE_STORAGE_DELETED = 'site_storage_deleted'; - static FORM_ACTION = 'form_action'; - static ACTIVITY_DATA_SENT = 'activity_data_sent'; + + static readonly SESSION_EXPIRED = 'session_expired'; + static readonly PASSWORD_CHANGE_FORCED = 'password_change_forced'; + static readonly USER_NOT_FULLY_SETUP = 'user_not_fully_setup'; + static readonly SITE_POLICY_NOT_AGREED = 'site_policy_not_agreed'; + static readonly LOGIN = 'login'; + static readonly LOGOUT = 'logout'; + static readonly LANGUAGE_CHANGED = 'language_changed'; + static readonly NOTIFICATION_SOUND_CHANGED = 'notification_sound_changed'; + static readonly SITE_ADDED = 'site_added'; + static readonly SITE_UPDATED = 'site_updated'; + static readonly SITE_DELETED = 'site_deleted'; + static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed'; + static readonly USER_DELETED = 'user_deleted'; + static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed'; + static readonly COURSE_STATUS_CHANGED = 'course_status_changed'; + static readonly SECTION_STATUS_CHANGED = 'section_status_changed'; + static readonly COMPONENT_FILE_ACTION = 'component_file_action'; + static readonly SITE_PLUGINS_LOADED = 'site_plugins_loaded'; + static readonly SITE_PLUGINS_COURSE_RESTRICT_UPDATED = 'site_plugins_course_restrict_updated'; + static readonly LOGIN_SITE_CHECKED = 'login_site_checked'; + static readonly LOGIN_SITE_UNCHECKED = 'login_site_unchecked'; + static readonly IAB_LOAD_START = 'inappbrowser_load_start'; + static readonly IAB_EXIT = 'inappbrowser_exit'; + static readonly APP_LAUNCHED_URL = 'app_launched_url'; // App opened with a certain URL (custom URL scheme). + static readonly FILE_SHARED = 'file_shared'; + static readonly KEYBOARD_CHANGE = 'keyboard_change'; + static readonly CORE_LOADING_CHANGED = 'core_loading_changed'; + static readonly ORIENTATION_CHANGE = 'orientation_change'; + static readonly LOAD_PAGE_MAIN_MENU = 'load_page_main_menu'; + static readonly SEND_ON_ENTER_CHANGED = 'send_on_enter_changed'; + static readonly MAIN_MENU_OPEN = 'main_menu_open'; + static readonly SELECT_COURSE_TAB = 'select_course_tab'; + static readonly WS_CACHE_INVALIDATED = 'ws_cache_invalidated'; + static readonly SITE_STORAGE_DELETED = 'site_storage_deleted'; + static readonly FORM_ACTION = 'form_action'; + static readonly ACTIVITY_DATA_SENT = 'activity_data_sent'; protected logger: CoreLogger; - protected observables: { [s: string]: Subject } = {}; - protected uniqueEvents = {}; + protected observables: { [eventName: string]: Subject } = {}; + protected uniqueEvents: { [eventName: string]: {data: unknown} } = {}; constructor() { this.logger = CoreLogger.getInstance('CoreEventsProvider'); @@ -89,7 +90,7 @@ export class CoreEventsProvider { * @param siteId Site where to trigger the event. Undefined won't check the site. * @return Observer to stop listening. */ - on(eventName: string, callBack: (value: any) => void, siteId?: string): CoreEventObserver { + on(eventName: string, callBack: (value: unknown) => void, siteId?: string): CoreEventObserver { // If it's a unique event and has been triggered already, call the callBack. // We don't need to create an observer because the event won't be triggered again. if (this.uniqueEvents[eventName]) { @@ -99,7 +100,7 @@ export class CoreEventsProvider { return { off: (): void => { // Nothing to do. - } + }, }; } @@ -107,10 +108,10 @@ export class CoreEventsProvider { if (typeof this.observables[eventName] == 'undefined') { // No observable for this event, create a new one. - this.observables[eventName] = new Subject(); + this.observables[eventName] = new Subject(); } - const subscription = this.observables[eventName].subscribe((value: any) => { + const subscription = this.observables[eventName].subscribe((value: {siteId?: string; [key: string]: unknown}) => { if (!siteId || value.siteId == siteId) { callBack(value); } @@ -121,7 +122,7 @@ export class CoreEventsProvider { off: (): void => { this.logger.debug(`Stop listening to event '${eventName}'`); subscription.unsubscribe(); - } + }, }; } @@ -136,11 +137,8 @@ export class CoreEventsProvider { * @param siteId Site where to trigger the event. Undefined won't check the site. * @return Observer to stop listening. */ - onMultiple(eventNames: string[], callBack: (value: any) => void, siteId?: string): CoreEventObserver { - - const observers = eventNames.map((name) => { - return this.on(name, callBack, siteId); - }); + onMultiple(eventNames: string[], callBack: (value: unknown) => void, siteId?: string): CoreEventObserver { + const observers = eventNames.map((name) => this.on(name, callBack, siteId)); // Create and return a CoreEventObserver. return { @@ -148,7 +146,7 @@ export class CoreEventsProvider { observers.forEach((observer) => { observer.off(); }); - } + }, }; } @@ -159,14 +157,11 @@ export class CoreEventsProvider { * @param data Data to pass to the observers. * @param siteId Site where to trigger the event. Undefined means no Site. */ - trigger(eventName: string, data?: any, siteId?: string): void { + trigger(eventName: string, data?: unknown, siteId?: string): void { this.logger.debug(`Event '${eventName}' triggered.`); if (this.observables[eventName]) { if (siteId) { - if (!data) { - data = {}; - } - data.siteId = siteId; + data = Object.assign(data || {}, { siteId }); } this.observables[eventName].next(data); } @@ -179,17 +174,14 @@ export class CoreEventsProvider { * @param data Data to pass to the observers. * @param siteId Site where to trigger the event. Undefined means no Site. */ - triggerUnique(eventName: string, data: any, siteId?: string): void { + triggerUnique(eventName: string, data: unknown, siteId?: string): void { if (this.uniqueEvents[eventName]) { this.logger.debug(`Unique event '${eventName}' ignored because it was already triggered.`); } else { this.logger.debug(`Unique event '${eventName}' triggered.`); if (siteId) { - if (!data) { - data = {}; - } - data.siteId = siteId; + data = Object.assign(data || {}, { siteId }); } // Store the data so it can be passed to observers that register from now on. @@ -203,6 +195,7 @@ export class CoreEventsProvider { } } } + } export class CoreEvents extends makeSingleton(CoreEventsProvider) {} From a790a093d10aabadfd8f61d915112430e6a0ae0e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:30:41 +0200 Subject: [PATCH 09/17] MOBILE-3565 core: Fix some ESLint of file helper and session --- src/app/services/file-helper.ts | 76 +++++++++++++++++--------------- src/app/services/file-session.ts | 14 +++--- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/app/services/file-helper.ts b/src/app/services/file-helper.ts index f0348a9a5..31ac0aa8c 100644 --- a/src/app/services/file-helper.ts +++ b/src/app/services/file-helper.ts @@ -13,16 +13,18 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { FileEntry } from '@ionic-native/file'; import { CoreApp } from '@services/app'; import { CoreFile } from '@services/file'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; -import { CoreWS } from '@services/ws'; +import { CoreWS, CoreWSExternalFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@core/constants'; +import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons/core.singletons'; /** @@ -42,8 +44,8 @@ export class CoreFileHelperProvider { * @param siteId The site ID. If not defined, current site. * @return Resolved on success. */ - async downloadAndOpenFile(file: any, component: string, componentId: string | number, state?: string, - onProgress?: (event: any) => any, 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); @@ -80,7 +82,7 @@ export class CoreFileHelperProvider { } if (state == CoreConstants.DOWNLOADING) { - throw new Error(Translate.instance.instant('core.erroropenfiledownloading')); + throw new CoreError(Translate.instance.instant('core.erroropenfiledownloading')); } if (state === CoreConstants.NOT_DOWNLOADED) { @@ -109,14 +111,11 @@ 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: any, fileUrl: string, component?: string, componentId?: string | number, - timemodified?: number, state?: string, onProgress?: (event: any) => any, siteId?: string): Promise { + protected 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) => { - return site.checkAndFixPluginfileURL(fileUrl); - }).then((fixedUrl) => { - + return CoreSites.instance.getSite(siteId).then((site) => site.checkAndFixPluginfileURL(fileUrl)).then((fixedUrl) => { if (CoreFile.instance.isAvailable()) { let promise; if (state) { @@ -134,16 +133,16 @@ export class CoreFileHelperProvider { if (state == CoreConstants.DOWNLOADED) { // File is downloaded, get the local file URL. return CoreFilepool.instance.getUrlByUrl( - siteId, fileUrl, component, componentId, timemodified, false, false, file); + siteId, fileUrl, component, componentId, timemodified, false, false, file); } else { if (!isOnline && !this.isStateDownloaded(state)) { // Not downloaded and user is offline, reject. - return Promise.reject(Translate.instance.instant('core.networkerrormsg')); + 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}); + onProgress({ calculating: true }); } return CoreFilepool.instance.shouldDownloadBeforeOpen(fixedUrl, file.filesize).then(() => { @@ -166,7 +165,7 @@ export class CoreFileHelperProvider { } else { // Outdated but offline, so we return the local URL. return CoreFilepool.instance.getUrlByUrl( - siteId, fileUrl, component, componentId, timemodified, false, false, file); + siteId, fileUrl, component, componentId, timemodified, false, false, file); } }); } @@ -191,27 +190,27 @@ export class CoreFileHelperProvider { * @return Resolved with internal URL on success, rejected otherwise. */ downloadFile(fileUrl: string, component?: string, componentId?: string | number, timemodified?: number, - onProgress?: (event: any) => any, file?: any, siteId?: string): Promise { + 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(Translate.instance.instant('core.cannotdownloadfiles')); + return Promise.reject(new CoreError(Translate.instance.instant('core.cannotdownloadfiles'))); } return CoreFilepool.instance.downloadUrl(siteId, fileUrl, false, component, componentId, - timemodified, onProgress, undefined, file).catch((error) => { + timemodified, onProgress, undefined, file).catch((error) => // Download failed, check the state again to see if the file was downloaded before. - return CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified).then((state) => { + CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified).then((state) => { if (this.isStateDownloaded(state)) { return CoreFilepool.instance.getInternalUrlByUrl(siteId, fileUrl); } else { return Promise.reject(error); } - }); - }); + }), + ); }); } @@ -219,9 +218,10 @@ export class CoreFileHelperProvider { * Get the file's URL. * * @param file The file. + * @deprecated since 3.9.5. Get directly the fileurl instead. */ - getFileUrl(file: any): string { - return file.fileurl || file.url; + getFileUrl(file: CoreWSExternalFile): string { + return file.fileurl; } /** @@ -229,7 +229,7 @@ export class CoreFileHelperProvider { * * @param file The file. */ - getFileTimemodified(file: any): number { + getFileTimemodified(file: CoreWSExternalFile): number { return file.timemodified || 0; } @@ -249,7 +249,7 @@ export class CoreFileHelperProvider { * @param file The file to check. * @return Whether the file should be opened in browser. */ - shouldOpenInBrowser(file: any): boolean { + shouldOpenInBrowser(file: CoreWSExternalFile): boolean { if (!file || !file.isexternalfile || !file.mimetype) { return false; } @@ -275,7 +275,7 @@ export class CoreFileHelperProvider { * @param files The files to check. * @return Total files size. */ - async getTotalFilesSize(files: any[]): Promise { + async getTotalFilesSize(files: (CoreWSExternalFile | FileEntry)[]): Promise { let totalSize = 0; for (const file of files) { @@ -291,27 +291,29 @@ export class CoreFileHelperProvider { * @param file The file to check. * @return File size. */ - async getFileSize(file: any): Promise { - if (file.filesize) { + async getFileSize(file: CoreWSExternalFile | FileEntry): Promise { + if ('filesize' in file && (file.filesize || file.filesize === 0)) { return file.filesize; } // If it's a remote file. First check if we have the file downloaded since it's more reliable. - if (file.filename && !file.name) { + if ('filename' in file) { + const fileUrl = file.fileurl; + try { const siteId = CoreSites.instance.getCurrentSiteId(); - const path = await CoreFilepool.instance.getFilePathByUrl(siteId, file.fileurl); + const path = await CoreFilepool.instance.getFilePathByUrl(siteId, fileUrl); const fileEntry = await CoreFile.instance.getFile(path); const fileObject = await CoreFile.instance.getFileObjectFromFileEntry(fileEntry); return fileObject.size; } catch (error) { // Error getting the file, maybe it's not downloaded. Get remote size. - const size = await CoreWS.instance.getRemoteFileSize(file.fileurl); + const size = await CoreWS.instance.getRemoteFileSize(fileUrl); if (size === -1) { - throw new Error('Couldn\'t determine file size: ' + file.fileurl); + throw new CoreError(`Couldn't determine file size: ${fileUrl}`); } return size; @@ -319,13 +321,13 @@ export class CoreFileHelperProvider { } // If it's a local file, get its size. - if (file.name) { + if ('name' in file) { const fileObject = await CoreFile.instance.getFileObjectFromFileEntry(file); return fileObject.size; } - throw new Error('Couldn\'t determine file size: ' + file.fileurl); + throw new CoreError('Couldn\'t determine file size'); } /** @@ -334,7 +336,7 @@ export class CoreFileHelperProvider { * @param file The file to check. * @return bool. */ - isOpenableInApp(file: {filename?: string, name?: string}): boolean { + isOpenableInApp(file: {filename?: string; name?: string}): boolean { const re = /(?:\.([^.]+))?$/; const ext = re.exec(file.filename || file.name)[1]; @@ -363,7 +365,7 @@ export class CoreFileHelperProvider { */ isFileTypeExcludedInApp(fileType: string): boolean { const currentSite = CoreSites.instance.getCurrentSite(); - const fileTypeExcludeList = currentSite && currentSite.getStoredConfig('tool_mobile_filetypeexclusionlist'); + const fileTypeExcludeList = currentSite && currentSite.getStoredConfig('tool_mobile_filetypeexclusionlist'); if (!fileTypeExcludeList) { return false; @@ -373,6 +375,10 @@ export class CoreFileHelperProvider { return !!fileTypeExcludeList.match(regEx); } + } export class CoreFileHelper extends makeSingleton(CoreFileHelperProvider) {} + +export type CoreFileHelperOnProgress = (event?: ProgressEvent | { calculating: true }) => void; + diff --git a/src/app/services/file-session.ts b/src/app/services/file-session.ts index 64a60e890..b03cd24a6 100644 --- a/src/app/services/file-session.ts +++ b/src/app/services/file-session.ts @@ -13,8 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { FileEntry } from '@ionic-native/file'; import { CoreSites } from '@services/sites'; +import { CoreWSExternalFile } from '@services/ws'; import { makeSingleton } from '@singletons/core.singletons'; /** @@ -26,9 +28,8 @@ import { makeSingleton } from '@singletons/core.singletons'; */ @Injectable() export class CoreFileSessionProvider { - protected files = {}; - constructor() { } + protected files: {[siteId: string]: {[component: string]: {[id: string]: (CoreWSExternalFile | FileEntry)[]}}} = {}; /** * Add a file to the session. @@ -38,7 +39,7 @@ export class CoreFileSessionProvider { * @param file File to add. * @param siteId Site ID. If not defined, current site. */ - addFile(component: string, id: string | number, file: any, siteId?: string): void { + addFile(component: string, id: string | number, file: CoreWSExternalFile | FileEntry, siteId?: string): void { siteId = siteId || CoreSites.instance.getCurrentSiteId(); this.initFileArea(component, id, siteId); @@ -68,7 +69,7 @@ export class CoreFileSessionProvider { * @param siteId Site ID. If not defined, current site. * @return Array of files in session. */ - getFiles(component: string, id: string | number, siteId?: string): any[] { + getFiles(component: string, id: string | number, siteId?: string): (CoreWSExternalFile | FileEntry)[] { siteId = siteId || CoreSites.instance.getCurrentSiteId(); if (this.files[siteId] && this.files[siteId][component] && this.files[siteId][component][id]) { return this.files[siteId][component][id]; @@ -106,7 +107,7 @@ export class CoreFileSessionProvider { * @param file File to remove. The instance should be exactly the same as the one stored in session. * @param siteId Site ID. If not defined, current site. */ - removeFile(component: string, id: string | number, file: any, siteId?: string): void { + removeFile(component: string, id: string | number, file: CoreWSExternalFile | FileEntry, siteId?: string): void { siteId = siteId || CoreSites.instance.getCurrentSiteId(); if (this.files[siteId] && this.files[siteId][component] && this.files[siteId][component][id]) { const position = this.files[siteId][component][id].indexOf(file); @@ -140,13 +141,14 @@ export class CoreFileSessionProvider { * @param newFiles Files to set. * @param siteId Site ID. If not defined, current site. */ - setFiles(component: string, id: string | number, newFiles: any[], siteId?: string): void { + setFiles(component: string, id: string | number, newFiles: (CoreWSExternalFile | FileEntry)[], siteId?: string): void { siteId = siteId || CoreSites.instance.getCurrentSiteId(); this.initFileArea(component, id, siteId); this.files[siteId][component][id] = newFiles; } + } export class CoreFileSession extends makeSingleton(CoreFileSessionProvider) {} From bb23f71028d8bc76ea90b1a1039ba2d6b175e09e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:30:55 +0200 Subject: [PATCH 10/17] MOBILE-3565 core: Fix some ESLint of CoreFileProvider --- src/app/services/file.ts | 462 +++++++++++++++++---------------------- 1 file changed, 203 insertions(+), 259 deletions(-) diff --git a/src/app/services/file.ts b/src/app/services/file.ts index d11cc422e..c674764a7 100644 --- a/src/app/services/file.ts +++ b/src/app/services/file.ts @@ -14,15 +14,18 @@ import { Injectable } from '@angular/core'; -import { FileEntry, DirectoryEntry, Entry, Metadata } from '@ionic-native/file'; +import { FileEntry, DirectoryEntry, Entry, Metadata, IFile } from '@ionic-native/file'; import { CoreApp } from '@services/app'; +import { CoreWSExternalFile } from '@services/ws'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; import CoreConfigConstants from '@app/config.json'; +import { CoreError } from '@classes/errors/error'; import { CoreLogger } from '@singletons/logger'; -import { makeSingleton, File, Zip, Platform } from '@singletons/core.singletons'; +import { makeSingleton, File, Zip, Platform, WebView } from '@singletons/core.singletons'; /** * Progress event used when writing a file data into a file. @@ -49,23 +52,35 @@ export type CoreFileProgressEvent = { */ export type CoreFileProgressFunction = (event: CoreFileProgressEvent) => void; +/** + * Constants to define the format to read a file. + */ +export const enum CoreFileFormat { + FORMATTEXT = 0, + FORMATDATAURL = 1, + FORMATBINARYSTRING = 2, + FORMATARRAYBUFFER = 3, + FORMATJSON = 4, +} + /** * Factory to interact with the file system. */ @Injectable() export class CoreFileProvider { + // Formats to read a file. - static FORMATTEXT = 0; - static FORMATDATAURL = 1; - static FORMATBINARYSTRING = 2; - static FORMATARRAYBUFFER = 3; - static FORMATJSON = 4; + static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT; + static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL; + static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING; + static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER; + static readonly FORMATJSON = CoreFileFormat.FORMATJSON; // Folders. - static SITESFOLDER = 'sites'; - static TMPFOLDER = 'tmp'; + static readonly SITESFOLDER = 'sites'; + static readonly TMPFOLDER = 'tmp'; - static CHUNK_SIZE = 1048576; // 1 MB. Same chunk size as Ionic Native. + static readonly CHUNK_SIZE = 1048576; // 1 MB. Same chunk size as Ionic Native. protected logger: CoreLogger; protected initialized = false; @@ -73,73 +88,9 @@ export class CoreFileProvider { protected isHTMLAPI = false; constructor() { - this.logger = CoreLogger.getInstance('CoreFileProvider'); - if (CoreApp.instance.isAndroid() && !Object.getOwnPropertyDescriptor(FileReader.prototype, 'onloadend')) { - // Cordova File plugin creates some getters and setter for FileReader, but Ionic's polyfills override them in Android. - // Create the getters and setters again. This code comes from FileReader.js in cordova-plugin-file. - // @todo: Check if this is still needed. - this.defineGetterSetter(FileReader.prototype, 'readyState', function(): any { - return this._localURL ? this._readyState : this._realReader.readyState; - }); - - this.defineGetterSetter(FileReader.prototype, 'error', function(): any { - return this._localURL ? this._error : this._realReader.error; - }); - - this.defineGetterSetter(FileReader.prototype, 'result', function(): any { - return this._localURL ? this._result : this._realReader.result; - }); - - this.defineEvent('onloadstart'); - this.defineEvent('onprogress'); - this.defineEvent('onload'); - this.defineEvent('onerror'); - this.defineEvent('onloadend'); - this.defineEvent('onabort'); - } - } - - /** - * Define an event for FileReader. - * - * @param eventName Name of the event. - */ - protected defineEvent(eventName: string): void { - this.defineGetterSetter(FileReader.prototype, eventName, function(): any { - return this._realReader[eventName] || null; - }, function(value: any): void { - this._realReader[eventName] = value; - }); - } - - /** - * Define a getter and, optionally, a setter for a certain property in an object. - * - * @param obj Object to set the getter/setter for. - * @param key Name of the property where to set them. - * @param getFunc The getter function. - * @param setFunc The setter function. - */ - protected defineGetterSetter(obj: any, key: string, getFunc: () => any, setFunc?: (value?: any) => any): void { - if (Object.defineProperty) { - const desc: any = { - get: getFunc, - configurable: true - }; - - if (setFunc) { - desc.set = setFunc; - } - - Object.defineProperty(obj, key, desc); - } else { - obj.__defineGetter__(key, getFunc); - if (setFunc) { - obj.__defineSetter__(key, setFunc); - } - } + // @todo: Check if redefining FileReader getters and setters is still needed in Android. } /** @@ -172,7 +123,6 @@ export class CoreFileProvider { } return Platform.instance.ready().then(() => { - if (CoreApp.instance.isAndroid()) { this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath; } else if (CoreApp.instance.isIOS()) { @@ -180,7 +130,7 @@ export class CoreFileProvider { } else if (!this.isAvailable() || this.basePath === '') { this.logger.error('Error getting device OS.'); - return Promise.reject(null); + return Promise.reject(new CoreError('Error getting device OS to initialize file system.')); } this.initialized = true; @@ -208,9 +158,7 @@ export class CoreFileProvider { this.logger.debug('Get file: ' + path); return File.instance.resolveLocalFilesystemUrl(this.addBasePathIfNeeded(path)); - }).then((entry) => { - return entry; - }); + }).then((entry) => entry); } /** @@ -246,39 +194,36 @@ 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 create(isDirectory: boolean, path: string, failIfExists?: boolean, base?: string): Promise { - return this.init().then(() => { - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); - base = base || this.basePath; + protected async create(isDirectory: boolean, path: string, failIfExists?: boolean, base?: string): + Promise { + await this.init(); - if (path.indexOf('/') == -1) { - if (isDirectory) { - this.logger.debug('Create dir ' + path + ' in ' + base); + // Remove basePath if it's in the path. + path = this.removeStartingSlash(path.replace(this.basePath, '')); + base = base || this.basePath; - return File.instance.createDir(base, path, !failIfExists); - } else { - this.logger.debug('Create file ' + path + ' in ' + base); + if (path.indexOf('/') == -1) { + if (isDirectory) { + this.logger.debug('Create dir ' + path + ' in ' + base); - return File.instance.createFile(base, path, !failIfExists); - } + return File.instance.createDir(base, path, !failIfExists); } else { - // The file plugin doesn't allow creating more than 1 level at a time (e.g. tmp/folder). - // We need to create them 1 by 1. - const firstDir = path.substr(0, path.indexOf('/')); - const restOfPath = path.substr(path.indexOf('/') + 1); + this.logger.debug('Create file ' + path + ' in ' + base); - this.logger.debug('Create dir ' + firstDir + ' in ' + base); - - return File.instance.createDir(base, firstDir, true).then((newDirEntry) => { - return this.create(isDirectory, restOfPath, failIfExists, newDirEntry.toURL()); - }).catch((error) => { - this.logger.error('Error creating directory ' + firstDir + ' in ' + base); - - return Promise.reject(error); - }); + return File.instance.createFile(base, path, !failIfExists); } - }); + } else { + // The file plugin doesn't allow creating more than 1 level at a time (e.g. tmp/folder). + // We need to create them 1 by 1. + const firstDir = path.substr(0, path.indexOf('/')); + const restOfPath = path.substr(path.indexOf('/') + 1); + + this.logger.debug('Create dir ' + firstDir + ' in ' + base); + + const newDirEntry = await File.instance.createDir(base, firstDir, true); + + return this.create(isDirectory, restOfPath, failIfExists, newDirEntry.toURL()); + } } /** @@ -288,8 +233,10 @@ export class CoreFileProvider { * @param failIfExists True if it should fail if the directory exists, false otherwise. * @return Promise to be resolved when the directory is created. */ - createDir(path: string, failIfExists?: boolean): Promise { - return this.create(true, path, failIfExists); + async createDir(path: string, failIfExists?: boolean): Promise { + const entry = await this.create(true, path, failIfExists); + + return entry; } /** @@ -299,8 +246,10 @@ export class CoreFileProvider { * @param failIfExists True if it should fail if the file exists, false otherwise.. * @return Promise to be resolved when the file is created. */ - createFile(path: string, failIfExists?: boolean): Promise { - return this.create(false, path, failIfExists); + async createFile(path: string, failIfExists?: boolean): Promise { + const entry = await this.create(true, path, failIfExists); + + return entry; } /** @@ -309,14 +258,14 @@ export class CoreFileProvider { * @param path Relative path to the directory. * @return Promise to be resolved when the directory is deleted. */ - removeDir(path: string): Promise { - return this.init().then(() => { - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); - this.logger.debug('Remove directory: ' + path); + async removeDir(path: string): Promise { + await this.init(); - return File.instance.removeRecursively(this.basePath, path); - }); + // Remove basePath if it's in the path. + path = this.removeStartingSlash(path.replace(this.basePath, '')); + this.logger.debug('Remove directory: ' + path); + + await File.instance.removeRecursively(this.basePath, path); } /** @@ -325,23 +274,25 @@ export class CoreFileProvider { * @param path Relative path to the file. * @return Promise to be resolved when the file is deleted. */ - removeFile(path: string): Promise { - return this.init().then(() => { - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); - this.logger.debug('Remove file: ' + path); + async removeFile(path: string): Promise { + await this.init(); - return File.instance.removeFile(this.basePath, path).catch((error) => { - // The delete can fail if the path has encoded characters. Try again if that's the case. - const decodedPath = decodeURI(path); + // Remove basePath if it's in the path. + path = this.removeStartingSlash(path.replace(this.basePath, '')); + this.logger.debug('Remove file: ' + path); - if (decodedPath != path) { - return File.instance.removeFile(this.basePath, decodedPath); - } else { - return Promise.reject(error); - } - }); - }); + try { + await File.instance.removeFile(this.basePath, path); + } catch (error) { + // The delete can fail if the path has encoded characters. Try again if that's the case. + const decodedPath = decodeURI(path); + + if (decodedPath != path) { + await File.instance.removeFile(this.basePath, decodedPath); + } else { + throw error; + } + } } /** @@ -350,10 +301,8 @@ export class CoreFileProvider { * @param fileEntry File Entry. * @return Promise resolved when the file is deleted. */ - removeFileByFileEntry(fileEntry: any): Promise { - return new Promise((resolve, reject): void => { - fileEntry.remove(resolve, reject); - }); + removeFileByFileEntry(entry: Entry): Promise { + return new Promise((resolve, reject) => entry.remove(resolve, reject)); } /** @@ -362,14 +311,26 @@ export class CoreFileProvider { * @param path Relative path to the directory. * @return Promise to be resolved when the contents are retrieved. */ - getDirectoryContents(path: string): Promise { - return this.init().then(() => { - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); - this.logger.debug('Get contents of dir: ' + path); + async getDirectoryContents(path: string): Promise<(FileEntry | DirectoryEntry)[]> { + await this.init(); - return File.instance.listDir(this.basePath, path); - }); + // Remove basePath if it's in the path. + path = this.removeStartingSlash(path.replace(this.basePath, '')); + this.logger.debug('Get contents of dir: ' + path); + + const result = await File.instance.listDir(this.basePath, path); + + return <(FileEntry | DirectoryEntry)[]> result; + } + + /** + * Type guard to check if the param is a DirectoryEntry. + * + * @param entry Param to check. + * @return Whether the param is a DirectoryEntry. + */ + protected isDirectoryEntry(entry: FileEntry | DirectoryEntry): entry is DirectoryEntry { + return entry.isDirectory === true; } /** @@ -378,19 +339,18 @@ export class CoreFileProvider { * @param entry Directory or file. * @return Promise to be resolved when the size is calculated. */ - protected getSize(entry: any): Promise { - return new Promise((resolve, reject): void => { - if (entry.isDirectory) { + protected getSize(entry: DirectoryEntry | FileEntry): Promise { + return new Promise((resolve, reject) => { + if (this.isDirectoryEntry(entry)) { const directoryReader = entry.createReader(); - directoryReader.readEntries((entries) => { + directoryReader.readEntries((entries: (DirectoryEntry | FileEntry)[]) => { const promises = []; for (let i = 0; i < entries.length; i++) { promises.push(this.getSize(entries[i])); } Promise.all(promises).then((sizes) => { - let directorySize = 0; for (let i = 0; i < sizes.length; i++) { const fileSize = Number(sizes[i]); @@ -402,12 +362,9 @@ export class CoreFileProvider { directorySize += fileSize; } resolve(directorySize); - }, reject); - }, reject); - - } else if (entry.isFile) { + } else { entry.file((file) => { resolve(file.size); }, reject); @@ -427,9 +384,7 @@ export class CoreFileProvider { this.logger.debug('Get size of dir: ' + path); - return this.getDir(path).then((dirEntry) => { - return this.getSize(dirEntry); - }); + return this.getDir(path).then((dirEntry) => this.getSize(dirEntry)); } /** @@ -444,9 +399,7 @@ export class CoreFileProvider { this.logger.debug('Get size of file: ' + path); - return this.getFile(path).then((fileEntry) => { - return this.getSize(fileEntry); - }); + return this.getFile(path).then((fileEntry) => this.getSize(fileEntry)); } /** @@ -455,7 +408,7 @@ export class CoreFileProvider { * @param path Relative path to the file. * @return Promise to be resolved when the file is retrieved. */ - getFileObjectFromFileEntry(entry: FileEntry): Promise { + getFileObjectFromFileEntry(entry: FileEntry): Promise { return new Promise((resolve, reject): void => { this.logger.debug('Get file object of: ' + entry.fullPath); entry.file(resolve, reject); @@ -496,15 +449,10 @@ export class CoreFileProvider { * Read a file from local file system. * * @param path Relative path to the file. - * @param format Format to read the file. Must be one of: - * FORMATTEXT - * FORMATDATAURL - * FORMATBINARYSTRING - * FORMATARRAYBUFFER - * FORMATJSON + * @param format Format to read the file. * @return Promise to be resolved when the file is read. */ - readFile(path: string, format: number = CoreFileProvider.FORMATTEXT): Promise { + readFile(path: string, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise { // Remove basePath if it's in the path. path = this.removeStartingSlash(path.replace(this.basePath, '')); this.logger.debug('Read file ' + path + ' with format ' + format); @@ -521,7 +469,7 @@ export class CoreFileProvider { const parsed = CoreTextUtils.instance.parseJSON(text, null); if (parsed == null && text != null) { - return Promise.reject('Error parsing JSON file: ' + path); + return Promise.reject(new CoreError('Error parsing JSON file: ' + path)); } return parsed; @@ -535,27 +483,21 @@ export class CoreFileProvider { * Read file contents from a file data object. * * @param fileData File's data. - * @param format Format to read the file. Must be one of: - * FORMATTEXT - * FORMATDATAURL - * FORMATBINARYSTRING - * FORMATARRAYBUFFER - * FORMATJSON + * @param format Format to read the file. * @return Promise to be resolved when the file is read. */ - readFileData(fileData: any, format: number = CoreFileProvider.FORMATTEXT): Promise { + readFileData(fileData: IFile, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise { format = format || CoreFileProvider.FORMATTEXT; this.logger.debug('Read file from file data with format ' + format); return new Promise((resolve, reject): void => { const reader = new FileReader(); - reader.onloadend = (evt): void => { - const target = evt.target; // Convert to to be able to use non-standard properties. - if (target.result !== undefined && target.result !== null) { + reader.onloadend = (event): void => { + if (event.target.result !== undefined && event.target.result !== null) { if (format == CoreFileProvider.FORMATJSON) { // Convert to object. - const parsed = CoreTextUtils.instance.parseJSON(target.result, null); + const parsed = CoreTextUtils.instance.parseJSON( event.target.result, null); if (parsed == null) { reject('Error parsing JSON file.'); @@ -563,10 +505,10 @@ export class CoreFileProvider { resolve(parsed); } else { - resolve(target.result); + resolve(event.target.result); } - } else if (target.error !== undefined && target.error !== null) { - reject(target.error); + } else if (event.target.error !== undefined && event.target.error !== null) { + reject(event.target.error); } else { reject({ code: null, message: 'READER_ONLOADEND_ERR' }); } @@ -575,7 +517,7 @@ export class CoreFileProvider { // Check if the load starts. If it doesn't start in 3 seconds, reject. // Sometimes in Android the read doesn't start for some reason, so the promise never finishes. let hasStarted = false; - reader.onloadstart = (evt): void => { + reader.onloadstart = () => { hasStarted = true; }; setTimeout(() => { @@ -597,7 +539,6 @@ export class CoreFileProvider { default: reader.readAsText(fileData); } - }); } @@ -609,7 +550,7 @@ 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: any, append?: boolean): Promise { + 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, '')); @@ -624,9 +565,8 @@ export class CoreFileProvider { data = new Blob([data], { type: type || 'text/plain' }); } - return File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append }).then(() => { - return fileEntry; - }); + return File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append }) + .then(() => fileEntry); }); }); } @@ -644,8 +584,7 @@ export class CoreFileProvider { * @return Promise resolved when done. */ async writeFileDataInFile(file: Blob, path: string, onProgress?: CoreFileProgressFunction, offset: number = 0, - append?: boolean): Promise { - + append?: boolean): Promise { offset = offset || 0; try { @@ -659,7 +598,7 @@ export class CoreFileProvider { onProgress && onProgress({ lengthComputable: true, loaded: offset, - total: file.size + total: file.size, }); if (offset >= file.size) { @@ -671,8 +610,8 @@ export class CoreFileProvider { return this.writeFileDataInFile(file, path, onProgress, offset, true); } catch (error) { if (error && error.target && error.target.error) { - // Error returned by the writer, get the "real" error. - error = error.target.error; + // Error returned by the writer, throw the "real" error. + throw error.target.error; } throw error; @@ -686,9 +625,7 @@ export class CoreFileProvider { * @return Promise to be resolved when the file is retrieved. */ getExternalFile(fullPath: string): Promise { - return File.instance.resolveLocalFilesystemUrl(fullPath).then((entry) => { - return entry; - }); + return File.instance.resolveLocalFilesystemUrl(fullPath).then((entry) => entry); } /** @@ -709,11 +646,11 @@ export class CoreFileProvider { * @param fullPath Absolute path to the file. * @return Promise to be resolved when the file is removed. */ - removeExternalFile(fullPath: string): Promise { + async removeExternalFile(fullPath: string): Promise { const directory = fullPath.substring(0, fullPath.lastIndexOf('/')); const filename = fullPath.substr(fullPath.lastIndexOf('/') + 1); - return File.instance.removeFile(directory, filename); + await File.instance.removeFile(directory, filename); } /** @@ -742,9 +679,7 @@ export class CoreFileProvider { 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) => { - return dirEntry.toInternalURL(); - }); + 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; @@ -776,8 +711,10 @@ export class CoreFileProvider { * try to create it (slower). * @return Promise resolved when the entry is moved. */ - moveDir(originalPath: string, newPath: string, destDirExists?: boolean): Promise { - return this.copyOrMoveFileOrDir(originalPath, newPath, true, false, destDirExists); + async moveDir(originalPath: string, newPath: string, destDirExists?: boolean): Promise { + const entry = await this.copyOrMoveFileOrDir(originalPath, newPath, true, false, destDirExists); + + return entry; } /** @@ -789,8 +726,10 @@ export class CoreFileProvider { * try to create it (slower). * @return Promise resolved when the entry is moved. */ - moveFile(originalPath: string, newPath: string, destDirExists?: boolean): Promise { - return this.copyOrMoveFileOrDir(originalPath, newPath, false, false, destDirExists); + async moveFile(originalPath: string, newPath: string, destDirExists?: boolean): Promise { + const entry = await this.copyOrMoveFileOrDir(originalPath, newPath, false, false, destDirExists); + + return entry; } /** @@ -802,8 +741,10 @@ export class CoreFileProvider { * try to create it (slower). * @return Promise resolved when the entry is copied. */ - copyDir(from: string, to: string, destDirExists?: boolean): Promise { - return this.copyOrMoveFileOrDir(from, to, true, true, destDirExists); + async copyDir(from: string, to: string, destDirExists?: boolean): Promise { + const entry = await this.copyOrMoveFileOrDir(from, to, true, true, destDirExists); + + return entry; } /** @@ -815,8 +756,10 @@ export class CoreFileProvider { * try to create it (slower). * @return Promise resolved when the entry is copied. */ - copyFile(from: string, to: string, destDirExists?: boolean): Promise { - return this.copyOrMoveFileOrDir(from, to, false, true, destDirExists); + async copyFile(from: string, to: string, destDirExists?: boolean): Promise { + const entry = await this.copyOrMoveFileOrDir(from, to, false, true, destDirExists); + + return entry; } /** @@ -830,16 +773,16 @@ 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 = 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)); @@ -885,10 +828,10 @@ export class CoreFileProvider { * path/ -> directory: 'path', name: '' * path -> directory: '', name: 'path' */ - getFileAndDirectoryFromPath(path: string): {directory: string, name: string} { + getFileAndDirectoryFromPath(path: string): {directory: string; name: string} { const file = { directory: '', - name: '' + name: '', }; file.directory = path.substring(0, path.lastIndexOf('/')); @@ -949,7 +892,8 @@ 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: any) => void, recreateDir: boolean = true): Promise { + unzipFile(path: string, destFolder?: string, onProgress?: (progress: ProgressEvent) => void, recreateDir: boolean = true): + Promise { // Get the source file. let fileEntry: FileEntry; @@ -960,10 +904,10 @@ export class CoreFileProvider { // Make sure the dest dir doesn't exist already. return this.removeDir(destFolder).catch(() => { // Ignore errors. - }).then(() => { + }).then(() => // Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail. - return this.createDir(destFolder); - }); + this.createDir(destFolder), + ); } }).then(() => { // If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath). @@ -972,7 +916,7 @@ export class CoreFileProvider { return Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress); }).then((result) => { if (result == -1) { - return Promise.reject('Unzip failed.'); + return Promise.reject(new CoreError('Unzip failed.')); } }); } @@ -985,18 +929,18 @@ export class CoreFileProvider { * @param newValue New value. * @return Promise resolved in success. */ - replaceInFile(path: string, search: string | RegExp, newValue: string): Promise { - return this.readFile(path).then((content) => { - if (typeof content == 'undefined' || content === null || !content.replace) { - return Promise.reject(null); - } + async replaceInFile(path: string, search: string | RegExp, newValue: string): Promise { + let content = await this.readFile(path); - if (content.match(search)) { - content = content.replace(search, newValue); + if (typeof content == 'undefined' || content === null || !content.replace) { + throw new CoreError(`Error reading file ${path}`); + } - return this.writeFile(path, content); - } - }); + if (content.match(search)) { + content = content.replace(search, newValue); + + await this.writeFile(path, content); + } } /** @@ -1007,7 +951,7 @@ export class CoreFileProvider { */ getMetadata(fileEntry: Entry): Promise { if (!fileEntry || !fileEntry.getMetadata) { - return Promise.reject(null); + return Promise.reject(new CoreError('Cannot get metadata from file entry.')); } return new Promise((resolve, reject): void => { @@ -1022,7 +966,7 @@ export class CoreFileProvider { * @param isDir True if directory, false if file. * @return Promise resolved with metadata. */ - getMetadataFromPath(path: string, isDir?: boolean): Promise { + getMetadataFromPath(path: string, isDir?: boolean): Promise { let promise; if (isDir) { promise = this.getDir(path); @@ -1030,9 +974,7 @@ export class CoreFileProvider { promise = this.getFile(path); } - return promise.then((entry) => { - return this.getMetadata(entry); - }); + return promise.then((entry) => this.getMetadata(entry)); } /** @@ -1057,22 +999,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 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); - return this.createDir(dirAndFile.directory).then((dirEntry) => { + return this.createDir(dirAndFile.directory).then((dirEntry) => // Now copy/move the file. - return new Promise((resolve, reject): void => { + new Promise((resolve, reject): void => { if (copy) { - fileEntry.copyTo(dirEntry, dirAndFile.name, resolve, reject); + fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); } else { - fileEntry.moveTo(dirEntry, dirAndFile.name, resolve, reject); + fileEntry.moveTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); } - }); - }); + }), + ); }); } @@ -1083,7 +1025,7 @@ export class CoreFileProvider { * @param to Relative new path of the file (inside the app folder). * @return Promise resolved when the entry is copied. */ - copyExternalFile(from: string, to: string): Promise { + copyExternalFile(from: string, to: string): Promise { return this.copyOrMoveExternalFile(from, to, true); } @@ -1094,7 +1036,7 @@ export class CoreFileProvider { * @param to Relative new path of the file (inside the app folder). * @return Promise resolved when the entry is moved. */ - moveExternalFile(from: string, to: string): Promise { + moveExternalFile(from: string, to: string): Promise { return this.copyOrMoveExternalFile(from, to, false); } @@ -1144,10 +1086,10 @@ export class CoreFileProvider { // Ask the user what he wants to do. return newName; } - }).catch(() => { + }).catch(() => // Folder doesn't exist, name is unique. Clean it and return it. - return CoreTextUtils.instance.removeSpecialCharactersForFiles(CoreTextUtils.instance.decodeURIComponent(fileName)); - }); + CoreTextUtils.instance.removeSpecialCharactersForFiles(CoreTextUtils.instance.decodeURIComponent(fileName)), + ); } /** @@ -1155,10 +1097,9 @@ export class CoreFileProvider { * * @return Promise resolved when done. */ - clearTmpFolder(): Promise { - return this.removeDir(CoreFileProvider.TMPFOLDER).catch(() => { - // Ignore errors because the folder might not exist. - }); + async clearTmpFolder(): Promise { + // Ignore errors because the folder might not exist. + await CoreUtils.instance.ignoreErrors(this.removeDir(CoreFileProvider.TMPFOLDER)); } /** @@ -1168,19 +1109,21 @@ export class CoreFileProvider { * @param files List of used files. * @return Promise resolved when done, rejected if failure. */ - removeUnusedFiles(dirPath: string, files: any[]): Promise { + async removeUnusedFiles(dirPath: string, files: (CoreWSExternalFile | FileEntry)[]): Promise { // Get the directory contents. - return this.getDirectoryContents(dirPath).then((contents) => { + try { + const contents = await this.getDirectoryContents(dirPath); + if (!contents.length) { return; } - const filesMap = {}; + const filesMap: {[fullPath: string]: FileEntry} = {}; const promises = []; // Index the received files by fullPath and ignore the invalid ones. files.forEach((file) => { - if (file.fullPath) { + if ('fullPath' in file) { filesMap[file.fullPath] = file; } }); @@ -1193,10 +1136,10 @@ export class CoreFileProvider { } }); - return Promise.all(promises); - }).catch(() => { + await Promise.all(promises); + } catch (error) { // Ignore errors, maybe it doesn't exist. - }); + } } /** @@ -1246,7 +1189,7 @@ export class CoreFileProvider { * @return Converted src. */ convertFileSrc(src: string): string { - return CoreApp.instance.isIOS() ? ( window).Ionic.WebView.convertFileSrc(src) : src; + return CoreApp.instance.isIOS() ? WebView.instance.convertFileSrc(src) : src; } /** @@ -1272,6 +1215,7 @@ export class CoreFileProvider { protected isPathInAppFolder(path: string): boolean { return !path || !path.match(/^[a-z0-9]+:\/\//i) || path.indexOf(this.basePath) != -1; } + } export class CoreFile extends makeSingleton(CoreFileProvider) {} From 739160400863a6b76faa607c7579463aa4126acb Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:31:36 +0200 Subject: [PATCH 11/17] MOBILE-3565 core: Fix some ESLint of CoreSitesProvider --- src/app/services/sites.ts | 650 +++++++++++++++++++------------------- 1 file changed, 333 insertions(+), 317 deletions(-) diff --git a/src/app/services/sites.ts b/src/app/services/sites.ts index 8c4dc2543..37aae6fff 100644 --- a/src/app/services/sites.ts +++ b/src/app/services/sites.ts @@ -25,11 +25,19 @@ import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@core/constants'; import CoreConfigConstants from '@app/config.json'; -import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { + CoreSite, CoreSiteWSPreSets, LocalMobileResponse, CoreSiteConfig, CoreSitePublicConfigResponse, CoreSiteInfoResponse, +} from '@classes/site'; import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { CoreError } from '@classes/errors/error'; +import { CoreSiteError } from '@classes/errors/siteerror'; import { makeSingleton, Translate, Http } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; +const SITES_TABLE = 'sites_2'; +const CURRENT_SITE_TABLE = 'current_site'; +const SCHEMA_VERSIONS_TABLE = 'schema_versions'; + /* * Service to manage and interact with sites. * It allows creating tables in the databases of all sites. Each service or component should be responsible of creating @@ -43,74 +51,71 @@ import { CoreLogger } from '@singletons/logger'; */ @Injectable() export class CoreSitesProvider { - // Variables for the database. - static SITES_TABLE = 'sites_2'; - static CURRENT_SITE_TABLE = 'current_site'; - static SCHEMA_VERSIONS_TABLE = 'schema_versions'; + // Variables for the database. protected appTablesSchema: CoreAppSchema = { name: 'CoreSitesProvider', version: 2, tables: [ { - name: CoreSitesProvider.SITES_TABLE, + name: SITES_TABLE, columns: [ { name: 'id', type: 'TEXT', - primaryKey: true + primaryKey: true, }, { name: 'siteUrl', type: 'TEXT', - notNull: true + notNull: true, }, { name: 'token', - type: 'TEXT' + type: 'TEXT', }, { name: 'info', - type: 'TEXT' + type: 'TEXT', }, { name: 'privateToken', - type: 'TEXT' + type: 'TEXT', }, { name: 'config', - type: 'TEXT' + type: 'TEXT', }, { name: 'loggedOut', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'oauthId', - type: 'INTEGER' + type: 'INTEGER', }, ], }, { - name: CoreSitesProvider.CURRENT_SITE_TABLE, + name: CURRENT_SITE_TABLE, columns: [ { name: 'id', type: 'INTEGER', - primaryKey: true + primaryKey: true, }, { name: 'siteId', type: 'TEXT', notNull: true, - unique: true + unique: true, }, ], }, ], - async migrate(db: SQLiteDB, oldVersion: number): Promise { + async migrate(db: SQLiteDB, oldVersion: number): Promise { if (oldVersion < 2) { - const newTable = CoreSitesProvider.SITES_TABLE; + const newTable = SITES_TABLE; const oldTable = 'sites'; try { @@ -137,10 +142,10 @@ export class CoreSitesProvider { }; // Constants to validate a site version. - protected WORKPLACE_APP = 3; - protected MOODLE_APP = 2; - protected VALID_VERSION = 1; - protected INVALID_VERSION = -1; + protected readonly WORKPLACE_APP = 3; + protected readonly MOODLE_APP = 2; + protected readonly VALID_VERSION = 1; + protected readonly INVALID_VERSION = -1; protected isWPApp: boolean; @@ -150,14 +155,14 @@ export class CoreSitesProvider { protected currentSite: CoreSite; protected sites: { [s: string]: CoreSite } = {}; protected appDB: SQLiteDB; - protected dbReady: Promise; // Promise resolved when the app DB is initialized. - protected siteSchemasMigration: { [siteId: string]: Promise } = {}; + protected dbReady: Promise; // Promise resolved when the app DB is initialized. + protected siteSchemasMigration: { [siteId: string]: Promise } = {}; // Schemas for site tables. Other providers can add schemas in here. protected siteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {}; protected siteTablesSchemas: SQLiteDBTableSchema[] = [ { - name: CoreSitesProvider.SCHEMA_VERSIONS_TABLE, + name: SCHEMA_VERSIONS_TABLE, columns: [ { name: 'name', @@ -166,17 +171,17 @@ export class CoreSitesProvider { }, { name: 'version', - type: 'INTEGER' - } - ] - } + type: 'INTEGER', + }, + ], + }, ]; // Site schema for this provider. protected siteSchema: CoreSiteSchema = { name: 'CoreSitesProvider', version: 2, - canBeCleared: [ CoreSite.WS_CACHE_TABLE ], + canBeCleared: [CoreSite.WS_CACHE_TABLE], tables: [ { name: CoreSite.WS_CACHE_TABLE, @@ -184,29 +189,29 @@ export class CoreSitesProvider { { name: 'id', type: 'TEXT', - primaryKey: true + primaryKey: true, }, { name: 'data', - type: 'TEXT' + type: 'TEXT', }, { name: 'key', - type: 'TEXT' + type: 'TEXT', }, { name: 'expirationTime', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'component', - type: 'TEXT' + type: 'TEXT', }, { name: 'componentId', - type: 'INTEGER' - } - ] + type: 'INTEGER', + }, + ], }, { name: CoreSite.CONFIG_TABLE, @@ -215,15 +220,15 @@ export class CoreSitesProvider { name: 'name', type: 'TEXT', unique: true, - notNull: true + notNull: true, }, { - name: 'value' - } - ] - } + name: 'value', + }, + ], + }, ], - async migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise { + async migrate(db: SQLiteDB, oldVersion: number): Promise { if (oldVersion && oldVersion < 2) { const newTable = CoreSite.WS_CACHE_TABLE; const oldTable = 'wscache'; @@ -246,7 +251,7 @@ export class CoreSitesProvider { // Error deleting old table, ignore. } } - } + }, }; constructor() { @@ -265,7 +270,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): any { + getDemoSiteData(name: string): {[name: string]: CoreSitesDemoSiteData} { const demoSites = CoreConfigConstants.demo_sites; name = name.toLowerCase(); @@ -287,11 +292,11 @@ export class CoreSitesProvider { siteUrl = CoreUrlUtils.instance.formatURL(siteUrl); if (!CoreUrlUtils.instance.isHttpURL(siteUrl)) { - return Promise.reject(Translate.instance.instant('core.login.invalidsite')); + return Promise.reject(new CoreError(Translate.instance.instant('core.login.invalidsite'))); } else if (!CoreApp.instance.isOnline()) { - return Promise.reject(Translate.instance.instant('core.networkerrormsg')); + return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); } else { - return this.checkSiteWithProtocol(siteUrl, protocol).catch((error) => { + return this.checkSiteWithProtocol(siteUrl, protocol).catch((error: CoreSiteError) => { // Do not continue checking if a critical error happened. if (error.critical) { return Promise.reject(error); @@ -300,7 +305,7 @@ export class CoreSitesProvider { // Retry with the other protocol. protocol = protocol == 'https://' ? 'http://' : 'https://'; - return this.checkSiteWithProtocol(siteUrl, protocol).catch((secondError) => { + return this.checkSiteWithProtocol(siteUrl, protocol).catch((secondError: CoreSiteError) => { if (secondError.critical) { return Promise.reject(secondError); } @@ -326,15 +331,17 @@ export class CoreSitesProvider { * @return A promise resolved when the site is checked. */ checkSiteWithProtocol(siteUrl: string, protocol: string): Promise { - let publicConfig; + let publicConfig: CoreSitePublicConfigResponse; // Now, replace the siteUrl with the protocol. - siteUrl = siteUrl.replace(/^http(s)?\:\/\//i, protocol); + siteUrl = siteUrl.replace(/^https?:\/\//i, protocol); - return this.siteExists(siteUrl).catch((error) => { + return this.siteExists(siteUrl).catch((error: CoreSiteError) => { // Do not continue checking if WS are not enabled. if (error.errorcode == 'enablewsdescription') { - return rejectWithCriticalError(error.error, error.errorcode); + error.critical = true; + + return Promise.reject(error); } // Site doesn't exist. Try to add or remove 'www'. @@ -343,13 +350,15 @@ export class CoreSitesProvider { return this.siteExists(treatedUrl).then(() => { // Success, use this new URL as site url. siteUrl = treatedUrl; - }).catch((secondError) => { + }).catch((secondError: CoreSiteError) => { // Do not continue checking if WS are not enabled. if (secondError.errorcode == 'enablewsdescription') { - return rejectWithCriticalError(secondError.error, secondError.errorcode); + secondError.critical = true; + + return Promise.reject(secondError); } - // Return the error message. + // Return the error. if (CoreTextUtils.instance.getErrorMessageFromError(error)) { return Promise.reject(error); } else { @@ -367,21 +376,27 @@ export class CoreSitesProvider { 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): any => { + return temporarySite.getPublicConfig().then((config) => { publicConfig = config; // Check that the user can authenticate. if (!config.enablewebservices) { - return rejectWithCriticalError(Translate.instance.instant('core.login.webservicesnotenabled')); + return Promise.reject(new CoreSiteError({ + message: Translate.instance.instant('core.login.webservicesnotenabled'), + })); } else if (!config.enablemobilewebservice) { - return rejectWithCriticalError(Translate.instance.instant('core.login.mobileservicesnotenabled')); + 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; } - return rejectWithCriticalError(message); + return Promise.reject(new CoreSiteError({ + message, + })); } // Everything ok. @@ -390,27 +405,27 @@ export class CoreSitesProvider { } return data; - }, (error): any => { + }, async (error) => { // Error, check if not supported. if (error.available === 1) { // Service supported but an error happened. Return error. - error.critical = true; - if (error.errorcode == 'codingerror') { // This could be caused by a redirect. Check if it's the case. - return CoreUtils.instance.checkRedirect(siteUrl).then((redirect) => { - 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'); - } + const redirect = await CoreUtils.instance.checkRedirect(siteUrl); - return Promise.reject(error); - }); + 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(error); + return Promise.reject(new CoreSiteError({ + message: error.error, + errorcode: error.errorcode, + critical: true, + })); } return data; @@ -418,24 +433,18 @@ export class CoreSitesProvider { } return data; - }, (error) => { + }, (error: CoreError) => // Local mobile check returned an error. This only happens if the plugin is installed and it returns an error. - return rejectWithCriticalError(error); - }).then((data) => { + Promise.reject(new CoreSiteError({ + message: error.message, + critical: true, + })), + ).then((data: LocalMobileResponse) => { siteUrl = temporarySite.getURL(); return { siteUrl, code: data.code, warning: data.warning, service: data.service, config: publicConfig }; }); }); - - // Return a rejected promise with a "critical" error. - function rejectWithCriticalError(message: string, errorCode?: string): Promise { - return Promise.reject({ - error: message, - errorcode: errorCode, - critical: true - }); - } } /** @@ -444,25 +453,41 @@ export class CoreSitesProvider { * @param siteUrl URL of the site to check. * @return A promise to be resolved if the site exists. */ - siteExists(siteUrl: string): Promise { - return Http.instance.post(siteUrl + '/login/token.php', {}).pipe(timeout(CoreWS.instance.getRequestTimeout())).toPromise() - .catch(() => { + async siteExists(siteUrl: string): Promise { + let data: CoreSitesLoginTokenResponse; + + try { + data = await Http.instance.post(siteUrl + '/login/token.php', {}).pipe(timeout(CoreWS.instance.getRequestTimeout())) + .toPromise(); + } catch (error) { // Default error messages are kinda bad, return our own message. - return Promise.reject({error: Translate.instance.instant('core.cannotconnecttrouble')}); - }).then((data: any) => { + throw new CoreSiteError({ + message: Translate.instance.instant('core.cannotconnecttrouble'), + }); + } - if (data === null) { - // Cannot connect. - return Promise.reject({error: Translate.instance.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION})}); - } + if (data === null) { + // Cannot connect. + throw new CoreSiteError({ + message: Translate.instance.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), + }); + } - if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) { - return Promise.reject({ errorcode: data.errorcode, error: data.error }); - } else if (data.error && data.error == 'Web services must be enabled in Advanced features.') { - return Promise.reject({ errorcode: 'enablewsdescription', error: data.error }); - } - // Other errors are not being checked because invalid login will be always raised and we cannot differ them. - }); + if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) { + throw new CoreSiteError({ + errorcode: data.errorcode, + message: data.error, + }); + } + + if (data.error && data.error == 'Web services must be enabled in Advanced features.') { + throw new CoreSiteError({ + errorcode: 'enablewsdescription', + message: data.error, + }); + } + + // Other errors are not being checked because invalid login will be always raised and we cannot differ them. } /** @@ -475,10 +500,10 @@ 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 { + getUserToken(siteUrl: string, username: string, password: string, service?: string, retry?: boolean): + Promise { if (!CoreApp.instance.isOnline()) { - return Promise.reject(Translate.instance.instant('core.networkerrormsg')); + return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); } if (!service) { @@ -493,14 +518,13 @@ export class CoreSitesProvider { const loginUrl = siteUrl + '/login/token.php'; const promise = Http.instance.post(loginUrl, params).pipe(timeout(CoreWS.instance.getRequestTimeout())).toPromise(); - return promise.then((data: any): any => { + return promise.then((data: CoreSitesLoginTokenResponse) => { if (typeof data == 'undefined') { - return Promise.reject(Translate.instance.instant('core.cannotconnecttrouble')); + return Promise.reject(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.error != 'undefined') { // We only allow one retry (to avoid loops). if (!retry && data.errorcode == 'requirecorrectaccess') { @@ -511,24 +535,28 @@ export class CoreSitesProvider { // 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({ error: Translate.instance.instant('core.login.sitehasredirect') }); + return Promise.reject(new CoreSiteError({ + message: Translate.instance.instant('core.login.sitehasredirect'), + })); } else { - return Promise.reject({ error: data.error, errorcode: data.errorcode }); + return Promise.reject(new CoreSiteError({ + message: data.error, + errorcode: data.errorcode, + })); } }); - } else if (typeof data.errorcode != 'undefined') { - return Promise.reject({ error: data.error, errorcode: data.errorcode }); } else { - return Promise.reject(data.error); + return Promise.reject(new CoreSiteError({ + message: data.error, + errorcode: data.errorcode, + })); } } else { - return Promise.reject(Translate.instance.instant('core.login.invalidaccount')); + return Promise.reject(new CoreError(Translate.instance.instant('core.login.invalidaccount'))); } } } - }, () => { - return Promise.reject(Translate.instance.instant('core.cannotconnecttrouble')); - }); + }, () => Promise.reject(new CoreError(Translate.instance.instant('core.cannotconnecttrouble')))); } /** @@ -568,7 +596,6 @@ export class CoreSitesProvider { candidateSite.setInfo(info); candidateSite.setOAuthId(oauthId); candidateSite.setLoggedOut(false); - } else { // New site, set site ID and info. isNewSite = true; @@ -579,11 +606,10 @@ export class CoreSitesProvider { // Create database tables before login and before any WS call. return this.migrateSiteSchemas(candidateSite); } - - }).then(() => { + }).then(() => // Try to get the site config. - return this.getSiteConfig(candidateSite).catch((error) => { + 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); @@ -607,8 +633,8 @@ export class CoreSitesProvider { CoreEvents.instance.trigger(CoreEventsProvider.SITE_ADDED, info, siteId); return siteId; - }); - }); + }), + ); } return this.treatInvalidAppVersion(result, siteUrl); @@ -630,7 +656,7 @@ export class CoreSitesProvider { * @param siteId If site is already added, it will invalidate the token. * @return A promise rejected with the error info. */ - protected treatInvalidAppVersion(result: number, siteUrl: string, siteId?: string): Promise { + protected async treatInvalidAppVersion(result: number, siteUrl: string, siteId?: string): Promise { let errorCode; let errorKey; let translateParams; @@ -647,23 +673,17 @@ export class CoreSitesProvider { default: errorCode = 'invalidmoodleversion'; errorKey = 'core.login.invalidmoodleversion'; - translateParams = {$a: CoreSite.MINIMUM_MOODLE_VERSION}; + translateParams = { $a: CoreSite.MINIMUM_MOODLE_VERSION }; } - let promise; - if (siteId) { - promise = this.setSiteLoggedOut(siteId, true); - } else { - promise = Promise.resolve(); + await this.setSiteLoggedOut(siteId, true); } - return promise.then(() => { - return Promise.reject({ - error: Translate.instance.instant(errorKey, translateParams), - errorcode: errorCode, - loggedout: true - }); + throw new CoreSiteError({ + message: Translate.instance.instant(errorKey, translateParams), + errorcode: errorCode, + loggedOut: true, }); } @@ -709,7 +729,7 @@ export class CoreSitesProvider { * @param info Site info. * @return Either VALID_VERSION, WORKPLACE_APP, MOODLE_APP or INVALID_VERSION. */ - protected isValidMoodleVersion(info: any): number { + protected isValidMoodleVersion(info: CoreSiteInfoResponse): number { if (!info) { return this.INVALID_VERSION; } @@ -745,10 +765,8 @@ export class CoreSitesProvider { * @param info Site info. * @return Either VALID_VERSION, WORKPLACE_APP or MOODLE_APP. */ - protected validateWorkplaceVersion(info: any): number { - const isWorkplace = !!info.functions && info.functions.some((func) => { - return func.name == 'tool_program_get_user_programs'; - }); + protected validateWorkplaceVersion(info: CoreSiteInfoResponse): number { + const isWorkplace = !!info.functions && info.functions.some((func) => func.name == 'tool_program_get_user_programs'); if (typeof this.isWPApp == 'undefined') { this.isWPApp = false; // @todo @@ -780,22 +798,6 @@ export class CoreSitesProvider { return ''; } - /** - * Check if site info is valid. If it's not, return error message. - * - * @param info Site info. - * @return True if valid, object with error message to show and its params if not valid. - */ - protected validateSiteInfo(info: any): any { - if (!info.firstname || !info.lastname) { - const moodleLink = `${info.siteurl}`; - - return { error: 'core.requireduserdatamissing', params: { $a: moodleLink } }; - } - - return true; - } - /** * Saves a site in local DB. * @@ -808,8 +810,8 @@ 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: any, privateToken: string = '', config?: any, - 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 = { @@ -823,7 +825,7 @@ export class CoreSitesProvider { oauthId, }; - return this.appDB.insertRecord(CoreSitesProvider.SITES_TABLE, entry); + await this.appDB.insertRecord(SITES_TABLE, entry); } /** @@ -842,15 +844,15 @@ 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. */ - checkRequiredMinimumVersion(config: any, siteId?: string): Promise { + 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); if (requiredVersion > appVersion) { const storesConfig: CoreStoreConfig = { - android: config.tool_mobile_androidappid || false, - ios: config.tool_mobile_iosappid || false, + 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, @@ -866,7 +868,6 @@ export class CoreSitesProvider { 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(() => { // Do nothing. @@ -874,21 +875,17 @@ export class CoreSitesProvider { if (siteId) { // Logout if it's the currentSite. - const promise = siteId == this.getCurrentSiteId() ? this.logout() : Promise.resolve(); + if (siteId == this.getCurrentSiteId()) { + await this.logout(); + } - return promise.then(() => { - // Always expire the token. - return this.setSiteLoggedOut(siteId, true); - }).then(() => { - return Promise.reject(null); - }); + // Always expire the token. + await this.setSiteLoggedOut(siteId, true); } - return Promise.reject(null); + throw new CoreError('Current app version is lower than required version.'); } } - - return Promise.resolve(); } /** @@ -920,48 +917,55 @@ export class CoreSitesProvider { * @param params Params of the page to go once authenticated if logged out. * @return Promise resolved with true if site is loaded, resolved with false if cannot login. */ - loadSite(siteId: string, pageName?: string, params?: any): Promise { + async loadSite(siteId: string, pageName?: string, params?: Record): Promise { this.logger.debug(`Load site ${siteId}`); - return this.getSite(siteId).then((site) => { - this.currentSite = site; + const site = await this.getSite(siteId); - if (site.isLoggedOut()) { - // Logged out, trigger session expired event and stop. - CoreEvents.instance.trigger(CoreEventsProvider.SESSION_EXPIRED, { - pageName, - params, - }, site.getId()); + this.currentSite = site; - return false; + if (site.isLoggedOut()) { + // Logged out, trigger session expired event and stop. + CoreEvents.instance.trigger(CoreEventsProvider.SESSION_EXPIRED, { + pageName, + params, + }, site.getId()); + + return false; + } + + // Check if local_mobile was installed to Moodle. + try { + await site.checkIfLocalMobileInstalledAndNotUsed(); + + // Local mobile was added. Throw invalid session to force reconnect and create a new token. + CoreEvents.instance.trigger(CoreEventsProvider.SESSION_EXPIRED, { + pageName, + params, + }, siteId); + + return false; + } catch (error) { + let config: CoreSitePublicConfigResponse; + + try { + config = await site.getPublicConfig(); + } catch (error) { + // Error getting config, probably the site doesn't have the WS } - // Check if local_mobile was installed to Moodle. - return site.checkIfLocalMobileInstalledAndNotUsed().then(() => { - // Local mobile was added. Throw invalid session to force reconnect and create a new token. - CoreEvents.instance.trigger(CoreEventsProvider.SESSION_EXPIRED, { - pageName, - params, - }, siteId); + try { + await this.checkRequiredMinimumVersion(config); + this.login(siteId); + // Update site info. We don't block the UI. + this.updateSiteInfo(siteId); + + return true; + } catch (error) { return false; - }, () => { - return site.getPublicConfig().catch(() => { - return {}; - }).then((config) => { - return this.checkRequiredMinimumVersion(config).then(() => { - this.login(siteId); - - // Update site info. We don't block the UI. - this.updateSiteInfo(siteId); - - return true; - }).catch(() => { - return false; - }); - }); - }); - }); + } + } } /** @@ -1045,7 +1049,7 @@ export class CoreSitesProvider { delete this.sites[siteId]; try { - await this.appDB.deleteRecords(CoreSitesProvider.SITES_TABLE, { id: siteId }); + await this.appDB.deleteRecords(SITES_TABLE, { id: siteId }); } catch (err) { // DB remove shouldn't fail, but we'll go ahead even if it does. } @@ -1064,7 +1068,7 @@ export class CoreSitesProvider { async hasSites(): Promise { await this.dbReady; - const count = await this.appDB.countRecords(CoreSitesProvider.SITES_TABLE); + const count = await this.appDB.countRecords(SITES_TABLE); return count > 0; } @@ -1083,14 +1087,14 @@ export class CoreSitesProvider { return this.currentSite; } - throw null; + throw new CoreError('No current site found.'); } else if (this.currentSite && this.currentSite.getId() == siteId) { return this.currentSite; } else if (typeof this.sites[siteId] != 'undefined') { return this.sites[siteId]; } else { // Retrieve and create the site. - const data = await this.appDB.getRecord(CoreSitesProvider.SITES_TABLE, { id: siteId }); + const data = await this.appDB.getRecord(SITES_TABLE, { id: siteId }); return this.makeSiteFromSiteListEntry(data); } @@ -1145,9 +1149,7 @@ export class CoreSitesProvider { * @return Promise resolved with the database. */ getSiteDb(siteId: string): Promise { - return this.getSite(siteId).then((site) => { - return site.getDb(); - }); + return this.getSite(siteId).then((site) => site.getDb()); } /** @@ -1157,9 +1159,7 @@ export class CoreSitesProvider { * @return Promise resolved with site home ID. */ getSiteHomeId(siteId?: string): Promise { - return this.getSite(siteId).then((site) => { - return site.getSiteHomeId(); - }); + return this.getSite(siteId).then((site) => site.getSiteHomeId()); } /** @@ -1171,7 +1171,7 @@ export class CoreSitesProvider { async getSites(ids?: string[]): Promise { await this.dbReady; - const sites = await this.appDB.getAllRecords(CoreSitesProvider.SITES_TABLE); + const sites = await this.appDB.getAllRecords(SITES_TABLE); const formattedSites = []; sites.forEach((site) => { @@ -1179,13 +1179,13 @@ export class CoreSitesProvider { // Parse info. const siteInfo = site.info ? CoreTextUtils.instance.parseJSON(site.info) : site.info; const basicInfo: CoreSiteBasicInfo = { - id: site.id, - siteUrl: site.siteUrl, - fullName: siteInfo && siteInfo.fullname, - siteName: CoreConfigConstants.sitename ? CoreConfigConstants.sitename : siteInfo && siteInfo.sitename, - avatar: siteInfo && siteInfo.userpictureurl, - siteHomeId: siteInfo && siteInfo.siteid || 1, - }; + id: site.id, + siteUrl: site.siteUrl, + fullName: siteInfo && siteInfo.fullname, + siteName: CoreConfigConstants.sitename ? CoreConfigConstants.sitename : siteInfo && siteInfo.sitename, + avatar: siteInfo && siteInfo.userpictureurl, + siteHomeId: siteInfo && siteInfo.siteid || 1, + }; formattedSites.push(basicInfo); } }); @@ -1231,11 +1231,9 @@ export class CoreSitesProvider { async getLoggedInSitesIds(): Promise { await this.dbReady; - const sites = await this.appDB.getRecords(CoreSitesProvider.SITES_TABLE, {loggedOut : 0}); + const sites = await this.appDB.getRecords(SITES_TABLE, { loggedOut : 0 }); - return sites.map((site) => { - return site.id; - }); + return sites.map((site) => site.id); } /** @@ -1246,11 +1244,9 @@ export class CoreSitesProvider { async getSitesIds(): Promise { await this.dbReady; - const sites = await this.appDB.getAllRecords(CoreSitesProvider.SITES_TABLE); + const sites = await this.appDB.getAllRecords(SITES_TABLE); - return sites.map((site) => { - return site.id; - }); + return sites.map((site) => site.id); } /** @@ -1267,7 +1263,7 @@ export class CoreSitesProvider { siteId, }; - await this.appDB.insertRecord(CoreSitesProvider.CURRENT_SITE_TABLE, entry); + await this.appDB.insertRecord(CURRENT_SITE_TABLE, entry); CoreEvents.instance.trigger(CoreEventsProvider.LOGIN, {}, siteId); } @@ -1284,7 +1280,7 @@ export class CoreSitesProvider { const promises = []; if (this.currentSite) { - const siteConfig = this.currentSite.getStoredConfig(); + const siteConfig = this.currentSite.getStoredConfig(); siteId = this.currentSite.getId(); this.currentSite = undefined; @@ -1293,7 +1289,7 @@ export class CoreSitesProvider { promises.push(this.setSiteLoggedOut(siteId, true)); } - promises.push(this.appDB.deleteRecords(CoreSitesProvider.CURRENT_SITE_TABLE, { id: 1 })); + promises.push(this.appDB.deleteRecords(CURRENT_SITE_TABLE, { id: 1 })); } try { @@ -1308,9 +1304,9 @@ export class CoreSitesProvider { * * @return Promise resolved if a session is restored. */ - async restoreSession(): Promise { + async restoreSession(): Promise { if (this.sessionRestored) { - return Promise.reject(null); + return Promise.reject(new CoreError('Session already restored.')); } await this.dbReady; @@ -1318,11 +1314,11 @@ export class CoreSitesProvider { this.sessionRestored = true; try { - const currentSite = await this.appDB.getRecord(CoreSitesProvider.CURRENT_SITE_TABLE, { id: 1 }); + const currentSite = await this.appDB.getRecord(CURRENT_SITE_TABLE, { id: 1 }); const siteId = currentSite.siteId; this.logger.debug(`Restore session in site ${siteId}`); - return this.loadSite(siteId); + await this.loadSite(siteId); } catch (err) { // No current session. } @@ -1335,18 +1331,18 @@ export class CoreSitesProvider { * @param loggedOut True to set the site as logged out, false otherwise. * @return Promise resolved when done. */ - async setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise { + async setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise { await this.dbReady; const site = await this.getSite(siteId); const newValues = { token: '', // Erase the token for security. - loggedOut: loggedOut ? 1 : 0 + loggedOut: loggedOut ? 1 : 0, }; site.setLoggedOut(loggedOut); - return this.appDB.updateRecords(CoreSitesProvider.SITES_TABLE, newValues, { id: siteId }); + await this.appDB.updateRecords(SITES_TABLE, newValues, { id: siteId }); } /** @@ -1365,12 +1361,12 @@ export class CoreSitesProvider { * @param privateToken User's private token. * @return A promise resolved when the site is updated. */ - updateSiteToken(siteUrl: string, username: string, token: string, privateToken: string = ''): Promise { + async updateSiteToken(siteUrl: string, username: string, token: string, privateToken: string = ''): Promise { const siteId = this.createSiteID(siteUrl, username); - return this.updateSiteTokenBySiteId(siteId, token, privateToken).then(() => { - return this.login(siteId); - }); + await this.updateSiteTokenBySiteId(siteId, token, privateToken); + + await this.login(siteId); } /** @@ -1381,7 +1377,7 @@ export class CoreSitesProvider { * @param privateToken User's private token. * @return A promise resolved when the site is updated. */ - async updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise { + async updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise { await this.dbReady; const site = await this.getSite(siteId); @@ -1395,7 +1391,7 @@ export class CoreSitesProvider { site.privateToken = privateToken; site.setLoggedOut(false); // Token updated means the user authenticated again, not logged out anymore. - return this.appDB.updateRecords(CoreSitesProvider.SITES_TABLE, newValues, { id: siteId }); + await this.appDB.updateRecords(SITES_TABLE, newValues, { id: siteId }); } /** @@ -1404,13 +1400,12 @@ export class CoreSitesProvider { * @param siteid Site's ID. * @return A promise resolved when the site is updated. */ - async updateSiteInfo(siteId: string): Promise { + async updateSiteInfo(siteId: string): Promise { await this.dbReady; const site = await this.getSite(siteId); try { - const info = await site.fetchSiteInfo(); site.setInfo(info); @@ -1429,9 +1424,10 @@ export class CoreSitesProvider { // Error getting config, keep the current one. } - const newValues: any = { + const newValues = { info: JSON.stringify(info), loggedOut: site.isLoggedOut() ? 1 : 0, + config: undefined, }; if (typeof config != 'undefined') { @@ -1440,7 +1436,7 @@ export class CoreSitesProvider { } try { - await this.appDB.updateRecords(CoreSitesProvider.SITES_TABLE, newValues, { id: siteId }); + await this.appDB.updateRecords(SITES_TABLE, newValues, { id: siteId }); } finally { CoreEvents.instance.trigger(CoreEventsProvider.SITE_UPDATED, info, siteId); } @@ -1456,7 +1452,7 @@ export class CoreSitesProvider { * @param username Username. * @return A promise to be resolved when the site is updated. */ - updateSiteInfoByUrl(siteUrl: string, username: string): Promise { + updateSiteInfoByUrl(siteUrl: string, username: string): Promise { const siteId = this.createSiteID(siteUrl, username); return this.updateSiteInfo(siteId); @@ -1499,7 +1495,7 @@ export class CoreSitesProvider { } try { - const siteEntries = await this.appDB.getAllRecords(CoreSitesProvider.SITES_TABLE); + const siteEntries = await this.appDB.getAllRecords(SITES_TABLE); const ids = []; const promises = []; @@ -1532,7 +1528,7 @@ export class CoreSitesProvider { async getStoredCurrentSiteId(): Promise { await this.dbReady; - const currentSite = await this.appDB.getRecord(CoreSitesProvider.CURRENT_SITE_TABLE, { id: 1 }); + const currentSite = await this.appDB.getRecord(CURRENT_SITE_TABLE, { id: 1 }); return currentSite.siteId; } @@ -1543,7 +1539,7 @@ export class CoreSitesProvider { * @param siteUrl URL of the site. * @return Promise resolved with the public config. */ - getSitePublicConfig(siteUrl: string): Promise { + getSitePublicConfig(siteUrl: string): Promise { const temporarySite = new CoreSite(undefined, siteUrl); return temporarySite.getPublicConfig(); @@ -1555,13 +1551,15 @@ export class CoreSitesProvider { * @param site The site to get the config. * @return Promise resolved with config if available. */ - protected getSiteConfig(site: CoreSite): Promise { + protected async getSiteConfig(site: CoreSite): Promise { if (!site.wsAvailable('tool_mobile_get_config')) { // WS not available, cannot get config. - return Promise.resolve(); + return; } - return site.getConfig(undefined, true); + const config = await site.getConfig(undefined, true); + + return config; } /** @@ -1572,9 +1570,7 @@ export class CoreSitesProvider { * @return Promise resolved with true if disabled. */ isFeatureDisabled(name: string, siteId?: string): Promise { - return this.getSite(siteId).then((site) => { - return site.isFeatureDisabled(name); - }); + return this.getSite(siteId).then((site) => site.isFeatureDisabled(name)); } /** @@ -1616,17 +1612,6 @@ export class CoreSitesProvider { return site && site.wsAvailable(method, checkPrefix); } - /** - * Check if a site is a legacy site by its info. - * - * @param info The site info. - * @return Whether it's a legacy Moodle. - * @deprecated since 3.7.1 - */ - isLegacyMoodleByInfo(info: any): boolean { - return false; - } - /** * Register a site schema. * @@ -1659,7 +1644,6 @@ export class CoreSitesProvider { // Add the schema to the list. It's done in the end to prevent a schema being applied twice. this.siteSchemas[schema.name] = schema; } - } else if (!schema.onlyCurrentSite) { // Add the schema to the list, it will be applied when the sites are created. this.siteSchemas[schema.name] = schema; @@ -1672,8 +1656,7 @@ export class CoreSitesProvider { * @param site Site. * @return Promise resolved when done. */ - migrateSiteSchemas(site: CoreSite): Promise { - + migrateSiteSchemas(site: CoreSite): Promise { if (this.siteSchemasMigration[site.id]) { return this.siteSchemasMigration[site.id]; } @@ -1681,9 +1664,8 @@ export class CoreSitesProvider { this.logger.debug(`Migrating all schemas of ${site.id}`); // First create tables not registerd with name/version. - const promise = site.getDb().createTablesFromSchema(this.siteTablesSchemas).then(() => { - return this.applySiteSchemas(site, this.siteSchemas); - }); + const promise = site.getDb().createTablesFromSchema(this.siteTablesSchemas) + .then(() => this.applySiteSchemas(site, this.siteSchemas)); this.siteSchemasMigration[site.id] = promise; @@ -1699,44 +1681,54 @@ export class CoreSitesProvider { * @param schemas Schemas to migrate. * @return Promise resolved when done. */ - protected applySiteSchemas(site: CoreSite, schemas: {[name: string]: CoreRegisteredSiteSchema}): Promise { + protected async applySiteSchemas(site: CoreSite, schemas: {[name: string]: CoreRegisteredSiteSchema}): Promise { const db = site.getDb(); // Fetch installed versions of the schema. - return db.getAllRecords(CoreSitesProvider.SCHEMA_VERSIONS_TABLE).then((records) => { - const versions = {}; - records.forEach((record) => { - versions[record.name] = record.version; - }); + const records = await db.getAllRecords(SCHEMA_VERSIONS_TABLE); - const promises = []; - for (const name in schemas) { - const schema = schemas[name]; - const oldVersion = versions[name] || 0; - if (oldVersion >= schema.version || (schema.siteId && site.getId() != schema.siteId)) { - // Version already applied or the schema shouldn't be registered to this site. - continue; - } + const versions: {[name: string]: number} = {}; + records.forEach((record) => { + versions[record.name] = record.version; + }); - this.logger.debug(`Migrating schema '${name}' of ${site.id} from version ${oldVersion} to ${schema.version}`); - - let promise: Promise = Promise.resolve(); - if (schema.tables) { - promise = promise.then(() => db.createTablesFromSchema(schema.tables)); - } - if (schema.migrate) { - promise = promise.then(() => schema.migrate(db, oldVersion, site.id)); - } - - // Set installed version. - promise = promise.then(() => db.insertRecord(CoreSitesProvider.SCHEMA_VERSIONS_TABLE, - {name, version: schema.version})); - - promises.push(promise); + const promises = []; + for (const name in schemas) { + const schema = schemas[name]; + const oldVersion = versions[name] || 0; + if (oldVersion >= schema.version || (schema.siteId && site.getId() != schema.siteId)) { + // Version already applied or the schema shouldn't be registered to this site. + continue; } - return Promise.all(promises); - }); + this.logger.debug(`Migrating schema '${name}' of ${site.id} from version ${oldVersion} to ${schema.version}`); + + promises.push(this.applySiteSchema(site, schema, oldVersion)); + } + + await Promise.all(promises); + } + + /** + * Install and upgrade the supplied schema for a certain site. + * + * @param site Site. + * @param schema Schema to migrate. + * @param oldVersion Old version of the schema. + * @return Promise resolved when done. + */ + protected async applySiteSchema(site: CoreSite, schema: CoreRegisteredSiteSchema, oldVersion: number): Promise { + const db = site.getDb(); + + if (schema.tables) { + await db.createTablesFromSchema(schema.tables); + } + if (schema.migrate) { + await schema.migrate(db, oldVersion, site.id); + } + + // Set installed version. + await db.insertRecord(SCHEMA_VERSIONS_TABLE, { name, version: schema.version }); } /** @@ -1747,7 +1739,7 @@ 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[]}> { + 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 = { @@ -1759,7 +1751,7 @@ export class CoreSitesProvider { // 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())); + CoreUrlUtils.instance.removeProtocolAndWWW(site.getURL())); const treatedUrl = CoreTextUtils.instance.removeEndingSlash(CoreUrlUtils.instance.removeProtocolAndWWW(url)); if (siteUrl == treatedUrl) { @@ -1830,9 +1822,11 @@ export class CoreSitesProvider { * @param search Searched text. * @return Site info list. */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars async findSites(search: string): Promise { return []; } + } export class CoreSites extends makeSingleton(CoreSitesProvider) {} @@ -1864,7 +1858,7 @@ export type CoreSiteCheckResponse = { /** * Site public config (if available). */ - config?: any; + config?: CoreSitePublicConfigResponse; }; /** @@ -1967,7 +1961,7 @@ export type CoreSiteSchema = { * @param siteId Site Id to migrate. * @return Promise resolved when done. */ - migrate?(db: SQLiteDB, oldVersion: number, siteId: string): Promise | void; + migrate?(db: SQLiteDB, oldVersion: number, siteId: string): Promise | void; }; /** @@ -2032,3 +2026,25 @@ export type CoreSitesCommonWSOptions = { readingStrategy?: CoreSitesReadingStrategy; // Reading strategy. siteId?: string; // Site ID. If not defined, current site. }; + +/** + * Data about a certain demo site. + */ +export type CoreSitesDemoSiteData = { + url: string; + username: string; + password: string; +}; + +/** + * Response of calls to login/token.php. + */ +export type CoreSitesLoginTokenResponse = { + token?: string; + privatetoken?: string; + error?: string; + errorcode?: string; + stacktrace?: string; + debuginfo?: string; + reproductionlink?: string; +}; From e514000623eb3ff1ae06e6a5f0c05f7cf513870d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:31:47 +0200 Subject: [PATCH 12/17] MOBILE-3565 core: Fix some ESLint of CoreSyncProvider --- src/app/services/sync.ts | 45 +++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/app/services/sync.ts b/src/app/services/sync.ts index 1954a59e6..525c5c558 100644 --- a/src/app/services/sync.ts +++ b/src/app/services/sync.ts @@ -17,6 +17,8 @@ import { CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreSites, CoreSiteSchema } from '@services/sites'; import { makeSingleton } from '@singletons/core.singletons'; +const SYNC_TABLE = 'sync'; + /* * Service that provides some features regarding synchronization. */ @@ -24,36 +26,35 @@ import { makeSingleton } from '@singletons/core.singletons'; export class CoreSyncProvider { // Variables for the database. - protected SYNC_TABLE = 'sync'; protected siteSchema: CoreSiteSchema = { name: 'CoreSyncProvider', version: 1, tables: [ { - name: this.SYNC_TABLE, + name: SYNC_TABLE, columns: [ { name: 'component', type: 'TEXT', - notNull: true + notNull: true, }, { name: 'id', type: 'TEXT', - notNull: true + notNull: true, }, { name: 'time', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'warnings', - type: 'TEXT' - } + type: 'TEXT', + }, ], - primaryKeys: ['component', 'id'] - } - ] + primaryKeys: ['component', 'id'], + }, + ], }; // Store blocked sync objects. @@ -63,7 +64,7 @@ export class CoreSyncProvider { CoreSites.instance.registerSiteSchema(this.siteSchema); // Unblock all blocks on logout. - CoreEvents.instance.on(CoreEventsProvider.LOGOUT, (data) => { + CoreEvents.instance.on(CoreEventsProvider.LOGOUT, (data: {siteId: string}) => { this.clearAllBlocks(data.siteId); }); } @@ -125,32 +126,33 @@ export class CoreSyncProvider { /** * Returns a sync record. + * * @param component Component name. * @param id Unique ID per component. * @param siteId Site ID. If not defined, current site. * @return Record if found or reject. */ - getSyncRecord(component: string, id: string | number, siteId?: string): Promise { - return CoreSites.instance.getSiteDb(siteId).then((db) => { - return db.getRecord(this.SYNC_TABLE, { component: component, id: id }); - }); + getSyncRecord(component: string, id: string | number, siteId?: string): Promise> { + return CoreSites.instance.getSiteDb(siteId).then((db) => db.getRecord(SYNC_TABLE, { component: component, id: id })); } /** * Inserts or Updates info of a sync record. + * * @param component Component name. * @param id Unique ID per component. * @param data Data that updates the record. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with done. */ - insertOrUpdateSyncRecord(component: string, id: string | number, data: any, siteId?: string): Promise { - return CoreSites.instance.getSiteDb(siteId).then((db) => { - data.component = component; - data.id = id; + async insertOrUpdateSyncRecord(component: string, id: string | number, data: Record, siteId?: string): + Promise { + const db = await CoreSites.instance.getSiteDb(siteId); - return db.insertRecord(this.SYNC_TABLE, data); - }); + data.component = component; + data.id = id; + + await db.insertRecord(SYNC_TABLE, data); } /** @@ -206,6 +208,7 @@ export class CoreSyncProvider { delete this.blockedItems[siteId][uniqueId][operation]; } } + } export class CoreSync extends makeSingleton(CoreSyncProvider) {} From 5cb0c6fe0cdd8850d643096a613511545b5a2b80 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:32:09 +0200 Subject: [PATCH 13/17] MOBILE-3565 core: Fix some ESLint of CoreUpdateManagerProvider --- src/app/services/update-manager.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/services/update-manager.ts b/src/app/services/update-manager.ts index b751e5d6d..96de478d9 100644 --- a/src/app/services/update-manager.ts +++ b/src/app/services/update-manager.ts @@ -20,6 +20,8 @@ import CoreConfigConstants from '@app/config.json'; import { makeSingleton } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; +const VERSION_APPLIED = 'version_applied'; + /** * Factory to handle app updates. This factory shouldn't be used outside of core. * @@ -27,12 +29,12 @@ import { CoreLogger } from '@singletons/logger'; */ @Injectable() export class CoreUpdateManagerProvider implements CoreInitHandler { + // Data for init delegate. name = 'CoreUpdateManager'; priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 300; blocking = true; - protected VERSION_APPLIED = 'version_applied'; protected logger: CoreLogger; constructor() { @@ -45,11 +47,11 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { * * @return Promise resolved when the update process finishes. */ - async load(): Promise { + async load(): Promise { const promises = []; const versionCode = CoreConfigConstants.versioncode; - const versionApplied: number = await CoreConfig.instance.get(this.VERSION_APPLIED, 0); + const versionApplied: number = await CoreConfig.instance.get(VERSION_APPLIED, 0); if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) { // @todo: H5P update. @@ -58,11 +60,12 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { try { await Promise.all(promises); - await CoreConfig.instance.set(this.VERSION_APPLIED, versionCode); + await CoreConfig.instance.set(VERSION_APPLIED, versionCode); } catch (error) { this.logger.error(`Error applying update from ${versionApplied} to ${versionCode}`, error); } } + } export class CoreUpdateManager extends makeSingleton(CoreUpdateManagerProvider) {} From e7de01acf61480075274235e7ff476a982b80633 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:33:50 +0200 Subject: [PATCH 14/17] MOBILE-3565 core: Fix some ESLint of CoreWSProvider --- src/app/services/ws.ts | 472 ++++++++++++++++++++--------------------- 1 file changed, 235 insertions(+), 237 deletions(-) diff --git a/src/app/services/ws.ts b/src/app/services/ws.ts index 61e3b3c27..fa7fc3884 100644 --- a/src/app/services/ws.ts +++ b/src/app/services/ws.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { HttpResponse } from '@angular/common/http'; +import { HttpResponse, HttpParams } from '@angular/common/http'; import { FileUploadOptions } from '@ionic-native/file-transfer/ngx'; import { Md5 } from 'ts-md5/dist/md5'; @@ -25,20 +25,28 @@ import { CoreApp } from '@services/app'; import { CoreFile, CoreFileProvider } from '@services/file'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { CoreConstants } from '@core/constants'; +import { CoreError } from '@classes/errors/error'; import { CoreInterceptor } from '@classes/interceptor'; -import { makeSingleton, Translate, FileTransfer, Http, Platform } from '@singletons/core.singletons'; +import { makeSingleton, Translate, FileTransfer, Http, Platform, NativeHttp } from '@singletons/core.singletons'; +import { CoreArray } from '@singletons/array'; import { CoreLogger } from '@singletons/logger'; +import { CoreWSError } from '@classes/errors/wserror'; +import { CoreAjaxError } from '@classes/errors/ajaxerror'; +import { CoreAjaxWSError } from '@classes/errors/ajaxwserror'; /** * This service allows performing WS calls and download/upload files. */ @Injectable() export class CoreWSProvider { + protected logger: CoreLogger; - protected mimeTypeCache = {}; // A "cache" to store file mimetypes to prevent performing too many HEAD requests. - protected ongoingCalls = {}; - protected retryCalls = []; + protected mimeTypeCache: {[url: string]: string} = {}; // 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[] = []; protected retryTimeout = 0; constructor() { @@ -46,7 +54,7 @@ export class CoreWSProvider { Platform.instance.ready().then(() => { if (CoreApp.instance.isIOS()) { - ( cordova).plugin.http.setHeader('User-Agent', navigator.userAgent); + NativeHttp.instance.setHeader('*', 'User-Agent', navigator.userAgent); } }); } @@ -61,20 +69,15 @@ export class CoreWSProvider { * @return Deferred promise resolved with the response data in success and rejected with the error message * if it fails. */ - protected addToRetryQueue(method: string, siteUrl: string, ajaxData: any, preSets: CoreWSPreSets): Promise { - const call: any = { + protected addToRetryQueue(method: string, siteUrl: string, data: unknown, preSets: CoreWSPreSets): Promise { + const call = { method, siteUrl, - ajaxData, + data, preSets, - deferred: {} + deferred: CoreUtils.instance.promiseDefer(), }; - call.deferred.promise = new Promise((resolve, reject): void => { - call.deferred.resolve = resolve; - call.deferred.reject = reject; - }); - this.retryCalls.push(call); return call.deferred.promise; @@ -88,14 +91,11 @@ export class CoreWSProvider { * @param preSets Extra settings and information. * @return Promise resolved with the response data in success and rejected if it fails. */ - call(method: string, data: any, preSets: CoreWSPreSets): Promise { - - let siteUrl; - + call(method: string, data: unknown, preSets: CoreWSPreSets): Promise { if (!preSets) { - return Promise.reject(this.createFakeWSError('core.unexpectederror', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.unexpectederror'))); } else if (!CoreApp.instance.isOnline()) { - return Promise.reject(this.createFakeWSError('core.networkerrormsg', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); } preSets.typeExpected = preSets.typeExpected || 'object'; @@ -103,18 +103,18 @@ export class CoreWSProvider { preSets.responseExpected = true; } - data = Object.assign({}, data); // Create a new object so the changes don't affect the original data. - data.wsfunction = method; - data.wstoken = preSets.wsToken; - siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json'; + const dataToSend = Object.assign({}, data); // Create a new object so the changes don't affect the original data. + dataToSend['wsfunction'] = method; + dataToSend['wstoken'] = preSets.wsToken; + const siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json'; // There are some ongoing retry calls, wait for timeout. if (this.retryCalls.length > 0) { this.logger.warn('Calls locked, trying later...'); - return this.addToRetryQueue(method, siteUrl, data, preSets); + return this.addToRetryQueue(method, siteUrl, data, preSets); } else { - return this.performPost(method, siteUrl, data, preSets); + return this.performPost(method, siteUrl, data, preSets); } } @@ -130,17 +130,17 @@ export class CoreWSProvider { * - errorcode: Error code returned by the site (if any). * - available: 0 if unknown, 1 if available, -1 if not available. */ - callAjax(method: string, data: any, preSets: CoreWSAjaxPreSets): Promise { + callAjax(method: string, data: Record, preSets: CoreWSAjaxPreSets): Promise { const cacheParams = { methodname: method, args: data, }; - let promise = this.getPromiseHttp('ajax', preSets.siteUrl, cacheParams); + let promise = this.getPromiseHttp('ajax', preSets.siteUrl, cacheParams); if (!promise) { - promise = this.performAjax(method, data, preSets); - promise = this.setPromiseHttp(promise, 'ajax', preSets.siteUrl, cacheParams); + promise = this.performAjax(method, data, preSets); + promise = this.setPromiseHttp(promise, 'ajax', preSets.siteUrl, cacheParams); } return promise; @@ -154,7 +154,9 @@ 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 convertValuesToString(data: any, stripUnicode?: boolean): any { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = Array.isArray(data) ? [] : {}; for (const key in data) { @@ -210,15 +212,14 @@ export class CoreWSProvider { * @param needsTranslate If the message needs to be translated. * @param translateParams Translation params, if needed. * @return Fake WS error. + * @deprecated since 3.9.5. Just create the error directly. */ - createFakeWSError(message: string, needsTranslate?: boolean, translateParams?: {}): CoreWSError { + createFakeWSError(message: string, needsTranslate?: boolean, translateParams?: {[name: string]: string}): CoreError { if (needsTranslate) { message = Translate.instance.instant(message, translateParams); } - return { - message, - }; + return new CoreError(message); } /** @@ -230,71 +231,68 @@ export class CoreWSProvider { * @param onProgress Function to call on progress. * @return Promise resolved with the downloaded file. */ - downloadFile(url: string, path: string, addExtension?: boolean, onProgress?: (event: ProgressEvent) => any): 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()) { - return Promise.reject(Translate.instance.instant('core.networkerrormsg')); + throw new CoreError(Translate.instance.instant('core.networkerrormsg')); } // Use a tmp path to download the file and then move it to final location. // This is because if the download fails, the local file is deleted. const tmpPath = path + '.tmp'; - // Create the tmp file as an empty file. - return CoreFile.instance.createFile(tmpPath).then((fileEntry) => { + try { + // Create the tmp file as an empty file. + const fileEntry = await CoreFile.instance.createFile(tmpPath); + const transfer = FileTransfer.instance.create(); transfer.onProgress(onProgress); - return transfer.download(url, fileEntry.toURL(), true).then(() => { - let promise; + // Download the file in the tmp file. + await transfer.download(url, fileEntry.toURL(), true); - if (addExtension) { - const ext = CoreMimetypeUtils.instance.getFileExtension(path); + let extension = ''; - // Google Drive extensions will be considered invalid since Moodle usually converts them. - if (!ext || ext == 'gdoc' || ext == 'gsheet' || ext == 'gslides' || ext == 'gdraw' || ext == 'php') { - // Not valid, get the file's mimetype. - promise = this.getRemoteFileMimeType(url).then((mime) => { - if (mime) { - const remoteExt = CoreMimetypeUtils.instance.getExtension(mime, url); - // If the file is from Google Drive, ignore mimetype application/json. - if (remoteExt && (!ext || mime != 'application/json')) { - if (ext) { - // Remove existing extension since we will use another one. - path = CoreMimetypeUtils.instance.removeExtension(path); - } - path += '.' + remoteExt; + if (addExtension) { + extension = CoreMimetypeUtils.instance.getFileExtension(path); - return remoteExt; - } + // Google Drive extensions will be considered invalid since Moodle usually converts them. + if (!extension || CoreArray.contains(['gdoc', 'gsheet', 'gslides', 'gdraw', 'php'], extension)) { + // Not valid, get the file's mimetype. + const mimetype = await this.getRemoteFileMimeType(url); + + if (mimetype) { + const remoteExtension = CoreMimetypeUtils.instance.getExtension(mimetype, url); + // If the file is from Google Drive, ignore mimetype application/json. + if (remoteExtension && (!extension || mimetype != 'application/json')) { + if (extension) { + // Remove existing extension since we will use another one. + path = CoreMimetypeUtils.instance.removeExtension(path); } + path += '.' + remoteExtension; - return ext; - }); - } else { - promise = Promise.resolve(ext); + extension = remoteExtension; + } } - } else { - promise = Promise.resolve(''); } + } - return promise.then((extension) => { - return CoreFile.instance.moveFile(tmpPath, path).then((movedEntry) => { - // Save the extension. - movedEntry.extension = extension; - movedEntry.path = path; - this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`); + // Move the file to the final location. + const movedEntry: CoreWSDownloadedFileEntry = await CoreFile.instance.moveFile(tmpPath, path); - return movedEntry; - }); - }); - }); - }).catch((err) => { - this.logger.error(`Error downloading ${url} to ${path}`, err); + // Save the extension. + movedEntry.extension = extension; + movedEntry.path = path; + this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`); - return Promise.reject(err); - }); + return movedEntry; + } catch (error) { + this.logger.error(`Error downloading ${url} to ${path}`, error); + + throw error; + } } /** @@ -304,13 +302,11 @@ export class CoreWSProvider { * @param url Base URL of the HTTP request. * @param params Params of the HTTP request. */ - protected getPromiseHttp(method: string, url: string, params?: any): any { + protected getPromiseHttp(method: string, url: string, params?: Record): Promise { const queueItemId = this.getQueueItemId(method, url, params); if (typeof this.ongoingCalls[queueItemId] != 'undefined') { return this.ongoingCalls[queueItemId]; } - - return false; } /** @@ -334,10 +330,10 @@ export class CoreWSProvider { this.mimeTypeCache[url] = mimeType; return mimeType || ''; - }).catch(() => { + }).catch(() => // Error, resolve with empty mimetype. - return ''; - }); + '', + ); } /** @@ -355,10 +351,10 @@ export class CoreWSProvider { } return -1; - }).catch(() => { + }).catch(() => // Error, return -1. - return -1; - }); + -1, + ); } /** @@ -378,7 +374,7 @@ export class CoreWSProvider { * @param params Params of the HTTP request. * @return Queue item ID. */ - protected getQueueItemId(method: string, url: string, params?: any): string { + protected getQueueItemId(method: string, url: string, params?: Record): string { if (params) { url += '###' + CoreInterceptor.serialize(params); } @@ -397,14 +393,14 @@ export class CoreWSProvider { * - errorcode: Error code returned by the site (if any). * - available: 0 if unknown, 1 if available, -1 if not available. */ - protected performAjax(method: string, data: any, preSets: CoreWSAjaxPreSets): Promise { - - let promise; + 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 rejectWithError(this.createFakeWSError('core.unexpectederror', true)); + return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.unexpectederror'))); } else if (!CoreApp.instance.isOnline()) { - return rejectWithError(this.createFakeWSError('core.networkerrormsg', true)); + return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.networkerrormsg'))); } if (typeof preSets.responseExpected == 'undefined') { @@ -415,7 +411,7 @@ export class CoreWSProvider { const ajaxData = [{ index: 0, methodname: method, - args: this.convertValuesToString(data) + args: this.convertValuesToString(data), }]; // The info= parameter has no function. It is just to help with debugging. @@ -426,18 +422,19 @@ export class CoreWSProvider { // Send params using GET. siteUrl += '&args=' + encodeURIComponent(JSON.stringify(ajaxData)); - promise = this.sendHTTPRequest(siteUrl, { + promise = this.sendHTTPRequest(siteUrl, { method: 'get', }); } else { - promise = this.sendHTTPRequest(siteUrl, { + promise = this.sendHTTPRequest(siteUrl, { method: 'post', - data: ajaxData, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: ajaxData, serializer: 'json', }); } - return promise.then((response: HttpResponse) => { + return promise.then((response) => { let data = response.body; // Some moodle web services return null. @@ -448,39 +445,24 @@ export class CoreWSProvider { // Check if error. Ajax layer should always return an object (if error) or an array (if success). if (!data || typeof data != 'object') { - return rejectWithError(this.createFakeWSError('core.serverconnection', true)); + return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection'))); } else if (data.error) { - return rejectWithError(data); + return Promise.reject(new CoreAjaxWSError(data)); } // Get the first response since only one request was done. data = data[0]; if (data.error) { - return rejectWithError(data.exception); + return Promise.reject(new CoreAjaxWSError(data.exception)); } return data.data; }, (data) => { const available = data.status == 404 ? -1 : 0; - return rejectWithError(this.createFakeWSError('core.serverconnection', true), available); + return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection'), available)); }); - - // Convenience function to return an error. - function rejectWithError(exception: any, available?: number): Promise { - if (typeof available == 'undefined') { - if (exception.errorcode) { - available = exception.errorcode == 'invalidrecord' ? -1 : 1; - } else { - available = 0; - } - } - - exception.available = available; - - return Promise.reject(exception); - } } /** @@ -489,16 +471,16 @@ export class CoreWSProvider { * @param url URL to perform the request. * @return Promise resolved with the response. */ - performHead(url: string): Promise> { - let promise = this.getPromiseHttp('head', url); + performHead(url: string): Promise> { + let promise = this.getPromiseHttp>('head', url); if (!promise) { - promise = this.sendHTTPRequest(url, { + promise = this.sendHTTPRequest(url, { method: 'head', responseType: 'text', }); - promise = this.setPromiseHttp(promise, 'head', url); + promise = this.setPromiseHttp>(promise, 'head', url); } return promise; @@ -513,12 +495,12 @@ export class CoreWSProvider { * @param preSets Extra settings and information. * @return Promise resolved with the response data in success and rejected with CoreWSError if it fails. */ - performPost(method: string, siteUrl: string, ajaxData: any, preSets: CoreWSPreSets): Promise { + performPost(method: string, siteUrl: string, ajaxData: unknown, preSets: CoreWSPreSets): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const options: any = {}; // This is done because some returned values like 0 are treated as null if responseType is json. if (preSets.typeExpected == 'number' || preSets.typeExpected == 'boolean' || preSets.typeExpected == 'string') { - // Avalaible values are: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType options.responseType = 'text'; } @@ -530,8 +512,8 @@ export class CoreWSProvider { // Perform the post request. const promise = Http.instance.post(requestUrl, ajaxData, options).pipe(timeout(this.getRequestTimeout())).toPromise(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any return promise.then((data: any) => { - // Some moodle web services return null. // If the responseExpected value is set to false, we create a blank object if the response is null. if (!data && !preSets.responseExpected) { @@ -539,7 +521,7 @@ export class CoreWSProvider { } if (!data) { - return Promise.reject(this.createFakeWSError('core.serverconnection', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection'))); } else if (typeof data != preSets.typeExpected) { // If responseType is text an string will be returned, parse before returning. if (typeof data == 'string') { @@ -548,7 +530,7 @@ export class CoreWSProvider { if (isNaN(data)) { this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`); - return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); } } else if (preSets.typeExpected == 'boolean') { if (data === 'true') { @@ -558,17 +540,17 @@ export class CoreWSProvider { } else { this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`); - return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); } } else { this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); - return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); } } else { this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); - return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); } } @@ -578,18 +560,18 @@ export class CoreWSProvider { this.logger.error('Error calling WS', method, data); } - return Promise.reject(data); + return Promise.reject(new CoreWSError(data)); } if (typeof data.debuginfo != 'undefined') { - return Promise.reject(this.createFakeWSError('Error. ' + data.message)); + return Promise.reject(new CoreError('Error. ' + data.message)); } return data; }, (error) => { // If server has heavy load, retry after some seconds. if (error.status == 429) { - const retryPromise = this.addToRetryQueue(method, siteUrl, ajaxData, preSets); + const retryPromise = this.addToRetryQueue(method, siteUrl, ajaxData, preSets); // Only process the queue one time. if (this.retryTimeout == 0) { @@ -610,7 +592,7 @@ export class CoreWSProvider { return retryPromise; } - return Promise.reject(this.createFakeWSError('core.serverconnection', true)); + return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection'))); }); } @@ -623,7 +605,7 @@ export class CoreWSProvider { const call = this.retryCalls.shift(); // Add a delay between calls. setTimeout(() => { - call.deferred.resolve(this.performPost(call.method, call.siteUrl, call.ajaxData, call.preSets)); + call.deferred.resolve(this.performPost(call.method, call.siteUrl, call.data, call.preSets)); this.processRetryQueue(); }, 200); } else { @@ -640,14 +622,14 @@ export class CoreWSProvider { * @param params Params of the HTTP request. * @return The promise saved. */ - protected setPromiseHttp(promise: Promise, method: string, url: string, params?: any): Promise { + protected setPromiseHttp(promise: Promise, method: string, url: string, params?: Record): + Promise { const queueItemId = this.getQueueItemId(method, url, params); - let timeout; this.ongoingCalls[queueItemId] = promise; // HTTP not finished, but we should delete the promise after timeout. - timeout = setTimeout(() => { + const timeout = setTimeout(() => { delete this.ongoingCalls[queueItemId]; }, this.getRequestTimeout()); @@ -667,22 +649,14 @@ export class CoreWSProvider { * @param data Arguments to pass to the method. * @param preSets Extra settings and information. * @return Promise resolved with the response data in success and rejected with the error message if it fails. - * @return Request response. If the request fails, returns an object with 'error'=true and 'message' properties. + * @return Request response. */ - syncCall(method: string, data: any, preSets: CoreWSPreSets): any { - const errorResponse = { - error: true, - message: '', - }; - + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + syncCall(method: string, data: any, preSets: CoreWSPreSets): T { if (!preSets) { - errorResponse.message = Translate.instance.instant('core.unexpectederror'); - - return errorResponse; + throw new CoreError(Translate.instance.instant('core.unexpectederror')); } else if (!CoreApp.instance.isOnline()) { - errorResponse.message = Translate.instance.instant('core.networkerrormsg'); - - return errorResponse; + throw new CoreError(Translate.instance.instant('core.networkerrormsg')); } preSets.typeExpected = preSets.typeExpected || 'object'; @@ -693,9 +667,7 @@ export class CoreWSProvider { data = this.convertValuesToString(data || {}, preSets.cleanUnicode); if (data == null) { // Empty cleaned text found. - errorResponse.message = Translate.instance.instant('core.unicodenotsupportedcleanerror'); - - return errorResponse; + throw new CoreError(Translate.instance.instant('core.unicodenotsupportedcleanerror')); } data.wsfunction = method; @@ -706,22 +678,21 @@ export class CoreWSProvider { data = CoreInterceptor.serialize(data); // Perform sync request using XMLHttpRequest. - const xhr = new ( window).XMLHttpRequest(); + const xhr = new XMLHttpRequest(); xhr.open('post', siteUrl, false); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8'); xhr.send(data); // Get response. - data = ('response' in xhr) ? xhr.response : xhr.responseText; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data = ('response' in xhr) ? xhr.response : ( xhr).responseText; // Check status. const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0); if (status < 200 || status >= 300) { // Request failed. - errorResponse.message = data; - - return errorResponse; + throw new CoreError(data); } // Treat response. @@ -734,18 +705,14 @@ export class CoreWSProvider { } if (!data) { - errorResponse.message = Translate.instance.instant('core.serverconnection'); + throw new CoreError(Translate.instance.instant('core.serverconnection')); } else if (typeof data != preSets.typeExpected) { this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"'); - errorResponse.message = Translate.instance.instant('core.errorinvalidresponse'); + throw new CoreError(Translate.instance.instant('core.errorinvalidresponse')); } if (typeof data.exception != 'undefined' || typeof data.debuginfo != 'undefined') { - errorResponse.message = data.message; - } - - if (errorResponse.message !== '') { - return errorResponse; + throw new CoreWSError(data); } return data; @@ -760,16 +727,16 @@ 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) => any): Promise { + 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(null); + return Promise.reject(new CoreError('Invalid options passed to upload file.')); } if (!CoreApp.instance.isOnline()) { - return Promise.reject(Translate.instance.instant('core.networkerrormsg')); + return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); } const uploadUrl = preSets.siteUrl + '/webservice/upload.php'; @@ -781,34 +748,40 @@ export class CoreWSProvider { options.params = { token: preSets.wsToken, filearea: options.fileArea || 'draft', - itemid: options.itemId || 0 + itemid: options.itemId || 0, }; options.chunkedMode = false; - options.headers = { - Connection: 'close' - }; + 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)); + this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response)); + if (data === null) { - return Promise.reject(Translate.instance.instant('core.errorinvalidresponse')); + return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); } if (!data) { - return Promise.reject(Translate.instance.instant('core.serverconnection')); + return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection'))); } else if (typeof data != 'object') { this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"'); - return Promise.reject(Translate.instance.instant('core.errorinvalidresponse')); + return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); } if (typeof data.exception !== 'undefined') { - return Promise.reject(data.message); - } else if (data && typeof data.error !== 'undefined') { - return Promise.reject(data.error); + return Promise.reject(new CoreWSError(data)); + } else if (typeof data.error !== 'undefined') { + return Promise.reject(new CoreWSError({ + errorcode: data.errortype, + message: data.error, + })); } else if (data[0] && typeof data[0].error !== 'undefined') { - return Promise.reject(data[0].error); + return Promise.reject(new CoreWSError({ + errorcode: data[0].errortype, + message: data[0].error, + })); } // We uploaded only 1 file, so we only return the first file returned. @@ -818,7 +791,7 @@ export class CoreWSProvider { }).catch((error) => { this.logger.error('Error while uploading file', filePath, error); - return Promise.reject(Translate.instance.instant('core.errorinvalidresponse')); + return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); }); } @@ -835,7 +808,7 @@ export class CoreWSProvider { responseType: 'text', }; - const response = await this.sendHTTPRequest(url, options); + const response = await this.sendHTTPRequest(url, options); const content = response.body; @@ -853,8 +826,7 @@ export class CoreWSProvider { * @param options Options for the request. * @return Promise resolved with the response. */ - async sendHTTPRequest(url: string, options: HttpRequestOptions): Promise> { - + async sendHTTPRequest(url: string, options: HttpRequestOptions): Promise> { // Set default values. options.responseType = options.responseType || 'json'; options.timeout = typeof options.timeout == 'undefined' ? this.getRequestTimeout() : options.timeout; @@ -867,8 +839,8 @@ export class CoreWSProvider { const content = await CoreFile.instance.readFile(url, format); - return new HttpResponse({ - body: content, + return new HttpResponse({ + body: content, headers: null, status: 200, statusText: 'OK', @@ -876,81 +848,78 @@ export class CoreWSProvider { }); } - return new Promise>((resolve, reject): void => { - // We cannot use Ionic Native plugin because it doesn't have the sendRequest method. - ( cordova).plugin.http.sendRequest(url, options, (response) => { - resolve(new CoreNativeToAngularHttpResponse(response)); - }, reject); - }); + return NativeHttp.instance.sendRequest(url, options).then((response) => new CoreNativeToAngularHttpResponse(response)); } else { - let observable: Observable; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let observable: Observable>; + const angularOptions = options; // Use Angular's library. - switch (options.method) { + switch (angularOptions.method) { case 'get': observable = Http.instance.get(url, { - headers: options.headers, - params: options.params, + headers: angularOptions.headers, + params: angularOptions.params, observe: 'response', - responseType: options.responseType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseType: angularOptions.responseType, }); break; case 'post': - if (options.serializer == 'json') { - options.data = JSON.stringify(options.data); + if (angularOptions.serializer == 'json') { + angularOptions.data = JSON.stringify(angularOptions.data); } - observable = Http.instance.post(url, options.data, { - headers: options.headers, + observable = Http.instance.post(url, angularOptions.data, { + headers: angularOptions.headers, observe: 'response', - responseType: options.responseType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseType: angularOptions.responseType, }); break; case 'head': observable = Http.instance.head(url, { - headers: options.headers, + headers: angularOptions.headers, observe: 'response', - responseType: options.responseType + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseType: angularOptions.responseType, }); break; default: - return Promise.reject('Method not implemented yet.'); + return Promise.reject(new CoreError('Method not implemented yet.')); } - if (options.timeout) { - observable = observable.pipe(timeout(options.timeout)); + if (angularOptions.timeout) { + observable = observable.pipe(timeout(angularOptions.timeout)); } return observable.toPromise(); } } + + /** + * Check if a URL works (it returns a 2XX status). + * + * @param url URL to check. + * @return Promise resolved with boolean: whether it works. + */ + async urlWorks(url: string): Promise { + try { + const result = await this.performHead(url); + + return result.status >= 200 && result.status < 300; + } catch (error) { + return false; + } + } + } export class CoreWS extends makeSingleton(CoreWSProvider) {} -/** - * Error returned by a WS call. - */ -export interface CoreWSError { - /** - * The error message. - */ - message: string; - - /** - * Name of the exception. Undefined for local errors (fake WS errors). - */ - exception?: string; - - /** - * The error code. Undefined for local errors (fake WS errors). - */ - errorcode?: string; -} - /** * File upload options. */ @@ -1084,7 +1053,7 @@ export type CoreWSPreSets = { * Defaults to false. Clean multibyte Unicode chars from data. */ cleanUnicode?: boolean; -} +}; /** * PreSets accepted by AJAX WS calls. @@ -1109,7 +1078,7 @@ export type CoreWSAjaxPreSets = { * Whether to send the parameters via GET. Only if noLogin is true. */ useGet?: boolean; -} +}; /** * Options for HTTP requests. @@ -1118,17 +1087,17 @@ export type HttpRequestOptions = { /** * The HTTP method. */ - method: string; + method: 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' | 'options' | 'upload' | 'download'; /** * Payload to send to the server. Only applicable on post, put or patch methods. */ - data?: any; + data?: Record; /** * Query params to be appended to the URL (only applicable on get, head, delete, upload or download methods). */ - params?: any; + params?: Record; /** * Response type. Defaults to json. @@ -1143,7 +1112,7 @@ export type HttpRequestOptions = { /** * Serializer to use. Defaults to 'urlencoded'. Only for mobile environments. */ - serializer?: string; + serializer?: 'json' | 'urlencoded' | 'utf8' | 'multipart'; /** * Whether to follow redirects. Defaults to true. Only for mobile environments. @@ -1153,16 +1122,45 @@ export type HttpRequestOptions = { /** * Headers. Only for mobile environments. */ - headers?: {[name: string]: string}; + headers?: Record; /** * File paths to use for upload or download. Only for mobile environments. */ - filePath?: string; + filePath?: string | string[]; /** * Name to use during upload. Only for mobile environments. */ - name?: string; + name?: string | string[]; }; +/** + * Options for JSON HTTP requests using Angular Http. + */ +type AngularHttpRequestOptions = Omit & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: Record | string; + params?: HttpParams | { + [param: string]: string | string[]; + }; +}; + +/** + * Data needed to retry a WS call. + */ +type RetryCall = { + method: string; + siteUrl: string; + data: unknown; + preSets: CoreWSPreSets; + deferred: PromiseDefer; +}; + +/** + * Downloaded file entry. It includes some calculated data. + */ +export type CoreWSDownloadedFileEntry = FileEntry & { + extension?: string; // File extension. + path?: string; // File path. +}; From 1241be46ec656a828e5b5302578047dce8070675 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:34:28 +0200 Subject: [PATCH 15/17] MOBILE-3565 core: Fix some ESLint of CoreDomUtilsProvider --- src/app/classes/ion-loading.ts | 55 +++ src/app/services/utils/dom.ts | 574 +++++++++++++++++--------- src/app/singletons/core.singletons.ts | 16 +- 3 files changed, 457 insertions(+), 188 deletions(-) create mode 100644 src/app/classes/ion-loading.ts diff --git a/src/app/classes/ion-loading.ts b/src/app/classes/ion-loading.ts new file mode 100644 index 000000000..370b9fa25 --- /dev/null +++ b/src/app/classes/ion-loading.ts @@ -0,0 +1,55 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreUtils } from '@services/utils/utils'; + +/** + * Class to improve the behaviour of HTMLIonLoadingElement. + * It's not a subclass of HTMLIonLoadingElement because we cannot override the dismiss function. + */ +export class CoreIonLoadingElement { + + protected isPresented = false; + protected isDismissed = false; + + constructor(public loading: HTMLIonLoadingElement) { } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + async dismiss(data?: any, role?: string): Promise { + if (!this.isPresented || this.isDismissed) { + this.isDismissed = true; + + return true; + } + + this.isDismissed = true; + + return this.loading.dismiss(data, role); + } + + /** + * Present the loading. + */ + async present(): Promise { + // Wait a bit before presenting the modal, to prevent it being displayed if dissmiss is called fast. + await CoreUtils.instance.wait(40); + + if (!this.isDismissed) { + this.isPresented = true; + + await this.loading.present(); + } + } + +} diff --git a/src/app/services/utils/dom.ts b/src/app/services/utils/dom.ts index a25a1df9a..e72f72056 100644 --- a/src/app/services/utils/dom.ts +++ b/src/app/services/utils/dom.ts @@ -12,21 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable, SimpleChange, ElementRef } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { Md5 } from 'ts-md5/dist/md5'; -import { Subject } from 'rxjs'; +import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { IonContent } from '@ionic/angular'; +import { AlertOptions, AlertButton, TextFieldTypes } from '@ionic/core'; +import { Md5 } from 'ts-md5'; import { CoreApp } from '@services/app'; import { CoreConfig } from '@services/config'; import { CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreFile } from '@services/file'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreWSExternalWarning } from '@services/ws'; +import { CoreTextUtils, CoreTextErrorObject } from '@services/utils/text'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@core/constants'; +import { CoreIonLoadingElement } from '@classes/ion-loading'; +import { CoreCanceledError } from '@classes/errors/cancelederror'; +import { CoreError } from '@classes/errors/error'; -import { makeSingleton, Translate } from '@singletons/core.singletons'; +import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; /* @@ -34,22 +39,23 @@ import { CoreLogger } from '@singletons/logger'; */ @Injectable() export class CoreDomUtilsProvider { - // List of input types that support keyboard. - protected INPUT_SUPPORT_KEYBOARD = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password', - 'search', 'tel', 'text', 'time', 'url', 'week']; - protected INSTANCE_ID_ATTR_NAME = 'core-instance-id'; - protected template = document.createElement('template'); // A template element to convert HTML to element. + // 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. + // eslint-disable-next-line @typescript-eslint/no-explicit-any protected instances: {[id: string]: any} = {}; // Store component/directive instances by id. protected lastInstanceId = 0; protected debugDisplay = false; // Whether to display debug messages. Store it in a variable to make it synchronous. - protected displayedAlerts = {}; // To prevent duplicated alerts. + protected displayedAlerts: Record = {}; // To prevent duplicated alerts. protected logger: CoreLogger; constructor(protected domSanitizer: DomSanitizer) { - this.logger = CoreLogger.getInstance('CoreDomUtilsProvider'); // Check if debug messages should be displayed. @@ -110,8 +116,14 @@ 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(size: any, message?: string, unknownMessage?: string, wifiThreshold?: number, limitedThreshold?: number, - alwaysConfirm?: boolean): Promise { + confirmDownloadSize( + size: {size: number; total: boolean}, + message?: string, + unknownMessage?: string, + wifiThreshold?: number, + limitedThreshold?: number, + alwaysConfirm?: boolean, + ): Promise { const readableSize = CoreTextUtils.instance.bytesToSize(size.size, 2); const getAvailableBytes = new Promise((resolve): void => { @@ -143,11 +155,11 @@ export class CoreDomUtilsProvider { } else { const availableSize = CoreTextUtils.instance.bytesToSize(availableBytes, 2); if (CoreApp.instance.isAndroid() && size.size > availableBytes - CoreConstants.MINIMUM_FREE_SPACE) { - return Promise.reject(Translate.instance.instant('core.course.insufficientavailablespace', - { size: readableSize })); + return Promise.reject(new CoreError(Translate.instance.instant('core.course.insufficientavailablespace', + { size: readableSize }))); } - return Translate.instance.instant('core.course.availablespace', {available: availableSize}); + return Translate.instance.instant('core.course.availablespace', { available: availableSize }); } }); @@ -164,7 +176,8 @@ export class CoreDomUtilsProvider { // 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})); + return this.showConfirm(wifiPrefix + Translate.instance.instant( + unknownMessage, { availableSpace: availableSpace })); } else if (!size.total) { // Filesize is only partial. @@ -199,9 +212,10 @@ export class CoreDomUtilsProvider { * Create a "cancelled" error. These errors won't display an error message in showErrorModal functions. * * @return The error object. + * @deprecated since 3.9.5. Just create the error directly. */ - createCanceledError(): any { - return {coreCanceled: true}; + createCanceledError(): CoreCanceledError { + return new CoreCanceledError(''); } /** @@ -211,7 +225,7 @@ export class CoreDomUtilsProvider { * @param changes Changes detected by KeyValueDiffer. * @return Changes in a format like ngOnChanges. */ - createChangesFromKeyValueDiff(changes: any): { [name: string]: SimpleChange } { + createChangesFromKeyValueDiff(changes: KeyValueChanges): { [name: string]: SimpleChange } { const newChanges: { [name: string]: SimpleChange } = {}; // Added items are considered first change. @@ -242,14 +256,14 @@ export class CoreDomUtilsProvider { ' Please use that function instead of this one.'); const urls = []; - let elements; const element = this.convertToElement(html); - elements = element.querySelectorAll('a, img, audio, video, source, track'); + const elements: (HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | + HTMLTrackElement)[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track')); for (let i = 0; i < elements.length; i++) { const element = elements[i]; - let url = element.tagName === 'A' ? element.href : element.src; + let url = 'href' in element ? element.href : element.src; if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) { urls.push(url); @@ -274,15 +288,13 @@ export class CoreDomUtilsProvider { * @return List of fake file objects with file URLs. * @deprecated since 3.8. Use CoreFilepoolProvider.extractDownloadableFilesFromHtmlAsFakeFileObjects instead. */ - extractDownloadableFilesFromHtmlAsFakeFileObjects(html: string): any[] { + extractDownloadableFilesFromHtmlAsFakeFileObjects(html: string): {fileurl: string}[] { const urls = this.extractDownloadableFilesFromHtml(html); // Convert them to fake file objects. - return urls.map((url) => { - return { - fileurl: url - }; - }); + return urls.map((url) => ({ + fileurl: url, + })); } /** @@ -303,7 +315,7 @@ export class CoreDomUtilsProvider { // Extract the URL form each match. matches.forEach((match) => { const submatches = match.match(/url\(\s*['"]?([^'"]*)['"]?\s*\)/im); - if (submatches && submatches[1]) { + if (submatches?.[1]) { urls.push(submatches[1]); } }); @@ -344,7 +356,7 @@ export class CoreDomUtilsProvider { * @param el HTML element to focus. */ focusElement(el: HTMLElement): void { - if (el && el.focus) { + if (el?.focus) { el.focus(); if (CoreApp.instance.isAndroid() && this.supportsInputKeyboard(el)) { // On some Android versions the keyboard doesn't open automatically. @@ -361,13 +373,13 @@ export class CoreDomUtilsProvider { * @param size Size to format. * @return Formatted size. If size is not valid, returns an empty string. */ - formatPixelsSize(size: any): string { + formatPixelsSize(size: string | number): string { if (typeof size == 'string' && (size.indexOf('px') > -1 || size.indexOf('%') > -1 || size == 'auto' || size == 'initial')) { // It seems to be a valid size. return size; } - size = parseInt(size, 10); + size = Number(size); if (!isNaN(size)) { return size + 'px'; } @@ -397,7 +409,7 @@ export class CoreDomUtilsProvider { * @param form The form to get the data from. * @return Object with the data. The keys are the names of the inputs. */ - getDataFromForm(form: HTMLFormElement): any { + getDataFromForm(form: HTMLFormElement): Record { if (!form || !form.elements) { return {}; } @@ -405,7 +417,7 @@ export class CoreDomUtilsProvider { const data = {}; for (let i = 0; i < form.elements.length; i++) { - const element: any = form.elements[i]; + const element = form.elements[i]; const name = element.name || ''; // Ignore submit inputs. @@ -436,7 +448,7 @@ export class CoreDomUtilsProvider { * @return Attribute value. */ getHTMLElementAttribute(html: string, attribute: string): string { - return this.convertToElement(html).children[0].getAttribute('src'); + return this.convertToElement(html).children[0].getAttribute(attribute); } /** @@ -449,8 +461,13 @@ export class CoreDomUtilsProvider { * @param innerMeasure If inner measure is needed: padding, margin or borders will be substracted. * @return Height in pixels. */ - getElementHeight(element: any, usePadding?: boolean, useMargin?: boolean, useBorder?: boolean, - innerMeasure?: boolean): number { + getElementHeight( + element: HTMLElement, + usePadding?: boolean, + useMargin?: boolean, + useBorder?: boolean, + innerMeasure?: boolean, + ): number { return this.getElementMeasure(element, false, usePadding, useMargin, useBorder, innerMeasure); } @@ -465,9 +482,14 @@ export class CoreDomUtilsProvider { * @param innerMeasure If inner measure is needed: padding, margin or borders will be substracted. * @return Measure in pixels. */ - getElementMeasure(element: any, getWidth?: boolean, usePadding?: boolean, useMargin?: boolean, useBorder?: boolean, - innerMeasure?: boolean): number { - + getElementMeasure( + element: HTMLElement, + getWidth?: boolean, + usePadding?: boolean, + useMargin?: boolean, + useBorder?: boolean, + innerMeasure?: boolean, + ): number { const offsetMeasure = getWidth ? 'offsetWidth' : 'offsetHeight'; const measureName = getWidth ? 'width' : 'height'; const clientMeasure = getWidth ? 'clientWidth' : 'clientHeight'; @@ -478,7 +500,7 @@ export class CoreDomUtilsProvider { // Measure not correctly taken. if (measure <= 0) { const style = getComputedStyle(element); - if (style && style.display == '') { + if (style?.display == '') { element.style.display = 'inline-block'; measure = element[offsetMeasure] || element[measureName] || element[clientMeasure] || 0; element.style.display = ''; @@ -518,7 +540,7 @@ export class CoreDomUtilsProvider { * @param measure Measure to get. * @return Result of the measure. */ - getComputedStyleMeasure(style: any, measure: string): number { + getComputedStyleMeasure(style: CSSStyleDeclaration, measure: string): number { return parseInt(style[measure], 10) || 0; } @@ -544,8 +566,13 @@ export class CoreDomUtilsProvider { * @param innerMeasure If inner measure is needed: padding, margin or borders will be substracted. * @return Width in pixels. */ - getElementWidth(element: any, usePadding?: boolean, useMargin?: boolean, useBorder?: boolean, - innerMeasure?: boolean): number { + getElementWidth( + element: HTMLElement, + usePadding?: boolean, + useMargin?: boolean, + useBorder?: boolean, + innerMeasure?: boolean, + ): number { return this.getElementMeasure(element, true, usePadding, useMargin, useBorder, innerMeasure); } @@ -554,7 +581,7 @@ export class CoreDomUtilsProvider { * * @param container Element to search in. * @param selector Selector to find the element to gets the position. - * @param positionParentClass Parent Class where to stop calculating the position. Default scroll-content. + * @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[] { @@ -563,7 +590,7 @@ export class CoreDomUtilsProvider { let positionLeft = 0; if (!positionParentClass) { - positionParentClass = 'scroll-content'; + positionParentClass = 'inner-scroll'; } if (!element) { @@ -588,7 +615,7 @@ export class CoreDomUtilsProvider { } // Finally, check again. - if (element && element.className.indexOf(positionParentClass) != -1) { + if (element?.className.indexOf(positionParentClass) != -1) { element = null; } } @@ -602,10 +629,9 @@ export class CoreDomUtilsProvider { * @param message The error message. * @return Title. */ - private getErrorTitle(message: string): any { + private getErrorTitle(message: string): SafeHtml | string { if (message == Translate.instance.instant('core.networkerrormsg') || message == Translate.instance.instant('core.fileuploader.errormustbeonlinetoupload')) { - return this.domSanitizer.bypassSecurityTrustHtml(this.getConnectionWarningIconHtml()); } @@ -619,21 +645,22 @@ export class CoreDomUtilsProvider { * @param needsTranslate Whether the error needs to be translated. * @return Error message, null if no error should be displayed. */ - getErrorMessage(error: any, needsTranslate?: boolean): string { + getErrorMessage(error: CoreError | CoreTextErrorObject | string, needsTranslate?: boolean): string { let extraInfo = ''; + let errorMessage: string; if (typeof error == 'object') { if (this.debugDisplay) { // Get the debug info. Escape the HTML so it is displayed as it is in the view. - if (error.debuginfo) { + if ('debuginfo' in error && error.debuginfo) { extraInfo = '

' + CoreTextUtils.instance.escapeHTML(error.debuginfo, false); } - if (error.backtrace) { + if ('backtrace' in error && error.backtrace) { extraInfo += '

' + CoreTextUtils.instance.replaceNewLines( - CoreTextUtils.instance.escapeHTML(error.backtrace, false), '
'); + CoreTextUtils.instance.escapeHTML(error.backtrace, false), '
'); } - // tslint:disable-next-line + // eslint-disable-next-line no-console console.error(error); } @@ -643,26 +670,28 @@ export class CoreDomUtilsProvider { return null; } - error = CoreTextUtils.instance.getErrorMessageFromError(error); - if (!error) { + errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error); + if (!errorMessage) { // No common properties found, just stringify it. - error = JSON.stringify(error); + errorMessage = JSON.stringify(error); extraInfo = ''; // No need to add extra info because it's already in the error. } // Try to remove tokens from the contents. - const matches = error.match(/token"?[=|:]"?(\w*)/, ''); - if (matches && matches[1]) { - error = error.replace(new RegExp(matches[1], 'g'), 'secret'); + const matches = errorMessage.match(/token"?[=|:]"?(\w*)/); + if (matches?.[1]) { + errorMessage = errorMessage.replace(new RegExp(matches[1], 'g'), 'secret'); } + } else { + errorMessage = error; } - if (error == CoreConstants.DONT_SHOW_ERROR) { + if (errorMessage == CoreConstants.DONT_SHOW_ERROR) { // The error shouldn't be shown, stop. return null; } - let message = CoreTextUtils.instance.decodeHTML(needsTranslate ? Translate.instance.instant(error) : error); + let message = CoreTextUtils.instance.decodeHTML(needsTranslate ? Translate.instance.instant(errorMessage) : errorMessage); if (extraInfo) { message += extraInfo; @@ -679,6 +708,7 @@ export class CoreDomUtilsProvider { * @param element The root element of the component/directive. * @return The instance, undefined if not found. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any getInstanceByElement(element: Element): any { const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME); @@ -691,8 +721,8 @@ export class CoreDomUtilsProvider { * @param error Error to check. * @return Whether it's a canceled error. */ - isCanceledError(error: any): boolean { - return error && error.coreCanceled; + isCanceledError(error: CoreError | CoreTextErrorObject | string): boolean { + return error instanceof CoreCanceledError; } /** @@ -702,19 +732,9 @@ export class CoreDomUtilsProvider { * @return Resolved if found, rejected if too many tries. */ waitElementToExist(findFunction: () => HTMLElement): Promise { - const promiseInterval = { - promise: null, - resolve: null, - reject: null - }; - + const promiseInterval = CoreUtils.instance.promiseDefer(); let tries = 100; - promiseInterval.promise = new Promise((resolve, reject): void => { - promiseInterval.resolve = resolve; - promiseInterval.reject = reject; - }); - const clear = setInterval(() => { const element: HTMLElement = findFunction(); @@ -758,7 +778,7 @@ export class CoreDomUtilsProvider { el.setAttribute('data-original-title', content); el.setAttribute('title', ''); - el.addEventListener('click', (e) => { + el.addEventListener('click', () => { // @todo }); }); @@ -781,7 +801,7 @@ export class CoreDomUtilsProvider { const elementMidPoint = Math.round((elementRect.bottom + elementRect.top) / 2); const scrollElRect = scrollEl.getBoundingClientRect(); - const scrollTopPos = (scrollElRect && scrollElRect.top) || 0; + const scrollTopPos = scrollElRect?.top || 0; return elementMidPoint > window.innerHeight || elementMidPoint < scrollTopPos; } @@ -793,9 +813,7 @@ export class CoreDomUtilsProvider { */ isRichTextEditorEnabled(): Promise { if (this.isRichTextEditorSupported()) { - return CoreConfig.instance.get(CoreConstants.SETTINGS_RICH_TEXT_EDITOR, true).then((enabled) => { - return !!enabled; - }); + return CoreConfig.instance.get(CoreConstants.SETTINGS_RICH_TEXT_EDITOR, true).then((enabled) => !!enabled); } return Promise.resolve(false); @@ -856,17 +874,15 @@ export class CoreDomUtilsProvider { * @return HTML without the element. */ removeElementFromHtml(html: string, selector: string, removeAll?: boolean): string { - let selected; - const element = this.convertToElement(html); if (removeAll) { - selected = element.querySelectorAll(selector); + const selected = element.querySelectorAll(selector); for (let i = 0; i < selected.length; i++) { selected[i].remove(); } } else { - selected = element.querySelector(selector); + const selected = element.querySelector(selector); if (selected) { selected.remove(); } @@ -901,7 +917,7 @@ export class CoreDomUtilsProvider { * @param map Mapping of the classes to replace. Keys must be the value to replace, values must be * the new class name. Example: {'correct': 'core-question-answer-correct'}. */ - replaceClassesInElement(element: HTMLElement, map: any): void { + replaceClassesInElement(element: HTMLElement, map: {[currentValue: string]: string}): void { for (const key in map) { const foundElements = element.querySelectorAll('.' + key); @@ -920,7 +936,11 @@ export class CoreDomUtilsProvider { * @param anchorFn Function to call with each anchor. Optional. * @return Treated HTML code. */ - restoreSourcesInHtml(html: string, paths: object, anchorFn?: (anchor: HTMLElement, href: string) => void): string { + restoreSourcesInHtml( + html: string, + paths: {[url: string]: string}, + anchorFn?: (anchor: HTMLElement, href: string) => void, + ): string { const element = this.convertToElement(html); // Treat elements with src (img, audio, video, ...). @@ -961,80 +981,88 @@ export class CoreDomUtilsProvider { /** * Scroll to somehere in the content. - * Checks hidden property _scroll to avoid errors if view is not active. * - * @param content Content where to execute the function. + * @param content Content to scroll. * @param x The x-value to scroll to. * @param y The y-value to scroll to. - * @param duration Duration of the scroll animation in milliseconds. Defaults to `300`. + * @param duration Duration of the scroll animation in milliseconds. * @return Returns a promise which is resolved when the scroll has completed. + * @deprecated since 3.9.5. Use directly the IonContent class. */ - scrollTo(content: any, x: number, y: number, duration?: number, done?: () => void): Promise { - // @todo - return Promise.resolve(); + scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise { + return content?.scrollByPoint(x, y, duration); } /** * Scroll to Bottom of the content. - * Checks hidden property _scroll to avoid errors if view is not active. * - * @param content Content where to execute the function. - * @param duration Duration of the scroll animation in milliseconds. Defaults to `300`. + * @param content Content to scroll. + * @param duration Duration of the scroll animation in milliseconds. * @return Returns a promise which is resolved when the scroll has completed. + * @deprecated since 3.9.5. Use directly the IonContent class. */ - scrollToBottom(content: any, duration?: number): Promise { - // @todo - return Promise.resolve(); + scrollToBottom(content: IonContent, duration?: number): Promise { + return content?.scrollToBottom(duration); } /** * Scroll to Top of the content. - * Checks hidden property _scroll to avoid errors if view is not active. * - * @param content Content where to execute the function. - * @param duration Duration of the scroll animation in milliseconds. Defaults to `300`. + * @param content Content to scroll. + * @param duration Duration of the scroll animation in milliseconds. * @return Returns a promise which is resolved when the scroll has completed. + * @deprecated since 3.9.5. Use directly the IonContent class. */ - scrollToTop(content: any, duration?: number): Promise { - // @todo - return Promise.resolve(); + scrollToTop(content: IonContent, duration?: number): Promise { + return content?.scrollToTop(duration); } /** - * Returns contentHeight of the content. - * Checks hidden property _scroll to avoid errors if view is not active. + * Returns height of the content. * * @param content Content where to execute the function. - * @return Content contentHeight or 0. + * @return Promise resolved with content height. */ - getContentHeight(content: any): number { - // @todo - return 0; + async getContentHeight(content: IonContent): Promise { + try { + const scrollElement = await content?.getScrollElement(); + + return scrollElement?.clientHeight || 0; + } catch (error) { + return 0; + } } /** - * Returns scrollHeight of the content. - * Checks hidden property _scroll to avoid errors if view is not active. + * Returns scroll height of the content. * * @param content Content where to execute the function. - * @return Content scrollHeight or 0. + * @return Promise resolved with scroll height. */ - getScrollHeight(content: any): number { - // @todo - return 0; + async getScrollHeight(content: IonContent): Promise { + try { + const scrollElement = await content?.getScrollElement(); + + return scrollElement?.scrollHeight || 0; + } catch (error) { + return 0; + } } /** * Returns scrollTop of the content. - * Checks hidden property _scrollContent to avoid errors if view is not active. - * Using navite value of scroll to avoid having non updated values. * * @param content Content where to execute the function. - * @return Content scrollTop or 0. + * @return Promise resolved with scroll top. */ - getScrollTop(content: any): number { - // @todo - return 0; + async getScrollTop(content: IonContent): Promise { + try { + const scrollElement = await content?.getScrollElement(); + + return scrollElement?.scrollTop || 0; + } catch (error) { + return 0; + } } /** @@ -1042,16 +1070,17 @@ export class CoreDomUtilsProvider { * * @param content The content that must be scrolled. * @param element The element to scroll to. - * @param scrollParentClass Parent class where to stop calculating the position. Default scroll-content. + * @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll. + * @param duration Duration of the scroll animation in milliseconds. * @return True if the element is found, false otherwise. */ - scrollToElement(content: any, element: HTMLElement, scrollParentClass?: string): boolean { + scrollToElement(content: IonContent, element: HTMLElement, scrollParentClass?: string, duration?: number): boolean { const position = this.getElementXY(element, undefined, scrollParentClass); if (!position) { return false; } - this.scrollTo(content, position[0], position[1]); + content?.scrollByPoint(position[0], position[1], duration); return true; } @@ -1061,28 +1090,40 @@ export class CoreDomUtilsProvider { * * @param content The content that must be scrolled. * @param selector Selector to find the element to scroll to. - * @param scrollParentClass Parent class where to stop calculating the position. Default scroll-content. + * @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll. + * @param duration Duration of the scroll animation in milliseconds. * @return True if the element is found, false otherwise. */ - scrollToElementBySelector(content: any, selector: string, scrollParentClass?: string): boolean { - const position = this.getElementXY(content.getScrollElement(), selector, scrollParentClass); - if (!position) { + async scrollToElementBySelector( + content: IonContent, + selector: string, + scrollParentClass?: string, + duration?: number, + ): Promise { + try { + const scrollElement = await content.getScrollElement(); + + const position = this.getElementXY(scrollElement, selector, scrollParentClass); + if (!position) { + return false; + } + + content?.scrollByPoint(position[0], position[1], duration); + + return true; + } catch (error) { return false; } - - this.scrollTo(content, position[0], position[1]); - - return true; } /** * Search for an input with error (core-input-error directive) and scrolls to it if found. * * @param content The content that must be scrolled. - * @param [scrollParentClass] Parent class where to stop calculating the position. Default scroll-content. + * @param scrollParentClass Parent class where to stop calculating the position. Default inner-scroll. * @return True if the element is found, false otherwise. */ - scrollToInputError(content: any, scrollParentClass?: string): boolean { + async scrollToInputError(content: IonContent, scrollParentClass?: string): Promise { if (!content) { return false; } @@ -1108,11 +1149,16 @@ export class CoreDomUtilsProvider { * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @return Promise resolved with the alert modal. */ - async showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise { + async showAlert( + header: string, + message: string, + buttonText?: string, + autocloseTime?: number, + ): Promise { return this.showAlertWithOptions({ - title, + header, message, - buttons: [buttonText || Translate.instance.instant('core.ok')] + buttons: [buttonText || Translate.instance.instant('core.ok')], }, autocloseTime); } @@ -1123,9 +1169,53 @@ export class CoreDomUtilsProvider { * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @return Promise resolved with the alert modal. */ - async showAlertWithOptions(options: any = {}, autocloseTime?: number): Promise { - // @todo - return Promise.resolve(); + async showAlertWithOptions(options: AlertOptions = {}, autocloseTime?: number): Promise { + const hasHTMLTags = CoreTextUtils.instance.hasHTMLTags( options.message || ''); + + if (hasHTMLTags) { + // Format the text. + options.message = await CoreTextUtils.instance.formatText( options.message); + } + + const alertId = Md5.hashAsciiStr((options.header || '') + '#' + (options.message || '')); + + if (this.displayedAlerts[alertId]) { + // There's already an alert with the same message and title. Return it. + return this.displayedAlerts[alertId]; + } + + const alert = await AlertController.instance.create(options); + + 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); + } + }); + + // Store the alert and remove it when dismissed. + this.displayedAlerts[alertId] = alert; + + // // Set the callbacks to trigger an observable event. + alert.onDidDismiss().then(() => { + delete this.displayedAlerts[alertId]; + }); + + if (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); + } + }, autocloseTime); + } + + return alert; } /** @@ -1137,7 +1227,7 @@ export class CoreDomUtilsProvider { * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @return Promise resolved with the alert modal. */ - showAlertTranslated(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise { + showAlertTranslated(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise { title = title ? Translate.instance.instant(title) : title; message = message ? Translate.instance.instant(message) : message; buttonText = buttonText ? Translate.instance.instant(buttonText) : buttonText; @@ -1153,7 +1243,11 @@ export class CoreDomUtilsProvider { * @param options More options. See https://ionicframework.com/docs/v3/api/components/alert/AlertController/ * @return Promise resolved if the user confirms and rejected with a canceled error if he cancels. */ - showDeleteConfirm(translateMessage: string = 'core.areyousure', translateArgs: any = {}, options?: any): Promise { + showDeleteConfirm( + translateMessage: string = 'core.areyousure', + translateArgs: Record = {}, + options?: AlertOptions, + ): Promise { return this.showConfirm(Translate.instance.instant(translateMessage, translateArgs), undefined, Translate.instance.instant('core.delete'), undefined, options); } @@ -1162,35 +1256,34 @@ export class CoreDomUtilsProvider { * Show a confirm modal. * * @param message Message to show in the modal body. - * @param title Title of the modal. + * @param header Header of the modal. * @param okText Text of the OK button. * @param cancelText Text of the Cancel button. - * @param options More options. See https://ionicframework.com/docs/v3/api/components/alert/AlertController/ + * @param options More options. * @return Promise resolved if the user confirms and rejected with a canceled error if he cancels. */ - showConfirm(message: string, title?: string, okText?: string, cancelText?: string, options: any = {}): Promise { + showConfirm(message: string, header?: string, okText?: string, cancelText?: string, options: AlertOptions = {}): Promise { return new Promise((resolve, reject): void => { - - options.title = title; + options.header = header; options.message = message; options.buttons = [ { text: cancelText || Translate.instance.instant('core.cancel'), role: 'cancel', - handler: (): void => { - reject(this.createCanceledError()); - } + handler: () => { + reject(new CoreCanceledError('')); + }, }, { text: okText || Translate.instance.instant('core.ok'), - handler: (data: any): void => { - resolve(data); - } - } + handler: () => { + resolve(); + }, + }, ]; - if (!title) { + if (!header) { options.cssClass = (options.cssClass || '') + ' core-nohead'; } @@ -1206,7 +1299,11 @@ export class CoreDomUtilsProvider { * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @return Promise resolved with the alert modal. */ - showErrorModal(error: any, needsTranslate?: boolean, autocloseTime?: number): Promise { + showErrorModal( + error: CoreError | CoreTextErrorObject | string, + needsTranslate?: boolean, + autocloseTime?: number, + ): Promise { const message = this.getErrorMessage(error, needsTranslate); if (message === null) { @@ -1214,7 +1311,7 @@ export class CoreDomUtilsProvider { return Promise.resolve(null); } - return this.showAlert(this.getErrorTitle(message), message, undefined, autocloseTime); + return this.showAlert( this.getErrorTitle(message), message, undefined, autocloseTime); } /** @@ -1226,10 +1323,15 @@ export class CoreDomUtilsProvider { * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @return Promise resolved with the alert modal. */ - showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise { + async showErrorModalDefault( + error: CoreError | CoreTextErrorObject | string, + defaultError: string, + needsTranslate?: boolean, + autocloseTime?: number, + ): Promise { if (this.isCanceledError(error)) { // It's a canceled error, don't display an error. - return; + return null; } let errorMessage = error; @@ -1250,10 +1352,13 @@ export class CoreDomUtilsProvider { * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @return Promise resolved with the alert modal. */ - showErrorModalFirstWarning(warnings: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise { - const error = warnings && warnings.length && warnings[0].message; - - return this.showErrorModalDefault(error, defaultError, needsTranslate, autocloseTime); + showErrorModalFirstWarning( + warnings: CoreWSExternalWarning[], + defaultError: string, + needsTranslate?: boolean, + autocloseTime?: number, + ): Promise { + return this.showErrorModalDefault(warnings?.[0], defaultError, needsTranslate, autocloseTime); } /** @@ -1261,15 +1366,29 @@ export class CoreDomUtilsProvider { * * @param text The text of the modal window. Default: core.loading. * @param needsTranslate Whether the 'text' needs to be translated. - * @return Loading modal instance. + * @return Loading element instance. * @description * Usage: - * let modal = domUtils.showModalLoading(myText); + * let modal = await domUtils.showModalLoading(myText); * ... * modal.dismiss(); */ - showModalLoading(text?: string, needsTranslate?: boolean): any { - // @todo + async showModalLoading(text?: string, needsTranslate?: boolean): Promise { + if (!text) { + text = Translate.instance.instant('core.loading'); + } else if (needsTranslate) { + text = Translate.instance.instant(text); + } + + const loadingElement = await LoadingController.instance.create({ + message: text, + }); + + const loading = new CoreIonLoadingElement(loadingElement); + + loading.present(); + + return loading; } /** @@ -1277,24 +1396,87 @@ export class CoreDomUtilsProvider { * * @param message The warning message. * @param link Link to the app to download if any. + * @return Promise resolved when done. */ - showDownloadAppNoticeModal(message: string, link?: string): void { - // @todo + async showDownloadAppNoticeModal(message: string, link?: string): Promise { + const buttons: AlertButton[] = [{ + text: Translate.instance.instant('core.ok'), + role: 'cancel', + }]; + + if (link) { + buttons.push({ + text: Translate.instance.instant('core.download'), + handler: (): void => { + CoreUtils.instance.openInBrowser(link); + }, + }); + } + + const alert = await AlertController.instance.create({ + message: message, + buttons: buttons, + }); + + await alert.present(); + + 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); + } } /** * Show a prompt modal to input some data. * * @param message Modal message. - * @param title Modal title. + * @param header Modal header. * @param placeholder Placeholder of the input element. By default, "Password". * @param type Type of the input element. By default, password. * @param options More options to pass to the alert. * @return Promise resolved with the input data if the user clicks OK, rejected if cancels. */ - showPrompt(message: string, title?: string, placeholder?: string, type: string = 'password'): Promise { - // @todo - return Promise.resolve(); + showPrompt( + message: string, + header?: string, + placeholder?: string, + type: TextFieldTypes | 'checkbox' | 'radio' | 'textarea' = 'password', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { + return new Promise((resolve, reject) => { + placeholder = placeholder ?? Translate.instance.instant('core.login.password'); + + const options: AlertOptions = { + header, + message, + inputs: [ + { + name: 'promptinput', + placeholder: placeholder, + type, + }, + ], + buttons: [ + { + text: Translate.instance.instant('core.cancel'), + role: 'cancel', + handler: () => { + reject(); + }, + }, + { + text: Translate.instance.instant('core.ok'), + handler: (data) => { + resolve(data.promptinput); + }, + }, + ], + }; + + this.showAlertWithOptions(options); + }); } /** @@ -1321,10 +1503,26 @@ export class CoreDomUtilsProvider { * @param dismissOnPageChange Dismiss the Toast on page change. * @return Toast instance. */ - showToast(text: string, needsTranslate?: boolean, duration: number = 2000, cssClass: string = '', - dismissOnPageChange: boolean = true): any { + async showToast( + text: string, + needsTranslate?: boolean, + duration: number = 2000, + cssClass: string = '', + ): Promise { + if (needsTranslate) { + text = Translate.instance.instant(text); + } - // @todo + const loader = await ToastController.instance.create({ + message: text, + duration: duration, + position: 'bottom', + cssClass: cssClass, + }); + + loader.present(); + + return loader; } /** @@ -1334,6 +1532,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 storeInstanceByElement(element: Element, instance: any): string { const id = String(this.lastInstanceId++); @@ -1349,9 +1548,11 @@ export class CoreDomUtilsProvider { * @param el HTML element to check. * @return Whether it supports input using keyboard. */ - supportsInputKeyboard(el: any): boolean { - return el && !el.disabled && (el.tagName.toLowerCase() == 'textarea' || - (el.tagName.toLowerCase() == 'input' && this.INPUT_SUPPORT_KEYBOARD.indexOf(el.type) != -1)); + supportsInputKeyboard(el: HTMLElement): boolean { + return el && + !( el).disabled && + (el.tagName.toLowerCase() == 'textarea' || + (el.tagName.toLowerCase() == 'input' && this.INPUT_SUPPORT_KEYBOARD.indexOf(( el).type) != -1)); } /** @@ -1421,7 +1622,7 @@ export class CoreDomUtilsProvider { hasImgToLoad = true; // Wait for image to load or fail. - promises.push(new Promise((resolve, reject): void => { + promises.push(new Promise((resolve) => { const imgLoaded = (): void => { resolve(); img.removeEventListener('load', imgLoaded); @@ -1434,9 +1635,7 @@ export class CoreDomUtilsProvider { } }); - return Promise.all(promises).then(() => { - return hasImgToLoad; - }); + return Promise.all(promises).then(() => hasImgToLoad); } /** @@ -1487,6 +1686,7 @@ export class CoreDomUtilsProvider { online: !!online, }, siteId); } + } export class CoreDomUtils extends makeSingleton(CoreDomUtilsProvider) {} diff --git a/src/app/singletons/core.singletons.ts b/src/app/singletons/core.singletons.ts index 78f49bffa..441573fa5 100644 --- a/src/app/singletons/core.singletons.ts +++ b/src/app/singletons/core.singletons.ts @@ -15,7 +15,13 @@ import { Injector, NgZone as NgZoneService } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Platform as PlatformService } from '@ionic/angular'; +import { + Platform as PlatformService, + AlertController as AlertControllerService, + LoadingController as LoadingControllerService, + ModalController as ModalControllerService, + ToastController as ToastControllerService, +} from '@ionic/angular'; import { Clipboard as ClipboardService } from '@ionic-native/clipboard/ngx'; import { Diagnostic as DiagnosticService } from '@ionic-native/diagnostic/ngx'; @@ -25,7 +31,9 @@ import { FileOpener as FileOpenerService } from '@ionic-native/file-opener/ngx'; import { FileTransfer as FileTransferService } from '@ionic-native/file-transfer/ngx'; import { Geolocation as GeolocationService } from '@ionic-native/geolocation/ngx'; import { Globalization as GlobalizationService } from '@ionic-native/globalization/ngx'; +import { HTTP } from '@ionic-native/http/ngx'; import { InAppBrowser as InAppBrowserService } from '@ionic-native/in-app-browser/ngx'; +import { WebView as WebViewService } from '@ionic-native/ionic-webview/ngx'; import { Keyboard as KeyboardService } from '@ionic-native/keyboard/ngx'; import { LocalNotifications as LocalNotificationsService } from '@ionic-native/local-notifications/ngx'; import { Network as NetworkService } from '@ionic-native/network/ngx'; @@ -74,6 +82,7 @@ export class Globalization extends makeSingleton(GlobalizationService) {} export class InAppBrowser extends makeSingleton(InAppBrowserService) {} export class Keyboard extends makeSingleton(KeyboardService) {} export class LocalNotifications extends makeSingleton(LocalNotificationsService) {} +export class NativeHttp extends makeSingleton(HTTP) {} export class Network extends makeSingleton(NetworkService) {} export class Push extends makeSingleton(PushService) {} export class QRScanner extends makeSingleton(QRScannerService) {} @@ -81,12 +90,17 @@ export class StatusBar extends makeSingleton(StatusBarService) {} export class SplashScreen extends makeSingleton(SplashScreenService) {} export class SQLite extends makeSingleton(SQLiteService) {} export class WebIntent extends makeSingleton(WebIntentService) {} +export class WebView extends makeSingleton(WebViewService) {} export class Zip extends makeSingleton(ZipService) {} // Convert some Angular and Ionic injectables to singletons. export class NgZone extends makeSingleton(NgZoneService) {} export class Http extends makeSingleton(HttpClient) {} export class Platform extends makeSingleton(PlatformService) {} +export class AlertController extends makeSingleton(AlertControllerService) {} +export class LoadingController extends makeSingleton(LoadingControllerService) {} +export class ModalController extends makeSingleton(ModalControllerService) {} +export class ToastController extends makeSingleton(ToastControllerService) {} // Convert external libraries injectables. export class Translate extends makeSingleton(TranslateService) {} From 6fc97ed30fbf1300eb965b1c9dcc72e6060c6d4e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 08:35:04 +0200 Subject: [PATCH 16/17] MOBILE-3565 core: Prepare translate and http libraries --- src/app/app.module.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index fc5c68fd0..c91cd54f6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -15,11 +15,13 @@ import { NgModule, Injector } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouteReuseStrategy } from '@angular/router'; +import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { IonicModule, IonicRouteStrategy, Platform } from '@ionic/angular'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; +import { CoreInterceptor } from '@classes/interceptor'; // Import core services. import { CoreAppProvider } from '@services/app'; @@ -49,23 +51,41 @@ import { CoreTimeUtilsProvider } from '@services/utils/time'; import { CoreUrlUtilsProvider } from '@services/utils/url'; import { CoreUtilsProvider } from '@services/utils/utils'; +// Import core modules. import { CoreEmulatorModule } from '@core/emulator/emulator.module'; import { CoreLoginModule } from '@core/login/login.module'; import { setSingletonsInjector } from '@singletons/core.singletons'; +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; + +// For translate loader. AoT requires an exported function for factories. +export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { + return new TranslateHttpLoader(http, './assets/lang/', '.json'); +} + @NgModule({ declarations: [AppComponent], entryComponents: [], imports: [ BrowserModule, IonicModule.forRoot(), + HttpClientModule, // HttpClient is used to make JSON requests. It fails for HEAD requests because there is no content. + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: (createTranslateLoader), + deps: [HttpClient], + }, + }), AppRoutingModule, CoreEmulatorModule, CoreLoginModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + { provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true }, CoreAppProvider, CoreConfigProvider, CoreCronDelegate, @@ -96,8 +116,8 @@ import { setSingletonsInjector } from '@singletons/core.singletons'; bootstrap: [AppComponent], }) export class AppModule { - constructor(injector: Injector, platform: Platform) { + constructor(injector: Injector, platform: Platform) { // Set the injector. setSingletonsInjector(injector); @@ -133,4 +153,5 @@ export class AppModule { // Execute the init processes. CoreInit.instance.executeInitProcesses(); } + } From 6592e2299827a033e0e8fa0c6b2a8aae8f27669c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Oct 2020 16:38:24 +0200 Subject: [PATCH 17/17] MOBILE-3565 core: Fix more ESLint after first ESLint fix integration --- src/app/classes/delegate.ts | 22 +-- src/app/classes/error.test.ts | 3 +- src/app/classes/interceptor.ts | 17 +- src/app/classes/queue-runner.ts | 2 + src/app/classes/site.ts | 30 ++-- src/app/classes/sqlitedb.ts | 58 ++++--- src/app/pipes/create-links.pipe.ts | 2 + src/app/pipes/no-tags.pipe.ts | 1 + src/app/pipes/pipes.module.ts | 2 +- src/app/pipes/time-ago.pipe.ts | 4 +- src/app/services/app.ts | 8 +- src/app/services/config.ts | 8 +- src/app/services/cron.ts | 12 +- src/app/services/filepool.ts | 217 ++++++++++++------------ src/app/services/geolocation.ts | 1 + src/app/services/groups.ts | 42 ++++- src/app/services/lang.ts | 22 +-- src/app/services/local-notifications.ts | 50 +++--- src/app/services/sites.ts | 73 +++++--- src/app/services/sync.ts | 12 +- src/app/services/update-manager.ts | 2 +- src/app/services/utils/iframe.ts | 2 +- src/app/services/utils/url.ts | 6 +- src/app/services/ws.ts | 1 + src/app/singletons/array.ts | 2 +- src/app/singletons/locutus.ts | 2 +- src/app/singletons/window.ts | 7 +- 27 files changed, 365 insertions(+), 243 deletions(-) diff --git a/src/app/classes/delegate.ts b/src/app/classes/delegate.ts index dc455ea1b..a3c9665fa 100644 --- a/src/app/classes/delegate.ts +++ b/src/app/classes/delegate.ts @@ -78,7 +78,7 @@ export class CoreDelegate { /** * Function to resolve the handlers init promise. */ - protected handlersInitResolve: (value?: any) => void; + protected handlersInitResolve: () => void; /** * Constructor of the Delegate. @@ -110,8 +110,8 @@ 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?: any[]): any { - return this.execute(this.enabledHandlers[handlerName], fnName, params); + protected executeFunctionOnEnabled(handlerName: string, fnName: string, params?: unknown[]): T { + 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?: any[]): any { + protected executeFunction(handlerName: string, fnName: string, params?: unknown[]): T { 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?: any[]): any { + private execute(handler: CoreDelegateHandler, fnName: string, params?: unknown[]): T { if (handler && handler[fnName]) { return handler[fnName].apply(handler, params); } else if (this.defaultHandler && this.defaultHandler[fnName]) { @@ -180,10 +180,10 @@ export class CoreDelegate { * @param onlyEnabled If check only enabled handlers or all. * @return Function returned value or default value. */ - protected hasFunction(handlerName: string, fnName: string, onlyEnabled: boolean = true): any { + protected hasFunction(handlerName: string, fnName: string, onlyEnabled: boolean = true): boolean { const handler = onlyEnabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName]; - return handler && handler[fnName]; + return handler && typeof handler[fnName] == 'function'; } /** @@ -240,7 +240,7 @@ export class CoreDelegate { * @param time Time this update process started. * @return Resolved when done. */ - protected updateHandler(handler: CoreDelegateHandler, time: number): Promise { + protected updateHandler(handler: CoreDelegateHandler): Promise { const siteId = CoreSites.instance.getCurrentSiteId(); const currentSite = CoreSites.instance.getCurrentSite(); let promise: Promise; @@ -255,7 +255,7 @@ export class CoreDelegate { if (!CoreSites.instance.isLoggedIn() || this.isFeatureDisabled(handler, currentSite)) { promise = Promise.resolve(false); } else { - promise = handler.isEnabled().catch(() => false); + promise = Promise.resolve(handler.isEnabled()).catch(() => false); } // Checks if the handler is enabled. @@ -304,7 +304,7 @@ export class CoreDelegate { // Loop over all the handlers. for (const name in this.handlers) { - promises.push(this.updateHandler(this.handlers[name], now)); + promises.push(this.updateHandler(this.handlers[name])); } try { @@ -326,7 +326,7 @@ export class CoreDelegate { * Update handlers Data. * Override this function to update handlers data. */ - updateData(): any { + updateData(): void { // To be overridden. } diff --git a/src/app/classes/error.test.ts b/src/app/classes/error.test.ts index 133f5bd01..e7f0e9cf5 100644 --- a/src/app/classes/error.test.ts +++ b/src/app/classes/error.test.ts @@ -11,9 +11,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + import Faker from 'faker'; -import { CoreError } from './error'; +import { CoreError } from '@classes/errors/error'; describe('CoreError', () => { diff --git a/src/app/classes/interceptor.ts b/src/app/classes/interceptor.ts index 587c9ca9d..c7fcaa782 100644 --- a/src/app/classes/interceptor.ts +++ b/src/app/classes/interceptor.ts @@ -30,28 +30,26 @@ 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 static serialize(obj: any, addNull?: boolean): string { let query = ''; - let fullSubName: string; - let subValue; - let innerObj; for (const name in obj) { const value = obj[name]; if (value instanceof Array) { for (let i = 0; i < value.length; ++i) { - subValue = value[i]; - fullSubName = name + '[' + i + ']'; - innerObj = {}; + const subValue = value[i]; + const fullSubName = name + '[' + i + ']'; + const innerObj = {}; innerObj[fullSubName] = subValue; query += this.serialize(innerObj) + '&'; } } else if (value instanceof Object) { for (const subName in value) { - subValue = value[subName]; - fullSubName = name + '[' + subName + ']'; - innerObj = {}; + const subValue = value[subName]; + const fullSubName = name + '[' + subName + ']'; + const innerObj = {}; innerObj[fullSubName] = subValue; query += this.serialize(innerObj) + '&'; } @@ -63,6 +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 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/queue-runner.ts b/src/app/classes/queue-runner.ts index aa9868ccd..3b05b1369 100644 --- a/src/app/classes/queue-runner.ts +++ b/src/app/classes/queue-runner.ts @@ -17,11 +17,13 @@ import { CoreUtils, PromiseDefer } from '@services/utils/utils'; /** * Function to add to the queue. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type CoreQueueRunnerFunction = (...args: any[]) => T | Promise; /** * Queue item. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type CoreQueueRunnerItem = { /** * Item ID. diff --git a/src/app/classes/site.ts b/src/app/classes/site.ts index 9651d195f..97cad6e9c 100644 --- a/src/app/classes/site.ts +++ b/src/app/classes/site.ts @@ -23,7 +23,7 @@ import { CoreWS, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets, Core import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUrlUtils, CoreUrlParams } from '@services/utils/url'; import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { CoreConstants } from '@core/constants'; import CoreConfigConstants from '@app/config.json'; @@ -857,7 +857,7 @@ export class CoreSite { let promise: Promise; if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) { - promise = this.db.getRecords(CoreSite.WS_CACHE_TABLE, { key: preSets.cacheKey }).then((entries) => { + 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 }); @@ -901,7 +901,7 @@ export class CoreSite { 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.')); @@ -915,7 +915,7 @@ export class CoreSite { * @param componentId Optional component id (if not included, returns sum for whole component) * @return Promise resolved when we have calculated the size */ - getComponentCacheSize(component: string, componentId?: number): Promise { + async getComponentCacheSize(component: string, componentId?: number): Promise { const params: Array = [component]; let extraClause = ''; if (componentId !== undefined && componentId !== null) { @@ -923,8 +923,10 @@ export class CoreSite { extraClause = ' AND componentId = ?'; } - return this.db.getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE + + const size = await this.db.getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE + ' WHERE component = ?' + extraClause, params); + + return size; } /** @@ -1018,7 +1020,7 @@ export class CoreSite { params['componentId'] = componentId; } - return this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, params); + await this.db.deleteRecords(CoreSite.WS_CACHE_TABLE, params); } /* @@ -1192,8 +1194,10 @@ export class CoreSite { * * @return Promise resolved with the total size of all data in the cache table (bytes) */ - getCacheUsage(): Promise { - return this.db.getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE); + async getCacheUsage(): Promise { + const size = await this.db.getFieldSql('SELECT SUM(length(data)) FROM ' + CoreSite.WS_CACHE_TABLE); + + return size; } /** @@ -1228,7 +1232,7 @@ export class CoreSite { * @param anchor Anchor text if needed. * @return URL with params. */ - createSiteUrl(path: string, params?: {[key: string]: unknown}, anchor?: string): string { + createSiteUrl(path: string, params?: CoreUrlParams, anchor?: string): string { return CoreUrlUtils.instance.addParamsToUrl(this.siteUrl + path, params, anchor); } @@ -1797,7 +1801,8 @@ export class CoreSite { * @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) => { + return this.db.getRecord(CoreSite.CONFIG_TABLE, { name }).then((entry) => entry.value) + .catch((error) => { if (typeof defaultValue != 'undefined') { return defaultValue; } @@ -2151,3 +2156,8 @@ export type CoreSiteCallExternalFunctionsResult = { exception?: string; // JSON-encoed exception info. }[]; }; + +export type CoreSiteConfigDBRecord = { + name: string; + value: string | number; +}; diff --git a/src/app/classes/sqlitedb.ts b/src/app/classes/sqlitedb.ts index 135770a52..ff0acb7da 100644 --- a/src/app/classes/sqlitedb.ts +++ b/src/app/classes/sqlitedb.ts @@ -15,6 +15,7 @@ import { SQLiteObject } from '@ionic-native/sqlite/ngx'; import { SQLite, Platform } from '@singletons/core.singletons'; +import { CoreError } from '@classes/errors/error'; /** * Schema of a table. @@ -411,6 +412,7 @@ export class 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?: SQLiteDBRecordValue[]): Promise { await this.ready(); @@ -425,7 +427,8 @@ export class SQLiteDB { * @param sqlStatements SQL statements to execute. * @return Promise resolved with the result. */ - async executeBatch(sqlStatements: (string | SQLiteDBRecordValue[])[][]): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async executeBatch(sqlStatements: (string | string[] | any)[]): Promise { await this.ready(); await this.db.sqlBatch(sqlStatements); @@ -453,9 +456,10 @@ export class SQLiteDB { * Format the data to where params. * * @param data Object data. + * @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]); } /** @@ -464,7 +468,7 @@ export class SQLiteDB { * @param table The table to query. * @return Promise resolved with the records. */ - async getAllRecords(table: string): Promise { + async getAllRecords(table: string): Promise { return this.getRecords(table); } @@ -510,7 +514,7 @@ export class SQLiteDB { async getFieldSql(sql: string, params?: SQLiteDBRecordValue[]): Promise { const record = await this.getRecordSql(sql, params); if (!record) { - throw null; + throw new CoreError('No record found.'); } return record[Object.keys(record)[0]]; @@ -574,10 +578,10 @@ export class SQLiteDB { * @param fields A comma separated list of fields to return. * @return Promise resolved with the record, rejected if not found. */ - getRecord(table: string, conditions?: SQLiteDBRecordValues, fields: string = '*'): Promise { + getRecord(table: string, conditions?: SQLiteDBRecordValues, fields: string = '*'): Promise { const selectAndParams = this.whereClause(conditions); - return this.getRecordSelect(table, selectAndParams[0], selectAndParams[1], fields); + return this.getRecordSelect(table, selectAndParams[0], selectAndParams[1], fields); } /** @@ -589,13 +593,13 @@ export class SQLiteDB { * @param fields A comma separated list of fields to return. * @return Promise resolved with the record, rejected if not found. */ - getRecordSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], fields: string = '*'): - Promise { + getRecordSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], fields: string = '*'): + Promise { if (select) { select = ' WHERE ' + select; } - return this.getRecordSql(`SELECT ${fields} FROM ${table} ${select}`, params); + return this.getRecordSql(`SELECT ${fields} FROM ${table} ${select}`, params); } /** @@ -608,11 +612,11 @@ export class SQLiteDB { * @param params List of sql parameters * @return Promise resolved with the records. */ - async getRecordSql(sql: string, params?: SQLiteDBRecordValue[]): Promise { - const result = await this.getRecordsSql(sql, params, 0, 1); + async getRecordSql(sql: string, params?: SQLiteDBRecordValue[]): Promise { + const result = await this.getRecordsSql(sql, params, 0, 1); if (!result || !result.length) { // Not found, reject. - throw null; + throw new CoreError('No records found.'); } return result[0]; @@ -629,11 +633,11 @@ export class SQLiteDB { * @param limitNum Return a subset comprising this many records in total. * @return Promise resolved with the records. */ - getRecords(table: string, conditions?: SQLiteDBRecordValues, sort: string = '', fields: string = '*', limitFrom: number = 0, - limitNum: number = 0): Promise { + getRecords(table: string, conditions?: SQLiteDBRecordValues, sort: string = '', fields: string = '*', + limitFrom: number = 0, limitNum: number = 0): Promise { const selectAndParams = this.whereClause(conditions); - return this.getRecordsSelect(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum); + return this.getRecordsSelect(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum); } /** @@ -648,11 +652,11 @@ export class SQLiteDB { * @param limitNum Return a subset comprising this many records in total. * @return Promise resolved with the records. */ - getRecordsList(table: string, field: string, values: SQLiteDBRecordValue[], sort: string = '', fields: string = '*', - limitFrom: number = 0, limitNum: number = 0): Promise { + getRecordsList(table: string, field: string, values: SQLiteDBRecordValue[], sort: string = '', + fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise { const selectAndParams = this.whereClauseList(field, values); - return this.getRecordsSelect(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum); + return this.getRecordsSelect(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum); } /** @@ -667,8 +671,8 @@ export class SQLiteDB { * @param limitNum Return a subset comprising this many records in total. * @return Promise resolved with the records. */ - getRecordsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], sort: string = '', - fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise { + getRecordsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], sort: string = '', + fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise { if (select) { select = ' WHERE ' + select; } @@ -678,7 +682,7 @@ export class SQLiteDB { const sql = `SELECT ${fields} FROM ${table} ${select} ${sort}`; - return this.getRecordsSql(sql, params, limitFrom, limitNum); + return this.getRecordsSql(sql, params, limitFrom, limitNum); } /** @@ -690,8 +694,8 @@ export class SQLiteDB { * @param limitNum Return a subset comprising this many records. * @return Promise resolved with the records. */ - async getRecordsSql(sql: string, params?: SQLiteDBRecordValue[], limitFrom?: number, limitNum?: number): - Promise { + async getRecordsSql(sql: string, params?: SQLiteDBRecordValue[], limitFrom?: number, limitNum?: number): + Promise { const limits = this.normaliseLimitFromNum(limitFrom, limitNum); if (limits[0] || limits[1]) { @@ -768,7 +772,7 @@ export class SQLiteDB { */ async insertRecords(table: string, dataObjects: SQLiteDBRecordValues[]): Promise { if (!Array.isArray(dataObjects)) { - throw null; + throw new CoreError('Invalid parameter supplied to insertRecords, it should be an array.'); } const statements = dataObjects.map((dataObject) => { @@ -854,7 +858,7 @@ export class SQLiteDB { async recordExists(table: string, conditions?: SQLiteDBRecordValues): Promise { const record = await this.getRecord(table, conditions); if (!record) { - throw null; + throw new CoreError('Record does not exist.'); } } @@ -869,7 +873,7 @@ export class SQLiteDB { async recordExistsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = []): Promise { const record = await this.getRecordSelect(table, select, params); if (!record) { - throw null; + throw new CoreError('Record does not exist.'); } } @@ -883,7 +887,7 @@ export class SQLiteDB { async recordExistsSql(sql: string, params?: SQLiteDBRecordValue[]): Promise { const record = await this.getRecordSql(sql, params); if (!record) { - throw null; + throw new CoreError('Record does not exist.'); } } diff --git a/src/app/pipes/create-links.pipe.ts b/src/app/pipes/create-links.pipe.ts index e031c9b0d..72a8ced9f 100644 --- a/src/app/pipes/create-links.pipe.ts +++ b/src/app/pipes/create-links.pipe.ts @@ -21,6 +21,7 @@ import { Pipe, PipeTransform } from '@angular/core'; name: 'coreCreateLinks', }) export class CoreCreateLinksPipe implements PipeTransform { + protected static replacePattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])(?![^<]*>|[^<>]*<\/)/gim; /** @@ -32,4 +33,5 @@ export class CoreCreateLinksPipe implements PipeTransform { transform(text: string): string { return text.replace(CoreCreateLinksPipe.replacePattern, '$1'); } + } diff --git a/src/app/pipes/no-tags.pipe.ts b/src/app/pipes/no-tags.pipe.ts index c1da56431..8fb0c83b5 100644 --- a/src/app/pipes/no-tags.pipe.ts +++ b/src/app/pipes/no-tags.pipe.ts @@ -31,4 +31,5 @@ export class CoreNoTagsPipe implements PipeTransform { transform(text: string): string { return text.replace(/(<([^>]+)>)/ig, ''); } + } diff --git a/src/app/pipes/pipes.module.ts b/src/app/pipes/pipes.module.ts index 053fec971..62373d7f6 100644 --- a/src/app/pipes/pipes.module.ts +++ b/src/app/pipes/pipes.module.ts @@ -28,6 +28,6 @@ import { CoreTimeAgoPipe } from './time-ago.pipe'; CoreCreateLinksPipe, CoreNoTagsPipe, CoreTimeAgoPipe, - ] + ], }) export class CorePipesModule {} diff --git a/src/app/pipes/time-ago.pipe.ts b/src/app/pipes/time-ago.pipe.ts index ad53c086a..a768b9a37 100644 --- a/src/app/pipes/time-ago.pipe.ts +++ b/src/app/pipes/time-ago.pipe.ts @@ -24,6 +24,7 @@ import moment from 'moment'; name: 'coreTimeAgo', }) export class CoreTimeAgoPipe implements PipeTransform { + private logger: CoreLogger; constructor() { @@ -48,6 +49,7 @@ export class CoreTimeAgoPipe implements PipeTransform { timestamp = numberTimestamp; } - return Translate.instance.instant('core.ago', {$a: moment(timestamp * 1000).fromNow(true)}); + return Translate.instance.instant('core.ago', { $a: moment(timestamp * 1000).fromNow(true) }); } + } diff --git a/src/app/services/app.ts b/src/app/services/app.ts index 496761fe9..84d223281 100644 --- a/src/app/services/app.ts +++ b/src/app/services/app.ts @@ -174,7 +174,8 @@ export class CoreAppProvider { await this.createVersionsTableReady; // Fetch installed version of the schema. - const entry = await this.db.getRecord(SCHEMA_VERSIONS_TABLE, { name: schema.name }); + const entry = await this.db.getRecord(SCHEMA_VERSIONS_TABLE, { name: schema.name }); + oldVersion = entry.version; } catch (error) { // No installed version yet. @@ -796,3 +797,8 @@ export type WindowForAutomatedTests = Window & { appProvider?: CoreAppProvider; appRef?: ApplicationRef; }; + +type SchemaVersionsDBEntry = { + name: string; + version: number; +}; diff --git a/src/app/services/config.ts b/src/app/services/config.ts index 46faa027b..5fb874158 100644 --- a/src/app/services/config.ts +++ b/src/app/services/config.ts @@ -81,7 +81,7 @@ export class CoreConfigProvider { await this.dbReady; try { - const entry = await this.appDB.getRecord(TABLE_NAME, { name }); + const entry = await this.appDB.getRecord(TABLE_NAME, { name }); return entry.value; } catch (error) { @@ -109,3 +109,9 @@ export class CoreConfigProvider { } export class CoreConfig extends makeSingleton(CoreConfigProvider) {} + +type ConfigDBEntry = { + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; +}; diff --git a/src/app/services/cron.ts b/src/app/services/cron.ts index ab6b2c9c4..53c23b38a 100644 --- a/src/app/services/cron.ts +++ b/src/app/services/cron.ts @@ -64,7 +64,7 @@ export class CoreCronDelegate { protected appDB: SQLiteDB; protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected handlers: { [s: string]: CoreCronHandler } = {}; - protected queuePromise = Promise.resolve(); + protected queuePromise: Promise = Promise.resolve(); constructor(zone: NgZone) { this.logger = CoreLogger.getInstance('CoreCronDelegate'); @@ -271,8 +271,9 @@ export class CoreCronDelegate { const id = this.getHandlerLastExecutionId(name); try { - const entry = await this.appDB.getRecord(CRON_TABLE, { id }); - const time = parseInt(entry.value, 10); + const entry = await this.appDB.getRecord(CRON_TABLE, { id }); + + const time = Number(entry.value); return isNaN(time) ? 0 : time; } catch (err) { @@ -573,3 +574,8 @@ export interface CoreCronHandler { export type WindowForAutomatedTests = Window & { cronProvider?: CoreCronDelegate; }; + +type CronDBEntry = { + id: string; + value: number; +}; diff --git a/src/app/services/filepool.ts b/src/app/services/filepool.ts index ac0ea8b9a..b910c9334 100644 --- a/src/app/services/filepool.ts +++ b/src/app/services/filepool.ts @@ -29,8 +29,9 @@ import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { SQLiteDB } from '@classes/sqlitedb'; +import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@core/constants'; -import { makeSingleton, Network, NgZone } from '@singletons/core.singletons'; +import { makeSingleton, Network, NgZone, Translate } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; /* @@ -56,7 +57,7 @@ export class CoreFilepoolProvider { protected static readonly ERR_FS_OR_NETWORK_UNAVAILABLE = 'CoreFilepoolError:ERR_FS_OR_NETWORK_UNAVAILABLE'; protected static readonly ERR_QUEUE_ON_PAUSE = 'CoreFilepoolError:ERR_QUEUE_ON_PAUSE'; - protected static readonly FILE_UPDATE_ANY_WHERE_CLAUSE = + protected static readonly FILE_UPDATE_UNKNOWN_WHERE_CLAUSE = 'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))'; // Variables for database. @@ -239,7 +240,7 @@ export class CoreFilepoolProvider { protected appDB: SQLiteDB; protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected queueState: string; - protected urlAttributes = [ + protected urlAttributes: RegExp[] = [ new RegExp('(\\?|&)token=([A-Za-z0-9]*)'), new RegExp('(\\?|&)forcedownload=[0-1]'), new RegExp('(\\?|&)preview=[A-Za-z0-9]+'), @@ -248,7 +249,7 @@ export class CoreFilepoolProvider { // To handle file downloads using the queue. protected queueDeferreds: { [s: string]: { [s: string]: CoreFilepoolPromiseDefer } } = {}; - protected sizeCache = {}; // A "cache" to store file sizes to prevent performing too many HEAD requests. + protected sizeCache: {[fileUrl: string]: number} = {}; // A "cache" to store file sizes. // Variables to prevent downloading packages/files twice at the same time. protected packagesPromises: { [s: string]: { [s: string]: Promise } } = {}; protected filePromises: { [s: string]: { [s: string]: Promise } } = {}; @@ -288,7 +289,7 @@ export class CoreFilepoolProvider { */ protected async addFileLink(siteId: string, fileId: string, component: string, componentId?: string | number): Promise { if (!component) { - throw null; + throw new CoreError('Cannot add link because component is invalid.'); } componentId = this.fixComponentId(componentId); @@ -358,8 +359,10 @@ export class CoreFilepoolProvider { * @return Promise resolved on success. */ protected async addFileToPool(siteId: string, fileId: string, data: CoreFilepoolFileEntry): Promise { - const record = Object.assign({}, data); - record.fileId = fileId; + const record = { + fileId, + ...data, + }; const db = await CoreSites.instance.getSiteDb(siteId); @@ -457,12 +460,12 @@ export class CoreFilepoolProvider { await this.dbReady; if (!CoreFile.instance.isAvailable()) { - throw null; + throw new CoreError('File system cannot be used.'); } const site = await CoreSites.instance.getSite(siteId); if (!site.canDownloadFiles()) { - throw null; + throw new CoreError('Site doesn\'t allow downloading files.'); } let file: CoreWSExternalFile; @@ -488,7 +491,7 @@ export class CoreFilepoolProvider { const queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress); return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => { - const newData: CoreFilepoolQueueEntry = {}; + const newData: CoreFilepoolQueueDBEntry = {}; let foundLink = false; if (entry) { @@ -562,14 +565,14 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @param timemodified The time this file was modified. * @param checkSize True if we shouldn't download files if their size is big, false otherwise. - * @param downloadAny True to download file in WiFi if their size is any, false otherwise. + * @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise. * Ignored if checkSize=false. * @param options Extra options (isexternalfile, repositorytype). * @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, downloadAny?: boolean, options: CoreFilepoolFileOptions = {}, + 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. @@ -584,7 +587,7 @@ export class CoreFilepoolProvider { } else { if (!CoreApp.instance.isOnline()) { // Cannot check size in offline, stop. - throw null; + throw new CoreError(Translate.instance.instant('core.cannotconnect')); } size = await CoreWS.instance.getRemoteFileSize(fileUrl); @@ -592,16 +595,16 @@ export class CoreFilepoolProvider { // Calculate the size of the file. const isWifi = CoreApp.instance.isWifi(); - const sizeAny = size <= 0; + const sizeUnknown = size <= 0; - if (!sizeAny) { + if (!sizeUnknown) { // Store the size in the cache. this.sizeCache[fileUrl] = size; } // Check if the file should be downloaded. - if (sizeAny) { - if (downloadAny && isWifi) { + if (sizeUnknown) { + if (downloadUnknown && isWifi) { await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, revision, true); } @@ -685,7 +688,7 @@ export class CoreFilepoolProvider { const count = await db.countRecords(CoreFilepoolProvider.LINKS_TABLE, conditions); if (count <= 0) { - return null; + throw new CoreError('Component doesn\'t have files'); } } @@ -696,7 +699,7 @@ 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 { + protected createComponentLink(component: string, componentId?: string | number): CoreFilepoolComponentLink | null { if (typeof component != 'undefined' && component != null) { return { component, componentId: this.fixComponentId(componentId) }; } @@ -779,7 +782,7 @@ export class CoreFilepoolProvider { if (poolFileObject && poolFileObject.fileId !== fileId) { this.logger.error('Invalid object to update passed'); - throw null; + throw new CoreError('Invalid object to update passed.'); } const downloadId = this.getFileDownloadId(fileUrl, filePath); @@ -793,7 +796,7 @@ export class CoreFilepoolProvider { this.filePromises[siteId][downloadId] = CoreSites.instance.getSite(siteId).then(async (site) => { if (!site.canDownloadFiles()) { - return Promise.reject(null); + throw new CoreError('Site doesn\'t allow downloading files.'); } const entry = await CoreWS.instance.downloadFile(fileUrl, filePath, addExtension, onProgress); @@ -952,7 +955,7 @@ export class CoreFilepoolProvider { try { await Promise.all(promises); // Success prefetching, store package as downloaded. - this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra); + await this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra); } catch (error) { // Error downloading, go back to previous status and reject the promise. await this.setPackagePreviousStatus(siteId, component, componentId); @@ -1014,7 +1017,7 @@ export class CoreFilepoolProvider { let alreadyDownloaded = true; if (!CoreFile.instance.isAvailable()) { - throw null; + throw new CoreError('File system cannot be used.'); } const file = await this.fixPluginfileURL(siteId, fileUrl); @@ -1093,14 +1096,12 @@ export class CoreFilepoolProvider { let urls = []; const element = CoreDomUtils.instance.convertToElement(html); - const elements = element.querySelectorAll('a, img, audio, video, source, track'); + const elements: (HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | + HTMLTrackElement)[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track')); for (let i = 0; i < elements.length; i++) { const element = elements[i]; - let url = element.tagName === 'A' - ? (element as HTMLAnchorElement).href - : (element as HTMLImageElement | HTMLVideoElement | HTMLAudioElement | - HTMLAudioElement | HTMLTrackElement | HTMLSourceElement).src; + let url = 'href' in element ? element.href : element.src; if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) { urls.push(url); @@ -1236,7 +1237,7 @@ export class CoreFilepoolProvider { componentId: this.fixComponentId(componentId), }; - const items = await db.getRecords(CoreFilepoolProvider.LINKS_TABLE, conditions); + const items = await db.getRecords(CoreFilepoolProvider.LINKS_TABLE, conditions); items.forEach((item) => { item.componentId = this.fixComponentId(item.componentId); }); @@ -1252,16 +1253,16 @@ export class CoreFilepoolProvider { * @return Resolved with the URL. Rejected otherwise. */ async getDirectoryUrlByUrl(siteId: string, fileUrl: string): Promise { - if (CoreFile.instance.isAvailable()) { - const file = await this.fixPluginfileURL(siteId, fileUrl); - const fileId = this.getFileIdByUrl(file.fileurl); - const filePath = await this.getFilePath(siteId, fileId, ''); - const dirEntry = await CoreFile.instance.getDir(filePath); - - return dirEntry.toURL(); + if (!CoreFile.instance.isAvailable()) { + throw new CoreError('File system cannot be used.'); } - throw null; + const file = await this.fixPluginfileURL(siteId, fileUrl); + const fileId = this.getFileIdByUrl(file.fileurl); + const filePath = await this.getFilePath(siteId, fileId, ''); + const dirEntry = await CoreFile.instance.getDir(filePath); + + return dirEntry.toURL(); } /** @@ -1346,7 +1347,8 @@ export class CoreFilepoolProvider { */ protected async getFileLinks(siteId: string, fileId: string): Promise { const db = await CoreSites.instance.getSiteDb(siteId); - const items = await db.getRecords(CoreFilepoolProvider.LINKS_TABLE, { fileId }); + const items = await db.getRecords(CoreFilepoolProvider.LINKS_TABLE, { fileId }); + items.forEach((item) => { item.componentId = this.fixComponentId(item.componentId); }); @@ -1421,7 +1423,7 @@ export class CoreFilepoolProvider { const files = []; const promises = items.map((item) => - db.getRecord(CoreFilepoolProvider.FILES_TABLE, { fileId: item.fileId }).then((fileEntry) => { + db.getRecord(CoreFilepoolProvider.FILES_TABLE, { fileId: item.fileId }).then((fileEntry) => { if (!fileEntry) { return; } @@ -1532,7 +1534,7 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @param timemodified The time this file was modified. * @param checkSize True if we shouldn't download files if their size is big, false otherwise. - * @param downloadAny True to download file in WiFi if their size is any, false otherwise. + * @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise. * Ignored if checkSize=false. * @param options Extra options (isexternalfile, repositorytype). * @param revision File revision. If not defined, it will be calculated using the URL. @@ -1544,12 +1546,12 @@ export class CoreFilepoolProvider { * 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, downloadAny?: boolean, + 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, - downloadAny, options, revision).catch(() => { + downloadUnknown, options, revision).catch(() => { // Ignore errors. }); }; @@ -1594,7 +1596,7 @@ export class CoreFilepoolProvider { return fileUrl; } - throw null; + throw new CoreError('File not found.'); } }, () => { // We do not have the file in store yet. Add to queue and return the fixed URL. @@ -1614,14 +1616,14 @@ export class CoreFilepoolProvider { * @return Resolved with the internal URL. Rejected otherwise. */ protected async getInternalSrcById(siteId: string, fileId: string): Promise { - if (CoreFile.instance.isAvailable()) { - const path = await this.getFilePath(siteId, fileId); - const fileEntry = await CoreFile.instance.getFile(path); - - return CoreFile.instance.convertFileSrc(fileEntry.toURL()); + if (!CoreFile.instance.isAvailable()) { + throw new CoreError('File system cannot be used.'); } - throw null; + const path = await this.getFilePath(siteId, fileId); + const fileEntry = await CoreFile.instance.getFile(path); + + return CoreFile.instance.convertFileSrc(fileEntry.toURL()); } /** @@ -1632,19 +1634,19 @@ export class CoreFilepoolProvider { * @return Resolved with the URL. Rejected otherwise. */ protected async getInternalUrlById(siteId: string, fileId: string): Promise { - if (CoreFile.instance.isAvailable()) { - const path = await this.getFilePath(siteId, fileId); - const fileEntry = await CoreFile.instance.getFile(path); - - // This URL is usually used to launch files or put them in HTML. In desktop we need the internal URL. - if (CoreApp.instance.isDesktop()) { - return fileEntry.toInternalURL(); - } else { - return fileEntry.toURL(); - } + if (!CoreFile.instance.isAvailable()) { + throw new CoreError('File system cannot be used.'); } - throw null; + const path = await this.getFilePath(siteId, fileId); + const fileEntry = await CoreFile.instance.getFile(path); + + // This URL is usually used to launch files or put them in HTML. In desktop we need the internal URL. + if (CoreApp.instance.isDesktop()) { + return fileEntry.toInternalURL(); + } else { + return fileEntry.toURL(); + } } /** @@ -1654,13 +1656,13 @@ export class CoreFilepoolProvider { * @return Resolved with the URL. */ protected async getInternalUrlByPath(filePath: string): Promise { - if (CoreFile.instance.isAvailable()) { - const fileEntry = await CoreFile.instance.getFile(filePath); - - return fileEntry.toURL(); + if (!CoreFile.instance.isAvailable()) { + throw new CoreError('File system cannot be used.'); } - throw null; + const fileEntry = await CoreFile.instance.getFile(filePath); + + return fileEntry.toURL(); } /** @@ -1671,14 +1673,14 @@ export class CoreFilepoolProvider { * @return Resolved with the URL. Rejected otherwise. */ async getInternalUrlByUrl(siteId: string, fileUrl: string): Promise { - if (CoreFile.instance.isAvailable()) { - const file = await this.fixPluginfileURL(siteId, fileUrl); - const fileId = this.getFileIdByUrl(file.fileurl); - - return this.getInternalUrlById(siteId, fileId); + if (!CoreFile.instance.isAvailable()) { + throw new CoreError('File system cannot be used.'); } - throw null; + const file = await this.fixPluginfileURL(siteId, fileUrl); + const fileId = this.getFileIdByUrl(file.fileurl); + + return this.getInternalUrlById(siteId, fileId); } /** @@ -1748,16 +1750,16 @@ export class CoreFilepoolProvider { * @return Resolved with the URL. */ async getPackageDirUrlByUrl(siteId: string, url: string): Promise { - if (CoreFile.instance.isAvailable()) { - const file = await this.fixPluginfileURL(siteId, url); - const dirName = this.getPackageDirNameByUrl(file.fileurl); - const dirPath = await this.getFilePath(siteId, dirName, ''); - const dirEntry = await CoreFile.instance.getDir(dirPath); - - return dirEntry.toURL(); + if (!CoreFile.instance.isAvailable()) { + throw new CoreError('File system cannot be used.'); } - throw null; + const file = await this.fixPluginfileURL(siteId, url); + const dirName = this.getPackageDirNameByUrl(file.fileurl); + const dirPath = await this.getFilePath(siteId, dirName, ''); + const dirEntry = await CoreFile.instance.getDir(dirPath); + + return dirEntry.toURL(); } /** @@ -1973,7 +1975,7 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @param timemodified The time this file was modified. * @param checkSize True if we shouldn't download files if their size is big, false otherwise. - * @param downloadAny True to download file in WiFi if their size is any, false otherwise. + * @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise. * Ignored if checkSize=false. * @param options Extra options (isexternalfile, repositorytype). * @param revision File revision. If not defined, it will be calculated using the URL. @@ -1983,10 +1985,10 @@ export class CoreFilepoolProvider { * 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, downloadAny?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): + checkSize: boolean = true, downloadUnknown?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): Promise { return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'src', - timemodified, checkSize, downloadAny, options, revision); + timemodified, checkSize, downloadUnknown, options, revision); } /** @@ -2017,7 +2019,7 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @param timemodified The time this file was modified. * @param checkSize True if we shouldn't download files if their size is big, false otherwise. - * @param downloadAny True to download file in WiFi if their size is any, false otherwise. + * @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise. * Ignored if checkSize=false. * @param options Extra options (isexternalfile, repositorytype). * @param revision File revision. If not defined, it will be calculated using the URL. @@ -2027,10 +2029,10 @@ export class CoreFilepoolProvider { * 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, downloadAny?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): + checkSize: boolean = true, downloadUnknown?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): Promise { return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'url', - timemodified, checkSize, downloadAny, options, revision); + timemodified, checkSize, downloadUnknown, options, revision); } /** @@ -2100,9 +2102,10 @@ export class CoreFilepoolProvider { */ protected async hasFileInPool(siteId: string, fileId: string): Promise { const db = await CoreSites.instance.getSiteDb(siteId); - const entry = await db.getRecord(CoreFilepoolProvider.FILES_TABLE, { fileId }); + const entry = await db.getRecord(CoreFilepoolProvider.FILES_TABLE, { fileId }); + if (typeof entry === 'undefined') { - throw null; + throw new CoreError('File not found in filepool.'); } return entry; @@ -2118,12 +2121,13 @@ export class CoreFilepoolProvider { protected async hasFileInQueue(siteId: string, fileId: string): Promise { await this.dbReady; - const entry = await this.appDB.getRecord(CoreFilepoolProvider.QUEUE_TABLE, { siteId, fileId }); + const entry = await this.appDB.getRecord(CoreFilepoolProvider.QUEUE_TABLE, { siteId, fileId }); + if (typeof entry === 'undefined') { - throw null; + 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; } @@ -2132,14 +2136,14 @@ export class CoreFilepoolProvider { * Invalidate all the files in a site. * * @param siteId The site ID. - * @param onlyAny True to only invalidate files from external repos or without revision/timemodified. + * @param onlyUnknown True to only invalidate files from external repos or without revision/timemodified. * It is advised to set it to true to reduce the performance and data usage of the app. * @return Resolved on success. */ - async invalidateAllFiles(siteId: string, onlyAny: boolean = true): Promise { + async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise { const db = await CoreSites.instance.getSiteDb(siteId); - const where = onlyAny ? CoreFilepoolProvider.FILE_UPDATE_ANY_WHERE_CLAUSE : null; + const where = onlyUnknown ? CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : null; await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, where); } @@ -2171,11 +2175,11 @@ export class CoreFilepoolProvider { * @param siteId The site ID. * @param component The component to invalidate. * @param componentId An ID to use in conjunction with the component. - * @param onlyAny True to only invalidate files from external repos or without revision/timemodified. - * It is advised to set it to true to reduce the performance and data usage of the app. + * @param onlyUnknown True to only invalidate files from external repos or without revision/timemodified. + * 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, onlyAny: boolean = true): + async invalidateFilesByComponent(siteId: string, component: string, componentId?: string | number, onlyUnknown: boolean = true): Promise { const db = await CoreSites.instance.getSiteDb(siteId); @@ -2191,8 +2195,8 @@ export class CoreFilepoolProvider { whereAndParams[0] = 'fileId ' + whereAndParams[0]; - if (onlyAny) { - whereAndParams[0] += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_ANY_WHERE_CLAUSE + ')'; + if (onlyUnknown) { + whereAndParams[0] += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')'; } await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]); @@ -2258,7 +2262,7 @@ export class CoreFilepoolProvider { * @param entry Filepool entry. * @return Whether it cannot determine updates. */ - protected isFileUpdateAny(entry: CoreFilepoolFileEntry): boolean { + protected isFileUpdateUnknown(entry: CoreFilepoolFileEntry): boolean { return !!entry.isexternalfile || (!entry.revision && !entry.timemodified); } @@ -2433,7 +2437,7 @@ export class CoreFilepoolProvider { let items: CoreFilepoolQueueEntry[]; try { - items = await this.appDB.getRecords(CoreFilepoolProvider.QUEUE_TABLE, undefined, + items = await this.appDB.getRecords(CoreFilepoolProvider.QUEUE_TABLE, undefined, 'priority DESC, added ASC', undefined, 0, 1); } catch (err) { throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; @@ -2444,7 +2448,7 @@ export class CoreFilepoolProvider { throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; } // Convert the links to an object. - item.linksUnserialized = CoreTextUtils.instance.parseJSON(item.links, []); + item.linksUnserialized = CoreTextUtils.instance.parseJSON(item.links, []); return this.processQueueItem(item); } @@ -2760,7 +2764,7 @@ export class CoreFilepoolProvider { const mimetype = await CoreUtils.instance.getMimeTypeFromUrl(url); // If the file is streaming (audio or video) we reject. if (mimetype.indexOf('video') != -1 || mimetype.indexOf('audio') != -1) { - throw null; + throw new CoreError('File is audio or video.'); } } @@ -2988,9 +2992,9 @@ export type CoreFilepoolFileEntry = CoreFilepoolFileOptions & { }; /** - * Entry from the file's queue. + * DB data for entry from file's queue. */ -export type CoreFilepoolQueueEntry = CoreFilepoolFileOptions & { +export type CoreFilepoolQueueDBEntry = CoreFilepoolFileOptions & { /** * The site the file belongs to. */ @@ -3025,7 +3029,12 @@ export type CoreFilepoolQueueEntry = CoreFilepoolFileOptions & { * File links (to link the file to components and componentIds). Serialized to store on DB. */ links?: string; +}; +/** + * Entry from the file's queue. + */ +export type CoreFilepoolQueueEntry = CoreFilepoolQueueDBEntry & { /** * File links (to link the file to components and componentIds). */ diff --git a/src/app/services/geolocation.ts b/src/app/services/geolocation.ts index 0423c0cfc..317e053f2 100644 --- a/src/app/services/geolocation.ts +++ b/src/app/services/geolocation.ts @@ -116,6 +116,7 @@ export class CoreGeolocationProvider { * * @param error Error. */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any protected isCordovaPermissionDeniedError(error?: any): boolean { return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED; } diff --git a/src/app/services/groups.ts b/src/app/services/groups.ts index b49c42339..ed829076a 100644 --- a/src/app/services/groups.ts +++ b/src/app/services/groups.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons/core.singletons'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreCourseBase } from '@/types/global'; @@ -79,9 +80,11 @@ export class CoreGroupsProvider { preSets.emergencyCache = false; } - const response = await site.read('core_group_get_activity_allowed_groups', params, preSets); + const response: CoreGroupGetActivityAllowedGroupsResponse = + await site.read('core_group_get_activity_allowed_groups', params, preSets); + if (!response || !response.groups) { - throw null; + throw new CoreError('Activity allowed groups not found.'); } return response; @@ -195,9 +198,11 @@ export class CoreGroupsProvider { preSets.emergencyCache = false; } - const response = await site.read('core_group_get_activity_groupmode', params, preSets); + const response: CoreGroupGetActivityGroupModeResponse = + await site.read('core_group_get_activity_groupmode', params, preSets); + if (!response || typeof response.groupmode == 'undefined') { - throw null; + throw new CoreError('Activity group mode not found.'); } return response.groupmode; @@ -267,9 +272,11 @@ export class CoreGroupsProvider { updateFrequency: CoreSite.FREQUENCY_RARELY, }; - const response = await site.read('core_group_get_course_user_groups', data, preSets); + const response: CoreGroupGetCourseUserGroupsResponse = + await site.read('core_group_get_course_user_groups', data, preSets); + if (!response || !response.groups) { - throw null; + throw new CoreError('User groups in course not found.'); } return response.groups; @@ -461,3 +468,26 @@ export type CoreGroupGetActivityAllowedGroupsResponse = { canaccessallgroups?: boolean; // Whether the user will be able to access all the activity groups. warnings?: CoreWSExternalWarning[]; }; + +/** + * Result of WS core_group_get_activity_groupmode. + */ +export type CoreGroupGetActivityGroupModeResponse = { + groupmode: number; // Group mode: 0 for no groups, 1 for separate groups, 2 for visible groups. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS core_group_get_course_user_groups. + */ +export type CoreGroupGetCourseUserGroupsResponse = { + groups: { + id: number; // Group record id. + name: string; // Multilang compatible name, course unique. + description: string; // Group description text. + descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + idnumber: string; // Id number. + courseid?: number; // Course id. + }[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/app/services/lang.ts b/src/app/services/lang.ts index 0f56a1edc..7a8d30cbb 100644 --- a/src/app/services/lang.ts +++ b/src/app/services/lang.ts @@ -30,9 +30,9 @@ export class CoreLangProvider { protected fallbackLanguage = 'en'; // Always use English as fallback language since it contains all strings. protected defaultLanguage = CoreConfigConstants.default_lang || 'en'; // Lang to use if device lang not valid or is forced. protected currentLanguage: string; // Save current language in a variable to speed up the get function. - protected customStrings = {}; // Strings defined using the admin tool. + protected customStrings: CoreLanguageObject = {}; // Strings defined using the admin tool. protected customStringsRaw: string; - protected sitePluginsStrings = {}; // Strings defined by site plugins. + protected sitePluginsStrings: CoreLanguageObject = {}; // Strings defined by site plugins. constructor() { // Set fallback language and language to use until the app determines the right language to use. @@ -110,11 +110,11 @@ export class CoreLangProvider { * @param language New language to use. * @return Promise resolved when the change is finished. */ - changeCurrentLanguage(language: string): Promise { + async changeCurrentLanguage(language: string): Promise { const promises = []; // Change the language, resolving the promise when we receive the first value. - promises.push(new Promise((resolve, reject): void => { + promises.push(new Promise((resolve, reject) => { const subscription = Translate.instance.use(language).subscribe((data) => { // It's a language override, load the original one first. const fallbackLang = Translate.instance.instant('core.parentlanguage'); @@ -165,13 +165,15 @@ export class CoreLangProvider { this.currentLanguage = language; - return Promise.all(promises).finally(() => { + try { + await Promise.all(promises); + } finally { // Load the custom and site plugins strings for the language. if (this.loadLangStrings(this.customStrings, language) || this.loadLangStrings(this.sitePluginsStrings, language)) { // Some lang strings have changed, emit an event to update the pipes. Translate.instance.onLangChange.emit({ lang: language, translations: Translate.instance.translations[language] }); } - }); + } } /** @@ -196,7 +198,7 @@ export class CoreLangProvider { * * @return Custom strings. */ - getAllCustomStrings(): unknown { + getAllCustomStrings(): CoreLanguageObject { return this.customStrings; } @@ -205,7 +207,7 @@ export class CoreLangProvider { * * @return Site plugins strings. */ - getAllSitePluginsStrings(): unknown { + getAllSitePluginsStrings(): CoreLanguageObject { return this.sitePluginsStrings; } @@ -220,7 +222,7 @@ export class CoreLangProvider { } // Get current language from config (user might have changed it). - return CoreConfig.instance.get('current_language').then((language) => language).catch(() => { + return CoreConfig.instance.get('current_language').then((language) => language).catch(() => { // User hasn't defined a language. If default language is forced, use it. if (CoreConfigConstants.default_lang && CoreConfigConstants.forcedefaultlanguage) { return CoreConfigConstants.default_lang; @@ -283,7 +285,7 @@ export class CoreLangProvider { * @param lang The language to check. * @return Promise resolved when done. */ - getTranslationTable(lang: string): Promise { + getTranslationTable(lang: string): Promise> { // Create a promise to convert the observable into a promise. return new Promise((resolve, reject): void => { const observer = Translate.instance.getTranslation(lang).subscribe((table) => { diff --git a/src/app/services/local-notifications.ts b/src/app/services/local-notifications.ts index 22da70f92..f3638d86e 100644 --- a/src/app/services/local-notifications.ts +++ b/src/app/services/local-notifications.ts @@ -20,9 +20,11 @@ import { CoreApp, CoreAppSchema } from '@services/app'; import { CoreConfig } from '@services/config'; import { CoreEventObserver, CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { SQLiteDB } from '@classes/sqlitedb'; +import { CoreSite } from '@classes/site'; import { CoreQueueRunner } from '@classes/queue-runner'; +import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@core/constants'; import CoreConfigConstants from '@app/config.json'; import { makeSingleton, NgZone, Platform, Translate, LocalNotifications, Push, Device } from '@singletons/core.singletons'; @@ -94,14 +96,9 @@ export class CoreLocalNotificationsProvider { protected appDB: SQLiteDB; protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected codes: { [s: string]: number } = {}; - protected codeRequestsQueue = {}; - protected observables = {}; - protected currentNotification = { - title: '', - texts: [], - ids: [], - timeouts: [], - }; + protected codeRequestsQueue: {[key: string]: CodeRequestsQueueItem} = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected observables: {[eventName: string]: {[component: string]: Subject}} = {}; protected triggerSubscription: Subscription; protected clickSubscription: Subscription; @@ -156,7 +153,7 @@ export class CoreLocalNotificationsProvider { }); }); - CoreEvents.instance.on(CoreEventsProvider.SITE_DELETED, (site) => { + CoreEvents.instance.on(CoreEventsProvider.SITE_DELETED, (site: CoreSite) => { if (site) { this.cancelSiteNotifications(site.id); } @@ -270,13 +267,15 @@ export class CoreLocalNotificationsProvider { try { // Check if we already have a code stored for that ID. - const entry = await this.appDB.getRecord(table, { id: id }); + const entry = await this.appDB.getRecord<{id: string; code: number}>(table, { id: id }); + this.codes[key] = entry.code; return entry.code; } catch (err) { // No code stored for that ID. Create a new code for it. - const entries = await this.appDB.getRecords(table, undefined, 'code DESC'); + const entries = await this.appDB.getRecords<{id: string; code: number}>(table, undefined, 'code DESC'); + let newCode = 0; if (entries.length > 0) { newCode = entries[0].code + 1; @@ -326,7 +325,7 @@ export class CoreLocalNotificationsProvider { */ protected getUniqueNotificationId(notificationId: number, component: string, siteId: string): Promise { if (!siteId || !component) { - return Promise.reject(null); + return Promise.reject(new CoreError('Site ID or component not supplied.')); } return this.getSiteCode(siteId).then((siteCode) => this.getComponentCode(component).then((componentCode) => @@ -372,7 +371,9 @@ export class CoreLocalNotificationsProvider { await this.dbReady; try { - const stored = await this.appDB.getRecord(CoreLocalNotificationsProvider.TRIGGERED_TABLE, { id: notification.id }); + const stored = await this.appDB.getRecord<{id: number; at: number}>(CoreLocalNotificationsProvider.TRIGGERED_TABLE, + { id: notification.id }); + let triggered = (notification.trigger && notification.trigger.at) || 0; if (typeof triggered != 'number') { @@ -398,6 +399,7 @@ export class CoreLocalNotificationsProvider { * * @param data Data received by the notification. */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any notifyClick(data: any): void { this.notifyEvent('click', data); } @@ -408,6 +410,7 @@ export class CoreLocalNotificationsProvider { * @param eventName Name of the event to notify. * @param data Data received by the notification. */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any notifyEvent(eventName: string, data: any): void { // Execute the code in the Angular zone, so change detection doesn't stop working. NgZone.instance.run(() => { @@ -426,6 +429,7 @@ export class CoreLocalNotificationsProvider { * @param data Notification data. * @return Parsed data. */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any protected parseNotificationData(data: any): any { if (!data) { return {}; @@ -454,11 +458,11 @@ export class CoreLocalNotificationsProvider { 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.promises.forEach((p) => { + request.deferreds.forEach((p) => { p.resolve(code); }); }).catch((error) => { - request.promises.forEach((p) => { + request.deferreds.forEach((p) => { p.reject(error); }); }); @@ -508,7 +512,7 @@ export class CoreLocalNotificationsProvider { return { off: (): void => { - this.observables[eventName][component].unsubscribe(callback); + this.observables[eventName][component].unsubscribe(); }, }; } @@ -539,13 +543,13 @@ export class CoreLocalNotificationsProvider { if (typeof this.codeRequestsQueue[key] != 'undefined') { // There's already a pending request for this store and ID, add the promise to it. - this.codeRequestsQueue[key].promises.push(deferred); + this.codeRequestsQueue[key].deferreds.push(deferred); } else { // Add a pending request to the queue. this.codeRequestsQueue[key] = { table: table, id: id, - promises: [deferred], + deferreds: [deferred], }; } @@ -682,7 +686,7 @@ export class CoreLocalNotificationsProvider { const entry = { id: notification.id, - at: notification.trigger && notification.trigger.at ? notification.trigger.at : Date.now(), + at: notification.trigger && notification.trigger.at ? notification.trigger.at.getTime() : Date.now(), }; return this.appDB.insertRecord(CoreLocalNotificationsProvider.TRIGGERED_TABLE, entry); @@ -709,3 +713,9 @@ export class CoreLocalNotificationsProvider { export class CoreLocalNotifications extends makeSingleton(CoreLocalNotificationsProvider) {} export type CoreLocalNotificationsClickCallback = (value: T) => void; + +type CodeRequestsQueueItem = { + table: string; + id: string; + deferreds: PromiseDefer[]; +}; diff --git a/src/app/services/sites.ts b/src/app/services/sites.ts index 37aae6fff..6efc5d214 100644 --- a/src/app/services/sites.ts +++ b/src/app/services/sites.ts @@ -26,7 +26,13 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@core/constants'; import CoreConfigConstants from '@app/config.json'; import { - CoreSite, CoreSiteWSPreSets, LocalMobileResponse, CoreSiteConfig, CoreSitePublicConfigResponse, CoreSiteInfoResponse, + CoreSite, + CoreSiteWSPreSets, + LocalMobileResponse, + CoreSiteInfo, + CoreSiteConfig, + CoreSitePublicConfigResponse, + CoreSiteInfoResponse, } from '@classes/site'; import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; import { CoreError } from '@classes/errors/error'; @@ -123,7 +129,7 @@ export class CoreSitesProvider { await db.tableExists(oldTable); // Move the records from the old table. - const sites = await db.getAllRecords(oldTable); + const sites = await db.getAllRecords(oldTable); const promises = []; sites.forEach((site) => { @@ -818,9 +824,9 @@ export class CoreSitesProvider { id, siteUrl, token, - info: info ? JSON.stringify(info) : info, + info: info ? JSON.stringify(info) : undefined, privateToken, - config: config ? JSON.stringify(config) : config, + config: config ? JSON.stringify(config) : undefined, loggedOut: 0, oauthId, }; @@ -1094,7 +1100,7 @@ export class CoreSitesProvider { return this.sites[siteId]; } else { // Retrieve and create the site. - const data = await this.appDB.getRecord(SITES_TABLE, { id: siteId }); + const data = await this.appDB.getRecord(SITES_TABLE, { id: siteId }); return this.makeSiteFromSiteListEntry(data); } @@ -1106,16 +1112,12 @@ export class CoreSitesProvider { * @param entry Site list entry. * @return Promised resolved with the created site. */ - makeSiteFromSiteListEntry(entry: any): Promise { - let info = entry.info; - let config = entry.config; - + makeSiteFromSiteListEntry(entry: SiteDBEntry): Promise { // Parse info and config. - info = info ? CoreTextUtils.instance.parseJSON(info) : info; - config = config ? CoreTextUtils.instance.parseJSON(config) : config; + const info = entry.info ? CoreTextUtils.instance.parseJSON(entry.info) : undefined; + const config = entry.config ? CoreTextUtils.instance.parseJSON(entry.config) : undefined; - const site = new CoreSite(entry.id, entry.siteUrl, entry.token, - info, entry.privateToken, config, entry.loggedOut == 1); + const site = new CoreSite(entry.id, entry.siteUrl, entry.token, info, entry.privateToken, config, entry.loggedOut == 1); site.setOAuthId(entry.oauthId); return this.migrateSiteSchemas(site).then(() => { @@ -1171,20 +1173,20 @@ export class CoreSitesProvider { async getSites(ids?: string[]): Promise { await this.dbReady; - const sites = await this.appDB.getAllRecords(SITES_TABLE); + const sites = await this.appDB.getAllRecords(SITES_TABLE); const formattedSites = []; sites.forEach((site) => { if (!ids || ids.indexOf(site.id) > -1) { // Parse info. - const siteInfo = site.info ? CoreTextUtils.instance.parseJSON(site.info) : site.info; + const siteInfo = site.info ? CoreTextUtils.instance.parseJSON(site.info) : undefined; const basicInfo: CoreSiteBasicInfo = { id: site.id, siteUrl: site.siteUrl, - fullName: siteInfo && siteInfo.fullname, - siteName: CoreConfigConstants.sitename ? CoreConfigConstants.sitename : siteInfo && siteInfo.sitename, - avatar: siteInfo && siteInfo.userpictureurl, - siteHomeId: siteInfo && siteInfo.siteid || 1, + fullName: siteInfo?.fullname, + siteName: CoreConfigConstants.sitename ? CoreConfigConstants.sitename : siteInfo?.sitename, + avatar: siteInfo?.userpictureurl, + siteHomeId: siteInfo?.siteid || 1, }; formattedSites.push(basicInfo); } @@ -1231,7 +1233,7 @@ export class CoreSitesProvider { async getLoggedInSitesIds(): Promise { await this.dbReady; - const sites = await this.appDB.getRecords(SITES_TABLE, { loggedOut : 0 }); + const sites = await this.appDB.getRecords(SITES_TABLE, { loggedOut : 0 }); return sites.map((site) => site.id); } @@ -1244,7 +1246,7 @@ export class CoreSitesProvider { async getSitesIds(): Promise { await this.dbReady; - const sites = await this.appDB.getAllRecords(SITES_TABLE); + const sites = await this.appDB.getAllRecords(SITES_TABLE); return sites.map((site) => site.id); } @@ -1314,7 +1316,7 @@ export class CoreSitesProvider { this.sessionRestored = true; try { - const currentSite = await this.appDB.getRecord(CURRENT_SITE_TABLE, { id: 1 }); + const currentSite = await this.appDB.getRecord(CURRENT_SITE_TABLE, { id: 1 }); const siteId = currentSite.siteId; this.logger.debug(`Restore session in site ${siteId}`); @@ -1495,7 +1497,7 @@ export class CoreSitesProvider { } try { - const siteEntries = await this.appDB.getAllRecords(SITES_TABLE); + const siteEntries = await this.appDB.getAllRecords(SITES_TABLE); const ids = []; const promises = []; @@ -1528,7 +1530,7 @@ export class CoreSitesProvider { async getStoredCurrentSiteId(): Promise { await this.dbReady; - const currentSite = await this.appDB.getRecord(CURRENT_SITE_TABLE, { id: 1 }); + const currentSite = await this.appDB.getRecord(CURRENT_SITE_TABLE, { id: 1 }); return currentSite.siteId; } @@ -1685,7 +1687,7 @@ export class CoreSitesProvider { const db = site.getDb(); // Fetch installed versions of the schema. - const records = await db.getAllRecords(SCHEMA_VERSIONS_TABLE); + const records = await db.getAllRecords(SCHEMA_VERSIONS_TABLE); const versions: {[name: string]: number} = {}; records.forEach((record) => { @@ -2048,3 +2050,24 @@ export type CoreSitesLoginTokenResponse = { debuginfo?: string; reproductionlink?: string; }; + +type SiteDBEntry = { + id: string; + siteUrl: string; + token: string; + info: string; + privateToken: string; + config: string; + loggedOut: number; + oauthId: number; +}; + +type CurrentSiteDBEntry = { + id: number; + siteId: string; +}; + +type SchemaVersionsDBEntry = { + name: string; + version: number; +}; diff --git a/src/app/services/sync.ts b/src/app/services/sync.ts index 525c5c558..fae730556 100644 --- a/src/app/services/sync.ts +++ b/src/app/services/sync.ts @@ -132,7 +132,7 @@ export class CoreSyncProvider { * @param siteId Site ID. If not defined, current site. * @return Record if found or reject. */ - getSyncRecord(component: string, id: string | number, siteId?: string): Promise> { + getSyncRecord(component: string, id: string | number, siteId?: string): Promise { return CoreSites.instance.getSiteDb(siteId).then((db) => db.getRecord(SYNC_TABLE, { component: component, id: id })); } @@ -145,8 +145,7 @@ export class CoreSyncProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with done. */ - async insertOrUpdateSyncRecord(component: string, id: string | number, data: Record, siteId?: string): - Promise { + async insertOrUpdateSyncRecord(component: string, id: string, data: CoreSyncRecord, siteId?: string): Promise { const db = await CoreSites.instance.getSiteDb(siteId); data.component = component; @@ -212,3 +211,10 @@ export class CoreSyncProvider { } export class CoreSync extends makeSingleton(CoreSyncProvider) {} + +export type CoreSyncRecord = { + component: string; + id: string; + time: number; + warnings: string; +}; diff --git a/src/app/services/update-manager.ts b/src/app/services/update-manager.ts index 96de478d9..daa952309 100644 --- a/src/app/services/update-manager.ts +++ b/src/app/services/update-manager.ts @@ -51,7 +51,7 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { const promises = []; const versionCode = CoreConfigConstants.versioncode; - const versionApplied: number = await CoreConfig.instance.get(VERSION_APPLIED, 0); + const versionApplied = await CoreConfig.instance.get(VERSION_APPLIED, 0); if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) { // @todo: H5P update. diff --git a/src/app/services/utils/iframe.ts b/src/app/services/utils/iframe.ts index 3dae9ab13..ecaa31db6 100644 --- a/src/app/services/utils/iframe.ts +++ b/src/app/services/utils/iframe.ts @@ -391,7 +391,7 @@ export class CoreIframeUtilsProvider { return; } - if (!CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain)) { + if (!CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol)) { // Scheme suggests it's an external resource. event && event.preventDefault(); diff --git a/src/app/services/utils/url.ts b/src/app/services/utils/url.ts index 760eb01aa..4af96bb88 100644 --- a/src/app/services/utils/url.ts +++ b/src/app/services/utils/url.ts @@ -424,18 +424,16 @@ export class CoreUrlUtilsProvider { isLocalFileUrl(url: string): boolean { const urlParts = CoreUrl.parse(url); - return this.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain); + return this.isLocalFileUrlScheme(urlParts.protocol); } /** * Check whether a URL scheme belongs to a local file. * * @param scheme Scheme to check. - * @param notUsed Unused parameter. * @return Whether the scheme belongs to a local file. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - isLocalFileUrlScheme(scheme: string, notUsed?: string): boolean { + isLocalFileUrlScheme(scheme: string): boolean { if (scheme) { scheme = scheme.toLowerCase(); } diff --git a/src/app/services/ws.ts b/src/app/services/ws.ts index fa7fc3884..51145b979 100644 --- a/src/app/services/ws.ts +++ b/src/app/services/ws.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { HttpResponse, HttpParams } from '@angular/common/http'; +import { FileEntry } from '@ionic-native/file'; import { FileUploadOptions } from '@ionic-native/file-transfer/ngx'; import { Md5 } from 'ts-md5/dist/md5'; import { Observable } from 'rxjs'; diff --git a/src/app/singletons/array.ts b/src/app/singletons/array.ts index a35ca1450..1e3da6b62 100644 --- a/src/app/singletons/array.ts +++ b/src/app/singletons/array.ts @@ -38,7 +38,7 @@ export class CoreArray { */ static flatten(arr: T[][]): T[] { if ('flat' in arr) { - return (arr as any).flat(); + return (arr as any).flat(); // eslint-disable-line @typescript-eslint/no-explicit-any } return [].concat(...arr); diff --git a/src/app/singletons/locutus.ts b/src/app/singletons/locutus.ts index 0cc32e5c7..8f11cc7a1 100644 --- a/src/app/singletons/locutus.ts +++ b/src/app/singletons/locutus.ts @@ -400,7 +400,7 @@ function unserialize (str) { } } -function substr_replace (str, replace, start, length) { // eslint-disable-line camelcase +function substr_replace (str, replace, start, length) { // discuss at: https://locutus.io/php/substr_replace/ // original by: Brett Zamir (https://brett-zamir.me) // example 1: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', 0) diff --git a/src/app/singletons/window.ts b/src/app/singletons/window.ts index 2fe01da42..59b6542e6 100644 --- a/src/app/singletons/window.ts +++ b/src/app/singletons/window.ts @@ -24,6 +24,7 @@ export type CoreWindowOpenOptions = { /** * NavController to use when opening the link in the app. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any navCtrl?: any; // @todo NavController; }; @@ -36,7 +37,7 @@ export class CoreWindow { private constructor() { // Nothing to do. } - + /** * "Safe" implementation of window.open. It will open the URL without overriding the app. * @@ -60,11 +61,13 @@ export class CoreWindow { await CoreUtils.instance.openFile(url); } else { let treated: boolean; + // eslint-disable-next-line @typescript-eslint/no-unused-vars options = options || {}; if (name != '_system') { // Check if it can be opened in the app. - treated = false; // @todo await CoreContentLinksHelper.instance.handleLink(url, undefined, options.navCtrl, true, true); + treated = false; + // @todo await CoreContentLinksHelper.instance.handleLink(url, undefined, options.navCtrl, true, true); } if (!treated) {