Add theme caching.
authorMichael M Slusarz <slusarz@curecanti.org>
Tue, 23 Nov 2010 05:20:11 +0000 (22:20 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Tue, 23 Nov 2010 07:58:53 +0000 (00:58 -0700)
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.

framework/Core/lib/Horde/Core/Factory/ThemesBuild.php [new file with mode: 0644]
framework/Core/lib/Horde/Themes.php
framework/Core/lib/Horde/Themes/Build.php [new file with mode: 0644]
framework/Core/lib/Horde/Themes/Css.php
framework/Core/lib/Horde/Themes/Element.php
framework/Core/package.xml
horde/docs/CHANGES
horde/services/portal/index.php

diff --git a/framework/Core/lib/Horde/Core/Factory/ThemesBuild.php b/framework/Core/lib/Horde/Core/Factory/ThemesBuild.php
new file mode 100644 (file)
index 0000000..c57135f
--- /dev/null
@@ -0,0 +1,125 @@
+<?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);
+            }
+        }
+    }
+
+}
index cff21a9..49f4ffd 100644 (file)
@@ -9,6 +9,7 @@
  *
  * @author   Michael Slusarz <slusarz@horde.org>
  * @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 (file)
index 0000000..fbe1d6c
--- /dev/null
@@ -0,0 +1,233 @@
+<?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);
+    }
+
+}
index 79eb0ce..5a32cdd 100644 (file)
@@ -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.
      * </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())
     {
@@ -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);
             }
index af389ce..a02dcd8 100644 (file)
@@ -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)
     {
index 595694c..e54aec8 100644 (file)
@@ -34,7 +34,7 @@ Application Framework.</description>
   <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).
@@ -164,6 +164,7 @@ Application Framework.</description>
        <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" />
@@ -258,6 +259,7 @@ Application Framework.</description>
       <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" />
@@ -784,6 +786,7 @@ Application Framework.</description>
    <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" />
@@ -828,6 +831,7 @@ Application Framework.</description>
    <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" />
index 0ab8689..ef1b8fe 100644 (file)
@@ -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.
index 40b121e..b4b3fab 100644 (file)
@@ -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']);
     }
 }