Merge pull request #7249 from pixelfed/fix/privacy-remove-all-chunkbyid-dangerzone

Fix remove-all follower purge chunkById key and gate the route behind sudo
pull/7252/head
Shlee 1 week ago committed by GitHub
commit ad21fc89db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -227,7 +227,6 @@ trait PrivacySettings
case 'remove-all':
Follower::whereFollowingId($profile->id)
->select(['profile_id', 'following_id'])
->chunkById(100, function ($followers) {
foreach ($followers as $follower) {
FeedUnfollowPipeline::dispatch($follower->profile_id, $follower->following_id)->onQueue('feed');

@ -309,7 +309,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['localization'])->grou
Route::post('privacy/blocked-instances', [SettingsController::class, 'blockedInstanceStore']);
Route::post('privacy/blocked-instances/unblock', [SettingsController::class, 'blockedInstanceUnblock'])->name('settings.privacy.blocked-instances.unblock');
Route::get('privacy/blocked-keywords', [SettingsController::class, 'blockedKeywords'])->name('settings.privacy.blocked-keywords');
Route::post('privacy/account', [SettingsController::class, 'privateAccountOptions'])->name('settings.privacy.account');
Route::post('privacy/account', [SettingsController::class, 'privateAccountOptions'])->name('settings.privacy.account')->middleware('dangerzone');
Route::prefix('remove')->middleware('dangerzone')->group(function () {
Route::get('request/temporary', [SettingsController::class, 'removeAccountTemporary'])->name('settings.remove.temporary');
Route::post('request/temporary', [SettingsController::class, 'removeAccountTemporarySubmit']);

@ -0,0 +1,93 @@
<?php
use App\Jobs\HomeFeedPipeline\FeedUnfollowPipeline;
use App\Models\Follower;
use App\Models\Profile;
use App\Models\User;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Queue;
uses(LazilyRefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| POST /settings/privacy/account — remove-all
|--------------------------------------------------------------------------
|
| The "Remove existing followers" option must delete every follower (across
| multiple chunks), dispatch a FeedUnfollowPipeline per follower, and flip the
| account to private + non-suggestable. A restrictive ->select() on the
| chunkById query previously omitted the primary key, so chunkById threw a
| RuntimeException after the first 100 rows -> 500, no deletion, no privacy
| switch.
|
*/
describe('remove-all', function () {
it('deletes all followers across chunks and switches the account to private', function () {
Queue::fake();
$user = User::factory()->create();
$user->refresh();
$profile = $user->profile;
$profile->update(['is_private' => false, 'is_suggestable' => true]);
// More than one chunk (chunkById uses 100) to exercise pagination past
// the first chunk, which is exactly where the missing-key bug threw.
$followerCount = 150;
for ($i = 0; $i < $followerCount; $i++) {
Follower::create([
'profile_id' => Profile::factory()->create()->id,
'following_id' => $profile->id,
'local_profile' => true,
]);
}
expect(Follower::whereFollowingId($profile->id)->count())->toBe($followerCount);
$this->actingAs($user)
->withSession(['auth.password_confirmed_at' => time()])
->post(route('settings.privacy.account'), [
'mode' => 'remove-all',
'duration' => 60,
])
->assertOk();
// A job per removed follower, across all chunks (not just the first 100).
Queue::assertPushed(FeedUnfollowPipeline::class, $followerCount);
// Every follower row is gone.
expect(Follower::whereFollowingId($profile->id)->count())->toBe(0);
// The account was switched to private and removed from the directory.
$profile->refresh();
expect((bool) $profile->is_private)->toBeTrue()
->and((bool) $profile->is_suggestable)->toBeFalse();
});
it('requires password confirmation (dangerzone) before removing followers', function () {
$user = User::factory()->create();
$user->refresh();
$profile = $user->profile;
$profile->update(['is_private' => false, 'is_suggestable' => true]);
Follower::create([
'profile_id' => Profile::factory()->create()->id,
'following_id' => $profile->id,
'local_profile' => true,
]);
// No confirmed-password session: dangerzone must redirect to the confirm
// page and the destructive action must not run.
$this->actingAs($user)
->post(route('settings.privacy.account'), [
'mode' => 'remove-all',
'duration' => 60,
])
->assertRedirect(route('password.confirm'));
expect(Follower::whereFollowingId($profile->id)->count())->toBe(1);
$profile->refresh();
expect((bool) $profile->is_private)->toBeFalse();
});
});
Loading…
Cancel
Save