Skip to content

Commit 09832cc

Browse files
committed
Ensure we can properly create an activity log entry; always return soft-deleted models
1 parent f1c1699 commit 09832cc

File tree

5 files changed

+105
-28
lines changed

5 files changed

+105
-28
lines changed

app/Events/ActivityLogged.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Pterodactyl\Events;
4+
5+
use Illuminate\Support\Str;
6+
use Pterodactyl\Models\ActivityLog;
7+
use Illuminate\Database\Eloquent\Model;
8+
9+
class ActivityLogged extends Event
10+
{
11+
public ActivityLog $model;
12+
13+
public function __construct(ActivityLog $model)
14+
{
15+
$this->model = $model;
16+
}
17+
18+
public function is(string $event): bool
19+
{
20+
return $this->model->event === $event;
21+
}
22+
23+
public function actor(): ?Model
24+
{
25+
return $this->isSystem() ? null : $this->model->actor;
26+
}
27+
28+
public function isServerEvent()
29+
{
30+
return Str::startsWith($this->model->event, 'server:');
31+
}
32+
33+
public function isSystem()
34+
{
35+
return is_null($this->model->actor_id);
36+
}
37+
}

app/Models/ActivityLog.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Pterodactyl\Models;
44

5+
use Illuminate\Support\Facades\Event;
6+
use Pterodactyl\Events\ActivityLogged;
57
use Illuminate\Database\Eloquent\Builder;
68
use Illuminate\Database\Eloquent\Relations\MorphTo;
79
use Illuminate\Database\Eloquent\Model as IlluminateModel;
@@ -16,13 +18,14 @@
1618
* @property string|null $description
1719
* @property string|null $actor_type
1820
* @property int|null $actor_id
19-
* @property \Illuminate\Support\Collection $properties
21+
* @property \Illuminate\Support\Collection|null $properties
2022
* @property string $timestamp
2123
* @property IlluminateModel|\Eloquent $actor
22-
* @property IlluminateModel|\Eloquent $subject
24+
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\ActivityLogSubject[] $subjects
25+
* @property int|null $subjects_count
2326
*
24-
* @method static Builder|ActivityLog forEvent(string $event)
2527
* @method static Builder|ActivityLog forActor(\Illuminate\Database\Eloquent\Model $actor)
28+
* @method static Builder|ActivityLog forEvent(string $action)
2629
* @method static Builder|ActivityLog newModelQuery()
2730
* @method static Builder|ActivityLog newQuery()
2831
* @method static Builder|ActivityLog query()
@@ -50,6 +53,8 @@ class ActivityLog extends Model
5053
'properties' => 'collection',
5154
];
5255

56+
protected $with = ['subjects'];
57+
5358
public static $validationRules = [
5459
'event' => ['required', 'string'],
5560
'batch' => ['nullable', 'uuid'],
@@ -60,7 +65,12 @@ class ActivityLog extends Model
6065

6166
public function actor(): MorphTo
6267
{
63-
return $this->morphTo();
68+
return $this->morphTo()->withTrashed();
69+
}
70+
71+
public function subjects()
72+
{
73+
return $this->hasMany(ActivityLogSubject::class);
6474
}
6575

6676
public function scopeForEvent(Builder $builder, string $action): Builder
@@ -75,4 +85,17 @@ public function scopeForActor(Builder $builder, IlluminateModel $actor): Builder
7585
{
7686
return $builder->whereMorphedTo('actor', $actor);
7787
}
88+
89+
/**
90+
* Boots the model event listeners. This will trigger an activity log event every
91+
* time a new model is inserted which can then be captured and worked with as needed.
92+
*/
93+
protected static function boot()
94+
{
95+
parent::boot();
96+
97+
static::created(function (self $model) {
98+
Event::dispatch(new ActivityLogged($model));
99+
});
100+
}
78101
}

app/Models/ActivityLogSubject.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ public function activityLog()
3535

3636
public function subject()
3737
{
38-
return $this->morphTo();
38+
return $this->morphTo()->withTrashed();
3939
}
4040
}

app/Services/Activity/ActivityLogService.php

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Webmozart\Assert\Assert;
77
use Illuminate\Support\Collection;
88
use Pterodactyl\Models\ActivityLog;
9+
use Illuminate\Support\Facades\Log;
910
use Illuminate\Contracts\Auth\Factory;
1011
use Illuminate\Database\Eloquent\Model;
1112
use Illuminate\Support\Facades\Request;
@@ -132,7 +133,9 @@ public function withRequestMetadata(): self
132133

133134
/**
134135
* Logs an activity log entry with the set values and then returns the
135-
* model instance to the caller.
136+
* model instance to the caller. If there is an exception encountered while
137+
* performing this action it will be logged to the disk but will not interrupt
138+
* the code flow.
136139
*/
137140
public function log(string $description = null): ActivityLog
138141
{
@@ -142,7 +145,13 @@ public function log(string $description = null): ActivityLog
142145
$activity->description = $description;
143146
}
144147

