ldc-loue/app/Lib/ToolboxHelper.php
2022-06-01 14:29:31 +02:00

419 lines
16 KiB
PHP

<?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\Session;
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;
}
/**
* 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;
}
/**
* Generate url using laravel route passing our group query if present
* This is used to keep ou group tracking
* Curd-policies is only used for root users as it cannot keep track of our group
* and validate things
*
* @param array|string $name
* @param mixed $parameters
* @param boolean $absolute
* @return string
* @throws \ErrorException If an object given as parameter is not a Model.
*/
public static function routeWithGroup($name, $parameters = [], bool $absolute = true): string
{
// * Handle if parameter if is an object.
if (\is_object($parameters)) {
$parents = \class_parents($parameters);
if ($parents === false or !\in_array(Model::class, $parents)) {
throw new \ErrorException(\sprintf(
'%s only support object of type %s',
__FUNCTION__,
Model::class
));
}
/** @var \Illuminate\Database\Eloquent\Model $model */
$model = $parameters;
$parameters = [Str::singular($model->getTable()) => $model];
}
// * Handle if parameter isnt and array like slug or else.
if (!is_array($parameters)) {
// * Best effort, result is not garantee.
$parameters = [$parameters];
}
// * Pass our group.
if ($group = Session::get('activeGroup', request()->group)) {
$parameters = array_merge($parameters, ['group' => $group]);
}
// * Call laravel route helper.
return route($name, $parameters, $absolute);
}
/**
* Store an image of different types
*
* @param string $table
* @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $image
* @return string
* @throws \ErrorException If file copy fails.
*/
public static function storeImage(string $table, $image): string
{
/** @var \Illuminate\Http\UploadedFile $image */
if (\is_object($image) and get_class($image) === UploadedFile::class) {
$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);
}
/** @var \SplFileInfo $image */
if (\is_object($image) and get_class($image) === \SplFileInfo::class) {
$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), '/');
}
if (\is_string($image)) {
return $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
{
$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;
}
/**
* PDF Attributes names
*
* @param Request $request
* @return array
*/
public static function pdfAttributes(Request $request): array
{
$pdfAttributes = ['pdf' => trans('documents PDF')];
if (\is_array($request->pdfValidate)) {
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
collect($request->pdfValidate)->map(function ($value, $index) use (&$pdfAttributes) {
$pdfAttributes["pdfValidate.$index"] = sprintf('document PDF n°%d', $index + 1);
});
}
return $pdfAttributes;
}
/**
* Pdf rules
*
* @return array
*/
public static function pdfRules(): array
{
return [
'pdf' => 'sometimes|array',
'pdfValidate' => 'required_with:pdf|array',
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
'pdfValidate.*' => function ($attribute, $value, $fail) {
if (!File::exists($value)) {
$fail(trans(":attribute n'existe pas dans le système de fichier"));
}
if (File::mimeType($value) !== 'application/pdf') {
$fail(trans(":attribute doit être un document de type 'application/pdf'"));
}
}
];
}
/**
* Merges post pdf fields with request data
*
* @param \Illuminate\Http\Request $request
* @return void
*/
public static function mergePdf(Request $request): void
{
$pdf = $request->pdf;
if (is_array($pdf)) {
$request->merge(['pdfValidate' => []]);
foreach ($pdf as $key => $pdfPath) {
// ! Empty pdf.
if ($pdfPath === 'false') {
// * Empty pdf and exit
$request->merge(['pdf' => []]);
return;
}
$decodedPath = \urldecode($pdfPath);
$request->merge([
"pdfValidate" => array_merge(
$request->pdfValidate,
[\public_path(ltrim($decodedPath, '/'))]
)
]);
$changedPdf = $request->pdf;
$changedPdf[$key] = $decodedPath;
$request->merge([
'pdf' => $changedPdf
]);
}
} //end if
}
/**
* Asserts that the field is unique
*
* @param mixed $id
* @param string $field
* @param mixed $value
* @return void
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
*/
public static function assertFieldIsUnique($id, string $field, $value)
{
Validator::make([$field => $value], [
$field => Rule::unique('users', $field)->ignore($id),
], [
$field . '.unique' => trans('La valeur :value pour le champ :attribute est déjà utilisé', [
'attribute' => $field,
'value' => $value
])
])->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');
}
}