Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,17 @@ yarn-error.log
/build
.DS_Store
/.phpunit.cache

# ai assistants
/.claude
/.codex
/.cursor
/.gemini
/.junie
/.windsurf
/.aider*
/.github/copilot-instructions.md
/.mcp.json
/AGENTS.md
/CLAUDE.md
/GEMINI.md
21 changes: 21 additions & 0 deletions config/zeus-bolt.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,33 @@

'defaultMailable' => FormSubmission::class,

/*
* uploads on a `public` disk are readable by anyone holding the URL.
* point this at a private S3 disk if your forms collect anything sensitive.
*/
'uploadDisk' => env('BOLT_FILESYSTEM_DISK', 'public'),

'uploadDirectory' => env('BOLT_FILESYSTEM_DIRECTORY', 'forms'),

'uploadVisibility' => env('BOLT_FILESYSTEM_VISIBILITY', 'public'),

/*
* the extensions the `file upload` field accepts. anything else is rejected server side.
* adding to this list is a security decision: executables (`php`, `cgi`, `sh`, `exe`)
* risk code execution, and markup (`svg`, `html`, `js`) risks stored xss.
*/
'uploadAcceptedFileTypes' => [
'jpg', 'jpeg', 'png', 'gif', 'webp',
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'txt', 'csv', 'zip',
],

/*
* the maximum size, in kilobytes, for every `file upload` field that does not set
* its own. leave null to let livewire's own upload limit govern instead.
*/
'uploadMaxSize' => null,

/*
* if you have installed Bolt Pro, you can enable the presets here
*/
Expand Down
9 changes: 9 additions & 0 deletions resources/lang/ar/forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@
'general' => 'خيارات عامة',
'color_type' => 'نوع اللون',
'allow_multiple' => 'السماح بمتعدد',
'accepted_file_types' => 'أنواع الملفات المسموح بها',
'accepted_file_types_helper' => 'اتركه فارغًا لقبول جميع أنواع الملفات المسموح بها في هذا الموقع.',
'max_size' => 'الحد الأقصى لحجم الملف',
'max_size_helper' => 'اختياري. اتركه فارغًا لاستخدام الحد الافتراضي للرفع في هذا الموقع.',
'max_size_unit' => 'وحدة الحجم',
'max_size_units' => [
'kb' => 'كيلوبايت',
'mb' => 'ميجابايت',
],
'is_inline' => 'في سطر واحد',
'more' => 'مزيد من خيارات الحقل',
'rows' => 'صفوف',
Expand Down
9 changes: 9 additions & 0 deletions resources/lang/en/forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@
'general' => 'General Options',
'color_type' => 'Color Type',
'allow_multiple' => 'Allow Multiple',
'accepted_file_types' => 'Accepted File Types',
'accepted_file_types_helper' => 'Leave empty to accept every file type allowed by this site.',
'max_size' => 'Max File Size',
'max_size_helper' => 'Optional. Leave empty to use this site\'s default upload limit.',
'max_size_unit' => 'Size Unit',
'max_size_units' => [
'kb' => 'KB',
'mb' => 'MB',
],
'is_inline' => 'Is inline',
'more' => 'More field options',
'rows' => 'Rows',
Expand Down
126 changes: 125 additions & 1 deletion src/Fields/Classes/FileUpload.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
namespace LaraZeus\Bolt\Fields\Classes;

use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Schemas\Components\Grid;
use Filament\Tables\Columns\Column;
use Illuminate\Support\Facades\Storage;
use LaraZeus\Accordion\Forms\Accordion;
Expand All @@ -12,6 +16,7 @@
use LaraZeus\Bolt\Fields\FieldsContract;
use LaraZeus\Bolt\Models\Field;
use LaraZeus\Bolt\Models\FieldResponse;
use Symfony\Component\Mime\MimeTypes;

