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