acf
This commit is contained in:
612
plugins/system/nrframework/NRFramework/Parser/ConditionLexer.php
Normal file
612
plugins/system/nrframework/NRFramework/Parser/ConditionLexer.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
215
plugins/system/nrframework/NRFramework/Parser/Lexer.php
Normal file
215
plugins/system/nrframework/NRFramework/Parser/Lexer.php
Normal 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();
|
||||
}
|
||||
}
|
||||
152
plugins/system/nrframework/NRFramework/Parser/Parser.php
Normal file
152
plugins/system/nrframework/NRFramework/Parser/Parser.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
227
plugins/system/nrframework/NRFramework/Parser/RingBuffer.php
Normal file
227
plugins/system/nrframework/NRFramework/Parser/RingBuffer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
359
plugins/system/nrframework/NRFramework/Parser/ShortcodeLexer.php
Normal file
359
plugins/system/nrframework/NRFramework/Parser/ShortcodeLexer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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.');
|
||||
}
|
||||
}
|
||||
@ -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;"> ✓</span>' : '<span style="color: red;"> ✗</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;"> ✓</span>' : '<span style="color: red;"> ✗</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);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
plugins/system/nrframework/NRFramework/Parser/Token.php
Normal file
57
plugins/system/nrframework/NRFramework/Parser/Token.php
Normal 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 .']';
|
||||
}
|
||||
}
|
||||
96
plugins/system/nrframework/NRFramework/Parser/Tokens.php
Normal file
96
plugins/system/nrframework/NRFramework/Parser/Tokens.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user