class FileUpload extends FieldsContract
{
Expand All @@ -35,6 +40,27 @@ public static function getOptions(?array $sections = null): array
->schema([
Toggle::make('options.allow_multiple')
->label(__('zeus-bolt::forms.fields.options.allow_multiple')),
Select::make('options.accepted_file_types')
->label(__('zeus-bolt::forms.fields.options.accepted_file_types'))
->helperText(__('zeus-bolt::forms.fields.options.accepted_file_types_helper'))
->multiple()
->options(fn (): array => self::getAllowedExtensionOptions()),
Grid::make()
->schema([
TextInput::make('options.max_size')
->label(__('zeus-bolt::forms.fields.options.max_size'))
->helperText(__('zeus-bolt::forms.fields.options.max_size_helper'))
->numeric()
->minValue(1),
ToggleButtons::make('options.max_size_unit')
->label(__('zeus-bolt::forms.fields.options.max_size_unit'))
->options([
'kb' => __('zeus-bolt::forms.fields.options.max_size_units.kb'),
'mb' => __('zeus-bolt::forms.fields.options.max_size_units.mb'),
])
->default('kb')
->grouped(),
]),
self::isActive(),
self::required(),
self::columnSpanFull(),
Expand All @@ -60,9 +86,44 @@ public static function getOptionsHidden(): array
self::hiddenHiddenLabel(),
self::hiddenVisibility(),
Hidden::make('options.allow_multiple')->default(false),
Hidden::make('options.accepted_file_types')->default([]),
Hidden::make('options.max_size')->default(null),
Hidden::make('options.max_size_unit')->default('kb'),
];
}

/**
* The extensions this application allows to be uploaded.
*
* @return array<int, string>
*/
protected static function defaultAllowedExtensions(): array
{
$extensions = config('zeus-bolt.uploadAcceptedFileTypes');

if (! is_array($extensions)) {
return [];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to return null? that is the default value for acceptedFileTypes in filament Fileupload component

}

return array_values(array_unique(array_map(
fn (string $extension): string => strtolower(ltrim($extension, '.')),
$extensions
)));
}

/**
* The allow list as select options, keyed by extension so a field stores the
* extension itself rather than its position in the list.
*
* @return array<string, string>
*/
public static function getAllowedExtensionOptions(): array
{
$allowedExtensions = self::defaultAllowedExtensions();

return array_combine($allowedExtensions, $allowedExtensions);
}

public function getResponse(Field $field, FieldResponse $resp): string
{
$responseValue = filled($resp->response) ? Bolt::isJson($resp->response) ? json_decode($resp->response) : [$resp->response] : [];
Expand Down Expand Up @@ -91,14 +152,77 @@ public function appendFilamentComponentsOptions($component, $zeusField, bool $ha
{
parent::appendFilamentComponentsOptions($component, $zeusField, $hasVisibility);

$allowedExtensions = self::getFieldAllowedExtensions($zeusField);

$component->disk(config('zeus-bolt.uploadDisk'))
->directory(config('zeus-bolt.uploadDirectory'))
->visibility(config('zeus-bolt.uploadVisibility'));
->visibility(config('zeus-bolt.uploadVisibility'))
->acceptedFileTypes(self::getMimeTypesForExtensions($allowedExtensions))
->rules(['extensions:' . implode(',', $allowedExtensions)]);

/** An unset max size means no cap of ours; livewire still applies its own. */
if (($maxSizeInKilobytes = self::getFieldMaxSizeInKilobytes($zeusField)) > 0) {
$component->maxSize($maxSizeInKilobytes);
}

if (isset($zeusField->options['allow_multiple']) && $zeusField->options['allow_multiple']) {
$component = $component->multiple();
}

return $component;
}

/**
* The extensions picked for one field, intersected with the allow list so it can only narrow.
*
* @return array<int, string>
*/
protected static function getFieldAllowedExtensions(Field $zeusField): array
{
$configAllowedExtensions = self::defaultAllowedExtensions();
$fieldSelectedExtensions = $zeusField->options['accepted_file_types'] ?? [];

if (! is_array($fieldSelectedExtensions) || blank($fieldSelectedExtensions)) {
return $configAllowedExtensions;
}

return array_values(array_intersect($configAllowedExtensions, array_map(
fn (string $extension): string => strtolower(ltrim($extension, '.')),
$fieldSelectedExtensions
)));
}

/**
* The max size in kilobytes for one field: its own if it sets one, otherwise the
* configured default, otherwise zero to let livewire's limit govern the upload.
*/
protected static function getFieldMaxSizeInKilobytes(Field $zeusField): int
{
$fieldSelectedMaxSize = self::convertToKilobytes(
(int) ($zeusField->options['max_size'] ?? 0),
$zeusField->options['max_size_unit'] ?? null,
);

return ($fieldSelectedMaxSize > 0) ? $fieldSelectedMaxSize : (int) config('zeus-bolt.uploadMaxSize');
}

protected static function convertToKilobytes(int $size, ?string $unit): int
{
return ($unit === 'mb') ? $size * 1024 : $size;
}

/**
* The mime types Filament validates the uploaded content against.
*
* @param array<int, string> $extensions
* @return array<int, string>
*/
protected static function getMimeTypesForExtensions(array $extensions): array
{
$mimeTypes = MimeTypes::getDefault();

return array_values(array_unique(array_merge(
...array_map(fn (string $extension): array => $mimeTypes->getMimeTypes($extension), $extensions)
)));
}
}
1 change: 1 addition & 0 deletions src/Filament/Resources/CategoryResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public static function form(Schema $schema): Schema
->disk(config('zeus-bolt.uploadDisk'))
->directory(config('zeus-bolt.uploadDirectory'))
->visibility(config('zeus-bolt.uploadVisibility'))
->image()
->columnSpan(['sm' => 2])
->label(__('zeus-bolt::category.logo')),
]),
Expand Down
Loading
Loading