// (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 { CoreSites } from '@services/sites'; import { CoreEvents } from '@singletons/events'; import { CoreSite } from '@classes/sites/site'; import { CoreLogger } from '@singletons/logger'; /** * Superclass to help creating delegates */ export class CoreDelegate { /** * Logger instance. */ protected logger: CoreLogger; /** * List of registered handlers. */ protected handlers: { [s: string]: HandlerType } = {}; /** * List of registered handlers enabled for the current site. */ protected enabledHandlers: { [s: string]: HandlerType } = {}; /** * Default handler */ protected defaultHandler?: HandlerType; /** * Time when last updateHandler functions started. */ protected lastUpdateHandlersStart = 0; /** * Feature prefix to check is feature is enabled or disabled in site. * This check is only made if not false. Override on the subclass or override isFeatureDisabled function. */ protected featurePrefix?: string; /** * Name of the property to be used to index the handlers. By default, the handler's name will be used. * If your delegate uses a Moodle component name to identify the handlers, please override this property. * E.g. CoreCourseModuleDelegate uses 'modName' to index the handlers. */ protected handlerNameProperty = 'name'; /** * Set of promises to update a handler, to prevent doing the same operation twice. */ protected updatePromises: {[siteId: string]: {[name: string]: Promise}} = {}; /** * Whether handlers have been initialized. */ protected handlersInitialized = false; /** * Promise to wait for handlers to be initialized. */ protected handlersInitPromise: Promise; /** * Function to resolve the handlers init promise. */ protected handlersInitResolve!: () => void; /** * Constructor of the Delegate. * * @param delegateName Delegate name used for logging purposes. * @param listenSiteEvents Whether to update the handler when a site event occurs (login, site updated, ...). */ constructor(delegateName: string, listenSiteEvents: boolean = true) { this.logger = CoreLogger.getInstance(delegateName); this.handlersInitPromise = new Promise((resolve): void => { this.handlersInitResolve = resolve; }); if (listenSiteEvents) { // Update handlers on this cases. CoreEvents.on(CoreEvents.LOGIN, () => this.updateHandlers()); CoreEvents.on(CoreEvents.SITE_UPDATED, () => this.updateHandlers()); CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => this.updateHandlers()); CoreEvents.on(CoreEvents.SITE_POLICY_AGREED, (data) => { if (data.siteId === CoreSites.getCurrentSiteId()) { this.updateHandlers(); } }); CoreEvents.on(CoreEvents.COMPLETE_REQUIRED_PROFILE_DATA_FINISHED, (data) => { if (data.siteId === CoreSites.getCurrentSiteId()) { this.updateHandlers(); } }); } } /** * Execute a certain function in a enabled handler. * If the handler isn't found or function isn't defined, call the same function in the default handler. * * @param handlerName The handler name. * @param fnName Name of the function to execute. * @param params Parameters to pass to the function. * @returns Function returned value or default value. */ protected executeFunctionOnEnabled(handlerName: string, fnName: string, params?: unknown[]): T | undefined { return this.execute(this.enabledHandlers[handlerName], fnName, params); } /** * Execute a certain function in a handler. * If the handler isn't found or function isn't defined, call the same function in the default handler. * * @param handlerName The handler name. * @param fnName Name of the function to execute. * @param params Parameters to pass to the function. * @returns Function returned value or default value. */ protected executeFunction(handlerName: string, fnName: string, params?: unknown[]): T | undefined { return this.execute(this.handlers[handlerName], fnName, params); } /** * Execute a certain function in a handler. * If the handler isn't found or function isn't defined, call the same function in the default handler. * * @param handler The handler. * @param fnName Name of the function to execute. * @param params Parameters to pass to the function. * @returns Function returned value or default value. */ private execute(handler: HandlerType, fnName: string, params?: unknown[]): T | undefined { if (handler && handler[fnName]) { return handler[fnName].apply(handler, params); } else if (this.defaultHandler && this.defaultHandler[fnName]) { return this.defaultHandler[fnName].apply(this.defaultHandler, params); } } /** * Get a handler. * * @param handlerName The handler name. * @param enabled Only enabled, or any. * @returns Handler. */ protected getHandler(handlerName: string, enabled: boolean = false): HandlerType { return enabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName]; } /** * Gets the handler full name for a given name. This is useful when the handlerNameProperty is different than "name". * E.g. blocks are indexed by blockName. If you call this function passing the blockName it will return the name. * * @param name Name used to indentify the handler. * @returns Full name of corresponding handler. */ getHandlerName(name: string): string { const handler = this.getHandler(name, true); if (!handler) { return ''; } return handler.name; } /** * Check if function exists on a handler. * * @param handlerName The handler name. * @param fnName Name of the function to execute. * @param onlyEnabled If check only enabled handlers or all. * @returns Function returned value or default value. */ protected hasFunction(handlerName: string, fnName: string, onlyEnabled: boolean = true): boolean { const handler = onlyEnabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName]; return handler && typeof handler[fnName] == 'function'; } /** * Check if a handler name has a registered handler (not necessarily enabled). * * @param name The handler name. * @param enabled Only enabled, or any. * @returns If the handler is registered or not. */ hasHandler(name: string, enabled: boolean = false): boolean { return enabled ? this.enabledHandlers[name] !== undefined : this.handlers[name] !== undefined; } /** * Check if the delegate has at least 1 registered handler (not necessarily enabled). * * @returns If there is at least 1 handler. */ hasHandlers(): boolean { return Object.keys(this.handlers).length > 0; } /** * Check if a time belongs to the last update handlers call. * This is to handle the cases where updateHandlers don't finish in the same order as they're called. * * @param time Time to check. * @returns Whether it's the last call. */ isLastUpdateCall(time: number): boolean { if (!this.lastUpdateHandlersStart) { return true; } return time == this.lastUpdateHandlersStart; } /** * Register a handler. * * @param handler The handler delegate object to register. * @returns True when registered, false if already registered. */ registerHandler(handler: HandlerType): boolean { const key = handler[this.handlerNameProperty] || handler.name; if (this.handlers[key] !== undefined) { this.logger.log(`Handler '${handler[this.handlerNameProperty]}' already registered`); return false; } this.logger.log(`Registered handler '${handler[this.handlerNameProperty]}'`); this.handlers[key] = handler; return true; } /** * Update the handler for the current site. * * @param handler The handler to check. * @returns Resolved when done. */ protected updateHandler(handler: HandlerType): Promise { const siteId = CoreSites.getCurrentSiteId(); const currentSite = CoreSites.getCurrentSite(); let promise: Promise; if (this.updatePromises[siteId] && this.updatePromises[siteId][handler.name] !== undefined) { // There's already an update ongoing for this handler, return the promise. return this.updatePromises[siteId][handler.name]; } else if (!this.updatePromises[siteId]) { this.updatePromises[siteId] = {}; } if (!currentSite || this.isFeatureDisabled(handler, currentSite)) { promise = Promise.resolve(false); } else { promise = Promise.resolve(handler.isEnabled()).catch(() => false); } // Checks if the handler is enabled. this.updatePromises[siteId][handler.name] = promise.then((enabled: boolean) => { // Check that site hasn't changed since the check started. if (CoreSites.getCurrentSiteId() === siteId) { const key = handler[this.handlerNameProperty] || handler.name; if (enabled) { this.enabledHandlers[key] = handler; } else { delete this.enabledHandlers[key]; } } return; }).finally(() => { // Update finished, delete the promise. delete this.updatePromises[siteId][handler.name]; }); return this.updatePromises[siteId][handler.name]; } /** * Check if feature is enabled or disabled in the site, depending on the feature prefix and the handler name. * * @param handler Handler to check. * @param site Site to check. * @returns Whether is enabled or disabled in site. */ protected isFeatureDisabled(handler: HandlerType, site: CoreSite): boolean { return this.featurePrefix !== undefined && site.isFeatureDisabled(this.featurePrefix + handler.name); } /** * Update the handlers for the current site. * * @returns Resolved when done. */ async updateHandlers(): Promise { const promises: Promise[] = []; const now = Date.now(); this.logger.debug('Updating handlers for current site.'); this.lastUpdateHandlersStart = now; // Loop over all the handlers. for (const name in this.handlers) { promises.push(this.updateHandler(this.handlers[name])); } 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(); this.updateData(); } } /** * Update handlers Data. * Override this function to update handlers data. */ updateData(): void { // To be overridden. } } /** * Base interface for any delegate. */ export interface CoreDelegateHandler { /** * Name of the handler, or name and sub context (AddonMessages, AddonMessages:blockContact, ...). * This name will be used to check if the feature is disabled. */ name: string; /** * Whether or not the handler is enabled on a site level. * * @returns Whether or not the handler is enabled on a site level. */ isEnabled(): Promise; } /** * Data returned by the delegate for each handler to be displayed. */ export interface CoreDelegateToDisplay { /** * Name of the handler. */ name?: string; /** * Priority of the handler. */ priority?: number; } /** * Base interface for a core delegate needed to be displayed. */ export interface CoreDelegateDisplayHandler extends CoreDelegateHandler { /** * The highest priority is displayed first. */ priority?: number; /** * Returns the data needed to render the handler. * * @returns Data. */ getDisplayData(): HandlerData; }