This commit is contained in:
2024-12-31 11:07:09 +01:00
parent df7915205d
commit e089172b15
1916 changed files with 165422 additions and 271 deletions

View File

@ -0,0 +1,612 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Lexer;
/**
* ConditionLexer
*
* Tokens:
* -------
* and : 'AND'
* or : 'OR'
* quotedval : quotes ~(quotes)* quotes
* literal : ~(whitespace | quotes)+
* ident : ('a'..'z' | 'A'..'Z' | '_' | '\-' | '\.')+
* quotes : '\'' | '\"'
* comma : ','
* l_paren : '('
* r_paren : ')'
*
* negate_op : '!'
* equals : '=' | 'equals'
* contains : '*=' | 'contains'
* contains_any : 'containsAny'
* contains_all : 'containsAll'
* contains_only : 'containsOnly'
* ends_with : '$=' | 'endsWith'
* starts_with : '^=' | 'startsWith'
* lt : '<' | 'lt' | 'lowerThan'
* lte : '<=' | 'lte' | 'lowerThanEqual'
* gt : '>' | 'gt' | 'greaterThan'
* gte : '>=' | 'gte' | 'greaterThanEqual'
* empty : 'empty'
*
* param : '--' . ident
* whitespace : ' ' | '\r' | '\n' | '\t'
*/
class ConditionLexer extends Lexer
{
/**
* ConditionLexer constructor
*
* @param string $input
*/
public function __construct($input)
{
parent::__construct($input);
// single char tokens
$this->tokens->addType('comma');
$this->tokens->addType('quote');
$this->tokens->addType('dquote');
$this->tokens->addType('l_paren');
$this->tokens->addType('r_paren');
// operators
$this->tokens->addType('negate_op');
$this->tokens->addType('equals');
$this->tokens->addType('contains');
$this->tokens->addType('contains_all');
$this->tokens->addType('contains_any');
$this->tokens->addType('contains_only');
$this->tokens->addType('ends_with');
$this->tokens->addType('starts_with');
$this->tokens->addType('lt');
$this->tokens->addType('gt');
$this->tokens->addType('lte');
$this->tokens->addType('gte');
$this->tokens->addType('empty');
// logical operators
$this->tokens->addType('and');
$this->tokens->addType('or');
// values/literals/identifiers/parameters
$this->tokens->addType('quotedvalue');
$this->tokens->addType('literal');
$this->tokens->addType('ident');
$this->tokens->addType('param');
}
/**
* Returns the next token from the input string
*
* @return NRFramework\Parser\Token
* @throws Exception
*/
public function nextToken()
{
while ($this->cur !== Lexer::EOF)
{
if (preg_match('/\s+/', $this->cur))
{
$this->whitespace();
continue;
}
switch ($this->cur)
{
// match tokens from single char predictions
case ',':
return $this->comma();
case "'":
return $this->quotedValue("'");
case '"':
return $this->quotedValue('"');
case '=':
return $this->equals();
case '!':
return $this->negate_op();
case '*':
return $this->contains();
case '$':
return $this->ends_with();
case '^':
return $this->starts_with();
case '<':
return $this->lt_or_lte();
case '>':
return $this->gt_or_gte();
case '(':
return $this->l_paren();
case ')':
return $this->r_paren();
case '-':
$this->mark();
$next_chars = $this->consume(2);
if ($next_chars === '--')
{
$this->reset();
return $this->param();
}
$this->reset();
// match other tokens
default:
if (!$this->isValidChar())
{
throw new Exceptions\SyntaxErrorException('Invalid character: ' . $this->cur);
}
$token = null;
// try to match literal operators
$token = $this->literal_ops();
if($token)
{
return $token;
}
// try to match boolean operators
$token = $this->_and();
if($token)
{
return $token;
}
$token = $this->_or();
if($token)
{
return $token;
}
// if we get here the token is certainly a literal
$pos = $this->index;
$token = $this->literal();
if ($token)
{
// check if the literal also qualifies to be an identifier
if ($this->isValidIdentifier($token->text))
{
$token = $this->tokens->create('ident', $token->text, $pos);
}
return $token;
}
return null;
}
}
return $this->tokens->create('EOF', '<EOF>', -1);
}
/**
* Checks if a string qualifies to be an identifier
*
* @return bool
*/
protected function isValidIdentifier($text)
{
$ident_regex = '/(^[a-zA-Z\_]{1}$)|(^[a-zA-Z\_](?=([\w\-\.]*))([\w\-\.]*))/';
return preg_match($ident_regex, $text);
}
/**
* Check if the current character is valid for
* some matching rules (and, or, literal, ident)
*
* @return boolean
*/
protected function isValidChar()
{
$r = '/[^\s\'\",=\!\(\)\~\*\<\>\$\^]/';
return preg_match($r, $this->cur);
}
/**
* literal : ~(whitespace | quotes)+ //one or more chars except whitespace and quotes
*
* @return Token|void
*/
protected function literal()
{
$pos = $this->index;
$buf = '';
do
{
if (!$this->isValidChar())
{
break;
}
$buf .= $this->cur;
$this->consume();
}
while ($this->cur !== Lexer::EOF);
if (strlen($buf) > 0)
{
return $this->tokens->create('literal', $buf, $pos);
}
}
/**
* and : 'AND'
*
* @return Token|void
*/
protected function _and()
{
$pos = $this->index;
$this->mark();
$buf = '';
$buf .= $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if (preg_match('/and/', strtolower($buf)))
{
return $this->tokens->create('and', trim($buf), $pos);
}
$this->reset();
}
/**
* or : 'OR'
*
* @return Token|void
*/
public function _or()
{
$pos = $this->index;
$this->mark();
$buf = '';
$buf .= $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if (preg_match('/or/', strtolower($buf)))
{
return $this->tokens->create('or', trim($buf), $pos);
}
$this->reset();
}
/**
* quotedval : quotes ~(quotes)* quotes
*
* @return Token|void
* @throws Exception
*/
protected function quotedValue($q)
{
$pos = $this->index;
$otherQuote = $q === '"' ? "'" : '"';
$quote_queue = [];
$buf = '';
$quote_queue[] = $q;
$this->consume();
while (!empty($quote_queue))
{
if ($this->cur === Lexer::EOF)
{
throw new Exceptions\SyntaxErrorException('Missing quote at: ' . $buf);
}
if ($this->cur === end($quote_queue))
{
array_pop($quote_queue);
// if it's not the opening quote
if (!empty($quote_queue))
{
$buf .= $this->cur;
}
}
else if ($this->cur === $otherQuote)
{
array_push($quote_queue, $otherQuote);
$buf .= $otherQuote;
}
else
{
$buf .= $this->cur;
}
$this->consume();
}
return $this->tokens->create('quotedvalue', $buf, $pos);
}
/**
* param : '--' . ident
*
* @return Token|void
*/
protected function param()
{
$pos = $this->index;
$this->mark();
$buf = '';
$buf .= $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '--')
{
$buf = '';
do
{
if (!$this->isValidChar())
{
break;
}
$buf .= $this->cur;
$this->consume();
}
while ($this->cur !== Lexer::EOF);
if (strlen($buf) > 0 && $this->isValidIdentifier($buf))
{
return $this->tokens->create('param', $buf, $pos);
}
}
$this->reset();
}
/**
* equals : '='
*
* @return Token|void
*/
protected function equals()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('equals', "=", $pos);
}
protected function negate_op()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('negate_op', "!", $pos);
}
/**
* comma : ','
*
* @return Token
*/
protected function comma()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('comma', ",", $pos);
}
/**
* l_paren : '('
*/
protected function l_paren()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('l_paren', '(', $pos);
}
/**
* r_paren : ')'
*/
protected function r_paren()
{
$pos = $this->index;
$this->consume();
return $this->tokens->create('r_paren', ')', $pos);
}
/**
* contains: '*='
*
* @return Token|void
*/
protected function contains()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '*=')
{
return $this->tokens->create('contains', "*=", $pos);
}
$this->reset();
}
/**
* contains_word: '~='
*
* @return Token|void
*/
protected function contains_word()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '~=')
{
return $this->tokens->create('contains_word', "~=", $pos);
}
$this->reset();
}
/**
* ends_with: '$='
*
* @return Token|void
*/
protected function ends_with()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '$=')
{
return $this->tokens->create('ends_with', "$=", $pos);
}
$this->reset();
}
/**
* starts_with: '$='
*
* @return Token|void
*/
protected function starts_with()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '^=')
{
return $this->tokens->create('starts_with', "^=", $pos);
}
$this->reset();
}
/**
* lt_or_lte: '<' | '<='
*
* @return Token|void
*/
protected function lt_or_lte()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '<=')
{
return $this->tokens->create('lte', "<=", $pos);
}
else
{
$this->reset();
$this->consume();
return $this->tokens->create('lt', '<', $pos);
}
$this->reset();
}
/**
* gt_or_gte: '>' | '>='
*
* @return Token|void
*/
protected function gt_or_gte()
{
$pos = $this->index;
$this->mark();
$buf = $this->cur;
$this->consume();
$buf .= $this->cur;
$this->consume();
if ($buf === '>=')
{
return $this->tokens->create('gte', ">=", $pos);
}
else
{
$this->reset();
$this->consume();
return $this->tokens->create('gt', '>', $pos);
}
$this->reset();
}
/**
* Literal Operators predictor
*
* @return Token|null
*/
protected function literal_ops()
{
$pos = $this->index;
$this->mark();
$lit = $this->literal();
if ($lit)
{
switch (strtolower($lit->text))
{
case 'equals':
return $this->tokens->create('equals', $lit->text, $pos);
case 'startswith':
return $this->tokens->create('starts_with', $lit->text, $pos);
case 'endswith':
return $this->tokens->create('ends_with', $lit->text, $pos);
case 'contains':
return $this->tokens->create('contains', $lit->text, $pos);
case 'containsall':
return $this->tokens->create('contains_all', $lit->text, $pos);
case 'containsany':
return $this->tokens->create('contains_any', $lit->text, $pos);
case 'containsonly':
return $this->tokens->create('contains_only', $lit->text, $pos);
case 'lt':
case 'lowerthan':
return $this->tokens->create('lt', $lit->text, $pos);
case 'lte':
case 'lowerthanequal':
return $this->tokens->create('lte', $lit->text, $pos);
case 'gt':
case 'greaterthan':
return $this->tokens->create('gt', $lit->text, $pos);
case 'gte':
case 'greaterthantequal':
return $this->tokens->create('gte', $lit->text, $pos);
case 'empty':
return $this->tokens->create('empty', $lit->text, $pos);
}
}
$this->reset();
}
}

