Bump to laravel-10

This commit is contained in:
Noah Lancien 2023-08-21 10:22:07 +02:00
parent afa735f0b8
commit a529367fba
264 changed files with 12505 additions and 4630 deletions

5
.browserslistrc Normal file
View file

@ -0,0 +1,5 @@
# 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
insert_final_newline = true
indent_style = space
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
@ -13,3 +13,9 @@ trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4
[*.vue]
indent_size = 2

View file

@ -5,20 +5,29 @@ APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=dev-mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=secret
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=database
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=dev-mailhog
MAIL_PORT=1025
@ -29,9 +38,33 @@ MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
MAIL_ADMIN=equipe.web@ldc.fr
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
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=
# CRM
CRM_ENDPOINT=
CRM_TOKEN=
CRM_PASSWORD=
# MESBR
MESBR_ENDPOINT=

55
.eslintrc.json Normal file
View file

@ -0,0 +1,55 @@
{
"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,5 +1,11 @@
* text=auto
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

15
.gitignore vendored
View file

@ -15,7 +15,7 @@ mix-manifest.json
/resources/views/front/basic
.env
.env.dev
.env.prod
.env.production
.env.backup
.phpunit.result.cache
Homestead.json
@ -26,7 +26,16 @@ package-lock.json
composer.lock
resources/front
resources/js/basic.js
.vscode
/.vscode
storage
README.txt
webpack.mix.js
vite.config.ts
.stylelintcache
/.fleet
/.idea
/.phpunit.cache
/public/build
/public/cache
/public/sitemap.xml
auth.json
docker-compose.override.yml

View file

@ -1,46 +1,24 @@
image: $CI_REGISTRY/service-web-lab/ldc-web-images-docker/ldcweb-qa:php7.3-1.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:
- Security
- CodingStandards
composer-security:
stage: Security
script:
- local-php-security-checker --format=json --path=./composer.lock > composer-security-checker-report.json
artifacts:
when: on_failure
expose_as: 'composer-security-checker'
paths:
- composer-security-checker-report.json
allow_failure: true
npm-security:
stage: Security
script:
- audit-ci -p npm -o json > npm-security-checker-report.json
artifacts:
when: on_failure
expose_as: 'npm-security-checker'
paths:
- npm-security-checker-report.json
allow_failure: true
phpcs:
stage: CodingStandards
before_script:
- composer config gitlab-token.gitlab.com $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
- composer install
cache:
paths:
- vendor/
script:
- vendor/bin/phpcs --report=junit --report-file=phpcs-report.xml
artifacts:
when: on_failure
expose_as: 'phpcs'
paths:
- phpcs-report.xml
reports:
junit: phpcs-report.xml
allow_failure: false
- lint
- security
- build
- test
- deploy

View file

@ -1,13 +0,0 @@
php:
preset: laravel
disabled:
- unused_use
finder:
not-name:
- index.php
- server.php
js:
finder:
not-name:
- webpack.mix.js
css: true

48
.stylelintrc.json Normal file
View file

@ -0,0 +1,48 @@
{
"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

@ -0,0 +1,36 @@
<?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 integer
*/
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');
});
return 0;
}
}

View file

@ -2,33 +2,20 @@
namespace App\Enums\Users;
use Kwaadpepper\Enum\BaseEnumRoutable;
use App\Traits\Enums\BaseEnum;
/**
* @method static self conceptor()
* @method static self admin()
*/
class RoleEnum extends BaseEnumRoutable
enum RoleEnum: int
{
/**
* @return array
*/
protected static function values(): array
{
return [
'conceptor' => 1,
'admin' => 2,
];
}
use BaseEnum;
case conceptor = 1;
case admin = 2;
/**
* @return array
* Optionnal labels definition
*/
protected static function labels(): array
{
return [
'conceptor' => trans('Concepteur'),
'admin' => trans('Administrator'),
];
}
private const LABELS = [
self::conceptor->name => 'Concepteur',
self::admin->name => 'Administrator',
];
}

View file

@ -2,26 +2,47 @@
namespace App\Exceptions;
use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
* A list of exception types with their corresponding custom log levels.
*
* @var array
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $dontReport = [
RedirectTo::class
];
protected $levels = [];
/**
* A list of the inputs that are never flashed for validation exceptions.
* A list of the exception types that are not reported.
*
* @var array
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
}

View file

@ -0,0 +1,33 @@
<?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

@ -2,14 +2,124 @@
namespace App\Http\Controllers;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
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
): 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): Builder
{
$table = $query->getModel()->getTable();
foreach ($fields as $f) {
$f = str_replace('?', '\?', $f);
if (is_array($f)) {
$query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($f)->map(function ($q) use ($table) {
return "`$table`.`$q`";
})->implode(', \' \', ')),
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
} else {
$query->orWhereRaw(
"UPPER(`$table`.`$f`) LIKE UPPER(?)",
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
}
}
return $query;
}
/**
* 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 = []): 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');
}
return $query;
}
}

View file

@ -14,9 +14,10 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
@ -29,16 +30,18 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
'htmlsanitize',
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class,
],
'api' => [
'throttle:60,1',
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
@ -53,6 +56,7 @@ class Kernel extends HttpKernel
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
@ -65,5 +69,6 @@ class Kernel extends HttpKernel
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
];
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
class Authenticate extends Middleware
@ -13,7 +14,7 @@ class Authenticate extends Middleware
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request): ?string
protected function redirectTo(Request $request): ?string
{
$logins = [
'backend' => route('bo.login')

View file

@ -1,16 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
class CheckForMaintenanceMode extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
];
}

View file

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

View file

@ -4,6 +4,7 @@ namespace App\Http\Middleware;
use App\Models\Settings;
use Closure;
use Illuminate\Http\Request;
class GameCheckDate
{
@ -14,12 +15,12 @@ class GameCheckDate
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
public function handle(Request $request, Closure $next)
{
/** @var \App\Models\Settings $settings */
$settings = app(Settings::class);
// Check si le teaser est actif
// Check si le teaser est actif.
if ($settings->start_date > now()) {
if ($settings->activated_teaser === 1 && $settings->teaser_end_activity > now()) {
return redirect()->route('teaser');
@ -27,7 +28,7 @@ class GameCheckDate
return redirect($settings->redirect_close);
}
// Check si le jeu est terminé
// Check si le jeu est terminé.
if ($settings->end_date < now()) {
if ($settings->activated_close_page === 1 && $settings->close_page_active_date > now()) {
return redirect()->route('end');

View file

@ -0,0 +1,61 @@
<?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

@ -0,0 +1,15 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [];
}

View file

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

View file

@ -0,0 +1,103 @@
<?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)
{
foreach ($headerList as $header) {
header_remove($header);
}
}
}

View file

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

View file

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

View file

@ -2,7 +2,7 @@
namespace App\Http\Middleware;
use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
@ -10,7 +10,7 @@ class TrustProxies extends Middleware
/**
* The trusted proxies for this application.
*
* @var array|string|null
* @var array<int, string>|string|null
*/
protected $proxies;
@ -19,5 +19,10 @@ class TrustProxies extends Middleware
*
* @var integer
*/
protected $headers = Request::HEADER_X_FORWARDED_ALL;
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -0,0 +1,15 @@
<?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,8 +9,9 @@ class VerifyCsrfToken extends Middleware
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
* @var array<int, string>
*/
protected $except = [
];
'/csp-report'
];
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class isAdmin
@ -14,7 +15,7 @@ class isAdmin
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
public function handle(Request $request, Closure $next)
{
$role = Auth::user()->role;
if ($role === 1 || $role === 2) {

View file

@ -0,0 +1,64 @@
<?php
namespace App\Lib;
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

@ -0,0 +1,190 @@
<?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

@ -0,0 +1,454 @@
<?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

@ -0,0 +1,519 @@
<?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\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Finder\SplFileInfo;
class ToolboxHelper
{
/**
* @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)
);
}
}

View file

@ -2,19 +2,22 @@
namespace App\Providers;
use App\Models\Prize;
use App\Models\Settings;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
}
/**
* Bootstrap any application services.
*
@ -26,26 +29,20 @@ class AppServiceProvider extends ServiceProvider
// * HA PROXY FIX.
App::singleton('clientIp', function () {
if (
config('app.env') === 'local'
) {
if (config('app.env') === 'local') {
return request()->getClientIp();
} else {
return request()->server('HTTP_X_FORWARDED_FOR') ??
request()->getClientIp();
}
});
// * LOCAL FORCE eg: for Carbon.
setlocale(LC_ALL, \sprintf(
'%s_%s.UTF-8',
config('app.locale'),
\strtoupper(config('app.locale'))
));
Carbon::setLocale(config('app.locale'));
// * FORCE BOOTSTRAP PAGINATOR.
if (!app()->runningInConsole()) {
Paginator::useBootstrap();
}
app()->singleton(Settings::class, function () {
if (!($settings = Settings::find(1))) {
@ -53,30 +50,5 @@ class AppServiceProvider extends ServiceProvider
}
return $settings;
});
if (!app()->runningInConsole()) {
// On ramène le nom du jeu et le nom de la société sur toute les pages.
$settingsGlobal = Cache::get('settingsGlobal');
if (!$settingsGlobal) {
$settingsGlobal = app(Settings::class)
->select('name', 'company', 'analytics', 'start_date', 'end_date')
->first();
Cache::put('settingsGlobal', $settingsGlobal);
}
View::share('settingsGlobal', $settingsGlobal);
// On ramène aussi tout les prix car on en a besoin sur le header.
$prizesGlobal = Cache::get('prizesGlobal');
if (!$prizesGlobal) {
$prizesGlobal = Prize::all();
Cache::put('prizesGlobal', $prizesGlobal);
}
View::share('prizesGlobal', $prizesGlobal);
Blade::if('superadmin', function () {
return auth()->user() && auth()->user()->role == 1;
});
}//end if
}
}

View file

@ -20,6 +20,5 @@ class AuthServiceProvider extends ServiceProvider
*/
public function boot()
{
$this->registerPolicies();
}
}

View file

@ -18,4 +18,23 @@ class EventServiceProvider extends ServiceProvider
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot(): void
{
}
/**
* Determine if events and listeners should be automatically discovered.
*
* @return boolean
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}

View file

@ -2,20 +2,14 @@
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* This namespace is applied to your controller routes.
*
* In addition, it is set as the URL generator's root namespace.
*
* @var string
*/
protected $namespace = null;
/**
* The path to the "home" route for your application.
*
@ -28,12 +22,16 @@ class RouteServiceProvider extends ServiceProvider
*
* @return void
*/
public function boot()
public function boot(): void
{
$this->configureRateLimiting();
$this->cspReportRoute();
$this->routes(function () {
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
->group(base_path('routes/web-fo.php'));
Route::middleware('web')
->namespace($this->namespace)
@ -44,4 +42,58 @@ class RouteServiceProvider extends ServiceProvider
->group(base_path('routes/web-re7.php'));
});
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(150)->by($request->user()?->id ?: app('clientIp'));
});
RateLimiter::for('web', function (Request $request) {
return Limit::perMinute(150)->by($request->user()?->id ?: app('clientIp'));
});
RateLimiter::for('credentials', function (Request $request) {
return Limit::perMinute(30)->by($request->user()?->id ?: app('clientIp'));
});
}
/**
* * Register CSP report URI
*
* Put this function inside RouteServiceProvider,
* and call it in RouteServiceProvider::boot
*
* @return void
*/
private function cspReportRoute()
{
// * CSP REPORT URI, Please add /csp-report to VerifyCsrfToken $except middleware
\Illuminate\Support\Facades\Route::post('/csp-report', function () {
$isCspRequest = request()->header('Content-Type') === 'application/csp-report';
if (!$isCspRequest or !request()->json('csp-report') or !is_array(request()->json('csp-report'))) {
abort(401);
}
// * Flatten if needed
$cspReport = collect(request()->json('csp-report'))
->flatMap(function ($v, $k) {
return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
})->all();
// * Filter array keys
$cspReport = collect(array_intersect_key($cspReport, [
'blocked-uri' => '', 'document-uri' => '', 'original-policy' => '',
'referrer' => '', 'violated-directive' => ''
]));
\Illuminate\Support\Facades\Log::warning(sprintf(
"CSP REPORT for %s : \n %s",
$cspReport->get('document-uri'),
$cspReport->implode("\n")
));
})->middleware('throttle:5,2')
->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
->name('cspReportUri');
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace App\Providers;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Finder\SplFileInfo;
class TranslationServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot(): void
{
foreach ($this->collectLocalesStrings() as $locale) {
// * Supported locales
Cache::rememberForever(sprintf('translations.%s', $locale), function () use ($locale) {
$translations = [
'php' => $this->phpTranslations($locale),
'json' => $this->jsonTranslations($locale),
];
return $translations;
});
}
}
/**
* Gather provided langs checking from files
*
* @return \Illuminate\Support\Collection<string, string>
*/
private function collectLocalesStrings(): Collection
{
return collect(File::allFiles(resource_path('lang/')))->flatMap(function (SplFileInfo $file) {
if ($file->getRelativePath() and \strpos($file->getRelativePath(), 'vendor') !== 0) {
return [$file->getRelativePath() => ''];
}
})->keys();
}
/**
* Gather all php translations
*
* @param string $locale
* @return array<string, string>
*/
private function phpTranslations(string $locale): array
{
$path = resource_path("lang/$locale");
return collect(File::allFiles($path))->flatMap(function ($file) use ($locale) {
$translation = $file->getBasename('.php');
return [$translation => trans($translation, [], $locale)];
})->all();
}
/**
* Gather json translations
*
* @param string $locale
* @return array<string, string>
*/
private function jsonTranslations(string $locale): array
{
$path = resource_path("lang/$locale.json");
if (is_string($path) && is_readable($path)) {
return json_decode(file_get_contents($path), true) ?? [];
}
return [];
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace App\Providers;
use App\Models\Prize;
use App\Models\Settings;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class ViewServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
if (!app()->runningInConsole()) {
$appInfos = Cache::remember('composer', 3600, function () {
return json_decode(File::get(\app_path('../composer.json')));
});
View::share('name', $appInfos->name);
View::share('version', $appInfos->version);
View::share('license', $appInfos->license);
// * FORCE BOOTSTRAP PAGINATOR
Paginator::defaultView('bo.modules.pagination');
// On ramène le nom du jeu et le nom de la société sur toute les pages.
$settingsGlobal = Cache::get('settingsGlobal');
if (!$settingsGlobal) {
$settingsGlobal = app(Settings::class)
->select('name', 'company', 'analytics', 'start_date', 'end_date')
->first();
Cache::put('settingsGlobal', $settingsGlobal);
}
View::share('settingsGlobal', $settingsGlobal);
// On ramène aussi tout les prix car on en a besoin sur le header.
$prizesGlobal = Cache::get('prizesGlobal');
if (!$prizesGlobal) {
$prizesGlobal = Prize::all();
Cache::put('prizesGlobal', $prizesGlobal);
}
View::share('prizesGlobal', $prizesGlobal);
// * CUSTOM IF STATEMENTS
Blade::if('superadmin', function () {
return auth()->user() && auth()->user()->role == 1;
});
} //end if
}
}

