diff --git a/.env.example b/.env.example index 11f5848..ee6c860 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ -APP_NAME=Laravel +APP_NAME=Incollector APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=http://localhost +APP_URL=https://incollector.s-web.test LOG_CHANNEL=stack LOG_DEPRECATIONS_CHANNEL=null diff --git a/.env.testing.example b/.env.testing.example new file mode 100644 index 0000000..b4c3ae7 --- /dev/null +++ b/.env.testing.example @@ -0,0 +1,47 @@ +APP_NAME=Incollector +APP_ENV=testing +APP_KEY=base64:oRXNjgvWgIcboJN2O0NgaBt3kMUUfgyQ1Wv0S9/7sAA= +APP_DEBUG=true +APP_URL="http://127.0.0.1:8888" + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=dev-mariadb +DB_PORT=3306 +DB_DATABASE=testing-incollector +DB_USERNAME= +DB_PASSWORD= + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +QUEUE_CONNECTION=database +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=mailhog +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS=incollector@ldc.fr +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_APP_CLUSTER=mt1 + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/.gitignore b/.gitignore index d30a3b9..356de13 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,11 @@ .vscode /storage/*.key +/storage/inco/* +/storage/marque/* /vendor .env +.env.testing .env.backup .phpunit.result.cache docker-compose.override.yml diff --git a/app/Console/Commands/LdcWebStatusChecker.php b/app/Console/Commands/LdcWebStatusChecker.php new file mode 100644 index 0000000..1ffec37 --- /dev/null +++ b/app/Console/Commands/LdcWebStatusChecker.php @@ -0,0 +1,78 @@ + 0) { + foreach ($files as $file) { + $data['logs'] = $data['logs'] + filesize($file); + } + } + + $data['code']['version'] = app()->version(); + + // Send by cURL. + $url = 'https://ldc-web.ldc.fr/api/status-checker'; + $ch = curl_init($url); + $payload = ['token' => 'CE8S52Qq7stf5k8frF4T', 'datas' => json_encode($data)]; + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_exec($ch); + curl_close($ch); + return 0; + } +} diff --git a/app/Console/Commands/UserCreate.php b/app/Console/Commands/UserCreate.php new file mode 100644 index 0000000..8de4f9f --- /dev/null +++ b/app/Console/Commands/UserCreate.php @@ -0,0 +1,49 @@ +ask('What is the firstname?'); + $lastname = $this->ask('What is the lastname?'); + $email = $this->ask('What is the email?'); + $pwd = $this->secret('What is the password?'); + + $user = new User(); + $user->firstname = $firstname; + $user->lastname = $lastname; + $user->email = $email; + $user->password = $pwd; + $user->saveOrFail(); + + $this->info('User created.'); + return 0; + } +} diff --git a/app/Console/Commands/XmlBrandReader.php b/app/Console/Commands/XmlBrandReader.php new file mode 100644 index 0000000..5ceae78 --- /dev/null +++ b/app/Console/Commands/XmlBrandReader.php @@ -0,0 +1,37 @@ +info('Running the Job, this could take some time.'); + $job = new XmlBrandReaderJob(); + $job->console = $this; + $job->handle(); + $this->info('Job completed'); + } +} diff --git a/app/Console/Commands/XmlIncoReader.php b/app/Console/Commands/XmlIncoReader.php index aee0898..b9b4620 100644 --- a/app/Console/Commands/XmlIncoReader.php +++ b/app/Console/Commands/XmlIncoReader.php @@ -2,7 +2,7 @@ namespace App\Console\Commands; -use App\Jobs\XmlIncoReader as JobsXmlIncoReader; +use App\Jobs\Arcadia\XmlIncoReaderJob; use Illuminate\Console\Command; class XmlIncoReader extends Command @@ -29,7 +29,7 @@ class XmlIncoReader extends Command public function handle() { $this->info('Running the Job, this could take some time.'); - $job = new JobsXmlIncoReader(); + $job = new XmlIncoReaderJob(); $job->console = $this; $job->handle(); $this->info('Job completed'); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 940765a..fab7506 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,6 +2,7 @@ namespace App\Console; +use App\Console\Commands\LdcWebStatusChecker; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -12,8 +13,7 @@ class Kernel extends ConsoleKernel * * @var array */ - protected $commands = [ - ]; + protected $commands = []; /** * Define the application's command schedule. @@ -23,8 +23,7 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { - // $schedule->command('inspire')->hourly(); - // $schedule->job(new XmlIncoReader)->weeklyOn(1, '8:00'); + $schedule->command(LdcWebStatusChecker::class)->everyFifteenMinutes(); } /** @@ -35,7 +34,5 @@ class Kernel extends ConsoleKernel protected function commands() { $this->load(__DIR__ . '/Commands'); - - require base_path('routes/console.php'); } } diff --git a/app/Enums/ApiError.php b/app/Enums/ApiError.php new file mode 100644 index 0000000..b5c58e4 --- /dev/null +++ b/app/Enums/ApiError.php @@ -0,0 +1,84 @@ + 101, + 'validationFailed' => 102, + 'apiAuthFailed' => 119, + 'methodNotAllowed' => 150, + 'wrongTimeStamp' => 151, + 'missingParameter' => 400, + 'authorizationException' => 403, + 'throttleRequestsException' => 429 + ]; + } + + /** + * Libellés des possibilités de cet énumération (label) + * + * @return array + */ + public static function labels(): array + { + return [ + 'modelNotFound' => '%s introuvable.', + 'validationFailed' => 'Impossible de valider les données', + 'apiAuthFailed' => 'Echec de l\'authentification client à l\'Api LDC-CRM pour ce service', + 'methodNotAllowed' => 'This method is not allowed for this request', + 'wrongTimeStamp' => 'Your request is malformed, please check timestamp header and parameter', + 'missingParameter' => 'The request is missing some parameters', + 'authorizationException' => 'You are not autorized to access this', + 'throttleRequestsException' => 'Throttle: too many request have been attempted, please retry later' + ]; + } +} diff --git a/app/Enums/ApiStatus.php b/app/Enums/ApiStatus.php new file mode 100644 index 0000000..c6a87a9 --- /dev/null +++ b/app/Enums/ApiStatus.php @@ -0,0 +1,16 @@ +error = $error; + $this->message = $message; + $this->httpCode = $httpCode; + parent::__construct( + strlen($this->message) ? $this->message : $error->label, + $error->value, + $previous + ); + } + + /** + * Get the Api Error + * + * @return ApiError + */ + public function getApiError(): ApiError + { + return $this->error; + } + + /** + * Get the http code + * + * @return integer + */ + public function getStatusCode(): int + { + return $this->httpCode; + } + + /** + * This is used by laravel only. + * It uses Log class to print the error. + * + * @return void + */ + public function report() + { + // @codeCoverageIgnoreStart + Log::debug(sprintf('CRM API: %s', $this->getMessage())); + // @codeCoverageIgnoreEnd + } +} diff --git a/app/Exceptions/Api/ApplicationException.php b/app/Exceptions/Api/ApplicationException.php new file mode 100644 index 0000000..06355f4 --- /dev/null +++ b/app/Exceptions/Api/ApplicationException.php @@ -0,0 +1,25 @@ +getMessage())); + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index a8f2183..44f2465 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -2,7 +2,20 @@ namespace App\Exceptions; +use App\Enums\ApiError; +use App\Enums\ApiStatus; +use App\Exceptions\Api\ApiErrorException; +use App\Http\Resources\Api\V1\ApiResponseResource; +use App\Lib\Helper; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\AuthenticationException; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Http\Exceptions\ThrottleRequestsException; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Validation\ValidationException; use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler; +use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Throwable; class Handler extends ExceptionHandler @@ -10,18 +23,18 @@ class Handler extends ExceptionHandler /** * A list of the exception types that are not reported. * - * @var string[] + * @var array */ protected $dontReport = [ - ]; + ApiErrorException::class + ]; /** * A list of the inputs that are never flashed for validation exceptions. * - * @var string[] + * @var array */ protected $dontFlash = [ - 'current_password', 'password', 'password_confirmation', ]; @@ -33,7 +46,125 @@ class Handler extends ExceptionHandler */ public function register() { - $this->reportable(function (Throwable $e) { - }); + } + + /** + * Render an exception into an HTTP response. + * + * @param mixed $request + * @param \Throwable $e + * @return \Symfony\Component\HttpFoundation\Response + */ + public function render($request, Throwable $e) + { + if (Helper::isRequestForApiV1($request) and $request->isJson()) { + return $this->handleApiV1Exception($request, $e); + } //end if + // @codeCoverageIgnoreStart + return parent::render($request, $e); + // @codeCoverageIgnoreEnd + } + + /** + * Generate a json response for any apiV2 error case + * + * @param Request $request + * @param \Throwable $e + * @return JsonResponse + */ + private function handleApiV1Exception(Request $request, \Throwable $e): JsonResponse + { + switch (get_class($e)) { + case AuthenticationException::class: + /** @var AuthenticationException $e */ + return $this->getReponse( + $request, + 401, + ApiError::apiAuthFailed()->value, + sprintf('%s, %s', ApiError::apiAuthFailed()->label, $e->getMessage()) + ); + case MethodNotAllowedHttpException::class: + /** @var MethodNotAllowedHttpException $e */ + return $this->getReponse( + $request, + $e->getStatusCode(), + ApiError::methodNotAllowed()->value, + sprintf('%s, %s', ApiError::methodNotAllowed()->label, $e->getMessage()) + ); + case ValidationException::class: + /** @var ValidationException $e */ + return $this->getReponse( + $request, + $e->status, + ApiError::validationFailed()->value, + sprintf(ApiError::validationFailed()->label, $e->getMessage()), + ['errors' => $e->errors()] + ); + case ModelNotFoundException::class: + /** @var ModelNotFoundException $e */ + return $this->getReponse( + $request, + 404, + ApiError::modelNotFound()->value, + sprintf( + ApiError::modelNotFound()->label, + \array_slice(explode('\\', $e->getModel()), -1)[0] + ) + ); + case ApiErrorException::class: + /** @var ApiErrorException $e */ + return $this->getReponse( + $request, + $e->getStatusCode(), + $e->getCode(), + $e->getMessage() + ); + case ThrottleRequestsException::class: + /** @var ThrottleRequestsException $e */ + return $this->getReponse( + $request, + 429, + ApiError::throttleRequestsException()->value, + ApiError::throttleRequestsException()->label + ); + case AuthorizationException::class: + /** @var AuthorizationException $e */ + return $this->getReponse( + $request, + 403, + ApiError::authorizationException()->value, + ApiError::authorizationException()->label + ); + } //end switch + // @codeCoverageIgnoreStart + if (config('app.env') === 'testing') { + // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found + print_r((string)$e); + } + return $this->getReponse($request, 500, $e->getCode(), $e->getMessage()); + // @codeCoverageIgnoreEnd + } + + /** + * Generate an ApiV2 json response + * + * @param Request $request + * @param integer $httpCode + * @param integer $apiCode + * @param string $message + * @param array $data + * @return JsonResponse + */ + private static function getReponse( + Request $request, + int $httpCode, + int $apiCode, + string $message, + array $data = [] + ): JsonResponse { + return (new ApiResponseResource( + (object)array_merge($data, ['message' => $message, 'code' => $apiCode]), + ApiStatus::KO() + ))->toResponse($request)->setStatusCode($httpCode); } } diff --git a/app/Exceptions/InvalidXmlException.php b/app/Exceptions/InvalidXmlException.php index 4db050d..a24f1f0 100644 --- a/app/Exceptions/InvalidXmlException.php +++ b/app/Exceptions/InvalidXmlException.php @@ -2,8 +2,8 @@ namespace App\Exceptions; -use Exception; +use ErrorException; -class InvalidXmlException extends Exception +class InvalidXmlException extends ErrorException { } diff --git a/app/Http/Controllers/Api/V1/ApiController.php b/app/Http/Controllers/Api/V1/ApiController.php new file mode 100644 index 0000000..7edee2f --- /dev/null +++ b/app/Http/Controllers/Api/V1/ApiController.php @@ -0,0 +1,19 @@ +json(); + } +} diff --git a/app/Http/Controllers/Api/V1/XmlIncoController.php b/app/Http/Controllers/Api/V1/XmlIncoController.php new file mode 100644 index 0000000..b60b9b9 --- /dev/null +++ b/app/Http/Controllers/Api/V1/XmlIncoController.php @@ -0,0 +1,58 @@ +arcadiaBrand; + + $xmlIncos = XmlInco::query(); + + if ($arcadiaBrand) { + // * Make sure we are accessing xmlIncos that belongs to our service. + $this->authorize('arcadiaBrandBelongsToService', $arcadiaBrand); + + $xmlIncos->whereHas('arcadiaBrands', function ($q) use ($arcadiaBrand) { + $q->whereId($arcadiaBrand->id); + }); + } + + return XmlIncoResource::collection($xmlIncos->simplePaginate( + $request->per_page, + ['*'], + 'page', + $request->page + )->items()); + } + + /** + * Return a specific xmlInco. + * + * @param XmlInco $xmlInco + * @return JsonResource + */ + public function show(XmlInco $xmlInco): JsonResource + { + // * Make sure we are accessing a xmlInco that belongs to our service. + $this->authorize('arcadiaBrandBelongsToService', $xmlInco->arcadiaBrand); + + return new XmlIncoResource($xmlInco); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 39910d7..7e65da8 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -14,12 +14,12 @@ class Kernel extends HttpKernel * @var array */ protected $middleware = [ - // \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustProxies::class, \Fruitcake\Cors\HandleCors::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, + \App\Http\Middleware\XssSanitizer::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, ]; @@ -33,16 +33,18 @@ class Kernel extends HttpKernel \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, - // \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ - // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', - \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\JsonOnly::class, + \App\Http\Middleware\ApiResponse::class, + \App\Http\Middleware\ApiCheckTimeStamps::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class ], ]; diff --git a/app/Http/Middleware/ApiCheckTimeStamps.php b/app/Http/Middleware/ApiCheckTimeStamps.php new file mode 100644 index 0000000..be7f528 --- /dev/null +++ b/app/Http/Middleware/ApiCheckTimeStamps.php @@ -0,0 +1,38 @@ +timestamp) or !Carbon::hasFormat($request->timestamp, 'Y-m-d H:i:s')) { + throw new ApiErrorException(ApiError::wrongTimeStamp()); + } + if ( + Carbon::parse($request->timestamp, 'utc') + ->isBefore(Carbon::now('UTC')->subSeconds(config('api.timestampTimeout', 10))) + ) { + throw new ApiErrorException(ApiError::wrongTimeStamp()); + } + return $next($request); + } +} diff --git a/app/Http/Middleware/ApiResponse.php b/app/Http/Middleware/ApiResponse.php new file mode 100644 index 0000000..f9dffc5 --- /dev/null +++ b/app/Http/Middleware/ApiResponse.php @@ -0,0 +1,57 @@ +header('Cache-Control', 'No-Store'); + + /** @var \App\Models\Service|null */ + if (\optional($service = app(Service::class))->id) { + $service->incrementStat(); + } + + /** @var object $data */ + $data = $response->getData(); + + // If reponse is already a V2ApiResponse then just return it. This happens if error handler comes to action. + if ( + isset($data->status) and + isset($data->data) and + isset($data->version) and + in_array($data->status, [ApiStatus::OK()->value, ApiStatus::KO()->value]) + ) { + return $response; + } + + /** @var \App\Enums\ApiStatus $status */ + $status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK() : ApiStatus::KO(); + return $response->setData(new ApiResponseResource((object)$response->getData(), $status)); + } +} diff --git a/app/Http/Middleware/JsonOnly.php b/app/Http/Middleware/JsonOnly.php new file mode 100644 index 0000000..8faf331 --- /dev/null +++ b/app/Http/Middleware/JsonOnly.php @@ -0,0 +1,24 @@ +isJson() or !$request->expectsJson() or is_null(json_decode($request->getContent()))) { + abort(400, 'Invalid JSON'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/XssSanitizer.php b/app/Http/Middleware/XssSanitizer.php new file mode 100644 index 0000000..6192424 --- /dev/null +++ b/app/Http/Middleware/XssSanitizer.php @@ -0,0 +1,25 @@ +all(); + array_walk_recursive($input, function (&$input) { + $input = strip_tags($input); + }); + $request->merge($input); + return $next($request); + } +} diff --git a/app/Http/Requests/Api/V1/PaginatorRequest.php b/app/Http/Requests/Api/V1/PaginatorRequest.php new file mode 100644 index 0000000..113b0bd --- /dev/null +++ b/app/Http/Requests/Api/V1/PaginatorRequest.php @@ -0,0 +1,31 @@ +check(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'page' => 'sometimes|numeric|min:1', + 'per_page' => 'sometimes|numeric|min:1|max:100', + ]; + } +} diff --git a/app/Http/Requests/Api/V1/XmlIncosIndexRequest.php b/app/Http/Requests/Api/V1/XmlIncosIndexRequest.php new file mode 100644 index 0000000..f4f93fc --- /dev/null +++ b/app/Http/Requests/Api/V1/XmlIncosIndexRequest.php @@ -0,0 +1,36 @@ +merge([ + 'brand' => $this->brand ? ArcadiaBrand::whereId($this->brand)->firstOrFail() : null + ]); + return auth()->check(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return \array_merge( + parent::rules(), + [ + 'brand' => 'nullable' + ] + ); + } +} diff --git a/app/Http/Resources/Api/V1/ApiResponseResource.php b/app/Http/Resources/Api/V1/ApiResponseResource.php new file mode 100644 index 0000000..277a992 --- /dev/null +++ b/app/Http/Resources/Api/V1/ApiResponseResource.php @@ -0,0 +1,42 @@ +status = $status; + $this->data = $data; + } + + /** + * Transform the resource into an array. + * + * @param mixed $request + * @return array + */ + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass, Squiz.Commenting.FunctionComment.Missing + public function toArray($request): array + { + return [ + 'status' => $this->status->value, + 'data' => (array)$this->data, + 'version' => config('api.version') + ]; + } +} diff --git a/app/Http/Resources/Api/V1/XmlIncoResource.php b/app/Http/Resources/Api/V1/XmlIncoResource.php new file mode 100644 index 0000000..7757d09 --- /dev/null +++ b/app/Http/Resources/Api/V1/XmlIncoResource.php @@ -0,0 +1,66 @@ + '', + 'no_art' => '', + 'prod_saison' => '', + 'allegations' => '', + 'conseil_prepa' => '', + 'astuces' => '', + 'descriptif' => '', + 'arg_vente' => '', + 'nb_portion' => '', + 'typ_portion' => '', + 'pds_moy_theo' => '', + 'lib_conso' => '', + 'lst_ing' => '', + 'lib_allergene' => '', + 'nutritional_values' => '', + 'pictures' => '', + ]; + + /** + * XmlIncoResource + * + * @param XmlInco $xmlInco + */ + public function __construct(XmlInco $xmlInco) + { + $this->xmlInco = $xmlInco; + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request|null $request + * @return array + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass + */ + public function toArray($request = null) + { + $data = array_intersect_key( + $this->xmlInco->toArray(), + $this->getSkeleton() + ); + + return $data; + } + + /** + * Returns the resource skeletton + * + * @return array + */ + public static function getSkeleton(): array + { + return self::$skeleton; + } +} diff --git a/app/Jobs/XArcadiaFlux.php b/app/Jobs/Arcadia/XArcadiaFlux.php similarity index 58% rename from app/Jobs/XArcadiaFlux.php rename to app/Jobs/Arcadia/XArcadiaFlux.php index 56818a0..cf73d14 100644 --- a/app/Jobs/XArcadiaFlux.php +++ b/app/Jobs/Arcadia/XArcadiaFlux.php @@ -1,9 +1,8 @@ getDisk()->exists($filename)) { + $this->log( + sprintf( + 'File %s could not be moved to invalid (not found)', + $filename + ), + LogType::warning() + ); + return; + } + if ($this->getDisk()->exists(sprintf('%s/%s', $this->invalidFolderName, $filename))) { + $ext = \pathinfo($filename, \PATHINFO_EXTENSION); + $newFileName = \sprintf( + '%s_%d.%s', + \substr($filename, 0, 0 - (strlen($ext) + 1)), + \time(), + $ext + ); + } + try { + if (!$this->getDisk()->move($filename, sprintf('%s/%s', $this->invalidFolderName, $newFileName))) { + $this->log( + sprintf( + 'File %s could not be moved to invalid', + $filename + ), + LogType::warning() + ); + } + } catch (\Exception $e) { + $this->log( + sprintf( + 'File %s could not be moved to invalid', + $filename + ), + LogType::warning() + ); + } + } + + /** + * Suppression des fichiers dans + * invalid qui ont plus de 6 mois + * + * @return void + */ + protected function clearOldInvalids(): void + { + $disk = $this->getDisk(); + $invalids = $disk->files($this->invalidFolderName); + + // /** @var File */ + foreach ($invalids as $invalid) { + $pi = \pathinfo($invalid, \PATHINFO_BASENAME); + if (in_array($pi, $this->ignoreFiles)) { + continue; + } + + if ($disk->lastModified($invalid) < strtotime(Carbon::now()->subMonth(6))) { + if (!$disk->delete($invalid)) { + Log::critical(sprintf('%s : Could not delete file %s', get_class($this), $invalid)); + }; + } + } + } + /** * Move the file to archives * @@ -107,7 +196,7 @@ abstract class XArcadiaFlux implements ShouldQueue LogType::warning() ); } - } catch (Exception $e) { + } catch (\Exception $e) { $this->log( sprintf( 'File %s could not be moved to archives', @@ -160,6 +249,7 @@ abstract class XArcadiaFlux implements ShouldQueue * @param string $message * @param LogType $logtype * @return void + * @throws \ErrorException If logtype is not handled. */ protected function log(string $message, LogType $logtype = null): void { @@ -187,8 +277,8 @@ abstract class XArcadiaFlux implements ShouldQueue Log::critical($message); break; default: - throw new Exception(sprintf('%s : unhandled log type %s', get_class($this), $logtype->label)); - }//end switch + throw new \ErrorException(sprintf('%s : unhandled log type %s', get_class($this), $logtype->label)); + } //end switch $this->consolePrint($message, $logtype); } @@ -198,9 +288,11 @@ abstract class XArcadiaFlux implements ShouldQueue * @param string $message * @param LogType $logtype * @return void + * @throws \ErrorException If logtype is not handled. */ protected function consolePrint(string $message, LogType $logtype): void { + $this->clearProgressBarFromLine(); if ($this->console) { switch ($logtype->value) { case LogType::debug()->value: @@ -216,8 +308,59 @@ abstract class XArcadiaFlux implements ShouldQueue $this->console->error($message); break; default: - throw new Exception(sprintf('%s : unhandled log type %s', get_class($this), $logtype->label)); + throw new \ErrorException(sprintf('%s : unhandled log type %s', get_class($this), $logtype->label)); } } } + + /** + * Start ProgressBar + * + * @param integer $total + * @return void + */ + protected function startProgressBar(int $total) + { + if ($this->console) { + $this->progressBar = $this->console->getOutput() + ->createProgressBar($total - 1); + $this->progressBar->start(); + } + } + + /** + * Clear the line to write someting + * + * @return void + */ + protected function clearProgressBarFromLine() + { + if ($this->progressBar) { + $this->progressBar->clear(); + } + } + + /** + * Increment 1 on progress bar + * + * @return void + */ + protected function incrementProgressBar() + { + if ($this->progressBar) { + $this->progressBar->advance(); + } + } + + /** + * End the progress bar + * + * @return void + */ + protected function endProgressBar() + { + if ($this->progressBar) { + $this->progressBar->finish(); + } + } } diff --git a/app/Jobs/Arcadia/XmlBrandReaderJob.php b/app/Jobs/Arcadia/XmlBrandReaderJob.php new file mode 100644 index 0000000..76070b2 --- /dev/null +++ b/app/Jobs/Arcadia/XmlBrandReaderJob.php @@ -0,0 +1,55 @@ + 'arcadia_id', + 'libMarq' => 'libelle', + ]; + + /** + * Fields that are allowed to be null + * + * @var array + */ + protected $nullableFields = []; + + /** + * Unique keys for updating rows + * + * @var array + */ + protected $uniqueKeyField = ['arcadia_id']; + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + + $this->parseData('dam', ArcadiaBrand::class); + } +} diff --git a/app/Jobs/XmlIncoReader.php b/app/Jobs/Arcadia/XmlIncoReaderJob.php similarity index 72% rename from app/Jobs/XmlIncoReader.php rename to app/Jobs/Arcadia/XmlIncoReaderJob.php index c41d374..64f65d0 100644 --- a/app/Jobs/XmlIncoReader.php +++ b/app/Jobs/Arcadia/XmlIncoReaderJob.php @@ -1,19 +1,36 @@ 'exploitant', // Consider this sub array * 'EXPLOITANT.ID_SOCIETE' => 'id_societe', * 'EXPLOITANT.NOM_SOCIETE' => 'nom_societe', + * @var array Columns to take from the xml. */ protected $takeColumns = [ 'NO_ART' => 'id', -// primary key on model + // primary key on model // this wil not return anything cf doc above 'ID_SOCIETE' => 'id_societe', 'NOM_SOCIETE' => 'nom_societe', @@ -40,22 +58,22 @@ class XmlIncoReader extends XmlJobs 'NOM' => 'nom', 'NUMERO' => 'numero', 'TAILLE_REF' => 'taille_ref', -// $poidsRef censé être dans INCO_COMPO (et généralement 100g) + // $poidsRef censé être dans INCO_COMPO (et généralement 100g) 'ALLEGATIONS' => 'allegations', 'CONSEIL_PREPA' => 'conseil_prepa', 'ASTUCES' => 'astuces', 'DESCRIPTIF' => 'descriptif', -// $descriptif + // $descriptif 'ARG_VENTE' => 'arg_vente', 'NB_PORTION' => 'nb_portion', 'TYP_PORTION' => 'typ_portion', 'PDS_MOY_THEO' => 'pds_moy_theo', 'LIB_CONSO' => 'lib_conso', 'LST_ING' => 'lst_ing', -// $ingredients + // $ingredients // list alergene 'LST_ALLERGENE' => 'lst_allergene', -// Consider this sub array + // Consider this sub array 'LST_ALLERGENE.LIB_ALLERGENE' => 'lib_allergene', // Allegations @@ -65,12 +83,12 @@ class XmlIncoReader extends XmlJobs 'INCO_COMPO' => 'inco_compo', -// Consider this sub array + // Consider this sub array 'INCO_COMPO.NOM' => 'nom', 'INCO_COMPO.TAILLE_REF' => 'taille_ref', 'INCO_COMPO.VAL_ENERG_KJ_NUT' => 'val_energ_kj_nut', 'INCO_COMPO.VAL_ENERG_KJ_R' => 'val_energ_kj_r', -// _R apports de la taille_ref + // _R apports de la taille_ref 'INCO_COMPO.VAL_ENERG_KCAL_R' => 'val_energ_kcal_r', 'INCO_COMPO.VAL_ENERG_KCAL_NUT' => 'val_energ_kcal_nut', 'INCO_COMPO.MAT_GRASS_R' => 'mat_grass_r', @@ -92,7 +110,7 @@ class XmlIncoReader extends XmlJobs 'INCO_COMPO.FRUITS_R' => 'fruits_r', 'INCO_COMPO.FRUITS_COQUE_SECH_R' => 'fruits_coque_sech_r', 'INCO_COMPO.POURCENT_APPORT_REF_E14_R' => 'pourcent_apport_ref_e14_r', -// POURCENT_*_P apports journaliers par portion + // POURCENT_*_P apports journaliers par portion 'INCO_COMPO.ENER_KJO_R' => 'ener_kjo_r', 'INCO_COMPO.POURCENT_APPORT_REF_KJO_R' => 'pourcent_apport_ref_kjo_r', 'INCO_COMPO.POURCENT_APPORT_REF_FAT_R' => 'pourcent_apport_ref_fat_r', @@ -110,9 +128,9 @@ class XmlIncoReader extends XmlJobs 'INCO_COMPO.POURCENT_APPORT_REF_SALTEQ_R' => 'pourcent_apport_ref_salteq_r', 'INCO_COMPO.APPORT_REF' => 'apport_ref', 'INCO_COMPO.TAILLE_PORT' => 'taille_port', -// $poidsPortion + // $poidsPortion 'INCO_COMPO.VAL_ENERG_KJ_P' => 'val_energ_kj_p', -// _P apports par portion + // _P apports par portion 'INCO_COMPO.VAL_ENERG_KCAL_P' => 'val_energ_kcal_p', 'INCO_COMPO.MAT_GRASS_P' => 'mat_grass_p', 'INCO_COMPO.AGS_P' => 'ags_p', @@ -155,9 +173,9 @@ class XmlIncoReader extends XmlJobs 'INCO_COMPO.COMPOSANTE_P' => 'composante_p', 'INCO_COMPO.POURCENT_FRUITS' => 'pourcent_fruits', 'INCO_COMPO.SCORE_NUTRI' => 'score_nutri', -// du coup nutriscore ? + // du coup nutriscore ? 'INCO_COMPO.CLASSEMENT' => 'classement', -// $nutriscore mais pas censé être dans INCO_COMPO + // $nutriscore mais pas censé être dans INCO_COMPO 'INCO_COMPO.MAT_GRASS_R_PREC' => 'mat_grass_r_prec', 'INCO_COMPO.AGS_R_PREC' => 'ags_r_prec', 'INCO_COMPO.GLUCIDES_R_PREC' => 'glucides_r_prec', @@ -170,6 +188,11 @@ class XmlIncoReader extends XmlJobs 'INCO_COMPO.FRUITS_COQUE_SECH_R_PREC' => 'fruits_coque_sech_r_prec', ]; + /** + * Fields that are allowed to be null + * + * @var array + */ protected $nullableFields = [ 'taille_ref', 'conseil_prepa', @@ -283,8 +306,18 @@ class XmlIncoReader extends XmlJobs 'inco_compo.fruits_coque_sech_r_prec' ]; + /** + * Unique keys for updating rows + * + * @var array + */ protected $uniqueKeyField = ['id']; + /** + * Columns to merge in + * + * @var array + */ protected $mergeColumns = [ 'allegations' => [ 'allegat_sante', @@ -390,15 +423,20 @@ class XmlIncoReader extends XmlJobs ] ]; + /** + * Execute the job. + * + * @return void + */ public function handle() { $this->parseData('INCO', XmlInco::class, function (SimpleXMLElement $xmlData, array &$line) { - // merge columns + // * merge columns foreach ($this->mergeColumns as $key => $values) { $line[$key] = []; - // merge columns and remove merged columns from line + // * merge columns and remove merged columns from line array_map(function ($kValue) use (&$line, $key) { if (array_key_exists($kValue, $line)) { $line[$key][$kValue] = $line[$kValue]; @@ -415,16 +453,27 @@ class XmlIncoReader extends XmlJobs // } } - // Boolean cast prod_saison + // * Boolean cast prod_saison $line['prod_saison'] = isset($line['prod_saison']) ? - (strtolower($line['prod_saison']) == 'non' ? false : true) : false; + (strtolower($line['prod_saison']) == 'non' ? false : true) : false; - $arImgPaths = $this->parseImage($xmlData); - $line['picture'] = $arImgPaths; + /** @var \Illuminate\Database\Eloquent\Model|null $model */ + $model = XmlInco::where($this->getUniqueValues($line))->first(); + + $line['pictures'] = $this->updateOrCreateImagesFiles( + $this->parseImages($xmlData), + $model + ); }); } - private function parseImage(SimpleXMLElement $xmlData) + /** + * Parse images + * + * @param SimpleXMLElement $xmlData + * @return array List of arrays [imgNamePath, imageData] + */ + private function parseImages(SimpleXMLElement $xmlData): array { $images = $xmlData->INCO->MEDIA_VISUEL_EAI->MEDIA_VISUEL_EAI; $arImgPaths = array(); @@ -432,51 +481,138 @@ class XmlIncoReader extends XmlJobs foreach ($images as $image) { $imageData = trim((string)$image); - if (strlen($imageData)) { - $imageData = base64_decode($imageData); - $imgName = strval($xmlData->INCO->INCO_COMPO->TBL_INCO_COMPO->ID_INCO); + if (strlen($imageData) and $imageData = base64_decode($imageData)) { + $imgName = strval($xmlData->INCO->INCO_COMPO->TBL_INCO_COMPO->ID_INCO); if (empty($imgName)) { $imgName = strval($xmlData->INCO->NO_ART); } $imgName = $imgName . "_" . $index; $imgNamePath = sprintf('%s/%s', rtrim($this->imagesPath, '/'), $imgName); - $disk = $this->getDisk(); - $info = getimagesizefromstring($imageData); - if ($info !== false) { - $mime = $info['mime']; - switch ($mime) { - case 'image/jpeg': - $imgExtension = 'jpg'; - break; - case 'image/png': - $imgExtension = 'png'; - break; - case 'image/gif': - $imgExtension = 'gif'; - break; - default: - throw new AbortXmlImportException(sprintf('Unknown image type %s', $imgName)); - } + if ($info === false) { + Log::debug("Could not get file info from $imgName"); + continue; + } + switch ($info['mime'] ?? '') { + case 'image/jpeg': + $imgExtension = 'jpg'; + break; + case 'image/png': + $imgExtension = 'png'; + break; + case 'image/gif': + $imgExtension = 'gif'; + break; + default: + Log::debug("Unknown image type from $imgName"); + } - $imgNamePath = sprintf('%s.%s', $imgNamePath, $imgExtension); + $imgNamePath = sprintf('%s.%s', $imgNamePath, $imgExtension); - if (!$disk->put($imgNamePath, $imageData)) { - throw new AbortXmlImportException(sprintf('The image %s could not be saved', $imgNamePath)); - } - - $arImgPaths[$index] = sprintf('%s/%s', $this->disk, $imgNamePath); - $index++; - } else { - // probably not an image could be a PDF, PPT, AI anything is possible - }//end if - }//end if - }//end foreach + $arImgPaths[$index] = compact('imgNamePath', 'imageData'); + $index++; + } //end if + } //end foreach return $arImgPaths; } + /** + * Update or create images + * + * @param array $images + * @param \Illuminate\Database\Eloquent\Model|null $model + * @return array Pictures pathes with checksums as keys that will be save into model. + */ + private function updateOrCreateImagesFiles(array $images, Model $model = null): array + { + $disk = $this->getDisk(); + + $oldChecksums = []; + $oldPicturesPaths = []; + + if ($model) { + /** @var array $oldChecksums */ + $oldChecksums = \array_keys($model->pictures ?? []); + /** @var array $oldPicturesPaths */ + $oldPicturesPaths = $model->pictures ?? []; + } + + /** @var array $oldPictures old pictures pathes with checksums as keys */ + $oldPictures = \array_combine($oldChecksums, $oldPicturesPaths); + + /** @var array $newPictures new pictures pathes with checksums as keys */ + $newPictures = collect($images)->mapWithKeys(function (array $imageProps) { + /** @var string $imgNamePath */ + /** @var string $imageData */ + extract($imageProps); + + $imageFinalPath = sprintf('%s/%s', $this->disk, $imgNamePath); + + // ? Sucessfully calculate checksum ? + if (!($newFileChk = hash('crc32b', $imageFinalPath . $imageData))) { + throw new \ErrorException('crc32b doesnt seems to be supported.'); + } + return [$newFileChk => $imageFinalPath]; + }); + + /** @var array $modelPictures pictures that wont change */ + $modelPictures = \array_intersect_key($oldPictures, $newPictures); + + /** @var \Illuminate\Support\Collection $removePictures pictures that we will remove (delete from disk) */ + $removePictures = collect(\array_diff_key($oldPictures, $newPictures)); + + /** @var \Illuminate\Support\Collection $addPictures pictures that we will create (save on disk) */ + $addPictures = collect(\array_diff_key($newPictures, $oldPictures)); + + // * Save or remove pictures from storage as needed + collect($images)->mapWithKeys(function ( + array $imageProps, + string $checksum + ) use ( + $disk, + $removePictures, + $addPictures + ) { + /** @var string $imgNamePath */ + /** @var string $imageData */ + extract($imageProps); + + if ($addPictures->has($checksum)) { + // * Save new picture + if (!$disk->put($imgNamePath, $imageData)) { + throw new AbortXmlImportException(sprintf('The image %s could not be saved', $imgNamePath)); + } + } + + if ($removePictures->has($checksum)) { + // * Remove old picture (if exists) + if ($disk->exists($imgNamePath) and !$disk->delete($imgNamePath)) { + throw new AbortXmlImportException(sprintf('The image %s could not be delete', $imgNamePath)); + } + } + }); + + return $newPictures; + } + + /** + * Get Unique Values for a specific file + * + * @param array $line + * @return array + */ + private function getUniqueValues(array $line): array + { + /** @var array Unique Key columns */ + $uniqueColumnsKey = []; + foreach ($this->uniqueKeyField as $uField) { + $uniqueColumnsKey[$uField] = $line[$uField]; + } + return $uniqueColumnsKey; + } + /** * Recursive extract to array * introduce sort of Hook to transform @@ -520,7 +656,7 @@ class XmlIncoReader extends XmlJobs } $extract[$key][] = $extraction; -// $this->extract($value); + // $this->extract($value); continue; } $value = strval($value); @@ -529,7 +665,7 @@ class XmlIncoReader extends XmlJobs } else { $extract[$key] = strlen($value) ? $value : null; } - }//end foreach + } //end foreach return $extract; } } diff --git a/app/Jobs/XmlJobs.php b/app/Jobs/Arcadia/XmlJobs.php similarity index 60% rename from app/Jobs/XmlJobs.php rename to app/Jobs/Arcadia/XmlJobs.php index 2c3d7bc..43933bb 100644 --- a/app/Jobs/XmlJobs.php +++ b/app/Jobs/Arcadia/XmlJobs.php @@ -1,38 +1,32 @@ getDisk(); $this->log('Démarrage du traitement'); + + $this->assertXsdIsHereIfAny(); + $files = $disk->files(); - $progress = 1; - $total = count(array_filter($files, function ($filename) { + $total = count(array_filter($files, function ($filename) { if ($this->fileHasToBeIgnored($filename)) { return false; } return true; })); + $this->startProgressBar($total); + foreach ($files as $filename) { if ($this->fileHasToBeIgnored($filename)) { continue; } - $this->log(sprintf(' traitement %d sur %s ', $progress++, $total)); + $this->incrementProgressBar(); + try { /** @var string $data */ $data = $disk->get($filename); + + $this->validateXmlFile($filename, $data); + try { - $this->validateSchema($data); if ($this->encapsulate) { $data = sprintf('%s', $data); - /** @var SimpleXMLElement|false */ + /** @var \SimpleXMLElement|false */ $xmlContent = simplexml_load_string($data); } else { - /** @var SimpleXMLElement|false */ + /** @var \SimpleXMLElement|false */ $xmlContent = simplexml_load_string($data, null, LIBXML_NOCDATA | LIBXML_PARSEHUGE); } if ($xmlContent === false) { throw new InvalidXmlException('XML is Invalid (simplexml_load_string returned false)'); } - } catch (ErrorException $e) { + } catch (\ErrorException $e) { throw new InvalidXmlException('XML is Invalid', 0, $e); } - $data = $this->extract($xmlContent); - + $data = $this->extract($xmlContent); $lines = []; - foreach ($data[$entityName] as $traca) { - $line = $this->processFields($traca); - if ($callback) { - $callback($xmlContent, $line); + + // * Multiple Entities within file + if (isset($data[$entityName])) { + foreach ($data[$entityName] as $traca) { + $line = $this->processFields($traca); + if ($afterParseCallback) { + $afterParseCallback($xmlContent, $line); + } + $lines[] = $line; + } + } else { + // * Only one entity within file + $line = $this->processFields($data); + if ($afterParseCallback) { + $afterParseCallback($xmlContent, $line); } $lines[] = $line; } - // Mise en base des données + + // * Mise en base des données foreach ($lines as $line) { /** @var array Unique Key columns */ $uniqueColumnsKey = []; foreach ($this->uniqueKeyField as $uField) { $uniqueColumnsKey[$uField] = $line[$uField]; } - forward_static_call_array( - [$modelClass, 'updateOrCreate'], - [$uniqueColumnsKey, $line] - ); - } + if (count($uniqueColumnsKey)) { + if ($saveCallback) { + $saveCallback($uniqueColumnsKey, $line); + } else { + forward_static_call_array( + [$modelClass, 'updateOrCreate'], + [$uniqueColumnsKey, $line] + ); + } + if ($afterSaveCallback) { + $afterSaveCallback($xmlContent, $line); + } + } + } //end foreach } catch (InvalidXmlException $e) { $message = $e->getMessage(); - if (get_class($e) === QueryException::class) { - $message = implode(' ', $e->errorInfo); - } $this->log( sprintf( 'File %s could not be parsed => %s on line %d', @@ -146,19 +173,6 @@ abstract class XmlJobs extends XArcadiaFlux $this->sendInvalidXmlEmail($filename, $e); $this->moveToInvalid($filename); continue; - } catch (ErrorException | TypeError $e) { - $this->log( - sprintf( - 'CRITICAL: File %s could not be parsed => %s on line %d', - $filename, - $e->getMessage(), - $e->getLine() - ), - LogType::critical() - ); - $this->sendErrorParseEmailToServiceWeb($e); - continue; -// ignore the file } catch (AbortXmlImportException $e) { $this->log( sprintf( @@ -169,15 +183,16 @@ abstract class XmlJobs extends XArcadiaFlux ), LogType::error() ); - $this->sendErrorParseEmailToServiceWeb($e); + report($e); continue; -// ignore the file - } catch (FileNotFoundException | QueryException $e) { - // This happens because of concurent xml jobs read + // * ignore the file + } catch (QueryException $e) { + $this->log(implode(' ', $e->errorInfo), LogType::critical()); + report($e); + continue; + } catch (FileNotFoundException $e) { + // This happens because of concurent xml jobs read. $message = $e->getMessage(); - if (get_class($e) === QueryException::class) { - $message = implode(' ', $e->errorInfo); - } $this->log( sprintf( 'ERROR: File %s could not be read (not found) => %s on line %d', @@ -188,13 +203,31 @@ abstract class XmlJobs extends XArcadiaFlux LogType::debug() ); continue; -// ignore the file - }//end try + // * ignore the file + } catch (\Exception | \ErrorException | \TypeError $e) { + $this->log( + sprintf( + 'File %s could not be parsed => %s on line %d', + $filename, + $e->getMessage(), + $e->getLine() + ), + LogType::critical() + ); + report($e); + continue; + // * ignore the file + } //end try $this->moveToArchives($filename); - }//end foreach + } //end foreach - // Clear old archives + // Clear old invalids. + $this->clearOldInvalids(); + + // Clear old archives. $this->clearOldArchives(); + + $this->endProgressBar(); } /** @@ -205,7 +238,7 @@ abstract class XmlJobs extends XArcadiaFlux */ protected function extract($sxe = null): array { - if (!$sxe instanceof SimpleXMLElement) { + if (!$sxe instanceof \SimpleXMLElement) { return array(); } $extract = array(); @@ -229,7 +262,7 @@ abstract class XmlJobs extends XArcadiaFlux * * @param array $fields * @return array - * @throws AbortXmlImportException + * @throws AbortXmlImportException If a field could not be validated. */ protected function processFields(array $fields): array { @@ -242,8 +275,8 @@ abstract class XmlJobs extends XArcadiaFlux throw new AbortXmlImportException(sprintf('The field %s is not allowed to be null', $key)); } if (is_array($field)) { - // Flatten data - $it = new RecursiveIteratorIterator(new RecursiveArrayIterator($field)); + // * Flatten data + $it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($field)); foreach ($it as $k => $f) { $ko = strtolower(sprintf('%s.%s', $key, $k)); if (!$this->isFieldNullable($ko) && is_null($f)) { @@ -261,7 +294,7 @@ abstract class XmlJobs extends XArcadiaFlux } else { $result[$key] = $field; } - }//end foreach + } //end foreach return $result; } @@ -309,24 +342,6 @@ abstract class XmlJobs extends XArcadiaFlux return in_array($filename, $this->ignoreFiles) or !$this->checkExtension($filename, 'xml'); } - /** - * Validate xml data against xsd file if provided - * - * @return void - */ - protected function validateSchema($data) - { - if ($this->xsdFile) { - // check against XSD - $xsd = storage_path($this->disk . "/" . $this->xsdFile); - $domDocument = new \DOMDocument(); - $domDocument->loadXML($data); - // free some memroy to avoid overload - unset($data); - $domDocument->schemaValidate($xsd); - } - } - /** * Check if the file is an xml file * @@ -342,78 +357,81 @@ abstract class XmlJobs extends XArcadiaFlux return $isXml; } - protected function sendErrorParseEmailToServiceWeb(Throwable $e) - { - Utils::sendEmailErrorManually($e); - } - - protected function sendInvalidXmlEmail(string $filename, Throwable $e) + /** + * Send invalid XML email + * + * @param string $filename + * @param \Throwable $e + * @return void + */ + protected function sendInvalidXmlEmail(string $filename, \Throwable $e) { $message = $e->getPrevious()->getMessage(); - Mail::send('emails.invalidxml', [ + Mail::send('mails.invalidxml', [ 'strError' => sprintf( 'File %s could not be parsed => %s on line %d', $filename, $message, $e->getLine() ), - 'appName' => config('app.name'), 'filename' => $filename, 'exception' => $e->getPrevious() ], function ($m) { $m->to( config('mail.contactsList.invalidInco'), - sprintf('%s Exception Handler', config('app.name')) + sprintf('%s XML Exception Handler', config('app.name')) )->subject('You sent me an invalid XML file, please check its validity before resend ASAP.'); }); } /** - * Move the file to invalid folder + * Assert XSD is readable if configured * - * @param string $filename * @return void + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException If xsd file cant be read. */ - protected function moveToInvalid(string $filename): void + private function assertXsdIsHereIfAny() { - $newFileName = $filename; - if (!$this->getDisk()->exists($filename)) { - $this->log( - sprintf( - 'File %s could not be moved to invalid (not found)', - $filename - ), - LogType::warning() - ); - return; - } - if ($this->getDisk()->exists(sprintf('%s/%s', $this->invalidFolderName, $filename))) { - $ext = \pathinfo($filename, \PATHINFO_EXTENSION); - $newFileName = \sprintf( - '%s_%d.%s', - \substr($filename, 0, 0 - (strlen($ext) + 1)), - \time(), - $ext - ); - } - try { - if (!$this->getDisk()->move($filename, sprintf('%s/%s', $this->invalidFolderName, $newFileName))) { - $this->log( - sprintf( - 'File %s could not be moved to invalid', - $filename - ), - LogType::warning() - ); - } - } catch (Exception $e) { - $this->log( - sprintf( - 'File %s could not be moved to invalid', - $filename - ), - LogType::warning() - ); + if ($this->xsdSchemaPath) { + $this->xsdSchema = File::get($this->xsdSchemaPath); } } + + /** + * Validate Xml file against XSD if loaded + * + * @param string $fileName + * @param string $fileContent + * @return void + * @throws InvalidXmlException If XML could not be parsed or validated against XSD. + * @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber + */ + private function validateXmlFile(string $fileName, string $fileContent) + { + if ($this->xsdSchema) { + $domDocument = new \DOMDocument('1.0', 'UTF-8'); + try { + if (!$domDocument->loadXML($fileContent)) { + throw new InvalidXmlException("XML Validation $fileName content could not be loaded to validate."); + } + if (!$domDocument->schemaValidateSource($this->xsdSchema, LIBXML_SCHEMA_CREATE)) { + throw new InvalidXmlException( + "XML Validation $fileName content could not be validated against xsd." + ); + } + } catch (\Exception $e) { + if (\get_class($e) === InvalidXmlException::class) { + throw $e; + } + throw new InvalidXmlException( + "XML Validation $fileName content could not be validated against xsd.", + 0, + 1, + __FILE__, + __LINE__, + $e + ); + } //end try + } //end if + } } diff --git a/app/Jobs/SetServiceStats.php b/app/Jobs/SetServiceStats.php new file mode 100644 index 0000000..87d3b60 --- /dev/null +++ b/app/Jobs/SetServiceStats.php @@ -0,0 +1,81 @@ +service = $service; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + /** @var \App\Models\ServiceStat */ + $serviceStat = ServiceStat::firstOrNew([ + 'date' => date('Y-m-d'), + 'service_id' => $this->service->id, + ]); + if (!$serviceStat->exists) { + $serviceStat->service()->associate($this->service); + } + $serviceStat->nb_calls += 1; + $serviceStat->saveOrFail(); + } + + /** + * Determine the time at which the job should timeout. + * + * @return \DateTime + */ + public function retryUntil() + { + return now()->addMinutes(rand(0, 360)); + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware() + { + return [ + (new WithoutOverlapping(self::KEY)) + ->releaseAfter(rand(90, 180)) + ]; + } +} diff --git a/app/Lib/Helper.php b/app/Lib/Helper.php new file mode 100644 index 0000000..3ddf065 --- /dev/null +++ b/app/Lib/Helper.php @@ -0,0 +1,180 @@ +path(), 'api/v1') === 0 ? 'v1' : null) ?? 'none'; + return strpos($routePrefix, 'v1') === 0; + } + + /** + * Try to get current route name + * + * @param Request $request + * @return string|null + */ + public static function getRouteName(Request $request) + { + return Route::currentRouteName() ?? ($request->route() ? $request->route()->getName() : null); + } + + /** + * Get the bearer token from request + * + * @param Request $request + * @return string|null + */ + public static function getBearerTokenFromRequest(Request $request): ?string + { + $header = $request->header('Authorization', ''); + + if (Str::contains($header, 'Bearer ')) { + $auths = \explode(',', $header); + foreach ($auths as $auth) { + if (Str::contains($auth, 'Bearer ')) { + return Str::substr($auth, 7); + } + } + } + return null; + } + + /** + * Try to get current route prefix + * + * @param Request $request + * @return string|null + */ + public static function getRoutePrefix(Request $request) + { + return (Route::getCurrentRoute() ? Route::getCurrentRoute()->getPrefix() : null) ?? + ($request->route() ? $request->route()->getPrefix() : null); + } + + /** + * Try to get route from request + * + * @param Request $request + * @return \Illuminate\Routing\Route|null + */ + public static function getRoute(Request $request) + { + return Route::getRoutes()->getByName(static::getRouteName($request)); + } + + /** + * Get composer.json infos + * + * @return array + */ + public static function getPackageInfo(): array + { + if (!static::$packageInfos) { + static::$packageInfos = \json_decode(\file_get_contents(__DIR__ . '/../../composer.json'), true); + } + return static::$packageInfos; + } + + /** + * Calculate the request signature + * + * @param string $secret + * @param string $uri + * @param string $method + * @param array $parameters + * @return string request signature + * @throws ApplicationException If an error occurs. + */ + public static function signRequest(string $secret, string $uri, string $method, array $parameters): string + { + try { + $uri = collect(\parse_url($uri)); + $method = \strtolower($method); + + // * Remove signature from parameters to calculate the hashmac. + $signature = collect([ + 'uri' => sprintf( + '%s://%s%s', + $uri->get('scheme'), + $uri->get('host'), + $uri->get('path') + ), + 'method' => $method + ]); + + $flattenRequestArray = function (array $array) use (&$flattenRequestArray) { + $out = []; + foreach ($array as $key => $value) { + if (is_array($value)) { + $subArray = $flattenRequestArray($value); + foreach ($subArray as $subKey => $subValue) { + $out["$key.$subKey"] = $subValue; + } + continue; + } + $out[$key] = $value; + } + return $out; + }; + + $parameters = $flattenRequestArray($parameters); + $parameters = collect($parameters); + + // * Remove signature from parameters. + $parameters->pull('signature'); + + // * Natural sort all parameters. + $parameters = $parameters->sortKeys(\SORT_NATURAL) + ->map(function ($value, $key) { + return sprintf('%s=%s', \rawurlencode($key), \rawurlencode($value)); + }); + + $signature->push($parameters->implode('&')); + + $calculatedHash = base64_encode(hash_hmac( + 'sha256', + $signature->implode('\n'), + Crypt::decryptString($secret), + true + )); + + if (!$calculatedHash) { + throw new ApplicationException('Failed to calculate requests hash'); + } + + return $calculatedHash; + } catch (DecryptException $e) { + throw new ApplicationException('Failed to decrypt service credentials', 0, $e); + } catch (\Exception $e) { + throw new ApplicationException('An error occured when authenticating service', 0, $e); + } //end try + } +} diff --git a/app/Lib/Utils.php b/app/Lib/Utils.php deleted file mode 100644 index 1583128..0000000 --- a/app/Lib/Utils.php +++ /dev/null @@ -1,209 +0,0 @@ - - */ -// phpcs:disable Generic.Files.LineLength.TooLong -class Utils -{ - /** - * Replace stange accents - * - * https://stackoverflow.com/questions/3371697/replacing-accented-characters-php - * @author Jérémy Munsch - * @param string $subject - * @return string - */ - public static function replaceSpecChar(string $subject): string - { - $char_map = array( - "ъ" => "-", "ь" => "-", "Ъ" => "-", "Ь" => "-", - "А" => "A", "Ă" => "A", "Ǎ" => "A", "Ą" => "A", "À" => "A", "Ã" => "A", "Á" => "A", "Æ" => "A", "Â" => "A", "Å" => "A", "Ǻ" => "A", "Ā" => "A", "א" => "A", - "Б" => "B", "ב" => "B", "Þ" => "B", - "Ĉ" => "C", "Ć" => "C", "Ç" => "C", "Ц" => "C", "צ" => "C", "Ċ" => "C", "Č" => "C", "©" => "C", "ץ" => "C", - "Д" => "D", "Ď" => "D", "Đ" => "D", "ד" => "D", "Ð" => "D", - "È" => "E", "Ę" => "E", "É" => "E", "Ë" => "E", "Ê" => "E", "Е" => "E", "Ē" => "E", "Ė" => "E", "Ě" => "E", "Ĕ" => "E", "Є" => "E", "Ə" => "E", "ע" => "E", - "Ф" => "F", "Ƒ" => "F", - "Ğ" => "G", "Ġ" => "G", "Ģ" => "G", "Ĝ" => "G", "Г" => "G", "ג" => "G", "Ґ" => "G", - "ח" => "H", "Ħ" => "H", "Х" => "H", "Ĥ" => "H", "ה" => "H", - "I" => "I", "Ï" => "I", "Î" => "I", "Í" => "I", "Ì" => "I", "Į" => "I", "Ĭ" => "I", "I" => "I", "И" => "I", "Ĩ" => "I", "Ǐ" => "I", "י" => "I", "Ї" => "I", "Ī" => "I", "І" => "I", - "Й" => "J", "Ĵ" => "J", - "ĸ" => "K", "כ" => "K", "Ķ" => "K", "К" => "K", "ך" => "K", - "Ł" => "L", "Ŀ" => "L", "Л" => "L", "Ļ" => "L", "Ĺ" => "L", "Ľ" => "L", "ל" => "L", - "מ" => "M", "М" => "M", "ם" => "M", - "Ñ" => "N", "Ń" => "N", "Н" => "N", "Ņ" => "N", "ן" => "N", "Ŋ" => "N", "נ" => "N", "ʼn" => "N", "Ň" => "N", - "Ø" => "O", "Ó" => "O", "Ò" => "O", "Ô" => "O", "Õ" => "O", "О" => "O", "Ő" => "O", "Ŏ" => "O", "Ō" => "O", "Ǿ" => "O", "Ǒ" => "O", "Ơ" => "O", - "פ" => "P", "ף" => "P", "П" => "P", - "ק" => "Q", - "Ŕ" => "R", "Ř" => "R", "Ŗ" => "R", "ר" => "R", "Р" => "R", "®" => "R", - "Ş" => "S", "Ś" => "S", "Ș" => "S", "Š" => "S", "С" => "S", "Ŝ" => "S", "ס" => "S", - "Т" => "T", "Ț" => "T", "ט" => "T", "Ŧ" => "T", "ת" => "T", "Ť" => "T", "Ţ" => "T", - "Ù" => "U", "Û" => "U", "Ú" => "U", "Ū" => "U", "У" => "U", "Ũ" => "U", "Ư" => "U", "Ǔ" => "U", "Ų" => "U", "Ŭ" => "U", "Ů" => "U", "Ű" => "U", "Ǖ" => "U", "Ǜ" => "U", "Ǚ" => "U", "Ǘ" => "U", - "В" => "V", "ו" => "V", - "Ý" => "Y", "Ы" => "Y", "Ŷ" => "Y", "Ÿ" => "Y", - "Ź" => "Z", "Ž" => "Z", "Ż" => "Z", "З" => "Z", "ז" => "Z", - "а" => "a", "ă" => "a", "ǎ" => "a", "ą" => "a", "à" => "a", "ã" => "a", "á" => "a", "æ" => "a", "â" => "a", "å" => "a", "ǻ" => "a", "ā" => "a", "א" => "a", - "б" => "b", "ב" => "b", "þ" => "b", - "ĉ" => "c", "ć" => "c", "ç" => "c", "ц" => "c", "צ" => "c", "ċ" => "c", "č" => "c", "©" => "c", "ץ" => "c", - "Ч" => "ch", "ч" => "ch", - "д" => "d", "ď" => "d", "đ" => "d", "ד" => "d", "ð" => "d", - "è" => "e", "ę" => "e", "é" => "e", "ë" => "e", "ê" => "e", "е" => "e", "ē" => "e", "ė" => "e", "ě" => "e", "ĕ" => "e", "є" => "e", "ə" => "e", "ע" => "e", - "ф" => "f", "ƒ" => "f", - "ğ" => "g", "ġ" => "g", "ģ" => "g", "ĝ" => "g", "г" => "g", "ג" => "g", "ґ" => "g", - "ח" => "h", "ħ" => "h", "х" => "h", "ĥ" => "h", "ה" => "h", - "i" => "i", "ï" => "i", "î" => "i", "í" => "i", "ì" => "i", "į" => "i", "ĭ" => "i", "ı" => "i", "и" => "i", "ĩ" => "i", "ǐ" => "i", "י" => "i", "ї" => "i", "ī" => "i", "і" => "i", - "й" => "j", "Й" => "j", "Ĵ" => "j", "ĵ" => "j", - "ĸ" => "k", "כ" => "k", "ķ" => "k", "к" => "k", "ך" => "k", - "ł" => "l", "ŀ" => "l", "л" => "l", "ļ" => "l", "ĺ" => "l", "ľ" => "l", "ל" => "l", - "מ" => "m", "м" => "m", "ם" => "m", - "ñ" => "n", "ń" => "n", "н" => "n", "ņ" => "n", "ן" => "n", "ŋ" => "n", "נ" => "n", "ʼn" => "n", "ň" => "n", - "ø" => "o", "ó" => "o", "ò" => "o", "ô" => "o", "õ" => "o", "о" => "o", "ő" => "o", "ŏ" => "o", "ō" => "o", "ǿ" => "o", "ǒ" => "o", "ơ" => "o", - "פ" => "p", "ף" => "p", "п" => "p", - "ק" => "q", - "ŕ" => "r", "ř" => "r", "ŗ" => "r", "ר" => "r", "р" => "r", "®" => "r", - "ş" => "s", "ś" => "s", "ș" => "s", "š" => "s", "с" => "s", "ŝ" => "s", "ס" => "s", - "т" => "t", "ț" => "t", "ט" => "t", "ŧ" => "t", "ת" => "t", "ť" => "t", "ţ" => "t", - "ù" => "u", "û" => "u", "ú" => "u", "ū" => "u", "у" => "u", "ũ" => "u", "ư" => "u", "ǔ" => "u", "ų" => "u", "ŭ" => "u", "ů" => "u", "ű" => "u", "ǖ" => "u", "ǜ" => "u", "ǚ" => "u", "ǘ" => "u", - "в" => "v", "ו" => "v", - "ý" => "y", "ы" => "y", "ŷ" => "y", "ÿ" => "y", - "ź" => "z", "ž" => "z", "ż" => "z", "з" => "z", "ז" => "z", "ſ" => "z", - "™" => "tm", - "@" => "at", - "Ä" => "ae", "Ǽ" => "ae", "ä" => "ae", "æ" => "ae", "ǽ" => "ae", - "ij" => "ij", "IJ" => "ij", - "я" => "ja", "Я" => "ja", - "Э" => "je", "э" => "je", - "ё" => "jo", "Ё" => "jo", - "ю" => "ju", "Ю" => "ju", - "œ" => "oe", "Œ" => "oe", "ö" => "oe", "Ö" => "oe", - "щ" => "sch", "Щ" => "sch", - "ш" => "sh", "Ш" => "sh", - "ß" => "ss", - "Ü" => "ue", - "Ж" => "zh", "ж" => "zh", - ); - return self::mbStrtr($subject, $char_map); - } - - /** - * Multibyte strtr - * - * @param string $str - * @param string|array $from - * @param string|null $to - * @return string - */ - public static function mbStrtr($str, $from, $to = null): string - { - if (is_array($from)) { - $from = array_map('utf8_decode', $from); - $from = array_map('utf8_decode', array_flip($from)); - return utf8_encode(strtr(utf8_decode($str), array_flip($from))); - } - return utf8_encode(strtr(utf8_decode($str), utf8_decode($from), utf8_decode($to))); - } - - /** - * Manually send an exception to team instead of going trough the error handler - * (requires package kwaadpepper/exception-handler) - * - * @param Throwable $e - * @return void - */ - public static function sendEmailErrorManually(Throwable $e) - { - Mail::send([ - 'html' => 'exception-handler::emails.exception', - 'text' => 'exception-handler::emails.exception-plain' - ], [ - 'appName' => config('app.name'), - 'code' => $e->getCode(), - 'line' => $e->getLine(), - 'ename' => \get_class($e), - 'emessage' => self::getAnonymizedMessage($e->getMessage()), - 'file' => str_replace(\realpath(\sprintf('%s/../', \app_path())), '', $e->getFile()), - 'stackTrace' => self::getAnonymizedStackTrace($e), - 'uri' => app()->runningInConsole() ? - 'CONSOLE' : ( - (request() and request()->fullUrl()) ? - request()->fullUrl() : 'none') - ], function ($m) { - /** @var \Illuminate\Mail\Message $m */ - $m->to( - config('mail.contactsList.serviceweb'), - sprintf('%s Exception Handler', config('app.name')) - )->subject('Need help !, an error occured in your application, please check Logs ASAP'); - }); - Log::alert(sprintf('Envoie d\'une erreur par mail : %s', json_encode($e))); - } - - /** - * Anonymize at least Sql queries - * - * @param string $message - * @return string - */ - private static function getAnonymizedMessage(string $message): string - { - $message = \preg_replace('/^(.*)( \(SQL:.*\))$/', '$1', $message) ?? $message; - return $message; - } - - /** - * Anonymize stack trace - * - * @param \Exception $e - * @return string - */ - private static function getAnonymizedStackTrace(Throwable $e): string - { - $projectPath = \realpath(\sprintf('%s/../', \app_path())); - $traceStack = $e->getTrace(); - $traceStr = ''; - $maxStackStage = 0; - foreach ($traceStack as $stackStage => $stack) { - $maxStackStage = $stackStage; - $traceStr .= \sprintf( - '#%s %s(%d): ', - $stackStage, - str_replace($projectPath, '', $stack['file']), - $stack['line'] - ); - $traceStr .= "{$stack['class']}->{$stack['function']}("; - $traceArgs = ''; - if (isset($stack['args'])) { - $argsCount = count($stack['args']); - for ($i = 0; $i < $argsCount; $i++) { - $type = \gettype($stack['args'][$i]); - switch ($type) { - case 'object': - $traceArgs .= sprintf('Object(%s)', \get_class($stack['args'][$i])); - break; - case 'array': - $traceArgs .= sprintf('Array(%d)', \count($stack['args'][$i])); - break; - case 'string': - $traceArgs .= sprintf('String(%d)', \strlen($stack['args'][$i])); - break; - default: - $traceArgs .= \gettype($stack['args'][$i]); - } - $traceArgs .= ($i !== ($argsCount - 1) ? ',' : ''); - } - $traceStr .= $traceArgs; - }//end if - $traceStr .= ")\n"; - }//end foreach - $maxStackStage++; - $traceStr .= "#$maxStackStage {main}"; - return $traceStr; - } -} diff --git a/app/Models/ArcadiaBrand.php b/app/Models/ArcadiaBrand.php new file mode 100644 index 0000000..caf9e33 --- /dev/null +++ b/app/Models/ArcadiaBrand.php @@ -0,0 +1,90 @@ +cacheFor = config('api.arcadiaBrandsDBCache') * 60; + + parent::__construct($attributes); + } + + /** + * Set the base cache tags that will be present + * on all queries. + * + * ! This is used to flush caching + * + * @return array + */ + protected function getCacheBaseTags(): array + { + return [ + 'incollector_arcadia_brands', + ]; + } + + /** + * Perform any actions required after the model boots. + * + * @return void + */ + protected static function booted() + { + static::created(function () { + // * Flush Eloquent cache. + self::flushQueryCache(); + }); + static::updated(function () { + // * Flush Eloquent cache. + self::flushQueryCache(); + }); + static::deleted(function () { + // * Flush Eloquent cache. + self::flushQueryCache(); + }); + } + + // * RELATIONS + + /** + * Service belongsToMany relation + * + * @return \Illuminate\Database\Eloquent\Relations\belongsToMany + */ + public function services() + { + return $this->belongsToMany(Service::class); + } +} diff --git a/app/Models/Service.php b/app/Models/Service.php new file mode 100644 index 0000000..17ecf76 --- /dev/null +++ b/app/Models/Service.php @@ -0,0 +1,224 @@ +cacheFor = config('api.servicesDBCache') * 60; + + parent::__construct($attributes); + } + + /** + * Set the base cache tags that will be present + * on all queries. + * + * ! This is used to flush caching + * + * @return array + */ + protected function getCacheBaseTags(): array + { + return [ + 'incollector_services', + ]; + } + + /** + * Perform any actions required after the model boots. + * + * @return void + */ + protected static function booted() + { + static::creating(function (self $service) { + $service->generateBearerToken(); + $service->generateBearerPassword(); + }); + static::created(function () { + // * Flush Eloquent cache. + self::flushQueryCache(); + }); + static::updated(function () { + // * Flush Eloquent cache. + self::flushQueryCache(); + }); + static::deleted(function () { + // * Flush Eloquent cache. + self::flushQueryCache(); + }); + } + + // * RELATIONS + + /** + * ArcadiaBrand belongsToMany relation + * + * @return \Illuminate\Database\Eloquent\Relations\belongsToMany + */ + public function arcadiaBrands() + { + return $this->belongsToMany(ArcadiaBrand::class); + } + + /** + * ServiceStat hasMany relation + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function servicesStats() + { + return $this->hasMany(ServiceStat::class); + } + + // * METHODS + + /** + * Increment API CRM Stat + * + * @return void + */ + public function incrementStat(): void + { + SetServiceStats::dispatch($this)->afterResponse() + ->onConnection('database')->onQueue('stats'); + } + + /** + * Generates new bearer token + * + * @return string + */ + public function generateBearerToken(): string + { + $this->bearer_token = hash('ripemd160', Str::uuid()); + $this->bearer_token_date = now(); + return $this->bearer_token; + } + + /** + * Generates new bearer password + * It store the crypted password in model + * and returns the plain password + * + * @return string + */ + public function generateBearerPassword(): string + { + $pwd = hash('sha256', Str::uuid()); + $this->bearer_pwd = Crypt::encryptString($pwd); + return $pwd; + } + + /** + * Get Service from bearerToken with its brand + * + * @param string $bearerToken + * @return \App\Models\Service + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException If the token matches no known service. + */ + public static function getFromBearerToken(string $bearerToken): self + { + $services = static::all(); + /** @var \App\Models\Service $service */ + foreach ($services as $service) { + if ($service->bearer_token and $bearerToken === $service->bearer_token) { + return $service->load([ + 'arcadiaBrands' => function ($query) use ($service) { + return $query->cacheTags(["service:{$service->id}:arcadiaBrands"]); + }, + ]); + } + } + throw new ModelNotFoundException('Could not find service from Service Token'); + } + + /** + * Authenticate the request for this service + * + * This will check hash-mac and that the request is wellformed + * + * The request should have the minimum following parameters + * - timestamp using format Y-m-d H:i:s, and must be UTC (Greenwich Mean Time) + * - bearerToken the public token generated by the crm + * - signature the hashmac signature of the request + * + * ! Signature must be calculated with every parameter urlencode once only + * ! All parameters to calculate the signature must be sorted (natural sort asc) + * + * ! Signature should look like + * * httpVerb\n + * * uri\n + * * queryString + * + * ! httpVerb as well as uri must be lower case + * ! uri should include scheme, host and path and nothing else, if path is empty a trailing slash should be added + * ! eg: https://ldc-crm.fr/customer/1 or https://ldc-crm.fr/ + * ! The query string shall be separated with '&', eg param1=value1¶m2=value2 + * ! Key names and values of the query string must be url encoded using RFC 3986 + * ! http://www.faqs.org/rfcs/rfc3986.html + * ! Signature must be hmac sha256 using the private key generatedby the crm + * + * * ! They should then be concat in a string separated by \n + * + * + * + * @param \Illuminate\Http\Request $request + * @return boolean + * @throws \App\Exceptions\Api\V2\CrmException If an error occurs. + */ + public function authenticateApi(Request $request): bool + { + // ! Hack to prevent Laravel to cast empty strings to null or the opposite. + // ! If cannot decode json user legacy all() method. + $data = \json_decode($request->getContent(), true); + $data = (\is_array($data) and \json_last_error() === \JSON_ERROR_NONE) ? + array_replace_recursive($request->all(), $data) : $request->all(); + + $calculatedHash = Helper::signRequest( + $this->bearer_pwd, + $request->getUri(), + $request->getMethod(), + $data + ); + + // ! Très important hash_equals https://fr.wikipedia.org/wiki/Attaque_temporelle + return \hash_equals($calculatedHash, $request->signature); + } +} diff --git a/app/Models/ServiceStat.php b/app/Models/ServiceStat.php new file mode 100644 index 0000000..f424a4a --- /dev/null +++ b/app/Models/ServiceStat.php @@ -0,0 +1,39 @@ + 0 + ]; + + // * RELATIONS + + /** + * Service BelongsTo relation + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function service() + { + return $this->belongsTo(Service::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index e77b07f..aa3e11f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\Hash; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable @@ -42,4 +43,21 @@ class User extends Authenticatable protected $casts = [ 'email_verified_at' => 'datetime', ]; + + /** + * Perform any actions required after the model boots. + * + * @return void + */ + protected static function booted() + { + static::creating(function (self $user) { + $user->password = Hash::make($user->password); + }); + static::updating(function (self $user) { + if (Hash::needsRehash($user->password)) { + $user->password = Hash::make($user->password); + } + }); + } } diff --git a/app/Models/XmlInco.php b/app/Models/XmlInco.php index e8d23f3..c489618 100644 --- a/app/Models/XmlInco.php +++ b/app/Models/XmlInco.php @@ -4,14 +4,26 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Support\Facades\Storage; -use Illuminate\Contracts\Filesystem\Filesystem; -use Illuminate\Support\Collection; +/** + * XmlInco + * + * pictures attribute is organized as following + * [ + * 'checksum' => 'storageFilePath' + * ] + * + * ! checksum includes fullfilepath + data + */ class XmlInco extends Model { use HasFactory; + /** + * The attributes that are mass assignable. + * + * @var array + */ protected $fillable = [ 'id', 'no_art', @@ -28,73 +40,40 @@ class XmlInco extends Model 'lst_ing', 'lib_allergene', 'nutritional_values', - 'picture', - ]; - - protected $casts = [ - 'picture' => 'array', - 'nutritional_values' => 'array', + 'pictures', ]; /** - * The Laravel disk name - * - * @var string - */ - protected $disk = 'inco'; - - protected $imagesPath = 'images'; - - /** - * The files to ignore + * The attributes that should be cast. * * @var array */ - protected $ignoreFiles = [ - '.gitignore', '.gitkeep' + protected $casts = [ + 'pictures' => 'array', + 'nutritional_values' => 'array', ]; - /** - * retrieve a collection of medias - */ - public function getMediasAttribute() - { - /** @var Filesystem */ - $disk = $this->getDisk(); - $files = $disk->files($this->imagesPath); - - $collFiles = new Collection(); - // /** @var File */ - foreach ($files as $file) { - $pi = \pathinfo($file, \PATHINFO_BASENAME); - if (in_array($pi, $this->ignoreFiles)) { - continue; - } - if (!in_array($pi, array($this->id))) { - continue; - } - $image = base64_encode($disk->get($file)); - $element = array( - "filename" => $pi, - "imgdata" => $image - ); - $collFiles->add($element); - } - return $collFiles; - } + // * GETTERS /** - * Get the concerned disk + * Get niceString attribute * - * @return Filesystem + * @return string */ - protected function getDisk(): Filesystem - { - return Storage::disk($this->disk); - } - - public function getNiceStringAttribute() + public function getNiceStringAttribute(): string { return sprintf('%s - %s', $this->id, $this->descriptif); } + + // * RELATIONS + + /** + * ArcadiaBrand belongsTo relation + * + * @return \Illuminate\Database\Eloquent\Relations\belongsTo + */ + public function arcadiaBrand() + { + return $this->belongsTo(ArcadiaBrand::class); + } } diff --git a/app/Policies/ArcadiaBrandPolicy.php b/app/Policies/ArcadiaBrandPolicy.php new file mode 100644 index 0000000..15c78a3 --- /dev/null +++ b/app/Policies/ArcadiaBrandPolicy.php @@ -0,0 +1,52 @@ +policies = [ + 'arcadia_brand' => [ + 'viewAny' => USR::make('everyOne'), + 'view' => USR::make('everyOne'), + 'create' => USR::make('noOne'), + 'update' => USR::make('noOne'), + 'delete' => USR::make('noOne') + ] + ]; + } + + /** + * Check if the customer belongs to the service + * + * @param Service $service + * @param ArcadiaBrand $arcadiaBrand + * @return boolean + */ + public function arcadiaBrandBelongsToService(Service $service, ArcadiaBrand $arcadiaBrand) + { + return $service->arcadiaBrands()->whereHas('services', function ($q) use ($arcadiaBrand) { + $q->whereId($arcadiaBrand->id); + })->exists(); + } +} diff --git a/app/Policies/Rules/UserStaticRules.php b/app/Policies/Rules/UserStaticRules.php new file mode 100644 index 0000000..32d42f7 --- /dev/null +++ b/app/Policies/Rules/UserStaticRules.php @@ -0,0 +1,32 @@ +getClientIp(); + } else { + return request()->server('HTTP_X_FORWARDED_FOR') ?? + request()->getClientIp(); + } + }); + + // * LOCAL FORCE eg: for Carbon + // * Remove numeric to prevent float seperator to breaking numeric transactions + \collect([\LC_TIME, \LC_MESSAGES, \LC_COLLATE, \LC_CTYPE, \LC_MONETARY]) + ->map(function ($contant) { + setlocale( + $contant, + \sprintf( + '%s_%s.UTF-8', + config('app.locale'), + \strtoupper(config('app.locale')) + ) + ); + }); + Carbon::setLocale(config('app.locale')); + + // * Force bootstrap paginator + Paginator::useBootstrap(); + + $appInfos = Cache::remember( + 'composer', + 360, + function () { + return json_decode(File::get(\app_path('../composer.json'))); + } + ); + View::share('name', $appInfos->name); + View::share('version', $appInfos->version); + View::share('license', $appInfos->license); } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 9425227..6c62e01 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -2,7 +2,12 @@ namespace App\Providers; +use App\Lib\Helper; +use App\Models\Service; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; class AuthServiceProvider extends ServiceProvider { @@ -11,9 +16,7 @@ class AuthServiceProvider extends ServiceProvider * * @var array */ - protected $policies = [ - // 'App\Models\Model' => 'App\Policies\ModelPolicy', - ]; + protected $policies = []; /** * Register any authentication / authorization services. @@ -23,5 +26,51 @@ class AuthServiceProvider extends ServiceProvider public function boot() { $this->registerPolicies(); + + Auth::viaRequest('api-hmac', function (Request $request) { + + /** + * Un token devrais être fourni pour chaque requete + */ + if (is_null($request->bearer_token)) { + return null; + } + + // * Si la route actuelle n'as pas de nom alors on refuse de la traiter. + if (!(Helper::getRoute($request))) { + // @codeCoverageIgnoreStart + return null; + // @codeCoverageIgnoreEnd + } + + $service = null; + + try { + // * On récupère le service actuel correspondant au token fourni. + $service = Service::getFromBearerToken($request->bearer_token ?? ''); + } catch (ModelNotFoundException $e) { + return null; + } + + // * Preload the arcadiaBrands as we will access it often later. + $service = $service->load('arcadiaBrands'); + + /** + ** All Api routes must pass ArcadiaBrand model to controller action since a ArcadiaBrand owns + ** a service token, otherwise the api cannot be called. + ** This hack is a custom binder instead of passing + ** this to route as a parameter + */ + app()->bind(Service::class, function () use ($service) { + return $service; + }); + + // * On authenticate la request en vérifiant sa signature. + if ($service->authenticateApi($request)) { + return $service; + } + + return null; + }); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 3bd3c81..6e792a1 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -19,15 +19,6 @@ class RouteServiceProvider extends ServiceProvider */ public const HOME = '/home'; - /** - * The controller namespace for the application. - * - * When present, controller route declarations will automatically be prefixed with this namespace. - * - * @var string|null - */ - // protected $namespace = 'App\\Http\\Controllers'; - /** * Define your route model bindings, pattern filters, etc. * @@ -56,8 +47,16 @@ class RouteServiceProvider extends ServiceProvider */ protected function configureRateLimiting() { + RateLimiter::for('web', function (Request $request) { + // @codeCoverageIgnoreStart + return Limit::perMinute(25) + ->by(optional($request->user())->id ?: app('clientIp')); + // @codeCoverageIgnoreEnd + }); + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass RateLimiter::for('api', function (Request $request) { - return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()); + return Limit::perMinute(config('api.apiRequestRatePerMinute', 100)) + ->by(app('clientIp')); }); } } diff --git a/composer.json b/composer.json index 85015c8..2d45737 100644 --- a/composer.json +++ b/composer.json @@ -1,19 +1,31 @@ { - "name": "laravel/laravel", + "name": "ldc/incollector", "type": "project", - "description": "The Laravel Framework.", - "keywords": ["framework", "laravel"], + "version": "1.0.0", + "description": "LDC Incollector - A workaround of Arcadia", + "authors": [ + { + "name": "Olivier Legat", + "email": "olivier.legat@ldc.fr" + }, + { + "name": "Jérémy Munsch", + "email": "jeremy.munsch@ldc.fr", + "homepage": "https://jeremydev.ovh" + } + ], "license": "MIT", "require": { "php": "^7.3|^8.0", "fruitcake/laravel-cors": "^2.0", "guzzlehttp/guzzle": "^7.0.1", + "kwaadpepper/crud-policies": "^0.3.5", "kwaadpepper/enum": "^2.1", "kwaadpepper/exception-handler": "^0.0.5", "laravel/framework": "^8.65", "laravel/sanctum": "^2.11", "laravel/tinker": "^2.5", - "squizlabs/php_codesniffer": "^3.6" + "rennokki/laravel-eloquent-query-cache": "^3.3" }, "require-dev": { "facade/ignition": "^2.5", @@ -22,7 +34,8 @@ "mockery/mockery": "^1.4.4", "nunomaduro/collision": "^5.10", "phpunit/phpunit": "^9.5.10", - "slevomat/coding-standard": "^7.1" + "slevomat/coding-standard": "^7.1", + "squizlabs/php_codesniffer": "^3.6" }, "autoload": { "psr-4": { @@ -37,6 +50,16 @@ } }, "scripts": { + "test": [ + "@configclear", + "./test.sh" + ], + "configclear": "@php artisan config:clear", + "runtestmigration": "@php artisan migrate:fresh --env=testing --seed", + "runtest": "@php artisan test --no-coverage --testsuite Api", + "runtestWithCoverage": "@php artisan test --testsuite Api", + "runserve": "@php artisan serve --host=127.0.0.1 --port=8888 --env=testing&", + "killserve": "kill $(ps -f | grep '[1]27.0.0.1:8888' | awk '{print $2}')", "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" diff --git a/composer.lock b/composer.lock index 1b23fa3..bb91026 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "675ecb5599b8e8d3dd09e345d535143b", + "content-hash": "739c10857a26ab983953d6da1124f1b2", "packages": [ { "name": "asm89/stack-cors", @@ -122,6 +122,353 @@ ], "time": "2021-08-15T20:50:18+00:00" }, + { + "name": "doctrine/cache", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "331b4d5dbaeab3827976273e9356b3b453c300ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/331b4d5dbaeab3827976273e9356b3b453c300ce", + "reference": "331b4d5dbaeab3827976273e9356b3b453c300ce", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^8.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "predis/predis": "~1.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.2 || ^6.0@dev", + "symfony/var-exporter": "^4.4 || ^5.2 || ^6.0@dev" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2021-07-17T14:49:29+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.3.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "719663b15983278227669c8595151586a2ff3327" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/719663b15983278227669c8595151586a2ff3327", + "reference": "719663b15983278227669c8595151586a2ff3327", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3", + "doctrine/event-manager": "^1.0", + "php": "^7.3 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "9.0.0", + "jetbrains/phpstorm-stubs": "2021.1", + "phpstan/phpstan": "1.5.3", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "9.5.16", + "psalm/plugin-phpunit": "0.16.1", + "squizlabs/php_codesniffer": "3.6.2", + "symfony/cache": "^5.2|^6.0", + "symfony/console": "^2.7|^3.0|^4.0|^5.0|^6.0", + "vimeo/psalm": "4.22.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.3.5" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2022-04-05T09:50:18+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "v0.5.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "9504165960a1f83cc1480e2be1dd0a0478561314" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/9504165960a1f83cc1480e2be1dd0a0478561314", + "reference": "9504165960a1f83cc1480e2be1dd0a0478561314", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0|^7.0|^8.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/v0.5.3" + }, + "time": "2021-03-21T12:59:47+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/41370af6a30faa9dc0368c4a6814d596e81aba7f", + "reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/1.1.x" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2020-05-29T18:28:51+00:00" + }, { "name": "doctrine/inflector", "version": "2.0.4", @@ -418,6 +765,178 @@ ], "time": "2020-12-29T14:50:06+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.13.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/master" + }, + "time": "2020-06-29T00:56:53+00:00" + }, + { + "name": "facade/flare-client-php", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/facade/flare-client-php.git", + "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facade/flare-client-php/zipball/b2adf1512755637d0cef4f7d1b54301325ac78ed", + "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed", + "shasum": "" + }, + "require": { + "facade/ignition-contracts": "~1.0", + "illuminate/pipeline": "^5.5|^6.0|^7.0|^8.0", + "php": "^7.1|^8.0", + "symfony/http-foundation": "^3.3|^4.1|^5.0", + "symfony/mime": "^3.4|^4.0|^5.1", + "symfony/var-dumper": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.14", + "phpunit/phpunit": "^7.5.16", + "spatie/phpunit-snapshot-assertions": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Facade\\FlareClient\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Send PHP errors to Flare", + "homepage": "https://github.com/facade/flare-client-php", + "keywords": [ + "exception", + "facade", + "flare", + "reporting" + ], + "support": { + "issues": "https://github.com/facade/flare-client-php/issues", + "source": "https://github.com/facade/flare-client-php/tree/1.9.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2021-09-13T12:16:46+00:00" + }, + { + "name": "facade/ignition-contracts", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/facade/ignition-contracts.git", + "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/3c921a1cdba35b68a7f0ccffc6dffc1995b18267", + "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v2.15.8", + "phpunit/phpunit": "^9.3.11", + "vimeo/psalm": "^3.17.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Facade\\IgnitionContracts\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://flareapp.io", + "role": "Developer" + } + ], + "description": "Solution contracts for Ignition", + "homepage": "https://github.com/facade/ignition-contracts", + "keywords": [ + "contracts", + "flare", + "ignition" + ], + "support": { + "issues": "https://github.com/facade/ignition-contracts/issues", + "source": "https://github.com/facade/ignition-contracts/tree/1.0.2" + }, + "time": "2020-10-16T08:27:54+00:00" + }, { "name": "fruitcake/laravel-cors", "version": "v2.2.0", @@ -882,6 +1401,72 @@ ], "time": "2022-03-20T21:55:58+00:00" }, + { + "name": "kwaadpepper/crud-policies", + "version": "0.3.5", + "source": { + "type": "git", + "url": "https://github.com/Kwaadpepper/crud-policies.git", + "reference": "7b7ef4c58839044f96e7862e4e27c945b96e8df2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Kwaadpepper/crud-policies/zipball/7b7ef4c58839044f96e7862e4e27c945b96e8df2", + "reference": "7b7ef4c58839044f96e7862e4e27c945b96e8df2", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.1", + "facade/flare-client-php": "^1.8", + "illuminate/support": "~8", + "kwaadpepper/enum": "^2.0", + "laravel/framework": "~8", + "mews/purifier": "^3.3" + }, + "require-dev": { + "orchestra/testbench": "^6.13", + "phpunit/phpunit": "~9.0", + "slevomat/coding-standard": "^7.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Kwaadpepper\\CrudPolicies\\Providers\\CrudPoliciesServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Kwaadpepper\\CrudPolicies\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jérémy Munsch", + "email": "github@jeremydev.ovh", + "homepage": "https://jeremydev.ovh" + } + ], + "description": "Makes CRUD easy, make policies easy and everyone happy", + "homepage": "https://github.com/kwaadpepper/crud-policies", + "keywords": [ + "Scafolding", + "crud", + "laravel", + "policies" + ], + "support": { + "issues": "https://github.com/Kwaadpepper/crud-policies/issues", + "source": "https://github.com/Kwaadpepper/crud-policies/tree/0.3.5" + }, + "time": "2022-03-22T09:32:05+00:00" + }, { "name": "kwaadpepper/enum", "version": "2.1.2", @@ -1611,6 +2196,87 @@ ], "time": "2021-11-21T11:48:40+00:00" }, + { + "name": "mews/purifier", + "version": "3.3.7", + "source": { + "type": "git", + "url": "https://github.com/mewebstudio/Purifier.git", + "reference": "9844bc82886e4291b3116afbd9ab5f532856662a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mewebstudio/Purifier/zipball/9844bc82886e4291b3116afbd9ab5f532856662a", + "reference": "9844bc82886e4291b3116afbd9ab5f532856662a", + "shasum": "" + }, + "require": { + "ezyang/htmlpurifier": "4.13.*", + "illuminate/config": "^5.8|^6.0|^7.0|^8.0|^9.0", + "illuminate/filesystem": "^5.8|^6.0|^7.0|^8.0|^9.0", + "illuminate/support": "^5.8|^6.0|^7.0|^8.0|^9.0", + "php": "^7.2|^8.0" + }, + "require-dev": { + "graham-campbell/testbench": "^3.2|^5.5.1", + "mockery/mockery": "^1.3.3", + "phpunit/phpunit": "^8.0|^9.0" + }, + "suggest": { + "laravel/framework": "To test the Laravel bindings", + "laravel/lumen-framework": "To test the Lumen bindings" + }, + "type": "package", + "extra": { + "laravel": { + "providers": [ + "Mews\\Purifier\\PurifierServiceProvider" + ], + "aliases": { + "Purifier": "Mews\\Purifier\\Facades\\Purifier" + } + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Mews\\Purifier\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Muharrem ERİN", + "email": "me@mewebstudio.com", + "homepage": "https://github.com/mewebstudio", + "role": "Developer" + } + ], + "description": "Laravel 5/6/7 HtmlPurifier Package", + "homepage": "https://github.com/mewebstudio/purifier", + "keywords": [ + "Purifier", + "htmlpurifier", + "laravel5 HtmlPurifier", + "laravel5 Purifier", + "laravel5 Security", + "laravel6 HtmlPurifier", + "laravel6 Purifier", + "laravel6 Security", + "security", + "xss" + ], + "support": { + "issues": "https://github.com/mewebstudio/Purifier/issues", + "source": "https://github.com/mewebstudio/Purifier/tree/3.3.7" + }, + "time": "2022-02-12T15:54:11+00:00" + }, { "name": "monolog/monolog", "version": "2.4.0", @@ -1998,6 +2664,55 @@ ], "time": "2021-12-04T23:24:31+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "time": "2016-08-06T20:24:11+00:00" + }, { "name": "psr/container", "version": "1.1.1", @@ -2657,60 +3372,71 @@ "time": "2021-09-25T23:10:38+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "name": "rennokki/laravel-eloquent-query-cache", + "version": "3.3.0", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "url": "https://github.com/renoki-co/laravel-eloquent-query-cache.git", + "reference": "00e73a7050bedca315c71bf433d0c283f8a73fbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/renoki-co/laravel-eloquent-query-cache/zipball/00e73a7050bedca315c71bf433d0c283f8a73fbc", + "reference": "00e73a7050bedca315c71bf433d0c283f8a73fbc", "shasum": "" }, "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" + "illuminate/database": "^8.83|^9.0.1", + "illuminate/support": "^8.83|^9.0.1" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "chelout/laravel-relationship-events": "^1.5", + "laravel/legacy-factories": "^1.3", + "mockery/mockery": "^1.5", + "orchestra/database": "^6.28|^7.0", + "orchestra/testbench": "^6.28|^7.0", + "orchestra/testbench-core": "^6.28|^7.0", + "phpunit/phpunit": "^9.5.13" }, - "bin": [ - "bin/phpcs", - "bin/phpcbf" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" + "autoload": { + "psr-4": { + "Rennokki\\QueryCache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "Apache-2.0" ], "authors": [ { - "name": "Greg Sherwood", - "role": "lead" + "name": "Alex Renoki", + "email": "alex@renoki.org", + "homepage": "https://github.com/rennokki", + "role": "Developer" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "description": "Adding cache on your Laravel Eloquent queries' results is now a breeze.", + "homepage": "https://github.com/renoki-co/laravel-eloquent-query-cache", "keywords": [ - "phpcs", - "standards" + "caching", + "eloquent", + "laravel", + "query", + "remember", + "sql" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/renoki-co/laravel-eloquent-query-cache/issues", + "source": "https://github.com/renoki-co/laravel-eloquent-query-cache/tree/3.3.0" }, - "time": "2021-12-12T21:44:58+00:00" + "funding": [ + { + "url": "https://github.com/rennokki", + "type": "github" + } + ], + "time": "2022-02-12T19:37:00+00:00" }, { "name": "swiftmailer/swiftmailer", @@ -5400,71 +6126,6 @@ ], "time": "2022-03-03T08:28:38+00:00" }, - { - "name": "facade/flare-client-php", - "version": "1.9.1", - "source": { - "type": "git", - "url": "https://github.com/facade/flare-client-php.git", - "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/facade/flare-client-php/zipball/b2adf1512755637d0cef4f7d1b54301325ac78ed", - "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed", - "shasum": "" - }, - "require": { - "facade/ignition-contracts": "~1.0", - "illuminate/pipeline": "^5.5|^6.0|^7.0|^8.0", - "php": "^7.1|^8.0", - "symfony/http-foundation": "^3.3|^4.1|^5.0", - "symfony/mime": "^3.4|^4.0|^5.1", - "symfony/var-dumper": "^3.4|^4.0|^5.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.14", - "phpunit/phpunit": "^7.5.16", - "spatie/phpunit-snapshot-assertions": "^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "Facade\\FlareClient\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Send PHP errors to Flare", - "homepage": "https://github.com/facade/flare-client-php", - "keywords": [ - "exception", - "facade", - "flare", - "reporting" - ], - "support": { - "issues": "https://github.com/facade/flare-client-php/issues", - "source": "https://github.com/facade/flare-client-php/tree/1.9.1" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2021-09-13T12:16:46+00:00" - }, { "name": "facade/ignition", "version": "2.17.5", @@ -5543,59 +6204,6 @@ }, "time": "2022-02-23T18:31:24+00:00" }, - { - "name": "facade/ignition-contracts", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/facade/ignition-contracts.git", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/3c921a1cdba35b68a7f0ccffc6dffc1995b18267", - "reference": "3c921a1cdba35b68a7f0ccffc6dffc1995b18267", - "shasum": "" - }, - "require": { - "php": "^7.3|^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^v2.15.8", - "phpunit/phpunit": "^9.3.11", - "vimeo/psalm": "^3.17.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Facade\\IgnitionContracts\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://flareapp.io", - "role": "Developer" - } - ], - "description": "Solution contracts for Ignition", - "homepage": "https://github.com/facade/ignition-contracts", - "keywords": [ - "contracts", - "flare", - "ignition" - ], - "support": { - "issues": "https://github.com/facade/ignition-contracts/issues", - "source": "https://github.com/facade/ignition-contracts/tree/1.0.2" - }, - "time": "2020-10-16T08:27:54+00:00" - }, { "name": "fakerphp/faker", "version": "v1.19.0", @@ -7891,6 +8499,62 @@ ], "time": "2022-03-29T12:44:16+00:00" }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.6.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2021-12-12T21:44:58+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.1", diff --git a/config/api.php b/config/api.php new file mode 100644 index 0000000..8f8c4df --- /dev/null +++ b/config/api.php @@ -0,0 +1,30 @@ + Helper::getPackageInfo()['version'], + + /** + * Timestamp timeout in seconds + * Request with timestamp older than 30sec + * will be rejected + */ + 'timestampTimeout' => 30, + + /** + * The number of request + * per user or ip per minutes + */ + 'apiRequestRatePerMinute' => 256, + + /** + * Service requests cache + * time in minutes + */ + 'servicesDBCache' => 10 +]; diff --git a/config/app.php b/config/app.php index be9aed0..91d45c7 100644 --- a/config/app.php +++ b/config/app.php @@ -67,7 +67,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => 'Europe/Paris', /* |-------------------------------------------------------------------------- @@ -80,7 +80,7 @@ return [ | */ - 'locale' => 'en', + 'locale' => 'fr', /* |-------------------------------------------------------------------------- @@ -106,7 +106,7 @@ return [ | */ - 'faker_locale' => 'en_US', + 'faker_locale' => 'fr_FR', /* |-------------------------------------------------------------------------- @@ -139,6 +139,7 @@ return [ /* * Laravel Framework Service Providers... */ + Illuminate\Auth\AuthServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class, Illuminate\Bus\BusServiceProvider::class, @@ -169,9 +170,9 @@ return [ /* * Application Service Providers... */ + App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, - // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, @@ -218,7 +219,6 @@ return [ 'Queue' => Illuminate\Support\Facades\Queue::class, 'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class, 'Redirect' => Illuminate\Support\Facades\Redirect::class, - // 'Redis' => Illuminate\Support\Facades\Redis::class, 'Request' => Illuminate\Support\Facades\Request::class, 'Response' => Illuminate\Support\Facades\Response::class, 'Route' => Illuminate\Support\Facades\Route::class, diff --git a/config/auth.php b/config/auth.php index 2c45f30..1f46d3c 100644 --- a/config/auth.php +++ b/config/auth.php @@ -40,6 +40,9 @@ return [ 'driver' => 'session', 'provider' => 'users', ], + 'api' => [ + 'driver' => 'api-hmac' + ], ], /* @@ -63,12 +66,7 @@ return [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, - ], - - // 'users' => [ - // 'driver' => 'database', - // 'table' => 'users', - // ], + ] ], /* diff --git a/config/exception-handler.php b/config/exception-handler.php index 83ec149..e6bd69b 100644 --- a/config/exception-handler.php +++ b/config/exception-handler.php @@ -11,5 +11,7 @@ return [ * Put a developper secured mailbox in here * so you can receive email if things go mad */ - 'contactsList' => [] + 'contactsList' => [ + env('MAIL_LIST_SERVICEWEB', 'equipe.web@ldc.fr') + ] ]; diff --git a/config/filesystems.php b/config/filesystems.php index 090cc99..8831119 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -57,6 +57,10 @@ return [ 'driver' => 'local', 'root' => storage_path('inco'), ], + 'arcadia_brand' => [ + 'driver' => 'local', + 'root' => storage_path('marque'), + ], ], /* diff --git a/config/ldcweb.php b/config/ldcweb.php new file mode 100644 index 0000000..7c62d71 --- /dev/null +++ b/config/ldcweb.php @@ -0,0 +1,5 @@ + env('PROJECT_UUID') +]; diff --git a/config/mail.php b/config/mail.php index e61dd6e..1639e8f 100644 --- a/config/mail.php +++ b/config/mail.php @@ -97,8 +97,9 @@ return [ ], 'contactsList' => [ - 'serviceweb' => explode(",", env('MAIL_LIST_SERVICEWEB', 'equipe.web@ldc.fr')), - 'invalidInco' => explode(",", env('MAIL_LIST_INVALIDINCO', 'equipe.web@ldc.fr')), + 'serviceweb' => explode(',', env('MAIL_LIST_SERVICEWEB', 'equipe.web@ldc.fr')), + // phpcs:ignore Generic.Files.LineLength.TooLong + 'invalidInco' => explode(',', env('MAIL_LIST_INVALIDINCO', 'equipe.web@ldc.fr,vincent.pierre@ldc.fr,christelle.manceau@ldc.fr')), ], /* diff --git a/config/trustedproxy.php b/config/trustedproxy.php new file mode 100644 index 0000000..e18fa5d --- /dev/null +++ b/config/trustedproxy.php @@ -0,0 +1,58 @@ + [ + // * HA PROXY prod "Virtual IP". + '192.168.31.9', + // * HA PROXY prod "Virtual IP" redondance. + '192.168.31.10', + // * HA PROXY dev "Virtual IP". + '192.168.34.15' + ], + + /* + * To trust one or more specific proxies that connect + * directly to your server, use an array or a string separated by comma of IP addresses: + */ + + /* + * Or, to trust all proxies that connect + * directly to your server, use a "*" + */ + + /* + * Which headers to use to detect proxy related data (For, Host, Proto, Port) + * + * Options include: + * + * - Illuminate\Http\Request::HEADER_X_FORWARDED_ALL (use all x-forwarded-* headers to establish trust) + * - Illuminate\Http\Request::HEADER_FORWARDED (use the FORWARDED header to establish trust) + * - Illuminate\Http\Request::HEADER_X_FORWARDED_AWS_ELB (If you are using AWS Elastic Load Balancer) + * + * - 'HEADER_X_FORWARDED_ALL' (use all x-forwarded-* headers to establish trust) + * - 'HEADER_FORWARDED' (use the FORWARDED header to establish trust) + * - 'HEADER_X_FORWARDED_AWS_ELB' (If you are using AWS Elastic Load Balancer) + * + * @link https://symfony.com/doc/current/deployment/proxies.html + */ + + 'headers' => Illuminate\Http\Request::HEADER_X_FORWARDED_FOR | + Illuminate\Http\Request::HEADER_X_FORWARDED_HOST | + Illuminate\Http\Request::HEADER_X_FORWARDED_PORT | + Illuminate\Http\Request::HEADER_X_FORWARDED_PROTO, +]; diff --git a/database/factories/ArcadiaBrandFactory.php b/database/factories/ArcadiaBrandFactory.php new file mode 100644 index 0000000..3140078 --- /dev/null +++ b/database/factories/ArcadiaBrandFactory.php @@ -0,0 +1,21 @@ + $this->faker->unique()->numberBetween(1, 999), + 'libelle' => $this->faker->unique()->company() + ]; + } +} diff --git a/database/factories/ServiceFactory.php b/database/factories/ServiceFactory.php new file mode 100644 index 0000000..b01d5b5 --- /dev/null +++ b/database/factories/ServiceFactory.php @@ -0,0 +1,20 @@ + $this->faker->unique()->name() . ' Service' + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 5cdba5b..1cd8cfd 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -18,8 +18,7 @@ class UserFactory extends Factory 'name' => $this->faker->name(), 'email' => $this->faker->unique()->safeEmail(), 'email_verified_at' => now(), - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', -// password + 'password' => 'password', 'remember_token' => Str::random(10), ]; } @@ -31,6 +30,7 @@ class UserFactory extends Factory */ public function unverified() { + // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass return $this->state(function (array $attributes) { return [ 'email_verified_at' => null, diff --git a/database/factories/XmlIncoFactory.php b/database/factories/XmlIncoFactory.php new file mode 100644 index 0000000..d656bb7 --- /dev/null +++ b/database/factories/XmlIncoFactory.php @@ -0,0 +1,105 @@ + $this->faker->unique()->words(rand(2, 3), true), + 'prod_saison' => $this->faker->boolean(), + 'allegations' => $this->faker->boolean(75) ? $this->faker->sentence(7) : null, + 'descriptif' => $this->faker->boolean(75) ? 'descriptif ' . $this->faker->text(150) : null, + 'pictures' => [], + 'astuces' => $this->faker->boolean(75) ? 'astuces ' . $this->faker->text(150) : null, + 'lib_conso' => $this->faker->boolean(75) ? $this->faker->sentence(7) : null, + 'nb_portion' => $this->faker->boolean(75) ? $this->faker->text(20) : null, + 'typ_portion' => $this->faker->boolean(75) ? 'typ_portion ' . $this->faker->text(150) : null, + 'pds_moy_theo' => $this->faker->boolean(75) ? $this->faker->numberBetween(0, 250) : null, + + 'conseil_prepa' => $this->faker->boolean(75) ? 'conseil_prepa ' . $this->faker->text(150) : null, + 'arg_vente' => $this->faker->boolean(75) ? 'arg_vente ' . $this->faker->text(150) : null, + 'lst_ing' => $this->faker->boolean(75) ? 'lst_ing ' . $this->faker->text(150) : null, + 'lib_allergene' => $this->faker->boolean(75) ? 'lib_allergene ' . $this->faker->text(150) : null, + + 'nutritional_values' => [ + 'val_energ_kj_r' => $this->faker->numberBetween(250, 1200), + 'val_energ_kcal_r' => $this->faker->numberBetween(50, 400), + 'mat_grass_r' => $this->faker->numberBetween(6, 30), + 'ags_r' => '5.3', + 'glucides_r' => '2', + 'sucre_r' => '1.4', + 'fib_alim_r' => '0.8', + 'proteine_r' => '17', + 'sel_r' => '0.47', + 'sodium_r' => '188', + 'fruits_r' => '0', + 'fruits_coque_sech_r' => '0', + 'pourcent_apport_ref_e14_r' => '11.4', + 'pourcent_apport_ref_kjo_r' => '11.5', + 'pourcent_apport_ref_fat_r' => '24.7', + 'pourcent_apport_ref_fatsat_r' => '29.8', + 'pourcent_apport_ref_choval_r' => '0.6', + 'pourcent_apport_ref_sugar_r' => '1.7', + 'pourcent_apport_ref_fibtg_r' => null, + 'pourcent_apport_ref_pro_r' => '33', + 'pourcent_apport_ref_salteq_r' => '6.7', + 'taille_port' => null, + 'val_energ_kj_p' => null, + 'val_energ_kcal_p' => null, + 'mat_grass_p' => null, + 'ags_p' => null, + 'glucides_p' => null, + 'sucre_p' => null, + 'fib_alim_p' => null, + 'proteine_p' => null, + 'sodium_p' => null, + 'fruits_p' => null, + 'sel_p' => null, + 'fruits_coque_sech_p' => null, + 'pourcent_apport_ref_e14_p' => null, + 'pourcent_apport_ref_kjo_p' => null, + 'pourcent_apport_ref_fat_p' => null, + 'pourcent_apport_ref_fatsat_p' => null, + 'pourcent_apport_ref_choval_p' => null, + 'pourcent_apport_ref_sugar_p' => null, + 'pourcent_apport_ref_fibtg_p' => null, + 'pourcent_apport_ref_pro_p' => null, + 'pourcent_apport_ref_salteq_p' => null, + 'apport_port' => 'Apport de r\u00e9f\u00e9rence pour un adulte type (8400Kj-2000Kcal)', + 'part_p' => '32', + 'part_g' => '4', + 'part_l' => '63', + 'p_sur_l' => '1.13', + 'pourcent_ags' => '35', + 'pourcent_agi' => '65', + 'pourcent_ss' => '70', + 'pourcent_amidon' => '30', + 'composante_n' => '9', + 'composante_p' => '5', + 'pourcent_fruits' => '0', + 'score_nutri' => '4', + 'classement' => 'C', + 'mat_grass_r_prec' => '=', + 'ags_r_prec' => '=', + 'glucides_r_prec' => '=', + 'sucre_r_prec' => '=', + 'fib_alim_r_prec' => '=', + 'proteine_r_prec' => '=', + 'sel_r_prec' => '=', + 'sodium_r_prec' => null, + 'fruits_r_prec' => null, + 'fruits_coque_sech_r_prec' => null + ] + ]; + } +} diff --git a/database/migrations/2022_04_05_144756_create_arcadia_brands_table.php b/database/migrations/2022_04_05_144756_create_arcadia_brands_table.php new file mode 100644 index 0000000..f42d374 --- /dev/null +++ b/database/migrations/2022_04_05_144756_create_arcadia_brands_table.php @@ -0,0 +1,35 @@ +id(); + + $table->unsignedBigInteger('arcadia_id')->unique(); + $table->string('libelle'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('arcadia_brands'); + } +} diff --git a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations/2022_04_05_145011_create_services_table.php similarity index 54% rename from database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php rename to database/migrations/2022_04_05_145011_create_services_table.php index 4315e16..ec4835d 100644 --- a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ b/database/migrations/2022_04_05_145011_create_services_table.php @@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreatePersonalAccessTokensTable extends Migration +class CreateServicesTable extends Migration { /** * Run the migrations. @@ -13,13 +13,14 @@ class CreatePersonalAccessTokensTable extends Migration */ public function up() { - Schema::create('personal_access_tokens', function (Blueprint $table) { + Schema::create('services', function (Blueprint $table) { $table->id(); - $table->morphs('tokenable'); + $table->string('name'); - $table->string('token', 64)->unique(); - $table->text('abilities')->nullable(); - $table->timestamp('last_used_at')->nullable(); + $table->string('bearer_token')->unique(); + $table->string('bearer_pwd', 312); + $table->timestamp('bearer_token_date'); + $table->timestamps(); }); } @@ -31,6 +32,6 @@ class CreatePersonalAccessTokensTable extends Migration */ public function down() { - Schema::dropIfExists('personal_access_tokens'); + Schema::dropIfExists('services'); } } diff --git a/database/migrations/2022_04_08_112055_create_arcadia_brand_service_table.php b/database/migrations/2022_04_08_112055_create_arcadia_brand_service_table.php new file mode 100644 index 0000000..a21d73d --- /dev/null +++ b/database/migrations/2022_04_08_112055_create_arcadia_brand_service_table.php @@ -0,0 +1,31 @@ +foreignId('service_id')->constrained('services'); + $table->foreignId('arcadia_brand_id')->constrained('arcadia_brands'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('arcadia_brand_service'); + } +} diff --git a/database/migrations/2021_11_19_102629_create_xml_incos_table.php b/database/migrations/2022_04_19_102629_create_xml_incos_table.php similarity index 88% rename from database/migrations/2021_11_19_102629_create_xml_incos_table.php rename to database/migrations/2022_04_19_102629_create_xml_incos_table.php index fbd3c42..e15767d 100644 --- a/database/migrations/2021_11_19_102629_create_xml_incos_table.php +++ b/database/migrations/2022_04_19_102629_create_xml_incos_table.php @@ -15,13 +15,13 @@ class CreateXmlIncosTable extends Migration public function up() { Schema::create('xml_incos', function (Blueprint $table) { - $table->id(); + $table->id()->comment('Inco ID (NO_ART)'); $table->string('nom')->nullable(); $table->boolean('prod_saison'); $table->string('allegations')->nullable(); $table->text('descriptif')->nullable(); - $table->json('picture'); + $table->json('pictures'); $table->text('astuces')->nullable(); $table->string('lib_conso')->nullable(); $table->string('nb_portion', 20)->nullable(); @@ -35,6 +35,8 @@ class CreateXmlIncosTable extends Migration $table->json('nutritional_values'); + $table->foreignId('arcadia_brand_id')->constrained('arcadia_brands'); + $table->timestamps(); }); } diff --git a/database/migrations/2022_04_19_145010_create_service_stats.php b/database/migrations/2022_04_19_145010_create_service_stats.php new file mode 100644 index 0000000..f2b05f8 --- /dev/null +++ b/database/migrations/2022_04_19_145010_create_service_stats.php @@ -0,0 +1,37 @@ +id(); + + $table->foreignId('service_id')->constrained('services'); + $table->date('date'); + $table->unsignedBigInteger('nb_calls')->default(0); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('service_stats'); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 57b73b5..0332abc 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,7 +2,13 @@ namespace Database\Seeders; +use App\Models\ArcadiaBrand; +use App\Models\Service; +use App\Models\XmlInco; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Storage; +use SplFileInfo; class DatabaseSeeder extends Seeder { @@ -13,6 +19,35 @@ class DatabaseSeeder extends Seeder */ public function run() { - // \App\Models\User::factory(10)->create(); + /** @var \Illuminate\Database\Eloquent\Collection $services */ + $services = Service::factory(5)->create(); + /** @var \Illuminate\Database\Eloquent\Collection $brands */ + $brands = ArcadiaBrand::factory(10)->create(); + + // * Associate brands to services + $services->map(function (Service $service) use (&$brands) { + $pullBrands = $brands->random(rand(1, 2)); + $service->arcadiaBrands()->sync($pullBrands); + $brands = $brands->diff($pullBrands); + }); + + $disk = Storage::disk('inco'); + + $brands->map(function (ArcadiaBrand $brand) use ($disk) { + XmlInco::factory(rand(150, 355))->make() + ->each(function (XmlInco $xmlInco) use ($disk, $brand) { + $xmlInco->pictures = collect([ + new SplFileInfo(resource_path('seeders/xml_inco/fakeimage1.png')), + new SplFileInfo(resource_path('seeders/xml_inco/fakeimage2.png')) + ])->random(rand(0, 2))->map(function (SplFileInfo $image) use ($disk) { + $imgNamePath = $image->getFilename(); + $imageFinalPath = "images/inco/$imgNamePath"; + $disk->put($imgNamePath, File::get($image->getPathname())); + return $imageFinalPath; + })->all(); + $xmlInco->arcadiaBrand()->associate($brand); + $xmlInco->saveOrFail(); + }); + }); } } diff --git a/phpunit.xml b/phpunit.xml index 4ae4d97..97cd15a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,31 +1,31 @@ - + - - ./tests/Unit - - - ./tests/Feature + + ./tests/Api - - - ./app - - + + + ./app + + + + + - - - - - - - - - + + + + + + + + + + + + + diff --git a/storage/inco/.gitkeep b/resources/documents/arcadia/marque.xsd similarity index 100% rename from storage/inco/.gitkeep rename to resources/documents/arcadia/marque.xsd diff --git a/resources/documents/inco/incollector.xsd b/resources/documents/inco/incollector.xsd new file mode 100644 index 0000000..6c9fe89 --- /dev/null +++ b/resources/documents/inco/incollector.xsd @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/seeders/xml_inco/fakeimage1.png b/resources/seeders/xml_inco/fakeimage1.png new file mode 100644 index 0000000..98a37a9 Binary files /dev/null and b/resources/seeders/xml_inco/fakeimage1.png differ diff --git a/resources/seeders/xml_inco/fakeimage2.png b/resources/seeders/xml_inco/fakeimage2.png new file mode 100644 index 0000000..7d98a4c Binary files /dev/null and b/resources/seeders/xml_inco/fakeimage2.png differ diff --git a/resources/views/emails/invalidxml.blade.php b/resources/views/mails/invalidxml.blade.php similarity index 84% rename from resources/views/emails/invalidxml.blade.php rename to resources/views/mails/invalidxml.blade.php index e212653..a6b96f2 100644 --- a/resources/views/emails/invalidxml.blade.php +++ b/resources/views/mails/invalidxml.blade.php @@ -1,11 +1,11 @@ - {{ $appName }} Exception Handler: Unvalid XML + XML Exception Handler: Invalid XML -

