/core/classes/raintpl.class.php

https://bitbucket.org/habri/apblog · PHP · 662 lines · 333 code · 161 blank · 168 comment · 45 complexity · c3b87a9d5283f943af51dc4ffbd7d034 MD5 · raw file

  1. <?php
  2. /**
  3. * RainTPL
  4. * --------
  5. * Realized by Federico Ulfo & maintained by the Rain Team
  6. * Distributed under GNU/LGPL 3 License
  7. *
  8. * @version 3.0 Alpha milestone: https://github.com/rainphp/raintpl3/issues/milestones?with_issues=no
  9. */
  10. class RainTpl{
  11. // variables
  12. public $var = array();
  13. // configuration
  14. protected static $conf = array(
  15. 'checksum' => array(),
  16. 'charset' => 'UTF-8',
  17. 'debug' => false,
  18. 'tpl_dir' => 'templates/',
  19. 'cache_dir' => 'cache/',
  20. 'base_url' => null,
  21. 'tpl_ext' => 'html',
  22. 'php_enabled' => false,
  23. 'template_syntax' => 'Rain',
  24. 'registered_tags' => array(),
  25. 'auto_escape' => false,
  26. 'tags' => array(
  27. 'loop' => array( '({loop.*?})' , '/{loop="(?<variable>\${0,1}[^"]*)"(?: as (?<key>\$.*?)(?: => (?<value>\$.*?)){0,1}){0,1}}/' ),
  28. 'loop_close' => array( '({\/loop})' , '/{\/loop}/' ),
  29. 'if' => array( '({if.*?})' , '/{if="([^"]*)"}/' ),
  30. 'elseif' => array( '({elseif.*?})' , '/{elseif="([^"]*)"}/' ),
  31. 'else' => array( '({else})' , '/{else}/' ),
  32. 'if_close' => array( '({\/if})' , '/{\/if}/' ),
  33. 'noparse' => array( '({noparse})' , '/{noparse}/' ),
  34. 'noparse_close' => array( '({\/noparse})' , '/{\/noparse}/' ),
  35. 'php' => array( '({php})' , '/{php}/' ),
  36. 'php_close' => array( '({\/php})' , '/{\/php}/' ),
  37. 'ignore' => array( '({ignore})' , '/{ignore}/' ),
  38. 'ignore_close' => array( '({\/ignore})' , '/{\/ignore}/' ),
  39. 'include' => array( '({include.*?})' , '/{include="([^"]*)"}/' ),
  40. 'function' => array( '({function.*?})' , '/{function="([a-zA-Z][a-zA-Z_0-9]*)(\(.*\)){0,1}"}/' ),
  41. 'e' => array( '({e.*?})' , '/{e.(.*)}/' ),
  42. 'variable' => array( '({\$.*?})' , '/{(\$.*?)}/' ),
  43. 'constant' => array( '({#.*?})' , '/{#(.*?)#{0,1}}/' ),
  44. 'function_no_var' => array( '({!.*?})' , '/{!(.*?)}/' ),
  45. ),
  46. 'plugins_dir' => 'plugins/',
  47. 'plugins' => array(
  48. //'path_replace' => array( 'hooks' => 'before_parse', 'tags' => array( 'a', 'img', 'link', 'script', 'input' ) ),
  49. ),
  50. );
  51. /**
  52. * Draw the template
  53. */
  54. public function draw( $template_file_path, $to_string = false ){
  55. extract( $this->var );
  56. ob_start();
  57. require_once $this->_check_template( $template_file_path );
  58. if( $to_string ) return ob_get_clean(); else echo ob_get_clean();
  59. }
  60. public function view($tpl = '',$assign = array()){
  61. if(is_array($assign)){
  62. foreach($assign as $key => $val){
  63. $this->assign($key,$val);
  64. }
  65. if(!empty($tpl)){
  66. $this->draw($tpl);
  67. }
  68. }elseif(!is_array($assign) && empty($assign)){
  69. if(!empty($tpl)){
  70. $this->draw($tpl);
  71. }
  72. }
  73. }
  74. /**
  75. * Configure the template
  76. */
  77. public static function configure( $setting, $value = null ){
  78. if( is_array( $setting ) )
  79. foreach( $setting as $key => $value )
  80. static::configure( $key, $value );
  81. else if( isset( static::$conf[$setting] ) ){
  82. static::$conf[$setting] = $value;
  83. static::$conf['checksum'][$setting] = $value; // take trace of all config
  84. }
  85. }
  86. /**
  87. * Assign variable
  88. * eg. $t->assign('name','mickey');
  89. *
  90. * @param mixed $variable_name Name of template variable or associative array name/value
  91. * @param mixed $value value assigned to this variable. Not set if variable_name is an associative array
  92. */
  93. public function assign( $variable, $value = null ){
  94. if( is_array( $variable ) )
  95. $this->var += $variable;
  96. else
  97. $this->var[ $variable ] = $value;
  98. }
  99. /**
  100. * Clean the expired files from cache
  101. * @param type $expire_time Set the expiration time
  102. */
  103. public static function clean( $expire_time = 2592000 ){
  104. $files = glob( static::$conf['cache_dir'] . "*.rtpl.php" );
  105. $time = time();
  106. foreach( $files as $file )
  107. if( $time - filemtime($file) > $expired_time )
  108. unlink($file);
  109. }
  110. public static function register_tag( $tag, $parse, $function ){
  111. static::$conf['registered_tags'][ $tag ] = array( "parse" => $parse, "function" => $function );
  112. }
  113. protected function _check_template( $template ){
  114. // set filename
  115. $template_name = basename( $template );
  116. $template_basedir = strpos($template,"/") ? dirname($template) . '/' : null;
  117. $template_directory = static::$conf['tpl_dir'] . $template_basedir;
  118. $template_filepath = $template_directory . $template_name . '.' . static::$conf['tpl_ext'];
  119. $parsed_template_filepath = static::$conf['cache_dir'] . $template_name . "." . md5( $template_directory . implode( static::$conf['checksum'] ) ) . '.rtpl.php';
  120. // if the template doesn't exsist throw an error
  121. if( !file_exists( $template_filepath ) ){
  122. $e = new RainTpl_NotFoundException( 'Template '. $template_filepath .' not found!' );
  123. throw $e->setTemplateFile($template_filepath);
  124. }
  125. // Compile the template if the original has been updated
  126. if( static::$conf['debug'] || !file_exists( $parsed_template_filepath ) || ( filemtime($parsed_template_filepath) < filemtime( $template_filepath ) ) )
  127. $this->_compile_file( $template_name, $template_basedir, $template_filepath, $parsed_template_filepath );
  128. return $parsed_template_filepath;
  129. }
  130. /**
  131. * Compile the file
  132. */
  133. protected function _compile_file( $template_name, $template_basedir, $template_filepath, $parsed_template_filepath ){
  134. // open the template
  135. $fp = fopen( $template_filepath, "r" );
  136. // lock the file
  137. if( flock( $fp, LOCK_SH ) ){
  138. // read the file
  139. $code = fread($fp, filesize( $template_filepath ) );
  140. // xml substitution
  141. $code = preg_replace( "/<\?xml(.*?)\?>/s", "##XML\\1XML##", $code );
  142. // disable php tag
  143. if( !static::$conf['php_enabled'] )
  144. $code = str_replace( array("<?","?>"), array("&lt;?","?&gt;"), $code );
  145. // xml re-substitution
  146. $code = preg_replace_callback ( "/##XML(.*?)XML##/s", function( $match ){
  147. return "<?php echo '<?xml ".stripslashes($match[1])." ?>'; ?>";
  148. }, $code );
  149. $parsed_code = $this->_compile_template( $code, $template_basedir, $template_filepath );
  150. $parsed_code = "<?php if(!class_exists('RainTpl')){exit;}?>" . $parsed_code;
  151. // fix the php-eating-newline-after-closing-tag-problem
  152. $parsed_code = str_replace( "?>\n", "?>\n\n", $parsed_code );
  153. // create directories
  154. if( !is_dir( static::$conf['cache_dir'] ) )
  155. mkdir( static::$conf['cache_dir'], 0755, true );
  156. // check if the cache is writable
  157. if( !is_writable( static::$conf['cache_dir'] ) )
  158. throw new RainTpl_Exception ('Cache directory ' . static::$conf['cache_dir'] . 'doesn\'t have write permission. Set write permission or set RAINTPL_CHECK_TEMPLATE_UPDATE to false. More details on http://www.raintpl.com/Documentation/Documentation-for-PHP-developers/Configuration/');
  159. // write compiled file
  160. file_put_contents( $parsed_template_filepath, $parsed_code );
  161. // release the file lock
  162. flock($fp, LOCK_UN);
  163. }
  164. // close the file
  165. fclose( $fp );
  166. }
  167. /**
  168. * Compile template
  169. * @access protected
  170. */
  171. protected function _compile_template( $code, $template_basedir, $template_filepath ){
  172. // before parse
  173. $code = $this->parse_hook( 'before_parse', array( 'code'=>$code, 'template_basedir'=>$template_basedir, 'template_filepath'=>$template_filepath ) );
  174. // set tags
  175. foreach( static::$conf['tags'] as $tag => $tag_array ){
  176. list( $split, $match ) = $tag_array;
  177. $tag_split[$tag] = $split;
  178. $tag_match[$tag] = $match;
  179. }
  180. $keys = array_keys( static::$conf['registered_tags'] );
  181. $tag_split += array_merge( $tag_split, $keys );
  182. //split the code with the tags regexp
  183. $code_split = preg_split( "/" . implode( "|", $tag_split ) . "/", $code, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
  184. //variables initialization
  185. $parsed_code = $comment_is_open = $ignore_is_open = NULL;
  186. $open_if = $loop_level = 0;
  187. //read all parsed code
  188. while( $html = array_shift( $code_split ) ){
  189. //close ignore tag
  190. if( !$comment_is_open && preg_match( $tag_match['ignore_close'], $html ) )
  191. $ignore_is_open = FALSE;
  192. //code between tag ignore id deleted
  193. elseif( $ignore_is_open ){
  194. //ignore the code
  195. }
  196. //close no parse tag
  197. elseif( preg_match( $tag_match['noparse_close'], $html ) )
  198. $comment_is_open = FALSE;
  199. //code between tag noparse is not compiled
  200. elseif( $comment_is_open )
  201. $parsed_code .= $html;
  202. //ignore
  203. elseif( preg_match( $tag_match['ignore'], $html ) )
  204. $ignore_is_open = TRUE;
  205. //noparse
  206. elseif( preg_match( $tag_match['noparse'], $html ) )
  207. $comment_is_open = TRUE;
  208. //include tag
  209. elseif( preg_match( $tag_match['include'], $html, $matches ) ){
  210. //variables substitution
  211. $include_var = $this->_var_replace( $matches[ 1 ], $loop_level );
  212. //dynamic include
  213. $parsed_code .= '<?php $tpl = new '.get_class($this).';' .
  214. '$tpl_dir_temp = static::$conf[\'tpl_dir\'];' .
  215. '$tpl->assign( $this->var );' .
  216. ( !$loop_level ? null : '$tpl->assign( "key", $key'.$loop_level.' ); $tpl->assign( "value", $value'.$loop_level.' );' ).
  217. '$tpl->draw( dirname("'.$include_var.'") . ( substr("'.$include_var.'",-1,1) != "/" ? "/" : "" ) . basename("'.$include_var.'") );'.
  218. '?>';
  219. }
  220. elseif( preg_match( $tag_match['e'], $html, $matches ) ) {
  221. // get function
  222. $word = $matches[1];
  223. // function
  224. $parsed_code .= "<?php _e('$word'); ?>";
  225. }
  226. //loop
  227. elseif( preg_match( $tag_match['loop'], $html, $matches ) ){
  228. //increase the loop counter
  229. $loop_level++;
  230. // check if is a function
  231. if( preg_match( "/.*\(.*\)/", $matches['variable'] ) )
  232. $var = $matches['variable'];
  233. else
  234. //replace the variable in the loop
  235. $var = $this->_var_replace($matches['variable'], $loop_level-1, $escape = false );
  236. //loop variables
  237. $counter = "\$counter$loop_level"; // count iteration
  238. if( isset($matches['key']) && isset($matches['value']) ){
  239. $key = $matches['key'];
  240. $value = $matches['value'];
  241. }
  242. elseif( isset($matches['key']) ){
  243. $key = "\$key$loop_level"; // key
  244. $value = $matches['key'];
  245. }
  246. else{
  247. $key = "\$key$loop_level"; // key
  248. $value = "\$value$loop_level"; // value
  249. }
  250. //loop code
  251. $parsed_code .= "<?php $counter=-1; if( is_array($var) && sizeof($var) ) foreach( $var as $key => $value ){ $counter++; ?>";
  252. }
  253. //close loop tag
  254. elseif( preg_match( $tag_match['loop_close'], $html ) ) {
  255. //iterator
  256. $counter = "\$counter$loop_level";
  257. //decrease the loop counter
  258. $loop_level--;
  259. //close loop code
  260. $parsed_code .= "<?php } ?>";
  261. }
  262. //if
  263. elseif( preg_match( $tag_match['if'], $html, $matches ) ){
  264. //increase open if counter (for intendation)
  265. $open_if++;
  266. //tag
  267. $tag = $matches[ 0 ];
  268. //condition attribute
  269. $condition = $matches[ 1 ];
  270. //variable substitution into condition (no delimiter into the condition)
  271. $parsed_condition = $this->_var_replace( $condition, $loop_level, $escape = false );
  272. //if code
  273. $parsed_code .= "<?php if( $parsed_condition ){ ?>";
  274. }
  275. //elseif
  276. elseif( preg_match( $tag_match['elseif'], $html, $matches ) ){
  277. //tag
  278. $tag = $matches[ 0 ];
  279. //condition attribute
  280. $condition = $matches[ 1 ];
  281. //variable substitution into condition (no delimiter into the condition)
  282. $parsed_condition = $this->_var_replace( $condition, $loop_level, $escape = false );
  283. //elseif code
  284. $parsed_code .= "<?php }elseif( $parsed_condition ){ ?>";
  285. }
  286. //else
  287. elseif( preg_match( $tag_match['else'], $html ) ) {
  288. //else code
  289. $parsed_code .= '<?php }else{ ?>';
  290. }
  291. //close if tag
  292. elseif( preg_match( $tag_match['if_close'], $html ) ) {
  293. //decrease if counter
  294. $open_if--;
  295. // close if code
  296. $parsed_code .= '<?php } ?>';
  297. }
  298. // function
  299. elseif( preg_match( $tag_match['function'], $html, $matches ) ) {
  300. // get function
  301. $function = $matches[1];
  302. // var replace
  303. if( isset($matches[2]) )
  304. $parsed_function = $function . $this->_var_replace( $matches[2], $loop_level, $escape = false, $echo = false );
  305. else
  306. $parsed_function = $function . "()";
  307. // function
  308. $parsed_code .= "<?php echo $parsed_function; ?>";
  309. }
  310. elseif(preg_match( $tag_match['php'], $html, $matches )){
  311. $parsed_code .= "<?php ";
  312. }
  313. elseif(preg_match( $tag_match['php_close'], $html, $matches )){
  314. $parsed_code .= " ?>";
  315. }
  316. //variables
  317. elseif( preg_match( $tag_match['variable'], $html, $matches ) ){
  318. //variables substitution (es. {$title})
  319. $parsed_code .= "<?php " . $this->_var_replace( $matches[1], $loop_level, $escape = true, $echo = true ) . "; ?>";
  320. }
  321. //constants
  322. elseif( preg_match( $tag_match['constant'], $html, $matches ) ){
  323. $parsed_code .= "<?php echo " . $this->_con_replace( $matches[1], $loop_level ) . "; ?>";
  324. }
  325. //function with no var like (continue;)
  326. elseif( preg_match( $tag_match['function_no_var'], $html, $matches ) ){
  327. $parsed_code .= "<?php " . $this->_con_replace( $matches[1], $loop_level ) . "; ?>";
  328. }
  329. // registered tags
  330. else{
  331. $found = false;
  332. foreach( static::$conf['registered_tags'] as $tag => $array ){
  333. if( preg_match( "/{$array['parse']}/", $html, $matches ) ){
  334. $found = true;
  335. $parsed_code .= "<?php echo call_user_func( static::\$conf['registered_tags']['$tag']['function'], array('".$matches[1]."') ); ?>";
  336. }
  337. }
  338. if( !$found )
  339. $parsed_code .= $html;
  340. }
  341. }
  342. if( $open_if > 0 ) {
  343. $e = new RainTpl_SyntaxException('Error! You need to close an {if} tag in ' . $template_filepath . ' template');
  344. throw $e->setTemplateFile($template_filepath);
  345. }
  346. if( $loop_level > 0 ) {
  347. $e = new RainTpl_SyntaxException('Error! You need to close the {loop} tag in ' . $template_filepath . ' template');
  348. throw $e->setTemplateFile($template_filepath);
  349. }
  350. // after_parse
  351. $parsed_code = $this->parse_hook( 'after_parse', array( 'code'=>$parsed_code, 'template_basedir'=>$template_basedir, 'template_filepath'=>$template_filepath ) );
  352. return $parsed_code;
  353. }
  354. protected function _var_replace( $html, $loop_level = NULL, $escape = true, $echo = false ){
  355. // change variable name if loop level
  356. if( $loop_level )
  357. $html = str_replace( array('$value','$key','$counter'), array('$value'.$loop_level,'$key'.$loop_level,'$counter'.$loop_level), $html );
  358. // if it is a variable
  359. if( preg_match_all('/(\$[a-z_A-Z][\.\[\]\"\'a-zA-Z_0-9]*)/', $html, $matches ) ){
  360. // substitute . and [] with [" "]
  361. for( $i=0;$i<count($matches[1]);$i++ ){
  362. $rep = preg_replace( '/\[(\${0,1}[a-zA-Z_0-9]*)\]/', '["$1"]', $matches[1][$i] );
  363. $rep = preg_replace( '/\.(\${0,1}[a-zA-Z_0-9]*)/', '["$1"]', $rep );
  364. $html = str_replace( $matches[0][$i], $rep, $html );
  365. }
  366. // update modifier
  367. $html = $this->_modifier_replace( $html );
  368. // if is not init
  369. if( !preg_match( '/\$.*=.*/', $rep ) ){
  370. // escape character
  371. if( static::$conf['auto_escape'] && $escape )
  372. //$html = "htmlspecialchars( $html )";
  373. $html = "htmlspecialchars( $html, ENT_COMPAT, '".static::$conf['charset']."', false )";
  374. // if is an assignment it doesn't add echo
  375. if( $echo )
  376. $html = "echo " . $html;
  377. }
  378. }
  379. return $html;
  380. }
  381. protected function _con_replace( $html ){
  382. $html = $this->_modifier_replace( $html );
  383. return $html;
  384. }
  385. protected function _modifier_replace( $html ){
  386. if( $pos = strrpos( $html, "|" ) ){
  387. $explode = explode( ":", substr( $html, $pos+1 ) );
  388. $function = $explode[0];
  389. $params = isset( $explode[1] ) ? "," . $explode[1] : null;
  390. $html = $function . "(" . $this->_modifier_replace( substr( $html, 0, $pos ) ) . "$params)";
  391. }
  392. return $html;
  393. }
  394. protected function parse_hook( $hook, $parameters ){
  395. foreach( static::$conf['plugins'] as $plugin_name => $plugin ){
  396. // if the selected hook is here
  397. if( ( is_string( $plugin['hooks'] ) && $hook == $plugin['hooks'] ) OR ( is_array($plugin['hooks']) && in_array( $hook, $plugin['hooks'] ) ) ){
  398. require_once static::$conf['plugins_dir'] . $plugin_name . ".raintpl.php";
  399. return call_user_func( $plugin_name . "_" . $hook, $parameters, static::$conf );
  400. }
  401. }
  402. switch( $hook ){
  403. case 'before_parse':
  404. return $parameters['code'];
  405. case 'after_parse':
  406. return $parameters['code'];
  407. }
  408. }
  409. }
  410. /**
  411. * Basic Rain tpl exception.
  412. */
  413. class RainTpl_Exception extends Exception{
  414. /**
  415. * Path of template file with error.
  416. */
  417. protected $templateFile = '';
  418. /**
  419. * Returns path of template file with error.
  420. *
  421. * @return string
  422. */
  423. public function getTemplateFile()
  424. {
  425. return $this->templateFile;
  426. }
  427. /**
  428. * Sets path of template file with error.
  429. *
  430. * @param string $templateFile
  431. * @return RainTpl_Exception
  432. */
  433. public function setTemplateFile($templateFile)
  434. {
  435. $this->templateFile = (string) $templateFile;
  436. return $this;
  437. }
  438. }
  439. /**
  440. * Exception thrown when template file does not exists.
  441. */
  442. class RainTpl_NotFoundException extends RainTpl_Exception{
  443. }
  444. /**
  445. * Exception thrown when syntax error occurs.
  446. */
  447. class RainTpl_SyntaxException extends RainTpl_Exception{
  448. /**
  449. * Line in template file where error has occured.
  450. *
  451. * @var int | null
  452. */
  453. protected $templateLine = null;
  454. /**
  455. * Tag which caused an error.
  456. *
  457. * @var string | null
  458. */
  459. protected $tag = null;
  460. /**
  461. * Returns line in template file where error has occured
  462. * or null if line is not defined.
  463. *
  464. * @return int | null
  465. */
  466. public function getTemplateLine()
  467. {
  468. return $this->templateLine;
  469. }
  470. /**
  471. * Sets line in template file where error has occured.
  472. *
  473. * @param int $templateLine
  474. * @return RainTpl_SyntaxException
  475. */
  476. public function setTemplateLine($templateLine)
  477. {
  478. $this->templateLine = (int) $templateLine;
  479. return $this;
  480. }
  481. /**
  482. * Returns tag which caused an error.
  483. *
  484. * @return string
  485. */
  486. public function getTag()
  487. {
  488. return $this->tag;
  489. }
  490. /**
  491. * Sets tag which caused an error.
  492. *
  493. * @param string $tag
  494. * @return RainTpl_SyntaxException
  495. */
  496. public function setTag($tag)
  497. {
  498. $this->tag = (string) $tag;
  499. return $this;
  500. }
  501. }