MOBILE-3620 filter: Implement filter services
parent
1559bf27b7
commit
b35ce619fb
|
@ -124,7 +124,7 @@ export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
|
||||||
* @return Function returned value or default value.
|
* @return Function returned value or default value.
|
||||||
*/
|
*/
|
||||||
protected executeFunction<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T | undefined {
|
protected executeFunction<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T | undefined {
|
||||||
return this.execute(this.handlers[handlerName], fnName, params);
|
return this.execute<T>(this.handlers[handlerName], fnName, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,290 @@
|
||||||
|
// (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, ViewContainerRef } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreFilterFilter, CoreFilterFormatTextOptions } from './filter';
|
||||||
|
import { CoreFilterDefaultHandler } from './handlers/default-filter';
|
||||||
|
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||||
|
import { CoreSite } from '@classes/site';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that all filter handlers must implement.
|
||||||
|
*/
|
||||||
|
export interface CoreFilterHandler extends CoreDelegateHandler {
|
||||||
|
/**
|
||||||
|
* Name of the filter. It should match the "filter" field returned in core_filters_get_available_in_context.
|
||||||
|
*/
|
||||||
|
filterName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter some text.
|
||||||
|
*
|
||||||
|
* @param text The text to filter.
|
||||||
|
* @param filter The filter.
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Filtered text (or promise resolved with the filtered text).
|
||||||
|
*/
|
||||||
|
filter(text: string, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string): string | Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle HTML. This function is called after "filter", and it will receive an HTMLElement containing the text that was
|
||||||
|
* filtered.
|
||||||
|
*
|
||||||
|
* @param container The HTML container to handle.
|
||||||
|
* @param filter The filter.
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param viewContainerRef The ViewContainerRef where the container is.
|
||||||
|
* @param component Component.
|
||||||
|
* @param componentId Component ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return If async, promise resolved when done.
|
||||||
|
*/
|
||||||
|
handleHtml?(
|
||||||
|
container: HTMLElement,
|
||||||
|
filter: CoreFilterFilter,
|
||||||
|
options: CoreFilterFormatTextOptions,
|
||||||
|
viewContainerRef: ViewContainerRef,
|
||||||
|
component?: string,
|
||||||
|
componentId?: string | number,
|
||||||
|
siteId?: string,
|
||||||
|
): void | Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the filter should be applied in a certain site based on some filter options.
|
||||||
|
*
|
||||||
|
* @param options Options.
|
||||||
|
* @param site Site.
|
||||||
|
* @return Whether filter should be applied.
|
||||||
|
*/
|
||||||
|
shouldBeApplied(options: CoreFilterFormatTextOptions, site?: CoreSite): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate to register filters.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreFilterDelegateService extends CoreDelegate<CoreFilterHandler> {
|
||||||
|
|
||||||
|
protected featurePrefix = 'CoreFilterDelegate_';
|
||||||
|
protected handlerNameProperty = 'filterName';
|
||||||
|
|
||||||
|
constructor(protected defaultHandler: CoreFilterDefaultHandler) {
|
||||||
|
super('CoreFilterDelegate', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a list of filters to some content.
|
||||||
|
*
|
||||||
|
* @param text The text to filter.
|
||||||
|
* @param filters Filters to apply.
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param skipFilters Names of filters that shouldn't be applied.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the filtered text.
|
||||||
|
*/
|
||||||
|
async filterText(
|
||||||
|
text: string,
|
||||||
|
filters?: CoreFilterFilter[],
|
||||||
|
options?: CoreFilterFormatTextOptions,
|
||||||
|
skipFilters?: string[],
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
|
||||||
|
// Wait for filters to be initialized.
|
||||||
|
await this.handlersInitPromise;
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
filters = filters || [];
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
for (let i = 0; i < filters.length; i++) {
|
||||||
|
const filter = filters[i];
|
||||||
|
if (!this.isEnabledAndShouldApply(filter, options, site, skipFilters)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newText = await this.executeFunctionOnEnabled<string>(
|
||||||
|
filter.filter,
|
||||||
|
'filter',
|
||||||
|
[text, filter, options, siteId],
|
||||||
|
);
|
||||||
|
|
||||||
|
text = newText || text;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error applying filter' + filter.filter, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove <nolink> tags for XHTML compatibility.
|
||||||
|
text = text.replace(/<\/?nolink>/gi, '');
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filters that have an enabled handler.
|
||||||
|
*
|
||||||
|
* @param contextLevel Context level of the filters.
|
||||||
|
* @param instanceId Instance ID.
|
||||||
|
* @return Filters.
|
||||||
|
*/
|
||||||
|
getEnabledFilters(contextLevel: string, instanceId: number): CoreFilterFilter[] {
|
||||||
|
const filters: CoreFilterFilter[] = [];
|
||||||
|
|
||||||
|
for (const name in this.enabledHandlers) {
|
||||||
|
const handler = <CoreFilterHandler> this.enabledHandlers[name];
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
contextid: -1,
|
||||||
|
contextlevel: contextLevel,
|
||||||
|
filter: handler.filterName,
|
||||||
|
inheritedstate: 1,
|
||||||
|
instanceid: instanceId,
|
||||||
|
localstate: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let filters handle an HTML element.
|
||||||
|
*
|
||||||
|
* @param container The HTML container to handle.
|
||||||
|
* @param filters Filters to apply.
|
||||||
|
* @param viewContainerRef The ViewContainerRef where the container is.
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param skipFilters Names of filters that shouldn't be applied.
|
||||||
|
* @param component Component.
|
||||||
|
* @param componentId Component ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async handleHtml(
|
||||||
|
container: HTMLElement,
|
||||||
|
filters: CoreFilterFilter[],
|
||||||
|
viewContainerRef?: ViewContainerRef,
|
||||||
|
options?: CoreFilterFormatTextOptions,
|
||||||
|
skipFilters?: string[],
|
||||||
|
component?: string,
|
||||||
|
componentId?: string | number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
|
||||||
|
// Wait for filters to be initialized.
|
||||||
|
await this.handlersInitPromise;
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
filters = filters || [];
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
for (let i = 0; i < filters.length; i++) {
|
||||||
|
const filter = filters[i];
|
||||||
|
if (!this.isEnabledAndShouldApply(filter, options, site, skipFilters)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.executeFunctionOnEnabled<void>(
|
||||||
|
filter.filter,
|
||||||
|
'handleHtml',
|
||||||
|
[container, filter, options, viewContainerRef, component, componentId, siteId],
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error handling HTML' + filter.filter, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a filter is enabled and should be applied.
|
||||||
|
*
|
||||||
|
* @param filters Filters to apply.
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param site Site.
|
||||||
|
* @param skipFilters Names of filters that shouldn't be applied.
|
||||||
|
* @return Whether the filter is enabled and should be applied.
|
||||||
|
*/
|
||||||
|
isEnabledAndShouldApply(
|
||||||
|
filter: CoreFilterFilter,
|
||||||
|
options: CoreFilterFormatTextOptions,
|
||||||
|
site: CoreSite,
|
||||||
|
skipFilters?: string[],
|
||||||
|
): boolean {
|
||||||
|
|
||||||
|
if (filter.localstate == -1 || (filter.localstate == 0 && filter.inheritedstate == -1)) {
|
||||||
|
// Filter is disabled, ignore it.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.shouldFilterBeApplied(filter, options, site)) {
|
||||||
|
// Filter shouldn't be applied.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipFilters && skipFilters.indexOf(filter.filter) != -1) {
|
||||||
|
// Skip this filter.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if at least 1 filter should be applied in a certain site and with certain options.
|
||||||
|
*
|
||||||
|
* @param filter Filter to check.
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param site Site. If not defined, current site.
|
||||||
|
* @return Promise resolved with true: whether the filter should be applied.
|
||||||
|
*/
|
||||||
|
async shouldBeApplied(filters: CoreFilterFilter[], options: CoreFilterFormatTextOptions, site?: CoreSite): Promise<boolean> {
|
||||||
|
// Wait for filters to be initialized.
|
||||||
|
await this.handlersInitPromise;
|
||||||
|
|
||||||
|
for (let i = 0; i < filters.length; i++) {
|
||||||
|
if (this.shouldFilterBeApplied(filters[i], options, site)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a filter should be applied in a certain site and with certain options.
|
||||||
|
*
|
||||||
|
* @param filter Filter to check.
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param site Site. If not defined, current site.
|
||||||
|
* @return Whether the filter should be applied.
|
||||||
|
*/
|
||||||
|
protected shouldFilterBeApplied(filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, site?: CoreSite): boolean {
|
||||||
|
if (!this.hasHandler(filter.filter, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!(this.executeFunctionOnEnabled<boolean>(filter.filter, 'shouldBeApplied', [options, site]));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreFilterDelegate extends makeSingleton(CoreFilterDelegateService) {}
|
|
@ -0,0 +1,358 @@
|
||||||
|
// (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 { CoreApp } from '@services/app';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreFilterDelegate } from './filter-delegate';
|
||||||
|
import {
|
||||||
|
CoreFilter,
|
||||||
|
CoreFilterFilter,
|
||||||
|
CoreFilterFormatTextOptions,
|
||||||
|
CoreFilterClassifiedFilters,
|
||||||
|
CoreFiltersGetAvailableInContextWSParamContext,
|
||||||
|
} from './filter';
|
||||||
|
// import { CoreCourseProvider } from '@features/course/providers/course';
|
||||||
|
// import { CoreCoursesProvider } from '@features/courses/providers/courses';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
|
||||||
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
import { CoreSite } from '@classes/site';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper service to provide filter functionalities.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreFilterHelperProvider {
|
||||||
|
|
||||||
|
protected logger: CoreLogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a module context is requested, we request all the modules in a course to decrease WS calls. If there are a lot of
|
||||||
|
* modules, checking the cache of all contexts can be really slow, so we use this memory cache to speed up the process.
|
||||||
|
*/
|
||||||
|
protected moduleContextsCache: {
|
||||||
|
[siteId: string]: {
|
||||||
|
[courseId: number]: {
|
||||||
|
[contextLevel: string]: {
|
||||||
|
contexts: CoreFilterClassifiedFilters;
|
||||||
|
time: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logger = CoreLogger.getInstance('CoreFilterHelperProvider');
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.WS_CACHE_INVALIDATED, (data: CoreEventSiteData) => {
|
||||||
|
delete this.moduleContextsCache[data.siteId || ''];
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.SITE_STORAGE_DELETED, (data: CoreEventSiteData) => {
|
||||||
|
delete this.moduleContextsCache[data.siteId || ''];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the contexts of all blocks in a course.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the contexts.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getBlocksContexts(courseId: number, siteId?: string): Promise<CoreFiltersGetAvailableInContextWSParamContext[]> {
|
||||||
|
return [];
|
||||||
|
// @todo
|
||||||
|
// const blocks = await this.courseProvider.getCourseBlocks(courseId, siteId);
|
||||||
|
|
||||||
|
// const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = [];
|
||||||
|
|
||||||
|
// blocks.forEach((block) => {
|
||||||
|
// contexts.push({
|
||||||
|
// contextlevel: 'block',
|
||||||
|
// instanceid: block.instanceid,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return contexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get some filters from memory cache. If not in cache, get them and store them in cache.
|
||||||
|
*
|
||||||
|
* @param contextLevel The context level.
|
||||||
|
* @param instanceId Instance ID related to the context.
|
||||||
|
* @param options Options for format text.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the filters.
|
||||||
|
*/
|
||||||
|
protected async getCacheableFilters(
|
||||||
|
contextLevel: string,
|
||||||
|
instanceId: number,
|
||||||
|
getFilters: () => Promise<CoreFiltersGetAvailableInContextWSParamContext[]>,
|
||||||
|
options: CoreFilterFormatTextOptions,
|
||||||
|
site: CoreSite,
|
||||||
|
): Promise<CoreFilterFilter[]> {
|
||||||
|
|
||||||
|
// Check the memory cache first.
|
||||||
|
const result = this.getFromMemoryCache(options.courseId ?? -1, contextLevel, instanceId, site);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteId = site.getId();
|
||||||
|
|
||||||
|
const contexts = await getFilters();
|
||||||
|
|
||||||
|
const filters = await CoreFilter.instance.getAvailableInContexts(contexts, siteId);
|
||||||
|
|
||||||
|
this.storeInMemoryCache(options.courseId ?? -1, contextLevel, filters, siteId);
|
||||||
|
|
||||||
|
return filters[contextLevel][instanceId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If user is enrolled in the course, return contexts of all enrolled courses to decrease number of WS requests.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the contexts.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getCourseContexts(courseId: number, siteId?: string): Promise<CoreFiltersGetAvailableInContextWSParamContext[]> {
|
||||||
|
// @todo
|
||||||
|
return [];
|
||||||
|
// const courseIds = await this.coursesProvider.getCourseIdsIfEnrolled(courseId, siteId);
|
||||||
|
|
||||||
|
// const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = [];
|
||||||
|
|
||||||
|
// courseIds.forEach((courseId) => {
|
||||||
|
// contexts.push({
|
||||||
|
// contextlevel: 'course',
|
||||||
|
// instanceid: courseId
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return contexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the contexts of all course modules in a course.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the contexts.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getCourseModulesContexts(courseId: number, siteId?: string): Promise<CoreFiltersGetAvailableInContextWSParamContext[]> {
|
||||||
|
// @todo
|
||||||
|
return [];
|
||||||
|
|
||||||
|
// const sections = await this.courseProvider.getSections(courseId, false, true, undefined, siteId);
|
||||||
|
|
||||||
|
// const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = [];
|
||||||
|
|
||||||
|
// sections.forEach((section) => {
|
||||||
|
// if (section.modules) {
|
||||||
|
// section.modules.forEach((module) => {
|
||||||
|
// if (module.uservisible) {
|
||||||
|
// contexts.push({
|
||||||
|
// contextlevel: 'module',
|
||||||
|
// instanceid: module.id
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return contexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters in a certain context, performing some checks like the site version.
|
||||||
|
* It's recommended to use this function instead of canGetFilters + getEnabledFilters because this function will check if
|
||||||
|
* it's really needed to call the WS.
|
||||||
|
*
|
||||||
|
* @param contextLevel The context level.
|
||||||
|
* @param instanceId Instance ID related to the context.
|
||||||
|
* @param options Options for format text.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the filters.
|
||||||
|
*/
|
||||||
|
async getFilters(
|
||||||
|
contextLevel: string,
|
||||||
|
instanceId: number,
|
||||||
|
options?: CoreFilterFormatTextOptions,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<CoreFilterFilter[]> {
|
||||||
|
options = options || {};
|
||||||
|
options.contextLevel = contextLevel;
|
||||||
|
options.instanceId = instanceId;
|
||||||
|
options.filter = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
siteId = site.getId();
|
||||||
|
|
||||||
|
const canGet = await CoreFilter.instance.canGetFilters(siteId);
|
||||||
|
if (!canGet) {
|
||||||
|
options.filter = true;
|
||||||
|
|
||||||
|
// We cannot check which filters are available, apply them all.
|
||||||
|
return CoreFilterDelegate.instance.getEnabledFilters(contextLevel, instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasFilters = true;
|
||||||
|
|
||||||
|
if (contextLevel == 'system' || (contextLevel == 'course' && instanceId == site.getSiteHomeId())) {
|
||||||
|
// No need to check the site filters because we're requesting the same context, so we'd do the same twice.
|
||||||
|
} else {
|
||||||
|
// Check if site has any filter to treat.
|
||||||
|
hasFilters = await this.siteHasFiltersToTreat(options, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFilters) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
options.filter = true;
|
||||||
|
|
||||||
|
if (contextLevel == 'module' && options.courseId) {
|
||||||
|
// Get all the modules filters with a single call to decrease the number of WS calls.
|
||||||
|
const getFilters = this.getCourseModulesContexts.bind(this, options.courseId, siteId);
|
||||||
|
|
||||||
|
return this.getCacheableFilters(contextLevel, instanceId, getFilters, options, site);
|
||||||
|
|
||||||
|
} else if (contextLevel == 'course') {
|
||||||
|
// If enrolled, get all enrolled courses filters with a single call to decrease number of WS calls.
|
||||||
|
const getFilters = this.getCourseContexts.bind(this, instanceId, siteId);
|
||||||
|
|
||||||
|
return this.getCacheableFilters(contextLevel, instanceId, getFilters, options, site);
|
||||||
|
} else if (contextLevel == 'block' && options.courseId) { // @todo && this.courseProvider.canGetCourseBlocks(site)
|
||||||
|
// Get all the course blocks filters with a single call to decrease number of WS calls.
|
||||||
|
const getFilters = this.getBlocksContexts.bind(this, options.courseId, siteId);
|
||||||
|
|
||||||
|
return this.getCacheableFilters(contextLevel, instanceId, getFilters, options, site);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreFilter.instance.getAvailableInContext(contextLevel, instanceId, siteId);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error getting filters, return an empty array', error, contextLevel, instanceId);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filters and format text.
|
||||||
|
*
|
||||||
|
* @param text Text to filter.
|
||||||
|
* @param contextLevel The context level.
|
||||||
|
* @param instanceId Instance ID related to the context.
|
||||||
|
* @param options Options for format text.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the formatted text and the filters.
|
||||||
|
*/
|
||||||
|
async getFiltersAndFormatText(
|
||||||
|
text: string,
|
||||||
|
contextLevel: string,
|
||||||
|
instanceId: number,
|
||||||
|
options?: CoreFilterFormatTextOptions,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<{text: string; filters: CoreFilterFilter[]}> {
|
||||||
|
|
||||||
|
const filters = await this.getFilters(contextLevel, instanceId, options, siteId);
|
||||||
|
|
||||||
|
text = await CoreFilter.instance.formatText(text, options, filters, siteId);
|
||||||
|
|
||||||
|
return { text, filters: filters };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get module context filters from the memory cache.
|
||||||
|
*
|
||||||
|
* @param courseId Course the module belongs to.
|
||||||
|
* @param contextLevel Context level.
|
||||||
|
* @param instanceId Instance ID.
|
||||||
|
* @param site Site.
|
||||||
|
* @return The filters, undefined if not found.
|
||||||
|
*/
|
||||||
|
protected getFromMemoryCache(
|
||||||
|
courseId: number,
|
||||||
|
contextLevel: string,
|
||||||
|
instanceId: number,
|
||||||
|
site: CoreSite,
|
||||||
|
): CoreFilterFilter[] | undefined {
|
||||||
|
|
||||||
|
const siteId = site.getId();
|
||||||
|
|
||||||
|
// Check if we have the context in the memory cache.
|
||||||
|
if (!this.moduleContextsCache[siteId]?.[courseId]?.[contextLevel]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedData = this.moduleContextsCache[siteId][courseId][contextLevel];
|
||||||
|
|
||||||
|
if (!CoreApp.instance.isOnline() || Date.now() <= cachedData.time + site.getExpirationDelay(CoreSite.FREQUENCY_RARELY)) {
|
||||||
|
// We can use cache, return the filters if found.
|
||||||
|
return cachedData.contexts[contextLevel] && cachedData.contexts[contextLevel][instanceId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if site has available any filter that should be treated by the app.
|
||||||
|
*
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean: whether it has filters to treat.
|
||||||
|
*/
|
||||||
|
async siteHasFiltersToTreat(options?: CoreFilterFormatTextOptions, siteId?: string): Promise<boolean> {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
// Get filters at site level.
|
||||||
|
const filters = await CoreFilter.instance.getAvailableInContext('system', 0, site.getId());
|
||||||
|
|
||||||
|
return CoreFilterDelegate.instance.shouldBeApplied(filters, options, site);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store filters in the memory cache.
|
||||||
|
*
|
||||||
|
* @param contexts Filters to store, classified by contextlevel and instanceid
|
||||||
|
* @param siteId Site ID.
|
||||||
|
*/
|
||||||
|
protected storeInMemoryCache(
|
||||||
|
courseId: number,
|
||||||
|
contextLevel: string,
|
||||||
|
contexts: CoreFilterClassifiedFilters,
|
||||||
|
siteId: string,
|
||||||
|
): void {
|
||||||
|
|
||||||
|
this.moduleContextsCache[siteId] = this.moduleContextsCache[siteId] || {};
|
||||||
|
this.moduleContextsCache[siteId][courseId] = this.moduleContextsCache[siteId][courseId] || {};
|
||||||
|
this.moduleContextsCache[siteId][courseId][contextLevel] = {
|
||||||
|
contexts: contexts,
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreFilterHelper extends makeSingleton(CoreFilterHelperProvider) {}
|
|
@ -0,0 +1,543 @@
|
||||||
|
// (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 { CoreApp } from '@services/app';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreSite } from '@classes/site';
|
||||||
|
import { CoreWSExternalWarning } from '@services/ws';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreFilterDelegate } from './filter-delegate';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
|
||||||
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to provide filter functionalities.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreFilterProvider {
|
||||||
|
|
||||||
|
protected readonly ROOT_CACHE_KEY = 'mmFilter:';
|
||||||
|
|
||||||
|
protected logger: CoreLogger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the contexts in memory to speed up the process, it can take a lot of time otherwise.
|
||||||
|
*/
|
||||||
|
protected contextsCache: {
|
||||||
|
[siteId: string]: {
|
||||||
|
[contextlevel: string]: {
|
||||||
|
[instanceid: number]: {
|
||||||
|
filters: CoreFilterFilter[];
|
||||||
|
time: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logger = CoreLogger.getInstance('CoreFilterProvider');
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.WS_CACHE_INVALIDATED, (data: CoreEventSiteData) => {
|
||||||
|
delete this.contextsCache[data.siteId || ''];
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.SITE_STORAGE_DELETED, (data: CoreEventSiteData) => {
|
||||||
|
delete this.contextsCache[data.siteId || ''];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not WS get available in context is available.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with true if ws is available, false otherwise.
|
||||||
|
* @since 3.4
|
||||||
|
*/
|
||||||
|
async canGetAvailableInContext(siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return this.canGetAvailableInContextInSite(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not WS get available in context is available in a certain site.
|
||||||
|
*
|
||||||
|
* @param site Site. If not defined, current site.
|
||||||
|
* @return Promise resolved with true if ws is available, false otherwise.
|
||||||
|
* @since 3.4
|
||||||
|
*/
|
||||||
|
canGetAvailableInContextInSite(site?: CoreSite): boolean {
|
||||||
|
site = site || CoreSites.instance.getCurrentSite();
|
||||||
|
|
||||||
|
return !!(site?.wsAvailable('core_filters_get_available_in_context'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not we can get the available filters: the WS is available and the feature isn't disabled.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean: whethe can get filters.
|
||||||
|
*/
|
||||||
|
async canGetFilters(siteId?: string): Promise<boolean> {
|
||||||
|
const wsAvailable = await this.canGetAvailableInContext(siteId);
|
||||||
|
const disabled = await this.checkFiltersDisabled(siteId);
|
||||||
|
|
||||||
|
return wsAvailable && !disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not we can get the available filters: the WS is available and the feature isn't disabled.
|
||||||
|
*
|
||||||
|
* @param site Site. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean: whethe can get filters.
|
||||||
|
*/
|
||||||
|
canGetFiltersInSite(site?: CoreSite): boolean {
|
||||||
|
return this.canGetAvailableInContextInSite(site) && this.checkFiltersDisabledInSite(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not checking the available filters is disabled in the site.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean: whether it's disabled.
|
||||||
|
*/
|
||||||
|
async checkFiltersDisabled(siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
return this.checkFiltersDisabledInSite(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not checking the available filters is disabled in the site.
|
||||||
|
*
|
||||||
|
* @param site Site. If not defined, current site.
|
||||||
|
* @return Whether it's disabled.
|
||||||
|
*/
|
||||||
|
checkFiltersDisabledInSite(site?: CoreSite): boolean {
|
||||||
|
site = site || CoreSites.instance.getCurrentSite();
|
||||||
|
|
||||||
|
return !!(site?.isFeatureDisabled('CoreFilterDelegate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify a list of filters into each context.
|
||||||
|
*
|
||||||
|
* @param contexts List of contexts.
|
||||||
|
* @param filters List of filters.
|
||||||
|
* @param hadSystemContext Whether the list of contexts originally had system context.
|
||||||
|
* @param hadSiteHomeContext Whether the list of contexts originally had site home context.
|
||||||
|
* @param site Site instance.
|
||||||
|
* @return Classified filters.
|
||||||
|
*/
|
||||||
|
protected classifyFilters(
|
||||||
|
contexts: CoreFiltersGetAvailableInContextWSParamContext[],
|
||||||
|
filters: CoreFilterFilter[],
|
||||||
|
hadSystemContext: boolean,
|
||||||
|
hadSiteHomeContext: boolean,
|
||||||
|
site: CoreSite,
|
||||||
|
): CoreFilterClassifiedFilters {
|
||||||
|
const classified: CoreFilterClassifiedFilters = {};
|
||||||
|
|
||||||
|
// Initialize all contexts.
|
||||||
|
contexts.forEach((context) => {
|
||||||
|
classified[context.contextlevel] = classified[context.contextlevel] || {};
|
||||||
|
classified[context.contextlevel][context.instanceid] = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (contexts.length == 1 && !hadSystemContext) {
|
||||||
|
// Only 1 context, no need to iterate over the filters.
|
||||||
|
classified[contexts[0].contextlevel][contexts[0].instanceid] = filters;
|
||||||
|
|
||||||
|
return classified;
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.forEach((filter) => {
|
||||||
|
if (hadSystemContext && filter.contextlevel == 'course' && filter.instanceid == site.getSiteHomeId()) {
|
||||||
|
if (hadSiteHomeContext) {
|
||||||
|
// We need to return both site home and system. Add site home first.
|
||||||
|
classified[filter.contextlevel][filter.instanceid].push(filter);
|
||||||
|
|
||||||
|
// Now copy the object so it can be modified.
|
||||||
|
filter = Object.assign({}, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate the system context based on the inherited data.
|
||||||
|
filter.contextlevel = 'system';
|
||||||
|
filter.instanceid = 0;
|
||||||
|
filter.contextid = -1;
|
||||||
|
filter.localstate = filter.inheritedstate;
|
||||||
|
}
|
||||||
|
|
||||||
|
classified[filter.contextlevel][filter.instanceid].push(filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
return classified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given some HTML code, this function returns the text as safe HTML.
|
||||||
|
*
|
||||||
|
* @param text The text to be formatted.
|
||||||
|
* @param options Formatting options.
|
||||||
|
* @param filters The filters to apply. Required if filter is set to true.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the formatted text.
|
||||||
|
*/
|
||||||
|
async formatText(
|
||||||
|
text: string,
|
||||||
|
options?: CoreFilterFormatTextOptions,
|
||||||
|
filters?: CoreFilterFilter[],
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
|
||||||
|
if (!text || typeof text != 'string') {
|
||||||
|
// No need to do any filters and cleaning.
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone object if needed so we can modify it.
|
||||||
|
options = options ? Object.assign({}, options) : {};
|
||||||
|
|
||||||
|
if (typeof options.clean == 'undefined') {
|
||||||
|
options.clean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.filter == 'undefined') {
|
||||||
|
options.filter = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.contextLevel) {
|
||||||
|
options.filter = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.filter) {
|
||||||
|
text = await CoreFilterDelegate.instance.filterText(text, filters, options, [], siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.clean) {
|
||||||
|
text = CoreTextUtils.instance.cleanTags(text, options.singleLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.shortenLength && options.shortenLength > 0) {
|
||||||
|
text = CoreTextUtils.instance.shortenText(text, options.shortenLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.highlight) {
|
||||||
|
text = CoreTextUtils.instance.highlightText(text, options.highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for available in contexts WS calls.
|
||||||
|
*
|
||||||
|
* @param contexts The contexts to check.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getAvailableInContextsCacheKey(contexts: CoreFiltersGetAvailableInContextWSParamContext[]): string {
|
||||||
|
return this.getAvailableInContextsPrefixCacheKey() + JSON.stringify(contexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get prefixed cache key for available in contexts WS calls.
|
||||||
|
*
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getAvailableInContextsPrefixCacheKey(): string {
|
||||||
|
return this.ROOT_CACHE_KEY + 'availableInContexts:';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available in several contexts.
|
||||||
|
*
|
||||||
|
* @param contexts The contexts to check.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the filters classified by context.
|
||||||
|
*/
|
||||||
|
async getAvailableInContexts(
|
||||||
|
contexts: CoreFiltersGetAvailableInContextWSParamContext[],
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<CoreFilterClassifiedFilters> {
|
||||||
|
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
siteId = site.getId();
|
||||||
|
|
||||||
|
const cacheResult = this.getFromMemoryCache(contexts, site);
|
||||||
|
|
||||||
|
if (cacheResult) {
|
||||||
|
return cacheResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextsToSend = contexts.slice(); // Copy the contexts array to be able to modify it.
|
||||||
|
|
||||||
|
const { hadSystemContext, hadSiteHomeContext } = this.replaceSystemContext(contextsToSend, site);
|
||||||
|
|
||||||
|
const data: CoreFiltersGetAvailableInContextWSParams = {
|
||||||
|
contexts: contextsToSend,
|
||||||
|
};
|
||||||
|
const preSets = {
|
||||||
|
cacheKey: this.getAvailableInContextsCacheKey(contextsToSend),
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
|
splitRequest: {
|
||||||
|
param: 'contexts',
|
||||||
|
maxLength: 300,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await site.read<CoreFilterGetAvailableInContextResult>(
|
||||||
|
'core_filters_get_available_in_context',
|
||||||
|
data,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
const classified = this.classifyFilters(contexts, result.filters, hadSystemContext, hadSiteHomeContext, site);
|
||||||
|
|
||||||
|
this.storeInMemoryCache(classified, siteId);
|
||||||
|
|
||||||
|
return classified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filters available in a certain context.
|
||||||
|
*
|
||||||
|
* @param contextLevel The context level to check: system, user, coursecat, course, module, block, ...
|
||||||
|
* @param instanceId The instance ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with the filters.
|
||||||
|
*/
|
||||||
|
async getAvailableInContext(contextLevel: string, instanceId: number, siteId?: string): Promise<CoreFilterFilter[]> {
|
||||||
|
const result = await this.getAvailableInContexts([{ contextlevel: contextLevel, instanceid: instanceId }], siteId);
|
||||||
|
|
||||||
|
return result[contextLevel][instanceId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get contexts filters from the memory cache.
|
||||||
|
*
|
||||||
|
* @param contexts Contexts to get.
|
||||||
|
* @param site Site.
|
||||||
|
* @return The filters classified by context and instance.
|
||||||
|
*/
|
||||||
|
protected getFromMemoryCache(
|
||||||
|
contexts: CoreFiltersGetAvailableInContextWSParamContext[],
|
||||||
|
site: CoreSite,
|
||||||
|
): CoreFilterClassifiedFilters | undefined {
|
||||||
|
|
||||||
|
if (!this.contextsCache[site.getId()]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have the contexts in the memory cache.
|
||||||
|
const siteContexts = this.contextsCache[site.getId()];
|
||||||
|
const isOnline = CoreApp.instance.isOnline();
|
||||||
|
const result: CoreFilterClassifiedFilters = {};
|
||||||
|
let allFound = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < contexts.length; i++) {
|
||||||
|
const context = contexts[i];
|
||||||
|
const cachedCtxt = siteContexts[context.contextlevel]?.[context.instanceid];
|
||||||
|
|
||||||
|
// Check the context isn't "expired". The time stored in this cache will not match the one in the site cache.
|
||||||
|
if (cachedCtxt && (!isOnline ||
|
||||||
|
Date.now() <= cachedCtxt.time + site.getExpirationDelay(CoreSite.FREQUENCY_RARELY))) {
|
||||||
|
|
||||||
|
result[context.contextlevel] = result[context.contextlevel] || {};
|
||||||
|
result[context.contextlevel][context.instanceid] = cachedCtxt.filters;
|
||||||
|
} else {
|
||||||
|
allFound = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allFound) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates all available in context WS calls.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID (empty for current site).
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateAllAvailableInContext(siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKeyStartingWith(this.getAvailableInContextsPrefixCacheKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates available in context WS call.
|
||||||
|
*
|
||||||
|
* @param contexts The contexts to check.
|
||||||
|
* @param siteId Site ID (empty for current site).
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateAvailableInContexts(
|
||||||
|
contexts: CoreFiltersGetAvailableInContextWSParamContext[],
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getAvailableInContextsCacheKey(contexts));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates available in context WS call.
|
||||||
|
*
|
||||||
|
* @param contextLevel The context level to check.
|
||||||
|
* @param instanceId The instance ID.
|
||||||
|
* @param siteId Site ID (empty for current site).
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateAvailableInContext(contextLevel: string, instanceId: number, siteId?: string): Promise<void> {
|
||||||
|
await this.invalidateAvailableInContexts([{ contextlevel: contextLevel, instanceid: instanceId }], siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of context to send to core_filters_get_available_in_context, search if the system context is in the list
|
||||||
|
* and, if so, replace it with a workaround.
|
||||||
|
*
|
||||||
|
* @param contexts The contexts to check.
|
||||||
|
* @param site Site instance.
|
||||||
|
* @return Whether the filters had system context and whether they had the site home context.
|
||||||
|
*/
|
||||||
|
protected replaceSystemContext(
|
||||||
|
contexts: CoreFiltersGetAvailableInContextWSParamContext[],
|
||||||
|
site: CoreSite,
|
||||||
|
): { hadSystemContext: boolean; hadSiteHomeContext: boolean } {
|
||||||
|
const result = {
|
||||||
|
hadSystemContext: false,
|
||||||
|
hadSiteHomeContext: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if any of the contexts is "system". We cannot use system context, so we'll have to use a wrokaround.
|
||||||
|
for (let i = 0; i < contexts.length; i++) {
|
||||||
|
const context = contexts[i];
|
||||||
|
|
||||||
|
if (context.contextlevel != 'system') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.hadSystemContext = true;
|
||||||
|
|
||||||
|
// Use course site home instead. Check if it's already in the list.
|
||||||
|
result.hadSiteHomeContext = contexts.some((context) =>
|
||||||
|
context.contextlevel == 'course' && context.instanceid == site.getSiteHomeId());
|
||||||
|
|
||||||
|
if (result.hadSiteHomeContext) {
|
||||||
|
// Site home is already in list, remove this context from the list.
|
||||||
|
contexts.splice(i, 1);
|
||||||
|
} else {
|
||||||
|
// Site home not in list, use it instead of system.
|
||||||
|
contexts[i] = {
|
||||||
|
contextlevel: 'course',
|
||||||
|
instanceid: site.getSiteHomeId(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store filters in the memory cache.
|
||||||
|
*
|
||||||
|
* @param filters Filters to store, classified by contextlevel and instanceid
|
||||||
|
* @param siteId Site ID.
|
||||||
|
*/
|
||||||
|
protected storeInMemoryCache(filters: CoreFilterClassifiedFilters, siteId: string): void {
|
||||||
|
this.contextsCache[siteId] = this.contextsCache[siteId] || {};
|
||||||
|
|
||||||
|
for (const contextLevel in filters) {
|
||||||
|
this.contextsCache[siteId][contextLevel] = this.contextsCache[siteId][contextLevel] || {};
|
||||||
|
|
||||||
|
for (const instanceId in filters[contextLevel]) {
|
||||||
|
this.contextsCache[siteId][contextLevel][instanceId] = {
|
||||||
|
filters: filters[contextLevel][instanceId],
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreFilter extends makeSingleton(CoreFilterProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of core_filters_get_available_in_context WS.
|
||||||
|
*/
|
||||||
|
export type CoreFiltersGetAvailableInContextWSParams = {
|
||||||
|
contexts: CoreFiltersGetAvailableInContextWSParamContext[]; // The list of contexts to check.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data about a context sent to core_filters_get_available_in_context.
|
||||||
|
*/
|
||||||
|
export type CoreFiltersGetAvailableInContextWSParamContext = {
|
||||||
|
contextlevel: string; // The context level where the filters are: (coursecat, course, module).
|
||||||
|
instanceid: number; // The instance id of item associated with the context.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter object returned by core_filters_get_available_in_context.
|
||||||
|
*/
|
||||||
|
export type CoreFilterFilter = {
|
||||||
|
contextlevel: string; // The context level where the filters are: (coursecat, course, module).
|
||||||
|
instanceid: number; // The instance id of item associated with the context.
|
||||||
|
contextid: number; // The context id.
|
||||||
|
filter: string; // Filter plugin name.
|
||||||
|
localstate: number; // Filter state: 1 for on, -1 for off, 0 if inherit.
|
||||||
|
inheritedstate: number; // 1 or 0 to use when localstate is set to inherit.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of core_filters_get_available_in_context.
|
||||||
|
*/
|
||||||
|
export type CoreFilterGetAvailableInContextResult = {
|
||||||
|
filters: CoreFilterFilter[]; // Available filters.
|
||||||
|
warnings: CoreWSExternalWarning[]; // List of warnings.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options that can be passed to format text.
|
||||||
|
*/
|
||||||
|
export type CoreFilterFormatTextOptions = {
|
||||||
|
contextLevel?: string; // The context level where the text is.
|
||||||
|
instanceId?: number; // The instance id related to the context.
|
||||||
|
clean?: boolean; // If true all HTML will be removed. Default false.
|
||||||
|
filter?: boolean; // If true the string will be run through applicable filters as well. Default true.
|
||||||
|
singleLine?: boolean; // If true then new lines will be removed (all the text in a single line).
|
||||||
|
shortenLength?: number; // Number of characters to shorten the text.
|
||||||
|
highlight?: string; // Text to highlight.
|
||||||
|
wsNotFiltered?: boolean; // If true it means the WS didn't filter the text for some reason.
|
||||||
|
courseId?: number; // Course ID the text belongs to. It can be used to improve performance.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters classified by context and instance.
|
||||||
|
*/
|
||||||
|
export type CoreFilterClassifiedFilters = {
|
||||||
|
[contextlevel: string]: {
|
||||||
|
[instanceid: number]: CoreFilterFilter[];
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,94 @@
|
||||||
|
// (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, ViewContainerRef } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreFilterHandler } from '../filter-delegate';
|
||||||
|
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '../filter';
|
||||||
|
import { CoreSite } from '@classes/site';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default handler used when the module doesn't have a specific implementation.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreFilterDefaultHandler implements CoreFilterHandler {
|
||||||
|
|
||||||
|
name = 'CoreFilterDefaultHandler';
|
||||||
|
filterName = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter some text.
|
||||||
|
*
|
||||||
|
* @param text The text to filter.
|
||||||
|
* @param filter The filter.
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Filtered text (or promise resolved with the filtered text).
|
||||||
|
*/
|
||||||
|
filter(
|
||||||
|
text: string,
|
||||||
|
filter: CoreFilterFilter, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
options: CoreFilterFormatTextOptions, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
siteId?: string, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
): string | Promise<string> {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle HTML. This function is called after "filter", and it will receive an HTMLElement containing the text that was
|
||||||
|
* filtered.
|
||||||
|
*
|
||||||
|
* @param container The HTML container to handle.
|
||||||
|
* @param filter The filter.
|
||||||
|
* @param options Options passed to the filters.
|
||||||
|
* @param viewContainerRef The ViewContainerRef where the container is.
|
||||||
|
* @param component Component.
|
||||||
|
* @param componentId Component ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return If async, promise resolved when done.
|
||||||
|
*/
|
||||||
|
handleHtml(
|
||||||
|
container: HTMLElement, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
filter: CoreFilterFilter, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
options: CoreFilterFormatTextOptions, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
viewContainerRef: ViewContainerRef, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
component?: string, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
componentId?: string | number, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
siteId?: string, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
): void | Promise<void> {
|
||||||
|
// To be overridden.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the filter should be applied in a certain site based on some filter options.
|
||||||
|
*
|
||||||
|
* @param options Options.
|
||||||
|
* @param site Site.
|
||||||
|
* @return Whether filter should be applied.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
shouldBeApplied(options: CoreFilterFormatTextOptions, site?: CoreSite): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue