Thomas Bruederli
2012-11-06 28de3911825bde79ee5a92762940ef310baab737
program/include/rcube_output_html.php
@@ -17,10 +17,7 @@
 +-----------------------------------------------------------------------+
 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
 +-----------------------------------------------------------------------+
 $Id$
 */
*/
/**
@@ -36,7 +33,7 @@
    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();
@@ -70,14 +67,24 @@
        $this->set_env('task', $task);
        $this->set_env('x_frame_options', $this->config->get('x_frame_options', 'sameorigin'));
        // add cookie info
        $this->set_env('cookie_domain', ini_get('session.cookie_domain'));
        $this->set_env('cookie_path', ini_get('session.cookie_path'));
        $this->set_env('cookie_secure', ini_get('session.cookie_secure'));
        // load the correct skin (in case user-defined)
        $this->set_skin($this->config->get('skin'));
        $skin = $this->config->get('skin');
        $this->set_skin($skin);
        $this->set_env('skin', $skin);
        if (!empty($_REQUEST['_extwin']))
          $this->set_env('extwin', 1);
        // add common javascripts
        $this->add_script('var '.JS_OBJECT_NAME.' = new rcube_webmail();', 'head_top');
        $this->add_script('var '.rcmail::JS_OBJECT_NAME.' = new rcube_webmail();', 'head_top');
        // don't wait for page onload. Call init at the bottom of the page (delayed)
        $this->add_script(JS_OBJECT_NAME.'.init();', 'docready');
        $this->add_script(rcmail::JS_OBJECT_NAME.'.init();', 'docready');
        $this->scripts_path = 'program/js/';
        $this->include_script('jquery.min.js');
@@ -150,14 +157,32 @@
        else {
            $skin_path = $this->config->get('skin_path');
            if (!$skin_path) {
                $skin_path = 'skins/default';
                $skin_path = 'skins/' . rcube_config::DEFAULT_SKIN;
            }
            $valid = !$skin;
        }
        $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']);
        }
    }
@@ -169,8 +194,39 @@
     */
    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;
    }
    /**
     * Find the given file in the current skin path stack
     *
     * @param string File name/path to resolve (starting with /)
     * @param string Reference to the base path of the matching skin
     * @param string Additional path to search in
     * @return mixed Relative path to the requested file or False if not found
     */
    public function get_skin_file($file, &$skin_path, $add_path = null)
    {
        $skin_paths = $this->skin_paths;
        if ($add_path)
            array_unshift($skin_paths, $add_path);
        foreach ($skin_paths as $skin_path) {
            $path = realpath($skin_path . $file);
            if (is_file($path)) {
                return $skin_path . $file;
            }
        }
        return false;
    }
@@ -183,7 +239,7 @@
     */
    public function add_gui_object($obj, $id)
    {
        $this->add_script(JS_OBJECT_NAME.".gui_object('$obj', '$id');");
        $this->add_script(rcmail::JS_OBJECT_NAME.".gui_object('$obj', '$id');");
    }
@@ -270,6 +326,8 @@
     */
    public function redirect($p = array(), $delay = 1)
    {
        if ($this->env['extwin'])
            $p['extwin'] = 1;
        $location = $this->app->url($p);
        header('Location: ' . $location);
        exit;
@@ -355,48 +413,67 @@
     */
    function parse($name = 'main', $exit = true, $write = true)
    {
        $skin_path = $this->config->get('skin_path');
        $plugin    = false;
        $realname  = $name;
        $temp      = explode('.', $name, 2);
        $this->template_name = $realname;
        $this->plugin_skin_path = null;
        $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->url . $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->url . $skin_dir;
            }
            // add plugin skin paths to search list
            $this->skin_paths = array_merge($plugin_skin_paths, $this->skin_paths);
        }
        // find skin template
        $path = false;
        foreach ($this->skin_paths as $skin_path) {
            $path = "$skin_path/templates/$name.html";
            // 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);
            }
            if (is_readable($path)) {
                $this->config->set('skin_path', $skin_path);
                $this->base_path = $skin_path;
                break;
            }
            else {
                $path = false;
            }
        }
        $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',
                    'file' => __FILE__, 'line' => __LINE__,
                    'message' => "Using deprecated template '".$this->deprecated_templates[$realname]
                        ."' in $skin_path/templates. Please rename to '".$realname."'"),
                true, 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',
                'line' => __LINE__,
                'file' => __FILE__,
                'message' => 'Error loading template for '.$realname
                ), true, true);
                ), true, $write);
            return false;
        }
