diff --git a/www/addons/remotestyles/main.js b/www/addons/remotestyles/main.js index 410e81d5ac3..794e967e55d 100644 --- a/www/addons/remotestyles/main.js +++ b/www/addons/remotestyles/main.js @@ -16,18 +16,35 @@ angular.module('mm.addons.remotestyles', []) .constant('mmaRemoteStylesComponent', 'mmaRemoteStyles') +.config(function($mmInitDelegateProvider, mmInitDelegateMaxAddonPriority) { + // Addons shouldn't use a priority higher than mmInitDelegateMaxAddonPriority, but this is a special case + // because it needs to be done before the mmLogin process. + $mmInitDelegateProvider.registerProcess('mmaRemoteStylesCurrent', + '$mmaRemoteStyles._preloadCurrentSite', mmInitDelegateMaxAddonPriority + 250, true); + $mmInitDelegateProvider.registerProcess('mmaRemoteStylesPreload', '$mmaRemoteStyles._preloadSites'); +}) + .run(function($mmEvents, mmCoreEventLogin, mmCoreEventLogout, mmCoreEventSiteAdded, mmCoreEventSiteUpdated, $mmaRemoteStyles, - $mmSite) { + $mmSite, mmCoreEventSiteDeleted) { - $mmEvents.on(mmCoreEventSiteAdded, $mmaRemoteStyles.load); - $mmEvents.on(mmCoreEventSiteUpdated, function(siteid) { + $mmEvents.on(mmCoreEventSiteAdded, function(siteId) { + $mmaRemoteStyles.addSite(siteId); + }); + $mmEvents.on(mmCoreEventSiteUpdated, function(siteId) { // Load only if current site was updated. - if (siteid === $mmSite.getId()) { - $mmaRemoteStyles.load(); + if (siteId === $mmSite.getId()) { + $mmaRemoteStyles.load(siteId); } }); - $mmEvents.on(mmCoreEventLogin, $mmaRemoteStyles.load); - // Remove added styles on logout. + // Enable styles of current site on login. + $mmEvents.on(mmCoreEventLogin, $mmaRemoteStyles.enable); + + // Disable added styles on logout. $mmEvents.on(mmCoreEventLogout, $mmaRemoteStyles.clear); + + // Remove site styles on logout. + $mmEvents.on(mmCoreEventSiteDeleted, function(site) { + $mmaRemoteStyles.removeSite(site.id); + }); }); diff --git a/www/addons/remotestyles/services/remotestyles.js b/www/addons/remotestyles/services/remotestyles.js index c2191652071..4659b47de82 100644 --- a/www/addons/remotestyles/services/remotestyles.js +++ b/www/addons/remotestyles/services/remotestyles.js @@ -22,12 +22,36 @@ angular.module('mm.addons.remotestyles') * @name $mmaRemoteStyles */ .factory('$mmaRemoteStyles', function($log, $q, $mmSite, $mmSitesManager, $mmFilepool, $http, $mmFS, mmaRemoteStylesComponent, - mmCoreNotDownloaded) { + mmCoreNotDownloaded, $mmUtil, md5) { $log = $log.getInstance('$mmaRemoteStyles'); var self = {}, - remoteStylesEl = angular.element(document.querySelector('#mobilecssurl')); + remoteStylesEls = {}; + + /** + * Add a style element for a site and load the styles for that element. The style will be disabled. + * + * @module mm.addons.remotestyles + * @ngdoc method + * @name $mmaRemoteStyles#addSite + * @param {String} siteId Site ID. + * @return {Promise} Promise resolved when added and loaded. + */ + self.addSite = function(siteId) { + if (!siteId || remoteStylesEls[siteId]) { + return $q.when(); + } + + var el = angular.element(''); + angular.element(document.head).append(el); + remoteStylesEls[siteId] = { + element: el, + hash: '' + }; + + return self.load(siteId, true); + }; /** * Clear remote styles added to the DOM. @@ -37,7 +61,47 @@ angular.module('mm.addons.remotestyles') * @name $mmaRemoteStyles#clear */ self.clear = function() { - remoteStylesEl.html(''); + // Disable all the styles. + angular.element(document.querySelectorAll('style[id*=mobilecssurl]')).attr('disabled', true); + }; + + /** + * Downloads a CSS file and remove old files if needed. + * + * @param {String} siteId Site ID. + * @param {String} url File URL. + * @return {Promise} Promise resolved when the file is downloaded. + */ + function downloadFileAndRemoveOld(siteId, url) { + return $mmFilepool.getFileStateByUrl(siteId, url).then(function(state) { + return state !== mmCoreNotDownloaded; + }).catch(function() { + return true; // An error occurred while getting state (shouldn't happen). Don't delete downloaded file. + }).then(function(isDownloaded) { + if (!isDownloaded) { + // File not downloaded, URL has changed or first time. Delete downloaded CSS files. + return $mmFilepool.removeFilesByComponent(siteId, mmaRemoteStylesComponent, 1); + } + }).then(function() { + return $mmFilepool.downloadUrl(siteId, url, false, mmaRemoteStylesComponent, 1); + }); + } + + /** + * Enable the styles of a certain site. + * + * @module mm.addons.remotestyles + * @ngdoc method + * @name $mmaRemoteStyles#addSite + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Void} + */ + self.enable = function(siteId) { + siteId = siteId || $mmSite.getId(); + + if (remoteStylesEls[siteId]) { + remoteStylesEls[siteId].element.attr('disabled', false); + } }; /** @@ -46,39 +110,25 @@ angular.module('mm.addons.remotestyles') * @module mm.addons.remotestyles * @ngdoc method * @name $mmaRemoteStyles#get - * @param {String} siteid Site ID. - * @return {Promise} Promise resolved with the styles. + * @param {String} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the styles and the URL of the loaded CSS file (local if downloaded). */ - self.get = function(siteid) { - var promise; + self.get = function(siteId) { + var fileUrl; - siteid = siteid || $mmSite.getId(); - if (!siteid) { + siteId = siteId || $mmSite.getId(); + if (!siteId) { return $q.reject(); } - // Downloads a CSS file and remove old files if needed. - function downloadFileAndRemoveOld(url) { - return $mmFilepool.getFileStateByUrl(siteid, url).then(function(state) { - return state !== mmCoreNotDownloaded; - }).catch(function() { - return true; // An error occurred while getting state (shouldn't happen). Don't delete downloaded file. - }).then(function(isDownloaded) { - if (!isDownloaded) { - // File not downloaded, URL has changed or first time. Delete downloaded CSS files. - return $mmFilepool.removeFilesByComponent(siteid, mmaRemoteStylesComponent, 1); - } - }).then(function() { - return $mmFilepool.downloadUrl(siteid, url, false, mmaRemoteStylesComponent, 1); - }); - } - - return $mmSitesManager.getSite(siteid).then(function(site) { + return $mmSitesManager.getSite(siteId).then(function(site) { var infos = site.getInfo(); if (infos && infos.mobilecssurl) { + fileUrl = infos.mobilecssurl; + if ($mmFS.isAvailable()) { // The file system is available. Download the file and remove old CSS files if needed. - return downloadFileAndRemoveOld(infos.mobilecssurl); + return downloadFileAndRemoveOld(siteId, infos.mobilecssurl); } else { // We return the online URL. We're probably on browser. return infos.mobilecssurl; @@ -86,7 +136,7 @@ angular.module('mm.addons.remotestyles') } else { if (infos.mobilecssurl === '') { // CSS URL is empty. Delete downloaded files (if any). - $mmFilepool.removeFilesByComponent(siteid, mmaRemoteStylesComponent, 1) + $mmFilepool.removeFilesByComponent(siteId, mmaRemoteStylesComponent, 1); } return $q.reject(); } @@ -95,7 +145,7 @@ angular.module('mm.addons.remotestyles') return $http.get(url); }).then(function(response) { if (typeof response.data == 'string') { - return response.data; + return {file: fileUrl, styles: response.data}; } else { return $q.reject(); } @@ -103,22 +153,144 @@ angular.module('mm.addons.remotestyles') }; /** - * Load styles for current site. + * Load styles for a certain site. * * @module mm.addons.remotestyles * @ngdoc method * @name $mmaRemoteStyles#load + * @param {String} [siteId] Site ID. If not defined, current site. + * @param {Boolean} disabled True if loaded styles should be disabled, false if they should be enabled. + * @return {Promise} Promise resolved when styles are loaded. */ - self.load = function() { - var siteid = $mmSite.getId(); - if (siteid) { - self.get(siteid).then(function(styles) { - if (siteid === $mmSite.getId()) { // Make sure it hasn't logout while retrieving styles. - remoteStylesEl.html(styles); + self.load = function(siteId, disabled) { + siteId = siteId || $mmSite.getId(); + disabled = !!disabled; + + $log.debug('Load site: ', siteId, disabled); + if (siteId && remoteStylesEls[siteId]) { + // Enable or disable the styles. + remoteStylesEls[siteId].element.attr('disabled', disabled); + + return self.get(siteId).then(function(data) { + var hash = md5.createHash(data.styles); + + // Update the styles only if they have changed. + if (remoteStylesEls[siteId].hash !== hash) { + remoteStylesEls[siteId].element.html(data.styles); + remoteStylesEls[siteId].hash = hash; + + // New styles will be applied even if the style is disabled. We'll disable it again if needed. + if (disabled && remoteStylesEls[siteId].element.attr('disabled') == 'disabled') { + remoteStylesEls[siteId].element.attr('disabled', true); + } } + + // Styles have been loaded, now treat the CSS. + treatCSSCode(siteId, data.file, data.styles); }); } + + return $q.reject(); }; + /** + * Preload the styles of the current site (stored in DB). Please do not use. + * + * @module mm.addons.remotestyles + * @ngdoc method + * @name $mmaRemoteStyles#_preloadCurrentSite + * @return {Promise} Promise resolved when loaded. + * @protected + */ + self._preloadCurrentSite = function() { + return $mmSitesManager.getStoredCurrentSiteId().then(function(siteId) { + return self.addSite(siteId); + }); + }; + + /** + * Preload the styles of all the stored sites. Please do not use. + * + * @module mm.addons.remotestyles + * @ngdoc method + * @name $mmaRemoteStyles#_preloadSites + * @return {Promise} Promise resolved when loaded. + * @protected + */ + self._preloadSites = function() { + return $mmSitesManager.getSitesIds().then(function(ids) { + var promises = []; + angular.forEach(ids, function(siteId) { + promises.push(self.addSite(siteId)); + }); + return $q.all(promises); + }); + }; + + /** + * Remove the styles of a certain site. + * + * @module mm.addons.remotestyles + * @ngdoc method + * @name $mmaRemoteStyles#removeSite + * @param {String} siteId Site ID. + * @return {Void} + */ + self.removeSite = function(siteId) { + if (siteId && remoteStylesEls[siteId]) { + remoteStylesEls[siteId].element.remove(); + delete remoteStylesEls[siteId]; + } + }; + + /** + * 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. + */ + function treatCSSCode(siteId, fileUrl, cssCode) { + if (!$mmFS.isAvailable()) { + return $q.reject(); + } + + var urls = $mmUtil.extractUrlsFromCSS(cssCode), + promises = [], + filePath, + updated = false; + + // Get the path of the CSS file. + promises.push($mmFilepool.getFilePathByUrl(siteId, fileUrl).then(function(path) { + filePath = path; + })); + + angular.forEach(urls, function(url) { + // Download the file only if it's an online URL. + if (url.indexOf('http') == 0) { + promises.push($mmFilepool.downloadUrl(siteId, url, false, mmaRemoteStylesComponent, 2).then(function(fileUrl) { + if (fileUrl != url) { + cssCode = cssCode.replace(new RegExp(url, 'g'), fileUrl); + updated = true; + } + }).catch(function(error) { + // It shouldn't happen. Ignore errors. + $log.warn('MMRMSTYLES Error treating file ', url, error); + })); + } + }); + + return $q.all(promises).then(function() { + // All files downloaded. Store the result if it has changed. + if (updated) { + return $mmFS.writeFile(filePath, cssCode); + } + }).then(function() { + return cssCode; + }); + } + return self; }); diff --git a/www/core/lib/sitesmanager.js b/www/core/lib/sitesmanager.js index 47cda2cadd8..db5a882f399 100644 --- a/www/core/lib/sitesmanager.js +++ b/www/core/lib/sitesmanager.js @@ -222,7 +222,7 @@ angular.module('mm.core') currentSite = candidateSite; // Store session. self.login(siteid); - $mmEvents.trigger(mmCoreEventSiteAdded); + $mmEvents.trigger(mmCoreEventSiteAdded, siteid); } else { return $translate(validation.error, validation.params).then(function(error) { return $q.reject(error); @@ -723,6 +723,20 @@ angular.module('mm.core') }); }; + /** + * Get the site ID stored in DB ad current site. + * + * @module mm.core + * @ngdoc method + * @name $mmSitesManager#getStoredCurrentSiteId + * @return {Promise} Promise resolved with the site ID. + */ + self.getStoredCurrentSiteId = function() { + return $mmApp.getDB().get(mmCoreCurrentSiteStore, 1).then(function(current_site) { + return current_site.siteid; + }); + }; + return self; }); diff --git a/www/core/lib/util.js b/www/core/lib/util.js index b2f15069853..6d895b017be 100644 --- a/www/core/lib/util.js +++ b/www/core/lib/util.js @@ -1039,6 +1039,31 @@ angular.module('mm.core') return true; }; + /** + * Search all the URLs in a CSS file content. + * + * @module mm.core + * @ngdoc method + * @name $mmUtil#extractUrlsFromCSS + * @param {String} code CSS code. + * @return {String[]} List of URLs. + */ + self.extractUrlsFromCSS = function(code) { + // First of all, search all the url(...) occurrences that don't include "data:". + var urls = [], + matches = code.match(/url\(\s*["']?(?!data:)([^)]+)\)/igm); + + // Extract the URL form each match. + angular.forEach(matches, function(match) { + var submatches = match.match(/url\(\s*['"]?([^'"]*)['"]?\s*\)/im); + if (submatches && submatches[1]) { + urls.push(submatches[1]); + } + }); + + return urls; + }; + return self; }; }); diff --git a/www/index.html b/www/index.html index f8b1ceb0338..5a9b67efd4b 100644 --- a/www/index.html +++ b/www/index.html @@ -17,7 +17,7 @@ - +