mirror of
https://github.com/projectsend/projectsend.git
synced 2026-10-04 13:33:22 +00:00
bccf3d1f29
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.
101 lines
5.1 KiB
PHP
101 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Platform\Http\Controllers;
|
|
|
|
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;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\Rule;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
/**
|
|
* System-wide privacy settings (staff-only): download IP logging
|
|
* 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.
|
|
*/
|
|
class PrivacySettingsController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly Settings $settings,
|
|
private readonly ActivityLogger $activity,
|
|
private readonly AccountContentDeletion $accountDeletion,
|
|
private readonly SelfDeletion $selfDeletion,
|
|
) {}
|
|
|
|
public function edit(Request $request): Response
|
|
{
|
|
return Inertia::render('system/settings/privacy', [
|
|
'download_ip_logging' => $this->settings->get(Setting::DownloadIpLogging),
|
|
'account_erasure_grace_days' => $this->settings->get(Setting::AccountErasureGraceDays),
|
|
'account_erasure_content_action' => $this->settings->get(Setting::AccountErasureContentAction),
|
|
'account_erasure_reassign_to' => $this->settings->get(Setting::AccountErasureReassignTo),
|
|
// Installation-wide on purpose: this is the default every
|
|
// 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),
|
|
]);
|
|
}
|
|
|
|
public function update(Request $request): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'download_ip_logging' => ['required', Rule::in(['all', 'anonymous_only', 'none'])],
|
|
'account_erasure_grace_days' => ['required', 'integer', 'min:0'],
|
|
'account_erasure_content_action' => ['required', Rule::in(['cascade_delete', 'reassign'])],
|
|
'account_erasure_reassign_to' => [
|
|
'nullable',
|
|
'integer',
|
|
'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'],
|
|
]);
|
|
|
|
$this->settings->set(Setting::DownloadIpLogging, $validated['download_ip_logging']);
|
|
$this->settings->set(Setting::AccountErasureGraceDays, $validated['account_erasure_grace_days']);
|
|
$this->settings->set(Setting::AccountErasureContentAction, $validated['account_erasure_content_action']);
|
|
// Only meaningful for 'reassign'; store 0 otherwise so a later switch
|
|
// back to 'cascade_delete' doesn't leave a stale target lying around.
|
|
$this->settings->set(
|
|
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']);
|
|
|
|
$this->activity->log(Action::SettingsUpdated, context: ['section' => 'privacy']);
|
|
|
|
return back();
|
|
}
|
|
}
|