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/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/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/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/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);
+ }
+ }
+}