@@ -417,7 +494,7 @@
        $output = $hook['content'];
        unset($hook['content']);
        $output = $this->parse_with_globals($output);
        $output = $this->parse_with_globals($this->fix_paths($output));
        // make sure all <form> tags have a valid request token
        $output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output);
@@ -457,7 +534,7 @@
    {
        $out = '';
        if (!$this->framed && !empty($this->js_env)) {
            $out .= JS_OBJECT_NAME . '.set_env('.self::json_serialize($this->js_env).");\n";
            $out .= rcmail::JS_OBJECT_NAME . '.set_env('.self::json_serialize($this->js_env).");\n";
        }
        if (!empty($this->js_labels)) {
            $this->command('add_label', $this->js_labels);
@@ -470,7 +547,7 @@
            $parent = $this->framed || preg_match('/^parent\./', $method);
            $out .= sprintf(
                "%s.%s(%s);\n",
                ($parent ? 'if(window.parent && parent.'.JS_OBJECT_NAME.') parent.' : '') . JS_OBJECT_NAME,
                ($parent ? 'if(window.parent && parent.'.rcmail::JS_OBJECT_NAME.') parent.' : '') . rcmail::JS_OBJECT_NAME,
                preg_replace('/^parent\./', '', $method),
                implode(',', $args)
            );
@@ -484,12 +561,17 @@
     * Make URLs starting with a slash point to skin directory
     *
     * @param  string Input string
     * @param  boolean True if URL should be resolved using the current skin path stack
     * @return string
     */
    public function abs_url($str)
    public function abs_url($str, $search_path = false)
    {
        if ($str[0] == '/')
            return $this->config->get('skin_path') . $str;
        if ($str[0] == '/') {
            if ($search_path && ($file_url = $this->get_skin_file($str, $skin_path)))
                return $file_url;
            return $this->base_path . $str;
        }
        else
            return $str;
    }
@@ -523,7 +605,7 @@
    {
        $GLOBALS['__version']   = html::quote(RCMAIL_VERSION);
        $GLOBALS['__comm_path'] = html::quote($this->app->comm_path);
        $GLOBALS['__skin_path'] = Q($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);
@@ -536,6 +618,43 @@
    protected function globals_callback($matches)
    {
        return $GLOBALS[$matches[1]];
    }
    /**
     * 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];
    }
@@ -644,7 +763,7 @@
                "\$_SESSION['\\1']",
                "\$this->app->config->get('\\1',get_boolean('\\3'))",
                "\$this->env['\\1']",
                "rcube_ui::get_input_value('\\1', rcube_ui::INPUT_GPC)",
                "rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
                "\$_COOKIE['\\1']",
                "\$this->browser->{'\\1'}",
                $this->template_name,
@@ -694,20 +813,52 @@
                }
                break;
            // frame
            case 'frame':
                return $this->frame($attrib);
                break;
            // 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']) {
                        return '';
                    }
                    $vars = $attrib + array('product' => $this->config->get('product_name'));
                    unset($vars['name'], $vars['command']);
                    $label = $this->app->gettext($attrib + array('vars' => $vars));
                    return !$attrib['noshow'] ? (get_boolean((string)$attrib['html']) ? $label : html::quote($label)) : '';
                    $label   = $this->app->gettext($attrib + array('vars' => $vars));
                    $quoting = !empty($attrib['quoting']) ? strtolower($attrib['quoting']) : (get_boolean((string)$attrib['html']) ? 'no' : '');
                    switch ($quoting) {
                        case 'no':
                        case 'raw':
                            break;
                        case 'javascript':
                        case 'js':
                            $label = rcmail::JQ($label);
                            break;
                        default:
                            $label = html::quote($label);
                            break;
                    }
                    return $label;
                }
                break;
            // 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;
                if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attrib['skin_path'])) {
                    $this->base_path = $skin_path;
                    $path = realpath($path);
                }
                if (is_readable($path)) {
                    if ($this->config->get('skin_include_php')) {
@@ -717,14 +868,16 @@
                      $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':
@@ -770,6 +923,13 @@
                        if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs))
                          $ver .= ' [SVN r'.$regs[1].']';
                    }
                    else if (is_file(INSTALL_PATH . '.git/index')) {
                        if (preg_match('/Date:\s+([^\n]+)/', @shell_exec('git log -1'), $regs)) {
                            if ($date = date('Ymd.Hi', strtotime($regs[1]))) {
                                $ver .= ' [GIT '.$date.']';
                            }
                        }
                    }
                    $content = html::quote($ver);
                }
                else if ($object == 'steptitle') {
@@ -812,7 +972,7 @@
                        }
                        break;
                    case 'request':
                        $value = rcube_ui::get_input_value($name, rcube_ui::INPUT_GPC);
                        $value = rcube_utils::get_input_value($name, rcube_utils::INPUT_GPC);
                        break;
                    case 'session':
                        $value = $_SESSION[$name];
@@ -917,7 +1077,7 @@
        if ($attrib['command']) {
            $this->add_script(sprintf(
                "%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');",
                JS_OBJECT_NAME,
                rcmail::JS_OBJECT_NAME,
                $command,
                $attrib['id'],
                $attrib['type'],
@@ -929,7 +1089,7 @@
            // make valid href to specific buttons
            if (in_array($attrib['command'], rcmail::$main_tasks)) {
                $attrib['href']    = $this->app->url(array('task' => $attrib['command']));
                $attrib['onclick'] = sprintf("%s.command('switch-task','%s');return false", JS_OBJECT_NAME, $attrib['command']);
                $attrib['onclick'] = sprintf("%s.command('switch-task','%s',null,event); return false", rcmail::JS_OBJECT_NAME, $attrib['command']);
            }
            else if ($attrib['task'] && in_array($attrib['task'], rcmail::$main_tasks)) {
                $attrib['href'] = $this->app->url(array('action' => $attrib['command'], 'task' => $attrib['task']));
@@ -937,7 +1097,7 @@
            else if (in_array($attrib['command'], $a_static_commands)) {
                $attrib['href'] = $this->app->url(array('action' => $attrib['command']));
            }
            else if ($attrib['command'] == 'permaurl' && !empty($this->env['permaurl'])) {
            else if (($attrib['command'] == 'permaurl' || $attrib['command'] == 'extwin') && !empty($this->env['permaurl'])) {
              $attrib['href'] = $this->env['permaurl'];
            }
        }
@@ -952,8 +1112,8 @@
        }
        else if ($command && !$attrib['onclick']) {
            $attrib['onclick'] = sprintf(
                "return %s.command('%s','%s',this)",
                JS_OBJECT_NAME,
                "return %s.command('%s','%s',this,event)",
                rcmail::JS_OBJECT_NAME,
                $command,
                $attrib['prop']
            );
@@ -1197,16 +1357,8 @@
            $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 = rcmail::get_instance()->plugins->exec_hook("send_page", array('content' => $output));
        $hook = $this->app->plugins->exec_hook("send_page", array('content' => $output));
        if (!$hook['abort']) {
            if ($this->charset != RCMAIL_CHARSET) {
                echo rcube_charset::convert($hook['content'], RCMAIL_CHARSET, $this->charset);
@@ -1219,27 +1371,30 @@
    /**
     * Callback function for preg_replace_callback in write()
     * Returns iframe object, registers some related env variables
     *
     * @return string Parsed string
     * @param array $attrib HTML attributes
     * @param boolean $is_contentframe Register this iframe as the 'contentframe' gui object
     * @return string IFRAME element
     */
    protected function file_callback($matches)
    public function frame($attrib, $is_contentframe = false)
    {
       $file = $matches[3];
        static $idcount = 0;
        // correct absolute paths
       if ($file[0] == '/') {
           $file = $this->base_path . $file;
        if (!$attrib['id']) {
            $attrib['id'] = 'rcmframe' . ++$idcount;
        }
        // add file modification timestamp
       if (preg_match('/\.(js|css)$/', $file)) {
            if ($fs = @filemtime($file)) {
                $file .= '?s=' . $fs;
            }
        $attrib['name'] = $attrib['id'];
        $attrib['src'] = $attrib['src'] ? $this->abs_url($attrib['src'], true) : 'program/resources/blank.gif';
        // register as 'contentframe' object
        if ($is_contentframe)
            $this->set_env('contentframe', $attrib['name']);
            $this->set_env('blankpage', $attrib['src']);
        }
       return $matches[1] . '=' . $matches[2] . $file . $matches[4];
        return html::iframe($attrib);
    }
@@ -1256,6 +1411,10 @@
    {
      if ($this->framed || !empty($_REQUEST['_framed'])) {
        $hiddenfield = new html_hiddenfield(array('name' => '_framed', 'value' => '1'));
        $hidden = $hiddenfield->show();
      }
      if ($this->env['extwin']) {
        $hiddenfield = new html_hiddenfield(array('name' => '_extwin', 'value' => '1'));
        $hidden = $hiddenfield->show();
      }
@@ -1328,7 +1487,7 @@
            $username = $this->app->user->get_username();
        }
        return rcube_idn_to_utf8($username);
        return rcube_utils::idn_to_utf8($username);
    }
@@ -1347,9 +1506,12 @@
        $_SESSION['temp'] = true;
        // save original url
        $url = rcube_ui::get_input_value('_url', rcube_ui::INPUT_POST);
        $url = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST);
        if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING']))
            $url = $_SERVER['QUERY_STRING'];
        // Disable autocapitalization on iPad/iPhone (#1488609)
        $attrib['autocapitalize'] = 'off';
        // set atocomplete attribute
        $user_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
@@ -1359,7 +1521,6 @@
        $input_task   = new html_hiddenfield(array('name' => '_task', 'value' => 'login'));
        $input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login'));
        $input_tzone  = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_'));
        $input_dst    = new html_hiddenfield(array('name' => '_dstactive', 'id' => 'rcmlogindst', 'value' => '_default_'));
        $input_url    = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => $url));
        $input_user   = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser')
            + $attrib + $user_attrib);
