diff --git a/ProcessMaker/Filters/Filter.php b/ProcessMaker/Filters/Filter.php index 8a0e44707e..551d5b36e9 100644 --- a/ProcessMaker/Filters/Filter.php +++ b/ProcessMaker/Filters/Filter.php @@ -2,28 +2,30 @@ namespace ProcessMaker\Filters; +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; +use ProcessMaker\Traits\InteractsWithRawFilter; class Filter { - const TYPE_PARTICIPANTS = 'Participants'; + use InteractsWithRawFilter; - const TYPE_PARTICIPANTS_FULLNAME = 'ParticipantsFullName'; + public const TYPE_PARTICIPANTS = 'Participants'; - const TYPE_ASSIGNEES_FULLNAME = 'AssigneesFullName'; + public const TYPE_PARTICIPANTS_FULLNAME = 'ParticipantsFullName'; - const TYPE_STATUS = 'Status'; + public const TYPE_ASSIGNEES_FULLNAME = 'AssigneesFullName'; - const TYPE_FIELD = 'Field'; + public const TYPE_STATUS = 'Status'; - const TYPE_PROCESS = 'Process'; + public const TYPE_FIELD = 'Field'; - const TYPE_RELATIONSHIP = 'Relationship'; + public const TYPE_PROCESS = 'Process'; + + public const TYPE_RELATIONSHIP = 'Relationship'; public string|null $subjectValue; @@ -49,7 +51,18 @@ 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->or = Arr::get($definition, 'or', []); + + $this->detectRawValue(); + } + + public static function filter(Builder $query, string|array $filterDefinitions): void { if (is_string($filterDefinitions)) { $filterDefinitions = json_decode($filterDefinitions, true); @@ -66,27 +79,16 @@ 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); } } - private function apply($query) + private function apply($query): void { if ($valueAliasMethod = $this->valueAliasMethod()) { $this->valueAliasAdapter($valueAliasMethod, $query); @@ -137,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}"); } @@ -212,12 +218,12 @@ private function subject() return $this->subjectValue; } - private function relationshipSubjectTypeParts() + private function relationshipSubjectTypeParts(): array { return explode('.', $this->subjectValue); } - private function value() + public function value() { if ($this->operator === 'contains') { return '%' . $this->value . '%'; @@ -227,6 +233,10 @@ private function value() return $this->value . '%'; } + if ($this->filteringWithRawValue()) { + return $this->getRawValue(); + } + return $this->value; } @@ -258,16 +268,16 @@ private function valueAliasMethod() return $method; } - private function valueAliasAdapter(string $method, Builder $query) + private function valueAliasAdapter(string $method, Builder $query): void { $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') { @@ -292,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) { @@ -312,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..ef92adec8a --- /dev/null +++ b/ProcessMaker/Traits/InteractsWithRawFilter.php @@ -0,0 +1,119 @@ +', '<', '>=', '<=']; + + /** + * Unwrap the raw() and retrieve the string value passed + * + * @return \Illuminate\Contracts\Database\Query\Expression + */ + public function getRawValue(): Expression + { + // Get the string equivalent of the raw() filter value + $value = $this->containsRawValue($this->getValue()) ? $this->getValue() : ''; + + // Remove the actual row( and ) from the string + $unwrappedRawValue = $this->unwrapRawValue($value); + + // Wrap it in a DB expression and return it + return DB::raw($unwrappedRawValue); + } + + /** + * 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, ')'); + } + + /** + * Sets related properties + * + * @return void + */ + protected function detectRawValue(): void + { + $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); + + // If so, validate it is being used with a compatible operator + if ($this->usesRawValue) { + $this->validateOperator(); + } + } + + /** + * Remove the initial "row(" and the final ")" to unwrap the filter value + * + * @param string $value + * + * @return string + */ + protected function unwrapRawValue(string $value): string + { + $stripped = Str::after($value, 'raw('); + + return Str::beforeLast($stripped, ')'); + } + + /** + * Get the string value of the filter + * + * @return array|string + */ + protected function getValue(): mixed + { + return $this->value ?? ''; + } + + /** + * 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.'); + } + } +} 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 = [