Compare commits

..

No commits in common. "flucollector" and "develop" have entirely different histories.

443 changed files with 5174 additions and 177326 deletions

View file

@ -1,5 +0,0 @@
# Browsers that we support
last 3 version
> 0.3%
not dead # no browsers without security updates

View file

@ -3,9 +3,9 @@ root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
@ -16,6 +16,3 @@ indent_size = 2
[docker-compose.yml]
indent_size = 4
[*.vue]
indent_size = 2

View file

@ -17,7 +17,7 @@ DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
@ -29,14 +29,17 @@ REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
MAIL_LIST_SERVICEWEB=
MAIL_LIST_INVALIDINCO=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
@ -46,16 +49,7 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Sentry
SENTRY_LARAVEL_DSN=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View file

@ -1,49 +0,0 @@
APP_NAME=Laravel
APP_ENV=testing
APP_KEY=base64:UypKzhOMAs2aDu3ovgw/lINvWZQWicbz9nfUXDpDzeQ=
APP_DEBUG=true
APP_URL="http://127.0.0.25:8888"
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3308
DB_DATABASE=testing_flucollector
DB_USERNAME=root
DB_PASSWORD=:Mysql.Docker
MIGRATIONPAGINATION=20000
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_FROM_ADDRESS=crm@ldc.fr
MAIL_FROM_NAME="${APP_NAME}"
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=
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}"

View file

@ -1,55 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:vue/vue3-strongly-recommended",
"@vue/eslint-config-typescript"
],
"overrides": [
{
"files": ["*.ts", "*.vue"],
"rules": {
"no-undef": "off"
}
},
{
"files": ["*.vue"],
"rules": {
"indent": "off"
}
}
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["vue", "@typescript-eslint"],
"ignorePatterns": ["webpack.mix.js", "**/vendor/*.js", "**/vendor/**/*.js"],
"rules": {
"indent": ["error", 4, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
"vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }],
"vue/one-component-per-file": "off"
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".vue", ".ts", ".d.ts"]
},
"alias": {
"extensions": [".vue", ".js", ".ts", ".scss", ".d.ts"],
"map": [
["~", "./resources/assets"],
["@", "./node_modules"]
]
}
}
}
}

14
.gitattributes vendored
View file

@ -1,11 +1,5 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
* text=auto
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore
.styleci.yml export-ignore

20
.gitignore vendored
View file

@ -1,26 +1,14 @@
.env
.env.backup
.env.production
.phpunit.result.cache
.stylelintcache
/.fleet
/.idea
/.phpunit.cache
/.vscode
/node_modules
/public/build
/public/cache
/public/hot
/public/sitemap.xml
/public/storage
/storage/*.key
/vendor
auth.json
.env
.env.backup
.phpunit.result.cache
docker-compose.override.yml
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.npmrc
storage/inco
storage/marque
/.idea

View file

@ -1,24 +0,0 @@
include:
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'lint/lint.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'security/security.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'build/build.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'test/test.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'deploy/deploy.yml'
stages:
- lint
- security
- build
- test
- deploy

View file

@ -1,11 +0,0 @@
## Comportement constaté
## Comportement attendu
## Solution envisagée
## Étapes pour la reproduction du bug
*

View file

@ -1,7 +0,0 @@
## Présentation du besoin
## Implémentation
## Maquettes, documentations...

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "client"]
path = client
url = git@lab.s-web.fr:ldc-web/projets/flucollector/flucollector-client.git

14
.styleci.yml Normal file
View file

@ -0,0 +1,14 @@
php:
preset: laravel
version: 8
disabled:
- no_unused_imports
finder:
not-name:
- index.php
- server.php
js:
finder:
not-name:
- webpack.mix.js
css: true

View file

@ -1,48 +0,0 @@
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-sass-guidelines",
"stylelint-config-idiomatic-order"
],
"plugins": [
"stylelint-scss"
],
"files": [
"**/*.css",
"**/*.scss"
],
"ignoreFiles": [
"**/vendor/*.css",
"**/vendor/**/*.css",
"**/vendor/*.scss",
"**/vendor/**/*.scss",
"**/vendor/*.sass",
"**/vendor/**/*.sass",
"**/vendor/*.less",
"**/vendor/**/*.less"
],
"customSyntax": "postcss-scss",
"rules": {
"indentation": 4,
"string-quotes": "double",
"selector-max-id": null,
"color-no-invalid-hex": true,
"order/properties-alphabetical-order": null,
"max-nesting-depth": 5,
"selector-max-compound-selectors": 5,
"declaration-colon-space-after": "always-single-line",
"function-parentheses-space-inside": "never-single-line",
"declaration-colon-newline-after": null,
"value-list-comma-newline-after": null,
"scss/double-slash-comment-empty-line-before": [
"always",
{
"ignore": [
"between-comments",
"stylelint-commands",
"inside-block"
]
}
]
}
}

View file

@ -1,20 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"rvest.vs-code-prettier-eslint",
"onecentlin.laravel-extension-pack",
"johnpapa.vscode-peacock",
"xdebug.php-debug",
"neilbrayfield.php-docblocker",
"bmewburn.vscode-intelephense-client",
"valeryanm.vscode-phpsab",
"syler.sass-indented",
"sibiraj-s.vscode-scss-formatter",
"mrmlnc.vscode-scss",
"stylelint.vscode-stylelint",
"vue.vscode-typescript-vue-plugin",
"wscats.vue",
"vue.volar",
"misterj.vue-volar-extention-pack"
]
}

49
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,49 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"hostname": "xdebug-incollector.s-web.test",
"port": 9003
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 0,
"runtimeArgs": [
"-dxdebug.start_with_request=yes"
],
"env": {
"XDEBUG_MODE": "debug,develop",
"XDEBUG_CONFIG": "client_port=${port}"
}
},
{
"name": "Launch Built-in web server",
"type": "php",
"request": "launch",
"runtimeArgs": [
"-dxdebug.mode=debug",
"-dxdebug.start_with_request=yes",
"-S",
"localhost:0"
],
"program": "",
"cwd": "${workspaceRoot}",
"port": 9003,
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
}
}
]
}

23
.vscode/settings.json vendored
View file

@ -1,24 +1,23 @@
{
"phpcs.executablePath": "vendor/bin/phpcs",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#a714ac",
"activityBar.activeBorder": "#eb549a",
"activityBar.activeBackground": "#bf106b",
"activityBar.background": "#bf106b",
"activityBar.background": "#a714ac",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#213b05",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#bf106b",
"statusBar.background": "#900c51",
"activityBarBadge.background": "#eb549a",
"activityBarBadge.foreground": "#15202b",
"sash.hoverBorder": "#a714ac",
"statusBar.background": "#0f437e",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#bf106b",
"statusBarItem.remoteBackground": "#900c51",
"statusBarItem.hoverBackground": "#a714ac",
"statusBarItem.remoteBackground": "#0f437e",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#900c51",
"titleBar.activeBackground": "#0f437e",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#900c5199",
"titleBar.inactiveBackground": "#0f437e99",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.remoteColor": "#900c51"
"peacock.remoteColor": "#0f437e"
}

View file

@ -1,6 +0,0 @@
# Changelog
### All notable changes to `Flucollector` will be documented in this file.
# version 1.0.0
Init release

View file

@ -1,21 +0,0 @@
MIT License (!! includes CC BY-NC 3.0 parts please read Readme.md)
Copyright (c) 2023
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,52 +1,16 @@
# Comment lancer les tests unitaires ?
# Incollector
### Attention, si vous utilisez juste un .env à la place d'un .env.testing votre base de donnée de developpement sera effacée à chaque lancement des tests unitaires
## .env
mettre :
```
Etapes :
- cp .env.testing.example .env.testing (spécial tests unitaires)
- php artisan key:generate
- parameter les BDD dans le .env.testing
- php artisan config:cache
- git submodule init
- git submodule update (récupérer le client)
- composer install
- service start mysql (votre base de donnée accessible sur 127.0.0.1:3306)
- chmod u+x ./test.sh
- php artisan optimize
- composer run test
```
Config Supervisor 2 Fichiers :
## Flux
Flux INCO dans :
```
storage/inco
```
`uwilflucollector-worker.conf`
**Pour les jobs en général**
## Scheduled Jobs
[program:uwilflucollector-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /srcs/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
startasgroup=true
killasgroup=true
user=www-data
umask=077
numprocs=8
redirect_stderr=true
stdout_logfile=/srcs/storage/logs/worker.log
stopwaitsecs=3600
`uwilflucollector-stats-worker.conf`
**Pour les jobs de stats uniquement**
[program:uwilflucollector-stats-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /srcs/artisan queue:work --queue=stats --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
startasgroup=true
killasgroup=true
user=www-data
umask=077
numprocs=1
redirect_stderr=true
stdout_logfile=/srcs/storage/logs/worker.log
stopwaitsecs=3600

View file

@ -1,61 +0,0 @@
<?php
namespace App\Casts;
use App\Lib\XmlInco\Allegations;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class AllegationCast implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array<string,mixed> $attributes
* @return mixed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
*/
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
// phpcs:enable
if (\is_null($value)) {
return $value;
}
if (!\is_string($value)) {
$value = \strval($value);
}
$value = \json_decode($value, true);
/** @var array<string[]> $value */
return new Allegations(...$value);
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array<string,mixed> $attributes
* @return mixed
* @throws \RuntimeException If $value is not an intance of PortionAmount.
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
*/
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
// phpcs:enable
if (\is_null($value)) {
return $value;
}
if (\is_object($value) and $value instanceof Allegations) {
return $value->toJson();
}
throw new \RuntimeException("Cannot cast {$key}, need instance of " . Allegations::class);
}
}

View file

@ -1,61 +0,0 @@
<?php
namespace App\Casts;
use App\Lib\XmlInco\NutritionalValues;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class NutrionalValuesCast implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array<string,mixed> $attributes
* @return mixed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
*/
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
// phpcs:enable
if (\is_null($value)) {
return $value;
}
if (!\is_string($value)) {
$value = \strval($value);
}
$value = \json_decode($value, true);
/** @var array<string,float|integer|null|string> $value */
return new NutritionalValues($value);
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array<string,mixed> $attributes
* @return mixed
* @throws \RuntimeException If $value is not an intance of PortionAmount.
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
*/
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
// phpcs:enable
if (\is_null($value)) {
return $value;
}
if (\is_object($value) and $value instanceof NutritionalValues) {
return $value->toJson();
}
throw new \RuntimeException("Cannot cast {$key}, need instance of " . NutritionalValues::class);
}
}

View file

@ -1,59 +0,0 @@
<?php
namespace App\Casts;
use App\Lib\XmlInco\PortionAmount;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class PortionAmountCast implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array<string,mixed> $attributes
* @return mixed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
*/
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
// phpcs:enable
if (\is_null($value)) {
return $value;
}
if (\is_numeric($value)) {
$value = \intval($value);
}
return PortionAmount::fromDatabase($value);
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array<string,mixed> $attributes
* @return mixed
* @throws \RuntimeException If $value is not an intance of PortionAmount.
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
*/
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
// phpcs:enable
if (\is_null($value)) {
return $value;
}
if (\is_object($value) and $value instanceof PortionAmount) {
return $value->toDatabaseValue();
}
throw new \RuntimeException("Cannot cast {$key}, need instance of " . PortionAmount::class);
}
}

View file

