Fixing a documentation and error mistake that weren't fixed through our sesarch/repla...
[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 /**
104 * Sets the template directory name
105 *
106 * @param string Template directory name
107 */
108 public function setTemplateDirectory($dir)
109 {
110 $this->templateDir = BSFunctions::fetch_source_path($dir);
111 }
112
113 // ###################################################################
114 /**
115 * Sets the file extension for the templates
116 *
117 * @param string File extension
118 */
119 public function setExtension($ext)
120 {
121 $this->extension = $ext;
122 }
123
124 // ###################################################################
125 /**
126 * Sets the name of the table to access for the datbase cache
127 *
128 * @param string DB table name
129 */
130 public function setDatabaseCache($table)
131 {
132 $this->dbCacheTable = $table;
133 }
134
135 // ###################################################################
136 /**
137 * Sets the pre-parse hook method which is called before any other
138 * processing is done on the template.
139 *
140 * @param string Method name
141 */
142 public function setPreParseHook($hook)
143 {
144 $this->preParseHook = $hook;
145 }
146
147 // ###################################################################
148 /**
149 * Takes an array of template names, loads them, and then stores a
150 * parsed version for optimum speed.
151 *
152 * @param array List of template names to be cached
153 */
154 public function cache($namearray)
155 {
156 if (sizeof($this->cache) > 0)
157 {
158 throw new Exception('You cannot cache templates more than once per initialization');
159 }
160 else
161 {
162 $dbCache = array();
163 if ($this->dbCacheTable)
164 {
165 $db =& BSApp::$db;
166 $cache = $db->query("SELECT * FROM {$this->dbCacheTable} WHERE filename IN ('" . implode("', '", $namearray) . "')");
167 while ($tpl = $cache->fetchArray())
168 {
169 $time = filemtime($this->templateDir . $tpl['filename'] . '.' . $this->extension);
170 $template = $tpl['template'];
171 if ($time > $tpl['timestamp'])
172 {
173 $template = $this->_parseTemplate($this->_loadTemplate($tpl['filename']));
174 $db->query("UPDATE {$this->dbCacheTable} SET template = '" . $db->escapeString($template) . "', timestamp = " . TIMENOW . " WHERE filename = '" . $tpl['filename'] . "'");
175 $tpl['template'] = $template;
176 }
177 $dbCache["$tpl[filename]"] = $template;
178 }
179 }
180 foreach ($namearray as $name)
181 {
182 if ($this->dbCacheTable)
183 {
184 if (isset($dbCache["$name"]))
185 {
186 $template = $dbCache["$name"];
187 }
188 else
189 {
190 $template = $this->_parseTemplate($this->_loadTemplate($name));
191 $db->query("INSERT INTO {$this->dbCacheTable} (filename, template, timestamp) VALUES ('$name', '" . $db->escapeString($template) . "', " . TIMENOW . ")");
192 }
193 }
194 else
195 {
196 $template = $this->_parseTemplate($this->_loadTemplate($name));
197 }
198
199 $this->cache[$name] = $template;
200 }
201 }
202 }
203
204 // ###################################################################
205 /**
206 * Loads a template from the cache or the _load function and stores the
207 * parsed version of it
208 *
209 * @param string The name of the template
210 *
211 * @return string A parsed and loaded template
212 */
213 public function fetch($name)
214 {
215 if (isset($this->cache[$name]))
216 {
217 $template = $this->cache[$name];
218 }
219 else
220 {
221 $this->uncached[$name] = (isset($this->uncached[$name]) ? $this->uncached[$name] + 1 : 0);
222 BSApp::debug("Manually loading template '$name'");
223 $template = $this->_loadTemplate($name);
224 $template = $this->_parseTemplate($template);
225 }
226
227 return $template;
228 }
229
230 // ###################################################################
231 /**
232 * Output a template fully compiled to the browser
233 *
234 * @param string Compiled and ready template
235 */
236 public function flush($template)
237 {
238 ob_start();
239
240 if (empty($template))
241 {
242 throw new Exception('There was no output to print');
243 }
244
245 if ($this->doneflush)
246 {
247 throw new Exception('A template has already been sent to the output buffer');
248 }
249
250 $debugBlock = '';
251 if (BSApp::get_debug())
252 {
253 $debugBlock .= "\n<div align=\"center\">Executed in " . round(BSFunctions::fetch_microtime_diff('0 ' . $_SERVER['REQUEST_TIME']), 10) . ' seconds</div>';
254 $debugBlock .= "\n<br /><div align=\"center\">" . BSApp::get_debug_list() . "</div>";
255
256 if (sizeof($this->uncached) > 0)
257 {
258 foreach ($this->uncached as $name => $count)
259 {
260 $tpls[] = $name . "($count)";
261 }
262 $debugBlock .= "<br /><div style=\"color: red\" align=\"center\"><strong>Uncached Templates:</strong>" . implode(', ', $tpls) . " )</div>\n";
263 }
264
265 if (BSApp::$db)
266 {
267 $queries = BSApp::$db->getHistory();
268
269 $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>';
270
271 foreach ($queries as $query)
272 {
273 $debugBlock .= "\n\t<tr style=\"background-color: rgb(230, 230, 230); color: black\">";
274 $debugBlock .= "\n\t\t<td>";
275 $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>";
276 }
277
278 $debugBlock .= "\n</table>\n\n\n";
279 }
280
281 $template = str_replace('</body>', $debugBlock . '</body>', $template);
282 }
283
284 print($template);
285 }
286
287 // ###################################################################
288 /**
289 * Loads an additional template from the database
290 *
291 * @param string The name of the template
292 *
293 * @return string Template data from the database
294 */
295 protected function _loadTemplate($name)
296 {
297 $path = $this->templateDir . $name . '.' . $this->extension;
298 if (is_file($path) && is_readable($path))
299 {
300 return @file_get_contents($path);
301 }
302 else
303 {
304 throw new Exception("Could not load the template '$path'");
305 }
306 }
307
308 // ###################################################################
309 /**
310 * A wrapper for all the parsing functions and compiling functins
311 *
312 * @param string Unparsed template data
313 *
314 * @return string Parsed template data
315 */
316 protected function _parseTemplate($template)
317 {
318 $template = str_replace('"', '\"', $template);
319
320 if (function_exists($this->preParseHook))
321 {
322 $template = call_user_func($this->preParseHook, $template);
323 }
324
325 $template = $this->_parseBlocksAndTokens($template);
326 $template = $this->_parsePhrases($template);
327 $template = $this->_parseConditionals($template);
328 return $template;
329 }
330
331 // ###################################################################
332 /**
333 * Parses anything with curly braces {} (including phrases)
334 *
335 * @param string Template data
336 *
337 * @return string Parsed template data
338 */
339 private function _parseBlocksAndTokens($template)
340 {
341 $stack = array();
342 $tokens = array();
343
344 while (1)
345 {
346 for ($i = 0; $i < strlen($template); $i++)
347 {
348 // we've run through the template and there's nothing in the stack--done
349 if ($i == strlen($template) - 1 && sizeof($stack) == 0)
350 {
351 return $template;
352 }
353
354 if ($template[$i] == '{')
355 {
356 // ignore escaped sequences
357 if ($template[$i - 1] != '\\')
358 {
359 array_push($stack, $i);
360 }
361 }
362 else if ($template[$i] == '}')
363 {
364 // there's no stack so it was probably escaped
365 if (sizeof($stack) == 0)
366 {
367 continue;
368 }
369 // we're good and nested
370 else if (sizeof($stack) == 1)
371 {
372 $open = array_pop($stack);
373 $token = substr($template, $open, $i - $open + 1);
374 $template = str_replace($token, $this->_parseToken($token), $template);
375 break;
376 }
377 // just pop it off
378 else
379 {
380 array_pop($stack);
381 }
382 }
383 }
384 }
385 }
386
387 // ###################################################################
388 /**
389 * Parses a curly brace token {}
390 *
391 * @param string Token
392 *
393 * @return string Parsed value
394 */
395 private function _parseToken($token)
396 {
397 // knock of the braces
398 $token = substr($token, 1, strlen($token) - 2);
399
400 // language token
401 if ($token[0] == '@' && $token[1] == '\\' && $token[2] == '"')
402 {
403 return '" . ' . $this->langcall . '(\'' . str_replace(array('\\\"', "'"), array('"', "\'"), substr($token, 3, strlen($token) - 5)) . '\')' . ' . "';
404 }
405 // normal PHP code
406 else
407 {
408 return '" . (' . $token . ') . "';
409 }
410 }
411
412 // ###################################################################
413 /**
414 * Prepares language and locale information inside templates
415 *
416 * @param string Template data to be processed
417 *
418 * @return string Language-ready template data
419 */
420 private function _parsePhrases($template)
421 {
422 $tagStart = '<lang ';
423 $tagEnd = '</lang>';
424
425 $start = -1; // start of open tag
426 $end = -1; // start of the close tag
427 $varEnd = -1; // end of the open tag
428
429 while ($start <= strlen($template))
430 {
431 // reset
432 $varMap = array(); // storage for all the substitution indices
433
434 // Find the start language object tag
435 $start = strpos($template, $tagStart, $end + 1);
436 if ($start === false)
437 {
438 break;
439 }
440
441 // look ahead to parse out all the variables
442 $i = $start + strlen($tagStart); // current position
443 $capture = ''; // current capture
444 $capturePos = $i; // the place to start capturing
445 $varNum = -1; // variable placeholder index
446 while ($i < strlen($template))
447 {
448 if ($template[$i] == '=')
449 {
450 // backtrack to find the previous variable substitution
451 $backPos = $i;
452 while ($backPos >= $start)
453 {
454 if ($template[$backPos] == '"')
455 {
456 // startPosition + length(startTag) + length(=\")
457 $varMap[intval($varNum)] = BSFunctions::substring($template, $capturePos + 3, $backPos - 1);
458 // remove our old substitution from the capture
459 $capture = BSFunctions::substring($template, $backPos + 1, $i);
460 break;
461 }
462 $backPos--;
463 }
464
465 // do we have a valid index?
466 if (intval($capture) > 0)
467 {
468 // set aside the index and restart capturing
469 $varNum = $capture;
470 $capture = '';
471 $capturePos = $i;
472 }
473 else
474 {
475 throw new Exception('Invalid language variable index "' . $capture . '"');
476 }
477 }
478 else if ($template[$i] == '>' && $template[$i - 1] == '"')
479 {
480 // the final variable substitution
481 $varMap[intval($varNum)] = BSFunctions::substring($template, $capturePos + 3, $i - 2);
482 $varEnds = $i;
483 break;
484 }
485
486 $capture .= $template[$i];
487 $i++;
488 }
489
490 // locate the end tag
491 $end = strpos($template, $tagEnd, $i);
492 if ($end === false)
493 {
494 break;
495 }
496
497 // this is the string that gets variable replacement
498 $str = BSFunctions::substring($template, $varEnds + 1, $end);
499
500 // create the complete varmap
501
502 for ($i = max(array_keys($varMap)); $i > 0; $i--)
503 {
504 if (!isset($varMap[$i]))
505 {
506 $varMap[$i] = '<strong>[MISSING SUBSTITUTION INDEX: ' . $i . ']</strong>';
507 }
508 }
509
510 // put all the keys in corresponding argument order
511 ksort($varMap);
512
513 // FINALLY, construct the call to sprintf()
514 $template = substr_replace($template, '" . ' . $this->langconst . '(\'' . $str . '\', "' . implode('", "', $varMap) . '") . "', $start, ($end + strlen($tagEnd)) - $start);
515 }
516
517 return $template;
518 }
519
520 // ###################################################################
521 /**
522 * Parser for in-line template conditionals
523 *
524 * @param string Template data awaiting processing
525 *
526 * @return string Parsed template data
527 */
528 private function _parseConditionals($template)
529 {
530 // tag data
531 $tag_start = '<if condition=\"';
532 $tag_start_end = '\">';
533 $tag_else = '<else />';
534 $tag_end = '</if>';
535
536 // tag stack
537 $stack = array();
538
539 // the information about the current active tag
540 $tag_full = array();
541 $parsed = array();
542
543 // start at 0
544 $offset = 0;
545
546 while (1)
547 {
548 if (strpos($template, $tag_start) === false)
549 {
550 break;
551 }
552
553 for ($i = $offset; $i < strlen($template); $i++)
554 {
555 // we've found ourselves a conditional!
556 if (substr($template, $i, strlen($tag_start)) == $tag_start)
557 {
558 // push the position into the tag stack
559 if ($tag_full)
560 {
561 array_push($stack, $i);
562 }
563 else
564 {
565 $tag_full['posi'] = $i;
566 }
567 }
568 // locate else tags
569 else if (substr($template, $i, strlen($tag_else)) == $tag_else)
570 {
571 if (sizeof($stack) == 0 && !isset($tag_full['else']))
572 {
573 $tag_full['else'] = $i;
574 }
575 }
576 // do we have an end tag?
577 else if (substr($template, $i, strlen($tag_end)) == $tag_end)
578 {
579 if (sizeof($stack) != 0)
580 {
581 array_pop($stack);
582 continue;
583 }
584
585 // calculate the position of the end tag
586 $tag_full['posf'] = $i + strlen($tag_end) - 1;
587
588 // extract the entire conditional from the template
589 $fullspread = substr($template, $tag_full['posi'], $tag_full['posf'] - $tag_full['posi'] + 1);
590
591 // remove the beginning tag
592 $conditional = substr($fullspread, strlen($tag_start));
593
594 // find the end of the expression
595 $temp_end = strpos($conditional, $tag_start_end);
596
597 // save the expression
598 $parsed[0] = stripslashes(substr($conditional, 0, $temp_end));
599
600 // remove the expression from the conditional
601 $conditional = substr($conditional, strlen($parsed[0]) + strlen($tag_start_end));
602
603 // remove the tailing end tag
604 $conditional = substr($conditional, 0, strlen($conditional) - strlen($tag_end));
605
606 // handle the else
607 if (isset($tag_full['else']))
608 {
609 // now relative to the start of the <if>
610 $relpos = $tag_full['else'] - $tag_full['posi'];
611
612 // calculate the length of the expression and opening tag
613 $length = strlen($parsed[0]) + strlen($tag_start) + strlen($tag_start_end);
614
615 // relative to the start of iftrue
616 $elsepos = $relpos - $length;
617
618 $parsed[1] = substr($conditional, 0, $elsepos);
619 $parsed[2] = substr($conditional, $elsepos + strlen($tag_else));
620 }
621 // no else to handle
622 else
623 {
624 $parsed[1] = $conditional;
625 $parsed[2] = '';
626 }
627
628 // final parsed output
629 $parsed = '" . ((' . stripslashes($parsed[0]) . ') ? "' . $parsed[1] . '" : "' . $parsed[2] . '") . "';
630
631 // replace the conditional
632 $template = str_replace($fullspread, $parsed, $template);
633
634 // reset the parser
635 $offset = $tag_full['posi'] + strlen($tag_start) + strlen($tag_start_end);
636 $tag_full = array();
637 $stack = array();
638 $parsed = array();
639 unset($fullspread, $conditional, $temp_end, $relpos, $length, $elsepos);
640 break;
641 }
642 }
643 }
644
645 return $template;
646 }
647 }
648
649 ?>