2 /*=====================================================================*\
3 || ###################################################################
4 || # Blue Static ISSO Framework
5 || # Copyright (c)2002-2007 Blue Static
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.
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
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 \*=====================================================================*/
23 * Database-Driven Template System (template.php)
28 require_once('ISSO/Functions.php');
31 * File-Based Template System
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:
39 * CREATE TABLE template (filename VARCHAR (250) NOT NULL, template TEXT NOT NULL, timestamp INT NOT NULL);
42 * @copyright Copyright (c)2002 - 2007, Blue Static
49 * The path, from the path of the application, where templates are stored
52 private $templateDir = '';
55 * The extension all the template files have
58 private $extension = 'tpl';
61 * The database table name for the template cache
64 private $dbCacheTable = null;
67 * The name of the function phrases are fetched with
70 private $langcall = 'gettext';
73 * The name of the function phrases are sprintf() parsed with
76 private $langconst = 'sprintf';
79 * Array of pre-compiled templates that are stored to decrease server load
82 protected $cache = array();
85 * A list of templates that weren't cached, but are still used
88 protected $uncached = array();
91 * Whether or not the page has been flush()'d already
94 private $doneflush = false;
97 * The name of a function that is called before template parsing of phrases and conditionals occurs
100 private $preParseHook = ':undefined:';
102 // ###################################################################
104 * Sets the template directory name
106 * @param string Template directory name
108 public function setTemplateDirectory($dir)
110 $this->templateDir
= BSFunctions
::FetchSourcePath($dir);
113 // ###################################################################
115 * Sets the file extension for the templates
117 * @param string File extension
119 public function setExtension($ext)
121 $this->extension
= $ext;
124 // ###################################################################
126 * Sets the name of the table to access for the datbase cache
128 * @param string DB table name
130 public function setDatabaseCache($table)
132 $this->dbCacheTable
= $table;
135 // ###################################################################
137 * Sets the pre-parse hook method which is called before any other
138 * processing is done on the template.
140 * @param string Method name
142 public function setPreParseHook($hook)
144 $this->preParseHook
= $hook;
147 // ###################################################################
149 * Takes an array of template names, loads them, and then stores a
150 * parsed version for optimum speed.
152 * @param array List of template names to be cached
154 public function cache($namearray)
156 if (sizeof($this->cache
) > 0)
158 throw new Exception('You cannot cache templates more than once per initialization');
163 if ($this->dbCacheTable
)
165 $db =& BSApp
::Registry()->getType('Db');
166 $cache = $db->query("SELECT * FROM {$this->dbCacheTable} WHERE filename IN ('" . implode("', '", $namearray) . "')");
167 while ($tpl = $db->fetchArray($cache))
169 $time = filemtime($this->templateDir
. $tpl['filename'] . '.' . $this->extension
);
170 $template = $tpl['template'];
171 if ($time > $tpl['timestamp'])
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;
177 $dbCache["$tpl[filename]"] = $template;
180 foreach ($namearray AS $name)
182 if ($this->dbCacheTable
)
184 if (isset($dbCache["$name"]))
186 $template = $dbCache["$name"];
190 $template = $this->_parseTemplate($this->_loadTemplate($name));
191 $db->query("INSERT INTO {$this->dbCacheTable} (filename, template, timestamp) VALUES ('$name', '" . $db->escapeString($template) . "', " . TIMENOW
. ")");
196 $template = $this->_parseTemplate($this->_loadTemplate($name));
199 $this->cache
[$name] = $template;
204 // ###################################################################
206 * Loads a template from the cache or the _load function and stores the
207 * parsed version of it
209 * @param string The name of the template
211 * @return string A parsed and loaded template
213 public function fetch($name)
215 if (isset($this->cache
[$name]))
217 $template = $this->cache
[$name];
221 $this->uncached
[$name]++
;
222 BSApp
::Debug("Manually loading template '$name'");
223 $template = $this->_loadTemplate($name);
224 $template = $this->_parseTemplate($template);
230 // ###################################################################
232 * Output a template fully compiled to the browser
234 * @param string Compiled and ready template
236 public function flush($template)
240 if (empty($template))
242 throw new Exception('There was no output to print');
245 if ($this->doneflush)
247 throw new Exception('A template has already been sent to the output buffer');
251 if (BSApp::GetDebug())
253 $debugBlock .= "\n
<div align
=\"center\"
>Executed in
" . round(BSFunctions::FetchMicrotimeDiff($_SERVER['REQUEST_TIME']), 10) . ' seconds</div>';
254 $debugBlock .= "\n
<br
/><div align
=\"center\"
>" . BSApp::GetDebugList() . "</div
>";
256 if (sizeof($this->uncached) > 0)
258 foreach ($this->uncached AS $name => $count)
260 $tpls[] = $name . "($count)";
262 $debugBlock .= "<br
/><div style
=\"color
: red\" align
=\"center\"
><strong
>Uncached Templates
:</strong
>" . implode(', ', $tpls) . " )</div
>\n";
265 if (BSApp::Registry()->getType('Db'))
267 $queries = BSApp::Registry()->getType('Db')->getHistory();
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>';
271 foreach ($queries AS $query)
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
>";
278 $debugBlock .= "\n
</table
>\n\n\n";
281 $template = str_replace('</body>', $debugBlock . '</body>', $template);
287 // ###################################################################
289 * Loads an additional template from the database
291 * @param string The name of the template
293 * @return string Template data from the database
295 protected function _loadTemplate($name)
297 $path = $this->templateDir . $name . '.' . $this->extension;
298 if (is_file($path) AND is_readable($path))
300 return @file_get_contents($path);
304 throw new Exception("Could not load the template
'$path'");
308 // ###################################################################
310 * A wrapper for all the parsing functions and compiling functins
312 * @param string Unparsed template data
314 * @return string Parsed template data
316 protected function _parseTemplate($template)
318 $template = str_replace('"', '\"', $template);
320 if (function_exists($this->preParseHook))
322 $template = call_user_func($this->preParseHook, $template);
325 $template = $this->_parseBlocksAndTokens($template);
326 $template = $this->_parsePhrases($template);
327 $template = $this->_parseConditionals($template);
331 // ###################################################################
333 * Parses anything with curly braces {} (including phrases)
335 * @param string Template data
337 * @return string Parsed template data
339 private function _parseBlocksAndTokens($template)
346 for ($i = 0; $i < strlen($template); $i++)
348 // we've run through the template and there's nothing in the stack--done
349 if ($i == strlen($template) - 1 AND sizeof($stack) == 0)
354 if ($template[$i] == '{')
356 // ignore escaped sequences
357 if ($template[$i - 1] != '\\')
359 array_push($stack, $i);
362 else if ($template[$i] == '}')
364 // there's no stack so it was probably escaped
365 if (sizeof($stack) == 0)
369 // we're good and nested
370 else if (sizeof($stack) == 1)
372 $open = array_pop($stack);
373 $token = substr($template, $open, $i - $open + 1);
374 $template = str_replace($token, $this->_parseToken($token), $template);
387 // ###################################################################
389 * Parses a curly brace token {}
391 * @param string Token
393 * @return string Parsed value
395 private function _parseToken($token)
397 // knock of the braces
398 $token = substr($token, 1, strlen($token) - 2);
401 if ($token[0] == '@' AND $token[1] == '\\' AND $token[2] == '"')
403 return '" . ' . $this->langcall . '(\'' . str_replace(array('\\\"', "'"), array('"', "\'
"), substr($token, 3, strlen($token) - 5)) . '\')' . ' . "';
408 return '" . (' . $token . ') . "';
412 // ###################################################################
414 * Prepares language and locale information inside templates
416 * @param string Template data to be processed
418 * @return string Language-ready template data
420 private function _parsePhrases($template)
422 $tagStart = '<lang
';
425 $start = -1; // start of open tag
426 $end = -1; // start of the close tag
427 $varEnd = -1; // end of the open tag
429 while ($start <= strlen($template))
432 $varMap = array(); // storage for all the substitution indices
434 // Find the start language object tag
435 $start = strpos($template, $tagStart, $end + 1);
436 if ($start === false)
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))
448 if ($template[$i] == '=')
450 // backtrack to find the previous variable substitution
452 while ($backPos >= $start)
454 if ($template[$backPos] == '"')
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);
465 // do we have a valid index?
466 if (intval($capture) > 0)
468 // set aside the index and restart capturing
475 throw new Exception('Invalid language variable index "' . $capture . '"');
478 else if ($template[$i] == '>' AND $template[$i - 1] == '"')
480 // the final variable substitution
481 $varMap[intval($varNum)] = BSFunctions::Substring($template, $capturePos + 3, $i - 2);
486 $capture .= $template[$i];
490 // locate the end tag
491 $end = strpos($template, $tagEnd, $i);
497 // this is the string that gets variable replacement
498 $str = BSFunctions::Substring($template, $varEnds + 1, $end);
500 // create the complete varmap
502 for ($i = max(array_keys($varMap)); $i > 0; $i--)
504 if (!isset($varMap[$i]))
506 $varMap[$i] = '<strong
>[MISSING SUBSTITUTION INDEX
: ' . $i . ']</strong
>';
510 // put all the keys in corresponding argument order
513 // FINALLY, construct the call to sprintf()
514 $template = substr_replace($template, '" . ' . $this->langconst . '(\'' . $str . '\', "' . implode('", "', $varMap) . '") . "', $start, ($end + strlen($tagEnd)) - $start);
520 // ###################################################################
522 * Parser for in-line template conditionals
524 * @param string Template data awaiting processing
526 * @return string Parsed template data
528 private function _parseConditionals($template)
531 $tag_start = '<if condition
=\"';
532 $tag_start_end = '\"
>';
533 $tag_else = '<else />';
539 // the information about the current active tag
548 if (strpos($template, $tag_start) === false)
553 for ($i = $offset; $i < strlen($template); $i++)
555 // we've found ourselves a conditional
!
556 if (substr($template, $i, strlen($tag_start)) == $tag_start)
558 // push the position into the tag stack
561 array_push($stack, $i);
565 $tag_full['posi'] = $i;
569 else if (substr($template, $i, strlen($tag_else)) == $tag_else)
571 if (sizeof($stack) == 0 AND !isset($tag_full['else']))
573 $tag_full['else'] = $i;
576 // do we have an end tag?
577 else if (substr($template, $i, strlen($tag_end)) == $tag_end)
579 if (sizeof($stack) != 0)
585 // calculate the position of the end tag
586 $tag_full['posf'] = $i +
strlen($tag_end) - 1;
588 // extract the entire conditional from the template
589 $fullspread = substr($template, $tag_full['posi'], $tag_full['posf'] - $tag_full['posi'] +
1);
591 // remove the beginning tag
592 $conditional = substr($fullspread, strlen($tag_start));
594 // find the end of the expression
595 $temp_end = strpos($conditional, $tag_start_end);
597 // save the expression
598 $parsed[0] = stripslashes(substr($conditional, 0, $temp_end));
600 // remove the expression from the conditional
601 $conditional = substr($conditional, strlen($parsed[0]) +
strlen($tag_start_end));
603 // remove the tailing end tag
604 $conditional = substr($conditional, 0, strlen($conditional) - strlen($tag_end));
607 if (isset($tag_full['else']))
609 // now relative to the start of the <if>
610 $relpos = $tag_full['else'] - $tag_full['posi'];
612 // calculate the length of the expression and opening tag
613 $length = strlen($parsed[0]) +
strlen($tag_start) +
strlen($tag_start_end);
615 // relative to the start of iftrue
616 $elsepos = $relpos - $length;
618 $parsed[1] = substr($conditional, 0, $elsepos);
619 $parsed[2] = substr($conditional, $elsepos +
strlen($tag_else));
624 $parsed[1] = $conditional;
628 // final parsed output
629 $parsed = '" . ((' . stripslashes($parsed[0]) . ') ? "' . $parsed[1] . '" : "' . $parsed[2] . '") . "';
631 // replace the conditional
632 $template = str_replace($fullspread, $parsed, $template);
635 $offset = $tag_full['posi'] +
strlen($tag_start) +
strlen($tag_start_end);
639 unset($fullspread, $conditional, $temp_end, $relpos, $length, $elsepos);