mirror of
https://github.com/projectsend/projectsend.git
synced 2026-10-07 13:51:03 +00:00
Stop serving a self-deleted account's files, and let them go at once
Reported from the shared free instance. A client deleted their own account on 2026-09-18. Their five files kept serving through share links with no expiry for the whole 30-day grace period. Somebody who asked to leave stayed published. Rule 1: once an account is soft-deleted, its uploads are served to nobody but staff. That covers the client scope (assignment, group, shared folder), share links, the public listing, public comments and zips. Staff keep them, because the grace period exists to undo a mistake. Nothing is deleted and share links are kept, so a restored account is served again. A withdrawn share link answers like a token that never existed. In practice this only meets self-deleted accounts: an administrator deleting an account that owns anything must already choose to delete or reassign it. Rule 2, new in Privacy settings: when someone deletes their own account, their files are deleted "when the grace period ends" (the default, and today's behaviour) or "right away". "Right away" uses DeletedAccountContent's cascade: their own uploads, and their folders only if nothing else is left inside. It runs in the same transaction as the account delete. A platform can force "right away" through the new ResolvingSelfDeletion hook. The screen then shows the choice as set by the hosting plan instead of offering a switch. A second setting decides whose deletion both rules apply to: any account (the default) or clients only. A staff member's uploads are often the organization's work for its clients. The delete-account screen now says what happens to the files before the person confirms. New strings are in all sixteen locales.
This commit is contained in:
@@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Modules\Audit\Action;
|
||||
use App\Modules\Audit\ActivityLogger;
|
||||
use App\Modules\Identity\AccountContentDeletion;
|
||||
use App\Modules\Identity\Erasure\SelfDeletion;
|
||||
use App\Modules\Platform\Settings\Setting;
|
||||
use App\Modules\Platform\Settings\Settings;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -18,7 +19,8 @@ use Inertia\Response;
|
||||
|
||||
/**
|
||||
* System-wide privacy settings (staff-only): download IP logging
|
||||
* granularity, the account-erasure retention window, how long API request
|
||||
* granularity, the account-erasure retention window, what deleting your
|
||||
* own account does to your files (see SelfDeletion), how long API request
|
||||
* telemetry is kept, and whether to discourage search engines from
|
||||
* indexing this installation.
|
||||
*/
|
||||
@@ -28,6 +30,7 @@ class PrivacySettingsController extends Controller
|
||||
private readonly Settings $settings,
|
||||
private readonly ActivityLogger $activity,
|
||||
private readonly AccountContentDeletion $accountDeletion,
|
||||
private readonly SelfDeletion $selfDeletion,
|
||||
) {}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
@@ -41,6 +44,12 @@ class PrivacySettingsController extends Controller
|
||||
// erasure will use, stored once for everybody, and the page is
|
||||
// already behind edit_settings.
|
||||
'reassign_candidates' => $this->accountDeletion->candidates(null),
|
||||
// The effective answer, not the stored one: where a platform
|
||||
// has made the choice, the screen shows what will happen and
|
||||
// says who decided, instead of a switch that does nothing.
|
||||
'account_self_delete_files' => $this->selfDeletion->deletesFilesImmediately() ? 'immediately' : 'after_grace_period',
|
||||
'account_self_delete_files_managed' => $this->selfDeletion->isManaged(),
|
||||
'account_self_delete_scope' => $this->settings->get(Setting::AccountSelfDeleteScope),
|
||||
'api_request_log_retention_days' => $this->settings->get(Setting::ApiRequestLogRetentionDays),
|
||||
'discourage_search_indexing' => $this->settings->get(Setting::DiscourageSearchIndexing),
|
||||
]);
|
||||
@@ -58,6 +67,8 @@ class PrivacySettingsController extends Controller
|
||||
'required_if:account_erasure_content_action,reassign',
|
||||
Rule::exists('users', 'id')->where('active', true),
|
||||
],
|
||||
'account_self_delete_files' => ['required', Rule::in(['after_grace_period', 'immediately'])],
|
||||
'account_self_delete_scope' => ['required', Rule::in(['any', 'clients'])],
|
||||
'api_request_log_retention_days' => ['required', 'integer', 'min:0', 'max:3650'],
|
||||
'discourage_search_indexing' => ['required', 'boolean'],
|
||||
]);
|
||||
@@ -71,6 +82,14 @@ class PrivacySettingsController extends Controller
|
||||
Setting::AccountErasureReassignTo,
|
||||
$validated['account_erasure_content_action'] === 'reassign' ? (int) $validated['account_erasure_reassign_to'] : 0,
|
||||
);
|
||||
// Not written while a platform decides it. The screen shows that
|
||||
// choice with the control disabled, so what comes back is only the
|
||||
// platform's answer echoed; storing it would record a decision
|
||||
// this installation never made.
|
||||
if (! $this->selfDeletion->isManaged()) {
|
||||
$this->settings->set(Setting::AccountSelfDeleteFiles, $validated['account_self_delete_files']);
|
||||
}
|
||||
$this->settings->set(Setting::AccountSelfDeleteScope, $validated['account_self_delete_scope']);
|
||||
$this->settings->set(Setting::ApiRequestLogRetentionDays, $validated['api_request_log_retention_days']);
|
||||
$this->settings->set(Setting::DiscourageSearchIndexing, $validated['discourage_search_indexing']);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user