View File

@ -0,0 +1,358 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Parser;
use NRFramework\Parser\ConditionLexer;
/**
* ConditionParser
* LL(1) recursive-decent parser
* Uses NRFramework\Parser\ConditionLexer as input source
*
* Grammar:
* --------
* expr : condition (logic_op condition)* (option)*
* condition : {negate_op} alias (parameter)* | (alias|l_func) ({negate_op}? operator (values)? (parameter)*
* alias : {ident}
* values : value ({comma} value)*
* value : {quotedval} | ({literal} | {ident})+
* func : {ident} {l_paren} values {r_paren}
* l_func : func
* r_func : func
* parameter : {param} ({equals} value)?
* option : {ident} ({equals} value)?
* logic_op : {and} | {or}
* operator : {equals} | {starts_with} | {ends_with} | {empty} | {contains} | {contains_any} | {contains_all}| {contains_only} | {lt} | {lte} | {gt} | {gte}
*/
class ConditionParser extends Parser
{
/**
* Constructor
*
* @param ConditionLexer $input
*/
public function __construct(ConditionLexer $input)
{
parent::__construct($input, 2);
}
/**
* value : {quotedval} | ({literal} | {ident})+
*
* @return string
* @throws Exception
*/
public function value()
{
if ($this->lookahead[0]->type === 'quotedvalue')
{
$text = $this->lookahead[0]->text;
$this->match('quotedvalue');
return $text;
}
else if ($this->lookahead[0]->type !== 'ident' && $this->lookahead[0]->type !== 'literal')
{
throw new \Exception("Syntax error in ConditionParser::value(); expecting 'ident' or 'literal'; found {$this->lookahead[0]}");
}
$text = $this->lookahead[0]->text;
$this->consume();
while ($this->lookahead[0]->type === 'ident' || $this->lookahead[0]->type === 'literal')
{
$text .= ' ' . $this->lookahead[0]->text;
$this->consume();
}
return $text;
}
/**
* values : value ({comma} value)*
*
* @return array
*/
public function values()
{
$vals = [];
$vals[] = $this->value();
while ($this->lookahead[0]->type === 'comma')
{
$this->consume();
$vals[] = $this->value();
}
return $vals;
}
/**
* func : {ident} {l_paren} values {r_paren}
*
*/
public function func()
{
$func_name = $this->lookahead[0]->text;
$this->match('ident');
$this->match('l_paren');
if ($this->lookahead[0]->type === 'quotedvalue' ||
$this->lookahead[0]->type === 'ident' ||
$this->lookahead[0]->type === 'literal')
{
$func_args = $this->values();
}
$this->match('r_paren');
return ['func_name' => $func_name, 'func_args' => $func_args ?? []];
}
/**
* parameter : {param} ({equals} value)?
*
* @return string
*/
public function param()
{
$param = $this->lookahead[0]->text;
$value = true;
$this->match('param');
// If this is the 'context' parameter make sure that it appears as the last token
// if ($param === 'context')
// {
// $this->consume(); // consume the 'equals' operator
// $value = $this->value(); // expect a value
// if ($this->lookahead[0]->type !== 'EOF')
// {
// throw new \Exception("Syntax error in ConditionParser::param(); the 'context' parameter can only appear as the last token");
// }
// }
// else
if ($this->isOperator($this->lookahead[0]->type))
{
if ($this->lookahead[0]->type === 'equals')
{
$this->consume(); // consume the 'equals' operator
$value = $this->value(); // expect a value
}
else
{
// only the 'equals' operator is supported for the 'param' rule.
throw new \Exception("Syntax error in ConditionParser::param(); expecting 'equals', found {$this->lookahead[0]}");
}
}
return ['param' => $param, 'value' => $value];
}
/**
* alias : {ident}
*
* @return string
*/
public function alias()
{
$sel = $this->lookahead[0]->text;
$this->match('ident');
return $sel;
}
/**
* condition : {negate_op} alias (parameter)* | alias ({negate_op}? operator values)? (parameter)*
*
* @return object
*/
public function condition()
{
$result = [];
$operator = '';
$params = [];
$negate_op = false;
if ($this->lookahead[0]->type === 'negate_op')
{
$this->match('negate_op');
$operator = 'empty';
$result['alias'] = $this->alias();
}
else
{
if($this->lookahead[0]->type === 'ident' && $this->lookahead[1]->type === 'l_paren')
{
$l_func = $this->func();
$result['l_func_name'] = $l_func['func_name'];
$result['l_func_args'] = $l_func['func_args'];
}
else
{
$result['alias'] = $this->alias();
}
if ($this->lookahead[0]->type === 'negate_op')
{
$this->match('negate_op');
$negate_op = true;
// expect an operator after '!'
if (!$this->isOperator($this->lookahead[0]->type))
{
throw new Exceptions\SyntaxErrorException("Expecting an 'operator' after '!', found {$this->lookahead[0]}");
}
}
if ($this->isOperator($this->lookahead[0]->type))
{
$operator = $this->operator();
if($this->lookahead[0]->type === 'ident' && $this->lookahead[1]->type === 'l_paren')
{
$r_func = $this->func();
$result['r_func_name'] = $r_func['func_name'];
$result['r_func_args'] = $r_func['func_args'];
}
else if (
$this->lookahead[0]->type === 'quotedvalue' ||
$this->lookahead[0]->type === 'ident' ||
$this->lookahead[0]->type === 'literal'
)
{
$values = $this->values();
if (count($values) === 1)
{
$values = $values[0];
}
$result['values'] = $values;
}
}
}
while ($this->lookahead[0]->type === 'param')
{
$params[] = $this->param();
}
if (!$operator) {
$operator = 'empty';
$negate_op = true;
}
//
$_params = [];
foreach($params as $p)
{
$_params[$p['param']] = $p['value'];
}
$result['operator'] = $operator;
$result['negate_op'] = $negate_op;
$result['params'] = $_params;
return $result;
}
/**
* operator : {equals} | {starts_with} | {ends_with} | {empty} | {contains} | {contains_any} | {contains_all}| {contains_only} | {lt} | {lte} | {gt} | {gte}
*
* @return string
* @throws Exception
*/
public function operator()
{
if (!$this->isOperator($this->lookahead[0]->type))
{
throw new Exceptions\SyntaxErrorException("Expecting an 'operator', found " . $this->lookahead[0]);
}
$op = $this->lookahead[0]->type;
$this->consume();
return $op;
}
/**
* expr : condition ({logic_op} condition)* (option)*
*
* @return array The condition expression results
*/
public function expr()
{
$logic_op = 'and';
$res = [
'conditions' => [$this->condition()],
'logic_op' => 'and',
'context' => null,
'global_params' => []
];
if ($this->lookahead[0]->type === 'or')
{
$logic_op = 'or';
}
while ($this->lookahead[0]->type !== 'EOF')
{
$this->match($logic_op);
$res['conditions'][] = $this->condition();
}
$res['logic_op'] = $logic_op;
// check the last parsed condition for global parameters
$globalParams = [
'debug',
'dateformat',
'context',
'nopreparecontent',
'excludebots'
];
$last_params = $res['conditions'][count($res['conditions'])-1]['params'];
foreach(array_keys($last_params) as $param_key)
{
if (in_array(strtolower($param_key), $globalParams))
{
$res['global_params'][strtolower($param_key)] = $last_params[$param_key];
unset($res['conditions'][count($res['conditions'])-1]['params'][$param_key]);
}
}
// foreach ($last_params as $idx => $param)
// {
// if (in_array($param['param'], $globalParams))
// {
// $res['global_params'][$param['param']] = $param['value'];
// unset($res['conditions'][count($res['conditions'])-1]['params'][$idx]);
// }
// }
return $res;
}
/**
* Helper method that checks if the given Token is an operator.
*/
protected function isOperator($token_type)
{
return in_array($token_type, [
'equals',
'starts_with',
'ends_with',
'contains',
'contains_any',
'contains_all',
'contains_only',
'lt',
'lte',
'gt',
'gte',
'empty'
]);
}
}

