// (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 { DownloadStatus } from '@/core/constants'; import { CoreSitePublicConfigResponse } from '@classes/sites/unauthenticated-site'; import { CoreFile } from '@services/file'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreWS } from '@services/ws'; import { makeSingleton } from '@singletons'; import { CoreStyleHandler, CoreStylesService } from '@features/styles/services/styles'; import { CoreLogger } from '@singletons/logger'; import { CoreUtils } from '@services/utils/utils'; const COMPONENT = 'mmaRemoteStyles'; /** * Service to handle remote themes. * A remote theme is a CSS sheet stored in the site that allows customising the Mobile app. */ @Injectable({ providedIn: 'root' }) export class AddonRemoteThemesHandlerService implements CoreStyleHandler { protected logger: CoreLogger; name = 'mobilecssurl'; priority = 1000; constructor() { this.logger = CoreLogger.getInstance('AddonRemoteThemes'); } /** * @inheritDoc */ // eslint-disable-next-line @typescript-eslint/no-unused-vars async isEnabled(siteId: string, config?: CoreSitePublicConfigResponse): Promise { return true; } /** * @inheritDoc */ async getStyle(siteId: string, config?: CoreSitePublicConfigResponse): Promise { if (siteId == CoreStylesService.TMP_SITE_ID) { if (!config) { return ''; } // Config received, it's a temp site. return this.getRemoteStyles(config.mobilecssurl); } const site = await CoreSites.getSite(siteId); const infos = site.getInfo(); if (!infos?.mobilecssurl) { if (infos?.mobilecssurl === '') { // CSS URL is empty. Delete downloaded files (if any). CoreFilepool.removeFilesByComponent(siteId, COMPONENT, 1); } return ''; } let fileUrl = infos.mobilecssurl; if (CoreFile.isAvailable()) { // The file system is available. Download the file and remove old CSS files if needed. fileUrl = await this.downloadFileAndRemoveOld(siteId, fileUrl); } this.logger.debug('Loading styles from: ', fileUrl); // Get the CSS content using HTTP because we will treat the styles before saving them in the file. const style = await this.getRemoteStyles(fileUrl); if (style != '') { // Treat the CSS. CoreUtils.ignoreErrors( CoreFilepool.treatCSSCode(siteId, infos.mobilecssurl, style, COMPONENT, 1), ); } return style; } /** * Get styles from the url. * * @param url Url to get the code from. * @returns The styles. */ protected async getRemoteStyles(url?: string): Promise { if (!url) { return ''; } return CoreWS.getText(url); } /** * Downloads a CSS file and remove old files if needed. * * @param siteId Site ID. * @param url File URL. * @returns Promise resolved when the file is downloaded. */ protected async downloadFileAndRemoveOld(siteId: string, url: string): Promise { try { // Check if the file is downloaded. const state = await CoreFilepool.getFileStateByUrl(siteId, url); if (state === DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED) { // File not downloaded, URL has changed or first time. Delete downloaded CSS files. await CoreFilepool.removeFilesByComponent(siteId, COMPONENT, 1); } } catch { // An error occurred while getting state (shouldn't happen). Don't delete downloaded file. } return CoreFilepool.downloadUrl(siteId, url, false, COMPONENT, 1); } } export const AddonRemoteThemesHandler = makeSingleton(AddonRemoteThemesHandlerService);