source: subversion/branches/devel-vnext/program/include/rcmail_template.inc @ 655

Last change on this file since 655 was 655, checked in by till, 6 years ago

# func.inc:

  • needed to reference rcmail_search_form to class rc_main

+ rcmail_template:

  • implemented calls so handlers can be static methods from classes
  • so we can OO roundcube completely
  • mail.html
    • removed a linebreak
  • Property svn:executable set to *
File size: 25.1 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcmail_template.inc                                   |
6 |                                                                       |
7 | This file is part of the RoundCube Webmail client                     |
8 | Copyright (C) 2007, RoundCube Dev. - Switzerland                      |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   Class to handle HTML page output using a skin template.             |
13 |   Extends rcube_html_page class from rcube_shared.inc                 |
14 |                                                                       |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17 +-----------------------------------------------------------------------+
18
19 $Id:  $
20
21*/
22
23require_once dirname(__FILE__) . '/rcube_shared.inc';
24
25/**
26 * rcmail_template
27 *
28 * @todo Documentation
29 * @todo CS
30 * @todo Remove globals
31 * @todo PHP5?
32 * @uses rcube_html_page
33 */
34class rcmail_template extends rcube_html_page
35{
36    var $config;
37    var $task = '';
38    var $framed = false;
39    var $ajax_call = false;
40    var $pagetitle = '';
41    var $env = array();
42    var $js_env = array();
43    var $js_commands = array();
44    var $object_handlers = array();
45
46
47    /**
48     * PHP 5 constructor
49     *
50     * @access public
51     * @param  array $config
52     * @param  string $task
53     * @todo   Use jQuery's $(document).ready() here.
54     */
55    function __construct($config, $task)
56    {
57        parent::__construct();
58
59        $this->task      = $task;
60        $this->config    = $config;
61        $this->ajax_call = !empty($_GET['_remote']) || !empty($_POST['_remote']);
62
63        // add common javascripts
64        if (!$this->ajax_call) {
65            $javascript = "var ".JS_OBJECT_NAME." = new rcube_webmail();";
66
67            // don't wait for page onload. Call init at the bottom of the page (delayed)
68            $javascript_foot = "if (window.call_init)\n call_init('".JS_OBJECT_NAME."');";
69
70            $this->add_script($javascript, 'head_top');
71            $this->add_script($javascript_foot, 'foot');
72            $this->scripts_path = 'program/js/';
73            $this->include_script('common.js');
74            $this->include_script('app.js');
75        }
76    }
77
78    /**
79     * Set environment variable
80     */
81    function set_env($name, $value, $addtojs=true)
82    {
83        $this->env[$name] = $value;
84        if ($addtojs || isset($this->js_env[$name])) {
85            $this->js_env[$name] = $value;
86        }
87    }
88
89
90    /**
91     * Set page title variable
92     */
93    function set_pagetitle($title)
94    {
95        $this->pagetitle = $title;
96    }
97
98
99    /**
100     * Register a template object handler
101     *
102     * @access protected
103     * @param  string Object name
104     * @param  string Function name to call
105     * @return void
106     */
107    function add_handler($obj, $func)
108    {
109        $this->object_handlers[$obj] = $func;
110    }
111
112  /**
113   * Register a list of template object handlers
114   *
115   * @param array Hash array with object=>handler pairs
116   */
117  function add_handlers($arr)
118  {
119    $this->object_handlers = array_merge($this->object_handlers, $arr);
120  }
121
122  /**
123   * Register a GUI object to the client script
124   *
125   * @param string Object name
126   * @param string Object ID
127   */
128  function add_gui_object($obj, $id)
129  {
130    $this->add_script(JS_OBJECT_NAME.".gui_object('$obj', '$id');");
131  }
132
133
134  /**
135   * Call a client method
136   *
137   * @param string Method to call
138   * @param ... Additional arguments
139   */
140  function command()
141  {
142    $this->js_commands[] = func_get_args();
143  }
144
145
146  /**
147   * Invoke display_message command
148   */
149  function show_message($message, $type='notice', $vars=NULL)
150  {
151    $this->command(
152      'display_message',
153      rcube_label(array('name' => $message, 'vars' => $vars)),
154      $type);
155  }
156
157
158  /**
159   * Delete all stored env variables and commands
160   */
161  function reset()
162  {
163    $this->env = array();
164    $this->js_env = array();
165    $this->js_commands = array();
166    $this->object_handlers = array();
167    parent::reset();
168  }
169
170  /**
171   * Send the request output to the client.
172   * This will either parse a skin tempalte or send an AJAX response
173   *
174   * @param string  Template name
175   * @param boolean True if script should terminate (default)
176   */
177    function send($templ=null, $exit=true)
178    {
179        if ($this->ajax_call) {
180            $this->remote_response('', !$exit);
181        }
182        elseif ($templ != 'iframe') {
183            //rc_main::tfk_debug("/Parsing $templ");
184            $this->parse($templ, false);
185        }
186        else {
187            $this->framed = $templ == 'iframe' ? true : $this->framed;
188            $this->write();
189        }
190
191        if ($exit) {
192            exit;
193        }
194    }
195
196
197  /**
198   * Send an AJAX response with executable JS code
199   *
200   * @param string  Additional JS code
201   * @param boolean True if output buffer should be flushed
202   */
203  function remote_response($add='', $flush=false)
204  {
205    static $s_header_sent = FALSE;
206
207    if (!$s_header_sent)
208    {
209      $s_header_sent = TRUE;
210      send_nocacheing_headers();
211      header('Content-Type: application/x-javascript; charset='.RCMAIL_CHARSET);
212      print '/** ajax response ['.date('d/M/Y h:i:s O')."] **/\n";
213    }
214
215    // unset default env vars
216    unset($this->js_env['task'], $this->js_env['action'], $this->js_env['comm_path']);
217
218    // send response code
219    echo rc_main::rcube_charset_convert($this->get_js_commands() . $add, RCMAIL_CHARSET, $this->get_charset());
220
221    if ($flush)  // flush the output buffer
222      flush();
223  }
224
225
226  /**
227   * @override
228   */
229    function write($template='')
230    {
231        // write all env variables to client
232        $js = $this->framed ? "if(window.parent) {\n" : '';
233        $js .= $this->get_js_commands() . ($this->framed ? ' }' : '');
234        $this->add_script($js, 'head_top');
235
236        // call super method
237        parent::write($template, $this->config['skin_path']);
238    }
239
240
241    /**
242     * Parse a specific skin template and deliver to stdout
243     *
244     * Either returns nothing, or exists hard (exit();)
245     *
246     * @access public
247     * @param  string  Template name
248     * @param  boolean Exit script
249     * @return void
250     * @link   http://php.net/manual/en/function.exit.php
251     */
252    function parse($name='main', $exit=true)
253    {
254        $skin_path = $this->config['skin_path'];
255
256        //rc_main::tfk_debug(var_export($this->config['skin_path'], true));
257
258        // read template file
259        $templ = '';
260        $path = "$skin_path/templates/$name.html";
261
262        if(($fp = @fopen($path, 'r')) === false) {
263            $message = '';
264            ob_start();
265            fopen($path, 'r');
266            $message.= ob_get_contents();
267            ob_end_clean();
268            rc_bugs::raise_error(
269                array(
270                    'code' => 501,
271                    'type' => 'php',
272                    'line' => __LINE__,
273                    'file' => __FILE__,
274                    'message' => "Error loading template for '$name': $message"
275                ),
276                TRUE,
277                TRUE
278            );
279            return FALSE;
280        }
281        $templ = fread($fp, filesize($path));
282        @fclose($fp);
283
284        //rc_main::tfk_debug("// parsed: $path");
285
286        // parse for specialtags
287        $output = $this->parse_conditions($templ);
288        $output = $this->parse_xml($output);
289
290        // add debug console
291        if ($this->config['debug_level'] & 8) {
292            $this->add_footer('<div style="position:absolute;top:5px;left:5px;width:400px;padding:0.2em;background:white;opacity:0.8;z-index:9000">
293                            <a href="#toggle" onclick="con=document.getElementById(\'dbgconsole\');con.style.display=(con.style.display==\'none\'?\'block\':\'none\');return false">console</a>
294                            <form action="/" name="debugform"><textarea name="console" id="dbgconsole" rows="20" cols="40" wrap="off" style="display:none;width:400px;border:none;font-size:x-small"></textarea></form></div>'
295            );
296        }
297        $output = $this->parse_with_globals($output);
298        $this->write(trim($output), $skin_path);
299        if ($exit) {
300            exit;
301        }
302    }
303
304
305    /**
306     * Return executable javascript code for all registered commands
307     * @access private
308     * @return string $out
309     */
310    function get_js_commands()
311    {
312        $out = '';
313        if (!$this->framed) {
314            $out .= ($this->ajax_call ? 'this' : JS_OBJECT_NAME) . '.set_env('.json_serialize($this->js_env).");\n";
315        }
316        foreach ($this->js_commands as $i => $args) {
317            $method = array_shift($args);
318            foreach ($args as $i => $arg) {
319                $args[$i] = json_serialize($arg);
320            }
321            $parent = $this->framed || preg_match('/^parent\./', $method);
322            $out .= sprintf(
323                        "%s.%s(%s);\n",
324                        $this->ajax_call ? 'this' : ($parent ? 'parent.' : '') . JS_OBJECT_NAME,
325                        preg_replace('/^parent\./', '', $method),
326                        implode(',', $args)
327            );
328        }
329        return $out;
330    }
331
332    /**
333     * Make URLs starting with a slash point to skin directory
334     *
335     * @access protected
336     * @param  string $str
337     * @todo   Check if str_replace() would be sufficient
338     * @return string
339     */
340    function abs_url($str)
341    {
342        return preg_replace('/^\//', $this->config['skin_path'].'/', $str);
343    }
344
345
346
347    /*****  Template parsing methods  *****/
348
349    /**
350     * Replace all strings ($varname) with the content
351     * of the according global variable.
352     */
353    function parse_with_globals($input)
354    {
355        $registry  = rc_registry::getInstance();
356        $COMM_PATH = $registry->get('COMM_PATH', 'core');
357
358        $GLOBALS['__comm_path'] = $COMM_PATH;
359        return preg_replace('/\$(__[a-z0-9_\-]+)/e', '$GLOBALS["\\1"]', $input);
360    }
361
362
363    /**
364     * Parse for conditional tags
365     *
366     * @access protected
367     * @param  string $input
368     * @return string
369     */
370    function parse_conditions($input)
371    {
372        $matches = preg_split(
373                        '/<roundcube:(if|elseif|else|endif)\s+([^>]+)>/is',
374                        $input,
375                        2,
376                        PREG_SPLIT_DELIM_CAPTURE
377        );
378        if ($matches && count($matches)==4) {
379            if (preg_match('/^(else|endif)$/i', $matches[1])) {
380                return $matches[0] . $this->parse_conditions($matches[3]);
381            }
382            $attrib = rc_main::parse_attrib_string($matches[2]);
383            if (isset($attrib['condition'])) {
384                $condmet = $this->check_condition($attrib['condition']);
385                $submatches = preg_split(
386                                '/<roundcube:(elseif|else|endif)\s+([^>]+)>/is',
387                                $matches[3],
388                                2,
389                                PREG_SPLIT_DELIM_CAPTURE
390                );
391                if ($condmet) {
392                    $result = $submatches[0];
393                    $result.= ($submatches[1] != 'endif' ? preg_replace('/.*<roundcube:endif\s+[^>]+>/Uis', '', $submatches[3], 1) : $submatches[3]);
394                }
395                else {
396                    $result = "<roundcube:$submatches[1] $submatches[2]>" . $submatches[3];
397                }
398                return $matches[0] . $this->parse_conditions($result);
399            }
400            rc_bugs::raise_error(
401                array(
402                    'code' => 500,
403                    'type' => 'php',
404                    'line' => __LINE__,
405                    'file' => __FILE__,
406                    'message' => "Unable to parse conditional tag " . $matches[2]
407                ),
408                TRUE,
409                FALSE
410            );
411        }
412        return $input;
413    }
414
415
416    /**
417     * Determines if a given condition is met
418     *
419     * @todo   Get rid off eval() once I understand what this does.
420     * @return True if condition is valid, False is not
421     */
422    function check_condition($condition)
423    {
424        $condition = preg_replace(
425                array('/session:([a-z0-9_]+)/i', '/config:([a-z0-9_]+)/i', '/env:([a-z0-9_]+)/i', '/request:([a-z0-9_]+)/ie'),
426                array("\$_SESSION['\\1']", "\$this->config['\\1']", "\$this->env['\\1']", "get_input_value('\\1', RCUBE_INPUT_GPC)"),
427                $condition
428        );
429        return @eval("return (".$condition.");");
430    }
431
432
433   /**
434    * Search for special tags in input and replace them
435    * with the appropriate content
436    *
437    * @access protected
438    * @param  string Input string to parse
439    * @return Altered input string
440    * @todo   Maybe a cache.
441    */
442   function parse_xml($input)
443   {
444        return preg_replace('/<roundcube:([-_a-z]+)\s+([^>]+)>/Uie', "\$this->xml_command('\\1', '\\2')", $input);
445   }
446
447
448    /**
449     * Convert a xml command tag into real content
450     *
451     * @access protected
452     * @param  string Tag command: object,button,label, etc.
453     * @param  string Attribute string
454     * @return Tag/Object content string
455     */
456    function xml_command($command, $str_attrib, $add_attrib=array())
457    {
458        $command = strtolower($command);
459        $attrib  = rc_main::parse_attrib_string($str_attrib) + $add_attrib;
460
461        //rc_main::tfk_debug("// $command");
462
463        // empty output if required condition is not met
464        if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
465            return '';
466        }
467
468        //rc_main::tfk_debug("// $command #2");
469
470        // execute command
471        switch ($command) {
472            // return a button
473            case 'button':
474                if ($attrib['command'])
475                return $this->button($attrib);
476                break;
477
478            // show a label
479            case 'label':
480                if ($attrib['name'] || $attrib['command'])
481                    return rc_main::Q(rcube_label($attrib + array('vars' => array('product' => $this->config['product_name']))));
482                break;
483
484            // include a file
485            case 'include':
486                $path = realpath($this->config['skin_path'].$attrib['file']);
487                if ($path === FALSE) {
488                    //rc_main::tfk_debug("Does not exist.");
489                    return $this->parse_xml('');
490                }
491                if (($tpl_filesize = filesize($path)) == 0) {
492                    return $this->parse_xml('');
493                }
494                if ($fp = @fopen($path, 'r')) {
495                    $incl = fread($fp, $tpl_filesize);
496                    fclose($fp);
497                    return $this->parse_xml($incl);
498                }
499                break;
500
501            case 'plugin.include':
502                //rc_main::tfk_debug(var_export($this->config['skin_path'], true));
503                $path = realpath($this->config['skin_path'].$attrib['file']);
504                if ($path === FALSE) {
505                    //rc_main::tfk_debug("Does not exist:");
506                    //rc_main::tfk_debug($this->config['skin_path']);
507                    //rc_main::tfk_debug($attrib['file']);
508                    //rc_main::tfk_debug($path);
509                }
510                $incl = file_get_contents($path);
511                if ($incl === FALSE) {
512                    //rc_main::tfk_debug("Could not read template.");
513                    return $this->parse_xml('');
514                }
515                return $this->parse_xml($incl);
516                break;
517
518            // return code for a specific application object
519            case 'object':
520                $object = strtolower($attrib['name']);
521
522                // execute object handler function
523                if ($this->object_handlers[$object] && function_exists($this->object_handlers[$object])) {
524                    return call_user_func($this->object_handlers[$object], $attrib);
525                }
526                else {
527                    // we are calling a class/method
528                    if ($this->object_handlers[$object] && is_array($this->object_handlers[$object])) {
529                        if (class_exists($this->object_handlers[$object][0])) {
530                            return call_user_func($this->object_handlers[$object], $attrib);
531                        }
532                        rc_main::tfk_debug('Unknown handler: ' . var_export($this->object_handlers[$object], true));
533                    }
534                }
535                if ($object=='productname') {
536                    $name = !empty($this->config['product_name']) ? $this->config['product_name'] : 'RoundCube Webmail';
537                    return rc_main::Q($name);
538                }
539                if ($object=='version') {
540                    return (string)RCMAIL_VERSION;
541                }
542                if ($object=='pagetitle') {
543                    $task  = $this->task;
544                    $title = !empty($this->config['product_name']) ? $this->config['product_name'].' :: ' : '';
545
546                    if (!empty($this->pagetitle))
547                        $title .= $this->pagetitle;
548                    else if ($task == 'login')
549                        $title = rcube_label(array('name' => 'welcome', 'vars' => array('product' => $this->config['product_name'])));
550                    else
551                        $title .= ucfirst($task);
552
553                    return rc_main::Q($title);
554                }
555                break;
556        }
557        return '';
558    }
559
560
561    /**
562     * Create and register a button
563     *
564     * @param  array Button attributes
565     * @return HTML button
566     * @todo   Remove all inline JS calls and use jQuery instead.
567     * @todo   Remove all sprintf()'s - they are pretty, but also slow.
568     */
569    function button($attrib)
570    {
571        $registry   = rc_registry::getInstance();
572        $CONFIG     = $registry->get('CONFIG', 'core');
573        $OUTPUT     = $registry->get('OUTPUT', 'core');
574        $BROWSER    = $registry->get('BROWSER', 'core');
575        $MAIN_TASKS = $registry->get('MAIN_TASKS', 'core');
576
577        static $sa_buttons = array();
578        static $s_button_count = 100;
579
580        // these commands can be called directly via url
581        $a_static_commands = array('compose', 'list');
582
583        $skin_path = $this->config['skin_path'];
584
585        if (!($attrib['command'] || $attrib['name'])) {
586            return '';
587        }
588        // try to find out the button type
589        if ($attrib['type']) {
590            $attrib['type'] = strtolower($attrib['type']);
591        }
592        else {
593            $attrib['type'] = ($attrib['image'] || $attrib['imagepas'] || $attrib['imageact']) ? 'image' : 'link';
594        }
595        $command = $attrib['command'];
596
597        // take the button from the stack
598        if($attrib['name'] && $sa_buttons[$attrib['name']]) {
599            $attrib = $sa_buttons[$attrib['name']];
600        }
601        // add button to button stack
602        else if($attrib['image'] || $attrib['imageact'] || $attrib['imagepas'] || $attrib['class'])
603        {
604            if (!$attrib['name']) {
605                $attrib['name'] = $command;
606            }
607            if (!$attrib['image']) {
608                $attrib['image'] = $attrib['imagepas'] ? $attrib['imagepas'] : $attrib['imageact'];
609            }
610            $sa_buttons[$attrib['name']] = $attrib;
611        }
612
613        // get saved button for this command/name
614        else if ($command && $sa_buttons[$command]) {
615            $attrib = $sa_buttons[$command];
616        }
617        //else
618        //  return '';
619
620
621        // set border to 0 because of the link arround the button
622        if ($attrib['type']=='image' && !isset($attrib['border'])) {
623            $attrib['border'] = 0;
624        }
625        if (!$attrib['id']) {
626            $attrib['id'] =  sprintf('rcmbtn%d', $s_button_count++);
627        }
628        // get localized text for labels and titles
629        if ($attrib['title']) {
630            $attrib['title'] = rc_main::Q(rcube_label($attrib['title']));
631        }
632        if ($attrib['label']) {
633            $attrib['label'] = rc_main::Q(rcube_label($attrib['label']));
634        }
635        if ($attrib['alt']) {
636            $attrib['alt'] = rc_main::Q(rcube_label($attrib['alt']));
637        }
638        // set title to alt attribute for IE browsers
639        if ($BROWSER['ie'] && $attrib['title'] && !$attrib['alt']) {
640            $attrib['alt'] = $attrib['title'];
641            unset($attrib['title']);
642        }
643
644        // add empty alt attribute for XHTML compatibility
645        if (!isset($attrib['alt'])) {
646            $attrib['alt'] = '';
647        }
648
649        // register button in the system
650        if ($attrib['command'])
651        {
652            $this->add_script(
653                sprintf(
654                    "%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');",
655                    JS_OBJECT_NAME,
656                    $command,
657                    $attrib['id'],
658                    $attrib['type'],
659                    $attrib['imageact'] ? $skin_path.$attrib['imageact'] : $attrib['classact'],
660                    $attrib['imagesel'] ? $skin_path.$attrib['imagesel'] : $attrib['classsel'],
661                    $attrib['imageover'] ? $skin_path.$attrib['imageover'] : ''
662                )
663            );
664
665            // make valid href to specific buttons
666            if (in_array($attrib['command'], $MAIN_TASKS)) {
667                $attrib['href'] = rc_main::Q(rc_main::rcmail_url(null, null, $attrib['command']));
668            }
669            else if (in_array($attrib['command'], $a_static_commands)) {
670                $attrib['href'] = rc_main::Q(rc_main::rcmail_url($attrib['command']));
671            }
672        }
673
674        // overwrite attributes
675        if (!$attrib['href']) {
676            $attrib['href'] = '#';
677        }
678        if ($command) {
679            $attrib['onclick'] = sprintf(
680                                    "return %s.command('%s','%s',this)",
681                                    JS_OBJECT_NAME,
682                                    $command,
683                                    $attrib['prop']
684            );
685        }
686        if ($command && $attrib['imageover']) {
687            $attrib['onmouseover'] = sprintf(
688                                        "return %s.button_over('%s','%s')",
689                                        JS_OBJECT_NAME,
690                                        $command,
691                                        $attrib['id']
692            );
693            $attrib['onmouseout'] = sprintf(
694                                        "return %s.button_out('%s','%s')",
695                                        JS_OBJECT_NAME,
696                                        $command,
697                                        $attrib['id']
698            );
699        }
700
701        if ($command && $attrib['imagesel']) {
702            $attrib['onmousedown'] = sprintf(
703                                        "return %s.button_sel('%s','%s')",
704                                        JS_OBJECT_NAME,
705                                        $command,
706                                        $attrib['id']
707            );
708            $attrib['onmouseup'] = sprintf(
709                                        "return %s.button_out('%s','%s')",
710                                        JS_OBJECT_NAME,
711                                        $command,
712                                        $attrib['id']
713            );
714        }
715
716        $out = '';
717
718        // generate image tag
719        if ($attrib['type']=='image') {
720            $attrib_str = rc_main::create_attrib_string(
721                                $attrib,
722                                array(
723                                    'style', 'class', 'id', 'width',
724                                    'height', 'border', 'hspace',
725                                    'vspace', 'align', 'alt'
726                                )
727            );
728            $img_tag = sprintf('<img src="%%s"%s />', $attrib_str);
729            $btn_content = sprintf($img_tag, $skin_path.$attrib['image']);
730            if ($attrib['label']) {
731                $btn_content .= ' '.$attrib['label'];
732            }
733            $link_attrib = array('href', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'title');
734        }
735        else if ($attrib['type']=='link') {
736            $btn_content = $attrib['label'] ? $attrib['label'] : $attrib['command'];
737            $link_attrib = array('href', 'onclick', 'title', 'id', 'class', 'style');
738        }
739        else if ($attrib['type']=='input') {
740            $attrib['type'] = 'button';
741
742            if ($attrib['label'])
743                $attrib['value'] = $attrib['label'];
744
745            $attrib_str = rc_main::create_attrib_string(
746                                $attrib,
747                                array(
748                                    'type', 'value', 'onclick',
749                                    'id', 'class', 'style'
750                                )
751            );
752            $out = sprintf('<input%s disabled="disabled" />', $attrib_str);
753        }
754
755        // generate html code for button
756        if ($btn_content) {
757            $attrib_str = rc_main::create_attrib_string($attrib, $link_attrib);
758            $out = sprintf('<a%s>%s</a>', $attrib_str, $btn_content);
759        }
760
761        return $out;
762    }
763
764}
765
766?>
Note: See TracBrowser for help on using the repository browser.