View File

@ -0,0 +1,804 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use DateTime;
use DateTimeZone;
use Exception;
use Joomla\CMS\Factory;
class ConditionsEvaluator
{
/**
* Payload associative array
*
* @var array
*/
protected $payload;
/**
* Parsed conditions
*
* @var array
*/
protected $conditions;
/**
* Framework Condition aliases
*
* @var array
*/
protected $condition_aliases;
/**
* Debug flag
*
* @var bool
*/
protected $debug;
/**
* @param array $conditions The parsed conditions
* @param array $payload Shortcode parser payload
*/
public function __construct($conditions, $payload = null, $debug = false)
{
$this->conditions = $conditions;
$this->payload = $payload;
$this->debug = $debug;
$this->generateConditionAliasesMap();
}
/**
* @return array
*/
public function evaluate() : array
{
$results = [];
$caseSensitive = false;
foreach($this->conditions as $condition)
{
// case sensitivity param
if (array_key_exists('caseSensitive', $condition['params']))
{
$caseSensitive = strtolower($condition['params']['caseSensitive']) != 'false';
}
$result = [
'operator' => $condition['operator'],
'params' => $condition['params']
];
$l_value = null;
$r_value = null;
if(array_key_exists('r_func_name', $condition))
{
$r_value = $this->applyFunction($condition['r_func_name'], $condition['r_func_args']);
$result['r_func_name'] = $condition['r_func_name'];
$result['r_func_args'] = $condition['r_func_args'];
$result['r_func_val'] = $r_value;
}
else
{
$r_value = $condition['values'] ?? null;
}
if (array_key_exists('alias', $condition) && $this->isPayloadCondition($condition['alias']))
{
$l_value = $this->payload[$condition['alias']];
$result = array_merge($result, $this->evaluatePayloadCondition($l_value, $r_value, $condition['operator'], $caseSensitive));
$result['pass'] = $condition['negate_op'] ? !$result['pass'] : $result['pass'];
$result['actual_value'] = $this->payload[$condition['alias']];
}
else if (array_key_exists('alias', $condition) && $this->isFrameworkCondition($condition['alias']))
{
$result = array_merge($result, $this->evaluateFrameworkCondition($condition, $r_value));
}
else if (array_key_exists('l_func_name', $condition))
{
$l_value = $this->applyFunction($condition['l_func_name'], $condition['l_func_args']);
$result['l_func_name'] = $condition['l_func_name'];
$result['l_func_args'] = $condition['l_func_args'];
$result['l_func_val'] = $l_value;
$result = array_merge($result, $this->evaluatePayloadCondition($l_value, $r_value, $condition['operator'], $caseSensitive));
$result['pass'] = $condition['negate_op'] ? !$result['pass'] : $result['pass'];
}
// not a payload or framework condition with the 'empty' op
else if ($condition['operator'] === 'empty')
{
$result['pass'] = !$condition['negate_op'];
}
//
else
{
// Unknown condition
throw new Exceptions\InvalidConditionException($condition['alias']);
}
$results[] = $result;
}
return $results;
}
/**
*
*/
public function applyFunction($func_name, $args)
{
$arg_values = [];
foreach($args as $arg)
{
if ($this->isPayloadCondition($arg))
{
$arg_values[] = $this->payload[$arg];
}
else if ($this->isFrameworkCondition($arg))
{
$conditions_helper = \NRFramework\Conditions\ConditionsHelper::getInstance();
$framework_condition = $conditions_helper->getCondition($this->condition_aliases[strtolower($arg)]);
// Some framework condition don't implement the 'value()' method.
if (method_exists($framework_condition, 'value'))
{
$arg_values[] = $framework_condition->value();
}
else
{
throw new Exceptions\ConditionValueException($arg);
}
}
else
{
$arg_values[] = $arg;
}
}
switch(strtolower($func_name))
{
case 'count':
return $this->funcCount($arg_values);
case 'today':
return $this->funcToday();
case 'now':
return $this->funcNow();
case 'date':
return $this->funcDate($arg_values);
case 'datediff':
return $this->funcDateDiff($arg_values);
default:
throw new Exceptions\UnknownFunctionException($func_name);
}
}
/**
*
*/
public function funcCount($args)
{
if (count($args) !== 1)
{
throw new Exception("count() accepts 1 argument. " . count($args) . " were given.");
}
if (is_array($args[0]))
{
return count($args[0]);
}
else if (is_string($args[0]))
{
return mb_strlen($args[0]);
}
else
{
throw new Exception("count() accepts only strings and arrays.");
}
}
/**
*
*/
public function funcToday()
{
return (new DateTime('today'))->format('Y-m-d');
}
/**
*
*/
public function funcNow()
{
return new DateTime('now');
}
/**
*
*/
public function funcDate($args)
{
if (count($args) < 1 || count($args) > 3)
{
throw new Exception("date() accepts between 1 and 3 arguments. " . count($args) . " were given.");
}
if ($args[0] instanceof \DateTime || $args[0] instanceof \DateTimeImmutable)
{
return $args[0];
}
$date = $args[0];
$format = null;
if (count($args) > 1)
{
$format = $args[1] === 'null' ? null : $args[1];
}
$timezone = new \DateTimeZone($args[2] ?? Factory::getApplication()->get('offset','UTC'));
if ($format)
{
return \DateTime::createFromFormat('!'.$format, $date, $timezone);
}
return new \DateTime($date, $timezone);
}
/**
*
*/
public function funcDateDiff($args)
{
if (count($args) != 2)
{
throw new Exception("dateDiff() accepts 2 arguments. " . count($args) . " were given.");
}
$date1 = $this->convertToDateTime($args[0]);
$date2 = $this->convertToDateTime($args[1]);
return abs($date1->diff($date2)->days);
}
/**
* @var array $condition
*
* @return array Evaluation result
*/
protected function evaluatePayloadCondition($l_value, $r_value, $operator, $caseSensitive = false) : array
{
if (!$caseSensitive)
{
$l_value = $this->_lowercaseValues($l_value);
$r_value = $this->_lowercaseValues($r_value);
}
$result = [];
switch($operator)
{
case 'equals':
$result = $this->evaluateEquals($l_value, $r_value);
break;
case 'starts_with':
$result = $this->evaluateStartsWith($l_value, $r_value);
break;
case 'ends_with':
$result = $this->evaluateEndsWith($l_value, $r_value);
break;
case 'contains':
$result = $this->evaluateContains($l_value, $r_value);
break;
case 'contains_any':
$result = $this->evaluateContainsAny($l_value, $r_value);
break;
case 'contains_all':
$result = $this->evaluateContainsAll($l_value, $r_value);
break;
case 'contains_only':
$result = $this->evaluateContainsOnly($l_value, $r_value);
break;
case 'lt':
$result = $this->evaluateLessThan($l_value, $r_value);
break;
case 'lte':
$result = $this->evaluateLessThanEquals($l_value, $r_value);
break;
case 'gt':
$result = $this->evaluateGreaterThan($l_value, $r_value);
break;
case 'gte':
$result = $this->evaluateGreaterThanEquals($l_value, $r_value);
break;
case 'empty':
$result = $this->evaluateEmpty($l_value);
break;
default:
throw new Exceptions\UnknownOperatorException($operator);
}
return $result;
}
/**
* @var array $condition
*
* @return array Evaluation result
*/
public function evaluateFrameworkCondition($condition, $r_value)
{
$operator = $condition['operator'];
// Certain framework operators only work on single values.
// Force fail if the parsed condition contains more than one value.
if (in_array($operator, [
'contains',
'lt', 'lte',
'gt', 'gte',
'starts_with',
'ends_with'
]))
{
if (is_array($r_value) && !empty($r_value))
{
throw new Exceptions\UnsupportedValueOperandException($operator, false);
}
}
//
$conditions_helper = \NRFramework\Conditions\ConditionsHelper::getInstance();
$result = ['actual_value' => null];
// Transform 'caseSensitive' parameter to 'ignoreCase'
if (array_key_exists('caseSensitive', $condition['params']))
{
$condition['params']['ignoreCase'] = !$condition['params']['caseSensitive'];
}
// Instantiate the framework condition
$framework_condition = $conditions_helper->getCondition(
$this->condition_aliases[strtolower($condition['alias'])],
$r_value,
$operator,
$condition['params']
);
// Try to grab the actual condition's value if 'debug' is enabled.
if ($this->debug)
{
// Some framework conditions don't implement the 'value()' method.
if (method_exists($framework_condition, 'value'))
{
$result['actual_value'] = $framework_condition->value();
}
}
// Special handling for Date/Time framework conditions
if (in_array(strtolower($condition['alias']), ['date', 'time', 'datetime']))
{
$pass = $this->evaluatePayloadCondition($framework_condition->value(), $r_value, $operator)['pass'];
}
// Check if the condition passes using the 'passOne()' helper method
else
{
$pass = $conditions_helper->passOne(
$this->condition_aliases[strtolower($condition['alias'])],
$r_value,
$operator,
$condition['params']
);
}
$result['pass'] = $condition['negate_op'] ? !$pass : $pass;
return $result;
}
/**
* Generates an array mapping Condition aliases to Condition class names
*/
protected function generateConditionAliasesMap()
{
$conditions_namespace = 'NRFramework\\Conditions\\Conditions\\';
$dir_iterator = new \RecursiveDirectoryIterator(JPATH_PLUGINS . "/system/nrframework/NRFramework/Conditions/Conditions/");
$iterator = new \RecursiveIteratorIterator($dir_iterator, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file)
{
$condition_class = str_replace(JPATH_PLUGINS . "/system/nrframework/NRFramework/Conditions/Conditions/", '', $file);
$condition_class = str_replace('.php', '', $condition_class);
$condition_class = str_replace('/', '\\', $condition_class);
if (class_exists($conditions_namespace . $condition_class))
{
$this->condition_aliases[strtolower($file->getBasename('.php'))] = $condition_class;
if (property_exists($conditions_namespace . $condition_class, 'shortcode_aliases'))
{
foreach(($conditions_namespace . $condition_class)::$shortcode_aliases as $alias)
{
$this->condition_aliases[$alias] = $condition_class;
}
}
}
}
}
/**
*
*/
protected function convertToDateTime($date, $format = null, $tz = null)
{
if ($tz == null)
{
$tz = Factory::getApplication()->getCfg('offset','UTC');
}
if ($date instanceof \DateTime || $date instanceof \DateTimeImmutable)
{
return $date;
}
try
{
if ($format)
{
return DateTime::createFromFormat($format, $date, $tz);
}
return new DateTime($date, new DateTimeZone($tz));
}
catch (\Throwable $t)
{
return null;
}
}
/**
*
*/
protected function isPayloadCondition($alias)
{
return $this->payload && array_key_exists($alias, $this->payload);
}
/**
*
*/
protected function isFrameworkCondition($alias)
{
return array_key_exists(strtolower($alias), $this->condition_aliases);
}
/**
* @return array Evaluation result
*/
protected function evaluateEquals($l_value, $r_value) : array
{
// are we comparing arrays?
if (is_array($l_value))
{
return $this->evaluateContainsAny($l_value, $r_value);
}
if (is_numeric($r_value))
{
return ['pass' => $l_value == $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date == $r_date
];
}
// generic equality test
return ['pass' => $l_value == $r_value];
}
/**
* @return array Evaluation result
*/
protected function evaluateStartsWith($l_value, $r_value) : array
{
if (!is_string($l_value))
{
throw new Exceptions\UnsupportedOperatorException('startsWith', $l_value, false);
}
if (!is_string($r_value))
{
throw new Exceptions\UnsupportedValueOperandException('startsWith', false);
}
return ['pass' => $this->_starts_with($l_value, $r_value)];
}
/**
* @return array Evaluation result
*/
protected function evaluateEndsWith($l_value, $r_value) : array
{
if (!is_string($l_value))
{
throw new Exceptions\UnsupportedOperatorException('endsWith', $l_value, false);
}
if (!is_string($r_value))
{
throw new Exceptions\UnsupportedValueOperandException('endsWith', false);
}
return ['pass' => $this->_ends_with($l_value, $r_value)];
}
/**
* @return array Evaluation result
*/
protected function evaluateContains($l_value, $r_value) : array
{
if (!is_string($l_value))
{
throw new Exceptions\UnsupportedOperatorException('contains', $l_value, false);
}
if (!is_string($r_value))
{
throw new Exceptions\UnsupportedValueOperandException('contains', false);
}
return ['pass' => strlen($l_value) > 0 && strpos($l_value, $r_value) !== false];
}
/**
* @return array Evaluation result
*/
protected function evaluateContainsAny($l_value, $r_value) : array
{
if (!is_array($l_value))
{
throw new Exceptions\UnsupportedOperatorException('containsAny', $l_value, true);
}
$r_value = (array) $r_value;
return ['pass' => !empty(array_intersect($l_value, $r_value))];
}
/**
* @return array Evaluation result
*/
protected function evaluateContainsAll($l_value, $r_value) : array
{
if (!is_array($l_value))
{
throw new Exceptions\UnsupportedOperatorException('containsAll', $l_value, true);
}
$r_value = (array) $r_value;
return ['pass' => count(array_intersect($l_value, $r_value)) == count($r_value)];
}
/**
* @return array Evaluation result
*/
protected function evaluateContainsOnly($l_value, $r_value) : array
{
if (!is_array($l_value))
{
throw new Exceptions\UnsupportedOperatorException('containsOnly', $l_value, true);
}
$r_value = (array) $r_value;
return ['pass' => count(array_diff($l_value, $r_value)) == 0];
}
/**
* @return array Evaluation result
*/
protected function evaluateLessThan($l_value, $r_value) : array
{
if (is_numeric($r_value))
{
return ['pass' => $l_value < $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date < $r_date
];
}
throw new Exceptions\SyntaxErrorException("The 'lessThan' operator accepts only numeric values and dates.");
}
/**
* @return array Evaluation result
*/
protected function evaluateLessThanEquals($l_value, $r_value) : array
{
if (is_numeric($r_value))
{
return ['pass' => $l_value <= $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date <= $r_date
];
}
throw new Exceptions\SyntaxErrorException("The 'lessThanEquals' operator accepts only numeric values and dates.");
}
/**
* @return array Evaluation result
*/
protected function evaluateGreaterThan($l_value, $r_value) : array
{
if (is_numeric($r_value))
{
return ['pass' => $l_value > $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date > $r_date
];
}
throw new Exceptions\SyntaxErrorException("The 'greaterThan' operator accepts only numeric values and dates.");
}
/**
* @return array Evaluation result
*/
protected function evaluateGreaterThanEquals($l_value, $r_value) : array
{
if (is_numeric($r_value))
{
return ['pass' => $l_value >= $r_value];
}
// check if we are comparing dates
$l_date = $this->convertToDateTime($l_value);
$r_date = $this->convertToDateTime($r_value);
if($l_date && $r_date)
{
if (is_string($r_value) && !preg_match("/\d{1,2}:\d{1,2}(:\d{1,2})?/", $r_value))
{
$l_date->setTime(0,0);
$r_date->setTime(0,0);
}
return [
'l_eval' => $l_date,
'r_eval' => $r_date,
'pass' => $l_date >= $r_date
];
}
throw new Exceptions\SyntaxErrorException("The 'greaterThanEquals' operator accepts only numeric values and dates.");
}
/**
* @return array Evaluation result
*/
protected function evaluateEmpty($payload_value) : array
{
// $payload_value = $this->payload[$payload_key];
if (is_array($payload_value))
{
return ['pass' => empty($payload_value)];
}
else if(is_string($payload_value))
{
$payload_value = trim($payload_value);
return ['pass' => empty($payload_value) || $payload_value == 'false'];
}
else if(is_bool($payload_value))
{
return ['pass' => !$payload_value];
}
return ['pass' => is_null($payload_value)];
}
/**
* @return bool
*/
protected function _starts_with($haystack, $needle)
{
return strlen($needle) > 0 && strncmp($haystack, $needle, strlen($needle)) === 0;
}
/**
* @return bool
*/
protected function _ends_with($haystack, $needle)
{
return strlen($needle) > 0 && substr($haystack, -strlen($needle)) === (string)$needle;
}
/**
* @return bool
*/
protected function _contains($haystack, $needle)
{
return strlen($needle) > 0 && strpos($haystack, $needle) !== false;
}
/**
* @return string|array
*/
protected function _lowercaseValues($value)
{
if (is_array($value))
{
foreach($value as $idx => $val)
{
if (is_string($val))
{
$value[$idx] = strtolower($val);
}
}
}
else if(is_string($value))
{
$value = strtolower($value);
}
return $value;
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class ConditionValueException extends \Exception
{
public function __construct($condition_name)
{
parent::__construct("008 - Condition Value Error: The Condition '" . $condition_name . "' does not return a value.");
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class InvalidConditionException extends \Exception
{
public function __construct($condition_name)
{
parent::__construct("002 - Invalid Condition: The condition '" . $condition_name . "' does not exist.");
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class SyntaxErrorException extends \Exception
{
public function __construct($message)
{
parent::__construct("001 - Syntax Error: " . $message);
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class UnknownFunctionException extends \Exception
{
public function __construct($func_name)
{
parent::__construct("007 - Unknown Function: " . $func_name);
}
}

View File

@ -0,0 +1,20 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class UnknownOperatorException extends \Exception
{
public function __construct($operator)
{
parent::__construct("003 - Unknown Comparison Operator: " . $operator);
}
}

View File

@ -0,0 +1,21 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class UnsupportedOperatorException extends \Exception
{
public function __construct($operator, $condition_name, $accepts_multi_values)
{
$message = 'The Comparison Operator "' . $operator . '" can only be used with Condition Operands that return ' . ($accepts_multi_values ? 'multiple values.' : 'single values.');
parent::__construct("005 - Unsupported Comparison Operator: " . $message);
}
}

View File

@ -0,0 +1,21 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser\Exceptions;
defined('_JEXEC') or die;
class UnsupportedValueOperandException extends \Exception
{
public function __construct($operator, $accepts_multi_values)
{
$message = 'The Comparison Operator "' . $operator . '" can only be used with ' . ($accepts_multi_values ? 'multiple values.' : 'single values.');
parent::__construct("006 - Unsupported Value Operand: " . $message);
}
}

View File

@ -0,0 +1,215 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Tokens;
/**
* Lexer base class
*
* TODO: Rename to Tokenizer??
*/
abstract class Lexer
{
/**
* EOF character
*/
const EOF = -1;
/**
* Tokens instance
*
* @var NRFramework\Parser\Tokens
*/
protected $tokens = null; // Tokens instance
/**
* Input string
*
* @var string
*/
protected $input;
/**
* Input string length
*/
protected $length;
/**
* The index of the current character
* in the input string
*
* @var integer
*/
protected $index = 0;
/**
* Current character in input string
*
* @var string
*/
protected $cur;
/**
* A Mark(position) inside the input string.
* Used when matching ahead of the 'current' character
*
* @var integer
*/
protected $mark = 0;
/**
* Holds the Lexer's state
*
* @var object
*/
protected $state;
/**
* Lexer constructor
*
* @param string $input
*/
public function __construct($input)
{
$this->input = $input;
$this->length = strlen($input);
$this->cur = $this->length >= 1 ? $this->input[0] : Lexer::EOF;
$this->tokens = new Tokens();
// inititalize state
$this->state = new \StdClass();
$this->state->skip_whitespace = true;
$this->state->tokenize_content = true;
}
/**
* Returns the next token from the input string.
*
* @return NRFramework\Parser\Token
*/
abstract function nextToken();
/**
* Moves n characters ahead in the input string.
* Returns all n characters.
* Detects "end of file".
*
* @param integer $n Number of characters to advance
* @return string The n previous characters
*/
public function consume($n = 1)
{
$prev = '';
for ($i=0; $i < $n; $i++)
{
$prev .= $this->cur;
if ( ($this->index + 1) >= $this->length)
{
$this->cur = Lexer::EOF;
break;
}
else
{
$this->index++;
$this->cur = $this->input[$this->index];
}
}
return $prev;
}
/**
* Sets the skip_whitespce state
*
* @param boolean $skip
* @return void
*/
public function setSkipWhitespaceState($skip = true)
{
$this->state->skip_whitespace = $skip;
}
/**
* Sets the tokenize_content state
*
* @param bool
* @return void
*/
public function setTokenizeContentState($state = true)
{
$this->state->tokenize_content = $state;
}
/**
* Gets the tokenize_content state
*
* @param bool
* @return bool
*/
public function getTokenizeContentState()
{
return $this->state->tokenize_content;
}
/**
* Marks the current index
*
* @return void
*/
public function mark()
{
$this->mark = $this->index;
}
/**
* Reset index to previously marked position (or at the start of the stream if not marked)
*
* @return void
*/
public function reset()
{
$this->index = $this->mark;
$this->cur = $this->input[$this->index];
$this->mark = 0;
}
/**
* Get the token types array from the Tokens instance
*
* @return void
*/
public function getTokensTypes()
{
return $this->tokens->getTypes();
}
/**
* Returns the current position in the input stream
*
* @return integer
*/
public function getStreamPosition()
{
return $this->index;
}
/**
* whitespace : (' '|'\t'|'\n'|'\r')
* Ignores any whitespace while advancing
* @return null
*/
protected function whitespace()
{
while (preg_match('/\s+/', $this->cur)) $this->consume();
}
}

View File

@ -0,0 +1,152 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Lexer;
use NRFramework\Parser\RingBuffer;
/**
* Parser base class
* LL(k) recursive-decent parser with backtracking support
*/
abstract class Parser
{
/**
* Lexer instance (feeds the parser with tokens)
*
* @var NRFramework\Parser\Lexer
*/
protected $input = null;
/**
* Ring buffer of the next k tokens
* from the input stream
*
* @var RingBuffer
*/
protected $lookahead = null;
/**
* k: Number of lookahead tokens
*
* @var int
*/
protected $k;
/**
* Array(stack) containing the current
* contents of the lookahead buffer when
* marking the position of the stream
*
* @var array
*/
protected $lookahead_history = null;
/**
* Lexer constructor
*
* @param Lexer $input
* @param integer $k, number of lookahead tokens
*/
public function __construct(Lexer $input, $k = 1)
{
if (!is_integer($k) || ($k < 1))
{
throw new \InvalidArgumentException('Parser: $k must be greater than 0!');
}
$this->k = $k;
$this->input = $input;
$this->lookahead_history = [];
// initialize lookahead buffer
$this->resetBuffer();
}
/**
* Checks the type of the next token.
* Advances the position in the token stream.
*
* @param string $type
* @return void
*
* @throws Exception
*/
public function match($type)
{
if ($this->lookahead[0]->type === $type)
{
$this->consume();
return;
}
throw new Exceptions\SyntaxErrorException('Expecting token ' . $type . ', found ' . $this->lookahead[0]);
}
/**
* Retrieves the next token from the input stream
* and add it to the buffer.
*
* @return void
*/
public function consume()
{
$this->lookahead[] = $this->input->nextToken();
}
/**
* Marks the position in the token stream
*
* @return void
*/
public function mark()
{
array_push($this->lookahead_history, $this->lookahead);
$this->input->mark();
}
/**
* Reset to a previously marked position
* in the token stream
*
* @return void
*/
public function reset()
{
$this->input->reset();
// reset lookahead buffer if not marked
if (empty($this->lookahead_history))
{
$this->resetBuffer();
}
// normal reset
else
{
$this->lookahead = array_pop($this->lookahead_history);
}
}
/**
* Resets and refills the lookahead buffer starting
* from the current position in the token stream
*
* @return void
*/
protected function resetBuffer()
{
$this->lookahead = new RingBuffer($this->k);
for ($i=0; $i < $this->k; $i++)
{
$this->consume();
}
}
}

View File

@ -0,0 +1,227 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
/**
* RingBuffer
*
* A circular buffer of fixed length.
* This class essentially implements a fixed-size FIFO stack but with
* "convenient" accessor methods compared to manually handling a vanilla PHP array.
*
* Used by NRFramework\Parser\Parser and NRFramework\Parser\Lexer.
*/
class RingBuffer implements \Countable, \ArrayAccess, \Iterator
{
/**
* Iterator position
* @var int
*/
protected $iterator_position = 0;
/**
* Position of the next element
* @var integer
*/
protected $position = 0;
/**
* Contents buffer
* @var \SplFixedArray
*/
protected $buffer;
/**
* Size of the ring buffer
* @var int
*/
protected $size;
/**
* RingBuffer constructor
*
* Handles arguments through 'func_get_args' (gotta love PHP)
*
* @param int $size Size of the ring buffer
* @param array $val Initial values
*/
public function __construct()
{
//argument checks
$argv = func_get_args();
$argc = count($argv);
switch($argc)
{
case 1:
// array
if (is_array($argv[0]))
{
$this ->size = count($argv[0]);
$this->buffer = \SplFixedArray::fromArray($argv[0]);
}
// size
else if (is_numeric($argv[0]))
{
if ($argv[0] < 1)
{
throw new \InvalidArgumentException('RingBuffer ctor: size must be greater than zero');
}
$size = (integer)$argv[0];
$this->buffer = \SplFixedArray::fromArray(array_fill(0, $size, null));
$this->size = $size;
}
else
{
throw new \InvalidArgumentException("RingBuffer ctor: arguments must be an array ,a numeric size or both");
}
break;
case 2:
if(is_array($argv[0]) && is_numeric($argv[1]))
{
if ($argv[1] < 1)
{
throw new \InvalidArgumentException('RingBuffer ctor: size must be greater than zero');
}
$arr_size = count($argv[0]);
$size = (integer)$argv[1];
if ($arr_size == $size)
{
$this->buffer = \SplFixedArray::fromArray($argv[0]);
$this->size = $size;
}
else if ($arr_size > $size)
{
$this->buffer = \SplFixedArray::fromArray(array_slice($argv[0], 0, $size));
$this->size = $size ;
}
else // $arr_size < $size
{
$this->buffer = \SplFixedArray::fromArray(array_merge($argv[0], array_fill(0, $size - $arr_size, null)));
$this->size = $size ;
$this->position = $arr_size ;
}
}
else
{
throw new \InvalidArgumentException("RingBuffer ctor: arguments must be an array ,a numeric size or both");
}
break;
default:
throw new \InvalidArgumentException('RingBuffer ctor: no arguments given');
}
}
/**
* Returns the internal buffer as an array
*
* @return \SplFixedArray
*/
public function buffer()
{
return $this->buffer;
}
/**
* 'Countable' interface methods
*/
/**
* Returns the size of the buffer
*
* @return int
*/
public function count() : int
{
return $this->size;
}
/**
* 'ArrayAccess' interface methods
*/
protected function offsetOf($offset)
{
return ($this->position + $offset) % $this->size;
}
public function offsetExists($offset) : bool
{
return ($offset >= 0) && ($offset < $this->size);
}
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
// if (!$this->offsetExists($offset))
if (($offset < 1) && ($offset >= $this->size))
{
throw new \OutOfBoundsException("RingBuffer: invalid offset $offset.");
}
return $this->buffer[($this->position + $offset) % $this->size];
}
public function offsetUnset($offset) : void
{
if (!$this->offsetExists($offset))
{
throw new \OutOfBoundsException("RingBuffer: invalid offset $offset.");
}
$this->buffer[$this->offsetOf($offset)] = null;
}
public function offsetSet($offset, $value) : void
{
if ($offset === null)
{
$this->buffer[$this->position] = $value;
$this->position = ($this->position + 1)%$this->size;
}
else if ($this->offsetExists($offset))
{
$this->buffer[$this->offsetOf($offset)] = $value;
}
else
{
throw new \OutOfBoundsException("RingBuffer: invalid offset $offset.");
}
}
/**
* 'Iterator' interface methods
*/
public function rewind() : void
{
$this->iterator_position = 0;
}
public function current() : mixed
{
return $this->buffer[$this->offsetOf($this->iterator_position)];
}
public function key() : mixed
{
return $this->iterator_position;
}
public function next() : void
{
$this->iterator_position++;
}
public function valid() : bool
{
return ($this->iterator_position >= 0) && ($this->iterator_position < $this->size);
}
}

View File

@ -0,0 +1,359 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Lexer;
/**
* ShortcodeLexer
*
* Tokenizes a string using the following grammar.
* Acts as the input "stream" for NRFramework\Parser\ShortcodeParser
*
* Tokens:
* -------
* sc_open : shortcode tag opening character(s), default: {
* sc_close : shortcode tag closing character(s), default: }
* if_keyword : if keyword, default: 'if'
* endif_keyword : endif keyword, default: '/if'
* text : any character sequence
* text_preserved : any character sequence with quoted values preserved
* whitespace : ' ' | '\r' | '\n' | '\t'
*/
class ShortcodeLexer extends Lexer
{
/**
* Shortcode opening character(s) (default: {)
*
* @var string
*/
protected $sc_open_char;
/**
* Shortcode closing character(s) (default: })
*
* @var string
*/
protected $sc_close_char;
/**
* if keyword (default: 'if')
*
* @var string
*/
protected $if_keyword;
/**
* endif keyword (default: '/if')
*
* @var string
*/
protected $endif_keyword;
/**
* ShortcodeLexer constructor
*
* @param string $input
* @param object $options
*/
public function __construct($input, $options = null)
{
parent::__construct($input);
$this->tokens->addType('sc_open');
$this->tokens->addType('sc_close');
$this->tokens->addType('if_keyword');
$this->tokens->addType('endif_keyword');
$this->tokens->addType('char');
$this->sc_open_char = $options->tag_open_char ?? '{';
$this->sc_close_char = $options->tag_close_char ?? '}';
$this->if_keyword = $options->if_keyword ?? 'if';
$this->endif_keyword = '/' . $this->if_keyword;
}
/**
* Returns the next token from the input string
*
* @return RestrictContent\Parser\Token
*/
public function nextToken()
{
static $if_flag = false;
while ($this->cur !== Lexer::EOF)
{
if ($this->state->skip_whitespace && preg_match('/\s+/', $this->cur))
{
$this->whitespace();
continue;
}
if ($this->predictScOpen())
{
$this->setTokenizeContentState(false);
$this->setSkipWhitespaceState(true);
return $this->sc_open();
}
else if ($this->predictScClose())
{
$this->setTokenizeContentState(true);
$this->setSkipWhitespaceState(false);
return $this->sc_close();
}
// check for if/endif
else if ($this->predictIf(false))
{
return $this->_if();
}
else if ($this->predictEndif(false))
{
return $this->_endif();
}
// check for text
else {
$preserve_quoted_values = !$this->getTokenizeContentState();
$token = $this->text($preserve_quoted_values);
return $token;
}
}
// return EOF token at the end of stream
return $this->tokens->create('EOF', '<EOF>', -1);
}
/**
* Predicts an upcoming 'if' keyword from the input stream
*
* @param bool $reset Reset to the marked position when the keyword is found
* @return bool
*/
protected function predictIf($reset = true)
{
$this->mark();
$tmp = $this->consume(2);
if ($tmp === $this->if_keyword)
{
if ($reset)
{
$this->reset();
}
return true;
}
$this->reset();
return false;
}
/**
* Predicts an upcoming 'endif' keyword from the input stream
*
* @param bool $reset Reset to the marked position when the keyword is found
* @return bool
*/
protected function predictEndif($reset = true)
{
$this->mark();
$tmp = $this->consume(3);
if ($tmp === $this->endif_keyword)
{
if ($reset)
{
$this->reset();
}
return true;
}
$this->reset();
return false;
}
/**
* Predicts any upcoming keyword
*
* @return bool
*/
protected function predictKeywords()
{
return $this->predictIf() || $this->predictEndif();
}
/**
* Predicts any upcoming special character
*
* @return bool
*/
protected function predictSpecialChars()
{
return $this->predictScOpen() || $this->predictScClose();
}
/**
* Predicts upcoming shortcode opening character(s), default: {
*
* @return bool
*/
protected function predictScOpen()
{
$sc_length = \strlen($this->sc_open_char);
$res = false;
$this->mark();
$tmp = $this->consume($sc_length);
if ($tmp === $this->sc_open_char)
{
$res = true;
}
$this->reset();
return $res;
}
/**
* Predicts upcoming shortcode closing character(s), default: {
*
* @return bool
*/
protected function predictScClose()
{
$sc_length = \strlen($this->sc_close_char);
$res = false;
$this->mark();
$tmp = $this->consume($sc_length);
if ($tmp === $this->sc_close_char)
{
$res = true;
}
$this->reset();
return $res;
}
/**
* sc_open : shortcode tag opening character, default: {
*
* @return Token
*/
protected function sc_open()
{
$pos = $this->index;
$length = \strlen($this->sc_open_char);
$this->consume($length);
return $this->tokens->create('sc_open', $this->sc_open_char, $pos);
}
/**
* sc_close : shortcode tag closeing character, default: }
*
* @return Token
*/
protected function sc_close()
{
$pos = $this->index;
$length = \strlen($this->sc_close_char);
$this->consume($length);
return $this->tokens->create('sc_close', $this->sc_close_char, $pos);
}
/**
* if_keyword, default: 'if'
*
* @return Token
*/
protected function _if()
{
return $this->tokens->create('if_keyword', $this->if_keyword, $this->index - \strlen($this->if_keyword));
}
/**
* endif_keyword, default: '/if'
*
* @return Token
*/
protected function _endif()
{
return $this->tokens->create('endif_keyword', $this->endif_keyword, $this->index - \strlen($this->endif_keyword));
}
/**
* text : any character sequence
*
* @param bool $preserve Preserve keywords and special characters inside quotes
* @return Token
*/
protected function text($preserve)
{
if ($preserve)
{
return $this->text_preserved();
}
$pos = $this->index;
$buf = '';
while ($this->cur !== Lexer::EOF)
{
if ($this->predictKeywords() || $this->predictSpecialChars())
{
return $this->tokens->create('text', $buf, $pos);
}
$buf .= $this->cur;
$this->consume();
}
return $this->tokens->create('EOF', '<EOF>', -1);
}
/**
* text_preserved : any character sequence with quoted values preserved
*
* @return Token
*/
protected function text_preserved()
{
$quote_queue = [];
$buf = '';
$pos = $this->index;
while ($this->cur !== Lexer::EOF)
{
// manage quote parsing
if ($this->cur == '"' || $this->cur == "'")
{
if ($this->cur == end($quote_queue))
{
// remove last added quote
array_pop($quote_queue);
}
else
{
// add quote to the queue
array_push($quote_queue, $this->cur);
}
}
// End parsing when any keyword or special character is found
// handles quoted values
if ($this->predictKeywords() || $this->predictSpecialChars())
{
// return the expression's text if no quotes are open
if (empty($quote_queue))
{
return $this->tokens->create('text_preserved', trim($buf), $pos);
}
}
// add current character to buffer
$buf .= $this->cur;
$this->consume();
}
return $this->tokens->create('EOF', '<EOF>', -1);
}
}

View File

@ -0,0 +1,250 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
use NRFramework\Parser\Parser;
use NRFramework\Parser\ShortcodeLexer;
/**
* ShortcodeParser
* LL(k = 3) recursive-decent parser
* Uses ShortcodeLexer as the input/token source
*
* Parses the following grammar:
* ------------------------------
* expr := shortcode* <-- Top-level expression (i.e. 0 or more shortcodes)
* shortcode := ifexpr content endifexpr
* ifexpr := {sc_open} {if_keyword} condition {sc_close}
* endifexpr := {sc_open} {end_ifkeyword} {sc_close}
* content := any text until endifexpr
* condition := any text with preserved quoted values until {sc_close}
*/
class ShortcodeParser extends Parser
{
/**
* shortcode opening character (e.g.: '{')
* @var string
*/
protected $sc_open;
/**
* shortcode closing character (e.g.: '}')
* @var string
*/
protected $sc_close;
/**
* Log parsing errors?
*
* @var boolean
*/
protected $log_errors;
/**
* The shortcode's position in the input text
* Used only for error logging
*
* @var int
*/
protected $shortcode_position;
/**
* Constructor
*
* @param ShortcodeLexer $input
* @param Object $options
*/
public function __construct(ShortcodeLexer $input, $options = null)
{
// k = 3, look 3 tokens ahead at most
parent::__construct($input, 3);
$this->sc_open = $options->tag_open_char ?? '{';
$this->sc_close = $options->tag_close_char ?? '}';
$this->log_errors = $options->log_errors ?? false;
$this->shortcode_position = $options->shortcode_position ?? 0;
}
/**
* Returns the correct content for replacement
* If $pass = null will return an array with [content, else-content]
*
* Call this method when the conditions have been parsed and
* the result is known
*
* @param string $content
* @param bool $pass
* @return string
*/
public function getReplacement($content, $pass)
{
//construct the else-tag, e.g. {else}
$elseTag = $this->sc_open . 'else' . $this->sc_close;
// split content on the else-tag
$replacement = $content;
$elseReplacement = '';
if (strpos($content, $elseTag) !== false)
{
list($replacement, $elseReplacement) = explode($elseTag, $content, 2);
}
return $pass === null ?
[$replacement, $elseReplacement] :
($pass ? $replacement : $elseReplacement);
}
/**
* Top-level parsing method
*
* Rule:
* expr := shortcode*
*
* @return array
*/
public function expr()
{
$shortcodes = [];
while ($this->lookahead[0]->type != 'EOF')
{
$position = $this->lookahead[0]->position;
try
{
if ($this->lookahead[0]->type == 'sc_open' &&
$this->lookahead[1]->type == 'if_keyword')
{
$shortcodes[] = $this->shortcode();
}
else
{
// this token is not part of a shortcode, keep going...
$this->consume();
}
}
catch (\Exception $error)
{
// something went horribly wrong while parsing a shortcode
// log the error and continue
$msg = $error->getMessage();
$near_text = $this->lookahead[0]->position + $this->shortcode_position;
$shortcodes[] = (object) [
'position' => $position,
'parser_error' => $msg,
'near_text' => $near_text
];
$this->consume();
}
}
return $shortcodes;
}
/**
* Rule
*
* shortcode := ifexpr content endifexpr
*
* @return object
*/
protected function shortcode()
{
$start = $this->lookahead[0]->position + $this->shortcode_position;
$conditions = $this->ifexpr();
$content = $this->content();
$length = $this->lookahead[2]->position + $this->shortcode_position - $start + 1;
$this->endifexpr();
return (object) [
'start' => $start,
'length' => $length,
'conditions' => $conditions,
'content' => $content
];
}
/**
* Rule:
* ifexpr : {sc_open} {if_keyword} condition {sc_close}
*
* @return string
*/
protected function ifexpr()
{
$this->match('sc_open');
$this->match('if_keyword');
$condition_text = $this->condition();
$this->match('sc_close');
return $condition_text;
}
/**
* Rule:
* endifexpr := {sc_open} {end_ifkeyword} {sc_close}
*
* @return void
*/
protected function endifexpr()
{
$this->match('sc_open');
$this->match('endif_keyword');
$this->match('sc_close');
}
/**
* Rule:
* condition := any text with preserved quoted values until {sc_close}
*
* @return string
* @throws Exception
*/
protected function condition()
{
$buf = '';
while ($this->lookahead[0]->type !== 'EOF')
{
if ($this->lookahead[0]->type === 'sc_close')
{
return htmlspecialchars_decode($buf);
}
$buf .= $this->lookahead[0]->text;
$this->consume();
}
throw new Exceptions\SyntaxErrorException('Invalid condition expression.');
}
/**
* Rule:
* content := any text until an endif expression
*
* @return string
* @throws Exception
*/
protected function content()
{
$buf = '';
while ($this->lookahead[0]->type !== 'EOF')
{
if ($this->lookahead[0]->type === 'sc_open' &&
$this->lookahead[1]->type === 'endif_keyword' &&
$this->lookahead[2]->type === 'sc_close')
{
return $buf;
}
$buf .= $this->lookahead[0]->text;
$this->consume();
}
throw new Exceptions\SyntaxErrorException('Missing shortcode tag character.');
}
}

View File

@ -0,0 +1,359 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
use DateTime;
use DateTimeImmutable;
defined('_JEXEC') or die;
class ShortcodeParserHelper {
/**
* Input text buffer
*
* @var string
*/
protected $text;
/**
* ShortcodeParser options
*
* @var object
*/
protected $parser_options;
/**
* Parsing payload, associative array
*
* @var array
*/
protected $payload;
/**
* Parsing context
*
* @var string
*/
protected $context;
/**
* List of areas in the content that should not be parsed for Smart Tags.
*
* @var array
*/
private $protectedAreas = [];
/**
* @param string $text Text buffer (stored as a reference)
* @param object $parser_options ShortcodeParser options
* @param array|null $payload Parser payload
* @param string|null $context Parser context
*/
public function __construct(&$text, $payload = null, $parser_options = null, $context = null)
{
$this->text =& $text;
$this->payload = $payload;
$this->context = $context;
if (!$parser_options)
{
$this->parser_options = new \stdClass();
$this->parser_options->tag_open_char = '{';
$this->parser_options->tag_close_char = '}';
$this->parser_options->if_keyword = 'if';
$this->parser_options->log_errors = 'false';
}
else
{
$this->parser_options = $parser_options;
}
}
/**
* The text being parsed may contain sensitive information and areas where parsing of shortcodes, such as <script> tags, must be skipped.
* This method aids in protecting these areas by replacing the sensitive content with a hash, which can be restored later.
*
* @return void
*/
private function protectAreas()
{
$reg = '/<script[\s\S]*?>[\s\S]*?<\/script>/';
preg_match_all($reg, $this->text, $protectedAreas);
if (!$protectedAreas[0])
{
return;
}
foreach ($protectedAreas[0] as $protectedArea)
{
$hash = md5($protectedArea);
$this->protectedAreas[] = [$hash, $protectedArea];
$this->text = str_replace($protectedArea, $hash, $this->text);
}
}
/**
* Restore protected areas in the result text.
*
* @return void
*/
private function restoreProtectedAreas()
{
if (empty($this->protectedAreas))
{
return;
}
foreach ($this->protectedAreas as $protectedArea)
{
$this->text = str_ireplace($protectedArea[0], $protectedArea[1], $this->text);
}
}
/**
*
*/
public function parseAndReplace()
{
$this->protectAreas();
$replacements = [];
$shortcodes_text = [];
$shortcode_lexer = new ShortcodeLexer($this->text, $this->parser_options);
$shortcode_parser = new ShortcodeParser($shortcode_lexer, $this->parser_options);
$shortcodes = $shortcode_parser->expr();
foreach ($shortcodes as $shortcode)
{
// check if the shortcode has errors
if (\property_exists($shortcode, 'parser_error'))
{
// we cannot remove the shortcode at this point because it's 'content' could not be parsed
// error message is added before the shortode
$this->text = substr_replace($this->text, $shortcode->parser_error, $shortcode->position, 0);
continue;
}
$cond_lexer = new ConditionLexer(htmlspecialchars_decode($shortcode->conditions));
$cond_parser = new ConditionParser($cond_lexer);
// parse the shortcode's 'conditions' expression
$conditions = [];
try
{
$conditions = $cond_parser->expr();
// check if the shortcode has the correct context
// if ($conditions['context'] !== $this->context)
// {
// continue;
// }
// get the parsed logical operator (and/or)
$logic_op = array_key_exists('logic_op', $conditions) ? $conditions['logic_op'] : 'and';
// check if the debug param is set and we are logged in as a Super User
$debug_enabled = array_key_exists('debug', $conditions['global_params']) && $conditions['global_params']['debug'] &&
\Joomla\CMS\Factory::getUser()->authorise('core.admin');
// check for the noPrepareContent global param
$prepare_content = !array_key_exists('nopreparecontent', $conditions['global_params']);
// evaluate conditions
$evaluator = new ConditionsEvaluator($conditions['conditions'], $this->payload, $debug_enabled);
$results = $evaluator->evaluate();
// get the final result
$pass = $logic_op === 'and' && !empty($results);
foreach($results as $result)
{
if ($logic_op === 'and')
{
$pass &= $result['pass'];
}
else
{
$pass |= $result['pass'];
}
}
//
$replacement = $shortcode_parser->getReplacement($shortcode->content, $pass);
if ($debug_enabled)
{
list($content, $alt_content) = $shortcode_parser->getReplacement($shortcode->content, null);
$replacement .= $this->prepareDebugInfo($shortcode->conditions, $conditions, $results, $pass, $content, $alt_content);
}
}
catch (\Exception $error)
{
// Log the error and remove the shortcode from the input text
$replacement = $error->getMessage();
}
// fire the onContentPrepare event for the replacement text
// if ($prepare_content)
// {
// $replacement = \Joomla\CMS\HTML\Helpers\Content::prepare($replacement);
// }
// store 'replacement' text
$replacements[] = $replacement;
// store the original shortcode text
$shortcodes_text[] = substr($this->text, $shortcode->start, $shortcode->length);
}
// replace all shortcodes
$this->replaceContent($shortcodes_text, $replacements);
$this->restoreProtectedAreas();
}
/**
* Performs content replacement in the input text buffer
*
* @param array $shortcodes_text Array containing the shortcodes text
* @param array $replacements Array containng the shortcode replacements
* @param array $debug_info Contains debug info for each shortcode
* @return void
*/
protected function replaceContent($shortcodes_text, $replacements)
{
$this->text = \str_replace($shortcodes_text, $replacements, $this->text);
}
/**
*
*/
protected function prepareDebugInfo($conditions_text, $conditions, $results, $pass, $content, $alt_content)
{
$format_value = function($value)
{
if (is_array($value))
{
return implode(', ', $value);
}
if ($value instanceof \DateTime || $value instanceof \DateTimeImmutable)
{
return $value->format(\DateTimeInterface::RFC1036);
}
return $value;
};
$format_condition = function($conditions) use ($results, $format_value, $conditions_text)
{
$res = [];
foreach ($conditions['conditions'] as $idx => $condition)
{
$params = count($condition['params']) ?
array_reduce(array_keys($condition['params']), function($acc, $key) use($condition) {
return $acc . "{$key}: " . $condition['params'][$key] . "<br>";
}, '') :
null;
$res[] = [
'title' => ($condition['alias'] ?? $results[$idx]['l_func_name'] . '('. $format_value($results[$idx]['l_func_args']) . ') ') . ($results[$idx]['pass'] ? '<span style="color: green;"> &#10003;</span>' : '<span style="color: red;"> &#10007;</span>'),
'body' => '<ul style="margin-bottom: 0">' . implode('',
array_filter([
'<li>Condition Operand: ' . (array_key_exists('l_func_name', $results[$idx]) ? $format_value($results[$idx]['l_func_val']) : $format_value($results[$idx]['actual_value'])) .
(array_key_exists('l_eval', $results[$idx]) && ($results[$idx]['l_eval'] != $results[$idx]['l_func_val']) ? ' (evaluated as "' . $format_value($results[$idx]['l_eval']) . '")': '') .'</li>',
$condition['values'] ? '<li>Value Operand: ' . $format_value($condition['values']) .
(array_key_exists('r_evbal', $results[$idx]) && ($results[$idx]['r_eval'] != $condition['values']) ? ' (evaluated as "' . $format_value($results[$idx]['r_eval']) .'")' : '') . '</li>' : null,
array_key_exists('r_func_name', $results[$idx]) ? '<li>Value Operand: ' . $results[$idx]['r_func_name'] . '('. $format_value($results[$idx]['r_func_args']) . '): ' . $format_value($results[$idx]['r_func_val']) .'</li>': null,
'<li>Operator: ' . $this->operatorToString($condition['operator']) .'</li>',
$params ? '<li style="list-style-type: none; margin-left: -1rem;">' . $this->debugInfoHTML(['title' => 'Parameters', 'body' => $params]) . '</li>': null
])
) . '</ul>'
];
}
return $res;
};
$title = $pass ? '<span style="color: green;"> &#10003;</span>' : '<span style="color: red;"> &#10007;</span>';
$info = [
'title' => str_replace('--debug', '', $conditions_text) . ' ' . $title,
'body' => '',
'children' => array_filter([
['title' => 'Conditions', 'body' => '', 'children' => $format_condition($conditions)],
count($conditions['conditions']) > 1 ? ['title' => 'Logical Operator: ' . $conditions['logic_op'], 'children' => []] : null,
['title' => 'Content', 'body' => $content, 'children' => []],
!empty($alt_content) ? ['title' => 'Alt. Content', 'body' => $alt_content, 'children' => []] : null
])
];
return '<div style="display: flex; justify-content: center; text-align: start;">' . $this->debugInfoHTML($info) . '</div>';
}
/**
*
*/
protected function debugInfoHTML($info)
{
$title = \array_key_exists('title', $info) ? $info['title'] : '';
$body = \array_key_exists('body', $info) ? $info['body'] : '';
$children = '';
if (\array_key_exists('children', $info))
{
foreach ($info['children'] as $c)
{
$children .= $this->debugInfoHTML($c);
}
}
return '<details style=""><summary style="cursor: pointer; ">' . $title . '</summary><div style="margin: 0.2em 0.5em;">' . $body . $children . '</div></details>';
}
/**
* Converts shortcode operators to a human readable string
*
*/
protected function operatorToString($op)
{
switch($op)
{
case 'equals':
return 'equals';
case 'starts_with':
return 'startsWith';
case 'ends_with':
return 'endsWith';
case 'contains':
return 'contains';
case 'contains_any':
return 'containsAny';
case 'contains_all':
return 'containsAll';
case 'contains_only':
return 'containsOnly';
case 'lt':
return 'lessThan';
case 'lte':
return 'lessThanEquals';
case 'gt':
return 'greaterThan';
case 'gte':
return 'greaterThanEquals';
case 'empty':
return 'empty';
default:
throw new Exceptions\UnknownOperatorException($op);
}
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
/**
* Token
* Represents a single lexer token
*/
class Token
{
/**
* Token type
*
* @var string
*/
public $type;
/**
* The token's text
*
* @var string
*/
public $text;
/**
* Token position in the input stream
*
* @var integer
*/
public $position;
public function __construct($type, $text, $pos)
{
$this->type = $type;
$this->text = $text;
$this->position = $pos;
}
/**
* __toString magic method (for debugging)
*
* @return string
*/
public function __toString()
{
return '[' . $this->text .', ' . $this->type . ', ' . $this->position .']';
}
}

View File

@ -0,0 +1,96 @@
<?php
/**
* @author Tassos.gr <info@tassos.gr>
* @link https://www.tassos.gr
* @copyright Copyright © 2024 Tassos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Parser;
defined('_JEXEC') or die;
/**
* Tokens
* Holds token types and manages creation of new tokens
*/
class Tokens
{
/**
* Token types array
*
* @var array
*/
protected $types = [];
public function __construct()
{
// default types
$this->addType('invalid_token');
$this->addType('EOF');
}
/**
* Adds a new token type
*
* @param string $type
* @return $this
*/
public function addType($type)
{
if (!$this->hasType($type))
{
$this->types[] = $type;
}
return $this;
}
/**
* Gets a token type id (i.e. it's array index)
*
* @param string $type
* @return int|null
*/
public function getTypeId($type)
{
$id = array_search($type, $this->types);
$id = $id !== false ? $id : null;
return $id;
}
/**
* Returns the token types array
*
* @return array
*/
public function getTypes()
{
return clone $this->types;
}
/**
* Creates a new token
*
* @param string $type
* @param string $text
* @param integer $position, Position of token in the input stream
* @return Token
*/
public function create($type, $text, $position)
{
return new Token($type, $text, $position);
}
/**
* Checks if a type is registered
*
* @param string $type
* @return boolean
*/
public function hasType($type)
{
return (bool)array_search($type, $this->types);
}
}