View file

View file

@ -0,0 +1,259 @@
<?php
namespace App\Traits\Enums;
use Illuminate\Support\Str;
/**
* @author Jérémy Munsch <jeremy.munsch@ldc.fr>
*/
trait BaseEnum
{
// * GENERIC METHODS
/**
* Get enum value
*
* @return string|integer
*/
public function value(): string|int
{
return self::toValues()[$this->name];
}
/**
* Get enum label
*
* @return string
*/
public function label(): string
{
return self::toLabels()[$this->name];
}
/**
* Get enum name
*
* @return string
*/
public function name(): string
{
return $this->name;
}
// * STATIC FETCH METHODS
/**
* Get enum labels
*
* @return array<string>
*/
public static function toLabels(): array
{
if (empty(self::getCache('labels'))) {
$labels = self::getLabelsConstant();
self::setCache('labels', \collect(self::cases())
->mapWithKeys(function ($enum) use ($labels) {
if (!\is_null($labels) and !isset($labels[$enum->name])) {
throw new \Error('Missing definition label ' . self::class . " for {$enum->name}");
}
return [$enum->name => \trans(
!\is_null($labels) ? $labels[$enum->name] : Str::ucfirst($enum->name)
)];
})->all());
}
return self::getCache('labels');
}
/**
* Get enum values
*
* @return array<integer|string>
*/
public static function toValues(): array
{
if (empty(self::getCache('values'))) {
self::setCache('values', \collect(self::cases())
->mapWithKeys(fn ($enum) => [$enum->name => $enum->value])->all());
}
return self::getCache('values');
}
/**
* Get enums
*
* @return array
*/
public static function all(): array
{
return self::cases();
}
/**
* Get enums
*
* @return array
*/
public static function toArray(): array
{
if (empty(self::getCache('enums'))) {
$definitions = self::getCustomDefinitions();
self::setCache('enums', \collect(self::cases())
->mapWithKeys(function ($enum) use ($definitions) {
$baseArray = [
'name' => $enum->name,
'label' => $enum->label(),
'value' => $enum->value,
];
foreach ($definitions as $index => $elements) {
$index = Str::lower(Str::singular($index));
if (!isset($elements[$enum->name])) {
throw new \Error("Missing definition {$index} " . self::class . " for {$enum->name}");
}
$baseArray[$index] = $elements[$enum->name];
}
return [$enum->name => (object)$baseArray];
})->all());
}
return self::getCache('enums');
}
/**
* Fetch enum using value
*
* @param \App\Traits\Enums\BaseEnum|integer|string $value
* @return \App\Traits\Enums\BaseEnum|null
*/
public static function make(self|int|string $value): self|null
{
return !\is_object($value) ?
\collect(self::cases())->keyBy('value')->get($value) : $value;
}
/**
* Test if 2 enum are equals
*
* @param object ...$others
* @return boolean
* @throws \Error If provided element are not enums taht extends BaseEnum.
*/
public function equals(object ...$others): bool
{
foreach ($others as $other) {
if (!\in_array(BaseEnum::class, \class_uses_recursive($other))) {
throw new \Error(__METHOD__ .
' requires $enum to use ' . BaseEnum::class);
}
if (
get_class($this) === get_class($other)
&& $this->value === $other->value
) {
return true;
}
}
return false;
}
/**
* Serialize to json
*
* @return mixed
*/
public function jsonSerialize(): mixed
{
return $this->value;
}
// * CACHE
/**
* Get cache array
*
* @param string $index
* @return array
*/
private static function getCache(string $index): array
{
self::initCache();
if (!isset($GLOBALS[self::class]['props'][$index])) {
$GLOBALS[self::class]['props'][$index] = [];
}
return $GLOBALS[self::class]['props'][$index];
}
/**
* Set cache array
*
* @param string $index
* @param array $items
* @return void
*/
private static function setCache(string $index, array $items): void
{
self::initCache();
if (!isset($GLOBALS[self::class]['props'][$index])) {
$GLOBALS[self::class]['props'][$index] = [];
}
$GLOBALS[self::class]['props'][$index] = $items;
}
/**
* Init cache if needed
*
* @return void
*/
private static function initCache(): void
{
if (!isset($GLOBALS[self::class])) {
$GLOBALS[self::class] = [
'props' => [
'enums' => [],
'values' => [],
'labels' => []
],
'customProps' => null
];
}
}
// * PRIVATE
/**
* Fetch the LABELS constant if exists
*
* @return array|null
*/
private static function getLabelsConstant(): array|null
{
$class_name = self::class;
try {
$constant_reflex = new \ReflectionClassConstant($class_name, 'LABELS');
return $constant_reflex->getValue();
} catch (\ReflectionException $e) {
return null;
}
}
/**
* Get custom constants
*
* @return array
*/
private static function getCustomDefinitions(): array
{
// ? Has custom props in cache ?
if (!isset($GLOBALS[self::class]) or \is_null($GLOBALS[self::class]['customProps'])) {
$names = \collect(self::cases())->keyBy('name');
$GLOBALS[self::class]['customProps'] = \collect((new \ReflectionClass(self::class))->getConstants())
->filter(function (mixed $constant, string $key) use ($names) {
if ($key === 'LABELS' or !\is_array($constant)) {
return false;
}
return \collect($constant)->keys()
->filter(fn (string $name) => $names->has($name))->count();
})->all();
}
return $GLOBALS[self::class]['customProps'];
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\Traits\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
/**
* ! Use this only on limited elements, (not on tags, or products, or posts for ex.)
*
* @property private static array $cacheConditions Eloquent condition to cache elements (to be used in where statement).
*/
trait IsCachedAsCollection
{
/**
* Get all available instances from cache.
* If cache was reset, it will fetch them from DB.
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getCached(): Collection
{
if (!Cache::has(static::getCacheKeyName())) {
try {
static::setCached();
} catch (QueryException $e) {
report($e);
return \collect();
}
}
return Cache::get(static::getCacheKeyName());
}
/**
* Remove cached elements
*
* @return void
*/
public static function removeCached(): void
{
Cache::forget(static::getCacheKeyName());
}
/**
* Put all instances into cache.
*
* @return void
*/
protected static function setCached(): void
{
$conditions = [];
if (property_exists(get_called_class(), 'cacheConditions')) {
$conditions = collect(static::${'cacheConditions'})->all();
}
Cache::put(static::getCacheKeyName(), static::queryCached(static::query())
->where($conditions)->get()
->pipe(fn (Collection $collection) => \call_user_func([self::class, 'filterQuery'], $collection)));
}
/**
* Change query for cached elements
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
protected static function queryCached(Builder $query): Builder
{
return $query;
}
/**
* Change query for cached elements
*
* @param \Illuminate\Support\Collection $results
* @return \Illuminate\Support\Collection
*/
protected static function filterQuery(Collection $results): Collection
{
return $results;
}
/**
* Get the class basename
*
* @return string
*/
protected static function getCacheKeyName(): string
{
return static::getCallClassBaseName();
}
/**
* Get the class basename
*
* @return string
*/
protected static function getCallClassBaseName(): string
{
return str_replace(__NAMESPACE__ . '\\', '', get_called_class());
}
}

View file

@ -0,0 +1,180 @@
<?php
namespace App\Traits\Models;
use App\Enums\InputType;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
trait MiniCrudModel
{
/**
* Fill the model with an array of attributes.
*
* @param array $attributes
* @return void
* @throws \Illuminate\Database\Eloquent\MassAssignmentException If fill prop is not allowed.
* @phpcs:disable Squiz.Commenting.FunctionComment.EmptyThrows
*/
public function fill(array $attributes)
{
// phpcs:enable
parent::fill($attributes);
if (isset($attributes['data'])) {
$this->setData($attributes['data']);
}
}
/**
* Apply function on data typed text while on save event
*
* @return void
*/
public function minicrudCreating()
{
/** @var array<string,array> */
$data = $this->data;
foreach ($data as &$input) {
switch ($input['inputType']) {
case InputType::text->value:
$input['text'] = clean($input['text']);
$input['text'] = FileStorageHelper::storeModelFilesFromHtml($this, $input['text']);
break;
} //end switch
} //end foreach
$this->data = $data;
}
/**
* Apply function on data typed text while on update event
*
* @return void
*/
public function minicrudUpdating()
{
$this->minicrudCreating();
}
/**
* Apply function on data typed text while on updated event
*
* @return void
*/
public function minicrudUpdated()
{
/** @var array */
foreach ($this->data as $index => $input) {
switch ($input['inputType']) {
case InputType::text->value:
$original = ToolboxHelper::getOriginal($this, 'data');
FileStorageHelper::removeOldModelFilesFromHtml(
$input['text'],
$original[$index]['text'] ?? ''
);
break;
} //end switch
} //end foreach
}
/**
* Apply function on data typed text while on deleted event
*
* @return void
*/
public function minicrudDeleted()
{
/** @var array */
foreach ($this->data as $input) {
switch ($input['inputType']) {
case InputType::text->value:
FileStorageHelper::removeModelFilesOfHtml($input['text']);
break;
} //end switch
} //end foreach
}
/**
* Set a specific prop
*
* @param array $data
* @return void
*/
private function setData(array $data): void
{
$attrs = $this->attributes;
if (isset($attrs['data'])) {
unset($attrs['data']);
}
$this->setAttribute('data', $data);
}
/**
* Set a specific prop
*
* @param array $value
* @return void
*/
protected function setDataAttribute(array $value): void
{
$this->attributes['data'] = $this->castAttributeAsJson(
'data',
$this->serializeData($value)
);
}
/**
* Set a specific prop
*
* @param string|null $data
* @return array
*/
protected function getDataAttribute(string|null $data): array
{
return $this->fromJson($data ?? $this->attributes['data'] ?? '[]');
}
/**
* Serialize properties
*
* @param array $data
* @return array
* @throws \ErrorException If input type is no handled.
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
*/
private function serializeData(array $data): array
{
foreach ($data as $o => $input) {
switch ($input['inputType']) {
case InputType::image->value:
$data[$o]['value'] = \preg_replace(
'/^\/|^/',
'',
FileStorageHelper::storeFile($this, $input['value'], true)
);
break;
case InputType::selectEnum->value:
case InputType::boolean->value:
case InputType::password->value:
case InputType::email->value:
case InputType::number->value:
case InputType::videoVimeo->value:
case InputType::videoYoutube->value:
case InputType::video->value:
break;
case InputType::string->value:
case InputType::text->value:
case InputType::url->value:
ToolboxHelper::assertNoInternalBoUrls('content', $input['value']);
break;
default:
throw new \ErrorException(sprintf(
'Unknown input Type %s',
$input['inputType']
));
} //end switch
} //end foreach
return $data;
}
}

View file

View file

@ -11,7 +11,7 @@ define('LARAVEL_START', microtime(true));
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
| loading of any of our classes manually. It's great to relax.
|
*/

View file

@ -1,5 +1,6 @@
{
"name": "laravel/laravel",
"version": "2.0.0",
"type": "project",
"description": "The Laravel Framework.",
"keywords": [
@ -8,69 +9,80 @@
],
"license": "MIT",
"require": {
"php": "^7.3",
"ext-curl": "*",
"ext-json": "*",
"fideloper/proxy": "^4.2",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"php": "^8.1",
"diglactic/laravel-breadcrumbs": "^8.0",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.0",
"laravel/tinker": "^2.8",
"laravel/ui": "^4.2",
"mews/purifier": "^3.4",
"sentry/sentry-laravel": "*",
"kwaadpepper/enum": "^2.1",
"kwaadpepper/exception-handler": "^0.0.6",
"kwaadpepper/serial-int-caster": "^1.0",
"laravel/framework": "^8.0",
"laravel/tinker": "^2.0",
"laravel/ui": "^3.0"
"kwaadpepper/serial-int-caster": "^1.0"
},
"require-dev": {
"doctrine/dbal": "^2.0",
"facade/ignition": "^2.3.6",
"fzaninotto/faker": "^1.9.1",
"mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^9.0",
"slevomat/coding-standard": "^7.0",
"squizlabs/php_codesniffer": "^3.6"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"extra": {
"laravel": {
"dont-discover": []
}
"doctrine/dbal": "^3.6",
"fakerphp/faker": "^1.9.1",
"kitloong/laravel-migrations-generator": "^6.8",
"krlove/eloquent-model-generator": "dev-l10-compatibility",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.0",
"slevomat/coding-standard": "^8.10",
"spatie/laravel-ignition": "^2.0",
"squizlabs/php_codesniffer": "^3.6",
"thedoctor0/laravel-factory-generator": "^1.2"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
},
"classmap": [
"class"
]
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/laravel-shift/eloquent-model-generator.git"
}
],
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
}
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

5698
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,7 @@
<?php
use Illuminate\Support\Facades\Facade;
return [
/*
@ -54,7 +56,7 @@ return [
'url' => env('APP_URL', 'http://localhost'),
'asset_url' => env('ASSET_URL', null),
'asset_url' => env('ASSET_URL'),
/*
|--------------------------------------------------------------------------
@ -141,6 +143,7 @@ return [
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
@ -174,6 +177,8 @@ return [
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TranslationServiceProvider::class,
App\Providers\ViewServiceProvider::class,
],
@ -188,45 +193,6 @@ return [
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
'aliases' => Facade::defaultAliases()->merge([])->toArray(),
];

78
config/breadcrumbs.php Normal file
View file

@ -0,0 +1,78 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| View Name
|--------------------------------------------------------------------------
|
| Choose a view to display when Breadcrumbs::render() is called.
| Built in templates are:
|
| - 'breadcrumbs::bootstrap5' - Bootstrap 5
| - 'breadcrumbs::bootstrap4' - Bootstrap 4
| - 'breadcrumbs::bulma' - Bulma
| - 'breadcrumbs::foundation6' - Foundation 6
| - 'breadcrumbs::json-ld' - JSON-LD Structured Data
| - 'breadcrumbs::materialize' - Materialize
| - 'breadcrumbs::tailwind' - Tailwind CSS
| - 'breadcrumbs::uikit' - UIkit
|
| Or a custom view, e.g. '_partials/breadcrumbs'.
|
*/
'view' => 'breadcrumbs/breadcrumb-layout',
/*
|--------------------------------------------------------------------------
| Breadcrumbs File(s)
|--------------------------------------------------------------------------
|
| The file(s) where breadcrumbs are defined. e.g.
|
| - base_path('routes/breadcrumbs.php')
| - glob(base_path('breadcrumbs/*.php'))
|
*/
'files' => [
base_path('routes/breadcrumbs-bo.php'),
base_path('routes/breadcrumbs-fo.php'),
],
/*
|--------------------------------------------------------------------------
| Exceptions
|--------------------------------------------------------------------------
|
| Determine when to throw an exception.
|
*/
// * When route-bound breadcrumbs are used but the current route doesn't have a name (UnnamedRouteException)
'unnamed-route-exception' => true,
// * When route-bound breadcrumbs are used and the matching breadcrumb doesn't exist (InvalidBreadcrumbException)
'missing-route-bound-breadcrumb-exception' => true,
// * When a named breadcrumb is used but doesn't exist (InvalidBreadcrumbException)
'invalid-named-breadcrumb-exception' => true,
/*
|--------------------------------------------------------------------------
| Classes
|--------------------------------------------------------------------------
|
| Subclass the default classes for more advanced customisations.
|
*/
// * Manager
'manager-class' => Diglactic\Breadcrumbs\Manager::class,
// * Generator
'generator-class' => Diglactic\Breadcrumbs\Generator::class,
];

View file

@ -36,9 +36,20 @@ return [
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true,
'host' => env('PUSHER_HOST') ?: 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html .
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'redis' => [

View file

@ -46,6 +46,7 @@ return [
'driver' => 'database',
'table' => 'cache',
'connection' => null,
'lock_connection' => null,
],
'file' => [
@ -61,8 +62,7 @@ return [
env('MEMCACHED_PASSWORD'),
],
'options' => [
// phpcs:ignore
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
// Memcached::OPT_CONNECT_TIMEOUT => 2000, .
],
'servers' => [
[
@ -76,6 +76,7 @@ return [
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],
'dynamodb' => [
@ -87,6 +88,10 @@ return [
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
],
/*
@ -100,6 +105,6 @@ return [
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache'),
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'),
];

View file

@ -74,7 +74,7 @@ return [
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'schema' => 'public',
'search_path' => 'public',
'sslmode' => 'prefer',
],
@ -129,7 +129,8 @@ return [
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
@ -137,7 +138,8 @@ return [
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],

View file

@ -1,17 +0,0 @@
<?php
return [
/**
* Send mail when app debug is turn off
* so you can be warned if an unhandled exception occurs
*/
'enableMaiLog' => !config('app.debug'),
/**
* Put a developper secured mailbox in here
* so you can receive email if things go mad
*/
'contactsList' => [
env('MAIL_LIST_SERVICEWEB', 'equipe.web@ldc.fr')
]
];

View file

@ -46,6 +46,7 @@ return [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],
'public' => [
@ -53,6 +54,14 @@ return [
'root' => storage_path('app/public'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
'throw' => false,
],
'private' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'visibility' => 'private',
'throw' => false,
],
's3' => [
@ -63,10 +72,13 @@ return [
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links

View file

@ -44,9 +44,9 @@ return [
*/
'argon' => [
'memory' => 1024,
'threads' => 2,
'time' => 2,
'memory' => 65536,
'threads' => 1,
'time' => 4,
],
];

View file

@ -19,6 +19,22 @@ return [
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => false,
],
/*
|--------------------------------------------------------------------------
| Log Channels
@ -41,36 +57,19 @@ return [
'ignore_exceptions' => false,
],
'stackProd' => [
'driver' => 'stack',
'channels' => ['dailyProd'],
'ignore_exceptions' => false,
],
'stackDaily' => [
'driver' => 'stack',
'channels' => ['daily'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
],
'dailyProd' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'notice',
'days' => 60,
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
'replace_placeholders' => true,
],
'slack' => [
@ -78,36 +77,44 @@ return [
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => 'critical',
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => SyslogUdpHandler::class,
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => 'debug',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => LOG_USER,
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => 'debug',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
@ -118,6 +125,23 @@ return [
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
'stdout' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDOUT_FORMATTER'),
'with' => [
'stream' => 'php://stdout',
],
],
'k8s' => [
'driver' => 'stack',
'channels' => ['daily', 'stdout'],
'ignore_exceptions' => false,
],
],
];

View file

@ -49,7 +49,7 @@ return [
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'auth_mode' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'),
],
'ses' => [
@ -57,16 +57,16 @@ return [
],
'mailgun' => [
'transport' => 'mailgun',
'transport' => 'mailgun'
],
'postmark' => [
'transport' => 'postmark',
'transport' => 'postmark'
],
'sendmail' => [
'transport' => 'sendmail',
'path' => '/usr/sbin/sendmail -bs',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
@ -77,6 +77,14 @@ return [
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
/*

132
config/purifier.php Normal file
View file

@ -0,0 +1,132 @@
<?php
/**
* Ok, glad you are here
* first we get a config instance, and set the settings
* $config = HTMLPurifier_Config::createDefault();
* $config->set('Core.Encoding', $this->config->get('purifier.encoding'));
* $config->set('Cache.SerializerPath', $this->config->get('purifier.cachePath'));
* if ( ! $this->config->get('purifier.finalize')) {
* $config->autoFinalize = false;
* }
* $config->load [$this->getConfig());
*
* You must NOT delete the default settings
* anything in settings should be compacted with params that needed to instance HTMLPurifier_Config.
*
* @link http://htmlpurifier.org/live/configdoc/plain.html
*/
// phpcs:disable Generic.Files.LineLength.TooLong
return [
'encoding' => 'UTF-8',
'finalize' => true,
'cachePath' => storage_path('app/purifier'),
'cacheFileMode' => 0755,
'settings' => [
'default' => [
'HTML.SafeIframe' => true,
// * For iframes
'URI.SafeIframeRegexp' => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/)%",
'HTML.Doctype' => 'HTML 4.01 Transitional',
// * Legacy filter en full avec safe legacy.
// 'HTML.Allowed' => 'h1[style|id],h2[style|id],h3[style|id],h4[style|id],h5[style|id],h6[style|id],figure[class|style],oembed[url],div[style],b,strong,i,em,u,s,pre,code[class],a[href|title|target|rel|id],ul[style],ol[style],dl[style],li,p[style],br,span[style|class],img[width|height|alt|src|style|class|title],iframe[src|width|height|class|frameborder|style],table[style|align|border|cellspacing|cellpadding|width],thead,tbody,th[colspan|align|valign|style],tr[align|valign|style],td[style|colspan|rowspan|scope|align|valign],hr,dl,dt,mark[class]',
// * Restricted basics.
'HTML.Allowed' => 'br,p[style],b[style],u[style],i[style],a[href|title|target|rel],strong[style],span[class],ul[style],ol[style],li[style],h2[style],h3[style],h4[style],hr,blockquote[style]',
// * Legacy filter en full avec safe legacy.
// 'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align,list-style-type,float,width,height,vertical-align,border,border-color,margin-left',
// * Restricted basics.
'CSS.AllowedProperties' => 'text-align,margin-left',
'AutoFormat.AutoParagraph' => false,
'AutoFormat.RemoveEmpty' => false,
'AutoFormat.RemoveEmpty.RemoveNbsp' => false,
],
"youtube" => [
"HTML.SafeIframe" => 'true',
"URI.SafeIframeRegexp" => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/)%",
],
'custom_definition' => [
'id' => 'html5-definitions',
'rev' => 1,
'debug' => true,
'elements' => [
// * http://developers.whatwg.org/sections.html
['section', 'Block', 'Flow', 'Common'],
['nav', 'Block', 'Flow', 'Common'],
['article', 'Block', 'Flow', 'Common'],
['aside', 'Block', 'Flow', 'Common'],
['header', 'Block', 'Flow', 'Common'],
['footer', 'Block', 'Flow', 'Common'],
// * Content model actually excludes several tags, not modelled here
['address', 'Block', 'Flow', 'Common'],
['hgroup', 'Block', 'Required: h1 | h2 | h3 | h4 | h5 | h6', 'Common'],
// * http://developers.whatwg.org/grouping-content.html
['figure', 'Block', 'Optional: (figcaption, Flow) | (Flow, figcaption) | Flow', 'Common'],
['figcaption', 'Inline', 'Flow', 'Common'],
['oembed', 'Inline', 'Flow', 'Common'],
// * http://developers.whatwg.org/the-video-element.html#the-video-element
['video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
'src' => 'URI',
'type' => 'Text',
'width' => 'Length',
'height' => 'Length',
'poster' => 'URI',
'preload' => 'Enum#auto,metadata,none',
'controls' => 'Bool',
]],
['source', 'Block', 'Flow', 'Common', [
'src' => 'URI',
'type' => 'Text',
]],
// * http://developers.whatwg.org/text-level-semantics.html
['s', 'Inline', 'Inline', 'Common'],
['var', 'Inline', 'Inline', 'Common'],
['sub', 'Inline', 'Inline', 'Common'],
['sup', 'Inline', 'Inline', 'Common'],
['mark', 'Inline', 'Inline', 'Common'],
['wbr', 'Inline', 'Empty', 'Core'],
// * http://developers.whatwg.org/edits.html
['ins', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
['del', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
],
'attributes' => [
['iframe', 'allowfullscreen', 'Bool'],
['table', 'height', 'Text'],
['td', 'border', 'Text'],
['th', 'border', 'Text'],
['tr', 'width', 'Text'],
['tr', 'height', 'Text'],
['tr', 'border', 'Text'],
['figure', 'class', 'Text'],
['figure', 'style', 'Text'],
['oembed', 'url', 'Text'],
['span', 'style', 'Text'],
['img', 'style', 'Text'],
],
],
'custom_attributes' => [
['a', 'target', 'Enum#_blank,_self,_target,_top'],
['a', 'id', 'Text'],
['ol', 'style', 'Text'],
['figure', 'style', 'Text'],
['img', 'style', 'Text'],
['span', 'style', 'Text'],
['oembed', 'url', 'Text'],
['h1', 'id', 'Text'],
['h2', 'id', 'Text'],
['h3', 'id', 'Text'],
['h4', 'id', 'Text'],
['h5', 'id', 'Text'],
['h6', 'id', 'Text'],
],
'custom_elements' => [
['u', 'Inline', 'Inline', 'Common'],
['oembed', 'Inline', 'Inline', 'Common'],
],
],
];

View file

@ -39,6 +39,7 @@ return [
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
'after_commit' => false,
],
'beanstalkd' => [
@ -47,6 +48,7 @@ return [
'queue' => 'default',
'retry_after' => 90,
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
@ -54,9 +56,10 @@ return [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'your-queue-name'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
@ -65,10 +68,27 @@ return [
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
'after_commit' => false,
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs

61
config/sentry.php Normal file
View file

@ -0,0 +1,61 @@
<?php
return [
'dsn' => env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')),
// The release version of your application.
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')).
'release' => env('SENTRY_RELEASE'),
// When left empty or `null` the Laravel environment will be used.
'environment' => env('SENTRY_ENVIRONMENT'),
'breadcrumbs' => [
// Capture Laravel logs in breadcrumbs.
'logs' => true,
// Capture SQL queries in breadcrumbs.
'sql_queries' => true,
// Capture bindings on SQL queries logged in breadcrumbs.
'sql_bindings' => false,
// Capture queue job information in breadcrumbs.
'queue_info' => true,
// Capture command information in breadcrumbs.
'command_info' => true,
],
'tracing' => [
// Trace queue jobs as their own transactions.
'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', false),
// Capture queue jobs as spans when executed on the sync driver.
'queue_jobs' => true,
// Capture SQL queries as spans.
'sql_queries' => true,
// Try to find out where the SQL query originated from and add it to the query spans.
'sql_origin' => true,
// Capture views as spans.
'views' => true,
// Indicates if the tracing integrations supplied by Sentry should be loaded.
'default_integrations' => true,
// Indicates that requests without a matching route should be traced.
'missing_routes' => false,
],
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send-default-pii.
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate.
'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_TRACES_SAMPLE_RATE'),
'before_send' => [\App\Lib\Helpers\ExceptionsHelper::class, 'sentryEventHandler'],
];

View file

@ -18,6 +18,7 @@ return [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
'scheme' => 'https',
],
'postmark' => [

View file

@ -100,7 +100,7 @@ return [
|
*/
'store' => env('SESSION_STORE', null),
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
@ -155,7 +155,7 @@ return [
|
*/
'domain' => env('SESSION_DOMAIN', null),
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------

View file

@ -15,6 +15,8 @@ return [
'paths' => [
resource_path('views'),
// ! for vendor publish (mail notifications par ex)
resource_path('views/vendor'),
],
/*

3
jsconfig.json Normal file
View file

@ -0,0 +1,3 @@
{
"include": ["./resources/js/**/*"]
}

View file

@ -1,40 +1,75 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production"
"prod": "vite build --mode production",
"dev": "NODE_ENV=development vite build -d --mode development",
"watch": "NODE_ENV=development vite build --mode development --watch",
"eslint": "node ./node_modules/.bin/eslint --max-warnings 0 -c .eslintrc.json ./resources",
"eslint-fix": "node ./node_modules/.bin/eslint --fix --max-warnings 0 -c .eslintrc.json ./resources",
"eslint-ci": "node ./node_modules/.bin/eslint --max-warnings 0 -c .eslintrc.json ./resources -f junit",
"stylelint": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --max-warnings 0 '**/resources/**/*.scss'",
"stylelint-fix": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --fix --max-warnings 0 '**/resources/**/*.scss'",
"stylelint-ci": "node ./node_modules/.bin/stylelint --config './.stylelintrc.json' --max-warnings 0 '**/resources/**/*.scss' -f json"
},
"devDependencies": {
"axios": "^0.21.1",
"bootstrap": "^4.6.0",
"@babel/types": "^7.21.2",
"@fancyapps/fancybox": "^3.5.7",
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.2",
"@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/vue-fontawesome": "^3.0.1",
"@rushstack/eslint-patch": "^1.1.4",
"@sipec/vue3-tags-input": "^3.0.4",
"@types/bootstrap": "^5.2.6",
"@types/toastify-js": "^1.11.1",
"@types/webpack-env": "^1.18.0",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"@vitejs/plugin-legacy": "^4.0.1",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"axios": "^1.1.2",
"bootstrap": "^5.3.1",
"chart.js": "^4.3.3",
"cropperjs": "^1.5.12",
"cross-env": "^7.0",
"cssnano": "^5.0.2",
"jquery": "^3.2",
"laravel-mix": "^6.0.19",
"laravel-mix-polyfill": "^3.0.0",
"lodash": "^4.17.19",
"popper.js": "^1.12",
"postcss": "^8.2.15",
"resolve-url-loader": "^4.0.0",
"sass": "^1.32.13",
"sass-loader": "^11.1.1",
"vue": "^2.6.12",
"vue-loader": "^15.9.6",
"vue-template-compiler": "^2.6.12"
},
"dependencies": {
"@fancyapps/fancybox": "^3.5.7",
"@fortawesome/fontawesome-free": "^5.15.3",
"chart.js": "^2.9.4",
"jquery": "^3.5.1",
"popper.js": "^1.16.1",
"scratchcard-js": "^1.4.10",
"swiper": "^6.6.1",
"v-clipboard": "^2.2.3"
"eslint": "^8.35.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-vue": "^9.4.0",
"laravel-vite-plugin": "^0.7.2",
"postcss": "^8.4.21",
"postcss-discard-comments": "^5.1.2",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
"prettier": "^2.7.1",
"resolve-url-loader": "^5.0.0",
"sass": "^1.58.3",
"sass-loader": "^12.6.0",
"scratchcard-js": "^1.5.0",
"slugify": "^1.6.5",
"stylelint": "^14.16.1",
"stylelint-config-idiomatic-order": "^8.1.0",
"stylelint-config-sass-guidelines": "^9.0.1",
"stylelint-config-standard-scss": "^6.1.0",
"stylelint-scss": "^4.4.0",
"stylelint-webpack-plugin": "^3.3.0",
"sweetalert2": "^11.4.8",
"swiper": "^10.2.0",
"toastify-js": "^1.12.0",
"ts-loader": "^9.3.1",
"typescript": "^4.9.5",
"typescript-eslint": "^0.0.1-alpha.0",
"v-clipboard": "^3.0.0-next.1",
"vite": "^4.0.0",
"vite-plugin-checker": "^0.5.6",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-stylelint": "^4.2.0",
"vue": "^3.2.38",
"vue-eslint-parser": "^9.1.0",
"vue-loader": "^16.8.3",
"vue-tsc": "^1.2.0",
"vue3-nestable": "^3.0.3"
}
}

View file

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="LDC Laravel" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
<ruleset name="LDC Laravel"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
<description>Created with the PHP Coding Standard Generator. http://edorian.github.com/php-coding-standard-generator/</description>
<arg name="colors" />
<arg name="extensions" value="php" />
<file>app/</file>
<file>config/</file>
<file>database/seeders</file>
<file>database/factories</file>
<file>routes/</file>
<rule ref="PSR1" />
<rule ref="PSR12" />
@ -19,6 +20,7 @@
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
<rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
<rule ref="Generic.Commenting.DocComment.SpacingBeforeTags"/>
<rule ref="Generic.Commenting.Todo"/>
<rule ref="Generic.Commenting.Fixme"/>
<rule ref="Generic.ControlStructures.InlineControlStructure"/>

View file

@ -12,20 +12,20 @@
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<coverage>
<include>
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
</include>
</coverage>
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<!-- <server name="DB_CONNECTION" value="sqlite"/> -->
<!-- <server name="DB_DATABASE" value=":memory:"/> -->
<server name="MAIL_MAILER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>

View file

@ -1,46 +0,0 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap');
require('@fancyapps/fancybox');
window.Vue = require('vue').default;
import Clipboard from 'v-clipboard';
import Chart from 'chart.js';
Vue.use(Clipboard);
/**
* The following block of code may be used to automatically register your
* Vue components. It will recursively scan this directory for the Vue
* components and automatically register them with their "basename".
*
* Eg. ./components/ExampleComponent.vue -> <example-component></example-component>
*/
// const files = require.context('./', true, /\.vue$/i)
// files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default))
Vue.component('win-notification', require('./components/WinNotification.vue').default);
Vue.component('gain-views', require('./components/GainViews.vue').default);
Vue.component('prizes', require('./components/Prizes.vue').default);
Vue.component('instant-win', require('./components/InstantWin.vue').default);
Vue.component('codes-ean', require('./components/CodesEAN.vue').default);
Vue.component('banned-ip', require('./components/BannedIP.vue').default);
Vue.component('faq-themes', require('./components/FaqThemes.vue').default);
Vue.component('faq-questions', require('./components/FaqQuestions.vue').default);
Vue.component('web-coupons', require('./components/WebCoupons.vue').default);
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
const app = new Vue({
el: '#app',
});

View file

@ -1,44 +0,0 @@
window._ = require('lodash');
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try {
window.Popper = require('popper.js').default;
window.$ = window.jQuery = require('jquery');
require('bootstrap');
} catch (e) { }
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
};
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// forceTLS: true
// });

View file

@ -1,237 +0,0 @@
<template>
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-md-3">
Liste des IP bannies
</div>
<div class="col-md-6 text-center">
<transition name="fade">
<span v-show="processing">
{{ response }}
</span>
</transition>
</div>
<div class="col-md-3 text-right">
<button class="btn btn-primary btn-sm text-white float-right" @click="saveData()" :disabled="processing">
Sauvegarder
<div class="spinner-border spinner-border-sm text-light" role="status" v-show="processing">
<span class="sr-only">Loading...</span>
</div>
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="col-md-10 offset-md-1">
<div class="row">
<label class="col-form-label col-md-6">Rechercher :</label>
<input type="text" @keyup="search" v-model="searchText" class="form-control form-control-sm col-md-6">
</div>
<hr/>
<table class="table table-bordered table-sm text-center">
<thead>
<tr>
<th class="w-75" scope="col">IP <span class="required-asterisk">*</span></th>
<th class="w-25" scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="item in displayedips" v-bind:key="item.id">
<td><input class="form-control form-control-sm" v-model="item.ip" type="text" disabled></td>
<td><button @click="removeItem(item.ip)" class="btn btn-dangerbtn-sm">Supprimer</button></td>
</tr>
</tbody>
</table>
<div class="form-group text-right">
<span v-show="filterMode"><i class="fas fa-exclamation-triangle"></i> L'ajout de ligne est désactiver lorsqu'un filtre est actif.</span>
<button class="btn btn-primary btn-sm text-white" :class="{ disabled: filterMode}" :disabled="filterMode" @click="newItem">Ajouter une ligne</button>
</div>
<nav aria-label="Pagination ips EAN">
<ul class="pagination justify-content-center">
<li class="page-item" :class="{ disabled: page == 1}">
<button type="button" class="page-link" :disabled="page == 1" @click="page = 1"> Première </button>
</li>
<li class="page-item" :class="{ disabled: page == 1}">
<button type="button" class="page-link" :disabled="page == 1" @click="page--">
<span aria-hidden="true">&laquo;</span>
</button>
</li>
<li class="page-item" :class="{ active: pageNumber == page}" v-for="pageNumber in resultPagination" @click="page = pageNumber">
<button type="button" class="page-link"> {{pageNumber}} </button>
</li>
<li class="page-item" :class="{ disabled: page == pages.length}">
<button type="button" @click="page++" :disabled="page == pages.length" class="page-link">
<span aria-hidden="true">&raquo;</span>
</button>
</li>
<li class="page-item" :class="{ disabled: page == pages.length}">
<button type="button" @click="page = pages.length" :disabled="page == pages.length" class="page-link"> Dernière </button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('BannedIP Component mounted.');
this.datas = JSON.parse(this.dbdatas);
this.baseDatas = JSON.parse(this.dbdatas);
},
props: [
'dbdatas',
],
data() {
return {
datas: [],
response: null,
processing: false,
page: 1,
perPage: 10,
pages: [],
baseDatas: [],
searchText: '',
filterMode: false,
resultPagination: []
}
},
methods: {
// Ajoute une ligne au tableau affiché
newItem() {
let ip = window.prompt("Entrer l'IP à bannir", "");
let ipExist = false;
if (ip != null) {
ipExist = this.baseDatas.filter(elem => elem.ip.toString().indexOf(ip) >= 0).length == 0 ? true : false;
}
if (ipExist) {
this.datas.push({ip: ip});
this.baseDatas.push({ip: ip});
this.setPages();
this.page = this.pages.length;
} else {
window.alert("L'IP entrée est déjà dans la liste.");
}
},
// Supprime une ligne dans la base si elle existait déjà
removeItem(ip) {
if (confirm("Êtes-vous sûr(e) ?")) {
let i = this.baseDatas.findIndex(element => element.ip == ip);
let iDatas = this.datas.findIndex(element => element.ip == ip);
if (i !== null && this.baseDatas[i]['ip'] !== null) {
axios.post('/bo/ajax/delete-banned-ip', {
ip: this.baseDatas[i]['ip'],
})
.catch(function (error) {
console.log(error);
});
this.baseDatas.splice(i, 1);
this.datas.splice(iDatas, 1);
} else {
this.baseDatas.splice(i, 1);
this.datas.splice(iDatas, 1);
}
}
},
// Sauvegarde les données dans la base
saveData() {
this.processing = true;
this.response = "En cours de sauvegarde...";
this.searchText = '';
axios.post('/bo/ajax/save-banned-ip', {
ips: this.baseDatas,
}).then(
(response) => {
this.datas = response.data;
this.baseDatas = JSON.parse(JSON.stringify(this.datas));
this.response = "Sauvegarde terminée !";
this.processing = false;
}
)
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la sauvegarde.";
this.processing = false;
});
},
// Calcul le nombre de page total dans la pagination
setPages () {
let numberOfPages = Math.ceil(this.datas.length / this.perPage);
if (this.pages.length > numberOfPages) {
this.pages.splice(numberOfPages);
}
for (let index = 0; index < numberOfPages; index++) {
if (!this.pages.includes(index + 1)) {
this.pages.push(index + 1);
}
}
this.paginationToPrint();
},
// S'occupe de la pagination des données affichées dans le tableau
paginate (datas) {
let page = this.page;
let perPage = this.perPage;
let from = (page * perPage) - perPage;
let to = (page * perPage);
return datas.slice(from, to);
},
// Recherche dans le tableau les ips ou produit corespondant à la recherche
search() {
if (this.page !== 1) {
this.page = 1;
}
if (this.searchText === "") {
this.filterMode = false;
} else {
this.filterMode = true;
}
let tmpIPs = this.baseDatas.filter(item => item.ip.toLowerCase().indexOf(this.searchText.toLowerCase()) >= 0);
this.datas = Object.assign(tmpIPs);
},
// Copie les données du tableau afficher dans le tableau baseDatas qui sert de référence pour sauvegarder, supprimer etc
updateBaseData(ip) {
this.baseDatas[this.datas.findIndex(element => element.ip == ip)] = this.datas[this.datas.findIndex(element => element.ip == ip)];
},
// Prépare les pages à afficher a l'écran pour la pagination
paginationToPrint() {
this.resultPagination = JSON.parse(JSON.stringify(this.pages));
if (this.page >= 3) {
if (this.page <= this.pages.length - 4) {
this.resultPagination.splice(0, this.page - 4);
} else {
this.resultPagination.splice(0, this.pages.length - 8);
}
}
if (this.page <= this.pages.length - 3) {
this.resultPagination.splice(8);
}
}
},
computed: {
displayedips () {
return this.paginate(this.datas);
}
},
watch: {
datas () {
this.setPages();
},
page () {
this.paginationToPrint();
}
}
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>

View file

@ -1,240 +0,0 @@
<template>
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-md-3">
Codes EAN
</div>
<div class="col-md-6 text-center">
<transition name="fade">
<span v-show="processing">
{{ response }}
</span>
</transition>
</div>
<div class="col-md-3 text-right">
<button class="btn btn-primary btn-sm text-white float-right" @click="saveData()" :disabled="processing">
Sauvegarder
<div class="spinner-border spinner-border-sm text-light" role="status" v-show="processing">
<span class="sr-only">Loading...</span>
</div>
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="col-md-10 offset-md-1">
<div class="row">
<label class="col-form-label col-md-6">Rechercher :</label>
<input type="text" @keyup="search" v-model="searchText" class="form-control form-control-sm col-md-6">
</div>
<hr/>
<table class="table table-bordered table-sm text-center">
<thead>
<tr>
<th class="w-25" scope="col">Code EAN <span class="required-asterisk">*</span></th>
<th class="w-50" scope="col">Produit concerné <span class="required-asterisk">*</span></th>
<th class="w-25" scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="item in displayedCodes" v-bind:key="item.id">
<td><input class="form-control form-control-sm" v-model="item.code" type="text" disabled></td>
<td><input class="form-control form-control-sm" v-model="item.product" type="text" @keyup="updateBaseData(item.code)" required></td>
<td><button @click="removeItem(item.id)" class="btn btn-danger btn-sm">Supprimer</button></td>
</tr>
</tbody>
</table>
<div class="form-group text-right">
<span v-show="filterMode"><i class="fas fa-exclamation-triangle"></i> L'ajout de ligne est désactiver lorsqu'un filtre est actif.</span>
<button class="btn btn-primary btn-sm text-white" :class="{ disabled: filterMode}" :disabled="filterMode" @click="newItem">Ajouter une ligne</button>
</div>
<nav aria-label="Pagination codes EAN">
<ul class="pagination justify-content-center">
<li class="page-item" :class="{ disabled: page == 1}">
<button type="button" class="page-link" :disabled="page == 1" @click="page = 1"> Première </button>
</li>
<li class="page-item" :class="{ disabled: page == 1}">
<button type="button" class="page-link" :disabled="page == 1" @click="page--">
<span aria-hidden="true">&laquo;</span>
</button>
</li>
<li class="page-item" :class="{ active: pageNumber == page}" v-for="pageNumber in resultPagination" @click="page = pageNumber">
<button type="button" class="page-link"> {{pageNumber}} </button>
</li>
<li class="page-item" :class="{ disabled: page == pages.length}">
<button type="button" @click="page++" :disabled="page == pages.length" class="page-link">
<span aria-hidden="true">&raquo;</span>
</button>
</li>
<li class="page-item" :class="{ disabled: page == pages.length}">
<button type="button" @click="page = pages.length" :disabled="page == pages.length" class="page-link"> Dernière </button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('CodesEAN Component mounted.');
this.datas = JSON.parse(this.dbdatas);
this.baseDatas = JSON.parse(this.dbdatas);
},
props: [
'dbdatas',
],
data() {
return {
datas: [],
response: null,
processing: false,
page: 1,
perPage: 10,
pages: [],
baseDatas: [],
searchText: '',
filterMode: false,
resultPagination: []
}
},
methods: {
// Ajoute une ligne au tableau affiché
newItem() {
let code = window.prompt('Entrer le code EAN', "");
let codeOk = false;
if (code != null) {
codeOk = this.baseDatas.filter(elem => elem.code == code).length == 0 ? true : false;
}
if (codeOk) {
this.datas.push({id: null, code: code, product: ''});
this.baseDatas.push({id: null, code: code, product: ''});
this.setPages();
this.page = this.pages.length;
} else {
window.alert('Le code entré existe déjà !');
}
},
// Supprime une ligne dans la base si elle existait déjà
removeItem(id) {
if (confirm("Êtes-vous sûr(e) ?")) {
let i = this.baseDatas.findIndex(element => element.id == id);
let iDatas = this.datas.findIndex(element => element.id == id);
if (i !== null && this.baseDatas[i]['id'] !== null) {
axios.post('/bo/ajax/delete-codes-ean', {
id: this.baseDatas[i]['id'],
})
.catch(function (error) {
console.log(error);
});
this.baseDatas.splice(i, 1);
this.datas.splice(iDatas, 1);
} else {
this.baseDatas.splice(i, 1);
this.datas.splice(iDatas, 1);
}
}
},
// Sauvegarde les données dans la base
saveData() {
this.processing = true;
this.response = "En cours de sauvegarde...";
this.searchText = '';
axios.post('/bo/ajax/save-codes-ean', {
codes: this.baseDatas,
}).then(
(response) => {
this.datas = response.data;
this.baseDatas = JSON.parse(JSON.stringify(this.datas));
this.response = "Sauvegarde terminée !";
this.processing = false;
}
)
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la sauvegarde.";
this.processing = false;
});
},
// Calcul le nombre de page total dans la pagination
setPages () {
let numberOfPages = Math.ceil(this.datas.length / this.perPage);
if (this.pages.length > numberOfPages) {
this.pages.splice(numberOfPages);
}
for (let index = 0; index < numberOfPages; index++) {
if (!this.pages.includes(index + 1)) {
this.pages.push(index + 1);
}
}
this.paginationToPrint();
},
// S'occupe de la pagination des données affichées dans le tableau
paginate (datas) {
let page = this.page;
let perPage = this.perPage;
let from = (page * perPage) - perPage;
let to = (page * perPage);
return datas.slice(from, to);
},
// Recherche dans le tableau les codes ou produit corespondant à la recherche
search() {
if (this.page !== 1) {
this.page = 1;
}
if (this.searchText === "") {
this.filterMode = false;
} else {
this.filterMode = true;
}
let tmpEAN = this.baseDatas.filter(item => item.code.toString().indexOf(this.searchText) >= 0);
let tmpProduct = this.baseDatas.filter(item => item.product.toLowerCase().indexOf(this.searchText.toLowerCase()) >= 0);
this.datas = Object.assign(tmpEAN, tmpProduct);
},
// Copie les données du tableau afficher dans le tableau baseDatas qui sert de référence pour sauvegarder, supprimer etc
updateBaseData(code) {
this.baseDatas[this.datas.findIndex(element => element.code == code)] = this.datas[this.datas.findIndex(element => element.code == code)];
},
// Prépare les pages à afficher a l'écran pour la pagination
paginationToPrint() {
this.resultPagination = JSON.parse(JSON.stringify(this.pages));
if (this.page >= 3) {
if (this.page <= this.pages.length - 4) {
this.resultPagination.splice(0, this.page - 4);
} else {
this.resultPagination.splice(0, this.pages.length - 8);
}
}
if (this.page <= this.pages.length - 3) {
this.resultPagination.splice(8);
}
}
},
computed: {
displayedCodes () {
return this.paginate(this.datas);
}
},
watch: {
datas () {
this.setPages();
},
page () {
this.paginationToPrint();
}
}
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>

View file

@ -1,159 +0,0 @@
<template>
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-md-3">
FAQ - Gestion des questions
</div>
<div class="col-md-6 text-center">
<transition name="fade">
<span v-show="processing">
{{ response }}
</span>
</transition>
</div>
<div class="col-md-3 text-right">
<button class="btn btn-primary btn-sm text-white float-right" @click="saveData()" :disabled="processing">
Sauvegarder
<div class="spinner-border spinner-border-sm text-light" role="status" v-show="processing">
<span class="sr-only">Loading...</span>
</div>
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="col-md-10 offset-md-1">
<div v-for="(item, index) in datas" :key="item.id">
<div class="form-inline">
<label class="col-form-label col-md-3">Question :</label>
<input type="text" v-model="item.question" class="form-control form-control-sm col-md-7" placeholder="Entrer votre question ici" required>
<a class="text-decoration-none col-md-2 col-form-label w-100 text-right" href="#_" @click="item.toggle = !item.toggle">
Détails
<i class="fas" :class="{ 'fa-chevron-down': !item.toggle, 'fa-chevron-up': item.toggle}"></i>
</a>
</div>
<transition name="slide-fade">
<div class="form-inline bg-light rounded" v-show="item.toggle">
<label class="col-form-label col-md-3">Réponse :</label>
<input type="text" v-model="item.answer" class="form-control form-control-sm col-md-7" placeholder="Entrer votre réponse ici" required><br/>
<div class="col-md-2 text-right">
<button class="btn btn-danger btn-sm" @click="removeItem(index)">Supprimer</button>
</div>
<div class="form-group col-md-4">
<label class="col-form-label">Votes positifs</label>
<input type="text" v-model="item.positive_vote" class="form-control form-control-sm" required>
</div>
<div class="form-group col-md-4">
<label class="col-form-label">Votes négatifs</label>
<input type="text" v-model="item.negative_vote" class="form-control form-control-sm" required>
</div>
<div class="form-group col-md-4">
<label class="col-form-label">Thème</label>
<select v-model="item.theme_id" class="custom-select custom-select-sm">
<option v-for="theme in themes" :key="theme.id" :value="theme.id" :selected="theme.id == item.theme_id">{{theme.name}}</option>
</select>
</div>
</div>
</transition>
<hr/>
</div>
<div class="form-group text-right">
<button class="btn btn-primary btn-sm text-white" @click="newItem">Ajouter une question</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('FaqQuestions Component mounted.');
this.datas = JSON.parse(this.dbdatas);
this.themes = JSON.parse(this.dbthemes);
},
props: [
'dbdatas',
'dbthemes',
],
data() {
return {
datas: [],
themes: [],
response: null,
processing: false
}
},
watch: {
},
methods: {
newItem() {
this.datas.push({id: null, question: '', answer: '', positive_vote: 0, negative_vote: 0, theme_id: null, toggle: false});
},
removeItem(index) {
if (confirm("Êtes-vous sûr(e) ?")) {
if (this.datas[index]['id'] !== null) {
this.processing = true;
this.response = "Suppression en cours...";
axios.post('/bo/ajax/delete-faq-question', {
id: this.datas[index]['id'],
}).then(
(response) => {
this.response = "Suppression terminée !";
this.processing = false;
}
)
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la suppression.";
this.processing = false;
});
this.datas.splice(index, 1);
} else {
this.datas.splice(index, 1);
}
}
},
saveData() {
this.processing = true;
this.response = "En cours de sauvegarde..."
axios.post('/bo/ajax/save-faq-question', {
questions: this.datas,
}).then(
(response) => {
this.datas = response.data;
this.response = "Sauvegarde terminée !";
this.processing = false;
}
)
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la sauvegarde.";
this.processing = false;
});
}
}
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .1s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to {
transform: translateX(10px);
opacity: 0;
}
</style>

