diff --git a/6.x/crud-fields.md b/6.x/crud-fields.md index 09b67aaf..0c4b2bc2 100644 --- a/6.x/crud-fields.md +++ b/6.x/crud-fields.md @@ -1025,9 +1025,11 @@ This helps you avoid most quirks when validating file uploads using Laravel's va use Backpack\CRUD\app\Library\Validation\Rules\ValidUpload; 'image' => ValidUpload::field('required') - ->file('file|mimes:jpeg,png,jpg,gif,svg|max:2048'), + ->file('file|mimes:jpeg,png,jpg,gif|max:2048'), ``` +**NOTE**: When using `withFiles`, files are also checked against a list of [allowed file types](/docs/{{version}}/crud-uploaders#allowed-file-types). File types that browsers render as pages, like `svg` and `html`, are not allowed by default. + Input preview: ![CRUD Field - upload](https://backpackforlaravel.com/uploads/docs-4-2/fields/upload.png) @@ -1076,9 +1078,11 @@ This will help you avoid most quirks of using Laravel's standard validation rule use Backpack\CRUD\app\Library\Validation\Rules\ValidUploadMultiple; 'photos' => ValidUploadMultiple::field('required|min:2|max:5') - ->file('file|mimes:jpeg,png,jpg,gif,svg|max:2048'), + ->file('file|mimes:jpeg,png,jpg,gif|max:2048'), ``` +**NOTE**: When using `withFiles`, files are also checked against a list of [allowed file types](/docs/{{version}}/crud-uploaders#allowed-file-types). File types that browsers render as pages, like `svg` and `html`, are not allowed by default. + **NOTE**: This field uses a `clear_{fieldName}` input to send the deleted files from the frontend to the backend. In case you are using `$guarded` add it there. Eg: `protected $guarded = ['id', 'clear_photos'];` @@ -1518,9 +1522,11 @@ Alternatively, you can manually implement the saving process yourself using mode use Backpack\Pro\Uploads\Validation\ValidDropzone; 'photos' => ValidDropzone::field('required|min:2|max:5') - ->file('file|mimes:jpeg,png,jpg,gif,svg|max:2048'), + ->file('file|mimes:jpeg,png,jpg,gif|max:2048'), ``` +**NOTE**: When using `withFiles`, files are also checked against a list of [allowed file types](/docs/{{version}}/crud-uploaders#allowed-file-types). File types that browsers render as pages, like `svg` and `html`, are not allowed by default. + **Step 5:** (optional) Configure the temp directory. Whenever new files are uploaded using the Dropzone operation, old files are automatically deleted from the temp directory. But you can also manually clean the temp directory. For more info and temp directory configuration options, see [this link](/docs/{{version}}/crud-how-to#configuring-the-temporary-directory). Input preview: diff --git a/6.x/crud-uploaders.md b/6.x/crud-uploaders.md index 2b49ef32..99a42197 100644 --- a/6.x/crud-uploaders.md +++ b/6.x/crud-uploaders.md @@ -54,7 +54,68 @@ When `temporaryUrl` is set to `true`, this configures the amount of time in minu - **`uploader`** - default: **null** This allows you to overwrite or set the uploader class for this field. You can use any class that implements `UploaderInterface`. - **`fileNamer`** - default: **null** -It accepts a `FileNameGeneratorInterface` instance or a closure. As the name implies, this will be used to generate the file name. Read more about in the [Naming uploaded files](#upload-name-files) section. +It accepts a `FileNameGeneratorInterface` instance or a closure. As the name implies, this will be used to generate the file name. Read more about in the [Naming uploaded files](#naming-files-when-using-uploaders) section. +- **`allowedExtensions`** - default: **null** (uses the `allowed_upload_extensions` config) +The extensions files uploaded to this field can be stored with. Read more about it in the [Allowed file types](#allowed-file-types) section. + + +### Allowed file types + +On top of your validation rules, Uploaders only store files whose extension is in an allow list (since `backpack/crud` 6.8.17). This protects your admins even when a field has no validation rules: a file that browsers render as a page (like `svg` or `html`) could run scripts in the session of the admin that opens it, when served from your application domain. + +With the default [file namer](#naming-files-when-using-uploaders), the extension a file is stored with comes from **its content**, not from the name the user sent. So a `payload.png` file that contains SVG markup is detected as `svg`, and rejected. When the content can't be identified, the file is stored with the `bin` extension. + +By default, the following extensions are allowed: +- **images:** `jpg`, `jpeg`, `png`, `gif`, `webp`, `avif`, `bmp`, `tif`, `tiff`, `ico`, `heic`, `heif` +- **documents:** `pdf`, `txt`, `csv`, `rtf`, `json`, `epub`, `doc`, `docx`, `xls`, `xlsx`, `ppt`, `pptx`, `odt`, `ods`, `odp` +- **archives:** `zip`, `rar`, `7z`, `gz`, `tgz`, `tar`, `bz2` +- **audio:** `mp3`, `wav`, `ogg`, `oga`, `opus`, `m4a`, `aac`, `flac`, `weba` +- **video:** `mp4`, `m4v`, `webm`, `mov`, `avi`, `mkv`, `mpeg`, `mpg`, `ogv`, `3gp` +- **unidentified content:** `bin` + +Extensions that web servers may execute (`php`, `phtml`, `phar`, `shtml`, `pl`, `py`, `cgi`, `asp`, `jsp`, `sh`, `exe`, `htaccess` and similar) are **always** rejected, even if you add them to the allow list. They are checked in every part of the file name, so a name like `shell.php.jpg` is rejected too. + +**Changing the allowed extensions for all fields** + +Set the `allowed_upload_extensions` key in your `config/backpack/crud.php` file: + +```php +use Backpack\CRUD\app\Library\Uploaders\Support\FileExtensions; + +'allowed_upload_extensions' => [...FileExtensions::DEFAULT_ALLOWED, 'dwg'], +``` + +If you published the config file before this option existed, you don't need to add it - Backpack will use the default list. + +**Changing the allowed extensions for one field** + +Pass `allowedExtensions` to the uploader configuration. It replaces the list from the config for that field, so you can use it both to restrict and to extend what is allowed: + +```php +use Backpack\CRUD\app\Library\Uploaders\Support\FileExtensions; + +// only accept pdfs +CRUD::field('invoice')->type('upload')->withFiles([ + 'allowedExtensions' => ['pdf'], +]); + +// the default extensions, plus svg +CRUD::field('logo')->type('upload')->withFiles([ + 'allowedExtensions' => [...FileExtensions::DEFAULT_ALLOWED, 'svg'], +]); +``` + +> **IMPORTANT**: Only allow `svg`, `html`, `xml` or other file types that browsers can render when you trust everyone who can upload to that field, or when the files are served from a different domain than your admin panel (eg. a cloud disk). Otherwise, a crafted file can run scripts in the session of the admin who opens it. + +**What happens when a file is not allowed** + +The form is returned with a validation error for that field, just like any other validation rule. For fields inside a `repeatable` or a relationship, the error is reported on the subfield of the row that sent the file (eg. `gallery.2.photos`). + +All the files sent in the form are checked **before** any uploader stores or deletes files. So if one file is rejected, nothing is changed: the new files are not stored and the previous files of the entry are kept. This applies to all fields in the form, including fields inside repeatables and relationships, and to the [Spatie MediaLibrary uploaders](#spatie-media-library). + +The `dropzone` field (PRO) checks the files when they are sent to the dropzone upload endpoint, so files that are not allowed never reach the temporary folder, and the error is shown in the field. That endpoint uses the options of fields defined with the array syntax (`'withFiles' => [...]`); for fields defined with `->withFiles([...])` it uses the default file namer and the `allowed_upload_extensions` config. + +> **NOTE**: The allow list is a safety net, not a replacement for validation. You should still validate each upload field with the file types it expects, using the [Custom Validation Rules](https://backpackforlaravel.com/docs/{{version}}/custom-validation-rules). ### Handling uploads in relationship fields @@ -116,8 +177,8 @@ public function categories() { ### Naming files when using Uploaders Backpack provides a naming strategy for uploaded files that works well for most scenarios: -- For `upload`, `upload_multiple` and `dropzone` fields, the file name will be the original file name slugged and with a random 4 character string appended to it, to avoid name collisions. Eg: `my file.pdf` becomes `my-file-aY5x.pdf`. -- For `image` it will generate a unique name for the file, and will keep the original extension. Eg: `my file.jpg` becomes `5f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c.jpg`. +- For `upload`, `upload_multiple` and `dropzone` fields, the file name will be the original file name slugged and with a random 4 character string appended to it, to avoid name collisions. The extension is detected from the file content. Eg: `my file.pdf` becomes `my-file-aY5x.pdf`. +- For `image` it will generate a random name for the file, with the extension of the image type. Eg: `5f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c.jpeg`. Only `jpeg`, `png`, `gif`, `webp` and `avif` images are accepted, and the image content must match its type. You can customize the naming strategy by creating a class that implements `FileNameGeneratorInterface` and pass it to the upload configuration (the default used by Backpack). @@ -131,6 +192,13 @@ CRUD::field('avatar')->type('upload')->withFiles([ 'fileNamer' => function($file, $uploader) { return 'the_file_name.png'; }, ]) ``` + +The names returned by your file namer must end with an [allowed extension](#allowed-file-types), otherwise the file is rejected. Keep in mind that the original file name (`$file->getClientOriginalName()`) is sent by the user: if you use it, the extension the user picked is the one that gets checked, not the file content. Names without an extension are rejected too. + +The default name generator can be changed for all fields in the `file_name_generator` key of your `config/backpack/crud.php` file. + +> **NOTE**: If you create your own Uploader class, always name the files you store with `$this->getFileName($file)`: besides calling the configured `fileNamer`, it makes sure the file type is allowed. Call it for all new files **before** deleting or replacing any previous file, so a rejected upload doesn't remove the files the entry already has. + ### Subfields in Uploaders diff --git a/6.x/custom-validation-rules.md b/6.x/custom-validation-rules.md index 86cfd6b9..0667fc6e 100644 --- a/6.x/custom-validation-rules.md +++ b/6.x/custom-validation-rules.md @@ -7,6 +7,8 @@ Some Backpack fields are more difficult to validate using standard Laravel validation rules. So we've created a few custom validation rules, that will make validation them dead-simple. +> **NOTE**: When the field uses `withFiles` or `withMedia`, the uploaded files are also checked against a list of [allowed file types](/docs/{{version}}/crud-uploaders#allowed-file-types), after your validation rules pass. If your rules accept a type that is not in that list (eg. `mimes:svg`), you also need to allow its extension in the uploader configuration. + ## `ValidUpload` for `upload` field type diff --git a/7.x/crud-fields.md b/7.x/crud-fields.md index d11d4157..7761685c 100644 --- a/7.x/crud-fields.md +++ b/7.x/crud-fields.md @@ -895,6 +895,18 @@ CRUD::field([ ]); ``` +> **Multiple locales** - Summernote editor supports multiple languages. The field automatically loads the language file that matches your app locale (ex: `es` → `es-ES`, `pt_BR` → `pt-BR`), and you can force a specific one using the `summernoteLocale` attribute (`'summernoteLocale' => 'pt-BR'`). Because the locale file is loaded dynamically, `php artisan basset:cache` cannot discover it - to make sure it gets pre-loaded, add the needed locale files to the `basset_preload` config in `config/backpack/ui.php`: + +```php +'basset_preload' => [ + 'scripts' => [ + 'https://cdn.jsdelivr.net/npm/summernote@0.9.1/dist/lang/summernote-es-ES.min.js', + 'https://cdn.jsdelivr.net/npm/summernote@0.9.1/dist/lang/summernote-pt-BR.min.js', + ], + 'styles' => [], +], +``` + > NOTE: Summernote does NOT sanitize the input. If you do not trust the users of this field, you should sanitize the input or output using something like HTML Purifier. Personally we like to use install [mewebstudio/Purifier](https://github.com/mewebstudio/Purifier) and add an [accessor or mutator](https://laravel.com/docs/8.x/eloquent-mutators#accessors-and-mutators) on the Model, so that wherever the model is created from (admin panel or app), the output will always be clean. [Example here](https://github.com/Laravel-Backpack/demo/commit/7342cffb418bb568b9e4ee279859685ddc0456c1). #### Uploading files with summernote @@ -1043,9 +1055,11 @@ This helps you avoid most quirks when validating file uploads using Laravel's va use Backpack\CRUD\app\Library\Validation\Rules\ValidUpload; 'image' => ValidUpload::field('required') - ->file('file|mimes:jpeg,png,jpg,gif,svg|max:2048'), + ->file('file|mimes:jpeg,png,jpg,gif|max:2048'), ``` +**NOTE**: When using `withFiles`, files are also checked against a list of [allowed file types](/docs/{{version}}/crud-uploaders#allowed-file-types). File types that browsers render as pages, like `svg` and `html`, are not allowed by default. + Input preview: ![CRUD Field - upload](https://backpackforlaravel.com/uploads/docs-4-2/fields/upload.png) @@ -1094,9 +1108,11 @@ This will help you avoid most quirks of using Laravel's standard validation rule use Backpack\CRUD\app\Library\Validation\Rules\ValidUploadMultiple; 'photos' => ValidUploadMultiple::field('required|min:2|max:5') - ->file('file|mimes:jpeg,png,jpg,gif,svg|max:2048'), + ->file('file|mimes:jpeg,png,jpg,gif|max:2048'), ``` +**NOTE**: When using `withFiles`, files are also checked against a list of [allowed file types](/docs/{{version}}/crud-uploaders#allowed-file-types). File types that browsers render as pages, like `svg` and `html`, are not allowed by default. + **NOTE**: This field uses a `clear_{fieldName}` input to send the deleted files from the frontend to the backend. In case you are using `$guarded` add it there. Eg: `protected $guarded = ['id', 'clear_photos'];` @@ -1332,6 +1348,13 @@ To customize, publish the lang files and edit the `date_time` array. Any option from [air-datepicker's documentation](https://air-datepicker.com/docs) can be passed inside the `air-datepicker` config array. +To control the picker from JavaScript (eg. link two pickers so that one excludes the dates +of the other), see [How to link two air-datepicker fields](/docs/{{version}}/crud-how-to#how-to-link-two-air-datepicker-fields). + +Input preview: + +![CRUD Field - Air Datepicker](https://backpackforlaravel.com/uploads/docs/fields/air-datepicker.jpeg) +
@@ -1494,9 +1517,11 @@ Alternatively, you can manually implement the saving process yourself using mode use Backpack\Pro\Uploads\Validation\ValidDropzone; 'photos' => ValidDropzone::field('required|min:2|max:5') - ->file('file|mimes:jpeg,png,jpg,gif,svg|max:2048'), + ->file('file|mimes:jpeg,png,jpg,gif|max:2048'), ``` +**NOTE**: When using `withFiles`, files are also checked against a list of [allowed file types](/docs/{{version}}/crud-uploaders#allowed-file-types). File types that browsers render as pages, like `svg` and `html`, are not allowed by default. + Input preview: ![CRUD Field - dropzone](https://user-images.githubusercontent.com/7188159/236273902-ca7fb5a5-e7ce-4a03-91a7-2af81598331c.png) @@ -1533,7 +1558,7 @@ Starting Backpack 6.7 you can now upload images using drag & drop directly into **Step 1:** Add the `AjaxUploadOperation` to your `EntityCrudController` where you defined your easyMDE field. **Step 2:** Add the `withFiles => true` attribute to your field definition. You can check other available options in the [uploaders documentation](https://backpackforlaravel.com/docs/crud-uploaders). -**Note:** EasyMDE provides some basic javascript file validation. By default only `jpg, jpeg, png, gif, svg, webp` are allowed and files up to 2MB. You can change this by setting the `imageMaxSize` and `imageAccept` options in the `easymdeAttributes` attribute. Eg: +**Note:** EasyMDE provides some basic javascript file validation. By default only `jpg, jpeg, png, gif, svg, webp` are allowed and files up to 2MB. This is only a convenience for the user: the server still validates the files with `ValidEasyMDE` (by default, `jpg` and `png` images up to 1MB) and only stores [allowed file types](/docs/{{version}}/crud-uploaders#allowed-file-types), so `svg` images are rejected unless you allow them. You can change the javascript validation by setting the `imageMaxSize` and `imageAccept` options in the `easymdeAttributes` attribute. Eg: ```php 'easymdeAttributes' => [ diff --git a/7.x/crud-filters.md b/7.x/crud-filters.md index 86f64aca..c5117490 100644 --- a/7.x/crud-filters.md +++ b/7.x/crud-filters.md @@ -254,6 +254,33 @@ CRUD::filter('birthday') }); ``` +#### Format + +By default, both the datepicker input and the filter badge use the global `backpack.ui.default_date_format` config value (`D MMM YYYY` by default). To use a different format, chain `format()` with [Moment.js format tokens](https://momentjs.com/docs/#/displaying/format/): + +```php +CRUD::filter('birthday') + ->type('date') + ->format('DD/MM/YYYY') // datepicker input + badge format + ->whenActive(function($value) { + // even with a custom format, the value is always in raw format: Y-m-d + CRUD::addClause('where', 'date', $value); + }); +``` + +With the example above, picking `2026-01-05` would show `05/01/2026` in the datepicker input and in the [filter badge](#filter-value-badges), while the URL parameter and the `$value` received by `whenActive()` stay `2026-01-05`. If you enable badges, you might also want a custom badge label: + +```php +CRUD::filter('birthday') + ->type('date') + ->format('DD/MM/YYYY') + ->showFilterValues() + ->filterValuesLabel('Born on :value') // "Born on 05/01/2026" + ->whenActive(function($value) { + CRUD::addClause('where', 'date', $value); + }); +``` +
@@ -277,6 +304,43 @@ CRUD::filter('from_to') }); ``` +#### Format + +The `locale.format` option (Moment.js tokens, see [daterangepicker options](https://www.daterangepicker.com/#options)) controls both the picker display and the [filter badge](#filter-value-badges): + +```php +CRUD::filter('from_to') + ->type('date_range') + ->date_range_options([ + 'locale' => ['format' => 'DD/MM/YYYY'], // picker + badge format + ]) + ->showFilterValues() + ->whenActive(function($value) { + $dates = json_decode($value); // always in raw format: Y-m-d H:i:s + CRUD::addClause('where', 'date', '>=', $dates->from); + CRUD::addClause('where', 'date', '<=', $dates->to); + }); +``` + +The badge will show something like `From To: 01/01/2026 → 31/01/2026`. If `timePicker` is enabled, times are included in the badge, using the same format: + +```php +CRUD::filter('from_to') + ->type('date_range') + ->date_range_options([ + 'timePicker' => true, + 'locale' => ['format' => 'DD/MM/YYYY HH:mm'], + ]) + // badge: "01/01/2026 00:00 → 31/01/2026 23:59" + ->whenActive(function($value) { + $dates = json_decode($value); + CRUD::addClause('where', 'date', '>=', $dates->from); + CRUD::addClause('where', 'date', '<=', $dates->to); + }); +``` + +The URL parameter and the `$value` passed to `whenActive()` always stay in the raw `Y-m-d H:i:s` format - the format option only changes how values are displayed. +
@@ -639,6 +703,36 @@ For `select2_ajax` filters, the badge automatically shows the human-readable tex > **Note:** Filter badges are disabled by default to avoid breaking existing layouts. Enable them explicitly at any level. +### Customizing the Display Value for Custom Filter Types + +For the built-in filter types, Backpack automatically computes a readable display value for badges (labels for `dropdown`/`select2`, ranges joined with `→`, etc.). The `date` and `date_range` filters also apply their user-defined formats, as shown above. + +If you create a [custom filter type](#creating-a-custom-filter-type) and want to control what its badge displays, register a **badge display value formatter** on the filter's `
  • ` element, using `data-badge-display-value-formatter`: + +```blade +{{-- resources/views/vendor/backpack/crud/filters/my_custom_filter.blade.php --}} +
  • + ... +
  • + +@push('after_scripts') + +@endpush +``` + +The formatter only affects the badge - the raw value stored in the URL parameter and passed to `whenActive()` is not modified. This is how the built-in `date` and `date_range` filters apply their user-defined formats to badges. + ## Examples diff --git a/7.x/crud-how-to.md b/7.x/crud-how-to.md index 1eed1502..504c9e7f 100644 --- a/7.x/crud-how-to.md +++ b/7.x/crud-how-to.md @@ -706,6 +706,99 @@ CRUD::field([ - We do not provide an interface to edit this relationship. We never needed it, nobody ever asked for it and it would be very difficult to create an interface that is easy-to-use and easy-to-understand for the admin. If you find yourself needing this, please let us know by opening an issue on GitHub. + +### How to link two air-datepicker fields (PRO) + +The `air-datepicker` field stores its air-datepicker instance on the visible input, and provides one JS helper to reach it: + +- `bpFieldAirDatepickerInstance(fieldName)` - returns the air-datepicker instance of a field, by field name (or `false`). With the instance you can use the [full air-datepicker API](https://air-datepicker.com/docs): `dp.update()`, `dp.selectDate()`, `dp.clear()`, `dp.onSelect`, etc. + +For example, let's prevent `end_date` from ever being before `start_date`, in a Create/Update form that has two single `air-datepicker` fields: + +```php +CRUD::field([ + 'name' => 'start_date', + 'label' => 'From date', + 'type' => 'air-datepicker', +]); + +CRUD::field([ + 'name' => 'end_date', + 'label' => 'To date', + 'type' => 'air-datepicker', +]); +``` + +Then add a script to the operation: + +```php +// in your CrudController, inside setupCreateOperation() and/or setupUpdateOperation() +Widget::add()->type('script')->content('assets/js/admin/forms/link-dates.js'); +``` + +```js +// assets/js/admin/forms/link-dates.js +$(function () { + // Backpack initializes its fields inside its own document-ready handler, + // registered after this one — defer with a macrotask so the instances + // already exist. + setTimeout(linkDates, 0); +}); + +function linkDates() { + var startDp = bpFieldAirDatepickerInstance('start_date'); + var endDp = bpFieldAirDatepickerInstance('end_date'); + + if (!startDp || !endDp) return; + + // What minDate/maxDate each picker was configured with in PHP ('' by default). + var startDefaultMaxDate = startDp.opts.maxDate; + var endDefaultMinDate = endDp.opts.minDate; + + // One way: "To date" can't be before "From date"... + linkAirDatepickers(startDp, endDp, 'minDate', endDefaultMinDate); + // ...and the other way: "From date" can't be after "To date". + linkAirDatepickers(endDp, startDp, 'maxDate', startDefaultMaxDate); +} + +function linkAirDatepickers(sourceDp, targetDp, limit, targetDefaultLimit) { + var originalOnSelect = sourceDp.opts.onSelect; // the field's own handler + + sourceDp.opts.onSelect = function (args) { + // keep the field's built-in behavior (updates the hidden input) + originalOnSelect.apply(this, arguments); + + if (args.date) { + targetDp.update({ [limit]: args.date }); + + // optional: clear the target if its current selection is now invalid + var targetSelected = targetDp.selectedDates[0]; + if (targetSelected && ( + (limit === 'minDate' && targetSelected.getTime() < args.date.getTime()) || + (limit === 'maxDate' && targetSelected.getTime() > args.date.getTime()) + )) { + targetDp.clear(); + } + } else { + // source cleared: restore the originally configured limit. + // NOTE: passing null throws in air-datepicker 3.6.0 — restore '' + // (or the original value) instead. + targetDp.update({ [limit]: targetDefaultLimit || '' }); + } + }; + + // also apply the link on page load, when editing an entry that already + // has a source value + var initialSource = sourceDp.selectedDates[0]; + if (initialSource) { + targetDp.update({ [limit]: initialSource }); + } +} +``` + + +See the field docs [here](/docs/{{version}}/crud-fields#air-datepicker). + ## Operations diff --git a/7.x/crud-operation-ajax-upload.md b/7.x/crud-operation-ajax-upload.md index 5e05eb59..cbbfa336 100644 --- a/7.x/crud-operation-ajax-upload.md +++ b/7.x/crud-operation-ajax-upload.md @@ -16,6 +16,8 @@ All Backpack fields that allow the user to upload files have their correspondent This enpoint comes with a default security configuration, only allow the upload of `jpg/jpeg/png/gif` images with a maximum size of 1MB. To overwrite this configuration, you need to validate your field using the previously mentioned rules that apply for your field, for example `ValidDropzone` or `ValidEasyMDE`. +After validation, the endpoint also checks the uploaded files against the [allowed file types](https://backpackforlaravel.com/docs/{{version}}/crud-uploaders#allowed-file-types) of the field. If any file is not allowed, none of the files in the request are stored, and a `422` response is returned with the error for the field. + ## How to Use diff --git a/7.x/crud-uploaders.md b/7.x/crud-uploaders.md index 0108d17b..24236da9 100644 --- a/7.x/crud-uploaders.md +++ b/7.x/crud-uploaders.md @@ -54,12 +54,73 @@ When `temporaryUrl` is set to `true`, this configures the amount of time in minu - **`uploader`** - default: **null** This allows you to overwrite or set the uploader class for this field. You can use any class that implements `UploaderInterface`. - **`fileNamer`** - default: **null** -It accepts a `FileNameGeneratorInterface` instance or a closure. As the name implies, this will be used to generate the file name. Read more about in the [Naming uploaded files](#upload-name-files) section. +It accepts a `FileNameGeneratorInterface` instance or a closure. As the name implies, this will be used to generate the file name. Read more about in the [Naming uploaded files](#naming-files-when-using-uploaders) section. +- **`allowedExtensions`** - default: **null** (uses the `allowed_upload_extensions` config) +The extensions files uploaded to this field can be stored with. Read more about it in the [Allowed file types](#allowed-file-types) section. ### Upload Validation -We can't stress enough how **IMPORTANT** is to properly validate and autenticate the file uploads and the upload endpoints. We have created a set of custom validation rules that will make validation of upload fields dead-simple. Please see the [Custom Validation Rules](https://backpackforlaravel/docs/custom-validation-rules) section for more information. +We can't stress enough how **IMPORTANT** is to properly validate and autenticate the file uploads and the upload endpoints. We have created a set of custom validation rules that will make validation of upload fields dead-simple. Please see the [Custom Validation Rules](https://backpackforlaravel.com/docs/{{version}}/custom-validation-rules) section for more information. + + +### Allowed file types + +On top of your validation rules, Uploaders only store files whose extension is in an allow list. This protects your admins even when a field has no validation rules: a file that browsers render as a page (like `svg` or `html`) could run scripts in the session of the admin that opens it, when served from your application domain. + +With the default [file namer](#naming-files-when-using-uploaders), the extension a file is stored with comes from **its content**, not from the name the user sent. So a `payload.png` file that contains SVG markup is detected as `svg`, and rejected. When the content can't be identified, the file is stored with the `bin` extension. + +By default, the following extensions are allowed: +- **images:** `jpg`, `jpeg`, `png`, `gif`, `webp`, `avif`, `bmp`, `tif`, `tiff`, `ico`, `heic`, `heif` +- **documents:** `pdf`, `txt`, `csv`, `rtf`, `json`, `epub`, `doc`, `docx`, `xls`, `xlsx`, `ppt`, `pptx`, `odt`, `ods`, `odp` +- **archives:** `zip`, `rar`, `7z`, `gz`, `tgz`, `tar`, `bz2` +- **audio:** `mp3`, `wav`, `ogg`, `oga`, `opus`, `m4a`, `aac`, `flac`, `weba` +- **video:** `mp4`, `m4v`, `webm`, `mov`, `avi`, `mkv`, `mpeg`, `mpg`, `ogv`, `3gp` +- **unidentified content:** `bin` + +Extensions that web servers may execute (`php`, `phtml`, `phar`, `shtml`, `pl`, `py`, `cgi`, `asp`, `jsp`, `sh`, `exe`, `htaccess` and similar) are **always** rejected, even if you add them to the allow list. They are checked in every part of the file name, so a name like `shell.php.jpg` is rejected too. + +**Changing the allowed extensions for all fields** + +Set the `allowed_upload_extensions` key in your `config/backpack/crud.php` file: + +```php +use Backpack\CRUD\app\Library\Uploaders\Support\FileExtensions; + +'allowed_upload_extensions' => [...FileExtensions::DEFAULT_ALLOWED, 'dwg'], +``` + +If you published the config file before this option existed, you don't need to add it - Backpack will use the default list. + +**Changing the allowed extensions for one field** + +Pass `allowedExtensions` to the uploader configuration. It replaces the list from the config for that field, so you can use it both to restrict and to extend what is allowed: + +```php +use Backpack\CRUD\app\Library\Uploaders\Support\FileExtensions; + +// only accept pdfs +CRUD::field('invoice')->type('upload')->withFiles([ + 'allowedExtensions' => ['pdf'], +]); + +// the default extensions, plus svg +CRUD::field('logo')->type('upload')->withFiles([ + 'allowedExtensions' => [...FileExtensions::DEFAULT_ALLOWED, 'svg'], +]); +``` + +> **IMPORTANT**: Only allow `svg`, `html`, `xml` or other file types that browsers can render when you trust everyone who can upload to that field, or when the files are served from a different domain than your admin panel (eg. a cloud disk). Otherwise, a crafted file can run scripts in the session of the admin who opens it. + +**What happens when a file is not allowed** + +The form is returned with a validation error for that field, just like any other validation rule. For fields inside a `repeatable` or a relationship, the error is reported on the subfield of the row that sent the file (eg. `gallery.2.photos`). + +All the files sent in the form are checked **before** any uploader stores or deletes files. So if one file is rejected, nothing is changed: the new files are not stored and the previous files of the entry are kept. This applies to all fields in the form, including fields inside repeatables and relationships, and to the [Spatie MediaLibrary uploaders](#spatie-media-library). + +Ajax uploaders (`dropzone`, `easymde`, `summernote`) check the files when they are sent to the [AjaxUpload endpoint](https://backpackforlaravel.com/docs/{{version}}/crud-operation-ajax-upload), so files that are not allowed never reach the temporary folder. The error is returned in the endpoint response and shown in the field. + +> **NOTE**: The allow list is a safety net, not a replacement for validation. You should still validate each upload field with the file types it expects, using the [Custom Validation Rules](https://backpackforlaravel.com/docs/{{version}}/custom-validation-rules). ## Available Uploaders @@ -153,6 +214,8 @@ Notice this custom class you're creating is extending `Backpack\CRUD\app\Library **`getUploadedFilesFromRequest`** - this is the method that will be called to get the values sent in the request. Some uploaders require you get the `->files()` others the `->input()`. By default it returns the `->files()`. +> **IMPORTANT**: Always name the files you store with **`$this->getFileName($file)`**. Besides calling the configured `fileNamer`, it makes sure the file type is [allowed](#allowed-file-types), and throws a `ValidationException` when it's not. Call it for all new files **before** deleting or replacing any previous file, so a rejected upload doesn't remove the files the entry already has. Uploaders that extend `Uploader` also get the uploaded files (`UploadedFile` instances) checked before any uploader in the form runs, but files your uploader gets in other ways (eg. base64 strings) are only checked when you call `getFileName()`. + This is the implementation of those methods in `SingleFile` uploader: ```php protected function shouldKeepPreviousValueUnchanged(Model $entry, $entryValue): bool @@ -292,8 +355,8 @@ public function categories() { ### Naming files when using Uploaders Backpack provides a naming strategy for uploaded files that works well for most scenarios: -- For `upload`, `upload_multiple` and `dropzone` fields, the file name will be the original file name slugged and with a random 4 character string appended to it, to avoid name collisions. Eg: `my file.pdf` becomes `my-file-aY5x.pdf`. -- For `image` it will generate a unique name for the file, and will keep the original extension. Eg: `my file.jpg` becomes `5f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c.jpg`. +- For `upload`, `upload_multiple` and `dropzone` fields, the file name will be the original file name slugged and with a random 4 character string appended to it, to avoid name collisions. The extension is detected from the file content. Eg: `my file.pdf` becomes `my-file-aY5x.pdf`. +- For `image` it will generate a random name for the file, with the extension of the image type. Eg: `5f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c.jpeg`. Only `jpeg`, `png`, `gif`, `webp` and `avif` images are accepted, and the image content must match its type. You can customize the naming strategy by creating a class that implements `FileNameGeneratorInterface` and pass it to the upload configuration (the default used by Backpack). @@ -307,6 +370,10 @@ CRUD::field('avatar')->type('upload')->withFiles([ 'fileNamer' => function($file, $uploader) { return 'the_file_name.png'; }, ]) ``` + +The names returned by your file namer must end with an [allowed extension](#allowed-file-types), otherwise the file is rejected. Keep in mind that the original file name (`$file->getClientOriginalName()`) is sent by the user: if you use it, the extension the user picked is the one that gets checked, not the file content. Names without an extension are rejected too. + +The default name generator can be changed for all fields in the `file_name_generator` key of your `config/backpack/crud.php` file. ### Subfields in Uploaders diff --git a/7.x/custom-validation-rules.md b/7.x/custom-validation-rules.md index 326b174d..5f2a4410 100644 --- a/7.x/custom-validation-rules.md +++ b/7.x/custom-validation-rules.md @@ -7,6 +7,8 @@ Some Backpack fields are more difficult to validate using standard Laravel validation rules. So we've created a few custom validation rules, that will make validation them dead-simple. +> **NOTE**: When the field uses `withFiles` or `withMedia`, the uploaded files are also checked against a list of [allowed file types](/docs/{{version}}/crud-uploaders#allowed-file-types), after your validation rules pass. If your rules accept a type that is not in that list (eg. `mimes:svg`), you also need to allow its extension in the uploader configuration. + ## `ValidUpload` for `upload` field type diff --git a/7.x/how-to-link-two-air-datepicker-fields.md b/7.x/how-to-link-two-air-datepicker-fields.md new file mode 100644 index 00000000..4b85b920 --- /dev/null +++ b/7.x/how-to-link-two-air-datepicker-fields.md @@ -0,0 +1,137 @@ +# How to link two air-datepicker fields + +--- + + +## About + +The [`air-datepicker`](/docs/{{version}}/crud-fields#air-datepicker) field is a Backpack PRO field type, powered by [air-datepicker](https://air-datepicker.com/). It can work as a date picker, datetime picker or range picker. + +Sometimes you want **two separate pickers** that depend on each other - for example a *From date* and a *To date*, where the *To date* should never be before the *From date*. Instead of letting the admin pick invalid dates and catching it in validation, you can make the second picker simply **disable** the invalid dates. + +All it takes is a small JavaScript file. This page shows how. + + +## Step 1. The fields + +Add two single `air-datepicker` fields to your Create and Update operations: + +```php +CRUD::field([ + 'name' => 'start_date', + 'label' => 'From date', + 'type' => 'air-datepicker', +]); + +CRUD::field([ + 'name' => 'end_date', + 'label' => 'To date', + 'type' => 'air-datepicker', +]); +``` + + +## Step 2. Add the script to the operation + +In your CrudController, inside `setupCreateOperation()` and/or `setupUpdateOperation()`, load a JavaScript file using a `script` widget: + +```php +use Backpack\CRUD\app\Library\Widget; + +Widget::add()->type('script')->content('assets/js/admin/forms/link-dates.js'); +``` + + +## Step 3. The JavaScript + +Create the JS file and link the two pickers: + +```js +// assets/js/admin/forms/link-dates.js +$(function () { + // Backpack initializes its fields inside its own document-ready handler, + // registered after this one - defer with a macrotask so the instances + // already exist. + setTimeout(linkDates, 0); +}); + +function linkDates() { + var startDp = bpFieldAirDatepickerInstance('start_date'); + var endDp = bpFieldAirDatepickerInstance('end_date'); + + if (!startDp || !endDp) return; + + // What minDate/maxDate each picker was configured with in PHP ('' by default). + var startDefaultMaxDate = startDp.opts.maxDate; + var endDefaultMinDate = endDp.opts.minDate; + + // One way: "To date" can't be before "From date"... + linkAirDatepickers(startDp, endDp, 'minDate', endDefaultMinDate); + // ...and the other way: "From date" can't be after "To date". + linkAirDatepickers(endDp, startDp, 'maxDate', startDefaultMaxDate); +} + +function linkAirDatepickers(sourceDp, targetDp, limit, targetDefaultLimit) { + var originalOnSelect = sourceDp.opts.onSelect; // the field's own handler + + sourceDp.opts.onSelect = function (args) { + // keep the field's built-in behavior (updates the hidden input) + originalOnSelect.apply(this, arguments); + + if (args.date) { + targetDp.update({ [limit]: args.date }); + + // optional: clear the target if its current selection is now invalid + var targetSelected = targetDp.selectedDates[0]; + if (targetSelected && ( + (limit === 'minDate' && targetSelected.getTime() < args.date.getTime()) || + (limit === 'maxDate' && targetSelected.getTime() > args.date.getTime()) + )) { + targetDp.clear(); + } + } else { + // source cleared: restore the originally configured limit. + // NOTE: passing null throws in air-datepicker 3.6.0 - restore '' + // (or the original value) instead. + targetDp.update({ [limit]: targetDefaultLimit || '' }); + } + }; + + // also apply the link on page load, when editing an entry that already + // has a source value + var initialSource = sourceDp.selectedDates[0]; + if (initialSource) { + targetDp.update({ [limit]: initialSource }); + } +} +``` + + +## How it works + +- The `air-datepicker` field stores its air-datepicker instance on the visible input, and provides one JS helper to reach it - `bpFieldAirDatepickerInstance(fieldName)`, which returns the instance (or `false`). With the instance you can use the [full air-datepicker API](https://air-datepicker.com/docs): `dp.update()`, `dp.selectDate()`, `dp.clear()`, `dp.onSelect`, etc. +- We wrap the source picker's `onSelect` callback, being careful to call the field's original handler first, so the hidden input keeps getting updated as usual. +- When the source date is picked, we update the target's `minDate` or `maxDate`. When the source is cleared, we restore the limit it was configured with in PHP. + + +## Notes + +- `onSelect` fires when a date is selected **and** when the picker is cleared; on clear `date` is `undefined`, so check for it (like above) to distinguish the two. +- `dp.update({ minDate: null })` throws in air-datepicker 3.6.0 - restore the originally configured value, or `''` when there was none, instead. +- The instances only exist after Backpack initializes its fields (in its own ready handler). Defer your code with `$(function(){ setTimeout(fn, 0); })`, or call `bpFieldAirDatepickerInstance()` from an event that runs after init. +- `bpFieldAirDatepickerInstance()` works with top-level form fields. For fields inside repeatable rows or modals, grab the row-scoped instance instead - for example `$('input[data-air-datepicker]', row).data('air-datepicker-instance')`. + + +## Doing more with the instance + +The instance getter is not just for linking fields. Use it for anything the air-datepicker API supports: + +```js +var dp = bpFieldAirDatepickerInstance('start_date'); + +dp.update({ minDate: new Date('2026-01-01') }); // change limits on the fly +dp.selectDate(new Date()); // pick a date programmatically +dp.clear(); // clear the picker +``` + +See the air-datepicker [docs](https://air-datepicker.com/docs) and [methods](https://air-datepicker.com/methods) for the full list.