@ -1,238 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Enums\Api\ApiError;
use App\Enums\Api\ApiStatus;
use App\Enums\XmlInco\PortionType;
use App\Exceptions\Api\ApiErrorException;
use App\Exceptions\Api\ApplicationException;
use App\Lib\XmlInco\Allegations;
use App\Lib\XmlInco\NutritionalValues;
use App\Lib\XmlInco\PortionAmount;
use App\Traits\Enums\BaseEnum;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class GenerateClient extends Command
{
private const TARGET = 'client/src';
private const CLIENTNAMESPACE = 'UWIL\\Flucollector';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'client:generate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate the flucollector client in `client` folder';
/**
* Execute the console command.
*
* @return integer
*/
public function handle(): int
{
$this->copyLibXmlIncoClasses();
$this->copyTraits();
$this->copyEnums();
$this->copyExceptions();
$this->copyRoutesList();
$this->copyApiConfigInfo();
return 0;
}
/**
* Copy config files
*
* @return void
*/
private function copyApiConfigInfo(): void
{
$this->info('Copy Api Config');
$rootTarget = app_path('../' . self::TARGET);
if (File::exists("$rootTarget/../apiConfig.readonly.json")) {
File::delete("$rootTarget/../apiConfig.readonly.json");
}
File::put(
"$rootTarget/../apiConfig.readonly.json",
\json_encode(config('api'), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Copy routes into a json file
*
* @return void
*/
private function copyRoutesList(): void
{
$this->info('Copy Routes');
$routes = [];
/** @var \Illuminate\Routing\Route */
foreach (Route::getRoutes() as $route) {
if (\strpos($route->getPrefix(), 'api') !== false) {
$routes[$route->getName()] = [
'uri' => $route->uri(),
'methods' => $route->methods()
];
}
}
$rootTarget = app_path('../' . self::TARGET);
if (File::exists("$rootTarget/routes.json")) {
File::delete("$rootTarget/routes.json");
}
File::put(
"$rootTarget/routes.json",
\json_encode($routes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Copy Exeption classes
*
* @return void
*/
private function copyExceptions(): void
{
$this->info('Copy Exceptions');
$rootTarget = app_path('../' . self::TARGET);
$classes = [
ApplicationException::class,
ApiErrorException::class
];
$this->copyToFolder($classes, 'Exceptions');
foreach ($classes as $class) {
$classBasename = class_basename($class);
$dest = "$rootTarget/Exceptions/$classBasename.php";
$this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
}
}
/**
* Copy base enums classes
*
* @return void
*/
private function copyTraits(): void
{
$this->info('Copy Traits');
$rootTarget = app_path('../' . self::TARGET);
$classes = [
BaseEnum::class
];
$this->copyToFolder($classes, 'Traits');
foreach ($classes as $class) {
$classBasename = class_basename($class);
$dest = "$rootTarget/Traits/$classBasename.php";
$this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
}
}
/**
* Copy enums classes
*
* @return void
*/
private function copyEnums(): void
{
$this->info('Copy Enums');
$classes = [
ApiError::class,
ApiStatus::class,
PortionType::class,
];
$this->copyToFolder($classes, 'Enums');
$rootTarget = app_path('../' . self::TARGET);
foreach ($classes as $class) {
$classBasename = class_basename($class);
$dest = "{$rootTarget}/Enums/{$classBasename}.php";
$this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
}
}
/**
* Copy lib XmlInco classes
*
* @return void
*/
private function copyLibXmlIncoClasses(): void
{
$this->info('Copy Lib XmlInco classes');
$classes = [
Allegations::class,
NutritionalValues::class,
PortionAmount::class,
];
$this->copyToFolder($classes, 'Lib');
$rootTarget = app_path('../' . self::TARGET);
foreach ($classes as $class) {
$classBasename = class_basename($class);
$dest = "{$rootTarget}/Lib/{$classBasename}.php";
$this->replaceInPhpFile($dest, 'App\\', self::CLIENTNAMESPACE . '\\');
}
}
/**
* Copy Classes to specified folder
*
* @param array $classes
* @param string $folder
* @return void
*/
private function copyToFolder(array $classes, string $folder): void
{
$rootTarget = app_path('../' . self::TARGET);
$folderNamespace = \str_replace('/', '\\', $folder);
foreach ($classes as $class) {
$classFileName = \str_replace("App\\$folderNamespace\\", '', sprintf('%s.php', $class));
$classFileName = \str_replace('\\', '/', $classFileName);
$classBasename = class_basename($class);
$source = \app_path("$folder/$classFileName");
$dest = "$rootTarget/$folder/$classBasename.php";
if (File::exists($dest)) {
File::delete($dest);
}
File::copy($source, $dest);
$this->replaceInPhpFile($dest, 'namespace App', 'namespace ' . self::CLIENTNAMESPACE);
}
}
/**
* Replaces a specific string with another on a file
*
* @param string $source
* @param string $search
* @param string $replace
* @return void
*/
private function replaceInPhpFile(string $source, string $search, string $replace): void
{
$fContent = File::get($source);
// Remove trailing \Api .
$fContent = Str::of($fContent)->replaceMatches("/\\\(Api)([\s;\\\])/", '$2');
// Remove trailing \XmlInco .
$fContent = Str::of($fContent)->replaceMatches("/\\\(XmlInco)([\s;\\\])/", '$2');
$fContent = Str::of($fContent)->replaceMatches("/\\\Traits\\\Enums/", '\Traits');
// Replace namespace top level.
$pos = strpos($fContent, $search);
if ($pos !== false) {
File::put($source, \str_replace($search, $replace, $fContent));
}
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class TestMail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uwil:mail:test';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send test email to equipe.web@ldc.fr';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
Mail::raw(sprintf('UWIL mail-test from %s %s', \config('app.name'), \config('app.url')), function ($msg) {
$msg->to('equipe.web@ldc.fr')->subject('Test Email');
});
}
}

View file

@ -1,231 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Enums\Users\RoleEnum;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class UserCreate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uwil:user:create';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a backend admin user';
/**
* The first name of the new user.
*
* @var string
*/
protected $first_name;
/**
* The last name of the new user.
*
* @var string
*/
protected $last_name;
/**
* The email of the new user.
*
* @var string
*/
protected $email;
/**
* The password of the new user.
*
* @var string
*/
protected $password;
/**
* The role of the new user.
*
* @var string
*/
protected $role;
/**
* Execute the console command.
*
* @return integer
*/
public function handle(): int
{
$this->first_name = null;
$this->addFirstName();
$this->last_name = null;
$this->addLastName();
$this->email = null;
$this->addEmail();
$this->password = null;
$this->addPassword();
$this->role = null;
$this->addRole();
$user = new User([
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email' => $this->email,
'password' => $this->password,
'role' => $this->role,
'published' => true,
'published_at' => Carbon::now(),
]);
$user->saveOrFail();
$this->info('User created 👌');
return 0;
}
/**
* Add a first name to the user.
*
* @return void
*/
private function addFirstName(): void
{
while (is_null($this->first_name)) {
try {
$tmp = $this->ask('Type the wanted user first name', 'Admin');
$validator = Validator::make(['first_name' => $tmp], [
'first_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->first_name = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid first name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
/**
* Add a last name to the user.
*
* @return void
*/
private function addLastName(): void
{
while (is_null($this->last_name)) {
try {
$tmp = $this->ask('Type the wanted user last name', 'Saïtama');
$validator = Validator::make(['last_name' => $tmp], [
'last_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->last_name = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid last_name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
/**
* Add an email to the user.
*
* @return void
*/
private function addEmail(): void
{
while (is_null($this->email)) {
try {
$tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr');
$validator = Validator::make(['email' => $tmp], [
'email' => 'required|unique:users,email|email:rfc,dns'
]);
$validator->validate();
$this->email = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid email %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
/**
* Add a password to the user.
*
* @return void
*/
private function addPassword(): void
{
while (
($this->password = $this->secret(
(!\is_null($this->password) ? 'Failed to confirm, ' : '') . 'Type the wanted user password'
)) !== $this->secret('Confirm the password')
) {
continue;
}; //end while
}
/**
* Add a role to the user.
*
* @return void
*/
private function addRole(): void
{
while (is_null($this->role)) {
try {
$tmp = $this->choice(
'Select his role',
[RoleEnum::conceptor->name, RoleEnum::admin->name, RoleEnum::visitor->name],
RoleEnum::visitor->name,
$maxAttempts = null,
$allowMultipleSelections = false
);
if ($tmp === RoleEnum::conceptor->name) {
$tmp = RoleEnum::conceptor;
}
if ($tmp === RoleEnum::admin->name) {
$tmp = RoleEnum::admin;
}
if ($tmp === RoleEnum::visitor->name) {
$tmp = RoleEnum::visitor;
}
$validator = Validator::make(['role' => $tmp], [
'role' => 'required'
]);
$validator->validate();
$this->role = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please choose from the selection',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
}

View file

@ -1,37 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Jobs\Arcadia\XmlBrandReaderJob;
use Illuminate\Console\Command;
class XmlBrandReader extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'xml:brand';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the XmlBrandReaderJob manually';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info('Running the Job, this could take some time.');
$job = new XmlBrandReaderJob();
$job->console = $this;
$job->handle();
$this->info(' Job completed');
}
}

View file

@ -2,11 +2,12 @@
namespace App\Console\Commands;
use App\Jobs\Arcadia\XmlIncoReaderJob;
use App\Jobs\XmlIncoReader as JobsXmlIncoReader;
use Illuminate\Console\Command;
class XmlIncoReader extends Command
{
/**
* The name and signature of the console command.
*
@ -29,9 +30,9 @@ class XmlIncoReader extends Command
public function handle()
{
$this->info('Running the Job, this could take some time.');
$job = new XmlIncoReaderJob();
$job = new JobsXmlIncoReader();
$job->console = $this;
$job->handle();
$this->info(' Job completed');
$this->info('Job completed');
}
}

View file

@ -7,14 +7,25 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
//$schedule->job(new XmlIncoReader)->weeklyOn(1, '8:00');
}
/**
@ -24,7 +35,7 @@ class Kernel extends ConsoleKernel
*/
protected function commands()
{
$this->load(__DIR__ . '/Commands');
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}

View file

@ -1,46 +0,0 @@
<?php
namespace App\Enums\ActivityLogs;
use App\Traits\Enums\BaseEnum;
enum ActivityLogsEventEnum: int
{
use BaseEnum;
case created = 1;
case updated = 2;
case duplicated = 3;
case deleted = 4;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::created->name => 'Création',
self::updated->name => 'Mise à jour',
self::duplicated->name => 'Duplication',
self::deleted->name => 'Suppression',
];
/**
* Custom added classes definition
*/
private const BOOTSTRAPCLASS = [
self::created->name => 'success',
self::updated->name => 'warning',
self::duplicated->name => 'secondary',
self::deleted->name => 'danger',
];
/**
* Get Class
*
* @return string
*/
public function bootstrapClass(): string
{
return self::BOOTSTRAPCLASS[$this->name];
}
}

View file

@ -1,36 +0,0 @@
<?php
namespace App\Enums\Api;
use App\Traits\Enums\BaseEnum;
enum ApiError: int
{
use BaseEnum;
case modelNotFound = 101;
case validationFailed = 102;
case authFailed = 117;
case apiAuthFailed = 119;
case methodNotAllowed = 150;
case wrongTimeStamp = 151;
case missingParameter = 400;
case authorizationException = 403;
case throttleRequestsException = 429;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::modelNotFound->name => '%s introuvable.',
self::validationFailed->name => 'Impossible de valider les données',
self::authFailed->name => 'Echec de l\'authentification du compte',
self::apiAuthFailed->name => 'Echec de l\'authentification client à l\'Api pour ce service',
self::methodNotAllowed->name => 'This method is not allowed for this request',
// phpcs:ignore Generic.Files.LineLength.TooLong
self::wrongTimeStamp->name => 'Your request is malformed, please check timestamp header and parameter',
self::missingParameter->name => 'The request is missing some parameters',
self::authorizationException->name => 'You are not autorized to access this',
self::throttleRequestsException->name => 'Throttle: too many request have been attempted, please retry later'
];
}

View file

@ -1,13 +0,0 @@
<?php
namespace App\Enums\Api;
use App\Traits\Enums\BaseEnum;
enum ApiStatus: int
{
use BaseEnum;
case OK = 1;
case KO = 2;
}

View file

@ -2,16 +2,20 @@
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
use Kwaadpepper\Enum\BaseEnumRoutable;
enum LogType: int
/**
* This is an enumeration class,
* the following methods are its possible values
*
* @method static self debug()
* @method static self log()
* @method static self warning()
* @method static self info()
* @method static self error()
* @method static self critical()
*
*/
class LogType extends BaseEnumRoutable
{
use BaseEnum;
case debug = 1;
case log = 2;
case warning = 3;
case info = 4;
case error = 5;
case critical = 6;
}

View file

@ -1,16 +0,0 @@
<?php
namespace App\Enums\Pagination;
use App\Traits\Enums\BaseEnum;
enum ItemsPerPaginationEnum: int
{
use BaseEnum;
case five = 5;
case twelve = 12;
case twentyfive = 25;
case fifty = 50;
case onehundred = 100;
}

View file

@ -1,21 +0,0 @@
<?php
namespace App\Enums\Theme;
use App\Traits\Enums\BaseEnum;
enum BootstrapThemeEnum: int
{
use BaseEnum;
case light = 1;
case dark = 2;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::light->name => 'Light',
self::dark->name => 'Dark',
];
}

View file

@ -1,23 +0,0 @@
<?php
namespace App\Enums\Users;
use App\Traits\Enums\BaseEnum;
enum RoleEnum: int
{
use BaseEnum;
case conceptor = 1;
case admin = 2;
case visitor = 3;
/**
* Optionnal labels definition.
*/
private const LABELS = [
self::conceptor->name => 'Concepteur',
self::admin->name => 'Administrateur',
self::visitor->name => 'Visiteur',
];
}

View file

@ -1,21 +0,0 @@
<?php
namespace App\Enums\XmlInco;
use App\Traits\Enums\BaseEnum;
enum PortionType: int
{
use BaseEnum;
case approximative = 0;
case exact = 1;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::approximative->name => 'approximativement',
self::exact->name => 'exactement'
];
}

View file

@ -2,6 +2,8 @@
namespace App\Exceptions;
class AbortXmlImportException extends \RuntimeException
use Exception;
class AbortXmlImportException extends Exception
{
}

View file

@ -1,99 +0,0 @@
<?php
namespace App\Exceptions\Api;
use App\Enums\Api\ApiError;
use Illuminate\Support\Facades\Log;
/**
* This would be thrown if any of the case
* referenced in \App\Enums\ApiError would
* occur within flucollector
*
* On the client an ApiErrorException would
* also be throw with the ApiError corresponding to
* the situation
*/
class ApiErrorException extends \RuntimeException
{
/**
* Api Error
* This value makes the error more explicit.
* Handling these errors are normal as inputs or
* request may be undesired or refused by flucollector.
*
* @var \App\Enums\Api\ApiError
*/
protected $error;
/**
* This is an explicit error message,
* ou may print this to the front or not depending
* on $error value
*
* @var string
*/
protected $message;
/**
* This is the http code returned by flucollector.
* This value could be used to handle errors.
* But please take in consideration $error first
*
* @var integer
*/
protected $httpCode;
/**
* Create a new exception base on ApiError enum.
*
* @param \App\Enums\Api\ApiError $error
* @param integer $httpCode
* @param string $message
* @param \Throwable|null $previous
*/
public function __construct(ApiError $error, int $httpCode = 400, string $message = '', \Throwable $previous = null)
{
$this->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 \App\Enums\Api\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('FluxCollector API: %s', $this->getMessage()));
// @codeCoverageIgnoreEnd
}
}

View file

@ -1,24 +0,0 @@
<?php
namespace App\Exceptions\Api;
use Illuminate\Support\Facades\Log;
/**
* This is a critical error.
* The request could not be handled. You may dump any value you can
* get before submitting a ticket to https://support.uwil.fr
*/
class ApplicationException extends \RuntimeException
{
/**
* This is used by laravel only.
* It uses Log class to print the error.
*
* @return void
*/
public function report()
{
Log::critical(sprintf('API: %s', $this->getMessage()));
}
}

View file

@ -2,39 +2,27 @@
namespace App\Exceptions;
use App\Enums\Api\ApiError;
use App\Enums\Api\ApiStatus;
use App\Exceptions\Api\ApiErrorException;
use App\Http\Resources\Api\ApiResponse;
use App\Lib\ApiHelper;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
* @var string[]
*/
protected $dontReport = [
ApiErrorException::class
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
* @var string[]
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
@ -44,132 +32,10 @@ class Handler extends ExceptionHandler
*
* @return void
*/
public function register(): void
public function register()
{
$this->reportable(function (\Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
$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(mixed $request, \Throwable $e): Response
{
if (ApiHelper::isRequestForApi($request) and $request->isJson()) {
return $this->handleApiException($request, $e);
} //end if
// @codeCoverageIgnoreStart
return parent::render($request, $e);
// @codeCoverageIgnoreEnd
}
/**
* Generate a json response for any apiV4 error case.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $e
* @return \Illuminate\Http\JsonResponse
*/
private function handleApiException(Request $request, \Throwable $e): JsonResponse
{
switch (get_class($e)) {
case AuthenticationException::class:
/** @var \Illuminate\Auth\AuthenticationException $e */
return $this->getReponse(
$request,
401,
ApiError::apiAuthFailed->value(),
sprintf('%s, %s', ApiError::apiAuthFailed->label(), $e->getMessage())
);
case MethodNotAllowedHttpException::class:
/** @var \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException $e */
return $this->getReponse(
$request,
$e->getStatusCode(),
ApiError::methodNotAllowed->value(),
sprintf('%s, %s', ApiError::methodNotAllowed->label(), $e->getMessage())
);
case ValidationException::class:
/** @var \Illuminate\Validation\ValidationException $e */
return $this->getReponse(
$request,
$e->status,
ApiError::validationFailed->value(),
sprintf(ApiError::validationFailed->label(), $e->getMessage()),
['errors' => $e->errors()]
);
case ModelNotFoundException::class:
/** @var \Illuminate\Database\Eloquent\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 \App\Exceptions\Api\ApiErrorException $e */
return $this->getReponse(
$request,
$e->getStatusCode(),
$e->getCode(),
$e->getMessage()
);
case ThrottleRequestsException::class:
/** @var \Illuminate\Http\Exceptions\ThrottleRequestsException $e */
return $this->getReponse(
$request,
429,
ApiError::throttleRequestsException->value(),
ApiError::throttleRequestsException->label()
);
case AuthorizationException::class:
/** @var \Illuminate\Auth\Access\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, \intval($e->getCode()), $e->getMessage());
// @codeCoverageIgnoreEnd
}
/**
* Generate an Api json response.
*
* @param \Illuminate\Http\Request $request
* @param integer $httpCode
* @param integer $apiCode
* @param string $message
* @param array $data
* @return \Illuminate\Http\JsonResponse
*/
private static function getReponse(
Request $request,
int $httpCode,
int $apiCode,
string $message,
array $data = []
): JsonResponse {
return (new ApiResponse(
(object)array_merge($data, ['message' => $message, 'code' => $apiCode]),
ApiStatus::KO
))->toResponse($request)->setStatusCode($httpCode);
}
}

View file

@ -2,6 +2,8 @@
namespace App\Exceptions;
class InvalidXmlException extends \RuntimeException
use Exception;
class InvalidXmlException extends Exception
{
}

View file

@ -1,19 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
class ApiController extends Controller
{
/**
* Just returns HTTP 200 response.
*
* @return \Illuminate\Http\JsonResponse
*/
public function ping(): JsonResponse
{
return response()->json();
}
}

View file

@ -1,63 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\XmlInco\SearchRequest;
use App\Http\Resources\Api\XmlListResource;
use App\Http\Resources\Api\XmlIncoResource;
use App\Models\Service;
use App\Models\XmlInco;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Carbon;
class XmlIncoController extends Controller
{
/**
* Returns all service questions.
*
* @param \App\Http\Requests\Api\XmlInco\SearchRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function search(SearchRequest $request, Service $service): ResourceCollection
{
return new XmlListResource($service->arcadiaBrand->xmlIncos()
->where(fn (Builder $q) => $this->searchQuery(
$q,
$request->search,
null,
'id',
'name'
))->limit(20)->get(['id', 'name']));
}
/**
* Get ids list of updated resources since a specific date
*
* @param \App\Models\Service $service
* @param \Illuminate\Support\Carbon $api_since
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function getIdListSince(Service $service, Carbon $api_since): ResourceCollection
{
return new XmlListResource(
$service->arcadiaBrand->xmlIncos()
->where('xml_incos.updated_at', '>=', $api_since)->get(['id', 'name']),
null,
true
);
}
/**
* Show Xml Inco resource
*
* @param \App\Models\XmlInco $api_xml_inco
* @return \App\Http\Resources\Api\XmlIncoResource
*/
public function show(XmlInco $api_xml_inco): XmlIncoResource
{
return new XmlIncoResource($api_xml_inco);
}
}

View file

@ -1,71 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Traits\Controllers\ChangesModelOrder;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
class ActivityLogsController extends Controller
{
use ChangesModelOrder;
use UpdateModelPublished;
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
$this->authorize('isConceptor');
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ActivityLog::query()->with('user');
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'model_class',
);
}
$searchFields = \implode(', ', [
trans('validation.custom.target'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$activitylogModels = $this->paginate($query);
return view('bo.pages.activity_logs.index', compact('activitylogModels', 'search', 'searchFields'));
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\ActivityLog $activity_log
* @return \Illuminate\Contracts\View\View
*/
public function show(ActivityLog $activity_log): \Illuminate\Contracts\View\View
{
$this->authorize('isConceptor');
$targetModelClass = $activity_log->model_class;
$targetModel = $activity_log->model_class::where(
(new $targetModelClass())->getRouteKeyName(),
$activity_log->model_id
)->first();
return view('bo.pages.activity_logs.show', [
'activitylogModel' => $activity_log,
'targetModel' => $targetModel,
]);
}
}

View file

@ -1,54 +0,0 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:backend')->except('logout');
}
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard(): StatefulGuard
{
return Auth::guard('backend');
}
/**
* Show the application's login form.
*
* @return \Illuminate\Contracts\View\View
*/
public function showLoginForm(): View
{
return view('bo.auth.login');
}
/**
* Where to redirect users after login.
*
* @return string
*/
protected function redirectTo(): string
{
return response()->redirectToIntended()->getTargetUrl();
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Support\Facades\Auth;
class ResetPasswordController extends Controller
{
use ResetsPasswords;
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard(): StatefulGuard
{
return Auth::guard('backend');
}
/**
* Where to redirect users after login.
*
* @return string
*/
protected function redirectTo(): string
{
return \route('bo.login');
}
}

View file

@ -1,59 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Http\Controllers\Controller;
use App\Models\ArcadiaBrand;
use Illuminate\Http\Request;
class BrandController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ArcadiaBrand::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ArcadiaBrand::query();
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'id',
'label',
);
}
$this->sortQuery($query);
$searchFields = trans('Arcadia id, Label');
$brandModels = $this->paginate($query, ItemsPerPaginationEnum::twentyfive);
return view('bo.pages.brands.index', compact('brandModels', 'searchFields'));
}
/**
* Display the specified resource.
*
* @param \App\Models\ArcadiaBrand $arcadiaBrand
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function show(ArcadiaBrand $arcadiaBrand)
{
return view('bo.pages.brands.show', ['brandModel' => $arcadiaBrand]);
}
}

View file

@ -1,19 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
class DashboardController extends Controller
{
/**
* Dashboard Page
*
* @return \Illuminate\Contracts\View\View
*/
public function index(): View
{
return view('bo.pages.dashboard');
}
}

View file

@ -1,204 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Services\StoreServiceRequest;
use App\Http\Requests\Bo\Services\UpdateServiceRequest;
use App\Mail\ServiceCredentialRenewed;
use App\Models\ArcadiaBrand;
use App\Models\Service;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
/**
* @codeCoverageIgnore
*/
class ServiceController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Service::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Service::query();
if ($request->brand) {
$query = $query->whereHas('arcadiaBrand', $request->brand);
}
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'name',
);
}
$this->sortQuery($query);
$searchFields = trans('Nom');
$serviceModels = $query->paginate(12);
return view('bo.pages.services.index', compact('serviceModels', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Service $service
* @return \Illuminate\Contracts\View\View
*/
public function create(Service $service): \Illuminate\Contracts\View\View
{
$brandModels = ArcadiaBrand::all();
return view('bo.pages.services.create', [
'serviceModel' => $service,
'brandModels' => $brandModels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Services\StoreServiceRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreServiceRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$service = new Service();
$service->fill($request->validated());
$service->generateBearerToken();
$service->saveOrFail();
$this->generateServicePassword($service);
if ($service->saveOrFail()) {
return redirect()->route('bo.services.edit', $service)
->with('success', __('crud.messages.has_been_created', [
'model' => Str::ucfirst(__('models.classes.service'))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_created', [
'model' => __('models.classes.service')
]));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Service $service
* @return \Illuminate\Contracts\View\View
*/
public function edit(Service $service): \Illuminate\Contracts\View\View
{
$brandModels = ArcadiaBrand::all();
return view('bo.pages.services.edit', [
'serviceModel' => $service,
'brandModels' => $brandModels
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Bo\Services\UpdateServiceRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateServiceRequest $request, Service $service): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $service) {
$service->fill($request->validated());
if ($service->saveOrFail()) {
return redirect()->route('bo.services.edit', $service->id)
->with('success', __('crud.messages.has_been_updated', [
'model' => Str::ucfirst(__('models.classes.service'))
]));
}
return redirect()->route('bo.services.edit', $service->id)
->with('error', __('crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(__('models.classes.service'))
]));
});
}
/**
* Show the specified resource.
*
* @param \App\Models\Service $service
* @return \Illuminate\Contracts\View\View
*/
public function show(Service $service): \Illuminate\Contracts\View\View
{
return view('bo.pages.services.show', [
'serviceModel' => $service
]);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Service $service
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Service $service): \Illuminate\Http\RedirectResponse
{
if ($service->deleteOrFail()) {
return redirect()->route('bo.services.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => __('models.classes.service')
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => __('models.classes.service')
]));
}
/**
* Regen service credentials.
*
* @param \App\Models\Service $service
* @return \Illuminate\Http\RedirectResponse
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed, Squiz.Commenting.FunctionComment.Missing
public function generateServicePassword(Service $service): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\User $user */
$user = auth()->user();
// * Special Condition for migration from v2 to v3.
$service->bearer_token = !Hash::needsRehash($service->bearer_token) ?
$service->generateBearerToken() : $service->bearer_token;
$bearerPwd = $service->generateBearerPassword();
$service->saveOrFail();
Mail::send(new ServiceCredentialRenewed($service, $user));
return back()
// phpcs:ignore Generic.Files.LineLength.TooLong
->with('error', trans("ATTENTION : METTEZ À JOUR LE SITE ! \nNouveau token d'API de :service : ':token' et mot de passe ':password'", [
'service' => $service->name,
'token' => $service->bearer_token,
'password' => $bearerPwd,
]));
}
}

View file

@ -1,90 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\StatsByServiceRequest;
use App\Models\Service;
use App\Models\ServiceStat;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Schema;
use League\Csv\Writer;
/**
* @codeCoverageIgnore
*/
class StatsController extends Controller
{
/**
* Shows stats by service.
*
* @param \App\Http\Requests\Bo\StatsByServiceRequest $request
* @return \Illuminate\Http\Response
*/
public function viewStatsByService(StatsByServiceRequest $request)
{
$stats = null;
if ($request->has('service')) {
/** @var \Illuminate\Database\Eloquent\Collection<\App\Models\ServiceStat> */
$stats = ServiceStat::query()
->with('service')
->whereDate('date', '>=', $request->start)
->whereDate('date', '<=', $request->end)
->whereHas('service', fn (Builder $q) => $q->where('id', $request->service))
->get();
}
return view('bo.pages.stats.view-stats-by-services', \compact('stats'));
}
/**
* Exports stats by service as a csv file.
*
* @param \App\Http\Requests\Bo\StatsByServiceRequest $request
* @return \Illuminate\Http\Response
*/
public function exportByBrandCsv(StatsByServiceRequest $request): Response
{
/** @var \App\Models\Service */
$service = Service::query()->findOrFail($request->service);
/** @var \Illuminate\Database\Eloquent\Collection<\App\Models\ServiceStat> */
$stats = ServiceStat::query()
->with('service.arcadiaBrand')
->whereDate('date', '>=', $request->start)
->whereDate('date', '<=', $request->end)
->whereHas('service', fn (Builder $q) => $q->where('id', $request->service))
->get();
$columns = Schema::getColumnListing((new ServiceStat())->getTable());
$columns = Arr::where($columns, fn ($value) => !in_array($value, ['date', 'service_id']));
/** @var array<string> $columns */
$header = '"date";"service";' . implode(';', $columns);
$w = Writer::createFromString($header . PHP_EOL);
$w->setDelimiter(';');
$w->setEnclosure('"');
$w->setOutputBOM(Writer::BOM_UTF8);
/** @var \App\Models\ServiceStat $stat */
foreach ($stats as $stat) {
/** @var array<string,string> $cols */
$cols = [
$stat->date->isoFormat('YYYY-MM-DD'),
$service->label
];
foreach ($columns as $colname) {
$cols[] = $stat->{$colname};
}
$w->insertOne($cols);
}
return response((string)$w, 200, [
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
// phpcs:ignore Generic.Files.LineLength.TooLong
'Content-Disposition' => "attachment; filename=\"Statistiques du {$request->start} au {$request->end} pour {$service->name}.csv\"",
'Content-Description' => 'File Transfer',
]);
}
}

View file

@ -1,169 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Users\StoreUserRequest;
use App\Http\Requests\Bo\Users\UpdateUserRequest;
use App\Models\User;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class UsersController extends Controller
{
use UpdateModelPublished;
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(User::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $userModels */
$userModels = User::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$userModels,
$search,
null,
'first_name',
'last_name',
'email',
'role',
);
}
$searchFields = \implode(', ', [
trans('validation.attributes.first_name'),
trans('validation.attributes.last_name'),
trans('validation.attributes.email'),
trans('validation.attributes.role'),
]);
/** Sort columns with a query */
$this->sortQuery($userModels);
/** Custom pagination */
$userModels = $this->paginate($userModels);
return view('bo.pages.users.index', compact('userModels', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function create(User $user): \Illuminate\Contracts\View\View
{
return view('bo.pages.users.create', ['userModel' => $user]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Users\StoreUserRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreUserRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$user = new User();
$user->fill($request->validated());
if ($user->saveOrFail()) {
return redirect()->route('bo.users.edit', $user)
->with('success', __('crud.messages.has_been_created', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_created', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
});
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function edit(User $user): \Illuminate\Contracts\View\View
{
return view('bo.pages.users.edit', ['userModel' => $user]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Users\UpdateUserRequest $request
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateUserRequest $request, User $user): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $user) {
$user->fill($request->validated());
if ($user->saveOrFail()) {
return redirect()->route('bo.users.edit', $user)
->with('success', __('crud.messages.has_been_updated', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
return redirect()->route('bo.users.edit', $user)
->with('error', __('crud.messages.cannot_be_updated', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(User $user): \Illuminate\Http\RedirectResponse
{
if ($user->deleteOrFail()) {
return redirect()->route('bo.users.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::ucfirst(__('models.classes.user'))
]));
}
/**
* Duplicate the specified resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function duplicate(User $user): \Illuminate\Contracts\View\View
{
return $this->create($user->replicate());
}
}

View file

@ -1,59 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Http\Controllers\Controller;
use App\Models\XmlInco;
use Illuminate\Http\Request;
class XmlIncoController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(XmlInco::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = XmlInco::query();
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'id',
'name',
);
}
$this->sortQuery($query);
$searchFields = trans('Arcadia id, Nom');
$xmlIncoModels = $this->paginate($query, ItemsPerPaginationEnum::twentyfive);
return view('bo.pages.xml_incos.index', compact('xmlIncoModels', 'searchFields'));
}
/**
* Display the specified resource.
*
* @param \App\Models\XmlInco $xml_inco
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function show(XmlInco $xml_inco)
{
return view('bo.pages.xml_incos.show', ['xmlIncoModel' => $xml_inco]);
}
}

View file

@ -2,230 +2,12 @@
namespace App\Http\Controllers;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Enums\Theme\BootstrapThemeEnum;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
class Controller extends BaseController
{
use AuthorizesRequests;
use ValidatesRequests;
/**
* Build Search query on specified fields
* splitting search into words or using whole sentence
*
* @param \Illuminate\Database\Eloquent\Builder $query The eloquent query builder.
* @param string $search The query string.
* @param callable $searchQuery The query string.
* @param string ...$fields Either a simple array or a string
* in case of a string the two fields
* would be concatenate to make the search.
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function searchQuery(
Builder $query,
string $search,
callable $searchQuery = null,
string ...$fields
): \Illuminate\Database\Eloquent\Builder {
if (count($fields)) {
// Search using words.
$words = explode(' ', $search);
$query->where(function ($query) use ($searchQuery, $words, $search, $fields) {
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$this->searchOnFields($query, $word, $fields);
} //end foreach
// Search whole using whole sentence.
$this->searchOnFields($query, $search, $fields);
if ($searchQuery) {
$searchQuery($query);
}
});
} //end if
return $query;
}
/**
* Build query search on all fields
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param array $fields
* @return \Illuminate\Database\Eloquent\Builder
*/
private function searchOnFields(
Builder $query,
string $search,
array $fields
): \Illuminate\Database\Eloquent\Builder {
$table = $query->getModel()->getTable();
foreach ($fields as $field) {
$field = str_replace('?', '\?', $field);
if (is_array($field)) {
$query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($field)->map(function ($q) use ($table) {
return "`$table`.`$q`";
})->implode(', \' \', ')),
['%' . htmlspecialchars($search) . '%']
);
return $query;
}
// Get models woth enum's label.
if (is_array($this->getSearchValue($query, $search, $field))) {
foreach ($this->getSearchValue($query, $search, $field) as $array) {
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($array->value) . '%']
);
}
return $query;
}
// Get models with relation's name/label.
if (Str::endsWith($field, '_id') && $query->getModel()->has(Str::remove('_id', $field))) {
$query->orWhereHas(Str::remove('_id', $field), function ($queryRelation) use ($field, $search) {
if (Schema::hasColumn(Str::remove('_id', $field) . 's', 'name')) {
$queryRelation->where('name', 'like', '%' . $search . '%');
} elseif (Schema::hasColumn(Str::remove('_id', $field) . 's', 'label')) {
$queryRelation->where('label', 'like', '%' . $search . '%');
}
});
return $query;
}
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($this->getSearchValue($query, $search, $field)) . '%']
);
} //end foreach
return $query;
}
/**
* Get the search value (on string or Enum).
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param string $field
* @return string|array
*/
protected function getSearchValue(Builder $query, string $search, string $field): string|array
{
if (
count($query->getModel()->getCasts()) and // Check if the model has casts.
isset($query->getModel()->getCasts()[$field]) and // Check if there is cast about specific field.
enum_exists($query->getModel()->getCasts()[$field]) // Check if the cast is an enum.
) {
$cases = collect($query->getModel()->getCasts()[$field]::toArray());
$enum = $cases->filter(function ($item) use ($search) {
return preg_match('/' . Str::lower($search) . '/', Str::lower($item->label));
});
return $enum->toArray();
} else {
return $search;
}
}
/**
* Sort columns with a query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $ignore
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function sortQuery(Builder $query, array $ignore = []): \Illuminate\Database\Eloquent\Builder
{
$rName = request()->route()->getName();
$table = $query->getModel()->getTable();
if (request()->sort_col) {
Session::put("$rName.sort_col", request()->sort_col);
Session::put("$rName.sorted", true);
}
if (request()->sort_way) {
Session::put("$rName.sort_way", request()->sort_way);
Session::put("$rName.sorted", true);
}
if (request()->rst) {
Session::remove("$rName.sorted");
Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way");
}
if (request()->sort_col and request()->sort_way and Schema::hasColumn($table, request()->sort_col)) {
if (
!isset($ignore) or
(isset($ignore) and !in_array(request()->sort_col, $ignore))
) {
$query = $query->orderBy(\sprintf("$table.%s", request()->sort_col), request()->sort_way);
}
} elseif (Schema::hasColumn($table, 'order')) {
$query = $query->orderBy('order', 'ASC');
Session::put("$rName.sort_col", 'order');
Session::put("$rName.sort_way", 'asc');
} elseif (Schema::hasColumn($table, 'updated_at')) {
$query = $query->orderBy('updated_at', 'DESC');
Session::put("$rName.sort_col", 'updated_at');
Session::put("$rName.sort_way", 'desc');
} elseif (Schema::hasColumn($table, 'created_at')) {
$query = $query->orderBy('created_at', 'DESC');
Session::put("$rName.sort_col", 'created_at');
Session::put("$rName.sort_way", 'desc');
}
return $query;
}
/**
* Customize pagination with cache or config (default).
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \App\Enums\Pagination\ItemsPerPaginationEnum|null $pagination
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
protected function paginate(
Builder $query,
ItemsPerPaginationEnum $pagination = null
): \Illuminate\Contracts\Pagination\LengthAwarePaginator {
$currentRoutePath = Str::slug(request()->route()->getName());
$cacheKey = "pagination.{$currentRoutePath}";
$pagination = $pagination ?? ItemsPerPaginationEnum::twelve;
$pagination = ToolboxHelper::getValidatedEnum(
Cache::get($cacheKey, $pagination->value()),
'pagination',
'\App\Enums\Pagination\ItemsPerPaginationEnum',
);
if (Cache::get($cacheKey) !== $pagination) {
Cache::put($cacheKey, $pagination);
}
$query = $query->paginate($pagination);
return $query;
}
/**
* Set the bootstrap theme or light (default).
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function setTheme(Request $request): \Illuminate\Http\RedirectResponse
{
$theme = ToolboxHelper::getValidatedEnum(
$request->theme ?: Cache::get('theme', BootstrapThemeEnum::light->value()),
'theme',
'\App\Http\Controllers\BootstrapThemeEnum',
);
if (Cache::get("theme") !== $theme) {
Cache::put("theme", $theme);
}
return redirect()->back()->with('success', __('crud.messages.theme_updated'));
}
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

View file

@ -11,40 +11,37 @@ class Kernel extends HttpKernel
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
* @var array
*/
protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\App\Http\Middleware\HtmlSanitizer::class
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
* @var array
*/
protected $middlewareGroups = [
'web' => [
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
],
'api' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\App\Http\Middleware\JsonOnly::class,
\App\Http\Middleware\ApiResponse::class,
\App\Http\Middleware\ApiCheckTimeStamps::class,
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
@ -54,17 +51,16 @@ class Kernel extends HttpKernel
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
* @var array
*/
protected $middlewareAliases = [
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];

View file

@ -1,43 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Enums\Api\ApiError;
use App\Exceptions\Api\ApiErrorException;
use App\Lib\ApiHelper;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
/**
* Check Http Header Timestamp with request timestamp
* to prevent replay attacks
* https://restfulapi.net/security-essentials/
*/
class ApiCheckTimeStamps
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::wrongTimeStamp
* if timestamp has an incorrect
* format or is timed out.
*/
public function handle(Request $request, \Closure $next): mixed
{
if (ApiHelper::isRequestForApi($request)) {
if (!\is_string($request->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);
}
}

View file

@ -1,59 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Enums\Api\ApiStatus;
use App\Http\Resources\Api\ApiResponse as ApiResponseResource;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ApiResponse
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
* @throws \ErrorException If the received response is not a JsonResponse.
*/
public function handle(Request $request, \Closure $next): mixed
{
/** @var \Illuminate\Http\JsonResponse $response */
$response = $next($request);
if (\get_class($response) !== JsonResponse::class) {
// @codeCoverageIgnoreStart
throw new \ErrorException('All api responses should be json.');
// @codeCoverageIgnoreEnd
}
// Set no cache header.
$response->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();
/** Check if it's and Api Response. */
if (
gettype($data) === "object" and
isset($data->status) and
isset($data->data) and
isset($data->version) and
in_array($data->status, [ApiStatus::OK->value(), ApiStatus::KO->value()])
) {
/** Return direct ApiResponse */
return $response;
}
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK : ApiStatus::KO;
return $response->setData(new ApiResponseResource((object)$data, $status));
}
}

View file

@ -3,37 +3,19 @@
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo(Request $request): ?string
protected function redirectTo($request)
{
$logins = [
'backend' => route('bo.login')
];
if (!$request->expectsJson()) {
// * Notify must login
Session::flash('warning', trans('Veuillez vous connecter pour continuer'));
foreach ($request->route()->gatherMiddleware() as $middleware) {
if (\is_string($middleware) and strpos($middleware, 'auth:') !== false) {
$route = $logins[str_replace('auth:', '', $middleware)] ?? null;
if ($route) {
return $route;
}
}
}
if (! $request->expectsJson()) {
return route('login');
}
return null;
}
}

View file

@ -9,7 +9,9 @@ class EncryptCookies extends Middleware
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
* @var array
*/
protected $except = [];
protected $except = [
//
];
}

View file

@ -1,61 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class HtmlSanitizer
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|array ...$params
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, \Closure $next, string|array ...$params): Response
{
$inputs = $request->all();
/** @var callable|null */
$cleanInput = null;
$cleanInput = function (
null|string|array &$input,
string $name,
?string $rootName = null
) use (
$params,
&$cleanInput
): void {
// * Nested element array
if (is_array($input)) {
array_walk_recursive($input, $cleanInput, $name);
}
if (!is_string($input)) {
return;
}
if (in_array($name, $params) or in_array($rootName ?? $name, $params)) {
// ? Is a json input ?
if ($isJson = \json_decode($input, true) and is_array($isJson)) {
// ! Decode each json field recursive.
\array_walk_recursive($isJson, function (string|array &$item) {
if (\is_string($item)) {
$item = clean($item);
$item = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($item);
}
});
} else {
$input = clean($input);
$input = ToolboxHelper::stripCkEditorSchemesOnMediaInputs($input);
}
} else {
$input = strip_tags($input);
}
};
\array_walk($inputs, $cleanInput);
$request->merge($inputs);
return $next($request);
}
}

View file

@ -1,27 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
class JsonOnly
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, \Closure $next): mixed
{
if (
(!$request->isJson() or
!$request->expectsJson() or
is_null(json_decode($request->getContent())))
) {
abort(400, 'Invalid JSON');
}
return $next($request);
}
}

View file

@ -9,7 +9,9 @@ class PreventRequestsDuringMaintenance extends Middleware
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
* @var array
*/
protected $except = [];
protected $except = [
//
];
}

View file

@ -2,39 +2,28 @@
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param string|null ...$guards
* @return \Symfony\Component\HttpFoundation\Response
* @throws \RuntimeException If guard is unhandled.
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null ...$guards
* @return mixed
*/
public function handle(Request $request, \Closure $next, ...$guards): Response
public function handle(Request $request, Closure $next, ...$guards)
{
// phpcs:enable phpcs:disable
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (auth($guard)->check()) {
switch ($guard) {
case 'frontend':
return \redirect()->route('fo.homepage');
case 'backend':
if (!$request->routeIs('bo.*')) {
continue;
}
return \redirect()->route('bo.dashboard');
default:
throw new \RuntimeException("Unhandled guard `{$guard}` .");
}
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}

View file

@ -1,103 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;
class SecureHeaders
{
/**
* Enumerate headers which you do not want in your application's responses.
* Great starting point would be to go check out @Scott_Helme's:
* https://securityheaders.com/
*
* @var array
*/
private $unwantedHeaderList = [
'X-Powered-By',
'Server',
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @return \Symfony\Component\HttpFoundation\Response
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function handle(Request $request, Closure $next): Response
{
// phpcs:enable
View::share('nonce', $nonce = Vite::useCspNonce());
// * For VueJS dev tool to execute
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-{$nonce}' 'strict-dynamic'";
$scheme = config('app.env') === 'local' ? 'http' : 'https';
$reportUri = Route::has('cspReportUri') ?
\sprintf(' report-uri %s ;', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : '';
$this->removeUnwantedHeaders($this->unwantedHeaderList);
$response = $next($request);
$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains;');
$cspPolicies = [
"default-src 'none';",
"manifest-src 'self';",
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob:;",
"img-src * data: blob:;",
'frame-src ' . implode(' ', [
'https://www.google.com/recaptcha/',
'https://recaptcha.google.com/recaptcha/',
'https://www.google.com/maps/'
]) . ';',
"font-src 'self' data: https://fonts.gstatic.com;",
// ! Using CKEditor will must have unsafe-inline style
// phpcs:ignore Generic.Files.LineLength.TooLong
"style-src 'self' 'unsafe-inline' " . \implode(' ', [
'https://cdnjs.cloudflare.com',
// * File Manager
'https://fonts.googleapis.com',
'https://cdn.jsdelivr.net',
'https://use.fontawesome.com',
'https://fonts.googleapis.com'
]) . ';',
// ! If you dont use ckeditor use this
// "style-src 'self' 'nonce-$nonce';",
// phpcs:ignore Generic.Files.LineLength.TooLong
"script-src 'self' {$jsDev} " . \implode(' ', [
'https: blob:',
'https://www.googletagmanager.com',
'https://cdnjs.cloudflare.com',
"$scheme://www.google-analytics.com",
]) . ';'
];
$response->headers->set(
'Content-Security-Policy',
implode(' ', $cspPolicies) . $reportUri
);
$response->headers->set('Feature-Policy', 'fullscreen \'self\';');
return $response;
}
/**
* Remove unwanted headers
*
* @param array $headerList
* @return void
*/
private function removeUnwantedHeaders(array $headerList): void
{
foreach ($headerList as $header) {
header_remove($header);
}
}
}

View file

@ -9,7 +9,7 @@ class TrimStrings extends Middleware
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
* @var array
*/
protected $except = [
'current_password',

View file

@ -9,7 +9,7 @@ class TrustHosts extends Middleware
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
* @return array
*/
public function hosts()
{

View file

@ -10,17 +10,17 @@ class TrustProxies extends Middleware
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
* @var array|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var integer
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |

View file

@ -1,15 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [];
}

View file

@ -9,9 +9,9 @@ class VerifyCsrfToken extends Middleware
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
* @var array
*/
protected $except = [
'/csp-report'
//
];
}

View file

@ -1,42 +0,0 @@
<?php
namespace App\Http\Requests\Api\XmlInco;
use Illuminate\Foundation\Http\FormRequest;
class SearchRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('api')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->merge([
'search' => is_string($this->search) ? strip_tags($this->search) : null
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string,\Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'search' => 'required|string|min:1'
];
}
}

View file

@ -1,57 +0,0 @@
<?php
namespace App\Http\Requests\Bo\Services;
use DragonCode\Support\Facades\Helpers\Arr;
use Illuminate\Foundation\Http\FormRequest;
class StoreServiceRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->merge([
'arcadia_brand_id' => Arr::get(Arr::first(Arr::wrap($this->arcadiaBrand ?? [])), 'id')
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'name' => 'required|string|min:5|max:255',
'arcadia_brand_id' => 'required|integer|min:0|exists:arcadia_brands,id',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'name' => trans('Nom du service'),
'arcadia_brand_id' => trans('Id de la marque'),
];
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace App\Http\Requests\Bo\Services;
class UpdateServiceRequest extends StoreServiceRequest
{
}

View file

@ -1,55 +0,0 @@
<?php
namespace App\Http\Requests\Bo;
use Exception;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Carbon;
use Illuminate\Validation\ValidationException;
/**
* @codeCoverageIgnore
*/
class StatsByServiceRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
* @throws \Illuminate\Validation\ValidationException If given dates are unparsable of period
* is bigger than 1 year.
* @throws \Exception If carbon date processing failed.
*/
public function rules(): array
{
try {
if ((Carbon::parse($this->end)->diffInDays(Carbon::parse($this->start)) + 1) > 365) {
throw ValidationException::withMessages([
'Vous devez sélectionner une période de maximum 1 ans'
]);
}
} catch (Exception $e) {
if (\get_class($e) !== ValidationException::class) {
throw ValidationException::withMessages([
'Les dates sélectionnées ne sembles pas être valides'
]);
}
throw $e;
}
return [
'brand' => 'sometimes|exists:arcadia_brands,id',
'start' => 'required_with:brand|before_or_equal:end|date|date_format:Y-m-d',
'end' => 'required_with:brand|after_or_equal:start|date|date_format:Y-m-d|after_or_equal:start',
];
}
}

View file

@ -1,42 +0,0 @@
<?php
namespace App\Http\Requests\Bo;
use Illuminate\Foundation\Http\FormRequest;
class UpdateChangeOrderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'direction' => $this->direction === 'up' ? true : false
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'direction' => 'required|boolean',
];
}
}

View file

@ -1,229 +0,0 @@
<?php
namespace App\Http\Requests\Bo;
use App\Enums\InputType;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\ValidationException;
abstract class UpdateMiniCrudRequest extends FormRequest
{
/**
* The model parameter name
*
* @var string
*/
protected $modelName = null;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return auth('backend')->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
* @phpcs:disable Generic.Metrics.NestingLevel.TooHigh
*/
public function rules()
{
// phpcs:enable
$rules = [];
foreach ($this->{$this->modelName}->data as $iptName => $input) {
$input = \json_decode(\json_encode($input), true);
switch ($input['inputType']) {
case InputType::number->value:
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate('required|numeric', $input)
]);
break;
case InputType::videoYoutube->value:
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate('regex:%([^"&?\/\s]{11})%i', $input)
]);
break;
case InputType::videoVimeo->value:
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate('regex:%([0-9]{8,10})%i', $input)
]);
break;
case InputType::video->value:
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate([
'regex:%([0-9]{8,10}|[^"&?\/\s]{11})%i'
], $input)
]);
break;
case InputType::string->value:
$rules = array_merge($rules, [
"data.$iptName.value" => $this->compileValidate('required|string|min:3', $input)
]);
break;
case InputType::text->value:
$mergeRules = $this->compileValidate('required|string|min:15', $input);
$mergeRules = collect($mergeRules)
->map(function ($rule) {
if (\strpos($rule, 'max:') !== false) {
$max = \intval(str_replace('max:', '', $rule));
return function ($attribute, $value, $fail) use ($max) {
if (\strlen(\strip_tags($value)) > $max) {
$fail("$attribute ne doit pas excéder $max caractères");
}
};
}
return $rule;
});
$rules = array_merge($rules, [
"data.$iptName.value" => $mergeRules
]);
break;
case InputType::image->value:
$required = $this->isUpdate() ? 'sometimes' : 'required';
$ipt = $this->data[$iptName];
$max = isset($ipt['dimensionsMax']);
$dim = \is_string($input['dimensions']) ? collect(explode(
',',
(isset($ipt['dimensionsMax']) and is_string($ipt['dimensionsMax'])) ?
$ipt['dimensionsMax'] : $input['dimensions']
))->mapWithKeys(function ($v) {
$v = explode(' ', $v);
return [$v[0] => $v[1]];
}) : $input['dimensions'];
// * Adapt dimensions
$sizes = (object)[
'minHeight' => $dim['height'],
'maxHeight' => $dim['height'],
'minWith' => $dim['width'],
'maxWith' => $dim['width'],
];
$rules = array_merge($rules, [
"data.$iptName.value" => [
function (string $attribute, $value, callable $fail) use ($sizes) {
return ToolboxHelper::validatePicture($attribute, $value, $fail, $sizes);
}
],
$this->compileValidate(
implode('|', [
$required,
'image',
'mimes:png,jpg,jpeg',
sprintf(
'dimensions:%s=%d,%s=%d',
$max ? 'max_height' : 'height',
$dim['height'],
$max ? 'max_width' : 'width',
$dim['width']
)
]),
$input
),
"data.$iptName.alt" => $this->compileValidate("$required|string|min:5", $input),
"data.$iptName.title" => $this->compileValidate("$required|string|min:5", $input)
]);
break;
case InputType::url->value:
$rules = array_merge($rules, [
"data.$iptName.value" => [
'required', 'url',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
try {
ToolboxHelper::assertNoInternalUrls($attribute, $value);
ToolboxHelper::assertNoInternalBoUrls($attribute, $value);
} catch (ValidationException $e) {
$fail(collect($e->errors())->first()[0]);
}
}
]
]);
break;
case InputType::boolean->value:
$all = $this->all();
$nested = ToolboxHelper::dotStringToNestedArray(
"data.$iptName.value",
$this->input("data.$iptName.value") ? true : false
);
$all = ToolboxHelper::arrayMergeRecursiveDistinct($all, $nested);
$this->merge($all);
$rules = array_merge($rules, [
"data.$iptName.value" => 'boolean'
]);
break;
} //end switch
} //end foreach
return $rules;
}
/**
* Undocumented function
*
* @param string|array $inputTypeValidate
* @param array $input
* @return array
*/
public function compileValidate(string|array $inputTypeValidate, array $input): array
{
if (\is_string($inputTypeValidate)) {
$inputTypeValidate = explode('|', $inputTypeValidate);
}
/** @var array|string */
$inputValidate = \collect($input)->get('validate', $input);
$inputValidate = \is_string($inputValidate) ? explode('|', $inputValidate) : $inputValidate;
foreach ($inputValidate as $validate) {
if (!\in_array($validate, $inputTypeValidate)) {
$inputTypeValidate[] = $validate;
}
}
$test = \collect($inputTypeValidate)->all();
$test = \collect($inputTypeValidate)->sort(function ($valueA, $valueB) {
$valueA = $this->ruleHasToComeFirst($valueA) ? 0 : 2;
$valueB = $this->ruleHasToComeFirst($valueB) ? 1 : 2;
return $valueA - $valueB;
})->unique()->all();
return \collect($inputTypeValidate)->unique()->sort(function ($valueA, $valueB) {
$valueA = $this->ruleHasToComeFirst($valueA) ? 0 : 2;
$valueB = $this->ruleHasToComeFirst($valueB) ? 1 : 2;
return $valueA - $valueB;
})->all();
}
/**
* Does the rulle need to come first ?
*
* @param string $rule
* @return boolean
*/
private function ruleHasToComeFirst(string $rule): bool
{
$rule = \collect(\explode(':', $rule, 1))->filter()->first(null, '');
return \collect([
'nullable', 'present', 'required', 'required_if',
'required_unless', 'required_with', 'required_with_all',
'required_without', 'required_without_all', 'required_with_all',
'missing', 'missing_if', 'missing_unless',
'missing_with', 'missing_with_all',
])->first(fn (string $firstRule) => \strpos($rule, $firstRule) === 0) ? true : false;
}
/**
* Is this request an update request ?
*
* @return boolean
*/
private function isUpdate(): bool
{
return $this->isMethod('PUT');
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace App\Http\Requests\Bo\Users;
use App\Enums\Users\RoleEnum;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\Password;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->merge([
'published' => $this->published ? true : false
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'first_name' => 'required|string|min:3|max:255',
'last_name' => 'required|string|min:3|max:255',
'email' => 'required|email:rfc,strict,dns,spoof,filter|unique:users,email',
'password' => [
'required', 'required_with:password_confirmation', 'confirmed', 'max:255', Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
'role' => ['required', new Enum(RoleEnum::class)],
'published' => 'required|boolean',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'first_name' => trans('validation.attributes.first_name'),
'last_name' => trans('validation.attributes.last_name'),
'email' => trans('validation.attributes.email'),
'password' => trans('validation.attributes.password'),
'role' => trans('validation.attributes.role'),
'published' => trans('validation.custom.publication')
];
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace App\Http\Requests\Bo\Users;
use Illuminate\Validation\Rules\Password;
class UpdateUserRequest extends StoreUserRequest
{
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation()
{
$this->merge([
'published' => $this->published ? true : false,
'password' => $this->password ? $this->password : $this->route('user')->password,
'password_confirmation' => $this->password_confirmation ?
$this->password_confirmation :
$this->route('user')->password
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return array_merge(parent::rules(), [
'email' => [
'required',
'email:rfc,strict,dns,spoof,filter',
'unique:users,email,' . $this->route('user')->id . ',id',
],
'password' => [
'required', 'required_with:password_confirmation', 'confirmed', 'max:255', Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
]);
}
}

View file

@ -1,43 +0,0 @@
<?php
namespace App\Http\Resources\Api;
use App\Enums\Api\ApiStatus;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ApiResponse extends JsonResource
{
/** @var \stdClass|null */
protected $data;
/** @var \App\Enums\ApiStatus */
protected $status;
/**
* @param \stdClass|null $data
* @param \App\Enums\Api\ApiStatus $status
*/
public function __construct(\stdClass|null $data = null, ApiStatus $status)
{
$this->status = $status;
$this->data = $data;
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function toArray(Request $request): array
{
//phpcs:enable
return [
'status' => $this->status->value(),
'data' => (array)($this->data ?? []),
'version' => config('api.version')
];
}
}

View file

@ -1,47 +0,0 @@
<?php
namespace App\Http\Resources\Api;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Arr;
class XmlIncoResource extends JsonResource
{
/**
* The resource instance.
*
* @var \App\Models\XmlInco
*/
public $resource;
/**
* XmlInco columns to keep
*
* @var array
*/
private array $skel = [
'id', 'name', 'description', 'tips',
'preparation_advices', 'sell_arguments',
'allegations', 'ingredient_list', 'allergen_list',
'nutritional_values', 'portion_amount', 'portion_type',
'is_seasonal', 'updated_at'
];
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function toArray(Request $request): array
{
// phpcs:enable
return \array_merge(
Arr::only($this->resource->toArray(), $this->skel),
['updated_at' => $this->resource->updated_at->toIso8601String()]
);
}
}

View file

@ -1,54 +0,0 @@
<?php
namespace App\Http\Resources\Api;
use App\Models\XmlInco;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection as JsonResourceCollection;
use Illuminate\Support\Collection;
class XmlListResource extends JsonResourceCollection
{
/** @var \Illuminate\Support\Collection<\App\Models\XmlInco> */
protected $xmlIncoList = null;
/**
* Send only ids
*
* @var boolean
*/
protected $onlyIds = false;
/**
* Create a new resource instance.
*
* @param mixed $resource
* @param \Illuminate\Support\Collection<\App\Models\XmlInco> $xmlIncoList
* @return void
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function __construct(mixed $resource, Collection $xmlIncoList = null)
{
//phpcs:enable
$this->xmlIncoList = $xmlIncoList;
parent::__construct($resource);
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Support\Collection
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function toArray(Request $request): \Illuminate\Support\Collection
{
//phpcs:enable
return $this->collection->map(function (XmlInco $xml_inco) {
return [
'id' => $xml_inco->id,
'name' => $xml_inco->name
];
});
}
}

View file

@ -1,372 +0,0 @@
<?php
namespace App\Jobs\Arcadia;
use App\Enums\LogType;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
abstract class XArcadiaFlux implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* The Laravel disk name
*
* @var string
*/
protected $disk = null;
/**
* This is for Laravel Console usage
*
* @var \Illuminate\Console\Command|null
*/
public $console = null;
/**
* This is for Laravel Console usage
*
* @var \Symfony\Component\Console\Helper\ProgressBar|null
*/
public $progressBar = null;
/**
* invalid xml folder
*
* @var string
*/
protected $invalidFolderName = 'invalid';
/**
* The relative folder where to archive data
*
* @var string
*/
private $archiveFolderName = 'archives';
/**
* The files to ignore
*
* @var array
*/
protected $ignoreFiles = [
'.gitignore', '.gitkeep'
];
/**
* Check if the file extension
*
* @param string $filename
* @param string $extension
* @return boolean
*/
protected function checkExtension(string $filename, string $extension): bool
{
$isExtension = strtolower(pathinfo($filename, \PATHINFO_EXTENSION)) === $extension;
if (!$isExtension) {
Log::warning(sprintf(
'%s : unwanted file type in %s wanted %s',
get_class($this),
$filename,
$extension
));
}
return $isExtension;
}
/**
* Move the file to invalid folder
*
* @param string $filename
* @return void
*/
protected function moveToInvalid(string $filename): void
{
$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
);
}
}
/**
* 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
*
* @param string $filename
* @return void
*/
protected function moveToArchives(string $filename): void
{
$newFileName = $filename;
if (!$this->getDisk()->exists($filename)) {
$this->log(
sprintf(
'File %s could not be moved to archives (not found)',
$filename
),
LogType::warning
);
return;
}
if ($this->getDisk()->exists(sprintf('%s/%s', $this->archiveFolderName, $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->archiveFolderName, $newFileName))) {
$this->log(
sprintf(
'File %s could not be moved to archives',
$filename
),
LogType::warning
);
}
} catch (\Exception $e) {
$this->log(
sprintf(
'File %s could not be moved to archives',
$filename
),
LogType::warning
);
}
}
/**
* Suppression des fichiers dans
* archives qui ont plus de 2j
*
* @return void
*/
protected function clearOldArchives(): void
{
$disk = $this->getDisk();
$archives = $disk->files($this->archiveFolderName);
// /** @var File */
foreach ($archives as $archive) {
$pi = \pathinfo($archive, \PATHINFO_BASENAME);
if (in_array($pi, $this->ignoreFiles)) {
continue;
}
if ($disk->lastModified($archive) < strtotime(Carbon::now()->subDays(2))) {
if (!$disk->delete($archive)) {
Log::critical(sprintf('%s : Could not delete file %s', get_class($this), $archive));
};
}
}
}
/**
* Get the concerned disk
*
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function getDisk(): Filesystem
{
return Storage::disk($this->disk);
}
/**
* Logs data
*
* @param string $message
* @param LogType $logtype
* @return void
* @throws \RuntimeException If logtype is not handled.
*/
protected function log(string $message, LogType $logtype = null): void
{
if (!$logtype) {
$logtype = LogType::debug;
}
$message = sprintf('%s : %s', get_class($this), $message);
switch ($logtype) {
case LogType::debug:
Log::debug($message);
break;
case LogType::info:
Log::info($message);
break;
case LogType::log:
Log::log($message);
break;
case LogType::warning:
Log::warning($message);
break;
case LogType::error:
Log::error($message);
break;
case LogType::critical:
Log::critical($message);
break;
default:
throw new \RuntimeException(sprintf('%s : unhandled log type %s', get_class($this), $logtype->label));
} //end switch
$this->consolePrint($message, $logtype);
}
/**
* Prints to the console
*
* @param string $message
* @param \App\Enums\LogType $logtype
* @return void
* @throws \RuntimeException If logtype is not handled.
*/
protected function consolePrint(string $message, LogType $logtype): void
{
$this->clearProgressBarFromLine();
if (!$this->console) {
return;
}
switch ($logtype) {
case LogType::debug:
case LogType::info:
case LogType::log:
$this->console->info($message);
break;
case LogType::warning:
$this->console->comment($message);
break;
case LogType::error:
case LogType::critical:
$this->console->error($message);
break;
default:
throw new \RuntimeException(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);
$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();
}
}
}

View file

@ -1,62 +0,0 @@
<?php
namespace App\Jobs\Arcadia;
use App\Models\ArcadiaBrand;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class XmlBrandReaderJob extends XmlJobs
{
/**
* The xsd file path used to validate files
*
* @var string|null
*/
protected $xsdSchemaPath = 'documents/arcadia/brand.xsd';
/**
* Disk name from config/filesystems.php
*
* @var string
*/
protected $disk = 'arcadia_brand';
/**
* @var array Columns to take from the xml.
*/
protected $takeColumns = [
'codMarq' => 'id',
'libMarq' => 'label',
];
/**
* Fields that are allowed to be null
*
* @var array
*/
protected $nullableFields = [];
/**
* Unique keys for updating rows
*
* @var array
*/
protected $uniqueKeyField = ['id'];
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
ArcadiaBrand::flushQueryCache();
$this->xsdSchemaPath = \resource_path($this->xsdSchemaPath);
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
$this->parseData('dam', ArcadiaBrand::class, function (\SimpleXMLElement $xmlContent, array &$line) {
Arr::set($line, 'id', intval(Arr::get($line, 'id')));
Arr::set($line, 'label', (string)Str::of(Arr::get($line, 'label'))->squish()->title());
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,81 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Service;
use App\Models\ServiceStat;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
/**
* @codeCoverageIgnore
*/
class SetServiceStats implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
private const KEY = '2202020';
/** @var \App\Models\Service */
protected $service;
/**
* Create a new job instance.
*
* @param Service $service
* @return void
*/
public function __construct(Service $service)
{
$this->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))
];
}
}

223
app/Jobs/XArcadiaFlux.php Normal file
View file

@ -0,0 +1,223 @@
<?php
namespace App\Jobs;
use App\Enums\LogType;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
abstract class XArcadiaFlux implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* The Laravel disk name
*
* @var string
*/
protected $disk = null;
/**
* This is for Laravel Console usage
*/
public $console = null;
/**
* The relative folder where to archive data
*
* @var string
*/
private $archiveFolderName = 'archives';
/**
* The files to ignore
*
* @var array
*/
protected $ignoreFiles = [
'.gitignore', '.gitkeep'
];
/**
* Check if the file extension
*
* @param string $filename
* @return boolean
*/
protected function checkExtension(string $filename, string $extension): bool
{
$isExtension = strtolower(pathinfo($filename, \PATHINFO_EXTENSION)) === $extension;
if (!$isExtension) {
Log::warning(sprintf(
'%s : unwanted file type in %s wanted %s',
get_class($this),
$filename,
$extension
));
}
return $isExtension;
}
/**
* Move the file to archives
*
* @param string $filename
* @return void
*/
protected function moveToArchives(string $filename): void
{
$newFileName = $filename;
if (!$this->getDisk()->exists($filename)) {
$this->log(
sprintf(
'File %s could not be moved to archives (not found)',
$filename
),
LogType::warning()
);
return;
}
if ($this->getDisk()->exists(sprintf('%s/%s', $this->archiveFolderName, $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->archiveFolderName, $newFileName))) {
$this->log(
sprintf(
'File %s could not be moved to archives',
$filename
),
LogType::warning()
);
}
} catch (Exception $e) {
$this->log(
sprintf(
'File %s could not be moved to archives',
$filename
),
LogType::warning()
);
}
}
/**
* Suppression des fichiers dans
* archives qui ont plus de 2j
*
* @return void
*/
protected function clearOldArchives(): void
{
$disk = $this->getDisk();
$archives = $disk->files($this->archiveFolderName);
// /** @var File */
foreach ($archives as $archive) {
$pi = \pathinfo($archive, \PATHINFO_BASENAME);
if (in_array($pi, $this->ignoreFiles)) {
continue;
}
if ($disk->lastModified($archive) < strtotime(Carbon::now()->subDays(2))) {
if (!$disk->delete($archive)) {
Log::critical(sprintf('%s : Could not delete file %s', get_class($this), $archive));
};
}
}
}
/**
* Get the concerned disk
*
* @return Filesystem
*/
protected function getDisk(): Filesystem
{
return Storage::disk($this->disk);
}
/**
* Logs data
*
* @param string $message
* @param LogType $logtype
* @return void
*/
protected function log(string $message, LogType $logtype = null): void
{
if (!$logtype) {
$logtype = LogType::debug();
}
$message = sprintf('%s : %s', get_class($this), $message);
switch ($logtype->value) {
case LogType::debug()->value:
Log::debug($message);
break;
case LogType::info()->value:
Log::info($message);
break;
case LogType::log()->value:
Log::log($message);
break;
case LogType::warning()->value:
Log::warning($message);
break;
case LogType::error()->value:
Log::error($message);
break;
case LogType::critical()->value:
Log::critical($message);
break;
default:
throw new Exception(sprintf('%s : unhandled log type %s', get_class($this), $logtype->label));
}//end switch
$this->consolePrint($message, $logtype);
}
/**
* Prints to the console
*
* @param string $message
* @param LogType $logtype
* @return void
*/
protected function consolePrint(string $message, LogType $logtype): void
{
if ($this->console) {
switch ($logtype->value) {
case LogType::debug()->value:
case LogType::info()->value:
case LogType::log()->value:
$this->console->info($message);
break;
case LogType::warning()->value:
$this->console->comment($message);
break;
case LogType::error()->value:
case LogType::critical()->value:
$this->console->error($message);
break;
default:
throw new Exception(sprintf('%s : unhandled log type %s', get_class($this), $logtype->label));
}
}
}
}

523
app/Jobs/XmlIncoReader.php Normal file
View file

@ -0,0 +1,523 @@
<?php
namespace App\Jobs;
use App\Exceptions\AbortXmlImportException;
use App\Models\XmlInco;
use SimpleXMLElement;
class XmlIncoReader extends XmlJobs
{
protected $disk = 'inco';
protected $imagesPath = 'images';
protected $xsdFile = 'xsd/incollector.xsd';
/**
* éléments à la racine : nom du noeud
* sous éléments d'un noeud PARENT :
* d'abord:
* 'PARENT' => 'parent', // Consider this sub array
* puis:
* "PARENT.LEAF" => 'leaf', // extract this data in the line
* Note : Xmljobs iterates the PARENT with RecursiveIteratorIterator::LEAVES_ONLY
* intermediate nodes are therefore ignored
* example : nodes : <EXPLOITANT><SOCIETE><ID_SOCIETE /><NOM_SOCIETE /></SOCIETE></EXPLOITANT>
* how to extract leaves :
* 'EXPLOITANT' => 'exploitant', // Consider this sub array
* 'EXPLOITANT.ID_SOCIETE' => 'id_societe',
* 'EXPLOITANT.NOM_SOCIETE' => 'nom_societe',
*/
protected $takeColumns = [
'NO_ART' => 'id', // primary key on model
//this wil not return anything cf doc above
'ID_SOCIETE' => 'id_societe',
'NOM_SOCIETE' => 'nom_societe',
'PROD_SAISON' => 'prod_saison',
'NOM' => 'nom',
'NUMERO' => 'numero',
'TAILLE_REF' => 'taille_ref', //$poidsRef censé être dans INCO_COMPO (et généralement 100g)
'ALLEGATIONS' => 'allegations',
'CONSEIL_PREPA' => 'conseil_prepa',
'ASTUCES' => 'astuces',
'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
// list alergene
'LST_ALLERGENE' => 'lst_allergene', // Consider this sub array
'LST_ALLERGENE.LIB_ALLERGENE' => 'lib_allergene',
// Allegations
'ALLEGAT_SANTE' => 'allegat_sante',
'ALLEGAT_NUTRIT' => 'allegat_nutrit',
'ALLEGAT_AUTRE' => 'allegat_autre',
'INCO_COMPO' => 'inco_compo', // 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
'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',
'INCO_COMPO.MAT_GRASS_NUT' => 'mat_grass_nut',
'INCO_COMPO.AGS_R' => 'ags_r',
'INCO_COMPO.AGS_NUT' => 'ags_nut',
'INCO_COMPO.GLUCIDES_R' => 'glucides_r',
'INCO_COMPO.GLUCIDES_NUT' => 'glucides_nut',
'INCO_COMPO.SUCRE_NUT' => 'sucre_nut',
'INCO_COMPO.SUCRE_R' => 'sucre_r',
'INCO_COMPO.FIB_ALIM_NUT' => 'fib_alim_nut',
'INCO_COMPO.FIB_ALIM_R' => 'fib_alim_r',
'INCO_COMPO.PROTEINE_R' => 'proteine_r',
'INCO_COMPO.PROTEINE_NUT' => 'proteine_nut',
'INCO_COMPO.SEL_R' => 'sel_r',
'INCO_COMPO.SEL_NUT' => 'sel_nut',
'INCO_COMPO.SODIUM_R' => 'sodium_r',
'INCO_COMPO.ENER_E14_R' => 'ener_e14_r',
'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
'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',
'INCO_COMPO.FATSAT_R' => 'fatsat_r',
'INCO_COMPO.POURCENT_APPORT_REF_FATSAT_R' => 'pourcent_apport_ref_fatsat_r',
'INCO_COMPO.CHOVAL_R' => 'choval_r',
'INCO_COMPO.POURCENT_APPORT_REF_CHOVAL_R' => 'pourcent_apport_ref_choval_r',
'INCO_COMPO.SUGAR_R' => 'sugar_r',
'INCO_COMPO.POURCENT_APPORT_REF_SUGAR_R' => 'pourcent_apport_ref_sugar_r',
'INCO_COMPO.FIBTG_R' => 'fibtg_r',
'INCO_COMPO.POURCENT_APPORT_REF_FIBTG_R' => 'pourcent_apport_ref_fibtg_r',
'INCO_COMPO.PRO_R' => 'pro_r',
'INCO_COMPO.POURCENT_APPORT_REF_PRO_R' => 'pourcent_apport_ref_pro_r',
'INCO_COMPO.SALTEQ_R' => 'salteq_r',
'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
'INCO_COMPO.VAL_ENERG_KJ_P' => 'val_energ_kj_p', //_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',
'INCO_COMPO.GLUCIDES_P' => 'glucides_p',
'INCO_COMPO.SUCRE_P' => 'sucre_p',
'INCO_COMPO.FIB_ALIM_P' => 'fib_alim_p',
'INCO_COMPO.PROTEINE_P' => 'proteine_p',
'INCO_COMPO.SODIUM_P' => 'sodium_p',
'INCO_COMPO.FRUITS_P' => 'fruits_p',
'INCO_COMPO.SEL_P' => 'sel_p',
'INCO_COMPO.FRUITS_COQUE_SECH_P' => 'fruits_coque_sech_p',
'INCO_COMPO.ENER_E14_P' => 'ener_e14_p',
'INCO_COMPO.POURCENT_APPORT_REF_E14_P' => 'pourcent_apport_ref_e14_p',
'INCO_COMPO.ENER_KJO_P' => 'ener_kjo_p',
'INCO_COMPO.POURCENT_APPORT_REF_KJO_P' => 'pourcent_apport_ref_kjo_p',
'INCO_COMPO.FAT_P' => 'fat_p',
'INCO_COMPO.POURCENT_APPORT_REF_FAT_P' => 'pourcent_apport_ref_fat_p',
'INCO_COMPO.FATSAT_P' => 'fatsat_p',
'INCO_COMPO.POURCENT_APPORT_REF_FATSAT_P' => 'pourcent_apport_ref_fatsat_p',
'INCO_COMPO.CHOVAL_P' => 'CHOVAL_P',
'INCO_COMPO.POURCENT_APPORT_REF_CHOVAL_P' => 'pourcent_apport_ref_choval_p',
'INCO_COMPO.SUGAR_P' => 'SUGAR_P',
'INCO_COMPO.POURCENT_APPORT_REF_SUGAR_P' => 'pourcent_apport_ref_sugar_p',
'INCO_COMPO.FIBTG_P' => 'fibtg_p',
'INCO_COMPO.POURCENT_APPORT_REF_FIBTG_P' => 'pourcent_apport_ref_fibtg_p',
'INCO_COMPO.PRO_P' => 'pro_p',
'INCO_COMPO.POURCENT_APPORT_REF_PRO_P' => 'pourcent_apport_ref_pro_p',
'INCO_COMPO.SALTEQ_P' => 'salteq_p',
'INCO_COMPO.POURCENT_APPORT_REF_SALTEQ_P' => 'pourcent_apport_ref_salteq_p',
'INCO_COMPO.APPORT_PORT' => 'apport_port',
'INCO_COMPO.PART_P' => 'part_p',
'INCO_COMPO.PART_G' => 'part_g',
'INCO_COMPO.PART_L' => 'part_l',
'INCO_COMPO.P_SUR_L' => 'p_sur_l',
'INCO_COMPO.POURCENT_AGS' => 'pourcent_ags',
'INCO_COMPO.POURCENT_AGI' => 'pourcent_agi',
'INCO_COMPO.POURCENT_SS' => 'pourcent_ss',
'INCO_COMPO.POURCENT_AMIDON' => 'pourcent_amidon',
'INCO_COMPO.COMPOSANTE_N' => 'composante_n',
'INCO_COMPO.COMPOSANTE_P' => 'composante_p',
'INCO_COMPO.POURCENT_FRUITS' => 'pourcent_fruits',
'INCO_COMPO.SCORE_NUTRI' => 'score_nutri', //du coup nutriscore ?
'INCO_COMPO.CLASSEMENT' => 'classement', //$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',
'INCO_COMPO.SUCRE_R_PREC' => 'sucre_r_prec',
'INCO_COMPO.FIB_ALIM_R_PREC' => 'fib_alim_r_prec',
'INCO_COMPO.PROTEINE_R_PREC' => 'proteine_r_prec',
'INCO_COMPO.SEL_R_PREC' => 'sel_r_prec',
'INCO_COMPO.SODIUM_R_PREC' => 'sodium_r_prec',
'INCO_COMPO.FRUITS_R_PREC' => 'fruits_r_prec',
'INCO_COMPO.FRUITS_COQUE_SECH_R_PREC' => 'fruits_coque_sech_r_prec',
];
protected $nullableFields = [
'taille_ref',
'conseil_prepa',
'typ_portion',
'nb_portion',
'descriptif',
'prod_saison',
'allegations',
'astuces', 'arg_vente',
'pds_moy_theo',
'lib_conso', 'lst_ing', 'lst_allergene',
'allegat_sante', 'allegat_nutrit', 'allegat_autre',
'inco_compo',
'inco_compo.nom',
'inco_compo.numero',
'inco_compo.taille',
'inco_compo.taille_ref',
'inco_compo.val_energ_kj_nut',
'inco_compo.val_energ_kj_r',
'inco_compo.val_energ_kcal_r',
'inco_compo.val_energ_kcal_nut',
'inco_compo.mat_grass_r',
'inco_compo.mat_grass_nut',
'inco_compo.ags_r',
'inco_compo.ags_nut',
'inco_compo.glucides_r',
'inco_compo.glucides_nut',
'inco_compo.sucre_nut',
'inco_compo.sucre_r',
'inco_compo.fib_alim_nut',
'inco_compo.fib_alim_r',
'inco_compo.proteine_r',
'inco_compo.proteine_nut',
'inco_compo.sel_r',
'inco_compo.sel_nut',
'inco_compo.sodium_r',
'inco_compo.ener_e14_r',
'inco_compo.fruits_r',
'inco_compo.fruits_coque_sech_r',
'inco_compo.pourcent_apport_ref_e14_r',
'inco_compo.ener_kjo_r',
'inco_compo.pourcent_apport_ref_kjo_r',
'inco_compo.pourcent_apport_ref_fat_r',
'inco_compo.fatsat_r',
'inco_compo.pourcent_apport_ref_fatsat_r',
'inco_compo.choval_r',
'inco_compo.pourcent_apport_ref_choval_r',
'inco_compo.sugar_r',
'inco_compo.pourcent_apport_ref_sugar_r',
'inco_compo.fibtg_r',
'inco_compo.pourcent_apport_ref_fibtg_r',
'inco_compo.pro_r',
'inco_compo.pourcent_apport_ref_pro_r',
'inco_compo.salteq_r',
'inco_compo.pourcent_apport_ref_salteq_r',
'inco_compo.apport_ref',
'inco_compo.taille_port',
'inco_compo.val_energ_kj_p',
'inco_compo.val_energ_kcal_p',
'inco_compo.mat_grass_p',
'inco_compo.ags_p',
'inco_compo.glucides_p',
'inco_compo.sucre_p',
'inco_compo.fib_alim_p',
'inco_compo.proteine_p',
'inco_compo.sodium_p',
'inco_compo.fruits_p',
'inco_compo.sel_p',
'inco_compo.fruits_coque_sech_p',
'inco_compo.ener_e14_p',
'inco_compo.pourcent_apport_ref_e14_p',
'inco_compo.ener_kjo_p',
'inco_compo.pourcent_apport_ref_kjo_p',
'inco_compo.fat_p',
'inco_compo.pourcent_apport_ref_fat_p',
'inco_compo.fatsat_p',
'inco_compo.pourcent_apport_ref_fatsat_p',
'inco_compo.choval_p',
'inco_compo.pourcent_apport_ref_choval_p',
'inco_compo.sugar_p',
'inco_compo.pourcent_apport_ref_sugar_p',
'inco_compo.fibtg_p',
'inco_compo.pourcent_apport_ref_fibtg_p',
'inco_compo.pro_p',
'inco_compo.pourcent_apport_ref_pro_p',
'inco_compo.salteq_p',
'inco_compo.pourcent_apport_ref_salteq_p',
'inco_compo.apport_port',
'inco_compo.part_p',
'inco_compo.part_g',
'inco_compo.part_l',
'inco_compo.p_sur_l',
'inco_compo.pourcent_ags',
'inco_compo.pourcent_agi',
'inco_compo.pourcent_ss',
'inco_compo.pourcent_amidon',
'inco_compo.composante_n',
'inco_compo.composante_p',
'inco_compo.pourcent_fruits',
'inco_compo.score_nutri',
'inco_compo.classement',
'inco_compo.mat_grass_r_prec',
'inco_compo.ags_r_prec',
'inco_compo.glucides_r_prec',
'inco_compo.sucre_r_prec',
'inco_compo.fib_alim_r_prec',
'inco_compo.proteine_r_prec',
'inco_compo.sel_r_prec',
'inco_compo.sodium_r_prec',
'inco_compo.fruits_r_prec',
'inco_compo.fruits_coque_sech_r_prec'
];
protected $uniqueKeyField = ['id'];
protected $mergeColumns = [
'allegations' => [
'allegat_sante',
'allegat_nutrit',
'allegat_autre'
],
'nutritional_values' => [
'taille_ref',
'val_energ_kj_nut',
'val_energ_kj_r',
'val_energ_kcal_r',
'val_energ_kcal_nut',
'mat_grass_r',
'mat_grass_nut',
'ags_r',
'ags_nut',
'glucides_r',
'glucides_nut',
'sucre_nut',
'sucre_r',
'fib_alim_nut',
'fib_alim_r',
'proteine_r',
'proteine_nut',
'sel_r',
'sel_nut',
'sodium_r',
'ener_e14_r',
'fruits_r',
'fruits_coque_sech_r',
'pourcent_apport_ref_e14_r',
'ener_kjo_r',
'pourcent_apport_ref_kjo_r',
'pourcent_apport_ref_fat_r',
'fatsat_r',
'pourcent_apport_ref_fatsat_r',
'choval_r',
'pourcent_apport_ref_choval_r',
'sugar_r',
'pourcent_apport_ref_sugar_r',
'fibtg_r',
'pourcent_apport_ref_fibtg_r',
'pro_r',
'pourcent_apport_ref_pro_r',
'salteq_r',
'pourcent_apport_ref_salteq_r',
'apport_ref',
'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',
'ener_e14_p',
'pourcent_apport_ref_e14_p',
'ener_kjo_p',
'pourcent_apport_ref_kjo_p',
'fat_p',
'pourcent_apport_ref_fat_p',
'fatsat_p',
'pourcent_apport_ref_fatsat_p',
'choval_p',
'pourcent_apport_ref_choval_p',
'sugar_p',
'pourcent_apport_ref_sugar_p',
'fibtg_p',
'pourcent_apport_ref_fibtg_p',
'pro_p',
'pourcent_apport_ref_pro_p',
'salteq_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',
]
];
public function handle()
{
$this->parseData('INCO', XmlInco::class, function (SimpleXMLElement $xmlData, array &$line) {
// merge columns
foreach ($this->mergeColumns as $key => $values) {
$line[$key] = [];
// 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];
unset($line[$kValue]);
}
}, $values);
if ($key == 'allegations') {
$line[$key] = implode(',', array_filter($line[$key]));
$line[$key] = strlen($line[$key]) ? $line[$key] : null;
}
//json column cast into array in model => no need for encode
// if ($key == 'nutritional_values') {
// $line[$key] = json_encode($line[$key], \JSON_FORCE_OBJECT);
// }
}
// Boolean cast prod_saison
$line['prod_saison'] = isset($line['prod_saison']) ?
(strtolower($line['prod_saison']) == 'non' ? false : true) : false;
$arImgPaths = $this->parseImage($xmlData);
$line['picture'] = $arImgPaths;
});
}
private function parseImage(SimpleXMLElement $xmlData)
{
$images = $xmlData->INCO->MEDIA_VISUEL_EAI->MEDIA_VISUEL_EAI;
$arImgPaths = array();
$index = 1;
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(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));
}
$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
}
}
}
return $arImgPaths;
}
/**
* Recursive extract to array
* introduce sort of Hook to transform
* <TYP_PORTION>
* <code><![CDATA[2]]></code>
* <libelle><![CDATA[Approximatif]]></libelle>
* </TYP_PORTION>
* extracted as "TYP_PORTION" => array:1 [
* 0 => array:2 [
* "code" => "2"
* "libelle" => "Approximatif"
* ]
* ]
* into this
* <TYP_PORTION><![CDATA[Approximatif]]></TYP_PORTION>
* extracted as "TYP_PORTION" => "Approximatif"
*
*
* @param \SimpleXMLElement|false $sxe
* @return array
*/
protected function extract($sxe = null): array
{
if (!$sxe instanceof SimpleXMLElement) {
return array();
}
$extract = array();
foreach ($sxe->children() as $key => $value) {
if ($value->count()) {
$extraction = $this->extract($value);
if(count($extraction)
&& array_key_exists("code",$extraction)
&& array_key_exists("libelle",$extraction)) {
$val = strval($extraction['libelle']);
$extract[$key] = strlen($val) ? $val : null;
continue;
}
$extract[$key][] = $extraction; //$this->extract($value);
continue;
}
$value = strval($value);
if (array_key_exists($key, $extract) and is_array($extract[$key])) {
$extract[$key][] = strlen($value) ? $value : null;
} else {
$extract[$key] = strlen($value) ? $value : null;
}
}
return $extract;
}
}