View file

@ -1,148 +0,0 @@
<template>
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-md-3">
FAQ - Gestion des thèmes
</div>
<div class="col-md-6 text-center">
<transition name="fade">
<span v-show="processing">
{{ response }}
</span>
</transition>
</div>
<div class="col-md-3 text-right">
<button class="btn btn-primary btn-sm text-white float-right" @click="saveData()" :disabled="processing">
Sauvegarder
<div class="spinner-border spinner-border-sm text-light" role="status" v-show="processing">
<span class="sr-only">Loading...</span>
</div>
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="col-md-10 offset-md-1">
<table class="table table-bordered text-center">
<thead>
<tr>
<th class="w-75" scope="col">Nom du thème <span class="required-asterisk">*</span></th>
<th class="w-50" scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in datas" v-bind:key="item.id">
<td><input class="form-control form-control-sm" v-model="item.name" type="text" required></td>
<td>
<button @click="removeItem(index)" :disabled="processing" class="btn btn-danger btn-sm">
Supprimer
<div class="spinner-border spinner-border-sm text-light" role="status" v-show="processing">
<span class="sr-only">Loading...</span>
</div>
</button>
</td>
</tr>
</tbody>
</table>
<div class="form-group text-right">
<button class="btn btn-primary btn-sm text-white" @click="newItem">Ajouter une ligne</button>
<input type="hidden" :name="name" :value="JSON.stringify(datas)" required>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('FaqThemes Component mounted.');
this.datas = JSON.parse(this.dbdatas);
},
props: [
'name',
'dbdatas',
],
data() {
return {
datas: [],
response: null,
processing: false
}
},
watch: {
},
methods: {
newItem() {
this.datas.push({id: null, name: ''});
},
removeItem(index) {
if (confirm("Êtes-vous sûr(e) ?")) {
if (this.datas[index]['id'] !== null) {
this.processing = true;
this.response = "Suppression en cours...";
axios.post('/bo/ajax/delete-faq-theme', {
id: this.datas[index]['id'],
}).then(
(response) => {
let result = response.data;
switch (result) {
case 0: case 2:
// 0 = Supprimé, 2 = Pas encore en base, supprimer juste dans la vue
this.response = "Suppression terminée !";
this.datas.splice(index, 1);
this.processing = false;
break;
case 1:
// 1 = Non supprimer car des questions lui sont lié
this.response = "Erreur : des questions sont encore liées à ce thème.";
setTimeout(function() {
this.processing = false;
}.bind(this), 1500);
break;
}
}
)
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la suppression.";
this.processing = false;
});
} else {
this.datas.splice(index, 1);
}
}
},
saveData() {
this.processing = true;
this.response = "En cours de sauvegarde..."
axios.post('/bo/ajax/save-faq-theme', {
themes: this.datas,
}).then(
(response) => {
this.datas = response.data;
this.response = "Sauvegarde terminée !";
this.processing = false;
}
)
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la sauvegarde.";
this.processing = false;
});
}
}
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>

