Merge ../butv10/core/framework
[isso.git] / Template.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Blue Static ISSO Framework
5 || # Copyright (c)2005-2008 Blue Static
6 || #
7 || # This program is free software; you can redistribute it and/or modify
8 || # it under the terms of the GNU General Public License as published by
9 || # the Free Software Foundation; version 2 of the License.
10 || #
11 || # This program is distributed in the hope that it will be useful, but
12 || # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 || # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 || # more details.
15 || #
16 || # You should have received a copy of the GNU General Public License along
17 || # with this program; if not, write to the Free Software Foundation, Inc.,
18 || # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 || ###################################################################
20 \*=====================================================================*/
21
22 /**
23 * Database-Driven Template System (template.php)
24 *
25 * @package ISSO
26 */
27
28 require_once(ISSO . '/Functions.php');
29
30 /**
31 * File-Based Template System
32 *
33 * This framework merely replaces the template loading functions with
34 * file-system based ones. It has an optional caching system in which
35 * template data will remain stored in the database as long as the filesystem
36 * file is modified. To do this, pass a table name to setDatabaseCache() and make sure
37 * there's a DB module that has access to a table with this schema:
38 *
39 * CREATE TABLE template (filename VARCHAR (250) NOT NULL, template TEXT NOT NULL, timestamp INT NOT NULL);
40 *
41 * @author Blue Static
42 * @copyright Copyright (c)2005 - 2008, Blue Static
43 * @package ISSO
44 *
45 */
46 class BSTemplate
47 {
48 /**
49 * The path, from the path of the application, where templates are stored
50 * @var string
51 */
52 private $templateDir = '';
53
54 /**
55 * The extension all the template files have
56 * @var string
57 */
58 private $extension = 'tpl';
59
60 /**
61 * The database table name for the template cache
62 * @var string
63 */
64 private $dbCacheTable = null;
65
66 /**
67 * The name of the function phrases are fetched with
68 * @var string
69 */
70 private $langcall = 'gettext';
71
72 /**
73 * The name of the function phrases are sprintf() parsed with
74 * @var string
75 */
76 private $langconst = 'sprintf';
77
78 /**
79 * Array of pre-compiled templates that are stored to decrease server load
80 * @var array
81 */
82 protected $cache = array();
83
84 /**
85 * A list of templates that weren't cached, but are still used
86 * @var array
87 */
88 protected $uncached = array();
89
90 /**
91 * Whether or not the page has been flush()'d already
92 * @var bool
93 */
94 private $doneflush = false;
95
96 /**
97 * The name of a function that is called before template parsing of phrases and conditionals occurs
98 * @var string
99 */
100 private $preParseHook = ':undefined:';
101
102 /**
103 * Sets the template directory name
104 *
105 * @param string Template directory name
106 */
107 public function setTemplateDirectory($dir)
108 {
109 $this->templateDir = BSFunctions::fetch_source_path($dir);
110 }
111
112 /**
113 * Sets the file extension for the templates
114 *
115 * @param string File extension
116 */
117 public function setExtension($ext)
118 {
119 $this->extension = $ext;
120 }
121
122 /**
123 * Sets the name of the table to access for the datbase cache
124 *
125 * @param string DB table name
126 */
127 public function setDatabaseCache($table)
128 {
129 $this->dbCacheTable = $table;
130 }
131
132 /**
133 * Sets the pre-parse hook method which is called before any other
134 * processing is done on the template.
135 *
136 * @param string Method name
137 */
138 public function setPreParseHook($hook)
139 {
140 $this->preParseHook = $hook;
141 }
142
143 /**
144 * Takes an array of template names, loads them, and then stores a
145 * parsed version for optimum speed.
146 *
147 * @param array List of template names to be cached
148 */
149 public function cache($namearray)
150 {
151 if (sizeof($this->cache) > 0)
152 {
153 throw new Exception('You cannot cache templates more than once per initialization');
154 }
155 else
156 {
157 $dbCache = array();
158 if ($this->dbCacheTable)
159 {
160 $db =& BSApp::$db;
161 $cache = $db->query("SELECT * FROM {$this->dbCacheTable} WHERE filename IN ('" . implode("', '", $namearray) . "')");
162 while ($tpl = $cache->fetchArray())
163 {
164 $time = filemtime($this->templateDir . $tpl['filename'] . '.' . $this->extension);
165 $template = $tpl['template'];
166 if ($time > $tpl['timestamp'])
167 {
168 $template = $this->_parseTemplate($this->_loadTemplate($tpl['filename']));
169 $db->query("UPDATE {$this->dbCacheTable} SET template = '" . $db->escapeString($template) . "', timestamp = " . TIMENOW . " WHERE filename = '" . $tpl['filename'] . "'");
170 $tpl['template'] = $template;
171 }
172 $dbCache["$tpl[filename]"] = $template;
173 }
174 }
175 foreach ($namearray as $name)
176 {
177 if ($this->dbCacheTable)
178 {
179 if (isset($dbCache["$name"]))
180 {
181 $template = $dbCache["$name"];
182 }
183 else
184 {
185 $template = $this->_parseTemplate($this->_loadTemplate($name));
186 $db->query("INSERT INTO {$this->dbCacheTable} (filename, template, timestamp) VALUES ('$name', '" . $db->escapeString($template) . "', " . TIMENOW . ")");
187 }
188 }
189 else
190 {
191 $template = $this->_parseTemplate($this->_loadTemplate($name));
192 }
193
194 $this->cache[$name] = $template;
195 }
196 }
197 }
198
199 /**
200 * Loads a template from the cache or the _load function and stores the
201 * parsed version of it
202 *
203 * @param string The name of the template
204 *
205 * @return string A parsed and loaded template
206 */
207 public function fetch($name)
208 {
209 if (isset($this->cache[$name]))
210 {
211 $template = $this->cache[$name];
212 }
213 else
214 {
215 $this->uncached[$name] = (isset($this->uncached[$name]) ? $this->uncached[$name] + 1 : 0);
216 BSApp::debug("Manually loading template '$name'");
217 $template = $this->_loadTemplate($name);
218 $template = $this->_parseTemplate($template);
219 }
220
221 return $template;
222 }
223
224 /**
225 * Output a template fully compiled to the browser
226 *
227 * @param string Compiled and ready template
228 */
229 public function flush($template)
230 {
231 ob_start();
232
233 if (empty($template))
234 {
235 throw new Exception('There was no output to print');
236 }
237
238 if ($this->doneflush)
239 {
240 throw new Exception('A template has already been sent to the output buffer');
241 }
242
243 $debugBlock = '';
244 if (BSApp::get_debug())
245 {
246 $debugBlock .= "\n<div align=\"center\">Executed in " . round(BSFunctions::fetch_microtime_diff('0 ' . $_SERVER['REQUEST_TIME']), 10) . ' seconds</div>';
247 $debugBlock .= "\n<br /><div align=\"center\">" . BSApp::get_debug_list() . "</div>";
248
249 if (sizeof($this->uncached) > 0)
250 {
251 foreach ($this->uncached as $name => $count)
252 {
253 $tpls[] = $name . "($count)";
254 }
255 $debugBlock .= "<br /><div style=\"color: red\" align=\"center\"><strong>Uncached Templates:</strong>" . implode(', ', $tpls) . " )</div>\n";
256 }
257
258 if (BSApp::$db)
259 {
260 $queries = BSApp::$db->getHistory();
261
262 $debugBlock .= "<br />\n" . '<table cellpadding="4" cellspacing="1" border="0" align="center" width="30%" style="background-color: rgb(60, 60, 60); color: white">' . "\n\t" . '<tr><td><strong>Query Debug</strong></td></tr>';
263
264 foreach ($queries as $query)
265 {
266 $debugBlock .= "\n\t<tr style=\"background-color: rgb(230, 230, 230); color: black\">";
267 $debugBlock .= "\n\t\t<td>";
268 $debugBlock .= "\n\t\t\t$query[query]\n\n\t\t\t<div style=\"font-size: 9px;\">($query[time])</div>\n<!--\n$query[trace]\n-->\n\t\t</td>\n\t</tr>";
269 }
270
271 $debugBlock .= "\n</table>\n\n\n";
272 }
273
274 $template = str_replace('</body>', $debugBlock . '</body>', $template);
275 }
276
277 print($template);
278 }
279
280 /**
281 * Loads an additional template from the database
282 *
283 * @param string The name of the template
284 *
285 * @return string Template data from the database
286 */
287 protected function _loadTemplate($name)
288 {
289 $path = $this->templateDir . $name . '.' . $this->extension;
290 if (is_file($path) && is_readable($path))
291 {
292 return @file_get_contents($path);
293 }
294 else
295 {
296 throw new Exception("Could not load the template '$path'");
297 }
298 }
299
300 /**
301 * A wrapper for all the parsing functions and compiling functins
302 *
303 * @param string Unparsed template data
304 *
305 * @return string Parsed template data
306 */
307 protected function _parseTemplate($template)
308 {
309 $template = str_replace('"', '\"', $template);
310
311 if (function_exists($this->preParseHook))
312 {
313 $template = call_user_func($this->preParseHook, $template);
314 }
315
316 $template = $this->_parseBlocksAndTokens($template);
317 $template = $this->_parsePhrases($template);
318 $template = $this->_parseConditionals($template);
319 return $template;
320 }
321
322 /**
323 * Parses anything with curly braces {} (including phrases)
324 *
325 * @param string Template data
326 *
327 * @return string Parsed template data
328 */
329 private function _parseBlocksAndTokens($template)
330 {
331 $stack = array();
332 $tokens = array();
333
334 while (1)
335 {
336 for ($i = 0; $i < strlen($template); $i++)
337 {
338 // we've run through the template and there's nothing in the stack--done
339 if ($i == strlen($template) - 1 && sizeof($stack) == 0)
340 {
341 return $template;
342 }
343
344 if ($template[$i] == '{')
345 {
346 // ignore escaped sequences
347 if ($template[$i - 1] != '\\')
348 {
349 array_push($stack, $i);
350 }
351 }
352 else if ($template[$i] == '}')
353 {
354 // there's no stack so it was probably escaped
355 if (sizeof($stack) == 0)
356 {
357 continue;
358 }
359 // we're good and nested
360 else if (sizeof($stack) == 1)
361 {
362 $open = array_pop($stack);
363 $token = substr($template, $open, $i - $open + 1);
364 $template = str_replace($token, $this->_parseToken($token), $template);
365 break;
366 }
367 // just pop it off
368 else
369 {
370 array_pop($stack);
371 }
372 }
373 }
374 }
375 }
376
377 /**
378 * Parses a curly brace token {}
379 *
380 * @param string Token
381 *
382 * @return string Parsed value
383 */
384 private function _parseToken($token)
385 {
386 // knock of the braces
387 $token = substr($token, 1, strlen($token) - 2);
388
389 // language token
390 if ($token[0] == '@' && $token[1] == '\\' && $token[2] == '"')
391 {
392 return '" . ' . $this->langcall . '(\'' . str_replace(array('\\\"', "'"), array('"', "\'"), substr($token, 3, strlen($token) - 5)) . '\')' . ' . "';
393 }
394 // normal PHP code
395 else
396 {
397 return '" . (' . $token . ') . "';
398 }
399 }
400
401 /**
402 * Prepares language and locale information inside templates
403 *
404 * @param string Template data to be processed
405 *
406 * @return string Language-ready template data
407 */
408 private function _parsePhrases($template)
409 {
410 $tagStart = '<lang ';
411 $tagEnd = '</lang>';
412
413 $start = -1; // start of open tag
414 $end = -1; // start of the close tag
415 $varEnd = -1; // end of the open tag
416
417 while ($start <= strlen($template))
418 {
419 // reset
420 $varMap = array(); // storage for all the substitution indices
421
422 // Find the start language object tag
423 $start = strpos($template, $tagStart, $end + 1);
424 if ($start === false)
425 {
426 break;
427 }
428
429 // look ahead to parse out all the variables
430 $i = $start + strlen($tagStart); // current position
431 $capture = ''; // current capture
432 $capturePos = $i; // the place to start capturing
433 $varNum = -1; // variable placeholder index
434 while ($i < strlen($template))
435 {
436 if ($template[$i] == '=')
437 {
438 // backtrack to find the previous variable substitution
439 $backPos = $i;
440 while ($backPos >= $start)
441 {
442 if ($template[$backPos] == '"')
443 {
444 // startPosition + length(startTag) + length(=\")
445 $varMap[intval($varNum)] = BSFunctions::substring($template, $capturePos + 3, $backPos - 1);
446 // remove our old substitution from the capture
447 $capture = BSFunctions::substring($template, $backPos + 1, $i);
448 break;
449 }
450 $backPos--;
451 }
452
453 // do we have a valid index?
454 if (intval($capture) > 0)
455 {
456 // set aside the index and restart capturing
457 $varNum = $capture;
458 $capture = '';
459 $capturePos = $i;
460 }
461 else
462 {
463 throw new Exception('Invalid language variable index "' . $capture . '"');
464 }
465 }
466 else if ($template[$i] == '>' && $template[$i - 1] == '"')
467 {
468 // the final variable substitution
469 $varMap[intval($varNum)] = BSFunctions::substring($template, $capturePos + 3, $i - 2);
470 $varEnds = $i;
471 break;
472 }
473
474 $capture .= $template[$i];
475 $i++;
476 }
477
478 // locate the end tag
479 $end = strpos($template, $tagEnd, $i);
480 if ($end === false)
481 {
482 break;
483 }
484
485 // this is the string that gets variable replacement
486 $str = BSFunctions::substring($template, $varEnds + 1, $end);
487
488 // create the complete varmap
489
490 for ($i = max(array_keys($varMap)); $i > 0; $i--)
491 {
492 if (!isset($varMap[$i]))
493 {
494 $varMap[$i] = '<strong>[MISSING SUBSTITUTION INDEX: ' . $i . ']</strong>';
495 }
496 }
497
498 // put all the keys in corresponding argument order
499 ksort($varMap);
500
501 // FINALLY, construct the call to sprintf()
502 $template = substr_replace($template, '" . ' . $this->langconst . '(\'' . $str . '\', "' . implode('", "', $varMap) . '") . "', $start, ($end + strlen($tagEnd)) - $start);
503 }
504
505 return $template;
506 }
507
508 /**
509 * Parser for in-line template conditionals
510 *
511 * @param string Template data awaiting processing
512 *
513 * @return string Parsed template data
514 */
515 private function _parseConditionals($template)
516 {
517 // tag data
518 $tag_start = '<if condition=\"';
519 $tag_start_end = '\">';
520 $tag_else = '<else />';
521 $tag_end = '</if>';
522
523 // tag stack
524 $stack = array();
525
526 // the information about the current active tag
527 $tag_full = array();
528 $parsed = array();
529
530 // start at 0
531 $offset = 0;
532
533 while (1)
534 {
535 if (strpos($template, $tag_start) === false)
536 {
537 break;
538 }
539
540 for ($i = $offset; $i < strlen($template); $i++)
541 {
542 // we've found ourselves a conditional!
543 if (substr($template, $i, strlen($tag_start)) == $tag_start)
544 {
545 // push the position into the tag stack
546 if ($tag_full)
547 {
548 array_push($stack, $i);
549 }
550 else
551 {
552 $tag_full['posi'] = $i;
553 }
554 }
555 // locate else tags
556 else if (substr($template, $i, strlen($tag_else)) == $tag_else)
557 {
558 if (sizeof($stack) == 0 && !isset($tag_full['else']))
559 {
560 $tag_full['else'] = $i;
561 }
562 }
563 // do we have an end tag?
564 else if (substr($template, $i, strlen($tag_end)) == $tag_end)
565 {
566 if (sizeof($stack) != 0)
567 {
568 array_pop($stack);
569 continue;
570 }
571
572 // calculate the position of the end tag
573 $tag_full['posf'] = $i + strlen($tag_end) - 1;
574
575 // extract the entire conditional from the template
576 $fullspread = substr($template, $tag_full['posi'], $tag_full['posf'] - $tag_full['posi'] + 1);
577
578 // remove the beginning tag
579 $conditional = substr($fullspread, strlen($tag_start));
580
581 // find the end of the expression
582 $temp_end = strpos($conditional, $tag_start_end);
583
584 // save the expression
585 $parsed[0] = stripslashes(substr($conditional, 0, $temp_end));
586
587 // remove the expression from the conditional
588 $conditional = substr($conditional, strlen($parsed[0]) + strlen($tag_start_end));
589
590 // remove the tailing end tag
591 $conditional = substr($conditional, 0, strlen($conditional) - strlen($tag_end));
592
593 // handle the else
594 if (isset($tag_full['else']))
595 {
596 // now relative to the start of the <if>
597 $relpos = $tag_full['else'] - $tag_full['posi'];
598
599 // calculate the length of the expression and opening tag
600 $length = strlen($parsed[0]) + strlen($tag_start) + strlen($tag_start_end);
601
602 // relative to the start of iftrue
603 $elsepos = $relpos - $length;
604
605 $parsed[1] = substr($conditional, 0, $elsepos);
606 $parsed[2] = substr($conditional, $elsepos + strlen($tag_else));
607 }
608 // no else to handle
609 else
610 {
611 $parsed[1] = $conditional;
612 $parsed[2] = '';
613 }
614
615 // final parsed output
616 $parsed = '" . ((' . stripslashes($parsed[0]) . ') ? "' . $parsed[1] . '" : "' . $parsed[2] . '") . "';
617
618 // replace the conditional
619 $template = str_replace($fullspread, $parsed, $template);
620
621 // reset the parser
622 $offset = $tag_full['posi'] + strlen($tag_start) + strlen($tag_start_end);
623 $tag_full = array();
624 $stack = array();
625 $parsed = array();
626 unset($fullspread, $conditional, $temp_end, $relpos, $length, $elsepos);
627 break;
628 }
629 }
630 }
631
632 return $template;
633 }
634 }
635
636 ?>