Skip to content
Merged
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
14 changes: 10 additions & 4 deletions 7.x/crud-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -1055,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)
Expand Down Expand Up @@ -1106,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'];`

Expand Down Expand Up @@ -1513,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)
Expand Down Expand Up @@ -1552,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' => [
Expand Down
2 changes: 2 additions & 0 deletions 7.x/crud-operation-ajax-upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


<a name="how-to-use"></a>
## How to Use
Expand Down
75 changes: 71 additions & 4 deletions 7.x/crud-uploaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a name="upload-validation"></a>
### 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.

<a name="allowed-file-types"></a>
### 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).

<a name="available-uploaders"></a>
## Available Uploaders
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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).

Expand All @@ -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.
<a name="subfields-in-uploaders"></a>
### Subfields in Uploaders

Expand Down
2 changes: 2 additions & 0 deletions 7.x/custom-validation-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a name="valid-upload-validation-rule"></a>
## `ValidUpload` for `upload` field type

Expand Down
137 changes: 137 additions & 0 deletions 7.x/how-to-link-two-air-datepicker-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# How to link two air-datepicker fields

---

<a name="about"></a>
## 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.

<a name="the-fields"></a>
## 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',
]);
```

<a name="the-script"></a>
## 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');
```

<a name="the-javascript"></a>
## 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 });
}
}
```

<a name="how-it-works"></a>
## 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.

<a name="notes"></a>
## 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')`.

<a name="advanced"></a>
## 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.