| +-----------------------------------------------------------------------+ $Id$ */ /** * Class to create HTML page output using a skin template * * @package View * @todo Documentation * @uses rcube_html_page */ class rcube_template extends rcube_html_page { var $config; var $task = ''; var $framed = false; var $pagetitle = ''; var $env = array(); var $js_env = array(); var $js_commands = array(); var $object_handlers = array(); public $ajax_call = false; /** * Constructor * * @todo Use jQuery's $(document).ready() here. */ public function __construct(&$config, $task) { parent::__construct(); $this->task = $task; $this->config = $config; // add common javascripts $javascript = 'var '.JS_OBJECT_NAME.' = new rcube_webmail();'; // don't wait for page onload. Call init at the bottom of the page (delayed) $javascript_foot = "if (window.call_init)\n call_init('".JS_OBJECT_NAME."');"; $this->add_script($javascript, 'head_top'); $this->add_script($javascript_foot, 'foot'); $this->scripts_path = 'program/js/'; $this->include_script('common.js'); $this->include_script('app.js'); // register common UI objects $this->add_handlers(array( 'loginform' => array($this, 'login_form'), 'username' => array($this, 'current_username'), 'message' => array($this, 'message_container'), 'charsetselector' => array($this, 'charset_selector'), )); } /** * Set environment variable * * @param string Property name * @param mixed Property value * @param boolean True if this property should be added to client environment */ public function set_env($name, $value, $addtojs = true) { $this->env[$name] = $value; if ($addtojs || isset($this->js_env[$name])) { $this->js_env[$name] = $value; } } /** * Set page title variable */ public function set_pagetitle($title) { $this->pagetitle = $title; } /** * Register a template object handler * * @param string Object name * @param string Function name to call * @return void */ public function add_handler($obj, $func) { $this->object_handlers[$obj] = $func; } /** * Register a list of template object handlers * * @param array Hash array with object=>handler pairs * @return void */ public function add_handlers($arr) { $this->object_handlers = array_merge($this->object_handlers, $arr); } /** * Register a GUI object to the client script * * @param string Object name * @param string Object ID * @return void */ public function add_gui_object($obj, $id) { $this->add_script(JS_OBJECT_NAME.".gui_object('$obj', '$id');"); } /** * Call a client method * * @param string Method to call * @param ... Additional arguments */ public function command() { $this->js_commands[] = func_get_args(); } /** * Add a localized label to the client environment */ public function add_label() { $arg_list = func_get_args(); foreach ($arg_list as $i => $name) { $this->command('add_label', $name, rcube_label($name)); } } /** * Invoke display_message command * * @param string Message to display * @param string Message type [notice|confirm|error] * @param array Key-value pairs to be replaced in localized text * @uses self::command() */ public function show_message($message, $type='notice', $vars=NULL) { $this->command( 'display_message', rcube_label(array('name' => $message, 'vars' => $vars)), $type); } /** * Delete all stored env variables and commands * * @return void * @uses rcube_html::reset() * @uses self::$env * @uses self::$js_env * @uses self::$js_commands * @uses self::$object_handlers */ public public function reset() { $this->env = array(); $this->js_env = array(); $this->js_commands = array(); $this->object_handlers = array(); parent::reset(); } /** * Send the request output to the client. * This will either parse a skin tempalte or send an AJAX response * * @param string Template name * @param boolean True if script should terminate (default) */ public function send($templ = null, $exit = true) { if ($templ != 'iframe') { $this->parse($templ, false); } else { $this->framed = $templ == 'iframe' ? true : $this->framed; $this->write(); } if ($exit) { exit; } } /** * Process template and write to stdOut * * @param string HTML template * @see rcube_html_page::write() * @override */ public function write($template = '') { // unlock interface after iframe load if ($this->framed) { array_unshift($this->js_commands, array('set_busy', false)); } // write all env variables to client $js = $this->framed ? "if(window.parent) {\n" : ''; $js .= $this->get_js_commands() . ($this->framed ? ' }' : ''); $this->add_script($js, 'head_top'); // call super method parent::write($template, $this->config['skin_path']); } /** * Parse a specific skin template and deliver to stdout * * Either returns nothing, or exists hard (exit();) * * @param string Template name * @param boolean Exit script * @return void * @link http://php.net/manual/en/function.exit.php */ private function parse($name = 'main', $exit = true) { $skin_path = $this->config['skin_path']; // read template file $templ = ''; $path = "$skin_path/templates/$name.html"; if (($fp = fopen($path, 'r')) === false) { $message = ''; ob_start(); fopen($path, 'r'); $message.= ob_get_contents(); ob_end_clean(); rcube_error::raise(array( 'code' => 501, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => 'Error loading template for '.$name.': '.$message ), true, true); return false; } $templ = fread($fp, filesize($path)); fclose($fp); // parse for specialtags $output = $this->parse_conditions($templ); $output = $this->parse_xml($output); // add debug console if ($this->config['debug_level'] & 8) { $this->add_footer('
console
' ); } $output = $this->parse_with_globals($output); $this->write(trim($output), $skin_path); if ($exit) { exit; } } /** * Return executable javascript code for all registered commands * * @return string $out */ private function get_js_commands() { $out = ''; if (!$this->framed && !empty($this->js_env)) { $out .= JS_OBJECT_NAME . '.set_env('.json_serialize($this->js_env).");\n"; } foreach ($this->js_commands as $i => $args) { $method = array_shift($args); foreach ($args as $i => $arg) { $args[$i] = json_serialize($arg); } $parent = $this->framed || preg_match('/^parent\./', $method); $out .= sprintf( "%s.%s(%s);\n", ($parent ? 'parent.' : '') . JS_OBJECT_NAME, preg_replace('/^parent\./', '', $method), implode(',', $args) ); } // add command to set page title if ($this->ajax_call && !empty($this->pagetitle)) { $out .= sprintf( "this.set_pagetitle('%s');\n", JQ((!empty($this->config['product_name']) ? $this->config['product_name'].' :: ' : '') . $this->pagetitle) ); } return $out; } /** * Make URLs starting with a slash point to skin directory * * @param string Input string * @return string */ public function abs_url($str) { return preg_replace('/^\//', $this->config['skin_path'].'/', $str); } /***** Template parsing methods *****/ /** * Replace all strings ($varname) * with the content of the according global variable. */ private function parse_with_globals($input) { $GLOBALS['__comm_path'] = Q($GLOBALS['COMM_PATH']); return preg_replace('/\$(__[a-z0-9_\-]+)/e', '$GLOBALS["\\1"]', $input); } /** * Public wrapper to dipp into template parsing. * * @param string $input * @return string * @uses rcube_template::parse_xml() * @since 0.1-rc1 */ public function just_parse($input) { return $this->parse_xml($input); } /** * Parse for conditional tags * * @param string $input * @return string */ private function parse_conditions($input) { $matches = preg_split('/]+)>/is', $input, 2, PREG_SPLIT_DELIM_CAPTURE); if ($matches && count($matches) == 4) { if (preg_match('/^(else|endif)$/i', $matches[1])) { return $matches[0] . $this->parse_conditions($matches[3]); } $attrib = parse_attrib_string($matches[2]); if (isset($attrib['condition'])) { $condmet = $this->check_condition($attrib['condition']); $submatches = preg_split('/]+)>/is', $matches[3], 2, PREG_SPLIT_DELIM_CAPTURE); if ($condmet) { $result = $submatches[0]; $result.= ($submatches[1] != 'endif' ? preg_replace('/.*]+>/Uis', '', $submatches[3], 1) : $submatches[3]); } else { $result = "" . $submatches[3]; } return $matches[0] . $this->parse_conditions($result); } rcube_error::raise(array( 'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__, 'message' => "Unable to parse conditional tag " . $matches[2] ), true, false); } return $input; } /** * Determines if a given condition is met * * @todo Get rid off eval() once I understand what this does. * @todo Extend this to allow real conditions, not just "set" * @param string Condition statement * @return boolean True if condition is met, False is not */ private function check_condition($condition) { $condition = preg_replace( array( '/session:([a-z0-9_]+)/i', '/config:([a-z0-9_]+)/i', '/env:([a-z0-9_]+)/i', '/request:([a-z0-9_]+)/ie' ), array( "\$_SESSION['\\1']", "\$this->config['\\1']", "\$this->env['\\1']", "get_input_value('\\1', RCUVE_INPUT_GPC)" ), $condition); return eval("return (".$condition.");"); } /** * Search for special tags in input and replace them * with the appropriate content * * @param string Input string to parse * @return string Altered input string * @todo Maybe a cache. */ private function parse_xml($input) { return preg_replace('/]+)>/Uie', "\$this->xml_command('\\1', '\\2')", $input); } /** * Convert a xml command tag into real content * * @param string Tag command: object,button,label, etc. * @param string Attribute string * @return string Tag/Object content */ private function xml_command($command, $str_attrib, $add_attrib = array()) { $command = strtolower($command); $attrib = parse_attrib_string($str_attrib) + $add_attrib; // empty output if required condition is not met if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) { return ''; } // execute command switch ($command) { // return a button case 'button': if ($attrib['command']) { return $this->button($attrib); } break; // show a label case 'label': if ($attrib['name'] || $attrib['command']) { return Q(rcube_label($attrib + array('vars' => array('product' => $this->config['product_name'])))); } break; // include a file case 'include': $path = realpath($this->config['skin_path'].$attrib['file']); if ($fsize = filesize($path)) { if ($this->config['skin_include_php']) { $incl = $this->include_php($path); } else if ($fp = fopen($path, 'r')) { $incl = fread($fp, $fsize); fclose($fp); } return $this->parse_xml($incl); } break; case 'plugin.include': //rcube::tfk_debug(var_export($this->config['skin_path'], true)); $path = realpath($this->config['skin_path'].$attrib['file']); if (!$path) { //rcube::tfk_debug("Does not exist:"); //rcube::tfk_debug($this->config['skin_path']); //rcube::tfk_debug($attrib['file']); //rcube::tfk_debug($path); } $incl = file_get_contents($path); if ($incl) { return $this->parse_xml($incl); } break; // return code for a specific application object case 'object': $object = strtolower($attrib['name']); // we are calling a class/method if (($handler = $this->object_handlers[$object]) && is_array($handler)) { if ((is_object($handler[0]) && method_exists($handler[0], $handler[1])) || (is_string($handler[0]) && class_exists($handler[0]))) return call_user_func($handler, $attrib); } else if (function_exists($handler)) { // execute object handler function return call_user_func($handler, $attrib); } if ($object=='productname') { $name = !empty($this->config['product_name']) ? $this->config['product_name'] : 'RoundCube Webmail'; return Q($name); } if ($object=='version') { return (string)RCMAIL_VERSION; } if ($object=='pagetitle') { $task = $this->task; $title = !empty($this->config['product_name']) ? $this->config['product_name'].' :: ' : ''; if (!empty($this->pagetitle)) { $title .= $this->pagetitle; } else if ($task == 'login') { $title = rcube_label(array('name' => 'welcome', 'vars' => array('product' => $this->config['product_name']))); } else { $title .= ucfirst($task); } return Q($title); } break; // return variable case 'var': $var = explode(':', $attrib['name']); $name = $var[1]; $value = ''; switch ($var[0]) { case 'env': $value = $this->env[$name]; break; case 'config': $value = $this->config[$name]; if (is_array($value) && $value[$_SESSION['imap_host']]) { $value = $value[$_SESSION['imap_host']]; } break; case 'request': $value = get_input_value($name, RCUBE_INPUT_GPC); break; case 'session': $value = $_SESSION[$name]; break; } if (is_array($value)) { $value = implode(', ', $value); } return Q($value); break; } return ''; } /** * Include a specific file and return it's contents * * @param string File path * @return string Contents of the processed file */ private function include_php($file) { ob_start(); include $file; $out = ob_get_contents(); ob_end_clean(); return $out; } /** * Create and register a button * * @param array Named button attributes * @return string HTML button * @todo Remove all inline JS calls and use jQuery instead. * @todo Remove all sprintf()'s - they are pretty, but also slow. */ private function button($attrib) { global $CONFIG, $OUTPUT, $MAIN_TASKS; static $sa_buttons = array(); static $s_button_count = 100; // these commands can be called directly via url $a_static_commands = array('compose', 'list'); $browser = new rcube_browser(); $skin_path = $this->config['skin_path']; if (!($attrib['command'] || $attrib['name'])) { return ''; } // try to find out the button type if ($attrib['type']) { $attrib['type'] = strtolower($attrib['type']); } else { $attrib['type'] = ($attrib['image'] || $attrib['imagepas'] || $attrib['imageact']) ? 'image' : 'link'; } $command = $attrib['command']; // take the button from the stack if ($attrib['name'] && $sa_buttons[$attrib['name']]) { $attrib = $sa_buttons[$attrib['name']]; } else if($attrib['image'] || $attrib['imageact'] || $attrib['imagepas'] || $attrib['class']) { // add button to button stack if (!$attrib['name']) { $attrib['name'] = $command; } if (!$attrib['image']) { $attrib['image'] = $attrib['imagepas'] ? $attrib['imagepas'] : $attrib['imageact']; } $sa_buttons[$attrib['name']] = $attrib; } else if ($command && $sa_buttons[$command]) { // get saved button for this command/name $attrib = $sa_buttons[$command]; } // set border to 0 because of the link arround the button if ($attrib['type']=='image' && !isset($attrib['border'])) { $attrib['border'] = 0; } if (!$attrib['id']) { $attrib['id'] = sprintf('rcmbtn%d', $s_button_count++); } // get localized text for labels and titles if ($attrib['title']) { $attrib['title'] = Q(rcube_label($attrib['title'])); } if ($attrib['label']) { $attrib['label'] = Q(rcube_label($attrib['label'])); } if ($attrib['alt']) { $attrib['alt'] = Q(rcube_label($attrib['alt'])); } // set title to alt attribute for IE browsers if ($browser->ie && $attrib['title'] && !$attrib['alt']) { $attrib['alt'] = $attrib['title']; unset($attrib['title']); } // add empty alt attribute for XHTML compatibility if (!isset($attrib['alt'])) { $attrib['alt'] = ''; } // register button in the system if ($attrib['command']) { $this->add_script(sprintf( "%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');", JS_OBJECT_NAME, $command, $attrib['id'], $attrib['type'], $attrib['imageact'] ? $skin_path.$attrib['imageact'] : $attrib['classact'], $attrib['imagesel'] ? $skin_path.$attrib['imagesel'] : $attrib['classsel'], $attrib['imageover'] ? $skin_path.$attrib['imageover'] : '' )); // make valid href to specific buttons if (in_array($attrib['command'], $MAIN_TASKS)) { $attrib['href'] = Q(rcmail_url(null, null, $attrib['command'])); } else if (in_array($attrib['command'], $a_static_commands)) { $attrib['href'] = Q(rcmail_url($attrib['command'])); } } // overwrite attributes if (!$attrib['href']) { $attrib['href'] = '#'; } if ($command) { $attrib['onclick'] = sprintf( "return %s.command('%s','%s',this)", JS_OBJECT_NAME, $command, $attrib['prop'] ); } if ($command && $attrib['imageover']) { $attrib['onmouseover'] = sprintf( "return %s.button_over('%s','%s')", JS_OBJECT_NAME, $command, $attrib['id'] ); $attrib['onmouseout'] = sprintf( "return %s.button_out('%s','%s')", JS_OBJECT_NAME, $command, $attrib['id'] ); } if ($command && $attrib['imagesel']) { $attrib['onmousedown'] = sprintf( "return %s.button_sel('%s','%s')", JS_OBJECT_NAME, $command, $attrib['id'] ); $attrib['onmouseup'] = sprintf( "return %s.button_out('%s','%s')", JS_OBJECT_NAME, $command, $attrib['id'] ); } $out = ''; // generate image tag if ($attrib['type']=='image') { $attrib_str = html::attrib_string( $attrib, array( 'style', 'class', 'id', 'width', 'height', 'border', 'hspace', 'vspace', 'align', 'alt', ) ); $img_tag = sprintf('', $attrib_str); $btn_content = sprintf($img_tag, $skin_path.$attrib['image']); if ($attrib['label']) { $btn_content .= ' '.$attrib['label']; } $link_attrib = array('href', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'title'); } else if ($attrib['type']=='link') { $btn_content = $attrib['label'] ? $attrib['label'] : $attrib['command']; $link_attrib = array('href', 'onclick', 'title', 'id', 'class', 'style'); } else if ($attrib['type']=='input') { $attrib['type'] = 'button'; if ($attrib['label']) { $attrib['value'] = $attrib['label']; } $attrib_str = html::attrib_string( $attrib, array( 'type', 'value', 'onclick', 'id', 'class', 'style' ) ); $out = sprintf('', $attrib_str); } // generate html code for button if ($btn_content) { $attrib_str = html::attrib_string($attrib, $link_attrib); $out = sprintf('%s', $attrib_str, $btn_content); } return $out; } /* ************* common functions delivering gui objects ************** */ /** * GUI object 'username' * Showing IMAP username of the current session * * @param array Named tag parameters (currently not used) * @return string HTML code for the gui object */ static function current_username($attrib) { global $USER; static $username; // alread fetched if (!empty($username)) { return $username; } // get e-mail address form default identity if ($sql_arr = $USER->get_identity()) { $s_username = $sql_arr['email']; } else if (strstr($_SESSION['username'], '@')) { $username = $_SESSION['username']; } else { $username = $_SESSION['username'].'@'.$_SESSION['imap_host']; } return $username; } /** * GUI object 'loginform' * Returns code for the webmail login form * * @param array Named parameters * @return string HTML code for the gui object */ private function login_form($attrib) { global $CONFIG, $SESS_HIDDEN_FIELD; $default_host = $CONFIG['default_host']; $_SESSION['temp'] = true; $input_user = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser', 'size' => 30, 'autocomplete' => 'off')); $input_pass = new html_passwordfield(array('name' => '_pass', 'id' => 'rcmloginpwd', 'size' => 30)); $input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login')); $input_host = null; if (is_array($default_host)) { $input_host = new html_select(array('name' => '_host', 'id' => 'rcmloginhost')); foreach ($default_host as $key => $value) { if (!is_array($value)) { $input_host->add($value, (is_numeric($key) ? $value : $key)); } else { $input_host = null; break; } } } else if (!strlen($default_host)) { $input_host = new html_inputfield(array('name' => '_host', 'id' => 'rcmloginhost', 'size' => 30)); } $form_name = !empty($attrib['form']) ? $attrib['form'] : 'form'; $this->add_gui_object('loginform', $form_name); // create HTML table with two cols $table = new html_table(array('cols' => 2)); $table->add('title', html::label('rcmloginuser', Q(rcube_label('username')))); $table->add(null, $input_user->show(get_input_value('_user', RCUVE_INPUT_POST))); $table->add('title', html::label('rcmloginpwd', Q(rcube_label('password')))); $table->add(null, $input_pass->show()); // add host selection row if (is_object($input_host)) { $table->add('title', html::label('rcmloginhost', Q(rcube_label('server')))); $table->add(null, $input_host->show(get_input_value('_host', RCUVE_INPUT_POST))); } $out = $SESS_HIDDEN_FIELD; $out .= $input_action->show(); $out .= $table->show(); // surround html output with a form tag if (empty($attrib['form'])) { $out = html::tag( 'form', array( 'name' => $form_name, 'action' => "./", 'method' => "post" ), $out); } return $out; } /** * GUI object 'searchform' * Returns code for search function * * @param array Named parameters * @return string HTML code for the gui object */ private function search_form($attrib) { // add some labels to client $this->add_label('searching'); $attrib['name'] = '_q'; if (empty($attrib['id'])) { $attrib['id'] = 'rcmqsearchbox'; } $input_q = new html_inputfield($attrib); $out = $input_q->show(); $this->add_gui_object('qsearchbox', $attrib['id']); // add form tag around text field if (empty($attrib['form'])) { $out = html::tag( 'form', array( 'name' => "rcmqsearchform", 'action' => "./", 'onsubmit' => JS_OBJECT_NAME . ".command('search');return false;", 'style' => "display:inline", ), $out); } return $out; } /** * Builder for GUI object 'message' * * @param array Named tag parameters * @return string HTML code for the gui object */ private function message_container($attrib) { if (isset($attrib['id']) === false) { $attrib['id'] = 'rcmMessageContainer'; } $this->add_gui_object('message', $attrib['id']); return html::div($attrib, ""); } /** * GUI object 'charsetselector' * * @param array Named parameters for the select tag * @return string HTML code for the gui object */ static function charset_selector($attrib) { // pass the following attributes to the form class $field_attrib = array('name' => '_charset'); foreach ($attrib as $attr => $value) { if (in_array($attr, array('id', 'class', 'style', 'size', 'tabindex'))) { $field_attrib[$attr] = $value; } } $charsets = array( 'US-ASCII' => 'ASCII (English)', 'EUC-JP' => 'EUC-JP (Japanese)', 'EUC-KR' => 'EUC-KR (Korean)', 'BIG5' => 'BIG5 (Chinese)', 'GB2312' => 'GB2312 (Chinese)', 'ISO-2022-JP' => 'ISO-2022-JP (Japanese)', 'ISO-8859-1' => 'ISO-8859-1 (Latin-1)', 'ISO-8859-2' => 'ISO-8895-2 (Central European)', 'ISO-8859-7' => 'ISO-8859-7 (Greek)', 'ISO-8859-9' => 'ISO-8859-9 (Turkish)', 'Windows-1251' => 'Windows-1251 (Cyrillic)', 'Windows-1252' => 'Windows-1252 (Western)', 'Windows-1255' => 'Windows-1255 (Hebrew)', 'Windows-1256' => 'Windows-1256 (Arabic)', 'Windows-1257' => 'Windows-1257 (Baltic)', 'UTF-8' => 'UTF-8' ); $select = new html_select($field_attrib); $select->add(array_values($charsets), array_keys($charsets)); $set = $_POST['_charset'] ? $_POST['_charset'] : $this->get_charset(); return $select->show($set); } } // end class rcube_template