setDictionary(['en-us', 'en-uk', config('app.locale')]); } catch (\RuntimeException) { $censor->setDictionary(['en-us', 'en-uk', 'fr']); } return $censor->censorString($string, true)['clean']; } /** * Run a terminal command and gives the output. * * @param array|string $command The command to execute on terminal. * @param bool $throw Wether to throw en esception on failure or not. * @return string|false Return false on failure only if $throw is false. * * @phpcs:disable Squiz.Commenting.FunctionComment.EmptyThrows * * @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber * * @throws \Illuminate\Process\Exceptions\ProcessTimedOutException * @throws \Symfony\Component\Process\Exception\LogicException * @throws \Symfony\Component\Process\Exception\InvalidArgumentException * @throws \Symfony\Component\Process\Exception\RuntimeException * @throws \LogicException * @throws \RuntimeException If the command fails. */ public static function runConsoleCommand(array | string $command, bool $throw = true): string | false { // phpcs:enable $process = Process::command($command)->run(); if ($process->failed()) { $code = $process->exitCode(); $ouput = $process->errorOutput(); if ($throw) { throw new \RuntimeException("Failed to run command exit code {$code} with output \n{$ouput}"); } return false; } return $process->output(); } /** * Return array merge recursive distinct. * * @param array $array1 * @param array $array2 * @return array */ public static function arrayMergeRecursiveDistinct(array &$array1, array &$array2): array { $merged = $array1; foreach ($array2 as $key => &$value) { if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { $merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value); } else { $merged[$key] = $value; } } return $merged; } /** * Validate recaptcha. */ public static function validateRecaptcha(Request $request, bool $isV3 = true): void { Validator::validate($request->all(), [ 'g-recaptcha-response' => [ 'required', 'string', // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed function ($attribute, $value, $fail) use ($isV3) { try { $response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [ 'secret' => config('services.google.recaptcha.private'), 'response' => \strval($value), 'remoteip' => app('clientIp'), ]); if ($e = $response->toException()) { throw $e; } if ($isV3) { 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 \RuntimeException(); } if (\floatval($response->json('score')) < 0.8) { $fail(trans('recaptcha.not_enough_score')); } return; } 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) ) { throw new \RuntimeException(); } } catch (\Exception $e) { $fail(trans('recaptcha.error')); } // end try }, ], ], [], ['g-recaptcha-response' => 'reCaptcha']); } /** * 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..). */ public static function stripCkEditorSchemesOnMediaInputs(string $input): string { $input = \preg_replace('/(?<=)/m', '', $input); return $input; } /** * Assert That no Back Office url is present on txt. * * @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 => trans( 'crud.libs.toolbox.assertNoInternalBoUrls', ['attribute' => $field] ), ]); } return $match; } catch (NotFoundHttpException $e) { return $match; } }, $text ); return $text; } /** * Assert That only external url is present on txt. * * @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); // Assert route matches existing routes. $routes->match($request); // ? Is url internal ? if (\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST)) { // Removes the matched url. throw ValidationException::withMessages([ $field => trans( 'crud.libs.toolbox.assertNoInternalUrls', ['attribute' => $field, 'appname' => config('app.name')] ), ]); } return $match; } catch (NotFoundHttpException $e) { return $match; } }, $text ); return $text; } /** * Mutltibyte string replace. */ public static function mbReplace( array | string $search, array | string $replace, array | string $subject, int &$count = 0 ): array | string | false { 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. * * @throws \Illuminate\Validation\ValidationException If the field is not unique. */ public static function assertFieldIsUnique( string $table, string $field, string | int | float | bool | null | array $value, ?int $id = null, ?callable $query = null ): void { if (! $query) { Validator::make([$field => $value], [ self::mbReplace('.', '\.', $field) => Rule::unique($table, $field)->ignore($id), ], [ self::mbReplace('.', '\.', "{$field}.unique") => trans('crud.libs.toolbox.assertFieldIsUnique', ['attribute' => $field, 'value' => $value]), ])->validate(); return; } Validator::make([$field => $value], [ $field => Rule::unique($table)->where($query)->ignore($id), ], [ $field . '.unique' => trans( 'crud.libs.toolbox.assertFieldIsUnique', ['attribute' => $field, 'value' => $value] ), ])->validate(); } /** * Get original polyfill with autotranslate behavior. */ public static function getOriginal(Model $model, string $attribute): mixed { if ( \in_array('Spatie\Translatable\HasTranslations', \class_uses_recursive($model)) and // @phpstan-ignore-next-line $model->isTranslatableAttribute($attribute) ) { // @phpstan-ignore-next-line return $model->getTranslation( $attribute, // @phpstan-ignore-next-line $model->getLocale(), // @phpstan-ignore-next-line $model->useFallbackLocale() ); } return $model->getOriginal($attribute); } /** * Dynamic model query. */ public static function queryColumnWithLocales( string $defaultLocale, Builder $q, string $column, string $value ): Builder { $currentLocale = app()->currentLocale(); $locales = collect(config('app.locales')) ->mapWithKeys(fn (string $locale) => [$locale => $locale]); if (! $locales->has($defaultLocale)) { // * Remove default locale. $locales->pull($defaultLocale); } if (! $locales->has($currentLocale)) { // * Remove current locale. $locales->pull($currentLocale); } return $q->where(function (Builder $q) use ($defaultLocale, $currentLocale, $locales, $column, $value) { // * Query default locale $q->whereRaw("JSON_EXTRACT({$column}, '$.{$defaultLocale}') = '{$value}'"); if ($defaultLocale !== $currentLocale) { // * Query current locale $q->whereRaw("JSON_EXTRACT({$column}, '$.{$currentLocale}') = '{$value}'"); } // * Query any other locale $locales->each(fn ($locale) => $q->orWhereRaw( "JSON_EXTRACT({$column}, '$.{$locale}') = '{$value}'" )); }); } /** * Generates an array from a dot separated string. */ public static function dotStringToNestedArray(string $path, mixed $value, string $separator = '.'): array { $arr = $value; $keys = array_reverse(explode($separator, $path)); foreach ($keys as $key) { $arr = [$key => $arr]; } return $arr; } /** * Convert deeply an object to array. * * @throws \RuntimeException If $input var is a scalar value. */ public static function objectToArray(mixed $input): array { // Only process if it's an object or array being passed to the function. if (! (is_object($input) or is_array($input))) { throw new \RuntimeException('Cannot handle $input of type ' . \gettype($input)); } $ret = (array) $input; foreach ($ret as &$item) { // Recursively process EACH element only if not scalar. $item = (is_object($item) or is_array($item)) ? self::objectToArray($item) : $item; } return $ret; } /** * Convert deeply an array to object. * * @throws \RuntimeException If $input var is a scalar value. */ public static function arrayToObject(mixed $input): object { // Only process if it's an object or array being passed to the function. if (! (is_object($input) or is_array($input))) { throw new \RuntimeException('Cannot handle $input of type ' . \gettype($input)); } $ret = (object) $input; foreach ($ret as &$item) { // Recursively process EACH element only if not scalar. $item = (is_object($item) or is_array($item)) ? self::arrayToObject($item) : $item; } return $ret; } /** * Validate picture. */ public static function validatePicture(string $attribute, ?string $value, callable $fail, object $sizes): void { if (! ($useStorage = Storage::exists($value)) and ! File::exists($value)) { $fail(trans(':attribute n\'existe pas dans le système de fichier', [ 'attribute' => $attribute, ])); return; } $fileFullPath = self::getUploadedFileFullPath($value, $useStorage); if ( ! \in_array(self::getUploadedFileMimeType($value, $useStorage), [ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/svg+xml', ]) ) { $fail(trans(':attribute doit être une image de type \'image/jpeg,image/jpg,image/png,image/gif,image/svg\'', [ 'attribute' => $attribute, ])); } if (self::getUploadedFileMimeType($value, $useStorage) !== 'image/svg+xml') { if (! ($dimensions = \getimagesize($fileFullPath))) { $fail(trans(':attribute impossible de valider les dimensions', [ 'attribute' => $attribute, ])); } else { $width = intval($dimensions[0]); $height = intval($dimensions[1]); $xMessage = trans( ":attribute la largeur doit être comprise entre {$sizes->minWidth}px et {$sizes->maxWidth}px" ); if ($sizes->minWidth === $sizes->maxWidth) { $xMessage = trans(":attribute la largeur doit être de {$sizes->minWidth}px"); } $yMessage = trans( ":attribute la hauteur doit être comprise entre {$sizes->minHeight}px et {$sizes->maxHeight}px" ); if ($sizes->minHeight === $sizes->maxHeight) { $yMessage = trans(":attribute la hauteur doit être de {$sizes->minHeight}px"); } if (($width < $sizes->minWidth) or ($width > $sizes->maxWidth)) { $fail($xMessage); } if (($height < $sizes->minHeight) or ($height > $sizes->maxHeight)) { $fail($yMessage); } } // end if } } /** * Get the file full path. */ private static function getUploadedFileFullPath(string $value, bool $useStorage): string { /** @var \Illuminate\Filesystem\FilesystemAdapter */ $disk = Storage::disk(); return ! $useStorage ? (realpath($value) ?: $value) : $disk->path($value); } /** * Get the file mimetype. */ private static function getUploadedFileMimeType(string $value, bool $useStorage): string | false { return ! $useStorage ? File::mimeType($value) : Storage::mimeType($value); } /** * Fetch class list from a specific namespace. * * @return array An array of classe namespaces */ public static function getClassesUsingNamespace(string $namespace): array { return collect(File::files(\app_path(Str::of($namespace)->replaceFirst('\App', '')))) ->filter(fn (SplFileInfo $splFileInfo) => $splFileInfo->getExtension() === 'php') ->map(fn (SplFileInfo $splFileInfo) => Str::of($splFileInfo->getBasename()) ->replace('.' . $splFileInfo->getExtension(), '')->prepend("{$namespace}\\"))->all(); } /** * Get all classes from a specific namespace that implement a specific interface. * * @return array An array of classe namespaces */ public static function getClassesUsingInterface(string $namespace, string $interface): array { return Arr::where( static::getClassesUsingNamespace($namespace), fn (string $class_namespace) => \collect(class_implements($class_namespace, true))->filter() ->contains(fn (string $class_interface) => $class_interface === $interface) ); } /** * Get validated pagination from request. */ public static function getValidatedEnum(int $default, string $field, string $enumPath): int { try { return \intval(Validator::make( [$field => \request()->get($field)], [$field => new Enum($enumPath)] )->validated()[$field]); } catch (ValidationException $e) { return $default; } } }