Merge branch 'develop' into trunk
This commit is contained in:
commit
102a0d8048
34 changed files with 1711 additions and 1047 deletions
|
|
@ -19,6 +19,7 @@ use Kwaadpepper\Enum\BaseEnumRoutable;
|
|||
* html autorisé
|
||||
* @method static self videoVimeo()
|
||||
* @method static self number()
|
||||
* @method static self email()
|
||||
*/
|
||||
final class InputType extends BaseEnumRoutable
|
||||
{
|
||||
|
|
@ -40,7 +41,8 @@ final class InputType extends BaseEnumRoutable
|
|||
'dropdownArray' => 7,
|
||||
'miniblocArray' => 8,
|
||||
'videoVimeo' => 9,
|
||||
'number' => 10
|
||||
'number' => 10,
|
||||
'email' => 11
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HtmlSanitizer
|
||||
|
|
@ -18,11 +18,17 @@ class HtmlSanitizer
|
|||
public function handle(Request $request, \Closure $next, ...$params)
|
||||
{
|
||||
$inputs = $request->all();
|
||||
array_walk_recursive($inputs, function (&$input, $name) use ($params) {
|
||||
/** @var callable|null */
|
||||
$cleanInput = null;
|
||||
$cleanInput = function (&$input, $name, $rootName = null) use ($params, &$cleanInput) {
|
||||
// * Nested element array
|
||||
if (is_array($input)) {
|
||||
array_walk_recursive($input, $cleanInput, $name);
|
||||
}
|
||||
if (!is_string($input)) {
|
||||
return;
|
||||
}
|
||||
if (in_array($name, $params)) {
|
||||
if (in_array($name, $params) or in_array($rootName ?? $name, $params)) {
|
||||
// ? Is a json input ?
|
||||
if ($isJson = \json_decode($input, true) and is_array($isJson)) {
|
||||
// ! Decode each json field recursive.
|
||||
|
|
@ -39,7 +45,8 @@ class HtmlSanitizer
|
|||
} else {
|
||||
$input = strip_tags($input);
|
||||
}
|
||||
});
|
||||
};
|
||||
\array_walk($inputs, $cleanInput);
|
||||
$request->merge($inputs);
|
||||
return $next($request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,27 +17,17 @@ class UpdateEngagementsRequest extends UpdateMiniCrudRequest
|
|||
*/
|
||||
protected $modelName = 'engagement';
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return auth('backend')->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function prepareForValidation()
|
||||
public function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'slug' => Str::slug($this->name)
|
||||
]);
|
||||
$title = strip_tags($this->title);
|
||||
$this->merge(['slug' => Str::slug($title)]);
|
||||
$this->mergePicture('banner');
|
||||
parent::prepareForValidation();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,10 +40,12 @@ class UpdateEngagementsRequest extends UpdateMiniCrudRequest
|
|||
return array_merge(
|
||||
[
|
||||
'title' => 'required|min:3|max:255',
|
||||
'title_simple' => 'required|min:3|max:255',
|
||||
'slug' => 'required|min:3|max:255',
|
||||
'seo_title' => 'required|min:53|max:60',
|
||||
'seo_description' => 'required|min:50|max:160',
|
||||
'banner_alt' => 'required_with:banner|nullable|string|min:6|max:255',
|
||||
'banner_title' => 'required_with:banner|nullable|string|min:6|max:255'
|
||||
'banner_alt' => 'required|nullable|string|min:6|max:255',
|
||||
'banner_title' => 'required|nullable|string|min:6|max:255'
|
||||
],
|
||||
self::pictureRules('banner', 1920, 340, 1920, 340),
|
||||
parent::rules()
|
||||
|
|
@ -68,10 +60,11 @@ class UpdateEngagementsRequest extends UpdateMiniCrudRequest
|
|||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'title' => trans('title'),
|
||||
'seo_title' => trans('title') . ' seo',
|
||||
'seo_description' => trans('description') . ' seo',
|
||||
'banner' => trans('bannière'),
|
||||
'title' => trans('Titre'),
|
||||
'title_simple' => trans('Titre sans style'),
|
||||
'seo_title' => trans('Titre') . ' seo',
|
||||
'seo_description' => trans('Description') . ' seo',
|
||||
'banner' => trans('Bannière'),
|
||||
'banner_alt' => trans('Texte alternatif de la bannière'),
|
||||
'banner_title' => trans('Titre de la bannière')
|
||||
];
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class StoreProductRequest extends FormRequest
|
|||
[
|
||||
'title' => 'required|min:6|max:255',
|
||||
'slug' => 'required|max:255',
|
||||
'seo_title' => 'required|min:45|max:70',
|
||||
'seo_title' => 'required|min:53|max:60',
|
||||
'seo_description' => 'required|min:50|max:160',
|
||||
'description' => 'required|string|min:15',
|
||||
'picture_alt' => 'required_with:picture|nullable|string|min:6|max:255',
|
||||
|
|
|
|||
|
|
@ -33,9 +33,10 @@ class RangeModelTitleRequest extends FormRequest
|
|||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed,Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
|
||||
function ($attribute, $value, $fail) {
|
||||
$table = (new $value())->getTable();
|
||||
Validator::validate(['model' => $this->id], [
|
||||
'model' => "required|exists:{$table},id",
|
||||
]);
|
||||
Validator::validate(
|
||||
['model' => $this->id],
|
||||
['model' => "required|exists:{$table},id"]
|
||||
);
|
||||
}
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
namespace App\Http\Requests\Bo;
|
||||
|
||||
use App\Enums\InputType;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
abstract class UpdateMiniCrudRequest extends FormRequest
|
||||
|
|
@ -26,6 +28,29 @@ abstract class UpdateMiniCrudRequest extends FormRequest
|
|||
return auth('backend')->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function prepareForValidation(): void
|
||||
{
|
||||
foreach ($this->{$this->modelName}->data as $iptName => $input) {
|
||||
$input = \json_decode(\json_encode($input), true);
|
||||
switch ($input['inputType']['value']) {
|
||||
case InputType::boolean()->value:
|
||||
$all = $this->all();
|
||||
$nested = Arr::undot([
|
||||
"data.$iptName.value" =>
|
||||
$this->input("data.$iptName.value") ? true : false
|
||||
]);
|
||||
$all = array_merge_recursive_distinct($all, $nested);
|
||||
$this->merge($all);
|
||||
break;
|
||||
} //end switch
|
||||
} //end foreach
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
|
|
@ -46,13 +71,14 @@ abstract class UpdateMiniCrudRequest extends FormRequest
|
|||
]);
|
||||
break;
|
||||
case InputType::string()->value:
|
||||
case InputType::email()->value:
|
||||
$rules = array_merge($rules, [
|
||||
"data.$iptName.value" => $this->compileValidate('required|string|min:1', $input)
|
||||
]);
|
||||
break;
|
||||
case InputType::text()->value:
|
||||
$mergeRules = $this->compileValidate('required|string|min:10', $input);
|
||||
$mergeRules = collect(\explode('|', $mergeRules))
|
||||
$mergeRules = collect($mergeRules)
|
||||
->map(function ($rule) {
|
||||
if (\strpos($rule, 'max:') !== false) {
|
||||
$max = \intval(str_replace('max:', '', $rule));
|
||||
|
|
@ -71,31 +97,36 @@ abstract class UpdateMiniCrudRequest extends FormRequest
|
|||
case InputType::image()->value:
|
||||
$ipt = $this->data[$iptName];
|
||||
$max = isset($ipt['dimensionsMax']);
|
||||
$dim = collect(explode(
|
||||
' par ',
|
||||
$ipt['dimensionsMax'] ?? $input['dimensions']
|
||||
))->mapWithKeys(function ($v) {
|
||||
$v = explode(' ', $v);
|
||||
return [$v[1] => $v[0]];
|
||||
});
|
||||
$dim = collect($ipt['dimensionsMax'] ?? $input['dimensions']);
|
||||
$rules = array_merge($rules, [
|
||||
"data.$iptName.value" => $this->compileValidate(
|
||||
implode('|', [
|
||||
'sometimes',
|
||||
'image',
|
||||
'mimes:png,jpg,jpeg',
|
||||
sprintf(
|
||||
'dimensions:%s=%d,%s=%d',
|
||||
$max ? 'max_height' : 'height',
|
||||
$dim['(hauteur)'],
|
||||
$max ? 'max_width' : 'width',
|
||||
$dim['(largeur)']
|
||||
)
|
||||
]),
|
||||
$input
|
||||
"data.$iptName.value" => array_merge(
|
||||
$this->compileValidate('sometimes', $input),
|
||||
[function ($attribute, $value, $fail) use ($max, $dim) {
|
||||
try {
|
||||
if (\is_object($value)) {
|
||||
Validator::make(
|
||||
[$attribute => $value],
|
||||
// Replace dots since laravel framework 8.76.
|
||||
[ToolboxHelper::mbReplace('.', '\.', $attribute) => [
|
||||
'file',
|
||||
'mimes:png,jpg,jpeg',
|
||||
sprintf(
|
||||
'dimensions:%s=%d,%s=%d',
|
||||
$max ? 'max_height' : 'height',
|
||||
$dim['hauteur'],
|
||||
$max ? 'max_width' : 'width',
|
||||
$dim['largeur']
|
||||
)
|
||||
]]
|
||||
)->validate();
|
||||
} //end if
|
||||
} catch (ValidationException $e) {
|
||||
$fail(collect($e->errors())->first()[0]);
|
||||
} //end try
|
||||
}]
|
||||
),
|
||||
"data.$iptName.alt" => $this->compileValidate('sometimes|string|min:5', $input),
|
||||
"data.$iptName.title" => $this->compileValidate('sometimes|string|min:5', $input)
|
||||
"data.$iptName.alt" => $this->compileValidate('required|string|min:5', $input),
|
||||
"data.$iptName.title" => $this->compileValidate('required|string|min:5', $input)
|
||||
]);
|
||||
break;
|
||||
case InputType::url()->value:
|
||||
|
|
@ -115,15 +146,8 @@ abstract class UpdateMiniCrudRequest extends FormRequest
|
|||
]);
|
||||
break;
|
||||
case InputType::boolean()->value:
|
||||
$all = $this->all();
|
||||
$nested = ToolboxHelper::dotStringToNestedArray(
|
||||
"data.$iptName.value",
|
||||
$this->input("data.$iptName.value") ? true : false
|
||||
);
|
||||
$all = array_merge_recursive_distinct($all, $nested);
|
||||
$this->merge($all);
|
||||
$rules = array_merge($rules, [
|
||||
"data.$iptName.value" => 'boolean'
|
||||
"data.$iptName.value" => $this->compileValidate('boolean', $input)
|
||||
]);
|
||||
break;
|
||||
} //end switch
|
||||
|
|
@ -136,12 +160,12 @@ abstract class UpdateMiniCrudRequest extends FormRequest
|
|||
*
|
||||
* @param string $inputTypeValidate
|
||||
* @param array $input
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
public function compileValidate(string $inputTypeValidate, array $input): string
|
||||
public function compileValidate(string $inputTypeValidate, array $input): array
|
||||
{
|
||||
if (!isset($input['validate']) or !\is_string($input['validate'])) {
|
||||
return $inputTypeValidate;
|
||||
return explode('|', $inputTypeValidate);
|
||||
}
|
||||
/** @var array */
|
||||
$inputValidate = explode('|', $input['validate']);
|
||||
|
|
@ -151,6 +175,6 @@ abstract class UpdateMiniCrudRequest extends FormRequest
|
|||
$out[] = $validate;
|
||||
}
|
||||
}
|
||||
return \implode('|', $out);
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
410
app/Lib/Helpers/FileStorageHelper.php
Normal file
410
app/Lib/Helpers/FileStorageHelper.php
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\Helpers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* This class is used to handle files in LDC Laravel stack
|
||||
*
|
||||
* * The storage strategy will be using date.
|
||||
* The file name may contain sufix timestamp highresolution time (hrtime or microtime) if is duplicate.
|
||||
* (function() { return time() . ' ' . Str::slug(microtime(true)) . ' ' . hrtime(true); })();
|
||||
*
|
||||
* posts/ - The model table name
|
||||
* 10/ - Year
|
||||
* 04/ - Month
|
||||
* 19/ - Day
|
||||
* 5f/ - First two letters of md5 file
|
||||
* black-cat-under-red-car.png - Slugified file name
|
||||
* black-cat-under-red-car-286071690833400.png - Slugified file name that is duplicated
|
||||
*
|
||||
* The final path will be storage/modelfiles/posts/10/04/19/5f/black-cat-under-red-car-286071690833400.png
|
||||
*
|
||||
* ! No limits of file per folder will be handled.
|
||||
*
|
||||
* @author Munsch Jérémy <jeremy.munsch@ldc.fr>
|
||||
*/
|
||||
class FileStorageHelper
|
||||
{
|
||||
/**
|
||||
* Destroy the file associate with the attribute.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeFile(Model $model, string $attribute): void
|
||||
{
|
||||
$path = \ltrim(\urldecode($model->{$attribute}), '/');
|
||||
if (\strpos($path, 'storage') !== 0) {
|
||||
return;
|
||||
}
|
||||
$path = ToolboxHelper::mbReplace('storage/', '', $path);
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the file associate with the attribute old value (before updated to DB).
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @param boolean $onlyIfUpdated
|
||||
* @return void
|
||||
*/
|
||||
public static function removeOldFile(Model $model, string $attribute, bool $onlyIfUpdated = true): void
|
||||
{
|
||||
// * Ignore If removal has to be done only if the field has changed and it actually hasnt.
|
||||
if ($onlyIfUpdated and $model->{$attribute} === $model->getOriginal($attribute)) {
|
||||
return;
|
||||
}
|
||||
$originalPath = \ltrim(\urldecode($model->getOriginal($attribute)), '/');
|
||||
if (\strpos($originalPath, 'storage') !== 0) {
|
||||
return;
|
||||
}
|
||||
$originalPath = ToolboxHelper::mbReplace('storage/', '', $originalPath);
|
||||
if (Storage::disk('public')->exists($originalPath)) {
|
||||
Storage::disk('public')->delete($originalPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a file using optimized storage strategy
|
||||
*
|
||||
* You can use filename param to overload file name.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model The model that will hold the picture.
|
||||
* @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $file The file, will just return itself is string.
|
||||
* @param boolean $slugify Force filename slugification.
|
||||
* @param string|null $filename This string may be used to store the file.
|
||||
* @return string
|
||||
* @throws \RuntimeException If file copy fails.
|
||||
*/
|
||||
public static function storeFile(Model $model, $file, bool $slugify = false, string $filename = null): string
|
||||
{
|
||||
$tableName = $model->getTable();
|
||||
if (\is_object($file) and $file instanceof \Illuminate\Http\UploadedFile) {
|
||||
/** @var \Illuminate\Http\UploadedFile $file */
|
||||
$filename = $filename ?: $file->getClientOriginalName();
|
||||
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
|
||||
$newPath = self::prepareMoveFile($tableName, $filename);
|
||||
$newPath = str_replace(storage_path('app'), '', $newPath);
|
||||
$file->storePubliclyAs(
|
||||
pathinfo($newPath, \PATHINFO_DIRNAME),
|
||||
pathinfo($newPath, \PATHINFO_BASENAME)
|
||||
);
|
||||
return 'storage' . str_replace('/public', '', $newPath);
|
||||
}
|
||||
if (\is_object($file) and $file instanceof \SplFileInfo) {
|
||||
/** @var \SplFileInfo $file */
|
||||
$filename = $filename ?: $file->getBaseName();
|
||||
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
|
||||
$newPath = self::prepareMoveFile($tableName, $filename);
|
||||
if (!File::copy($file->getRealPath(), $newPath)) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'Failed to copy file %s to %s',
|
||||
$file->getRealPath(),
|
||||
$newPath
|
||||
));
|
||||
}
|
||||
return \ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/');
|
||||
}
|
||||
return strval($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store all files from string attribute html
|
||||
* If oldHtmlString is passed, a diff is done with new and old files
|
||||
* to keep only files that are present in the new html string.
|
||||
*
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model The model that will hold the picture.
|
||||
* @param string $htmlString The html string to parse and modify files.
|
||||
* @param string|null $oldHtmlString The old html string with alreadyParsed
|
||||
* files using storeModelFilesFromHtml.
|
||||
* @return string Html with files pathes replaced using storage strategy.
|
||||
*/
|
||||
public static function storeModelFilesFromHtml(
|
||||
Model $model,
|
||||
string $htmlString,
|
||||
string $oldHtmlString = null
|
||||
): string {
|
||||
\collect(self::onlyUploadedFilesWithFileManager(self::findFilesInHtml($htmlString)))
|
||||
->each(function (string $path) use ($model, &$htmlString) {
|
||||
/** @var \Illuminate\Filesystem\FilesystemAdapter */
|
||||
$disk = Storage::disk('uploads');
|
||||
$fileInfo = new \SplFileInfo($disk->path(
|
||||
ToolboxHelper::mbReplace('/uploads/', '', \urldecode($path))
|
||||
));
|
||||
$htmlString = ToolboxHelper::mbReplace(
|
||||
$path,
|
||||
'/' . self::storeFile($model, $fileInfo, true),
|
||||
$htmlString
|
||||
);
|
||||
});
|
||||
if ($oldHtmlString) {
|
||||
self::removeOldModelFilesFromHtml($htmlString, $oldHtmlString);
|
||||
}
|
||||
return $htmlString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all old files from string attribute html that are stored in public that
|
||||
* are nove present anymore on new html string
|
||||
*
|
||||
* @param string $newHtmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
|
||||
* @param string $oldHtmlString The old html string with alreadyParsed files using storeModelFilesFromHtml.
|
||||
* @return void
|
||||
*/
|
||||
public static function removeOldModelFilesFromHtml(string $newHtmlString, string $oldHtmlString): void
|
||||
{
|
||||
self::removeOldFiles(self::findFilesInHtml($oldHtmlString), self::findFilesInHtml($newHtmlString));
|
||||
}
|
||||
/**
|
||||
* Remove all old files that are stored in public that
|
||||
* are not present anymore on new array files
|
||||
*
|
||||
* @param string[] $oldFiles The old array files.
|
||||
* @param string[] $newFiles The new array files.
|
||||
* @return void
|
||||
*/
|
||||
public static function removeOldFiles(array $oldFiles, array $newFiles): void
|
||||
{
|
||||
$oldFiles = \collect(self::onlyUploadedFilesWithFileStorageHelper($oldFiles));
|
||||
$filesToKeep = \collect(self::onlyUploadedFilesWithFileStorageHelper($newFiles));
|
||||
$filesToRemove = $oldFiles->diff($filesToKeep);
|
||||
$filesToRemove->each(function (string $publicStorageFilePath) {
|
||||
$path = ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($publicStorageFilePath), '/'));
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Remove all files from string attribute html that are stored in public
|
||||
*
|
||||
* @param string $htmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
|
||||
* @return void
|
||||
*/
|
||||
public static function removeModelFilesOfHtml(string $htmlString): void
|
||||
{
|
||||
$filesToRemove = \collect(self::onlyUploadedFilesWithFileStorageHelper(self::findFilesInHtml($htmlString)));
|
||||
$filesToRemove->each(function (string $publicStorageFilePath) {
|
||||
$path = ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($publicStorageFilePath), '/'));
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Find all occurence of files in html
|
||||
* @param string $html
|
||||
* @return array
|
||||
*/
|
||||
public static function findFilesInHtml(string $html): array
|
||||
{
|
||||
preg_match_all('/href="(?P<href>.*)"|src="(?P<src>.*)"|url="(?P<url>.*)"/Um', $html, $m, PREG_SET_ORDER, 0);
|
||||
return collect($m)->pluck('href')->filter()
|
||||
->concat(collect($m)->pluck('url')->filter())
|
||||
->concat(collect($m)->pluck('src')->filter())
|
||||
->all();
|
||||
}
|
||||
/**
|
||||
* Filter array to get only uploaded files that exists
|
||||
* on disk 'public' (using helper storage strategy)
|
||||
*
|
||||
* @param array $urls
|
||||
* @return array
|
||||
*/
|
||||
public static function onlyUploadedFilesWithFileStorageHelper(array $urls): array
|
||||
{
|
||||
return \collect($urls)->unique()
|
||||
// * Filter to get only /uploads and using app url or empty
|
||||
->filter(function (string $url) {
|
||||
$path = \parse_url($url, \PHP_URL_PATH);
|
||||
return \strpos(\ltrim($path, '/'), 'storage/modelfiles') === 0;
|
||||
})->filter(function (string $url) {
|
||||
$path = \parse_url($url, \PHP_URL_PATH);
|
||||
return Storage::disk('public')
|
||||
->exists(ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($path), '/')));
|
||||
})->all();
|
||||
}
|
||||
/**
|
||||
* Filter array to get only uploaded files that exists
|
||||
* on disk 'uploads'
|
||||
*
|
||||
* @param array $urls
|
||||
* @return array
|
||||
*/
|
||||
private static function onlyUploadedFilesWithFileManager(array $urls): array
|
||||
{
|
||||
return \collect($urls)->unique()
|
||||
// * Filter to get only /uploads and using app url or empty
|
||||
->filter(function (string $url) {
|
||||
$host = \parse_url($url, \PHP_URL_HOST);
|
||||
$path = \parse_url($url, \PHP_URL_PATH);
|
||||
return ($host === \parse_url(config('app.url'), PHP_URL_HOST) or \is_null($host)) and
|
||||
\strpos(\ltrim($path, '/'), 'uploads') === 0;
|
||||
})->filter(function (string $url) {
|
||||
$path = \parse_url($url, \PHP_URL_PATH);
|
||||
return Storage::disk('uploads')
|
||||
->exists(ToolboxHelper::mbReplace('uploads/', '', \ltrim(\urldecode($path), '/')));
|
||||
})->all();
|
||||
}
|
||||
/**
|
||||
* Prepare storage path and return
|
||||
* the file full path
|
||||
*
|
||||
* @param string $tableName Model table name.
|
||||
* @param string $filename The basename for the file.
|
||||
* @return string
|
||||
*/
|
||||
private static function prepareMoveFile(string $tableName, string $filename): string
|
||||
{
|
||||
// * Security filter + beautifier
|
||||
$filename = self::sanitizeFileName($filename);
|
||||
// * Security filter
|
||||
$filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
|
||||
// * Slugify filename
|
||||
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
|
||||
(\strlen($ext) ? ".{$ext}" : '');
|
||||
$folderPath = self::getStoragePath($tableName, $filename);
|
||||
$filename = self::getFileUniqueName($folderPath, $filename);
|
||||
return "{$folderPath}/{$filename}";
|
||||
}
|
||||
/**
|
||||
* Create sub folders to store files using pattern
|
||||
* storage/modelfiles/posts/10/04/19/5f
|
||||
* year/month/day/md5 first 2 letters
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
private static function getStoragePath(string $table, string $filename): string
|
||||
{
|
||||
$filenameHash = \md5($filename);
|
||||
$pathFolder = \storage_path(sprintf(
|
||||
'app/public/modelfiles/%s/%s/%s/%s/%s',
|
||||
$table,
|
||||
gmdate('y'),
|
||||
gmdate('m'),
|
||||
gmdate('d'),
|
||||
substr($filenameHash, 0, 2)
|
||||
));
|
||||
if (!File::exists($pathFolder)) {
|
||||
File::makeDirectory($pathFolder, 0755, true);
|
||||
}
|
||||
return $pathFolder;
|
||||
}
|
||||
/**
|
||||
* Get unique filename if the path is already occupied
|
||||
*
|
||||
* @param string $folderPath
|
||||
* @param string $filename
|
||||
* @return string
|
||||
* @throws \RuntimeException If a unique filename cannot be obtained.
|
||||
*/
|
||||
private static function getFileUniqueName(string $folderPath, string $filename): string
|
||||
{
|
||||
$attempts = 0;
|
||||
$maxAttempts = 20;
|
||||
while (File::exists("{$folderPath}/{$filename}")) {
|
||||
if ($attempts++ >= $maxAttempts) {
|
||||
throw new \RuntimeException("Failed to has a unique filename for {$folderPath}/{$filename}");
|
||||
}
|
||||
$filename = pathinfo($filename, \PATHINFO_FILENAME) .
|
||||
\uniqid('-') . '.' .
|
||||
pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
}
|
||||
return $filename;
|
||||
}
|
||||
/**
|
||||
* Slugify the filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
public static function slugifyFileName(string $filename): string
|
||||
{
|
||||
return Str::slug(pathinfo($filename, \PATHINFO_FILENAME)) . '.' . pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
}
|
||||
/**
|
||||
* Sanitize File name for uploaded files
|
||||
*
|
||||
* @param string $filename The original uploaded client file name.
|
||||
* @param boolean $beautify Beautify the filename.
|
||||
* @return string The sanitized file name.
|
||||
*/
|
||||
public static function sanitizeFileName(string $filename, bool $beautify = true): string
|
||||
{
|
||||
// * Sanitize filename
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
$filename = preg_replace(
|
||||
'~
|
||||
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
||||
[\x00-\x1F]| # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
|
||||
[\x7F\xA0\xAD]| # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
|
||||
[#\[\]@!$&\'()+,;=]| # URI reserved https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
[{}^\~`] # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
|
||||
~x',
|
||||
'-',
|
||||
$filename
|
||||
);
|
||||
// phpcs:enable
|
||||
// * Avoids '.' '..' or '.hiddenFiles' .
|
||||
$filename = Str::of($filename)->ltrim('.-');
|
||||
// * Optional beautification
|
||||
if ($beautify) {
|
||||
$filename = static::beautifyFilename($filename);
|
||||
}
|
||||
// * Maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
|
||||
/** @var string */
|
||||
$extension = pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
/** @var string */
|
||||
$filename = pathinfo($filename, \PATHINFO_FILENAME);
|
||||
$encoding = mb_detect_encoding($filename);
|
||||
$encoding = $encoding ? $encoding : null;
|
||||
return \mb_strcut(
|
||||
$filename,
|
||||
0,
|
||||
255 - (!empty($extension) ? strlen($extension) + 1 : 0),
|
||||
$encoding
|
||||
) . (!empty($extension) ? ".$extension" : '');
|
||||
}
|
||||
/**
|
||||
* Beautify a filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
private static function beautifyFilename(string $filename): string
|
||||
{
|
||||
// Reduce consecutive characters.
|
||||
$filename = preg_replace([
|
||||
// * "file name.zip" becomes 'file-name.zip'
|
||||
'/ +/',
|
||||
// * "file___name.zip" becomes 'file-name.zip'
|
||||
'/_+/',
|
||||
// * "file---name.zip" becomes 'file-name.zip'
|
||||
'/-+/'
|
||||
], '-', $filename);
|
||||
$filename = preg_replace([
|
||||
// * "file--.--.-.--name.zip" becomes "file.name.zip"
|
||||
'/-*\.-*/',
|
||||
// * "file...name..zip" becomes "file.name.zip"
|
||||
'/\.{2,}/'
|
||||
], '.', $filename);
|
||||
// Lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 .
|
||||
$filename = mb_strtolower($filename, mb_detect_encoding($filename));
|
||||
// * ".file-name.-" becomes "file-name"
|
||||
$filename = trim($filename, '.-');
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
299
app/Lib/Helpers/ToolboxHelper.php
Normal file
299
app/Lib/Helpers/ToolboxHelper.php
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\Helpers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ToolboxHelper
|
||||
{
|
||||
/**
|
||||
* Strip scheme (https) on oembed urls
|
||||
* Only embed allowed are for MP4 self hosted videos right now
|
||||
*
|
||||
* https is force by ckeditor MediaElements which natively supports
|
||||
* only cloud hosted medias (youtube, spotify etc..)
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
public static function stripCkEditorSchemesOnMediaInputs(string $input): string
|
||||
{
|
||||
$input = \preg_replace('/(?<=<oembed url=")(https:\/\/)(?=.*?">)/m', '', $input);
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That no Back Office url is present on txt
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $text
|
||||
* @return string
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalBoUrls(string $field, string $text): string
|
||||
{
|
||||
$routes = Route::getRoutes();
|
||||
// * Url replace and email replace.
|
||||
$text = preg_replace_callback(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
|
||||
function ($matches) use ($routes, $field) {
|
||||
$match = collect($matches)->first() ?? '';
|
||||
$url = $match;
|
||||
try {
|
||||
$request = Request::create($url);
|
||||
// If a route is match then it do not throws exception.
|
||||
if (
|
||||
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
|
||||
trim($routes->match($request)->getPrefix() ?? '', '/') === 'bo'
|
||||
) {
|
||||
// Removes the matched url.
|
||||
throw ValidationException::withMessages([
|
||||
$field => ' :attribute : Il est interdit de coller une url du back office dans ce champ'
|
||||
]);
|
||||
}
|
||||
return $match;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
return $match;
|
||||
}
|
||||
},
|
||||
$text
|
||||
);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That only external url is present on txt
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $text
|
||||
* @return string
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalUrls(string $field, string $text): string
|
||||
{
|
||||
self::assertNoInternalBoUrls($field, $text);
|
||||
$routes = Route::getRoutes();
|
||||
// * Url replace and email replace.
|
||||
$text = preg_replace_callback(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
|
||||
function ($matches) use ($routes, $field) {
|
||||
$match = collect($matches)->first() ?? '';
|
||||
$url = $match;
|
||||
try {
|
||||
$request = Request::create($url);
|
||||
// If a route is match then it do not throws exception.
|
||||
if (
|
||||
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
|
||||
$routes->match($request)
|
||||
) {
|
||||
// Removes the matched url.
|
||||
throw ValidationException::withMessages([
|
||||
$field => sprintf(
|
||||
':attribute : Il est interdit de coller une url de %s dans ce champ',
|
||||
config('app.name')
|
||||
)
|
||||
]);
|
||||
}
|
||||
return $match;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
return $match;
|
||||
}
|
||||
},
|
||||
$text
|
||||
);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutltibyte string replace
|
||||
* @param mixed $search
|
||||
* @param mixed $replace
|
||||
* @param mixed $subject
|
||||
* @param integer $count
|
||||
* @return mixed
|
||||
*/
|
||||
public static function mbReplace($search, $replace, $subject, int &$count = 0)
|
||||
{
|
||||
if (!is_array($search) && is_array($replace)) {
|
||||
return false;
|
||||
}
|
||||
if (is_array($subject)) {
|
||||
// Call mb_replace for each single string in $subject .
|
||||
foreach ($subject as &$string) {
|
||||
$string = &self::mbReplace($search, $replace, $string, $count);
|
||||
}
|
||||
} elseif (is_array($search)) {
|
||||
if (!is_array($replace)) {
|
||||
foreach ($search as &$string) {
|
||||
$subject = self::mbReplace($string, $replace, $subject, $count);
|
||||
}
|
||||
} else {
|
||||
$n = max(count($search), count($replace));
|
||||
while ($n--) {
|
||||
$subject = self::mbReplace(current($search), current($replace), $subject, $count);
|
||||
next($search);
|
||||
next($replace);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$parts = mb_split(preg_quote($search), $subject);
|
||||
$count = count($parts) - 1;
|
||||
$subject = implode($replace, $parts);
|
||||
} //end if
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the field is unique
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $field
|
||||
* @param mixed $value
|
||||
* @param integer|null $id
|
||||
* @param callable|null $query
|
||||
* @return void
|
||||
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
|
||||
*/
|
||||
public static function assertFieldIsUnique(
|
||||
string $table,
|
||||
string $field,
|
||||
$value,
|
||||
?int $id = null,
|
||||
?callable $query = null
|
||||
) {
|
||||
if (!$query) {
|
||||
Validator::make([$field => $value], [
|
||||
self::mbReplace('.', '\.', $field) => Rule::unique($table, $field)->ignore($id),
|
||||
], [
|
||||
self::mbReplace('.', '\.', "{$field}.unique")
|
||||
=> trans('La valeur :value pour le champ :attribute est déjà utilisé', [
|
||||
'attribute' => $field,
|
||||
'value' => $value
|
||||
])
|
||||
])->validate();
|
||||
return;
|
||||
}
|
||||
Validator::make([$field => $value], [
|
||||
$field => Rule::unique($table)->where($query)->ignore($id),
|
||||
], [
|
||||
$field . '.unique' => trans('La valeur pour le champ :attribute est déjà utilisé', [
|
||||
'attribute' => $field
|
||||
])
|
||||
])->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* * Register CSP report URI
|
||||
*
|
||||
* Put this function inside RouteServiceProvider,
|
||||
* and call it in RouteServiceProvider::boot
|
||||
*
|
||||
* ! Use this in conjonction with kwaadpepper/exception-handler
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function cspReportRoute()
|
||||
{
|
||||
// * CSP REPORT URI, Please add /csp-report to VerifyCsrfToken $except middleware
|
||||
\Illuminate\Support\Facades\Route::post('/csp-report', function () {
|
||||
$isCspRequest = request()->header('Content-Type') === 'application/csp-report';
|
||||
if (!$isCspRequest or !request()->json('csp-report') or !is_array(request()->json('csp-report'))) {
|
||||
abort(401);
|
||||
}
|
||||
// * Flatten if needed
|
||||
$cspReport = collect(request()->json('csp-report'))
|
||||
->flatMap(function ($v, $k) {
|
||||
return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
|
||||
})->all();
|
||||
// * Filter array keys
|
||||
$cspReport = collect(array_intersect_key($cspReport, [
|
||||
'blocked-uri' => '', 'document-uri' => '', 'original-policy' => '',
|
||||
'referrer' => '', 'violated-directive' => ''
|
||||
]));
|
||||
report(new \Illuminate\Http\Client\HttpClientException(sprintf(
|
||||
"CSP REPORT for %s : \n %s",
|
||||
$cspReport->get('document-uri'),
|
||||
$cspReport->implode("\n")
|
||||
)));
|
||||
})->middleware('throttle:5,2')
|
||||
->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
|
||||
->name('cspReportUri');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove unused pictures in ckeditor
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeUnusedImagesOnCkEditor(Model $model, string $attribute)
|
||||
{
|
||||
// * Remove unused inline uploaded images
|
||||
if ($model->{$attribute} != $model->getOriginal($attribute)) {
|
||||
$updatedPicturesPath = collect(
|
||||
ToolboxHelper::getCkEditorLocalUploadedFiles($model->{$attribute})
|
||||
);
|
||||
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
|
||||
->map(function (string $originalPicturePath) use ($updatedPicturesPath) {
|
||||
// * If a picture is not used anymore
|
||||
if (!$updatedPicturesPath->contains($originalPicturePath)) {
|
||||
if (File::exists(\public_path($originalPicturePath))) {
|
||||
File::delete(\public_path($originalPicturePath));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files path uploaded with ckeditor custom upload plugin (crud-policies)
|
||||
*
|
||||
* @param string $text
|
||||
* @param array $detectExtensions
|
||||
* @return array
|
||||
*/
|
||||
public static function getCkEditorLocalUploadedFiles(
|
||||
string $text,
|
||||
array $detectExtensions = ['png', 'svg', 'jpg', 'gif']
|
||||
): array {
|
||||
$out = [];
|
||||
\preg_match_all(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/src="(?P<url>\/storage\/images\/crud-policies\/uploads\/\d{4}-\d{2}-\d{2}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[-a-zA-Z0-9@%._\+~#=]{2,256}\.(?:' . implode('|', $detectExtensions) . '))"/',
|
||||
$text,
|
||||
$out
|
||||
);
|
||||
return collect($out['url'] ?? [])->map(function (string $urlEncodedFilePath) {
|
||||
return \urldecode($urlEncodedFilePath);
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove pictures in ckeditor
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeImagesOnCkEditor(Model $model, string $attribute)
|
||||
{
|
||||
// * Remove inline uploaded images
|
||||
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
|
||||
->map(function (string $originalPicturePath) {
|
||||
// * If a picture is not used anymore
|
||||
if (File::exists(\public_path($originalPicturePath))) {
|
||||
File::delete(\public_path($originalPicturePath));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,499 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ToolboxHelper
|
||||
{
|
||||
/**
|
||||
* Strip scheme (https) on oembed urls
|
||||
* Only embed allowed are for MP4 self hosted videos right now
|
||||
*
|
||||
* https is force by ckeditor MediaElements which natively supports
|
||||
* only cloud hosted medias (youtube, spotify etc..)
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
public static function stripCkEditorSchemesOnMediaInputs(string $input): string
|
||||
{
|
||||
$input = \preg_replace('/(?<=<oembed url=")(https:\/\/)(?=.*?">)/m', '', $input);
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That no Back Office url is present on txt
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $text
|
||||
* @return string
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalBoUrls(string $field, string $text): string
|
||||
{
|
||||
$routes = Route::getRoutes();
|
||||
// * Url replace and email replace.
|
||||
$text = preg_replace_callback(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
|
||||
function ($matches) use ($routes, $field) {
|
||||
$match = collect($matches)->first() ?? '';
|
||||
$url = $match;
|
||||
try {
|
||||
$request = Request::create($url);
|
||||
// If a route is match then it do not throws exception.
|
||||
if (
|
||||
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
|
||||
trim($routes->match($request)->getPrefix() ?? '', '/') === 'bo'
|
||||
) {
|
||||
// Removes the matched url.
|
||||
throw ValidationException::withMessages([
|
||||
$field => sprintf(
|
||||
'%s : Il est interdit de coller une url du back office dans ce champ',
|
||||
trans($field)
|
||||
)
|
||||
]);
|
||||
}
|
||||
return $match;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
return $match;
|
||||
}
|
||||
},
|
||||
$text
|
||||
);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files path uploaded with ckeditor custom upload plugin (crud-policies)
|
||||
*
|
||||
* @param string $text
|
||||
* @param array $detectExtensions
|
||||
* @return array
|
||||
*/
|
||||
public static function getCkEditorLocalUploadedFiles(
|
||||
string $text,
|
||||
array $detectExtensions = ['png', 'svg', 'jpg', 'gif']
|
||||
): array {
|
||||
$out = [];
|
||||
\preg_match_all(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/src="(?P<url>\/storage\/images\/crud-policies\/uploads\/\d{4}-\d{2}-\d{2}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[-a-zA-Z0-9@%._\+~#=]{2,256}\.(?:' . implode('|', $detectExtensions) . '))"/',
|
||||
$text,
|
||||
$out
|
||||
);
|
||||
return collect($out['url'] ?? [])->map(function (string $urlEncodedFilePath) {
|
||||
return \urldecode($urlEncodedFilePath);
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That only external url is present on txt
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $text
|
||||
* @return string
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalUrls(string $field, string $text): string
|
||||
{
|
||||
self::assertNoInternalBoUrls($field, $text);
|
||||
$routes = Route::getRoutes();
|
||||
// * Url replace and email replace.
|
||||
$text = preg_replace_callback(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
|
||||
function ($matches) use ($routes, $field) {
|
||||
$match = collect($matches)->first() ?? '';
|
||||
$url = $match;
|
||||
try {
|
||||
$request = Request::create($url);
|
||||
// If a route is match then it do not throws exception.
|
||||
if (
|
||||
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
|
||||
$routes->match($request)
|
||||
) {
|
||||
// Removes the matched url.
|
||||
throw ValidationException::withMessages([
|
||||
$field => sprintf(
|
||||
'%s : Il est interdit de coller une url de %s dans ce champ',
|
||||
trans($field),
|
||||
config('app.name')
|
||||
)
|
||||
]);
|
||||
}
|
||||
return $match;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
return $match;
|
||||
}
|
||||
},
|
||||
$text
|
||||
);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array from a dot separated string
|
||||
*
|
||||
* @param string $path
|
||||
* @param mixed $value
|
||||
* @param string $separator
|
||||
* @return array
|
||||
*/
|
||||
public static function dotStringToNestedArray(string $path, $value, string $separator = '.'): array
|
||||
{
|
||||
$arr = $value;
|
||||
$keys = array_reverse(explode($separator, $path));
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$arr = [$key => $arr];
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy image
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeImage(Model $model, string $attribute)
|
||||
{
|
||||
$path = ltrim($model->{$attribute}, '/');
|
||||
if (File::exists($path) and \strpos($path, 'storage') === 0) {
|
||||
File::delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy image
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeOldImage(Model $model, string $attribute)
|
||||
{
|
||||
$originalPath = ltrim($model->getOriginal($attribute), '/');
|
||||
if (File::exists($originalPath) and \strpos($originalPath, 'storage') === 0) {
|
||||
File::delete($originalPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove pictures in ckeditor
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeImagesOnCkEditor(Model $model, string $attribute)
|
||||
{
|
||||
// * Remove inline uploaded images
|
||||
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
|
||||
->map(function (string $originalPicturePath) {
|
||||
// * If a picture is not used anymore
|
||||
if (File::exists(\public_path($originalPicturePath))) {
|
||||
File::delete(\public_path($originalPicturePath));
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Remove unused pictures in ckeditor
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeUnusedImagesOnCkEditor(Model $model, string $attribute)
|
||||
{
|
||||
// * Remove unused inline uploaded images
|
||||
if ($model->{$attribute} != $model->getOriginal($attribute)) {
|
||||
$updatedPicturesPath = collect(
|
||||
ToolboxHelper::getCkEditorLocalUploadedFiles($model->{$attribute})
|
||||
);
|
||||
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
|
||||
->map(function (string $originalPicturePath) use ($updatedPicturesPath) {
|
||||
// * If a picture is not used anymore
|
||||
if (!$updatedPicturesPath->contains($originalPicturePath)) {
|
||||
if (File::exists(\public_path($originalPicturePath))) {
|
||||
File::delete(\public_path($originalPicturePath));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an image of different types
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $image
|
||||
* @return string
|
||||
* @throws \ErrorException If file copy fails.
|
||||
*/
|
||||
public static function storeImage(Model $model, $image): string
|
||||
{
|
||||
$table = $model->getTable();
|
||||
if (\is_object($image) and $image instanceof \Illuminate\Http\UploadedFile) {
|
||||
/** @var \Illuminate\Http\UploadedFile $image */
|
||||
$newPath = self::prepareMoveFile($table, $image->getClientOriginalName());
|
||||
$newPath = str_replace(storage_path('app'), '', $newPath);
|
||||
$image->storePubliclyAs(
|
||||
pathinfo($newPath, \PATHINFO_DIRNAME),
|
||||
pathinfo($newPath, \PATHINFO_BASENAME)
|
||||
);
|
||||
return 'storage' . str_replace('/public', '', $newPath);
|
||||
}
|
||||
if (\is_object($image) and $image instanceof \SplFileInfo) {
|
||||
/** @var \SplFileInfo $image */
|
||||
$newPath = self::prepareMoveFile($table, $image->getBaseName());
|
||||
if (!File::copy($image->getRealPath(), $newPath)) {
|
||||
throw new \ErrorException(sprintf(
|
||||
'Failed to copy file %s to %s',
|
||||
$image->getRealPath(),
|
||||
$newPath
|
||||
));
|
||||
}
|
||||
return \ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/');
|
||||
}
|
||||
return strval($image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare storage path and return
|
||||
* the image full path
|
||||
*
|
||||
* @param string $table Model table name.
|
||||
* @param string $filename The basename for the file.
|
||||
* @return string
|
||||
*/
|
||||
private static function prepareMoveFile(string $table, string $filename): string
|
||||
{
|
||||
// * Security filter + beautifier
|
||||
$filename = self::sanitizeFileName($filename);
|
||||
// * Security filter
|
||||
$filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
|
||||
// * Slugify filename
|
||||
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
|
||||
(\strlen($ext) ? ".{$ext}" : '');
|
||||
|
||||
$pathFolder = \storage_path(sprintf('app/public/images/%s', $table));
|
||||
if (!File::exists($pathFolder)) {
|
||||
File::makeDirectory($pathFolder, 0755, true);
|
||||
}
|
||||
$filename = sprintf('%s-%s', Str::uuid(), $filename);
|
||||
$fullPath = sprintf('%s/%s', $pathFolder, $filename);
|
||||
if (File::exists($fullPath)) {
|
||||
File::delete($fullPath);
|
||||
}
|
||||
return $fullPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the field is unique
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $field
|
||||
* @param mixed $value
|
||||
* @param integer|null $id
|
||||
* @param callable|null $query
|
||||
* @return void
|
||||
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
|
||||
*/
|
||||
public static function assertFieldIsUnique(
|
||||
string $table,
|
||||
string $field,
|
||||
$value,
|
||||
?int $id = null,
|
||||
?callable $query = null
|
||||
) {
|
||||
if (!$query) {
|
||||
Validator::make([$field => $value], [
|
||||
$field => Rule::unique($table, $field)->ignore($id),
|
||||
], [
|
||||
$field . '.unique' => trans('La valeur :value pour le champ :attribute est déjà utilisé', [
|
||||
'attribute' => $field,
|
||||
'value' => $value
|
||||
])
|
||||
])->validate();
|
||||
return;
|
||||
}
|
||||
Validator::make([$field => $value], [
|
||||
$field => Rule::unique($table)->where($query)->ignore($id),
|
||||
], [
|
||||
$field . '.unique' => trans('La valeur pour le champ :attribute est déjà utilisé', [
|
||||
'attribute' => $field
|
||||
])
|
||||
])->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate recaptcha V3
|
||||
*
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
public static function validateRecaptchaV3(Request $request)
|
||||
{
|
||||
Validator::validate($request->all(), [
|
||||
'g-recaptcha-response' => [
|
||||
'required', 'string',
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
|
||||
function ($attribute, $value, $fail) {
|
||||
try {
|
||||
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
|
||||
'secret' => config('recaptcha.private'),
|
||||
'response' => \strval($value),
|
||||
'remoteip' => app('clientIp')
|
||||
]);
|
||||
if ($e = $response->toException()) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if (
|
||||
!$response->json('success') or
|
||||
!$response->json('challenge_ts') or
|
||||
Carbon::parse($response->json('challenge_ts'))->lessThan(\now('utc')->subMinutes(5)) or
|
||||
$response->json('hostname') !== \parse_url(config('app.url'), \PHP_URL_HOST) or
|
||||
!$response->json('score')
|
||||
) {
|
||||
throw new \ErrorException();
|
||||
}
|
||||
if (\floatval($response->json('score')) < 0.8) {
|
||||
$fail('Vous n\'avez pas obtenu un score assez élevé lors de la validation du recaptcha');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$fail('Erreur lors de la validation du formulaire veuillez réssayer');
|
||||
} //end try
|
||||
}
|
||||
]
|
||||
], [], ['g-recaptcha-response' => 'reCaptcha']);
|
||||
}
|
||||
|
||||
/**
|
||||
* * Register CSP report URI
|
||||
*
|
||||
* Put this function inside RouteServiceProvider,
|
||||
* and call it in RouteServiceProvider::boot
|
||||
*
|
||||
* ! Use this in conjonction with kwaadpepper/exception-handler
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function cspReportRoute()
|
||||
{
|
||||
// * CSP REPORT URI, Please add /csp-report to VerifyCsrfToken $except middleware
|
||||
\Illuminate\Support\Facades\Route::post('/csp-report', function () {
|
||||
$isCspRequest = request()->header('Content-Type') === 'application/csp-report';
|
||||
if (!$isCspRequest or !request()->json('csp-report') or !is_array(request()->json('csp-report'))) {
|
||||
abort(401);
|
||||
}
|
||||
// * Flatten if needed
|
||||
$cspReport = collect(request()->json('csp-report'))
|
||||
->flatMap(function ($v, $k) {
|
||||
return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
|
||||
})->all();
|
||||
// * Filter array keys
|
||||
$cspReport = collect(array_intersect_key($cspReport, [
|
||||
'blocked-uri' => '', 'document-uri' => '', 'original-policy' => '',
|
||||
'referrer' => '', 'violated-directive' => ''
|
||||
]));
|
||||
report(new \Illuminate\Http\Client\HttpClientException(sprintf(
|
||||
"CSP REPORT for %s : \n %s",
|
||||
$cspReport->get('document-uri'),
|
||||
$cspReport->implode("\n")
|
||||
)));
|
||||
})->middleware('throttle:5,2')
|
||||
->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
|
||||
->name('cspReportUri');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize File name for uploaded files
|
||||
*
|
||||
* @param string $filename The original uploaded client file name.
|
||||
* @param boolean $beautify Beautify the filename.
|
||||
* @return string The sanitized file name.
|
||||
*/
|
||||
public static function sanitizeFileName(string $filename, bool $beautify = true): string
|
||||
{
|
||||
// * Sanitize filename
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
$filename = preg_replace(
|
||||
'~
|
||||
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
||||
[\x00-\x1F]| # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
|
||||
[\x7F\xA0\xAD]| # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
|
||||
[#\[\]@!$&\'()+,;=]| # URI reserved https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
[{}^\~`] # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
|
||||
~x',
|
||||
'-',
|
||||
$filename
|
||||
);
|
||||
// phpcs:enable
|
||||
// * Avoids '.' '..' or '.hiddenFiles' .
|
||||
$filename = Str::of($filename)->ltrim('.-');
|
||||
// * Optional beautification
|
||||
if ($beautify) {
|
||||
$filename = static::beautifyFilename($filename);
|
||||
}
|
||||
// * Maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
|
||||
/** @var string */
|
||||
$extension = pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
/** @var string */
|
||||
$filename = pathinfo($filename, \PATHINFO_FILENAME);
|
||||
|
||||
$encoding = mb_detect_encoding($filename);
|
||||
$encoding = $encoding ? $encoding : null;
|
||||
return \mb_strcut(
|
||||
$filename,
|
||||
0,
|
||||
255 - (!empty($extension) ? strlen($extension) + 1 : 0),
|
||||
$encoding
|
||||
) . (!empty($extension) ? ".$extension" : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautify a filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
private static function beautifyFilename(string $filename): string
|
||||
{
|
||||
// Reduce consecutive characters.
|
||||
$filename = preg_replace([
|
||||
// * "file name.zip" becomes 'file-name.zip'
|
||||
'/ +/',
|
||||
// * "file___name.zip" becomes 'file-name.zip'
|
||||
'/_+/',
|
||||
// * "file---name.zip" becomes 'file-name.zip'
|
||||
'/-+/'
|
||||
], '-', $filename);
|
||||
$filename = preg_replace([
|
||||
// * "file--.--.-.--name.zip" becomes "file.name.zip"
|
||||
'/-*\.-*/',
|
||||
// * "file...name..zip" becomes "file.name.zip"
|
||||
'/\.{2,}/'
|
||||
], '.', $filename);
|
||||
// Lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 .
|
||||
$filename = mb_strtolower($filename, mb_detect_encoding($filename));
|
||||
// * ".file-name.-" becomes "file-name"
|
||||
$filename = trim($filename, '.-');
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,9 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Enums\Engagements\EngagementType;
|
||||
use App\Lib\Helpers\FileStorageHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use App\Traits\Models\MiniCrudModel;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Kwaadpepper\Enum\Traits\CastsEnums;
|
||||
|
||||
|
|
@ -33,7 +34,8 @@ class Engagement extends Model
|
|||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'data' => 'array'
|
||||
'data' => 'array',
|
||||
'banner_dimensions' => 'array'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -52,12 +54,14 @@ class Engagement extends Model
|
|||
*/
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'title_simple',
|
||||
'slug',
|
||||
'seo_title',
|
||||
'seo_description',
|
||||
'banner',
|
||||
'banner_title',
|
||||
'banner_alt',
|
||||
'banner_dimensions',
|
||||
'type',
|
||||
'data'
|
||||
];
|
||||
|
|
@ -70,14 +74,23 @@ class Engagement extends Model
|
|||
protected static function booted()
|
||||
{
|
||||
static::creating(function (self $model) {
|
||||
$model->banner = ToolboxHelper::storeImage($model, $model->banner);
|
||||
$model->data = clean($model->data);
|
||||
$model->banner = FileStorageHelper::storeFile($model, $model->banner);
|
||||
});
|
||||
static::updating(function (self $model) {
|
||||
ToolboxHelper::removeOldImage($model, 'banner');
|
||||
$model->banner = ToolboxHelper::storeImage($model, $model->banner);
|
||||
if ($model->banner != $model->getOriginal('banner')) {
|
||||
$model->banner = FileStorageHelper::storeFile($model, $model->banner);
|
||||
}
|
||||
});
|
||||
static::updated(function (self $model) {
|
||||
ToolboxHelper::removeUnusedImagesOnCkEditor($model, 'title');
|
||||
if ($model->banner != $model->getOriginal('banner')) {
|
||||
$model->banner = FileStorageHelper::removeOldFile($model, 'banner');
|
||||
}
|
||||
});
|
||||
static::deleted(function (self $model) {
|
||||
ToolboxHelper::removeImage($model, 'banner');
|
||||
FileStorageHelper::removeFile($model, 'banner');
|
||||
ToolboxHelper::removeImagesOnCkEditor($model, 'title');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Enums\PublishedStatus;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use App\Lib\Helpers\FileStorageHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
@ -61,7 +62,7 @@ class News extends Model
|
|||
ToolboxHelper::assertNoInternalBoUrls('content', $model->content);
|
||||
$model->slug = Str::slug($model->title);
|
||||
$model->content = clean($model->content);
|
||||
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture);
|
||||
if ($model->published->equals(PublishedStatus::published())) {
|
||||
$model->published_at = now();
|
||||
}
|
||||
|
|
@ -80,17 +81,17 @@ class News extends Model
|
|||
}
|
||||
// * Store new picture if changed
|
||||
if ($model->picture != $model->getOriginal('picture')) {
|
||||
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture);
|
||||
}
|
||||
});
|
||||
static::updated(function (self $model) {
|
||||
ToolboxHelper::removeUnusedImagesOnCkEditor($model, 'content');
|
||||
if ($model->picture != $model->getOriginal('picture')) {
|
||||
$model->picture = ToolboxHelper::removeOldImage($model, 'picture');
|
||||
$model->picture = FileStorageHelper::removeOldFile($model, 'picture');
|
||||
}
|
||||
});
|
||||
static::deleting(function (self $model) {
|
||||
ToolboxHelper::removeImage($model, 'picture');
|
||||
FileStorageHelper::removeFile($model, 'picture');
|
||||
ToolboxHelper::removeImagesOnCkEditor($model, 'content');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Enums\Products\ProductLabelEnum;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use App\Lib\Helpers\FileStorageHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
|
@ -90,8 +91,8 @@ class Product extends Model
|
|||
$model->preparation_advices = clean($model->preparation_advices);
|
||||
$model->sales_pitch = clean($model->sales_pitch);
|
||||
$model->allergen_list = clean($model->allergen_list);
|
||||
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
|
||||
$model->picture_thumb = ToolboxHelper::storeImage($model, $model->picture_thumb);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture);
|
||||
$model->picture_thumb = FileStorageHelper::storeFile($model, $model->picture_thumb);
|
||||
if (!$model->status) {
|
||||
$model->publish_at = null;
|
||||
} else {
|
||||
|
|
@ -121,11 +122,11 @@ class Product extends Model
|
|||
$model->allergen_list = clean($model->allergen_list);
|
||||
// * Store new picture if changed
|
||||
if ($model->picture != $model->getOriginal('picture')) {
|
||||
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture);
|
||||
}
|
||||
// * Store new picture_thumb if changed
|
||||
if ($model->picture_thumb != $model->getOriginal('picture_thumb')) {
|
||||
$model->picture_thumb = ToolboxHelper::storeImage($model, $model->picture_thumb);
|
||||
$model->picture_thumb = FileStorageHelper::storeFile($model, $model->picture_thumb);
|
||||
}
|
||||
if ($model->link1) {
|
||||
self::validateLinkAttribute('link1', $model->link1);
|
||||
|
|
@ -141,12 +142,12 @@ class Product extends Model
|
|||
ToolboxHelper::removeUnusedImagesOnCkEditor($model, 'sales_pitch');
|
||||
ToolboxHelper::removeUnusedImagesOnCkEditor($model, 'allergen_list');
|
||||
if ($model->picture != $model->getOriginal('picture')) {
|
||||
$model->picture = ToolboxHelper::removeOldImage($model, 'picture');
|
||||
$model->picture = FileStorageHelper::removeOldFile($model, 'picture');
|
||||
}
|
||||
});
|
||||
static::deleting(function (self $model) {
|
||||
ToolboxHelper::removeImage($model, 'picture');
|
||||
ToolboxHelper::removeImage($model, 'picture_thumb');
|
||||
FileStorageHelper::removeFile($model, 'picture');
|
||||
FileStorageHelper::removeFile($model, 'picture_thumb');
|
||||
ToolboxHelper::removeImagesOnCkEditor($model, 'description');
|
||||
ToolboxHelper::removeImagesOnCkEditor($model, 'ingredients');
|
||||
ToolboxHelper::removeImagesOnCkEditor($model, 'preparation_advices');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Enums\Ranges\RangeCategoryEnum;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use App\Lib\Helpers\FileStorageHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
|
@ -78,8 +79,8 @@ class Range extends Model
|
|||
$model->slug = Str::slug($model->title);
|
||||
$model->description = clean($model->description);
|
||||
$model->argumentation = clean($model->argumentation);
|
||||
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
|
||||
$model->picture_thumb = ToolboxHelper::storeImage($model, $model->picture_thumb);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture);
|
||||
$model->picture_thumb = FileStorageHelper::storeFile($model, $model->picture_thumb);
|
||||
if (!$model->status) {
|
||||
$model->publish_at = null;
|
||||
} else {
|
||||
|
|
@ -97,10 +98,10 @@ class Range extends Model
|
|||
ToolboxHelper::assertNoInternalBoUrls('description', $model->description);
|
||||
ToolboxHelper::assertNoInternalBoUrls('argumentation', $model->argumentation);
|
||||
if ($model->picture != $model->getOriginal('picture')) {
|
||||
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture);
|
||||
}
|
||||
if ($model->picture_thumb != $model->getOriginal('picture_thumb')) {
|
||||
$model->picture_thumb = ToolboxHelper::storeImage($model, $model->picture_thumb);
|
||||
$model->picture_thumb = FileStorageHelper::storeFile($model, $model->picture_thumb);
|
||||
}
|
||||
// * If status changed
|
||||
if (\boolval($model->getOriginal('status')) !== $model->status) {
|
||||
|
|
@ -121,15 +122,15 @@ class Range extends Model
|
|||
});
|
||||
static::updated(function (self $model) {
|
||||
if ($model->picture != $model->getOriginal('picture')) {
|
||||
$model->picture = ToolboxHelper::removeOldImage($model, 'picture');
|
||||
$model->picture = FileStorageHelper::removeOldFile($model, 'picture');
|
||||
}
|
||||
if ($model->picture_v != $model->getOriginal('picture_v')) {
|
||||
$model->picture_v = ToolboxHelper::removeOldImage($model, 'picture_v');
|
||||
$model->picture_v = FileStorageHelper::removeOldFile($model, 'picture_v');
|
||||
}
|
||||
});
|
||||
static::deleting(function (self $model) {
|
||||
ToolboxHelper::removeImage($model, 'picture');
|
||||
ToolboxHelper::removeImage($model, 'picture_v');
|
||||
FileStorageHelper::removeFile($model, 'picture');
|
||||
FileStorageHelper::removeFile($model, 'picture_v');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ namespace App\Models;
|
|||
use App\Casts\RgbaColorNullable;
|
||||
use App\Enums\Sliders\SliderElementPosition;
|
||||
use App\Enums\Sliders\SliderType;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use App\Lib\Helpers\FileStorageHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
|
@ -84,7 +85,7 @@ class Slider extends Model
|
|||
{
|
||||
static::creating(function (self $model) {
|
||||
ToolboxHelper::assertNoInternalBoUrls('link', $model->link);
|
||||
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture);
|
||||
$model->order = self::whereType($model->type)->max('order') ?? 0;
|
||||
if (!$model->status) {
|
||||
$model->publish_at = null;
|
||||
|
|
@ -95,7 +96,7 @@ class Slider extends Model
|
|||
static::updating(function (self $model) {
|
||||
ToolboxHelper::assertNoInternalBoUrls('link', $model->link);
|
||||
if ($model->picture != $model->getOriginal('picture')) {
|
||||
$model->picture = ToolboxHelper::storeImage($model, $model->picture);
|
||||
$model->picture = FileStorageHelper::storeFile($model, $model->picture);
|
||||
}
|
||||
// * If status changed
|
||||
if (\boolval($model->getOriginal('status')) !== $model->status) {
|
||||
|
|
@ -110,11 +111,11 @@ class Slider extends Model
|
|||
});
|
||||
static::updated(function (self $model) {
|
||||
if ($model->picture != $model->getOriginal('picture')) {
|
||||
$model->picture = ToolboxHelper::removeOldImage($model, 'picture');
|
||||
$model->picture = FileStorageHelper::removeOldFile($model, 'picture');
|
||||
}
|
||||
});
|
||||
static::deleting(function (self $model) {
|
||||
ToolboxHelper::removeImage($model, 'picture');
|
||||
FileStorageHelper::removeFile($model, 'picture');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use App\Models\News;
|
||||
use App\Models\Product;
|
||||
use App\Models\Range;
|
||||
|
|
|
|||
25
app/Providers/ViewServiceProvider.php
Normal file
25
app/Providers/ViewServiceProvider.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Engagement;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
class ViewServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// Shares this data with all views.
|
||||
if (Schema::hasTable('engagements')) {
|
||||
$globalEngagements = Engagement::get();
|
||||
View::share('globalEngagements', $globalEngagements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
namespace App\Traits\Models;
|
||||
|
||||
use App\Enums\InputType;
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use App\Lib\Helpers\FileStorageHelper;
|
||||
|
||||
trait MiniCrudModel
|
||||
{
|
||||
|
|
@ -57,7 +58,7 @@ trait MiniCrudModel
|
|||
foreach ($data as $o => $input) {
|
||||
switch ($input['inputType']->value ?? $input['inputType']['value']) {
|
||||
case InputType::image()->value:
|
||||
$data[$o]['value'] = ToolboxHelper::storeImage($this, $input['value']);
|
||||
$data[$o]['value'] = FileStorageHelper::storeFile($this, $input['value']);
|
||||
break;
|
||||
case InputType::boolean()->value:
|
||||
case InputType::text()->value:
|
||||
|
|
@ -73,7 +74,7 @@ trait MiniCrudModel
|
|||
'Unknown input Type %s',
|
||||
$input['inputType']->label ?? $input['inputType']['label']
|
||||
));
|
||||
}
|
||||
} //end switch
|
||||
} //end foreach
|
||||
return json_encode($data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ trait HasPicture
|
|||
];
|
||||
return [
|
||||
$name => [
|
||||
'required',
|
||||
'sometimes',
|
||||
function (string $attribute, $value, callable $fail) use ($sizes) {
|
||||
return $this->validatePicture($attribute, $value, $fail, $sizes);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ return [
|
|||
App\Providers\EventServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\TranslationServiceProvider::class,
|
||||
|
||||
App\Providers\ViewServiceProvider::class
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
@ -191,7 +191,6 @@ return [
|
|||
*/
|
||||
|
||||
'aliases' => [
|
||||
|
||||
'App' => Illuminate\Support\Facades\App::class,
|
||||
'Arr' => Illuminate\Support\Arr::class,
|
||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
||||
|
|
@ -229,8 +228,6 @@ return [
|
|||
'Str' => Illuminate\Support\Str::class,
|
||||
'URL' => Illuminate\Support\Facades\URL::class,
|
||||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
|
||||
],
|
||||
|
||||
'View' => Illuminate\Support\Facades\View::class
|
||||
]
|
||||
];
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,15 +8,6 @@ return [
|
|||
'ranges' => 'Ranges',
|
||||
'products' => 'Products',
|
||||
'recipes' => 'Recipes',
|
||||
'breeders' => 'Breeders',
|
||||
'engagements' => [
|
||||
'title' => 'Engagements',
|
||||
'renewableEnergies' => 'Renewable energy',
|
||||
'localCereals' => 'Sustainable livestock / Local cereals',
|
||||
'agroforestry' => 'Agroforestry',
|
||||
'cereals' => 'Cereals',
|
||||
'animalWelfare' => 'Animal welfare',
|
||||
'beekeeping' => 'Beekeeping',
|
||||
],
|
||||
'breeders' => 'Breeders'
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -9,14 +9,6 @@ return [
|
|||
'products' => 'Produits',
|
||||
'recipes' => 'Recettes',
|
||||
'breeders' => 'Éleveurs',
|
||||
'engagements' => [
|
||||
'title' => 'Engagements',
|
||||
'renewableEnergies' => 'Énergies renouvelables',
|
||||
'localCereals' => 'Élevage durable / Céréales locales',
|
||||
'agroforestry' => 'Agroforesteries',
|
||||
'cereals' => 'Céréales',
|
||||
'animalWelfare' => 'Bien être animal',
|
||||
'beekeeping' => 'Apiculture',
|
||||
],
|
||||
'engagements' => 'Engagements'
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -137,9 +137,6 @@ export default defineComponent({
|
|||
"|",
|
||||
"removeFormat",
|
||||
"|",
|
||||
"undo",
|
||||
"redo",
|
||||
"|",
|
||||
"sourceEditing",
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@
|
|||
class="fw-bold"
|
||||
>
|
||||
{{ humanFileSize(intOriginalFile.size) }}
|
||||
{{ `hauteur ${intOriginalFileDimensions.height}px` }}
|
||||
{{ `largeur ${intOriginalFileDimensions.width}px` }}
|
||||
{{ `hauteur ${intOriginalFileDimensions.height}px` }}
|
||||
</span>
|
||||
</small>
|
||||
<img
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
@extends('layouts.back')
|
||||
|
||||
@section('title', $engagement->title)
|
||||
@section('title', $engagement->title_simple )
|
||||
|
||||
@section('breadcrumb', request()->route()->getName())
|
||||
|
||||
|
|
@ -11,13 +11,13 @@
|
|||
@method('PUT')
|
||||
@csrf
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="col-md-12">
|
||||
@include('back.engagements.form')
|
||||
@include('back.modules.minicrud-form-inputs', ['model' => $engagement])
|
||||
<div class="form-group text-center mt-3">
|
||||
<button type="submit" class="btn btn-primary"
|
||||
title="{{ __('Modifier la page :model en base', [
|
||||
'model' => $engagement->title
|
||||
'model' => $engagement->title_simple
|
||||
]) }}" data-bs-toggle="tooltip">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,93 +1,132 @@
|
|||
<div class="form-group">
|
||||
<label for="engagementTitle" class="col-form-label"><b>{{ __('Titre') }}*</b></label>
|
||||
<div class="col-12 form-group">
|
||||
@php
|
||||
$data = [
|
||||
'name' => 'title',
|
||||
'text' => old('title', $engagement->title ?? ''),
|
||||
];
|
||||
@endphp
|
||||
<div class="ck-editor-bo" data-json='@json($data)'></div>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'title',
|
||||
'helperId' => 'engagementTitleHelp',
|
||||
'helper' => __('Doit être un titre court (max 35 caractères)')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="w-25 mx-auto">
|
||||
|
||||
<div class="row ">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementSeoTitle" class="col-form-label"><b>{{ __('Titre seo') }}*</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'engagementSeoTitle'])'></div>
|
||||
<input name="seo_title" type="text" value="{{ old('seo_title', $engagement->seo_title ?? '') }}" class="form-control"
|
||||
id="engagementSeoTitle" aria-describedby="engagementSeoTitleHelp" placeholder="{{ __('Un titre en 70 caractères maximum') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'seo_title', 'helperId' => 'engagementSeoTitleHelp' , 'helper' => __('Doit décrire précisément la page, recommandation pour un positionnement optimal dans les moteurs de recherche : entre 53 et 60 caractères, balise <title> ')
|
||||
])
|
||||
</div>
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementSeoDescription" class="col-form-label"><b>{{ __('Description seo') }}*</b></label>
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'engagementSeoDescription',
|
||||
'class' => 'form-control',
|
||||
'name' => 'seo_description',
|
||||
'placeholder' => __('Une description du contenu de la page pour les moteurs de recherche'),
|
||||
'ariaDescribedby' => 'engagementSeoDescriptionHelp',
|
||||
'value' => old('engagementSeoDescription', $engagement->seo_description),
|
||||
'required' => true,
|
||||
'value' => old('seo_description', $engagement->seo_description ?? '')
|
||||
];
|
||||
@endphp
|
||||
<div id="input-text-auto" data-json='@json($data)'></div>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'seo_description',
|
||||
'helperId' => 'engagementSeoDescriptionHelp',
|
||||
'helper' => __(' Doit décrire précisément le contenu de la page, requiert entre 50 et 160 caractères, balise <meta> description')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="w-25 mx-auto">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementBannerAlt" class="col-form-label"><b>{{ __('Texte alternatif de la bannière') }}*</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'engagementBannerAlt'])'></div>
|
||||
<input name="banner_alt" type="text" value="{{ old('banner_alt', $engagement->banner_alt ?? '') }}" class="form-control"
|
||||
id="engagementBannerAlt" aria-describedby="engagementBannerAltHelp" placeholder="{{ __('Une image de petits chatons') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'banner_alt',
|
||||
'helperId' => 'engagementBannerAltHelp',
|
||||
'helper' => __('Doit décrire votre bannière pour les moteurs de recherches et les non voyants (5 à 6 mots)')
|
||||
])
|
||||
</div>
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementBannerTitle" class="col-form-label"><b>{{ __('Titre de la bannière') }}*</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'engagementBannerTitle'])'></div>
|
||||
<input name="banner_title" type="text" value="{{ old('banner_title', $engagement->banner_title ?? '') }}" class="form-control"
|
||||
id="engagementBannerTitle" aria-describedby="engagementBannerTitleHelp" placeholder="{{ __('Une description de l\'article pour les moteurs de recherche') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'banner_title',
|
||||
'helperId' => 'engagementBannerTitleHelp' ,
|
||||
'helper' => __('Utile pour les utilisateurs, s\'affiche en passant la souris sur la bannière')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'engagementBanner',
|
||||
'name' => 'banner',
|
||||
'helper' => __('L\'image qui va servir de bannière, doit être de 1920px (largeur) par 340px (hauteur)'),
|
||||
'value' => $engagement->banner ?? '',
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', ['inputName' => 'banner'])
|
||||
<div id="image-input" data-json='@json($data)'></div>
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend>{{ __('Informations générales') }}</legend>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementTitle" class="col-form-label"><b>{{ __('Titre') }}</b></label>
|
||||
@php
|
||||
$data = [
|
||||
'name' => 'title',
|
||||
'csrf' => csrf_token(),
|
||||
'text' => old('title', $engagement->title ?? ''),
|
||||
];
|
||||
@endphp
|
||||
<div id="ck-editor-bo" data-json='@json($data)'></div>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'title',
|
||||
'helperId' => 'engagementTitleHelp' ,
|
||||
'helper' => __('Doit être un titre court (max 35 caractères).')
|
||||
])
|
||||
</div>
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementTitreSimple" class="col-form-label">
|
||||
<b>{{ __('Titre sans style') }}*</b>
|
||||
</label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'engagementTitreSimple'])'></div>
|
||||
<input name="title_simple" type="text" value="{{ old('title_simple', $engagement->title_simple ?? '') }}" class="form-control"
|
||||
id="engagementTitreSimple" aria-describedby="engagementTitreSimpleHelp" placeholder="{{ __('Une image de petits chatons') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'title_simple',
|
||||
'helperId' => 'engagementTitreSimpleHelp',
|
||||
'helper' => __('Doit être un titre court (max 35 caractères) sans style.')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend>{{ __('Seo') }}</legend>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementSeoTitle" class="col-form-label">
|
||||
<b>{{ __('Titre seo') }}*</b>
|
||||
</label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'engagementSeoTitle'])'></div>
|
||||
<input name="seo_title" type="text" value="{{ old('seo_title', $engagement->seo_title ?? '') }}" class="form-control"
|
||||
id="engagementSeoTitle" aria-describedby="engagementSeoTitleHelp" placeholder="{{ __('Un titre en 70 caractères maximum') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'seo_title',
|
||||
'helperId' => 'engagementSeoTitleHelp' ,
|
||||
'helper' => __('Doit décrire précisément la page, recommandation pour un positionnement optimal dans les moteurs de recherche : entre 53 et 60 caractères, balise <title>.')
|
||||
])
|
||||
</div>
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementSeoDescription" class="col-form-label">
|
||||
<b>{{ __('Description seo') }}</b>
|
||||
</label>
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'engagementSeoDescription',
|
||||
'class' => 'form-control',
|
||||
'name' => 'seo_description',
|
||||
'placeholder' => __('Une description du contenu de la page pour les moteurs de recherche.'),
|
||||
'ariaDescribedby' => 'engagementSeoDescriptionHelp',
|
||||
'value' => old('engagementSeoDescription', $engagement->seo_description),
|
||||
'required' => true,
|
||||
'value' => old('seo_description', $engagement->seo_description ?? '')
|
||||
];
|
||||
@endphp
|
||||
<div id="input-text-auto" data-json='@json($data)'></div>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'seo_description',
|
||||
'helperId' => 'engagementSeoDescriptionHelp',
|
||||
'helper' => __(' Doit décrire précisément le contenu de la page, requiert entre 50 et 160 caractères, balise <meta> description.')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend>{{ __('Bannière') }}</legend>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementBannerAlt" class="col-form-label"><b>{{ __('Texte alternatif de la bannière') }}*</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'engagementBannerAlt'])'></div>
|
||||
<input name="banner_alt" type="text" value="{{ old('banner_alt', $engagement->banner_alt ?? '') }}" class="form-control"
|
||||
id="engagementBannerAlt" aria-describedby="engagementBannerAltHelp" placeholder="{{ __('Une image de petits chatons') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'banner_alt',
|
||||
'helperId' => 'engagementBannerAltHelp',
|
||||
'helper' => __('Doit décrire votre bannière pour les moteurs de recherches et les non voyants (5 à 6 mots).')
|
||||
])
|
||||
</div>
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="engagementBannerTitle" class="col-form-label"><b>{{ __('Titre de la bannière') }}*</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => 'engagementBannerTitle'])'></div>
|
||||
<input name="banner_title" type="text" value="{{ old('banner_title', $engagement->banner_title ?? '') }}" class="form-control"
|
||||
id="engagementBannerTitle" aria-describedby="engagementBannerTitleHelp" placeholder="{{ __('Une description de l\'article pour les moteurs de recherche') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'banner_title',
|
||||
'helperId' => 'engagementBannerTitleHelp' ,
|
||||
'helper' => __('Utile pour les utilisateurs, s\'affiche en passant la souris sur la bannière.')
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@php
|
||||
$data = [
|
||||
'id' => 'engagementBanner',
|
||||
'name' => 'banner',
|
||||
'helper' => __('L\'image qui va servir de bannière, doit être de 1920px (largeur) par 340px (hauteur).'),
|
||||
'value' => $engagement->banner ?? '',
|
||||
'width' => $engagement->banner_dimensions['largeur'],
|
||||
'height' => $engagement->banner_dimensions['hauteur']
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', ['inputName' => 'banner'])
|
||||
<div id="image-input" data-json='@json($data)'></div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,59 +1,57 @@
|
|||
@auth('backend')
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(request()->routeIs('bo.news.*')) active @endif" href="{{ route('bo.news.index') }}" title="{{ __('Administrer les :model', [
|
||||
'model' => __('models.classes.news')
|
||||
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.news') }}</a>
|
||||
<a class="nav-link @if(request()->routeIs('bo.news.*')) active @endif" href="{{ route('bo.news.index') }}"
|
||||
title="{{ __('Administrer les :model', ['model' => __('models.classes.news')]) }}"
|
||||
data-bs-toggle="tooltip">
|
||||
{{ __('models.classes.news') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(request()->routeIs('bo.tags.*')) active @endif" href="{{ route('bo.tags.index') }}" title="{{ __('Administrer les :model', [
|
||||
'model' => __('models.classes.tags')
|
||||
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.tags') }}</a>
|
||||
<a class="nav-link @if(request()->routeIs('bo.tags.*')) active @endif" href="{{ route('bo.tags.index') }}"
|
||||
title="{{ __('Administrer les :model', ['model' => __('models.classes.tags')]) }}"
|
||||
data-bs-toggle="tooltip">
|
||||
{{ __('models.classes.tags') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(request()->routeIs('bo.sliders.*')) active @endif" href="{{ route('bo.sliders.index') }}" title="{{ __('Administrer les :model', [
|
||||
'model' => __('models.classes.sliders')
|
||||
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.sliders') }}</a>
|
||||
<a class="nav-link @if(request()->routeIs('bo.sliders.*')) active @endif" href="{{ route('bo.sliders.index') }}"
|
||||
title="{{ __('Administrer les :model', ['model' => __('models.classes.sliders')]) }}"
|
||||
data-bs-toggle="tooltip">
|
||||
{{ __('models.classes.sliders') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(request()->routeIs('bo.ranges.*')) active @endif" href="{{ route('bo.ranges.index') }}" title="{{ __('Administrer les :model', [
|
||||
'model' => __('models.classes.ranges')
|
||||
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.ranges') }}</a>
|
||||
<a class="nav-link @if(request()->routeIs('bo.ranges.*')) active @endif" href="{{ route('bo.ranges.index') }}"
|
||||
title="{{ __('Administrer les :model', ['model' => __('models.classes.ranges')]) }}"
|
||||
data-bs-toggle="tooltip">
|
||||
{{ __('models.classes.ranges') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(request()->routeIs('bo.products.*')) active @endif" href="{{ route('bo.products.index') }}" title="{{ __('Administrer les :model', [
|
||||
'model' => __('models.classes.products')
|
||||
]) }}" data-bs-toggle="tooltip">{{ __('models.classes.products') }}</a>
|
||||
<a class="nav-link @if(request()->routeIs('bo.products.*')) active @endif" href="{{ route('bo.products.index') }}"
|
||||
title="{{ __('Administrer les :model', ['model' => __('models.classes.products')]) }}"
|
||||
data-bs-toggle="tooltip">
|
||||
{{ __('models.classes.products') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle @if(request()->routeIs('bo.engagements.*')) active @endif" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ __('models.classes.engagements.title') }}
|
||||
<a class="nav-link dropdown-toggle @if(request()->routeIs('bo.engagements.*')) active @endif"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
{{ __('models.classes.engagements') }}
|
||||
</a>
|
||||
<div class="dropdown-menu p-0">
|
||||
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'energies-renouvelables') }}" title="{{ __('Administrer la page :model', [
|
||||
'model' => __('models.classes.engagements.renewableEnergies')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
|
||||
{{ __('models.classes.engagements.renewableEnergies') }}
|
||||
</a>
|
||||
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'elevage-durable-cereales-locales') }}" title="{{ __('Administrer la page :model', [
|
||||
'model' => __('models.classes.engagements.localCereals')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
|
||||
{{ __('models.classes.engagements.localCereals') }}
|
||||
</a>
|
||||
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'agroforesterie') }}" title="{{ __('Administrer la page :model', [
|
||||
'model' => __('models.classes.engagements.agroforestry')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
|
||||
{{ __('models.classes.engagements.agroforestry') }}
|
||||
</a>
|
||||
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'cereales') }}" title="{{ __('Administrer la page :model', [
|
||||
'model' => __('models.classes.engagements.cereals')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
|
||||
{{ __('models.classes.engagements.cereals') }}
|
||||
</a>
|
||||
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'bien-etre-animal') }}" title="{{ __('Administrer la page :model', [
|
||||
'model' => __('models.classes.engagements.animalWelfare')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
|
||||
{{ __('models.classes.engagements.animalWelfare') }}
|
||||
</a>
|
||||
<a class="nav-link dropdown-item" href="{{ route('bo.engagements.edit', 'apiculture') }}" title="{{ __('Administrer la page :model', [
|
||||
'model' => __('models.classes.engagements.beekeeping')]) }}" data-bs-toggle="tooltip" data-bs-placement="right">
|
||||
{{ __('models.classes.engagements.beekeeping') }}
|
||||
</a>
|
||||
@foreach ($globalEngagements as $globalEngagement)
|
||||
<a class="nav-link dropdown-item"
|
||||
href="{{ route('bo.engagements.edit', $globalEngagement->slug) }}"
|
||||
title="{{ __('Administrer la page :model', ['model' => $globalEngagement->title_simple]) }}"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="right">
|
||||
{{ $globalEngagement->title_simple }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<div id="minicrud">
|
||||
@foreach ($model->data as $k => $input)
|
||||
<hr class="w-25 mx-auto">
|
||||
@php
|
||||
$input = json_decode(json_encode($input), true);
|
||||
@endphp
|
||||
|
|
@ -8,118 +7,197 @@
|
|||
@case(App\Enums\InputType::url()->value)
|
||||
|
||||
@case(App\Enums\InputType::string()->value)
|
||||
<div class="form-group">
|
||||
<label for="{{$k}}"><b>{{ $input['label'] }}*</b></label>
|
||||
<div class="word-counter" data-json='@json(['id' => $k])'></div>
|
||||
<input id="{{$k}}" type="text" class="form-control" name="data[{{$k}}][value]"
|
||||
placeholder="{{ $input['label'] }}" value="{{ old("data.$k.value") ?? ($input['value'] ?? '') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][value]',
|
||||
'helperId' => '{{$k}}StringHelp',
|
||||
'helper' => $input['helper'],
|
||||
])
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<div class="d-flex flex-row justify-content-start align-items-center mb-2">
|
||||
<legend class="w-auto m-0">{{ $input['label'] }}</legend>
|
||||
@if(!empty($input['tooltip']))
|
||||
<span class="ms-1" data-bs-toggle="tooltip" data-bs-html="true" title="{{ $input['tooltip'] }}">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="form-group">
|
||||
<div class="word-counter" data-json='@json(['id' => $k])'></div>
|
||||
<input id="{{$k}}" type="text" class="form-control" name="data[{{$k}}][value]"
|
||||
placeholder="{{ $input['label'] }}" value="{{ old("data.$k.value") ?? ($input['value'] ?? '') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][value]',
|
||||
'helperId' => '{{$k}}StringHelp',
|
||||
'helper' => $input['helper'],
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
@break
|
||||
|
||||
@case(App\Enums\InputType::number()->value)
|
||||
<div class="form-group">
|
||||
<label for="{{$k}}"><b>{{ $input['label'] }}*</b></label>
|
||||
<input id="{{$k}}" type="number" step="0.01" class="form-control"
|
||||
name="data[{{ $k }}][value]" placeholder="{{ $input['label'] }}"
|
||||
value="{{ old("data.$k.value") ?? ($input['value'] ?? '') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][value]',
|
||||
'helperId' => '{{$k}}NumberHelp',
|
||||
'helper' => $input['helper'],
|
||||
])
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<div class="d-flex flex-row justify-content-start align-items-center mb-2">
|
||||
<legend class="w-auto m-0">{{ $input['label'] }}</legend>
|
||||
@if(!empty($input['tooltip']))
|
||||
<span class="ms-1" data-bs-toggle="tooltip" data-bs-html="true" title="{{ $input['tooltip'] }}">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="form-group">
|
||||
<input id="{{$k}}" type="number" step="0.01" class="form-control"
|
||||
name="data[{{ $k }}][value]" placeholder="{{ $input['label'] }}"
|
||||
value="{{ old("data.$k.value") ?? ($input['value'] ?? '') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][value]',
|
||||
'helperId' => '{{$k}}NumberHelp',
|
||||
'helper' => $input['helper'],
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
@break
|
||||
|
||||
@case(App\Enums\InputType::text()->value)
|
||||
<div class="form-group vueComponent">
|
||||
<label class="col-form-label"><b>{{ $input['label'] }}*</b></label>
|
||||
@php
|
||||
$editor = [
|
||||
'csrf' => csrf_token(),
|
||||
'name' => "data[$k][text]",
|
||||
'text' => old("data.$k.text") ?? ($input['text'] ?? ''),
|
||||
];
|
||||
@endphp
|
||||
<div class="ck-editor-bo" data-json='@json($editor)'></div>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][text]',
|
||||
'helperId' => '{{$k}}TextHelp',
|
||||
'helper' => $input['helper'],
|
||||
])
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<div class="d-flex flex-row justify-content-start align-items-center mb-2">
|
||||
<legend class="w-auto m-0">{{ $input['label'] }}</legend>
|
||||
@if(!empty($input['tooltip']))
|
||||
<span class="ms-1" data-bs-toggle="tooltip" data-bs-html="true" title="{{ $input['tooltip'] }}">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="form-group vueComponent">
|
||||
@php
|
||||
$editor = [
|
||||
'csrf' => csrf_token(),
|
||||
'name' => "data[$k][text]",
|
||||
'text' => old("data.$k.text") ?? ($input['text'] ?? ''),
|
||||
];
|
||||
@endphp
|
||||
<div class="ck-editor-bo" data-json='@json($editor)'></div>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][text]',
|
||||
'helperId' => '{{$k}}TextHelp',
|
||||
'helper' => $input['helper'],
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
@break
|
||||
|
||||
@case(App\Enums\InputType::boolean()->value)
|
||||
<div class="form-group">
|
||||
<label class="user-select-none">
|
||||
<input type="checkbox" name="data[{{$k}}][value]" value="1"
|
||||
@if (old("data.$k.value") ?? ($input['value'] ?? 0)) checked @endif>
|
||||
{{ $input['label'] }}*
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][value]',
|
||||
'helperId' => '{{$k}}BooleanHelp',
|
||||
'helper' => $input['helper'],
|
||||
])
|
||||
</label>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<div class="d-flex flex-row justify-content-start align-items-center mb-2">
|
||||
<legend class="w-auto m-0">{{ $input['label'] }}</legend>
|
||||
@if(!empty($input['tooltip']))
|
||||
<span class="ms-1" data-bs-toggle="tooltip" data-bs-html="true" title="{{ $input['tooltip'] }}">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="form-group">
|
||||
<label class="user-select-none">
|
||||
<input type="checkbox" name="data[{{$k}}][value]" value="1"
|
||||
@if (old("data.$k.value") ?? ($input['value'] ?? 0)) checked @endif>
|
||||
{{ $input['label'] }}*
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][value]',
|
||||
'helperId' => '{{$k}}BooleanHelp',
|
||||
'helper' => $input['helper'],
|
||||
])
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
@break
|
||||
|
||||
@case(App\Enums\InputType::image()->value)
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="{{$k}}alt" class="col-form-label">
|
||||
<b>{{ __('Texte alternatif de l\'image') }}</b>
|
||||
</label>
|
||||
<div class="word-counter" data-json='@json(['id' => $k.'alt'])'></div>
|
||||
<input name="data[{{$k}}][alt]" type="text"
|
||||
value="{{ old("data.$k.alt") ?? $input['alt'] ?? '' }}"
|
||||
class="form-control" id="{{$k}}alt"
|
||||
aria-describedby="{{$k}}altHelp"
|
||||
placeholder="{{ __('Texte alternatif à l\'image') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][alt]',
|
||||
'helperId' => '{{$k}}altHelp',
|
||||
'helper' => __(
|
||||
'Doit décrire votre image pour les moteurs de recherches et les non voyants (5 à 6 mots)'
|
||||
),
|
||||
])
|
||||
</div>
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="{{$k}}title" class="col-form-label">
|
||||
<b>{{ __('Titre de l\'image') }}</b>
|
||||
</label>
|
||||
<div class="word-counter" data-json='@json(['id' => $k.'title'])'></div>
|
||||
<input name="data[{{$k}}][title]" type="text"
|
||||
value="{{ old("data.$k.title") ?? $input['title'] ?? '' }}"
|
||||
class="form-control" id="{{$k}}title"
|
||||
aria-describedby="{{$k}}titleHelp"
|
||||
placeholder="{{ __('Titre de l\'image') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][title]',
|
||||
'helperId' => '{{$k}}titleHelp',
|
||||
'helper' => __(
|
||||
'Utile pour les utilisateurs, s\'affiche en passant la souris sur l\'image'
|
||||
),
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@php
|
||||
$data = [
|
||||
'id' => $k . 'picture',
|
||||
'name' => 'data[' . $k . '][value]',
|
||||
'helper' => __('L\'image qui va servir de vignette à l\'article, doit être de ') . $input['dimensions'],
|
||||
'value' => $input['value'] ?? '',
|
||||
];
|
||||
@endphp
|
||||
@include('back.modules.input-error', ['inputName' => 'data[' . $k . '][value]'])
|
||||
<div class="image-input" data-json='@json($data)'></div>
|
||||
<div class="col">
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<div class="d-flex flex-row justify-content-start align-items-center mb-2">
|
||||
<legend class="w-auto m-0">{{ $input['label'] }}</legend>
|
||||
@if(!empty($input['tooltip']))
|
||||
<span class="ms-1" data-bs-toggle="tooltip" data-bs-html="true" title="{{ $input['tooltip'] }}">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="{{$k}}alt" class="col-form-label">
|
||||
<b>{{ __('Texte alternatif de l\'image') }}</b>
|
||||
</label>
|
||||
<div class="word-counter" data-json='@json(['id' => $k.'alt'])'></div>
|
||||
<input name="data[{{$k}}][alt]" type="text"
|
||||
value="{{ old("data.$k.alt") ?? $input['alt'] ?? '' }}"
|
||||
class="form-control" id="{{$k}}alt"
|
||||
aria-describedby="{{$k}}altHelp"
|
||||
placeholder="{{ __('Texte alternatif à l\'image') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][alt]',
|
||||
'helperId' => '{{$k}}altHelp',
|
||||
'helper' => __(
|
||||
'Doit décrire votre image pour les moteurs de recherches et les non voyants (5 à 6 mots)'
|
||||
),
|
||||
])
|
||||
</div>
|
||||
<div class="col-12 col-md-6 form-group">
|
||||
<label for="{{$k}}title" class="col-form-label">
|
||||
<b>{{ __('Titre de l\'image') }}</b>
|
||||
</label>
|
||||
<div class="word-counter" data-json='@json(['id' => $k.'title'])'></div>
|
||||
<input name="data[{{$k}}][title]" type="text"
|
||||
value="{{ old("data.$k.title") ?? $input['title'] ?? '' }}"
|
||||
class="form-control" id="{{$k}}title"
|
||||
aria-describedby="{{$k}}titleHelp"
|
||||
placeholder="{{ __('Titre de l\'image') }}" required>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'data[{{$k}}][title]',
|
||||
'helperId' => '{{$k}}titleHelp',
|
||||
'helper' => __(
|
||||
'Utile pour les utilisateurs, s\'affiche en passant la souris sur l\'image'
|
||||
),
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@php
|
||||
$data = [
|
||||
'id' => $k . 'picture',
|
||||
'name' => 'data[' . $k . '][value]',
|
||||
'helper' => __("L'image qui va servir de vignette à l'article, doit être de " .
|
||||
$input['dimensions']['largeur'] . "px (largeur) et " .
|
||||
$input['dimensions']['hauteur'] . "px (hauteur)."),
|
||||
'value' => $input['value'] ?? '',
|
||||
'width' => $input['dimensions']['largeur'],
|
||||
'height' => $input['dimensions']['hauteur']
|
||||
];
|
||||
@endphp
|
||||
<div class="image-input" data-json='@json($data)'></div>
|
||||
@include('back.modules.input-error', ['inputName' => 'data[' . $k . '][value]'])
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
@break
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
@csrf
|
||||
@method('PUT')
|
||||
@include('back.news.form', compact('newsModel'))
|
||||
<button type="submit" class="btn btn-primary" title="{{ __('Enregister :model en base', [
|
||||
<button type="submit" class="btn btn-primary" title="{{ __('Enregister :model en base', [
|
||||
'model' => Str::singular(__('models.classes.news'))
|
||||
]) }}" data-bs-toggle="tooltip">{{ __('Enregister') }}</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -282,11 +282,6 @@
|
|||
<legend>{{ __('Description du produit') }}</legend>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 form-group">
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'description',
|
||||
'helperId' => 'productModelDescriptionHelp',
|
||||
'helper' => __('Relisez bien votre description avant de la publier.'),
|
||||
])
|
||||
@php
|
||||
$data = [
|
||||
'name' => 'description',
|
||||
|
|
@ -294,6 +289,11 @@
|
|||
];
|
||||
@endphp
|
||||
<div class="ck-editor-bo" data-json='@json($data)'></div>
|
||||
@include('back.modules.input-error', [
|
||||
'inputName' => 'description',
|
||||
'helperId' => 'productModelDescriptionHelp',
|
||||
'helper' => __('Relisez bien votre description avant de la publier.'),
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
|||
|
|
@ -62,9 +62,12 @@ Route::prefix('bo')->name('bo.')->group(function () {
|
|||
Route::prefix('engagements')
|
||||
->name('engagements.')
|
||||
->group(function () {
|
||||
Route::get('/{engagement:slug}/edit', [EngagementsController::class, 'edit'])->name('edit');
|
||||
Route::get('/{engagement:slug}/edit', [EngagementsController::class, 'edit'])
|
||||
->name('edit');
|
||||
Route::match(['post', 'put'], '/{engagement}', [EngagementsController::class, 'update'])
|
||||
->name('update');
|
||||
->name('update')
|
||||
->withoutMiddleware('htmlsanitize')
|
||||
->middleware('htmlsanitize:title,text');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue