diff --git a/package-lock.json b/package-lock.json index bd88b08bc..aa7eda930 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8574,6 +8574,11 @@ "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", "dev": true }, + "font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", diff --git a/package.json b/package.json index 774cc91a8..b999cd2c8 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "cordova-support-google-services": "^1.2.1", "cordova.plugins.diagnostic": "^6.0.2", "es6-promise-plugin": "^4.2.2", + "font-awesome": "^4.7.0", "moment": "^2.29.0", "nl.kingsquare.cordova.background-audio": "^1.0.1", "phonegap-plugin-multidex": "^1.0.0", diff --git a/src/app/classes/delegate.ts b/src/app/classes/delegate.ts index 464eca45b..dc455ea1b 100644 --- a/src/app/classes/delegate.ts +++ b/src/app/classes/delegate.ts @@ -63,7 +63,7 @@ export class CoreDelegate { /** * Set of promises to update a handler, to prevent doing the same operation twice. */ - protected updatePromises: {[siteId: string]: {[name: string]: Promise}} = {}; + protected updatePromises: {[siteId: string]: {[name: string]: Promise}} = {}; /** * Whether handlers have been initialized. @@ -73,7 +73,7 @@ export class CoreDelegate { /** * Promise to wait for handlers to be initialized. */ - protected handlersInitPromise: Promise; + protected handlersInitPromise: Promise; /** * Function to resolve the handlers init promise. @@ -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: any, fnName: string, params?: any[]): any { + private execute(handler: CoreDelegateHandler, fnName: string, params?: any[]): any { if (handler && handler[fnName]) { return handler[fnName].apply(handler, params); } else if (this.defaultHandler && this.defaultHandler[fnName]) { @@ -243,7 +243,7 @@ export class CoreDelegate { protected updateHandler(handler: CoreDelegateHandler, time: number): Promise { const siteId = CoreSites.instance.getCurrentSiteId(); const currentSite = CoreSites.instance.getCurrentSite(); - let promise; + let promise: Promise; if (this.updatePromises[siteId] && this.updatePromises[siteId][handler.name]) { // There's already an update ongoing for this handler, return the promise. @@ -252,18 +252,14 @@ export class CoreDelegate { this.updatePromises[siteId] = {}; } - if (!CoreSites.instance.isLoggedIn()) { - promise = Promise.reject(null); - } else if (this.isFeatureDisabled(handler, currentSite)) { + if (!CoreSites.instance.isLoggedIn() || this.isFeatureDisabled(handler, currentSite)) { promise = Promise.resolve(false); } else { - promise = Promise.resolve(handler.isEnabled()); + promise = handler.isEnabled().catch(() => false); } // Checks if the handler is enabled. - this.updatePromises[siteId][handler.name] = promise.catch(() => { - return false; - }).then((enabled: boolean) => { + this.updatePromises[siteId][handler.name] = promise.then((enabled: boolean) => { // Check that site hasn't changed since the check started. if (CoreSites.instance.getCurrentSiteId() === siteId) { const key = handler[this.handlerNameProperty] || handler.name; @@ -298,9 +294,9 @@ export class CoreDelegate { * * @return Resolved when done. */ - protected updateHandlers(): Promise { - const promises = [], - now = Date.now(); + protected async updateHandlers(): Promise { + const promises = []; + const now = Date.now(); this.logger.debug('Updating handlers for current site.'); @@ -311,21 +307,19 @@ export class CoreDelegate { promises.push(this.updateHandler(this.handlers[name], now)); } - return Promise.all(promises).then(() => { - return true; - }, () => { - // Never reject. - return true; - }).then(() => { + try { + await Promise.all(promises); + } catch (e) { + // Never reject + } - // Verify that this call is the last one that was started. - if (this.isLastUpdateCall(now)) { - this.handlersInitialized = true; - this.handlersInitResolve(); + // Verify that this call is the last one that was started. + if (this.isLastUpdateCall(now)) { + this.handlersInitialized = true; + this.handlersInitResolve(); - this.updateData(); - } - }); + this.updateData(); + } } /** @@ -335,6 +329,7 @@ export class CoreDelegate { updateData(): any { // To be overridden. } + } export interface CoreDelegateHandler { @@ -346,7 +341,8 @@ export interface CoreDelegateHandler { /** * Whether or not the handler is enabled on a site level. + * * @return Whether or not the handler is enabled on a site level. */ - isEnabled(): boolean | Promise; + isEnabled(): Promise; } diff --git a/src/app/classes/interceptor.ts b/src/app/classes/interceptor.ts index 9877231c1..587c9ca9d 100644 --- a/src/app/classes/interceptor.ts +++ b/src/app/classes/interceptor.ts @@ -32,7 +32,7 @@ export class CoreInterceptor implements HttpInterceptor { */ static serialize(obj: any, addNull?: boolean): string { let query = ''; - let fullSubName; + let fullSubName: string; let subValue; let innerObj; @@ -68,10 +68,11 @@ export class CoreInterceptor implements HttpInterceptor { const newReq = req.clone({ headers: req.headers.set('Content-Type', 'application/x-www-form-urlencoded'), body: typeof req.body == 'object' && String(req.body) != '[object File]' ? - CoreInterceptor.serialize(req.body) : req.body + CoreInterceptor.serialize(req.body) : req.body, }); // Pass on the cloned request instead of the original request. return next.handle(newReq); } + } diff --git a/src/app/classes/native-to-angular-http.ts b/src/app/classes/native-to-angular-http.ts index e21e7eb7e..08349dd30 100644 --- a/src/app/classes/native-to-angular-http.ts +++ b/src/app/classes/native-to-angular-http.ts @@ -92,7 +92,8 @@ export class CoreNativeToAngularHttpResponse extends AngularHttpResponse { headers: new HttpHeaders(nativeResponse.headers), status: nativeResponse.status, statusText: HTTP_STATUS_MESSAGES[nativeResponse.status] || '', - url: nativeResponse.url || '' + url: nativeResponse.url || '', }); } + } diff --git a/src/app/classes/queue-runner.ts b/src/app/classes/queue-runner.ts index fbc5d1d22..aa9868ccd 100644 --- a/src/app/classes/queue-runner.ts +++ b/src/app/classes/queue-runner.ts @@ -53,6 +53,7 @@ export type CoreQueueRunnerAddOptions = { * A queue to prevent having too many concurrent executions. */ export class CoreQueueRunner { + protected queue: {[id: string]: CoreQueueRunnerItem} = {}; protected orderedQueue: CoreQueueRunnerItem[] = []; protected numberRunning = 0; @@ -140,4 +141,5 @@ export class CoreQueueRunner { return item.deferred.promise; } + } diff --git a/src/app/classes/singletons-factory.ts b/src/app/classes/singletons-factory.ts index 3415762a2..562b9a3a4 100644 --- a/src/app/classes/singletons-factory.ts +++ b/src/app/classes/singletons-factory.ts @@ -80,4 +80,5 @@ export class CoreSingletonsFactory { }; } + } diff --git a/src/app/classes/sqlitedb.ts b/src/app/classes/sqlitedb.ts index c7aadbefc..135770a52 100644 --- a/src/app/classes/sqlitedb.ts +++ b/src/app/classes/sqlitedb.ts @@ -131,6 +131,7 @@ export interface SQLiteDBForeignKeySchema { * this.db = new SQLiteDB('MyDB'); */ export class SQLiteDB { + db: SQLiteObject; promise: Promise; @@ -240,10 +241,10 @@ export class SQLiteDB { * * @return Promise resolved when done. */ - async close(): Promise { + async close(): Promise { await this.ready(); - return this.db.close(); + await this.db.close(); } /** @@ -253,7 +254,7 @@ export class SQLiteDB { * @param conditions The conditions to build the where clause. Must not contain numeric indexes. * @return Promise resolved with the count of records returned from the specified criteria. */ - countRecords(table: string, conditions?: object): Promise { + async countRecords(table: string, conditions?: SQLiteDBRecordValues): Promise { const selectAndParams = this.whereClause(conditions); return this.countRecordsSelect(table, selectAndParams[0], selectAndParams[1]); @@ -268,7 +269,8 @@ export class SQLiteDB { * @param countItem The count string to be used in the SQL call. Default is COUNT('x'). * @return Promise resolved with the count of records returned from the specified criteria. */ - countRecordsSelect(table: string, select: string = '', params?: any, countItem: string = 'COUNT(\'x\')'): Promise { + async countRecordsSelect(table: string, select: string = '', params?: SQLiteDBRecordValue[], + countItem: string = 'COUNT(\'x\')'): Promise { if (select) { select = 'WHERE ' + select; } @@ -285,14 +287,13 @@ export class SQLiteDB { * @param params An array of sql parameters. * @return Promise resolved with the count. */ - countRecordsSql(sql: string, params?: any): Promise { - return this.getFieldSql(sql, params).then((count) => { - if (typeof count != 'number' || count < 0) { - return 0; - } + async countRecordsSql(sql: string, params?: SQLiteDBRecordValue[]): Promise { + const count = await this.getFieldSql(sql, params); + if (typeof count != 'number' || count < 0) { + return 0; + } - return count; - }); + return count; } /** @@ -306,11 +307,11 @@ export class SQLiteDB { * @param tableCheck Check constraint for the table. * @return Promise resolved when success. */ - createTable(name: string, columns: SQLiteDBColumnSchema[], primaryKeys?: string[], uniqueKeys?: string[][], - foreignKeys?: SQLiteDBForeignKeySchema[], tableCheck?: string): Promise { + async createTable(name: string, columns: SQLiteDBColumnSchema[], primaryKeys?: string[], uniqueKeys?: string[][], + foreignKeys?: SQLiteDBForeignKeySchema[], tableCheck?: string): Promise { const sql = this.buildCreateTableSql(name, columns, primaryKeys, uniqueKeys, foreignKeys, tableCheck); - return this.execute(sql); + await this.execute(sql); } /** @@ -319,9 +320,8 @@ export class SQLiteDB { * @param table Table schema. * @return Promise resolved when success. */ - createTableFromSchema(table: SQLiteDBTableSchema): Promise { - return this.createTable(table.name, table.columns, table.primaryKeys, table.uniqueKeys, - table.foreignKeys, table.tableCheck); + async createTableFromSchema(table: SQLiteDBTableSchema): Promise { + await this.createTable(table.name, table.columns, table.primaryKeys, table.uniqueKeys, table.foreignKeys, table.tableCheck); } /** @@ -330,13 +330,13 @@ export class SQLiteDB { * @param tables List of table schema. * @return Promise resolved when success. */ - createTablesFromSchema(tables: SQLiteDBTableSchema[]): Promise { + async createTablesFromSchema(tables: SQLiteDBTableSchema[]): Promise { const promises = []; tables.forEach((table) => { promises.push(this.createTableFromSchema(table)); }); - return Promise.all(promises); + await Promise.all(promises); } /** @@ -345,12 +345,14 @@ export class SQLiteDB { * * @param table The table to delete from. * @param conditions The conditions to build the where clause. Must not contain numeric indexes. - * @return Promise resolved when done. + * @return Promise resolved with the number of affected rows. */ - deleteRecords(table: string, conditions?: object): Promise { + async deleteRecords(table: string, conditions?: SQLiteDBRecordValues): Promise { if (conditions === null || typeof conditions == 'undefined') { // No conditions, delete the whole table. - return this.execute(`DELETE FROM ${table}`); + const result = await this.execute(`DELETE FROM ${table}`); + + return result.rowsAffected; } const selectAndParams = this.whereClause(conditions); @@ -364,9 +366,9 @@ export class SQLiteDB { * @param table The table to delete from. * @param field The name of a field. * @param values The values field might take. - * @return Promise resolved when done. + * @return Promise resolved with the number of affected rows. */ - deleteRecordsList(table: string, field: string, values: any[]): Promise { + async deleteRecordsList(table: string, field: string, values: SQLiteDBRecordValue[]): Promise { const selectAndParams = this.whereClauseList(field, values); return this.deleteRecordsSelect(table, selectAndParams[0], selectAndParams[1]); @@ -378,14 +380,16 @@ export class SQLiteDB { * @param table The table to delete from. * @param select A fragment of SQL to be used in a where clause in the SQL call. * @param params Array of sql parameters. - * @return Promise resolved when done. + * @return Promise resolved with the number of affected rows. */ - deleteRecordsSelect(table: string, select: string = '', params?: any[]): Promise { + async deleteRecordsSelect(table: string, select: string = '', params?: SQLiteDBRecordValue[]): Promise { if (select) { select = 'WHERE ' + select; } - return this.execute(`DELETE FROM ${table} ${select}`, params); + const result = await this.execute(`DELETE FROM ${table} ${select}`, params); + + return result.rowsAffected; } /** @@ -394,8 +398,8 @@ export class SQLiteDB { * @param name The table name. * @return Promise resolved when success. */ - dropTable(name: string): Promise { - return this.execute(`DROP TABLE IF EXISTS ${name}`); + async dropTable(name: string): Promise { + await this.execute(`DROP TABLE IF EXISTS ${name}`); } /** @@ -407,7 +411,7 @@ export class SQLiteDB { * @param params Query parameters. * @return Promise resolved with the result. */ - async execute(sql: string, params?: any[]): Promise { + async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise { await this.ready(); return this.db.executeSql(sql, params); @@ -421,10 +425,10 @@ export class SQLiteDB { * @param sqlStatements SQL statements to execute. * @return Promise resolved with the result. */ - async executeBatch(sqlStatements: any[]): Promise { + async executeBatch(sqlStatements: (string | SQLiteDBRecordValue[])[][]): Promise { await this.ready(); - return this.db.sqlBatch(sqlStatements); + await this.db.sqlBatch(sqlStatements); } /** @@ -432,27 +436,35 @@ export class SQLiteDB { * * @param data Data to insert. */ - protected formatDataToInsert(data: object): void { + protected formatDataToInsert(data: SQLiteDBRecordValues): void { if (!data) { return; } // Remove undefined entries and convert null to "NULL". for (const name in data) { - const value = data[name]; - if (typeof value == 'undefined') { + if (typeof data[name] == 'undefined') { delete data[name]; } } } + /** + * Format the data to where params. + * + * @param data Object data. + */ + protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] { + return Object.keys(data).map((key) => data[key]); + } + /** * Get all the records from a table. * * @param table The table to query. * @return Promise resolved with the records. */ - getAllRecords(table: string): Promise { + async getAllRecords(table: string): Promise { return this.getRecords(table); } @@ -464,7 +476,7 @@ export class SQLiteDB { * @param conditions The conditions to build the where clause. Must not contain numeric indexes. * @return Promise resolved with the field's value. */ - getField(table: string, field: string, conditions?: object): Promise { + async getField(table: string, field: string, conditions?: SQLiteDBRecordValues): Promise { const selectAndParams = this.whereClause(conditions); return this.getFieldSelect(table, field, selectAndParams[0], selectAndParams[1]); @@ -479,7 +491,8 @@ export class SQLiteDB { * @param params Array of sql parameters. * @return Promise resolved with the field's value. */ - getFieldSelect(table: string, field: string, select: string = '', params?: any[]): Promise { + async getFieldSelect(table: string, field: string, select: string = '', params?: SQLiteDBRecordValue[]): + Promise { if (select) { select = 'WHERE ' + select; } @@ -494,10 +507,10 @@ export class SQLiteDB { * @param params An array of sql parameters. * @return Promise resolved with the field's value. */ - async getFieldSql(sql: string, params?: any[]): Promise { + async getFieldSql(sql: string, params?: SQLiteDBRecordValue[]): Promise { const record = await this.getRecordSql(sql, params); if (!record) { - return Promise.reject(null); + throw null; } return record[Object.keys(record)[0]]; @@ -512,17 +525,14 @@ export class SQLiteDB { * meaning return empty. Other values will become part of the returned SQL fragment. * @return A list containing the constructed sql fragment and an array of parameters. */ - getInOrEqual(items: any, equal: boolean = true, onEmptyItems?: any): any[] { - let sql; - let params; - - if (typeof onEmptyItems == 'undefined') { - onEmptyItems = false; - } + getInOrEqual(items: SQLiteDBRecordValue | SQLiteDBRecordValue[], equal: boolean = true, onEmptyItems?: SQLiteDBRecordValue): + SQLiteDBQueryParams { + let sql = ''; + let params: SQLiteDBRecordValue[]; // Default behavior, return empty data on empty array. - if (Array.isArray(items) && !items.length && onEmptyItems === false) { - return ['', []]; + if (Array.isArray(items) && !items.length && typeof onEmptyItems == 'undefined') { + return { sql: '', params: [] }; } // Handle onEmptyItems on empty array of items. @@ -530,7 +540,7 @@ export class SQLiteDB { if (onEmptyItems === null) { // Special case, NULL value. sql = equal ? ' IS NULL' : ' IS NOT NULL'; - return [sql, []]; + return { sql, params: [] }; } else { items = [onEmptyItems]; // Rest of cases, prepare items for processing. } @@ -544,7 +554,7 @@ export class SQLiteDB { params = items; } - return [sql, params]; + return { sql, params }; } /** @@ -564,7 +574,7 @@ 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?: object, 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); @@ -579,7 +589,8 @@ 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: any[] = [], fields: string = '*'): Promise { + getRecordSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], fields: string = '*'): + Promise { if (select) { select = ' WHERE ' + select; } @@ -597,11 +608,11 @@ export class SQLiteDB { * @param params List of sql parameters * @return Promise resolved with the records. */ - async getRecordSql(sql: string, params?: any[]): Promise { + async getRecordSql(sql: string, params?: SQLiteDBRecordValue[]): Promise { const result = await this.getRecordsSql(sql, params, 0, 1); if (!result || !result.length) { // Not found, reject. - return Promise.reject(null); + throw null; } return result[0]; @@ -618,8 +629,8 @@ export class SQLiteDB { * @param limitNum Return a subset comprising this many records in total. * @return Promise resolved with the records. */ - getRecords(table: string, conditions?: object, 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); @@ -637,8 +648,8 @@ 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: any[], 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); @@ -656,8 +667,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: any[] = [], 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; } @@ -679,7 +690,8 @@ export class SQLiteDB { * @param limitNum Return a subset comprising this many records. * @return Promise resolved with the records. */ - async getRecordsSql(sql: string, params?: any[], 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]) { @@ -706,31 +718,31 @@ export class SQLiteDB { * @param data A data object with values for one or more fields in the record. * @return Array with the SQL query and the params. */ - protected getSqlInsertQuery(table: string, data: object): any[] { + protected getSqlInsertQuery(table: string, data: SQLiteDBRecordValues): SQLiteDBQueryParams { this.formatDataToInsert(data); const keys = Object.keys(data); const fields = keys.join(','); const questionMarks = ',?'.repeat(keys.length).substr(1); - return [ - `INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`, - keys.map((key) => data[key]) - ]; + return { + sql: `INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`, + params: this.formatDataToSQLParams(data), + }; } /** * Initialize the database. */ init(): void { - this.promise = Platform.instance.ready().then(() => { - return SQLite.instance.create({ + this.promise = Platform.instance.ready() + .then(() => SQLite.instance.create({ name: this.name, - location: 'default' + location: 'default', + })) + .then((db: SQLiteObject) => { + this.db = db; }); - }).then((db: SQLiteObject) => { - this.db = db; - }); } /** @@ -740,7 +752,7 @@ export class SQLiteDB { * @param data A data object with values for one or more fields in the record. * @return Promise resolved with new rowId. Please notice this rowId is internal from SQLite. */ - async insertRecord(table: string, data: object): Promise { + async insertRecord(table: string, data: SQLiteDBRecordValues): Promise { const sqlAndParams = this.getSqlInsertQuery(table, data); const result = await this.execute(sqlAndParams[0], sqlAndParams[1]); @@ -754,18 +766,18 @@ export class SQLiteDB { * @param dataObjects List of objects to be inserted. * @return Promise resolved when done. */ - insertRecords(table: string, dataObjects: object[]): Promise { + async insertRecords(table: string, dataObjects: SQLiteDBRecordValues[]): Promise { if (!Array.isArray(dataObjects)) { - return Promise.reject(null); + throw null; } - const statements = []; + const statements = dataObjects.map((dataObject) => { + const statement = this.getSqlInsertQuery(table, dataObject); - dataObjects.forEach((dataObject) => { - statements.push(this.getSqlInsertQuery(table, dataObject)); + return [statement.sql, statement.params]; }); - return this.executeBatch(statements); + await this.executeBatch(statements); } /** @@ -777,12 +789,13 @@ export class SQLiteDB { * @param fields A comma separated list of fields to return. * @return Promise resolved when done. */ - insertRecordsFrom(table: string, source: string, conditions?: object, fields: string = '*'): Promise { + async insertRecordsFrom(table: string, source: string, conditions?: SQLiteDBRecordValues, fields: string = '*'): + Promise { const selectAndParams = this.whereClause(conditions); const select = selectAndParams[0] ? 'WHERE ' + selectAndParams[0] : ''; const params = selectAndParams[1]; - return this.execute(`INSERT INTO ${table} SELECT ${fields} FROM ${source} ${select}`, params); + await this.execute(`INSERT INTO ${table} SELECT ${fields} FROM ${source} ${select}`, params); } /** @@ -794,19 +807,19 @@ export class SQLiteDB { * @param limitNum How many results to return. * @return Normalised limit params in array: [limitFrom, limitNum]. */ - normaliseLimitFromNum(limitFrom: any, limitNum: any): number[] { + normaliseLimitFromNum(limitFrom: number, limitNum: number): number[] { // We explicilty treat these cases as 0. - if (typeof limitFrom == 'undefined' || limitFrom === null || limitFrom === '' || limitFrom === -1) { + if (!limitFrom || limitFrom === -1) { limitFrom = 0; - } - if (typeof limitNum == 'undefined' || limitNum === null || limitNum === '' || limitNum === -1) { - limitNum = 0; + } else { + limitFrom = Math.max(0, limitFrom); } - limitFrom = parseInt(limitFrom, 10); - limitNum = parseInt(limitNum, 10); - limitFrom = Math.max(0, limitFrom); - limitNum = Math.max(0, limitNum); + if (!limitNum || limitNum === -1) { + limitNum = 0; + } else { + limitNum = Math.max(0, limitNum); + } return [limitFrom, limitNum]; } @@ -816,10 +829,10 @@ export class SQLiteDB { * * @return Promise resolved when open. */ - async open(): Promise { + async open(): Promise { await this.ready(); - return this.db.open(); + await this.db.open(); } /** @@ -838,10 +851,10 @@ export class SQLiteDB { * @param conditions The conditions to build the where clause. Must not contain numeric indexes. * @return Promise resolved if exists, rejected otherwise. */ - async recordExists(table: string, conditions?: object): Promise { + async recordExists(table: string, conditions?: SQLiteDBRecordValues): Promise { const record = await this.getRecord(table, conditions); if (!record) { - return Promise.reject(null); + throw null; } } @@ -853,10 +866,10 @@ export class SQLiteDB { * @param params An array of sql parameters. * @return Promise resolved if exists, rejected otherwise. */ - async recordExistsSelect(table: string, select: string = '', params: any[] = []): Promise { + async recordExistsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = []): Promise { const record = await this.getRecordSelect(table, select, params); if (!record) { - return Promise.reject(null); + throw null; } } @@ -867,10 +880,10 @@ export class SQLiteDB { * @param params An array of sql parameters. * @return Promise resolved if exists, rejected otherwise. */ - async recordExistsSql(sql: string, params?: any[]): Promise { + async recordExistsSql(sql: string, params?: SQLiteDBRecordValue[]): Promise { const record = await this.getRecordSql(sql, params); if (!record) { - return Promise.reject(null); + throw null; } } @@ -881,7 +894,8 @@ export class SQLiteDB { * @return Promise resolved if exists, rejected otherwise. */ async tableExists(name: string): Promise { - await this.recordExists('sqlite_master', {type: 'table', tbl_name: name}); + // eslint-disable-next-line @typescript-eslint/naming-convention + await this.recordExists('sqlite_master', { type: 'table', tbl_name: name }); } /** @@ -890,31 +904,12 @@ export class SQLiteDB { * @param string table The database table to update. * @param data An object with the fields to update: fieldname=>fieldvalue. * @param conditions The conditions to build the where clause. Must not contain numeric indexes. - * @return Promise resolved when updated. + * @return Promise resolved with the number of affected rows. */ - updateRecords(table: string, data: any, conditions?: any): Promise { - - this.formatDataToInsert(data); - - if (!data || !Object.keys(data).length) { - // No fields to update, consider it's done. - return Promise.resolve(); - } - + async updateRecords(table: string, data: SQLiteDBRecordValues, conditions?: SQLiteDBRecordValues): Promise { const whereAndParams = this.whereClause(conditions); - const sets = []; - let sql; - let params; - for (const key in data) { - sets.push(`${key} = ?`); - } - - sql = `UPDATE ${table} SET ${sets.join(', ')} WHERE ${whereAndParams[0]}`; - // Create the list of params using the "data" object and the params for the where clause. - params = Object.keys(data).map((key) => data[key]).concat(whereAndParams[1]); - - return this.execute(sql, params); + return this.updateRecordsWhere(table, data, whereAndParams[0], whereAndParams[1]); } /** @@ -924,34 +919,35 @@ export class SQLiteDB { * @param data An object with the fields to update: fieldname=>fieldvalue. * @param where Where clause. Must not include the "WHERE" word. * @param whereParams Params for the where clause. - * @return Promise resolved when updated. + * @return Promise resolved with the number of affected rows. */ - updateRecordsWhere(table: string, data: any, where?: string, whereParams?: any[]): Promise { + async updateRecordsWhere(table: string, data: SQLiteDBRecordValues, where?: string, whereParams?: SQLiteDBRecordValue[]): + Promise { + this.formatDataToInsert(data); if (!data || !Object.keys(data).length) { // No fields to update, consider it's done. - return Promise.resolve(); + return 0; } const sets = []; - let sql; - let params; - for (const key in data) { sets.push(`${key} = ?`); } - sql = `UPDATE ${table} SET ${sets.join(', ')}`; + let sql = `UPDATE ${table} SET ${sets.join(', ')}`; if (where) { sql += ` WHERE ${where}`; } // Create the list of params using the "data" object and the params for the where clause. - params = Object.keys(data).map((key) => data[key]); + let params = this.formatDataToSQLParams(data); if (where && whereParams) { params = params.concat(whereParams); } - return this.execute(sql, params); + const result = await this.execute(sql, params); + + return result.rowsAffected; } /** @@ -960,9 +956,12 @@ export class SQLiteDB { * @param conditions The conditions to build the where clause. Must not contain numeric indexes. * @return An array list containing sql 'where' part and 'params'. */ - whereClause(conditions: any = {}): any[] { + whereClause(conditions: SQLiteDBRecordValues = {}): SQLiteDBQueryParams { if (!conditions || !Object.keys(conditions).length) { - return ['1 = 1', []]; + return { + sql: '1 = 1', + params: [], + }; } const where = []; @@ -979,7 +978,10 @@ export class SQLiteDB { } } - return [where.join(' AND '), params]; + return { + sql: where.join(' AND '), + params, + }; } /** @@ -989,39 +991,50 @@ export class SQLiteDB { * @param values The values field might take. * @return An array containing sql 'where' part and 'params'. */ - whereClauseList(field: string, values: any[]): any[] { + whereClauseList(field: string, values: SQLiteDBRecordValue[]): SQLiteDBQueryParams { if (!values || !values.length) { - return ['1 = 2', []]; // Fake condition, won't return rows ever. + return { + sql: '1 = 2', // Fake condition, won't return rows ever. + params: [], + }; } const params = []; - let select = ''; + let sql = ''; values.forEach((value) => { - if (typeof value == 'boolean') { - value = Number(value); - } - if (typeof value == 'undefined' || value === null) { - select = field + ' IS NULL'; + sql = field + ' IS NULL'; } else { params.push(value); } }); if (params && params.length) { - if (select !== '') { - select = select + ' OR '; + if (sql !== '') { + sql = sql + ' OR '; } if (params.length == 1) { - select = select + field + ' = ?'; + sql = sql + field + ' = ?'; } else { const questionMarks = ',?'.repeat(params.length).substr(1); - select = select + field + ' IN (' + questionMarks + ')'; + sql = sql + field + ' IN (' + questionMarks + ')'; } } - return [select, params]; + return { sql, params }; } + } + +export type SQLiteDBRecordValues = { + [key in string ]: SQLiteDBRecordValue; +}; + +export type SQLiteDBQueryParams = { + sql: string; + params: SQLiteDBRecordValue[]; +}; + +type SQLiteDBRecordValue = number | string; diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts new file mode 100644 index 000000000..55bee79bb --- /dev/null +++ b/src/app/components/components.module.ts @@ -0,0 +1,27 @@ +// (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 { NgModule } from '@angular/core'; +import { CoreIconComponent } from './icon/icon'; + +@NgModule({ + declarations: [ + CoreIconComponent, + ], + imports: [], + exports: [ + CoreIconComponent, + ] +}) +export class CoreComponentsModule {} diff --git a/src/app/components/icon/core-icon.html b/src/app/components/icon/core-icon.html new file mode 100644 index 000000000..7c89b545c --- /dev/null +++ b/src/app/components/icon/core-icon.html @@ -0,0 +1 @@ +
diff --git a/src/app/components/icon/icon.scss b/src/app/components/icon/icon.scss new file mode 100644 index 000000000..0b3467edb --- /dev/null +++ b/src/app/components/icon/icon.scss @@ -0,0 +1,52 @@ +// TODO ionic 5 +:host-context([dir=rtl]) ion-icon { + &.core-icon-dir-flip, + &.fa-caret-right, + &.ion-md-send, &.ion-ios-send { + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); + } +} + +// Slash +@font-face { + font-family: "Moodle Slash Icon"; + font-style: normal; + font-weight: 400; + src: url("/assets/fonts/slash-icon.woff") format("woff"); +} + +:host { + &.fa { + font-size: 24px; + } + + // Center font awesome icons + &.fa::before { + width: 1em; + height: 1em; + text-align: center; + } + + &.icon-slash { + position: relative; + &::after { + content: "/"; + font-family: "Moodle Slash Icon"; + font-size: 0.75em; + margin-top: 0.125em; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + text-align: center; + color: var(--ion-color-danger); + } + + &.fa::after { + font-size: 1em; + margin-top: 0; + } + } +} diff --git a/src/app/components/icon/icon.ts b/src/app/components/icon/icon.ts new file mode 100644 index 000000000..ff70d868e --- /dev/null +++ b/src/app/components/icon/icon.ts @@ -0,0 +1,135 @@ +// (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 { Component, Input, OnChanges, OnDestroy, ElementRef, SimpleChange } from '@angular/core'; + +/** + * Core Icon is a component that enables the posibility to add fontawesome icon to the html. It's recommended if both fontawesome + * or ionicons can be used in the name attribute. To use fontawesome just place the full icon name with the fa- prefix and + * the component will detect it. + * Check available icons at https://fontawesome.com/v4.7.0/icons/. + */ +@Component({ + selector: 'core-icon', + templateUrl: 'core-icon.html', + styleUrls: ['icon.scss'], +}) +export class CoreIconComponent implements OnChanges, OnDestroy { + // Common params. + @Input() name: string; + @Input('color') color?: string; + @Input('slash') slash?: boolean; // Display a red slash over the icon. + + // Ionicons params. + @Input('isActive') isActive?: boolean; + @Input('md') md?: string; + @Input('ios') ios?: string; + + // FontAwesome params. + @Input('fixed-width') fixedWidth: string; + + @Input('label') ariaLabel?: string; + @Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false. + + protected element: HTMLElement; + protected newElement: HTMLElement; + + constructor(el: ElementRef) { + this.element = el.nativeElement; + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (!changes.name || !this.name) { + return; + } + + const oldElement = this.newElement ? this.newElement : this.element; + + // Use a new created element to avoid ion-icon working. + // This is necessary to make the FontAwesome stuff work. + // It is also required to stop Ionic overriding the aria-label attribute. + this.newElement = document.createElement('ion-icon'); + if (this.name.startsWith('fa-')) { + this.newElement.classList.add('fa'); + this.newElement.classList.add(this.name); + if (this.isTrueProperty(this.fixedWidth)) { + this.newElement.classList.add('fa-fw'); + } + if (this.color) { + this.newElement.classList.add('fa-' + this.color); + } + } + + !this.ariaLabel && this.newElement.setAttribute('aria-hidden', 'true'); + !this.ariaLabel && this.newElement.setAttribute('role', 'presentation'); + this.ariaLabel && this.newElement.setAttribute('aria-label', this.ariaLabel); + this.ariaLabel && this.newElement.setAttribute('title', this.ariaLabel); + + const attrs = this.element.attributes; + for (let i = attrs.length - 1; i >= 0; i--) { + if (attrs[i].name == 'class') { + // We don't want to override the classes we already added. Add them one by one. + if (attrs[i].value) { + const classes = attrs[i].value.split(' '); + for (let j = 0; j < classes.length; j++) { + if (classes[j]) { + this.newElement.classList.add(classes[j]); + } + } + } + + } else { + this.newElement.setAttribute(attrs[i].name, attrs[i].value); + } + } + + if (this.slash) { + this.newElement.classList.add('icon-slash'); + } + + if (this.flipRtl) { + this.newElement.classList.add('core-icon-dir-flip'); + } + + oldElement.parentElement.replaceChild(this.newElement, oldElement); + } + + /** + * Check if the value is true or on. + * + * @param val value to be checked. + * @return If has a value equivalent to true. + */ + isTrueProperty(val: any): boolean { + if (typeof val === 'string') { + val = val.toLowerCase().trim(); + + return (val === 'true' || val === 'on' || val === ''); + } + + return !!val; + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + if (this.newElement) { + this.newElement.remove(); + } + } +} diff --git a/src/app/pipes/create-links.pipe.ts b/src/app/pipes/create-links.pipe.ts new file mode 100644 index 000000000..e031c9b0d --- /dev/null +++ b/src/app/pipes/create-links.pipe.ts @@ -0,0 +1,35 @@ +// (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 { Pipe, PipeTransform } from '@angular/core'; + +/** + * Pipe to search URLs that are not inside tags and add the corresponding tags. + */ +@Pipe({ + name: 'coreCreateLinks', +}) +export class CoreCreateLinksPipe implements PipeTransform { + protected static replacePattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])(?![^<]*>|[^<>]*<\/)/gim; + + /** + * Takes some text and adds anchor tags to all links that aren't inside anchors. + * + * @param text Text to treat. + * @return Treated text. + */ + 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 new file mode 100644 index 000000000..c1da56431 --- /dev/null +++ b/src/app/pipes/no-tags.pipe.ts @@ -0,0 +1,34 @@ +// (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 { Pipe, PipeTransform } from '@angular/core'; + +/** + * Pipe to remove HTML tags. + */ +@Pipe({ + name: 'coreNoTags', +}) +export class CoreNoTagsPipe implements PipeTransform { + + /** + * Takes a text and removes HTML tags. + * + * @param text The text to treat. + * @return Treated text. + */ + transform(text: string): string { + return text.replace(/(<([^>]+)>)/ig, ''); + } +} diff --git a/src/app/pipes/pipes.module.ts b/src/app/pipes/pipes.module.ts new file mode 100644 index 000000000..053fec971 --- /dev/null +++ b/src/app/pipes/pipes.module.ts @@ -0,0 +1,33 @@ +// (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 { NgModule } from '@angular/core'; +import { CoreCreateLinksPipe } from './create-links.pipe'; +import { CoreNoTagsPipe } from './no-tags.pipe'; +import { CoreTimeAgoPipe } from './time-ago.pipe'; + +@NgModule({ + declarations: [ + CoreCreateLinksPipe, + CoreNoTagsPipe, + CoreTimeAgoPipe, + ], + imports: [], + exports: [ + CoreCreateLinksPipe, + CoreNoTagsPipe, + CoreTimeAgoPipe, + ] +}) +export class CorePipesModule {} diff --git a/src/app/pipes/time-ago.pipe.ts b/src/app/pipes/time-ago.pipe.ts new file mode 100644 index 000000000..ad53c086a --- /dev/null +++ b/src/app/pipes/time-ago.pipe.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 { Pipe, PipeTransform } from '@angular/core'; +import { Translate } from '@singletons/core.singletons'; +import { CoreLogger } from '@singletons/logger'; +import moment from 'moment'; + +/** + * Pipe to turn a UNIX timestamp to "time ago". + */ +@Pipe({ + name: 'coreTimeAgo', +}) +export class CoreTimeAgoPipe implements PipeTransform { + private logger: CoreLogger; + + constructor() { + this.logger = CoreLogger.getInstance('CoreTimeAgoPipe'); + } + + /** + * Turn a UNIX timestamp to "time ago". + * + * @param timestamp The UNIX timestamp (without milliseconds). + * @return Formatted time. + */ + transform(timestamp: string | number): string { + if (typeof timestamp == 'string') { + // Convert the value to a number. + const numberTimestamp = parseInt(timestamp, 10); + if (isNaN(numberTimestamp)) { + this.logger.error('Invalid value received', timestamp); + + return timestamp; + } + timestamp = numberTimestamp; + } + + return Translate.instance.instant('core.ago', {$a: moment(timestamp * 1000).fromNow(true)}); + } +} diff --git a/src/app/services/filepool.ts b/src/app/services/filepool.ts index 1373615b3..ac0ea8b9a 100644 --- a/src/app/services/filepool.ts +++ b/src/app/services/filepool.ts @@ -27,7 +27,7 @@ import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrlUtils } from '@services/utils/url'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { SQLiteDB } from '@classes/sqlitedb'; import { CoreConstants } from '@core/constants'; import { makeSingleton, Network, NgZone } from '@singletons/core.singletons'; @@ -44,210 +44,214 @@ import { CoreLogger } from '@singletons/logger'; */ @Injectable() export class CoreFilepoolProvider { + // Constants. - protected QUEUE_PROCESS_INTERVAL = 0; - protected FOLDER = 'filepool'; - protected WIFI_DOWNLOAD_THRESHOLD = 20971520; // 20MB. - protected DOWNLOAD_THRESHOLD = 2097152; // 2MB. - protected QUEUE_RUNNING = 'CoreFilepool:QUEUE_RUNNING'; - protected QUEUE_PAUSED = 'CoreFilepool:QUEUE_PAUSED'; - protected ERR_QUEUE_IS_EMPTY = 'CoreFilepoolError:ERR_QUEUE_IS_EMPTY'; - protected ERR_FS_OR_NETWORK_UNAVAILABLE = 'CoreFilepoolError:ERR_FS_OR_NETWORK_UNAVAILABLE'; - protected ERR_QUEUE_ON_PAUSE = 'CoreFilepoolError:ERR_QUEUE_ON_PAUSE'; - protected FILE_UPDATE_UNKNOWN_WHERE_CLAUSE = + protected static readonly QUEUE_PROCESS_INTERVAL = 0; + protected static readonly FOLDER = 'filepool'; + protected static readonly WIFI_DOWNLOAD_THRESHOLD = 20971520; // 20MB. + protected static readonly DOWNLOAD_THRESHOLD = 2097152; // 2MB. + protected static readonly QUEUE_RUNNING = 'CoreFilepool:QUEUE_RUNNING'; + protected static readonly QUEUE_PAUSED = 'CoreFilepool:QUEUE_PAUSED'; + protected static readonly ERR_QUEUE_IS_EMPTY = 'CoreFilepoolError:ERR_QUEUE_IS_EMPTY'; + 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 = 'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))'; // Variables for database. - protected QUEUE_TABLE = 'filepool_files_queue'; // Queue of files to download. - protected FILES_TABLE = 'filepool_files'; // Downloaded files. - protected LINKS_TABLE = 'filepool_files_links'; // Links between downloaded files and components. - protected PACKAGES_TABLE = 'filepool_packages'; // Downloaded packages (sets of files). + protected static readonly QUEUE_TABLE = 'filepool_files_queue'; // Queue of files to download. + protected static readonly FILES_TABLE = 'filepool_files'; // Downloaded files. + protected static readonly LINKS_TABLE = 'filepool_files_links'; // Links between downloaded files and components. + protected static readonly PACKAGES_TABLE = 'filepool_packages'; // Downloaded packages (sets of files). protected appTablesSchema: CoreAppSchema = { name: 'CoreFilepoolProvider', version: 1, tables: [ { - name: this.QUEUE_TABLE, + name: CoreFilepoolProvider.QUEUE_TABLE, columns: [ { name: 'siteId', - type: 'TEXT' + type: 'TEXT', }, { name: 'fileId', - type: 'TEXT' + type: 'TEXT', }, { name: 'added', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'priority', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'url', - type: 'TEXT' + type: 'TEXT', }, { name: 'revision', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'timemodified', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'isexternalfile', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'repositorytype', - type: 'TEXT' + type: 'TEXT', }, { name: 'path', - type: 'TEXT' + type: 'TEXT', }, { name: 'links', - type: 'TEXT' + type: 'TEXT', }, ], primaryKeys: ['siteId', 'fileId'], }, ], }; + protected siteSchema: CoreSiteSchema = { name: 'CoreFilepoolProvider', version: 1, tables: [ { - name: this.FILES_TABLE, + name: CoreFilepoolProvider.FILES_TABLE, columns: [ { name: 'fileId', type: 'TEXT', - primaryKey: true + primaryKey: true, }, { name: 'url', type: 'TEXT', - notNull: true + notNull: true, }, { name: 'revision', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'timemodified', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'stale', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'downloadTime', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'isexternalfile', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'repositorytype', - type: 'TEXT' + type: 'TEXT', }, { name: 'path', - type: 'TEXT' + type: 'TEXT', }, { name: 'extension', - type: 'TEXT' - } - ] + type: 'TEXT', + }, + ], }, { - name: this.LINKS_TABLE, + name: CoreFilepoolProvider.LINKS_TABLE, columns: [ { name: 'fileId', - type: 'TEXT' + type: 'TEXT', }, { name: 'component', - type: 'TEXT' + type: 'TEXT', }, { name: 'componentId', - type: 'TEXT' - } + type: 'TEXT', + }, ], - primaryKeys: ['fileId', 'component', 'componentId'] + primaryKeys: ['fileId', 'component', 'componentId'], }, { - name: this.PACKAGES_TABLE, + name: CoreFilepoolProvider.PACKAGES_TABLE, columns: [ { name: 'id', type: 'TEXT', - primaryKey: true + primaryKey: true, }, { name: 'component', - type: 'TEXT' + type: 'TEXT', }, { name: 'componentId', - type: 'TEXT' + type: 'TEXT', }, { name: 'status', - type: 'TEXT' + type: 'TEXT', }, { name: 'previous', - type: 'TEXT' + type: 'TEXT', }, { name: 'updated', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'downloadTime', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'previousDownloadTime', - type: 'INTEGER' + type: 'INTEGER', }, { name: 'extra', - type: 'TEXT' - } - ] - } - ] + type: 'TEXT', + }, + ], + }, + ], }; protected logger: CoreLogger; protected appDB: SQLiteDB; - protected dbReady: Promise; // Promise resolved when the app DB is initialized. - protected tokenRegex = new RegExp('(\\?|&)token=([A-Za-z0-9]*)'); + protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected queueState: string; protected urlAttributes = [ - this.tokenRegex, + new RegExp('(\\?|&)token=([A-Za-z0-9]*)'), new RegExp('(\\?|&)forcedownload=[0-1]'), new RegExp('(\\?|&)preview=[A-Za-z0-9]+'), - new RegExp('(\\?|&)offline=[0-1]', 'g') + new RegExp('(\\?|&)offline=[0-1]', 'g'), ]; - protected queueDeferreds = {}; // To handle file downloads using the queue. + + // 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. // Variables to prevent downloading packages/files twice at the same time. - protected packagesPromises = {}; - protected filePromises: { [s: string]: { [s: string]: Promise } } = {}; + protected packagesPromises: { [s: string]: { [s: string]: Promise } } = {}; + protected filePromises: { [s: string]: { [s: string]: Promise } } = {}; constructor() { this.logger = CoreLogger.getInstance('CoreFilepoolProvider'); @@ -282,22 +286,21 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved on success. */ - protected addFileLink(siteId: string, fileId: string, component: string, componentId?: string | number): Promise { + protected async addFileLink(siteId: string, fileId: string, component: string, componentId?: string | number): Promise { if (!component) { - return Promise.reject(null); + throw null; } componentId = this.fixComponentId(componentId); - return CoreSites.instance.getSiteDb(siteId).then((db) => { - const newEntry = { - fileId, - component, - componentId: componentId || '' - }; + const db = await CoreSites.instance.getSiteDb(siteId); + const newEntry: CoreFilepoolLinksRecord = { + fileId, + component, + componentId: componentId || '', + }; - return db.insertRecord(this.LINKS_TABLE, newEntry); - }); + await db.insertRecord(CoreFilepoolProvider.LINKS_TABLE, newEntry); } /** @@ -312,12 +315,11 @@ export class CoreFilepoolProvider { * Use this method to create a link between a URL and a component. You usually do not need to call this manually since * downloading a file automatically does this. Note that this method does not check if the file exists in the pool. */ - addFileLinkByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number): Promise { - return this.fixPluginfileURL(siteId, fileUrl).then((file) => { - const fileId = this.getFileIdByUrl(file.fileurl); + async addFileLinkByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number): Promise { + const file = await this.fixPluginfileURL(siteId, fileUrl); + const fileId = this.getFileIdByUrl(file.fileurl); - return this.addFileLink(siteId, fileId, component, componentId); - }); + await this.addFileLink(siteId, fileId, component, componentId); } /** @@ -328,13 +330,10 @@ export class CoreFilepoolProvider { * @param links Array of objects containing the component and optionally componentId. * @return Promise resolved on success. */ - protected addFileLinks(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): Promise { - const promises = []; - links.forEach((link) => { - promises.push(this.addFileLink(siteId, fileId, link.component, link.componentId)); - }); + protected async addFileLinks(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): Promise { + const promises = links.map((link) => this.addFileLink(siteId, fileId, link.component, link.componentId)); - return Promise.all(promises); + await Promise.all(promises); } /** @@ -346,7 +345,7 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component (optional). * @return Resolved on success. */ - addFilesToQueue(siteId: string, files: any[], component?: string, componentId?: string | number): Promise { + addFilesToQueue(siteId: string, files: CoreWSExternalFile[], component?: string, componentId?: string | number): Promise { return this.downloadOrPrefetchFiles(siteId, files, true, false, component, componentId); } @@ -358,13 +357,13 @@ export class CoreFilepoolProvider { * @param data Additional information to store about the file (timemodified, url, ...). See FILES_TABLE schema. * @return Promise resolved on success. */ - protected addFileToPool(siteId: string, fileId: string, data: any): Promise { - const values = Object.assign({}, data); - values.fileId = fileId; + protected async addFileToPool(siteId: string, fileId: string, data: CoreFilepoolFileEntry): Promise { + const record = Object.assign({}, data); + record.fileId = fileId; - return CoreSites.instance.getSiteDb(siteId).then((db) => { - return db.insertRecord(this.FILES_TABLE, values); - }); + const db = await CoreSites.instance.getSiteDb(siteId); + + await db.insertRecord(CoreFilepoolProvider.FILES_TABLE, record); } /** @@ -403,19 +402,19 @@ export class CoreFilepoolProvider { * @param revision The revision of the file. * @param timemodified The time this file was modified. Can be used to check file state. * @param filePath Filepath to download the file to. If not defined, download to the filepool folder. + * @param onProgress Function to call on progress. * @param options Extra options (isexternalfile, repositorytype). * @param link The link to add for the file. * @return Promise resolved when the file is downloaded. */ protected async addToQueue(siteId: string, fileId: string, url: string, priority: number, revision: number, - timemodified: number, filePath: string, onProgress?: (event: any) => any, options: any = {}, - link?: CoreFilepoolComponentLink): Promise { - + timemodified: number, filePath: string, onProgress?: CoreFilepoolOnProgressCallback, + options: CoreFilepoolFileOptions = {}, link?: CoreFilepoolComponentLink): Promise { await this.dbReady; this.logger.debug(`Adding ${fileId} to the queue`); - await this.appDB.insertRecord(this.QUEUE_TABLE, { + await this.appDB.insertRecord(CoreFilepoolProvider.QUEUE_TABLE, { siteId, fileId, url, @@ -426,7 +425,7 @@ export class CoreFilepoolProvider { isexternalfile: options.isexternalfile ? 1 : 0, repositorytype: options.repositorytype, links: JSON.stringify(link ? [link] : []), - added: Date.now() + added: Date.now(), }); // Check if the queue is running. @@ -453,115 +452,105 @@ export class CoreFilepoolProvider { * @return Resolved on success. */ async addToQueueByUrl(siteId: string, fileUrl: string, component?: string, componentId?: string | number, - timemodified: number = 0, filePath?: string, onProgress?: (event: any) => any, priority: number = 0, options: any = {}, - revision?: number, alreadyFixed?: boolean): Promise { + timemodified: number = 0, filePath?: string, onProgress?: CoreFilepoolOnProgressCallback, priority: number = 0, + options: CoreFilepoolFileOptions = {}, revision?: number, alreadyFixed?: boolean): Promise { await this.dbReady; - let fileId; - let queueDeferred; - if (!CoreFile.instance.isAvailable()) { - return Promise.reject(null); + throw null; } - return CoreSites.instance.getSite(siteId).then((site) => { - if (!site.canDownloadFiles()) { - return Promise.reject(null); - } + const site = await CoreSites.instance.getSite(siteId); + if (!site.canDownloadFiles()) { + throw null; + } - if (alreadyFixed) { - // Already fixed, if we reached here it means it can be downloaded. - return {fileurl: fileUrl}; - } else { - return this.fixPluginfileURL(siteId, fileUrl); - } - }).then((file) => { + let file: CoreWSExternalFile; + if (alreadyFixed) { + // Already fixed, if we reached here it means it can be downloaded. + file = { fileurl: fileUrl }; + } else { + file = await this.fixPluginfileURL(siteId, fileUrl); + } - fileUrl = file.fileurl; - timemodified = file.timemodified || timemodified; - revision = revision || this.getRevisionFromUrl(fileUrl); - fileId = this.getFileIdByUrl(fileUrl); + fileUrl = file.fileurl; + timemodified = file.timemodified || timemodified; + revision = revision || this.getRevisionFromUrl(fileUrl); + const fileId = this.getFileIdByUrl(fileUrl); - const primaryKey = { siteId, fileId }; + const primaryKey = { siteId, fileId }; - // Set up the component. - const link = this.createComponentLink(component, componentId); + // Set up the component. + const link = this.createComponentLink(component, componentId); - // Retrieve the queue deferred now if it exists. - // This is to prevent errors if file is removed from queue while we're checking if the file is in queue. - queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress); + // Retrieve the queue deferred now if it exists. + // This is to prevent errors if file is removed from queue while we're checking if the file is in queue. + const queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress); - return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => { - const newData: any = {}; - let foundLink = false; + return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => { + const newData: CoreFilepoolQueueEntry = {}; + let foundLink = false; - if (entry) { - // We already have the file in queue, we update the priority and links. - if (entry.priority < priority) { - newData.priority = priority; - } - if (revision && entry.revision !== revision) { - newData.revision = revision; - } - if (timemodified && entry.timemodified !== timemodified) { - newData.timemodified = timemodified; - } - if (filePath && entry.path !== filePath) { - newData.path = filePath; - } - if (entry.isexternalfile !== options.isexternalfile && (entry.isexternalfile || options.isexternalfile)) { - newData.isexternalfile = options.isexternalfile; - } - if (entry.repositorytype !== options.repositorytype && (entry.repositorytype || options.repositorytype)) { - newData.repositorytype = options.repositorytype; - } - - if (link) { - // We need to add the new link if it does not exist yet. - if (entry.links && entry.links.length) { - for (const i in entry.links) { - const fileLink = entry.links[i]; - if (fileLink.component == link.component && fileLink.componentId == link.componentId) { - foundLink = true; - break; - } - } - } - - if (!foundLink) { - newData.links = entry.links || []; - newData.links.push(link); - newData.links = JSON.stringify(entry.links); - } - } - - if (Object.keys(newData).length) { - // Update only when required. - this.logger.debug(`Updating file ${fileId} which is already in queue`); - - return this.appDB.updateRecords(this.QUEUE_TABLE, newData, primaryKey).then(() => { - return this.getQueuePromise(siteId, fileId, true, onProgress); - }); - } - - this.logger.debug(`File ${fileId} already in queue and does not require update`); - if (queueDeferred) { - // If we were able to retrieve the queue deferred before, we use that one. - return queueDeferred.promise; - } else { - // Create a new deferred and return its promise. - return this.getQueuePromise(siteId, fileId, true, onProgress); - } - } else { - return this.addToQueue( - siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link); + if (entry) { + // We already have the file in queue, we update the priority and links. + if (entry.priority < priority) { + newData.priority = priority; } - }, () => { - // Unsure why we could not get the record, let's add to the queue anyway. + if (revision && entry.revision !== revision) { + newData.revision = revision; + } + if (timemodified && entry.timemodified !== timemodified) { + newData.timemodified = timemodified; + } + if (filePath && entry.path !== filePath) { + newData.path = filePath; + } + if (entry.isexternalfile !== options.isexternalfile && (entry.isexternalfile || options.isexternalfile)) { + newData.isexternalfile = options.isexternalfile; + } + if (entry.repositorytype !== options.repositorytype && (entry.repositorytype || options.repositorytype)) { + newData.repositorytype = options.repositorytype; + } + + if (link) { + // We need to add the new link if it does not exist yet. + if (entry.linksUnserialized && entry.linksUnserialized.length) { + foundLink = entry.linksUnserialized.some((fileLink) => + fileLink.component == link.component && fileLink.componentId == link.componentId); + } + + if (!foundLink) { + const links = entry.linksUnserialized || []; + links.push(link); + newData.links = JSON.stringify(links); + } + } + + if (Object.keys(newData).length) { + // Update only when required. + this.logger.debug(`Updating file ${fileId} which is already in queue`); + + return this.appDB.updateRecords(CoreFilepoolProvider.QUEUE_TABLE, newData, primaryKey).then(() => + this.getQueuePromise(siteId, fileId, true, onProgress)); + } + + this.logger.debug(`File ${fileId} already in queue and does not require update`); + if (queueDeferred) { + // If we were able to retrieve the queue deferred before, we use that one. + return queueDeferred.promise; + } else { + // Create a new deferred and return its promise. + return this.getQueuePromise(siteId, fileId, true, onProgress); + } + } else { return this.addToQueue( siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link); - }); - }); + } + }, () => + // Unsure why we could not get the record, let's add to the queue anyway. + this.addToQueue( + siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link), + ); } /** @@ -573,54 +562,52 @@ 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 downloadUnknown True to download file in WiFi if their size is unknown, false otherwise. + * @param downloadAny True to download file in WiFi if their size is any, 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 addToQueueIfNeeded(siteId: string, fileUrl: string, component: string, componentId?: string | number, - timemodified: number = 0, checkSize: boolean = true, downloadUnknown?: boolean, options: any = {}, revision?: number) - : Promise { - let promise; + protected async addToQueueIfNeeded(siteId: string, fileUrl: string, component: string, componentId?: string | number, + timemodified: number = 0, checkSize: boolean = true, downloadAny?: boolean, options: CoreFilepoolFileOptions = {}, + revision?: number): Promise { + if (!checkSize) { + // No need to check size, just add it to the queue. + await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, + revision, true); + } - if (checkSize) { - if (typeof this.sizeCache[fileUrl] != 'undefined') { - promise = Promise.resolve(this.sizeCache[fileUrl]); - } else { - if (!CoreApp.instance.isOnline()) { - // Cannot check size in offline, stop. - return Promise.reject(null); - } + let size: number; - promise = CoreWS.instance.getRemoteFileSize(fileUrl); + if (typeof this.sizeCache[fileUrl] != 'undefined') { + size = this.sizeCache[fileUrl]; + } else { + if (!CoreApp.instance.isOnline()) { + // Cannot check size in offline, stop. + throw null; } - // Calculate the size of the file. - return promise.then((size) => { - const isWifi = CoreApp.instance.isWifi(); - const sizeUnknown = size <= 0; + size = await CoreWS.instance.getRemoteFileSize(fileUrl); + } - if (!sizeUnknown) { - // Store the size in the cache. - this.sizeCache[fileUrl] = size; - } + // Calculate the size of the file. + const isWifi = CoreApp.instance.isWifi(); + const sizeAny = size <= 0; - // Check if the file should be downloaded. - if (sizeUnknown) { - if (downloadUnknown && isWifi) { - return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, - 0, options, revision, true); - } - } else if (this.shouldDownload(size)) { - return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, - options, revision, true); - } - }); - } else { - // No need to check size, just add it to the queue. - return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, - revision, true); + if (!sizeAny) { + // Store the size in the cache. + this.sizeCache[fileUrl] = size; + } + + // Check if the file should be downloaded. + if (sizeAny) { + if (downloadAny && isWifi) { + await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, + 0, options, revision, true); + } + } else if (this.shouldDownload(size)) { + await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, + options, revision, true); } } @@ -634,14 +621,14 @@ export class CoreFilepoolProvider { */ protected checkQueueProcessing(): void { if (!CoreFile.instance.isAvailable() || !CoreApp.instance.isOnline()) { - this.queueState = this.QUEUE_PAUSED; + this.queueState = CoreFilepoolProvider.QUEUE_PAUSED; return; - } else if (this.queueState === this.QUEUE_RUNNING) { + } else if (this.queueState === CoreFilepoolProvider.QUEUE_RUNNING) { return; } - this.queueState = this.QUEUE_RUNNING; + this.queueState = CoreFilepoolProvider.QUEUE_RUNNING; this.processQueue(); } @@ -651,20 +638,18 @@ export class CoreFilepoolProvider { * @param siteId Site ID. * @return Promise resolved when all status are cleared. */ - clearAllPackagesStatus(siteId: string): Promise { + async clearAllPackagesStatus(siteId: string): Promise { this.logger.debug('Clear all packages status for site ' + siteId); - return CoreSites.instance.getSite(siteId).then((site) => { - // Get all the packages to be able to "notify" the change in the status. - return site.getDb().getAllRecords(this.PACKAGES_TABLE).then((entries) => { - // Delete all the entries. - return site.getDb().deleteRecords(this.PACKAGES_TABLE).then(() => { - entries.forEach((entry) => { - // Trigger module status changed, setting it as not downloaded. - this.triggerPackageStatusChanged(siteId, CoreConstants.NOT_DOWNLOADED, entry.component, entry.componentId); - }); - }); - }); + const site = await CoreSites.instance.getSite(siteId); + // Get all the packages to be able to "notify" the change in the status. + const entries: CoreFilepoolPackageEntry[] = await site.getDb().getAllRecords(CoreFilepoolProvider.PACKAGES_TABLE); + // Delete all the entries. + await site.getDb().deleteRecords(CoreFilepoolProvider.PACKAGES_TABLE); + + entries.forEach((entry) => { + // Trigger module status changed, setting it as not downloaded. + this.triggerPackageStatusChanged(siteId, CoreConstants.NOT_DOWNLOADED, entry.component, entry.componentId); }); } @@ -674,13 +659,13 @@ export class CoreFilepoolProvider { * @param siteId ID of the site to clear. * @return Promise resolved when the filepool is cleared. */ - clearFilepool(siteId: string): Promise { - return CoreSites.instance.getSiteDb(siteId).then((db) => { - return Promise.all([ - db.deleteRecords(this.FILES_TABLE), - db.deleteRecords(this.LINKS_TABLE) - ]); - }); + async clearFilepool(siteId: string): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + + await Promise.all([ + db.deleteRecords(CoreFilepoolProvider.FILES_TABLE), + db.deleteRecords(CoreFilepoolProvider.LINKS_TABLE), + ]); } /** @@ -691,19 +676,17 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Resolved means yes, rejected means no. */ - componentHasFiles(siteId: string, component: string, componentId?: string | number): Promise { - return CoreSites.instance.getSiteDb(siteId).then((db) => { - const conditions = { - component, - componentId: this.fixComponentId(componentId) - }; + async componentHasFiles(siteId: string, component: string, componentId?: string | number): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + const conditions = { + component, + componentId: this.fixComponentId(componentId), + }; - return db.countRecords(this.LINKS_TABLE, conditions).then((count) => { - if (count <= 0) { - return Promise.reject(null); - } - }); - }); + const count = await db.countRecords(CoreFilepoolProvider.LINKS_TABLE, conditions); + if (count <= 0) { + return null; + } } /** @@ -786,65 +769,57 @@ export class CoreFilepoolProvider { * @param poolFileObject When set, the object will be updated, a new entry will not be created. * @return Resolved with internal URL on success, rejected otherwise. */ - protected downloadForPoolByUrl(siteId: string, fileUrl: string, options: any = {}, filePath?: string, - onProgress?: (event: any) => any, poolFileObject?: CoreFilepoolFileEntry): Promise { - + protected async downloadForPoolByUrl(siteId: string, fileUrl: string, options: CoreFilepoolFileOptions = {}, filePath?: string, + onProgress?: CoreFilepoolOnProgressCallback, poolFileObject?: CoreFilepoolFileEntry): Promise { const fileId = this.getFileIdByUrl(fileUrl); const extension = CoreMimetypeUtils.instance.guessExtensionFromUrl(fileUrl); const addExtension = typeof filePath == 'undefined'; - const pathPromise = filePath ? filePath : this.getFilePath(siteId, fileId, extension); + filePath = filePath || (await this.getFilePath(siteId, fileId, extension)); - return Promise.resolve(pathPromise).then((filePath) => { - if (poolFileObject && poolFileObject.fileId !== fileId) { - this.logger.error('Invalid object to update passed'); + if (poolFileObject && poolFileObject.fileId !== fileId) { + this.logger.error('Invalid object to update passed'); + throw null; + } + + const downloadId = this.getFileDownloadId(fileUrl, filePath); + + if (this.filePromises[siteId] && this.filePromises[siteId][downloadId]) { + // There's already a download ongoing for this file in this location, return the promise. + return this.filePromises[siteId][downloadId]; + } else if (!this.filePromises[siteId]) { + this.filePromises[siteId] = {}; + } + + this.filePromises[siteId][downloadId] = CoreSites.instance.getSite(siteId).then(async (site) => { + if (!site.canDownloadFiles()) { return Promise.reject(null); } - const downloadId = this.getFileDownloadId(fileUrl, filePath); + const entry = await CoreWS.instance.downloadFile(fileUrl, filePath, addExtension, onProgress); + const fileEntry = entry; + await CorePluginFile.instance.treatDownloadedFile(fileUrl, fileEntry, siteId, onProgress); - if (this.filePromises[siteId] && this.filePromises[siteId][downloadId]) { - // There's already a download ongoing for this file in this location, return the promise. - return this.filePromises[siteId][downloadId]; - } else if (!this.filePromises[siteId]) { - this.filePromises[siteId] = {}; - } + const data: CoreFilepoolFileEntry = poolFileObject || {}; + data.downloadTime = Date.now(); + data.stale = 0; + data.url = fileUrl; + data.revision = options.revision; + data.timemodified = options.timemodified; + data.isexternalfile = options.isexternalfile ? 1 : 0; + data.repositorytype = options.repositorytype; + data.path = fileEntry.path; + data.extension = fileEntry.extension; - this.filePromises[siteId][downloadId] = CoreSites.instance.getSite(siteId).then((site) => { - if (!site.canDownloadFiles()) { - return Promise.reject(null); - } + await this.addFileToPool(siteId, fileId, data); - let fileEntry; - - return CoreWS.instance.downloadFile(fileUrl, filePath, addExtension, onProgress).then((entry) => { - fileEntry = entry; - - return CorePluginFile.instance.treatDownloadedFile(fileUrl, fileEntry, siteId, onProgress); - }).then(() => { - const data: CoreFilepoolFileEntry = poolFileObject || {}; - - data.downloadTime = Date.now(); - data.stale = 0; - data.url = fileUrl; - data.revision = options.revision; - data.timemodified = options.timemodified; - data.isexternalfile = options.isexternalfile ? 1 : 0; - data.repositorytype = options.repositorytype; - data.path = fileEntry.path; - data.extension = fileEntry.extension; - - return this.addFileToPool(siteId, fileId, data).then(() => { - return fileEntry.toURL(); - }); - }); - }).finally(() => { - // Download finished, delete the promise. - delete this.filePromises[siteId][downloadId]; - }); - - return this.filePromises[siteId][downloadId]; + return fileEntry.toURL(); + }).finally(() => { + // Download finished, delete the promise. + delete this.filePromises[siteId][downloadId]; }); + + return this.filePromises[siteId][downloadId]; } /** @@ -860,19 +835,19 @@ export class CoreFilepoolProvider { * the files directly inside the filepool folder. * @return Resolved on success. */ - downloadOrPrefetchFiles(siteId: string, files: any[], prefetch: boolean, ignoreStale?: boolean, component?: string, - componentId?: string | number, dirPath?: string): Promise { + downloadOrPrefetchFiles(siteId: string, files: CoreWSExternalFile[], prefetch: boolean, ignoreStale?: boolean, + component?: string, componentId?: string | number, dirPath?: string): Promise { const promises = []; // Download files. files.forEach((file) => { - const url = file.url || file.fileurl; + const url = file.fileurl; const timemodified = file.timemodified; const options = { isexternalfile: file.isexternalfile, repositorytype: file.repositorytype, }; - let path; + let path: string; if (dirPath) { // Calculate the path to the file. @@ -888,7 +863,7 @@ export class CoreFilepoolProvider { siteId, url, component, componentId, timemodified, path, undefined, 0, options)); } else { promises.push(this.downloadUrl( - siteId, url, ignoreStale, component, componentId, timemodified, path, undefined, options)); + siteId, url, ignoreStale, component, componentId, timemodified, undefined, path, options)); } }); @@ -909,11 +884,10 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Promise resolved when the package is downloaded. */ - protected downloadOrPrefetchPackage(siteId: string, fileList: any[], prefetch?: boolean, component?: string, - componentId?: string | number, extra?: string, dirPath?: string, onProgress?: (event: any) => any): Promise { - + protected downloadOrPrefetchPackage(siteId: string, fileList: CoreWSExternalFile[], prefetch?: boolean, component?: string, + componentId?: string | number, extra?: string, dirPath?: string, onProgress?: CoreFilepoolOnProgressCallback): + Promise { const packageId = this.getPackageId(component, componentId); - let promise; if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId]) { // There's already a download ongoing for this package, return the promise. @@ -923,24 +897,24 @@ export class CoreFilepoolProvider { } // Set package as downloading. - promise = this.storePackageStatus(siteId, CoreConstants.DOWNLOADING, component, componentId).then(() => { + const promise = this.storePackageStatus(siteId, CoreConstants.DOWNLOADING, component, componentId).then(async () => { const promises = []; let packageLoaded = 0; fileList.forEach((file) => { - const fileUrl = file.url || file.fileurl; + const fileUrl = file.fileurl; const options = { isexternalfile: file.isexternalfile, repositorytype: file.repositorytype, }; - let path; - let promise; + let path: string; + let promise: Promise; let fileLoaded = 0; - let onFileProgress; + let onFileProgress: (progress: ProgressEvent) => void; if (onProgress) { // There's a onProgress event, create a function to receive file download progress events. - onFileProgress = (progress: any): void => { + onFileProgress = (progress: ProgressEvent): void => { if (progress && progress.loaded) { // Add the new size loaded to the package loaded. packageLoaded = packageLoaded + (progress.loaded - fileLoaded); @@ -948,7 +922,7 @@ export class CoreFilepoolProvider { onProgress({ packageDownload: true, loaded: packageLoaded, - fileProgress: progress + fileProgress: progress, }); } }; @@ -975,16 +949,16 @@ export class CoreFilepoolProvider { promises.push(promise); }); - return Promise.all(promises).then(() => { + try { + await Promise.all(promises); // Success prefetching, store package as downloaded. - return this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra); - }).catch((error) => { + this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra); + } catch (error) { // Error downloading, go back to previous status and reject the promise. - return this.setPackagePreviousStatus(siteId, component, componentId).then(() => { - return Promise.reject(error); - }); - }); + await this.setPackagePreviousStatus(siteId, component, componentId); + throw error; + } }).finally(() => { // Download finished, delete the promise. delete this.packagesPromises[siteId][packageId]; @@ -1008,8 +982,8 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Promise resolved when all files are downloaded. */ - downloadPackage(siteId: string, fileList: any[], component: string, componentId?: string | number, extra?: string, - dirPath?: string, onProgress?: (event: any) => any): Promise { + downloadPackage(siteId: string, fileList: CoreWSExternalFile[], component: string, componentId?: string | number, + extra?: string, dirPath?: string, onProgress?: CoreFilepoolOnProgressCallback): Promise { return this.downloadOrPrefetchPackage(siteId, fileList, false, component, componentId, extra, dirPath, onProgress); } @@ -1033,88 +1007,80 @@ export class CoreFilepoolProvider { * not force a file to be re-downloaded if it is already part of the pool. You should mark a file as stale using * invalidateFileByUrl to trigger a download. */ - downloadUrl(siteId: string, fileUrl: string, ignoreStale?: boolean, component?: string, componentId?: string | number, - timemodified: number = 0, onProgress?: (event: any) => any, filePath?: string, options: any = {}, revision?: number) - : Promise { - let fileId; - let promise; + async downloadUrl(siteId: string, fileUrl: string, ignoreStale?: boolean, component?: string, componentId?: string | number, + timemodified: number = 0, onProgress?: CoreFilepoolOnProgressCallback, filePath?: string, + options: CoreFilepoolFileOptions = {}, revision?: number): Promise { + let promise: Promise; let alreadyDownloaded = true; - if (CoreFile.instance.isAvailable()) { - return this.fixPluginfileURL(siteId, fileUrl).then((file) => { - - fileUrl = file.fileurl; - timemodified = file.timemodified || timemodified; - - options = Object.assign({}, options); // Create a copy to prevent modifying the original object. - options.timemodified = timemodified || 0; - options.revision = revision || this.getRevisionFromUrl(fileUrl); - fileId = this.getFileIdByUrl(fileUrl); - - const links = this.createComponentLinks(component, componentId); - - return this.hasFileInPool(siteId, fileId).then((fileObject) => { - - if (typeof fileObject === 'undefined') { - // We do not have the file, download and add to pool. - this.notifyFileDownloading(siteId, fileId, links); - alreadyDownloaded = false; - - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress); - - } else if (this.isFileOutdated(fileObject, options.revision, options.timemodified) && - CoreApp.instance.isOnline() && !ignoreStale) { - // The file is outdated, force the download and update it. - this.notifyFileDownloading(siteId, fileId, links); - alreadyDownloaded = false; - - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, fileObject); - } - - // Everything is fine, return the file on disk. - if (filePath) { - promise = this.getInternalUrlByPath(filePath); - } else { - promise = this.getInternalUrlById(siteId, fileId); - } - - return promise.then((response) => { - return response; - }, () => { - // The file was not found in the pool, weird. - this.notifyFileDownloading(siteId, fileId, links); - alreadyDownloaded = false; - - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, fileObject); - }); - - }, () => { - // The file is not in the pool just yet. - this.notifyFileDownloading(siteId, fileId, links); - alreadyDownloaded = false; - - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress); - }).then((response) => { - if (typeof component != 'undefined') { - this.addFileLink(siteId, fileId, component, componentId).catch(() => { - // Ignore errors. - }); - } - - if (!alreadyDownloaded) { - this.notifyFileDownloaded(siteId, fileId, links); - } - - return response; - }, (err) => { - this.notifyFileDownloadError(siteId, fileId, links); - - return Promise.reject(err); - }); - }); - } else { - return Promise.reject(null); + if (!CoreFile.instance.isAvailable()) { + throw null; } + + const file = await this.fixPluginfileURL(siteId, fileUrl); + fileUrl = file.fileurl; + timemodified = file.timemodified || timemodified; + + options = Object.assign({}, options); // Create a copy to prevent modifying the original object. + options.timemodified = timemodified || 0; + options.revision = revision || this.getRevisionFromUrl(fileUrl); + const fileId = this.getFileIdByUrl(fileUrl); + + const links = this.createComponentLinks(component, componentId); + + return this.hasFileInPool(siteId, fileId).then((fileObject) => { + if (typeof fileObject === 'undefined') { + // We do not have the file, download and add to pool. + this.notifyFileDownloading(siteId, fileId, links); + alreadyDownloaded = false; + + return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress); + } else if (this.isFileOutdated(fileObject, options.revision, options.timemodified) && + CoreApp.instance.isOnline() && !ignoreStale) { + // The file is outdated, force the download and update it. + this.notifyFileDownloading(siteId, fileId, links); + alreadyDownloaded = false; + + return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, fileObject); + } + + // Everything is fine, return the file on disk. + if (filePath) { + promise = this.getInternalUrlByPath(filePath); + } else { + promise = this.getInternalUrlById(siteId, fileId); + } + + return promise.then((url) => url, () => { + // The file was not found in the pool, weird. + this.notifyFileDownloading(siteId, fileId, links); + alreadyDownloaded = false; + + return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, fileObject); + }); + }, () => { + // The file is not in the pool just yet. + this.notifyFileDownloading(siteId, fileId, links); + alreadyDownloaded = false; + + return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress); + }).then((url) => { + if (typeof component != 'undefined') { + this.addFileLink(siteId, fileId, component, componentId).catch(() => { + // Ignore errors. + }); + } + + if (!alreadyDownloaded) { + this.notifyFileDownloaded(siteId, fileId, links); + } + + return url; + }, (err) => { + this.notifyFileDownloadError(siteId, fileId, links); + + return Promise.reject(err); + }); } /** @@ -1125,14 +1091,16 @@ export class CoreFilepoolProvider { */ extractDownloadableFilesFromHtml(html: string): string[] { let urls = []; - let elements; const element = CoreDomUtils.instance.convertToElement(html); - elements = element.querySelectorAll('a, img, audio, video, source, track'); + const elements = 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 = element.tagName === 'A' + ? (element as HTMLAnchorElement).href + : (element as HTMLImageElement | HTMLVideoElement | HTMLAudioElement | + HTMLAudioElement | HTMLTrackElement | HTMLSourceElement).src; if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) { urls.push(url); @@ -1163,11 +1131,9 @@ export class CoreFilepoolProvider { const urls = this.extractDownloadableFilesFromHtml(html); // Convert them to fake file objects. - return urls.map((url) => { - return { - fileurl: url - }; - }); + return urls.map((url) => ({ + fileurl: url, + })); } /** @@ -1178,39 +1144,39 @@ export class CoreFilepoolProvider { * @param siteId SiteID to get migrated. * @return Promise resolved when done. */ - protected fillExtensionInFile(entry: CoreFilepoolFileEntry, siteId: string): Promise { + protected async fillExtensionInFile(entry: CoreFilepoolFileEntry, siteId: string): Promise { if (typeof entry.extension != 'undefined') { // Already filled. - return Promise.resolve(); + return; } - return CoreSites.instance.getSiteDb(siteId).then((db) => { - const extension = CoreMimetypeUtils.instance.getFileExtension(entry.path); - if (!extension) { - // Files does not have extension. Invalidate file (stale = true). - // Minor problem: file will remain in the filesystem once downloaded again. - this.logger.debug('Staled file with no extension ' + entry.fileId); + const db = await CoreSites.instance.getSiteDb(siteId); + const extension = CoreMimetypeUtils.instance.getFileExtension(entry.path); + if (!extension) { + // Files does not have extension. Invalidate file (stale = true). + // Minor problem: file will remain in the filesystem once downloaded again. + this.logger.debug('Staled file with no extension ' + entry.fileId); - return db.updateRecords(this.FILES_TABLE, { stale: 1 }, { fileId: entry.fileId }); - } + await db.updateRecords(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, { fileId: entry.fileId }); - // File has extension. Save extension, and add extension to path. - const fileId = entry.fileId; - entry.fileId = CoreMimetypeUtils.instance.removeExtension(fileId); - entry.extension = extension; + return; + } - return db.updateRecords(this.FILES_TABLE, entry, { fileId }).then(() => { - if (entry.fileId == fileId) { - // File ID hasn't changed, we're done. - this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId); + // File has extension. Save extension, and add extension to path. + const fileId = entry.fileId; + entry.fileId = CoreMimetypeUtils.instance.removeExtension(fileId); + entry.extension = extension; - return; - } + await db.updateRecords(CoreFilepoolProvider.FILES_TABLE, entry, { fileId }); + if (entry.fileId == fileId) { + // File ID hasn't changed, we're done. + this.logger.debug('Removed extesion ' + extension + ' from file ' + entry.fileId); - // Now update the links. - return db.updateRecords(this.LINKS_TABLE, { fileId: entry.fileId }, { fileId }); - }); - }); + return; + } + + // Now update the links. + await db.updateRecords(CoreFilepoolProvider.LINKS_TABLE, { fileId: entry.fileId }, { fileId }); } /** @@ -1246,18 +1212,13 @@ export class CoreFilepoolProvider { * @param timemodified The timemodified of the file. * @return Promise resolved with the file data to use. */ - protected fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise { + protected async fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise { + const file = await CorePluginFile.instance.getDownloadableFile({ fileurl: fileUrl, timemodified }); + const site = await CoreSites.instance.getSite(siteId); - return CorePluginFile.instance.getDownloadableFile({fileurl: fileUrl, timemodified}).then((file) => { + file.fileurl = await site.checkAndFixPluginfileURL(file.fileurl); - return CoreSites.instance.getSite(siteId).then((site) => { - return site.checkAndFixPluginfileURL(file.fileurl); - }).then((fixedUrl) => { - file.fileurl = fixedUrl; - - return file; - }); - }); + return file; } /** @@ -1268,19 +1229,19 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved with the files. */ - protected getComponentFiles(db: SQLiteDB, component: string, componentId?: string | number): Promise { + protected async getComponentFiles(db: SQLiteDB, component: string, componentId?: string | number): + Promise { const conditions = { component, componentId: this.fixComponentId(componentId), }; - return db.getRecords(this.LINKS_TABLE, conditions).then((items) => { - items.forEach((item) => { - item.componentId = this.fixComponentId(item.componentId); - }); - - return items; + const items = await db.getRecords(CoreFilepoolProvider.LINKS_TABLE, conditions); + items.forEach((item) => { + item.componentId = this.fixComponentId(item.componentId); }); + + return items; } /** @@ -1290,19 +1251,17 @@ export class CoreFilepoolProvider { * @param fileUrl The file URL. * @return Resolved with the URL. Rejected otherwise. */ - getDirectoryUrlByUrl(siteId: string, fileUrl: string): Promise { + async getDirectoryUrlByUrl(siteId: string, fileUrl: string): Promise { if (CoreFile.instance.isAvailable()) { - return this.fixPluginfileURL(siteId, fileUrl).then((file) => { - const fileId = this.getFileIdByUrl(file.fileurl); - const filePath = this.getFilePath(siteId, fileId, ''); // No extension, the function will return a string. + 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 CoreFile.instance.getDir(filePath).then((dirEntry) => { - return dirEntry.toURL(); - }); - }); + return dirEntry.toURL(); } - return Promise.reject(null); + throw null; } /** @@ -1356,7 +1315,7 @@ export class CoreFilepoolProvider { // If site supports it, since 3.8 we use tokenpluginfile instead of pluginfile. // For compatibility with files already downloaded, we need to use pluginfile to calculate the file ID. - url = url.replace(/\/tokenpluginfile\.php\/[^\/]+\//, '/webservice/pluginfile.php/'); + url = url.replace(/\/tokenpluginfile\.php\/[^/]+\//, '/webservice/pluginfile.php/'); // Remove the revision number from the URL so updates on the file aren't detected as a different file. url = this.removeRevisionFromUrl(url); @@ -1385,16 +1344,14 @@ export class CoreFilepoolProvider { * @param fileId The file ID. * @return Promise resolved with the links. */ - protected getFileLinks(siteId: string, fileId: string): Promise { - return CoreSites.instance.getSiteDb(siteId).then((db) => { - return db.getRecords(this.LINKS_TABLE, { fileId }); - }).then((items) => { - items.forEach((item) => { - item.componentId = this.fixComponentId(item.componentId); - }); - - return items; + protected async getFileLinks(siteId: string, fileId: string): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + const items = await db.getRecords(CoreFilepoolProvider.LINKS_TABLE, { fileId }); + items.forEach((item) => { + item.componentId = this.fixComponentId(item.componentId); }); + + return items; } /** @@ -1405,27 +1362,25 @@ export class CoreFilepoolProvider { * @param extension Previously calculated extension. Empty to not add any. Undefined to calculate it. * @return The path to the file relative to storage root. */ - protected getFilePath(siteId: string, fileId: string, extension?: string): string | Promise { + protected async getFilePath(siteId: string, fileId: string, extension?: string): Promise { let path = this.getFilepoolFolderPath(siteId) + '/' + fileId; + if (typeof extension == 'undefined') { // We need the extension to be able to open files properly. - return this.hasFileInPool(siteId, fileId).then((entry) => { + try { + const entry = await this.hasFileInPool(siteId, fileId); + if (entry.extension) { path += '.' + entry.extension; } - - return path; - }).catch(() => { + } catch (error) { // If file not found, use the path without extension. - return path; - }); - } else { - if (extension) { - path += '.' + extension; } - - return path; + } else if (extension) { + path += '.' + extension; } + + return path; } /** @@ -1435,12 +1390,11 @@ export class CoreFilepoolProvider { * @param fileUrl The file URL. * @return Promise resolved with the path to the file relative to storage root. */ - getFilePathByUrl(siteId: string, fileUrl: string): Promise { - return this.fixPluginfileURL(siteId, fileUrl).then((file) => { - const fileId = this.getFileIdByUrl(file.fileurl); + async getFilePathByUrl(siteId: string, fileUrl: string): Promise { + const file = await this.fixPluginfileURL(siteId, fileUrl); + const fileId = this.getFileIdByUrl(file.fileurl); - return this.getFilePath(siteId, fileId); - }); + return this.getFilePath(siteId, fileId); } /** @@ -1450,7 +1404,7 @@ export class CoreFilepoolProvider { * @return The root path to the filepool of the site. */ getFilepoolFolderPath(siteId: string): string { - return CoreFile.instance.getSiteFolder(siteId) + '/' + this.FOLDER; + return CoreFile.instance.getSiteFolder(siteId) + '/' + CoreFilepoolProvider.FOLDER; } /** @@ -1461,35 +1415,30 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved with the files on success. */ - getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise { - return CoreSites.instance.getSiteDb(siteId).then((db) => { - return this.getComponentFiles(db, component, componentId).then((items) => { - const promises = []; - const files = []; + async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + const items = await this.getComponentFiles(db, component, componentId); + const files = []; - items.forEach((item) => { - promises.push(db.getRecord(this.FILES_TABLE, { fileId: item.fileId }).then((fileEntry) => { - if (!fileEntry) { - return; - } + const promises = items.map((item) => + db.getRecord(CoreFilepoolProvider.FILES_TABLE, { fileId: item.fileId }).then((fileEntry) => { + if (!fileEntry) { + return; + } - files.push({ - url: fileEntry.url, - path: fileEntry.path, - extension: fileEntry.extension, - revision: fileEntry.revision, - timemodified: fileEntry.timemodified - }); - }).catch(() => { - // File not found, ignore error. - })); + files.push({ + url: fileEntry.url, + path: fileEntry.path, + extension: fileEntry.extension, + revision: fileEntry.revision, + timemodified: fileEntry.timemodified, }); + }).catch(() => { + // File not found, ignore error. + })); + await Promise.all(promises); - return Promise.all(promises).then(() => { - return files; - }); - }); - }); + return files; } /** @@ -1513,9 +1462,7 @@ export class CoreFilepoolProvider { })); }); - return Promise.all(promises).then(() => { - return size; - }); + return Promise.all(promises).then(() => size); }); } @@ -1529,9 +1476,9 @@ export class CoreFilepoolProvider { * @param revision File revision. If not defined, it will be calculated using the URL. * @return Promise resolved with the file state. */ - async getFileStateByUrl(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string, revision?: number) - : Promise { - let file; + async getFileStateByUrl(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string, revision?: number): + Promise { + let file: CoreWSExternalFile; try { file = await this.fixPluginfileURL(siteId, fileUrl, timemodified); @@ -1585,7 +1532,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 downloadUnknown True to download file in WiFi if their size is unknown, false otherwise. + * @param downloadAny True to download file in WiFi if their size is any, 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. @@ -1596,71 +1543,64 @@ export class CoreFilepoolProvider { * This handles the queue and validity of the file. If there is a local file and it's valid, return the local URL. * If the file isn't downloaded or it's outdated, return the online URL and add it to the queue to be downloaded later. */ - protected getFileUrlByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, - mode: string = 'url', timemodified: number = 0, checkSize: boolean = true, downloadUnknown?: boolean, - options: any = {}, revision?: number): Promise { + protected async getFileUrlByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, + mode: string = 'url', timemodified: number = 0, checkSize: boolean = true, downloadAny?: 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(() => { + // Ignore errors. + }); + }; - let fileId; - const addToQueue = (fileUrl): void => { - // Add the file to queue if needed and ignore errors. - this.addToQueueIfNeeded(siteId, fileUrl, component, componentId, timemodified, checkSize, - downloadUnknown, options, revision).catch(() => { - // Ignore errors. - }); - }; + const file = await this.fixPluginfileURL(siteId, fileUrl, timemodified); + fileUrl = file.fileurl; + timemodified = file.timemodified || timemodified; + revision = revision || this.getRevisionFromUrl(fileUrl); + const fileId = this.getFileIdByUrl(fileUrl); - return this.fixPluginfileURL(siteId, fileUrl, timemodified).then((file) => { - - fileUrl = file.fileurl; - timemodified = file.timemodified || timemodified; - revision = revision || this.getRevisionFromUrl(fileUrl); - fileId = this.getFileIdByUrl(fileUrl); - - return this.hasFileInPool(siteId, fileId).then((entry) => { - let response; - - if (typeof entry === 'undefined') { - // We do not have the file, add it to the queue, and return real URL. - addToQueue(fileUrl); - response = fileUrl; - - } else if (this.isFileOutdated(entry, revision, timemodified) && CoreApp.instance.isOnline()) { - // The file is outdated, we add to the queue and return real URL. - addToQueue(fileUrl); - response = fileUrl; - } else { - // We found the file entry, now look for the file on disk. - if (mode === 'src') { - response = this.getInternalSrcById(siteId, fileId); - } else { - response = this.getInternalUrlById(siteId, fileId); - } - - response = response.then((internalUrl) => { - // The file is on disk. - return internalUrl; - }).catch(() => { - // We could not retrieve the file, delete the entries associated with that ID. - this.logger.debug('File ' + fileId + ' not found on disk'); - this.removeFileById(siteId, fileId); - addToQueue(fileUrl); - - if (CoreApp.instance.isOnline()) { - // We still have a chance to serve the right content. - return fileUrl; - } - - return Promise.reject(null); - }); - } - - return response; - }, () => { - // We do not have the file in store yet. Add to queue and return the fixed URL. + return this.hasFileInPool(siteId, fileId).then(async (entry) => { + if (typeof entry === 'undefined') { + // We do not have the file, add it to the queue, and return real URL. addToQueue(fileUrl); return fileUrl; - }); + } + + if (this.isFileOutdated(entry, revision, timemodified) && CoreApp.instance.isOnline()) { + // The file is outdated, we add to the queue and return real URL. + addToQueue(fileUrl); + + return fileUrl; + } + + try { + // We found the file entry, now look for the file on disk. + if (mode === 'src') { + return this.getInternalSrcById(siteId, fileId); + } else { + return this.getInternalUrlById(siteId, fileId); + } + } catch (error) { + // The file is not on disk. + // We could not retrieve the file, delete the entries associated with that ID. + this.logger.debug('File ' + fileId + ' not found on disk'); + this.removeFileById(siteId, fileId); + addToQueue(fileUrl); + + if (CoreApp.instance.isOnline()) { + // We still have a chance to serve the right content. + return fileUrl; + } + + throw null; + } + }, () => { + // We do not have the file in store yet. Add to queue and return the fixed URL. + addToQueue(fileUrl); + + return fileUrl; }); } @@ -1673,16 +1613,15 @@ export class CoreFilepoolProvider { * @param fileId The file ID. * @return Resolved with the internal URL. Rejected otherwise. */ - protected getInternalSrcById(siteId: string, fileId: string): Promise { + protected async getInternalSrcById(siteId: string, fileId: string): Promise { if (CoreFile.instance.isAvailable()) { - return Promise.resolve(this.getFilePath(siteId, fileId)).then((path) => { - return CoreFile.instance.getFile(path).then((fileEntry) => { - return CoreFile.instance.convertFileSrc(fileEntry.toURL()); - }); - }); + const path = await this.getFilePath(siteId, fileId); + const fileEntry = await CoreFile.instance.getFile(path); + + return CoreFile.instance.convertFileSrc(fileEntry.toURL()); } - return Promise.reject(null); + throw null; } /** @@ -1692,21 +1631,20 @@ export class CoreFilepoolProvider { * @param fileId The file ID. * @return Resolved with the URL. Rejected otherwise. */ - protected getInternalUrlById(siteId: string, fileId: string): Promise { + protected async getInternalUrlById(siteId: string, fileId: string): Promise { if (CoreFile.instance.isAvailable()) { - return Promise.resolve(this.getFilePath(siteId, fileId)).then((path) => { - return CoreFile.instance.getFile(path).then((fileEntry) => { - // 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(); - } - }); - }); + 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(); + } } - return Promise.reject(null); + throw null; } /** @@ -1715,14 +1653,14 @@ export class CoreFilepoolProvider { * @param filePath The file path. * @return Resolved with the URL. */ - protected getInternalUrlByPath(filePath: string): Promise { + protected async getInternalUrlByPath(filePath: string): Promise { if (CoreFile.instance.isAvailable()) { - return CoreFile.instance.getFile(filePath).then((fileEntry) => { - return fileEntry.toURL(); - }); + const fileEntry = await CoreFile.instance.getFile(filePath); + + return fileEntry.toURL(); } - return Promise.reject(null); + throw null; } /** @@ -1732,16 +1670,15 @@ export class CoreFilepoolProvider { * @param fileUrl The file URL. * @return Resolved with the URL. Rejected otherwise. */ - getInternalUrlByUrl(siteId: string, fileUrl: string): Promise { + async getInternalUrlByUrl(siteId: string, fileUrl: string): Promise { if (CoreFile.instance.isAvailable()) { - return this.fixPluginfileURL(siteId, fileUrl).then((file) => { - const fileId = this.getFileIdByUrl(file.fileurl); + const file = await this.fixPluginfileURL(siteId, fileUrl); + const fileId = this.getFileIdByUrl(file.fileurl); - return this.getInternalUrlById(siteId, fileId); - }); + return this.getInternalUrlById(siteId, fileId); } - return Promise.reject(null); + throw null; } /** @@ -1752,14 +1689,13 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved with the data. */ - getPackageData(siteId: string, component: string, componentId?: string | number): Promise { + async getPackageData(siteId: string, component: string, componentId?: string | number): Promise { componentId = this.fixComponentId(componentId); - return CoreSites.instance.getSite(siteId).then((site) => { - const packageId = this.getPackageId(component, componentId); + const site = await CoreSites.instance.getSite(siteId); + const packageId = this.getPackageId(component, componentId); - return site.getDb().getRecord(this.PACKAGES_TABLE, { id: packageId }); - }); + return site.getDb().getRecord(CoreFilepoolProvider.PACKAGES_TABLE, { id: packageId }); } /** @@ -1811,19 +1747,17 @@ export class CoreFilepoolProvider { * @param url An URL to identify the package. * @return Resolved with the URL. */ - getPackageDirUrlByUrl(siteId: string, url: string): Promise { + async getPackageDirUrlByUrl(siteId: string, url: string): Promise { if (CoreFile.instance.isAvailable()) { - return this.fixPluginfileURL(siteId, url).then((file) => { - const dirName = this.getPackageDirNameByUrl(file.fileurl); - const dirPath = this.getFilePath(siteId, dirName, ''); // No extension, the function will return a string. + 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 CoreFile.instance.getDir(dirPath).then((dirEntry) => { - return dirEntry.toURL(); - }); - }); + return dirEntry.toURL(); } - return Promise.reject(null); + throw null; } /** @@ -1834,12 +1768,13 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Download promise or undefined. */ - getPackageDownloadPromise(siteId: string, component: string, componentId?: string | number): Promise { + getPackageDownloadPromise(siteId: string, component: string, componentId?: string | number): Promise { const packageId = this.getPackageId(component, componentId); if (this.packagesPromises[siteId] && this.packagesPromises[siteId][packageId]) { return this.packagesPromises[siteId][packageId]; } } + /** * Get a package extra data. * @@ -1849,9 +1784,7 @@ export class CoreFilepoolProvider { * @return Promise resolved with the extra data. */ getPackageExtra(siteId: string, component: string, componentId?: string | number): Promise { - return this.getPackageData(siteId, component, componentId).then((entry) => { - return entry.extra; - }); + return this.getPackageData(siteId, component, componentId).then((entry) => entry.extra); } /** @@ -1873,12 +1806,14 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved with the status. */ - getPackagePreviousStatus(siteId: string, component: string, componentId?: string | number): Promise { - return this.getPackageData(siteId, component, componentId).then((entry) => { + async getPackagePreviousStatus(siteId: string, component: string, componentId?: string | number): Promise { + try { + const entry = await this.getPackageData(siteId, component, componentId); + return entry.previous || CoreConstants.NOT_DOWNLOADED; - }).catch(() => { + } catch (error) { return CoreConstants.NOT_DOWNLOADED; - }); + } } /** @@ -1889,12 +1824,14 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved with the status. */ - getPackageStatus(siteId: string, component: string, componentId?: string | number): Promise { - return this.getPackageData(siteId, component, componentId).then((entry) => { + async getPackageStatus(siteId: string, component: string, componentId?: string | number): Promise { + try { + const entry = await this.getPackageData(siteId, component, componentId); + return entry.status || CoreConstants.NOT_DOWNLOADED; - }).catch(() => { + } catch (error) { return CoreConstants.NOT_DOWNLOADED; - }); + } } /** @@ -1929,7 +1866,8 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Deferred. */ - protected getQueueDeferred(siteId: string, fileId: string, create: boolean = true, onProgress?: (event: any) => any): any { + protected getQueueDeferred(siteId: string, fileId: string, create: boolean = true, onProgress?: CoreFilepoolOnProgressCallback): + CoreFilepoolPromiseDefer { if (!this.queueDeferreds[siteId]) { if (!create) { return; @@ -1957,7 +1895,7 @@ export class CoreFilepoolProvider { * @param fileId The file ID. * @return On progress function, undefined if not found. */ - protected getQueueOnProgress(siteId: string, fileId: string): (event: any) => any { + protected getQueueOnProgress(siteId: string, fileId: string): CoreFilepoolOnProgressCallback { const deferred = this.getQueueDeferred(siteId, fileId, false); if (deferred) { return deferred.onProgress; @@ -1973,8 +1911,8 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Promise. */ - protected getQueuePromise(siteId: string, fileId: string, create: boolean = true, onProgress?: (event: any) => any) - : Promise { + protected getQueuePromise(siteId: string, fileId: string, create: boolean = true, onProgress?: CoreFilepoolOnProgressCallback): + Promise { return this.getQueueDeferred(siteId, fileId, create, onProgress).promise; } @@ -1984,12 +1922,12 @@ export class CoreFilepoolProvider { * @param files Package files. * @return Highest revision. */ - getRevisionFromFileList(files: any[]): number { + getRevisionFromFileList(files: CoreWSExternalFile[]): number { let revision = 0; files.forEach((file) => { - if (file.url || file.fileurl) { - const r = this.getRevisionFromUrl(file.url || file.fileurl); + if (file.fileurl) { + const r = this.getRevisionFromUrl(file.fileurl); if (r > revision) { revision = r; } @@ -2035,7 +1973,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 downloadUnknown True to download file in WiFi if their size is unknown, false otherwise. + * @param downloadAny True to download file in WiFi if their size is any, 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. @@ -2045,9 +1983,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, downloadUnknown?: boolean, options: any = {}, revision?: number): Promise { + checkSize: boolean = true, downloadAny?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): + Promise { return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'src', - timemodified, checkSize, downloadUnknown, options, revision); + timemodified, checkSize, downloadAny, options, revision); } /** @@ -2056,7 +1995,7 @@ export class CoreFilepoolProvider { * @param files List of files. * @return Time modified. */ - getTimemodifiedFromFileList(files: any[]): number { + getTimemodifiedFromFileList(files: CoreWSExternalFile[]): number { let timemodified = 0; files.forEach((file) => { @@ -2078,7 +2017,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 downloadUnknown True to download file in WiFi if their size is unknown, false otherwise. + * @param downloadAny True to download file in WiFi if their size is any, 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. @@ -2088,9 +2027,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, downloadUnknown?: boolean, options: any = {}, revision?: number): Promise { + checkSize: boolean = true, downloadAny?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): + Promise { return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'url', - timemodified, checkSize, downloadUnknown, options, revision); + timemodified, checkSize, downloadAny, options, revision); } /** @@ -2111,13 +2051,12 @@ export class CoreFilepoolProvider { // 'file' param not found. Extract what's after the last '/' without params. filename = CoreUrlUtils.instance.getLastFileWithoutParams(fileUrl); } - } else if (CoreUrlUtils.instance.isGravatarUrl(fileUrl)) { // Extract gravatar ID. filename = 'gravatar_' + CoreUrlUtils.instance.getLastFileWithoutParams(fileUrl); } else if (CoreUrlUtils.instance.isThemeImageUrl(fileUrl)) { // Extract user ID. - const matches = fileUrl.match(/\/core\/([^\/]*)\//); + const matches = fileUrl.match(/\/core\/([^/]*)\//); if (matches && matches[1]) { filename = matches[1]; } @@ -2130,7 +2069,7 @@ export class CoreFilepoolProvider { // If there are hashes in the URL, extract them. const index = filename.indexOf('#'); - let hashes; + let hashes: string[]; if (index != -1) { hashes = filename.split('#'); @@ -2159,16 +2098,14 @@ export class CoreFilepoolProvider { * @param fileUrl The file URL. * @return Resolved with file object from DB on success, rejected otherwise. */ - protected hasFileInPool(siteId: string, fileId: string): Promise { - return CoreSites.instance.getSiteDb(siteId).then((db) => { - return db.getRecord(this.FILES_TABLE, { fileId }).then((entry) => { - if (typeof entry === 'undefined') { - return Promise.reject(null); - } + protected async hasFileInPool(siteId: string, fileId: string): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + const entry = await db.getRecord(CoreFilepoolProvider.FILES_TABLE, { fileId }); + if (typeof entry === 'undefined') { + throw null; + } - return entry; - }); - }); + return entry; } /** @@ -2181,12 +2118,12 @@ export class CoreFilepoolProvider { protected async hasFileInQueue(siteId: string, fileId: string): Promise { await this.dbReady; - const entry = await this.appDB.getRecord(this.QUEUE_TABLE, { siteId, fileId }); + const entry = await this.appDB.getRecord(CoreFilepoolProvider.QUEUE_TABLE, { siteId, fileId }); if (typeof entry === 'undefined') { throw null; } // Convert the links to an object. - entry.links = CoreTextUtils.instance.parseJSON(entry.links, []); + entry.linksUnserialized = CoreTextUtils.instance.parseJSON(entry.links, []); return entry; } @@ -2195,16 +2132,16 @@ export class CoreFilepoolProvider { * Invalidate all the files in a site. * * @param siteId The site ID. - * @param onlyUnknown True to only invalidate files from external repos or without revision/timemodified. + * @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. * @return Resolved on success. */ - async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise { + async invalidateAllFiles(siteId: string, onlyAny: boolean = true): Promise { const db = await CoreSites.instance.getSiteDb(siteId); - const where = onlyUnknown ? this.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : null; + const where = onlyAny ? CoreFilepoolProvider.FILE_UPDATE_ANY_WHERE_CLAUSE : null; - await db.updateRecordsWhere(this.FILES_TABLE, { stale: 1 }, where); + await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, where); } /** @@ -2219,14 +2156,13 @@ export class CoreFilepoolProvider { * You can manully call addToQueueByUrl to add this file to the queue immediately. * Please note that, if a file is stale, the user will be presented the stale file if there is no network access. */ - invalidateFileByUrl(siteId: string, fileUrl: string): Promise { - return this.fixPluginfileURL(siteId, fileUrl).then((file) => { - const fileId = this.getFileIdByUrl(file.fileurl); + async invalidateFileByUrl(siteId: string, fileUrl: string): Promise { + const file = await this.fixPluginfileURL(siteId, fileUrl); + const fileId = this.getFileIdByUrl(file.fileurl); - return CoreSites.instance.getSiteDb(siteId).then((db) => { - return db.updateRecords(this.FILES_TABLE, { stale: 1 }, { fileId }); - }); - }); + const db = await CoreSites.instance.getSiteDb(siteId); + + await db.updateRecords(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, { fileId }); } /** @@ -2235,13 +2171,12 @@ 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 onlyUnknown True to only invalidate files from external repos or without revision/timemodified. + * @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. * @return Resolved when done. */ - async invalidateFilesByComponent(siteId: string, component: string, componentId?: string | number, onlyUnknown: boolean = true) - : Promise { - + async invalidateFilesByComponent(siteId: string, component: string, componentId?: string | number, onlyAny: boolean = true): + Promise { const db = await CoreSites.instance.getSiteDb(siteId); const items = await this.getComponentFiles(db, component, componentId); @@ -2256,11 +2191,11 @@ export class CoreFilepoolProvider { whereAndParams[0] = 'fileId ' + whereAndParams[0]; - if (onlyUnknown) { - whereAndParams[0] += ' AND (' + this.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')'; + if (onlyAny) { + whereAndParams[0] += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_ANY_WHERE_CLAUSE + ')'; } - await db.updateRecordsWhere(this.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]); + await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]); } /** @@ -2284,8 +2219,8 @@ export class CoreFilepoolProvider { * @param revision File revision. If not defined, it will be calculated using the URL. * @return Promise resolved with a boolean: whether a file is downloadable. */ - async isFileDownloadable(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string, revision?: number) - : Promise { + async isFileDownloadable(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string, revision?: number): + Promise { const state = await this.getFileStateByUrl(siteId, fileUrl, timemodified, filePath, revision); return state != CoreConstants.NOT_DOWNLOADABLE; @@ -2298,12 +2233,11 @@ export class CoreFilepoolProvider { * @param fileUrl File URL. * @param Promise resolved if file is downloading, rejected otherwise. */ - isFileDownloadingByUrl(siteId: string, fileUrl: string): Promise { - return this.fixPluginfileURL(siteId, fileUrl).then((file) => { - const fileId = this.getFileIdByUrl(file.fileurl); + async isFileDownloadingByUrl(siteId: string, fileUrl: string): Promise { + const file = await this.fixPluginfileURL(siteId, fileUrl); + const fileId = this.getFileIdByUrl(file.fileurl); - return this.hasFileInQueue(siteId, fileId); - }); + await this.hasFileInQueue(siteId, fileId); } /** @@ -2324,7 +2258,7 @@ export class CoreFilepoolProvider { * @param entry Filepool entry. * @return Whether it cannot determine updates. */ - protected isFileUpdateUnknown(entry: CoreFilepoolFileEntry): boolean { + protected isFileUpdateAny(entry: CoreFilepoolFileEntry): boolean { return !!entry.isexternalfile || (!entry.revision && !entry.timemodified); } @@ -2336,8 +2270,7 @@ export class CoreFilepoolProvider { * @param links The links to the components. */ protected notifyFileActionToComponents(siteId: string, eventData: CoreFilepoolFileEventData, - links: CoreFilepoolComponentLink[]): void { - + links: CoreFilepoolComponentLink[]): void { links.forEach((link) => { const data: CoreFilepoolComponentFileEventData = Object.assign({ component: link.component, @@ -2416,7 +2349,6 @@ export class CoreFilepoolProvider { CoreEvents.instance.trigger(this.getFileEventName(siteId, fileId), data); this.notifyFileActionToComponents(siteId, data, links); - } /** @@ -2449,8 +2381,8 @@ export class CoreFilepoolProvider { * @param onProgress Function to call on progress. * @return Promise resolved when all files are downloaded. */ - prefetchPackage(siteId: string, fileList: any[], component: string, componentId?: string | number, extra?: string, - dirPath?: string, onProgress?: (event: any) => any): Promise { + prefetchPackage(siteId: string, fileList: CoreWSExternalFile[], component: string, componentId?: string | number, + extra?: string, dirPath?: string, onProgress?: CoreFilepoolOnProgressCallback): Promise { return this.downloadOrPrefetchPackage(siteId, fileList, true, component, componentId, extra, dirPath, onProgress); } @@ -2462,15 +2394,13 @@ export class CoreFilepoolProvider { * The queue process is site agnostic. */ protected processQueue(): void { - let promise; + let promise: Promise; - if (this.queueState !== this.QUEUE_RUNNING) { + if (this.queueState !== CoreFilepoolProvider.QUEUE_RUNNING) { // Silently ignore, the queue is on pause. - promise = Promise.reject(this.ERR_QUEUE_ON_PAUSE); - + promise = Promise.reject(CoreFilepoolProvider.ERR_QUEUE_ON_PAUSE); } else if (!CoreFile.instance.isAvailable() || !CoreApp.instance.isOnline()) { - promise = Promise.reject(this.ERR_FS_OR_NETWORK_UNAVAILABLE); - + promise = Promise.reject(CoreFilepoolProvider.ERR_FS_OR_NETWORK_UNAVAILABLE); } else { promise = this.processImportantQueueItem(); } @@ -2479,19 +2409,16 @@ export class CoreFilepoolProvider { // All good, we schedule next execution. setTimeout(() => { this.processQueue(); - }, this.QUEUE_PROCESS_INTERVAL); - + }, CoreFilepoolProvider.QUEUE_PROCESS_INTERVAL); }, (error) => { - // We had an error, in which case we pause the processing. - if (error === this.ERR_FS_OR_NETWORK_UNAVAILABLE) { + if (error === CoreFilepoolProvider.ERR_FS_OR_NETWORK_UNAVAILABLE) { this.logger.debug('Filesysem or network unavailable, pausing queue processing.'); - - } else if (error === this.ERR_QUEUE_IS_EMPTY) { + } else if (error === CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY) { this.logger.debug('Queue is empty, pausing queue processing.'); } - this.queueState = this.QUEUE_PAUSED; + this.queueState = CoreFilepoolProvider.QUEUE_PAUSED; }); } @@ -2500,23 +2427,24 @@ export class CoreFilepoolProvider { * * @return Resolved on success. Rejected on failure. */ - protected async processImportantQueueItem(): Promise { + protected async processImportantQueueItem(): Promise { await this.dbReady; - let items; + let items: CoreFilepoolQueueEntry[]; try { - items = await this.appDB.getRecords(this.QUEUE_TABLE, undefined, 'priority DESC, added ASC', undefined, 0, 1); + items = await this.appDB.getRecords(CoreFilepoolProvider.QUEUE_TABLE, undefined, + 'priority DESC, added ASC', undefined, 0, 1); } catch (err) { - throw this.ERR_QUEUE_IS_EMPTY; + throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; } const item = items.pop(); if (!item) { - throw this.ERR_QUEUE_IS_EMPTY; + throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; } // Convert the links to an object. - item.links = CoreTextUtils.instance.parseJSON(item.links, []); + item.linksUnserialized = CoreTextUtils.instance.parseJSON(item.links, []); return this.processQueueItem(item); } @@ -2527,7 +2455,7 @@ export class CoreFilepoolProvider { * @param item The object from the queue store. * @return Resolved on success. Rejected on failure. */ - protected processQueueItem(item: CoreFilepoolQueueEntry): Promise { + protected async processQueueItem(item: CoreFilepoolQueueEntry): Promise { // Cast optional fields to undefined instead of null. const siteId = item.siteId; const fileId = item.fileId; @@ -2539,101 +2467,103 @@ export class CoreFilepoolProvider { repositorytype: item.repositorytype || undefined, }; const filePath = item.path || undefined; - const links = item.links || []; + const links = item.linksUnserialized || []; this.logger.debug('Processing queue item: ' + siteId + ', ' + fileId); + let entry: CoreFilepoolFileEntry; + // Check if the file is already in pool. - return this.hasFileInPool(siteId, fileId).catch(() => { + try { + entry = await this.hasFileInPool(siteId, fileId); + } catch (error) { // File not in pool. - }).then((entry: CoreFilepoolFileEntry) => { - - if (entry && !options.isexternalfile && !this.isFileOutdated(entry, options.revision, options.timemodified)) { - // We have the file, it is not stale, we can update links and remove from queue. - this.logger.debug('Queued file already in store, ignoring...'); - this.addFileLinks(siteId, fileId, links).catch(() => { - // Ignore errors. - }); - this.removeFromQueue(siteId, fileId).catch(() => { - // Ignore errors. - }).finally(() => { - this.treatQueueDeferred(siteId, fileId, true); - }); - - return; - } - - // The file does not exist, or is stale, ... download it. - const onProgress = this.getQueueOnProgress(siteId, fileId); - - return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, entry).then(() => { - // Success, we add links and remove from queue. - this.addFileLinks(siteId, fileId, links).catch(() => { - // Ignore errors. - }); + } + if (entry && !options.isexternalfile && !this.isFileOutdated(entry, options.revision, options.timemodified)) { + // We have the file, it is not stale, we can update links and remove from queue. + this.logger.debug('Queued file already in store, ignoring...'); + this.addFileLinks(siteId, fileId, links).catch(() => { + // Ignore errors. + }); + this.removeFromQueue(siteId, fileId).catch(() => { + // Ignore errors. + }).finally(() => { this.treatQueueDeferred(siteId, fileId, true); - this.notifyFileDownloaded(siteId, fileId, links); + }); - // Wait for the item to be removed from queue before resolving the promise. - // If the item could not be removed from queue we still resolve the promise. - return this.removeFromQueue(siteId, fileId).catch(() => { - // Ignore errors. - }); - }, (errorObject) => { - // Whoops, we have an error... - let dropFromQueue = false; + return; + } - if (errorObject && errorObject.source === fileUrl) { - // This is most likely a FileTransfer error. - if (errorObject.code === 1) { // FILE_NOT_FOUND_ERR. - // The file was not found, most likely a 404, we remove from queue. - dropFromQueue = true; - } else if (errorObject.code === 2) { // INVALID_URL_ERR. - // The URL is invalid, we drop the file from the queue. - dropFromQueue = true; - } else if (errorObject.code === 3) { // CONNECTION_ERR. - // If there was an HTTP status, then let's remove from the queue. - dropFromQueue = true; - } else if (errorObject.code === 4) { // ABORTED_ERR. - // The transfer was aborted, we will keep the file in queue. - } else if (errorObject.code === 5) { // NOT_MODIFIED_ERR. - // We have the latest version of the file, HTTP 304 status. - dropFromQueue = true; - } else { - // Unknown error, let's remove the file from the queue to avoi locking down the queue. - dropFromQueue = true; - } + // The file does not exist, or is stale, ... download it. + const onProgress = this.getQueueOnProgress(siteId, fileId); + + return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, entry).then(() => { + // Success, we add links and remove from queue. + this.addFileLinks(siteId, fileId, links).catch(() => { + // Ignore errors. + }); + + this.treatQueueDeferred(siteId, fileId, true); + this.notifyFileDownloaded(siteId, fileId, links); + + // Wait for the item to be removed from queue before resolving the promise. + // If the item could not be removed from queue we still resolve the promise. + return this.removeFromQueue(siteId, fileId).catch(() => { + // Ignore errors. + }); + }, (errorObject) => { + // Whoops, we have an error... + let dropFromQueue = false; + + if (errorObject && errorObject.source === fileUrl) { + // This is most likely a FileTransfer error. + if (errorObject.code === 1) { // FILE_NOT_FOUND_ERR. + // The file was not found, most likely a 404, we remove from queue. + dropFromQueue = true; + } else if (errorObject.code === 2) { // INVALID_URL_ERR. + // The URL is invalid, we drop the file from the queue. + dropFromQueue = true; + } else if (errorObject.code === 3) { // CONNECTION_ERR. + // If there was an HTTP status, then let's remove from the queue. + dropFromQueue = true; + } else if (errorObject.code === 4) { // ABORTED_ERR. + // The transfer was aborted, we will keep the file in queue. + } else if (errorObject.code === 5) { // NOT_MODIFIED_ERR. + // We have the latest version of the file, HTTP 304 status. + dropFromQueue = true; } else { + // Any error, let's remove the file from the queue to avoi locking down the queue. dropFromQueue = true; } + } else { + dropFromQueue = true; + } - let errorMessage = null; - // Some Android devices restrict the amount of usable storage using quotas. - // If this quota would be exceeded by the download, it throws an exception. - // We catch this exception here, and report a meaningful error message to the user. - if (errorObject instanceof FileTransferError && errorObject.exception && errorObject.exception.includes('EDQUOT')) { - errorMessage = 'core.course.insufficientavailablequota'; - } + let errorMessage = null; + // Some Android devices restrict the amount of usable storage using quotas. + // If this quota would be exceeded by the download, it throws an exception. + // We catch this exception here, and report a meaningful error message to the user. + if (errorObject instanceof FileTransferError && errorObject.exception && errorObject.exception.includes('EDQUOT')) { + errorMessage = 'core.course.insufficientavailablequota'; + } - if (dropFromQueue) { - this.logger.debug('Item dropped from queue due to error: ' + fileUrl, errorObject); + if (dropFromQueue) { + this.logger.debug('Item dropped from queue due to error: ' + fileUrl, errorObject); - return this.removeFromQueue(siteId, fileId).catch(() => { - // Consider this as a silent error, never reject the promise here. - }).then(() => { - this.treatQueueDeferred(siteId, fileId, false, errorMessage); - this.notifyFileDownloadError(siteId, fileId, links); - }); - } else { - // We considered the file as legit but did not get it, failure. + return this.removeFromQueue(siteId, fileId).catch(() => { + // Consider this as a silent error, never reject the promise here. + }).then(() => { this.treatQueueDeferred(siteId, fileId, false, errorMessage); this.notifyFileDownloadError(siteId, fileId, links); + }); + } else { + // We considered the file as legit but did not get it, failure. + this.treatQueueDeferred(siteId, fileId, false, errorMessage); + this.notifyFileDownloadError(siteId, fileId, links); - return Promise.reject(errorObject); - } - - }); + return Promise.reject(errorObject); + } }); } @@ -2644,10 +2574,10 @@ export class CoreFilepoolProvider { * @param fileId The file ID. * @return Resolved on success. Rejected on failure. It is advised to silently ignore failures. */ - protected async removeFromQueue(siteId: string, fileId: string): Promise { + protected async removeFromQueue(siteId: string, fileId: string): Promise { await this.dbReady; - return this.appDB.deleteRecords(this.QUEUE_TABLE, { siteId, fileId }); + await this.appDB.deleteRecords(CoreFilepoolProvider.QUEUE_TABLE, { siteId, fileId }); } /** @@ -2657,60 +2587,58 @@ export class CoreFilepoolProvider { * @param fileId The file ID. * @return Resolved on success. */ - protected removeFileById(siteId: string, fileId: string): Promise { - return CoreSites.instance.getSiteDb(siteId).then((db) => { - // Get the path to the file first since it relies on the file object stored in the pool. - // Don't use getFilePath to prevent performing 2 DB requests. - let path = this.getFilepoolFolderPath(siteId) + '/' + fileId; - let fileUrl; + protected async removeFileById(siteId: string, fileId: string): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + // Get the path to the file first since it relies on the file object stored in the pool. + // Don't use getFilePath to prevent performing 2 DB requests. + let path = this.getFilepoolFolderPath(siteId) + '/' + fileId; + let fileUrl: string; - return this.hasFileInPool(siteId, fileId).then((entry) => { - fileUrl = entry.url; + try { + const entry = await this.hasFileInPool(siteId, fileId); + fileUrl = entry.url; - if (entry.extension) { - path += '.' + entry.extension; + if (entry.extension) { + path += '.' + entry.extension; + } + } catch (error) { + // If file not found, use the path without extension. + } + + const conditions = { + fileId, + }; + + // Get links to components to notify them after remove. + const links = await this.getFileLinks(siteId, fileId); + const promises = []; + + // Remove entry from filepool store. + promises.push(db.deleteRecords(CoreFilepoolProvider.FILES_TABLE, conditions)); + + // Remove links. + promises.push(db.deleteRecords(CoreFilepoolProvider.LINKS_TABLE, conditions)); + + // Remove the file. + if (CoreFile.instance.isAvailable()) { + promises.push(CoreFile.instance.removeFile(path).catch((error) => { + if (error && error.code == 1) { + // Not found, ignore error since maybe it was deleted already. + } else { + return Promise.reject(error); } + })); + } - return path; - }).catch(() => { - // If file not found, use the path without extension. - return path; - }).then((path) => { - const conditions = { - fileId, - }; + await Promise.all(promises); - // Get links to components to notify them after remove. - return this.getFileLinks(siteId, fileId).then((links) => { - const promises = []; + this.notifyFileDeleted(siteId, fileId, links); - // Remove entry from filepool store. - promises.push(db.deleteRecords(this.FILES_TABLE, conditions)); - - // Remove links. - promises.push(db.deleteRecords(this.LINKS_TABLE, conditions)); - - // Remove the file. - if (CoreFile.instance.isAvailable()) { - promises.push(CoreFile.instance.removeFile(path).catch((error) => { - if (error && error.code == 1) { - // Not found, ignore error since maybe it was deleted already. - } else { - return Promise.reject(error); - } - })); - } - - return Promise.all(promises).then(() => { - this.notifyFileDeleted(siteId, fileId, links); - - return CorePluginFile.instance.fileDeleted(fileUrl, path, siteId).catch((error) => { - // Ignore errors. - }); - }); - }); - }); - }); + try { + await CorePluginFile.instance.fileDeleted(fileUrl, path, siteId); + } catch (error) { + // Ignore errors. + } } /** @@ -2721,14 +2649,11 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Resolved on success. */ - removeFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise { - return CoreSites.instance.getSiteDb(siteId).then((db) => { - return this.getComponentFiles(db, component, componentId); - }).then((items) => { - return Promise.all(items.map((item) => { - return this.removeFileById(siteId, item.fileId); - })); - }); + async removeFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise { + const db = await CoreSites.instance.getSiteDb(siteId); + const items = await this.getComponentFiles(db, component, componentId); + + await Promise.all(items.map((item) => this.removeFileById(siteId, item.fileId))); } /** @@ -2738,12 +2663,11 @@ export class CoreFilepoolProvider { * @param fileUrl The file URL. * @return Resolved on success, rejected on failure. */ - removeFileByUrl(siteId: string, fileUrl: string): Promise { - return this.fixPluginfileURL(siteId, fileUrl).then((file) => { - const fileId = this.getFileIdByUrl(file.fileurl); + async removeFileByUrl(siteId: string, fileUrl: string): Promise { + const file = await this.fixPluginfileURL(siteId, fileUrl); + const fileId = this.getFileIdByUrl(file.fileurl); - return this.removeFileById(siteId, fileId); - }); + await this.removeFileById(siteId, fileId); } /** @@ -2772,32 +2696,29 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved when the status is changed. Resolve param: new status. */ - setPackagePreviousStatus(siteId: string, component: string, componentId?: string | number): Promise { + async setPackagePreviousStatus(siteId: string, component: string, componentId?: string | number): Promise { componentId = this.fixComponentId(componentId); this.logger.debug(`Set previous status for package ${component} ${componentId}`); - return CoreSites.instance.getSite(siteId).then((site) => { - const packageId = this.getPackageId(component, componentId); + const site = await CoreSites.instance.getSite(siteId); + const packageId = this.getPackageId(component, componentId); - // Get current stored data, we'll only update 'status' and 'updated' fields. - return site.getDb().getRecord(this.PACKAGES_TABLE, { id: packageId }).then((entry: CoreFilepoolPackageEntry) => { - const newData: CoreFilepoolPackageEntry = {}; - if (entry.status == CoreConstants.DOWNLOADING) { - // Going back from downloading to previous status, restore previous download time. - newData.downloadTime = entry.previousDownloadTime; - } - newData.status = entry.previous || CoreConstants.NOT_DOWNLOADED; - newData.updated = Date.now(); - this.logger.debug(`Set previous status '${entry.status}' for package ${component} ${componentId}`); + // Get current stored data, we'll only update 'status' and 'updated' fields. + const entry = site.getDb().getRecord(CoreFilepoolProvider.PACKAGES_TABLE, { id: packageId }); + const newData: CoreFilepoolPackageEntry = {}; + if (entry.status == CoreConstants.DOWNLOADING) { + // Going back from downloading to previous status, restore previous download time. + newData.downloadTime = entry.previousDownloadTime; + } + newData.status = entry.previous || CoreConstants.NOT_DOWNLOADED; + newData.updated = Date.now(); + this.logger.debug(`Set previous status '${entry.status}' for package ${component} ${componentId}`); - return site.getDb().updateRecords(this.PACKAGES_TABLE, newData, { id: packageId }).then(() => { - // Success updating, trigger event. - this.triggerPackageStatusChanged(site.id, newData.status, component, componentId); + await site.getDb().updateRecords(CoreFilepoolProvider.PACKAGES_TABLE, newData, { id: packageId }); + // Success updating, trigger event. + this.triggerPackageStatusChanged(site.id, newData.status, component, componentId); - return newData.status; - }); - }); - }); + return newData.status; } /** @@ -2807,7 +2728,8 @@ export class CoreFilepoolProvider { * @return Whether file should be downloaded. */ shouldDownload(size: number): boolean { - return size <= this.DOWNLOAD_THRESHOLD || (CoreApp.instance.isWifi() && size <= this.WIFI_DOWNLOAD_THRESHOLD); + return size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD || + (CoreApp.instance.isWifi() && size <= CoreFilepoolProvider.WIFI_DOWNLOAD_THRESHOLD); } /** @@ -2824,23 +2746,22 @@ export class CoreFilepoolProvider { * - The file cannot be streamed. * If the file is big and can be streamed, the promise returned by this function will be rejected. */ - shouldDownloadBeforeOpen(url: string, size: number): Promise { - if (size >= 0 && size <= this.DOWNLOAD_THRESHOLD) { + async shouldDownloadBeforeOpen(url: string, size: number): Promise { + if (size >= 0 && size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD) { // The file is small, download it. - return Promise.resolve(); + return; } if (CoreApp.instance.isDesktop()) { // In desktop always download first. - return Promise.resolve(); + return; } - return CoreUtils.instance.getMimeTypeFromUrl(url).then((mimetype) => { - // If the file is streaming (audio or video) we reject. - if (mimetype.indexOf('video') != -1 || mimetype.indexOf('audio') != -1) { - return Promise.reject(null); - } - }); + 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; + } } /** @@ -2853,65 +2774,63 @@ export class CoreFilepoolProvider { * @param extra Extra data to store for the package. If you want to store more than 1 value, use JSON.stringify. * @return Promise resolved when status is stored. */ - storePackageStatus(siteId: string, status: string, component: string, componentId?: string | number, extra?: string) - : Promise { + async storePackageStatus(siteId: string, status: string, component: string, componentId?: string | number, extra?: string): + Promise { this.logger.debug(`Set status '${status}' for package ${component} ${componentId}`); componentId = this.fixComponentId(componentId); - return CoreSites.instance.getSite(siteId).then((site) => { - const packageId = this.getPackageId(component, componentId); - let downloadTime; - let previousDownloadTime; + const site = await CoreSites.instance.getSite(siteId); + const packageId = this.getPackageId(component, componentId); + let downloadTime: number; + let previousDownloadTime: number; - if (status == CoreConstants.DOWNLOADING) { - // Set download time if package is now downloading. - downloadTime = CoreTimeUtils.instance.timestamp(); + if (status == CoreConstants.DOWNLOADING) { + // Set download time if package is now downloading. + downloadTime = CoreTimeUtils.instance.timestamp(); + } + + let previousStatus: string; + // Search current status to set it as previous status. + try { + const entry = site.getDb().getRecord(CoreFilepoolProvider.PACKAGES_TABLE, { id: packageId }); + if (typeof extra == 'undefined' || extra === null) { + extra = entry.extra; + } + if (typeof downloadTime == 'undefined') { + // Keep previous download time. + downloadTime = entry.downloadTime; + previousDownloadTime = entry.previousDownloadTime; + } else { + // The downloadTime will be updated, store current time as previous. + previousDownloadTime = entry.downloadTime; } - // Search current status to set it as previous status. - return site.getDb().getRecord(this.PACKAGES_TABLE, { id: packageId }).then((entry: CoreFilepoolPackageEntry) => { - if (typeof extra == 'undefined' || extra === null) { - extra = entry.extra; - } - if (typeof downloadTime == 'undefined') { - // Keep previous download time. - downloadTime = entry.downloadTime; - previousDownloadTime = entry.previousDownloadTime; - } else { - // The downloadTime will be updated, store current time as previous. - previousDownloadTime = entry.downloadTime; - } + previousStatus = entry.status; + } catch (error) { + // No previous status. + } - return entry.status; - }).catch(() => { - // No previous status. - }).then((previousStatus: string) => { - const packageEntry: CoreFilepoolPackageEntry = { - id: packageId, - component, - componentId, - status, - previous: previousStatus, - updated: Date.now(), - downloadTime, - previousDownloadTime, - extra, - }; - let promise; + const packageEntry: CoreFilepoolPackageEntry = { + id: packageId, + component, + componentId, + status, + previous: previousStatus, + updated: Date.now(), + downloadTime, + previousDownloadTime, + extra, + }; - if (previousStatus === status) { - // The package already has this status, no need to change it. - promise = Promise.resolve(); - } else { - promise = site.getDb().insertRecord(this.PACKAGES_TABLE, packageEntry); - } + if (previousStatus === status) { + // The package already has this status, no need to change it. + return; + } - return promise.then(() => { - // Success inserting, trigger event. - this.triggerPackageStatusChanged(siteId, status, component, componentId); - }); - }); - }); + await site.getDb().insertRecord(CoreFilepoolProvider.PACKAGES_TABLE, packageEntry); + + // Success inserting, trigger event. + this.triggerPackageStatusChanged(siteId, status, component, componentId); } /** @@ -2927,11 +2846,10 @@ export class CoreFilepoolProvider { * @return Promise resolved with the CSS code. */ treatCSSCode(siteId: string, fileUrl: string, cssCode: string, component?: string, componentId?: string | number, - revision?: number): Promise { - + revision?: number): Promise { const urls = CoreDomUtils.instance.extractUrlsFromCSS(cssCode); const promises = []; - let filePath; + let filePath: string; let updated = false; // Get the path of the CSS file. @@ -2943,8 +2861,7 @@ export class CoreFilepoolProvider { // Download the file only if it's an online URL. if (!CoreUrlUtils.instance.isLocalFileUrl(url)) { promises.push(this.downloadUrl(siteId, url, false, component, componentId, 0, undefined, undefined, undefined, - revision).then((fileUrl) => { - + revision).then((fileUrl) => { if (fileUrl != url) { cssCode = cssCode.replace(new RegExp(CoreTextUtils.instance.escapeForRegex(url), 'g'), fileUrl); updated = true; @@ -2961,9 +2878,7 @@ export class CoreFilepoolProvider { if (updated) { return CoreFile.instance.writeFile(filePath, cssCode); } - }).then(() => { - return cssCode; - }); + }).then(() => cssCode); } /** @@ -2999,6 +2914,7 @@ export class CoreFilepoolProvider { componentId: this.fixComponentId(componentId), status, }; + CoreEvents.instance.trigger(CoreEventsProvider.PACKAGE_STATUS_CHANGED, data, siteId); } @@ -3012,23 +2928,34 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component. * @return Promise resolved when status is stored. */ - updatePackageDownloadTime(siteId: string, component: string, componentId?: string | number): Promise { + async updatePackageDownloadTime(siteId: string, component: string, componentId?: string | number): Promise { componentId = this.fixComponentId(componentId); - return CoreSites.instance.getSite(siteId).then((site) => { - const packageId = this.getPackageId(component, componentId); + const site = await CoreSites.instance.getSite(siteId); + const packageId = this.getPackageId(component, componentId); - return site.getDb().updateRecords(this.PACKAGES_TABLE, { downloadTime: CoreTimeUtils.instance.timestamp() }, { id: packageId }); - }); + await site.getDb().updateRecords(CoreFilepoolProvider.PACKAGES_TABLE, + { downloadTime: CoreTimeUtils.instance.timestamp() }, { id: packageId }); } + } export class CoreFilepool extends makeSingleton(CoreFilepoolProvider) {} +/** + * File options. + */ +type CoreFilepoolFileOptions = { + revision?: number; // File's revision. + timemodified?: number; // File's timemodified. + isexternalfile?: number; // 1 if it's a external file (from an external repository), 0 otherwise. + repositorytype?: string; // Type of the repository this file belongs to. +}; + /** * Entry from filepool. */ -export type CoreFilepoolFileEntry = { +export type CoreFilepoolFileEntry = CoreFilepoolFileOptions & { /** * The fileId to identify the file. */ @@ -3039,16 +2966,6 @@ export type CoreFilepoolFileEntry = { */ url?: string; - /** - * File's revision. - */ - revision?: number; - - /** - * File's timemodified. - */ - timemodified?: number; - /** * 1 if file is stale (needs to be updated), 0 otherwise. */ @@ -3059,16 +2976,6 @@ export type CoreFilepoolFileEntry = { */ downloadTime?: number; - /** - * 1 if it's a external file (from an external repository), 0 otherwise. - */ - isexternalfile?: number; - - /** - * Type of the repository this file belongs to. - */ - repositorytype?: string; - /** * File's path. */ @@ -3083,7 +2990,7 @@ export type CoreFilepoolFileEntry = { /** * Entry from the file's queue. */ -export type CoreFilepoolQueueEntry = { +export type CoreFilepoolQueueEntry = CoreFilepoolFileOptions & { /** * The site the file belongs to. */ @@ -3109,35 +3016,20 @@ export type CoreFilepoolQueueEntry = { */ url?: string; - /** - * File's revision. - */ - revision?: number; - - /** - * File's timemodified. - */ - timemodified?: number; - - /** - * 1 if it's a external file (from an external repository), 0 otherwise. - */ - isexternalfile?: number; - - /** - * Type of the repository this file belongs to. - */ - repositorytype?: string; - /** * File's path. */ path?: string; + /** + * File links (to link the file to components and componentIds). Serialized to store on DB. + */ + links?: string; + /** * File links (to link the file to components and componentIds). */ - links?: CoreFilepoolComponentLink[]; + linksUnserialized?: CoreFilepoolComponentLink[]; }; /** @@ -3249,3 +3141,24 @@ export type CoreFilepoolComponentFileEventData = CoreFilepoolFileEventData & { */ componentId: string | number; }; + +/** + * Function called when file download progress ocurred. + */ +export type CoreFilepoolOnProgressCallback = (event: T) => void; + +/** + * Deferred promise for file pool. It's similar to the result of $q.defer() in AngularJS. + */ +type CoreFilepoolPromiseDefer = PromiseDefer & { + onProgress?: CoreFilepoolOnProgressCallback; // On Progress function. +}; + +/** + * Links table record type. + */ +type CoreFilepoolLinksRecord = { + fileId: string; // File Id. + component: string; // Component name. + componentId: number | string; // Component Id. +}; diff --git a/src/app/services/geolocation.ts b/src/app/services/geolocation.ts index cded645ae..7fe1db82a 100644 --- a/src/app/services/geolocation.ts +++ b/src/app/services/geolocation.ts @@ -70,7 +70,7 @@ export class CoreGeolocationProvider { } if (!CoreApp.instance.isIOS()) { - await Diagnostic.instance.switchToLocationSettings(); + Diagnostic.instance.switchToLocationSettings(); await CoreApp.instance.waitForResume(30000); locationEnabled = await Diagnostic.instance.isLocationEnabled(); @@ -91,8 +91,7 @@ export class CoreGeolocationProvider { const authorizationStatus = await Diagnostic.instance.getLocationAuthorizationStatus(); switch (authorizationStatus) { - // This constant is hard-coded because it is not declared in @ionic-native/diagnostic v4. - case 'DENIED_ONCE': + case Diagnostic.instance.permissionStatus.DENIED_ONCE: if (failOnDeniedOnce) { throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied); } @@ -107,7 +106,6 @@ export class CoreGeolocationProvider { case Diagnostic.instance.permissionStatus.GRANTED_WHEN_IN_USE: // Location is authorized. return; - case Diagnostic.instance.permissionStatus.DENIED: default: throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied); } @@ -133,7 +131,7 @@ export enum CoreGeolocationErrorReason { export class CoreGeolocationError extends CoreError { - readonly reason: CoreGeolocationErrorReason; + reason: CoreGeolocationErrorReason; constructor(reason: CoreGeolocationErrorReason) { super(`GeolocationError: ${reason}`); diff --git a/src/app/services/groups.ts b/src/app/services/groups.ts index c1e4090bf..b49c42339 100644 --- a/src/app/services/groups.ts +++ b/src/app/services/groups.ts @@ -17,19 +17,20 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { makeSingleton, Translate } from '@singletons/core.singletons'; +import { CoreWSExternalWarning } from '@services/ws'; +import { CoreCourseBase } from '@/types/global'; /* * Service to handle groups. */ @Injectable() export class CoreGroupsProvider { - // Group mode constants. - static NOGROUPS = 0; - static SEPARATEGROUPS = 1; - static VISIBLEGROUPS = 2; - protected ROOT_CACHE_KEY = 'mmGroups:'; - constructor() { } + // Group mode constants. + static readonly NOGROUPS = 0; + static readonly SEPARATEGROUPS = 1; + static readonly VISIBLEGROUPS = 2; + protected readonly ROOT_CACHE_KEY = 'mmGroups:'; /** * Check if group mode of an activity is enabled. @@ -39,12 +40,14 @@ export class CoreGroupsProvider { * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved with true if the activity has groups, resolved with false otherwise. */ - activityHasGroups(cmId: number, siteId?: string, ignoreCache?: boolean): Promise { - return this.getActivityGroupMode(cmId, siteId, ignoreCache).then((groupmode) => { + async activityHasGroups(cmId: number, siteId?: string, ignoreCache?: boolean): Promise { + try { + const groupmode = await this.getActivityGroupMode(cmId, siteId, ignoreCache); + return groupmode === CoreGroupsProvider.SEPARATEGROUPS || groupmode === CoreGroupsProvider.VISIBLEGROUPS; - }).catch(() => { + } catch (error) { return false; - }); + } } /** @@ -56,32 +59,32 @@ export class CoreGroupsProvider { * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved when the groups are retrieved. */ - getActivityAllowedGroups(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise { - return CoreSites.instance.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + async getActivityAllowedGroups(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): + Promise { + const site = await CoreSites.instance.getSite(siteId); - const params = { - cmid: cmId, - userid: userId, - }; - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getActivityAllowedGroupsCacheKey(cmId, userId), - updateFrequency: CoreSite.FREQUENCY_RARELY, - }; + userId = userId || site.getUserId(); - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + const params = { + cmid: cmId, + userid: userId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getActivityAllowedGroupsCacheKey(cmId, userId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; - return site.read('core_group_get_activity_allowed_groups', params, preSets).then((response) => { - if (!response || !response.groups) { - return Promise.reject(null); - } + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } - return response; - }); - }); + const response = await site.read('core_group_get_activity_allowed_groups', params, preSets); + if (!response || !response.groups) { + throw null; + } + + return response; } /** @@ -104,20 +107,20 @@ export class CoreGroupsProvider { * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved when the groups are retrieved. If not allowed, empty array will be returned. */ - getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise { + async getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): + Promise { siteId = siteId || CoreSites.instance.getCurrentSiteId(); // Get real groupmode, in case it's forced by the course. - return this.activityHasGroups(cmId, siteId, ignoreCache).then((hasGroups) => { - if (hasGroups) { - // Get the groups available for the user. - return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); - } + const hasGroups = await this.activityHasGroups(cmId, siteId, ignoreCache); + if (hasGroups) { + // Get the groups available for the user. + return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); + } - return { - groups: [] - }; - }); + return { + groups: [], + }; } /** @@ -130,44 +133,43 @@ export class CoreGroupsProvider { * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved with the group info. */ - getActivityGroupInfo(cmId: number, addAllParts?: boolean, userId?: number, siteId?: string, ignoreCache?: boolean) - : Promise { - + async getActivityGroupInfo(cmId: number, addAllParts?: boolean, userId?: number, siteId?: string, ignoreCache?: boolean): + Promise { const groupInfo: CoreGroupInfo = { - groups: [] + groups: [], }; - return this.getActivityGroupMode(cmId, siteId, ignoreCache).then((groupMode) => { - groupInfo.separateGroups = groupMode === CoreGroupsProvider.SEPARATEGROUPS; - groupInfo.visibleGroups = groupMode === CoreGroupsProvider.VISIBLEGROUPS; + const groupMode = await this.getActivityGroupMode(cmId, siteId, ignoreCache); - if (groupInfo.separateGroups || groupInfo.visibleGroups) { - return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); - } + groupInfo.separateGroups = groupMode === CoreGroupsProvider.SEPARATEGROUPS; + groupInfo.visibleGroups = groupMode === CoreGroupsProvider.VISIBLEGROUPS; - return { + let result: CoreGroupGetActivityAllowedGroupsResponse; + if (groupInfo.separateGroups || groupInfo.visibleGroups) { + result = await this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); + } else { + result = { groups: [], - canaccessallgroups: false }; - }).then((result) => { - if (result.groups.length <= 0) { - groupInfo.separateGroups = false; - groupInfo.visibleGroups = false; + } + + if (result.groups.length <= 0) { + groupInfo.separateGroups = false; + groupInfo.visibleGroups = false; + groupInfo.defaultGroupId = 0; + } else { + // The "canaccessallgroups" field was added in 3.4. Add all participants for visible groups in previous versions. + if (result.canaccessallgroups || (typeof result.canaccessallgroups == 'undefined' && groupInfo.visibleGroups)) { + groupInfo.groups.push({ id: 0, name: Translate.instance.instant('core.allparticipants') }); groupInfo.defaultGroupId = 0; } else { - // The "canaccessallgroups" field was added in 3.4. Add all participants for visible groups in previous versions. - if (result.canaccessallgroups || (typeof result.canaccessallgroups == 'undefined' && groupInfo.visibleGroups)) { - groupInfo.groups.push({ id: 0, name: Translate.instance.instant('core.allparticipants') }); - groupInfo.defaultGroupId = 0; - } else { - groupInfo.defaultGroupId = result.groups[0].id; - } - - groupInfo.groups = groupInfo.groups.concat(result.groups); + groupInfo.defaultGroupId = result.groups[0].id; } - return groupInfo; - }); + groupInfo.groups = groupInfo.groups.concat(result.groups); + } + + return groupInfo; } /** @@ -178,29 +180,27 @@ export class CoreGroupsProvider { * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved when the group mode is retrieved. */ - getActivityGroupMode(cmId: number, siteId?: string, ignoreCache?: boolean): Promise { - return CoreSites.instance.getSite(siteId).then((site) => { - const params = { - cmid: cmId, - }; - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getActivityGroupModeCacheKey(cmId), - updateFrequency: CoreSite.FREQUENCY_RARELY, - }; + async getActivityGroupMode(cmId: number, siteId?: string, ignoreCache?: boolean): Promise { + const site = await CoreSites.instance.getSite(siteId); + const params = { + cmid: cmId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getActivityGroupModeCacheKey(cmId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; - if (ignoreCache) { - preSets.getFromCache = false; - preSets.emergencyCache = false; - } + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } - return site.read('core_group_get_activity_groupmode', params, preSets).then((response) => { - if (!response || typeof response.groupmode == 'undefined') { - return Promise.reject(null); - } + const response = await site.read('core_group_get_activity_groupmode', params, preSets); + if (!response || typeof response.groupmode == 'undefined') { + throw null; + } - return response.groupmode; - }); - }); + return response.groupmode; } /** @@ -219,16 +219,15 @@ export class CoreGroupsProvider { * @param siteId Site to get the groups from. If not defined, use current site. * @return Promise resolved when the groups are retrieved. */ - getAllUserGroups(siteId?: string): Promise { - return CoreSites.instance.getSite(siteId).then((site) => { - siteId = siteId || site.getId(); + async getAllUserGroups(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + siteId = siteId || site.getId(); - if (site.isVersionGreaterEqualThan('3.6')) { - return this.getUserGroupsInCourse(0, siteId); - } + if (site.isVersionGreaterEqualThan('3.6')) { + return this.getUserGroupsInCourse(0, siteId); + } - // @todo Get courses. - }); + // @todo Get courses. } /** @@ -239,17 +238,13 @@ export class CoreGroupsProvider { * @param userId ID of the user. If not defined, use the userId related to siteId. * @return Promise resolved when the groups are retrieved. */ - getUserGroups(courses: any[], siteId?: string, userId?: number): Promise { + async getUserGroups(courses: CoreCourseBase[] | number[], siteId?: string, userId?: number): Promise { // Get all courses one by one. - const promises = courses.map((course) => { - const courseId = typeof course == 'object' ? course.id : course; + const promises = this.getCourseIds(courses).map((courseId) => this.getUserGroupsInCourse(courseId, siteId, userId)); - return this.getUserGroupsInCourse(courseId, siteId, userId); - }); + const courseGroups = await Promise.all(promises); - return Promise.all(promises).then((courseGroups) => { - return [].concat(...courseGroups); - }); + return [].concat(...courseGroups); } /** @@ -260,26 +255,24 @@ export class CoreGroupsProvider { * @param userId ID of the user. If not defined, use ID related to siteid. * @return Promise resolved when the groups are retrieved. */ - getUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise { - return CoreSites.instance.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); - const data = { - userid: userId, - courseid: courseId, - }; - const preSets = { - cacheKey: this.getUserGroupsInCourseCacheKey(courseId, userId), - updateFrequency: CoreSite.FREQUENCY_RARELY, - }; + async getUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise { + const site = await CoreSites.instance.getSite(siteId); + userId = userId || site.getUserId(); + const data = { + userid: userId, + courseid: courseId, + }; + const preSets = { + cacheKey: this.getUserGroupsInCourseCacheKey(courseId, userId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; - return site.read('core_group_get_course_user_groups', data, preSets).then((response) => { - if (response && response.groups) { - return response.groups; - } else { - return Promise.reject(null); - } - }); - }); + const response = await site.read('core_group_get_course_user_groups', data, preSets); + if (!response || !response.groups) { + throw null; + } + + return response.groups; } /** @@ -310,12 +303,11 @@ export class CoreGroupsProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ - invalidateActivityAllowedGroups(cmId: number, userId?: number, siteId?: string): Promise { - return CoreSites.instance.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + async invalidateActivityAllowedGroups(cmId: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + userId = userId || site.getUserId(); - return site.invalidateWsCacheForKey(this.getActivityAllowedGroupsCacheKey(cmId, userId)); - }); + await site.invalidateWsCacheForKey(this.getActivityAllowedGroupsCacheKey(cmId, userId)); } /** @@ -325,10 +317,10 @@ export class CoreGroupsProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ - invalidateActivityGroupMode(cmId: number, siteId?: string): Promise { - return CoreSites.instance.getSite(siteId).then((site) => { - return site.invalidateWsCacheForKey(this.getActivityGroupModeCacheKey(cmId)); - }); + async invalidateActivityGroupMode(cmId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getActivityGroupModeCacheKey(cmId)); } /** @@ -339,12 +331,12 @@ export class CoreGroupsProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ - invalidateActivityGroupInfo(cmId: number, userId?: number, siteId?: string): Promise { + async invalidateActivityGroupInfo(cmId: number, userId?: number, siteId?: string): Promise { const promises = []; promises.push(this.invalidateActivityAllowedGroups(cmId, userId, siteId)); promises.push(this.invalidateActivityGroupMode(cmId, siteId)); - return Promise.all(promises); + await Promise.all(promises); } /** @@ -353,14 +345,14 @@ export class CoreGroupsProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ - invalidateAllUserGroups(siteId?: string): Promise { - return CoreSites.instance.getSite(siteId).then((site) => { - if (site.isVersionGreaterEqualThan('3.6')) { - return this.invalidateUserGroupsInCourse(0, siteId); - } + async invalidateAllUserGroups(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); - return site.invalidateWsCacheForKeyStartingWith(this.getUserGroupsInCoursePrefixCacheKey()); - }); + if (site.isVersionGreaterEqualThan('3.6')) { + return this.invalidateUserGroupsInCourse(0, siteId); + } + + await site.invalidateWsCacheForKeyStartingWith(this.getUserGroupsInCoursePrefixCacheKey()); } /** @@ -371,18 +363,13 @@ export class CoreGroupsProvider { * @param userId User ID. If not defined, use current user. * @return Promise resolved when the data is invalidated. */ - invalidateUserGroups(courses: any[], siteId?: string, userId?: number): Promise { - return CoreSites.instance.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + async invalidateUserGroups(courses: CoreCourseBase[] | number[], siteId?: string, userId?: number): Promise { + const site = await CoreSites.instance.getSite(siteId); + userId = userId || site.getUserId(); - const promises = courses.map((course) => { - const courseId = typeof course == 'object' ? course.id : course; + const promises = this.getCourseIds(courses).map((courseId) => this.invalidateUserGroupsInCourse(courseId, site.id, userId)); - return this.invalidateUserGroupsInCourse(courseId, site.id, userId); - }); - - return Promise.all(promises); - }); + await Promise.all(promises); } /** @@ -393,12 +380,11 @@ export class CoreGroupsProvider { * @param userId User ID. If not defined, use current user. * @return Promise resolved when the data is invalidated. */ - invalidateUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise { - return CoreSites.instance.getSite(siteId).then((site) => { - userId = userId || site.getUserId(); + async invalidateUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise { + const site = await CoreSites.instance.getSite(siteId); + userId = userId || site.getUserId(); - return site.invalidateWsCacheForKey(this.getUserGroupsInCourseCacheKey(courseId, userId)); - }); + await site.invalidateWsCacheForKey(this.getUserGroupsInCourseCacheKey(courseId, userId)); } /** @@ -418,10 +404,30 @@ export class CoreGroupsProvider { return groupInfo.defaultGroupId; } + + protected getCourseIds(courses: CoreCourseBase[] | number[]): number[] { + return courses.length > 0 && typeof courses[0] === 'object' + ? (courses as CoreCourseBase[]).map((course) => course.id) + : courses as number[]; + } + } export class CoreGroups extends makeSingleton(CoreGroupsProvider) {} +/** + * Specific group info. + */ +export type CoreGroup = { + id: number; // Group 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; // Coure Id. +}; + + /** * Group info for an activity. */ @@ -429,7 +435,7 @@ export type CoreGroupInfo = { /** * List of groups. */ - groups?: any[]; + groups?: CoreGroup[]; /** * Whether it's separate groups. @@ -446,3 +452,12 @@ export type CoreGroupInfo = { */ defaultGroupId?: number; }; + +/** + * WS core_group_get_activity_allowed_groups response type. + */ +export type CoreGroupGetActivityAllowedGroupsResponse = { + groups: CoreGroup[]; // List of groups. + canaccessallgroups?: boolean; // Whether the user will be able to access all the activity groups. + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/app/services/handlers/site-info-cron-handler.ts b/src/app/services/handlers/site-info-cron-handler.ts new file mode 100644 index 000000000..85a86852e --- /dev/null +++ b/src/app/services/handlers/site-info-cron-handler.ts @@ -0,0 +1,62 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@services/cron'; +import { CoreSites } from '@services/sites'; + +/** + * Cron handler to update site info every certain time. + */ +@Injectable() +export class CoreSiteInfoCronHandler implements CoreCronHandler { + + name = 'CoreSiteInfoCronHandler'; + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param siteId ID of the site affected, undefined for all sites. + * @return Promise resolved when done, rejected on failure. + */ + async execute(siteId?: string): Promise { + if (!siteId) { + const siteIds = await CoreSites.instance.getSitesIds(); + + await Promise.all(siteIds.map((siteId) => CoreSites.instance.updateSiteInfo(siteId))); + } else { + await CoreSites.instance.updateSiteInfo(siteId); + } + } + + /** + * Returns handler's interval in milliseconds. Defaults to CoreCronDelegate.DEFAULT_INTERVAL. + * + * @return Interval time (in milliseconds). + */ + getInterval(): number { + return 10800000; // 3 hours. + } + + /** + * Check whether it's a synchronization process or not. True if not defined. + * + * @return Whether it's a synchronization process or not. + */ + isSync(): boolean { + return false; + } + +} diff --git a/src/app/services/init.ts b/src/app/services/init.ts index 5575b8b33..8e06292fc 100644 --- a/src/app/services/init.ts +++ b/src/app/services/init.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; -import { CoreUtils } from '@services/utils/utils'; +import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { CoreLogger } from '@singletons/logger'; import { makeSingleton } from '@singletons/core.singletons'; @@ -50,12 +50,13 @@ export type CoreInitHandler = { */ @Injectable() export class CoreInitDelegate { + static readonly DEFAULT_PRIORITY = 100; // Default priority for init processes. static readonly MAX_RECOMMENDED_PRIORITY = 600; - protected initProcesses = {}; + protected initProcesses: { [s: string]: CoreInitHandler } = {}; protected logger: CoreLogger; - protected readiness; + protected readiness: CoreInitReadinessPromiseDefer; constructor() { this.logger = CoreLogger.getInstance('CoreInitDelegate'); @@ -163,6 +164,14 @@ export class CoreInitDelegate { this.logger.log(`Registering process '${handler.name}'.`); this.initProcesses[handler.name] = handler; } + } export class CoreInit extends makeSingleton(CoreInitDelegate) {} + +/** + * Deferred promise for init readiness. + */ +type CoreInitReadinessPromiseDefer = PromiseDefer & { + resolved?: boolean; // If true, readiness have been resolved. +}; diff --git a/src/app/services/lang.ts b/src/app/services/lang.ts index c49e3c0a6..0f56a1edc 100644 --- a/src/app/services/lang.ts +++ b/src/app/services/lang.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import CoreConfigConstants from '@app/config.json'; -import { CoreApp, CoreAppProvider } from '@services/app'; +import { CoreAppProvider } from '@services/app'; import { CoreConfig } from '@services/config'; import { makeSingleton, Translate, Platform, Globalization } from '@singletons/core.singletons'; @@ -26,6 +26,7 @@ import * as moment from 'moment'; */ @Injectable() 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. @@ -51,7 +52,7 @@ export class CoreLangProvider { }); }); - Translate.instance.onLangChange.subscribe((event: any) => { + Translate.instance.onLangChange.subscribe(() => { // @todo: Set platform lang and dir. }); } @@ -63,7 +64,7 @@ export class CoreLangProvider { * @param strings Object with the strings to add. * @param prefix A prefix to add to all keys. */ - addSitePluginsStrings(lang: string, strings: any, prefix?: string): void { + addSitePluginsStrings(lang: string, strings: string[], prefix?: string): void { lang = lang.replace(/_/g, '-'); // Use the app format instead of Moodle format. // Initialize structure if it doesn't exist. @@ -109,7 +110,7 @@ export class CoreLangProvider { * @param language New language to use. * @return Promise resolved when the change is finished. */ - changeCurrentLanguage(language: string): Promise { + changeCurrentLanguage(language: string): Promise { const promises = []; // Change the language, resolving the promise when we receive the first value. @@ -127,7 +128,7 @@ export class CoreLangProvider { setTimeout(() => { fallbackSubs.unsubscribe(); }); - }, (error) => { + }, () => { // Resolve with the original language. resolve(data); @@ -168,7 +169,7 @@ export class CoreLangProvider { // 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]}); + Translate.instance.onLangChange.emit({ lang: language, translations: Translate.instance.translations[language] }); } }); } @@ -195,7 +196,7 @@ export class CoreLangProvider { * * @return Custom strings. */ - getAllCustomStrings(): any { + getAllCustomStrings(): unknown { return this.customStrings; } @@ -204,7 +205,7 @@ export class CoreLangProvider { * * @return Site plugins strings. */ - getAllSitePluginsStrings(): any { + getAllSitePluginsStrings(): unknown { return this.sitePluginsStrings; } @@ -213,16 +214,13 @@ export class CoreLangProvider { * * @return Promise resolved with the current language. */ - getCurrentLanguage(): Promise { - + async getCurrentLanguage(): Promise { if (typeof this.currentLanguage != 'undefined') { - return Promise.resolve(this.currentLanguage); + return this.currentLanguage; } // Get current language from config (user might have changed it). - return CoreConfig.instance.get('current_language').then((language) => { - return 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; @@ -237,7 +235,6 @@ export class CoreLangProvider { if (CoreConfigConstants.languages && typeof CoreConfigConstants.languages[language] == 'undefined') { // Code is NOT supported. Fallback to language without dash. E.g. 'en-US' would fallback to 'en'. language = language.substr(0, language.indexOf('-')); - } } @@ -247,10 +244,10 @@ export class CoreLangProvider { } return language; - }).catch(() => { + }).catch(() => // Error getting locale. Use default language. - return this.defaultLanguage; - }); + this.defaultLanguage, + ); } catch (err) { // Error getting locale. Use default language. return Promise.resolve(this.defaultLanguage); @@ -286,7 +283,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) => { @@ -322,14 +319,13 @@ export class CoreLangProvider { const list: string[] = strings.split(/(?:\r\n|\r|\n)/); list.forEach((entry: string) => { const values: string[] = entry.split('|'); - let lang: string; if (values.length < 3) { // Not enough data, ignore the entry. return; } - lang = values[2].replace(/_/g, '-'); // Use the app format instead of Moodle format. + const lang = values[2].replace(/_/g, '-'); // Use the app format instead of Moodle format. if (lang == this.currentLanguage) { currentLangChanged = true; @@ -353,7 +349,7 @@ export class CoreLangProvider { // 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] + translations: Translate.instance.translations[this.currentLanguage], }); } } @@ -365,7 +361,7 @@ export class CoreLangProvider { * @param lang Language to load. * @return Whether the translation table was modified. */ - loadLangStrings(langObject: any, lang: string): boolean { + loadLangStrings(langObject: CoreLanguageObject, lang: string): boolean { let langApplied = false; if (langObject[lang]) { @@ -396,7 +392,7 @@ export class CoreLangProvider { * @param key String key. * @param value String value. */ - loadString(langObject: any, lang: string, key: string, value: string): void { + loadString(langObject: CoreLanguageObject, lang: string, key: string, value: string): void { lang = lang.replace(/_/g, '-'); // Use the app format instead of Moodle format. if (Translate.instance.translations[lang]) { @@ -425,7 +421,7 @@ export class CoreLangProvider { * * @param strings Strings to unload. */ - protected unloadStrings(strings: any): void { + protected unloadStrings(strings: CoreLanguageObject): void { // Iterate over all languages and strings. for (const lang in strings) { if (!Translate.instance.translations[lang]) { @@ -446,6 +442,20 @@ export class CoreLangProvider { } } } + } export class CoreLang extends makeSingleton(CoreLangProvider) {} + +/** + * Language object has two leves, first per language and second per string key. + */ +type CoreLanguageObject = { + [s: string]: { // Lang name. + [s: string]: { // String key. + value: string; // Value with replacings done. + original?: string; // Original value of the string. + applied?: boolean; // If the key is applied to the translations table or not. + }; + }; +}; diff --git a/src/app/services/local-notifications.ts b/src/app/services/local-notifications.ts index 18a11f940..22da70f92 100644 --- a/src/app/services/local-notifications.ts +++ b/src/app/services/local-notifications.ts @@ -18,7 +18,7 @@ import { ILocalNotification } from '@ionic-native/local-notifications'; import { CoreApp, CoreAppSchema } from '@services/app'; import { CoreConfig } from '@services/config'; -import { CoreEvents, CoreEventsProvider } from '@services/events'; +import { CoreEventObserver, CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { SQLiteDB } from '@classes/sqlitedb'; @@ -33,56 +33,57 @@ import { CoreLogger } from '@singletons/logger'; */ @Injectable() export class CoreLocalNotificationsProvider { + // Variables for the database. - protected SITES_TABLE = 'notification_sites'; // Store to asigne unique codes to each site. - protected COMPONENTS_TABLE = 'notification_components'; // Store to asigne unique codes to each component. - protected TRIGGERED_TABLE = 'notifications_triggered'; // Store to prevent re-triggering notifications. + protected static readonly SITES_TABLE = 'notification_sites'; // Store to asigne unique codes to each site. + protected static readonly COMPONENTS_TABLE = 'notification_components'; // Store to asigne unique codes to each component. + protected static readonly TRIGGERED_TABLE = 'notifications_triggered'; // Store to prevent re-triggering notifications. protected tablesSchema: CoreAppSchema = { name: 'CoreLocalNotificationsProvider', version: 1, tables: [ { - name: this.SITES_TABLE, + name: CoreLocalNotificationsProvider.SITES_TABLE, columns: [ { name: 'id', type: 'TEXT', - primaryKey: true + primaryKey: true, }, { name: 'code', type: 'INTEGER', - notNull: true + notNull: true, }, ], }, { - name: this.COMPONENTS_TABLE, + name: CoreLocalNotificationsProvider.COMPONENTS_TABLE, columns: [ { name: 'id', type: 'TEXT', - primaryKey: true + primaryKey: true, }, { name: 'code', type: 'INTEGER', - notNull: true + notNull: true, }, ], }, { - name: this.TRIGGERED_TABLE, + name: CoreLocalNotificationsProvider.TRIGGERED_TABLE, columns: [ { name: 'id', type: 'INTEGER', - primaryKey: true + primaryKey: true, }, { name: 'at', type: 'INTEGER', - notNull: true + notNull: true, }, ], }, @@ -91,7 +92,7 @@ export class CoreLocalNotificationsProvider { 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 codes: { [s: string]: number } = {}; protected codeRequestsQueue = {}; protected observables = {}; @@ -99,8 +100,9 @@ export class CoreLocalNotificationsProvider { title: '', texts: [], ids: [], - timeouts: [] + timeouts: [], }; + protected triggerSubscription: Subscription; protected clickSubscription: Subscription; protected clearSubscription: Subscription; @@ -110,7 +112,6 @@ export class CoreLocalNotificationsProvider { protected queueRunner: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477). constructor() { - this.logger = CoreLogger.getInstance('CoreLocalNotificationsProvider'); this.queueRunner = new CoreQueueRunner(10); this.appDB = CoreApp.instance.getDB(); @@ -149,7 +150,7 @@ export class CoreLocalNotificationsProvider { // Create the default channel for local notifications. this.createDefaultChannel(); - Translate.instance.onLangChange.subscribe((event: any) => { + Translate.instance.onLangChange.subscribe(() => { // Update the channel name. this.createDefaultChannel(); }); @@ -187,7 +188,6 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved when the notifications are cancelled. */ async cancelSiteNotifications(siteId: string): Promise { - if (!this.isAvailable()) { return; } else if (!siteId) { @@ -228,15 +228,15 @@ export class CoreLocalNotificationsProvider { * * @return Promise resolved when done. */ - protected createDefaultChannel(): Promise { + protected async createDefaultChannel(): Promise { if (!CoreApp.instance.isAndroid()) { - return Promise.resolve(); + return; } - return Push.instance.createChannel({ + await Push.instance.createChannel({ id: 'default-channel-id', description: Translate.instance.instant('addon.calendar.calendarreminders'), - importance: 4 + importance: 4, }).catch((error) => { this.logger.error('Error changing channel name', error); }); @@ -297,7 +297,7 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved when the component code is retrieved. */ protected getComponentCode(component: string): Promise { - return this.requestCode(this.COMPONENTS_TABLE, component); + return this.requestCode(CoreLocalNotificationsProvider.COMPONENTS_TABLE, component); } /** @@ -308,16 +308,16 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved when the site code is retrieved. */ protected getSiteCode(siteId: string): Promise { - return this.requestCode(this.SITES_TABLE, siteId); + return this.requestCode(CoreLocalNotificationsProvider.SITES_TABLE, siteId); } /** * Create a unique notification ID, trying to prevent collisions. Generated ID must be a Number (Android). * The generated ID shouldn't be higher than 2147483647 or it's going to cause problems in Android. * This function will prevent collisions and keep the number under Android limit if: - * -User has used less than 21 sites. - * -There are less than 11 components. - * -The notificationId passed as parameter is lower than 10000000. + * - User has used less than 21 sites. + * - There are less than 11 components. + * - The notificationId passed as parameter is lower than 10000000. * * @param notificationId Notification ID. * @param component Component triggering the notification. @@ -329,12 +329,10 @@ export class CoreLocalNotificationsProvider { return Promise.reject(null); } - return this.getSiteCode(siteId).then((siteCode) => { - return this.getComponentCode(component).then((componentCode) => { + return this.getSiteCode(siteId).then((siteCode) => this.getComponentCode(component).then((componentCode) => // We use the % operation to keep the number under Android's limit. - return (siteCode * 100000000 + componentCode * 10000000 + notificationId) % 2147483647; - }); - }); + (siteCode * 100000000 + componentCode * 10000000 + notificationId) % 2147483647, + )); } /** @@ -343,7 +341,7 @@ export class CoreLocalNotificationsProvider { * @param eventName Name of the event. * @param notification Notification. */ - protected handleEvent(eventName: string, notification: any): void { + protected handleEvent(eventName: string, notification: ILocalNotification): void { if (notification && notification.data) { this.logger.debug('Notification event: ' + eventName + '. Data:', notification.data); @@ -374,7 +372,7 @@ export class CoreLocalNotificationsProvider { await this.dbReady; try { - const stored = await this.appDB.getRecord(this.TRIGGERED_TABLE, { id: notification.id }); + const stored = await this.appDB.getRecord(CoreLocalNotificationsProvider.TRIGGERED_TABLE, { id: notification.id }); let triggered = (notification.trigger && notification.trigger.at) || 0; if (typeof triggered != 'number') { @@ -443,15 +441,14 @@ export class CoreLocalNotificationsProvider { */ protected processNextRequest(): void { const nextKey = Object.keys(this.codeRequestsQueue)[0]; - let request, - promise; + let promise: Promise; if (typeof nextKey == 'undefined') { // No more requests in queue, stop. return; } - request = this.codeRequestsQueue[nextKey]; + const request = this.codeRequestsQueue[nextKey]; // Check if request is valid. if (typeof request == 'object' && typeof request.table != 'undefined' && typeof request.id != 'undefined') { @@ -483,7 +480,7 @@ export class CoreLocalNotificationsProvider { * @param callback Function to call with the data received by the notification. * @return Object with an "off" property to stop listening for clicks. */ - registerClick(component: string, callback: Function): any { + registerClick(component: string, callback: CoreLocalNotificationsClickCallback): CoreEventObserver { return this.registerObserver('click', component, callback); } @@ -495,7 +492,7 @@ export class CoreLocalNotificationsProvider { * @param callback Function to call with the data received by the notification. * @return Object with an "off" property to stop listening for events. */ - registerObserver(eventName: string, component: string, callback: Function): any { + registerObserver(eventName: string, component: string, callback: CoreLocalNotificationsClickCallback): CoreEventObserver { this.logger.debug(`Register observer '${component}' for event '${eventName}'.`); if (typeof this.observables[eventName] == 'undefined') { @@ -504,7 +501,7 @@ export class CoreLocalNotificationsProvider { if (typeof this.observables[eventName][component] == 'undefined') { // No observable for this component, create a new one. - this.observables[eventName][component] = new Subject(); + this.observables[eventName][component] = new Subject(); } this.observables[eventName][component].subscribe(callback); @@ -512,7 +509,7 @@ export class CoreLocalNotificationsProvider { return { off: (): void => { this.observables[eventName][component].unsubscribe(callback); - } + }, }; } @@ -522,10 +519,10 @@ export class CoreLocalNotificationsProvider { * @param id Notification ID. * @return Promise resolved when it is removed. */ - async removeTriggered(id: number): Promise { + async removeTriggered(id: number): Promise { await this.dbReady; - return this.appDB.deleteRecords(this.TRIGGERED_TABLE, { id: id }); + await this.appDB.deleteRecords(CoreLocalNotificationsProvider.TRIGGERED_TABLE, { id: id }); } /** @@ -536,9 +533,9 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved when the code is retrieved. */ protected requestCode(table: string, id: string): Promise { - const deferred = CoreUtils.instance.promiseDefer(), - key = table + '#' + id, - isQueueEmpty = Object.keys(this.codeRequestsQueue).length == 0; + const deferred = CoreUtils.instance.promiseDefer(); + const key = table + '#' + id; + const isQueueEmpty = Object.keys(this.codeRequestsQueue).length == 0; if (typeof this.codeRequestsQueue[key] != 'undefined') { // There's already a pending request for this store and ID, add the promise to it. @@ -548,7 +545,7 @@ export class CoreLocalNotificationsProvider { this.codeRequestsQueue[key] = { table: table, id: id, - promises: [deferred] + promises: [deferred], }; } @@ -591,7 +588,6 @@ export class CoreLocalNotificationsProvider { * @return Promise resolved when the notification is scheduled. */ async schedule(notification: ILocalNotification, component: string, siteId: string, alreadyUnique?: boolean): Promise { - if (!alreadyUnique) { notification.id = await this.getUniqueNotificationId(notification.id, component, siteId); } @@ -605,12 +601,29 @@ export class CoreLocalNotificationsProvider { notification.smallIcon = notification.smallIcon || 'res://smallicon'; notification.color = notification.color || CoreConfigConstants.notificoncolor; - const led: any = notification.led || {}; - notification.led = { - color: led.color || 'FF9900', - on: led.on || 1000, - off: led.off || 1000 - }; + if (notification.led !== false) { + let ledColor = 'FF9900'; + let ledOn = 1000; + let ledOff = 1000; + + if (typeof notification.led === 'string') { + ledColor = notification.led; + } else if (Array.isArray(notification.led)) { + ledColor = notification.led[0] || ledColor; + ledOn = notification.led[1] || ledOn; + ledOff = notification.led[2] || ledOff; + } else if (typeof notification.led === 'object') { + ledColor = notification.led.color || ledColor; + ledOn = notification.led.on || ledOn; + ledOff = notification.led.off || ledOff; + } + + notification.led = { + color: ledColor, + on: ledOn, + off: ledOff, + }; + } } const queueId = 'schedule-' + notification.id; @@ -626,36 +639,34 @@ export class CoreLocalNotificationsProvider { * @param notification Notification to schedule. * @return Promise resolved when scheduled. */ - protected scheduleNotification(notification: ILocalNotification): Promise { + protected async scheduleNotification(notification: ILocalNotification): Promise { // Check if the notification has been triggered already. - return this.isTriggered(notification, false).then((triggered) => { - // Cancel the current notification in case it gets scheduled twice. - return LocalNotifications.instance.cancel(notification.id).finally(() => { - if (!triggered) { - // Check if sound is enabled for notifications. - let promise; + const triggered = await this.isTriggered(notification, false); - if (this.canDisableSound()) { - promise = CoreConfig.instance.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true); - } else { - promise = Promise.resolve(true); - } + // Cancel the current notification in case it gets scheduled twice. + LocalNotifications.instance.cancel(notification.id).finally(async () => { + if (!triggered) { + let soundEnabled: boolean; - return promise.then((soundEnabled) => { - if (!soundEnabled) { - notification.sound = null; - } else { - delete notification.sound; // Use default value. - } - - notification.foreground = true; - - // Remove from triggered, since the notification could be in there with a different time. - this.removeTriggered(notification.id); - LocalNotifications.instance.schedule(notification); - }); + // Check if sound is enabled for notifications. + if (!this.canDisableSound()) { + soundEnabled = true; + } else { + soundEnabled = await CoreConfig.instance.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true); } - }); + + if (!soundEnabled) { + notification.sound = null; + } else { + delete notification.sound; // Use default value. + } + + notification.foreground = true; + + // Remove from triggered, since the notification could be in there with a different time. + this.removeTriggered(notification.id); + LocalNotifications.instance.schedule(notification); + } }); } @@ -666,15 +677,15 @@ export class CoreLocalNotificationsProvider { * @param notification Triggered notification. * @return Promise resolved when stored, rejected otherwise. */ - async trigger(notification: ILocalNotification): Promise { + async trigger(notification: ILocalNotification): Promise { await this.dbReady; 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 : Date.now(), }; - return this.appDB.insertRecord(this.TRIGGERED_TABLE, entry); + return this.appDB.insertRecord(CoreLocalNotificationsProvider.TRIGGERED_TABLE, entry); } /** @@ -684,14 +695,17 @@ export class CoreLocalNotificationsProvider { * @param newName The new name. * @return Promise resolved when done. */ - async updateComponentName(oldName: string, newName: string): Promise { + async updateComponentName(oldName: string, newName: string): Promise { await this.dbReady; - const oldId = this.COMPONENTS_TABLE + '#' + oldName, - newId = this.COMPONENTS_TABLE + '#' + newName; + const oldId = CoreLocalNotificationsProvider.COMPONENTS_TABLE + '#' + oldName; + const newId = CoreLocalNotificationsProvider.COMPONENTS_TABLE + '#' + newName; - return this.appDB.updateRecords(this.COMPONENTS_TABLE, {id: newId}, {id: oldId}); + await this.appDB.updateRecords(CoreLocalNotificationsProvider.COMPONENTS_TABLE, { id: newId }, { id: oldId }); } + } export class CoreLocalNotifications extends makeSingleton(CoreLocalNotificationsProvider) {} + +export type CoreLocalNotificationsClickCallback = (value: T) => void; diff --git a/src/app/services/plugin-file-delegate.ts b/src/app/services/plugin-file-delegate.ts index 584deb83f..273632bc4 100644 --- a/src/app/services/plugin-file-delegate.ts +++ b/src/app/services/plugin-file-delegate.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { FileEntry } from '@ionic-native/file'; -import { CoreFilepool } from '@services/filepool'; +import { CoreFilepool, CoreFilepoolOnProgressCallback } from '@services/filepool'; import { CoreWSExternalFile } from '@services/ws'; import { CoreConstants } from '@core/constants'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; @@ -26,6 +26,7 @@ import { makeSingleton } from '@singletons/core.singletons'; */ @Injectable() export class CorePluginFileDelegate extends CoreDelegate { + protected handlerNameProperty = 'component'; constructor() { @@ -40,14 +41,12 @@ export class CorePluginFileDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - fileDeleted(fileUrl: string, path: string, siteId?: string): Promise { - const handler = this.getHandlerForFile({fileurl: fileUrl}); + async fileDeleted(fileUrl: string, path: string, siteId?: string): Promise { + const handler = this.getHandlerForFile({ fileurl: fileUrl }); if (handler && handler.fileDeleted) { - return handler.fileDeleted(fileUrl, path, siteId); + await handler.fileDeleted(fileUrl, path, siteId); } - - return Promise.resolve(); } /** @@ -71,9 +70,8 @@ export class CorePluginFileDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the file to use. Rejected if cannot download. */ - protected async getHandlerDownloadableFile(file: CoreWSExternalFile, handler: CorePluginFileHandler, siteId?: string) - : Promise { - + protected async getHandlerDownloadableFile(file: CoreWSExternalFile, handler: CorePluginFileHandler, siteId?: string): + Promise { const isDownloadable = await this.isFileDownloadable(file, siteId); if (!isDownloadable.downloadable) { @@ -132,7 +130,7 @@ export class CorePluginFileDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with file size and a boolean to indicate if it is the total size or only partial. */ - async getFilesDownloadSize(files: CoreWSExternalFile[], siteId?: string): Promise<{ size: number, total: boolean }> { + async getFilesDownloadSize(files: CoreWSExternalFile[], siteId?: string): Promise<{ size: number; total: boolean }> { const filteredFiles = []; await Promise.all(files.map(async (file) => { @@ -153,10 +151,10 @@ export class CorePluginFileDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with file size and a boolean to indicate if it is the total size or only partial. */ - async getFilesSize(files: CoreWSExternalFile[], siteId?: string): Promise<{ size: number, total: boolean }> { + async getFilesSize(files: CoreWSExternalFile[], siteId?: string): Promise<{ size: number; total: boolean }> { const result = { size: 0, - total: true + total: true, }; await Promise.all(files.map(async (file) => { @@ -231,7 +229,7 @@ export class CorePluginFileDelegate extends CoreDelegate { * @param siteId Site ID. If not defined, current site. * @return Promise with the data. */ - isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise { + async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise { const handler = this.getHandlerForFile(file); if (handler && handler.isFileDownloadable) { @@ -239,7 +237,7 @@ export class CorePluginFileDelegate extends CoreDelegate { } // Default to true. - return Promise.resolve({downloadable: true}); + return { downloadable: true }; } /** @@ -272,15 +270,15 @@ export class CorePluginFileDelegate extends CoreDelegate { * @param onProgress Function to call on progress. * @return Promise resolved when done. */ - treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: (event: any) => any): Promise { - const handler = this.getHandlerForFile({fileurl: fileUrl}); + async treatDownloadedFile(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: CoreFilepoolOnProgressCallback): + Promise { + const handler = this.getHandlerForFile({ fileurl: fileUrl }); if (handler && handler.treatDownloadedFile) { - return handler.treatDownloadedFile(fileUrl, file, siteId, onProgress); + await handler.treatDownloadedFile(fileUrl, file, siteId, onProgress); } - - return Promise.resolve(); } + } export class CorePluginFile extends makeSingleton(CorePluginFileDelegate) {} @@ -320,7 +318,7 @@ export interface CorePluginFileHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - fileDeleted?(fileUrl: string, path: string, siteId?: string): Promise; + fileDeleted?(fileUrl: string, path: string, siteId?: string): Promise; /** * Check whether a file can be downloaded. If so, return the file to download. @@ -375,7 +373,8 @@ export interface CorePluginFileHandler extends CoreDelegateHandler { * @param onProgress Function to call on progress. * @return Promise resolved when done. */ - treatDownloadedFile?(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: (event: any) => any): Promise; + treatDownloadedFile?(fileUrl: string, file: FileEntry, siteId?: string, onProgress?: CoreFilepoolOnProgressCallback): + Promise; } /** diff --git a/src/app/services/utils/time.ts b/src/app/services/utils/time.ts index 05ec9a78b..b3bba19be 100644 --- a/src/app/services/utils/time.ts +++ b/src/app/services/utils/time.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; -import * as moment from 'moment'; +import moment, { LongDateFormatKey } from 'moment'; import { CoreConstants } from '@core/constants'; import { makeSingleton, Translate } from '@singletons/core.singletons'; @@ -24,7 +24,7 @@ import { makeSingleton, Translate } from '@singletons/core.singletons'; @Injectable() export class CoreTimeUtilsProvider { - protected FORMAT_REPLACEMENTS = { // To convert PHP strf format to Moment format. + protected static readonly FORMAT_REPLACEMENTS = { // To convert PHP strf format to Moment format. '%a': 'ddd', '%A': 'dddd', '%d': 'DD', @@ -65,7 +65,7 @@ export class CoreTimeUtilsProvider { '%x': 'L', '%n': '\n', '%t': '\t', - '%%': '%' + '%%': '%', }; /** @@ -97,7 +97,8 @@ export class CoreTimeUtilsProvider { converted += ']'; } - converted += typeof this.FORMAT_REPLACEMENTS[char] != 'undefined' ? this.FORMAT_REPLACEMENTS[char] : char; + converted += typeof CoreTimeUtilsProvider.FORMAT_REPLACEMENTS[char] != 'undefined' ? + CoreTimeUtilsProvider.FORMAT_REPLACEMENTS[char] : char; } else { // Not a PHP format. We need to escape them, otherwise the letters could be confused with Moment formats. if (!escaping) { @@ -129,7 +130,7 @@ export class CoreTimeUtilsProvider { } // The component ion-datetime doesn't support escaping characters ([]), so we remove them. - let fixed = format.replace(/[\[\]]/g, ''); + let fixed = format.replace(/[[\]]/g, ''); if (fixed.indexOf('A') != -1) { // Do not use am/pm format because there is a bug in ion-datetime. @@ -250,7 +251,6 @@ export class CoreTimeUtilsProvider { * @return Duration in a short human readable format. */ formatDurationShort(duration: number): string { - const minutes = Math.floor(duration / 60); const seconds = duration - minutes * 60; const durations = []; @@ -346,7 +346,7 @@ export class CoreTimeUtilsProvider { * @param localizedFormat Format to use. * @return Localized ISO format */ - getLocalizedDateFormat(localizedFormat: any): string { + getLocalizedDateFormat(localizedFormat: LongDateFormatKey): string { return moment.localeData().longDateFormat(localizedFormat); } @@ -366,6 +366,7 @@ export class CoreTimeUtilsProvider { return moment().startOf('day').unix(); } } + } export class CoreTimeUtils extends makeSingleton(CoreTimeUtilsProvider) {} diff --git a/src/app/services/utils/url.ts b/src/app/services/utils/url.ts index 0ebee5cbe..760eb01aa 100644 --- a/src/app/services/utils/url.ts +++ b/src/app/services/utils/url.ts @@ -55,7 +55,7 @@ export class CoreUrlUtilsProvider { * @param boolToNumber Whether to convert bools to 1 or 0. * @return URL with params. */ - addParamsToUrl(url: string, params?: {[key: string]: any}, anchor?: string, boolToNumber?: boolean): string { + addParamsToUrl(url: string, params?: CoreUrlParams, anchor?: string, boolToNumber?: boolean): string { let separator = url.indexOf('?') != -1 ? '&' : '?'; for (const key in params) { @@ -63,7 +63,7 @@ export class CoreUrlUtilsProvider { if (boolToNumber && typeof value == 'boolean') { // Convert booleans to 1 or 0. - value = value ? 1 : 0; + value = value ? '1' : '0'; } // Ignore objects. @@ -102,7 +102,7 @@ export class CoreUrlUtilsProvider { canUseTokenPluginFile(url: string, siteUrl: string, accessKey?: string): boolean { // Do not use tokenpluginfile if site doesn't use slash params, the URL doesn't work. // Also, only use it for "core" pluginfile endpoints. Some plugins can implement their own endpoint (like customcert). - return accessKey && !url.match(/[\&?]file=/) && ( + return accessKey && !url.match(/[&?]file=/) && ( url.indexOf(CoreTextUtils.instance.concatenatePaths(siteUrl, 'pluginfile.php')) === 0 || url.indexOf(CoreTextUtils.instance.concatenatePaths(siteUrl, 'webservice/pluginfile.php')) === 0); } @@ -113,13 +113,13 @@ export class CoreUrlUtilsProvider { * @param url URL to treat. * @return Object with the params. */ - extractUrlParams(url: string): any { + extractUrlParams(url: string): CoreUrlParams { const regex = /[?&]+([^=&]+)=?([^&]*)?/gi; const subParamsPlaceholder = '@@@SUBPARAMS@@@'; - const params: any = {}; + const params: CoreUrlParams = {}; const urlAndHash = url.split('#'); const questionMarkSplit = urlAndHash[0].split('?'); - let subParams; + let subParams: string; if (questionMarkSplit.length > 2) { // There is more than one question mark in the URL. This can happen if any of the params is a URL with params. @@ -190,10 +190,10 @@ export class CoreUrlUtilsProvider { url = url.replace('/pluginfile', '/webservice/pluginfile'); } - url = this.addParamsToUrl(url, {token}); + url = this.addParamsToUrl(url, { token }); } - return this.addParamsToUrl(url, {offline: 1}); // Always send offline=1 (it's for external repositories). + return this.addParamsToUrl(url, { offline: '1' }); // Always send offline=1 (it's for external repositories). } /** @@ -206,7 +206,7 @@ export class CoreUrlUtilsProvider { url = url.trim(); // Check if the URL starts by http or https. - if (! /^http(s)?\:\/\/.*/i.test(url)) { + if (! /^http(s)?:\/\/.*/i.test(url)) { // Test first allways https. url = 'https://' + url; } @@ -228,7 +228,7 @@ export class CoreUrlUtilsProvider { * @param page Docs page to go to. * @return Promise resolved with the Moodle docs URL. */ - getDocsUrl(release?: string, page: string = 'Mobile_app'): Promise { + async getDocsUrl(release?: string, page: string = 'Mobile_app'): Promise { let docsUrl = 'https://docs.moodle.org/en/' + page; if (typeof release != 'undefined') { @@ -240,11 +240,13 @@ export class CoreUrlUtilsProvider { } } - return CoreLang.instance.getCurrentLanguage().then((lang) => { + try { + const lang = await CoreLang.instance.getCurrentLanguage(); + return docsUrl.replace('/en/', '/' + lang + '/'); - }).catch(() => { + } catch (error) { return docsUrl; - }); + } } /** @@ -258,13 +260,13 @@ export class CoreUrlUtilsProvider { return; } - let videoId; - const params: any = {}; + let videoId: string; + const params: CoreUrlParams = {}; url = CoreTextUtils.instance.decodeHTML(url); // Get the video ID. - let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/); + let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/); if (match && match[2].length === 11) { videoId = match[2]; @@ -276,7 +278,7 @@ export class CoreUrlUtilsProvider { } // Now get the playlist (if any). - match = url.match(/[?&]list=([^#\&\?]+)/); + match = url.match(/[?&]list=([^#&?]+)/); if (match && match[1]) { params.list = match[1]; @@ -286,13 +288,15 @@ export class CoreUrlUtilsProvider { match = url.match(/[?&]start=(\d+)/); if (match && match[1]) { - params.start = parseInt(match[1], 10); + params.start = parseInt(match[1], 10).toString(); } else { // No start param, but it could have a time param. match = url.match(/[?&]t=(\d+h)?(\d+m)?(\d+s)?/); if (match) { - params.start = (match[1] ? parseInt(match[1], 10) * 3600 : 0) + (match[2] ? parseInt(match[2], 10) * 60 : 0) + - (match[3] ? parseInt(match[3], 10) : 0); + const start = (match[1] ? parseInt(match[1], 10) * 3600 : 0) + + (match[2] ? parseInt(match[2], 10) * 60 : 0) + + (match[3] ? parseInt(match[3], 10) : 0); + params.start = start.toString(); } } @@ -328,7 +332,7 @@ export class CoreUrlUtilsProvider { return; } - const matches = url.match(/^([^\/:\.\?]*):\/\//); + const matches = url.match(/^([^/:.?]*):\/\//); if (matches && matches[1]) { return matches[1]; } @@ -361,11 +365,11 @@ export class CoreUrlUtilsProvider { getUsernameFromUrl(url: string): string { if (url.indexOf('@') > -1) { // Get URL without protocol. - const withoutProtocol = url.replace(/^[^?@\/]*:\/\//, ''); + const withoutProtocol = url.replace(/^[^?@/]*:\/\//, ''); const matches = withoutProtocol.match(/[^@]*/); // Make sure that @ is at the start of the URL, not in a param at the end. - if (matches && matches.length && !matches[0].match(/[\/|?]/)) { + if (matches && matches.length && !matches[0].match(/[/|?]/)) { return matches[0]; } } @@ -408,7 +412,7 @@ export class CoreUrlUtilsProvider { * @return Whether the url uses http or https protocol. */ isHttpURL(url: string): boolean { - return /^https?\:\/\/.+/i.test(url); + return /^https?:\/\/.+/i.test(url); } /** @@ -427,10 +431,11 @@ export class CoreUrlUtilsProvider { * Check whether a URL scheme belongs to a local file. * * @param scheme Scheme to check. - * @param domain The domain. Needed because in Android the WebView scheme is http. + * @param notUsed Unused parameter. * @return Whether the scheme belongs to a local file. */ - isLocalFileUrlScheme(scheme: string, domain: string): boolean { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isLocalFileUrlScheme(scheme: string, notUsed?: string): boolean { if (scheme) { scheme = scheme.toLowerCase(); } @@ -483,7 +488,7 @@ export class CoreUrlUtilsProvider { * @return URL without params. */ removeUrlParams(url: string): string { - const matches = url.match(/^[^\?]+/); + const matches = url.match(/^[^?]+/); return matches && matches[0]; } @@ -515,6 +520,9 @@ export class CoreUrlUtilsProvider { return url; } + } export class CoreUrlUtils extends makeSingleton(CoreUrlUtilsProvider) {} + +export type CoreUrlParams = {[key: string]: string}; diff --git a/src/app/singletons/logger.ts b/src/app/singletons/logger.ts index c189192da..ee8f9fc4e 100644 --- a/src/app/singletons/logger.ts +++ b/src/app/singletons/logger.ts @@ -27,12 +27,18 @@ import { environment } from '@/environments/environment'; * Then you can call the log function you want to use in this logger instance. */ export class CoreLogger { + log: LogFunction; info: LogFunction; warn: LogFunction; debug: LogFunction; error: LogFunction; + // Avoid creating singleton instances. + private constructor() { + // Nothing to do. + } + /** * Get a logger instance for a certain class, service or component. * @@ -88,6 +94,7 @@ export class CoreLogger { logFn.apply(null, args); }; } + } /** diff --git a/src/app/singletons/url.ts b/src/app/singletons/url.ts index 36a2d9acb..d17af5bc6 100644 --- a/src/app/singletons/url.ts +++ b/src/app/singletons/url.ts @@ -72,7 +72,9 @@ interface UrlParts { export class CoreUrl { // Avoid creating singleton instances. - private constructor() {} + private constructor() { + // Nothing to do. + } /** * Parse parts of a url, using an implicit protocol if it is missing from the url. @@ -123,14 +125,14 @@ export class CoreUrl { // Match using common suffixes. const knownSuffixes = [ - '\/my\/?', - '\/\\\?redirect=0', - '\/index\\\.php', - '\/course\/view\\\.php', - '\/login\/index\\\.php', - '\/mod\/page\/view\\\.php', + '/my/?', + '/\\?redirect=0', + '/index\\.php', + '/course/view\\.php', + '\\/login/index\\.php', + '/mod/page/view\\.php', ]; - const match = url.match(new RegExp(`^https?:\/\/(.*?)(${knownSuffixes.join('|')})`)); + const match = url.match(new RegExp(`^https?://(.*?)(${knownSuffixes.join('|')})`)); if (match) { return match[1]; @@ -184,10 +186,10 @@ export class CoreUrl { */ static sameDomainAndPath(urlA: string, urlB: string): boolean { // Add protocol if missing, the parse function requires it. - if (!urlA.match(/^[^\/:\.\?]*:\/\//)) { + if (!urlA.match(/^[^/:.?]*:\/\//)) { urlA = `https://${urlA}`; } - if (!urlB.match(/^[^\/:\.\?]*:\/\//)) { + if (!urlB.match(/^[^/:.?]*:\/\//)) { urlB = `https://${urlB}`; } @@ -197,4 +199,5 @@ export class CoreUrl { return partsA.domain == partsB.domain && CoreTextUtils.instance.removeEndingSlash(partsA.path) == CoreTextUtils.instance.removeEndingSlash(partsB.path); } + } diff --git a/src/app/singletons/window.ts b/src/app/singletons/window.ts index 4ea9f6d46..2fe01da42 100644 --- a/src/app/singletons/window.ts +++ b/src/app/singletons/window.ts @@ -32,6 +32,11 @@ export type CoreWindowOpenOptions = { */ export class CoreWindow { + // Avoid creating singleton instances. + private constructor() { + // Nothing to do. + } + /** * "Safe" implementation of window.open. It will open the URL without overriding the app. * @@ -73,4 +78,5 @@ export class CoreWindow { } } } + } diff --git a/src/assets/fonts/slash-icon.woff b/src/assets/fonts/slash-icon.woff new file mode 100644 index 000000000..5e02178e4 Binary files /dev/null and b/src/assets/fonts/slash-icon.woff differ diff --git a/src/global.scss b/src/global.scss index d854de84a..36c0be50d 100644 --- a/src/global.scss +++ b/src/global.scss @@ -9,6 +9,7 @@ * https://ionicframework.com/docs/layout/global-stylesheets */ + /* Core CSS required for Ionic components to work properly */ @import "~@ionic/angular/css/core.css"; @@ -24,3 +25,6 @@ @import "~@ionic/angular/css/text-alignment.css"; @import "~@ionic/angular/css/text-transformation.css"; @import "~@ionic/angular/css/flex-utils.css"; + +/* Font awesome */ +@import "~font-awesome/scss/font-awesome.scss"; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index e941e9b2f..fb74b1408 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -24,3 +24,10 @@ declare global { } } + +/** + * Course base definition. + */ +export type CoreCourseBase = { + id: number; // Course Id. +};