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'; // 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::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::EmailNotificationsEnabled, self::DiscourageSearchIndexing, self::PublicListingEnabled, self::ClientsCanPreviewFiles, self::PublicListingPreviewEnabled, self::CheckForUpdates, self::ExpiredFilesAutoDeleteEnabled, self::PublicCommentsEnabled, self::CommentsGuestModeration, self::OrphanFilesAutoDeleteEnabled, self::CaptchaOnLogin, self::CaptchaOnRegistration, self::CaptchaOnPasswordReset, self::CaptchaOnPublicComments, self::PasswordRejectBreached, self::GettingStartedPending => SettingType::Boolean, self::ClientsAutoGroup, self::ClientsMembershipDenyCooldownDays, self::MaxFileSizeMb, self::MaxZipDownloadSizeMb, self::DefaultClientStorageQuotaMb, self::AccountErasureGraceDays, self::AccountErasureReassignTo, self::ApiRequestLogRetentionDays, self::FailedJobRetentionDays, self::NotificationRetentionDays, self::ExpiredFilesDeleteAfterDays, self::CommentsEditWindowMinutes, self::OrphanFilesDeleteAfterDays, 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|list */ 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, self::OrphanFilesAutoDeleteEnabled => false, self::CheckForUpdates, 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::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, 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', 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', ], }; } }