@@ -1397,7 +1558,7 @@
        $table = new html_table(array('cols' => 2));
        $table->add('title', html::label('rcmloginuser', html::quote($this->app->gettext('username'))));
        $table->add('input', $input_user->show(rcube_ui::get_input_value('_user', rcube_ui::INPUT_GPC)));
        $table->add('input', $input_user->show(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC)));
        $table->add('title', html::label('rcmloginpwd', html::quote($this->app->gettext('password'))));
        $table->add('input', $input_pass->show());
@@ -1405,13 +1566,12 @@
        // add host selection row
        if (is_object($input_host) && !$hide_host) {
            $table->add('title', html::label('rcmloginhost', html::quote($this->app->gettext('server'))));
            $table->add('input', $input_host->show(rcube_ui::get_input_value('_host', rcube_ui::INPUT_GPC)));
            $table->add('input', $input_host->show(rcube_utils::get_input_value('_host', rcube_utils::INPUT_GPC)));
        }
        $out  = $input_task->show();
        $out .= $input_action->show();
        $out .= $input_tzone->show();
        $out .= $input_dst->show();
        $out .= $input_url->show();
        $out .= $table->show();
@@ -1423,6 +1583,9 @@
        if (empty($attrib['form'])) {
            $out = $this->form_tag(array('name' => $form_name, 'method' => 'post'), $out);
        }
        // include script for timezone detection
        $this->include_script('jstz.min.js');
        return $out;
    }
