)/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(?: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\/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(?: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); } } /** * 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, \filter_var( \strip_tags(\urldecode($image->getClientOriginalName())), \FILTER_SANITIZE_STRING )); $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 { $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'); } }