Passed
Push — master ( 87b47c...561fa9 )
by Nils
06:37
created

TaskWorker::getMiscSetting()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 15
rs 10
1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This file is part of the TeamPass project.
6
 * 
7
 * TeamPass is free software: you can redistribute it and/or modify it
8
 * under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, version 3 of the License.
10
 * 
11
 * TeamPass is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 * 
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 * 
19
 * Certain components of this file may be under different licenses. For
20
 * details, see the `licenses` directory or individual file headers.
21
 * ---
22
 * @file      background_tasks___worker.php
23
 * @author    Nils Laumaillé ([email protected])
24
 * @copyright 2009-2026 Teampass.net
25
 * @license   GPL-3.0
26
 * @see       https://www.teampass.net
27
 */
28
29
use TeampassClasses\ConfigManager\ConfigManager;
30
use TeampassClasses\Language\Language;
31
require_once __DIR__.'/../sources/main.functions.php';
32
require_once __DIR__.'/background_tasks___functions.php';
33
require_once __DIR__.'/traits/ItemHandlerTrait.php';
34
require_once __DIR__.'/traits/UserHandlerTrait.php';
35
require_once __DIR__.'/traits/EmailTrait.php';
36
require_once __DIR__.'/traits/MigrateUserHandlerTrait.php';
37
require_once __DIR__ . '/taskLogger.php';
38
39
class TaskWorker {
40
    use ItemHandlerTrait;
41
    use UserHandlerTrait;
42
    use EmailTrait;
43
    use MigrateUserHandlerTrait;
44
45
    private $taskId;
46
    private $processType;
47
    private $taskData;
48
    private $settings;
49
    private $logger;
50
51
    public function __construct(int $taskId, string $processType, array $taskData) {
52
        $this->taskId = $taskId;
53
        $this->processType = $processType;
54
        $this->taskData = $taskData;
55
        
56
        $configManager = new ConfigManager();
57
        $this->settings = $configManager->getAllSettings();
58
        $this->logger = new TaskLogger($this->settings, LOG_TASKS_FILE);
59
    }
60
61
    /**
62
     * Execute the task based on its type.
63
     * This method will handle different types of tasks such as item copy, new item creation,
64
     * user cache tree building, email sending, and user key generation.
65
     * 
66
     * @return void
67
     */
68
    public function execute() {
69
        try {
70
            if (LOG_TASKS=== true) $this->logger->log('Processing task: ' . print_r($this->taskData, true), 'DEBUG');
71
            // Dispatch selon le type de processus
72
            switch ($this->processType) {
73
                case 'item_copy':
74
                    $this->processSubTasks($this->taskData);
75
                    break;
76
                case 'new_item':
77
                    $this->processSubTasks($this->taskData);
78
                    break;
79
                case 'item_update_create_keys':
80
                    $this->processSubTasks($this->taskData);
81
                    break;
82
                case 'user_build_cache_tree':
83
                    $this->handleUserBuildCacheTree($this->taskData);
84
                    break;
85
                case 'send_email':
86
                    $this->sendEmail($this->taskData);
87
                    break;
88
                case 'create_user_keys':
89
                    $this->generateUserKeys($this->taskData);
90
                    break;
91
                case 'migrate_user_personal_items':
92
                    $this->migratePersonalItems($this->taskData);
93
                    break;
94
                case 'database_backup':
95
                    $this->handleDatabaseBackup($this->taskData);
96
                    break;
97
                default:
98
                    throw new Exception("Type of subtask unknown: {$this->processType}");
99
            }
100
101
            // Mark the task as completed
102
            try {
103
                $this->completeTask();
104
            } catch (Exception $e) {
105
                $this->handleTaskFailure($e);
106
            }
107
108
        } catch (Exception $e) {
109
            $this->handleTaskFailure($e);
110
        }
111
112
    }
113
    /**
114
     * Perform a scheduled database backup (encrypted) into files/backups.
115
     */
116
    private function handleDatabaseBackup(array $taskData): void
117
    {
118
        require_once __DIR__ . '/../sources/backup.functions.php';
119
120
        // Default target dir: <path_to_files_folder>/backups
121
        $baseFilesDir = (string)($this->settings['path_to_files_folder'] ?? (__DIR__ . '/../files'));
122
        $targetDir = rtrim($baseFilesDir, '/') . '/backups';
123
124
        // Allow override via task arguments (optional)
125
        if (!empty($taskData['output_dir']) && is_string($taskData['output_dir'])) {
126
            $targetDir = rtrim($taskData['output_dir'], '/');
127
        }
128
129
        if (!is_dir($targetDir)) {
130
            if (!@mkdir($targetDir, 0770, true) && !is_dir($targetDir)) {
131
                throw new Exception('Cannot create backup target dir: ' . $targetDir);
132
            }
133
        }
134
        if (!is_writable($targetDir)) {
135
            throw new Exception('Backup target dir is not writable: ' . $targetDir);
136
        }
137
138
        // Use stored encryption key (same as UI)
139
        $encryptionKey = (string)($this->settings['bck_script_passkey'] ?? '');
140
        if ($encryptionKey === '') {
141
            throw new Exception('Missing encryption key (bck_script_passkey).');
142
        }
143
144
// Auto-disconnect connected users before running a scheduled backup.
145
// Exclude the user who enqueued the task (manual run), if provided.
146
try {
147
    if (function_exists('loadClasses') && !class_exists('DB')) {
148
        loadClasses('DB');
149
    }
150
    $excludeUserId = (int) ($taskData['initiator_user_id'] ?? 0);
151
    $now = time();
152
153
    if ($excludeUserId > 0) {
154
        $connectedUsers = DB::query(
155
            'SELECT id FROM ' . prefixTable('users') . ' WHERE session_end >= %i AND id != %i',
156
            $now,
157
            $excludeUserId
158
        );
159
    } else {
160
        $connectedUsers = DB::query(
161
            'SELECT id FROM ' . prefixTable('users') . ' WHERE session_end >= %i',
162
            $now
163
        );
164
    }
165
166
    foreach ($connectedUsers as $u) {
167
        DB::update(
168
            prefixTable('users'),
169
            [
170
                'key_tempo' => '',
171
                'timestamp' => '',
172
                'session_end' => '',
173
            ],
174
            'id = %i',
175
            (int) $u['id']
176
        );
177
    }
178
} catch (Throwable $ignored) {
179
    // Best effort only - do not block backups if disconnection cannot be done
180
}
181
182
        $res = tpCreateDatabaseBackup($this->settings, $encryptionKey, [
183
            'output_dir' => $targetDir,
184
            'filename_prefix' => 'scheduled-',
185
        ]);
186
187
        if (($res['success'] ?? false) !== true) {
188
            throw new Exception($res['message'] ?? 'Backup failed');
189
        }
190
191
        
192
// Best effort: write metadata sidecar next to the backup file
193
try {
194
    if (!empty($res['filepath']) && is_string($res['filepath']) && function_exists('tpWriteBackupMetadata')) {
195
        tpWriteBackupMetadata((string) $res['filepath'], '', '', ['source' => 'scheduled']);
196
    }
197
} catch (Throwable $ignored) {
198
    // do not block backups if metadata cannot be written
199
}
200
// Store a tiny summary for the task completion "arguments" field (no secrets)
201
        $this->taskData['backup_file'] = $res['filename'] ?? '';
202
        $this->taskData['backup_size_bytes'] = (int)($res['size_bytes'] ?? 0);
203
        $this->taskData['backup_encrypted'] = (bool)($res['encrypted'] ?? false);
204
205
        // Retention purge (scheduled backups only)
206
        $backupSource = (string)($taskData['source'] ?? '');
207
        $backupDir = (string)($taskData['output_dir'] ?? '');   // from task arguments (reliable)
208
        if ($backupDir === '') {
209
            $backupDir = (string) $targetDir;
210
        }
211
212
        // keep for debug/trace
213
        $this->taskData['output_dir'] = $backupDir;
214
215
        if ($backupSource === 'scheduler' && $backupDir !== '') {
216
            $days = (int)$this->getMiscSetting('bck_scheduled_retention_days', '30');
217
            $deleted = $this->purgeOldScheduledBackups($backupDir, $days);
218
219
            $this->upsertMiscSetting('bck_scheduled_last_purge_at', (string)time());
220
            $this->upsertMiscSetting('bck_scheduled_last_purge_deleted', (string)$deleted);
221
222
            if (LOG_TASKS === true) {
223
                $this->logger->log("database_backup: purge retention={$days}d dir={$backupDir} deleted={$deleted}", 'INFO');
224
            }
225
        }
226
227
        // If launched by scheduler, update scheduler status in teampass_misc
228
        if (!empty($taskData['source']) && $taskData['source'] === 'scheduler') {
229
            $this->updateSchedulerState('completed', 'Backup created: ' . ($this->taskData['backup_file'] ?? ''));
230
            try {
231
                $this->queueScheduledBackupReportEmail('completed', 'Backup created: ' . ($this->taskData['backup_file'] ?? ''));
232
            } catch (Throwable $ignored) {
233
                // best effort only - never block the backup process
234
            }
235
        }
236
237
        if (LOG_TASKS === true) {
238
            $this->logger->log(
239
                'database_backup: created ' . ($this->taskData['backup_file'] ?? '') . ' (' . $this->taskData['backup_size_bytes'] . ' bytes)',
240
                'INFO'
241
            );
242
        }
243
    }
244
245
    /**
246
     * Mark the task as completed in the database.
247
     * This method updates the task status to 'completed' and sets the finished_at timestamp.
248
     * 
249
     * @return void
250
     */
251
    private function updateSchedulerState(string $status, string $message): void
252
    {
253
        $this->upsertMiscSetting('bck_scheduled_last_status', $status);
254
        $this->upsertMiscSetting('bck_scheduled_last_message', mb_substr($message, 0, 500));
255
        $this->upsertMiscSetting('bck_scheduled_last_completed_at', (string)time());
256
    }
257
258
    private function formatBytes(int $bytes): string
259
    {
260
        if ($bytes < 1024) {
261
            return (string)$bytes . ' B';
262
        }
263
264
        $units = ['KB', 'MB', 'GB', 'TB', 'PB'];
265
        $i = 0;
266
        $value = (float)$bytes / 1024.0;
267
        while ($value >= 1024.0 && $i < count($units) - 1) {
268
            $value /= 1024.0;
269
            $i++;
270
        }
271
        return number_format($value, 1, '.', '') . ' ' . $units[$i];
272
    }
273
274
    private function queueScheduledBackupReportEmail(string $status, string $message): void
275
    {
276
        $enabled = (int)$this->getMiscSetting('bck_scheduled_email_report_enabled', '0');
277
        if ($enabled !== 1) {
278
            return;
279
        }
280
281
        $onlyFailures = (int)$this->getMiscSetting('bck_scheduled_email_report_only_failures', '0');
282
        if ($onlyFailures === 1 && $status !== 'failed') {
283
            return;
284
        }
285
286
        $admins = DB::query(
287
            'SELECT login, email, user_language FROM ' . prefixTable('users') . " WHERE admin = %i AND disabled = %i AND email != ''",
288
            1,
289
            0
290
        );
291
292
        if (empty($admins)) {
293
            return;
294
        }
295
296
        $backupFile = (string)($this->taskData['backup_file'] ?? '');
297
        $sizeBytes = (int)($this->taskData['backup_size_bytes'] ?? 0);
298
        $outputDir = (string)($this->taskData['output_dir'] ?? '');
299
        $retentionDays = (int)$this->getMiscSetting('bck_scheduled_retention_days', '30');
300
        $purgeDeleted = (int)$this->getMiscSetting('bck_scheduled_last_purge_deleted', '0');
301
302
        $dt = date('Y-m-d H:i:s');
303
304
        foreach ($admins as $a) {
305
            $email = (string)($a['email'] ?? '');
306
            if ($email === '') {
307
                continue;
308
            }
309
310
            $ul = (string)($a['user_language'] ?? 'english');
311
            if ($ul === '' || $ul === '0') {
312
                $ul = 'english';
313
            }
314
315
            $lang = new Language($ul);
316
317
            $subjectTpl = (string)$lang->get('email_subject_scheduled_backup_report');
318
            if ($subjectTpl === '') {
319
                $subjectTpl = 'Scheduled backup report: #tp_status#';
320
            }
321
            $bodyTpl = (string)$lang->get('email_body_scheduled_backup_report');
322
            if ($bodyTpl === '') {
323
                $bodyTpl = 'Hello,<br><br>Status: #tp_status#<br>Message: #tp_message#<br><br>';
324
            }
325
326
            $subject = str_replace(['#tp_status#'], [$status], $subjectTpl);
327
328
            $body = str_replace(
329
                ['#tp_status#', '#tp_datetime#', '#tp_message#', '#tp_file#', '#tp_size#', '#tp_output_dir#', '#tp_retention_days#', '#tp_purge_deleted#'],
330
                [
331
                    htmlspecialchars((string)$status, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
332
                    htmlspecialchars((string)$dt, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
333
                    htmlspecialchars((string)$message, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
334
                    htmlspecialchars((string)$backupFile, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
335
                    htmlspecialchars($this->formatBytes($sizeBytes), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
336
                    htmlspecialchars((string)$outputDir, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
337
                    htmlspecialchars((string)$retentionDays, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
338
                    htmlspecialchars((string)$purgeDeleted, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
339
                ],
340
                $bodyTpl
341
            );
342
343
            $receiverName = (string)($a['login'] ?? $lang->get('administrator'));
344
            prepareSendingEmail($subject, $body, $email, $receiverName);
345
        }
346
    }
347
348
349
    private function upsertMiscSetting(string $key, string $value): void
350
    {
351
        $table = prefixTable('misc');
352
353
        $exists = (int)DB::queryFirstField(
354
            'SELECT COUNT(*) FROM ' . $table . ' WHERE type = %s AND intitule = %s',
355
            'settings',
356
            $key
357
        );
358
359
        if ($exists > 0) {
360
            DB::update($table, ['valeur' => $value], 'type = %s AND intitule = %s', 'settings', $key);
361
        } else {
362
            DB::insert($table, ['type' => 'settings', 'intitule' => $key, 'valeur' => $value]);
363
        }
364
    }
365
366
    private function getMiscSetting(string $key, string $default = ''): string
367
    {
368
        $table = prefixTable('misc');
369
370
        $val = DB::queryFirstField(
371
            'SELECT valeur FROM ' . $table . ' WHERE type = %s AND intitule = %s LIMIT 1',
372
            'settings',
373
            $key
374
        );
375
376
        if ($val === null || $val === false || $val === '') {
377
            return $default;
378
        }
379
380
        return (string) $val;
381
    }
382
383
    private function purgeOldScheduledBackups(string $dir, int $retentionDays): int
384
    {
385
        if ($retentionDays <= 0) {
386
            return 0; // 0 => désactivé
387
        }
388
389
        if (!is_dir($dir)) {
390
            return 0;
391
        }
392
393
        $cutoff = time() - ($retentionDays * 86400);
394
        $deleted = 0;
395
396
        foreach (glob(rtrim($dir, '/') . '/scheduled-*.sql') as $file) {
397
            if (!is_file($file)) {
398
                continue;
399
            }
400
401
            $mtime = @filemtime($file);
402
            if ($mtime !== false && $mtime < $cutoff) {
403
                if (@unlink($file)) {
404
                    $deleted++;
405
                    // Also remove metadata sidecar if present
406
                    @unlink($file . '.meta.json');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

406
                    /** @scrutinizer ignore-unhandled */ @unlink($file . '.meta.json');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
407
}
408
            }
409
        }
410
411
        return $deleted;
412
    }
413
414
    private function completeTask() {
415
        // Prepare data for updating the task status
416
        $updateData = [
417
            'is_in_progress' => -1,
418
            'finished_at' => time(),
419
            'status' => 'completed',
420
            'error_message' => null,   // <-- on efface toute erreur précédente
421
        ];
422
423
        // Prepare anonimzation of arguments
424
        if ($this->processType === 'send_email') {
425
            $arguments = json_encode(
426
                [
427
                    'email' => $this->taskData['receivers'],
428
                    'login' => $this->taskData['receiver_name'],
429
                ]
430
            );
431
        } elseif ($this->processType === 'create_user_keys' || $this->processType === 'migrate_user_personal_items') {
432
            $arguments = json_encode(
433
                [
434
                    'user_id' => $this->taskData['new_user_id'],
435
                ]
436
            );
437
        } elseif ($this->processType === 'item_update_create_keys') {
438
            $arguments = json_encode(
439
                [
440
                    'item_id' => $this->taskData['item_id'],
441
                    'author' => $this->taskData['author'],
442
                ]
443
            );
444
        } elseif ($this->processType === 'database_backup') {
445
            $arguments = json_encode(
446
                [
447
                    'file' => $this->taskData['backup_file'] ?? '',
448
                    'size_bytes' => $this->taskData['backup_size_bytes'] ?? 0,
449
                    'encrypted' => $this->taskData['backup_encrypted'] ?? false,
450
                ]
451
            );
452
        } else {
453
            $arguments = '';
454
        }
455
456
        if (LOG_TASKS=== true) $this->logger->log('Process: '.$this->processType.' -- '.print_r($arguments, true), 'DEBUG');
457
458
        // Add 'arguments' only if not empty
459
        if (!empty($arguments)) {
460
            $updateData['arguments'] = $arguments;
461
        }
462
463
        // Store completed status in the database
464
        DB::update(
465
            prefixTable('background_tasks'),
466
            $updateData,
467
            'increment_id = %i',
468
            $this->taskId
469
        );
470
471
        if (LOG_TASKS=== true) $this->logger->log('Finishing task: ' . $this->taskId, 'DEBUG');
472
    }
473
474
    /**
475
     * Handle task failure by updating the task status in the database.
476
     * This method sets the task status to 'failed', updates the finished_at timestamp,
477
     * and logs the error message.
478
     * 
479
     * @param Exception $e The exception that occurred during task processing.
480
     * @return void
481
     */
482
    private function handleTaskFailure(Throwable $e) {
483
        DB::update(
484
            prefixTable('background_tasks'),
485
            [
486
                'is_in_progress' => -1,
487
                'finished_at' => time(),
488
                'status' => 'failed',
489
                'error_message' => $e->getMessage()
490
            ],
491
            'increment_id = %i',
492
            $this->taskId
493
        );
494
        $this->logger->log('Task failure: ' . $e->getMessage(), 'ERROR');
495
496
        // If a scheduled backup failed, update scheduler state and optionally send email report (via background tasks)
497
        if ($this->processType === 'database_backup' && (string)($this->taskData['source'] ?? '') === 'scheduler') {
498
            try {
499
                $this->updateSchedulerState('failed', $e->getMessage());
500
                $this->queueScheduledBackupReportEmail('failed', $e->getMessage());
501
            } catch (Throwable $ignored) {
502
                // best effort only
503
            }
504
        }
505
506
    // Purge retention even on failure (safe: only scheduled-*.sql)
507
        $backupDir = (string)($this->taskData['output_dir'] ?? '');
508
        if ($backupDir !== '' && is_dir($backupDir)) {
509
        $days = (int)$this->getMiscSetting('bck_scheduled_retention_days', '30');
510
        $deleted = $this->purgeOldScheduledBackups($backupDir, $days);
511
512
        $this->upsertMiscSetting('bck_scheduled_last_purge_at', (string)time());
513
        $this->upsertMiscSetting('bck_scheduled_last_purge_deleted', (string)$deleted);
514
        }
515
    }
516
517
    /**
518
     * Handle subtasks for the current task.
519
     * This method retrieves all subtasks related to the current task and processes them.
520
     * If all subtasks are completed, it marks the main task as completed.
521
     * 
522
     * @param array $arguments Arguments for the subtasks.
523
     * @return void
524
     */
525
    private function processSubTasks($arguments) {
526
        if (LOG_TASKS=== true) $this->logger->log('processSubTasks: '.print_r($arguments, true), 'DEBUG');
527
        // Get all subtasks related to this task
528
        $subtasks = DB::query(
529
            'SELECT * FROM ' . prefixTable('background_subtasks') . ' WHERE task_id = %i AND is_in_progress = 0 ORDER BY `task` ASC',
530
            $this->taskId
531
        );
532
    
533
        // Check if there are any subtasks to process
534
        if (empty($subtasks)) {
535
            if (LOG_TASKS=== true) $this->logger->log('No subtask was found for task: ' . $this->taskId, 'DEBUG');
536
            return;
537
        }
538
    
539
        // Process each subtask
540
        foreach ($subtasks as $subtask) {
541
            try {
542
                // Get the subtask data
543
                $subtaskData = json_decode($subtask['task'], true);
544
545
                if (LOG_TASKS=== true) $this->logger->log('Processing subtask: ' . $subtaskData['step'], 'DEBUG');
546
547
                // Mark subtask as in progress
548
                DB::update(
549
                    prefixTable('background_tasks'),
550
                    ['updated_at' => time()],
551
                    'increment_id = %i',
552
                    $this->taskId
553
                );
554
555
                // Process the subtask based on its type
556
                switch ($subtaskData['step'] ?? '') {
557
                    case 'create_users_pwd_key':
558
                        $this->generateUserPasswordKeys($arguments);
559
                        break;
560
                    case 'create_users_fields_key':
561
                        $this->generateUserFieldKeys($subtaskData);
562
                        break;
563
                    case 'create_users_files_key':
564
                        $this->generateUserFileKeys($subtaskData);
565
                        break;
566
                    default:
567
                        throw new Exception("Type de sous-tâche inconnu (".$subtaskData['step'].")");
568
                }                
569
        
570
                // Mark subtask as completed
571
                DB::update(
572
                    prefixTable('background_subtasks'),
573
                    [
574
                        'is_in_progress' => -1,
575
                        'finished_at' => time(),
576
                        'status' => 'completed',
577
                    ],
578
                    'increment_id = %i',
579
                    $subtask['increment_id']
580
                );
581
        
582
            } catch (Exception $e) {
583
                // Mark subtask as failed
584
                DB::update(
585
                    prefixTable('background_subtasks'),
586
                    [
587
                        'is_in_progress' => -1,
588
                        'finished_at' => time(),
589
                        'updated_at' => time(),
590
                        'status' => 'failed',
591
                        'error_message' => $e->getMessage(),
592
                    ],
593
                    'increment_id = %i',
594
                    $subtask['increment_id']
595
                );
596
        
597
                $this->logger->log('processSubTasks : ' . $e->getMessage(), 'ERROR');
598
            }
599
        }
600
    
601
        // Are all subtasks completed?
602
        $remainingSubtasks = DB::queryFirstField(
603
            'SELECT COUNT(*) FROM ' . prefixTable('background_subtasks') . ' WHERE task_id = %i AND is_in_progress = 0',
604
            $this->taskId
605
        );
606
    
607
        if ($remainingSubtasks == 0) {
608
            $this->completeTask();
609
        }
610
    }
611
}
612
613
// Prepare the environment
614
// Get the task ID and process type from command line arguments
615
if ($argc < 3) {
616
    error_log("Usage: php background_tasks___worker.php <task_id> <process_type> [<task_data>]");
617
    exit(1);
618
}
619
$taskId = (int)$argv[1];
620
$processType = $argv[2];
621
$taskData = $argv[3] ?? null;
622
if ($taskData) {
623
    $taskData = json_decode($taskData, true);
624
    if (!is_array($taskData)) {
625
        $taskData = [];
626
    }
627
} else {
628
    $taskData = [];
629
}
630
631
// Initialize the worker
632
$worker = new TaskWorker($taskId, $processType, $taskData);
633
$worker->execute();
634