View file

@ -1,115 +0,0 @@
<template>
<div class="card">
<div class="card-header">
Gestion des vues correspondants au gain
<button class="btn btn-primary btn-sm text-white float-right" @click="saveData()">Sauvegarder</button>
</div>
<div class="card-body">
<div class="col-md-10 offset-md-1">
<table class="table table-bordered text-center">
<thead>
<tr>
<th scope="col">Nom de la page de gain <span class="required-asterisk">*</span></th>
<th scope="col">Nom du fichier <span class="required-asterisk">*</span></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in datas" v-bind:key="item.id">
<td><input class="form-control form-control-sm" v-model="item.name" type="text" required></td>
<td><input class="form-control form-control-sm" v-model="item.file_name" type="text" required></td>
<td><button @click="removeItem(index)" class="btn btn-danger btn-sm">Supprimer</button></td>
</tr>
</tbody>
</table>
<div class="form-group text-right">
<button class="btn btn-primary btn-sm text-white" @click="newItem">Ajouter une ligne</button>
<input type="hidden" :name="name" :value="JSON.stringify(datas)" required>
</div>
<transition name="fade">
<p class="alert alert-info" v-show="processing">
{{ response }}
</p>
</transition>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('GainViews Component mounted.');
this.datas = JSON.parse(this.dbdatas);
},
props: [
'name',
'dbdatas',
],
data() {
return {
datas: [],
response: null,
processing: false
}
},
watch: {
},
methods: {
newItem() {
this.datas.push({name: '', file_name: ''});
},
removeItem(index) {
if (confirm("Êtes-vous sûr(e) ?")) {
if (this.datas[index]['id'] !== undefined) {
this.processing = true;
this.response = "Suppression en cours...";
axios.post('/bo/ajax/delete-gain-views', {
id: this.datas[index]['id'],
}).then(
response => (this.response = response.data)
)
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la suppression.";
});
this.datas.splice(index, 1);
setTimeout(function() {
this.processing = false;
}.bind(this), 3000);
} else {
this.datas.splice(index, 1);
}
}
},
saveData() {
this.processing = true;
this.response = "En cours de sauvegarde..."
axios.post('/bo/ajax/save-gain-views', {
gainViews: this.datas,
}).then(
response => (this.datas = response.data)
)
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la sauvegarde.";
});
this.response = "Sauvegarde terminée !";
setTimeout(function() {
this.processing = false;
}.bind(this), 3000);
}
}
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>

