Merge branch 'develop' into trunk

This commit is contained in:
Olivier Legat 2022-10-11 17:54:33 +02:00
commit 102a0d8048
34 changed files with 1711 additions and 1047 deletions

View file

@ -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
];
}
}

View file

@ -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);
}

View file

@ -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')
];

View file

@ -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',

View file

@ -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"]
);
}
]
];

View file

@ -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;
}
}

View 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;
}
}

View 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));
}
});
}
}

View file

@ -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;
}
}

View file

@ -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');
});
}
}

View file

@ -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');
});
}

View file

@ -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');

View file

@ -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');
});
}

View file

@ -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;

View file

@ -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');
});
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View 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);
}
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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
]
];

View file

@ -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'
]
];

View file

@ -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'
]
];

View file

@ -137,9 +137,6 @@ export default defineComponent({
"|",
"removeFormat",
"|",
"undo",
"redo",
"|",
"sourceEditing",
],
},

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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');
});
});
});