diff --git a/src/core/classes/database/database-table-proxy.ts b/src/core/classes/database/database-table-proxy.ts index d9d676bcc..7e183f95a 100644 --- a/src/core/classes/database/database-table-proxy.ts +++ b/src/core/classes/database/database-table-proxy.ts @@ -23,6 +23,7 @@ import { CoreDatabaseConditions, GetDBRecordPrimaryKey, CoreDatabaseQueryOptions, + CoreDatabaseTableConstructor, } from './database-table'; import { CoreDebugDatabaseTable } from './debug-database-table'; import { CoreEagerDatabaseTable } from './eager-database-table'; @@ -42,6 +43,14 @@ export class CoreDatabaseTableProxy< protected config: CoreDatabaseConfiguration; protected target = asyncInstance>(); protected environmentObserver?: CoreEventObserver; + protected targetConstructors: Record< + CoreDatabaseCachingStrategy, + CoreDatabaseTableConstructor + > = { + [CoreDatabaseCachingStrategy.Eager]: CoreEagerDatabaseTable, + [CoreDatabaseCachingStrategy.Lazy]: CoreLazyDatabaseTable, + [CoreDatabaseCachingStrategy.None]: CoreDatabaseTable, + }; constructor( config: Partial, @@ -58,7 +67,13 @@ export class CoreDatabaseTableProxy< * @inheritdoc */ async initialize(): Promise { - this.environmentObserver = CoreEvents.on(CoreConfigProvider.ENVIRONMENT_UPDATED, () => this.updateTarget()); + this.environmentObserver = CoreEvents.on(CoreConfigProvider.ENVIRONMENT_UPDATED, async () => { + if (!(await this.shouldUpdateTarget())) { + return; + } + + this.updateTarget(); + }); await this.updateTarget(); } @@ -202,6 +217,20 @@ export class CoreDatabaseTableProxy< this.target.setInstance(newTarget); } + /** + * Check whether the underlying target should be updated. + * + * @returns Whether target should be updated. + */ + protected async shouldUpdateTarget(): Promise { + const config = await this.getRuntimeConfig(); + const target = await this.target.getInstance(); + const originalTarget = target instanceof CoreDebugDatabaseTable ? target.getTarget() : target; + + return (config.debug && target === originalTarget) + || originalTarget?.constructor !== this.targetConstructors[config.cachingStrategy]; + } + /** * Create proxy target. * @@ -221,14 +250,9 @@ export class CoreDatabaseTableProxy< * @returns Database table. */ protected createTable(cachingStrategy: CoreDatabaseCachingStrategy): CoreDatabaseTable { - switch (cachingStrategy) { - case CoreDatabaseCachingStrategy.Eager: - return new CoreEagerDatabaseTable(this.database, this.tableName, this.primaryKeyColumns); - case CoreDatabaseCachingStrategy.Lazy: - return new CoreLazyDatabaseTable(this.database, this.tableName, this.primaryKeyColumns); - case CoreDatabaseCachingStrategy.None: - return new CoreDatabaseTable(this.database, this.tableName, this.primaryKeyColumns); - } + const DatabaseTable = this.targetConstructors[cachingStrategy]; + + return new DatabaseTable(this.database, this.tableName, this.primaryKeyColumns); } } diff --git a/src/core/classes/database/database-table.ts b/src/core/classes/database/database-table.ts index 6ff8f92d5..f198b333e 100644 --- a/src/core/classes/database/database-table.ts +++ b/src/core/classes/database/database-table.ts @@ -336,6 +336,23 @@ export class CoreDatabaseTable< } +/** + * CoreDatabaseTable constructor. + */ +export type CoreDatabaseTableConstructor< + DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, + PrimaryKeyColumn extends keyof DBRecord = 'id', + PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey +> = { + + new ( + database: SQLiteDB, + tableName: string, + primaryKeyColumns?: PrimaryKeyColumn[] + ): CoreDatabaseTable; + +}; + /** * Infer primary key type from database record and primary key column types. */ diff --git a/src/core/classes/database/debug-database-table.ts b/src/core/classes/database/debug-database-table.ts index 3e236c5c3..d7e58b56b 100644 --- a/src/core/classes/database/debug-database-table.ts +++ b/src/core/classes/database/debug-database-table.ts @@ -43,6 +43,13 @@ export class CoreDebugDatabaseTable< this.logger = CoreLogger.getInstance(`CoreDatabase[${this.tableName}]`); } + /** + * Get underlying table instance. + */ + getTarget(): CoreDatabaseTable { + return this.target; + } + /** * @inheritdoc */ diff --git a/src/core/features/login/pages/credentials/credentials.ts b/src/core/features/login/pages/credentials/credentials.ts index f8777a1d3..6e805abe7 100644 --- a/src/core/features/login/pages/credentials/credentials.ts +++ b/src/core/features/login/pages/credentials/credentials.ts @@ -333,7 +333,14 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.viewLeft = true; - CoreEvents.trigger(CoreEvents.LOGIN_SITE_UNCHECKED, { config: this.siteConfig }, this.siteId); + CoreEvents.trigger( + CoreEvents.LOGIN_SITE_UNCHECKED, + { + config: this.siteConfig, + loginSuccessful: !!this.siteId, + }, + this.siteId, + ); this.valueChangeSubscription?.unsubscribe(); } diff --git a/src/core/features/login/pages/reconnect/reconnect.ts b/src/core/features/login/pages/reconnect/reconnect.ts index 0a884a552..f40c6fd05 100644 --- a/src/core/features/login/pages/reconnect/reconnect.ts +++ b/src/core/features/login/pages/reconnect/reconnect.ts @@ -57,6 +57,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { protected viewLeft = false; protected eventThrown = false; protected redirectData?: CoreRedirectPayload; + protected loginSuccessful = false; constructor( protected fb: FormBuilder, @@ -117,7 +118,14 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.viewLeft = true; - CoreEvents.trigger(CoreEvents.LOGIN_SITE_UNCHECKED, { config: this.siteConfig }, this.siteId); + CoreEvents.trigger( + CoreEvents.LOGIN_SITE_UNCHECKED, + { + config: this.siteConfig, + loginSuccessful: this.loginSuccessful, + }, + this.siteId, + ); } /** @@ -214,6 +222,8 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { this.credForm.controls['password'].reset(); // Go to the site initial page. + this.loginSuccessful = true; + await CoreNavigator.navigateToSiteHome({ params: this.redirectData, }); diff --git a/src/core/features/styles/services/styles.ts b/src/core/features/styles/services/styles.ts index 7bb761f42..fd07b1f6c 100644 --- a/src/core/features/styles/services/styles.ts +++ b/src/core/features/styles/services/styles.ts @@ -108,19 +108,11 @@ export class CoreStylesService { * Listen events. */ protected listenEvents(): void { - let addingSite: string | undefined; - // When a new site is added to the app, add its styles. CoreEvents.on(CoreEvents.SITE_ADDED, async (data) => { - addingSite = data.siteId; - try { await this.addSite(data.siteId); - if (addingSite == data.siteId) { - addingSite = undefined; - } - // User has logged in, remove tmp styles and enable loaded styles. if (data.siteId == CoreSites.getCurrentSiteId()) { this.unloadTmpStyles(); @@ -164,10 +156,10 @@ export class CoreStylesService { }); // Unload temporary styles when site config is "unchecked" in login. - CoreEvents.on(CoreEvents.LOGIN_SITE_UNCHECKED, (data) => { - if (data.siteId && data.siteId === addingSite) { - // The tmp styles are from a site that is being added permanently. - // Wait for the final site styles to be loaded before removing the tmp styles so there is no blink effect. + CoreEvents.on(CoreEvents.LOGIN_SITE_UNCHECKED, ({ loginSuccessful }) => { + if (loginSuccessful) { + // The tmp styles have been added for a site we've logged into, so we'll wait for the final + // site styles to be loaded before removing the tmp styles so there is no blink effect. return; } diff --git a/src/core/services/config.ts b/src/core/services/config.ts index f8d65ee23..a76ec716b 100644 --- a/src/core/services/config.ts +++ b/src/core/services/config.ts @@ -120,6 +120,19 @@ export class CoreConfigProvider { } } + /** + * Get an app setting directly from the database, without using any optimizations.. + * + * @param name The config name. + * @return Resolves upon success along with the config data. Reject on failure. + */ + async getFromDB(name: string): Promise { + const db = CoreApp.getDB(); + const record = await db.getRecord(CONFIG_TABLE_NAME, { name }); + + return record.value; + } + /** * Check whether the given app setting exists. * @@ -151,9 +164,14 @@ export class CoreConfigProvider { * Update config with the given values. * * @param config Config updates. + * @param reset Whether to reset environment before applying the patch. */ - patchEnvironment(config: Partial): void { - this.defaultEnvironment = this.defaultEnvironment ?? CoreConstants.CONFIG; + patchEnvironment(config: Partial, reset: boolean = false): void { + this.defaultEnvironment = this.defaultEnvironment ?? { ...CoreConstants.CONFIG }; + + if (reset) { + this.resetEnvironmentSilently(); + } Object.assign(CoreConstants.CONFIG, config); CoreEvents.trigger(CoreConfigProvider.ENVIRONMENT_UPDATED, CoreConstants.CONFIG); @@ -169,8 +187,7 @@ export class CoreConfigProvider { return; } - Object.keys(CoreConstants.CONFIG).forEach(key => delete CoreConstants.CONFIG[key]); - Object.assign(CoreConstants.CONFIG, this.defaultEnvironment); + this.resetEnvironmentSilently(); CoreEvents.trigger(CoreConfigProvider.ENVIRONMENT_UPDATED, CoreConstants.CONFIG); } @@ -185,6 +202,20 @@ export class CoreConfigProvider { this.patchEnvironment(JSON.parse(CoreUtils.getCookie('MoodleAppConfig') ?? '{}')); } + /** + * Reset config values to its original state without emitting any events. + */ + protected resetEnvironmentSilently(): void { + if (!this.defaultEnvironment) { + // The environment config hasn't been modified; there's not need to reset. + + return; + } + + Object.keys(CoreConstants.CONFIG).forEach(key => delete CoreConstants.CONFIG[key]); + Object.assign(CoreConstants.CONFIG, this.defaultEnvironment); + } + } export const CoreConfig = makeSingleton(CoreConfigProvider); diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 1dedfd9cf..e865afc2d 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -66,6 +66,7 @@ import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { CoreConfig } from './config'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); +export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id'; /* * Service to manage and interact with sites. @@ -1032,13 +1033,31 @@ export class CoreSitesProvider { try { const data = await this.sitesTable.getOneByPrimaryKey({ id: siteId }); - return this.makeSiteFromSiteListEntry(data); + return this.addSiteFromSiteListEntry(data); } catch { throw new CoreError('SiteId not found'); } } } + /** + * Get a site directly from the database, without using any optimizations. + * + * @param siteId Site id. + * @return Site. + */ + async getSiteFromDB(siteId: string): Promise { + const db = CoreApp.getDB(); + + try { + const record = await db.getRecord(SITES_TABLE_NAME, { id: siteId }); + + return this.makeSiteFromSiteListEntry(record); + } catch { + throw new CoreError('SiteId not found'); + } + } + /** * Finds a site with a certain URL. It will return the first site found. * @@ -1052,7 +1071,7 @@ export class CoreSitesProvider { return this.sites[data.id]; } - return this.makeSiteFromSiteListEntry(data); + return this.addSiteFromSiteListEntry(data); } /** @@ -1061,8 +1080,25 @@ export class CoreSitesProvider { * @param entry Site list entry. * @return Promised resolved with the created site. */ - makeSiteFromSiteListEntry(entry: SiteDBEntry): Promise { + addSiteFromSiteListEntry(entry: SiteDBEntry): Promise { // Parse info and config. + const site = this.makeSiteFromSiteListEntry(entry); + + return this.migrateSiteSchemas(site).then(() => { + // Set site after migrating schemas, or a call to getSite could get the site while tables are being created. + this.sites[entry.id] = site; + + return site; + }); + } + + /** + * Make a site instance from a database entry. + * + * @param entry Site database entry. + * @return Site. + */ + makeSiteFromSiteListEntry(entry: SiteDBEntry): CoreSite { const info = entry.info ? CoreTextUtils.parseJSON(entry.info) : undefined; const config = entry.config ? CoreTextUtils.parseJSON(entry.config) : undefined; @@ -1077,12 +1113,7 @@ export class CoreSitesProvider { ); site.setOAuthId(entry.oauthId || undefined); - return this.migrateSiteSchemas(site).then(() => { - // Set site after migrating schemas, or a call to getSite could get the site while tables are being created. - this.sites[entry.id] = site; - - return site; - }); + return site; } /** @@ -1231,7 +1262,7 @@ export class CoreSitesProvider { * @return Promise resolved when current site is stored. */ async login(siteId: string): Promise { - await CoreConfig.set('current_site_id', siteId); + await CoreConfig.set(CORE_SITE_CURRENT_SITE_ID_CONFIG, siteId); CoreEvents.trigger(CoreEvents.LOGIN, {}, siteId); } @@ -1477,7 +1508,7 @@ export class CoreSitesProvider { siteEntries.forEach((site) => { if (!this.sites[site.id]) { - promises.push(this.makeSiteFromSiteListEntry(site)); + promises.push(this.addSiteFromSiteListEntry(site)); } if (this.sites[site.id].containsUrl(url)) { @@ -1504,7 +1535,7 @@ export class CoreSitesProvider { async getStoredCurrentSiteId(): Promise { await this.migrateCurrentSiteLegacyTable(); - return CoreConfig.get('current_site_id'); + return CoreConfig.get(CORE_SITE_CURRENT_SITE_ID_CONFIG); } /** @@ -1513,7 +1544,7 @@ export class CoreSitesProvider { * @return Promise resolved when done. */ async removeStoredCurrentSite(): Promise { - await CoreConfig.delete('current_site_id'); + await CoreConfig.delete(CORE_SITE_CURRENT_SITE_ID_CONFIG); } /** @@ -1789,7 +1820,7 @@ export class CoreSitesProvider { const { siteId } = await db.getRecord<{ siteId: string }>('current_site'); - await CoreConfig.set('current_site_id', siteId); + await CoreConfig.set(CORE_SITE_CURRENT_SITE_ID_CONFIG, siteId); await CoreApp.deleteTableSchema('current_site'); await db.dropTable('current_site'); } finally { diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index 547acc270..84b8522d3 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -56,6 +56,7 @@ export interface CoreEventsData { [CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData; [CoreEvents.IAB_LOAD_START]: InAppBrowserEvent; [CoreEvents.LOGIN_SITE_CHECKED]: CoreEventLoginSiteCheckedData; + [CoreEvents.LOGIN_SITE_UNCHECKED]: CoreEventLoginSiteUncheckedData; [CoreEvents.SEND_ON_ENTER_CHANGED]: CoreEventSendOnEnterChangedData; [CoreEvents.COMPONENT_FILE_ACTION]: CoreFilepoolComponentFileEventData; [CoreEvents.FILE_SHARED]: CoreEventFileSharedData; @@ -400,6 +401,14 @@ export type CoreEventLoginSiteCheckedData = { config: CoreSitePublicConfigResponse; }; +/** + * Data passed to LOGIN_SITE_UNCHECKED event. + */ +export type CoreEventLoginSiteUncheckedData = { + config?: CoreSitePublicConfigResponse; + loginSuccessful: boolean; +}; + /** * Data passed to SEND_ON_ENTER_CHANGED event. */