View file

@ -1,85 +0,0 @@
<template>
<div>
<div class="card">
<div class="card-header">
Instants gagnants
<button v-if="cangenerate" class="btn btn-sm btn-success float-right" @click="generate()"><i class="fas fa-cog"></i> Générer</button>
<button v-else class="btn btn-sm btn-success float-right" v-clipboard="() => clipboard" @click="copyToClipboard()">
<span v-show="!copying"><i class="far fa-copy"></i> Exporter</span>
<span v-show="copying"> <i class="fas fa-check"></i> Texte copié !</span>
</button>
</div>
<div class="card-body">
<div v-if="cangenerate">
Aucun instant gagnant pour le moment.
</div>
<table class="table table-sm table-striped text-center" v-else>
<thead class="thead-light">
<tr>
<th class="w-25" scope="col">Date d'apparition</th>
<th class="w-50" scope="col">Lot</th>
<th class="w-25" scope="col">Gagné</th>
</tr>
</thead>
<tbody>
<tr v-for="item in datas" v-bind:key="item.id">
<td>{{item.appear_date}}</td>
<td>{{item.prize.label_one}}</td>
<td>{{item.won === false ? "Non" : "Oui"}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('Instant Win Component mounted.');
this.datas = JSON.parse(this.instantwin);
this.clipboard = "Voici la liste des instants gagnants :\n";
this.datas.forEach(
element => this.clipboard += "Le " + new Date(element.appear_date).toLocaleDateString() + " à "
+ element.appear_date.substring(element.appear_date.length - 8, element.appear_date.length - 3)
+ " - " + element.prize.label_one + "\n"
);
$('.toast').toast();
},
props: {
cangenerate: Number,
startdate: String,
enddate: String,
instantwin: String
},
data() {
return {
datas: [],
response: null,
clipboard: '',
copying: false
}
},
methods: {
generate() {
axios.get('/bo/ajax/instant-win-generate')
.then(
response => (this.datas = response.data)
)
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la génération.";
});
this.cangenerate = 0;
},
copyToClipboard() {
this.copying = true;
setTimeout(function() {
this.copying = false;
}.bind(this), 1000);
}
}
}
</script>

