mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-12 06:48:55 +00:00
4ce6793da9
Setup ended by handing somebody a login form and an empty dashboard. Everything this application can do was one menu away, and which menu was theirs to discover. The first time the administrator signs in to a new installation they now land on a short ordered list of what is worth doing first — add a client, upload a file, group the people who get the same things, choose how the file lists and the email look, point it at a mail server, add the team, check the scheduler — each a link straight to the screen that does it. The list is filtered twice, and both filters matter. By permission, because a link that answers 403 is worse than no link. And by edition: a managed installation is not sent off to configure a mail server somebody else runs, to create staff accounts that are not its to create, or to check a scheduler it does not host. Those three drop out on Cloud and the other five remain. Two steps tick themselves, because the database can answer them: a client exists, a file exists. Nothing else is checkable without guessing — a theme that was never changed looks exactly like one chosen deliberately — and a tick meaning "we assume so" is worse than no tick. The invitation to the Discord is at the very bottom, after the list. Somebody who has just installed this came with a job in mind, and opening with a social invitation is the fastest way to lose them. The marker is raised where a first administrator comes into existence — the setup screen and `projectsend:admin`, so a container provisioned from environment variables is welcomed too — and it is false by default, so an installation that updates into this feature is not congratulated on an install it finished a year ago. RedirectToWhatsNew becomes RedirectToGreeting and answers for both: they are the same interruption, and a second middleware on the same route would have to know about the first to avoid arguing with it. Installing wins; release notes for a version you never ran are the wrong greeting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
480 lines
23 KiB
PHP
480 lines
23 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';
|
|
|
|
// 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';
|
|
|
|
// 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 many days a self-deleted account is retained (soft-deleted)
|
|
// before PurgeErasuresCommand permanently erases it. Consumed by
|
|
// ProfileController.
|
|
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).
|
|
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 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::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::DefaultClientStorageQuotaMb,
|
|
self::AccountErasureGraceDays,
|
|
self::AccountErasureReassignTo,
|
|
self::ApiRequestLogRetentionDays,
|
|
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<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,
|
|
self::OrphanFilesAutoDeleteEnabled => false,
|
|
|
|
self::CheckForUpdates,
|
|
self::CommentsGuestModeration,
|
|
// 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::DefaultClientStorageQuotaMb => 0,
|
|
self::AccountErasureGraceDays => 30,
|
|
self::AccountErasureReassignTo => 0,
|
|
self::ApiRequestLogRetentionDays => 30,
|
|
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',
|
|
],
|
|
};
|
|
}
|
|
}
|