Page MenuHomePhorge

D1654.1775186032.diff
No OneTemporary

Authored By
Unknown
Size
8 KB
Referenced Files
None
Subscribers
None

D1654.1775186032.diff

diff --git a/src/app/Console/Commands/UserForceDelete.php b/src/app/Console/Commands/UserForceDelete.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/UserForceDelete.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+
+class UserForceDelete extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:force-delete {user}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Delete a user for realz';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $user = \App\User::withTrashed()->where('email', $this->argument('user'))->first();
+
+ if (!$user) {
+ return 1;
+ }
+
+ if (!$user->trashed()) {
+ $this->error('The user is not yet deleted');
+ return 1;
+ }
+
+ DB::beginTransaction();
+ $user->forceDelete();
+ DB::commit();
+ }
+}
diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php
--- a/src/app/Entitlement.php
+++ b/src/app/Entitlement.php
@@ -119,6 +119,8 @@
if ($this->entitleable instanceof \App\Domain) {
return $this->entitleable->namespace;
}
+
+ return null;
}
/**
diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php
--- a/src/app/Observers/EntitlementObserver.php
+++ b/src/app/Observers/EntitlementObserver.php
@@ -93,8 +93,19 @@
$entitlement->createTransaction(\App\Transaction::ENTITLEMENT_DELETED);
}
+ /**
+ * Handle the entitlement "deleting" event.
+ *
+ * @param \App\Entitlement $entitlement The entitlement.
+ *
+ * @return void
+ */
public function deleting(Entitlement $entitlement)
{
+ if ($entitlement->trashed()) {
+ return;
+ }
+
// Start calculating the costs for the consumption of this entitlement if the
// existing consumption spans >= 14 days.
//
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -4,7 +4,9 @@
use App\Entitlement;
use App\Domain;
+use App\Transaction;
use App\User;
+use App\Wallet;
use Illuminate\Support\Facades\DB;
class UserObserver
@@ -105,6 +107,11 @@
*/
public function deleting(User $user)
{
+ if ($user->isForceDeleting()) {
+ $this->forceDeleting($user);
+ return;
+ }
+
// TODO: Especially in tests we're doing delete() on a already deleted user.
// Should we escape here - for performance reasons?
// TODO: I think all of this should use database transactions
@@ -138,9 +145,8 @@
$users = array_unique($users);
$domains = array_unique($domains);
- // Note: Domains/users need to be deleted one by one to make sure
- // events are fired and observers can do the proper cleanup.
- // Entitlements have no delete event handlers as for now.
+ // Domains/users/entitlements need to be deleted one by one to make sure
+ // events are fired and observers can do the proper cleanup.
if (!empty($users)) {
foreach (User::whereIn('id', $users)->get() as $_user) {
$_user->delete();
@@ -163,6 +169,69 @@
}
/**
+ * Handle the "deleting" event on forceDelete() call.
+ *
+ * @param User $user The user that is being deleted.
+ *
+ * @return void
+ */
+ public function forceDeleting(User $user)
+ {
+ // TODO: We assume that at this moment all belongings are already soft-deleted.
+
+ // Remove owned users/domains
+ $wallets = $user->wallets()->pluck('id')->all();
+ $assignments = Entitlement::withTrashed()->whereIn('wallet_id', $wallets)->get();
+ $entitlements = [];
+ $domains = [];
+ $users = [];
+
+ foreach ($assignments as $entitlement) {
+ $entitlements[] = $entitlement->id;
+
+ if ($entitlement->entitleable_type == Domain::class) {
+ $domains[] = $entitlement->entitleable_id;
+ } elseif (
+ $entitlement->entitleable_type == User::class
+ && $entitlement->entitleable_id != $user->id
+ ) {
+ $users[] = $entitlement->entitleable_id;
+ }
+ }
+
+ $users = array_unique($users);
+ $domains = array_unique($domains);
+
+ // Remove the user "direct" entitlements explicitely, if they belong to another
+ // user's wallet they will not be removed by the wallets foreign key cascade
+ Entitlement::withTrashed()
+ ->where('entitleable_id', $user->id)
+ ->where('entitleable_type', User::class)
+ ->forceDelete();
+
+ // Users need to be deleted one by one to make sure observers can do the proper cleanup.
+ if (!empty($users)) {
+ foreach (User::withTrashed()->whereIn('id', $users)->get() as $_user) {
+ $_user->forceDelete();
+ }
+ }
+
+ // Domains can be just removed
+ if (!empty($domains)) {
+ Domain::withTrashed()->whereIn('id', $domains)->forceDelete();
+ }
+
+ // Remove transactions, they also have no foreign key constraint
+ Transaction::where('object_type', Entitlement::class)
+ ->whereIn('object_id', $entitlements)
+ ->delete();
+
+ Transaction::where('object_type', Wallet::class)
+ ->whereIn('object_id', $wallets)
+ ->delete();
+ }
+
+ /**
* Handle the "retrieving" event.
*
* @param User $user The user that is being retrieved.
diff --git a/src/tests/Feature/Console/UserForceDeleteTest.php b/src/tests/Feature/Console/UserForceDeleteTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/UserForceDeleteTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Tests\Feature\Console;
+
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class UserForceDeleteTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('user@force-delete.com');
+ $this->deleteTestDomain('force-delete.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('user@force-delete.com');
+ $this->deleteTestDomain('force-delete.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test the command
+ */
+ public function testHandle(): void
+ {
+ // Non-existing user
+ $this->artisan('user:force-delete unknown@unknown.org')
+ ->assertExitCode(1);
+
+ Queue::fake();
+ $user = $this->getTestUser('user@force-delete.com');
+ $domain = $this->getTestDomain('force-delete.com', [
+ 'status' => \App\Domain::STATUS_NEW,
+ 'type' => \App\Domain::TYPE_HOSTED,
+ ]);
+ $package_kolab = \App\Package::where('title', 'kolab')->first();
+ $package_domain = \App\Package::where('title', 'domain-hosting')->first();
+ $user->assignPackage($package_kolab);
+ $domain->assignPackage($package_domain, $user);
+ $wallet = $user->wallets()->first();
+ $entitlements = $wallet->entitlements->pluck('id')->all();
+
+ $this->assertCount(5, $entitlements);
+
+ // Non-deleted user
+ $this->artisan('user:force-delete user@force-delete.com')
+ ->assertExitCode(1);
+
+ $user->delete();
+
+ $this->assertTrue($user->trashed());
+ $this->assertTrue($domain->fresh()->trashed());
+
+ // Deleted user
+ $this->artisan('user:force-delete user@force-delete.com')
+ ->assertExitCode(0);
+
+ $this->assertCount(
+ 0,
+ \App\User::withTrashed()->where('email', 'user@force-delete.com')->get()
+ );
+ $this->assertCount(
+ 0,
+ \App\Domain::withTrashed()->where('namespace', 'force-delete.com')->get()
+ );
+ $this->assertCount(
+ 0,
+ \App\Wallet::where('id', $wallet->id)->get()
+ );
+ $this->assertCount(
+ 0,
+ \App\Entitlement::withTrashed()->where('wallet_id', $wallet->id)->get()
+ );
+ $this->assertCount(
+ 0,
+ \App\Entitlement::withTrashed()->where('entitleable_id', $user->id)->get()
+ );
+ $this->assertCount(
+ 0,
+ \App\Transaction::whereIn('object_id', $entitlements)
+ ->where('object_type', \App\Entitlement::class)
+ ->get()
+ );
+
+ // TODO: Test that it also deletes users in a group account
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 3:13 AM (2 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822358
Default Alt Text
D1654.1775186032.diff (8 KB)

Event Timeline