{{ $appName }} à reçu un fichier xml non valide !!

+

{{ config('app.name') }} à reçu un fichier xml non valide

{{ $strError }}

FICHIER: `{{ $filename }}`

{{ $exception->getMessage() }}

@@ -15,7 +15,7 @@ file : {{$exception->getFile() }}
line : {{$exception->getLine() }}

- Merci de corriger le flux, puis de renvoyer le fichier en provoquant un mouvement EAI.

+ Merci de corriger les données, puis de renvoyer le fichier en provoquant un mouvement EAI.

Pour rappel : Il est recommandé de valider un fichier XML à l'aide d'un DTD
https://www.w3schools.com/xml/xml_dtd.asp
ou bien XSD https://www.w3schools.com/xml/schema_intro.asp

diff --git a/routes/api.php b/routes/api.php index 89f34a8..789bc2e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,19 +1,19 @@ name('apiV1.')->group(function () { -Route::middleware('auth:sanctum')->get('/user', function (Request $request) { - return $request->user(); + // * PUBLIC + Route::get('/ping', [V1ApiController::class, 'ping'])->name('ping'); + + // * PRIVATE + Route::middleware('auth:api')->group(function () { + Route::prefix('inco')->name('incos.')->group(function () { + Route::get('/', [V1XmlIncoController::class, 'index'])->name('index'); + Route::get('/{xmlinco}', [V1XmlIncoController::class, 'show'])->name('show'); + }); + }); }); diff --git a/routes/channels.php b/routes/channels.php deleted file mode 100644 index b75e9e1..0000000 --- a/routes/channels.php +++ /dev/null @@ -1,18 +0,0 @@ -id === (int)$id; -}); diff --git a/routes/console.php b/routes/console.php deleted file mode 100644 index e216465..0000000 --- a/routes/console.php +++ /dev/null @@ -1,19 +0,0 @@ -comment(Inspiring::quote()); -})->purpose('Display an inspiring quote'); diff --git a/storage/.gitignore b/storage/.gitignore deleted file mode 100644 index e3a4197..0000000 --- a/storage/.gitignore +++ /dev/null @@ -1 +0,0 @@ -inco/* diff --git a/storage/inco/xsd/incollector.xsd b/storage/inco/xsd/incollector.xsd deleted file mode 100644 index 54069fe..0000000 --- a/storage/inco/xsd/incollector.xsd +++ /dev/null @@ -1,273 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/storage/inco/xsd/.gitkeep b/storage/marque/archives/.gitkeep similarity index 100% rename from storage/inco/xsd/.gitkeep rename to storage/marque/archives/.gitkeep diff --git a/storage/marque/invalid/.gitkeep b/storage/marque/invalid/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..65347d2 --- /dev/null +++ b/test.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +#composer runtestmigration >/dev/null +composer runserve +# If argument is passed run with coverage +if [ -z "$1" ]; then + echo 'no coverage' + composer runtest +else + echo 'with coverage' + composer runtestWithCoverage +fi +composer killserve diff --git a/tests/Api/V1/ApiBase.php b/tests/Api/V1/ApiBase.php new file mode 100644 index 0000000..1f268e2 --- /dev/null +++ b/tests/Api/V1/ApiBase.php @@ -0,0 +1,77 @@ +assertFragment(['status' => $status->value]); + } + + /** + * Raw Function to call api + * + * @param string $method + * @param string $uri + * @param array $data + * @param array $headers + * @return \Illuminate\Testing\TestResponse + */ + protected function callApi(string $method, string $uri, array $data = [], array $headers = []): TestResponse + { + if (!isset($data['timestamp'])) { + $data['timestamp'] = \time(); + } + return $this->json($method, $uri, $data, $headers); + } + + /** + * Raw Function to call api with Token + * + * @param Service $service + * @param string $method + * @param string $uri + * @param array $data + * @param array $headers + * @return \Illuminate\Testing\TestResponse + */ + protected function callApiWithToken( + Service $service, + string $method, + string $uri, + array $data = [], + array $headers = [] + ): TestResponse { + $data['bearer_token'] = $service->bearer_token; + $data['timestamp'] = \gmdate('Y-m-d H:i:s'); + + $data['signature'] = Helper::signRequest( + $service->bearer_pwd, + $uri, + $method, + $data + ); + + return $this->callApi($method, $uri, $data, $headers); + } +} diff --git a/tests/Api/V1/ApiTest.php b/tests/Api/V1/ApiTest.php new file mode 100644 index 0000000..a69b13c --- /dev/null +++ b/tests/Api/V1/ApiTest.php @@ -0,0 +1,186 @@ +call('get', route('apiV1.ping'), [ + 'timestamp' => \gmdate('Y-m-d H:i:s') + ]); + $response->assertStatus(400); + } + + /** + * Test timestamp middleware works as expected. + * + * @return void + */ + public function testTimestampMiddleware() + { + $response = $this->json('get', route('apiV1.ping'), [ + 'timestamp' => \gmdate('Y-m-d H:i:s') + ]); + $json = $response->decodeResponseJson(); + $this->assertApiResponseStatusIs($json, ApiStatus::OK()); + } + + /** + * Timestamp middleware should fail if timestamp parameter is missing. + * + * @return void + */ + public function testTimestampMiddlewareCheckFailMissing() + { + $response = $this->json('get', route('apiV1.ping'), []); + $json = $response->decodeResponseJson(); + $this->assertApiResponseStatusIs($json, ApiStatus::KO()); + $json->assertFragment(['data' => [ + 'code' => ApiError::wrongTimeStamp()->value, + 'message' => ApiError::wrongTimeStamp()->label + ]]); + } + + /** + * Timestamp middleware should fail if timestamp parameter has wrong format. + * + * @return void + */ + public function testTimestampMiddlewareCheckFailWrongFormat() + { + $response = $this->json('get', route('apiV1.ping'), ['timestamp' => \gmdate('Y/m/d')]); + $json = $response->decodeResponseJson(); + $this->assertApiResponseStatusIs($json, ApiStatus::KO()); + $json->assertFragment(['data' => [ + 'code' => ApiError::wrongTimeStamp()->value, + 'message' => ApiError::wrongTimeStamp()->label + ]]); + } + + /** + * Timestamp middleware should fail if timestamp is older than "config('api.timestampTimeout', 10)". + * + * @return void + */ + public function testTimestampMiddlewareCheckFailIfTooOld() + { + $response = $this->json('get', route('apiV1.ping'), [ + 'timestamp' => \gmdate('Y-m-d H:i:s', \time() - config('api.timestampTimeout', 10)) + ]); + $json = $response->decodeResponseJson(); + $this->assertApiResponseStatusIs($json, ApiStatus::KO()); + $json->assertFragment(['data' => [ + 'code' => ApiError::wrongTimeStamp()->value, + 'message' => ApiError::wrongTimeStamp()->label + ]]); + } + + /** + * Test api route throttle. + * + * ! This has to be the last test of this class. + * + * @return void + */ + public function testApiThrottle() + { + for ($i = config('api.apiRequestRatePerMinute', 100); $i >= 0; $i--) { + $this->json('get', route('apiV1.ping'), [ + 'timestamp' => \gmdate('Y-m-d H:i:s') + ]); + } + + $response = $this->json('get', route('apiV1.ping'), [ + 'timestamp' => \gmdate('Y-m-d H:i:s') + ]); + $json = $response->decodeResponseJson(); + $this->assertApiResponseStatusIs($json, ApiStatus::KO()); + $response->assertStatus(429); + } + + /** + * Test wrong http method call + * + * @return void + */ + public function testWrongMethodCall() + { + $response = $this->json('post', route('apiV1.ping'), [ + 'timestamp' => \gmdate('Y-m-d H:i:s', \time()) + ]); + $json = $response->decodeResponseJson(); + $this->assertApiResponseStatusIs($json, ApiStatus::KO()); + $this->assertEquals(ApiError::methodNotAllowed()->value, $json->json('data.code')); + } + + /** + * Test Auth fail when no token + * + * @return void + */ + public function testAuthFailNoToken() + { + $response = $this->json('get', route('apiV1.incos.show', [ + 'xmlinco' => Str::uuid() + ]), [ + 'timestamp' => \gmdate('Y-m-d H:i:s', \time()) + ]); + $json = $response->decodeResponseJson(); + $this->assertApiResponseStatusIs($json, ApiStatus::KO()); + $this->assertEquals(ApiError::apiAuthFailed()->value, $json->json('data.code')); + } + + /** + * Test Auth fail when unknown token + * + * @return void + */ + public function testAuthFailUnknownToken() + { + $response = $this->json('get', route('apiV1.incos.show', [ + 'xmlinco' => Str::uuid() + ]), [ + 'bearer_token' => 'qsdqsdqsd', + 'timestamp' => \gmdate('Y-m-d H:i:s', \time()) + ]); + $json = $response->decodeResponseJson(); + $this->assertApiResponseStatusIs($json, ApiStatus::KO()); + $this->assertEquals(ApiError::apiAuthFailed()->value, $json->json('data.code')); + } + + /** + * Test Auth fail when wrong signature + * + * @return void + */ + public function testAuthFailWrongSignature() + { + $service = Service::query()->firstOrFail(); + $response = $this->json('get', route('apiV1.incos.show', [ + 'xmlinco' => Str::uuid() + ]), [ + 'bearer_token' => $service->bearer_token, + 'timestamp' => \gmdate('Y-m-d H:i:s', \time()), + 'signature' => 'sdqdqsdqd' + ]); + $json = $response->decodeResponseJson(); + $this->assertApiResponseStatusIs($json, ApiStatus::KO()); + $this->assertEquals(ApiError::apiAuthFailed()->value, $json->json('data.code')); + } +} diff --git a/tests/Api/V1/XmlIncoTest.php b/tests/Api/V1/XmlIncoTest.php new file mode 100644 index 0000000..c7efcc8 --- /dev/null +++ b/tests/Api/V1/XmlIncoTest.php @@ -0,0 +1,161 @@ +firstOrFail(); + $arcadiaBrand = $xmlInco->arcadiaBrand; + $this->assertInstanceOf(ArcadiaBrand::class, $arcadiaBrand); + } + + /** + * Test customer Authentification ok. + * route: apiV2.customers.authenticate + * + * @return void + */ + public function testXmlIncoIndex() + { + /** @var \App\Models\Service $service */ + extract($this->getService(true)); + + $response = $this->callApiWithToken( + $service, + 'get', + route('apiV1.incos.index'), + ['per_page' => '2'] + ); + + $json = $response->decodeResponseJson(); + + $this->assertApiResponseStatusIs($json, ApiStatus::OK()); + $response->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'prod_saison', + 'allegations', + 'descriptif', + 'pictures' => [ + '*' + ], + 'astuces', + 'lib_conso', + 'nb_portion', + 'typ_portion', + 'pds_moy_theo', + 'conseil_prepa', + 'arg_vente', + 'lst_ing', + 'lib_allergene', + 'nutritional_values' => [ + 'val_energ_kj_r', + 'val_energ_kcal_r', + 'mat_grass_r', + 'ags_r', + 'glucides_r', + 'sucre_r', + 'fib_alim_r', + 'sel_r', + 'sodium_r', + 'fruits_r', + 'fruits_coque_sech_r', + 'pourcent_apport_ref_e14_r', + 'pourcent_apport_ref_kjo_r', + 'pourcent_apport_ref_fat_r', + 'pourcent_apport_ref_fatsat_r', + 'pourcent_apport_ref_choval_r', + 'pourcent_apport_ref_sugar_r', + 'pourcent_apport_ref_fibtg_r', + 'pourcent_apport_ref_pro_r', + 'pourcent_apport_ref_salteq_r', + 'taille_port', + 'val_energ_kj_p', + 'val_energ_kcal_p', + 'mat_grass_p', + 'ags_p', + 'glucides_p', + 'sucre_p', + 'fib_alim_p', + 'proteine_p', + 'sodium_p', + 'fruits_p', + 'sel_p', + 'fruits_coque_sech_p', + 'pourcent_apport_ref_e14_p', + 'pourcent_apport_ref_kjo_p', + 'pourcent_apport_ref_fat_p', + 'pourcent_apport_ref_fatsat_p', + 'pourcent_apport_ref_choval_p', + 'pourcent_apport_ref_sugar_p', + 'pourcent_apport_ref_fibtg_p', + 'pourcent_apport_ref_pro_p', + 'pourcent_apport_ref_salteq_p', + 'apport_port', + 'part_p', + 'part_g', + 'part_l', + 'p_sur_l', + 'pourcent_ags', + 'pourcent_agi', + 'pourcent_ss', + 'pourcent_amidon', + 'composante_n', + 'composante_p', + 'pourcent_fruits', + 'score_nutri', + 'classement', + 'mat_grass_r_prec', + 'ags_r_prec', + 'glucides_r_prec', + 'sucre_r_prec', + 'fib_alim_r_prec', + 'proteine_r_prec', + 'sel_r_prec', + 'sodium_r_prec', + 'fruits_r_prec', + 'fruits_coque_sech_r_prec', + ], + ] + ] + ]); + } + + /** + * Get Customer models with given status (active or not) + * + * @param boolean $random Randomize the customer to get. + * @return array + */ + protected function getService(bool $random = true): array + { + if ($random) { + /** @var \App\Models\Service $service */ + $service = Service::inRandomOrder()->whereHas('arcadiaBrands')->firstOrFail(); + } else { + /** @var \App\Models\Service $service */ + $service = Service::query()->whereHas('arcadiaBrands')->firstOrFail(); + } + + return compact('service'); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 0894c60..0000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,20 +0,0 @@ -get('/'); - - $response->assertStatus(200); - } -} diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index 62e8c3c..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertTrue(true); - } -}