@@ -1481,7 +1644,7 @@
        if (empty($attrib['form'])) {
            $out = $this->form_tag(array(
                'name' => "rcmqsearchform",
                'onsubmit' => JS_OBJECT_NAME . ".command('search');return false;",
                'onsubmit' => rcmail::JS_OBJECT_NAME . ".command('search'); return false",
                'style' => "display:inline"),
                $out);
        }
@@ -1560,16 +1723,20 @@
            'GB2312'       => 'GB2312 ('.$this->app->gettext('chinese').')',
        );
        if (!empty($_POST['_charset']))
           $set = $_POST['_charset'];
       else if (!empty($attrib['selected']))
           $set = $attrib['selected'];
       else
           $set = $this->get_charset();
        if (!empty($_POST['_charset'])) {
            $set = $_POST['_charset'];
        }
        else if (!empty($attrib['selected'])) {
            $set = $attrib['selected'];
        }
        else {
            $set = $this->get_charset();
        }
       $set = strtoupper($set);
       if (!isset($charsets[$set]))
           $charsets[$set] = $set;
        $set = strtoupper($set);
        if (!isset($charsets[$set])) {
            $charsets[$set] = $set;
        }
        $select = new html_select($field_attrib);
        $select->add(array_values($charsets), array_keys($charsets));