From 39f5c943a7b7a42a2d9d35062bff85a7692e0ed0 Mon Sep 17 00:00:00 2001 From: Alex Runyan Date: Thu, 23 May 2024 19:03:04 -0400 Subject: [PATCH 1/4] Adding support for raw() filter --- ProcessMaker/Filters/Filter.php | 81 ++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/ProcessMaker/Filters/Filter.php b/ProcessMaker/Filters/Filter.php index 8a0e44707e..152c9d0a09 100644 --- a/ProcessMaker/Filters/Filter.php +++ b/ProcessMaker/Filters/Filter.php @@ -2,28 +2,28 @@ namespace ProcessMaker\Filters; +use Illuminate\Support\Str; +use Illuminate\Support\Facades\DB; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; use ProcessMaker\Models\ProcessRequestToken; use ProcessMaker\Models\User; -use ProcessMaker\Query\BaseField; -use ProcessMaker\Query\Expression; class Filter { - const TYPE_PARTICIPANTS = 'Participants'; + public const TYPE_PARTICIPANTS = 'Participants'; - const TYPE_PARTICIPANTS_FULLNAME = 'ParticipantsFullName'; + public const TYPE_PARTICIPANTS_FULLNAME = 'ParticipantsFullName'; - const TYPE_ASSIGNEES_FULLNAME = 'AssigneesFullName'; + public const TYPE_ASSIGNEES_FULLNAME = 'AssigneesFullName'; - const TYPE_STATUS = 'Status'; + public const TYPE_STATUS = 'Status'; - const TYPE_FIELD = 'Field'; + public const TYPE_FIELD = 'Field'; - const TYPE_PROCESS = 'Process'; + public const TYPE_PROCESS = 'Process'; - const TYPE_RELATIONSHIP = 'Relationship'; + public const TYPE_RELATIONSHIP = 'Relationship'; public string|null $subjectValue; @@ -33,6 +33,8 @@ class Filter public $value; + protected bool $usesRawValue = false; + public array $or; public array $operatorWhitelist = [ @@ -49,7 +51,17 @@ class Filter 'starts_with', ]; - public static function filter(Builder $query, string|array $filterDefinitions) + public function __construct($definition) + { + $this->subjectType = $definition['subject']['type']; + $this->subjectValue = Arr::get($definition, 'subject.value'); + $this->operator = $definition['operator']; + $this->value = $definition['value']; + $this->usesRawValue = $this->containsRawValue($this->value ?? ''); + $this->or = Arr::get($definition, 'or', []); + } + + public static function filter(Builder $query, string|array $filterDefinitions): void { if (is_string($filterDefinitions)) { $filterDefinitions = json_decode($filterDefinitions, true); @@ -66,26 +78,39 @@ public static function filter(Builder $query, string|array $filterDefinitions) }); } - public function __construct($definition) - { - $this->subjectType = $definition['subject']['type']; - $this->subjectValue = Arr::get($definition, 'subject.value'); - $this->operator = $definition['operator']; - $this->value = $definition['value']; - $this->or = Arr::get($definition, 'or', []); - } - - public function addToQuery(Builder $query) + public function addToQuery(Builder $query): void { if (!empty($this->or)) { - $query->where(function ($query) { - $this->apply($query); - }); + $query->where(fn ($query) => $this->apply($query)); } else { $this->apply($query); } } + /** + * Use regex to find the raw() pattern and extract its content + * + * @param string|null $value + * + * @return string + */ + public function getRawValue(string $value = null): string + { + return Str::match('/(?<=raw\().*(?=\))/', $value ?? $this->value); + } + + /** + * Determine if the value is using the raw() function + * + * @param string $value + * + * @return bool + */ + public function containsRawValue(string $value): bool + { + return Str::contains($value, 'raw('); + } + private function apply($query) { if ($valueAliasMethod = $this->valueAliasMethod()) { @@ -212,7 +237,7 @@ private function subject() return $this->subjectValue; } - private function relationshipSubjectTypeParts() + private function relationshipSubjectTypeParts(): array { return explode('.', $this->subjectValue); } @@ -227,6 +252,10 @@ private function value() return $this->value . '%'; } + if ($this->usesRawValue) { + return DB::raw($this->getRawValue()); + } + return $this->value; } @@ -261,13 +290,13 @@ private function valueAliasMethod() private function valueAliasAdapter(string $method, Builder $query) { $operator = $this->operator(); + if ($operator === 'in') { $operator = '='; } - $values = (array) $this->value(); + $values = (array) $this->value(); $expression = (object) ['operator' => $operator]; - $model = $query->getModel(); if ($method === 'valueAliasParticipant') { From 27f7ab494fb3e067190badabb030f913c4bf1ffc Mon Sep 17 00:00:00 2001 From: Alex Runyan Date: Fri, 24 May 2024 11:09:49 -0400 Subject: [PATCH 2/4] Refactored raw() special filter to its own trait and finished business logic --- ProcessMaker/Filters/Filter.php | 62 +++---- .../Traits/InteractsWithRawFilter.php | 153 ++++++++++++++++++ 2 files changed, 175 insertions(+), 40 deletions(-) create mode 100644 ProcessMaker/Traits/InteractsWithRawFilter.php diff --git a/ProcessMaker/Filters/Filter.php b/ProcessMaker/Filters/Filter.php index 152c9d0a09..64a00d651d 100644 --- a/ProcessMaker/Filters/Filter.php +++ b/ProcessMaker/Filters/Filter.php @@ -2,15 +2,17 @@ namespace ProcessMaker\Filters; -use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; use ProcessMaker\Models\ProcessRequestToken; use ProcessMaker\Models\User; +use ProcessMaker\Traits\InteractsWithRawFilter; class Filter { + use InteractsWithRawFilter; + public const TYPE_PARTICIPANTS = 'Participants'; public const TYPE_PARTICIPANTS_FULLNAME = 'ParticipantsFullName'; @@ -33,8 +35,6 @@ class Filter public $value; - protected bool $usesRawValue = false; - public array $or; public array $operatorWhitelist = [ @@ -57,8 +57,9 @@ public function __construct($definition) $this->subjectValue = Arr::get($definition, 'subject.value'); $this->operator = $definition['operator']; $this->value = $definition['value']; - $this->usesRawValue = $this->containsRawValue($this->value ?? ''); $this->or = Arr::get($definition, 'or', []); + + $this->detectRawValue(); } public static function filter(Builder $query, string|array $filterDefinitions): void @@ -87,31 +88,7 @@ public function addToQuery(Builder $query): void } } - /** - * Use regex to find the raw() pattern and extract its content - * - * @param string|null $value - * - * @return string - */ - public function getRawValue(string $value = null): string - { - return Str::match('/(?<=raw\().*(?=\))/', $value ?? $this->value); - } - - /** - * Determine if the value is using the raw() function - * - * @param string $value - * - * @return bool - */ - public function containsRawValue(string $value): bool - { - return Str::contains($value, 'raw('); - } - - private function apply($query) + private function apply($query): void { if ($valueAliasMethod = $this->valueAliasMethod()) { $this->valueAliasAdapter($valueAliasMethod, $query); @@ -162,16 +139,20 @@ private function applyQueryBuilderMethod($query) * @param [type] $query * @return void */ - private function manuallyAddJsonWhere($query) + private function manuallyAddJsonWhere($query): void { $parts = explode('.', $this->subjectValue); + array_shift($parts); + $selector = implode('"."', $parts); $operator = $this->operator(); $value = $this->value(); + if (!is_numeric($value)) { - $value = \DB::connection()->getPdo()->quote($value); + $value = DB::connection()->getPdo()->quote($value); } + $query->whereRaw("json_unquote(json_extract(`data`, '$.\"{$selector}\"')) {$operator} {$value}"); } @@ -242,7 +223,7 @@ private function relationshipSubjectTypeParts(): array return explode('.', $this->subjectValue); } - private function value() + public function value() { if ($this->operator === 'contains') { return '%' . $this->value . '%'; @@ -252,8 +233,8 @@ private function value() return $this->value . '%'; } - if ($this->usesRawValue) { - return DB::raw($this->getRawValue()); + if ($this->filteringWithRawValue()) { + return $this->getParsedRawQueryValue(); } return $this->value; @@ -287,7 +268,7 @@ private function valueAliasMethod() return $method; } - private function valueAliasAdapter(string $method, Builder $query) + private function valueAliasAdapter(string $method, Builder $query): void { $operator = $this->operator(); @@ -321,19 +302,20 @@ private function convertUserIdsToUsernames($values) }, $values); } - private function filterByProcessId(Builder $query) + private function filterByProcessId(Builder $query): void { if ($query->getModel() instanceof ProcessRequestToken) { $query->whereIn('process_request_id', function ($query) { - $query->select('id')->from('process_requests') - ->whereIn('process_id', (array) $this->value()); + $query->select('id') + ->from('process_requests') + ->whereIn('process_id', (array) $this->value()); }); } else { $this->applyQueryBuilderMethod($query); } } - private function filterByRelationship(Builder $query) + private function filterByRelationship(Builder $query): void { $relationshipName = $this->relationshipSubjectTypeParts()[0]; $query->whereHas($relationshipName, function ($rel) { @@ -341,7 +323,7 @@ private function filterByRelationship(Builder $query) }); } - private function filterByRequestData(Builder $query) + private function filterByRequestData(Builder $query): void { $query->whereHas('processRequest', function ($rel) { $this->applyQueryBuilderMethod($rel); diff --git a/ProcessMaker/Traits/InteractsWithRawFilter.php b/ProcessMaker/Traits/InteractsWithRawFilter.php new file mode 100644 index 0000000000..1acb4b0c17 --- /dev/null +++ b/ProcessMaker/Traits/InteractsWithRawFilter.php @@ -0,0 +1,153 @@ +', + '<', + '>=', + '<=', + 'between', + ]; + + /** + * Use regex to find the raw() pattern and extract its content + * + * @param string|array|null $value + * + * @return string + */ + public function getRawValue(string|array $value = null): mixed + { + $value = $value ?? $this->value ?? ''; + + if (!$this->containsRawValue($value)) { + return ''; + } + + $match = static function (string $string) { + return Str::match('/(?<=raw\().*(?=\))/', $string); + }; + + // If we receive a string, parse it and extract the + // value set for the raw() query + if (is_string($value)) { + return $match($value); + } + + // Otherwise, we have an array which we need to iterate + // through and replace the values with the + // raw() query string + foreach ($value as $key => $string) { + $value[$key] = $match($string); + } + + return $value; + } + + /** + * Returns the parsed DB::raw() instance to apply to the query + * + * @return \Illuminate\Contracts\Database\Query\Expression + */ + public function getParsedRawQueryValue(): mixed + { + $value = $this->getRawValue(); + + // If wer have an array, we need to iterate and replace + // the raw() query string with the filled + // out DB::raw() instances + if (is_array($value)) { + foreach ($value as $key => $string) { + $value[$key] = DB::raw($string); + } + } else { + // Otherwise, we have a string for the raw() + // value we can set and return + $value = DB::raw($value); + } + + return $value; + } + + /** + * Determine if the value is using the raw() function + * + * @param string $value + * + * @return bool + */ + public function containsRawValue(string|array $value): bool + { + $containsRawValue = static function (string $string) { + return Str::contains($string, 'raw(') + && Str::endsWith($string, ')'); + }; + + // If we receive a string, check it for the + // special raw() filtering + if (is_string($value)) { + return $containsRawValue($value); + } + + // Otherwise, if we receive an array, check for the raw() \ + // string to occur in ~any~ of its values + foreach ($value as $string) { + if ($containsRawValue($string)) { + return true; + } + } + + return false; + } + + /** + * Sets related properties + * + * @return void + */ + protected function detectRawValue(): void + { + if ($this->usesRawValue = $this->containsRawValue($this->value ?? '')) { + $this->validateOperator(); + } + } + + /** + * Returns true when this particular filter instance is using a raw() query filter + * + * @return bool + */ + protected function filteringWithRawValue(): bool + { + return $this->usesRawValue === true; + } + + /** + * Validate the operator for this raw() filter + * + * @return bool + */ + private function validateOperator(): void + { + $allowed = $this->validRawFilterOperators; + + if (!in_array($this->operator(), $allowed, true)) { + abort(422, 'Invalid operator: Only '.implode(', ', $allowed). ' are allowed.'); + } + } +} From 6ded79716cb18492ec25e545313b8e00f4dcb8b1 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Fri, 24 May 2024 12:57:26 -0700 Subject: [PATCH 3/4] Add raw filter test --- tests/unit/ProcessMaker/FilterTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/unit/ProcessMaker/FilterTest.php b/tests/unit/ProcessMaker/FilterTest.php index 952736f8f9..aa1b3182a0 100644 --- a/tests/unit/ProcessMaker/FilterTest.php +++ b/tests/unit/ProcessMaker/FilterTest.php @@ -37,6 +37,22 @@ public function testFormData() ); } + public function testRawValue() + { + $sql = $this->filter([ + [ + 'subject' => ['type' => 'Field', 'value' => 'due_at'], + 'operator' => '>', + 'value' => 'raw(NOW())', + ], + ], ProcessRequestToken::class); + + $this->assertEquals( + 'select * from `process_request_tokens` where (`due_at` > NOW())', + $sql + ); + } + public function testCompareDataInteger() { $filter = [ From 15eedec2f4cc43811f0b2830e16a51162d749c3b Mon Sep 17 00:00:00 2001 From: Alex Runyan Date: Wed, 29 May 2024 09:19:05 -0400 Subject: [PATCH 4/4] Updated to replace regex matching with string manipulation; Removed support for "between" operator, since it is not needed yet. Simplified new code --- ProcessMaker/Filters/Filter.php | 2 +- .../Traits/InteractsWithRawFilter.php | 128 +++++++----------- 2 files changed, 48 insertions(+), 82 deletions(-) diff --git a/ProcessMaker/Filters/Filter.php b/ProcessMaker/Filters/Filter.php index 64a00d651d..551d5b36e9 100644 --- a/ProcessMaker/Filters/Filter.php +++ b/ProcessMaker/Filters/Filter.php @@ -234,7 +234,7 @@ public function value() } if ($this->filteringWithRawValue()) { - return $this->getParsedRawQueryValue(); + return $this->getRawValue(); } return $this->value; diff --git a/ProcessMaker/Traits/InteractsWithRawFilter.php b/ProcessMaker/Traits/InteractsWithRawFilter.php index 1acb4b0c17..ef92adec8a 100644 --- a/ProcessMaker/Traits/InteractsWithRawFilter.php +++ b/ProcessMaker/Traits/InteractsWithRawFilter.php @@ -4,6 +4,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; +use Illuminate\Contracts\Database\Query\Expression; trait InteractsWithRawFilter { @@ -14,117 +15,82 @@ trait InteractsWithRawFilter * * @var array */ - private array $validRawFilterOperators = [ - '=', - '!=', - '>', - '<', - '>=', - '<=', - 'between', - ]; + private array $validRawFilterOperators = ['=', '!=', '>', '<', '>=', '<=']; /** - * Use regex to find the raw() pattern and extract its content + * Unwrap the raw() and retrieve the string value passed * - * @param string|array|null $value - * - * @return string + * @return \Illuminate\Contracts\Database\Query\Expression */ - public function getRawValue(string|array $value = null): mixed + public function getRawValue(): Expression { - $value = $value ?? $this->value ?? ''; - - if (!$this->containsRawValue($value)) { - return ''; - } + // Get the string equivalent of the raw() filter value + $value = $this->containsRawValue($this->getValue()) ? $this->getValue() : ''; - $match = static function (string $string) { - return Str::match('/(?<=raw\().*(?=\))/', $string); - }; + // Remove the actual row( and ) from the string + $unwrappedRawValue = $this->unwrapRawValue($value); - // If we receive a string, parse it and extract the - // value set for the raw() query - if (is_string($value)) { - return $match($value); - } - - // Otherwise, we have an array which we need to iterate - // through and replace the values with the - // raw() query string - foreach ($value as $key => $string) { - $value[$key] = $match($string); - } + // Wrap it in a DB expression and return it + return DB::raw($unwrappedRawValue); + } - return $value; + /** + * Determine if the value is using the raw() function + * + * @param string $value + * + * @return bool + */ + public function containsRawValue(string $value): bool + { + return Str::contains($value, 'raw(') + && Str::endsWith($value, ')'); } /** - * Returns the parsed DB::raw() instance to apply to the query + * Sets related properties * - * @return \Illuminate\Contracts\Database\Query\Expression + * @return void */ - public function getParsedRawQueryValue(): mixed + protected function detectRawValue(): void { - $value = $this->getRawValue(); - - // If wer have an array, we need to iterate and replace - // the raw() query string with the filled - // out DB::raw() instances - if (is_array($value)) { - foreach ($value as $key => $string) { - $value[$key] = DB::raw($string); - } - } else { - // Otherwise, we have a string for the raw() - // value we can set and return - $value = DB::raw($value); - } + $value = $this->getValue(); + + // Sometimes, the value is an array, which likely means + // this filter is set to the use the "between" operator + $value = is_string($value) ? $value : ''; + + // Detect if this particular filter includes a raw() value + $this->usesRawValue = $this->containsRawValue($value); - return $value; + // If so, validate it is being used with a compatible operator + if ($this->usesRawValue) { + $this->validateOperator(); + } } /** - * Determine if the value is using the raw() function + * Remove the initial "row(" and the final ")" to unwrap the filter value * * @param string $value * - * @return bool + * @return string */ - public function containsRawValue(string|array $value): bool + protected function unwrapRawValue(string $value): string { - $containsRawValue = static function (string $string) { - return Str::contains($string, 'raw(') - && Str::endsWith($string, ')'); - }; - - // If we receive a string, check it for the - // special raw() filtering - if (is_string($value)) { - return $containsRawValue($value); - } + $stripped = Str::after($value, 'raw('); - // Otherwise, if we receive an array, check for the raw() \ - // string to occur in ~any~ of its values - foreach ($value as $string) { - if ($containsRawValue($string)) { - return true; - } - } - - return false; + return Str::beforeLast($stripped, ')'); } /** - * Sets related properties + * Get the string value of the filter * - * @return void + * @return array|string */ - protected function detectRawValue(): void + protected function getValue(): mixed { - if ($this->usesRawValue = $this->containsRawValue($this->value ?? '')) { - $this->validateOperator(); - } + return $this->value ?? ''; } /**