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.
--- /dev/null
+<?php
+/**
+ * A Horde_Injector:: based Horde_Themes_Build:: factory.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package Core
+ * @author Michael Slusarz <slusarz@horde.org>
+ * @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 <slusarz@horde.org>
+ * @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);
+ }
+ }
+ }
+
+}
*
* @author Michael Slusarz <slusarz@horde.org>
* @category Horde
+ * @license http://www.fsf.org/copyleft/lgpl.html LGPL
* @package Core
*/
class Horde_Themes
--- /dev/null
+<?php
+/**
+ * This class is responsible for parsing/building a theme.
+ *
+ * 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.
+ *
+ * @author Michael Slusarz <slusarz@horde.org>
+ * @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);
+ }
+
+}
if ($cache_type == 'none') {
$css_out = array();
foreach ($css as $file) {
- $css_out[] = $file['u'];
+ $css_out[] = $file['uri'];
}
return $css_out;
}
$out = '';
foreach ($css as $file) {
- $mtime[] = filemtime($file['f']);
+ $mtime[] = filemtime($file['fs']);
}
$sig = hash('md5', serialize($css) . max($mtime));
* 'themeonly' - (boolean) If true, only load the theme files.
* </pre>
*
- * @return array TODO
+ * @return array An array of 2-element array arrays containing 2 keys:
+ * <pre>
+ * fs - (string) Filesystem location of stylesheet.
+ * uri - (string) URI of stylesheet.
+ * </pre>
*/
public function getStylesheets($theme = '', array $opts = array())
{
$theme = $GLOBALS['prefs']->getValue('theme');
}
- $css = array();
+ $add_css = $css_out = array();
$css_list = empty($opts['nobase'])
? $this->getBaseStylesheetList()
: array();
$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;
$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);
}
: 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)
{
<api>beta</api>
</stability>
<license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
- <notes>
+ <notes>* Add cache support for themes.
* Add Horde_Session.
* Add Horde::addInlineJsVars().
* Remove Horde::nocacheUrl() and Horde::url() (Ticket #9160).
<file name="ShareBase.php" role="php" />
<file name="Template.php" role="php" />
<file name="TextFilter.php" role="php" />
+ <file name="ThemesBuild.php" role="php" />
<file name="Token.php" role="php" />
<file name="Tree.php" role="php" />
<file name="Twitter.php" role="php" />
<file name="Files.php" role="php" />
</dir> <!-- /lib/Horde/Script -->
<dir name="Themes">
+ <file name="Build.php" role="php" />
<file name="Css.php" role="php" />
<file name="Element.php" role="php" />
<file name="Image.php" role="php" />
<install as="Horde/Core/Factory/ShareBase.php" name="lib/Horde/Core/Factory/ShareBase.php" />
<install as="Horde/Core/Factory/Template.php" name="lib/Horde/Core/Factory/Template.php" />
<install as="Horde/Core/Factory/TextFilter.php" name="lib/Horde/Core/Factory/TextFilter.php" />
+ <install as="Horde/Core/Factory/ThemesBuild.php" name="lib/Horde/Core/Factory/ThemesBuild.php" />
<install as="Horde/Core/Factory/Token.php" name="lib/Horde/Core/Factory/Token.php" />
<install as="Horde/Core/Factory/Tree.php" name="lib/Horde/Core/Factory/Tree.php" />
<install as="Horde/Core/Factory/Twitter.php" name="lib/Horde/Core/Factory/Twitter.php" />
<install as="Horde/Registry/Application.php" name="lib/Horde/Registry/Application.php" />
<install as="Horde/Registry/Caller.php" name="lib/Horde/Registry/Caller.php" />
<install as="Horde/Script/Files.php" name="lib/Horde/Script/Files.php" />
+ <install as="Horde/Themes/Build.php" name="lib/Horde/Themes/Build.php" />
<install as="Horde/Themes/Css.php" name="lib/Horde/Themes/Css.php" />
<install as="Horde/Themes/Element.php" name="lib/Horde/Themes/Element.php" />
<install as="Horde/Themes/Image.php" name="lib/Horde/Themes/Image.php" />
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.
$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']);
}
}