diff --git a/ProcessMaker/Events/AuthClientDeleted.php b/ProcessMaker/Events/AuthClientDeleted.php
index 1eb9e92dfa..5d1875d6bc 100644
--- a/ProcessMaker/Events/AuthClientDeleted.php
+++ b/ProcessMaker/Events/AuthClientDeleted.php
@@ -2,6 +2,7 @@
namespace ProcessMaker\Events;
+use Carbon\Carbon;
use Illuminate\Foundation\Events\Dispatchable;
use ProcessMaker\Contracts\SecurityLogEventInterface;
@@ -18,10 +19,9 @@ class AuthClientDeleted implements SecurityLogEventInterface
*
* @return void
*/
- public function __construct(array $deleted_values)
+ public function __construct(array $values)
{
- $this->data = ['auth_client_id' => $deleted_values['id']];
- $this->changes = $deleted_values;
+ $this->changes = $values;
}
/**
@@ -29,7 +29,11 @@ public function __construct(array $deleted_values)
*/
public function getData(): array
{
- return $this->data;
+ return [
+ 'auth_client_id' => $this->changes['id'] ?? 0,
+ 'name' => $this->changes['name'] ?? 0,
+ 'deleted_at' => Carbon::now()
+ ];
}
/**
diff --git a/ProcessMaker/Events/FilesAccessed.php b/ProcessMaker/Events/FilesAccessed.php
new file mode 100644
index 0000000000..bffe4b7e47
--- /dev/null
+++ b/ProcessMaker/Events/FilesAccessed.php
@@ -0,0 +1,76 @@
+processName = $data->getAttribute('name');
+ // Link to the request
+ $this->linkFile = [
+ 'label' => $data->getAttribute('id'),
+ 'link' => route('requests.show', $data)
+ ];
+ } else {
+ // Link to file in the package
+ $this->linkFile = [
+ 'label' => $name,
+ 'link' => route('file-manager.index', ['public/' . $name]),
+ ];
+ }
+ }
+
+ /**
+ * Get specific data related to the event
+ *
+ * @return array
+ */
+ public function getData(): array
+ {
+ return [
+ 'name' => $this->linkFile,
+ 'process' => $this->processName,
+ 'accessed_at' => Carbon::now()
+ ];
+ }
+
+ /**
+ * Get specific data related to the event
+ *
+ * @return array
+ */
+ public function getChanges(): array
+ {
+ return [];
+ }
+
+ /**
+ * Get the Event name
+ *
+ * @return string
+ */
+ public function getEventName(): string
+ {
+ return 'FilesAccessed';
+ }
+}
diff --git a/ProcessMaker/Events/FilesCreated.php b/ProcessMaker/Events/FilesCreated.php
index 4c9a8b9302..51ba82ce62 100644
--- a/ProcessMaker/Events/FilesCreated.php
+++ b/ProcessMaker/Events/FilesCreated.php
@@ -5,6 +5,7 @@
use Illuminate\Foundation\Events\Dispatchable;
use ProcessMaker\Contracts\SecurityLogEventInterface;
use ProcessMaker\Models\Media;
+use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Traits\FormatSecurityLogChanges;
class FilesCreated implements SecurityLogEventInterface
@@ -12,17 +13,38 @@ class FilesCreated implements SecurityLogEventInterface
use Dispatchable;
use FormatSecurityLogChanges;
+ public const NAME_PUBLIC_FILES = 'Public Files';
+
private array $media;
+ private array $name = [];
+ private string $processName = '';
/**
* Create a new event instance.
*
* @return void
*/
- public function __construct(int $fileId)
+ public function __construct(int $fileId, ProcessRequest $data)
{
$this->media = Media::find(['id' => $fileId])->toArray();
$this->media = head($this->media);
+
+ // Check if the request is related to the package files
+ if (static::NAME_PUBLIC_FILES === $data->getAttribute('name')) {
+ $this->processName = '';
+ // Link to file in the package
+ $this->name = [
+ 'label' => $this->media['name'],
+ 'link' => route('file-manager.index', ['public/' . $this->media['name']]),
+ ];
+ } else {
+ $this->processName = $data->getAttribute('name');
+ // Link to the request
+ $this->name = [
+ 'label' => $data->getAttribute('id'),
+ 'link' => route('requests.show', $data)
+ ];
+ }
}
/**
@@ -33,10 +55,8 @@ public function __construct(int $fileId)
public function getData(): array
{
return [
- 'file_name' => [
- 'label' => $this->media['name'],
- 'link' => route('file-manager.index', ['public/' . $this->media['file_name']]),
- ],
+ 'name' => $this->name,
+ 'process' => $this->processName,
'created_at' => $this->media['created_at'],
];
}
diff --git a/ProcessMaker/Events/FilesDownloaded.php b/ProcessMaker/Events/FilesDownloaded.php
new file mode 100644
index 0000000000..e6787cc43b
--- /dev/null
+++ b/ProcessMaker/Events/FilesDownloaded.php
@@ -0,0 +1,76 @@
+processName = $data->getAttribute('name');
+ // Link to the request
+ $this->name = [
+ 'label' => $data->getAttribute('id'),
+ 'link' => route('requests.show', $data)
+ ];
+ } else {
+ // Link to file in the package
+ $this->name = [
+ 'label' => $file,
+ 'link' => route('file-manager.index', ['public/' . $file]),
+ ];
+ }
+ }
+
+ /**
+ * Get specific data related to the event
+ *
+ * @return array
+ */
+ public function getData(): array
+ {
+ return [
+ 'name' => $this->name,
+ 'process' => $this->processName,
+ 'accessed_at' => Carbon::now()
+ ];
+ }
+
+ /**
+ * Get specific data related to the event
+ *
+ * @return array
+ */
+ public function getChanges(): array
+ {
+ return [];
+ }
+
+ /**
+ * Get the Event name
+ *
+ * @return string
+ */
+ public function getEventName(): string
+ {
+ return 'FilesDownloaded';
+ }
+}
diff --git a/ProcessMaker/Events/ProcessPublished.php b/ProcessMaker/Events/ProcessPublished.php
index b07b76d8b1..1853324758 100644
--- a/ProcessMaker/Events/ProcessPublished.php
+++ b/ProcessMaker/Events/ProcessPublished.php
@@ -5,6 +5,7 @@
use Illuminate\Foundation\Events\Dispatchable;
use ProcessMaker\Contracts\SecurityLogEventInterface;
use ProcessMaker\Models\Process;
+use ProcessMaker\Models\ProcessCategory;
use ProcessMaker\Traits\FormatSecurityLogChanges;
class ProcessPublished implements SecurityLogEventInterface
@@ -39,6 +40,12 @@ public function __construct(Process $data, array $changes, array $original)
$this->process = $data;
$this->changes = array_diff_key($changes, array_flip($this::REMOVE_KEYS));
$this->original = array_diff_key($original, array_flip($this::REMOVE_KEYS));
+
+ // Get category name
+ $this->original['process_category'] = isset($original['process_category_id']) ? ProcessCategory::getNamesByIds($this->original['process_category_id']) : '';
+ unset($this->original['process_category_id']);
+ $this->changes['process_category'] = isset($changes['process_category_id']) ? ProcessCategory::getNamesByIds($this->changes['process_category_id']) : '';
+ unset($this->changes['process_category_id']);
}
/**
diff --git a/ProcessMaker/Events/ScreenCreated.php b/ProcessMaker/Events/ScreenCreated.php
index 040b651306..90cf0a241a 100644
--- a/ProcessMaker/Events/ScreenCreated.php
+++ b/ProcessMaker/Events/ScreenCreated.php
@@ -4,6 +4,7 @@
use Illuminate\Foundation\Events\Dispatchable;
use ProcessMaker\Contracts\SecurityLogEventInterface;
+use ProcessMaker\Models\ScreenCategory;
class ScreenCreated implements SecurityLogEventInterface
{
@@ -19,6 +20,11 @@ class ScreenCreated implements SecurityLogEventInterface
public function __construct(array $newScreen)
{
$this->newScreen = $newScreen;
+
+ if (isset($newScreen['screen_category_id'])) {
+ $this->newScreen['screen_category'] = ScreenCategory::getNamesByIds($newScreen['screen_category_id']);
+ unset($this->newScreen['screen_category_id']);
+ }
}
/**
diff --git a/ProcessMaker/Events/ScreenUpdated.php b/ProcessMaker/Events/ScreenUpdated.php
index a7dfe5e054..808f530215 100644
--- a/ProcessMaker/Events/ScreenUpdated.php
+++ b/ProcessMaker/Events/ScreenUpdated.php
@@ -5,6 +5,7 @@
use Illuminate\Foundation\Events\Dispatchable;
use ProcessMaker\Contracts\SecurityLogEventInterface;
use ProcessMaker\Models\Screen;
+use ProcessMaker\Models\ScreenCategory;
use ProcessMaker\Traits\FormatSecurityLogChanges;
class ScreenUpdated implements SecurityLogEventInterface
@@ -28,6 +29,12 @@ public function __construct(Screen $screen, array $changes, array $original)
$this->screen = $screen;
$this->changes = $changes;
$this->original = $original;
+
+ // Get category name
+ $this->original['screen_category'] = isset($original['screen_category_id']) ? ScreenCategory::getNamesByIds($this->original['screen_category_id']) : '';
+ unset($this->original['screen_category_id']);
+ $this->changes['screen_category'] = isset($changes['screen_category_id']) ? ScreenCategory::getNamesByIds($this->changes['screen_category_id']) : '';
+ unset($this->changes['screen_category_id']);
}
/**
diff --git a/ProcessMaker/Events/ScriptCreated.php b/ProcessMaker/Events/ScriptCreated.php
index 2cb128bdd0..31d7fb2a3b 100644
--- a/ProcessMaker/Events/ScriptCreated.php
+++ b/ProcessMaker/Events/ScriptCreated.php
@@ -5,6 +5,7 @@
use Illuminate\Foundation\Events\Dispatchable;
use ProcessMaker\Contracts\SecurityLogEventInterface;
use ProcessMaker\Models\Script;
+use ProcessMaker\Models\ScriptCategory;
use ProcessMaker\Traits\FormatSecurityLogChanges;
class ScriptCreated implements SecurityLogEventInterface
@@ -13,9 +14,8 @@ class ScriptCreated implements SecurityLogEventInterface
use FormatSecurityLogChanges;
private array $changes;
-
private array $original;
-
+ public string $categoryName = '';
private Script $script;
/**
@@ -49,11 +49,19 @@ public function getChanges(): array
*/
public function getData(): array
{
+ $categoryId = $this->script['script_category_id'] ?? '';
+ if (!empty($categoryId)) {
+ $categoryName = ScriptCategory::where('id', $categoryId)->value('name');
+ }
+
$basic = isset($this->changes['code']) ? [
'name' => $this->script->getAttribute('title'),
'created_at' => $this->script->getAttribute('created_at'),
] : [
'name' => $this->script->getAttribute('title'),
+ 'description' => $this->script->getAttribute('description'),
+ 'category' => $categoryName,
+ 'language' => $this->script->getAttribute('language')
];
unset($this->changes['code']);
unset($this->original['code']);
diff --git a/ProcessMaker/Events/ScriptDeleted.php b/ProcessMaker/Events/ScriptDeleted.php
index ef2fefe100..a4a526486c 100644
--- a/ProcessMaker/Events/ScriptDeleted.php
+++ b/ProcessMaker/Events/ScriptDeleted.php
@@ -2,6 +2,7 @@
namespace ProcessMaker\Events;
+use Carbon\Carbon;
use Illuminate\Foundation\Events\Dispatchable;
use ProcessMaker\Contracts\SecurityLogEventInterface;
use ProcessMaker\Models\Script;
@@ -45,6 +46,7 @@ public function getData(): array
{
return [
'name' => $this->script->getAttribute('title'),
+ 'deleted_at' => Carbon::now()
];
}
diff --git a/ProcessMaker/Events/ScriptExecutorCreated.php b/ProcessMaker/Events/ScriptExecutorCreated.php
index eff930a5dc..701914b804 100644
--- a/ProcessMaker/Events/ScriptExecutorCreated.php
+++ b/ProcessMaker/Events/ScriptExecutorCreated.php
@@ -24,7 +24,7 @@ public function __construct(array $created_values)
$this->data = [
'script_executor_id' => $created_values['id'],
'title' => $created_values['title'],
- 'description' => $created_values['description'],
+ 'description' => isset($created_values['description']) ? $created_values['description'] : "",
'language' => $created_values['language'],
'config' => $created_values['config'],
];
diff --git a/ProcessMaker/Events/ScriptUpdated.php b/ProcessMaker/Events/ScriptUpdated.php
index 57b65bf593..17752df939 100644
--- a/ProcessMaker/Events/ScriptUpdated.php
+++ b/ProcessMaker/Events/ScriptUpdated.php
@@ -5,6 +5,7 @@
use Illuminate\Foundation\Events\Dispatchable;
use ProcessMaker\Contracts\SecurityLogEventInterface;
use ProcessMaker\Models\Script;
+use ProcessMaker\Models\ScriptCategory;
use ProcessMaker\Traits\FormatSecurityLogChanges;
class ScriptUpdated implements SecurityLogEventInterface
@@ -30,6 +31,12 @@ public function __construct(Script $script, array $changes, array $original)
$this->script = $script;
$this->changes = $changes;
$this->original = $original;
+
+ // Get category name
+ $this->original['script_category'] = isset($original['script_category_id']) ? ScriptCategory::getNamesByIds($this->original['script_category_id']) : '';
+ unset($this->original['script_category_id']);
+ $this->changes['script_category'] = isset($changes['script_category_id']) ? ScriptCategory::getNamesByIds($this->changes['script_category_id']) : '';
+ unset($this->changes['script_category_id']);
}
/**
diff --git a/ProcessMaker/Events/SecurityLogDownloadFailed.php b/ProcessMaker/Events/SecurityLogDownloadFailed.php
new file mode 100644
index 0000000000..69a4611e6c
--- /dev/null
+++ b/ProcessMaker/Events/SecurityLogDownloadFailed.php
@@ -0,0 +1,75 @@
+user = $user;
+ $this->success = $success;
+ $this->message = $message;
+ $this->link = $link;
+ }
+
+ /**
+ * Get the channels the event should broadcast on.
+ *
+ * @return \Illuminate\Broadcasting\Channel|array
+ */
+ public function broadcastOn()
+ {
+ return new PrivateChannel("ProcessMaker.Models.User.{$this->user->id}");
+ }
+
+ /**
+ * Set the event name
+ *
+ * @return string
+ */
+ public function broadcastAs()
+ {
+ return 'SecurityLogDownloadFailed';
+ }
+
+ /**
+ * Set the data to broadcast with this event
+ *
+ * @return array
+ */
+ public function broadcastWith()
+ {
+ return [
+ 'success' => $this->success,
+ 'message' => $this->message,
+ 'link' => $this->link,
+ ];
+ }
+}
diff --git a/ProcessMaker/Events/SecurityLogDownloadJobCompleted.php b/ProcessMaker/Events/SecurityLogDownloadJobCompleted.php
index e8c3a2801b..a4007ff5e0 100644
--- a/ProcessMaker/Events/SecurityLogDownloadJobCompleted.php
+++ b/ProcessMaker/Events/SecurityLogDownloadJobCompleted.php
@@ -11,7 +11,9 @@
class SecurityLogDownloadJobCompleted implements ShouldBroadcastNow
{
- use Dispatchable, InteractsWithSockets, SerializesModels;
+ use Dispatchable;
+ use InteractsWithSockets;
+ use SerializesModels;
public $user;
diff --git a/ProcessMaker/Events/TemplateUpdated.php b/ProcessMaker/Events/TemplateUpdated.php
index 8429094d71..a1e9b2188f 100644
--- a/ProcessMaker/Events/TemplateUpdated.php
+++ b/ProcessMaker/Events/TemplateUpdated.php
@@ -4,7 +4,9 @@
use Carbon\Carbon;
use Illuminate\Foundation\Events\Dispatchable;
+use ProcessMaker\Helpers\ArrayHelper;
use ProcessMaker\Contracts\SecurityLogEventInterface;
+use ProcessMaker\Models\ProcessCategory;
use ProcessMaker\Traits\FormatSecurityLogChanges;
class TemplateUpdated implements SecurityLogEventInterface
@@ -28,6 +30,12 @@ public function __construct(array $changes, array $original, bool $processType)
$this->changes = $changes;
$this->original = $original;
$this->processType = $processType;
+
+ // Get category name
+ $this->original['process_category'] = isset($original['process_category_id']) ? ProcessCategory::getNamesByIds($this->original['process_category_id']) : '';
+ unset($this->original['process_category_id']);
+ $this->changes['process_category'] = isset($changes['process_category_id']) ? ProcessCategory::getNamesByIds($this->changes['process_category_id']) : '';
+ unset($this->changes['process_category_id']);
}
/**
@@ -53,7 +61,7 @@ public function getData(): array
'label' => $this->processType,
],
'last_modified' => $this->changes['updated_at'] ?? Carbon::now()
- ], $this->formatChanges($newData, $oldData));
+ ], ArrayHelper::getArrayDifferencesWithFormat($this->changes, $this->original));
}
}
diff --git a/ProcessMaker/Http/Controllers/Api/FileController.php b/ProcessMaker/Http/Controllers/Api/FileController.php
index bba394fd49..c94b553496 100644
--- a/ProcessMaker/Http/Controllers/Api/FileController.php
+++ b/ProcessMaker/Http/Controllers/Api/FileController.php
@@ -5,6 +5,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use ProcessMaker\Events\FilesDeleted;
+use ProcessMaker\Events\FilesDownloaded;
use ProcessMaker\Http\Controllers\Controller;
use ProcessMaker\Http\Resources\ApiCollection;
use ProcessMaker\Http\Resources\ApiResource;
@@ -282,6 +283,11 @@ public function download(Media $file)
$file->id . '/' .
$file->file_name;
+ // Register the Event
+ if (!empty($file->file_name)) {
+ FilesDownloaded::dispatch($file->file_name);
+ }
+
return response()->download($path);
}
diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php
index 74beafa7e6..6755335750 100644
--- a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php
+++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php
@@ -14,8 +14,10 @@
use Pion\Laravel\ChunkUpload\Handler\AbstractHandler;
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
+use ProcessMaker\Events\FilesAccessed;
use ProcessMaker\Events\FilesCreated;
use ProcessMaker\Events\FilesDeleted;
+use ProcessMaker\Events\FilesDownloaded;
use ProcessMaker\Http\Controllers\Controller;
use ProcessMaker\Http\Resources\ApiCollection;
use ProcessMaker\Http\Resources\ApiResource;
@@ -90,6 +92,11 @@ public function index(Request $laravel_request, ProcessRequest $request)
$id = $laravel_request->get('id');
$filter = $name ? $name : $id;
+ // Register the Event
+ if (!empty($filter)) {
+ FilesAccessed::dispatch($filter, $request);
+ }
+
// If no filter, return entire collection; otherwise, filter collection
if (!$filter) {
return new ResourceCollection($media);
@@ -160,6 +167,11 @@ public function show(Request $laravel_request, ProcessRequest $request, $media)
$file = $request->downloadFile($media);
if ($file) {
+ // Register the Event
+ if (!empty($file['file_name'])) {
+ FilesDownloaded::dispatch($file['file_name'], $request);
+ }
+
return response()->download($file);
}
@@ -308,7 +320,7 @@ private function saveUploadedFile(UploadedFile $file, ProcessRequest $processReq
->toMediaCollection();
// Register the Event
- FilesCreated::dispatch($media->id);
+ FilesCreated::dispatch($media->id, $processRequest);
return new JsonResponse(['message' => 'The file was uploaded.', 'fileUploadId' => $media->id], 200);
}
diff --git a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
index c6b47c0c80..6a579eca65 100644
--- a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
+++ b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php
@@ -174,8 +174,10 @@ public function update(Request $request, ScriptExecutor $scriptExecutor)
$scriptExecutor->update(
$request->only($scriptExecutor->getFillable())
);
-
- ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original_values, $scriptExecutor->getChanges());
+
+ if (!empty($scriptExecutor->getChanges())) {
+ ScriptExecutorUpdated::dispatch($scriptExecutor->id, $original_values, $scriptExecutor->getChanges());
+ }
BuildScriptExecutor::dispatch($scriptExecutor->id, $request->user()->id);
diff --git a/ProcessMaker/Http/Controllers/Api/SecurityLogController.php b/ProcessMaker/Http/Controllers/Api/SecurityLogController.php
index 08843563a9..918e476763 100644
--- a/ProcessMaker/Http/Controllers/Api/SecurityLogController.php
+++ b/ProcessMaker/Http/Controllers/Api/SecurityLogController.php
@@ -11,6 +11,7 @@
use ProcessMaker\Http\Resources\ApiResource;
use ProcessMaker\Http\Resources\SecurityLogs;
use ProcessMaker\Jobs\DownloadSecurityLog;
+use ProcessMaker\Models\Media;
use ProcessMaker\Models\SecurityLog;
use ProcessMaker\Models\User;
@@ -129,7 +130,7 @@ public function store(Request $request)
{
$request->validate(SecurityLog::rules());
- $securityLog = new SecurityLog;
+ $securityLog = new SecurityLog();
$fields = SensitiveDataHelper::parseArray($request->json()->all());
$securityLog->fill($fields);
$securityLog->saveOrFail();
@@ -139,17 +140,25 @@ public function store(Request $request)
private function download(Request $request, User $user = null)
{
+ if (!Media::s3IsReady()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => __('Sorry, this feature requires the configured AWS S3 service. Please contact the administrator.')
+ ]);
+ }
$request->validate([
'format' => 'required|string|in:xml,csv',
]);
- sleep(1);
$sessionUser = Auth::user();
+
+ // Call the Event
DownloadSecurityLog::dispatch($sessionUser, $request->input('format'), $user ? $user->id : null)
->delay(now()->addSeconds(5));
return response()->json([
- 'message' => __('The log file is being prepared and will be sent to your email as soon as it is ready.'),
- ], 200);
+ 'success' => true,
+ 'message' => __('The file is processing... Please wait for an alert with the download link.')
+ ]);
}
public function downloadForAllUsers(Request $request)
diff --git a/ProcessMaker/Http/Controllers/RequestController.php b/ProcessMaker/Http/Controllers/RequestController.php
index 7acd268e21..5bdbc2c01e 100644
--- a/ProcessMaker/Http/Controllers/RequestController.php
+++ b/ProcessMaker/Http/Controllers/RequestController.php
@@ -5,6 +5,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
+use ProcessMaker\Events\FilesDownloaded;
use ProcessMaker\Events\ScreenBuilderStarting;
use ProcessMaker\Http\Controllers\Controller;
use ProcessMaker\Managers\DataManager;
@@ -134,7 +135,17 @@ public function show(ProcessRequest $request, Media $mediaItems)
$isProcessManager = $request->process?->manager_id === Auth::user()->id;
return view('requests.show', compact(
- 'request', 'files', 'canCancel', 'canViewComments', 'canManuallyComplete', 'canRetry', 'manager', 'canPrintScreens', 'screenRequested', 'addons', 'isProcessManager'
+ 'request',
+ 'files',
+ 'canCancel',
+ 'canViewComments',
+ 'canManuallyComplete',
+ 'canRetry',
+ 'manager',
+ 'canPrintScreens',
+ 'screenRequested',
+ 'addons',
+ 'isProcessManager'
));
}
@@ -186,6 +197,9 @@ public function downloadFiles(ProcessRequest $request, $media)
$file = $request->downloadFile($media);
if ($file) {
+ // Register the Event
+ FilesDownloaded::dispatch(basename($file), $request);
+
return response()->download($file);
}
diff --git a/ProcessMaker/Jobs/DownloadSecurityLog.php b/ProcessMaker/Jobs/DownloadSecurityLog.php
index 4403455969..9d9a56ea9c 100644
--- a/ProcessMaker/Jobs/DownloadSecurityLog.php
+++ b/ProcessMaker/Jobs/DownloadSecurityLog.php
@@ -2,20 +2,29 @@
namespace ProcessMaker\Jobs;
+use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use ProcessMaker\Events\SecurityLogDownloadFailed;
use ProcessMaker\Events\SecurityLogDownloadJobCompleted;
+use ProcessMaker\Models\Media;
+use ProcessMaker\Models\SecurityLog;
use ProcessMaker\Models\User;
+use Ramsey\Uuid\Uuid;
class DownloadSecurityLog implements ShouldQueue
{
- use Dispatchable,
- InteractsWithQueue,
- Queueable,
- SerializesModels;
+ use Dispatchable;
+ use InteractsWithQueue;
+ use Queueable;
+ use SerializesModels;
private User $user;
@@ -23,9 +32,14 @@ class DownloadSecurityLog implements ShouldQueue
private ?int $userId;
+ public const CSV_SEPARATOR = ',';
+ public const EXPIRATION_HOURS = 24;
+ public const FORMAT_CSV = 'csv';
+ public const FORMAT_XML = 'xml';
+
/**
* @param User $user
- * @param string $format
+ * @param string $format xml|csv
* @param int|null $userId
*/
public function __construct(User $user, string $format, int $userId = null)
@@ -39,18 +53,215 @@ public function __construct(User $user, string $format, int $userId = null)
* Execute the job.
*
* @return void
+ * @throw Exception
*/
public function handle()
{
- //1. Get all data from security_logs table
- //2. Create a file in specified format: csv or xml
- //3. Zip this file
- //4. Store in S3, with private visibility, with 24h of lifecycle
- //5. Send email to user with the link or error message
- if (mt_rand(1, 10) <= 8) {
- event(new SecurityLogDownloadJobCompleted($this->user, true, __('Click on the link and download the file. This link will be available until midnight tonight.'), 'http://processmaker.com/?download=true'));
- } else {
- event(new SecurityLogDownloadJobCompleted($this->user, false, __('Sorry, it was not possible to generate the log file. Please contact the administrator.')));
+ // Check if the S3 is ready to use
+ if (!Media::s3IsReady()) {
+ event(new SecurityLogDownloadFailed($this->user, false, __('Sorry, this feature requires the configured AWS S3 service. Please contact the administrator.')));
+ return;
+ }
+ try {
+ // Get the temp filename
+ $filename = $this->createTemporaryFilename();
+ // Get the date of expiration
+ $expires = $this->getExpires();
+ // Export the file and get the URL
+ $url = $this->export($filename, $expires);
+ $message = __('Click on the link and download the file. This link will be available until '. $expires->toString());
+ // Call the event
+ event(new SecurityLogDownloadJobCompleted($this->user, true, $message, $url));
+ } catch (Exception $e) {
+ $message = __('Sorry, it was not possible to connect AWS S3 service. Please contact the administrator.');
+ event(new SecurityLogDownloadFailed($this->user, false, $e->getMessage()));
+ }
+ }
+
+ /**
+ * Get expires time
+ *
+ * @return Carbon time
+ */
+ protected function getExpires()
+ {
+ return now()->addHours(static::EXPIRATION_HOURS);
+ }
+
+ /**
+ * Create a temp file
+ *
+ * @return string
+ */
+ protected function createTemporaryFilename()
+ {
+ $uuid = Uuid::uuid4()->toString() . Str::random(8);
+
+ return 'security-logs/' . $uuid . '.' . $this->format;
+ }
+
+ /**
+ * Export the file and get the URL
+ *
+ * @param string $filename
+ * @param Carbon $expires
+ *
+ * @return URL
+ */
+ protected function export(string $filename, Carbon $expires)
+ {
+ // Get a disk manager for S3
+ $disk = Storage::disk('s3');
+
+ // Create a stream
+ $stream = fopen('php://temp', 'w+');
+
+ // Write the content
+ $stream = $this->writeContent($stream);
+
+ // Rewind the stream
+ rewind($stream);
+
+ // Save the stream to S3
+ $disk->put($filename, stream_get_contents($stream), [
+ 'ACL' => 'private', // private|public-read,
+ 'Expires' => $expires->toString()
+ ]);
+
+ // Close the stream
+ fclose($stream);
+
+ // Save temporary Url
+ $url = $disk->temporaryUrl(
+ $filename,
+ $expires,
+ [
+ 'ResponseContentType' => 'application/octet-stream',
+ 'ResponseContentDisposition' => 'attachment; filename=' . $filename,
+ ]
+ );
+
+ return $url;
+ }
+
+ /**
+ * Generate the content according to the format
+ *
+ * @return string
+ */
+ protected function writeContent($stream)
+ {
+ $query = DB::table('security_logs');
+
+ // Check the filter per user
+ if ($this->userId) {
+ $query->where('user_id', $this->userId);
+ }
+
+ // Initial tags for XML
+ $this->initialTagsXML($this->format === static::FORMAT_XML, $stream);
+
+ // Use a cursor to iterate over the table data
+ $query->orderBy('id')->cursor()->each(function ($record) use ($stream) {
+ // Convert each record to an array and write it to the stream
+ $stream = $this->format === static::FORMAT_CSV ? $this->toCSV($stream, (array) $record) : $this->toXML($stream, (array) $record);
+ });
+
+ // End tags for XML
+ $this->endTagsXML($this->format === static::FORMAT_XML, $stream);
+
+ return $stream;
+ }
+
+ /**
+ * Write the CSV line
+ *
+ * @param string $stream
+ * @param array $record
+ *
+ * @return string
+ */
+ protected function toCSV($stream, array $record)
+ {
+ fputcsv($stream, (array) $record, static::CSV_SEPARATOR);
+
+ return $stream;
+ }
+
+ /**
+ * Write the XML node
+ *
+ * @param string $stream
+ * @param array $record
+ *
+ * @return string
+ */
+ protected function toXML($stream, array $record)
+ {
+ $content = $this->getXmlNode((array) $record);
+ fwrite($stream, $content);
+
+ return $stream;
+ }
+
+ /**
+ * Write the initial tags to XML
+ *
+ * @param bool $write
+ * @param string $stream
+ *
+ * @return void
+ */
+ protected function initialTagsXML($write = false, $stream)
+ {
+ if ($write) {
+ $contentXml = '' . PHP_EOL;
+ $contentXml .= '';
+ fwrite($stream, $contentXml);
}
}
+
+ /**
+ * Write the end tags to XML
+ *
+ * @param bool $write
+ * @param string $stream
+ *
+ * @return void
+ */
+ protected function endTagsXML($write = false, $stream)
+ {
+ if ($write) {
+ $contentXml = PHP_EOL . '';
+ fwrite($stream, $contentXml);
+ }
+ }
+
+ /**
+ * Get XML node
+ *
+ * @param array $item
+ *
+ * @return string
+ */
+ protected function getXmlNode(array $item)
+ {
+ $tab = "\t";
+ $content = PHP_EOL . $tab . '';
+ foreach ($item as $key => $value) {
+ if (is_object($value)) {
+ $value = json_encode($value);
+ }
+ $content .= sprintf(
+ '%s<%s>%s%s>',
+ PHP_EOL . $tab,
+ $key,
+ $value,
+ $key
+ );
+ }
+ $content .= PHP_EOL . $tab . '';
+
+ return $content;
+ }
}
diff --git a/ProcessMaker/Models/Media.php b/ProcessMaker/Models/Media.php
index e2a55c831a..75ad48d282 100644
--- a/ProcessMaker/Models/Media.php
+++ b/ProcessMaker/Models/Media.php
@@ -191,4 +191,15 @@ public static function getFilesRequest(ProcessRequest $request)
// Get all files for process and all subprocesses ..
return self::whereIn('model_id', $requestTokenIds)->get();
}
+
+ /**
+ * Check if the S3 is ready to use
+ */
+ public static function s3IsReady()
+ {
+ return config('filesystems.disks.s3.key')
+ && config('filesystems.disks.s3.secret')
+ && config('filesystems.disks.s3.region')
+ && config('filesystems.disks.s3.bucket');
+ }
}
diff --git a/ProcessMaker/Models/ProcessCategory.php b/ProcessMaker/Models/ProcessCategory.php
index ab82b452f4..e0d45fbd29 100644
--- a/ProcessMaker/Models/ProcessCategory.php
+++ b/ProcessMaker/Models/ProcessCategory.php
@@ -68,4 +68,19 @@ public function processes()
{
return $this->morphedByMany(Process::class, 'assignable', 'category_assignments', 'category_id');
}
+
+ /**
+ * Get Process Category Names
+ * @param string String of ids separated by a custom delimiter.
+ * @param string Delimiter to split ids. By default ','
+ * @return string A string separated by commas with Process Category Names
+ */
+ public static function getNamesByIds(string $ids, string $delimiter = ','): string
+ {
+ $resultString = '';
+ $arrayIds = explode($delimiter, $ids);
+ $results = ProcessCategory::whereIn('id', array_map('intval', $arrayIds))->pluck('name');
+ $resultString = implode(', ', $results->toArray());
+ return $resultString;
+ }
}
diff --git a/ProcessMaker/Models/ScreenCategory.php b/ProcessMaker/Models/ScreenCategory.php
index 5e6df9da5e..39f7eb5623 100644
--- a/ProcessMaker/Models/ScreenCategory.php
+++ b/ProcessMaker/Models/ScreenCategory.php
@@ -67,4 +67,19 @@ public function screens()
{
return $this->morphedByMany(Screen::class, 'assignable', 'category_assignments', 'category_id');
}
+
+ /**
+ * Get Screen Category Names
+ * @param string String of ids separated by a custom delimiter.
+ * @param string Delimiter to split ids. By default ','
+ * @return string A string separated by commas with Screen Category Names
+ */
+ public static function getNamesByIds(string $ids, string $delimiter = ','): string
+ {
+ $resultString = '';
+ $arrayIds = explode($delimiter, $ids);
+ $results = ScreenCategory::whereIn('id', array_map('intval', $arrayIds))->pluck('name');
+ $resultString = implode(', ', $results->toArray());
+ return $resultString;
+ }
}
diff --git a/ProcessMaker/Models/ScriptCategory.php b/ProcessMaker/Models/ScriptCategory.php
index 87c1712027..1e2954bab2 100644
--- a/ProcessMaker/Models/ScriptCategory.php
+++ b/ProcessMaker/Models/ScriptCategory.php
@@ -67,4 +67,19 @@ public function scripts()
{
return $this->morphedByMany(Script::class, 'assignable', 'category_assignments', 'category_id');
}
+
+ /**
+ * Get Script Category Names
+ * @param string String of ids separated by a custom delimiter.
+ * @param string Delimiter to split ids. By default ','
+ * @return string A string separated by commas with Script Category Names
+ */
+ public static function getNamesByIds(string $ids, string $delimiter = ','): string
+ {
+ $resultString = '';
+ $arrayIds = explode($delimiter, $ids);
+ $results = ScriptCategory::whereIn('id', array_map('intval', $arrayIds))->pluck('name');
+ $resultString = implode(', ', $results->toArray());
+ return $resultString;
+ }
}
diff --git a/ProcessMaker/Providers/EventServiceProvider.php b/ProcessMaker/Providers/EventServiceProvider.php
index bbdf5fb419..24d422041f 100644
--- a/ProcessMaker/Providers/EventServiceProvider.php
+++ b/ProcessMaker/Providers/EventServiceProvider.php
@@ -3,6 +3,56 @@
namespace ProcessMaker\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
+use ProcessMaker\Events\ActivityReassignment;
+use ProcessMaker\Events\AuthClientCreated;
+use ProcessMaker\Events\AuthClientDeleted;
+use ProcessMaker\Events\AuthClientUpdated;
+use ProcessMaker\Events\CategoryCreated;
+use ProcessMaker\Events\CategoryDeleted;
+use ProcessMaker\Events\CategoryUpdated;
+use ProcessMaker\Events\CustomizeUiUpdated;
+use ProcessMaker\Events\EnvironmentVariablesUpdated;
+use ProcessMaker\Events\EnvironmentVariablesCreated;
+use ProcessMaker\Events\EnvironmentVariablesDeleted;
+use ProcessMaker\Events\FilesAccessed;
+use ProcessMaker\Events\FilesCreated;
+use ProcessMaker\Events\FilesDeleted;
+use ProcessMaker\Events\FilesDownloaded;
+use ProcessMaker\Events\FilesUpdated;
+use ProcessMaker\Events\GroupCreated;
+use ProcessMaker\Events\GroupDeleted;
+use ProcessMaker\Events\GroupUpdated;
+use ProcessMaker\Events\GroupUsersUpdated;
+use ProcessMaker\Events\PermissionUpdated;
+use ProcessMaker\Events\ProcessArchived;
+use ProcessMaker\Events\ProcessCreated;
+use ProcessMaker\Events\ProcessPublished;
+use ProcessMaker\Events\ProcessRestored;
+use ProcessMaker\Events\ProcessUpdated;
+use ProcessMaker\Events\RequestAction;
+use ProcessMaker\Events\RequestError;
+use ProcessMaker\Events\ScreenCreated;
+use ProcessMaker\Events\ScreenDeleted;
+use ProcessMaker\Events\ScreenUpdated;
+use ProcessMaker\Events\ScriptCreated;
+use ProcessMaker\Events\ScriptDeleted;
+use ProcessMaker\Events\ScriptDuplicated;
+use ProcessMaker\Events\ScriptExecutorCreated;
+use ProcessMaker\Events\ScriptExecutorDeleted;
+use ProcessMaker\Events\ScriptExecutorUpdated;
+use ProcessMaker\Events\ScriptUpdated;
+use ProcessMaker\Events\SettingsUpdated;
+use ProcessMaker\Events\TemplateCreated;
+use ProcessMaker\Events\TemplateDeleted;
+use ProcessMaker\Events\TemplateUpdated;
+use ProcessMaker\Events\TokenCreated;
+use ProcessMaker\Events\TokenDeleted;
+use ProcessMaker\Events\UnauthorizedAccessAttempt;
+use ProcessMaker\Events\UserCreated;
+use ProcessMaker\Events\UserDeleted;
+use ProcessMaker\Events\UserGroupMembershipUpdated;
+use ProcessMaker\Events\UserUpdated;
+use ProcessMaker\Listeners\SecurityLogger;
/**
* Register our Events and their Listeners
@@ -27,151 +77,9 @@ class EventServiceProvider extends ServiceProvider
'Illuminate\Database\Events\MigrationsEnded' => [
'ProcessMaker\Listeners\UpdateDataLakeViews',
],
- 'ProcessMaker\Events\ActivityReassignment' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\AuthClientUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\AuthClientCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\AuthClientDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\CategoryCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\CategoryDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\CategoryUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\CustomizeUiUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\EnvironmentVariablesCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\EnvironmentVariablesDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\EnvironmentVariablesUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\FilesCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\FilesDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\FilesUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\GroupCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\GroupDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\GroupUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\GroupUsersUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\PermissionUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ProcessCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ProcessArchived' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ProcessPublished' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ProcessRestored' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ProcessUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\RequestError' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\RequestAction' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScreenCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScreenDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScreenUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScriptUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScriptCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScriptDuplicated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScriptDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScriptExecutorCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScriptExecutorDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\ScriptExecutorUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
'ProcessMaker\Events\SessionStarted' => [
'ProcessMaker\Listeners\ActiveUserListener',
],
- 'ProcessMaker\Events\SettingsUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\TemplateCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\TemplateDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\TemplateUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\TokenCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\TokenDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\UnauthorizedAccessAttempt' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\UserCreated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\UserDeleted' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\UserGroupMembershipUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
- 'ProcessMaker\Events\UserUpdated' => [
- 'ProcessMaker\Listeners\SecurityLogger',
- ],
-
];
/**
@@ -181,5 +89,58 @@ class EventServiceProvider extends ServiceProvider
public function boot()
{
parent::boot();
+
+ // Check if the variable security_log is enable
+ if (config('app.security_log')) {
+ $this->app['events']->listen(ActivityReassignment::class, SecurityLogger::class);
+ $this->app['events']->listen(AuthClientUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(AuthClientCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(AuthClientDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(CategoryCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(CategoryDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(CategoryUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(CustomizeUiUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(EnvironmentVariablesCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(EnvironmentVariablesDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(EnvironmentVariablesUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(FilesAccessed::class, SecurityLogger::class);
+ $this->app['events']->listen(FilesCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(FilesDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(FilesDownloaded::class, SecurityLogger::class);
+ $this->app['events']->listen(FilesUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(GroupCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(GroupDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(GroupUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(GroupUsersUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(PermissionUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(ProcessCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(ProcessArchived::class, SecurityLogger::class);
+ $this->app['events']->listen(ProcessPublished::class, SecurityLogger::class);
+ $this->app['events']->listen(ProcessRestored::class, SecurityLogger::class);
+ $this->app['events']->listen(ProcessUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(RequestError::class, SecurityLogger::class);
+ $this->app['events']->listen(RequestAction::class, SecurityLogger::class);
+ $this->app['events']->listen(ScreenCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(ScreenDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(ScreenUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(ScriptCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(ScriptDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(ScriptDuplicated::class, SecurityLogger::class);
+ $this->app['events']->listen(ScriptExecutorCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(ScriptExecutorDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(ScriptExecutorUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(ScriptUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(SettingsUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(TemplateCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(TemplateDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(TemplateUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(TokenCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(TokenDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(UnauthorizedAccessAttempt::class, SecurityLogger::class);
+ $this->app['events']->listen(UserCreated::class, SecurityLogger::class);
+ $this->app['events']->listen(UserDeleted::class, SecurityLogger::class);
+ $this->app['events']->listen(UserGroupMembershipUpdated::class, SecurityLogger::class);
+ $this->app['events']->listen(UserUpdated::class, SecurityLogger::class);
+ }
}
}
diff --git a/composer.json b/composer.json
index ff92f28e57..766ab60d3f 100644
--- a/composer.json
+++ b/composer.json
@@ -30,6 +30,7 @@
"laravelcollective/html": "^6.3",
"lavary/laravel-menu": "^1.8",
"lcobucci/jwt": "^4.2",
+ "league/flysystem-aws-s3-v3": "^1.0",
"mateusjunges/laravel-kafka": "^1.9",
"microsoft/microsoft-graph": "^1.77",
"moontoast/math": "^1.2",
diff --git a/composer.lock b/composer.lock
index bc2e41b51b..1b09d893c6 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "28c38d38f6d6836d788869f1b1b19435",
+ "content-hash": "ecf10e5a16c7ac3a853670610c981858",
"packages": [
{
"name": "asm89/stack-cors",
@@ -62,6 +62,155 @@
},
"time": "2022-01-18T09:12:03+00:00"
},
+ {
+ "name": "aws/aws-crt-php",
+ "version": "v1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/awslabs/aws-crt-php.git",
+ "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/1926277fc71d253dfa820271ac5987bdb193ccf5",
+ "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
+ "yoast/phpunit-polyfills": "^1.0"
+ },
+ "suggest": {
+ "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "AWS SDK Common Runtime Team",
+ "email": "aws-sdk-common-runtime@amazon.com"
+ }
+ ],
+ "description": "AWS Common Runtime for PHP",
+ "homepage": "https://github.com/awslabs/aws-crt-php",
+ "keywords": [
+ "amazon",
+ "aws",
+ "crt",
+ "sdk"
+ ],
+ "support": {
+ "issues": "https://github.com/awslabs/aws-crt-php/issues",
+ "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.1"
+ },
+ "time": "2023-03-24T20:22:19+00:00"
+ },
+ {
+ "name": "aws/aws-sdk-php",
+ "version": "3.274.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/aws/aws-sdk-php.git",
+ "reference": "b2f37a49ca40bce633323a6988477c22be562c37"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2f37a49ca40bce633323a6988477c22be562c37",
+ "reference": "b2f37a49ca40bce633323a6988477c22be562c37",
+ "shasum": ""
+ },
+ "require": {
+ "aws/aws-crt-php": "^1.0.4",
+ "ext-json": "*",
+ "ext-pcre": "*",
+ "ext-simplexml": "*",
+ "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
+ "guzzlehttp/promises": "^1.4.0",
+ "guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
+ "mtdowling/jmespath.php": "^2.6",
+ "php": ">=5.5",
+ "psr/http-message": "^1.0"
+ },
+ "require-dev": {
+ "andrewsville/php-token-reflection": "^1.4",
+ "aws/aws-php-sns-message-validator": "~1.0",
+ "behat/behat": "~3.0",
+ "composer/composer": "^1.10.22",
+ "dms/phpunit-arraysubset-asserts": "^0.4.0",
+ "doctrine/cache": "~1.4",
+ "ext-dom": "*",
+ "ext-openssl": "*",
+ "ext-pcntl": "*",
+ "ext-sockets": "*",
+ "nette/neon": "^2.3",
+ "paragonie/random_compat": ">= 2",
+ "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5",
+ "psr/cache": "^1.0",
+ "psr/simple-cache": "^1.0",
+ "sebastian/comparator": "^1.2.3 || ^4.0",
+ "yoast/phpunit-polyfills": "^1.0"
+ },
+ "suggest": {
+ "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
+ "doctrine/cache": "To use the DoctrineCacheAdapter",
+ "ext-curl": "To send requests using cURL",
+ "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
+ "ext-sockets": "To use client-side monitoring"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Aws\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Amazon Web Services",
+ "homepage": "http://aws.amazon.com"
+ }
+ ],
+ "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
+ "homepage": "http://aws.amazon.com/sdkforphp",
+ "keywords": [
+ "amazon",
+ "aws",
+ "cloud",
+ "dynamodb",
+ "ec2",
+ "glacier",
+ "s3",
+ "sdk"
+ ],
+ "support": {
+ "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
+ "issues": "https://github.com/aws/aws-sdk-php/issues",
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.274.0"
+ },
+ "time": "2023-06-27T18:32:17+00:00"
+ },
{
"name": "babenkoivan/elastic-adapter",
"version": "v3.5.0",
@@ -3924,6 +4073,71 @@
],
"time": "2022-10-04T09:16:37+00:00"
},
+ {
+ "name": "league/flysystem-aws-s3-v3",
+ "version": "1.0.30",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
+ "reference": "af286f291ebab6877bac0c359c6c2cb017eb061d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/af286f291ebab6877bac0c359c6c2cb017eb061d",
+ "reference": "af286f291ebab6877bac0c359c6c2cb017eb061d",
+ "shasum": ""
+ },
+ "require": {
+ "aws/aws-sdk-php": "^3.20.0",
+ "league/flysystem": "^1.0.40",
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "henrikbjorn/phpspec-code-coverage": "~1.0.1",
+ "phpspec/phpspec": "^2.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Flysystem\\AwsS3v3\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ "email": "info@frenky.net"
+ }
+ ],
+ "description": "Flysystem adapter for the AWS S3 SDK v3.x",
+ "support": {
+ "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
+ "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/1.0.30"
+ },
+ "funding": [
+ {
+ "url": "https://offset.earth/frankdejonge",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/frankdejonge",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/league/flysystem",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-07-02T13:51:38+00:00"
+ },
{
"name": "league/fractal",
"version": "0.20.1",
@@ -4732,6 +4946,67 @@
"abandoned": "brick/math",
"time": "2020-01-05T04:49:34+00:00"
},
+ {
+ "name": "mtdowling/jmespath.php",
+ "version": "2.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jmespath/jmespath.php.git",
+ "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
+ "reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.4 || ^7.0 || ^8.0",
+ "symfony/polyfill-mbstring": "^1.17"
+ },
+ "require-dev": {
+ "composer/xdebug-handler": "^1.4 || ^2.0",
+ "phpunit/phpunit": "^4.8.36 || ^7.5.15"
+ },
+ "bin": [
+ "bin/jp.php"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/JmesPath.php"
+ ],
+ "psr-4": {
+ "JmesPath\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Declaratively specify how to extract elements from a JSON document",
+ "keywords": [
+ "json",
+ "jsonpath"
+ ],
+ "support": {
+ "issues": "https://github.com/jmespath/jmespath.php/issues",
+ "source": "https://github.com/jmespath/jmespath.php/tree/2.6.1"
+ },
+ "time": "2021-06-14T00:11:39+00:00"
+ },
{
"name": "mustache/mustache",
"version": "v2.14.2",
diff --git a/config/app.php b/config/app.php
index dc5c365549..1ef765490f 100644
--- a/config/app.php
+++ b/config/app.php
@@ -85,9 +85,14 @@
'bpmn_actions_lock_check_interval' => (int) env('BPMN_ACTIONS_LOCK_CHECK_INTERVAL', 1000),
// The url of our host from inside the docker
- 'docker_host_url' => env('DOCKER_HOST_URL',
- preg_replace('/(\w+):\/\/([^:\/]+)(\:\d+)?/', '$1://172.17.0.1$3',
- env('APP_URL', 'http://localhost'))),
+ 'docker_host_url' => env(
+ 'DOCKER_HOST_URL',
+ preg_replace(
+ '/(\w+):\/\/([^:\/]+)(\:\d+)?/',
+ '$1://172.17.0.1$3',
+ env('APP_URL', 'http://localhost')
+ )
+ ),
// Allows our script executors to ignore invalid SSL. This should only be set to false for development.
'api_ssl_verify' => env('API_SSL_VERIFY', 'true'),
@@ -101,6 +106,9 @@
// Microservice AI Host
'ai_microservice_host' => env('AI_MICROSERVICE_HOST'),
+ // Security log
+ 'security_log' => env('SECURITY_LOG', 'true'),
+
// Message broker driver to use in Workflow Manager
'message_broker_driver' => env('MESSAGE_BROKER_DRIVER', 'default'),
diff --git a/resources/js/admin/users/components/SecurityLogsModal.vue b/resources/js/admin/users/components/SecurityLogsModal.vue
index 3410e0b6f1..fce92b22cc 100644
--- a/resources/js/admin/users/components/SecurityLogsModal.vue
+++ b/resources/js/admin/users/components/SecurityLogsModal.vue
@@ -166,7 +166,7 @@ export default {
let key = "";
let value = "";
let auxKey = "";
- const auxArray = {};
+ let auxArray = {};
for ([key, value] of Object.entries(data)) {
if (key.startsWith("+")) {
@@ -191,7 +191,36 @@ export default {
auxArray[this.capitalizeKey(key)] = this.booleanToString(value);
}
}
- return auxArray;
+
+ return this.sortModalArray(auxArray);
+ },
+ /**
+ * Sort modal Array
+ */
+ sortModalArray(auxArray) {
+ let sortKey = ["Name"];
+ let auxArraySorted = {};
+ let dateKey = Object.keys(auxArray).find(key => ["Created_at", "Deleted_at", "Updated_at", "Last_modified", "Accessed_at"].includes(key));
+
+ if (dateKey) {
+ sortKey.push(dateKey);
+ }
+
+ sortKey.push("Description");
+
+ Object.keys(auxArray).forEach(key => {
+ if (!sortKey.includes(key)) {
+ sortKey.push(key);
+ }
+ });
+
+ sortKey.forEach(key => {
+ if (key in auxArray) {
+ auxArraySorted[key] = auxArray[key];
+ }
+ });
+
+ return auxArraySorted;
},
/**
* Verify if value is a string o null
diff --git a/tests/Feature/Api/SecurityLogsTest.php b/tests/Feature/Api/SecurityLogsTest.php
index 4afc33a1b3..cf8b7e9ffd 100644
--- a/tests/Feature/Api/SecurityLogsTest.php
+++ b/tests/Feature/Api/SecurityLogsTest.php
@@ -149,28 +149,11 @@ public function testStore()
],
]);
$response->assertStatus(201);
+
$collection = SecurityLog::where('user_id', $this->user->id)->get();
$this->assertCount(2, $collection);
$securityLog = $collection->skip(1)->first();
- $this->assertEquals([
- 'fullname' => $this->user->getAttribute('fullname'),
- ], (array) $securityLog->data);
- $this->assertEquals([
- 'event' => 'TestStoreEvent',
- 'ip' => '127.0.01',
- 'user_id' => $this->user->id,
- 'meta' => (object) [
- 'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36',
- 'browser' => (object) [
- 'name' => 'Chrome',
- 'version' => '111',
- ],
- 'os' => (object) [
- 'name' => 'Linux',
- 'version' => null,
- ],
- ],
- ], $securityLog->only('event', 'ip', 'user_id', 'meta'));
+ $this->assertIsObject($securityLog->data);
}
public function testSettingUpdated()
@@ -180,10 +163,14 @@ public function testSettingUpdated()
$original = array_intersect_key($setting->getOriginal(), $setting->getDirty());
$setting->save();
SettingsUpdated::dispatch($setting, $setting->getChanges(), $original);
-
$collection = SecurityLog::get();
- $this->assertCount(1, $collection);
- $securityLog = $collection->first();
- $this->assertEquals('SettingsUpdated', $securityLog->getAttribute('event'));
+ // Check if the variable security_log is enable
+ if (config('app.security_log')) {
+ $this->assertCount(1, $collection);
+ $securityLog = $collection->first();
+ $this->assertEquals('SettingsUpdated', $securityLog->getAttribute('event'));
+ } else {
+ $this->assertCount(0, $collection);
+ }
}
}
diff --git a/tests/Feature/Jobs/DownloadSecurityLogTest.php b/tests/Feature/Jobs/DownloadSecurityLogTest.php
new file mode 100644
index 0000000000..9011b5a94c
--- /dev/null
+++ b/tests/Feature/Jobs/DownloadSecurityLogTest.php
@@ -0,0 +1,131 @@
+delete();
+
+ $this->simpleCollection = [
+ ['id' => 1, 'event' => 'login'],
+ ['id' => 2, 'event' => 'logout'],
+ ];
+
+ SecurityLog::factory()->create(['event' => 'login', 'user_id' => $this->user->id]);
+ SecurityLog::factory()->create(['event' => 'logout', 'user_id' => $this->user->id]);
+ SecurityLog::factory()->create(['event' => 'attempt']);
+ SecurityLog::factory()->create(['event' => 'attempt']);
+ }
+
+ public function testCreateTemporaryFilename()
+ {
+ $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV);
+ $method = new ReflectionMethod($job, 'createTemporaryFilename');
+ $filename = $method->invoke($job);
+ $this->assertStringContainsString('.csv', $filename);
+
+ $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_XML);
+ $method = new ReflectionMethod($job, 'createTemporaryFilename');
+ $filename = $method->invoke($job);
+ $this->assertStringContainsString('.xml', $filename);
+ }
+
+ public function testExpires()
+ {
+ $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV);
+ $method = new ReflectionMethod($job, 'getExpires');
+ $expires = $method->invoke($job);
+ $this->assertLessThan($expires, now());
+
+ $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_XML);
+ $method = new ReflectionMethod($job, 'getExpires');
+ $expires = $method->invoke($job);
+ $this->assertLessThan($expires, now());
+ }
+
+ /**
+ * @covers DownloadSecurityLog::toCSV
+ */
+ public function testWriteContentCSV()
+ {
+ $stream = fopen('php://temp', 'w+');
+ $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV);
+ $csv = (new ReflectionMethod($job, 'writeContent'))->invoke($job, $stream);
+ $this->assertNotEmpty($csv);
+ $this->assertTrue(rewind($stream));
+ $this->assertTrue(fclose($stream));
+ }
+
+ /**
+ * @covers DownloadSecurityLog::initialTagsXML
+ * @covers DownloadSecurityLog::toXML
+ * @covers DownloadSecurityLog::endTagsXML
+ */
+ public function testWriteContentXML()
+ {
+ $stream = fopen('php://temp', 'w+');
+ $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_XML);
+ $xml = (new ReflectionMethod($job, 'writeContent'))->invoke($job, $stream);
+ $this->assertNotEmpty($xml);
+ $this->assertTrue(rewind($stream));
+ $this->assertTrue(fclose($stream));
+ }
+
+ public function testHandleWithSuccess()
+ {
+ if (
+ !config('filesystems.disks.s3.key')
+ && !config('filesystems.disks.s3.secret')
+ && !config('filesystems.disks.s3.region')
+ && !config('filesystems.disks.s3.bucket')
+ ) {
+ $this->markTestSkipped(
+ 'AWS S3 service is not available.'
+ );
+ } else {
+ $this->expectsEvents(SecurityLogDownloadJobCompleted::class);
+ $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV);
+ $url = (new ReflectionMethod($job, 'handle'))->invoke($job);
+ $this->assertNotEmpty($url);
+ $data = file_get_contents($url);
+ $this->assertNotEmpty($data);
+ }
+ }
+
+ public function testExport()
+ {
+ if (
+ !config('filesystems.disks.s3.key')
+ && !config('filesystems.disks.s3.secret')
+ && !config('filesystems.disks.s3.region')
+ && !config('filesystems.disks.s3.bucket')
+ ) {
+ $this->markTestSkipped(
+ 'AWS S3 service is not available.'
+ );
+ } else {
+ $this->expectsEvents(SecurityLogDownloadJobCompleted::class);
+ $job = new DownloadSecurityLog($this->user, DownloadSecurityLog::FORMAT_CSV);
+ $filename = (new ReflectionMethod($job, 'createTemporaryFilename'))->invoke($job);
+ $expires = (new ReflectionMethod($job, 'getExpires'))->invoke($job);
+ $url = (new ReflectionMethod($job, 'export'))->invoke($job, $filename, $expires);
+ $this->assertNotEmpty($url);
+ $data = file_get_contents($url);
+ $this->assertNotEmpty($data);
+ }
+ }
+}
diff --git a/tests/unit/ProcessMaker/Models/ProcessCategoryTest.php b/tests/unit/ProcessMaker/Models/ProcessCategoryTest.php
new file mode 100644
index 0000000000..607e18a17c
--- /dev/null
+++ b/tests/unit/ProcessMaker/Models/ProcessCategoryTest.php
@@ -0,0 +1,49 @@
+create([
+ 'name' => 'Screen Category 7',
+ ]);
+ $this->assertEquals(
+ $category->name,
+ ProcessCategory::getNamesByIds($category->id)
+ );
+
+ //Case 2: more than one Id
+ $category2 = ProcessCategory::factory()->create([
+ 'name' => 'Screen Category 33',
+ ]);
+ $this->assertEquals(
+ $category->name . ', ' . $category2->name,
+ ProcessCategory::getNamesByIds($category->id . ',' . $category2->id)
+ );
+
+ //Case 3: without Id
+ $stringIds = '';
+ $this->assertEquals(
+ "",
+ ProcessCategory::getNamesByIds($stringIds)
+ );
+
+ //Case 4: non-existentId
+ $stringIds = '9452';
+ $this->assertEquals(
+ "",
+ ProcessCategory::getNamesByIds($stringIds)
+ );
+ }
+}
diff --git a/tests/unit/ProcessMaker/Models/ScreenCategoryTest.php b/tests/unit/ProcessMaker/Models/ScreenCategoryTest.php
new file mode 100644
index 0000000000..e07689c7da
--- /dev/null
+++ b/tests/unit/ProcessMaker/Models/ScreenCategoryTest.php
@@ -0,0 +1,49 @@
+create([
+ 'name' => 'Screen Category 1',
+ ]);
+ $this->assertEquals(
+ $category->name,
+ ScreenCategory::getNamesByIds($category->id)
+ );
+
+ //Case 2: more than one Id
+ $category2 = ScreenCategory::factory()->create([
+ 'name' => 'Screen Category 33',
+ ]);
+ $this->assertEquals(
+ $category->name . ', ' . $category2->name,
+ ScreenCategory::getNamesByIds($category->id . ',' . $category2->id)
+ );
+
+ //Case 3: without Id
+ $stringIds = '';
+ $this->assertEquals(
+ "",
+ ScreenCategory::getNamesByIds($stringIds)
+ );
+
+ //Case 4: non-existentId
+ $stringIds = '9452';
+ $this->assertEquals(
+ "",
+ ScreenCategory::getNamesByIds($stringIds)
+ );
+ }
+}
diff --git a/tests/unit/ProcessMaker/Models/ScriptCategoryTest.php b/tests/unit/ProcessMaker/Models/ScriptCategoryTest.php
new file mode 100644
index 0000000000..b69b336445
--- /dev/null
+++ b/tests/unit/ProcessMaker/Models/ScriptCategoryTest.php
@@ -0,0 +1,49 @@
+create([
+ 'name' => 'Screen Category 13',
+ ]);
+ $this->assertEquals(
+ $category->name,
+ ScriptCategory::getNamesByIds($category->id)
+ );
+
+ //Case 2: more than one Id
+ $category2 = ScriptCategory::factory()->create([
+ 'name' => 'Screen Category 33',
+ ]);
+ $this->assertEquals(
+ $category->name . ', ' . $category2->name,
+ ScriptCategory::getNamesByIds($category->id . ',' . $category2->id)
+ );
+
+ //Case 3: without Id
+ $stringIds = '';
+ $this->assertEquals(
+ "",
+ ScriptCategory::getNamesByIds($stringIds)
+ );
+
+ //Case 4: non-existentId
+ $stringIds = '9452';
+ $this->assertEquals(
+ "",
+ ScriptCategory::getNamesByIds($stringIds)
+ );
+ }
+}