From: Michael M Slusarz Date: Tue, 23 Nov 2010 05:20:11 +0000 (-0700) Subject: Add theme caching. X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=d62ec32ea32b9f736c10ebc2f06cf06356c1c07c;p=horde.git Add theme caching. Themes are only cached if Horde_Cache is configured. TODO: Config option to disable this (and configurable lifetime) Script in horde/bin that will flush cache Debug logging Right now, themes are cached for 1 day and will be invalidated if the app's (or horde's) version number changes. --- diff --git a/framework/Core/lib/Horde/Core/Factory/ThemesBuild.php b/framework/Core/lib/Horde/Core/Factory/ThemesBuild.php new file mode 100644 index 000000000..c57135fca --- /dev/null +++ b/framework/Core/lib/Horde/Core/Factory/ThemesBuild.php @@ -0,0 +1,125 @@ + + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Core + */ + +/** + * A Horde_Injector:: based Horde_Themes_Build:: factory. + * + * Copyright 2010 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @category Horde + * @package Core + * @author Michael Slusarz + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Core + */ +class Horde_Core_Factory_ThemesBuild +{ + /** + * The list of cache IDs mapped to instance IDs. + * + * @var array + */ + private $_cacheids = array(); + + /** + * The injector. + * + * @var Horde_Injector + */ + private $_injector; + + /** + * Instances. + * + * @var array + */ + private $_instances = array(); + + /** + * Constructor. + * + * @param Horde_Injector $injector The injector to use. + */ + public function __construct(Horde_Injector $injector) + { + $this->_injector = $injector; + } + + /** + * Return the Horde_Themes_Build:: instance. + * + * @param string $app The application name. + * @param string $theme The theme name. + * + * @return Horde_Themes_Build The singleton instance. + */ + public function create($app, $theme) + { + $sig = implode('|', array($app, $theme)); + + if (isset($this->_instances[$sig])) { + return $this->_instances[$sig]; + } + + $cache = $this->_injector->getInstance('Horde_Cache'); + if ($cache instanceof Horde_Cache_Null) { + $instance = new Horde_Themes_Build($app, $theme); + } else { + $id = $sig . '|' . $GLOBALS['registry']->getVersion($app); + if ($app != 'horde') { + $id .= '|' . $GLOBALS['registry']->getVersion('horde'); + } + + $cache_sig = hash('md5', $id); + + try { + $instance = @unserialize($cache->get($cache_sig, 86400)); + } catch (Exception $e) { + $instance = null; + } + + if (!($instance instanceof Horde_Themes_Build)) { + $instance = new Horde_Themes_Build($app, $theme); + $instance->build(); + + if (empty($this->_cacheids)) { + register_shutdown_function(array($this, 'shutdown')); + } + } + + $this->_cacheids[$sig] = $cache_sig; + } + + $this->_instances[$sig] = $instance; + + return $this->_instances[$sig]; + } + + /** + * Store object in cache. + */ + public function shutdown() + { + $cache = $this->_injector->getInstance('Horde_Cache'); + + foreach ($this->_instances as $key => $val) { + if ($val->changed) { + $cache->set($this->_cacheids[$key], serialize($val), 86400); + } + } + } + +} diff --git a/framework/Core/lib/Horde/Themes.php b/framework/Core/lib/Horde/Themes.php index cff21a91d..49f4ffd58 100644 --- a/framework/Core/lib/Horde/Themes.php +++ b/framework/Core/lib/Horde/Themes.php @@ -9,6 +9,7 @@ * * @author Michael Slusarz * @category Horde + * @license http://www.fsf.org/copyleft/lgpl.html LGPL * @package Core */ class Horde_Themes diff --git a/framework/Core/lib/Horde/Themes/Build.php b/framework/Core/lib/Horde/Themes/Build.php new file mode 100644 index 000000000..fbe1d6cf8 --- /dev/null +++ b/framework/Core/lib/Horde/Themes/Build.php @@ -0,0 +1,233 @@ + + * @category Horde + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @package Core + */ +class Horde_Themes_Build implements Serializable +{ + /* Constants */ + const HORDE_DEFAULT = 1; + const APP_DEFAULT = 2; + const HORDE_THEME = 4; + const APP_THEME = 8; + + /** + * Has the data changed? + * + * @var boolean + */ + public $changed = false; + + /** + * Application name. + * + * @var string + */ + protected $_app; + + /** + * Theme data. + * + * @var array + */ + protected $_data = array(); + + /** + * Theme name. + * + * @var string + */ + protected $_theme; + + /** + * Constructor. + * + * @param string $app The application name. + * @param string $theme The theme name. + */ + public function __construct($app, $theme) + { + $this->_app = $app; + $this->_theme = $theme; + } + + /** + * Build the entire theme data structure. + * + * @throws UnexpectedValueException + */ + public function build() + { + $this->_data = array(); + + $this->_build('horde', 'default', self::HORDE_DEFAULT); + $this->_build('horde', $this->_theme, self::HORDE_THEME); + if ($this->_app != 'horde') { + $this->_build($this->_app, 'default', self::APP_DEFAULT); + $this->_build($this->_app, $this->_theme, self::APP_THEME); + } + + $this->changed = true; + } + + /** + * Add theme data from an app/theme combo. + * + * @param string $app The application name. + * @param string $theme The theme name. + * @param integer $mask Mask for the app/theme combo. + * + * @throws UnexpectedValueException + */ + protected function _build($app, $theme, $mask) + { + $path = $GLOBALS['registry']->get('themesfs', $app) . '/'. $theme; + $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); + + foreach ($it as $val) { + if (!$val->isDir()) { + $sub = $it->getSubPathname(); + + if (isset($this->_data[$sub])) { + $this->_data[$sub] |= $mask; + } else { + $this->_data[$sub] = $mask; + } + } + } + } + + /** + */ + public function get($item, $mask = 0) + { + if (!($entry = $this->_get($item))) { + return null; + } + + if ($mask) { + $entry &= $mask; + } + + if ($entry & self::APP_THEME) { + $app = $this->_app; + $theme = $this->_theme; + } elseif ($entry & self::HORDE_THEME) { + $app = 'horde'; + $theme = $this->_theme; + } elseif ($entry & self::APP_DEFAULT) { + $app = $this->_app; + $theme = 'default'; + } else { + $app = 'horde'; + $theme = 'default'; + } + + return $this->_getOutput($app, $theme, $item); + } + + /** + */ + protected function _get($item) + { + if (!isset($this->_data[$item])) { + $entry = 0; + + $path = $GLOBALS['registry']->get('themesfs', 'horde'); + if (file_exists($path . '/default/' . $item)) { + $entry |= self::HORDE_DEFAULT; + } + if (file_exists($path . '/' . $this->_theme . '/' . $item)) { + $entry |= self::HORDE_THEME; + } + + if ($this->_app != 'horde') { + $path = $GLOBALS['registry']->get('themesfs', $this->_app); + if (file_exists($path . '/default/' . $item)) { + $entry |= self::APP_DEFAULT; + } + if (file_exists($path . '/' . $this->_theme . '/' . $item)) { + $entry |= self::APP_THEME; + } + } + + $this->_data[$item] = $entry; + $this->changed = true; + } + + return $this->_data[$item]; + } + + /** + */ + protected function _getOutput($app, $theme, $item) + { + return array( + 'fs' => $GLOBALS['registry']->get('themesfs', $app) . '/' . $theme . '/' . $item, + 'uri' => $GLOBALS['registry']->get('themesuri', $app) . '/' . $theme . '/' . $item + ); + } + + /** + */ + public function getAll($item, $mask = 0) + { + if (!($entry = $this->_get($item))) { + return array(); + } + + if ($mask) { + $entry &= $mask; + } + $out = array(); + + if ($entry & self::APP_THEME) { + $out[] = $this->_getOutput($this->_app, $this->_theme, $item); + } + if ($entry & self::HORDE_THEME) { + $out[] = $this->_getOutput('horde', $this->_theme, $item); + } + if ($entry & self::APP_DEFAULT) { + $out[] = $this->_getOutput($this->_app, 'default', $item); + } + if ($entry & self::HORDE_DEFAULT) { + $out[] = $this->_getOutput('horde', 'default', $item); + } + + return $out; + } + + /* Serializable methods. */ + + /** + */ + public function serialize() + { + return serialize(array( + $this->_app, + $this->_data, + $this->_theme + )); + } + + /** + */ + public function unserialize($data) + { + list( + $this->_app, + $this->_data, + $this->_theme + ) = unserialize($data); + } + +} diff --git a/framework/Core/lib/Horde/Themes/Css.php b/framework/Core/lib/Horde/Themes/Css.php index 79eb0ce76..5a32cdd01 100644 --- a/framework/Core/lib/Horde/Themes/Css.php +++ b/framework/Core/lib/Horde/Themes/Css.php @@ -83,7 +83,7 @@ class Horde_Themes_Css if ($cache_type == 'none') { $css_out = array(); foreach ($css as $file) { - $css_out[] = $file['u']; + $css_out[] = $file['uri']; } return $css_out; } @@ -92,7 +92,7 @@ class Horde_Themes_Css $out = ''; foreach ($css as $file) { - $mtime[] = filemtime($file['f']); + $mtime[] = filemtime($file['fs']); } $sig = hash('md5', serialize($css) . max($mtime)); @@ -158,7 +158,11 @@ class Horde_Themes_Css * 'themeonly' - (boolean) If true, only load the theme files. * * - * @return array TODO + * @return array An array of 2-element array arrays containing 2 keys: + *
+     * fs - (string) Filesystem location of stylesheet.
+     * uri - (string) URI of stylesheet.
+     * 
*/ public function getStylesheets($theme = '', array $opts = array()) { @@ -166,7 +170,7 @@ class Horde_Themes_Css $theme = $GLOBALS['prefs']->getValue('theme'); } - $css = array(); + $add_css = $css_out = array(); $css_list = empty($opts['nobase']) ? $this->getBaseStylesheetList() : array(); @@ -176,50 +180,48 @@ class Horde_Themes_Css $curr_app = empty($opts['app']) ? $GLOBALS['registry']->getApp() : $opts['app']; - if (empty($opts['nohorde'])) { - $apps = array_unique(array('horde', $curr_app)); - } else { - $apps = ($curr_app == 'horde') ? array() : array($curr_app); - } + $mask = empty($opts['nohorde']) + ? 0 + : Horde_Themes_Build::APP_DEFAULT | Horde_Themes_Build::APP_THEME; $sub = empty($opts['sub']) ? null : $opts['sub']; - foreach ($apps as $app) { - $themes_fs = $GLOBALS['registry']->get('themesfs', $app) . '/'; - $themes_uri = Horde::url($GLOBALS['registry']->get('themesuri', $app), false, -1) . '/'; + $build = $GLOBALS['injector']->getInstance('Horde_Core_Factory_ThemesBuild')->create($curr_app, $theme); - foreach (array_filter(array_unique(array('default', $theme))) as $theme_name) { - foreach ($css_list as $css_name) { - if (empty($opts['subonly'])) { - $css[$themes_fs . $theme_name . '/' . $css_name] = $themes_uri . $theme_name . '/' . $css_name; - } + foreach ($css_list as $css_name) { + if (empty($opts['subonly'])) { + $css_out = array_merge($css_out, array_reverse($build->getAll($css_name, $mask))); + } - if ($sub && ($app == $curr_app)) { - $css[$themes_fs . $theme_name . '/' . $sub . '/' . $css_name] = $themes_uri . $theme_name . '/' . $sub . '/' . $css_name; - } - } + if ($sub) { + $css_out = array_merge($css_out, array_reverse($build->getAll($sub . '/' . $css_name, $mask))); } } /* Add additional stylesheets added by code. */ - $css = array_merge($css, $this->_cssFiles); + foreach ($this->_cssFiles as $f => $u) { + if (file_exists($f)) { + $add_css[$f] = $u; + } + } /* Add user-defined additional stylesheets. */ try { - $css = array_merge($css, Horde::callHook('cssfiles', array($theme), 'horde')); + $add_css = array_merge($add_css, Horde::callHook('cssfiles', array($theme), 'horde')); } catch (Horde_Exception_HookNotSet $e) {} + if ($curr_app != 'horde') { try { - $css = array_merge($css, Horde::callHook('cssfiles', array($theme), $curr_app)); + $add_css = array_merge($add_css, Horde::callHook('cssfiles', array($theme), $curr_app)); } catch (Horde_Exception_HookNotSet $e) {} } - $css_out = array(); - foreach ($css as $f => $u) { - if (file_exists($f)) { - $css_out[] = array('f' => $f, 'u' => $u); - } + foreach ($add_css as $f => $u) { + $css_out[] = array( + 'fs' => $f, + 'uri' => $u + ); } return $css_out; @@ -282,12 +284,12 @@ class Horde_Themes_Css $out = ''; foreach ($files as $file) { - $path = substr($file['u'], 0, strrpos($file['u'], '/') + 1); + $path = substr($file['uri'], 0, strrpos($file['uri'], '/') + 1); // Fix relative URLs, convert graphics URLs to data URLs // (if possible), remove multiple whitespaces, and strip // comments. - $tmp = preg_replace(array('/(url\(["\']?)([^\/])/i', '/\s+/', '/\/\*.*?\*\//'), array('$1' . $path . '$2', ' ', ''), implode('', file($file['f'], FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES))); + $tmp = preg_replace(array('/(url\(["\']?)([^\/])/i', '/\s+/', '/\/\*.*?\*\//'), array('$1' . $path . '$2', ' ', ''), implode('', file($file['fs'], FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES))); if ($dataurl) { $tmp = preg_replace_callback('/(background(?:-image)?:[^;}]*(?:url\(["\']?))(.*?)((?:["\']?\)))/i', array($this, '_stylesheetCallback'), $tmp); } diff --git a/framework/Core/lib/Horde/Themes/Element.php b/framework/Core/lib/Horde/Themes/Element.php index af389ceb8..a02dcd8ce 100644 --- a/framework/Core/lib/Horde/Themes/Element.php +++ b/framework/Core/lib/Horde/Themes/Element.php @@ -110,52 +110,34 @@ class Horde_Themes_Element : null; } - $this->_data = null; - - $app_list = array($this->app); - if (($this->app != 'horde') && empty($this->_opts['nohorde'])) { - $app_list[] = 'horde'; - } - $path = '/' . $this->_dirname . (is_null($this->_name) ? '' : '/' . $this->_name); - - /* Check themes first. */ $theme = array_key_exists('theme', $this->_opts) ? $this->_opts['theme'] : $prefs->getValue('theme'); - foreach (array_unique(array($theme, 'default')) as $theme) { - $tpath = '/' . $theme . $path; - - if (is_null($this->_name)) { - $this->_data = array( - 'fs' => $registry->get('themesfs', $this->app) . $tpath, - 'uri' => $registry->get('themesuri', $this->app) . $tpath - ); - } else { - foreach ($app_list as $app) { - $filepath = $registry->get('themesfs', $app) . $tpath; - if (file_exists($filepath)) { - $this->_data = array( - 'fs' => $filepath, - 'uri' => $registry->get('themesuri', $app) . $tpath - ); - break 2; - } - } - } + if (is_null($this->_name)) { + /* Return directory only. */ + $this->_data = array( + 'fs' => $registry->get('themesfs', $this->app) . '/' . $theme . '/' . $this->_dirname, + 'uri' => $registry->get('themesuri', $this->app) . '/' . $theme . '/' . $this->_dirname + ); + } else { + $build = $GLOBALS['injector']->getInstance('Horde_Core_Factory_ThemesBuild')->create($this->app, $theme); + $mask = empty($this->_opts['nohorde']) + ? 0 + : Horde_Themes_Build::APP_DEFAULT | Horde_Themes_Build::APP_THEME; + + $this->_data = $build->get($this->_dirname . '/' . $this->_name, $mask); } - return isset($this->_data[$name]) - ? $this->_data[$name] - : null; + return $this->_data[$name]; } /** - * Convert a URI into a Horde_Themes_Image object. + * Convert a URI into a Horde_Themes_Element object. * * @param string $uri The URI to convert. * - * @return Horde_Themes_Image An image object. + * @return Horde_Themes_Element A theme element object. */ static public function fromUri($uri) { diff --git a/framework/Core/package.xml b/framework/Core/package.xml index 595694cb2..e54aec898 100644 --- a/framework/Core/package.xml +++ b/framework/Core/package.xml @@ -34,7 +34,7 @@ Application Framework. beta LGPL - + * Add cache support for themes. * Add Horde_Session. * Add Horde::addInlineJsVars(). * Remove Horde::nocacheUrl() and Horde::url() (Ticket #9160). @@ -164,6 +164,7 @@ Application Framework. + @@ -258,6 +259,7 @@ Application Framework. + @@ -784,6 +786,7 @@ Application Framework. + @@ -828,6 +831,7 @@ Application Framework. + diff --git a/horde/docs/CHANGES b/horde/docs/CHANGES index 0ab868939..ef1b8fe76 100644 --- a/horde/docs/CHANGES +++ b/horde/docs/CHANGES @@ -2,6 +2,7 @@ v4.0-cvs -------- +[mms] Add theme caching. [mms] Add hook to allow browser capabilities to be modified. [jan] Add a configuration switch for automatic creation of default shares. [cjh] Move from Net_DNS to Net_DNS2. diff --git a/horde/services/portal/index.php b/horde/services/portal/index.php index 40b121eb1..b4b3fab8c 100644 --- a/horde/services/portal/index.php +++ b/horde/services/portal/index.php @@ -53,8 +53,8 @@ $layout_html = $view->toHtml(); $css = $injector->getInstance('Horde_Themes_Css'); foreach ($view->getApplications() as $app) { - foreach ($css->getStylesheets('', array('app' => $app)) as $f => $u) { - $css->addStylesheet($f, $u); + foreach ($css->getStylesheets('', array('app' => $app)) as $val) { + $css->addStylesheet($val['fs'], $val['uri']); } }