View file

@ -1,362 +0,0 @@
<template>
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-md-3">Lots</div>
<div class="col-md-6 text-center">
<transition name="fade">
<span v-show="processing">
{{ response }}
</span>
</transition>
</div>
<div class="col-md-3 text-right">
<button
class="btn btn-primary btn-sm text-white float-right"
@click="saveData()"
:disabled="processing"
>
Sauvegarder
<div
class="spinner-border spinner-border-sm text-light"
role="status"
v-show="processing"
>
<span class="sr-only">Loading...</span>
</div>
</button>
</div>
</div>
</div>
<!-- TODO : Mettre chaque lot en onglet -->
<div class="card-body">
<div class="col-md-10 offset-md-1">
<div v-for="(item, index) in datas">
<table class="table table-bordered">
<tbody>
<tr>
<td class="w-50">
ID du lot <span class="required-asterisk">*</span><br />
<small class="text-muted"
>ATTENTION ! Bien vérifier que chaque id de lot est unique et
correspond bien aux id de la table des IG</small
>
</td>
<td class="w-50 align-middle">
<input
class="form-control form-control-sm"
v-model="item.arcadia_id"
type="number"
required
/>
</td>
</tr>
<tr>
<td>Libellé au singulier <span class="required-asterisk">*</span></td>
<td class="align-middle">
<input
class="form-control form-control-sm"
v-model="item.label_one"
type="text"
required
/>
</td>
</tr>
<tr>
<td>Libellé au pluriel <span class="required-asterisk">*</span></td>
<td class="align-middle">
<input
class="form-control form-control-sm"
v-model="item.label_many"
type="text"
required
/>
</td>
</tr>
<tr>
<td colspan="2">
<div>
Argumentaire
<table class="table">
<tr>
<td>Titre <span class="required-asterisk">*</span></td>
<td class="align-middle">
<input
class="form-control form-control-sm"
v-model="item.arg_title"
type="text"
required
/>
</td>
</tr>
<tr>
<td>Sous-titre</td>
<td class="align-middle">
<input
class="form-control form-control-sm"
v-model="item.arg_subtitle"
type="text"
/>
</td>
</tr>
<tr>
<td>Texte <span class="required-asterisk">*</span></td>
<td class="align-middle">
<ck-editor-component-vue
@input="(dbtext) => (item.arg_content = dbtext)"
:id="`arg_content${index}`"
:name="`arg_content${index}`"
:required="true"
:dbtext="item.arg_content"
/>
</td>
</tr>
</table>
</div>
</td>
</tr>
<tr>
<td>Quantité <span class="required-asterisk">*</span></td>
<td class="align-middle">
<input
class="form-control form-control-sm"
v-model="item.quantity"
type="number"
required
/>
</td>
</tr>
<tr>
<td>
Code Opération Arcadia <span class="required-asterisk">*</span><br />
<small class="text-muted"
>Si ce champ est renseigné, un courrier sera créé en CRM pour l'export
Arcadia</small
>
</td>
<td class="align-middle">
<input
class="form-control form-control-sm"
v-model="item.arcadia_operation_code"
type="number"
required
/>
</td>
</tr>
<tr>
<td>
Code Support Arcadia <span class="required-asterisk">*</span><br />
<small class="text-muted"
>Si ce champ est renseigné, un courrier sera créé en CRM pour l'export
Arcadia</small
>
</td>
<td class="align-middle">
<input
class="form-control form-control-sm"
v-model="item.arcadia_support_code"
type="number"
required
/>
</td>
</tr>
<tr>
<td>Pdf preuve de gain</td>
<td class="align-middle">
<input
type="file"
accept="application/pdf"
name="pdf_winning_proof"
v-on:change="handleFileUpload($event, index)"
/>
<span v-if="item.pdf_winning_proof != null"
>Fichier actuel :
<span class="badge badge-primary">{{
item.pdf_winning_proof
}}</span></span
>
</td>
</tr>
<tr>
<td>
Preuve de gain obligatoire <span class="required-asterisk">*</span
><br />
<small class="text-muted"
>Si activé, il sera obligatoire de fournir des preuves de gain pour ce
lot (sticker, ticket de caisse, code barre).</small
>
</td>
<td class="align-middle">
<div class="onoffswitch">
<input
type="checkbox"
:id="'need_proof_' + index"
class="onoffswitch-checkbox"
v-model="item.need_proof"
/>
<label class="onoffswitch-label" :for="'need_proof_' + index">
<span class="onoffswitch-inner"></span>
<span class="onoffswitch-switch"></span>
</label>
</div>
</td>
</tr>
<tr>
<td>
Page de gain du lot <span class="required-asterisk">*</span><br />
</td>
<td class="align-middle">
<select class="custom-select" v-model="item.gain_view_id">
<option v-for="gv in gainviews" :value="gv.id" v-bind:key="gv.id">
{{ gv.name }}
</option>
</select>
</td>
</tr>
<tr>
<td>Supprimer le lot</td>
<td class="align-middle">
<button @click="removeItem(index)" class="btn btn-danger btn-sm">
Supprimer
</button>
</td>
</tr>
</tbody>
</table>
<hr />
</div>
<div class="form-group text-right">
<button class="btn btn-primary btn-sm text-white" @click="newItem">
Ajouter un lot
</button>
<input type="hidden" :name="name" :value="JSON.stringify(datas)" required />
</div>
</div>
</div>
</div>
</template>
<script>
import ckEditorComponentVue from "./ckEditorComponent.vue";
export default {
components: {
ckEditorComponentVue,
},
mounted() {
console.log("GainViews Component mounted.");
this.datas = JSON.parse(this.dbdatas);
this.gainviews = JSON.parse(this.dbgainviews);
},
props: ["name", "dbdatas", "dbgainviews"],
data() {
return {
datas: [],
gainviews: [],
response: null,
processing: false,
pdf_winning_proof: [],
};
},
watch: {},
methods: {
handleFileUpload(event, index) {
let files = event.target.files || event.dataTransfer.files;
if (!files.length) return;
this.pdf_winning_proof[index] = files[0];
if (!this.pdf_winning_proof[index]) {
event.preventDefault();
alert("Aucun fichier choisi.");
return;
}
if (this.pdf_winning_proof[index].size > 1024 * 1024 * 20) {
event.preventDefault();
alert("Fichier trop volumineux (> 20MB)");
return;
}
},
newItem() {
this.datas.push({
id: null,
arcadia_id: null,
label_one: "",
label_many: "",
arg_title: "",
arg_subtitle: "",
arg_content: "",
quantity: 0,
notifications: 0,
additional_form: 0,
arcadia_operation_code: null,
arcadia_support_code: null,
pdf_winning_proof: null,
need_proof: 1,
gain_view_id: null,
});
},
removeItem(index) {
if (confirm("Êtes-vous sûr(e) ?")) {
if (this.datas[index]["id"] !== undefined) {
this.processing = true;
this.response = "Suppression en cours...";
axios
.post("/bo/ajax/delete-prize", {
id: this.datas[index]["id"],
})
.then((response) => (this.response = response.data))
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la suppression.";
});
this.datas.splice(index, 1);
setTimeout(
function () {
this.processing = false;
}.bind(this),
3000
);
} else {
this.datas.splice(index, 1);
}
}
},
saveData() {
let formData = new FormData();
$.each(this.pdf_winning_proof, function (index, value) {
formData.append("pdf[" + index + "]", value);
});
formData.append("prizes", JSON.stringify(this.datas));
this.processing = true;
this.response = "En cours de sauvegarde...";
axios
.post("/bo/ajax/save-prizes", formData)
.then((response) => {
this.datas = response.data;
this.response = "Sauvegarde terminée !";
this.processing = false;
})
.catch(function (error) {
console.log(error);
this.response = "Erreur lors de la sauvegarde.";
this.processing = false;
});
},
},
};
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

