2 /*=====================================================================*\
3 || ###################################################################
4 || # Blue Static ISSO Framework
5 || # Copyright ©2002-[#]year[#] 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 [#]gpl[#] 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 ©2002 - [#]year[#], Blue Static
50 * The path, from the path of the application, where templates are stored
53 private $templateDir = '';
56 * The extension all the template files have
59 private $extension = 'tpl';
62 * The database table name for the template cache
65 private $dbCacheTable = null
;
68 * The name of the function phrases are fetched with
71 private $langcall = 'gettext';
74 * The name of the function phrases are sprintf() parsed with
77 private $langconst = 'sprintf';
80 * Array of pre-compiled templates that are stored to decrease server load
83 protected $cache = array();
86 * A list of the number of times each template has been used
89 protected $usage = array();
92 * A list of templates that weren't cached, but are still used
95 protected $uncached = array();
98 * Whether or not the page has been flush()'d already
101 private $doneflush = false
;
104 * The name of a function that is called before template parsing of phrases and conditionals occurs
107 private $preParseHook = ':undefined:';
109 // ###################################################################
111 * Sets the template directory name
113 * @param string Template directory name
115 public function setTemplateDirectory($dir)
117 $this->templateDir
= BSFunctions
::FetchSourcePath($dir);
120 // ###################################################################
122 * Sets the file extension for the templates
124 * @param string File extension
126 public function setExtension($ext)
128 $this->extension
= $ext;
131 // ###################################################################
133 * Sets the name of the table to access for the datbase cache
135 * @param string DB table name
137 public function setDatabaseCache($table)
139 $this->dbCacheTable
= $table;
142 // ###################################################################
144 * Sets the pre-parse hook method which is called before any other
145 * processing is done on the template.
147 * @param string Method name
149 public function setPreParseHook($hook)
151 $this->preParseHook
= $hook;
154 // ###################################################################
156 * Takes an array of template names, loads them, and then stores a
157 * parsed version for optimum speed.
159 * @param array List of template names to be cached
161 public function cache($namearray)
163 if (sizeof($this->cache
) > 0)
165 trigger_error('You cannot cache templates more than once per initialization');
170 if ($this->dbCacheTable
)
172 $db =& BSRegister
::GetType('Db');
173 $cache = $db->query("SELECT * FROM {$this->dbCacheTable} WHERE filename IN ('" . implode("', '", $namearray) . "')");
174 while ($tpl = $db->fetchArray($cache))
176 $time = filemtime(BSRegister
::GetAppPath() . $this->templateDir
. $tpl['filename'] . '.' . $this->extension
);
177 $template = $tpl['template'];
178 if ($time > $tpl['timestamp'])
180 $template = $this->_parseTemplate($this->_loadTemplate($tpl['filename']));
181 $db->query("UPDATE {$this->dbCacheTable} SET template = '" . $db->escapeString($template) . "', timestamp = " . TIMENOW
. " WHERE filename = '" . $tpl['filename'] . "'");
182 $tpl['template'] = $template;
184 $dbCache["$tpl[filename]"] = $template;
187 foreach ($namearray AS $name)
189 if ($this->dbCacheTable
)
191 if (isset($dbCache["$name"]))
193 $template = $dbCache["$name"];
197 $template = $this->_parseTemplate($this->_loadTemplate($name));
198 $db->query("INSERT INTO {$this->dbCacheTable} (filename, template, timestamp) VALUES ('$name', '" . $db->escapeString($template) . "', " . TIMENOW
. ")");
203 $template = $this->_parseTemplate($this->_loadTemplate($name));
206 $this->cache
["$name"] = $template;
207 $this->usage
["$name"] = 0;
212 // ###################################################################
214 * Loads a template from the cache or the _load function and stores the
215 * parsed version of it
217 * @param string The name of the template
219 * @return string A parsed and loaded template
221 public function fetch($name)
223 if (isset($this->cache
["$name"]))
225 $template = $this->cache
["$name"];
229 $this->uncached
[] = $name;
230 BSRegister
::Debug("Manually loading template '$name'");
231 $template = $this->_loadTemplate($name);
232 $template = $this->_parseTemplate($template);
235 if (!isset($this->usage
["$name"]))
237 $this->usage
["$name"] = 0;
240 $this->usage
["$name"]++
;
245 // ###################################################################
247 * Output a template fully compiled to the browser
249 * @param string Compiled and ready template
251 public function flush($template)
255 if (empty($template))
257 trigger_error('There was no output to print');
261 if ($this->doneflush
)
263 trigger_error('A template has already been sent to the output buffer');
268 if (BSRegister
::GetDebug())
270 if (defined('SVN') AND preg_match('#^\$Id:?#', constant('SVN')))
272 $debugBlock .= preg_replace('#\$' . 'Id: (.+?) ([0-9].+?) [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(.+?) (.+?) \$#', "\n<br />\n" . '<div align="center"><strong>\1</strong> — r\2</div>', constant('SVN'));
275 if (defined('ISSO_MT_START'))
277 $debugBlock .= "\n<div align=\"center\">Executed in " . round(BSFunctions
::FetchMicrotimeDiff(ISSO_MT_START
), 10) . ' seconds</div>';
280 $debugBlock .= "\n<br /><div align=\"center\">" . BSRegister
::GetDebugList() . "</div>";
284 foreach ($this->usage
AS $name => $count)
286 if (in_array($name, $this->uncached
))
288 $optlist[] = $name . '[' . $count . ']';
290 $usage[] = $name . " ($count)";
292 $sizeof = sizeof($this->uncached
);
295 $debugBlock .= "<br /><div style=\"color: red\" align=\"center\"><strong>Uncached Template(s):</strong> $sizeof ( " . implode(' ', $optlist) . " )</div>\n";
298 $debugBlock .= (sizeof($this->uncached
) < 1 ?
"<br />\n" : '') . "<div align=\"center\"><select><option>Template Usage (" . array_sum($this->usage
) . ")</option>";
299 foreach ($usage AS $tpl)
301 $debugBlock .= "<option>--- $tpl</option>";
303 $debugBlock .= "</select></div>\n";
305 if (BSRegister
::GetType('Db'))
307 $queries = BSRegister
::GetType('Db')->getHistory();
309 $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>';
311 foreach ($queries AS $query)
313 $debugBlock .= "\n\t<tr style=\"background-color: rgb(230, 230, 230); color: black\">";
314 $debugBlock .= "\n\t\t<td>";
315 $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>";
318 $debugBlock .= "\n</table>\n\n\n";
322 $template = str_replace('</body>', $debugBlock . '</body>', $template);
327 // ###################################################################
329 * Loads an additional template from the database
331 * @param string The name of the template
333 * @return string Template data from the database
335 protected function _loadTemplate($name)
337 $path = BSRegister
::GetAppPath() . $this->templateDir
. $name . '.' . $this->extension
;
340 if (($template = @file_get_contents($path)) !== false
)
346 trigger_error("Could not load the template '$path'");
352 trigger_error("Could not load the template '$path'");
357 // ###################################################################
359 * A wrapper for all the parsing functions and compiling functins
361 * @param string Unparsed template data
363 * @return string Parsed template data
365 protected function _parseTemplate($template)
367 $template = str_replace('"', '\"', $template);
369 if (function_exists($this->preParseHook
))
371 $template = call_user_func($this->preParseHook
, $template);
374 $template = $this->_parseBlocksAndTokens($template);
375 $template = $this->_parsePhrases($template);
376 $template = $this->_parseConditionals($template);
380 // ###################################################################
382 * Parses anything with curly braces {} (including phrases)
384 * @param string Template data
386 * @return string Parsed template data
388 private function _parseBlocksAndTokens($template)
395 for ($i = 0; $i < strlen($template); $i++
)
397 // we've run through the template and there's nothing in the stack--done
398 if ($i == strlen($template) - 1 AND sizeof($stack) == 0)
403 if ($template[$i] == '{')
405 // ignore escaped sequences
406 if ($template[$i - 1] != '\\')
408 array_push($stack, $i);
411 else if ($template[$i] == '}')
413 // there's no stack so it was probably escaped
414 if (sizeof($stack) == 0)
418 // we're good and nested
419 else if (sizeof($stack) == 1)
421 $open = array_pop($stack);
422 $token = substr($template, $open, $i - $open +
1);
423 $template = str_replace($token, $this->_parseToken($token), $template);
436 // ###################################################################
438 * Parses a curly brace token {}
440 * @param string Token
442 * @return string Parsed value
444 private function _parseToken($token)
446 // knock of the braces
447 $token = substr($token, 1, strlen($token) - 2);
450 if ($token[0] == '@' AND $token[1] == '\\' AND $token[2] == '"')
452 return '" . ' . $this->langcall
. '(\'' . str_replace(array('\\\"', "'"), array('"', "\'"), substr($token, 3, strlen($token) - 5)) . '\')' . ' . "';
457 return '" . (' . $token . ') . "';
461 // ###################################################################
463 * Prepares language and locale information inside templates
465 * @param string Template data to be processed
467 * @return string Language-ready template data
469 private function _parsePhrases($template)
471 $tagStart = '<lang ';
474 $start = -1; // start of open tag
475 $end = -1; // start of the close tag
476 $varEnd = -1; // end of the open tag
478 while ($start <= strlen($template))
481 $varMap = array(); // storage for all the substitution indices
483 // Find the start language object tag
484 $start = strpos($template, $tagStart, $end +
1);
485 if ($start === false
)
490 // look ahead to parse out all the variables
491 $i = $start +
strlen($tagStart); // current position
492 $capture = ''; // current capture
493 $capturePos = $i; // the place to start capturing
494 $varNum = -1; // variable placeholder index
495 while ($i < strlen($template))
497 if ($template[$i] == '=')
499 // backtrack to find the previous variable substitution
501 while ($backPos >= $start)
503 if ($template[$backPos] == '"')
505 // startPosition + length(startTag) + length(=\")
506 $varMap[intval($varNum)] = BSFunctions
::Substring($template, $capturePos +
3, $backPos - 1);
507 // remove our old substitution from the capture
508 $capture = BSFunctions
::Substring($template, $backPos +
1, $i);
514 // do we have a valid index?
515 if (intval($capture) > 0)
517 // set aside the index and restart capturing
524 trigger_error('Invalid language variable index "' . $capture . '"');
527 else if ($template[$i] == '>' AND $template[$i - 1] == '"')
529 // the final variable substitution
530 $varMap[intval($varNum)] = BSFunctions
::Substring($template, $capturePos +
3, $i - 2);
535 $capture .= $template[$i];
539 // locate the end tag
540 $end = strpos($template, $tagEnd, $i);
546 // this is the string that gets variable replacement
547 $str = BSFunctions
::Substring($template, $varEnds +
1, $end);
549 // create the complete varmap
551 for ($i = max(array_keys($varMap)); $i > 0; $i--)
553 if (!isset($varMap[$i]))
555 $varMap[$i] = '<strong>[MISSING SUBSTITUTION INDEX: ' . $i . ']</strong>';
559 // put all the keys in corresponding argument order
562 // FINALLY, construct the call to sprintf()
563 $template = substr_replace($template, '" . ' . $this->langconst
. '(\'' . $str . '\', "' . implode('", "', $varMap) . '") . "', $start, ($end +
strlen($tagEnd)) - $start);
569 // ###################################################################
571 * Parser for in-line template conditionals
573 * @param string Template data awaiting processing
575 * @return string Parsed template data
577 private function _parseConditionals($template)
580 $tag_start = '<if condition=\"';
581 $tag_start_end = '\">';
582 $tag_else = '<else />';
588 // the information about the current active tag
597 if (strpos($template, $tag_start) === false
)
602 for ($i = $offset; $i < strlen($template); $i++
)
604 // we've found ourselves a conditional!
605 if (substr($template, $i, strlen($tag_start)) == $tag_start)
607 // push the position into the tag stack
610 array_push($stack, $i);
614 $tag_full['posi'] = $i;
618 else if (substr($template, $i, strlen($tag_else)) == $tag_else)
620 if (sizeof($stack) == 0 AND !isset($tag_full['else']))
622 $tag_full['else'] = $i;
625 // do we have an end tag?
626 else if (substr($template, $i, strlen($tag_end)) == $tag_end)
628 if (sizeof($stack) != 0)
634 // calculate the position of the end tag
635 $tag_full['posf'] = $i +
strlen($tag_end) - 1;
637 // extract the entire conditional from the template
638 $fullspread = substr($template, $tag_full['posi'], $tag_full['posf'] - $tag_full['posi'] +
1);
640 // remove the beginning tag
641 $conditional = substr($fullspread, strlen($tag_start));
643 // find the end of the expression
644 $temp_end = strpos($conditional, $tag_start_end);
646 // save the expression
647 $parsed[0] = stripslashes(substr($conditional, 0, $temp_end));
649 // remove the expression from the conditional
650 $conditional = substr($conditional, strlen($parsed[0]) +
strlen($tag_start_end));
652 // remove the tailing end tag
653 $conditional = substr($conditional, 0, strlen($conditional) - strlen($tag_end));
656 if (isset($tag_full['else']))
658 // now relative to the start of the <if>
659 $relpos = $tag_full['else'] - $tag_full['posi'];
661 // calculate the length of the expression and opening tag
662 $length = strlen($parsed[0]) +
strlen($tag_start) +
strlen($tag_start_end);
664 // relative to the start of iftrue
665 $elsepos = $relpos - $length;
667 $parsed[1] = substr($conditional, 0, $elsepos);
668 $parsed[2] = substr($conditional, $elsepos +
strlen($tag_else));
673 $parsed[1] = $conditional;
677 // final parsed output
678 $parsed = '" . ((' . stripslashes($parsed[0]) . ') ? "' . $parsed[1] . '" : "' . $parsed[2] . '") . "';
680 // replace the conditional
681 $template = str_replace($fullspread, $parsed, $template);
684 $offset = $tag_full['posi'] +
strlen($tag_start) +
strlen($tag_start_end);
688 unset($fullspread, $conditional, $temp_end, $relpos, $length, $elsepos);
698 // ###################################################################
700 * Debugging function used to print characters in a string that are
701 * around a certain position.
705 * @param string The haystack string
706 * @param integer Position to print around
708 function print_around($str, $pos)
710 echo '>>> PA >>>>>>>>[';
711 echo htmlspecialchars($str[ $pos - 5 ]);
712 echo htmlspecialchars($str[ $pos - 4 ]);
713 echo htmlspecialchars($str[ $pos - 3 ]);
714 echo htmlspecialchars($str[ $pos - 2 ]);
715 echo htmlspecialchars($str[ $pos - 1 ]);
717 echo htmlspecialchars($str[ $pos +
0 ]);
718 echo htmlspecialchars($str[ $pos +
1 ]);
719 echo htmlspecialchars($str[ $pos +
2 ]);
720 echo htmlspecialchars($str[ $pos +
3 ]);
721 echo htmlspecialchars($str[ $pos +
4 ]);
722 echo htmlspecialchars($str[ $pos +
5 ]);
723 echo ']<<<<<<<< PA <<<';
726 /*=====================================================================*\
727 || ###################################################################
730 || ###################################################################
731 \*=====================================================================*/