mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 17:15:08 +00:00
616a355d54
A client who may create folders creates them at the top of the library, beside the ones staff made, and their uploads land at the root too. An administrator opening /files gets one flat pile with nothing saying which parts belong to whom. With the new "Give each client a folder of their own" setting, every new client gets a folder named after them and it acts as their root: what they upload and any folder they create goes inside it. /files becomes a list of clients rather than a pile. The sentence this feature has to keep true: **the home is a default location, not a boundary.** Folder::scopeVisibleToClient is untouched, so a folder staff shared with a client still reaches them and sits beside their own. Making the home a jail would have silently revoked every share that already exists -- a data-access change wearing the clothes of a tidying-up feature. There is a test named after that rule. What the client sees is the *inside* of their folder, not a folder wearing their own name, which is not information to them. The breadcrumb is trimmed of it for the same reason: "Invoices", not "Acme Ltd / Invoices". Some decisions worth naming: - **A column, not a convention.** `folders.home_for_user_id`, unique. Matching on the name breaks the moment two clients share one, and `created_by` plus a null parent catches every root folder a client ever made themselves. The question is asked on each upload and each portal listing and the answer has to be exact. - **created_by is the client**, because that is how scopeVisibleToClient already grants somebody their own folder -- no assignment row to keep in step with it. That is also why this writes the row rather than calling FolderService::create(), which takes created_by from auth()->id(). - **On model events**, not in the services that make and rename clients. There are nine of those (ClientAccounts, ClientProvisioning, the profile screen, two update endpoints, AccountConversion, invitations, LDAP, social) and a rule repeated in nine places is missing from the tenth. - **Turning the setting on creates nothing.** Existing clients get a folder when an administrator presses a button that says how many are waiting, and it reports created/total/already-had afterwards. Somebody should be able to switch this on, look, and switch it off without having reorganised a library. It moves no files either. - **Nobody deletes a home from a folder screen**, staff included, and the client cannot rename theirs -- they own it, so ownership alone would have let them, and its name follows the account anyway. - **The name always follows the client**, over a hand-typed one. A folder still called "Acme Ltd" under an account now called something else misleads the administrator the feature exists for. Verified in a real browser as well as in tests: the screen mounts, the panel reads "24 of your existing clients have no folder yet", and pressing the button answers "24 of 24 clients got a folder. 0 already had one."
619 lines
31 KiB
PHP
619 lines
31 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Platform\Settings;
|
|
|
|
/**
|
|
* Every application setting, typed and with its default in code — the
|
|
* replacement for v1's ~230 loose option rows read per request. The
|
|
* database stores overrides only; an absent row means the default.
|
|
*
|
|
* Add a case here (plus type and default below) when a feature gains a
|
|
* setting. Settings are per-install.
|
|
*/
|
|
enum Setting: string
|
|
{
|
|
case SiteName = 'site_name';
|
|
|
|
// Client self-registration (v1 parity: clients_can_register /
|
|
// clients_auto_approve). Consumed by the Clients module.
|
|
case ClientsCanRegister = 'clients_can_register';
|
|
case ClientsAutoApprove = 'clients_auto_approve';
|
|
|
|
// Registration group options (v1 parity): auto-join a group on
|
|
// signup (0 = off), and which groups registrants may request
|
|
// membership to (none | public | all). Consumed by Clients/Groups.
|
|
case ClientsAutoGroup = 'clients_auto_group';
|
|
case ClientsCanSelectGroup = 'clients_can_select_group';
|
|
|
|
// Days a denied membership request blocks re-requesting (0 = none).
|
|
case ClientsMembershipDenyCooldownDays = 'clients_membership_deny_cooldown_days';
|
|
|
|
// How long a staff-sent client invitation link stays valid before the
|
|
// invited address has to ask for a new one. Consumed by
|
|
// InvitationController and InvitationRedemptionController, wherever a
|
|
// fresh Invitation is issued.
|
|
case ClientInvitationExpiryHours = 'client_invitation_expiry_hours';
|
|
|
|
// Whether the client portal offers inline preview at all — the whole
|
|
// affordance, images included, not just the media types. Staff are
|
|
// never gated by it: it exists so an installation can decide that a
|
|
// client either downloads a file or does not get it, without taking
|
|
// the tool away from the people who administer the library. See
|
|
// PreviewKind and FileThumbnailController::preview.
|
|
case ClientsCanPreviewFiles = 'clients_can_preview_files';
|
|
|
|
// Give every new client a folder of their own, named after them, and
|
|
// treat it as their root: their uploads and the folders they create
|
|
// land inside it instead of at the top of the library. What the
|
|
// administrator gets out of it is a /files that reads as one folder
|
|
// per client rather than a flat pile.
|
|
//
|
|
// It is NOT a boundary. A folder staff share with a client is still
|
|
// theirs to see, alongside their own -- making the home a jail would
|
|
// silently break every share that already exists. See ClientHomeFolders.
|
|
//
|
|
// Off by default, and turning it on changes nothing that already
|
|
// exists: existing clients get a home only when an administrator asks
|
|
// for one, with the button beside the switch. A setting that quietly
|
|
// reorganised a library on save would be one nobody could try.
|
|
case ClientsHomeFolders = 'clients_home_folders';
|
|
|
|
// Maximum upload size in MB (0 = unlimited).
|
|
case MaxFileSizeMb = 'max_file_size_mb';
|
|
|
|
// Prefills a new client's storage_quota_mb at creation time only —
|
|
// not a runtime fallback (0 = unlimited). Consumed by ClientsController.
|
|
case DefaultClientStorageQuotaMb = 'default_client_storage_quota_mb';
|
|
|
|
// Security: who must have two-factor authentication enabled
|
|
// (none | staff | clients | all) — see TwoFactorEnforcement.
|
|
case TwoFactorEnforcement = 'two_factor_enforcement';
|
|
|
|
// The two halves of the password policy, read by PasswordPolicy and
|
|
// applied through Password::defaults() at every field that *sets* a
|
|
// password. Deliberately not a composition policy: NIST SP 800-63B
|
|
// advises against mandatory uppercase/number/symbol rules and for
|
|
// length plus breach-checking, which is what these two express.
|
|
//
|
|
// The minimum is clamped to 8..128 on read, so no stored value can
|
|
// take an installation below Laravel's own default.
|
|
case PasswordMinLength = 'password_min_length';
|
|
|
|
// Whether a new password is checked against the haveibeenpwned breach
|
|
// corpus. The check is k-anonymous (only a SHA-1 prefix leaves the
|
|
// server) and fails open, so this exists for outbound egress and
|
|
// latency on isolated installs, not for correctness.
|
|
case PasswordRejectBreached = 'password_reject_breached';
|
|
|
|
// Master switch for transactional email (file shared, account
|
|
// approved/denied, …). Off by default — a fresh install's mailer
|
|
// isn't configured yet. SMTP transport itself is .env-only (no
|
|
// credential storage in this table); see Setting::EmailNotificationsEnabled
|
|
// usage in the Email settings page.
|
|
case EmailNotificationsEnabled = 'email_notifications_enabled';
|
|
|
|
// Freeform recipient list for admin-facing notifications (new client
|
|
// registration, client upload) — not tied to specific User rows,
|
|
// since "admin" isn't a role here, just whoever staff configure.
|
|
// Seeded with the first administrator's email at setup/CLI creation.
|
|
case AdminNotificationEmails = 'admin_notification_emails';
|
|
|
|
// Security: who is restricted to Setting::AllowedUploadExtensions
|
|
// (none | clients | all) — see UploadTypeRestriction. v1 parity:
|
|
// "Limit file types uploading to".
|
|
case UploadTypeRestriction = 'upload_type_restriction';
|
|
|
|
// Lowercase extensions (no dots) accepted for upload when the
|
|
// restriction above applies to the uploader. v1 parity: allowed_file_types.
|
|
case AllowedUploadExtensions = 'allowed_upload_extensions';
|
|
|
|
// File comments. Governed by settings rather than permission keys
|
|
// because the roles screen is capability-gated off in the cloud
|
|
// edition, and because anonymous authors have no role to hold a key
|
|
// at all — see CommentAuthors' docblock.
|
|
//
|
|
// CommentsScope decides which files accept comments (CommentScope);
|
|
// CommentsAuthors decides who may write one (CommentAuthors).
|
|
// PublicCommentsEnabled is separate from both on purpose: without it,
|
|
// a logged-in client on a public file could publish a world-visible
|
|
// comment that no administrator ever decided to allow.
|
|
case CommentsScope = 'comments_scope';
|
|
case CommentsAuthors = 'comments_authors';
|
|
case PublicCommentsEnabled = 'public_comments_enabled';
|
|
// Only meaningful when CommentsAuthors is `everyone` — an anonymous
|
|
// comment stays invisible until a moderator approves it.
|
|
case CommentsGuestModeration = 'comments_guest_moderation';
|
|
// How long after posting an author may still edit or delete their own
|
|
// comment (0 disables editing entirely).
|
|
case CommentsEditWindowMinutes = 'comments_edit_window_minutes';
|
|
|
|
// Largest zip download that can be asked for, in MB (0 = unlimited).
|
|
// The cap is on total size rather than on file count because bytes are
|
|
// what a build actually costs — worker time, the temp copies a remote
|
|
// disk needs, and the archive itself. The file count has its own,
|
|
// deliberately fixed, rail in ZipDownloadsController::MAX_FILES.
|
|
case MaxZipDownloadSizeMb = 'max_zip_download_size_mb';
|
|
|
|
// Whether a download's IP is recorded in the activity log (all |
|
|
// anonymous_only | none). Only affects Action::FileDownloaded /
|
|
// ShareLinkDownloaded entries — see ActivityLogger::shouldRecordIp().
|
|
// v1 parity: privacy_record_downloads_ip_address.
|
|
case DownloadIpLogging = 'download_ip_logging';
|
|
|
|
// How long API request telemetry is kept before pruning. Distinct from
|
|
// the activity log, which is an audit trail and is never pruned — see
|
|
// the api_request_logs migration for why the two are separate.
|
|
case ApiRequestLogRetentionDays = 'api_request_log_retention_days';
|
|
|
|
// How long a permanently failed queue job is kept, and how long a
|
|
// notification somebody has already read is kept. Both tables grow
|
|
// with use and neither ever shrank on its own: the failed-jobs list
|
|
// waited for somebody to press "Delete all failed", and nothing at
|
|
// all pruned notifications. An installation nobody tends should not
|
|
// pay for that with its database.
|
|
//
|
|
// 0 means keep indefinitely in both cases, the same explicit choice
|
|
// ApiRequestLogRetentionDays offers.
|
|
case FailedJobRetentionDays = 'failed_job_retention_days';
|
|
case NotificationRetentionDays = 'notification_retention_days';
|
|
|
|
// How many days a deleted account is retained (soft-deleted) before
|
|
// PurgeErasuresCommand permanently erases it. Consumed by
|
|
// ErasureSchedule, which every deletion path calls.
|
|
case AccountErasureGraceDays = 'account_erasure_grace_days';
|
|
|
|
// What the unattended erasure does with the files and folders a purged
|
|
// account owned: 'cascade_delete' removes them, 'reassign' hands them to
|
|
// AccountErasureReassignTo. The admin-initiated delete asks this per
|
|
// account; the cron job cannot, so it follows this setting. Consumed by
|
|
// AccountEraser.
|
|
case AccountErasureContentAction = 'account_erasure_content_action';
|
|
|
|
// The active account that inherits a purged account's content when
|
|
// AccountErasureContentAction is 'reassign'. 0 = none chosen; if it is
|
|
// unset or no longer valid at erase time, AccountEraser cascade-deletes
|
|
// instead, so content is never left orphaned.
|
|
case AccountErasureReassignTo = 'account_erasure_reassign_to';
|
|
|
|
// Whether PurgeExpiredFilesCommand's daily run actually deletes
|
|
// anything (off by default — deletion is destructive, so an admin
|
|
// must opt in) and how many days after a file's own expires_at it
|
|
// waits before doing so. Consumed by FileRetentionSettingsController
|
|
// and PurgeExpiredFilesCommand.
|
|
case ExpiredFilesAutoDeleteEnabled = 'expired_files_auto_delete_enabled';
|
|
case ExpiredFilesDeleteAfterDays = 'expired_files_delete_after_days';
|
|
|
|
// Whether PurgeOrphanFilesCommand's daily run actually deletes
|
|
// anything (off by default) and how many days after an orphan is
|
|
// first found it waits before doing so. Hardcoded off — regardless
|
|
// of this stored value — whenever external storage is active; this
|
|
// feature only ever operates on the local disk (see
|
|
// PurgeOrphanFilesCommand and FileRetentionSettingsController).
|
|
// Virus scanning — see docs/feature-virus-scanning.md and
|
|
// App\Modules\Files\Scanning\ScanningConfig, which is what reads
|
|
// these (the address and the on/off switch can both be overruled by a
|
|
// managed configuration in the environment).
|
|
case VirusScanningEnabled = 'virus_scanning_enabled';
|
|
case VirusScannerAddress = 'virus_scanner_address';
|
|
case VirusScanMaxSizeMb = 'virus_scan_max_size_mb';
|
|
// 'allow' or 'block' — what happens to a file the scanner cannot open
|
|
// (too large, or encrypted). Allowing marks it "not scanned" rather
|
|
// than calling it clean.
|
|
case VirusUnscannablePolicy = 'virus_unscannable_policy';
|
|
// 'allow' or 'hold' — what happens to new uploads while the scanner
|
|
// is unreachable.
|
|
case VirusScannerDownPolicy = 'virus_scanner_down_policy';
|
|
// How long a file waits for an unreachable scanner before the policy
|
|
// above is applied.
|
|
case VirusScannerWaitMinutes = 'virus_scanner_wait_minutes';
|
|
// How fast "Scan existing files" works through a library that was
|
|
// uploaded before scanning was switched on.
|
|
case VirusScanExistingRatePerMinute = 'virus_scan_existing_rate_per_minute';
|
|
|
|
case OrphanFilesAutoDeleteEnabled = 'orphan_files_auto_delete_enabled';
|
|
case OrphanFilesDeleteAfterDays = 'orphan_files_delete_after_days';
|
|
|
|
// Emits <meta name="robots" content="noindex"> site-wide when true.
|
|
// v1 parity: privacy_noindex_site.
|
|
case DiscourageSearchIndexing = 'discourage_search_indexing';
|
|
|
|
// Toggles only the browsable *directory* (the front page listing
|
|
// every public group/file) — a specific public group's page and
|
|
// downloads work off its own `public` flag regardless of this
|
|
// setting, same as a share link isn't gated by any global toggle.
|
|
// v1 parity: the public_listing_* feature, rescoped to v2's
|
|
// group/file public flags instead of tokens. See PublicGroupsController.
|
|
case PublicListingEnabled = 'public_listing_enabled';
|
|
|
|
// The same switch as ClientsCanPreviewFiles, for the anonymous side:
|
|
// whether a public file page offers to show the file as well as hand
|
|
// it over. v1 parity: public_listing_enable_preview. See
|
|
// PublicGroupsController::preview.
|
|
case PublicListingPreviewEnabled = 'public_listing_preview_enabled';
|
|
|
|
// The configurable base URL segment for the public listing (e.g.
|
|
// "public" -> /public, /public/{group-slug}). Consumed by
|
|
// PublicGroupsController's guard against every request's first path
|
|
// segment.
|
|
case PublicListingSlug = 'public_listing_slug';
|
|
|
|
// The active theme (a ThemeRegistry key) shared by the guest public
|
|
// pages and the client portal — one selection covers both, matching
|
|
// v1's single selected_clients_template. See PublicThemeRegistry.
|
|
case Theme = 'theme';
|
|
|
|
// The active theme for outgoing notification emails — a header/footer
|
|
// skin, independent of the per-notification subject/body editor
|
|
// (EmailTemplate). See EmailThemeRegistry.
|
|
case EmailTheme = 'email_theme';
|
|
|
|
// Which of the installed translation catalogues the language switcher
|
|
// actually offers. Installing a pack (dropping lang/{locale}.json in)
|
|
// makes a language *available*; this makes it *offered*, so an
|
|
// operator can run an English-and-Spanish site without the other
|
|
// fourteen catalogues showing up on their clients' screens. English
|
|
// is always offered whatever this says — it is the translation key,
|
|
// not a catalogue. See LocaleRegistry.
|
|
case EnabledLocales = 'enabled_locales';
|
|
|
|
// The language everyone starts in — the last word after a signed-in
|
|
// person's own preference and the browser's Accept-Language header,
|
|
// and therefore what an anonymous visitor with no matching header
|
|
// actually sees. English is only the default because it is the
|
|
// default default; an operator whose clients are all Spanish should
|
|
// not have to make each of them switch. Empty means "whatever
|
|
// APP_LOCALE says", so an existing install that set the environment
|
|
// variable keeps working until someone chooses here. Always resolved
|
|
// through LocaleRegistry::defaultLocale(), which refuses to hand back
|
|
// a language that is not enabled.
|
|
case DefaultLocale = 'default_locale';
|
|
|
|
// The clock everyone reads the application by — v1 parity: its
|
|
// `timezone` option. Unlike v1, nothing here calls
|
|
// date_default_timezone_set(): storage and computation stay UTC and
|
|
// this is applied only where a date is shown or a calendar day is
|
|
// asked for. That is what makes it safe for a signed-in user to
|
|
// override it with their own (users.timezone) without any two
|
|
// viewers disagreeing about what is actually stored.
|
|
//
|
|
// Empty means "whatever APP_TIMEZONE says", exactly like
|
|
// DefaultLocale above — an install that set the environment variable
|
|
// keeps working until someone chooses here. Always resolved through
|
|
// TimezoneRegistry, which refuses to hand back an identifier tzdata
|
|
// no longer knows.
|
|
case Timezone = 'timezone';
|
|
|
|
// Community-edition only (Capability::SystemUpdates) — self-hosted
|
|
// opt-out for the daily "is a newer version available" check. There
|
|
// is no in-app self-updater (see CheckForUpdatesCommand's docblock
|
|
// for why); this only controls whether the dashboard's System card
|
|
// is allowed to tell the admin a newer release exists.
|
|
case CheckForUpdates = 'check_for_updates';
|
|
|
|
// Whether this installation fetches the project's news feed for the
|
|
// dashboard card. Its own key rather than riding on CheckForUpdates
|
|
// above, because they are two different wants: "do not tell me about
|
|
// releases" and "do not show me the project's news" are asked
|
|
// separately, and an installation with no outbound access at all
|
|
// wants both off while an ordinary one may want updates and no feed.
|
|
//
|
|
// On by default, so nothing changes for an installation that has
|
|
// never seen this switch.
|
|
case FetchNews = 'fetch_news';
|
|
|
|
// Cached result of the last update check — never written directly by
|
|
// a settings form, only by CheckForUpdatesCommand. Empty string means
|
|
// "no successful check yet" (fresh install, or checks disabled).
|
|
case LatestKnownVersion = 'latest_known_version';
|
|
case LatestVersionCheckedAt = 'latest_version_checked_at';
|
|
|
|
// The rest of the cached release, for the "what's new" modal
|
|
// (LatestReleaseInfo) — title/notes/url/date of the GitHub release
|
|
// LatestKnownVersion came from. Notes is the release's raw Markdown
|
|
// body, rendered as plain text on the frontend (never as HTML —
|
|
// it's third-party content).
|
|
case LatestReleaseTitle = 'latest_release_title';
|
|
case LatestReleaseNotes = 'latest_release_notes';
|
|
case LatestReleaseUrl = 'latest_release_url';
|
|
case LatestReleasePublishedAt = 'latest_release_published_at';
|
|
|
|
// The version `projectsend:update` last brought this database in line
|
|
// with — written by that command only, never by a settings form.
|
|
//
|
|
// Its whole purpose is to be compared against config('projectsend.version')
|
|
// as read by the *web* process, which is not the same number when a
|
|
// manual install replaced its files and never reloaded PHP-FPM: OPcache
|
|
// keeps serving the code it compiled before the update, silently, while
|
|
// artisan reports the new version to the person trying to work out why.
|
|
// See RunningCodeState.
|
|
//
|
|
// Written downwards as well as upwards, because the command runs on
|
|
// every container boot and on every update.sh run — including the ones
|
|
// that go backwards.
|
|
case AppliedVersion = 'applied_version';
|
|
case AppliedVersionAt = 'applied_version_at';
|
|
|
|
// An update finished and the administrator has not been shown what it
|
|
// brought yet — the two versions it moved between, so the page can
|
|
// name every release in the gap rather than only the newest.
|
|
//
|
|
// Written by UpdateInstallation, and only for a genuine update of an
|
|
// existing installation: a fresh install has nothing to catch up on,
|
|
// and a container that reboots has not updated anything. Cleared when
|
|
// the page is read. Both empty means nothing is waiting.
|
|
case UpdateWelcomeFrom = 'update_welcome_from';
|
|
case UpdateWelcomeTo = 'update_welcome_to';
|
|
|
|
// This installation has just been installed and its administrator has
|
|
// not been shown the getting-started page yet.
|
|
//
|
|
// Raised where the first administrator is created — the setup screen
|
|
// and the provisioning command both — and cleared when the page is
|
|
// read. False by default, which is what keeps an installation that
|
|
// updates into this feature from being congratulated on an
|
|
// installation it completed a year ago.
|
|
case GettingStartedPending = 'getting_started_pending';
|
|
|
|
// Cached result of the last dashboard news feed fetch — never written
|
|
// directly by a settings form, only by FetchNewsCommand. Both editions
|
|
// see this (unlike CheckForUpdates above, which is Community-only);
|
|
// gated on the view_news permission, not a capability. Each item's
|
|
// `content` is HTML-Purified at fetch time (see FetchNewsCommand), so
|
|
// it's already safe to render — unlike LatestReleaseNotes above, this
|
|
// one legitimately needs inline HTML (links to the full changelog).
|
|
case NewsItems = 'news_items';
|
|
case NewsLastFetchedAt = 'news_last_fetched_at';
|
|
|
|
// CAPTCHA on public forms (v1 parity: captcha_method). Which service
|
|
// is active — 'none' or a CaptchaProvider value — and, on cloud only,
|
|
// whether the platform's own keys are used ('managed') or this
|
|
// installation's ('own'). The credentials themselves are not here:
|
|
// this store has no encrypted type, so they live in captcha_providers.
|
|
case CaptchaProvider = 'captcha_provider';
|
|
case CaptchaKeySource = 'captcha_key_source';
|
|
|
|
// Which forms actually ask for a token. Separate switches because an
|
|
// installation that takes no self-registrations, or does not want a
|
|
// challenge in front of its own staff login, should be able to say so
|
|
// without turning the whole feature off.
|
|
case CaptchaOnLogin = 'captcha_on_login';
|
|
case CaptchaOnRegistration = 'captcha_on_registration';
|
|
case CaptchaOnPasswordReset = 'captcha_on_password_reset';
|
|
case CaptchaOnPublicComments = 'captcha_on_public_comments';
|
|
|
|
public function type(): SettingType
|
|
{
|
|
return match ($this) {
|
|
self::SiteName,
|
|
self::ClientsCanSelectGroup,
|
|
self::TwoFactorEnforcement,
|
|
self::UploadTypeRestriction,
|
|
self::VirusScannerAddress,
|
|
self::VirusUnscannablePolicy,
|
|
self::VirusScannerDownPolicy,
|
|
self::DownloadIpLogging,
|
|
self::CommentsScope,
|
|
self::CommentsAuthors,
|
|
self::PublicListingSlug,
|
|
self::DefaultLocale,
|
|
self::Timezone,
|
|
self::Theme,
|
|
self::EmailTheme,
|
|
self::LatestKnownVersion,
|
|
self::LatestVersionCheckedAt,
|
|
self::LatestReleaseTitle,
|
|
self::LatestReleaseNotes,
|
|
self::LatestReleaseUrl,
|
|
self::LatestReleasePublishedAt,
|
|
self::AppliedVersion,
|
|
self::AppliedVersionAt,
|
|
self::UpdateWelcomeFrom,
|
|
self::UpdateWelcomeTo,
|
|
self::NewsLastFetchedAt,
|
|
self::CaptchaProvider,
|
|
self::CaptchaKeySource,
|
|
self::AccountErasureContentAction => SettingType::String,
|
|
|
|
self::ClientsCanRegister,
|
|
self::ClientsAutoApprove,
|
|
self::ClientsHomeFolders,
|
|
self::EmailNotificationsEnabled,
|
|
self::DiscourageSearchIndexing,
|
|
self::PublicListingEnabled,
|
|
self::ClientsCanPreviewFiles,
|
|
self::PublicListingPreviewEnabled,
|
|
self::CheckForUpdates,
|
|
self::FetchNews,
|
|
self::ExpiredFilesAutoDeleteEnabled,
|
|
self::PublicCommentsEnabled,
|
|
self::CommentsGuestModeration,
|
|
self::OrphanFilesAutoDeleteEnabled,
|
|
self::CaptchaOnLogin,
|
|
self::CaptchaOnRegistration,
|
|
self::CaptchaOnPasswordReset,
|
|
self::CaptchaOnPublicComments,
|
|
self::PasswordRejectBreached,
|
|
self::VirusScanningEnabled,
|
|
self::GettingStartedPending => SettingType::Boolean,
|
|
|
|
self::ClientsAutoGroup,
|
|
self::ClientsMembershipDenyCooldownDays,
|
|
self::ClientInvitationExpiryHours,
|
|
self::MaxFileSizeMb,
|
|
self::MaxZipDownloadSizeMb,
|
|
self::DefaultClientStorageQuotaMb,
|
|
self::AccountErasureGraceDays,
|
|
self::AccountErasureReassignTo,
|
|
self::ApiRequestLogRetentionDays,
|
|
self::FailedJobRetentionDays,
|
|
self::NotificationRetentionDays,
|
|
self::ExpiredFilesDeleteAfterDays,
|
|
self::CommentsEditWindowMinutes,
|
|
self::OrphanFilesDeleteAfterDays,
|
|
self::VirusScanMaxSizeMb,
|
|
self::VirusScannerWaitMinutes,
|
|
self::VirusScanExistingRatePerMinute,
|
|
self::PasswordMinLength => SettingType::Integer,
|
|
|
|
self::AdminNotificationEmails,
|
|
self::AllowedUploadExtensions,
|
|
self::EnabledLocales,
|
|
self::NewsItems => SettingType::Json,
|
|
};
|
|
}
|
|
|
|
// Widen this union as settings with new types are added — PHPStan
|
|
// enforces that it matches what the cases actually return.
|
|
/**
|
|
* @return string|bool|int|list<string>|list<array{title: string, date: string, content: string, link: string}>
|
|
*/
|
|
public function default(): string|bool|int|array
|
|
{
|
|
return match ($this) {
|
|
self::SiteName => 'ProjectSend',
|
|
self::TwoFactorEnforcement => 'none',
|
|
// 12 rather than Laravel's 8: this was the value security audit
|
|
// finding 14 settled on, and the setting exists so an
|
|
// installation can move it, not so an upgrade can lower it.
|
|
self::PasswordMinLength => 12,
|
|
self::PasswordRejectBreached => true,
|
|
self::ClientsCanSelectGroup => 'none',
|
|
self::CaptchaProvider => 'none',
|
|
// Inert without Capability::CaptchaManagedKeys, so this is the
|
|
// default only in the edition where it means anything. A cloud
|
|
// tenant is protected from the first request rather than after
|
|
// finding the screen.
|
|
self::CaptchaKeySource => 'managed',
|
|
|
|
self::ClientsCanRegister,
|
|
self::ClientsAutoApprove,
|
|
self::EmailNotificationsEnabled,
|
|
self::DiscourageSearchIndexing,
|
|
self::PublicListingEnabled,
|
|
self::ExpiredFilesAutoDeleteEnabled,
|
|
self::PublicCommentsEnabled,
|
|
// Off until somebody points it at a scanner, or a managed
|
|
// configuration names one — see ScanningConfig.
|
|
self::VirusScanningEnabled,
|
|
self::OrphanFilesAutoDeleteEnabled => false,
|
|
// Off, because switching it on is a change to how a library is
|
|
// laid out and that should be somebody's decision rather than
|
|
// something an upgrade did to them overnight.
|
|
self::ClientsHomeFolders => false,
|
|
|
|
self::CheckForUpdates,
|
|
self::FetchNews,
|
|
self::CommentsGuestModeration,
|
|
// On, so that an installation updating into these switches
|
|
// keeps the preview it already had rather than losing it to a
|
|
// setting nobody has seen yet.
|
|
self::ClientsCanPreviewFiles,
|
|
self::PublicListingPreviewEnabled,
|
|
// On by default, but only ever consulted once a provider is
|
|
// configured — so a fresh install is not protecting forms it
|
|
// has no keys for.
|
|
self::CaptchaOnLogin,
|
|
self::CaptchaOnRegistration,
|
|
self::CaptchaOnPasswordReset,
|
|
self::CaptchaOnPublicComments => true,
|
|
|
|
self::ClientsAutoGroup => 0,
|
|
self::ClientsMembershipDenyCooldownDays => 30,
|
|
self::ClientInvitationExpiryHours => 72,
|
|
self::MaxFileSizeMb => 2048,
|
|
self::MaxZipDownloadSizeMb => 2048,
|
|
self::DefaultClientStorageQuotaMb => 0,
|
|
self::AccountErasureGraceDays => 30,
|
|
self::AccountErasureReassignTo => 0,
|
|
self::ApiRequestLogRetentionDays => 30,
|
|
self::FailedJobRetentionDays => 30,
|
|
self::NotificationRetentionDays => 90,
|
|
self::ExpiredFilesDeleteAfterDays => 30,
|
|
self::OrphanFilesDeleteAfterDays => 30,
|
|
self::CommentsEditWindowMinutes => 15,
|
|
|
|
// Large enough for ordinary documents and archives, small
|
|
// enough that one upload does not hold the scanner for
|
|
// minutes. Must stay under clamd's own StreamMaxLength.
|
|
self::VirusScanMaxSizeMb => 512,
|
|
self::VirusScannerWaitMinutes => 10,
|
|
self::VirusScanExistingRatePerMinute => 60,
|
|
|
|
// Both of these let files through, which is the product
|
|
// owner's decision (2026-09-14): a scanner that cannot answer
|
|
// must not stop people working. What pays for it is
|
|
// visibility — every file allowed through this way is marked
|
|
// "not scanned", and the dashboard and projectsend:status both
|
|
// say so while it is happening.
|
|
self::VirusUnscannablePolicy,
|
|
self::VirusScannerDownPolicy => 'allow',
|
|
|
|
self::AdminNotificationEmails => [],
|
|
|
|
// English only out of the box: a fresh install offering
|
|
// sixteen languages nobody asked for is noise, and an
|
|
// operator who wants more turns them on deliberately.
|
|
self::EnabledLocales => ['en'],
|
|
|
|
self::UploadTypeRestriction => 'all',
|
|
self::DownloadIpLogging => 'all',
|
|
// Erasure removes a person's data, so deleting the files they
|
|
// uploaded is the sensible zero-config default; reassign is opt-in
|
|
// and needs a fallback account chosen.
|
|
self::AccountErasureContentAction => 'cascade_delete',
|
|
// Commenting is on for every file out of the box, but only
|
|
// between people who are logged in: reaching the public
|
|
// requires PublicCommentsEnabled, which is off by default.
|
|
self::CommentsScope => 'all',
|
|
self::CommentsAuthors => 'staff_and_clients',
|
|
self::PublicListingSlug => 'public',
|
|
// Empty means no scanner configured here. A managed
|
|
// configuration in the environment beats this when present —
|
|
// ScanningConfig, not this default, is what a caller asks.
|
|
self::VirusScannerAddress => '',
|
|
self::DefaultLocale => '',
|
|
self::Timezone => '',
|
|
self::Theme => 'default',
|
|
self::EmailTheme => 'default',
|
|
self::LatestKnownVersion => '',
|
|
self::LatestVersionCheckedAt => '',
|
|
self::LatestReleaseTitle => '',
|
|
self::LatestReleaseNotes => '',
|
|
self::LatestReleaseUrl => '',
|
|
self::LatestReleasePublishedAt => '',
|
|
// Empty means no update has ever been applied through the
|
|
// command — a fresh install, or one that predates it. Never a
|
|
// notice, in either direction.
|
|
self::AppliedVersion => '',
|
|
self::AppliedVersionAt => '',
|
|
self::UpdateWelcomeFrom => '',
|
|
self::UpdateWelcomeTo => '',
|
|
// False, so that an installation which updates into this
|
|
// feature is not welcomed to an installation it finished a
|
|
// year ago. Only creating the first administrator raises it.
|
|
self::GettingStartedPending => false,
|
|
self::NewsLastFetchedAt => '',
|
|
|
|
self::NewsItems => [],
|
|
|
|
// Documents, images, audio, video, archives. Deliberately
|
|
// excludes server-executable extensions (php, cgi, …) and
|
|
// browser-renderable-with-script types (htm, html, svg) —
|
|
// FileThumbnailController::preview() serves files inline
|
|
// using the stored mime type, so an allowed .html/.svg
|
|
// would execute script in the previewer's browser.
|
|
self::AllowedUploadExtensions => [
|
|
'pdf', 'doc', 'docx', 'docm', 'dot', 'dotx', 'rtf', 'txt', 'csv', 'odt', 'ott',
|
|
'ppt', 'pptx', 'pptm', 'pps', 'ppsx', 'odp',
|
|
'xls', 'xlsx', 'xlsm', 'xltx', 'ods',
|
|
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tif', 'tiff', 'webp', 'heic', 'psd', 'ai', 'eps',
|
|
'mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac',
|
|
'mp4', 'mov', 'avi', 'mpg', 'mpeg', 'webm', 'mkv', 'wmv',
|
|
'zip', 'rar', '7z', 'tar', 'gz', 'tgz', 'bz2', 'xz', 'iso',
|
|
],
|
|
};
|
|
}
|
|
}
|