Removing hook from template, misc cleanup
[squirrelmail.git] / class / template / Template.class.php
CommitLineData
de4d58cb 1<?php
de4d58cb 2/**
3 * Template.class.php
4 *
5 * This file contains an abstract (PHP 4, so "abstract" is relative)
6 * class meant to define the basic template interface for the
7 * SquirrelMail core application. Subclasses should extend this
8 * class with any custom functionality needed to interface a target
9 * templating engine with SquirrelMail.
10 *
11 * @copyright &copy; 2003-2006 The SquirrelMail Project Team
12 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
13 * @version $Id$
14 * @package squirrelmail
15 * @subpackage Template
16 * @since 1.5.2
17 *
18 */
19
948be6d4 20/** load template functions */
d281e128 21require(SM_PATH . 'functions/template/general_util.php');
948be6d4 22
de4d58cb 23/**
24 * The SquirrelMail Template class.
25 *
26 * Basic template class for capturing values and pluging them into a template.
27 * This class uses a similar API to Smarty.
28 *
29 * Methods that must be implemented by subclasses are as follows (see method
30 * stubs below for further information about expected behavior):
31 *
32 * assign()
33 * assign_by_ref()
d4c2aa24 34 * clear_all_assign()
35 * get_template_vars()
de4d58cb 36 * append()
37 * append_by_ref()
38 * apply_template()
39 *
01520835 40 * @author Paul Lesniewski <paul at squirrelmail.org>
de4d58cb 41 * @package squirrelmail
42 *
43 */
44class Template
45{
46
47 /**
48 * The template ID
49 *
50 * @var string
51 *
52 */
d4c2aa24 53 var $template_set_id = '';
de4d58cb 54
55 /**
d4c2aa24 56 * The template set base directory (relative path from
57 * the main SquirrelMail directory (SM_PATH))
de4d58cb 58 *
59 * @var string
60 *
61 */
62 var $template_dir = '';
63
64 /**
65 * The template engine (please use constants defined in constants.php)
66 *
67 * @var string
68 *
69 */
70 var $template_engine = '';
71
72 /**
d4c2aa24 73 * The fall-back template ID
de4d58cb 74 *
75 * @var string
76 *
77 */
d4c2aa24 78 var $fallback_template_set_id = '';
de4d58cb 79
80 /**
d4c2aa24 81 * The fall-back template directory (relative
82 * path from the main SquirrelMail directory (SM_PATH))
de4d58cb 83 *
84 * @var string
85 *
86 */
d4c2aa24 87 var $fallback_template_dir = '';
de4d58cb 88
89 /**
d4c2aa24 90 * The fall-back template engine (please use
91 * constants defined in constants.php)
de4d58cb 92 *
93 * @var string
94 *
95 */
d4c2aa24 96 var $fallback_template_engine = '';
de4d58cb 97
98 /**
d4c2aa24 99 * Template file cache. Structured as an array, whose keys
100 * are all the template file names (with path information relative
101 * to the template set's base directory, e.g., "css/style.css")
102 * found in all parent template sets including the ultimate fall-back
103 * template set. Array values are sub-arrays with the
104 * following key-value pairs:
de4d58cb 105 *
d4c2aa24 106 * PATH -- file path, relative to SM_PATH
107 * SET_ID -- the ID of the template set that this file belongs to
108 * ENGINE -- the engine needed to render this template file
de4d58cb 109 *
110 */
d4c2aa24 111 var $template_file_cache = array();
de4d58cb 112
113 /**
d4c2aa24 114 * Extra template engine class objects for rendering templates
115 * that require a different engine than the one for the current
116 * template set. Keys should be the name of the template engine,
117 * values are the corresponding class objects.
118 *
119 * @var array
120 *
121 */
122 var $other_template_engine_objects = array();
123
740c26f7 124 /**
de4d58cb 125 * Constructor
126 *
127 * Please do not call directly. Use Template::construct_template().
128 *
d4c2aa24 129 * @param string $template_set_id the template ID
de4d58cb 130 *
131 */
d4c2aa24 132 function Template($template_set_id) {
de4d58cb 133//FIXME: find a way to test that this is ONLY ever called
134// from the construct_template() method (I doubt it
135// is worth the trouble to parse the current stack trace)
136// if (???)
137// trigger_error('Please do not use default Template() constructor. Instead, use Template::construct_template().', E_USER_ERROR);
138
d4c2aa24 139 $this->set_up_template($template_set_id);
de4d58cb 140
141 }
142
143 /**
144 * Construct Template
145 *
146 * This method should always be called instead of trying
147 * to get a Template object from the normal/default constructor,
148 * and is necessary in order to control the return value.
149 *
d4c2aa24 150 * @param string $template_set_id the template ID
de4d58cb 151 *
152 * @return object The correct Template object for the given template set
153 *
d4c2aa24 154 * @static
155 *
de4d58cb 156 */
d4c2aa24 157 function construct_template($template_set_id) {
de4d58cb 158
d4c2aa24 159 $template = new Template($template_set_id);
34904996 160 $template->override_plugins();
de4d58cb 161 return $template->get_template_engine_subclass();
162
163 }
164
165 /**
166 * Set up internal attributes
167 *
168 * This method does most of the work for setting up
169 * newly constructed objects.
170 *
d4c2aa24 171 * @param string $template_set_id the template ID
de4d58cb 172 *
173 */
d4c2aa24 174 function set_up_template($template_set_id) {
de4d58cb 175
176 // FIXME: do we want to place any restrictions on the ID like
177 // making sure no slashes included?
178 // get template ID
179 //
d4c2aa24 180 $this->template_set_id = $template_set_id;
de4d58cb 181
182
d4c2aa24 183 $this->fallback_template_set_id = Template::get_fallback_template_set();
de4d58cb 184
185
186 // set up template directories
187 //
188 $this->template_dir
d4c2aa24 189 = Template::calculate_template_file_directory($this->template_set_id);
190 $this->fallback_template_dir
191 = Template::calculate_template_file_directory($this->fallback_template_set_id);
de4d58cb 192
193
d4c2aa24 194 // determine template engine
195 // FIXME: assuming PHP template engine may not necessarily be a good thing
de4d58cb 196 //
d4c2aa24 197 $this->template_engine = Template::get_template_config($this->template_set_id,
198 'template_engine',
199 SQ_PHP_TEMPLATE);
de4d58cb 200
de4d58cb 201
d4c2aa24 202 // get template file cache
203 //
204 $this->template_file_cache = Template::cache_template_file_hierarchy();
de4d58cb 205
d4c2aa24 206 }
de4d58cb 207
d4c2aa24 208 /**
209 * Determine what the ultimate fallback template set is.
210 *
211 * NOTE that if the fallback setting cannot be found in the
212 * main SquirrelMail configuration settings that the value
213 * of $default is returned.
214 *
215 * @param string $default The template set ID to use if
216 * the fallback setting cannot be
217 * found in SM config (optional;
218 * defaults to "default").
219 *
220 * @return string The ID of the fallback template set.
221 *
222 * @static
223 *
224 */
225 function get_fallback_template_set($default='default') {
de4d58cb 226
d4c2aa24 227// FIXME: do we want to place any restrictions on the ID such as
228// making sure no slashes included?
de4d58cb 229
d4c2aa24 230 // values are in main SM config file
de4d58cb 231 //
d4c2aa24 232 global $templateset_fallback, $aTemplateSet;
233 $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet)
234 ? array() : $aTemplateSet);
235 $templateset_fallback = (!isset($templateset_fallback)
293906dd 236 ? $default : $templateset_fallback);
d4c2aa24 237
293906dd 238 // iterate through all template sets, is this a valid skin ID?
239 //
240 $found_it = FALSE;
241 foreach ($aTemplateSet as $aTemplate) {
f4138e60 242 if ($aTemplate['ID'] === $templateset_fallback) {
293906dd 243 $found_it = TRUE;
244 break;
245 }
246 }
247
248 if ($found_it)
249 return $templateset_fallback;
250
251 // FIXME: note that it is possible for $default to
252 // point to an invalid (nonexistent) template set
253 // and that error will not be caught here
254 //
255 return $default;
d4c2aa24 256
257 }
258
259 /**
260 * Determine what the default template set is.
261 *
262 * NOTE that if the default setting cannot be found in the
263 * main SquirrelMail configuration settings that the value
264 * of $default is returned.
265 *
266 * @param string $default The template set ID to use if
267 * the default setting cannot be
268 * found in SM config (optional;
269 * defaults to "default").
270 *
271 * @return string The ID of the default template set.
272 *
273 * @static
274 *
275 */
276 function get_default_template_set($default='default') {
277
278// FIXME: do we want to place any restrictions on the ID such as
279// making sure no slashes included?
280
281 // values are in main SM config file
282 //
283 global $templateset_default, $aTemplateSet;
284 $aTemplateSet = (!isset($aTemplateSet) || !is_array($aTemplateSet)
285 ? array() : $aTemplateSet);
286 $templateset_default = (!isset($templateset_default)
293906dd 287 ? $default : $templateset_default);
d4c2aa24 288
293906dd 289 // iterate through all template sets, is this a valid skin ID?
290 //
291 $found_it = FALSE;
292 foreach ($aTemplateSet as $aTemplate) {
f4138e60 293 if ($aTemplate['ID'] === $templateset_default) {
293906dd 294 $found_it = TRUE;
295 break;
296 }
297 }
298
299 if ($found_it)
300 return $templateset_default;
301
302 // FIXME: note that it is possible for $default to
303 // point to an invalid (nonexistent) template set
304 // and that error will not be caught here
305 //
306 return $default;
de4d58cb 307
308 }
309
310 /**
34904996 311 * Allow template set to override plugin configuration by either
312 * adding or removing plugins.
313 *
314 */
315 function override_plugins() {
316
317 global $disable_plugins, $plugins, $squirrelmail_plugin_hooks;
318 if ($disable_plugins) return;
319
320 $add_plugins = Template::get_template_config($this->template_set_id,
321 'add_plugins', array());
322 $remove_plugins = Template::get_template_config($this->template_set_id,
323 'remove_plugins', array());
324
325//FIXME (?) we assume $add_plugins and $remove_plugins are arrays -- we could
326// error check here, or just assume that template authors or admins
327// won't screw up their config files
328
329
330 // disable all plugins? (can still add some by using $add_plugins)
331 //
332 if (in_array('*', $remove_plugins)) {
333 $plugins = array();
334 $squirrelmail_plugin_hooks = array();
335 $remove_plugins = array();
336 }
337
338
339 foreach ($add_plugins as $plugin_name) {
340 // add plugin to global plugin array
341 //
342 $plugins[] = $plugin_name;
343
344
345 // enable plugin -- emulate code from use_plugin() function
346 // in SquirrelMail core, but also need to call the
347 // "squirrelmail_plugin_init_<plugin_name>" function, which
348 // in static configuration is not called (this inconsistency
349 // could be a source of anomalous-seeming bugs in poorly
350 // coded plugins)
351 //
352 if (file_exists(SM_PATH . "plugins/$plugin_name/setup.php")) {
353 include_once(SM_PATH . "plugins/$plugin_name/setup.php");
354
355 $function = "squirrelmail_plugin_init_$plugin_name";
356 if (function_exists($function))
357 $function();
358 }
359 }
360
361 foreach ($remove_plugins as $plugin_name) {
362 // remove plugin from both global plugin & plugin hook arrays
363 //
364 $plugin_key = array_search($plugin_name, $plugins);
365 if (!is_null($plugin_key) && $plugin_key !== FALSE) {
366 unset($plugins[$plugin_key]);
367 if (is_array($squirrelmail_plugin_hooks))
368 foreach (array_keys($squirrelmail_plugin_hooks) as $hookName) {
369 unset($squirrelmail_plugin_hooks[$hookName][$plugin_name]);
370 }
371 }
372 }
373
374 }
375
376 /**
de4d58cb 377 * Instantiate and return correct subclass for this template
378 * set's templating engine.
379 *
d4c2aa24 380 * @param string $template_set_id The template set whose engine
381 * is to be used as an override
382 * (if not given, this template
383 * set's engine is used) (optional).
384 *
de4d58cb 385 * @return object The Template subclass object for the template engine.
386 *
387 */
d4c2aa24 388 function get_template_engine_subclass($template_set_id='') {
389
390 if (empty($template_set_id)) $template_set_id = $this->template_set_id;
391 // FIXME: assuming PHP template engine may not necessarily be a good thing
392 $engine = Template::get_template_config($template_set_id,
393 'template_engine', SQ_PHP_TEMPLATE);
394
de4d58cb 395
396 $engine_class_file = SM_PATH . 'class/template/'
d4c2aa24 397 . $engine . 'Template.class.php';
de4d58cb 398
399 if (!file_exists($engine_class_file)) {
d4c2aa24 400 trigger_error('Unknown template engine (' . $engine
de4d58cb 401 . ') was specified in template configuration file',
402 E_USER_ERROR);
403 }
404
d4c2aa24 405 $engine_class = $engine . 'Template';
406 require_once($engine_class_file);
407 return new $engine_class($template_set_id);
de4d58cb 408
409 }
410
411 /**
412 * Determine the relative template directory path for
413 * the given template ID.
414 *
d4c2aa24 415 * @param string $template_set_id The template ID from which to build
416 * the directory path
de4d58cb 417 *
418 * @return string The relative template path (based off of SM_PATH)
419 *
d4c2aa24 420 * @static
421 *
de4d58cb 422 */
d4c2aa24 423 function calculate_template_file_directory($template_set_id) {
de4d58cb 424
d4c2aa24 425 return 'templates/' . $template_set_id . '/';
de4d58cb 426
427 }
428
429 /**
430 * Determine the relative images directory path for
431 * the given template ID.
432 *
d4c2aa24 433 * @param string $template_set_id The template ID from which to build
434 * the directory path
de4d58cb 435 *
436 * @return string The relative images path (based off of SM_PATH)
437 *
d4c2aa24 438 * @static
439 *
de4d58cb 440 */
d4c2aa24 441 function calculate_template_images_directory($template_set_id) {
de4d58cb 442
d4c2aa24 443 return 'templates/' . $template_set_id . '/images/';
de4d58cb 444
445 }
446
447 /**
448 * Return the relative template directory path for this template set.
449 *
450 * @return string The relative path to the template directory based
451 * from the main SquirrelMail directory (SM_PATH).
452 *
453 */
454 function get_template_file_directory() {
455
456 return $this->template_dir;
457
458 }
459
d4c2aa24 460 /**
461 * Return the template ID for the fallback template set.
462 *
463 * @return string The ID of the fallback template set.
464 *
465 */
466 function get_fallback_template_set_id() {
467
468 return $this->fallback_template_set_id;
469
470 }
de4d58cb 471
472 /**
d4c2aa24 473 * Return the relative template directory path for the
474 * fallback template set.
de4d58cb 475 *
d4c2aa24 476 * @return string The relative path to the fallback template
477 * directory based from the main SquirrelMail
478 * directory (SM_PATH).
479 *
480 */
481 function get_fallback_template_file_directory() {
482
483 return $this->fallback_template_dir;
484
485 }
486
487 /**
488 * Get template set config setting
489 *
490 * Given a template set ID and setting name, returns the
491 * setting's value. Note that settings are cached in
492 * session, so "live" changes to template configuration
493 * won't be reflected until the user logs out and back
494 * in again.
495 *
496 * @param string $template_set_id The template set for which
497 * to look up the setting.
498 * @param string $setting The name of the setting to
499 * retrieve.
500 * @param mixed $default When the requested setting
501 * is not found, the contents
502 * of this value are returned
503 * instead (optional; default
504 * is NULL).
505 * NOTE that unlike sqGetGlobalVar(),
506 * this function will also return
507 * the default value if the
508 * requested setting is found
509 * but is empty.
510 * @param boolean $live_config When TRUE, the target template
511 * set's configuration file is
512 * reloaded every time this
513 * method is called. Default
514 * behavior is to only load the
515 * configuration file if it had
516 * never been loaded before, but
517 * not again after that (optional;
518 * default FALSE). Use with care!
519 * Should mostly be used for
520 * debugging.
521 *
522 * @return mixed The desired setting's value or if not found,
523 * the contents of $default are returned.
524 *
525 * @static
526 *
527 */
528 function get_template_config($template_set_id, $setting,
529 $default=NULL, $live_config=FALSE) {
530
531 sqGetGlobalVar('template_configuration_settings',
532 $template_configuration_settings,
533 SQ_SESSION,
534 array());
535
536 if ($live_config) unset($template_configuration_settings[$template_set_id]);
537
538
539 // NOTE: could use isset() instead of empty() below, but
540 // this function is designed to replace empty values
541 // as well as non-existing values with $default
542 //
543 if (!empty($template_configuration_settings[$template_set_id][$setting]))
544 return $template_configuration_settings[$template_set_id][$setting];
545
546
547 // if template set configuration has been loaded, but this
548 // setting is not known, return $default
549 //
550 if (!empty($template_configuration_settings[$template_set_id]))
551 return $default;
552
553
554 // otherwise (template set configuration has not been loaded before),
555 // load it into session and return the desired setting after that
556 //
557 $template_config_file = SM_PATH
558 . Template::calculate_template_file_directory($template_set_id)
559 . 'config.php';
560
561 if (!file_exists($template_config_file)) {
562
563 trigger_error('No template configuration file was found where expected: ("'
564 . $template_config_file . '")', E_USER_ERROR);
565
566 } else {
567
568 // we require() the file to let PHP do the variable value
569 // parsing for us, and read the file in manually so we can
570 // know what variable names are used in the config file
571 // (settings can be different depending on specific requirements
572 // of different template engines)... the other way this may
573 // be accomplished is to somehow diff the symbol table
574 // before/after the require(), but anyway, this code should
575 // only run once for this template set...
576 //
577 require($template_config_file);
578 $file_contents = implode("\n", file($template_config_file));
579
580
581 // note that this assumes no template settings have
582 // a string in them that looks like a variable name like $x
583 // also note that this will attempt to grab things like
584 // $Id found in CVS headers, so we try to adjust for that
585 // by checking that the variable is actually set
586 //
587 preg_match_all('/\$(\w+)/', $file_contents, $variables, PREG_PATTERN_ORDER);
588 foreach ($variables[1] as $variable) {
589 if (isset($$variable))
590 $template_configuration_settings[$template_set_id][$variable]
591 = $$variable;
592 }
593
594 sqsession_register($template_configuration_settings,
595 'template_configuration_settings');
596
597 // NOTE: could use isset() instead of empty() below, but
598 // this function is designed to replace empty values
599 // as well as non-existing values with $default
600 //
601 if (!empty($template_configuration_settings[$template_set_id][$setting]))
602 return $template_configuration_settings[$template_set_id][$setting];
603 else
604 return $default;
605
606 }
607
608 }
609
610 /**
611 * Obtain template file hierarchy from cache.
612 *
613 * If the file hierarchy does not exist in session, it is
614 * constructed and stored in session before being returned
615 * to the caller.
616 *
617 * @param boolean $regenerate_cache When TRUE, the file hierarchy
618 * is reloaded and stored fresh
619 * (optional; default FALSE).
620 * @param array $additional_files Must be in same form as the
621 * files in the file hierarchy
622 * cache. These are then added
623 * to the cache (optional; default
624 * empty - no additional files).
625 *
626 * @return array Template file hierarchy array, whose keys
627 * are all the template file names (with path
628 * information relative to the template set's
629 * base directory, e.g., "css/style.css")
630 * found in all parent template sets including
631 * the ultimate fall-back template set.
632 * Array values are sub-arrays with the
633 * following key-value pairs:
634 *
635 * PATH -- file path, relative to SM_PATH
636 * SET_ID -- the ID of the template set that this file belongs to
637 * ENGINE -- the engine needed to render this template file
638 *
639 * @static
de4d58cb 640 *
641 */
d4c2aa24 642 function cache_template_file_hierarchy($regenerate_cache=FALSE,
643 $additional_files=array()) {
644
645 sqGetGlobalVar('template_file_hierarchy', $template_file_hierarchy,
646 SQ_SESSION, array());
647
648
649 if ($regenerate_cache) unset($template_file_hierarchy);
650
d4c2aa24 651 if (!empty($template_file_hierarchy)) {
652
653 // have to add additional files if given before returning
654 //
655 if (!empty($additional_files)) {
656 $template_file_hierarchy = array_merge($template_file_hierarchy,
657 $additional_files);
658 sqsession_register($template_file_hierarchy,
659 'template_file_hierarchy');
660 }
661
662 return $template_file_hierarchy;
663 }
664
665
666 // nothing in cache apparently, so go build it now
667 //
668 // FIXME: not sure if there is any possibility that
669 // this could be called when $sTemplateID has
670 // yet to be defined... throw error for now,
671 // but if the error occurs, it's a coding error
672 // rather than a configuration error
673 //
674 global $sTemplateID;
675 if (empty($sTemplateID)) {
676
677 trigger_error('Template set ID unknown', E_USER_ERROR);
678
679 } else {
680
681 $template_file_hierarchy = Template::catalog_template_files($sTemplateID);
682
683 // additional files, if any
684 //
685 if (!empty($additional_files)) {
686 $template_file_hierarchy = array_merge($template_file_hierarchy,
687 $additional_files);
688 }
689
690 sqsession_register($template_file_hierarchy,
691 'template_file_hierarchy');
692
693 return $template_file_hierarchy;
694
695 }
696
697 }
698
699 /**
700 * Traverse template hierarchy and catalogue all template
701 * files (for storing in cache).
702 *
703 * Paths to all files in all parent, grand-parent, great grand
704 * parent, etc. template sets (including the fallback template)
705 * are catalogued; for identically named files, the file earlier
706 * in the hierarchy (closest to this template set) is used.
707 *
708 * @param string $template_set_id The template set in which to
709 * search for files
710 * @param array $file_list The file list so far to be added
711 * to (allows recursive behavior)
712 * (optional; default empty array).
713 * @param string $directory The directory in which to search for
714 * files (must be given as full path).
715 * If empty, starts at top-level template
716 * set directory (optional; default empty).
717 * NOTE! Use with care, as behavior is
718 * unpredictable if directory given is not
719 * part of correct template set.
720 *
721 * @return mixed The top-level caller will have an array of template
722 * files returned to it; recursive calls to this function
723 * do not receive any return value at all. The format
724 * of the template file array is as described for the
725 * Template class attribute $template_file_cache
726 *
727 * @static
728 *
729 */
730 function catalog_template_files($template_set_id, $file_list=array(), $directory='') {
731
732 $template_base_dir = SM_PATH
733 . Template::calculate_template_file_directory($template_set_id);
734
735 if (empty($directory)) {
736 $directory = $template_base_dir;
737 }
738
739 $files_and_dirs = list_files($directory, '', FALSE, TRUE, FALSE, TRUE);
de4d58cb 740
d4c2aa24 741 // recurse for all the subdirectories in the template set
742 //
743 foreach ($files_and_dirs['DIRECTORIES'] as $dir) {
744 $file_list = Template::catalog_template_files($template_set_id, $file_list, $dir);
745 }
746
747 // place all found files in the cache
748 // FIXME: assuming PHP template engine may not necessarily be a good thing
749 //
750 $engine = Template::get_template_config($template_set_id,
751 'template_engine', SQ_PHP_TEMPLATE);
752 foreach ($files_and_dirs['FILES'] as $file) {
753
754 // remove the part of the file path corresponding to the
755 // template set's base directory
756 //
757 $relative_file = substr($file, strlen($template_base_dir));
758
948be6d4 759 /**
760 * only put file in cache if not already found in earlier template
761 * PATH should be relative to SquirrelMail top directory
762 */
d4c2aa24 763 if (!isset($file_list[$relative_file])) {
764 $file_list[$relative_file] = array(
948be6d4 765 'PATH' => substr($file,strlen(SM_PATH)),
d4c2aa24 766 'SET_ID' => $template_set_id,
767 'ENGINE' => $engine,
768 );
769 }
770
771 }
772
773
774 // now if we are currently at the top-level of the template
775 // set base directory, we need to move on to the parent
776 // template set, if any
777 //
778 if ($directory == $template_base_dir) {
779
780 // use fallback when we run out of parents
781 //
782 $fallback_id = Template::get_fallback_template_set();
783 $parent_id = Template::get_template_config($template_set_id,
784 'parent_template_set',
785 $fallback_id);
786
787 // were we already all the way to the last level? just exit
788 //
789 // note that this code allows the fallback set to have
790 // a parent, too, but can result in endless loops
791 // if ($parent_id == $template_set_id) {
792 //
793 if ($fallback_id == $template_set_id) {
794 return $file_list;
795 }
796
797 $file_list = Template::catalog_template_files($parent_id, $file_list);
798
799 }
800
801 return $file_list;
de4d58cb 802
803 }
804
d4c2aa24 805 /**
806 * Look for a template file in a plugin; add to template
807 * file cache if found.
808 *
809 * The file is searched for in the following order:
810 *
811 * - A directory for the current template set within the plugin:
812 * SM_PATH/plugins/<plugin name>/templates/<template name>/
813 * - In a directory for one of the current template set's ancestor
814 * (inherited) template sets within the plugin:
815 * SM_PATH/plugins/<plugin name>/templates/<parent template name>/
816 * - In a directory for the fallback template set within the plugin:
817 * SM_PATH/plugins/<plugin name>/templates/<fallback template name>/
818 *
819 * @param string $plugin The name of the plugin
820 * @param string $file The name of the template file
821 * @param string $template_set_id The ID of the template for which
822 * to start looking for the file
823 * (optional; default is current
824 * template set ID).
825 *
826 * @return boolean TRUE if the template file was found, FALSE otherwise.
827 *
828 */
829 function find_and_cache_plugin_template_file($plugin, $file, $template_set_id='') {
830
831 if (empty($template_set_id))
832 $template_set_id = $this->template_set_id;
833
834 $file_path = SM_PATH . 'plugins/' . $plugin . '/'
835 . $this->calculate_template_file_directory($template_set_id)
836 . $file;
837
838 if (file_exists($file_path)) {
839 // FIXME: assuming PHP template engine may not necessarily be a good thing
840 $engine = $this->get_template_config($template_set_id,
841 'template_engine', SQ_PHP_TEMPLATE);
842 $file_list = array('plugins/' . $plugin . '/' . $file => array(
eb2425ec 843 'PATH' => substr($file_path, strlen(SM_PATH)),
844 'SET_ID' => $template_set_id,
845 'ENGINE' => $engine,
d4c2aa24 846 )
847 );
848 $this->template_file_cache
849 = $this->cache_template_file_hierarchy(FALSE, $file_list);
850 return TRUE;
851 }
852
853
854 // not found yet, try parent template set
855 // (use fallback when we run out of parents)
856 //
857 $fallback_id = $this->get_fallback_template_set();
858 $parent_id = $this->get_template_config($template_set_id,
859 'parent_template_set',
860 $fallback_id);
861
862 // were we already all the way to the last level? just exit
863 //
864 // note that this code allows the fallback set to have
865 // a parent, too, but can result in endless loops
866 // if ($parent_id == $template_set_id) {
867 //
868 if ($fallback_id == $template_set_id) {
869 return FALSE;
870 }
871
872 return $this->find_and_cache_plugin_template_file($plugin, $file, $parent_id);
873
874 }
de4d58cb 875
876 /**
877 * Find the right template file.
878 *
d4c2aa24 879 * The template file is taken from the template file cache, thus
880 * the file is taken from the current template, one of its
881 * ancestors or the fallback template.
882 *
883 * Note that it is perfectly acceptable to load template files from
884 * template subdirectories. For example, JavaScript templates found
885 * in the js/ subdirectory would be loaded by passing
886 * "js/<javascript file name>" as the $filename.
887 *
888 * Note that the caller can also ask for ALL files in a directory
889 * (and those in the same directory for all ancestor template sets)
890 * by giving a $filename that is a directory name (ending with a
891 * slash).
892 *
893 * If not found and the file is a plugin template file (indicated
894 * by the presence of "plugins/" on the beginning of $filename),
895 * the target plugin is searched for a substitue template file
896 * before just returning nothing.
de4d58cb 897 *
898 * Plugin authors must note that the $filename MUST be prefaced
899 * with "plugins/<plugin name>/" in order to correctly resolve the
900 * template file.
901 *
de4d58cb 902 * @param string $filename The name of the template file,
903 * possibly prefaced with
904 * "plugins/<plugin name>/"
905 * indicating that it is a plugin
d4c2aa24 906 * template, or ending with a
907 * slash, indicating that all files
908 * for that directory name should
909 * be returned.
e5d57179 910 * @param boolean $directories_ok When TRUE, directory names
911 * are acceptable search values,
912 * and when returning a list of
913 * directory contents, sub-directory
914 * names will also be included
915 * (optional; default FALSE).
916 * NOTE that empty directories
917 * are NOT included in the cache!
918 * @param boolean $directories_only When TRUE, only directory names
919 * are included in the returned
920 * results. (optional; default
921 * FALSE). Setting this argument
922 * to TRUE forces $directories_ok
923 * to TRUE as well.
924 * NOTE that empty directories
925 * are NOT included in the cache!
d4c2aa24 926 *
927 * @return mixed The full path to the template file or a list
928 * of all files in the given directory if $filename
929 * ends with a slash; if not found, an empty string
930 * is returned. The caller is responsible for
931 * throwing errors or other actions if template
932 * file is not found.
de4d58cb 933 *
934 */
e5d57179 935 function get_template_file_path($filename,
936 $directories_ok=FALSE,
937 $directories_only=FALSE) {
de4d58cb 938
e5d57179 939 if ($directories_only) $directories_ok = TRUE;
940
941
942 // only looking for directory listing first...
943 //
d4c2aa24 944 // return list of all files in a directory (and that
945 // of any ancestors)
de4d58cb 946 //
d4c2aa24 947 if ($filename{strlen($filename) - 1} == '/') {
948
949 $return_array = array();
950 foreach ($this->template_file_cache as $file => $file_info) {
951
952 // only want files in the requested directory
953 // (AND not in a subdirectory!)
954 //
e5d57179 955 if (!$directories_only && strpos($file, $filename) === 0
d4c2aa24 956 && strpos($file, '/', strlen($filename)) === FALSE)
948be6d4 957 $return_array[] = SM_PATH . $file_info['PATH'];
de4d58cb 958
e5d57179 959 // directories too? detect by finding any
960 // array key that matches a file in a sub-directory
961 // of the directory being processed
962 //
963 if ($directories_ok && strpos($file, $filename) === 0
964 && ($pos = strpos($file, '/', strlen($filename))) !== FALSE
965 && strpos($file, '/', $pos + 1) === FALSE) {
966 $directory_name = SM_PATH
967 . substr($file_info['PATH'],
968 0,
969 strrpos($file_info['PATH'], '/'));
970 if (!in_array($directory_name, $return_array))
971 $return_array[] = $directory_name;
972 }
973
d4c2aa24 974 }
975 return $return_array;
976
977 }
978
e5d57179 979
980 // just looking for singular file or directory below...
981 //
d4c2aa24 982 // figure out what to do with files not found
983 //
e5d57179 984 if ($directories_only || empty($this->template_file_cache[$filename]['PATH'])) {
985
986 // if looking for directories...
987 // have to iterate through cache and detect
988 // directory by matching any file inside of it
989 //
990 if ($directories_ok) {
991 foreach ($this->template_file_cache as $file => $file_info) {
992 if (strpos($file, $filename) === 0
993 && ($pos = strpos($file, '/', strlen($filename))) !== FALSE
994 && strpos($file, '/', $pos + 1) === FALSE) {
995 return SM_PATH . substr($file_info['PATH'],
996 0,
997 strrpos($file_info['PATH'], '/'));
998 }
999 }
1000
1001 if ($directories_only) return '';
1002 }
d4c2aa24 1003
e5d57179 1004 // plugins get one more chance
de4d58cb 1005 //
e5d57179 1006 if (strpos($filename, 'plugins/') === 0) {
1007
1008 $plugin_name = substr($filename, 8, strpos($filename, '/', 8) - 8);
1009 $file = substr($filename, strlen($plugin_name) + 9);
1010
1011 if (!$this->find_and_cache_plugin_template_file($plugin_name, $file))
1012 return '';
1013 //FIXME: technically I guess we should check for directories
1014 // here too, but that's overkill (no need) presently
1015 // (plugin-provided alternate stylesheet dirs?!? bah.)
de4d58cb 1016
e5d57179 1017 }
de4d58cb 1018
e5d57179 1019 // nothing... return empty string (yes, the else is intentional!)
1020 //
1021 else return '';
de4d58cb 1022
d4c2aa24 1023 }
de4d58cb 1024
948be6d4 1025 return SM_PATH . $this->template_file_cache[$filename]['PATH'];
de4d58cb 1026
d4c2aa24 1027 }
de4d58cb 1028
d4c2aa24 1029 /**
1030 * Get template engine needed to render given template file.
1031 *
1032 * If at all possible, just returns a reference to $this, but
1033 * some template files may require a different engine, thus
1034 * an object for that engine (which will subsequently be kept
1035 * in this object for future use) is returned.
1036 *
1037 * @param string $filename The name of the template file,
1038 *
1039 * @return object The needed template object to render the template.
1040 *
1041 */
1042 function get_rendering_template_engine_object($filename) {
1043
1044 // for files that we cannot find engine info for,
1045 // just return $this
1046 //
1047 if (empty($this->template_file_cache[$filename]['ENGINE']))
1048 return $this;
1049
1050
1051 // otherwise, compare $this' engine to the file's engine
1052 //
1053 $engine = $this->template_file_cache[$filename]['ENGINE'];
1054 if ($this->template_engine == $engine)
1055 return $this;
1056
1057
1058 // need to load another engine... if already instantiated,
1059 // and stored herein, return that
1060 // FIXME: this assumes same engine setup in all template
1061 // set config files that have same engine in common
1062 // (but keeping a separate class object for every
1063 // template set seems like overkill... for now we
1064 // won't do that unless it becomes a problem)
1065 //
1066 if (!empty($this->other_template_engine_objects[$engine])) {
1067 $rendering_engine = $this->other_template_engine_objects[$engine];
de4d58cb 1068
de4d58cb 1069
d4c2aa24 1070 // otherwise, instantiate new engine object, add to cache
1071 // and return it
1072 //
1073 } else {
1074 $template_set_id = $this->template_file_cache[$filename]['SET_ID'];
1075 $this->other_template_engine_objects[$engine]
1076 = $this->get_template_engine_subclass($template_set_id);
1077 $rendering_engine = $this->other_template_engine_objects[$engine];
1078 }
de4d58cb 1079
de4d58cb 1080
d4c2aa24 1081 // now, need to copy over all the assigned variables
1082 // from $this to the rendering engine (YUCK! -- we need
1083 // to discourage template authors from creating
1084 // situations where engine changes occur)
1085 //
1086 $rendering_engine->clear_all_assign();
1087 $rendering_engine->assign($this->get_template_vars());
de4d58cb 1088
de4d58cb 1089
d4c2aa24 1090 // finally ready to go
1091 //
1092 return $rendering_engine;
de4d58cb 1093
d4c2aa24 1094 }
de4d58cb 1095
d4c2aa24 1096 /**
1097 * Return all JavaScript files provided by the template.
1098 *
1099 * All files found in the template set's "js" directory (and
1100 * that of its ancestors) with the extension ".js" are returned.
1101 *
1102 * @param boolean $full_path When FALSE, only the file names
1103 * are included in the return array;
1104 * otherwise, path information is
1105 * included (relative to SM_PATH)
1106 * (OPTIONAL; default only file names)
1107 *
1108 * @return array The required file names/paths.
1109 *
1110 */
1111 function get_javascript_includes($full_path=FALSE) {
de4d58cb 1112
d4c2aa24 1113 // since any page from a parent template set
1114 // could end up being loaded, we have to load
1115 // all js files from ancestor template sets,
1116 // not just this set
1117 //
1118 //$directory = SM_PATH . $this->get_template_file_directory() . 'js';
1119 //$js_files = list_files($directory, '.js', !$full_path);
1120 //
1121 $js_files = $this->get_template_file_path('js/');
1122
1123
1124 // parse out .js files only
1125 //
1126 $return_array = array();
1127 foreach ($js_files as $file) {
1128
1129 if (substr($file, strlen($file) - 3) != '.js') continue;
de4d58cb 1130
d4c2aa24 1131 if ($full_path) {
1132 $return_array[] = $file;
1133 } else {
1134 $return_array[] = basename($file);
de4d58cb 1135 }
1136
1137 }
1138
d4c2aa24 1139 return $return_array;
de4d58cb 1140
1141 }
1142
1143 /**
d4c2aa24 1144 * Return all alternate stylesheets provided by template.
1145 *
e5d57179 1146 * All (non-empty) directories found in the template set's
1147 * "css/alternates" directory (and that of its ancestors)
1148 * are returned.
d4c2aa24 1149 *
1150 * Note that prettified names are constructed herein by
e5d57179 1151 * taking the directory name, changing underscores to spaces
1152 * and capitalizing each word in the resultant name.
de4d58cb 1153 *
1154 * @param boolean $full_path When FALSE, only the file names
1155 * are included in the return array;
1156 * otherwise, path information is
1157 * included (relative to SM_PATH)
1158 * (OPTIONAL; default only file names)
1159 *
d4c2aa24 1160 * @return array A list of the available alternate stylesheets,
1161 * where the keys are the file names (formatted
1162 * according to $full_path) for the stylesheets,
1163 * and the values are the prettified version of
1164 * the file names for display to the user.
1165 *
de4d58cb 1166 */
d4c2aa24 1167 function get_alternative_stylesheets($full_path=FALSE) {
de4d58cb 1168
d4c2aa24 1169 // since any page from a parent template set
1170 // could end up being loaded, we will load
1171 // all alternate css files from ancestor
1172 // template sets, not just this set
1173 //
e5d57179 1174 $css_directories = $this->get_template_file_path('css/alternates/', TRUE, TRUE);
d4c2aa24 1175
1176
e5d57179 1177 // prettify names
d4c2aa24 1178 //
1179 $return_array = array();
e5d57179 1180 foreach ($css_directories as $directory) {
d4c2aa24 1181
e5d57179 1182 // CVS directories are not wanted
1183 //
1184 if (strpos($directory, '/CVS') === strlen($directory) - 4) continue;
d4c2aa24 1185
e5d57179 1186 $pretty_name = ucwords(str_replace('_', ' ', basename($directory)));
d4c2aa24 1187
1188 if ($full_path) {
e5d57179 1189 $return_array[$directory] = $pretty_name;
d4c2aa24 1190 } else {
e5d57179 1191 $return_array[basename($directory)] = $pretty_name;
de4d58cb 1192 }
d4c2aa24 1193
de4d58cb 1194 }
1195
d4c2aa24 1196 return $return_array;
de4d58cb 1197
1198 }
1199
1200 /**
1201 * Return all standard stylsheets provided by the template.
1202 *
d4c2aa24 1203 * All files found in the template set's "css" directory (and
1204 * that of its ancestors) with the extension ".css" except
1205 * "rtl.css" (which is dealt with separately) are returned.
de4d58cb 1206 *
1207 * @param boolean $full_path When FALSE, only the file names
1208 * are included in the return array;
1209 * otherwise, path information is
1210 * included (relative to SM_PATH)
1211 * (OPTIONAL; default only file names)
1212 *
1213 * @return array The required file names/paths.
1214 *
1215 */
1216 function get_stylesheets($full_path=FALSE) {
1217
d4c2aa24 1218 // since any page from a parent template set
1219 // could end up being loaded, we have to load
1220 // all css files from ancestor template sets,
1221 // not just this set
1222 //
1223 //$directory = SM_PATH . $this->get_template_file_directory() . 'css';
1224 //$css_files = list_files($directory, '.css', !$full_path);
1225 //
1226 $css_files = $this->get_template_file_path('css/');
1227
de4d58cb 1228
1229 // need to leave out "rtl.css"
d4c2aa24 1230 //
de4d58cb 1231 $return_array = array();
d4c2aa24 1232 foreach ($css_files as $file) {
de4d58cb 1233
d4c2aa24 1234 if (substr($file, strlen($file) - 4) != '.css') continue;
1235 if (strtolower(basename($file)) == 'rtl.css') continue;
de4d58cb 1236
d4c2aa24 1237 if ($full_path) {
1238 $return_array[] = $file;
1239 } else {
1240 $return_array[] = basename($file);
1241 }
de4d58cb 1242
1243 }
1244
c404d448 1245
1246 // return sheets for the current template set
1247 // last so we can enable any custom overrides
1248 // of styles in ancestor sheets
1249 //
1250 return array_reverse($return_array);
de4d58cb 1251
1252 }
1253
1254 /**
1255 * Generate links to all this template set's standard stylesheets
1256 *
1257 * Subclasses can override this function if stylesheets are
1258 * created differently for the template set's target output
1259 * interface.
1260 *
1261 * @return string The stylesheet links as they should be sent
1262 * to the browser.
1263 *
1264 */
1265 function fetch_standard_stylesheet_links()
1266 {
1267
1268 $sheets = $this->get_stylesheets(TRUE);
1269 return $this->fetch_external_stylesheet_links($sheets);
1270
1271 }
1272
1273 /**
1274 * Push out any other stylesheet links as provided (for
1275 * stylesheets not included with the current template set)
1276 *
1277 * Subclasses can override this function if stylesheets are
1278 * created differently for the template set's target output
1279 * interface.
1280 *
1281 * @param mixed $sheets List of the desired stylesheets
1282 * (file path to be used in stylesheet
1283 * href attribute) to output (or single
1284 * stylesheet file path).
1285FIXME: We could make the incoming array more complex so it can
1286 also contain the other parameters for create_css_link()
1287 such as $name, $alt, $mtype, and $xhtml_end
1288 But do we need to?
1289 *
1290 * @return string The stylesheet links as they should be sent
1291 * to the browser.
1292 *
1293 */
1294 function fetch_external_stylesheet_links($sheets)
1295 {
1296
1297 if (!is_array($sheets)) $sheets = array($sheets);
1298 $output = '';
1299
1300 foreach ($sheets as $sheet) {
1301 $output .= create_css_link($sheet);
1302 }
1303
1304 return $output;
1305
1306 }
1307
1308 /**
1309 * Send HTTP header(s) to browser.
1310 *
1311 * Subclasses can override this function if headers are
1312 * managed differently in the template set's target output
1313 * interface.
1314 *
1315 * @param mixed $headers A list of (or a single) header
1316 * text to be sent.
1317 *
1318 */
1319 function header($headers)
1320 {
1321
1322 if (!is_array($headers)) $headers = array($headers);
1323
1324 foreach ($headers as $header) {
1325 header($header);
1326 }
1327
1328 }
1329
1330 /**
1331 * Generate a link to the right-to-left stylesheet for
d4c2aa24 1332 * this template set by getting the "rtl.css" file from
1333 * this template set, its parent (or grandparent, etc.)
1334 * template set, the fall-back template set, or finally,
1335 * fall back to SquirrelMail's own "rtl.css" if need be.
de4d58cb 1336 *
1337 * Subclasses can override this function if stylesheets are
1338 * created differently for the template set's target output
1339 * interface.
1340 *
1341 * @return string The stylesheet link as it should be sent
1342 * to the browser.
1343 *
1344 */
1345 function fetch_right_to_left_stylesheet_link()
1346 {
1347
1348 // get right template file
1349 //
1350 $sheet = $this->get_template_file_path('css/rtl.css');
1351
1352 // fall back to SquirrelMail's own default stylesheet
1353 //
1354 if (empty($sheet)) {
1355 $sheet = SM_PATH . 'css/rtl.css';
1356 }
1357
1358 return create_css_link($sheet);
1359
1360 }
1361
1362 /**
1363 * Display the template
1364 *
1365 * @param string $file The template file to use
1366 *
1367 */
1368 function display($file)
1369 {
1370
1371 echo $this->fetch($file);
1372
1373 }
1374
1375 /**
1376 * Applies the template and returns the resultant content string.
1377 *
1378 * @param string $file The template file to use
1379 *
1380 * @return string The template contents after applying the given template
1381 *
1382 */
1383 function fetch($file) {
1384
1385 // get right template file
1386 //
1387 $template = $this->get_template_file_path($file);
1388
d4c2aa24 1389
de4d58cb 1390 // special case stylesheet.tpl falls back to SquirrelMail's
1391 // own default stylesheet
1392 //
1393 if (empty($template) && $file == 'css/stylesheet.tpl') {
1394 $template = SM_PATH . 'css/default.css';
1395 }
1396
d4c2aa24 1397
de4d58cb 1398 if (empty($template)) {
1399
1400 trigger_error('The template "' . htmlspecialchars($file)
1401 . '" could not be fetched!', E_USER_ERROR);
1402
1403 } else {
1404
1405 $aPluginOutput = array();
1406 $aPluginOutput = concat_hook_function('template_construct_' . $file,
d849b570 1407 $temp=array(&$aPluginOutput, &$this));
de4d58cb 1408 $this->assign('plugin_output', $aPluginOutput);
1409
d4c2aa24 1410 //$output = $this->apply_template($template);
1411 $rendering_engine = $this->get_rendering_template_engine_object($file);
1412 $output = $rendering_engine->apply_template($template);
de4d58cb 1413
1414 // CAUTION: USE OF THIS HOOK IS HIGHLY DISCOURAGED AND CAN
1415 // RESULT IN NOTICABLE PERFORMANCE DEGREDATION. Plugins
1416 // using this hook will probably be rejected by the
1417 // SquirrelMail team.
1418 //
d849b570 1419 do_hook('template_output', $output);
de4d58cb 1420
1421 return $output;
1422
1423 }
1424
1425 }
1426
1427 /**
1428 * Assigns values to template variables
1429 *
1430 * Note: this is an abstract method that must be implemented by subclass.
1431 *
1432 * @param array|string $tpl_var the template variable name(s)
1433 * @param mixed $value the value to assign
1434 *
1435 */
1436 function assign($tpl_var, $value = NULL) {
1437
1438 trigger_error('Template subclass (' . $this->template_engine . 'Template.class.php) needs to implement the assign() method.', E_USER_ERROR);
1439
1440 }
1441
1442 /**
1443 * Assigns values to template variables by reference
1444 *
1445 * Note: this is an abstract method that must be implemented by subclass.
1446 *
1447 * @param string $tpl_var the template variable name
1448 * @param mixed $value the referenced value to assign
1449 *
1450 */
1451 function assign_by_ref($tpl_var, &$value) {
1452
1453 trigger_error('Template subclass (' . $this->template_engine . 'Template.class.php) needs to implement the assign_by_ref() method.', E_USER_ERROR);
1454
1455 }
1456
1457 /**
d4c2aa24 1458 * Clears the values of all assigned varaiables.
1459 *
1460 */
1461 function clear_all_assign() {
1462
1463 trigger_error('Template subclass (' . $this->template_engine . 'Template.class.php) needs to implement the clear_all_assign() method.', E_USER_ERROR);
1464
1465 }
1466
1467 /**
1468 * Returns assigned variable value(s).
1469 *
1470 * @param string $varname If given, the value of that variable
1471 * is returned, assuming it has been
1472 * previously assigned. If not specified
1473 * an array of all assigned variables is
1474 * returned. (optional)
1475 *
1476 * @return mixed Desired single variable value or list of all
1477 * assigned variable values.
1478 *
1479 */
1480 function get_template_vars($varname=NULL) {
1481
1482 trigger_error('Template subclass (' . $this->template_engine . 'Template.class.php) needs to implement the get_template_vars() method.', E_USER_ERROR);
1483
1484 }
1485
1486 /**
de4d58cb 1487 * Appends values to template variables
1488 *
1489 * Note: this is an abstract method that must be implemented by subclass.
1490 *
1491 * @param array|string $tpl_var the template variable name(s)
1492 * @param mixed $value the value to append
1493 * @param boolean $merge when $value is given as an array,
1494 * this indicates whether or not that
1495 * array itself should be appended as
1496 * a new template variable value or if
1497 * that array's values should be merged
1498 * into the existing array of template
1499 * variable values
1500 *
1501 */
1502 function append($tpl_var, $value = NULL, $merge = FALSE) {
1503
1504 trigger_error('Template subclass (' . $this->template_engine . 'Template.class.php) needs to implement the append() method.', E_USER_ERROR);
1505
1506 }
1507
1508 /**
1509 * Appends values to template variables by reference
1510 *
1511 * Note: this is an abstract method that must be implemented by subclass.
1512 *
1513 * @param string $tpl_var the template variable name
1514 * @param mixed $value the referenced value to append
1515 * @param boolean $merge when $value is given as an array,
1516 * this indicates whether or not that
1517 * array itself should be appended as
1518 * a new template variable value or if
1519 * that array's values should be merged
1520 * into the existing array of template
1521 * variable values
1522 *
1523 */
1524 function append_by_ref($tpl_var, &$value, $merge = FALSE) {
1525
1526 trigger_error('Template subclass (' . $this->template_engine . 'Template.class.php) needs to implement the append_by_ref() method.', E_USER_ERROR);
1527
1528 }
1529
1530 /**
1531 * Applys the template and generates final output destined
1532 * for the user's browser
1533 *
1534 * Note: this is an abstract method that must be implemented by subclass.
1535 *
1536 * @param string $filepath The full file path to the template to be applied
1537 *
1538 * @return string The output for the given template
1539 *
1540 */
1541 function apply_template($filepath) {
1542
1543 trigger_error('Template subclass (' . $this->template_engine . 'Template.class.php) needs to implement the apply_template() method.', E_USER_ERROR);
1544
1545 }
1546
1547}
1548