Your IP : 216.73.216.228


Current Path : /proc/self/root/proc/self/root/var/tmp/
Upload File :
Current File : //proc/self/root/proc/self/root/var/tmp/phpycY9IU

<?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.");
    }
}<?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.");
    }
}<?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);
    }
}<?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);
    }
}<?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);
    }
}<?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);
    }
}<?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);
    }
}<?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();
    }
}
<?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'
        ]);    
    }
}
<?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;
    }
}