Skip to content

Commit 38785c9

Browse files
committed
fix: implement validation for raw expressions, including support for 'between' operator for advanced filtering
1 parent 492e4a5 commit 38785c9

2 files changed

Lines changed: 110 additions & 30 deletions

File tree

‎ProcessMaker/Filters/BaseFilter.php‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ProcessMaker\Filters;
44

5+
use Illuminate\Contracts\Database\Query\Expression;
56
use Illuminate\Database\Eloquent\Builder;
67
use Illuminate\Support\Arr;
78
use Illuminate\Support\Facades\DB;
@@ -98,6 +99,8 @@ public function addToQuery(Builder $query): void
9899

99100
private function apply($query): void
100101
{
102+
$this->validateRawFilterContext();
103+
101104
if ($valueAliasMethod = $this->valueAliasMethod()) {
102105
$this->valueAliasAdapter($valueAliasMethod, $query);
103106
} elseif ($this->subjectType === self::TYPE_STAGE) {
@@ -175,7 +178,9 @@ private function manuallyAddJsonWhere($query): void
175178
$operator = $this->operator();
176179
$value = $this->value();
177180

178-
if (!is_numeric($value)) {
181+
if ($value instanceof Expression) {
182+
$value = $value->getValue(DB::connection()->getQueryGrammar());
183+
} elseif (!is_numeric($value)) {
179184
$value = DB::connection()->getPdo()->quote($value);
180185
}
181186

@@ -259,6 +264,34 @@ private function isJsonData()
259264
return $this->subjectType === self::TYPE_FIELD && str_starts_with($this->subjectValue, 'data.');
260265
}
261266

267+
private function validateRawFilterContext(): void
268+
{
269+
if (!$this->filteringWithRawValue()) {
270+
return;
271+
}
272+
273+
if ($this->subjectType !== self::TYPE_FIELD) {
274+
abort(422, 'Raw filters are only supported for fields.');
275+
}
276+
277+
if ($this->isJsonData()) {
278+
return;
279+
}
280+
281+
$allowedFields = [
282+
'created_at',
283+
'updated_at',
284+
'initiated_at',
285+
'completed_at',
286+
'due_at',
287+
'started_at',
288+
];
289+
290+
if (!in_array($this->subjectValue, $allowedFields, true)) {
291+
abort(422, 'Raw filters are only supported for temporal fields.');
292+
}
293+
}
294+
262295
private function subject()
263296
{
264297
if ($this->isJsonData()) {
@@ -299,11 +332,7 @@ public function value()
299332
return $this->value . '%';
300333
}
301334

302-
if ($this->filteringWithRawValue()) {
303-
return $this->getRawValue();
304-
}
305-
306-
return $this->value;
335+
return $this->valueWithRawExpressions($this->value);
307336
}
308337

309338
abstract protected function valueAliasMethod();

‎ProcessMaker/Traits/InteractsWithRawFilter.php‎

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44

55
use Illuminate\Contracts\Database\Query\Expression;
66
use Illuminate\Support\Facades\DB;
7-
use Illuminate\Support\Str;
8-
97
trait InteractsWithRawFilter
108
{
9+
private const MAX_INTERVAL = 365;
10+
1111
private bool $usesRawValue = false;
1212

1313
/**
1414
* Operators allowed to be used with raw()
1515
*
1616
* @var array
1717
*/
18-
private array $validRawFilterOperators = ['=', '!=', '>', '<', '>=', '<='];
18+
private array $validRawFilterOperators = ['=', '!=', '>', '<', '>=', '<=', 'between'];
1919

2020
/**
2121
* Unwrap the raw() and retrieve the string value passed
@@ -24,14 +24,30 @@ trait InteractsWithRawFilter
2424
*/
2525
public function getRawValue(): Expression
2626
{
27-
// Get the string equivalent of the raw() filter value
28-
$value = $this->containsRawValue($this->getValue()) ? $this->getValue() : '';
27+
$value = $this->getValue();
28+
$matches = [];
29+
$pattern = '/^\s*raw\(\s*NOW\(\)\s*(?:(\+|-)\s*INTERVAL\s+([1-9]\d*)\s+'
30+
. '(SECOND|MINUTE|HOUR|DAY|WEEK|MONTH))?\s*\)\s*$/iD';
31+
32+
if (!is_string($value) || preg_match($pattern, $value, $matches) !== 1) {
33+
abort(422, 'Invalid raw filter expression.');
34+
}
35+
36+
if (isset($matches[2]) && (int) $matches[2] > self::MAX_INTERVAL) {
37+
abort(422, 'Raw filter interval exceeds the maximum allowed value.');
38+
}
2939

30-
// Remove the actual row( and ) from the string
31-
$unwrappedRawValue = $this->unwrapRawValue($value);
40+
$expression = 'NOW()';
41+
if (isset($matches[1])) {
42+
$expression .= sprintf(
43+
' %s INTERVAL %d %s',
44+
$matches[1],
45+
(int) $matches[2],
46+
strtoupper($matches[3])
47+
);
48+
}
3249

33-
// Wrap it in a DB expression and return it
34-
return DB::raw($unwrappedRawValue);
50+
return DB::raw($expression);
3551
}
3652

3753
/**
@@ -43,8 +59,7 @@ public function getRawValue(): Expression
4359
*/
4460
public function containsRawValue(string $value): bool
4561
{
46-
return Str::contains($value, 'raw(')
47-
&& Str::endsWith($value, ')');
62+
return preg_match('/^\s*raw\s*\(/i', $value) === 1;
4863
}
4964

5065
/**
@@ -54,18 +69,19 @@ public function containsRawValue(string $value): bool
5469
*/
5570
protected function detectRawValue(): void
5671
{
57-
$value = $this->getValue();
58-
59-
// Sometimes, the value is an array, which likely means
60-
// this filter is set to the use the "between" operator
61-
$value = is_string($value) ? $value : '';
72+
$values = is_array($this->getValue()) ? $this->getValue() : [$this->getValue()];
73+
$this->usesRawValue = false;
6274

63-
// Detect if this particular filter includes a raw() value
64-
$this->usesRawValue = $this->containsRawValue($value);
75+
if ($this->operator === 'between' && (!is_array($this->getValue()) || count($values) !== 2)) {
76+
abort(422, 'The between operator requires exactly two values.');
77+
}
6578

66-
// If so, validate it is being used with a compatible operator
67-
if ($this->usesRawValue) {
68-
$this->validateOperator();
79+
foreach ($values as $value) {
80+
if (is_string($value) && $this->containsRawValue($value)) {
81+
$this->usesRawValue = true;
82+
$this->validateOperator();
83+
$this->validateRawValue($value);
84+
}
6985
}
7086
}
7187

@@ -78,9 +94,7 @@ protected function detectRawValue(): void
7894
*/
7995
protected function unwrapRawValue(string $value): string
8096
{
81-
$stripped = Str::after($value, 'raw(');
82-
83-
return Str::beforeLast($stripped, ')');
97+
return substr($value, 4, -1);
8498
}
8599

86100
/**
@@ -103,6 +117,43 @@ protected function filteringWithRawValue(): bool
103117
return $this->usesRawValue === true;
104118
}
105119

120+
protected function valueWithRawExpressions(mixed $value): mixed
121+
{
122+
if (is_array($value)) {
123+
return array_map(fn ($item) => $this->valueWithRawExpressions($item), $value);
124+
}
125+
126+
if (is_string($value) && $this->containsRawValue($value)) {
127+
return $this->getRawValueFor($value);
128+
}
129+
130+
return $value;
131+
}
132+
133+
private function getRawValueFor(string $value): Expression
134+
{
135+
$originalValue = $this->value;
136+
$this->value = $value;
137+
138+
try {
139+
return $this->getRawValue();
140+
} finally {
141+
$this->value = $originalValue;
142+
}
143+
}
144+
145+
private function validateRawValue(string $value): void
146+
{
147+
$originalValue = $this->value;
148+
$this->value = $value;
149+
150+
try {
151+
$this->getRawValue();
152+
} finally {
153+
$this->value = $originalValue;
154+
}
155+
}
156+
106157
/**
107158
* Validate the operator for this raw() filter
108159
*

0 commit comments

Comments
 (0)