From 8fa22e9c2f3a38985464252045e57b6be4eb76ae Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Fri, 26 Oct 2012 14:20:40 +0200 Subject: [PATCH] Prepare template processor to handle skin inheritance --- program/include/rcube_output_html.php | 183 +++++++++++++++++--------- 1 file changed, 122 insertions(+), 61 deletions(-) diff --git a/program/include/rcube_output_html.php b/program/include/rcube_output_html.php index 6138e2a30..2e3cd506d 100644 --- a/program/include/rcube_output_html.php +++ b/program/include/rcube_output_html.php @@ -33,7 +33,7 @@ class rcube_output_html extends rcube_output protected $js_env = array(); protected $js_labels = array(); protected $js_commands = array(); - protected $plugin_skin_path; + protected $skin_paths = array(); protected $template_name; protected $scripts_path = ''; protected $script_files = array(); @@ -161,9 +161,27 @@ class rcube_output_html extends rcube_output $this->config->set('skin_path', $skin_path); + // register skin path(s) + $this->skin_paths = array(); + $this->load_skin($skin_path); + return $valid; } + /** + * Helper method to recursively read skin meta files and register search paths + */ + private function load_skin($skin_path) + { + $this->skin_paths[] = $skin_path; + + // read meta file and check for dependecies + $meta = @json_decode(@file_get_contents($skin_path.'/meta.json'), true); + if ($meta['extends'] && is_dir('skins/' . $meta['extends'])) { + $this->load_skin('skins/' . $meta['extends']); + } + } + /** * Check if a specific template exists @@ -173,8 +191,14 @@ class rcube_output_html extends rcube_output */ public function template_exists($name) { - $filename = $this->config->get('skin_path') . '/templates/' . $name . '.html'; - return (is_file($filename) && is_readable($filename)) || ($this->deprecated_templates[$name] && $this->template_exists($this->deprecated_templates[$name])); + $found = false; + foreach ($this->skin_paths as $skin_path) { + $filename = $skin_path . '/templates/' . $name . '.html'; + $found = (is_file($filename) && is_readable($filename)) || ($this->deprecated_templates[$name] && $this->template_exists($this->deprecated_templates[$name])); + if ($found) + break; + } + return $found; } @@ -359,41 +383,60 @@ class rcube_output_html extends rcube_output */ function parse($name = 'main', $exit = true, $write = true) { - $skin_path = $this->config->get('skin_path'); $plugin = false; $realname = $name; - $temp = explode('.', $name, 2); - - $this->plugin_skin_path = null; - $this->template_name = $realname; + $this->template_name = $realname; + $temp = explode('.', $name, 2); if (count($temp) > 1) { $plugin = $temp[0]; $name = $temp[1]; $skin_dir = $plugin . '/skins/' . $this->config->get('skin'); - $skin_path = $this->plugin_skin_path = $this->app->plugins->dir . $skin_dir; - // fallback to default skin - if (!is_dir($skin_path)) { + // apply skin search escalation list to plugin directory + $plugin_skin_paths = array(); + foreach ($this->skin_paths as $skin_path) { + $plugin_skin_paths[] = $this->app->plugins->dir . $plugin . '/' . $skin_path; + } + + // add fallback to default skin + if (is_dir($this->app->plugins->dir . $plugin . '/skins/default')) { $skin_dir = $plugin . '/skins/default'; - $skin_path = $this->plugin_skin_path = $this->app->plugins->dir . $skin_dir; + $plugin_skin_paths[] = $this->app->plugins->dir . $skin_dir; } + + // add plugin skin paths to search list + $this->skin_paths = array_merge($plugin_skin_paths, $this->skin_paths); } - $path = "$skin_path/templates/$name.html"; + // find skin template + $path = false; + foreach ($this->skin_paths as $skin_path) { + $path = "$skin_path/templates/$name.html"; - if (!is_readable($path) && $this->deprecated_templates[$realname]) { - $path = "$skin_path/templates/".$this->deprecated_templates[$realname].".html"; - if (is_readable($path)) - rcube::raise_error(array('code' => 502, 'type' => 'php', + // fallback to deprecated template names + if (!is_readable($path) && $this->deprecated_templates[$realname]) { + $path = "$skin_path/templates/" . $this->deprecated_templates[$realname] . ".html"; + rcube::raise_error(array( + 'code' => 502, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Using deprecated template '".$this->deprecated_templates[$realname] - ."' in $skin_path/templates. Please rename to '".$realname."'"), - true, false); + 'message' => "Using deprecated template '" . $this->deprecated_templates[$realname] + . "' in $skin_path/templates. Please rename to '$realname'"), + true, false); + } + + if (is_readable($path)) { + $this->config->set('skin_path', $skin_path); + $this->base_path = $skin_path; + break; + } + else { + $path = false; + } } // read template file - if (($templ = @file_get_contents($path)) === false) { + if (!$path || ($templ = @file_get_contents($path)) === false) { rcube::raise_error(array( 'code' => 501, 'type' => 'php', @@ -421,7 +464,7 @@ class rcube_output_html extends rcube_output $output = $hook['content']; unset($hook['content']); - $output = $this->parse_with_globals($output); + $output = $this->parse_with_globals($this->fix_paths($output)); // make sure all
tags have a valid request token $output = preg_replace_callback('/]+)>/Ui', array($this, 'alter_form_tag'), $output); @@ -493,7 +536,7 @@ class rcube_output_html extends rcube_output public function abs_url($str) { if ($str[0] == '/') - return $this->config->get('skin_path') . $str; + return $this->base_path . $str; else return $str; } @@ -527,7 +570,7 @@ class rcube_output_html extends rcube_output { $GLOBALS['__version'] = html::quote(RCMAIL_VERSION); $GLOBALS['__comm_path'] = html::quote($this->app->comm_path); - $GLOBALS['__skin_path'] = html::quote($this->config->get('skin_path')); + $GLOBALS['__skin_path'] = html::quote($this->base_path); return preg_replace_callback('/\$(__[a-z0-9_\-]+)/', array($this, 'globals_callback'), $input); @@ -543,6 +586,43 @@ class rcube_output_html extends rcube_output } + /** + * Correct absolute paths in images and other tags + * add timestamp to .js and .css filename + */ + protected function fix_paths($output) + { + return preg_replace_callback( + '!(src|href|background)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i', + array($this, 'file_callback'), $output); + } + + + /** + * Callback function for preg_replace_callback in write() + * + * @return string Parsed string + */ + protected function file_callback($matches) + { + $file = $matches[3]; + + // correct absolute paths + if ($file[0] == '/') { + $file = $this->base_path . $file; + } + + // add file modification timestamp + if (preg_match('/\.(js|css)$/', $file)) { + if ($fs = @filemtime($file)) { + $file .= '?s=' . $fs; + } + } + + return $matches[1] . '=' . $matches[2] . $file . $matches[4]; + } + + /** * Public wrapper to dipp into template parsing. * @@ -705,6 +785,9 @@ class rcube_output_html extends rcube_output // show a label case 'label': + if ($attrib['expression']) + $attrib['name'] = eval("return " . $this->parse_expression($attrib['expression']) .";"); + if ($attrib['name'] || $attrib['command']) { // @FIXME: 'noshow' is useless, remove? if ($attrib['noshow']) { @@ -736,8 +819,17 @@ class rcube_output_html extends rcube_output // include a file case 'include': - if (!$this->plugin_skin_path || !is_file($path = realpath($this->plugin_skin_path . $attrib['file']))) - $path = realpath(($attrib['skin_path'] ? $attrib['skin_path'] : $this->config->get('skin_path')).$attrib['file']); + $old_base_path = $this->base_path; + $skin_paths = $this->skin_paths; + if ($attrib['skin_path']) + array_unshift($skin_paths, $attrib['skin_path']); + foreach ($skin_paths as $skin_path) { + $path = realpath($skin_path . $attrib['file']); + if (is_file($path)) { + $this->base_path = $skin_path; + break; + } + } if (is_readable($path)) { if ($this->config->get('skin_include_php')) { @@ -747,14 +839,16 @@ class rcube_output_html extends rcube_output $incl = file_get_contents($path); } $incl = $this->parse_conditions($incl); - return $this->parse_xml($incl); + $incl = $this->parse_xml($incl); + $incl = $this->fix_paths($incl); + $this->base_path = $old_base_path; + return $incl; } break; case 'plugin.include': $hook = $this->app->plugins->exec_hook("template_plugin_include", $attrib); return $hook['content']; - break; // define a container block case 'container': @@ -1234,14 +1328,6 @@ class rcube_output_html extends rcube_output $output = substr_replace($output, $css, $pos, 0); } - $this->base_path = $base_path; - - // correct absolute paths in images and other tags - // add timestamp to .js and .css filename - $output = preg_replace_callback( - '!(src|href|background)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i', - array($this, 'file_callback'), $output); - // trigger hook with final HTML content to be sent $hook = $this->app->plugins->exec_hook("send_page", array('content' => $output)); if (!$hook['abort']) { @@ -1255,31 +1341,6 @@ class rcube_output_html extends rcube_output } - /** - * Callback function for preg_replace_callback in write() - * - * @return string Parsed string - */ - protected function file_callback($matches) - { - $file = $matches[3]; - - // correct absolute paths - if ($file[0] == '/') { - $file = $this->base_path . $file; - } - - // add file modification timestamp - if (preg_match('/\.(js|css)$/', $file)) { - if ($fs = @filemtime($file)) { - $file .= '?s=' . $fs; - } - } - - return $matches[1] . '=' . $matches[2] . $file . $matches[4]; - } - - /** * Returns iframe object, registers some related env variables *