View file

@ -1,74 +0,0 @@
<template>
<div>
<table class="table table-bordered">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Identifiant coupon <span class="required-asterisk">*</span></th>
<th scope="col">Pourcentage <span class="required-asterisk">*</span></th>
<th scope="col">Valeur faciale <span class="required-asterisk">*</span></th>
<th scope="col">Valable sur <span class="required-asterisk">*</span></th>
<th scope="col">Nom image <span class="required-asterisk">*</span></th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="emails">
<tr v-for="(item, index) in datas" v-bind:key="item.id">
<td>{{index + 1}}</td>
<td><input class="form-control form-control-sm" v-model="item.web_coupon_identifiant" type="text" required></td>
<td>
<input class="form-control form-control-sm" :class="{ border: totalPercentage > 100, 'border-danger': totalPercentage > 100}"
v-model="item.percentage" type="number" required>
</td>
<td><input class="form-control form-control-sm" v-model="item.face_value" type="text" required></td>
<td><input class="form-control form-control-sm" v-model="item.valid_on" type="text" required></td>
<td><input class="form-control form-control-sm" v-model="item.img_name" type="text" required></td>
<td><button @click="removeItem(index)" class="btn btn-danger btn-sm">Supprimer</button></td>
</tr>
</tbody>
</table>
<div class="form-group text-right">
<small v-show="totalPercentage > 100" class="text-danger">Poucentage total > 100%</small>
<a class="btn btn-primary btn-sm text-white" @click="newItem" role="button">Ajouter un coupon</a>
<input type="hidden" :name="name" :value="JSON.stringify(datas)" required>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('WebCoupons Component mounted.');
this.datas = JSON.parse(this.dbdatas);
},
props: [
'name',
'dbdatas',
],
data() {
return {
datas: []
}
},
watch: {
},
methods: {
newItem() {
this.datas.push({web_coupon_identifiant: '', percentage: 0, face_value: 0, valid_on: '', img_name: ''});
},
removeItem(index) {
if (confirm("Êtes-vous sûr(e) ?")) {
this.datas.splice(index, 1);
}
}
},
computed: {
totalPercentage() {
let total = 0;
this.datas.forEach(element => total += parseInt(element.percentage));
return total;
}
}
}
</script>

View file

@ -1,61 +0,0 @@
<template>
<table class="table table-bordered">
<tbody>
<tr class="form-group">
<td colspan="2">
<table class="table table-sm text-center">
<thead>
<tr>
<th class="w-auto" scope="col"></th>
<th class="w-75" scope="col">Email <span class="required-asterisk">*</span></th>
<th class="w-auto" scope="col"></th>
</tr>
</thead>
<tbody id="emails">
<tr v-for="(item, index) in datas" v-bind:key="item.id">
<td>{{index + 1}}</td>
<td><input class="form-control form-control-sm" v-model="item.email" type="email" required></td>
<td><button @click="removeItem(index)" class="btn btn-danger btn-sm">Supprimer</button></td>
</tr>
</tbody>
</table>
<div class="form-group text-right">
<a class="btn btn-primary btn-sm text-white" @click="newItem" role="button">Ajouter un email</a>
<input type="hidden" :name="name" :value="JSON.stringify(datas)" required>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
mounted() {
console.log('WinNotification Component mounted.');
this.datas = JSON.parse(this.dbdatas);
},
props: [
'name',
'dbdatas',
],
data() {
return {
datas: []
}
},
watch: {
},
methods: {
newItem() {
this.datas.push({email: ''});
},
removeItem(index) {
if (confirm("Êtes-vous sûr(e) ?")) {
this.datas.splice(index, 1);
}
}
}
}
</script>

View file

