From 912df52a3e46b313623817d3cb136b5eaac3c99d Mon Sep 17 00:00:00 2001 From: avl Date: Sun, 17 Aug 2008 05:27:03 +0300 Subject: [PATCH] Initial repo 0.2 alpha --- ipf/exception.php | 3 + ipf/exception/template.php | 3 + ipf/template.php | 120 ++++++++ ipf/template/compiler.php | 481 +++++++++++++++++++++++++++++++ ipf/template/context.php | 24 ++ ipf/template/context/request.php | 20 ++ ipf/template/contextvars.php | 26 ++ ipf/template/safestring.php | 25 ++ ipf/template/tag.php | 10 + ipf/template/tag/url.php | 11 + 10 files changed, 723 insertions(+) create mode 100644 ipf/exception.php create mode 100644 ipf/exception/template.php create mode 100644 ipf/template.php create mode 100644 ipf/template/compiler.php create mode 100644 ipf/template/context.php create mode 100644 ipf/template/context/request.php create mode 100644 ipf/template/contextvars.php create mode 100644 ipf/template/safestring.php create mode 100644 ipf/template/tag.php create mode 100644 ipf/template/tag/url.php diff --git a/ipf/exception.php b/ipf/exception.php new file mode 100644 index 0000000..c418db8 --- /dev/null +++ b/ipf/exception.php @@ -0,0 +1,3 @@ +tpl = $template; + if (is_null($folders)) { + $this->folders = IPF::get('template_dirs'); + } else { + $this->folders = $folders; + } + if (is_null($cache)) { + $this->cache = IPF::get('tmp'); + } else { + $this->cache = $cache; + } + + } + + function render($c=null) + { + $this->compiled_template = $this->getCompiledTemplateName(); + if (!file_exists($this->compiled_template) or IPF::get('debug')) { + $compiler = new IPF_Template_Compiler($this->tpl, $this->folders); + $this->template_content = $compiler->getCompiledTemplate(); + $this->write(); + } + if (is_null($c)) { + $c = new IPF_Template_Context(); + } + $this->context = $c; + ob_start(); + $t = $c; + try { + include $this->compiled_template; + } catch (Exception $e) { + ob_clean(); + throw $e; + } + $a = ob_get_contents(); + ob_end_clean(); + return $a; + } + + function getCompiledTemplateName() + { + $_tmp = var_export($this->folders, true); + return $this->cache.'/IPF_Template-'.md5($_tmp.$this->tpl).'.phps'; + } + + function write() + { + $fp = @fopen($this->compiled_template, 'a'); + if ($fp !== false) { + flock($fp, LOCK_EX); + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, $this->template_content, strlen($this->template_content)); + flock($fp, LOCK_UN); + fclose($fp); + @chmod($this->compiled_template, 0777); + return true; + } else { + throw new IPF_Exception_Template(sprintf(__('Cannot write the compiled template: %s'), $this->compiled_template)); + } + return false; + } + +} + +function IPF_Template_unsafe($string) +{ + return new IPF_Template_SafeString($string, true); +} + +function IPF_Template_htmlspecialchars($string) +{ + return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8'); +} + +function IPF_Template_dateFormat($date, $format='%b %e, %Y') +{ + if (substr(PHP_OS,0,3) == 'WIN') { + $_win_from = array ('%e', '%T', '%D'); + $_win_to = array ('%#d', '%H:%M:%S', '%m/%d/%y'); + $format = str_replace($_win_from, $_win_to, $format); + } + $date = date('Y-m-d H:i:s', strtotime($date.' GMT')); + return strftime($format, strtotime($date)); +} + +function IPF_Template_timeFormat($time, $format='Y-m-d H:i:s') +{ + return date($format, $time); +} + +function IPF_Template_safeEcho($mixed, $echo=true) +{ + if (!is_object($mixed) or 'IPF_Template_SafeString' !== get_class($mixed)) { + if ($echo) { + echo htmlspecialchars((string) $mixed, ENT_COMPAT, 'UTF-8'); + } else { + return htmlspecialchars((string) $mixed, ENT_COMPAT, 'UTF-8'); + } + } else { + if ($echo) { + echo $mixed->value; + } else { + return $mixed->value; + } + } +} diff --git a/ipf/template/compiler.php b/ipf/template/compiler.php new file mode 100644 index 0000000..935faac --- /dev/null +++ b/ipf/template/compiler.php @@ -0,0 +1,481 @@ + 'strtoupper', + 'lower' => 'strtolower', + 'escxml' => 'htmlspecialchars', + 'escape' => 'IPF_Template_htmlspecialchars', + 'strip_tags' => 'strip_tags', + 'escurl' => 'rawurlencode', + 'capitalize' => 'ucwords', + // Not var_export because of recursive issues. + 'debug' => 'print_r', + 'fulldebug' => 'var_export', + 'count' => 'count', + 'nl2br' => 'nl2br', + 'trim' => 'trim', + 'unsafe' => 'IPF_Template_unsafe', + 'safe' => 'IPF_Template_unsafe', + 'date' => 'IPF_Template_dateFormat', + 'time' => 'IPF_Template_timeFormat', + ); + + public $_usedModifiers = array(); + + protected $_allowedTags = array( + 'url' => 'IPF_Template_Tag_Url', + ); + protected $_extraTags = array(); + + protected $_blockStack = array(); + + protected $_transStack = array(); + protected $_transPlural = false; + + protected $_sourceFile; + + protected $_currentTag; + + public $templateFolders = array(); + + public $templateContent = ''; + + public $_extendBlocks = array(); + + public $_extendedTemplate = ''; + + function __construct($template_file, $folders=array(), $load=true) + { + $allowedtags = IPF::get('template_tags', array()); + $this->_allowedTags = array_merge($allowedtags, $this->_allowedTags); + $modifiers = IPF::get('template_modifiers', array()); + $this->_modifier = array_merge($modifiers, $this->_modifier); + + foreach ($this->_allowedTags as $name=>$model) { + $this->_extraTags[$name] = new $model(); + } + $this->_sourceFile = $template_file; + $this->_allowedInVar = array_merge($this->_vartype, $this->_op); + $this->_allowedInExpr = array_merge($this->_vartype, $this->_op); + $this->_allowedAssign = array_merge($this->_vartype, $this->_assignOp, + $this->_op); + $this->templateFolders = $folders; + if ($load) { + $this->loadTemplateFile($template_file); + } + } + + function compile() + { + $this->compileBlocks(); + $tplcontent = $this->templateContent; + $tplcontent = preg_replace('!{\*(.*?)\*}!s', '', $tplcontent); + $tplcontent = preg_replace('!<\?php(.*?)\?>!s', '', $tplcontent); + preg_match_all('!{literal}(.*?){/literal}!s', $tplcontent, $_match); + $this->_literals = $_match[1]; + $tplcontent = preg_replace("!{literal}(.*?){/literal}!s", '{literal}', $tplcontent); + // Core regex to parse the template + $result = preg_replace_callback('/{((.).*?)}/s', + array($this, '_callback'), + $tplcontent); + if (count($this->_blockStack)) { + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + return $result; + } + + function getCompiledTemplate() + { + $result = $this->compile(); + if (count($this->_usedModifiers)) { + $code = array(); + foreach ($this->_usedModifiers as $modifier) { + $code[] = 'IPF::loadFunction(\''.$modifier.'\'); '; + } + $result = ''.$result; + } + $result = str_replace(array('?>', ''), '', $result); + $result = str_replace("?>\n", "?>\n\n", $result); + return $result; + } + + function compileBlocks() + { + $tplcontent = $this->templateContent; + $this->_extendedTemplate = ''; + // Match extends on the first line of the template + if (preg_match("!{extends\s['\"](.*?)['\"]}!", $tplcontent, $_match)) { + $this->_extendedTemplate = $_match[1]; + } + // Get the blocks in the current template + $cnt = preg_match_all("!{block\s(\S+?)}(.*?){/block}!s", $tplcontent, $_match); + // Compile the blocks + for ($i=0; $i<$cnt; $i++) { + if (!isset($this->_extendBlocks[$_match[1][$i]]) + or false !== strpos($this->_extendBlocks[$_match[1][$i]], '~~{~~superblock~~}~~')) { + $compiler = clone($this); + $compiler->templateContent = $_match[2][$i]; + $_tmp = $compiler->compile(); + $this->updateModifierStack($compiler); + if (!isset($this->_extendBlocks[$_match[1][$i]])) { + $this->_extendBlocks[$_match[1][$i]] = $_tmp; + } else { + $this->_extendBlocks[$_match[1][$i]] = str_replace('~~{~~superblock~~}~~', $_tmp, $this->_extendBlocks[$_match[1][$i]]); + } + } + } + if (strlen($this->_extendedTemplate) > 0) { + // The template of interest is now the extended template + // as we are not in a base template + $this->loadTemplateFile($this->_extendedTemplate); + $this->_sourceFile = $this->_extendedTemplate; + $this->compileBlocks(); //It will recurse to the base template. + } else { + // Replace the current blocks by a place holder + if ($cnt) { + $this->templateContent = preg_replace("!{block\s(\S+?)}(.*?){/block}!s", "{block $1}", $tplcontent, -1); + } + } + } + + function loadTemplateFile($file) + { + // FIXME: Very small security check, could be better. + if (strpos($file, '..') !== false) { + throw new IPF_Exception(sprintf(__('Template file contains invalid characters: %s'), $file)); + } + foreach ($this->templateFolders as $folder) { + if (file_exists($folder.'/'.$file)) { + $this->templateContent = file_get_contents($folder.'/'.$file); + return; + } + } + // File not found in all the folders. + throw new IPF_Exception(sprintf(__('Template file not found: %s'), $file)); + } + + function _callback($matches) + { + list(,$tag, $firstcar) = $matches; + if (!preg_match('/^\$|[\'"]|[a-zA-Z\/]$/', $firstcar)) { + trigger_error(sprintf(__('Invalid tag syntax: %s'), $tag), E_USER_ERROR); + return ''; + } + $this->_currentTag = $tag; + if (in_array($firstcar, array('$', '\'', '"'))) { + if ('blocktrans' !== end($this->_blockStack)) { + return '_parseVariable($tag).'); ?>'; + } else { + $tok = explode('|', $tag); + $this->_transStack[substr($tok[0], 1)] = $this->_parseVariable($tag); + return '%%'.substr($tok[0], 1).'%%'; + } + } else { + if (!preg_match('/^(\/?[a-zA-Z0-9_]+)(?:(?:\s+(.*))|(?:\((.*)\)))?$/', $tag, $m)) { + trigger_error(sprintf(__('Invalid function syntax: %s'), $tag), E_USER_ERROR); + return ''; + } + if (count($m) == 4){ + $m[2] = $m[3]; + } + if (!isset($m[2])) $m[2] = ''; + if($m[1] == 'ldelim') return '{'; + if($m[1] == 'rdelim') return '}'; + if ($m[1] != 'include') { + return '_parseFunction($m[1], $m[2]).'?>'; + } else { + return $this->_parseFunction($m[1], $m[2]); + } + } + } + + function _parseVariable($expr) + { + $tok = explode('|', $expr); + $res = $this->_parseFinal(array_shift($tok), $this->_allowedInVar); + foreach ($tok as $modifier) { + if (!preg_match('/^(\w+)(?:\:(.*))?$/', $modifier, $m)) { + trigger_error(sprintf(__('Invalid modifier syntax: (%s) %s'), $this->_currentTag, $modifier), E_USER_ERROR); + return ''; + } + $targs = array($res); + if(isset($m[2])){ + $res = $this->_modifier[$m[1]].'('.$res.','.$m[2].')'; + } + else if (isset($this->_modifier[$m[1]])) { + $res = $this->_modifier[$m[1]].'('.$res.')'; + } else { + trigger_error(sprintf(__('Unknown modifier: (%s) %s'), $this->_currentTag, $m[1]), E_USER_ERROR); + return ''; + } + if (!in_array($this->_modifier[$m[1]], $this->_usedModifiers)) { + $this->_usedModifiers[] = $this->_modifier[$m[1]]; + } + } + return $res; + } + + function _parseFunction($name, $args) + { + switch ($name) { + case 'if': + $res = 'if ('.$this->_parseFinal($args, $this->_allowedInExpr).'): '; + array_push($this->_blockStack, 'if'); + break; + case 'else': + if (end($this->_blockStack) != 'if') { + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + $res = 'else: '; + break; + case 'elseif': + if (end($this->_blockStack) != 'if') { + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + $res = 'elseif('.$this->_parseFinal($args, $this->_allowedInExpr).'):'; + break; + case 'foreach': + $res = 'foreach ('.$this->_parseFinal($args, array(T_AS, T_DOUBLE_ARROW, T_STRING, T_OBJECT_OPERATOR), array(';','!')).'): '; + array_push($this->_blockStack, 'foreach'); + break; + case 'while': + $res = 'while('.$this->_parseFinal($args,$this->_allowedInExpr).'):'; + array_push($this->_blockStack, 'while'); + break; + case '/foreach': + case '/if': + case '/while': + $short = substr($name,1); + if(end($this->_blockStack) != $short){ + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + array_pop($this->_blockStack); + $res = 'end'.$short.'; '; + break; + case 'assign': + $res = $this->_parseFinal($args, $this->_allowedAssign).'; '; + break; + case 'literal': + if(count($this->_literals)){ + $res = '?>'.array_shift($this->_literals).''.$this->_extendBlocks[$args].'~~{~~superblock~~}~~_parseFinal($args, $this->_allowedAssign); + $res = 'echo(__('.$argfct.'));'; + break; + case 'blocktrans': + array_push($this->_blockStack, 'blocktrans'); + $res = ''; + $this->_transStack = array(); + if ($args) { + $this->_transPlural = true; + $_args = $this->_parseFinal($args, $this->_allowedAssign, + array(';', '[', ']'), true); + $res .= '$_b_t_c='.trim(array_shift($_args)).'; '; + } + $res .= 'ob_start(); '; + break; + case '/blocktrans': + $short = substr($name,1); + if(end($this->_blockStack) != $short){ + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + $res = ''; + if ($this->_transPlural) { + $res .= '$_b_t_p=ob_get_contents(); ob_end_clean(); echo('; + $res .= 'IPF_Translation::sprintf(_n($_b_t_s, $_b_t_p, $_b_t_c), array('; + $_tmp = array(); + foreach ($this->_transStack as $key=>$_trans) { + $_tmp[] = '\''.addslashes($key).'\' => IPF_Template_safeEcho('.$_trans.', false)'; + } + $res .= implode(', ', $_tmp); + unset($_trans, $_tmp); + $res .= ')));'; + $this->_transStack = array(); + } else { + $res .= '$_b_t_s=ob_get_contents(); ob_end_clean(); '; + if (count($this->_transStack) == 0) { + $res .= 'echo(__($_b_t_s)); '; + } else { + $res .= 'echo(IPF_Translation::sprintf(__($_b_t_s), array('; + $_tmp = array(); + foreach ($this->_transStack as $key=>$_trans) { + $_tmp[] = '\''.addslashes($key).'\' => IPF_Template_safeEcho('.$_trans.', false)'; + } + $res .= implode(', ', $_tmp); + unset($_trans, $_tmp); + $res .= '))); '; + $this->_transStack = array(); + } + } + $this->_transPlural = false; + array_pop($this->_blockStack); + break; + case 'plural': + $res = '$_b_t_s=ob_get_contents(); ob_end_clean(); ob_start(); '; + break; + case 'include': + // XXX fixme: Will need some security check, when online editing. + $argfct = preg_replace('!^[\'"](.*)[\'"]$!', '$1', $args); + $_comp = new IPF_Template_Compiler($argfct, $this->templateFolders); + $res = $_comp->compile(); + $this->updateModifierStack($_comp); + break; + default: + $_end = false; + $oname = $name; + if (substr($name, 0, 1) == '/') { + $_end = true; + $name = substr($name, 1); + } + // Here we should allow custom blocks. + + // Here we start the template tag calls at the template tag + // {tag ...} is not a block, so it must be a function. + if (!isset($this->_allowedTags[$name])) { + trigger_error(sprintf(__('The function tag "%s" is not allowed.'), $name), E_USER_ERROR); + } + $argfct = $this->_parseFinal($args, $this->_allowedAssign); + // $argfct is a string that can be copy/pasted in the PHP code + // but we need the array of args. + $res = ''; + if (isset($this->_extraTags[$name])) { + if (false == $_end) { + if (method_exists($this->_extraTags[$name], 'start')) { + $res .= '$_extra_tag = IPF::factory(\''.$this->_allowedTags[$name].'\', $t); $_extra_tag->start('.$argfct.'); '; + } + if (method_exists($this->_extraTags[$name], 'genStart')) { + $res .= $this->_extraTags[$name]->genStart(); + } + } else { + if (method_exists($this->_extraTags[$name], 'end')) { + $res .= '$_extra_tag = IPF::factory(\''.$this->_allowedTags[$name].'\', $t); $_extra_tag->end('.$argfct.'); '; + } + if (method_exists($this->_extraTags[$name], 'genEnd')) { + $res .= $this->_extraTags[$name]->genEnd(); + } + } + } + if ($res == '') { + trigger_error(sprintf(__('The function tag "{%s ...}" is not supported.'), $oname), E_USER_ERROR); + } + } + return $res; + } + + function _parseFinal($string, $allowed=array(), + $exceptchar=array(';'), $getAsArray=false) + { + $tokens = token_get_all(''); + $result = ''; + $first = true; + $inDot = false; + $firstok = array_shift($tokens); + $afterAs = false; + $f_key = ''; + $f_val = ''; + $results = array(); + + // il y a un bug, parfois le premier token n'est pas T_OPEN_TAG... + if ($firstok == '<' && $tokens[0] == '?' && is_array($tokens[1]) + && $tokens[1][0] == T_STRING && $tokens[1][1] == 'php') { + array_shift($tokens); + array_shift($tokens); + } + foreach ($tokens as $tok) { + if (is_array($tok)) { + list($type, $str) = $tok; + $first = false; + if($type == T_CLOSE_TAG){ + continue; + } + if ($type == T_AS) { + $afterAs = true; + } + if ($type == T_STRING && $inDot) { + $result .= $str; + } elseif ($type == T_VARIABLE) { + $result .= '$t->_vars[\''.substr($str, 1).'\']'; + } elseif ($type == T_WHITESPACE || in_array($type, $allowed)) { + $result .= $str; + } else { + trigger_error(sprintf(__('Invalid syntax: (%s) %s.'), $this->_currentTag, $str), E_USER_ERROR); + return ''; + } + } else { + if (in_array($tok, $exceptchar)) { + trigger_error(sprintf(__('Invalid character: (%s) %s.'), $this->_currentTag, $tok), E_USER_ERROR); + } elseif ($tok == '.') { + $inDot = true; + $result .= '->'; + } elseif ($tok == '~') { + $result .= '.'; + } elseif ($tok =='[') { + $result.=$tok; + } elseif ($tok ==']') { + $result.=$tok; + } elseif ($getAsArray && $tok == ',') { + $results[]=$result; + $result=''; + } else { + $result .= $tok; + } + $first = false; + } + } + if (!$getAsArray) { + return $result; + } else { + if ($result != '') { + $results[] = $result; + } + return $results; + } + } + + protected function updateModifierStack($compiler) + { + foreach ($compiler->_usedModifiers as $_um) { + if (!in_array($_um, $this->_usedModifiers)) { + $this->_usedModifiers[] = $_um; + } + } + } +} + diff --git a/ipf/template/context.php b/ipf/template/context.php new file mode 100644 index 0000000..53818e3 --- /dev/null +++ b/ipf/template/context.php @@ -0,0 +1,24 @@ +_vars = new IPF_Template_ContextVars($vars); + } + + function get($var) + { + if (isset($this->_vars[$var])) { + return $this->_vars[$var]; + } + return ''; + } + + function set($var, $value) + { + $this->_vars[$var] = $value; + } +} diff --git a/ipf/template/context/request.php b/ipf/template/context/request.php new file mode 100644 index 0000000..0074620 --- /dev/null +++ b/ipf/template/context/request.php @@ -0,0 +1,20 @@ + $request), $vars); + foreach (IPF::get('template_context_processors', array()) as $proc) { + IPF::loadFunction($proc); + $vars = array_merge($proc($request), $vars); + } + $params = array('request' => $request, + 'context' => $vars); + + //IPF_Signal::send('IPF_Template_Context_Request::construct', + // 'IPF_Template_Context_Request', $params); + $this->_vars = new IPF_Template_ContextVars($params['context']); + } +} + diff --git a/ipf/template/contextvars.php b/ipf/template/contextvars.php new file mode 100644 index 0000000..637c12e --- /dev/null +++ b/ipf/template/contextvars.php @@ -0,0 +1,26 @@ +offsetExists($prop)) { + return ''; + } + return parent::offsetGet($prop); + } + + function __get($prop) + { + if (isset($this->$prop)) { + return $this->$prop; + } else { + return $this->offsetGet($prop); + } + } + + function __toString() + { + return var_export($this, true); + } +} diff --git a/ipf/template/safestring.php b/ipf/template/safestring.php new file mode 100644 index 0000000..b8f76a7 --- /dev/null +++ b/ipf/template/safestring.php @@ -0,0 +1,25 @@ +value = $mixed->value; + } else { + $this->value = ($safe) ? $mixed : htmlspecialchars($mixed, ENT_COMPAT, 'UTF-8'); + } + } + + function __toString() + { + return $this->value; + } + + public static function markSafe($string) + { + return new IPF_Template_SafeString($string, true); + } +} \ No newline at end of file diff --git a/ipf/template/tag.php b/ipf/template/tag.php new file mode 100644 index 0000000..ed98a33 --- /dev/null +++ b/ipf/template/tag.php @@ -0,0 +1,10 @@ +context = $context; + } +} diff --git a/ipf/template/tag/url.php b/ipf/template/tag/url.php new file mode 100644 index 0000000..1f75995 --- /dev/null +++ b/ipf/template/tag/url.php @@ -0,0 +1,11 @@ +