Skip to content

Commit a5dfb1f

Browse files
committed
Merge branch 'develop' into bugfix/FOUR-30871
2 parents 33a0d79 + da9e81b commit a5dfb1f

148 files changed

Lines changed: 7900 additions & 740 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ OPEN_AI_NLQ_TO_PMQL_ENABLED=true
4545
OPEN_AI_PROCESS_TRANSLATIONS_ENABLED=true
4646
OPEN_AI_SECRET="sk-O2D..."
4747
AI_MICROSERVICE_HOST="http://localhost:8010"
48+
AI_MICROSERVICE_HOST_WS="wss://localhost:8010"
4849
PROCESS_REQUEST_ERRORS_RATE_LIMIT=1
4950
PROCESS_REQUEST_ERRORS_RATE_LIMIT_DURATION=86400
5051
CUSTOM_EXECUTORS=false
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace ProcessMaker\CaseRetention;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
7+
final class CaseRetentionLogCsvWriter
8+
{
9+
/**
10+
* Stream CSV rows to a writable stream (no column header row). UTF-8 BOM prepended.
11+
*
12+
* @param resource $stream
13+
*/
14+
public static function writeQueryToStream(Builder $query, $stream): void
15+
{
16+
fwrite($stream, "\xEF\xBB\xBF");
17+
18+
$query->clone()->chunkById(500, function ($rows) use ($stream) {
19+
foreach ($rows as $row) {
20+
$caseIds = $row->case_ids;
21+
if (is_array($caseIds)) {
22+
$caseIds = json_encode($caseIds);
23+
}
24+
25+
fputcsv($stream, [
26+
$row->id,
27+
$row->process_id,
28+
$caseIds,
29+
$row->deleted_count,
30+
$row->total_time_taken,
31+
self::csvDateColumn($row->deleted_at),
32+
self::csvDateColumn($row->created_at),
33+
]);
34+
}
35+
});
36+
}
37+
38+
public static function csvDateColumn(mixed $value): string
39+
{
40+
if ($value === null || $value === '') {
41+
return '';
42+
}
43+
if ($value instanceof \DateTimeInterface) {
44+
return $value->format('Y-m-d H:i:s');
45+
}
46+
47+
return (string) $value;
48+
}
49+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace ProcessMaker\CaseRetention;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
7+
final class CaseRetentionLogQueryFilter
8+
{
9+
public static function applyIfFilled(Builder $query, ?string $filter): void
10+
{
11+
if ($filter === null || trim($filter) === '') {
12+
return;
13+
}
14+
15+
self::apply($query, trim($filter));
16+
}
17+
18+
/**
19+
* Search log id, process_id, numeric columns, and JSON case_ids — not date columns.
20+
*/
21+
public static function apply(Builder $query, string $term): void
22+
{
23+
$like = '%' . $term . '%';
24+
$driver = $query->getConnection()->getDriverName();
25+
26+
$query->where(function ($q) use ($like, $driver) {
27+
$q->where('id', 'like', $like)
28+
->orWhere('process_id', 'like', $like)
29+
->orWhere('deleted_count', 'like', $like)
30+
->orWhere('total_time_taken', 'like', $like);
31+
32+
if ($driver === 'pgsql') {
33+
$q->orWhereRaw('case_ids::text ILIKE ?', [$like]);
34+
} else {
35+
$q->orWhereRaw('CAST(case_ids AS CHAR) LIKE ?', [$like]);
36+
}
37+
});
38+
}
39+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace ProcessMaker\Console\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use ProcessMaker\Jobs\EvaluateProcessRetentionJob;
7+
use ProcessMaker\Models\Process;
8+
use ProcessMaker\Models\ProcessCategory;
9+
use ProcessMaker\Services\CaseRetentionTierService;
10+
11+
class EvaluateCaseRetention extends Command
12+
{
13+
/**
14+
* The name and signature of the console command.
15+
*
16+
* @var string
17+
*/
18+
protected $signature = 'cases:retention:evaluate';
19+
20+
/**
21+
* The console command description.
22+
*
23+
* @var string
24+
*/
25+
protected $description = 'Evaluate and delete cases past their retention period';
26+
27+
/**
28+
* Execute the console command.
29+
*/
30+
public function handle()
31+
{
32+
// Only run if case retention policy is enabled
33+
$enabled = config('app.case_retention_policy_enabled', false);
34+
if (!$enabled) {
35+
$this->info('Case retention policy is disabled');
36+
$this->error('Skipping case retention evaluation');
37+
38+
return;
39+
}
40+
41+
$this->info('Case retention policy is enabled');
42+
$this->info('Dispatching retention evaluation jobs for all processes');
43+
// Get the allowed periods for the current tier (support for downgrading to a lower tier)
44+
$tierAllowedPeriods = CaseRetentionTierService::allowedPeriodsForCurrentTier();
45+
46+
// Get system category IDs to exclude
47+
$systemCategoryIds = ProcessCategory::where('is_system', true)->pluck('id');
48+
49+
// Exclude processes that are templates or in system categories
50+
$jobCount = 0;
51+
$query = Process::where('is_template', '!=', 1);
52+
53+
// Exclude processes in system categories
54+
if ($systemCategoryIds->isNotEmpty()) {
55+
$query->where(function ($q) use ($systemCategoryIds) {
56+
$q->where(function ($subQuery) use ($systemCategoryIds) {
57+
$subQuery->whereNotIn('process_category_id', $systemCategoryIds)
58+
->orWhereNull('process_category_id');
59+
});
60+
})
61+
->whereDoesntHave('categories', function ($q) use ($systemCategoryIds) {
62+
// Exclude processes with any category assignment to system categories
63+
$q->whereIn('process_categories.id', $systemCategoryIds);
64+
});
65+
}
66+
67+
$query->chunkById(100, function ($processes) use (&$jobCount, $tierAllowedPeriods) {
68+
foreach ($processes as $process) {
69+
dispatch(new EvaluateProcessRetentionJob($process->id, $tierAllowedPeriods));
70+
$jobCount++;
71+
}
72+
});
73+
74+
$this->info("Dispatched {$jobCount} retention evaluation job(s) to the queue");
75+
$this->info('Jobs will be processed asynchronously by queue workers');
76+
}
77+
}

ProcessMaker/Console/Kernel.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ protected function schedule(Schedule $schedule)
9090
break;
9191
}
9292

93+
// evaluate cases retention policy
94+
$schedule->command('cases:retention:evaluate')
95+
->daily()
96+
->onOneServer()
97+
->withoutOverlapping()
98+
->runInBackground();
99+
93100
// 5 minutes is recommended in https://laravel.com/docs/12.x/horizon#metrics
94101
$schedule->command('horizon:snapshot')->everyFiveMinutes();
95102
}

