From 73c3ec5c43b1b8eb265b489478271e169a3307b4 Mon Sep 17 00:00:00 2001 From: sulimanbenhalim Date: Tue, 27 Oct 2026 15:04:33 +0200 Subject: [PATCH 1/4] fix: erase the link cards that deleted accounts left behind --- ...se_the_link_previews_of_erased_commits.php | 23 +++++++++++++++++++ tests/Unit/Actions/DeleteUserTest.php | 18 +++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php diff --git a/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php new file mode 100644 index 000000000..c263bbcca --- /dev/null +++ b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php @@ -0,0 +1,23 @@ +string('app.url'))->after('://')->before('/')->value(); + + DB::table('link_previews') + ->whereLike('url', "%{$host}/@%/commits/%") + ->whereNotExists(fn (Builder $query): Builder => $query->from('commits') + ->whereNotNull('user_id') + ->whereRaw("commits.id::text = substring(lower(link_previews.url) from '/commits/([0-9a-f-]{36})')")) + ->delete(); + } +}; diff --git a/tests/Unit/Actions/DeleteUserTest.php b/tests/Unit/Actions/DeleteUserTest.php index df3242c95..956774f75 100644 --- a/tests/Unit/Actions/DeleteUserTest.php +++ b/tests/Unit/Actions/DeleteUserTest.php @@ -353,3 +353,21 @@ resolve(DeleteUser::class)->handle($user); expect(PushSubscription::query()->count())->toBe(0); }); + +it('erases the previews of links to commits that deleted accounts already took with them', function (): void { + $tombstone = Commit::factory()->create(['user_id' => null, 'body' => '', 'deleted_at' => now()]); + $gone = fake()->uuid(); + $leftovers = collect([ + LinkPreview::factory()->create(['url' => url("/@gone/commits/{$tombstone->id}")]), + LinkPreview::factory()->create(['url' => url('/@gone/commits/'.mb_strtoupper($gone))]), + ]); + $kept = collect([Commit::factory()->create(), Commit::factory()->create(['deleted_at' => now()])]) + ->map(fn (Commit $commit): LinkPreview => LinkPreview::factory()->create(['url' => url("/@{$commit->user->handle}/commits/{$commit->id}")])) + ->push(LinkPreview::factory()->create(['url' => "https://example.com/@gone/commits/{$gone}"])); + + $migration = require database_path('migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php'); + $migration->up(); + + $leftovers->each(fn (LinkPreview $preview) => $this->assertModelMissing($preview)); + $kept->each(fn (LinkPreview $preview) => $this->assertModelExists($preview)); +}); From c8684d274132da70ca3801e9ca5a0e1686d12bd1 Mon Sep 17 00:00:00 2001 From: sulimanbenhalim Date: Tue, 27 Oct 2026 16:15:51 +0200 Subject: [PATCH 2/4] fix: keep the previews of other sites whose host only ends in the app's host --- ...10_27_160000_erase_the_link_previews_of_erased_commits.php | 4 +++- tests/Unit/Actions/DeleteUserTest.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php index c263bbcca..e420f9b61 100644 --- a/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php +++ b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php @@ -14,7 +14,9 @@ public function up(): void $host = Str::of(config()->string('app.url'))->after('://')->before('/')->value(); DB::table('link_previews') - ->whereLike('url', "%{$host}/@%/commits/%") + ->where(fn (Builder $query): Builder => $query + ->whereLike('url', "%://{$host}/@%/commits/%") + ->orWhereLike('url', "%.{$host}/@%/commits/%")) ->whereNotExists(fn (Builder $query): Builder => $query->from('commits') ->whereNotNull('user_id') ->whereRaw("commits.id::text = substring(lower(link_previews.url) from '/commits/([0-9a-f-]{36})')")) diff --git a/tests/Unit/Actions/DeleteUserTest.php b/tests/Unit/Actions/DeleteUserTest.php index 956774f75..a365aece8 100644 --- a/tests/Unit/Actions/DeleteUserTest.php +++ b/tests/Unit/Actions/DeleteUserTest.php @@ -360,10 +360,12 @@ $leftovers = collect([ LinkPreview::factory()->create(['url' => url("/@gone/commits/{$tombstone->id}")]), LinkPreview::factory()->create(['url' => url('/@gone/commits/'.mb_strtoupper($gone))]), + LinkPreview::factory()->create(['url' => str_replace('://', '://www.', url("/@gone/commits/{$gone}"))]), ]); $kept = collect([Commit::factory()->create(), Commit::factory()->create(['deleted_at' => now()])]) ->map(fn (Commit $commit): LinkPreview => LinkPreview::factory()->create(['url' => url("/@{$commit->user->handle}/commits/{$commit->id}")])) - ->push(LinkPreview::factory()->create(['url' => "https://example.com/@gone/commits/{$gone}"])); + ->push(LinkPreview::factory()->create(['url' => "https://example.com/@gone/commits/{$gone}"])) + ->push(LinkPreview::factory()->create(['url' => str_replace('://', '://not', url("/@gone/commits/{$gone}"))])); $migration = require database_path('migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php'); $migration->up(); From aebcea99a23dcd5b7ac977a816cc70bb19464028 Mon Sep 17 00:00:00 2001 From: sulimanbenhalim Date: Tue, 27 Oct 2026 20:54:00 +0200 Subject: [PATCH 3/4] fix: keep the previews of other sites that only carry a commit link in their query --- ..._27_160000_erase_the_link_previews_of_erased_commits.php | 6 ++---- tests/Unit/Actions/DeleteUserTest.php | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php index e420f9b61..0b8f81c9a 100644 --- a/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php +++ b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php @@ -11,12 +11,10 @@ { public function up(): void { - $host = Str::of(config()->string('app.url'))->after('://')->before('/')->value(); + $host = preg_quote(Str::of(config()->string('app.url'))->after('://')->before('/')->value()); DB::table('link_previews') - ->where(fn (Builder $query): Builder => $query - ->whereLike('url', "%://{$host}/@%/commits/%") - ->orWhereLike('url', "%.{$host}/@%/commits/%")) + ->whereRaw('url ~* ?', ["^https?://([^/?#]+\\.)?{$host}/@[^/]+/commits/"]) ->whereNotExists(fn (Builder $query): Builder => $query->from('commits') ->whereNotNull('user_id') ->whereRaw("commits.id::text = substring(lower(link_previews.url) from '/commits/([0-9a-f-]{36})')")) diff --git a/tests/Unit/Actions/DeleteUserTest.php b/tests/Unit/Actions/DeleteUserTest.php index a365aece8..b95bd2382 100644 --- a/tests/Unit/Actions/DeleteUserTest.php +++ b/tests/Unit/Actions/DeleteUserTest.php @@ -365,7 +365,8 @@ $kept = collect([Commit::factory()->create(), Commit::factory()->create(['deleted_at' => now()])]) ->map(fn (Commit $commit): LinkPreview => LinkPreview::factory()->create(['url' => url("/@{$commit->user->handle}/commits/{$commit->id}")])) ->push(LinkPreview::factory()->create(['url' => "https://example.com/@gone/commits/{$gone}"])) - ->push(LinkPreview::factory()->create(['url' => str_replace('://', '://not', url("/@gone/commits/{$gone}"))])); + ->push(LinkPreview::factory()->create(['url' => str_replace('://', '://not', url("/@gone/commits/{$gone}"))])) + ->push(LinkPreview::factory()->create(['url' => 'https://example.com/redirect?to='.url("/@gone/commits/{$gone}")])); $migration = require database_path('migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php'); $migration->up(); From 31da34736caad4865de860051d41d5e32b211d03 Mon Sep 17 00:00:00 2001 From: sulimanbenhalim Date: Tue, 27 Oct 2026 21:41:28 +0200 Subject: [PATCH 4/4] fix: keep previews of the app's own pages that only mention a commit in their query --- ..._10_27_160000_erase_the_link_previews_of_erased_commits.php | 2 +- tests/Unit/Actions/DeleteUserTest.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php index 0b8f81c9a..c6d293402 100644 --- a/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php +++ b/database/migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php @@ -14,7 +14,7 @@ public function up(): void $host = preg_quote(Str::of(config()->string('app.url'))->after('://')->before('/')->value()); DB::table('link_previews') - ->whereRaw('url ~* ?', ["^https?://([^/?#]+\\.)?{$host}/@[^/]+/commits/"]) + ->whereRaw('url ~* ?', ["^https?://([^/?#]+\\.)?{$host}/@[^/?#]+/commits/"]) ->whereNotExists(fn (Builder $query): Builder => $query->from('commits') ->whereNotNull('user_id') ->whereRaw("commits.id::text = substring(lower(link_previews.url) from '/commits/([0-9a-f-]{36})')")) diff --git a/tests/Unit/Actions/DeleteUserTest.php b/tests/Unit/Actions/DeleteUserTest.php index b95bd2382..2e99e53fd 100644 --- a/tests/Unit/Actions/DeleteUserTest.php +++ b/tests/Unit/Actions/DeleteUserTest.php @@ -366,7 +366,8 @@ ->map(fn (Commit $commit): LinkPreview => LinkPreview::factory()->create(['url' => url("/@{$commit->user->handle}/commits/{$commit->id}")])) ->push(LinkPreview::factory()->create(['url' => "https://example.com/@gone/commits/{$gone}"])) ->push(LinkPreview::factory()->create(['url' => str_replace('://', '://not', url("/@gone/commits/{$gone}"))])) - ->push(LinkPreview::factory()->create(['url' => 'https://example.com/redirect?to='.url("/@gone/commits/{$gone}")])); + ->push(LinkPreview::factory()->create(['url' => 'https://example.com/redirect?to='.url("/@gone/commits/{$gone}")])) + ->push(LinkPreview::factory()->create(['url' => url("/@gone?redirect=/commits/{$gone}")])); $migration = require database_path('migrations/2026_10_27_160000_erase_the_link_previews_of_erased_commits.php'); $migration->up();