From eff08198e1e4570dde6df031e2b3218fda551ea4 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 2 May 2018 14:52:19 +0200 Subject: [PATCH] MOBILE-2333 siteplugins: Support CSS files --- .../remotethemes/providers/remotethemes.ts | 58 +-------- src/core/siteplugins/providers/helper.ts | 120 +++++++++++++++++- src/core/siteplugins/providers/siteplugins.ts | 2 + src/providers/filepool.ts | 108 ++++++++++++---- 4 files changed, 205 insertions(+), 83 deletions(-) diff --git a/src/addon/remotethemes/providers/remotethemes.ts b/src/addon/remotethemes/providers/remotethemes.ts index 18aa85ff1..067bca557 100644 --- a/src/addon/remotethemes/providers/remotethemes.ts +++ b/src/addon/remotethemes/providers/remotethemes.ts @@ -18,8 +18,6 @@ import { CoreFileProvider } from '@providers/file'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreConstants } from '@core/constants'; import { Md5 } from 'ts-md5/dist/md5'; @@ -36,8 +34,7 @@ export class AddonRemoteThemesProvider { protected stylesEls: {[siteId: string]: {element: HTMLStyleElement, hash: string}} = {}; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private fileProvider: CoreFileProvider, - private filepoolProvider: CoreFilepoolProvider, private http: Http, private utils: CoreUtilsProvider, - private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider) { + private filepoolProvider: CoreFilepoolProvider, private http: Http, private utils: CoreUtilsProvider) { this.logger = logger.getInstance('AddonRemoteThemesProvider'); } @@ -225,7 +222,8 @@ export class AddonRemoteThemesProvider { } // Styles have been loaded, now treat the CSS. - this.treatCSSCode(siteId, data.fileUrl, data.styles).catch(() => { + this.filepoolProvider.treatCSSCode(siteId, data.fileUrl, data.styles, AddonRemoteThemesProvider.COMPONENT, 2) + .catch(() => { // Ignore errors. }); }); @@ -304,56 +302,6 @@ export class AddonRemoteThemesProvider { } } - /** - * Search for files in a CSS code and try to download them. Once downloaded, replace their URLs - * and store the result in the CSS file. - * - * @param {string} siteId Site ID. - * @param {string} fileUrl CSS file URL. - * @param {string} cssCode CSS code. - * @return {Promise} Promise resolved with the CSS code. - */ - protected treatCSSCode(siteId: string, fileUrl: string, cssCode: string): Promise { - if (!this.fileProvider.isAvailable()) { - return Promise.reject(null); - } - - const urls = this.domUtils.extractUrlsFromCSS(cssCode), - promises = []; - let filePath, - updated = false; - - // Get the path of the CSS file. - promises.push(this.filepoolProvider.getFilePathByUrl(siteId, fileUrl).then((path) => { - filePath = path; - })); - - urls.forEach((url) => { - // Download the file only if it's an online URL. - if (url.indexOf('http') == 0) { - promises.push(this.filepoolProvider.downloadUrl(siteId, url, false, AddonRemoteThemesProvider.COMPONENT, 2) - .then((fileUrl) => { - if (fileUrl != url) { - cssCode = cssCode.replace(new RegExp(this.textUtils.escapeForRegex(url), 'g'), fileUrl); - updated = true; - } - }).catch((error) => { - // It shouldn't happen. Ignore errors. - this.logger.warn('Error treating file ', url, error); - })); - } - }); - - return Promise.all(promises).then(() => { - // All files downloaded. Store the result if it has changed. - if (updated) { - return this.fileProvider.writeFile(filePath, cssCode); - } - }).then(() => { - return cssCode; - }); - } - /** * Unload styles for a temporary site. */ diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index 9d6fa24e5..ba2915e56 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -13,12 +13,15 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; +import { Http } from '@angular/http'; import { CoreEventsProvider } from '@providers/events'; +import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLangProvider } from '@providers/lang'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSite } from '@classes/site'; import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitePluginsProvider } from './siteplugins'; import { CoreCompileProvider } from '@core/compile/providers/compile'; @@ -56,12 +59,12 @@ export class CoreSitePluginsHelperProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private injector: Injector, private mainMenuDelegate: CoreMainMenuDelegate, private moduleDelegate: CoreCourseModuleDelegate, - private userDelegate: CoreUserDelegate, private langProvider: CoreLangProvider, + private userDelegate: CoreUserDelegate, private langProvider: CoreLangProvider, private http: Http, private sitePluginsProvider: CoreSitePluginsProvider, private prefetchDelegate: CoreCourseModulePrefetchDelegate, - private compileProvider: CoreCompileProvider, private utils: CoreUtilsProvider, + private compileProvider: CoreCompileProvider, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private courseOptionsDelegate: CoreCourseOptionsDelegate, eventsProvider: CoreEventsProvider, private courseFormatDelegate: CoreCourseFormatDelegate, private profileFieldDelegate: CoreUserProfileFieldDelegate, - private textUtils: CoreTextUtilsProvider) { + private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider) { this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); // Fetch the plugins on login. @@ -88,6 +91,64 @@ export class CoreSitePluginsHelperProvider { }); } + /** + * Download the styles for a handler (if any). + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {string} [siteId] Site ID. If not provided, current site. + * @return {Promise} Promise resolved with the CSS code. + */ + downloadStyles(plugin: any, handlerName: string, handlerSchema: any, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + // Get the absolute URL. If it's a relative URL, add the site URL to it. + let url = handlerSchema.styles && handlerSchema.styles.url; + if (url && !this.urlUtils.isAbsoluteURL(url)) { + url = this.textUtils.concatenatePaths(site.getURL(), url); + } + + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + componentId = uniqueName + '#main'; + + // Remove the CSS files for this handler that aren't used anymore. Don't block the call for this. + this.filepoolProvider.getFilesByComponent(site.id, CoreSitePluginsProvider.COMPONENT, componentId).then((files) => { + files.forEach((file) => { + if (file.url != url) { + // It's not the current file, delete it. + this.filepoolProvider.removeFileByUrl(site.id, file.url).catch(() => { + // Ignore errors. + }); + } + }); + }).catch(() => { + // Ignore errors. + }); + + if (!url) { + // No styles. + return ''; + } + + // Download the file if not downloaded or the version changed. + return this.filepoolProvider.downloadUrl(site.id, url, false, CoreSitePluginsProvider.COMPONENT, componentId, 0, + undefined, undefined, undefined, handlerSchema.styles.version).then((url) => { + + // File is downloaded, get the contents. + return this.http.get(url).toPromise(); + }).then((response): any => { + const text = response && response.text(); + + if (typeof text == 'string') { + return text; + } else { + return Promise.reject(null); + } + }); + }); + } + /** * Execute a handler's init method if it has any. * @@ -284,6 +345,35 @@ export class CoreSitePluginsHelperProvider { return this.utils.allPromises(promises); } + /** + * Load the styles for a handler. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {string} fileUrl CSS file URL. + * @param {string} cssCode CSS code. + * @param {number} [version] Styles version. + * @param {string} [siteId] Site ID. If not provided, current site. + */ + loadStyles(plugin: any, handlerName: string, fileUrl: string, cssCode: string, version?: number, siteId?: string): void { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Create the style and add it to the header. + const styleEl = document.createElement('style'), + uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName); + + styleEl.setAttribute('id', 'siteplugin-' + uniqueName); + styleEl.innerHTML = cssCode; + + document.head.appendChild(styleEl); + + // Styles have been loaded, now treat the CSS. + this.filepoolProvider.treatCSSCode(siteId, fileUrl, cssCode, CoreSitePluginsProvider.COMPONENT, uniqueName, version) + .catch(() => { + // Ignore errors. + }); + } + /** * Register a site plugin handler in the right delegate. * @@ -294,8 +384,28 @@ export class CoreSitePluginsHelperProvider { */ registerHandler(plugin: any, handlerName: string, handlerSchema: any): Promise { - // Wait for the init JS to be executed. - return this.executeHandlerInit(plugin, handlerSchema).then((result) => { + // Wait for the init JS to be executed and for the styles to be downloaded. + const promises = [], + siteId = this.sitesProvider.getCurrentSiteId(); + let result, + cssCode; + + promises.push(this.downloadStyles(plugin, handlerName, handlerSchema, siteId).then((code) => { + cssCode = code; + }).catch((error) => { + this.logger.error('Error getting styles for plugin', handlerName, handlerSchema, error); + })); + + promises.push(this.executeHandlerInit(plugin, handlerSchema).then((initResult) => { + result = initResult; + })); + + return Promise.all(promises).then(() => { + if (cssCode) { + // Load the styles. + this.loadStyles(plugin, handlerName, handlerSchema.styles.url, cssCode, handlerSchema.styles.version, siteId); + } + let promise; switch (handlerSchema.delegate) { diff --git a/src/core/siteplugins/providers/siteplugins.ts b/src/core/siteplugins/providers/siteplugins.ts index bd7519cc9..ac3f02c10 100644 --- a/src/core/siteplugins/providers/siteplugins.ts +++ b/src/core/siteplugins/providers/siteplugins.ts @@ -58,6 +58,8 @@ export interface CoreSitePluginsHandler { */ @Injectable() export class CoreSitePluginsProvider { + static COMPONENT = 'CoreSitePlugins'; + protected ROOT_CACHE_KEY = 'CoreSitePlugins:'; protected logger; diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index 673606dad..e92f148c7 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -22,6 +22,7 @@ import { CoreLoggerProvider } from './logger'; import { CorePluginFileDelegate } from './plugin-file-delegate'; import { CoreSitesProvider } from './sites'; import { CoreWSProvider } from './ws'; +import { CoreDomUtilsProvider } from './utils/dom'; import { CoreMimetypeUtilsProvider } from './utils/mimetype'; import { CoreTextUtilsProvider } from './utils/text'; import { CoreTimeUtilsProvider } from './utils/time'; @@ -435,7 +436,7 @@ export class CoreFilepoolProvider { private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider, private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private eventsProvider: CoreEventsProvider, initDelegate: CoreInitDelegate, - network: Network, private pluginFileDelegate: CorePluginFileDelegate) { + network: Network, private pluginFileDelegate: CorePluginFileDelegate, private domUtils: CoreDomUtilsProvider) { this.logger = logger.getInstance('CoreFilepoolProvider'); this.appDB = this.appProvider.getDB(); @@ -625,13 +626,14 @@ export class CoreFilepoolProvider { * @param {Function} [onProgress] Function to call on progress. * @param {number} [priority=0] The priority this file should get in the queue (range 0-999). * @param {any} [options] Extra options (isexternalfile, repositorytype). + * @param {number} [revision] File revision. If not defined, it will be calculated using the URL. * @return {Promise} Resolved on success. */ addToQueueByUrl(siteId: string, fileUrl: string, component?: string, componentId?: string | number, timemodified: number = 0, - filePath?: string, onProgress?: (event: any) => any, priority: number = 0, options: any = {}): Promise { + filePath?: string, onProgress?: (event: any) => any, priority: number = 0, options: any = {}, revision?: number) + : Promise { let fileId, link, - revision, queueDeferred; if (!this.fileProvider.isAvailable()) { @@ -646,7 +648,7 @@ export class CoreFilepoolProvider { return this.fixPluginfileURL(siteId, fileUrl).then((fileUrl) => { const primaryKey = { siteId: siteId, fileId: fileId }; - revision = this.getRevisionFromUrl(fileUrl); + revision = revision || this.getRevisionFromUrl(fileUrl); fileId = this.getFileIdByUrl(fileUrl); // Set up the component. @@ -747,10 +749,12 @@ export class CoreFilepoolProvider { * @param {boolean} [downloadUnknown] True to download file in WiFi if their size is unknown, false otherwise. * Ignored if checkSize=false. * @param {any} [options] Extra options (isexternalfile, repositorytype). + * @param {number} [revision] File revision. If not defined, it will be calculated using the URL. * @return {Promise} 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 = {}): Promise { + timemodified: number = 0, checkSize: boolean = true, downloadUnknown?: boolean, options: any = {}, revision?: number) + : Promise { let promise; if (checkSize) { @@ -779,16 +783,17 @@ export class CoreFilepoolProvider { if (sizeUnknown) { if (downloadUnknown && isWifi) { return this.addToQueueByUrl( - siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options); + siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, revision); } } else if (size <= this.DOWNLOAD_THRESHOLD || (isWifi && size <= this.WIFI_DOWNLOAD_THRESHOLD)) { return this.addToQueueByUrl( - siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options); + siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, revision); } }); } else { // No need to check size, just add it to the queue. - return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options); + return this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, 0, options, + revision); } } @@ -1155,6 +1160,7 @@ export class CoreFilepoolProvider { * @param {number} [timemodified=0] The time this file was modified. Can be used to check file state. * @param {string} [filePath] Filepath to download the file to. If not defined, download to the filepool folder. * @param {any} [options] Extra options (isexternalfile, repositorytype). + * @param {number} [revision] File revision. If not defined, it will be calculated using the URL. * @return {Promise} Resolved with internal URL on success, rejected otherwise. * @description * Downloads a file on the spot. @@ -1164,7 +1170,8 @@ export class CoreFilepoolProvider { * 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 = {}): Promise { + timemodified: number = 0, onProgress?: (event: any) => any, filePath?: string, options: any = {}, revision?: number) + : Promise { let fileId, promise; @@ -1174,7 +1181,7 @@ export class CoreFilepoolProvider { options = Object.assign({}, options); // Create a copy to prevent modifying the original object. options.timemodified = timemodified || 0; - options.revision = this.getRevisionFromUrl(fileUrl); + options.revision = revision || this.getRevisionFromUrl(fileUrl); fileId = this.getFileIdByUrl(fileUrl); return this.hasFileInPool(siteId, fileId).then((fileObject) => { @@ -1585,15 +1592,16 @@ export class CoreFilepoolProvider { * @param {string} fileUrl File URL. * @param {number} [timemodified=0] The time this file was modified. * @param {string} [filePath] Filepath to download the file to. If defined, no extension will be added. + * @param {number} [revision] File revision. If not defined, it will be calculated using the URL. * @return {Promise} Promise resolved with the file state. */ - getFileStateByUrl(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string): Promise { - let fileId, - revision; + getFileStateByUrl(siteId: string, fileUrl: string, timemodified: number = 0, filePath?: string, revision?: number) + : Promise { + let fileId; return this.fixPluginfileURL(siteId, fileUrl).then((fixedUrl) => { fileUrl = fixedUrl; - revision = this.getRevisionFromUrl(fileUrl); + revision = revision || this.getRevisionFromUrl(fileUrl); fileId = this.getFileIdByUrl(fileUrl); // Check if the file is in queue (waiting to be downloaded). @@ -1638,6 +1646,7 @@ export class CoreFilepoolProvider { * @param {boolean} [downloadUnknown] True to download file in WiFi if their size is unknown, false otherwise. * Ignored if checkSize=false. * @param {any} [options] Extra options (isexternalfile, repositorytype). + * @param {number} [revision] File revision. If not defined, it will be calculated using the URL. * @return {Promise} Resolved with the URL to use. * @description * This will return a URL pointing to the content of the requested URL. @@ -1647,21 +1656,20 @@ export class CoreFilepoolProvider { */ protected getFileUrlByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, mode: string = 'url', timemodified: number = 0, checkSize: boolean = true, downloadUnknown?: boolean, - options: any = {}): Promise { + options: any = {}, revision?: number): Promise { - let fileId, - revision; + 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).catch(() => { + downloadUnknown, options, revision).catch(() => { // Ignore errors. }); }; return this.fixPluginfileURL(siteId, fileUrl).then((fixedUrl) => { fileUrl = fixedUrl; - revision = this.getRevisionFromUrl(fileUrl); + revision = revision || this.getRevisionFromUrl(fileUrl); fileId = this.getFileIdByUrl(fileUrl); return this.hasFileInPool(siteId, fileId).then((entry) => { @@ -2083,15 +2091,16 @@ export class CoreFilepoolProvider { * @param {boolean} [downloadUnknown] True to download file in WiFi if their size is unknown, false otherwise. * Ignored if checkSize=false. * @param {any} [options] Extra options (isexternalfile, repositorytype). + * @param {number} [revision] File revision. If not defined, it will be calculated using the URL. * @return {Promise} Resolved with the URL to use. * @description * This will return a URL pointing to the content of the requested URL. * 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 = {}): Promise { + checkSize: boolean = true, downloadUnknown?: boolean, options: any = {}, revision?: number): Promise { return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'src', - timemodified, checkSize, downloadUnknown, options); + timemodified, checkSize, downloadUnknown, options, revision); } /** @@ -2125,15 +2134,16 @@ export class CoreFilepoolProvider { * @param {boolean} [downloadUnknown] True to download file in WiFi if their size is unknown, false otherwise. * Ignored if checkSize=false. * @param {any} [options] Extra options (isexternalfile, repositorytype). + * @param {number} [revision] File revision. If not defined, it will be calculated using the URL. * @return {Promise} Resolved with the URL to use. * @description * This will return a URL pointing to the content of the requested URL. * 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 = {}): Promise { + checkSize: boolean = true, downloadUnknown?: boolean, options: any = {}, revision?: number): Promise { return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'url', - timemodified, checkSize, downloadUnknown, options); + timemodified, checkSize, downloadUnknown, options, revision); } /** @@ -2803,6 +2813,58 @@ export class CoreFilepoolProvider { }); } + /** + * Search for files in a CSS code and try to download them. Once downloaded, replace their URLs + * and store the result in the CSS file. + * + * @param {string} siteId Site ID. + * @param {string} fileUrl CSS file URL. + * @param {string} cssCode CSS code. + * @param {string} [component] The component to link the file to. + * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {number} [revision] Revision to use in all files. If not defined, it will be calculated using the URL of each file. + * @return {Promise} Promise resolved with the CSS code. + */ + treatCSSCode(siteId: string, fileUrl: string, cssCode: string, component?: string, componentId?: string | number, + revision?: number): Promise { + + const urls = this.domUtils.extractUrlsFromCSS(cssCode), + promises = []; + let filePath, + updated = false; + + // Get the path of the CSS file. + promises.push(this.getFilePathByUrl(siteId, fileUrl).then((path) => { + filePath = path; + })); + + urls.forEach((url) => { + // Download the file only if it's an online URL. + if (url.indexOf('http') == 0) { + promises.push(this.downloadUrl(siteId, url, false, component, componentId, 0, undefined, undefined, undefined, + revision).then((fileUrl) => { + + if (fileUrl != url) { + cssCode = cssCode.replace(new RegExp(this.textUtils.escapeForRegex(url), 'g'), fileUrl); + updated = true; + } + }).catch((error) => { + // It shouldn't happen. Ignore errors. + this.logger.warn('Error treating file ', url, error); + })); + } + }); + + return Promise.all(promises).then(() => { + // All files downloaded. Store the result if it has changed. + if (updated) { + return this.fileProvider.writeFile(filePath, cssCode); + } + }).then(() => { + return cssCode; + }); + } + /** * Remove extension from fileId in queue, used to migrate from previous file handling. *