View file

@ -1,31 +1,38 @@
<?php
namespace App\Jobs\Arcadia;
namespace App\Jobs;
use App\Enums\LogType;
use App\Exceptions\AbortXmlImportException;
use App\Exceptions\InvalidXmlException;
use App\Lib\Utils;
use ErrorException;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
use SimpleXMLElement;
use Throwable;
use TypeError;
abstract class XmlJobs extends XArcadiaFlux
{
/**
* The xsd file path used to validate files
* invalid xml folder
*
* @var string|null
* @var string
*/
protected $xsdSchemaPath = null;
protected $invalidFolderName = 'invalid';
/**
* The xsd file content used to validate files
* xsd file location
*
* @var string|null
* @var string
*/
protected $xsdSchema = null;
protected $xsdFile = '';
/**
* The columns to take from the XML
@ -50,117 +57,83 @@ abstract class XmlJobs extends XArcadiaFlux
*/
protected $encapsulate = false;
/**
* Unique keys for updating rows
*
* @var array
*/
protected $uniqueKeyField = [];
/**
* Parse Data from the XML and gives it to the callback
*
* @param string $entityName
* @param string $modelClass
* @param callable $afterParseCallback After parse is done.
* @param callable $saveCallback Instead of updateOrCreate, execute $saveCallback.
* @param callable $afterSaveCallback After save is done.
* @param string $entityName
* @param string $modelClass
* @return void
* @throws \App\Exceptions\InvalidXmlException If xml file is not valid.
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
*/
protected function parseData(
string $entityName,
string $modelClass,
callable $afterParseCallback = null,
callable $saveCallback = null,
callable $afterSaveCallback = null
): void {
// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
protected function parseData(string $entityName, string $modelClass, callable $callback = null): void
{
/** @var Filesystem */
$disk = $this->getDisk();
$this->log('Démarrage du traitement');
$this->assertXsdIsHereIfAny();
$files = $disk->files();
$total = count(array_filter($files, function ($filename) {
$progress = 1;
$total = count(array_filter($files, function ($filename) {
if ($this->fileHasToBeIgnored($filename)) {
return false;
}
return true;
}));
if (count($files)) {
$this->startProgressBar($total);
}
foreach ($files as $filename) {
if ($this->fileHasToBeIgnored($filename)) {
continue;
}
$this->incrementProgressBar();
$this->log(sprintf(' traitement %d sur %s ', $progress++, $total));
try {
/** @var string $data */
$data = $disk->get($filename);
$this->validateXmlFile($filename, $data);
try {
$this->validateSchema($data);
if ($this->encapsulate) {
$data = sprintf('<Root>%s</Root>', $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);
/** @var array<array<string,string>> */
$lines = [];
// * Unwrap if neceessary.
$data = isset($data[$entityName]) ? $data[$entityName] : $data;
foreach ($data as $traca) {
foreach ($data[$entityName] as $traca) {
$line = $this->processFields($traca);
if ($afterParseCallback) {
$afterParseCallback($xmlContent, $line);
if ($callback) {
$callback($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];
}
if (count($uniqueColumnsKey)) {
if ($saveCallback) {
$saveCallback($uniqueColumnsKey, $line);
} else {
forward_static_call_array(
[$modelClass, 'updateOrCreate'],
[$uniqueColumnsKey, $line]
);
}
if ($afterSaveCallback) {
$afterSaveCallback($xmlContent, $line);
}
}
} //end foreach
forward_static_call_array(
[$modelClass, 'updateOrCreate'],
[$uniqueColumnsKey, $line]
);
}
} 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',
@ -168,11 +141,24 @@ abstract class XmlJobs extends XArcadiaFlux
$message,
$e->getLine()
),
LogType::critical
LogType::critical()
);
$this->reportInvalidXmlSentryAlert($filename, $e);
$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(
@ -181,18 +167,17 @@ abstract class XmlJobs extends XArcadiaFlux
$e->getMessage(),
$e->getLine()
),
LogType::error
LogType::error()
);
report($e);
$this->sendErrorParseEmailToServiceWeb($e);
continue;
// * 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.
// ignore the file
} catch (FileNotFoundException | QueryException $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',
@ -200,34 +185,16 @@ abstract class XmlJobs extends XArcadiaFlux
$message,
$e->getLine()
),
LogType::debug
LogType::debug()
);
continue;
// * 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
// ignore the file
}//end try
$this->moveToArchives($filename);
} //end foreach
}//end foreach
// Clear old invalids.
$this->clearOldInvalids();
// Clear old archives.
// Clear old archives
$this->clearOldArchives();
$this->endProgressBar();
}
/**
@ -238,7 +205,7 @@ abstract class XmlJobs extends XArcadiaFlux
*/
protected function extract($sxe = null): array
{
if (!$sxe instanceof \SimpleXMLElement) {
if (!$sxe instanceof SimpleXMLElement) {
return array();
}
$extract = array();
@ -262,7 +229,7 @@ abstract class XmlJobs extends XArcadiaFlux
*
* @param array $fields
* @return array
* @throws AbortXmlImportException If a field could not be validated.
* @throws AbortXmlImportException
*/
protected function processFields(array $fields): array
{
@ -275,8 +242,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)) {
@ -294,7 +261,7 @@ abstract class XmlJobs extends XArcadiaFlux
} else {
$result[$key] = $field;
}
} //end foreach
}//end foreach
return $result;
}
@ -339,8 +306,24 @@ abstract class XmlJobs extends XArcadiaFlux
*/
protected function fileHasToBeIgnored(string $filename): bool
{
return in_array($filename, $this->ignoreFiles) or
!$this->checkExtension($filename, 'xml');
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);
}
}
/**
@ -358,78 +341,77 @@ abstract class XmlJobs extends XArcadiaFlux
return $isXml;
}
/**
* Report Invalid Xml using sentry
*
* @param string $filename
* @param \Throwable $e
* @return void
*/
protected function reportInvalidXmlSentryAlert(string $filename, \Throwable $e)
protected function sendErrorParseEmailToServiceWeb(Throwable $e)
{
\report(new \RuntimeException(
sprintf(
Utils::sendEmailErrorManually($e);
}
protected function sendInvalidXmlEmail(string $filename, Throwable $e)
{
$message = $e->getPrevious()->getMessage();
Mail::send('emails.invalidxml', [
'strError' => sprintf(
'File %s could not be parsed => %s on line %d',
$filename,
$e->getPrevious()->getMessage(),
$message,
$e->getLine()
),
$e->getCode(),
$e
));
'appName' => config('app.name'),
'filename' => $filename,
'exception' => $e->getPrevious()
], function ($m) {
$m->to(
config('mail.contactsList.invalidInco'),
sprintf('%s Exception Handler', config('app.name'))
)->subject('You sent me an invalid XML file, please check its validity before resend ASAP.');
});
}
/**
* Assert XSD is readable if configured
* Move the file to invalid folder
*
* @param string $filename
* @return void
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException If xsd file cant be read.
* @throws \RuntimeException If schame is empty.
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/
private function assertXsdIsHereIfAny()
protected function moveToInvalid(string $filename): void
{
// phpcs:enable
if (!$this->xsdSchemaPath) {
$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 (empty($this->xsdSchema = File::get($this->xsdSchemaPath))) {
throw new \RuntimeException('xsdSchema is empty');
};
}
/**
* Validate Xml file against XSD if loaded
*
* @param string $fileName
* @param string $fileContent
* @return void
* @throws \App\Exceptions\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) {
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
);
}
$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."
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) {
if (\get_class($e) === InvalidXmlException::class) {
throw $e;
}
throw new InvalidXmlException(
"XML Validation $fileName content could not be validated against xsd.",
0,
$e
} catch (Exception $e) {
$this->log(
sprintf(
'File %s could not be moved to invalid',
$filename
),
LogType::warning()
);
}
}

View file

@ -1,184 +0,0 @@
<?php
namespace App\Lib;
use App\Exceptions\Api\ApplicationException;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
/**
* Api Helper Class
*/
class ApiHelper
{
/**
* Package infos
*
* @var array
*/
private static $packageInfos;
/**
* Tells if the request is within api routes.
*
* @param \Illuminate\Http\Request $request
* @return boolean
*/
public static function isRequestForApi(Request $request): bool
{
// * Small hack to ensure to know if route is From APIv2.
$routeName = static::getRouteName($request) ??
(strpos($request->path(), 'api') === 0 ? 'api.' : null) ?? 'none';
return strpos($routeName, 'api.') === 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(
#[\SensitiveParameter] 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
}
}

View file

@ -1,64 +0,0 @@
<?php
namespace App\Lib\Helpers;
use Illuminate\Container\Container;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
class CollectionHelper
{
/**
* Paginate a collection
*
* @param \Illuminate\Support\Collection $results
* @param integer $pageSize
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public static function paginate(Collection $results, int $pageSize): LengthAwarePaginator
{
$page = Paginator::resolveCurrentPage('page');
return self::paginateManually($results, $pageSize, $page);
}
/**
* Paginate a collection
*
* @param \Illuminate\Support\Collection $results
* @param integer $pageSize
* @param integer $page
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public static function paginateManually(Collection $results, int $pageSize, int $page = 1): LengthAwarePaginator
{
$total = $results->count();
return self::paginator($results->forPage($page, $pageSize), $total, $pageSize, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => 'page',
]);
}
/**
* Create a new length-aware paginator instance.
*
* @param \Illuminate\Support\Collection $items
* @param integer $total
* @param integer $perPage
* @param integer $currentPage
* @param array $options
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected static function paginator(Collection $items, int $total, int $perPage, int $currentPage, array $options)
{
return Container::getInstance()->makeWith(LengthAwarePaginator::class, [
'items' => $items,
'total' => $total,
'perPage' => $perPage,
'currentPage' => $currentPage,
'options' => $options
]);
}
}

View file

@ -1,190 +0,0 @@
<?php
namespace App\Lib\Helpers;
class ExceptionsHelper
{
/**
* Handle sentry event
*
* @param \Sentry\Event $event
* @return null|\Sentry\Event
*/
public static function sentryEventHandler(\Sentry\Event $event): ?\Sentry\Event
{
// Remove unwanted reports.
$event->setModules([]);
$event->setOsContext(null);
/** @var \Sentry\ExceptionDataBag[] */
$exceptionBag = [];
foreach ($event->getExceptions() as $exceptionDataBag) {
$exceptionBag[] = self::getAnonymizedSentryException($exceptionDataBag);
}
$event->setExceptions($exceptionBag);
return $event;
}
/**
* Anonimize Sentry Exception bag
*
* @param \Sentry\ExceptionDataBag $exceptionDataBag
* @return \Sentry\ExceptionDataBag
*/
private static function getAnonymizedSentryException(
\Sentry\ExceptionDataBag $exceptionDataBag
): \Sentry\ExceptionDataBag {
$exceptionDataBag->setValue(self::getAnonymizedMessage($exceptionDataBag->getValue()));
if ($stackTrace = $exceptionDataBag->getStacktrace()) {
$exceptionDataBag->setStacktrace(self::getAnonymizedSentryStackTrace($stackTrace));
}
return $exceptionDataBag;
}
/**
* Anonymise sentry stacktrace
*
* @param \Sentry\Stacktrace $stackTrace
* @return \Sentry\Stacktrace
*/
private static function getAnonymizedSentryStackTrace(\Sentry\Stacktrace $stackTrace): \Sentry\Stacktrace
{
/** @var \Sentry\Frame[] */
$frames = \array_reverse($stackTrace->getFrames(), true);
$i = \count($frames) - 1;
// * Remove all but keep first (index.php).
while ($i-- > 0) {
$stackTrace->removeFrame(count($stackTrace->getFrames()) - 1);
}
foreach ($frames as $index => $frame) {
$frame->setPostContext([]);
$frame->setPreContext([]);
$frame->setContextLine('');
$frame->setVars(self::getAnonymizedSentryParams($frame->getVars()));
$stackTrace->addFrame($frame);
if ($index === 0) {
$stackTrace->removeFrame(count($stackTrace->getFrames()) - 1);
}
}
return $stackTrace;
}
/**
* Get anonymized sentry params
*
* @param mixed[] $params
* @return string[]
*/
private static function getAnonymizedSentryParams(array $params): array
{
foreach ($params as &$param) {
$type = \strtolower(\gettype($param));
if (\is_string($param)) {
$type = \explode(' ', \trim(strtolower($param)))[0] ?? null;
}
switch (true) {
case $type === 'lambda':
case $type === 'object':
continue 2;
case $type === 'array' and \is_array($param):
$param = sprintf('Array(%d)', \count($param));
continue 2;
default:
if (\is_numeric($param)) {
$param = \ucfirst($type);
continue 2;
}
if (\is_string($param)) {
$param = sprintf('String(%d)', \strlen($param));
continue 2;
}
} //end switch
} //end foreach
return $params;
}
/**
* Anonymize at least Sql queries
*
* @param string $message
* @return string
*/
public static function getAnonymizedMessage(string $message): string
{
$message = \preg_replace('/^(.*)( \(SQL:.*\))$/', '$1', $message) ?? $message;
if ($sqlPos = \strpos($message, 'to use near')) {
$message = \mb_strcut($message, 0, $sqlPos);
}
return $message;
}
/**
* Anonymize stack trace
*
* @param \Throwable $e
* @return string
*/
public 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'] ?? ''
);
if (isset($stack['class'])) {
$traceStr .= "{$stack['class']}->{$stack['function']}(";
if (isset($stack['args'])) {
$traceStr .= self::handleArgs($stack);
}
$traceStr .= ")\n";
} elseif (isset($stack['function'])) {
$traceStr .= "{$stack['function']}(";
if (isset($stack['args'])) {
$traceStr .= self::handleArgs($stack);
}
$traceStr .= ")\n";
}
} //end foreach
$maxStackStage++;
$traceStr .= "#$maxStackStage {main}";
return $traceStr;
}
/**
* Handle Args
*
* @param array $stack
* @return string
*/
public static function handleArgs(array $stack): string
{
$traceArgs = '';
$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) ? ',' : '');
}
return $traceArgs;
}
}

