diff --git a/resources/js/components/fieldtypes/assets/Asset.js b/resources/js/components/fieldtypes/assets/Asset.js
index 980a9c9f12c..9f46c85e805 100644
--- a/resources/js/components/fieldtypes/assets/Asset.js
+++ b/resources/js/components/fieldtypes/assets/Asset.js
@@ -8,6 +8,7 @@ export default {
props: {
asset: Object,
readOnly: Boolean,
+ errors: Array,
showFilename: {
type: Boolean,
default: true,
diff --git a/resources/js/components/fieldtypes/assets/AssetRow.vue b/resources/js/components/fieldtypes/assets/AssetRow.vue
index 813451e3e15..a79f9b6a18c 100644
--- a/resources/js/components/fieldtypes/assets/AssetRow.vue
+++ b/resources/js/components/fieldtypes/assets/AssetRow.vue
@@ -24,11 +24,14 @@
diff --git a/resources/js/components/fieldtypes/assets/AssetTile.vue b/resources/js/components/fieldtypes/assets/AssetTile.vue
index ab4bac1aa4a..8ee015c74b0 100644
--- a/resources/js/components/fieldtypes/assets/AssetTile.vue
+++ b/resources/js/components/fieldtypes/assets/AssetTile.vue
@@ -20,6 +20,15 @@
+
+
+
+
+
+
@@ -34,7 +43,7 @@
-
+
diff --git a/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue b/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue
index cf90836dbab..5729c72ffe7 100644
--- a/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue
+++ b/resources/js/components/fieldtypes/assets/AssetsFieldtype.vue
@@ -111,6 +111,7 @@
v-for="asset in assets"
:key="asset.id"
:asset="asset"
+ :errors="errorsForAsset(asset.id)"
:read-only="isReadOnly"
:show-filename="config.show_filename"
:show-set-alt="showSetAlt"
@@ -147,6 +148,7 @@
v-for="asset in assets"
:key="asset.id"
:asset="asset"
+ :errors="errorsForAsset(asset.id)"
:read-only="isReadOnly"
:show-filename="config.show_filename"
:show-set-alt="showSetAlt"
@@ -233,6 +235,7 @@ export default {
innerDragging: false,
displayMode: 'grid',
lockedDynamicFolder: this.meta.dynamicFolder,
+ errorsById: {},
};
},
@@ -611,6 +614,14 @@ export default {
this.loadAssets([...this.value, id]);
}
},
+
+ errorsForAsset(id) {
+ if (Object.keys(this.errorsById).length === 0 || !this.errorsById.hasOwnProperty(id)) {
+ return [];
+ }
+
+ return this.errorsById[id];
+ },
},
watch: {
@@ -630,6 +641,29 @@ export default {
}
},
+ 'publishContainer.errors': {
+ immediate: true,
+ handler(errors) {
+ this.errorsById = Object.entries(errors).reduce((acc, [key, value]) => {
+ const prefix = this.fieldPathKeys || this.handle;
+
+ if (!key.startsWith(prefix)) {
+ return acc;
+ }
+
+ const subKey = key.replace(`${prefix}.`, '');
+ const assetIndex = subKey.split('.').shift();
+ const assetId = this.assetIds[assetIndex];
+
+ if (assetId) {
+ acc[assetId] = value;
+ }
+
+ return acc;
+ }, {});
+ },
+ },
+
loading(loading) {
this.$progress.loading(`assets-fieldtype-${this.$.uid}`, loading);
},
diff --git a/src/Fields/Field.php b/src/Fields/Field.php
index 44169f28192..42f031df836 100644
--- a/src/Fields/Field.php
+++ b/src/Fields/Field.php
@@ -137,11 +137,29 @@ public function alwaysSave()
public function rules()
{
- $rules = [$this->handle => $this->addNullableRule(array_merge(
+ $temp_rules = collect($this->addNullableRule(array_merge(
$this->get('required') ? ['required'] : [],
Validator::explodeRules($this->fieldtype()->fieldRules()),
Validator::explodeRules($this->fieldtype()->rules())
- ))];
+ )));
+
+ $rules = [];
+ if ($this->type() === 'assets') {
+ $rules = $temp_rules->reduce(function ($result, $rule) {
+ // These rules need to be applied to the field as a whole vs each asset in the field
+ if (Str::of($rule)->before(':')->is(['array', 'required', 'nullable', 'max', 'min'])) {
+ $result[$this->handle] ??= [];
+ $result[$this->handle][] = $rule;
+ } else {
+ $result["{$this->handle}.*"] ??= [];
+ $result["{$this->handle}.*"][] = $rule;
+ }
+
+ return $result;
+ }, []);
+ } else {
+ $rules = [$this->handle => $temp_rules->all()];
+ }
$extra = collect($this->fieldtype()->extraRules())->map(function ($rules) {
return $this->addNullableRule(Validator::explodeRules($rules));
diff --git a/src/Fieldtypes/Assets/DimensionsRule.php b/src/Fieldtypes/Assets/DimensionsRule.php
index 6232ee23abb..4a95347eae3 100644
--- a/src/Fieldtypes/Assets/DimensionsRule.php
+++ b/src/Fieldtypes/Assets/DimensionsRule.php
@@ -2,89 +2,60 @@
namespace Statamic\Fieldtypes\Assets;
-use Illuminate\Contracts\Validation\Rule;
+use Closure;
+use Illuminate\Contracts\Validation\ValidationRule;
use Statamic\Contracts\GraphQL\CastableToValidationString;
use Statamic\Facades\Asset;
use Statamic\Statamic;
+use Stringable;
use Symfony\Component\HttpFoundation\File\UploadedFile;
-class DimensionsRule implements CastableToValidationString, Rule
+class DimensionsRule implements CastableToValidationString, Stringable, ValidationRule
{
- protected $parameters;
+ protected array $raw_parameters;
- public function __construct($parameters = null)
+ public function __construct(protected $parameters)
{
- $this->parameters = $parameters;
+ $this->raw_parameters = $parameters;
+ $this->parameters = array_reduce($parameters, function ($acc, $item) {
+ [$key, $value] = array_pad(explode('=', $item, 2), 2, null);
+ $acc[$key] = $value;
+
+ return $acc;
+ }, []);
}
- /**
- * Determine if the validation rule passes.
- *
- * @param string $attribute
- * @param mixed $value
- * @return bool
- */
- public function passes($attribute, $value)
+ public function validate(string $attribute, mixed $value, Closure $fail): void
{
- return collect($value)->every(function ($id) {
- if ($id instanceof UploadedFile) {
- if (in_array($id->getMimeType(), ['image/svg+xml', 'image/svg'])) {
- return true;
- }
-
- $size = getimagesize($id->getPathname());
- } else {
- if (! $asset = Asset::find($id)) {
- return false;
- }
-
- if ($asset->isSvg()) {
- return true;
- }
-
- $size = $asset->dimensions();
+ $size = [0, 0];
+
+ if ($value instanceof UploadedFile) {
+ if (in_array($value->getMimeType(), ['image/svg+xml', 'image/svg'])) {
+ return;
}
- [$width, $height] = $size;
+ $size = getimagesize($value->getPathname());
+ } elseif ($asset = Asset::find($value)) {
+ if ($asset->isSvg()) {
+ return;
+ }
- $parameters = $this->parseNamedParameters($this->parameters);
+ $size = $asset->dimensions();
+ }
- if ($this->failsBasicDimensionChecks($parameters, $width, $height) ||
- $this->failsRatioCheck($parameters, $width, $height)) {
- return false;
- }
+ [$width, $height] = $size;
- return true;
- });
+ if ($this->failsBasicDimensionChecks($this->parameters, $width, $height) ||
+ $this->failsRatioCheck($this->parameters, $width, $height)) {
+ $fail($this->message());
+ }
}
- /**
- * Get the validation error message.
- *
- * @return string
- */
- public function message()
+ public function message(): string
{
return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.dimensions');
}
- /**
- * Parse named parameters to $key => $value items.
- *
- * @param array $parameters
- * @return array
- */
- protected function parseNamedParameters($parameters)
- {
- return array_reduce($parameters, function ($result, $item) {
- [$key, $value] = array_pad(explode('=', $item, 2), 2, null);
-
- $result[$key] = $value;
-
- return $result;
- });
- }
-
/**
* Test if the given width and height fail any conditions.
*
@@ -126,8 +97,13 @@ protected function failsRatioCheck($parameters, $width, $height)
return abs($numerator / $denominator - $width / $height) > $precision;
}
+ public function __toString()
+ {
+ return 'dimensions:'.implode(',', $this->raw_parameters);
+ }
+
public function toGqlValidationString(): string
{
- return 'dimensions:'.implode(',', $this->parameters);
+ return $this->__toString();
}
}
diff --git a/src/Fieldtypes/Assets/ImageRule.php b/src/Fieldtypes/Assets/ImageRule.php
index 8044190761b..f97860db8a5 100644
--- a/src/Fieldtypes/Assets/ImageRule.php
+++ b/src/Fieldtypes/Assets/ImageRule.php
@@ -2,57 +2,52 @@
namespace Statamic\Fieldtypes\Assets;
-use Illuminate\Contracts\Validation\Rule;
+use Closure;
+use Illuminate\Contracts\Validation\ValidationRule;
use Statamic\Contracts\GraphQL\CastableToValidationString;
use Statamic\Facades\Asset;
use Statamic\Statamic;
+use Stringable;
use Symfony\Component\HttpFoundation\File\UploadedFile;
-class ImageRule implements CastableToValidationString, Rule
+class ImageRule implements CastableToValidationString, Stringable, ValidationRule
{
- protected $parameters;
+ public $extensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'avif'];
- public function __construct($parameters = null)
+ public function __construct(protected $parameters)
{
- $this->parameters = $parameters;
+ if ($this->parameters !== ['image']) {
+ $this->extensions = array_map(strtolower(...), $this->parameters);
+ }
}
- /**
- * Determine if the validation rule passes.
- *
- * @param string $attribute
- * @param mixed $value
- * @return bool
- */
- public function passes($attribute, $value)
+ public function validate(string $attribute, mixed $value, Closure $fail): void
{
- $extensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'avif'];
+ $extension = '';
- return collect($value)->every(function ($id) use ($extensions) {
- if ($id instanceof UploadedFile) {
- return in_array($id->guessExtension(), $extensions);
- }
+ if ($value instanceof UploadedFile) {
+ $extension = $value->guessExtension();
+ } elseif ($asset = Asset::find($value)) {
+ $extension = $asset->extension();
+ }
- if (! $asset = Asset::find($id)) {
- return false;
- }
+ if (! in_array($extension, $this->extensions)) {
+ $fail($this->message());
+ }
+ }
- return $asset->guessedExtensionIsOneOf($extensions);
- });
+ public function message(): string
+ {
+ return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.image', ['extensions' => implode(', ', $this->extensions)]);
}
- /**
- * Get the validation error message.
- *
- * @return string
- */
- public function message()
+ public function __toString()
{
- return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.image');
+ return 'image:'.implode(',', $this->extensions);
}
public function toGqlValidationString(): string
{
- return 'image:'.implode(',', $this->parameters);
+ return $this->__toString();
}
}
diff --git a/src/Fieldtypes/Assets/MaxRule.php b/src/Fieldtypes/Assets/MaxRule.php
index 8a5f04e86f2..c18341c201a 100644
--- a/src/Fieldtypes/Assets/MaxRule.php
+++ b/src/Fieldtypes/Assets/MaxRule.php
@@ -2,28 +2,47 @@
namespace Statamic\Fieldtypes\Assets;
+use Closure;
+use Illuminate\Contracts\Validation\ValidationRule;
+use Statamic\Contracts\GraphQL\CastableToValidationString;
+use Statamic\Facades\Asset;
use Statamic\Statamic;
+use Stringable;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
-class MaxRule extends SizeBasedRule
+class MaxRule implements CastableToValidationString, Stringable, ValidationRule
{
- /**
- * Determine if the the rule passes for the given size.
- *
- * @param int $size
- * @return bool
- */
- public function sizePasses($size)
+ public function __construct(protected $parameters)
{
- return $size <= $this->parameters[0];
}
- /**
- * Get the validation error message.
- *
- * @return string
- */
- public function message()
+ public function validate(string $attribute, mixed $value, Closure $fail): void
{
- return str_replace(':max', $this->parameters[0], __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.max.file'));
+ $size = 0;
+
+ if ($value instanceof UploadedFile) {
+ $size = $value->getSize() / 1024;
+ } elseif ($asset = Asset::find($value)) {
+ $size = $asset->size() / 1024;
+ }
+
+ if ($size > $this->parameters[0]) {
+ $fail($this->message());
+ }
+ }
+
+ public function message(): string
+ {
+ return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.max.file', ['max' => $this->parameters[0]]);
+ }
+
+ public function __toString(): string
+ {
+ return 'max_filesize:'.$this->parameters[0];
+ }
+
+ public function toGqlValidationString(): string
+ {
+ return $this->__toString();
}
}
diff --git a/src/Fieldtypes/Assets/MimesRule.php b/src/Fieldtypes/Assets/MimesRule.php
index c184a34d578..81d60f36b6a 100644
--- a/src/Fieldtypes/Assets/MimesRule.php
+++ b/src/Fieldtypes/Assets/MimesRule.php
@@ -2,59 +2,52 @@
namespace Statamic\Fieldtypes\Assets;
-use Illuminate\Contracts\Validation\Rule;
+use Closure;
+use Illuminate\Contracts\Validation\ValidationRule;
use Statamic\Contracts\GraphQL\CastableToValidationString;
use Statamic\Facades\Asset;
use Statamic\Statamic;
+use Stringable;
use Symfony\Component\HttpFoundation\File\UploadedFile;
-class MimesRule implements CastableToValidationString, Rule
+class MimesRule implements CastableToValidationString, Stringable, ValidationRule
{
- protected $parameters;
-
- public function __construct($parameters)
+ public function __construct(protected $parameters)
{
if (in_array('jpg', $parameters) || in_array('jpeg', $parameters)) {
$parameters = array_unique(array_merge($parameters, ['jpg', 'jpeg']));
}
- $this->parameters = $parameters;
+ $this->parameters = array_map(strtolower(...), $parameters);
}
- /**
- * Determine if the validation rule passes.
- *
- * @param string $attribute
- * @param mixed $value
- * @return bool
- */
- public function passes($attribute, $value)
+ public function validate(string $attribute, mixed $value, Closure $fail): void
{
- return collect($value)->every(function ($id) {
- if ($id instanceof UploadedFile) {
- return in_array($id->guessExtension(), $this->parameters);
- }
+ $mime = '';
- if (! $asset = Asset::find($id)) {
- return false;
- }
+ if ($value instanceof UploadedFile) {
+ $mime = $value->guessExtension();
+ } elseif ($asset = Asset::find($value)) {
+ $mime = $asset->extension();
+ }
- return $asset->guessedExtensionIsOneOf($this->parameters);
- });
+ if (! in_array($mime, $this->parameters)) {
+ $fail($this->message());
+ }
}
- /**
- * Get the validation error message.
- *
- * @return string
- */
- public function message()
+ public function message(): string
{
- return str_replace(':values', implode(', ', $this->parameters), __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.mimes'));
+ return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.mimes', ['values' => implode(', ', $this->parameters)]);
}
- public function toGqlValidationString(): string
+ public function __toString()
{
return 'mimes:'.implode(',', $this->parameters);
}
+
+ public function toGqlValidationString(): string
+ {
+ return $this->__toString();
+ }
}
diff --git a/src/Fieldtypes/Assets/MimetypesRule.php b/src/Fieldtypes/Assets/MimetypesRule.php
index 65e3e0400bd..6c91fe32f8b 100644
--- a/src/Fieldtypes/Assets/MimetypesRule.php
+++ b/src/Fieldtypes/Assets/MimetypesRule.php
@@ -2,54 +2,47 @@
namespace Statamic\Fieldtypes\Assets;
-use Illuminate\Contracts\Validation\Rule;
+use Closure;
+use Illuminate\Contracts\Validation\ValidationRule;
use Statamic\Contracts\GraphQL\CastableToValidationString;
use Statamic\Facades\Asset;
use Statamic\Statamic;
+use Stringable;
use Symfony\Component\HttpFoundation\File\UploadedFile;
-class MimetypesRule implements CastableToValidationString, Rule
+class MimetypesRule implements CastableToValidationString, Stringable, ValidationRule
{
- protected $parameters;
-
- public function __construct($parameters)
+ public function __construct(protected $parameters)
{
- $this->parameters = $parameters;
}
- /**
- * Determine if the validation rule passes.
- *
- * @param string $attribute
- * @param mixed $value
- * @return bool
- */
- public function passes($attribute, $value)
+ public function validate(string $attribute, mixed $value, Closure $fail): void
{
- return collect($value)->every(function ($id) {
- if ($id instanceof UploadedFile) {
- $mimeType = $id->getMimeType();
- } elseif (! ($mimeType = optional(Asset::find($id))->mimeType())) {
- return false;
- }
-
- return in_array($mimeType, $this->parameters) ||
- in_array(explode('/', $mimeType)[0].'/*', $this->parameters);
- });
+ $mime_type = '';
+
+ if ($value instanceof UploadedFile) {
+ $mime_type = $value->getMimeType();
+ } elseif ($asset = Asset::find($value)) {
+ $mime_type = $asset->mimeType();
+ }
+
+ if (! in_array($mime_type, $this->parameters) && ! in_array(explode('/', $mime_type)[0].'/*', $this->parameters)) {
+ $fail($this->message());
+ }
}
- /**
- * Get the validation error message.
- *
- * @return string
- */
public function message()
{
- return str_replace(':values', implode(', ', $this->parameters), __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.mimetypes'));
+ return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.mimetypes', ['values' => implode(', ', $this->parameters)]);
}
- public function toGqlValidationString(): string
+ public function __toString()
{
return 'mimetypes:'.implode(',', $this->parameters);
}
+
+ public function toGqlValidationString(): string
+ {
+ return $this->__toString();
+ }
}
diff --git a/src/Fieldtypes/Assets/MinRule.php b/src/Fieldtypes/Assets/MinRule.php
index b37bfaa5874..8d9edb30446 100644
--- a/src/Fieldtypes/Assets/MinRule.php
+++ b/src/Fieldtypes/Assets/MinRule.php
@@ -2,28 +2,47 @@
namespace Statamic\Fieldtypes\Assets;
+use Closure;
+use Illuminate\Contracts\Validation\ValidationRule;
+use Statamic\Contracts\GraphQL\CastableToValidationString;
+use Statamic\Facades\Asset;
use Statamic\Statamic;
+use Stringable;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
-class MinRule extends SizeBasedRule
+class MinRule implements CastableToValidationString, Stringable, ValidationRule
{
- /**
- * Determine if the the rule passes for the given size.
- *
- * @param int $size
- * @return bool
- */
- public function sizePasses($size)
+ public function __construct(protected $parameters)
{
- return $size >= $this->parameters[0];
}
- /**
- * Get the validation error message.
- *
- * @return string
- */
- public function message()
+ public function validate(string $attribute, mixed $value, Closure $fail): void
{
- return str_replace(':min', $this->parameters[0], __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.min.file'));
+ $size = 0;
+
+ if ($value instanceof UploadedFile) {
+ $size = $value->getSize() / 1024;
+ } elseif ($asset = Asset::find($value)) {
+ $size = $asset->size() / 1024;
+ }
+
+ if ($size < $this->parameters[0]) {
+ $fail($this->message());
+ }
+ }
+
+ public function message(): string
+ {
+ return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.min.file', ['min' => $this->parameters[0]]);
+ }
+
+ public function __toString(): string
+ {
+ return 'min_filesize:'.$this->parameters[0];
+ }
+
+ public function toGqlValidationString(): string
+ {
+ return $this->__toString();
}
}
diff --git a/src/Fieldtypes/Assets/SizeBasedRule.php b/src/Fieldtypes/Assets/SizeBasedRule.php
deleted file mode 100644
index 875c3c612d0..00000000000
--- a/src/Fieldtypes/Assets/SizeBasedRule.php
+++ /dev/null
@@ -1,75 +0,0 @@
-parameters = $parameters;
- }
-
- /**
- * Determine if the validation rule passes.
- *
- * @param string $attribute
- * @param mixed $value
- * @return bool
- */
- public function passes($attribute, $value)
- {
- return collect($value)->every(function ($id) {
- if (($size = $this->getFileSize($id)) === false) {
- return false;
- }
-
- return $this->sizePasses($size);
- });
- }
-
- /**
- * Determine if the the rule passes for the given size.
- *
- * @param int $size
- * @return bool
- */
- abstract public function sizePasses($size);
-
- /**
- * Get the validation error message.
- *
- * @return string
- */
- abstract public function message();
-
- /**
- * Get the file size.
- *
- * @param string|UploadedFile $id
- * @return int|false
- */
- protected function getFileSize($id)
- {
- if ($id instanceof UploadedFile) {
- return $id->getSize() / 1024;
- }
-
- if ($asset = Asset::find($id)) {
- return $asset->size() / 1024;
- }
-
- return false;
- }
-
- public function toGqlValidationString(): string
- {
- return 'size:'.implode(',', $this->parameters);
- }
-}
diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php
index 3c10d8e448f..d42dcf94f5f 100644
--- a/tests/Assets/AssetTest.php
+++ b/tests/Assets/AssetTest.php
@@ -12,6 +12,7 @@
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Validator;
use League\Flysystem\PathTraversalDetected;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
@@ -1553,7 +1554,10 @@ public function it_gets_dimensions()
public function it_passes_the_dimensions_validation()
{
$file = UploadedFile::fake()->image('image.jpg', 30, 60);
- $validDimensions = (new DimensionsRule(['max_width=10']))->passes('Image', [$file]);
+ $validDimensions = Validator::make(
+ ['Image' => $file],
+ ['Image' => [new DimensionsRule(['max_width=10'])]],
+ )->passes();
$this->assertFalse($validDimensions);
}
diff --git a/tests/Feature/GraphQL/FormTest.php b/tests/Feature/GraphQL/FormTest.php
index d3ce69622d4..06dc9016307 100644
--- a/tests/Feature/GraphQL/FormTest.php
+++ b/tests/Feature/GraphQL/FormTest.php
@@ -357,13 +357,15 @@ public function it_returns_string_based_validation_rules_for_mimes_mimetypes_dim
'form' => [
'rules' => [
'name' => [
- 'mimes:image/jpeg,image/png',
- 'mimetypes:image/jpeg,image/png',
- 'dimensions:1024',
- 'size:1000',
- 'image:jpeg',
- 'thevalidationrule:foo,bar',
- 'Tests\\Feature\\GraphQL\\TestValidationRuleWithoutToString::class',
+ '*' => [
+ 'mimes:image/jpeg,image/png',
+ 'mimetypes:image/jpeg,image/png',
+ 'dimensions:1024',
+ 'size:1000',
+ 'image:jpeg',
+ 'thevalidationrule:foo,bar',
+ 'Tests\\Feature\\GraphQL\\TestValidationRuleWithoutToString::class',
+ ],
'array',
'nullable',
],
|