diff --git a/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php b/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php index 9ae077f110..7f5b6a07bf 100644 --- a/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php +++ b/src/app/Library/CrudPanel/Traits/FieldsProtectedMethods.php @@ -277,6 +277,8 @@ protected function makeSureSubfieldsHaveNecessaryAttributes($field) $subfield['name'] = Str::replace(' ', '', $subfield['name']); $subfield['parentFieldName'] = $field['name']; + $subfield['baseFieldName'] = is_array($subfield['name']) ? implode(',', $subfield['name']) : $subfield['name']; + $subfield['baseFieldName'] = Str::afterLast($subfield['baseFieldName'], '.'); if (! isset($field['model'])) { // we're inside a simple 'repeatable' with no model/relationship, so @@ -290,8 +292,6 @@ protected function makeSureSubfieldsHaveNecessaryAttributes($field) $currentEntity = $subfield['baseEntity'] ?? $field['entity']; $subfield['baseModel'] = $subfield['baseModel'] ?? $field['model']; $subfield['baseEntity'] = isset($field['baseEntity']) ? $field['baseEntity'].'.'.$currentEntity : $currentEntity; - $subfield['baseFieldName'] = is_array($subfield['name']) ? implode(',', $subfield['name']) : $subfield['name']; - $subfield['baseFieldName'] = Str::afterLast($subfield['baseFieldName'], '.'); } $field['subfields'][$key] = $this->makeSureFieldHasNecessaryAttributes($subfield); diff --git a/src/app/Library/Uploaders/Support/UploadersRepository.php b/src/app/Library/Uploaders/Support/UploadersRepository.php index a751820332..5b531f284b 100644 --- a/src/app/Library/Uploaders/Support/UploadersRepository.php +++ b/src/app/Library/Uploaders/Support/UploadersRepository.php @@ -2,7 +2,9 @@ namespace Backpack\CRUD\app\Library\Uploaders\Support; +use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade as CRUD; use Backpack\CRUD\app\Library\Uploaders\Support\Interfaces\UploaderInterface; +use Illuminate\Support\Str; final class UploadersRepository { @@ -119,4 +121,73 @@ public function getRegisteredUploadNames(string $uploadName): array return $uploader->getName(); }, $this->getRepeatableUploadersFor($uploadName)); } + + /** + * Get the uploaders classes for the given group of uploaders. + */ + public function getAjaxUploadTypes(string $uploaderMacro = 'withFiles'): array + { + $ajaxFieldTypes = []; + foreach ($this->uploaderClasses[$uploaderMacro] as $fieldType => $uploader) { + if (is_a($uploader, 'Backpack\Pro\Uploads\BackpackAjaxUploader', true)) { + $ajaxFieldTypes[] = $fieldType; + } + } + + return $ajaxFieldTypes; + } + + /** + * Get an uploader instance for a given crud object. + */ + public function getFieldUploaderInstance(string $requestInputName): UploaderInterface + { + if (strpos($requestInputName, '#') !== false) { + $repeatableContainerName = Str::before($requestInputName, '#'); + $requestInputName = Str::after($requestInputName, '#'); + $uploaders = $this->getRepeatableUploadersFor($repeatableContainerName); + //TODO: Implement the logic for repeatable uploaders + dd('here'); + } + + if (empty($crudObject = CRUD::fields()[$requestInputName])) { + abort(500, 'Could not find the field in the CRUD fields.'); + } + + if (! $uploaderMacro = $this->getUploadCrudObjectMacroType($crudObject)) { + abort(500, 'There is no uploader defined for the given field type.'); + } + + if (! $this->isValidUploadField($crudObject, $uploaderMacro)) { + abort(500, 'Invalid field for upload.'); + } + + $uploaderConfiguration = $crudObject[$uploaderMacro] ?? []; + $uploaderConfiguration = ! is_array($uploaderConfiguration) ? [] : $uploaderConfiguration; + $uploaderClass = $this->getUploadFor($crudObject['type'], $uploaderMacro); + + return new $uploaderClass(['name' => $requestInputName], $uploaderConfiguration); + } + + /** + * Get the upload field macro type for the given object. + */ + private function getUploadCrudObjectMacroType(array $crudObject): string + { + return isset($crudObject['withFiles']) ? 'withFiles' : ($crudObject['withMedia'] ? 'withMedia' : null); + } + + private function isValidUploadField($crudObject, $uploaderMacro) + { + if (Str::contains($crudObject['name'], '#')) { + $container = Str::before($crudObject['name'], '#'); + $field = array_filter(CRUD::fields()[$container]['subfields'] ?? [], function ($item) use ($crudObject, $uploaderMacro) { + return $item['name'] === $crudObject['name'] && in_array($item['type'], $this->getAjaxUploadTypes($uploaderMacro)); + }); + + return ! empty($field); + } + + return in_array($crudObject['type'], $this->getAjaxUploadTypes($uploaderMacro)); + } } diff --git a/src/app/Library/Validation/Rules/BackpackCustomRule.php b/src/app/Library/Validation/Rules/BackpackCustomRule.php index 5a51c4cdad..7d4d750588 100644 --- a/src/app/Library/Validation/Rules/BackpackCustomRule.php +++ b/src/app/Library/Validation/Rules/BackpackCustomRule.php @@ -2,18 +2,21 @@ namespace Backpack\CRUD\app\Library\Validation\Rules; +use Backpack\CRUD\app\Library\Validation\Rules\Support\ValidateArrayContract; use Closure; use Illuminate\Contracts\Validation\DataAwareRule; use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidatorAwareRule; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Str; -/** - * @method static static itemRules() - */ abstract class BackpackCustomRule implements ValidationRule, DataAwareRule, ValidatorAwareRule { + use Support\HasFiles; + /** * @var \Illuminate\Contracts\Validation\Validator */ @@ -30,6 +33,12 @@ public static function field(string|array|ValidationRule|Rule $rules = []): self $instance = new static(); $instance->fieldRules = self::getRulesAsArray($rules); + if ($instance->validatesArrays()) { + if (! in_array('array', $instance->getFieldRules())) { + $instance->fieldRules[] = 'array'; + } + } + return $instance; } @@ -43,7 +52,18 @@ public static function field(string|array|ValidationRule|Rule $rules = []): self */ public function validate(string $attribute, mixed $value, Closure $fail): void { - // is the extending class reponsability the implementation of the validation logic + $value = $this->ensureValueIsValid($value); + + if ($value === false) { + $fail('Invalid value for the attribute.')->translate(); + + return; + } + + $errors = $this->validateOnSubmit($attribute, $value); + foreach ($errors as $error) { + $fail($error)->translate(); + } } /** @@ -96,19 +116,118 @@ protected static function getRulesAsArray($rules) return $rules; } + protected function ensureValueIsValid($value) + { + if ($this->validatesArrays() && ! is_array($value)) { + try { + $value = json_decode($value, true) ?? []; + } catch(\Exception $e) { + return false; + } + } + + return $value; + } + + private function validatesArrays(): bool + { + return is_a($this, ValidateArrayContract::class); + } + + private function validateAndGetErrors(string $attribute, mixed $value, array $rules): array + { + $validator = Validator::make($value, [ + $attribute => $rules, + ], $this->validator->customMessages, $this->validator->customAttributes); + + return $validator->errors()->messages()[$attribute] ?? []; + } + + protected function getValidationAttributeString(string $attribute) + { + return Str::substrCount($attribute, '.') > 1 ? + Str::before($attribute, '.').'.*.'.Str::afterLast($attribute, '.') : + $attribute; + } + + protected function validateOnSubmit(string $attribute, mixed $value): array + { + return $this->validateRules($attribute, $value); + } + + protected function validateFieldAndFile(string $attribute, null|array $data = null, array|null $customRules = null): array + { + $fieldErrors = $this->validateFieldRules($attribute, $data, $customRules); + $fileErrors = $this->validateFileRules($attribute, $data); + + return array_merge($fieldErrors, $fileErrors); + } + /** * Implementation. */ - public function validateFieldRules(string $attribute, mixed $value, Closure $fail): void + public function validateFieldRules(string $attribute, null|array|UploadedFile $data = null, array|null $customRules = null): array { - $validator = Validator::make([$attribute => $value], [ - $attribute => $this->getFieldRules(), - ], $this->validator->customMessages, $this->validator->customAttributes); + $data = $data ?? $this->data; + $validationRuleAttribute = $this->getValidationAttributeString($attribute); + + $data = $this->prepareValidatorData($data, $attribute); + + return $this->validateAndGetErrors($validationRuleAttribute, $data, $customRules ?? $this->getFieldRules()); + } - if ($validator->fails()) { - foreach ($validator->errors()->messages()[$attribute] as $message) { - $fail($message)->translate(); + protected function prepareValidatorData(array|UploadedFile $data, string $attribute): array + { + if ($this->validatesArrays() && is_array($data)) { + return Arr::has($data, $attribute) ? $data : [$attribute => Arr::get($data, $attribute)]; + } + + return [$attribute => $data]; + } + + protected function validateFileRules(string $attribute, mixed $data): array + { + $data = $data ?? $this->data; + $items = is_array($data) && array_key_exists($attribute, $data) ? $data[$attribute] : $data; + $items = is_array($items) ? $items : [$items]; + $errors = []; + // we validate each file individually to avoid returning messages like: `field.0` is not a pdf. + foreach ($items as $sentFiles) { + if (! is_array($sentFiles)) { + try { + if (is_file($sentFiles)) { + $errors[] = $this->validateAndGetErrors($attribute, [$attribute => $sentFiles], $this->getFileRules()); + } + continue; + } catch(\Exception) { + $errors[] = 'Unknown datatype, aborting upload process.'; + break; + } + } + + if (is_multidimensional_array($sentFiles)) { + foreach ($sentFiles as $key => $value) { + foreach ($value[$attribute] as $file) { + if (is_file($file)) { + $errors[] = $this->validateAndGetErrors($attribute, [$attribute => $file], $this->getFileRules()); + } + } + } + continue; + } + + foreach ($sentFiles as $key => $value) { + if (is_file($value)) { + $errors[] = $this->validateAndGetErrors($attribute, [$attribute => $value], $this->getFileRules()); + } } } + + return array_unique(array_merge(...$errors)); + } + + public function validateRules(string $attribute, mixed $value): array + { + return $this->validateFieldAndFile($attribute, $value); } } diff --git a/src/app/Library/Validation/Rules/Support/ValidateArrayContract.php b/src/app/Library/Validation/Rules/Support/ValidateArrayContract.php new file mode 100644 index 0000000000..d8339a4d96 --- /dev/null +++ b/src/app/Library/Validation/Rules/Support/ValidateArrayContract.php @@ -0,0 +1,7 @@ +validateArrayData($attribute, $fail, $value); - $this->validateItems($attribute, $value, $fail); - } - - public static function field(string|array|ValidationRule|Rule $rules = []): self - { - $instance = new static(); - $instance->fieldRules = self::getRulesAsArray($rules); - - if (! in_array('array', $instance->getFieldRules())) { - $instance->fieldRules[] = 'array'; - } - - return $instance; - } - - protected function validateItems(string $attribute, array $items, Closure $fail): void - { - $cleanAttribute = Str::afterLast($attribute, '.'); - foreach ($items as $file) { - $validator = Validator::make([$cleanAttribute => $file], [ - $cleanAttribute => $this->getFileRules(), - ], $this->validator->customMessages, $this->validator->customAttributes); - - if ($validator->fails()) { - foreach ($validator->errors()->messages() ?? [] as $attr => $message) { - foreach ($message as $messageText) { - $fail($messageText)->translate(); - } - } - } - } - } - - protected function validateArrayData(string $attribute, Closure $fail, null|array $data = null, null|array $rules = null): void - { - $data = $data ?? $this->data; - $rules = $rules ?? $this->getFieldRules(); - $validationRuleAttribute = $this->getValidationAttributeString($attribute); - $validator = Validator::make($data, [ - $validationRuleAttribute => $rules, - ], $this->validator->customMessages, $this->validator->customAttributes); - - if ($validator->fails()) { - foreach ($validator->errors()->messages()[$attribute] as $message) { - $fail($message)->translate(); - } - } - } - - protected static function ensureValidValue($value) - { - if (! is_array($value)) { - try { - $value = json_decode($value, true); - } catch (\Exception $e) { - return false; - } - } - - return $value; - } - - private function getValidationAttributeString($attribute) - { - return Str::substrCount($attribute, '.') > 1 ? - Str::before($attribute, '.').'.*.'.Str::afterLast($attribute, '.') : - $attribute; - } -} diff --git a/src/app/Library/Validation/Rules/ValidUpload.php b/src/app/Library/Validation/Rules/ValidUpload.php index b997e322e6..4ac1786b67 100644 --- a/src/app/Library/Validation/Rules/ValidUpload.php +++ b/src/app/Library/Validation/Rules/ValidUpload.php @@ -7,7 +7,6 @@ use Closure; use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidationRule; -use Illuminate\Support\Facades\Validator; class ValidUpload extends BackpackCustomRule { @@ -21,27 +20,21 @@ class ValidUpload extends BackpackCustomRule * @param Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail * @return void */ - public function validate(string $attribute, mixed $value, Closure $fail): void + public function validateRules(string $attribute, mixed $value): array { $entry = CrudPanelFacade::getCurrentEntry(); if (! array_key_exists($attribute, $this->data) && $entry) { - return; + return []; } - $this->validateFieldRules($attribute, $value, $fail); + $fieldErrors = $this->validateFieldRules($attribute, $value); if (! empty($value) && ! empty($this->getFileRules())) { - $validator = Validator::make([$attribute => $value], [ - $attribute => $this->getFileRules(), - ], $this->validator->customMessages, $this->validator->customAttributes); - - if ($validator->fails()) { - foreach ($validator->errors()->messages()[$attribute] as $message) { - $fail($message)->translate(); - } - } + $fileErrors = $this->validateFileRules($attribute, $value); } + + return array_merge($fieldErrors, $fileErrors ?? []); } public static function field(string|array|ValidationRule|Rule $rules = []): self diff --git a/src/app/Library/Validation/Rules/ValidUploadMultiple.php b/src/app/Library/Validation/Rules/ValidUploadMultiple.php index 02bea084c4..961405869d 100644 --- a/src/app/Library/Validation/Rules/ValidUploadMultiple.php +++ b/src/app/Library/Validation/Rules/ValidUploadMultiple.php @@ -3,36 +3,18 @@ namespace Backpack\CRUD\app\Library\Validation\Rules; use Backpack\CRUD\app\Library\CrudPanel\CrudPanelFacade; -use Closure; +use Backpack\CRUD\app\Library\Validation\Rules\Support\ValidateArrayContract; -class ValidUploadMultiple extends ValidFileArray +class ValidUploadMultiple extends BackpackCustomRule implements ValidateArrayContract { - /** - * Run the validation rule. - * - * @param string $attribute - * @param mixed $value - * @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail - * @return void - */ - public function validate(string $attribute, mixed $value, Closure $fail): void + public function validateRules(string $attribute, mixed $value): array { - if (! $value = self::ensureValidValue($value)) { - $fail('Unable to determine the value type.'); - - return; - } - $entry = CrudPanelFacade::getCurrentEntry() !== false ? CrudPanelFacade::getCurrentEntry() : null; // `upload_multiple` sends [[0 => null]] when user doesn't upload anything // assume that nothing changed on field so nothing is sent on the request. if (count($value) === 1 && empty($value[0])) { - if ($entry) { - unset($this->data[$attribute]); - } else { - $this->data[$attribute] = []; - } + $this->data[$attribute] = []; $value = []; } @@ -49,15 +31,9 @@ public function validate(string $attribute, mixed $value, Closure $fail): void $data = $this->data; $data[$attribute] = array_diff($value, $filesDeleted); - $this->validateArrayData($attribute, $fail, $data); - - $this->validateItems($attribute, $value, $fail); - - return; + return $this->validateFieldAndFile($attribute, $data); } - $this->validateArrayData($attribute, $fail); - - $this->validateItems($attribute, $value, $fail); + return $this->validateFieldAndFile($attribute, $value); } }