View file

@ -1,454 +0,0 @@
<?php
namespace App\Lib\Helpers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* This class is used to handle files in LDC Laravel stack
*
* * The storage strategy will be using date.
* The file name may contain sufix timestamp highresolution time (hrtime or microtime) if is duplicate.
* (function() { return time() . ' ' . Str::slug(microtime(true)) . ' ' . hrtime(true); })();
*
* posts/ - The model table name
* 10/ - Year
* 04/ - Month
* 19/ - Day
* 5f/ - First two letters of md5 file
* black-cat-under-red-car.png - Slugified file name
* black-cat-under-red-car-286071690833400.png - Slugified file name that is duplicated
*
* The final path will be storage/modelfiles/posts/10/04/19/5f/black-cat-under-red-car-286071690833400.png
*
* ! No limits of file per folder will be handled.
*
* @author Munsch Jérémy <jeremy.munsch@ldc.fr>
*/
class FileStorageHelper
{
/**
* Destroy the file associate with the attribute.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeFile(Model $model, string $attribute): void
{
$path = \ltrim(\urldecode($model->{$attribute}), '/');
if (\strpos($path, 'storage') !== 0) {
return;
}
$path = ToolboxHelper::mbReplace('storage/', '', $path);
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
/**
* Destroy the file associate with the attribute old value (before updated to DB).
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @param boolean $onlyIfUpdated
* @return void
*/
public static function removeOldFile(Model $model, string $attribute, bool $onlyIfUpdated = true): void
{
// * Ignore If removal has to be done only if the field has changed and it actually hasnt.
if ($onlyIfUpdated and $model->{$attribute} === ToolboxHelper::getOriginal($model, $attribute)) {
return;
}
$originalPath = \ltrim(\urldecode(ToolboxHelper::getOriginal($model, $attribute)), '/');
if (\strpos($originalPath, 'storage') !== 0) {
return;
}
$originalPath = ToolboxHelper::mbReplace('storage/', '', $originalPath);
if (Storage::disk('public')->exists($originalPath)) {
Storage::disk('public')->delete($originalPath);
}
}
/**
* Store a file using optimized storage strategy
*
* You can use filename param to overload file name.
*
* @param \Illuminate\Database\Eloquent\Model $model The model that will hold the picture.
* @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $file The file, will just return itself is string.
* @param boolean $slugify Force filename slugification.
* @param string|null $filename This string may be used to store the file.
* @param boolean $private To store the file in a private space.
* @return string
* @throws \RuntimeException If file copy fails.
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
*/
public static function storeFile(
Model $model,
$file,
bool $slugify = false,
string $filename = null,
bool $private = false
): string {
// phpcs:enable
$tableName = $model->getTable();
if (\is_object($file) and $file instanceof \Illuminate\Http\UploadedFile) {
/** @var \Illuminate\Http\UploadedFile $file */
$filename = $filename ?: $file->getClientOriginalName();
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
$newPath = self::prepareMoveFile($tableName, $filename, $private);
$newPath = str_replace(storage_path('app'), '', $newPath);
if (!$private) {
// * Store public file.
$file->storePubliclyAs(
pathinfo($newPath, \PATHINFO_DIRNAME),
pathinfo($newPath, \PATHINFO_BASENAME)
);
return 'storage' . str_replace('/public', '', $newPath);
}
$file->storeAs(
pathinfo($newPath, \PATHINFO_DIRNAME),
pathinfo($newPath, \PATHINFO_BASENAME)
);
return str_replace('/private/', '', $newPath);
} //end if
if (\is_object($file) and $file instanceof \SplFileInfo) {
/** @var \SplFileInfo $file */
$filename = $filename ?: $file->getBaseName();
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
$newPath = self::prepareMoveFile($tableName, $filename, $private);
if (!File::copy($file->getRealPath(), $newPath)) {
throw new \RuntimeException(sprintf(
'Failed to copy file %s to %s',
$file->getRealPath(),
$newPath
));
}
return $private ?
\ltrim(str_replace(\storage_path('app/private'), '/storage/app/private', $newPath), '/') :
\ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/');
}
return strval($file);
}
/**
* Store all files from string attribute html
* If oldHtmlString is passed, a diff is done with new and old files
* to keep only files that are present in the new html string.
*
* @param \Illuminate\Database\Eloquent\Model $model The model that will hold the picture.
* @param string $htmlString The html string to parse and modify files.
* @param string|null $oldHtmlString The old html string with alreadyParsed
* files using storeModelFilesFromHtml.
* @return string Html with files pathes replaced using storage strategy.
*/
public static function storeModelFilesFromHtml(
Model $model,
string $htmlString,
string $oldHtmlString = null
): string {
\collect(self::onlyUploadedFilesWithFileManager(self::findFilesInHtml($htmlString)))
->each(function (string $path) use ($model, &$htmlString) {
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$disk = Storage::disk('uploads');
$fileInfo = new \SplFileInfo($disk->path(
ToolboxHelper::mbReplace('/uploads/', '', \urldecode($path))
));
$htmlString = ToolboxHelper::mbReplace(
$path,
'/' . self::storeFile($model, $fileInfo, true),
$htmlString
);
});
if ($oldHtmlString) {
self::removeOldModelFilesFromHtml($htmlString, $oldHtmlString);
}
return $htmlString;
}
/**
* Remove all old files from string attribute html that are stored in public that
* are nove present anymore on new html string
*
* @param string $newHtmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
* @param string $oldHtmlString The old html string with alreadyParsed files using storeModelFilesFromHtml.
* @return void
*/
public static function removeOldModelFilesFromHtml(string $newHtmlString, string $oldHtmlString): void
{
self::removeOldFiles(self::findFilesInHtml($oldHtmlString), self::findFilesInHtml($newHtmlString));
}
/**
* Remove all old files that are stored in public that
* are not present anymore on new array files
*
* @param string[] $oldFiles The old array files.
* @param string[] $newFiles The new array files.
* @return void
*/
public static function removeOldFiles(array $oldFiles, array $newFiles): void
{
$oldFiles = \collect(self::onlyUploadedFilesWithFileStorageHelper($oldFiles));
$filesToKeep = \collect(self::onlyUploadedFilesWithFileStorageHelper($newFiles));
$filesToRemove = $oldFiles->diff($filesToKeep);
$filesToRemove->each(function (string $publicStorageFilePath) {
$path = ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($publicStorageFilePath), '/'));
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
});
}
/**
* Remove all files from string attribute html that are stored in public
*
* @param string $htmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
* @return void
*/
public static function removeModelFilesOfHtml(string $htmlString): void
{
$filesToRemove = \collect(self::onlyUploadedFilesWithFileStorageHelper(self::findFilesInHtml($htmlString)));
$filesToRemove->each(function (string $publicStorageFilePath) {
$path = ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($publicStorageFilePath), '/'));
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
});
}
/**
* Find all occurence of files in html
*
* @param string $html
* @return array
*/
public static function findFilesInHtml(string $html): array
{
preg_match_all('/href="(?P<href>.*)"|src="(?P<src>.*)"|url="(?P<url>.*)"/Um', $html, $m, PREG_SET_ORDER, 0);
return collect($m)->pluck('href')->filter()
->concat(collect($m)->pluck('url')->filter())
->concat(collect($m)->pluck('src')->filter())
->all();
}
/**
* Filter array to get only uploaded files that exists
* on disk 'public' (using helper storage strategy)
*
* @param array $urls
* @return array
*/
public static function onlyUploadedFilesWithFileStorageHelper(array $urls): array
{
return \collect($urls)->unique()
// * Filter to get only /uploads and using app url or empty
->filter(function (string $url) {
$path = \parse_url($url, \PHP_URL_PATH);
return \strpos(\ltrim($path, '/'), 'storage/modelfiles') === 0;
})->filter(function (string $url) {
$path = \parse_url($url, \PHP_URL_PATH);
return Storage::disk('public')
->exists(ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($path), '/')));
})->all();
}
/**
* Filter array to get only uploaded files that exists
* on disk 'uploads'
*
* @param array $urls
* @return array
*/
private static function onlyUploadedFilesWithFileManager(array $urls): array
{
return \collect($urls)->unique()
// * Filter to get only /uploads and using app url or empty
->filter(function (string $url) {
$host = \parse_url($url, \PHP_URL_HOST);
$path = \parse_url($url, \PHP_URL_PATH);
return ($host === \parse_url(config('app.url'), PHP_URL_HOST) or \is_null($host)) and
\strpos(\ltrim($path, '/'), 'uploads') === 0;
})->filter(function (string $url) {
$path = \parse_url($url, \PHP_URL_PATH);
return Storage::disk('uploads')
->exists(ToolboxHelper::mbReplace('uploads/', '', \ltrim(\urldecode($path), '/')));
})->all();
}
/**
* Prepare storage path and return
* the file full path
*
* @param string $tableName Model table name.
* @param string $filename The basename for the file.
* @param boolean $private May the file be stored in a private dir.
* @return string
*/
private static function prepareMoveFile(string $tableName, string $filename, bool $private = false): string
{
// * Security filter
$filename = \htmlspecialchars($filename);
// * Security filter + beautifier
$filename = self::sanitizeFileName($filename);
// * Slugify filename
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
(\strlen($ext) ? ".{$ext}" : '');
$folderPath = self::getStoragePath(
$private ? 'app/private/modelfiles' : 'app/public/modelfiles',
$tableName,
$filename,
$private
);
$filename = self::getFileUniqueName($folderPath, $filename);
return "{$folderPath}/{$filename}";
}
/**
* Create sub folders to store files using pattern
* storage/modelfiles/posts/10/04/19/5f
* year/month/day/md5 first 2 letters
*
* @param string $storage_dir
* @param string $table
* @param string $filename
* @return string
*/
private static function getStoragePath(string $storage_dir, string $table, string $filename): string
{
$filenameHash = \md5($filename);
$pathFolder = \storage_path(sprintf(
'%s/%s/%s/%s/%s/%s',
$storage_dir,
$table,
gmdate('y'),
gmdate('m'),
gmdate('d'),
substr($filenameHash, 0, 2)
));
if (!File::exists($pathFolder)) {
File::makeDirectory($pathFolder, 0755, true);
}
return $pathFolder;
}
/**
* Get unique filename if the path is already occupied
*
* @param string $folderPath
* @param string $filename
* @return string
* @throws \RuntimeException If a unique filename cannot be obtained.
*/
private static function getFileUniqueName(string $folderPath, string $filename): string
{
$attempts = 0;
$maxAttempts = 20;
while (File::exists("{$folderPath}/{$filename}")) {
if ($attempts++ >= $maxAttempts) {
throw new \RuntimeException("Failed to have a unique filename for {$folderPath}/{$filename}");
}
$filename = pathinfo($filename, \PATHINFO_FILENAME) .
\uniqid('-') . '.' .
pathinfo($filename, \PATHINFO_EXTENSION);
}
return $filename;
}
/**
* Slugify the filename
*
* @param string $filename
* @return string
*/
public static function slugifyFileName(string $filename): string
{
return Str::slug(pathinfo($filename, \PATHINFO_FILENAME)) . '.' . pathinfo($filename, \PATHINFO_EXTENSION);
}
/**
* Sanitize File name for uploaded files
*
* @param string $filename The original uploaded client file name.
* @param boolean $beautify Beautify the filename.
* @return string The sanitized file name.
*/
public static function sanitizeFileName(string $filename, bool $beautify = true): string
{
// * Sanitize filename
// phpcs:disable Generic.Files.LineLength.TooLong
$filename = preg_replace(
'~
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
[\x00-\x1F]| # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
[\x7F\xA0\xAD]| # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
[#\[\]@!$&\'()+,;=]| # URI reserved https://tools.ietf.org/html/rfc3986#section-2.2
[{}^\~`] # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
~x',
'-',
$filename
);
// phpcs:enable
// * Avoids '.' '..' or '.hiddenFiles' .
$filename = Str::of($filename)->ltrim('.-');
// * Optional beautification
if ($beautify) {
$filename = static::beautifyFilename($filename);
}
// * Maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
/** @var string */
$extension = pathinfo($filename, \PATHINFO_EXTENSION);
/** @var string */
$filename = pathinfo($filename, \PATHINFO_FILENAME);
$encoding = mb_detect_encoding($filename);
$encoding = $encoding ? $encoding : null;
return \mb_strcut(
$filename,
0,
255 - (!empty($extension) ? strlen($extension) + 1 : 0),
$encoding
) . (!empty($extension) ? ".$extension" : '');
}
/**
* Beautify a filename
*
* @param string $filename
* @return string
*/
private static function beautifyFilename(string $filename): string
{
// Reduce consecutive characters.
$filename = preg_replace([
// * "file name.zip" becomes 'file-name.zip'
'/ +/',
// * "file___name.zip" becomes 'file-name.zip'
'/_+/',
// * "file---name.zip" becomes 'file-name.zip'
'/-+/'
], '-', $filename);
$filename = preg_replace([
// * "file--.--.-.--name.zip" becomes "file.name.zip"
'/-*\.-*/',
// * "file...name..zip" becomes "file.name.zip"
'/\.{2,}/'
], '.', $filename);
// Lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 .
$filename = mb_strtolower($filename, mb_detect_encoding($filename));
// * ".file-name.-" becomes "file-name"
$filename = trim($filename, '.-');
return $filename;
}
}

View file

@ -1,601 +0,0 @@
<?php
namespace App\Lib\Helpers;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Finder\SplFileInfo;
class ToolboxHelper
{
/**
* Multibyte strtok
*
* @param string $delimiters
* @param string $str
* @return string|false
*/
public static function mbStrtok(string $delimiters, string $str = null): string|false
{
// * Keep track of the position on the string for each subsequent call.
static $pos = 0;
static $string = "";
// * If a new string is passed, reset the static parameters.
if ($str != null) {
$pos = 0;
$string = $str;
}
// * Initialize the token.
$token = "";
while ($pos < mb_strlen($string)) {
$char = mb_substr($string, $pos, 1);
$pos++;
if (mb_strpos($delimiters, $char) === false) {
$token .= $char;
} else {
// * Don't return empty strings.
if ($token != "") {
return $token;
}
}
}
// * Check whether there is a last token to return.
if ($token != "") {
return $token;
} else {
return false;
}
}
/**
* Multibyte sub str replace
*
* @param string $original
* @param string $replacement
* @param integer $position
* @param integer $length
* @return string
*/
public static function mbSubstrReplace(string $original, string $replacement, int $position, int $length): string
{
$startString = mb_substr($original, 0, $position, "UTF-8");
$endString = mb_substr($original, $position + $length, mb_strlen($original), "UTF-8");
return $startString . $replacement . $endString;
}
/**
* @param array<int|string, mixed> $array1
* @param array<int|string, mixed> $array2
*
* @return array<int|string, mixed>
*/
public static function arrayMergeRecursiveDistinct(array &$array1, array &$array2): array
{
$merged = $array1;
foreach ($array2 as $key => &$value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
} else {
$merged[$key] = $value;
}
}
return $merged;
}
/**
* Validate recaptcha
*
* @param \Illuminate\Http\Request $request
* @param boolean $isV3
* @return void
*/
public static function validateRecaptcha(Request $request, bool $isV3 = true)
{
Validator::validate($request->all(), [
'g-recaptcha-response' => [
'required', 'string',
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
function ($attribute, $value, $fail) use ($isV3) {
try {
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.google.recaptcha.private'),
'response' => \strval($value),
'remoteip' => app('clientIp')
]);
if ($e = $response->toException()) {
throw $e;
}
if ($isV3) {
if (
!$response->json('success') or
!$response->json('challenge_ts') or
Carbon::parse($response->json('challenge_ts'))->lessThan(\now('utc')->subMinutes(5)) or
$response->json('hostname') !== \parse_url(config('app.url'), \PHP_URL_HOST) or
!$response->json('score')
) {
throw new \RuntimeException();
}
if (\floatval($response->json('score')) < 0.8) {
$fail("Vous n'avez pas obtenu un score assez élevé lors de la validation du recaptcha");
}
return;
}
if (
!$response->json('success') or
!$response->json('challenge_ts') or
Carbon::parse($response->json('challenge_ts'))->lessThan(\now('utc')->subMinutes(5)) or
$response->json('hostname') !== \parse_url(config('app.url'), \PHP_URL_HOST)
) {
throw new \RuntimeException();
}
} catch (\Exception $e) {
$fail('Erreur lors de la validation du formulaire veuillez réssayer');
} //end try
}
]
], [], ['g-recaptcha-response' => 'reCaptcha']);
}
/**
* Strip scheme (https) on oembed urls
* Only embed allowed are for MP4 self hosted videos right now
*
* https is force by ckeditor MediaElements which natively supports
* only cloud hosted medias (youtube, spotify etc..)
*
* @param string $input
* @return string
*/
public static function stripCkEditorSchemesOnMediaInputs(string $input): string
{
$input = \preg_replace('/(?<=<oembed url=")(https:\/\/)(?=.*?">)/m', '', $input);
return $input;
}
/**
* Assert That no Back Office url is present on txt
*
* @param string $field
* @param string $text
* @return string
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
*/
public static function assertNoInternalBoUrls(string $field, string $text): string
{
$routes = Route::getRoutes();
// * Url replace and email replace.
$text = preg_replace_callback(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
function ($matches) use ($routes, $field) {
$match = collect($matches)->first() ?? '';
$url = $match;
try {
$request = Request::create($url);
// If a route is match then it do not throws exception.
if (
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
trim($routes->match($request)->getPrefix() ?? '', '/') === 'bo'
) {
// Removes the matched url.
throw ValidationException::withMessages([
$field => trans(
'crud.libs.toolbox.assertNoInternalBoUrls',
['attribute' => $field]
)
]);
}
return $match;
} catch (NotFoundHttpException $e) {
return $match;
}
},
$text
);
return $text;
}
/**
* Assert That only external url is present on txt
*
* @param string $field
* @param string $text
* @return string
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
*/
public static function assertNoInternalUrls(string $field, string $text): string
{
self::assertNoInternalBoUrls($field, $text);
$routes = Route::getRoutes();
// * Url replace and email replace.
$text = preg_replace_callback(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
function ($matches) use ($routes, $field) {
$match = collect($matches)->first() ?? '';
$url = $match;
try {
$request = Request::create($url);
// If a route is match then it do not throws exception.
if (
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
$routes->match($request)
) {
// Removes the matched url.
throw ValidationException::withMessages([
$field => trans(
'crud.libs.toolbox.assertNoInternalUrls',
['attribute' => $field, 'appname' => config('app.name')]
)
]);
}
return $match;
} catch (NotFoundHttpException $e) {
return $match;
}
},
$text
);
return $text;
}
/**
* Mutltibyte string replace
*
* @param array|string $search
* @param array|string $replace
* @param array|string $subject
* @param integer $count
* @return array|string
*/
public static function mbReplace(
array|string $search,
array|string $replace,
array|string $subject,
int &$count = 0
): array|string {
if (!is_array($search) && is_array($replace)) {
return false;
}
if (is_array($subject)) {
// Call mb_replace for each single string in $subject .
foreach ($subject as &$string) {
$string = &self::mbReplace($search, $replace, $string, $count);
}
} elseif (is_array($search)) {
if (!is_array($replace)) {
foreach ($search as &$string) {
$subject = self::mbReplace($string, $replace, $subject, $count);
}
} else {
$n = max(count($search), count($replace));
while ($n--) {
$subject = self::mbReplace(current($search), current($replace), $subject, $count);
next($search);
next($replace);
}
}
} else {
$parts = mb_split(preg_quote($search), $subject);
$count = count($parts) - 1;
$subject = implode($replace, $parts);
} //end if
return $subject;
}
/**
* Asserts that the field is unique
*
* @param string $table
* @param string $field
* @param string|integer|float|boolean|null|array $value
* @param integer|null $id
* @param callable|null $query
* @return void
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
*/
public static function assertFieldIsUnique(
string $table,
string $field,
string|int|float|bool|null|array $value,
?int $id = null,
?callable $query = null
) {
if (!$query) {
Validator::make([$field => $value], [
self::mbReplace('.', '\.', $field) => Rule::unique($table, $field)->ignore($id),
], [
self::mbReplace('.', '\.', "{$field}.unique")
=> trans('crud.libs.toolbox.assertFieldIsUnique', ['attribute' => $field, 'value' => $value])
])->validate();
return;
}
Validator::make([$field => $value], [
$field => Rule::unique($table)->where($query)->ignore($id),
], [
$field . '.unique' => trans(
'crud.libs.toolbox.assertFieldIsUnique',
['attribute' => $field, 'value' => $value]
)
])->validate();
}
/**
* Get original polyfill with autotranslate behavior
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return mixed
*/
public static function getOriginal(Model $model, string $attribute): mixed
{
if (\in_array('Spatie\Translatable\HasTranslations', \class_uses_recursive($model))) {
return $model->getTranslation(
$attribute,
$model->getLocale(),
$model->useFallbackLocale()
);
}
return $model->getOriginal($attribute);
}
/**
* Dynamic model query
*
* @param string $defaultLocale
* @param \Illuminate\Contracts\Database\Eloquent\Builder $q
* @param string $column
* @param string $value
* @return \Illuminate\Contracts\Database\Eloquent\Builder
*/
public static function queryColumnWithLocales(
string $defaultLocale,
Builder $q,
string $column,
string $value
): Builder {
$currentLocale = app()->currentLocale();
$locales = collect(config('app.locales'))
->mapWithKeys(fn (string $locale) => [$locale => $locale]);
if (!$locales->has($defaultLocale)) {
// * Remove default locale.
$locales->pull($defaultLocale);
}
if (!$locales->has($currentLocale)) {
// * Remove current locale.
$locales->pull($currentLocale);
}
return $q->where(function (Builder $q) use ($defaultLocale, $currentLocale, $locales, $column, $value) {
// * Query default locale
$q->whereRaw("JSON_EXTRACT({$column}, '$.{$defaultLocale}') = '{$value}'");
if ($defaultLocale !== $currentLocale) {
// * Query current locale
$q->whereRaw("JSON_EXTRACT({$column}, '$.{$currentLocale}') = '{$value}'");
}
// * Query any other locale
$locales->each(fn ($locale) => $q->orWhereRaw(
"JSON_EXTRACT({$column}, '$.{$locale}') = '{$value}'"
));
});
}
/**
* Generates an array from a dot separated string
*
* @param string $path
* @param mixed $value
* @param string $separator
* @return array
*/
public static function dotStringToNestedArray(string $path, mixed $value, string $separator = '.'): array
{
$arr = $value;
$keys = array_reverse(explode($separator, $path));
foreach ($keys as $key) {
$arr = [$key => $arr];
}
return $arr;
}
/**
* Convert deeply an object to array
*
* @param array|object $input
* @return array
* @throws \RuntimeException If $input var is a scalar value.
*/
public static function objectToArray($input): array
{
// Only process if it's an object or array being passed to the function.
if (is_object($input) or is_array($input)) {
$ret = (array)$input;
foreach ($ret as &$item) {
// Recursively process EACH element only if not scalar.
$item = (is_object($item) or is_array($item)) ?
self::objectToArray($item) : $item;
}
return $ret;
} else {
throw new \RuntimeException('Cannot handle $input of type ' . \gettype($input));
}
}
/**
* Convert deeply an array to object
*
* @param array|object $input
* @return object
* @throws \RuntimeException If $input var is a scalar value.
*/
public static function arrayToObject($input): object
{
// Only process if it's an object or array being passed to the function.
if (is_object($input) or is_array($input)) {
$ret = (object)$input;
foreach ($ret as &$item) {
// Recursively process EACH element only if not scalar.
$item = (is_object($item) or is_array($item)) ?
self::arrayToObject($item) : $item;
}
return $ret;
} else {
throw new \RuntimeException('Cannot handle $input of type ' . \gettype($input));
}
}
/**
* Validate picture
*
* @param string $attribute
* @param string|null $value
* @param callback $fail
* @param object $sizes
* @return void
*/
public static function validatePicture(string $attribute, string|null $value, callable $fail, object $sizes)
{
if (!($useStorage = Storage::exists($value)) and !File::exists($value)) {
$fail(trans(':attribute n\'existe pas dans le système de fichier', [
'attribute' => $attribute
]));
return;
}
/** @var string */
$fileFullPath = self::getUploadedFileFullPath($value, $useStorage);
if (
!\in_array(self::getUploadedFileMimeType($value, $useStorage), [
'image/jpeg', 'image/jpg', 'image/png', 'image/gif'
])
) {
$fail(trans(':attribute doit être une image de type \'image/jpeg,image/jpg,image/png,image/gif\'', [
'attribute' => $attribute
]));
}
if (!($dimensions = \getimagesize($fileFullPath)) or !isset($dimensions[0]) or !isset($dimensions[1])) {
$fail(trans(':attribute impossible de valider les dimensions', [
'attribute' => $attribute
]));
} else {
$width = intval($dimensions[0]);
$height = intval($dimensions[1]);
$xMessage = trans(
":attribute la largeur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px"
);
if ($sizes->minWith === $sizes->maxWith) {
$xMessage = trans(":attribute la largeur doit être de {$sizes->minWith}px");
}
$yMessage = trans(
":attribute la hauteur doit être comprise entre {$sizes->minWith}px et {$sizes->maxWith}px"
);
if ($sizes->minHeight === $sizes->maxHeight) {
$yMessage = trans(":attribute la hauteur doit être de {$sizes->minWith}px");
}
if (($width < $sizes->minWith) or ($width > $sizes->maxWith)) {
$fail($xMessage);
}
if (($height < $sizes->minHeight) or ($height > $sizes->maxHeight)) {
$fail($yMessage);
}
} //end if
}
/**
* Get the file full path
*
* @param string $value
* @param boolean $useStorage
* @return string
*/
private static function getUploadedFileFullPath(string $value, bool $useStorage): string
{
/** @var \Illuminate\Filesystem\FilesystemAdapter */
$disk = Storage::disk();
return !$useStorage ? (realpath($value) ?: $value) : $disk->path($value);
}
/**
* Get the file mimetype
*
* @param string $value
* @param boolean $useStorage
* @return string|false
*/
private static function getUploadedFileMimeType(string $value, bool $useStorage)
{
return !$useStorage ? File::mimeType($value) : Storage::mimeType($value);
}
/**
* Fetch class list from a specific namespace
*
* @param string $namespace
* @return array<string> An array of classe namespaces
*/
public static function getClassesUsingNamespace(string $namespace): array
{
return collect(File::files(\app_path(Str::of($namespace)->replaceFirst('\App', ''))))
->filter(fn (SplFileInfo $splFileInfo) => $splFileInfo->getExtension() === 'php')
->map(fn (SplFileInfo $splFileInfo) => Str::of($splFileInfo->getBasename())
->replace('.' . $splFileInfo->getExtension(), '')->prepend("{$namespace}\\"))->all();
}
/**
* Get all classes from a specific namespace that implement a specific interface
*
* @param string $namespace
* @param string $interface
* @return array<string> An array of classe namespaces
*/
public static function getClassesUsingInterface(string $namespace, string $interface): array
{
return Arr::where(
static::getClassesUsingNamespace($namespace),
fn (string $class_namespace) => \collect(class_implements($class_namespace, true))->filter()
->contains(fn (string $class_interface) => $class_interface === $interface)
);
}
/**
* Get validated pagination from request.
*
* @param integer $default
* @param string $field
* @param string $enumPath
* @return integer
*/
public static function getValidatedEnum(int $default, string $field, string $enumPath): int
{
try {
return \intval(Validator::make(
[$field => \request()->get($field)],
[$field => new Enum($enumPath)]
)->validated()[$field]);
} catch (ValidationException $e) {
return $default;
}
}
}

210
app/Lib/Utils.php Normal file
View file

@ -0,0 +1,210 @@
<?php
namespace App\Lib;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Throwable;
/**
* Some utilities methods
*
* @author Jérémy Munsch <jeremy.munsch@ldc.fr>
*/
// 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 <jeremy.munsch@jeremydev.ovh>
* @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;
}
}

View file

@ -1,132 +0,0 @@
<?php
namespace App\Lib\XmlInco;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
/**
* Des listes d'allégations
*
* @property-read string[] $health
* @property-read string[] $nutrition
* @property-read string[] $other
*
* @package App\Lib
*/
class Allegations implements \Stringable, \JsonSerializable, Jsonable, Arrayable
{
/**
* Valeur minimale, ou exacte
*
* @var string[]
*/
private array $health;
/**
* Valeur minimale, ou exacte
*
* @var string[]
*/
private array $nutrition;
/**
* Valeur minimale, ou exacte
*
* @var string[]
*/
private array $other;
/**
* Allegation
*
* ! All array are filtered keeping only strings.
*
* @param string[] $health Health allegations.
* @param string[] $nutrition Nutrition allegations.
* @param string[] $other Other allegations.
* @return void
*/
public function __construct(array $health = [], array $nutrition = [], array $other = [])
{
$this->health = \array_filter($health, fn ($el) => \is_string($el));
$this->nutrition = \array_filter($nutrition, fn ($el) => \is_string($el));
$this->other = \array_filter($other, fn ($el) => \is_string($el));
}
// * Methods
/**
* Getter
*
* @param string $name
* @return array
* @throws \Error If property access is unhandled.
*/
public function __get(string $name): array
{
if (\in_array($name, ['health', 'nutrition', 'other'])) {
return $this->{$name};
}
$class = __CLASS__;
throw new \Error("Undefined property: {$class}::{$name}");
}
/**
* Get all
*
* @return array<string,string[]>
*/
public function all(): array
{
return $this->toArray();
}
// * Implements Interfaces
/**
* Convert the object to its JSON representation.
*
* @param integer $options
* @return string
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
*/
public function toJson($options = 0)
{
// phpcs:enable
return \json_encode($this->jsonSerialize(), $options);
}
/**
* Convert the object into something JSON serializable.
*
* @return array<string,string[]>
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
/**
* Get the instance as an array.
*
* @return array<string,string[]>
*/
public function toArray()
{
return [
'health' => $this->health,
'nutrition' => $this->nutrition,
'other' => $this->other
];
}
/**
* @return string
*/
public function __toString()
{
return \implode("\n", \array_map(fn (array $arr) => \implode(', ', $arr), $this->toArray()));
}
}

View file

@ -1,281 +0,0 @@
<?php
namespace App\Lib\XmlInco;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
/**
* Les valeurs nutritionelles
*
* @property-read array<string,float|string|null> $values
*
* @package App\Lib
*/
class NutritionalValues implements \Stringable, \JsonSerializable, Jsonable, Arrayable
{
/**
* Liste des valeurs nutritionelles
*
* @var array<string,float|integer|null|string>
*/
private array $values;
/**
* Nutritional values properties
*
* @var string[]
*/
private static $properties = [
'contribution_for',
'nutritional_score',
'ranking',
'energy',
'energy_kj',
'protein',
'carbohydrates',
'sugar',
'fat',
'saturated_fatty_acids',
'dietary_fiber',
'salt',
'energy_percent',
'energy_kj_percent',
'protein_percent',
'carbohydrates_percent',
'sugar_percent',
'fat_percent',
'saturated_fatty_acids_percent',
'dietary_fiber_percent',
'salt_percent'
];
/**
* Nutritional values properties units mesure
*
* @var array<string,string|null>
*/
private static $propertiesUnits = [
'contribution_for' => null,
'nutritional_score' => null,
'ranking' => null,
'energy' => 'Kcal',
'energy_kj' => 'Kj',
'protein' => 'g',
'carbohydrates' => 'g',
'sugar' => 'g',
'fat' => 'g',
'saturated_fatty_acids' => 'g',
'dietary_fiber' => 'g',
'salt' => 'g',
'energy_percent' => '%',
'energy_kj_percent' => '%',
'protein_percent' => '%',
'carbohydrates_percent' => '%',
'sugar_percent' => '%',
'fat_percent' => '%',
'saturated_fatty_acids_percent' => '%',
'dietary_fiber_percent' => '%',
'salt_percent' => '%'
];
/**
* Translation values of inco unreadable keys
*
* @var string[]
*/
private static $cleanValues = [
'apport_port' => 'contribution_for',
'score_nutri' => 'nutritional_score',
'classement' => 'ranking',
'val_energ_kcal_r' => 'energy',
'val_energ_kcal_p' => 'energy_percent',
'val_energ_kj_r' => 'energy_kj',
'pourcent_apport_ref_kjo_r' => 'energy_kj_percent',
'proteine_r' => 'protein',
'pourcent_apport_ref_pro_r' => 'protein_percent',
'sucre_r' => 'sugar',
'pourcent_apport_ref_sugar_r' => 'sugar_percent',
'mat_grass_r' => 'fat',
'pourcent_apport_ref_fat_r' => 'fat_percent',
'fib_alim_r' => 'dietary_fiber',
'pourcent_apport_ref_fibtg_r' => 'dietary_fiber_percent',
'glucides_r' => 'carbohydrates',
'glucides_p' => 'carbohydrates_percent',
'ags_r' => 'saturated_fatty_acids',
'pourcent_ags' => 'saturated_fatty_acids_percent',
'sel_r' => 'salt',
'pourcent_apport_ref_salteq_r' => 'salt_percent'
];
/**
* Human readable translations for inco translation
*
* @var string[]
*/
private static $humanReadable = [
'Pour' => 'contribution_for',
'Classement' => 'ranking',
'Nutriscore' => 'nutritional_score',
'Énergie' => 'energy',
'Énergie kJ' => 'energy_kj',
'Protéines' => 'protein',
'Glucides' => 'carbohydrates',
'Sucres' => 'sugar',
'Matière grasse' => 'fat',
'Acides Gras Saturés' => 'saturated_fatty_acids',
'Fibres alimentaires' => 'dietary_fiber',
'Sel' => 'salt'
];
/**
* Allegation
*
* ! All array are filtered keeping only strings.
*
* @param string[] $values Nutritional values.
* @return void
*/
public function __construct(array $values = [])
{
$emptySkel = \array_map(fn () => null, \array_flip(self::$properties));
$values = \array_map(fn ($value) => \is_null($value) ?
$value : (\is_numeric($value) ? \floatval($value) : \strval($value)), $values);
$this->values = \array_merge($emptySkel, $values);
}
// * Methods
/**
* Getter
*
* @param string $name
* @return array|float|string|null
* @throws \Error If property access is unhandled.
*/
public function __get(string $name): array|float|string|null
{
if (\in_array($name, self::$properties)) {
return $this->values[$name];
}
if ($name === 'values') {
return $this->values;
}
$class = __CLASS__;
throw new \Error("Undefined property: {$class}::{$name}");
}
/**
* Get human readable values
*
* @return array<string,float|string|null>
*/
public function humanReadableValues(): array
{
$o = [];
foreach (self::$humanReadable as $k => $t) {
$o[$k] = $this->values[$t];
}
return $o;
}
/**
* Get units
*
* @param boolean $forHumanReadable
* @return array
*/
public static function getUnits(bool $forHumanReadable = false): array
{
if ($forHumanReadable) {
$out = [];
$humanReadable = array_flip(self::$humanReadable);
foreach (self::$propertiesUnits as $propertyIndex => $unit) {
if (\array_key_exists($propertyIndex, $humanReadable)) {
$out[$humanReadable[$propertyIndex]] = $unit;
}
}
return $out;
}
return self::$propertiesUnits;
}
// * Static Methods
/**
* Convert Xml Inco values into proper Nutritional values
*
* @param array $incoNutritionalValues
* @return self
*/
public static function makeNutritionalValues(array $incoNutritionalValues): self
{
$incoNutritionalValues = \array_map(fn ($value) => \is_null($value) ?
$value : (\is_numeric($value) ? \floatval($value) : \strval($value)), $incoNutritionalValues);
$o = [];
foreach (self::$cleanValues as $k => $t) {
$o[$t] = $incoNutritionalValues[$k] ?? null;
}
return new self($o);
}
// * Implements Interfaces
/**
* Convert the object to its JSON representation.
*
* @param integer $options Json_encode php options.
* @param boolean $fullData Serialize all properties or ignore empty one. Default is false (ignore).
* @return string
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
*/
public function toJson($options = 0, bool $fullData = false)
{
// phpcs:enable
return \json_encode($this->jsonSerialize($fullData), $options);
}
/**
* Convert the object into something JSON serializable.
*
* @param boolean $fullData Serialize all properties or ignore empty one. Default is false (ignore).
* @return array<string,float|string|null>
*/
#[\ReturnTypeWillChange]
public function jsonSerialize(bool $fullData = false)
{
if ($fullData) {
return $this->toArray();
}
// * Save BDD space dont serialize empty values.
return array_filter($this->toArray(), fn ($value) => !\is_null($value));
}
/**
* Get the instance as an array.
*
* @return array<string,float|string|null>
*/
public function toArray()
{
return $this->values;
}
/**
* @return string
*/
public function __toString()
{
$values = $this->humanReadableValues();
$units = self::getUnits(true);
return \implode("\n", \array_map(
fn (float|string|null $value, string $index) =>
"{$index} : {$value}" . ($units[$index] ? ' ' . $units[$index] : ''),
$values,
\array_keys($values)
));
}
}

View file

@ -1,260 +0,0 @@
<?php
namespace App\Lib\XmlInco;
use App\Enums\XmlInco\PortionType;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
/**
* Si le type de portion est exact, seul la partie basse est utilisée,
* Si le type de portion est environ, la partie basse seule ou les deux peuvent
* etre utilisés.
*
* @property-read integer $low_part
* @property-read integer $high_part
*
* @package App\Lib
*/
class PortionAmount implements \Stringable, \JsonSerializable, Jsonable, Arrayable
{
/**
* Valeur minimale, ou exacte
*
* @var integer
*/
private int $low_part;
/**
* Valeur maximale
*
* @var integer
*/
private int $high_part;
/**
* The portion is approximative
*
* @var boolean
*/
private bool $approximative;
/**
* The number of decimals allowed, this should not change
*
* @var integer
*/
private static int $allowedDecimalSize = 2;
/**
* Binary masks to store and retrieve values
*
* @var array<string,integer>
*/
private static $masks = [
'low_part' => ((1 << 15) - 1),
'high_part' => (1 << 31) - (1 << 16),
'approximative' => 1 << 31,
];
/**
* NbPortion
*
* @param float $low_part Min value is 0, max depends on low_part mask.
* @param float $high_part Min value is 0, max depends on low_part mask.
* @param boolean $approximative
* @return void
* @throws \RuntimeException If low_part part does not fit in.
* @throws \RuntimeException If high_part part does not fit in.
*/
public function __construct(float $low_part, float $high_part = 0, bool $approximative = false)
{
$maxMask = self::$masks['low_part'];
$maxValue = \bcdiv($maxMask, pow(10, self::$allowedDecimalSize), self::$allowedDecimalSize);
if (($maxValue - $low_part) < 0 or $low_part < 0) {
throw new \RuntimeException("low_part has to be a value between 0 and {$maxValue}, given is {$low_part}");
}
if (($maxValue - $high_part) < 0 or $high_part < 0) {
throw new \RuntimeException("high_part has to be a value between 0 and {$maxValue}, given is {$high_part}");
}
$this->low_part = \bcmul($low_part, pow(10, self::$allowedDecimalSize), self::$allowedDecimalSize);
$this->high_part = \bcmul($high_part, pow(10, self::$allowedDecimalSize), self::$allowedDecimalSize);
$this->approximative = $approximative;
}
// * Casts
/**
* To database value
*
* @return integer
*/
public function toDatabaseValue(): int
{
return $this->low_part |
$this->high_part << 16 |
($this->approximative ? self::$masks['approximative'] : 0);
}
/**
* Make NbPortion from database value
*
* @param integer $databaseValue
* @return \App\Lib\XmlInco\PortionAmount
*/
public static function fromDatabase(int $databaseValue): self
{
$low_part = \bcdiv(
($databaseValue & self::$masks['low_part']),
pow(10, self::$allowedDecimalSize),
self::$allowedDecimalSize
);
$high_part = \bcdiv(
($databaseValue & self::$masks['high_part']) >> 16,
pow(10, self::$allowedDecimalSize),
self::$allowedDecimalSize
);
$approximative = $databaseValue & self::$masks['approximative'];
return new self(
$low_part,
$high_part,
$approximative,
);
}
/**
* Make NbPortion from json value
*
* @param array<string,integer|boolean> $jsonArray
* @return \App\Lib\XmlInco\PortionAmount
*/
public static function fromJson(array $jsonArray): self
{
$low_part = \bcdiv(
$jsonArray['low_part'],
pow(10, self::$allowedDecimalSize),
self::$allowedDecimalSize
);
$high_part = \bcdiv(
$jsonArray['high_part'],
pow(10, self::$allowedDecimalSize),
self::$allowedDecimalSize
);
$approximative = $jsonArray['approximative'];
return new self(
$low_part,
$high_part,
$approximative,
);
}
// * STATIC Methods
/**
* Get mask for database query
*
* @return array
*/
public static function getMasks(): array
{
return self::$masks;
}
// * Methods
/**
* Get low part
*
* @return float
*/
public function getLowValue(): float
{
return \bcdiv(
$this->low_part,
pow(10, self::$allowedDecimalSize),
self::$allowedDecimalSize
);
}
/**
* Get high part
*
* @return float
*/
public function getHighValue(): float
{
return \bcdiv(
$this->high_part,
pow(10, self::$allowedDecimalSize),
self::$allowedDecimalSize
);
}
/**
* Get Portion type
*
* @return \App\Enums\XmlInco\PortionType
*/
public function getType(): PortionType
{
return match ($this->approximative) {
true => PortionType::approximative,
false => PortionType::exact
};
}
// * Implements Interfaces
/**
* Convert the object to its JSON representation.
*
* @param integer $options
* @return string
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
*/
public function toJson($options = 0)
{
// phpcs:enable
return \json_encode($this->jsonSerialize(), $options);
}
/**
* Convert the object into something JSON serializable.
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
/**
* Get the instance as an array.
*
* @return array<TKey,TValue>
*/
public function toArray()
{
return [
'low_part' => $this->low_part,
'high_part' => $this->high_part,
'approximative' => $this->approximative
];
}
/**
* @return string
*/
public function __toString()
{
if ($this->high_part) {
return \trans('entre :low_part et :high_part', [
'low_part' => $this->getLowValue(),
'high_part' => $this->getHighValue()
]);
}
return $this->approximative ?
trans('environ :amount:', ['amount' => $this->getLowValue()]) : $this->getLowValue();
}
}

View file

@ -1,89 +0,0 @@
<?php
namespace App\Mail;
use App\Models\Service;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ServiceCredentialRenewed extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The concerned service.
*
* @var \App\Models\Service
*/
public $service;
/**
* The user who did the action.
*
* @var \App\Models\User
*/
public $user;
/**
* Create a new message instance.
*
* @param \App\Models\Service $service
* @param \App\Models\User $user
*/
public function __construct(Service $service, User $user)
{
$this->service = $service;
$this->user = $user;
$this->service->load('arcadiaBrand');
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '!!! Warning !!!, New service credential have been generated !',
to: [new Address(
config('mail.contactsList.serviceweb', 'equipe.web@ldc.fr'),
sprintf('%s service warning', config('app.name'))
)]
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content(): Content
{
return new Content(
markdown: 'emails.service-renewed',
with: [
'appName' => config('app.name'),
'service' => $this->service,
'brand' => $this->service->arcadiaBrand,
'by' => sprintf('%s %s (%s)', $this->user->first_name, $this->user->last_name, $this->user->email),
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -1,133 +0,0 @@
<?php
namespace App\Models;
use App\Enums\ActivityLogs\ActivityLogsEventEnum;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Schema;
/**
* @property string $model Target model.
* @property string $model_id Id of the target model.
* @property string $event Event of this activity (ActivityLogsEventEnum).
* @property string $data Changes.
* @property-read \Illuminate\Support\Carbon $created_at Created date.
*
* @method public static function addActivity($model, $eventEnum) Add new activity to the activity logs list.
* @method public static function getValuesChanged($activity, $model, $eventEnum)
* Get old, new and type of values changed.
*
* @property-read \App\Models\User $user User BelongsTo relation.
*/
class ActivityLog extends Model
{
use HasFactory;
use Notifiable;
/**
* Indicates if the model should be timestamped.
*
* @var boolean
*/
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'is_anonymous',
'model_class',
'model_id',
'event',
'data',
'created_at',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'is_anonymous' => 'bool',
'event' => ActivityLogsEventEnum::class,
'data' => 'json',
'created_at' => 'datetime',
];
// * METHODS
/**
* Add new activity to the activity logs list.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param \App\Enums\ActivityLogs\ActivityLogsEventEnum $eventEnum
* @return void
*/
public static function addActivity(Model $model, ActivityLogsEventEnum $eventEnum): void
{
$isAnonymous = true;
/** @var \Illuminate\Database\Eloquent\Model|null */
$userModel = auth('backend')->user();
if (is_object($userModel)) {
$userModel = $userModel->getKeyForSelectQuery();
$isAnonymous = false;
}
$activity = new self();
$activity->user_id = $userModel;
$activity->is_console = app()->runningInConsole();
$activity->is_anonymous = $isAnonymous;
$activity->model_class = \get_class($model);
$activity->model_id = $model->getKey();
$activity->event = $eventEnum;
$activity->data = static::getChangedColumns($activity, $model, $eventEnum);
$activity->created_at = Carbon::now();
$activity->saveOrFail();
}
/**
* Get old, new columns that has changed.
*
* @param \self $activity
* @param \Illuminate\Database\Eloquent\Model $model
* @param \App\Enums\ActivityLogs\ActivityLogsEventEnum $eventEnum
* @return array|null
*/
public static function getChangedColumns(self $activity, Model $model, ActivityLogsEventEnum $eventEnum): array|null
{
$modelClassName = $activity->model_class;
/** Get type of each fields of the target model */
$targetModel = $activity->model_class::where(
(new $modelClassName())->getRouteKeyName(),
$activity->model_id
)->first();
if ($targetModel != null) {
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
$targetModelTypes = collect($targetModel->toArray())
->map(function ($field, $fieldIndex) use ($targetModel) {
return Schema::getColumnType($targetModel->getTable(), $fieldIndex);
})->toArray();
// phpcs:enable
}
/** Return old, new and type of values changed */
return ($eventEnum === ActivityLogsEventEnum::updated) ?
array_intersect_key($targetModelTypes, $model->getChanges()) : null;
}
// * RELATIONS
/**
* User BelongsTo relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View file

@ -1,118 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Rennokki\QueryCache\Traits\QueryCacheable;
/**
* @property-read integer $id The arcadia brand id.
* @property-read string $label The arcadia brand name.
*
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\Service> $services
* The Services for the ArcadiaBrand.
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\XmlInco> $xmlIncos
* The XmlIncos for the ArcadiaBrand.
* @package App\Models
*/
class ArcadiaBrand extends Model
{
use HasFactory;
use QueryCacheable;
/**
* Indicates if the IDs are auto-incrementing.
*
* @var boolean
*/
public $incrementing = false;
/**
* The timeout cache for this model
*
* @var integer
*/
protected $cacheFor = 0;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'id',
'label'
];
/**
* ArcadiaBrand
*
* @param array $attributes
*/
public function __construct(array $attributes = [])
{
$this->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
/**
* Get all of the Services for the ArcadiaBrand
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function services(): HasMany
{
return $this->hasMany(Service::class);
}
/**
* Get all of the xmlIncos for the ArcadiaBrand
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function xmlIncos(): HasMany
{
return $this->hasMany(XmlInco::class);
}
}

View file

@ -1,238 +0,0 @@
<?php
namespace App\Models;
use App\Jobs\SetServiceStats;
use App\Lib\ApiHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Str;
use Rennokki\QueryCache\Traits\QueryCacheable;
/**
* @property integer $id The service id.
* @property string $name Le nom du service de la marque.
* @property string $bearer_token Le Token pour identifier le service.
* @property string $bearer_pwd La clé du service pour l'Api.
* @property string $bearer_token_date Date de génération des clés bearer du services
* @property integer $arcadia_brand_id Foreign key Arcadia brand.
*
* @property-read \App\Models\ArcadiaBrand $arcadiaBrand The Arcadia Brand that owns the Service.
* @property-read \Illuminate\Database\Eloquent\Collection<\App\Models\ServiceStat> $servicesStats
* The Services Stats for the Service.
* @package App\Models
*/
class Service extends Model
{
use HasFactory;
use QueryCacheable;
/**
* The timeout cache for this model
*
* @var integer
*/
protected $cacheFor = 0;
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'name',
'arcadia_brand_id'
];
/**
* Service
*
* @param array $attributes
*/
public function __construct(array $attributes = [])
{
$this->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 $model) {
$model->generateBearerToken();
$model->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
/**
* Get the Arcadia Brand that owns the Service
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function arcadiaBrand(): BelongsTo
{
return $this->belongsTo(ArcadiaBrand::class);
}
/**
* Get all of the Services Stats for the Service
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function servicesStats(): HasMany
{
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([
'arcadiaBrand' => function ($query) use ($service) {
return $query->cacheTags(["service:{$service->id}:arcadiaBrand"]);
},
]);
}
}
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 Flucollector
* - 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&param2=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 Flucollector
*
* * ! 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 = ApiHelper::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);
}
}

View file

@ -1,59 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property integer $id The service id.
* @property \Carbon\Carbon $date La journée concernée.
* @property integer $nb_calls Le nombre d'appels du service pour la journée.
* @property integer $service_id Service foreign key.
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @package App\Models
*/
class ServiceStat extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = ['date', 'service_id'];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'date' => 'immutable_date',
'nb_calls' => 'int',
];
/**
* The model's attributes.
*
* @var array
*/
protected $attributes = [
'nb_calls' => 0
];
// * RELATIONS
/**
* Get the Service that owns the ServiceStat
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function service(): BelongsTo
{
return $this->belongsTo(Service::class);
}
}

Some files were not shown because too many files have changed in this diff Show more