@ -1,252 +0,0 @@
<template>
<div>
<div class="wordCounter">
<span class="words"
><u>Mots</u>&nbsp;:&nbsp;<b>{{ words }}</b></span
>
<span class="chars"
><u>Caractères</u>&nbsp;:&nbsp;<b>{{ chars }}</b></span
>
</div>
<div ref="ckeditor"></div>
<input
:id="internalId"
class="form-control"
type="hidden"
:name="name"
:value="internalText"
:required="internalRequired"
/>
</div>
</template>
<script>
import "../vendor/ckeditor/build/ckeditor";
// import { CKUploadAdaptater } from '../../back/CkUploadAdaptater'
export default {
props: ["id", "required", "name", "dbtext"],
mounted() {
this.internalText = this.dbtext;
this.internalId = this.id ? this.id : "";
this.internalRequired = this.required ? this.required : false;
this.makeSuperBuild();
},
data() {
return {
internalText: "",
internalId: "",
internalRequired: false,
editor: null,
words: 0,
chars: 0,
emitTimer: null,
};
},
watch: {
dbtext() {
if (this.internalText != this.dbtext) {
this.internalText = this.dbtext;
if (this.editor) {
this.editor.setData(this.internalText);
}
}
},
},
methods: {
makeSuperBuild() {
let self = this;
ClassicEditor.create(self.$refs.ckeditor, {
// extraPlugins: [(editor) => {
// editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
// // Configure the URL to the upload script in your back-end here!
// return new CKUploadAdaptater(loader);
// }
// }],
wordCount: {
onUpdate: (stats) => {
this.chars = stats.characters;
this.words = stats.words;
},
},
link: {
// Automatically add target="_blank" and rel="noopener noreferrer" to all external links.
addTargetToExternalLinks: true,
// Let the users control the "download" attribute of each link.
decorators: [
{
mode: "manual",
label: "Ouvrir dans un nouvel onglet",
attributes: {
target: "_blank",
rel: "noopener noreferrer",
},
},
],
},
typing: {
transformations: {
extra: [
{ from: ":)", to: "😊" },
{ from: ":-P", to: "😛" },
{ from: "8-)", to: "😎" },
{ from: ";-)", to: "😉" },
{ from: ":-D", to: "😄" },
{ from: ":twisted:", to: "😈" },
{ from: ":mrgreen:", to: "😁" },
{ from: ":lol:", to: "😆" },
{ from: ":roll:", to: "🙄" },
{ from: ":-?", to: "😕" },
{ from: ":cry:", to: "😢" },
{ from: ":-o", to: "😮" },
{ from: ":evil:", to: "😡" },
{ from: ":-|", to: "😐" },
{ from: ":oops:", to: "😳" },
{ from: ":-x", to: "😠" },
{ from: "8-O", to: "😲" },
{ from: ":-(", to: "😧" },
{ from: ":arrow:", to: "➡" },
{ from: ":idea:", to: "💡" },
],
},
},
mediaEmbed: {
previewsInData: false,
removeProviders: [
"instagram",
"twitter",
"googleMaps",
"flickr",
"facebook",
"dailymotion",
"spotify",
"youtube",
],
// see https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-media-embed/src/mediaembedediting.js
providers: [
{
// display local videos in oembed tags
name: "localVideo",
url: /^(\/(?:content|storage)\/.*)/,
html: (match) => {
let path = match[1];
return (
'<video controls allowfullscreen style="max-width: 90%; margin: 0 auto; display: block;">' +
`<source src="${path}" type="video/mp4">` +
`<p>Votre navigateur ne prend pas en charge les vidéos HTML5. Voici <a href="${path}">un lien pour télécharger la vidéo</a>.</p>` +
"</video>"
);
},
},
],
},
toolbar: {
items: [
"bold",
"italic",
"underline",
"|",
"removeFormat",
"|",
"undo",
"redo",
],
},
table: {
contentToolbar: [
"tableColumn",
"tableRow",
"mergeTableCells",
"tableProperties",
"tableCellProperties",
"toggleTableCaption",
],
// Configuration of the TableProperties plugin.
tableProperties: {
// ...
},
// Configuration of the TableCellProperties plugin.
tableCellProperties: {
// ...
},
},
language: "fr",
image: {
toolbar: [
"imageTextAlternative",
"imageStyle:inline",
"imageStyle:block",
"imageStyle:side",
"linkImage",
],
},
// video: {
// resizeUnit: 'px',
// // Configure the available video resize options.
// resizeOptions: [
// {
// name: 'videoResize:original',
// value: null,
// label: 'Original',
// icon: 'original'
// },
// {
// name: 'videoResize:50',
// value: 50,
// label: '50',
// icon: 'medium'
// },
// {
// name: 'videoResize:75',
// value: '75',
// label: '75',
// icon: 'large'
// }
// ]
// }
})
.then((editor) => {
editor.setData(this.internalText);
self.editor = editor;
editor.model.document.on("change:data", () => {
// * Keep track of internal editor
let data = editor.getData();
self.$set(self, "internalText", data);
self.$emit("input", data);
});
})
.catch((error) => {
console.error("Oops, something went wrong!");
console.error(
"Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:"
);
console.warn("Build id: x0tjc6omqq4d-iydt761tuj51");
console.error(error);
});
},
},
};
</script>
<style lang="scss">
.wordCounter {
color: initial;
border: 1px solid rgb(196, 196, 196);
border-bottom: 0;
background-color: #fafafa;
position: relative;
.words {
position: relative;
left: 2rem;
}
.chars {
position: absolute;
left: 9rem;
}
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
!function(e){const n=e.af=e.af||{};n.dictionary=Object.assign(n.dictionary||{},{"%0 of %1":"%0 van %1","Align center":"Belyn in die middel","Align left":"Belyn links","Align right":"Belyn regs","Block quote":"Verwysingsaanhaling",Bold:"Vet",Cancel:"Kanselleer",Italic:"Kursief",Justify:"Belyn beide kante","Remove color":"Verwyder kleur","Remove Format":"Verwyder formatering","Restore default":"Herstel verstek",Save:"Stoor","Show more items":"Wys meer items","Text alignment":"Teksbelyning","Text alignment toolbar":"Teksbelyning nutsbank",Underline:"Onderstreep"}),n.getPluralForm=function(e){return 1!=e}}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
!function(e){const n=e.af=e.af||{};n.dictionary=Object.assign(n.dictionary||{},{"%0 of %1":"%0 van %1","Align center":"Belyn in die middel","Align left":"Belyn links","Align right":"Belyn regs","Block quote":"Verwysingsaanhaling",Bold:"Vet",Cancel:"Kanselleer",Italic:"Kursief",Justify:"Belyn beide kante","Remove color":"Verwyder kleur","Remove Format":"Verwyder formatering",Save:"Stoor","Show more items":"Wys meer items","Text alignment":"Teksbelyning","Text alignment toolbar":"Teksbelyning nutsbank",Underline:"Onderstreep"}),n.getPluralForm=function(e){return 1!=e}}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
!function(e){const t=e.ast=e.ast||{};t.dictionary=Object.assign(t.dictionary||{},{"%0 of %1":"",Aquamarine:"",Black:"",Blue:"",Bold:"Negrina","Break text":"","Bulleted List":"Llista con viñetes","Bulleted list styles toolbar":"",Cancel:"Encaboxar","Centered image":"","Change image text alternative":"",Circle:"",Decimal:"","Decimal with leading zero":"","Dim grey":"",Disc:"",Downloadable:"","Dropdown toolbar":"","Edit block":"","Edit link":"","Editor editing area: %0":"","Editor toolbar":"","Full size image":"Imaxen a tamañu completu",Green:"",Grey:"","Image resize list":"","Image toolbar":"","image widget":"complementu d'imaxen","In line":"","Insert image":"",Italic:"Cursiva","Left aligned image":"","Light blue":"","Light green":"","Light grey":"",Link:"Enllazar","Link image":"","Link URL":"URL del enllaz","List properties":"","Lower-latin":"","Lower–roman":"",Next:"","Numbered List":"Llista numberada","Numbered list styles toolbar":"","Open in a new tab":"","Open link in new tab":"",Orange:"",Original:"",Previous:"",Purple:"",Red:"",Redo:"Refacer","Remove color":"","Resize image":"","Resize image to %0":"","Resize image to the original size":"","Restore default":"","Reversed order":"","Rich Text Editor":"Editor de testu arriquecíu","Right aligned image":"",Save:"Guardar","Show more items":"","Side image":"Imaxen llateral",Square:"","Start at":"","Start index must be greater than 0.":"","Text alternative":"","This link has no URL":"","Toggle the circle list style":"","Toggle the decimal list style":"","Toggle the decimal with leading zero list style":"","Toggle the disc list style":"","Toggle the lower–latin list style":"","Toggle the lower–roman list style":"","Toggle the square list style":"","Toggle the upper–latin list style":"","Toggle the upper–roman list style":"",Turquoise:"",Underline:"",Undo:"Desfacer",Unlink:"Desenllazar","Upload failed":"","Upper-latin":"","Upper-roman":"",White:"","Wrap text":"",Yellow:""}),t.getPluralForm=function(e){return 1!=e}}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
!function(e){const t=e.ast=e.ast||{};t.dictionary=Object.assign(t.dictionary||{},{"%0 of %1":"",Aquamarine:"",Black:"",Blue:"",Bold:"Negrina","Bulleted List":"Llista con viñetes","Bulleted list styles toolbar":"",Cancel:"Encaboxar",Circle:"",Decimal:"","Decimal with leading zero":"","Dim grey":"",Disc:"",Downloadable:"","Dropdown toolbar":"","Edit block":"","Edit link":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor editing area: %0":"","Editor toolbar":"",Green:"",Grey:"",Italic:"Cursiva","Light blue":"","Light green":"","Light grey":"",Link:"Enllazar","Link URL":"URL del enllaz","List properties":"","Lower-latin":"","Lower–roman":"",Next:"","Numbered List":"Llista numberada","Numbered list styles toolbar":"","Open in a new tab":"","Open link in new tab":"",Orange:"",Previous:"",Purple:"",Red:"",Redo:"Refacer","Remove color":"","Reversed order":"","Rich Text Editor":"Editor de testu arriquecíu",Save:"Guardar","Show more items":"",Square:"","Start at":"","Start index must be greater than 0.":"","This link has no URL":"","Toggle the circle list style":"","Toggle the decimal list style":"","Toggle the decimal with leading zero list style":"","Toggle the disc list style":"","Toggle the lower–latin list style":"","Toggle the lower–roman list style":"","Toggle the square list style":"","Toggle the upper–latin list style":"","Toggle the upper–roman list style":"",Turquoise:"",Underline:"",Undo:"Desfacer",Unlink:"Desenllazar","Upper-latin":"","Upper-roman":"",White:"",Yellow:""}),t.getPluralForm=function(e){return 1!=e}}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
!function(a){const i=a.bs=a.bs||{};i.dictionary=Object.assign(i.dictionary||{},{"%0 of %1":"%0 od %1","Align center":"Centrirati","Align left":"Lijevo poravnanje","Align right":"Desno poravnanje",Big:"","Block quote":"Citat",Bold:"Podebljano","Break text":"",Cancel:"Poništi","Centered image":"Centrirana slika","Change image text alternative":"Promijeni ALT atribut za sliku","Choose heading":"Odaberi naslov",Default:"Zadani","Document colors":"","Font Color":"Boja","Font Family":"Font","Font Size":"Veličina fonta","Full size image":"",Heading:"Naslov","Heading 1":"Naslov 1","Heading 2":"Naslov 2","Heading 3":"Naslov 3","Heading 4":"Naslov 4","Heading 5":"Naslov 5","Heading 6":"Naslov 6","Horizontal line":"Horizontalna linija",Huge:"","Image resize list":"Lista veličina slike","Image toolbar":"","image widget":"","In line":"","Insert image":"Umetni sliku",Italic:"Zakrivljeno",Justify:"","Left aligned image":"Lijevo poravnata slika",Original:"Original",Paragraph:"Paragraf","Remove color":"Ukloni boju","Resize image":"Promijeni veličinu slike","Resize image to %0":"","Resize image to the original size":"Postavi originalnu veličinu slike","Restore default":"Vrati na zadano","Right aligned image":"Desno poravnata slika",Save:"Sačuvaj","Show more items":"Prikaži više stavki","Side image":"",Small:"","Text alignment":"Poravnanje teksta","Text alignment toolbar":"Traka za poravnanje teksta","Text alternative":"ALT atribut",Tiny:"",Underline:"Podcrtano","Upload failed":"Učitavanje slike nije uspjelo","Wrap text":"Prelomi tekst"}),i.getPluralForm=function(a){return a%10==1&&a%100!=11?0:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?1:2}}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
!function(a){const n=a.bs=a.bs||{};n.dictionary=Object.assign(n.dictionary||{},{"%0 of %1":"%0 od %1","Align center":"Centrirati","Align left":"Lijevo poravnanje","Align right":"Desno poravnanje",Big:"","Block quote":"Citat",Bold:"Podebljano",Cancel:"Poništi","Choose heading":"Odaberi naslov",Default:"Zadani","Document colors":"","Font Color":"Boja","Font Family":"Font","Font Size":"Veličina fonta",Heading:"Naslov","Heading 1":"Naslov 1","Heading 2":"Naslov 2","Heading 3":"Naslov 3","Heading 4":"Naslov 4","Heading 5":"Naslov 5","Heading 6":"Naslov 6","Horizontal line":"Horizontalna linija",Huge:"",Italic:"Zakrivljeno",Justify:"",Paragraph:"Paragraf","Remove color":"Ukloni boju",Save:"Sačuvaj","Show more items":"Prikaži više stavki",Small:"","Text alignment":"Poravnanje teksta","Text alignment toolbar":"Traka za poravnanje teksta",Tiny:"",Underline:"Podcrtano"}),n.getPluralForm=function(a){return a%10==1&&a%100!=11?0:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?1:2}}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
!function(e){const i=e.eo=e.eo||{};i.dictionary=Object.assign(i.dictionary||{},{"%0 of %1":"",Aquamarine:"",Black:"",Blue:"",Bold:"grasa","Break text":"","Bulleted List":"Bula Listo","Bulleted list styles toolbar":"",Cancel:"Nuligi","Centered image":"","Change image text alternative":"Ŝanĝu la alternativan tekston de la bildo","Choose heading":"Elektu ĉapon",Circle:"",Decimal:"","Decimal with leading zero":"","Dim grey":"",Disc:"",Downloadable:"","Dropdown toolbar":"","Edit block":"","Edit link":"","Editor editing area: %0":"","Editor toolbar":"","Full size image":"Bildo kun reala dimensio",Green:"",Grey:"",Heading:"Ĉapo","Heading 1":"Ĉapo 1","Heading 2":"Ĉapo 2","Heading 3":"Ĉapo 3","Heading 4":"","Heading 5":"","Heading 6":"","Image resize list":"","Image toolbar":"","image widget":"bilda fenestraĵo","In line":"","Insert image":"Enmetu bildon",Italic:"kursiva","Left aligned image":"","Light blue":"","Light green":"","Light grey":"",Link:"Ligilo","Link image":"","Link URL":"URL de la ligilo","List properties":"","Lower-latin":"","Lower–roman":"",Next:"","Numbered List":"Numerita Listo","Numbered list styles toolbar":"","Open in a new tab":"","Open link in new tab":"",Orange:"",Original:"",Paragraph:"Paragrafo",Previous:"",Purple:"",Red:"",Redo:"Refari","Remove color":"","Resize image":"","Resize image to %0":"","Resize image to the original size":"","Restore default":"","Reversed order":"","Rich Text Editor":"Redaktilo de Riĉa Teksto","Right aligned image":"",Save:"Konservi","Show more items":"","Side image":"Flanka biildo",Square:"","Start at":"","Start index must be greater than 0.":"","Text alternative":"Alternativa teksto","This link has no URL":"","Toggle the circle list style":"","Toggle the decimal list style":"","Toggle the decimal with leading zero list style":"","Toggle the disc list style":"","Toggle the lower–latin list style":"","Toggle the lower–roman list style":"","Toggle the square list style":"","Toggle the upper–latin list style":"","Toggle the upper–roman list style":"",Turquoise:"",Underline:"",Undo:"Malfari",Unlink:"Malligi","Upload failed":"","Upper-latin":"","Upper-roman":"",White:"","Wrap text":"",Yellow:""}),i.getPluralForm=function(e){return 1!=e}}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
!function(e){const i=e.eo=e.eo||{};i.dictionary=Object.assign(i.dictionary||{},{"%0 of %1":"",Aquamarine:"",Black:"",Blue:"",Bold:"grasa","Bulleted List":"Bula Listo","Bulleted list styles toolbar":"",Cancel:"Nuligi","Choose heading":"Elektu ĉapon",Circle:"",Decimal:"","Decimal with leading zero":"","Dim grey":"",Disc:"",Downloadable:"","Dropdown toolbar":"","Edit block":"","Edit link":"","Editor block content toolbar":"","Editor contextual toolbar":"","Editor editing area: %0":"","Editor toolbar":"",Green:"",Grey:"",Heading:"Ĉapo","Heading 1":"Ĉapo 1","Heading 2":"Ĉapo 2","Heading 3":"Ĉapo 3","Heading 4":"","Heading 5":"","Heading 6":"",Italic:"kursiva","Light blue":"","Light green":"","Light grey":"",Link:"Ligilo","Link URL":"URL de la ligilo","List properties":"","Lower-latin":"","Lower–roman":"",Next:"","Numbered List":"Numerita Listo","Numbered list styles toolbar":"","Open in a new tab":"","Open link in new tab":"",Orange:"",Paragraph:"Paragrafo",Previous:"",Purple:"",Red:"",Redo:"Refari","Remove color":"","Reversed order":"","Rich Text Editor":"Redaktilo de Riĉa Teksto",Save:"Konservi","Show more items":"",Square:"","Start at":"","Start index must be greater than 0.":"","This link has no URL":"","Toggle the circle list style":"","Toggle the decimal list style":"","Toggle the decimal with leading zero list style":"","Toggle the disc list style":"","Toggle the lower–latin list style":"","Toggle the lower–roman list style":"","Toggle the square list style":"","Toggle the upper–latin list style":"","Toggle the upper–roman list style":"",Turquoise:"",Underline:"",Undo:"Malfari",Unlink:"Malligi","Upper-latin":"","Upper-roman":"",White:"",Yellow:""}),i.getPluralForm=function(e){return 1!=e}}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));

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