From 4ce65d59ca91871cfd126497158200a818720bce Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 13:30:08 +0200 Subject: Adding upstream version 0.13.1. Signed-off-by: Daniel Baumann --- vendor/ipl/web/src/Filter/ParseException.php | 36 ++ vendor/ipl/web/src/Filter/Parser.php | 568 +++++++++++++++++++++++++++ vendor/ipl/web/src/Filter/QueryString.php | 94 +++++ vendor/ipl/web/src/Filter/Renderer.php | 186 +++++++++ 4 files changed, 884 insertions(+) create mode 100644 vendor/ipl/web/src/Filter/ParseException.php create mode 100644 vendor/ipl/web/src/Filter/Parser.php create mode 100644 vendor/ipl/web/src/Filter/QueryString.php create mode 100644 vendor/ipl/web/src/Filter/Renderer.php (limited to 'vendor/ipl/web/src/Filter') diff --git a/vendor/ipl/web/src/Filter/ParseException.php b/vendor/ipl/web/src/Filter/ParseException.php new file mode 100644 index 0000000..bcafd09 --- /dev/null +++ b/vendor/ipl/web/src/Filter/ParseException.php @@ -0,0 +1,36 @@ +char = $char; + $this->charPos = $charPos; + } + + public function getChar() + { + return $this->char; + } + + public function getCharPos() + { + return $this->charPos; + } +} diff --git a/vendor/ipl/web/src/Filter/Parser.php b/vendor/ipl/web/src/Filter/Parser.php new file mode 100644 index 0000000..d33fd86 --- /dev/null +++ b/vendor/ipl/web/src/Filter/Parser.php @@ -0,0 +1,568 @@ +setQueryString($queryString); + } + } + + /** + * Set the query string to parse + * + * @param string $queryString + * + * @return $this + */ + public function setQueryString($queryString) + { + $this->string = (string) $queryString; + $this->length = strlen($queryString); + + return $this; + } + + /** + * Set whether strict mode is enabled + * + * @param bool $strict + * + * @return $this + */ + public function setStrict($strict = true) + { + $this->strict = (bool) $strict; + + return $this; + } + + /** + * Parse the string and derive a filter rule from it + * + * @return Filter\Rule + */ + public function parse() + { + if ($this->length === 0) { + return Filter::all(); + } + + $this->pos = 0; + $this->termIndex = 0; + + return $this->readFilters(); + } + + /** + * Read filters + * + * @param int $nestingLevel + * @param string $op + * @param array $filters + * @param bool $explicit + * + * @return Filter\Chain|Filter\Condition + * @throws ParseException + */ + protected function readFilters($nestingLevel = 0, $op = null, $filters = null, $explicit = true) + { + $filters = empty($filters) ? [] : $filters; + $isNone = false; + + while ($this->pos < $this->length) { + $filter = $this->readCondition(); + $next = $this->readChar(); + + if ($filter === false) { + if ($next === '!') { + $isNone = true; + $this->termIndex++; + continue; + } + + if ($op === null && ($this->strict || count($filters) > 0) && ($next === '&' || $next === '|')) { + $op = $next; + $this->termIndex++; + continue; + } + + if ($next === false) { + // Nothing more to read + break; + } + + if ($next === ')') { + if ($nestingLevel > 0) { + if (! $explicit) { + // The current chain was not initiated by a `(`, + // so this `)` does not belong to it, but still ends it + $this->pos--; + } else { + $this->termIndex++; + $next = $this->nextChar(); + if ($next !== false && ! in_array($next, ['&', '|', ')'])) { + $this->pos++; + $this->parseError($next, 'Expected logical operator'); + } + } + + break; + } + + $this->parseError($next); + } + + if ($next === '(') { + $this->termIndex++; + + $rule = $this->readFilters($nestingLevel + 1, $isNone ? '!' : null); + if ($this->strict || ! $rule instanceof Filter\Chain || ! $rule->isEmpty()) { + $filters[] = $rule; + } + + $isNone = false; + continue; + } + + if ($next === $op) { + $this->termIndex++; + continue; + } + + if (in_array($next, ['&', '|'])) { + $this->termIndex++; + + // It's a different logical operator, continue parsing based on its precedence + if ($op === '&') { + if (! empty($filters)) { + if (count($filters) > 1) { + $all = Filter::all(...$filters); + $filters = [$all]; + + $this->emit(self::ON_CHAIN, [$all]); + } else { + $filters = [$filters[0]]; + } + } + + $op = $next; + } elseif ($op === '|' || ($op === '!' && $next === '&')) { + $rule = $this->readFilters( + $nestingLevel + 1, + $next, + [array_pop($filters)], + false + ); + if (! $rule instanceof Filter\Chain || ! $rule->isEmpty()) { + $filters[] = $rule; + } + } + + continue; + } + + $this->parseError($next, "$op level $nestingLevel"); + } else { + if ($isNone) { + $isNone = false; + if ($filter->getValue() === true) { + // $filter is a result of `!column` + $filter->setValue(false); + $filters[] = $filter; + + $this->emit(self::ON_CONDITION, [$filter]); + } else { + // $filter is a result of `!column=[value]` + $none = Filter::none($filter); + $filters[] = $none; + + $this->emit(self::ON_CONDITION, [$filter]); + $this->emit(self::ON_CHAIN, [$none]); + } + } else { + $filters[] = $filter; + $this->emit(self::ON_CONDITION, [$filter]); + } + + if ($next === false) { + // Got filter, nothing more to read + break; + } + + if ($next === ')') { + if ($nestingLevel > 0) { + if (! $explicit) { + // The current chain was not initiated by a `(`, + // so this `)` does not belong to it, but still ends it + $this->pos--; + } else { + $this->termIndex++; + $next = $this->nextChar(); + if ($next !== false && ! in_array($next, ['&', '|', ')'])) { + $this->pos++; + $this->parseError($next, 'Expected logical operator'); + } + } + + break; + } + + $this->parseError($next); + } + + if ($next === $op) { + $this->termIndex++; + continue; + } + + if (in_array($next, ['&', '|'])) { + $this->termIndex++; + + // It's a different logical operator, continue parsing based on its precedence + if ($op === null || $op === '&') { + if ($op === '&') { + if (count($filters) > 1) { + $all = Filter::all(...$filters); + $filters = [$all]; + + $this->emit(self::ON_CHAIN, [$all]); + } else { + $filters = [$filters[0]]; + } + } + + $op = $next; + } elseif ($op === '|' || ($op === '!' && $next === '&')) { + $rule = $this->readFilters( + $nestingLevel + 1, + $next, + [array_pop($filters)], + false + ); + if (! $rule instanceof Filter\Chain || ! $rule->isEmpty()) { + $filters[] = $rule; + } + } + + continue; + } + + $this->parseError($next); + } + } + + if ($nestingLevel === 0 && $this->pos < $this->length) { + $this->parseError($op, 'Did not read full filter'); + } + + switch ($op) { + case '&': + $chain = Filter::all(...$filters); + break; + case '|': + $chain = Filter::any(...$filters); + break; + case '!': + $chain = Filter::none(...$filters); + break; + case null: + if ((! $this->strict || $nestingLevel === 0) && ! empty($filters)) { + // There is only one filter expression, no chain + return $filters[0]; + } + + $chain = Filter::all(...$filters); + break; + default: + $this->parseError($op); + } + + $this->emit(self::ON_CHAIN, [$chain]); + + return $chain; + } + + /** + * Read the next condition + * + * @return false|Filter\Condition + * + * @throws ParseException + */ + protected function readCondition() + { + if ('' === ($column = $this->readColumn())) { + return false; + } + + $columnIndex = $this->termIndex++; + + foreach (['<', '>'] as $operator) { + if (($pos = strpos($column, $operator)) !== false) { + if ($this->nextChar() === '=') { + break; + } + + $operatorIndex = $this->termIndex++; + + $value = substr($column, $pos + 1); + $column = substr($column, 0, $pos); + + $valueIndex = null; + if (ctype_digit($value)) { + $value = (float) $value; + $valueIndex = $this->termIndex++; + } elseif ($value) { + $valueIndex = $this->termIndex++; + } + + $condition = $this->createCondition($column, $operator, $value); + $condition->metaData() + ->set('columnIndex', $columnIndex) + ->set('operatorIndex', $operatorIndex) + ->set('valueIndex', $valueIndex); + + return $condition; + } + } + + if (in_array($this->nextChar(), ['~', '=', '>', '<', '!'], true)) { + $operator = $this->readChar(); + } else { + $operator = false; + } + + if ($operator === false) { + $condition = Filter::equal($column, true); + $condition->metaData() + ->set('columnIndex', $columnIndex) + ->set('operatorIndex', null) + ->set('valueIndex', null); + + return $condition; + } + + $operatorIndex = $this->termIndex++; + + $toFloat = false; + if ($operator === '=') { + $last = substr($column, -1); + if ($last === '>' || $last === '<') { + $operator = $last . $operator; + $column = substr($column, 0, -1); + $toFloat = true; + } + } elseif (in_array($operator, ['>', '<', '!'], true)) { + $toFloat = $operator === '>' || $operator === '<'; + if (in_array($this->nextChar(), ['~', '='], true)) { + $operator .= $this->readChar(); + } + } + + $valueIndex = null; + $value = $this->readValue(); + if ($toFloat && ctype_digit($value)) { + $value = (float) $value; + $valueIndex = $this->termIndex++; + } elseif ($value) { + $valueIndex = $this->termIndex++; + } + + $condition = $this->createCondition($column, $operator, $value); + $condition->metaData() + ->set('columnIndex', $columnIndex) + ->set('operatorIndex', $operatorIndex) + ->set('valueIndex', $valueIndex); + + return $condition; + } + + /** + * Read the next column + * + * @return false|string false if there is none + */ + protected function readColumn() + { + $str = $this->readUntil('~', '=', '(', ')', '&', '|', '>', '<', '!'); + + if ($str === false) { + return $str; + } + + return rawurldecode($str); + } + + /** + * Read the next value + * + * @return string|string[] + * + * @throws ParseException In case there's a missing `)` + */ + protected function readValue() + { + if ($this->nextChar() === '(') { + $this->readChar(); + $var = array_map('rawurldecode', preg_split('~\|~', $this->readUntil(')'))); + + if ($this->readChar() !== ')') { + $this->parseError(null, 'Expected ")"'); + } + } else { + $var = rawurldecode($this->readUntil(')', '&', '|', '>', '<')); + } + + return $var; + } + + /** + * Read until any of the given chars appears + * + * @param string ...$chars + * + * @return string + */ + protected function readUntil(...$chars) + { + $buffer = ''; + while (($c = $this->readChar()) !== false) { + if (in_array($c, $chars, true)) { + $this->pos--; + break; + } + + $buffer .= $c; + } + + return $buffer; + } + + /** + * Read a single character + * + * @return false|string false if there is no character left + */ + protected function readChar() + { + if ($this->length > $this->pos) { + return $this->string[$this->pos++]; + } + + return false; + } + + /** + * Look at the next character + * + * @return false|string false if there is no character left + */ + protected function nextChar() + { + if ($this->length > $this->pos) { + return $this->string[$this->pos]; + } + + return false; + } + + /** + * Create and return a condition + * + * @param string $column + * @param string $operator + * @param mixed $value + * + * @return Filter\Condition + */ + protected function createCondition($column, $operator, $value) + { + $column = trim($column); + + switch ($operator) { + case '~': + return Filter::like($column, $value); + case '!~': + return Filter::unlike($column, $value); + case '=': + return Filter::equal($column, $value); + case '!=': + return Filter::unequal($column, $value); + case '>': + return Filter::greaterThan($column, $value); + case '>=': + return Filter::greaterThanOrEqual($column, $value); + case '<': + return Filter::lessThan($column, $value); + case '<=': + return Filter::lessThanOrEqual($column, $value); + } + } + + /** + * Throw a parse exception + * + * @param string $char + * @param string $extraMsg + * + * @throws ParseException + */ + protected function parseError($char = null, $extraMsg = null) + { + if ($extraMsg === null) { + $extra = ''; + } else { + $extra = ': ' . $extraMsg; + } + + if ($char === null) { + if ($this->pos < $this->length) { + $char = $this->string[$this->pos]; + } else { + $char = $this->string[--$this->pos]; + } + } + + throw new ParseException( + $this->string, + $char, + $this->pos, + $extra + ); + } +} diff --git a/vendor/ipl/web/src/Filter/QueryString.php b/vendor/ipl/web/src/Filter/QueryString.php new file mode 100644 index 0000000..e1bb533 --- /dev/null +++ b/vendor/ipl/web/src/Filter/QueryString.php @@ -0,0 +1,94 @@ +parse(); + } + + /** + * Assemble a query string for the given rule + * + * @param Filter\Rule $rule + * + * @return string + */ + public static function render(Filter\Rule $rule) + { + return (new Renderer($rule))->render(); + } + + /** + * Get the symbol associated with the given rule + * + * @param Filter\Rule $rule + * + * @return string + */ + public static function getRuleSymbol(Filter\Rule $rule) + { + switch (true) { + case $rule instanceof Filter\Unlike: + return '!~'; + case $rule instanceof Filter\Unequal: + return '!='; + case $rule instanceof Filter\Like: + return '~'; + case $rule instanceof Filter\Equal: + return '='; + case $rule instanceof Filter\GreaterThan: + return '>'; + case $rule instanceof Filter\LessThan: + return '<'; + case $rule instanceof Filter\GreaterThanOrEqual: + return '>='; + case $rule instanceof Filter\LessThanOrEqual: + return '<='; + case $rule instanceof Filter\All: + return '&'; + case $rule instanceof Filter\Any: + case $rule instanceof Filter\None: + return '|'; + default: + throw new InvalidArgumentException('Unknown rule type provided'); + } + } +} diff --git a/vendor/ipl/web/src/Filter/Renderer.php b/vendor/ipl/web/src/Filter/Renderer.php new file mode 100644 index 0000000..513470e --- /dev/null +++ b/vendor/ipl/web/src/Filter/Renderer.php @@ -0,0 +1,186 @@ +filter = $filter; + } + + /** + * Set whether strict mode is enabled + * + * @param bool $strict + * + * @return $this + */ + public function setStrict($strict = true) + { + $this->strict = (bool) $strict; + + return $this; + } + + /** + * Assemble and return the filter as query string + * + * @return string + */ + public function render() + { + if ($this->string !== null) { + return $this->string; + } + + $this->string = ''; + $filter = $this->filter; + + if ($filter instanceof Filter\Chain) { + $this->renderChain($filter, $this->strict); + } else { + /** @var Filter\Condition $filter */ + $this->renderCondition($filter); + } + + return $this->string; + } + + /** + * Assemble the given filter Chain + * + * @param Filter\Chain $chain + * @param bool $wrap + * + * @return void + */ + protected function renderChain(Filter\Chain $chain, $wrap = false) + { + if (! $this->strict && $chain->isEmpty()) { + return; + } + + $chainOperator = null; + switch (true) { + case $chain instanceof Filter\All: + $chainOperator = '&'; + break; + case $chain instanceof Filter\None: + $this->string .= '!'; + + // Force wrap, it may be the root node + if (! $wrap) { + if ($chain->count() > 1) { + $wrap = true; + } else { + $iterator = $chain->getIterator(); + $wrap = $iterator->current() instanceof Filter\None; + } + } + + // None shares the operator with Any + case $chain instanceof Filter\Any: + $chainOperator = '|'; + break; + } + + if ($wrap) { + $this->string .= '('; + } + + foreach ($chain as $rule) { + if ($rule instanceof Filter\Chain) { + $this->renderChain($rule, $this->strict || $rule->count() > 1); + } else { + /** @var Filter\Condition $rule */ + $this->renderCondition($rule); + } + + $this->string .= $chainOperator; + } + + if (! $chain->isEmpty() && (! $this->strict || ! ($chain instanceof Filter\Any && $chain->count() === 1))) { + // Remove redundant chain operator added last + $this->string = substr($this->string, 0, -1); + } elseif ($chain->isEmpty() && $chain instanceof Filter\Any) { + // If the chain is empty and strict mode is on, we need a + // chain operator to designate it's an OR, not an AND + $this->string .= $chainOperator; + } + + if ($wrap) { + $this->string .= ')'; + } + } + + /** + * Assemble the given filter Condition + * + * @param Filter\Condition $condition + * + * @return void + */ + protected function renderCondition(Filter\Condition $condition) + { + $value = $condition->getValue(); + if (is_bool($value) && ! $value) { + $this->string .= '!'; + } + + $this->string .= rawurlencode($condition->getColumn()); + + if (is_bool($value)) { + return; + } + + switch (true) { + case $condition instanceof Filter\Unlike: + $this->string .= '!~'; + break; + case $condition instanceof Filter\Unequal: + $this->string .= '!='; + break; + case $condition instanceof Filter\Like: + $this->string .= '~'; + break; + case $condition instanceof Filter\Equal: + $this->string .= '='; + break; + case $condition instanceof Filter\GreaterThan: + $this->string .= rawurlencode('>'); + break; + case $condition instanceof Filter\LessThan: + $this->string .= rawurlencode('<'); + break; + case $condition instanceof Filter\GreaterThanOrEqual: + $this->string .= rawurlencode('>') . '='; + break; + case $condition instanceof Filter\LessThanOrEqual: + $this->string .= rawurlencode('<') . '='; + break; + } + + if (is_array($value)) { + $this->string .= '(' . join('|', array_map('rawurlencode', $value)) . ')'; + } elseif ($value !== null) { + $this->string .= rawurlencode($value); + } + } +} -- cgit v1.2.3