ProcessMaker/Contracts/PermissionRepositoryInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@ public function getGroupPermissionsById(int $groupId): array;
3333
* Get nested group permissions (recursive)
3434
*/
3535
public function getNestedGroupPermissions(int $groupId): array;
36+
37+
/**
38+
* Get all users affected by permissions inherited from the given group subtree.
39+
*/
40+
public function getAffectedUserIdsForGroup(int $groupId): array;
3641
}

ProcessMaker/Helpers/ScreenTemplateHelper.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@
44

55
class ScreenTemplateHelper
66
{
7+
private const RENDERABLE_STRING_FIELDS = [
8+
'ariaLabel',
9+
'content',
10+
'fieldValue',
11+
'helper',
12+
'label',
13+
'loadingLabel',
14+
'placeholder',
15+
];
16+
17+
/**
18+
* Remove serialized Vue component definitions from screen config.
19+
*
20+
* Screen templates can contain old inspector metadata where inspector.type
21+
* is a serialized Vue component object. That data is not needed at runtime
22+
* and can reach renderer paths that expect Mustache templates to be strings.
23+
*/
24+
public static function sanitizeScreenConfig(mixed $config): array
25+
{
26+
if (!is_array($config)) {
27+
return [];
28+
}
29+
30+
return self::sanitizeConfigValue($config);
31+
}
32+
733
/**
834
* Remove screen components from the configuration based on the provided components.
935
*
@@ -402,4 +428,59 @@ public static function generateCss($cssArray)
402428

403429
return $cssString;
404430
}
431+
432+
private static function sanitizeConfigValue(mixed $value, ?string $key = null): mixed
433+
{
434+
if ($key === 'validation' && is_array($value) && $value === []) {
435+
$sanitized = null;
436+
} elseif (in_array($key, self::RENDERABLE_STRING_FIELDS, true)) {
437+
$sanitized = self::sanitizeRenderableString($value);
438+
} elseif (!is_array($value)) {
439+
$sanitized = $value;
440+
} elseif (array_is_list($value)) {
441+
$sanitized = array_map(fn ($item) => self::sanitizeConfigValue($item), $value);
442+
} else {
443+
$sanitized = [];
444+
foreach ($value as $childKey => $childValue) {
445+
if ($childKey === 'inspector' && is_array($childValue)) {
446+
$sanitized[$childKey] = array_map(
447+
fn ($item) => self::sanitizeInspectorItem($item),
448+
$childValue
449+
);
450+
continue;
451+
}
452+
453+
$sanitized[$childKey] = self::sanitizeConfigValue($childValue, (string) $childKey);
454+
}
455+
}
456+
457+
return $sanitized;
458+
}
459+
460+
private static function sanitizeInspectorItem(mixed $item): mixed
461+
{
462+
if (!is_array($item)) {
463+
return $item;
464+
}
465+
466+
$sanitized = [];
467+
foreach ($item as $key => $value) {
468+
if ($key === 'type' && is_array($value)) {
469+
continue;
470+
}
471+
472+
$sanitized[$key] = self::sanitizeConfigValue($value, (string) $key);
473+
}
474+
475+
return $sanitized;
476+
}
477+
478+
private static function sanitizeRenderableString(mixed $value): string
479+
{
480+
if ($value === null || is_array($value)) {
481+
return '';
482+
}
483+
484+
return is_string($value) ? $value : (string) $value;
485+
}
405486
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace ProcessMaker\Http\Controllers\Admin;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Facades\Http;
7+
use ProcessMaker\Http\Controllers\Controller;
8+
9+
class CasesRetentionController extends Controller
10+
{
11+
public function index(Request $request)
12+
{
13+
return view('admin.cases-retention.index');
14+
}
15+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace ProcessMaker\Http\Controllers\Admin;
4+
5+
use Illuminate\Http\Request;
6+
use ProcessMaker\Http\Controllers\Controller;
7+
8+
class LogsController extends Controller
9+
{
10+
/**
11+
* Display the logs index page.
12+
* This view loads log components from installed packages (package-email-start-event, package-ai).
13+
*
14+
* @return \Illuminate\Contracts\View\View
15+
*/
16+
public function index()
17+
{
18+
return view('admin.logs.index');
19+
}
20+
}

ProcessMaker/Http/Controllers/Api/Actions/Cases/DeleteCase.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,20 +99,4 @@ private function getTaskDraftIds(array $tokenIds): array
9999
->pluck('id')
100100
->all();
101101
}
102-
103-
private function dispatchSavedSearchRecount(): void
104-
{
105-
if (!config('savedsearch.count', false)) {
106-
return;
107-
}
108-
109-
$jobClass = 'ProcessMaker\\Package\\SavedSearch\\Jobs\\RecountAllSavedSearches';
110-
if (!class_exists($jobClass)) {
111-
return;
112-
}
113-
114-
DB::afterCommit(static function () use ($jobClass): void {
115-
$jobClass::dispatch(['request', 'task']);
116-
});
117-
}
118102
}

0 commit comments

Comments
 (0)