145-
return $this->save();
148+
try {
149+
return $this->save();
150+
} catch (\Throwable|\Exception $exception) {
151+
Log::error($exception);
152+
}
153+
154+
return $activity;
146155
}
147156

148157
/**
@@ -166,9 +175,9 @@ public function clone(): self
166175
public function transaction(\Closure $callback)
167176
{
168177
return $this->connection->transaction(function () use ($callback) {
169-
$response = $callback($activity = $this->getActivity());
178+
$response = $callback($this);
170179

171-
$this->save($activity);
180+
$this->save();
172181

173182
return $response;
174183
});
@@ -209,14 +218,12 @@ protected function getActivity(): ActivityLog
209218
*
210219
* @throws \Throwable
211220
*/
212-
protected function save(ActivityLog $activity = null): ActivityLog
221+
protected function save(): ActivityLog
213222
{
214-
$activity = $activity ?? $this->activity;
223+
Assert::notNull($this->activity);
215224

216-
Assert::notNull($activity);
217-
218-
$response = $this->connection->transaction(function () use ($activity) {
219-
$activity->save();
225+
$response = $this->connection->transaction(function () {
226+
$this->activity->save();
220227

221228
$subjects = Collection::make($this->subjects)
222229
->map(fn (Model $subject) => [
@@ -229,7 +236,7 @@ protected function save(ActivityLog $activity = null): ActivityLog
229236

230237
ActivityLogSubject::insert($subjects);
231238

232-
return $activity;
239+
return $this->activity;
233240
});
234241

235242
$this->activity = null;

tests/Integration/Api/Client/Server/Backup/DeleteBackupTest.php

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
use Mockery;
66
use Illuminate\Http\Response;
77
use Pterodactyl\Models\Backup;
8-
use Pterodactyl\Models\AuditLog;
98
use Pterodactyl\Models\Permission;
9+
use Illuminate\Support\Facades\Event;
10+
use Pterodactyl\Events\ActivityLogged;
1011
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
1112
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
1213

@@ -34,32 +35,41 @@ public function testUserWithoutPermissionCannotDeleteBackup()
3435
/**
3536
* Tests that a backup can be deleted for a server and that it is properly updated
3637
* in the database. Once deleted there should also be a corresponding record in the
37-
* audit logs table for this API call.
38+
* activity logs table for this API call.
3839
*/
3940
public function testBackupCanBeDeleted()
4041
{
42+
Event::fake([ActivityLogged::class]);
43+
4144
[$user, $server] = $this->generateTestAccount([Permission::ACTION_BACKUP_DELETE]);
4245

4346
/** @var \Pterodactyl\Models\Backup $backup */
4447
$backup = Backup::factory()->create(['server_id' => $server->id]);
4548

46-
$this->repository->expects('setServer->delete')->with(Mockery::on(function ($value) use ($backup) {
47-
return $value instanceof Backup && $value->uuid === $backup->uuid;
48-
}))->andReturn(new Response());
49+
$this->repository->expects('setServer->delete')->with(
50+
Mockery::on(function ($value) use ($backup) {
51+
return $value instanceof Backup && $value->uuid === $backup->uuid;
52+
})
53+
)->andReturn(new Response());
4954

5055
$this->actingAs($user)->deleteJson($this->link($backup))->assertStatus(Response::HTTP_NO_CONTENT);
5156

5257
$backup->refresh();
58+
$this->assertSoftDeleted($backup);
5359

54-
$this->assertNotNull($backup->deleted_at);
60+
Event::assertDispatched(ActivityLogged::class, function (ActivityLogged $event) use ($backup, $user) {
61+
$this->assertTrue($event->isServerEvent());
62+
$this->assertTrue($event->is('server:backup.delete'));
63+
$this->assertTrue($user->is($event->actor()));
64+
$this->assertCount(2, $event->model->subjects);
5565

56-
$this->actingAs($user)->deleteJson($this->link($backup))->assertStatus(Response::HTTP_NOT_FOUND);
66+
$subjects = $event->model->subjects;
67+
$this->assertCount(1, $subjects->filter(fn ($model) => $model->subject->is($backup)));
68+
$this->assertCount(1, $subjects->filter(fn ($model) => $model->subject->is($backup->server)));
5769

58-
$event = $backup->audits()->where('action', AuditLog::SERVER__BACKUP_DELETED)->latest()->first();
70+
return true;
71+
});
5972

60-
$this->assertNotNull($event);
61-
$this->assertFalse($event->is_system);
62-
$this->assertEquals($backup->server_id, $event->server_id);
63-
$this->assertEquals($user->id, $event->user_id);
73+
$this->actingAs($user)->deleteJson($this->link($backup))->assertStatus(Response::HTTP_NOT_FOUND);
6474
}
6575
}

0 commit comments

Comments
 (0)