diff --git a/.eslintrc.js b/.eslintrc.js index ba8d87e78..b53547465 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -148,6 +148,7 @@ var appConfig = { 'warn', { allowFinally: true, + terminationMethod: ['catch', 'finally'], }, ], 'arrow-body-style': ['error', 'as-needed'], diff --git a/src/app/classes/singletons-factory.ts b/src/app/classes/singletons-factory.ts index 562b9a3a4..d574f6591 100644 --- a/src/app/classes/singletons-factory.ts +++ b/src/app/classes/singletons-factory.ts @@ -40,7 +40,7 @@ export class CoreSingletonsFactory { /** * Angular injector used to resolve singleton instances. */ - private injector: Injector; + private injector?: Injector; /** * Set the injector that will be used to resolve instances in the singletons created with this factory. @@ -68,6 +68,10 @@ export class CoreSingletonsFactory { static get instance(): Service { // Initialize instances lazily. if (!this.serviceInstance) { + if (!factory.injector) { + throw new Error('Can\'t resolve a singleton instance without an injector'); + } + this.serviceInstance = factory.injector.get(injectionToken); } diff --git a/src/app/classes/sqlitedb.ts b/src/app/classes/sqlitedb.ts index 6cd1e7b68..a46729325 100644 --- a/src/app/classes/sqlitedb.ts +++ b/src/app/classes/sqlitedb.ts @@ -133,8 +133,8 @@ export interface SQLiteDBForeignKeySchema { */ export class SQLiteDB { - db: SQLiteObject; - promise: Promise; + db?: SQLiteObject; + promise!: Promise; /** * Create and open the database. @@ -164,7 +164,7 @@ export class SQLiteDB { foreignKeys?: SQLiteDBForeignKeySchema[], tableCheck?: string, ): string { - const columnsSql = []; + const columnsSql: string[] = []; let sql = `CREATE TABLE IF NOT EXISTS ${name} (`; // First define all the columns. @@ -225,8 +225,8 @@ export class SQLiteDB { for (const index in foreignKeys) { const foreignKey = foreignKeys[index]; - if (!foreignKey.columns || !!foreignKey.columns.length) { - return; + if (!foreignKey?.columns.length) { + continue; } sql += `, FOREIGN KEY (${foreignKey.columns.join(', ')}) REFERENCES ${foreignKey.table} `; @@ -251,7 +251,7 @@ export class SQLiteDB { async close(): Promise { await this.ready(); - await this.db.close(); + await this.db!.close(); } /** @@ -348,10 +348,7 @@ export class SQLiteDB { * @return Promise resolved when success. */ async createTablesFromSchema(tables: SQLiteDBTableSchema[]): Promise { - const promises = []; - tables.forEach((table) => { - promises.push(this.createTableFromSchema(table)); - }); + const promises = tables.map(table => this.createTableFromSchema(table)); await Promise.all(promises); } @@ -432,7 +429,7 @@ export class SQLiteDB { async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise { await this.ready(); - return this.db.executeSql(sql, params); + return this.db!.executeSql(sql, params); } /** @@ -447,7 +444,7 @@ export class SQLiteDB { async executeBatch(sqlStatements: (string | string[] | any)[]): Promise { await this.ready(); - await this.db.sqlBatch(sqlStatements); + await this.db!.sqlBatch(sqlStatements); } /** @@ -532,7 +529,7 @@ export class SQLiteDB { * @return Promise resolved with the field's value. */ async getFieldSql(sql: string, params?: SQLiteDBRecordValue[]): Promise { - const record = await this.getRecordSql(sql, params); + const record = await this.getRecordSql>(sql, params); if (!record) { throw new CoreError('No record found.'); } @@ -552,7 +549,7 @@ export class SQLiteDB { getInOrEqual( items: SQLiteDBRecordValue | SQLiteDBRecordValue[], equal: boolean = true, - onEmptyItems?: SQLiteDBRecordValue, + onEmptyItems?: SQLiteDBRecordValue | null, ): SQLiteDBQueryParams { let sql = ''; let params: SQLiteDBRecordValue[]; @@ -564,7 +561,7 @@ export class SQLiteDB { // Handle onEmptyItems on empty array of items. if (Array.isArray(items) && !items.length) { - if (onEmptyItems === null) { // Special case, NULL value. + if (onEmptyItems === null || typeof onEmptyItems === 'undefined') { // Special case, NULL value. sql = equal ? ' IS NULL' : ' IS NOT NULL'; return { sql, params: [] }; @@ -758,7 +755,7 @@ export class SQLiteDB { const result = await this.execute(sql, params); // Retrieve the records. - const records = []; + const records: T[] = []; for (let i = 0; i < result.rows.length; i++) { records.push(result.rows.item(i)); } @@ -868,7 +865,7 @@ export class SQLiteDB { * @param limitNum How many results to return. * @return Normalised limit params in array: [limitFrom, limitNum]. */ - normaliseLimitFromNum(limitFrom: number, limitNum: number): number[] { + normaliseLimitFromNum(limitFrom?: number, limitNum?: number): number[] { // We explicilty treat these cases as 0. if (!limitFrom || limitFrom === -1) { limitFrom = 0; @@ -893,7 +890,7 @@ export class SQLiteDB { async open(): Promise { await this.ready(); - await this.db.open(); + await this.db!.open(); } /** @@ -994,11 +991,7 @@ export class SQLiteDB { return 0; } - const sets = []; - for (const key in data) { - sets.push(`${key} = ?`); - } - + const sets = Object.keys(data).map(key => `${key} = ?`); let sql = `UPDATE ${table} SET ${sets.join(', ')}`; if (where) { sql += ` WHERE ${where}`; @@ -1029,8 +1022,8 @@ export class SQLiteDB { }; } - const where = []; - const params = []; + const where: string[] = []; + const params: SQLiteDBRecordValue[] = []; for (const key in conditions) { const value = conditions[key]; @@ -1064,7 +1057,7 @@ export class SQLiteDB { }; } - const params = []; + const params: SQLiteDBRecordValue[] = []; let sql = ''; values.forEach((value) => { diff --git a/src/app/core/emulator/classes/sqlitedb.ts b/src/app/core/emulator/classes/sqlitedb.ts index d40b02a5c..b327029fd 100644 --- a/src/app/core/emulator/classes/sqlitedb.ts +++ b/src/app/core/emulator/classes/sqlitedb.ts @@ -20,7 +20,6 @@ import { SQLiteDB } from '@classes/sqlitedb'; * Class to mock the interaction with the SQLite database. */ export class SQLiteDBMock extends SQLiteDB { - promise: Promise; /** * Create and open the database. @@ -46,9 +45,11 @@ export class SQLiteDBMock extends SQLiteDB { * * @return Promise resolved when done. */ - emptyDatabase(): Promise { + async emptyDatabase(): Promise { + await this.ready(); + return new Promise((resolve, reject): void => { - this.db.transaction((tx) => { + this.db!.transaction((tx) => { // Query all tables from sqlite_master that we have created and can modify. const args = []; const query = `SELECT * FROM sqlite_master @@ -63,7 +64,7 @@ export class SQLiteDBMock extends SQLiteDB { } // Drop all the tables. - const promises = []; + const promises: Promise[] = []; for (let i = 0; i < result.rows.length; i++) { promises.push(new Promise((resolve, reject): void => { @@ -73,7 +74,7 @@ export class SQLiteDBMock extends SQLiteDB { })); } - Promise.all(promises).then(resolve, reject); + Promise.all(promises).then(resolve).catch(reject); }, reject); }); }); @@ -88,14 +89,18 @@ export class SQLiteDBMock extends SQLiteDB { * @param params Query parameters. * @return Promise resolved with the result. */ - execute(sql: string, params?: any[]): Promise { + async execute(sql: string, params?: any[]): Promise { + await this.ready(); + return new Promise((resolve, reject): void => { // With WebSQL, all queries must be run in a transaction. - this.db.transaction((tx) => { + this.db!.transaction((tx) => { tx.executeSql(sql, params, (tx, results) => { resolve(results); }, (tx, error) => { + // eslint-disable-next-line no-console console.error(sql, params, error); + reject(error); }); }); @@ -110,11 +115,13 @@ export class SQLiteDBMock extends SQLiteDB { * @param sqlStatements SQL statements to execute. * @return Promise resolved with the result. */ - executeBatch(sqlStatements: any[]): Promise { + async executeBatch(sqlStatements: any[]): Promise { + await this.ready(); + return new Promise((resolve, reject): void => { // Create a transaction to execute the queries. - this.db.transaction((tx) => { - const promises = []; + this.db!.transaction((tx) => { + const promises: Promise[] = []; // Execute all the queries. Each statement can be a string or an array. sqlStatements.forEach((statement) => { @@ -133,7 +140,9 @@ export class SQLiteDBMock extends SQLiteDB { tx.executeSql(query, params, (tx, results) => { resolve(results); }, (tx, error) => { + // eslint-disable-next-line no-console console.error(query, params, error); + reject(error); }); })); diff --git a/src/app/services/app.ts b/src/app/services/app.ts index 115a33608..1b3bd700a 100644 --- a/src/app/services/app.ts +++ b/src/app/services/app.ts @@ -45,13 +45,13 @@ export class CoreAppProvider { protected db: SQLiteDB; protected logger: CoreLogger; - protected ssoAuthenticationDeferred: PromiseDefer; + protected ssoAuthenticationDeferred?: PromiseDefer; protected isKeyboardShown = false; protected keyboardOpening = false; protected keyboardClosing = false; protected backActions: {callback: () => boolean; priority: number}[] = []; protected mainMenuId = 0; - protected mainMenuOpen: number; + protected mainMenuOpen?: number; protected forceOffline = false; // Variables for DB. @@ -224,7 +224,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 | null { if (this.isMac() && storesConfig.mac) { return 'itms-apps://itunes.apple.com/app/' + storesConfig.mac; } @@ -332,6 +332,8 @@ export class CoreAppProvider { try { // @todo return require('os').platform().indexOf('linux') === 0; + + return false; } catch (ex) { return false; } @@ -349,6 +351,8 @@ export class CoreAppProvider { try { // @todo return require('os').platform().indexOf('darwin') === 0; + + return false; } catch (ex) { return false; } @@ -439,6 +443,8 @@ export class CoreAppProvider { try { // @todo return require('os').platform().indexOf('win') === 0; + + return false; } catch (ex) { return false; } @@ -521,7 +527,9 @@ export class CoreAppProvider { * @return Promise resolved once SSO authentication finishes. */ async waitForSSOAuthentication(): Promise { - await this.ssoAuthenticationDeferred && this.ssoAuthenticationDeferred.promise; + const promise = this.ssoAuthenticationDeferred?.promise; + + await promise; } /** @@ -530,7 +538,7 @@ export class CoreAppProvider { * @param timeout Maximum time to wait, use null to wait forever. */ async waitForResume(timeout: number | null = null): Promise { - let deferred = CoreUtils.instance.promiseDefer(); + let deferred: PromiseDefer | null = CoreUtils.instance.promiseDefer(); const stopWaiting = () => { if (!deferred) { @@ -556,13 +564,13 @@ export class CoreAppProvider { * @return Object with siteid, state, params and timemodified. */ getRedirect = Record>(): CoreRedirectData { - if (localStorage && localStorage.getItem) { + if (localStorage?.getItem) { try { const paramsJson = localStorage.getItem('CoreRedirectParams'); const data: CoreRedirectData = { - siteId: localStorage.getItem('CoreRedirectSiteId'), - page: localStorage.getItem('CoreRedirectState'), - timemodified: parseInt(localStorage.getItem('CoreRedirectTime'), 10), + siteId: localStorage.getItem('CoreRedirectSiteId') || undefined, + page: localStorage.getItem('CoreRedirectState') || undefined, + timemodified: parseInt(localStorage.getItem('CoreRedirectTime') || '0', 10), }; if (paramsJson) { diff --git a/src/app/services/init.ts b/src/app/services/init.ts index 26bb797e5..5a8e042bd 100644 --- a/src/app/services/init.ts +++ b/src/app/services/init.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; -import { CoreUtils, PromiseDefer } from '@services/utils/utils'; +import { CoreUtils, PromiseDefer, OrderedPromiseData } from '@services/utils/utils'; import { CoreLogger } from '@singletons/logger'; import { makeSingleton } from '@singletons/core.singletons'; @@ -56,7 +56,7 @@ export class CoreInitDelegate { protected initProcesses: { [s: string]: CoreInitHandler } = {}; protected logger: CoreLogger; - protected readiness: CoreInitReadinessPromiseDefer; + protected readiness?: CoreInitReadinessPromiseDefer; constructor() { this.logger = CoreLogger.getInstance('CoreInitDelegate'); @@ -68,7 +68,7 @@ export class CoreInitDelegate { * Reserved for core use, do not call directly. */ executeInitProcesses(): void { - let ordered = []; + const ordered: CoreInitHandler[] = []; if (typeof this.readiness == 'undefined') { this.initReadiness(); @@ -78,15 +78,15 @@ export class CoreInitDelegate { for (const name in this.initProcesses) { ordered.push(this.initProcesses[name]); } - ordered.sort((a, b) => b.priority - a.priority); + ordered.sort((a, b) => (b.priority || 0) - (a.priority || 0)); - ordered = ordered.map((data: CoreInitHandler) => ({ + const orderedPromises: OrderedPromiseData[] = ordered.map((data: CoreInitHandler) => ({ function: this.prepareProcess.bind(this, data), blocking: !!data.blocking, })); // Execute all the processes in order to solve dependencies. - CoreUtils.instance.executeOrderedPromises(ordered).finally(this.readiness.resolve); + CoreUtils.instance.executeOrderedPromises(orderedPromises).finally(this.readiness!.resolve); } /** @@ -94,7 +94,9 @@ export class CoreInitDelegate { */ protected initReadiness(): void { this.readiness = CoreUtils.instance.promiseDefer(); - this.readiness.promise.then(() => this.readiness.resolved = true); + + // eslint-disable-next-line promise/catch-or-return + this.readiness.promise.then(() => this.readiness!.resolved = true); } /** @@ -103,7 +105,7 @@ export class CoreInitDelegate { * @return Whether it's ready. */ isReady(): boolean { - return this.readiness.resolved; + return this.readiness?.resolved || false; } /** @@ -133,7 +135,7 @@ export class CoreInitDelegate { this.initReadiness(); } - await this.readiness.promise; + await this.readiness!.promise; } /** diff --git a/src/app/services/lang.ts b/src/app/services/lang.ts index dc405bd31..5a4046615 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 currentLanguage?: string; // Save current language in a variable to speed up the get function. protected customStrings: CoreLanguageObject = {}; // Strings defined using the admin tool. - protected customStringsRaw: string; + protected customStringsRaw?: string; protected sitePluginsStrings: CoreLanguageObject = {}; // Strings defined by site plugins. constructor() { @@ -123,7 +123,7 @@ export class CoreLangProvider { * @return Promise resolved when the change is finished. */ async changeCurrentLanguage(language: string): Promise { - const promises = []; + const promises: Promise[] = []; // Change the language, resolving the promise when we receive the first value. promises.push(new Promise((resolve, reject) => { @@ -363,8 +363,8 @@ export class CoreLangProvider { if (currentLangChanged) { // Some lang strings have changed, emit an event to update the pipes. Translate.instance.onLangChange.emit({ - lang: this.currentLanguage, - translations: Translate.instance.translations[this.currentLanguage], + lang: this.currentLanguage!, + translations: Translate.instance.translations[this.currentLanguage!], }); } } diff --git a/src/app/services/utils/iframe.ts b/src/app/services/utils/iframe.ts index ecaa31db6..ed8f61b72 100644 --- a/src/app/services/utils/iframe.ts +++ b/src/app/services/utils/iframe.ts @@ -113,7 +113,7 @@ export class CoreIframeUtilsProvider { } // Remove the warning and show the iframe - CoreDomUtils.instance.removeElement(element.parentElement, 'div.core-iframe-offline-warning'); + CoreDomUtils.instance.removeElement(element.parentElement!, 'div.core-iframe-offline-warning'); element.classList.remove('core-iframe-offline-disabled'); if (isSubframe) { @@ -131,9 +131,9 @@ export class CoreIframeUtilsProvider { * @param element Element to treat (iframe, embed, ...). * @return Window and Document. */ - getContentWindowAndDocument(element: CoreFrameElement): { window: Window; document: Document } { - let contentWindow: Window = 'contentWindow' in element ? element.contentWindow : undefined; - let contentDocument: Document; + getContentWindowAndDocument(element: CoreFrameElement): { window: Window | null; document: Document | null } { + let contentWindow: Window | null = 'contentWindow' in element ? element.contentWindow : null; + let contentDocument: Document | null = null; try { contentDocument = 'contentDocument' in element && element.contentDocument @@ -209,7 +209,8 @@ export class CoreIframeUtilsProvider { contentWindow.open = (url: string, name: string) => { this.windowOpen(url, name, element, navCtrl); - return null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return null as any; }; } @@ -233,31 +234,35 @@ export class CoreIframeUtilsProvider { * @param navCtrl NavController to use if a link can be opened in the app. */ treatFrame(element: CoreFrameElement, isSubframe?: boolean, navCtrl?: NavController): void { - if (element) { + if (!element) { + return; + } + + const treatElement = (sendResizeEvent: boolean = false) => { this.checkOnlineFrameInOffline(element, isSubframe); - let winAndDoc = this.getContentWindowAndDocument(element); + const { window, document } = this.getContentWindowAndDocument(element); + // Redefine window.open in this element and sub frames, it might have been loaded already. - this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document, navCtrl); + if (window && document) { + this.redefineWindowOpen(element, window, document, navCtrl); + } + // Treat links. - this.treatFrameLinks(element, winAndDoc.document); + if (document) { + this.treatFrameLinks(element, document); + } - element.addEventListener('load', () => { - this.checkOnlineFrameInOffline(element, isSubframe); + // Send a resize events to the iframe so it calculates the right size if needed. + if (window && sendResizeEvent) { + setTimeout(() => window.dispatchEvent(new Event('resize')), 1000); + } + }; - // Element loaded, redefine window.open and treat links again. - winAndDoc = this.getContentWindowAndDocument(element); - this.redefineWindowOpen(element, winAndDoc.window, winAndDoc.document, navCtrl); - this.treatFrameLinks(element, winAndDoc.document); + treatElement(); - if (winAndDoc.window) { - // Send a resize events to the iframe so it calculates the right size if needed. - setTimeout(() => { - winAndDoc.window.dispatchEvent(new Event('resize')); - }, 1000); - } - }); - } + // Element loaded, redefine window.open and treat links again. + element.addEventListener('load', () => treatElement(true)); } /** @@ -279,7 +284,7 @@ export class CoreIframeUtilsProvider { } // Find the link being clicked. - let el = event.target; + let el: Element | null = event.target as Element; while (el && el.tagName !== 'A') { el = el.parentElement; } @@ -386,19 +391,24 @@ export class CoreIframeUtilsProvider { } const urlParts = CoreUrl.parse(link.href); - if (!link.href || (urlParts.protocol && urlParts.protocol == 'javascript')) { + if (!link.href || !urlParts || (urlParts.protocol && urlParts.protocol == 'javascript')) { // Links with no URL and Javascript links are ignored. return; } - if (!CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol)) { + if (urlParts.protocol && !CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol)) { // Scheme suggests it's an external resource. event && event.preventDefault(); const frameSrc = element && (( element).src || ( element).data); // If the frame is not local, check the target to identify how to treat the link. - if (element && !CoreUrlUtils.instance.isLocalFileUrl(frameSrc) && (!link.target || link.target == '_self')) { + if ( + element && + frameSrc && + !CoreUrlUtils.instance.isLocalFileUrl(frameSrc) && + (!link.target || link.target == '_self') + ) { // Load the link inside the frame itself. if (element.tagName.toLowerCase() == 'object') { element.setAttribute('data', link.href); @@ -455,8 +465,8 @@ export class CoreIframeUtilsProvider { const linksPath = CoreTextUtils.instance.concatenatePaths(wwwPath, 'assets/js/iframe-treat-links.js'); const recaptchaPath = CoreTextUtils.instance.concatenatePaths(wwwPath, 'assets/js/iframe-recaptcha.js'); - userScriptWindow.WKUserScript.addScript({ id: 'CoreIframeUtilsLinksScript', file: linksPath }); - userScriptWindow.WKUserScript.addScript({ + userScriptWindow.WKUserScript?.addScript({ id: 'CoreIframeUtilsLinksScript', file: linksPath }); + userScriptWindow.WKUserScript?.addScript({ id: 'CoreIframeUtilsRecaptchaScript', file: recaptchaPath, injectionTime: WKUserScriptInjectionTime.END, diff --git a/src/app/services/utils/mimetype.ts b/src/app/services/utils/mimetype.ts index 06588c49e..d6b0e0767 100644 --- a/src/app/services/utils/mimetype.ts +++ b/src/app/services/utils/mimetype.ts @@ -116,7 +116,7 @@ export class CoreMimetypeUtilsProvider { */ protected fillGroupMimeInfo(group: string): void { const mimetypes = {}; // Use an object to prevent duplicates. - const extensions = []; // Extensions are unique. + const extensions: string[] = []; // Extensions are unique. for (const extension in this.extToMime) { const data = this.extToMime[extension]; @@ -140,13 +140,13 @@ export class CoreMimetypeUtilsProvider { * @param url URL of the file. It will be used if there's more than one possible extension. * @return Extension. */ - getExtension(mimetype: string, url?: string): string { + getExtension(mimetype: string, url?: string): string | undefined { mimetype = mimetype || ''; mimetype = mimetype.split(';')[0]; // Remove codecs from the mimetype if any. if (mimetype == 'application/x-forcedownload' || mimetype == 'application/forcedownload') { // Couldn't get the right mimetype, try to guess it. - return this.guessExtensionFromUrl(url); + return url && this.guessExtensionFromUrl(url); } const extensions = this.mimeToExt[mimetype]; @@ -154,7 +154,7 @@ export class CoreMimetypeUtilsProvider { if (extensions.length > 1 && url) { // There's more than one possible extension. Check if the URL has extension. const candidate = this.guessExtensionFromUrl(url); - if (extensions.indexOf(candidate) != -1) { + if (candidate && extensions.indexOf(candidate) != -1) { return candidate; } } @@ -173,19 +173,22 @@ export class CoreMimetypeUtilsProvider { const filename = CoreUtils.instance.isFileEntry(file) ? (file as FileEntry).name : file.filename; const extension = !CoreUtils.instance.isFileEntry(file) && file.mimetype ? this.getExtension(file.mimetype) - : this.getFileExtension(filename); - const mimeType = !CoreUtils.instance.isFileEntry(file) && file.mimetype ? file.mimetype : this.getMimeType(extension); + : (filename && this.getFileExtension(filename)); + const mimeType = !CoreUtils.instance.isFileEntry(file) && file.mimetype + ? file.mimetype + : (extension && this.getMimeType(extension)); // @todo linting: See if this can be removed (file as CoreWSExternalFile).mimetype = mimeType; - if (this.canBeEmbedded(extension)) { + if (extension && this.canBeEmbedded(extension)) { const embedType = this.getExtensionType(extension); // @todo linting: See if this can be removed - (file as { embedType: string }).embedType = embedType; + (file as { embedType?: string }).embedType = embedType; - path = CoreFile.instance.convertFileSrc(path ?? (CoreUtils.instance.isFileEntry(file) ? file.toURL() : file.fileurl)); + path = path ?? (CoreUtils.instance.isFileEntry(file) ? file.toURL() : file.fileurl); + path = path && CoreFile.instance.convertFileSrc(path); switch (embedType) { case 'image': @@ -223,7 +226,7 @@ export class CoreMimetypeUtilsProvider { * @param extension Extension. * @return Icon. Undefined if not found. */ - getExtensionIconName(extension: string): string { + getExtensionIconName(extension: string): string | undefined { if (this.extToMime[extension]) { if (this.extToMime[extension].icon) { return this.extToMime[extension].icon; @@ -242,7 +245,7 @@ export class CoreMimetypeUtilsProvider { * @param extension Extension. * @return Type of the extension. */ - getExtensionType(extension: string): string { + getExtensionType(extension: string): string | undefined { extension = this.cleanExtension(extension); if (this.extToMime[extension] && this.extToMime[extension].string) { @@ -270,8 +273,8 @@ export class CoreMimetypeUtilsProvider { * @return The path to a file icon. */ getFileIcon(filename: string): string { - const ext = this.getFileExtension(filename); - const icon = this.getExtensionIconName(ext) || 'unknown'; + const extension = this.getFileExtension(filename); + const icon = (extension && this.getExtensionIconName(extension)) || 'unknown'; return this.getFileIconForType(icon); } @@ -302,14 +305,14 @@ export class CoreMimetypeUtilsProvider { * @param fileUrl The file URL. * @return The lowercased extension without the dot, or undefined. */ - guessExtensionFromUrl(fileUrl: string): string { + guessExtensionFromUrl(fileUrl: string): string | undefined { const split = fileUrl.split('.'); let candidate; let extension; let position; if (split.length > 1) { - candidate = split.pop().toLowerCase(); + candidate = split.pop()!.toLowerCase(); // Remove params if any. position = candidate.indexOf('?'); if (position > -1) { @@ -338,7 +341,7 @@ export class CoreMimetypeUtilsProvider { * @param filename The file name. * @return The lowercased extension, or undefined. */ - getFileExtension(filename: string): string { + getFileExtension(filename: string): string | undefined { const dot = filename.lastIndexOf('.'); let ext; @@ -382,7 +385,7 @@ export class CoreMimetypeUtilsProvider { * @param extension Extension. * @return Mimetype. */ - getMimeType(extension: string): string { + getMimeType(extension: string): string | undefined { extension = this.cleanExtension(extension); if (this.extToMime[extension] && this.extToMime[extension].type) { @@ -400,9 +403,9 @@ export class CoreMimetypeUtilsProvider { */ getMimetypeDescription(obj: FileEntry | { filename: string; mimetype: string } | string, capitalise?: boolean): string { const langPrefix = 'assets.mimetypes.'; - let filename = ''; - let mimetype = ''; - let extension = ''; + let filename: string | undefined = ''; + let mimetype: string | undefined = ''; + let extension: string | undefined = ''; if (typeof obj == 'object' && CoreUtils.instance.isFileEntry(obj)) { // It's a FileEntry. Don't use the file function because it's asynchronous and the type isn't reliable. @@ -419,7 +422,7 @@ export class CoreMimetypeUtilsProvider { if (!mimetype) { // Try to calculate the mimetype using the extension. - mimetype = this.getMimeType(extension); + mimetype = extension && this.getMimeType(extension); } } @@ -478,7 +481,7 @@ export class CoreMimetypeUtilsProvider { * @param mimetype Mimetype. * @return Type of the mimetype. */ - getMimetypeType(mimetype: string): string { + getMimetypeType(mimetype: string): string | undefined { mimetype = mimetype.split(';')[0]; // Remove codecs from the mimetype if any. const extensions = this.mimeToExt[mimetype]; @@ -542,9 +545,9 @@ export class CoreMimetypeUtilsProvider { isExtensionInGroup(extension: string, groups: string[]): boolean { extension = this.cleanExtension(extension); - if (groups && groups.length && this.extToMime[extension] && this.extToMime[extension].groups) { - for (let i = 0; i < this.extToMime[extension].groups.length; i++) { - const group = this.extToMime[extension].groups[i]; + if (groups?.length && this.extToMime[extension]?.groups) { + for (let i = 0; i < this.extToMime[extension].groups!.length; i++) { + const group = this.extToMime[extension].groups![i]; if (groups.indexOf(group) != -1) { return true; } diff --git a/src/app/services/utils/text.ts b/src/app/services/utils/text.ts index 86a7c3fb0..dd202be38 100644 --- a/src/app/services/utils/text.ts +++ b/src/app/services/utils/text.ts @@ -175,11 +175,11 @@ export class CoreTextUtilsProvider { // Filter invalid messages, and convert them to messages in case they're errors. const messages: string[] = []; - paragraphs.forEach((paragraph) => { + paragraphs.forEach(paragraph => { // If it's an error, get its message. const message = this.getErrorMessageFromError(paragraph); - if (paragraph) { + if (paragraph && message) { messages.push(message); } }); @@ -248,8 +248,7 @@ export class CoreTextUtilsProvider { // First, we use a regexpr. text = text.replace(/(<([^>]+)>)/ig, ''); // Then, we rely on the browser. We need to wrap the text to be sure is HTML. - const element = this.convertToElement(text); - text = element.textContent; + text = this.convertToElement(text).textContent!; // Recover or remove new lines. text = this.replaceNewLines(text, singleLine ? ' ' : '
'); @@ -326,7 +325,7 @@ export class CoreTextUtilsProvider { text = text.replace(/_/gi, ' '); // This RegEx will detect any word change including Unicode chars. Some languages without spaces won't be counted fine. - return text.match(/\S+/gi).length; + return text.match(/\S+/gi)?.length || 0; } /** @@ -359,8 +358,7 @@ export class CoreTextUtilsProvider { */ decodeHTMLEntities(text: string): string { if (text) { - const element = this.convertToElement(text); - text = element.textContent; + text = this.convertToElement(text).textContent!; } return text; @@ -512,7 +510,7 @@ export class CoreTextUtilsProvider { if (clean) { formatted = this.cleanTags(formatted, singleLine); } - if (shortenLength > 0) { + if (shortenLength && shortenLength > 0) { formatted = this.shortenText(formatted, shortenLength); } if (highlight) { @@ -529,10 +527,11 @@ export class CoreTextUtilsProvider { * @param error Error object. * @return Error message, undefined if not found. */ - getErrorMessageFromError(error: string | CoreError | CoreTextErrorObject): string { + getErrorMessageFromError(error?: string | CoreError | CoreTextErrorObject): string | undefined { if (typeof error == 'string') { return error; } + if (error instanceof CoreError) { return error.message; } @@ -546,12 +545,12 @@ export class CoreTextUtilsProvider { * @param files Files to extract the URL from. They need to have the URL in a 'url' or 'fileurl' attribute. * @return Pluginfile URL, undefined if no files found. */ - getTextPluginfileUrl(files: CoreWSExternalFile[]): string { - if (files && files.length) { + getTextPluginfileUrl(files: CoreWSExternalFile[]): string | undefined { + if (files?.length) { const url = files[0].fileurl; // Remove text after last slash (encoded or not). - return url.substr(0, Math.max(url.lastIndexOf('/'), url.lastIndexOf('%2F'))); + return url?.substr(0, Math.max(url.lastIndexOf('/'), url.lastIndexOf('%2F'))); } return undefined; @@ -876,7 +875,7 @@ export class CoreTextUtilsProvider { // Current lang not found. Try to find the first language. const matches = text.match(anyLangRegEx); if (matches && matches[0]) { - language = matches[0].match(/lang="([a-zA-Z0-9_-]+)"/)[1]; + language = matches[0].match(/lang="([a-zA-Z0-9_-]+)"/)![1]; currentLangRegEx = new RegExp('<(?:lang|span)[^>]+lang="' + language + '"[^>]*>(.*?)', 'g'); } else { // No multi-lang tag found, stop. diff --git a/src/app/services/utils/utils.ts b/src/app/services/utils/utils.ts index 83df2f679..e48f2493f 100644 --- a/src/app/services/utils/utils.ts +++ b/src/app/services/utils/utils.ts @@ -42,9 +42,9 @@ export class CoreUtilsProvider { protected readonly DONT_CLONE = ['[object FileEntry]', '[object DirectoryEntry]', '[object DOMFileSystem]']; protected logger: CoreLogger; - protected iabInstance: InAppBrowserObject; + protected iabInstance?: InAppBrowserObject; protected uniqueIds: {[name: string]: number} = {}; - protected qrScanData: {deferred: PromiseDefer; observable: Subscription}; + protected qrScanData?: {deferred: PromiseDefer; observable: Subscription}; constructor(protected zone: NgZone) { this.logger = CoreLogger.getInstance('CoreUtilsProvider'); @@ -61,22 +61,14 @@ export class CoreUtilsProvider { * @return New error message. */ addDataNotDownloadedError(error: Error | string, defaultError?: string): string { - let errorMessage = error; + const errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error) || defaultError || ''; - if (error && typeof error != 'string') { - errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error); + if (this.isWebServiceError(error)) { + return errorMessage; } - if (typeof errorMessage != 'string') { - errorMessage = defaultError || ''; - } - - if (!this.isWebServiceError(error)) { - // Local error. Add an extra warning. - errorMessage += '

' + Translate.instance.instant('core.errorsomedatanotdownloaded'); - } - - return errorMessage; + // Local error. Add an extra warning. + return errorMessage + '

' + Translate.instance.instant('core.errorsomedatanotdownloaded'); } /** @@ -116,12 +108,16 @@ export class CoreUtilsProvider { * @param result Object where to put the properties. If not defined, a new object will be created. * @return The object. */ - arrayToObject(array: unknown[], propertyName?: string, result?: unknown): unknown { - result = result || {}; - array.forEach((entry) => { + arrayToObject | string>( + array: T[], + propertyName?: string, + result: Record = {}, + ): Record { + for (const entry of array) { const key = propertyName ? entry[propertyName] : entry; + result[key] = entry; - }); + } return result; } @@ -144,7 +140,7 @@ export class CoreUtilsProvider { maxLevels: number = 0, level: number = 0, undefinedIsNull: boolean = true, - ): boolean { + ): boolean | undefined { if (typeof itemA == 'function' || typeof itemB == 'function') { return true; // Don't compare functions. } else if (typeof itemA == 'object' && typeof itemB == 'object') { @@ -266,9 +262,9 @@ export class CoreUtilsProvider { } return newArray; - } else if (typeof source == 'object' && source !== null) { + } else if (this.isObject(source)) { // Check if the object shouldn't be copied. - if (source && source.toString && this.DONT_CLONE.indexOf(source.toString()) != -1) { + if (source.toString && this.DONT_CLONE.indexOf(source.toString()) != -1) { // Object shouldn't be copied, return it as it is. return source; } @@ -365,7 +361,7 @@ export class CoreUtilsProvider { * @return Promise resolved when all promises are resolved. */ executeOrderedPromises(orderedPromisesData: OrderedPromiseData[]): Promise { - const promises = []; + const promises: Promise[] = []; let dependency = Promise.resolve(); // Execute all the processes in order. @@ -465,8 +461,8 @@ export class CoreUtilsProvider { checkAll?: boolean, ...args: P ): Promise { - const promises = []; - const enabledSites = []; + const promises: Promise[] = []; + const enabledSites: string[] = []; for (const i in siteIds) { const siteId = siteIds[i]; @@ -626,7 +622,7 @@ export class CoreUtilsProvider { // Get the keys of the countries. return this.getCountryList().then((countries) => { // Sort translations. - const sortedCountries = []; + const sortedCountries: { code: string; name: string }[] = []; Object.keys(countries).sort((a, b) => countries[a].localeCompare(countries[b])).forEach((key) => { sortedCountries.push({ code: key, name: countries[key] }); @@ -669,7 +665,7 @@ export class CoreUtilsProvider { const table = await CoreLang.instance.getTranslationTable(lang); // Gather all the keys for countries, - const keys = []; + const keys: string[] = []; for (const name in table) { if (name.indexOf('assets.countries.') === 0) { @@ -696,7 +692,7 @@ export class CoreUtilsProvider { getMimeTypeFromUrl(url: string): Promise { // First check if it can be guessed from the URL. const extension = CoreMimetypeUtils.instance.guessExtensionFromUrl(url); - const mimetype = CoreMimetypeUtils.instance.getMimeType(extension); + const mimetype = extension && CoreMimetypeUtils.instance.getMimeType(extension); if (mimetype) { return Promise.resolve(mimetype); @@ -730,6 +726,16 @@ export class CoreUtilsProvider { return 'isFile' in file; } + /** + * Check if a value is an object. + * + * @param object Variable. + * @return Type guard indicating if this is an object. + */ + isObject(object: unknown): object is Record { + return typeof object === 'object' && object !== null; + } + /** * Given a list of files, check if there are repeated names. * @@ -741,12 +747,12 @@ export class CoreUtilsProvider { return false; } - const names = []; + const names: string[] = []; // Check if there are 2 files with the same name. for (let i = 0; i < files.length; i++) { const file = files[i]; - const name = this.isFileEntry(file) ? file.name : file.filename; + const name = (this.isFileEntry(file) ? file.name : file.filename) || ''; if (names.indexOf(name) > -1) { return Translate.instance.instant('core.filenameexist', { $a: name }); @@ -885,7 +891,7 @@ export class CoreUtilsProvider { path = CoreFile.instance.unconvertFileSrc(path); const extension = CoreMimetypeUtils.instance.getFileExtension(path); - const mimetype = CoreMimetypeUtils.instance.getMimeType(extension); + const mimetype = extension && CoreMimetypeUtils.instance.getMimeType(extension); if (mimetype == 'text/html' && CoreApp.instance.isAndroid()) { // Open HTML local files in InAppBrowser, in system browser some embedded files aren't loaded. @@ -902,7 +908,7 @@ export class CoreUtilsProvider { } try { - await FileOpener.instance.open(path, mimetype); + await FileOpener.instance.open(path, mimetype || ''); } catch (error) { this.logger.error('Error opening file ' + path + ' with mimetype ' + mimetype); this.logger.error('Error: ', JSON.stringify(error)); @@ -924,7 +930,7 @@ export class CoreUtilsProvider { * @param options Override default options passed to InAppBrowser. * @return The opened window. */ - openInApp(url: string, options?: InAppBrowserOptions): InAppBrowserObject { + openInApp(url: string, options?: InAppBrowserOptions): InAppBrowserObject | undefined { if (!url) { return; } @@ -950,7 +956,7 @@ export class CoreUtilsProvider { if (CoreApp.instance.isDesktop() || CoreApp.instance.isMobile()) { let loadStopSubscription; - const loadStartUrls = []; + const loadStartUrls: string[] = []; // Trigger global events when a url is loaded or the window is closed. This is to make it work like in Ionic 1. const loadStartSubscription = this.iabInstance.on('loadstart').subscribe((event) => { @@ -1076,10 +1082,10 @@ export class CoreUtilsProvider { if (typeof value == 'undefined' || value == null) { // Filter undefined and null values. return; - } else if (typeof value == 'object') { + } else if (this.isObject(value)) { // It's an object, return at least an entry for each property. const keys = Object.keys(value); - let entries = []; + let entries: unknown[] = []; keys.forEach((key) => { const newElKey = elKey ? elKey + '[' + key + ']' : key; @@ -1110,9 +1116,9 @@ export class CoreUtilsProvider { if (sortByKey || sortByValue) { return entries.sort((a, b) => { if (sortByKey) { - return a[keyName] >= b[keyName] ? 1 : -1; + return (a[keyName] as number) >= (b[keyName] as number) ? 1 : -1; } else { - return a[valueName] >= b[valueName] ? 1 : -1; + return (a[valueName] as number) >= (b[valueName] as number) ? 1 : -1; } }); } @@ -1135,7 +1141,7 @@ export class CoreUtilsProvider { keyName: string, valueName: string, keyPrefix?: string, - ): {[name: string]: unknown} { + ): {[name: string]: unknown} | undefined { if (!objects) { return; } @@ -1206,13 +1212,13 @@ export class CoreUtilsProvider { * @return The deferred promise. */ promiseDefer(): PromiseDefer { - const deferred: PromiseDefer = {}; + const deferred: Partial> = {}; deferred.promise = new Promise((resolve, reject): void => { deferred.resolve = resolve; deferred.reject = reject; }); - return deferred; + return deferred as PromiseDefer; } /** @@ -1257,7 +1263,11 @@ export class CoreUtilsProvider { * @param key Key to check. * @return Whether the two objects/arrays have the same value (or lack of one) for a given key. */ - sameAtKeyMissingIsBlank(obj1: unknown, obj2: unknown, key: string): boolean { + sameAtKeyMissingIsBlank( + obj1: Record | unknown[], + obj2: Record | unknown[], + key: string, + ): boolean { let value1 = typeof obj1[key] != 'undefined' ? obj1[key] : ''; let value2 = typeof obj2[key] != 'undefined' ? obj2[key] : ''; @@ -1426,19 +1436,19 @@ export class CoreUtilsProvider { * @return Array without duplicate values. */ uniqueArray(array: T[], key?: string): T[] { - const filtered = []; const unique = {}; // Use an object to make it faster to check if it's duplicate. - array.forEach((entry) => { + return array.filter(entry => { const value = key ? entry[key] : entry; - if (!unique[value]) { + if (value in unique) { unique[value] = true; - filtered.push(entry); - } - }); - return filtered; + return true; + } + + return false; + }); } /** @@ -1487,7 +1497,14 @@ export class CoreUtilsProvider { * * @return Promise resolved with the QR string, rejected if error or cancelled. */ - async startScanQR(): Promise { + async startScanQR(): Promise { + try { + return this.startScanQR(); + } catch (error) { + // do nothing + } + + if (!CoreApp.instance.isMobile()) { return Promise.reject('QRScanner isn\'t available in desktop apps.'); } @@ -1613,21 +1630,21 @@ export type PromiseDefer = { /** * The promise. */ - promise?: Promise; + promise: Promise; /** * Function to resolve the promise. * * @param value The resolve value. */ - resolve?: (value?: T) => void; // Function to resolve the promise. + resolve: (value?: T) => void; // Function to resolve the promise. /** * Function to reject the promise. * * @param reason The reject param. */ - reject?: (reason?: unknown) => void; + reject: (reason?: unknown) => void; }; /** diff --git a/src/app/singletons/logger.ts b/src/app/singletons/logger.ts index ee8f9fc4e..c31c3d008 100644 --- a/src/app/singletons/logger.ts +++ b/src/app/singletons/logger.ts @@ -15,6 +15,12 @@ import moment from 'moment'; import { environment } from '@/environments/environment'; + +/** + * Log function type. + */ +type LogFunction = (...data: unknown[]) => void; + /** * Helper service to display messages in the console. * @@ -34,9 +40,13 @@ export class CoreLogger { debug: LogFunction; error: LogFunction; - // Avoid creating singleton instances. - private constructor() { - // Nothing to do. + // Avoid creating instances. + private constructor(log: LogFunction, info: LogFunction, warn: LogFunction, debug: LogFunction, error: LogFunction) { + this.log = log; + this.info = info; + this.warn = warn; + this.debug = debug; + this.error = error; } /** @@ -54,29 +64,23 @@ export class CoreLogger { // eslint-disable-next-line @typescript-eslint/no-empty-function const muted = () => {}; - return { - log: muted, - info: muted, - warn: muted, - debug: muted, - error: muted, - }; + return new CoreLogger(muted, muted, muted, muted, muted); } className = className || ''; - return { + return new CoreLogger( // eslint-disable-next-line no-console - log: CoreLogger.prepareLogFn(console.log.bind(console), className), + CoreLogger.prepareLogFn(console.log.bind(console), className), // eslint-disable-next-line no-console - info: CoreLogger.prepareLogFn(console.info.bind(console), className), + CoreLogger.prepareLogFn(console.info.bind(console), className), // eslint-disable-next-line no-console - warn: CoreLogger.prepareLogFn(console.warn.bind(console), className), + CoreLogger.prepareLogFn(console.warn.bind(console), className), // eslint-disable-next-line no-console - debug: CoreLogger.prepareLogFn(console.debug.bind(console), className), + CoreLogger.prepareLogFn(console.debug.bind(console), className), // eslint-disable-next-line no-console - error: CoreLogger.prepareLogFn(console.error.bind(console), className), - }; + CoreLogger.prepareLogFn(console.error.bind(console), className), + ); } /** @@ -96,8 +100,3 @@ export class CoreLogger { } } - -/** - * Log function type. - */ -type LogFunction = (...data: unknown[]) => void; diff --git a/tsconfig.json b/tsconfig.json index 553929710..5c880d8f6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,8 @@ "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, "module": "esnext", "moduleResolution": "node", "importHelpers": true,