Give all the instance variables visibility tags
[isso.git] / template.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Iris Studios Shared Object Framework [#]version[#]
5 || # Copyright ©2002-[#]year[#] Iris Studios, Inc.
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
24 * template.php
25 *
26 * @package ISSO
27 */
28
29 /**
30 * Database-Driven Template System
31 *
32 * This framework is a backend to the database template engine and
33 * contains all the parsing algorithms.
34 *
35 * Hooks:
36 * $this->pre_parse_hook - Name of the function to execute on
37 * a template before the parsing occurs
38 *
39 * @author Iris Studios, Inc.
40 * @copyright Copyright ©2002 - [#]year[#], Iris Studios, Inc.
41 * @version $Revision$
42 * @package ISSO
43 *
44 */
45 class Template
46 {
47 /**
48 * Framework registry object
49 * @var object
50 * @access private
51 */
52 var $registry = null;
53
54 /**
55 * Name of the database table templates are in
56 * @var string
57 * @access public
58 */
59 var $tablename = '';
60
61 /**
62 * Name of the table column template names are in
63 * @var string
64 * @access public
65 */
66 var $namecolumn = '';
67
68 /**
69 * Name of the table column templates are in
70 * @var string
71 * @access public
72 */
73 var $datacolumn = '';
74
75 /**
76 * Additional WHERE clauses for the template fetch SQL
77 * @var string
78 * @access public
79 */
80 var $extrawhere = '';
81
82 /**
83 * The name of the function phrases are fetched with
84 * @var string
85 * @access public
86 */
87 var $langcall = '$GLOBALS[\'isso:null-framework\']->modules[\'localize\']->string';
88
89 /**
90 * The name of the function phrases are sprintf() parsed with
91 * @var string
92 * @access public
93 */
94 var $langconst = 'sprintf';
95
96 /**
97 * Array of pre-compiled templates that are stored to decrease server load
98 * @var array
99 * @access private
100 */
101 var $cache = array();
102
103 /**
104 * A list of the number of times each template has been used
105 * @var array
106 * @access private
107 */
108 var $usage = array();
109
110 /**
111 * A list of templates that weren't cached, but are still used
112 * @var array
113 * @access private
114 */
115 var $uncached = array();
116
117 /**
118 * Whether or not the page has been flush()'d already
119 * @var bool
120 * @access private
121 */
122 var $doneflush = false;
123
124 /**
125 * The name of a function that is called before template parsing of phrases and conditionals occurs
126 * @var string
127 * @access public
128 */
129 var $pre_parse_hook = ':=NO METHOD=:';
130
131 // ###################################################################
132 /**
133 * Constructor
134 */
135 function __construct(&$registry)
136 {
137 $this->registry =& $registry;
138 }
139
140 // ###################################################################
141 /**
142 * (PHP 4) Constructor
143 */
144 function Template(&$registry)
145 {
146 $this->__construct($registry);
147 }
148
149 // ###################################################################
150 /**
151 * Takes an array of template names, loads them, and then stores a
152 * parsed version for optimum speed.
153 *
154 * @access public
155 *
156 * @param array List of template names to be cached
157 */
158 function cache($namearray)
159 {
160 if (sizeof($this->cache) > 0)
161 {
162 trigger_error('You cannot cache templates more than once per initialization', E_USER_WARNING);
163 }
164 else
165 {
166 $templates = $this->registry->modules[ISSO_DB_LAYER]->query("SELECT * FROM " . $this->tablename . " WHERE " . $this->namecolumn . " IN ('" . implode("', '", $namearray) . "')" . ($this->extrawhere ? $this->extrawhere : ''));
167 while ($template = $this->registry->modules[ISSO_DB_LAYER]->fetch_array($templates))
168 {
169 $this->cache[ $template[ $this->namecolumn ] ] = $this->_parse($template[ $this->datacolumn ]);
170 $this->usage[ $template[ $this->namecolumn ] ] = 0;
171 }
172 }
173 }
174
175 // ###################################################################
176 /**
177 * Loads a template from the cache or the _load function and stores the
178 * parsed version of it
179 *
180 * @access public
181 *
182 * @param string The name of the template
183 *
184 * @return string A parsed and loaded template
185 */
186 function fetch($name)
187 {
188 if (isset($this->cache["$name"]))
189 {
190 $template = $this->cache["$name"];
191 }
192 else
193 {
194 $this->uncached[] = $name;
195 $this->registry->debug("Manually loading template `$name`");
196 $template = $this->_load($name);
197 $template = $this->_parse($template);
198 }
199
200 if (!isset($this->usage["$name"]))
201 {
202 $this->usage["$name"] = 0;
203 }
204
205 $this->usage["$name"]++;
206
207 return $template;
208 }
209
210 // ###################################################################
211 /**
212 * Output a template fully compiled to the browser
213 *
214 * @access public
215 *
216 * @param string Compiled and ready template
217 */
218 function flush($template)
219 {
220 ob_start();
221
222 if (empty($template))
223 {
224 trigger_error('There was no output to print', E_USER_ERROR);
225 exit;
226 }
227
228 if ($this->registry->debug AND isset($_GET['query']))
229 {
230 if (is_array($this->registry->modules[ISSO_DB_LAYER]->history))
231 {
232 echo '<pre>';
233 foreach ($this->registry->modules[ISSO_DB_LAYER]->history AS $query)
234 {
235 echo $query . "\n\n<hr />\n\n";
236 }
237 echo '</pre>';
238 }
239 exit;
240 }
241
242 if ($this->doneflush)
243 {
244 trigger_error('A template has already been sent to the output buffer', E_USER_ERROR);
245 exit;
246 }
247
248 $template = str_replace('</body>', $this->registry->construct_debug_block(true) . '</body>', $template);
249
250 print($template);
251 }
252
253 // ###################################################################
254 /**
255 * Loads an additional template from the database
256 *
257 * @access private
258 *
259 * @param string The name of the template
260 *
261 * @return string Template data from the database
262 */
263 function _load($name)
264 {
265 if ($template = $this->registry->modules[ISSO_DB_LAYER]->query("SELECT * FROM " . $this->tablename . " WHERE " . $this->namecolumn . " = '$name'" . ($this->extrawhere ? $this->extrawhere : '')))
266 {
267 return $template[ $this->datacolumn ];
268 }
269 else
270 {
271 trigger_error("The template '$name' could not be loaded", E_USER_ERROR);
272 exit;
273 }
274 }
275
276 // ###################################################################
277 /**
278 * A wrapper for all the parsing functions and compiling functins
279 *
280 * @access protected
281 *
282 * @param string Unparsed template data
283 *
284 * @return string Parsed template data
285 */
286 function _parse($template)
287 {
288 $template = str_replace('"', '\"', $template);
289
290 if (function_exists($this->pre_parse_hook))
291 {
292 $this->registry->debug("running pre-parse hook: {$this->pre_parse_hook}");
293 $template = call_user_func($this->pre_parse_hook, $template);
294 }
295
296 $template = $this->_parse_phrases($template);
297 $template = $this->_parse_conditionals($template);
298 return $template;
299 }
300
301 // ###################################################################
302 /**
303 * Prepares language and locale information inside templates
304 *
305 * @access private
306 *
307 * @param string Template data to be processed
308 *
309 * @return string Language-ready template data
310 */
311 function _parse_phrases($template)
312 {
313 $tag_start = '<lang ';
314 $tag_start_end = '\">';
315 $tag_end = '</lang>';
316
317 $location_start = -1;
318 $location_end = -1;
319
320 // Process the empty phrase objects -- do this now so we don't have to worry about it when we're parsing later
321 $template = preg_replace('#\{@\\\"(.*?)\\\"\}#ie', '$this->_phrase_string(\'$1\')', $template);
322
323 while (1)
324 {
325 // Find the start language object tag
326 $location_start = strpos($template, $tag_start, $location_end + 1);
327 if ($location_start === false)
328 {
329 break;
330 }
331
332 // Find the end tag
333 $location_end = strpos($template, $tag_end, $location_end + strlen($tag_end));
334 if ($location_end === false)
335 {
336 break;
337 }
338
339 // Extract the language object
340 $phrase_bunch = substr($template, $location_start, ($location_end + strlen($tag_end)) - $location_start);
341
342 // Find the close to the opening <lang>
343 $close_of_open = strpos($phrase_bunch, $tag_start_end);
344 if ($close_of_open === false)
345 {
346 break;
347 }
348
349 // Extract the opening tag so it can be parsed
350 $init_tag = substr($phrase_bunch, 0, ($close_of_open + strlen($tag_start_end)));
351 $init_tag = str_replace($tag_start, '', $init_tag);
352 $init_tag = substr($init_tag, 0, strlen($init_tag) - 1);
353
354 // Get the args out of the tag
355 $args = preg_split('#([0-9].*?)=#', $init_tag);
356 foreach ($args AS $arg)
357 {
358 if ($arg AND $arg != ' ')
359 {
360 $arg = trim($arg);
361 $arg = substr($arg, 2);
362 $arg = substr($arg, 0, strlen($arg) - 2);
363 $arglist[] = $arg;
364 }
365 }
366
367 // Just get the phrase name
368 $phrase_name = preg_replace('#<lang(.*?)>(.*?)</lang>#i', '$2', $phrase_bunch);
369
370 // Wrap the parsed data into the build function
371 $function_wrap = '" . ' . $this->langconst . '("' . /*str_replace(array('\"', "'"), array('"', "\'"),*/ $phrase_name/*)*/ . '", "' . implode('", "', $arglist) . '") . "';
372
373 // Replace the fully-parsed string back into the template
374 $template = substr_replace($template, $function_wrap, $location_start, $location_end + strlen($tag_end) - $location_start);
375
376 unset($arglist);
377 }
378
379 return $template;
380 }
381
382 // ###################################################################
383 /**
384 * Turns a localized phrase tag into a function call
385 *
386 * @access private
387 *
388 * @param string Phrase text
389 *
390 * @return string Function call for phrase text
391 */
392 function _phrase_string($text)
393 {
394 return '" . ' . $this->langcall . '(\'' . str_replace(array('\\\"', "'"), array('"', "\'"), $text) . '\') . "';
395 }
396
397 // ###################################################################
398 /**
399 * Parser for in-line template conditionals
400 *
401 * @access private
402 *
403 * @param string Template data awaiting processing
404 *
405 * @return string Parsed template data
406 */
407 function _parse_conditionals($template)
408 {
409 // tag data
410 $tag_start = '<if condition=\"';
411 $tag_start_end = '\">';
412 $tag_else = '<else />';
413 $tag_end = '</if>';
414
415 // tag stack
416 $stack = array();
417
418 // the information about the current active tag
419 $tag_full = array();
420 $parsed = array();
421
422 // start at 0
423 $offset = 0;
424
425 while (1)
426 {
427 if (strpos($template, $tag_start) === false)
428 {
429 break;
430 }
431
432 for ($i = $offset; $i < strlen($template); $i++)
433 {
434 // we've found ourselves a conditional!
435 if (substr($template, $i, strlen($tag_start)) == $tag_start)
436 {
437 // push the position into the tag stack
438 if ($tag_full)
439 {
440 array_push($stack, $i);
441 }
442 else
443 {
444 $tag_full['posi'] = $i;
445 }
446 }
447 // locate else tags
448 else if (substr($template, $i, strlen($tag_else)) == $tag_else)
449 {
450 if (count($stack) == 0 AND !isset($tag_full['else']))
451 {
452 $tag_full['else'] = $i;
453 }
454 }
455 // do we have an end tag?
456 else if (substr($template, $i, strlen($tag_end)) == $tag_end)
457 {
458 if (count($stack) != 0)
459 {
460 array_pop($stack);
461 continue;
462 }
463
464 // calculate the position of the end tag
465 $tag_full['posf'] = $i + strlen($tag_end) - 1;
466
467 // extract the entire conditional from the template
468 $fullspread = substr($template, $tag_full['posi'], $tag_full['posf'] - $tag_full['posi'] + 1);
469
470 // remove the beginning tag
471 $conditional = substr($fullspread, strlen($tag_start));
472
473 // find the end of the expression
474 $temp_end = strpos($conditional, $tag_start_end);
475
476 // save the expression
477 $parsed[0] = stripslashes(substr($conditional, 0, $temp_end));
478
479 // remove the expression from the conditional
480 $conditional = substr($conditional, strlen($parsed[0]) + strlen($tag_start_end));
481
482 // remove the tailing end tag
483 $conditional = substr($conditional, 0, strlen($conditional) - strlen($tag_end));
484
485 // handle the else
486 if (isset($tag_full['else']))
487 {
488 // now relative to the start of the <if>
489 $relpos = $tag_full['else'] - $tag_full['posi'];
490
491 // calculate the length of the expression and opening tag
492 $length = strlen($parsed[0]) + strlen($tag_start) + strlen($tag_start_end);
493
494 // relative to the start of iftrue
495 $elsepos = $relpos - $length;
496
497 $parsed[1] = substr($conditional, 0, $elsepos);
498 $parsed[2] = substr($conditional, $elsepos + strlen($tag_else));
499 }
500 // no else to handle
501 else
502 {
503 $parsed[1] = $conditional;
504 $parsed[2] = '';
505 }
506 #var_dump($parsed);
507
508 // final parsed output
509 $parsed = '" . ((' . stripslashes($parsed[0]) . ') ? "' . $parsed[1] . '" : "' . $parsed[2] . '") . "';
510
511 // replace the conditional
512 $template = str_replace($fullspread, $parsed, $template);
513
514 // reset the parser
515 $offset = $tag_full['posi'] + strlen($tag_start) + strlen($tag_start_end);
516 $tag_full = array();
517 $stack = array();
518 $parsed = array();
519 unset($fullspread, $conditional, $temp_end, $relpos, $length, $elsepos);
520 break;
521 }
522 }
523 }
524
525 return $template;
526 }
527 }
528
529 // ###################################################################
530 /**
531 * Debugging function used to print characters in a string that are
532 * around a certain position.
533 *
534 * @access private
535 *
536 * @param string The haystack string
537 * @param integer Position to print around
538 */
539 function print_around($str, $pos)
540 {
541 echo '>>> PA >>>>>>>>[';
542 echo htmlspecialchars($str[ $pos - 5 ]);
543 echo htmlspecialchars($str[ $pos - 4 ]);
544 echo htmlspecialchars($str[ $pos - 3 ]);
545 echo htmlspecialchars($str[ $pos - 2 ]);
546 echo htmlspecialchars($str[ $pos - 1 ]);
547 echo '©';
548 echo htmlspecialchars($str[ $pos + 0 ]);
549 echo htmlspecialchars($str[ $pos + 1 ]);
550 echo htmlspecialchars($str[ $pos + 2 ]);
551 echo htmlspecialchars($str[ $pos + 3 ]);
552 echo htmlspecialchars($str[ $pos + 4 ]);
553 echo htmlspecialchars($str[ $pos + 5 ]);
554 echo ']<<<<<<<< PA <<<';
555 }
556
557 /*=====================================================================*\
558 || ###################################################################
559 || # $HeadURL$
560 || # $Id$
561 || ###################################################################
562 \*=====================================================================*/
563 ?>