Compare commits

...

No commits in common. "develop" and "laravel-8" have entirely different histories.

323 changed files with 23322 additions and 5734 deletions

View file

@ -31,16 +31,9 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
# Exception Handler
MAIL_LIST_SERVICEWEB=
# Google Analytics
GOOGLE_ANALYTICS_ID=
# Google Recaptcha
GOOGLE_RECAPTCHA_PUBLIC=
GOOGLE_RECAPTCHA_PRIVATE=
GOOGLE_RECAPTCHA_VERSION=
# LDC Web Status Checker
# SENTRY
SENTRY_LARAVEL_DSN=
# LDC WEB STATUS CHECKER
LDC_STATUS_CHECKER_API_KEY="MY_KEY"
LDC_STATUS_CHECKER_UUID="UUID_PROJECT"
# LDC Web Admin Secure
LDC_WEB_API_KEY="MY_KEY"

41
.eslintrc.json Normal file
View file

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

9
.gitignore vendored
View file

@ -1,19 +1,14 @@
/node_modules
/public/hot
/public/storage
/public/css
/public/js
/public/images
/public/fonts
/public/build
/public/medias
/public/uploads
/public/assets
/public/cache
/public/mix-manifest.json
/public/sitemap.xml
/storage/*.key
/vendor
.env
.env.backup
.phpunit.result.cache
docker-compose.override.yml
Homestead.json

View file

@ -1,118 +0,0 @@
stages:
- Security
- CodingStandards
- Builds
composer-security:
stage: Security
image: $CI_REGISTRY/service-web-lab/ldc-web-images-docker/ldcweb-qa:php7.3-1.0
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
needs: []
npm-security:
stage: Security
image: node:17.9.1
before_script:
- npm install --location=global audit-ci@^6
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
needs: []
phpcs:
stage: CodingStandards
image: $CI_REGISTRY/service-web-lab/ldc-web-images-docker/ldcweb-qa:php7.3-1.0
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
needs:
- composer-security
- npm-security
eslint:
stage: CodingStandards
image: node:17.9.1
before_script:
- npm install
cache:
paths:
- node_modules/
script:
- npm run eslint-ci --silent 2>/dev/null 1> eslint-report.xml
artifacts:
when: on_failure
expose_as: 'eslint'
paths:
- eslint-report.xml
reports:
junit: eslint-report.xml
allow_failure: false
needs:
- composer-security
- npm-security
stylelint:
stage: CodingStandards
image: node:17.9.1
before_script:
- npm install
cache:
paths:
- node_modules/
script:
- npm run stylelint-ci --silent 2>/dev/null 1> stylelint-report.json
artifacts:
when: on_failure
expose_as: 'stylelint'
paths:
- stylelint-report.json
allow_failure: false
needs:
- composer-security
- npm-security
front-build:
stage: Builds
image: node:17.9.1
before_script:
- npm install
cache:
paths:
- node_modules/
script:
- npm run prod > front-build-report.json
artifacts:
when: on_failure
expose_as: 'front-build-report'
paths:
- front-build-report.json
allow_failure: false
needs:
- phpcs
- eslint
- stylelint

14
.styleci.yml Normal file
View file

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

43
.stylelintrc.json Normal file
View file

@ -0,0 +1,43 @@
{
"extends": [
"stylelint-config-prettier",
"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,
"scss/double-slash-comment-empty-line-before": [
"always",
{
"ignore": [
"between-comments",
"stylelint-commands",
"inside-block"
]
}
]
}
}

22
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#27b2e7",
"activityBar.background": "#27b2e7",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#fce2f5",
"activityBarBadge.foreground": "#15202b",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#27b2e7",
"statusBar.background": "#1695c5",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#27b2e7",
"statusBarItem.remoteBackground": "#1695c5",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#1695c5",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#1695c599",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.remoteColor": "#1695c5"
}

View file

@ -1,10 +0,0 @@
# 01/08/2022
init
with packages :
- kwaadpepper/exception-handler
- kwaadpepper/refresh-sitemap
- ldc/web-admin-secure-laravel
- ldc/web-status-checker-laravel
with Google Services keys
with gitlab-ci config file

View file

@ -1,29 +1,7 @@
# Laravel Base Project
## Installation
```
cp .env.example .env
php artisan key:generate
```
## reste à faire
## Crons
Put every min in PROD :
```
php artisan schedule:run
```
### LDC Web Status Report
#### Available Commands
```
ldc:status-report
ldc:send-status-report
```
### LDC Web Admin Secure
#### Available Commands
```
ldcweb:admin-secure-sync
```
### Refresh-Sitemap
#### Available Commands
```
sitemap:refresh --dry-run
```
FO :
ts
view
route
les fichiers à la racine

View file

@ -7,9 +7,6 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
/**
* Creates a new user
*/
class CreateAdmin extends Command
{
/**
@ -29,7 +26,7 @@ class CreateAdmin extends Command
/**
* Execute the console command.
*
* @return integer
* @return void
*/
public function handle()
{
@ -50,41 +47,52 @@ class CreateAdmin extends Command
continue;
}
}
$name = null;
while (is_null($name)) {
$firstname = null;
while (is_null($firstname)) {
try {
$tmp = $this->ask('Type the wanted user name', 'Admin');
$validator = Validator::make(['name' => $tmp], [
'name' => 'required|min:5,max:255'
$tmp = $this->ask('Type the wanted user firstname', 'Admin');
$validator = Validator::make(['firstname' => $tmp], [
'firstname' => 'required|min:5,max:255'
]);
$validator->validate();
$name = $tmp;
$firstname = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid name %s',
'Please provide a valid firstname %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
$password = null;
while (
($password = $this->secret(
(!\is_null($password) ? 'Failed to confirm, ' : '') . 'Type the wanted user password'
)) !== $this->secret('Confirm the password')
) {
continue;
};
$lastname = null;
while (is_null($lastname)) {
try {
$tmp = $this->ask('Type the wanted user lastname', 'Saïtama');
$validator = Validator::make(['lastname' => $tmp], [
'lastname' => 'required|min:5,max:255'
]);
$validator->validate();
$lastname = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid lastname %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
}
}
$admin = new User([
'name' => $name,
'email' => $email,
'password' => $password
]);
$admin->saveOrFail();
$password = $this->secret('Type the wanted user password');
$user = new User();
$user->firstname = $firstname;
$user->lastname = $lastname;
$user->email = $email;
$user->password = $password;
$user->email_verified_at = now();
$user->saveOrFail();
$this->info('User created.');
return 0;
}
}

View file

@ -2,7 +2,7 @@
namespace App\Exceptions;
use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
@ -34,6 +34,9 @@ class Handler extends ExceptionHandler
public function register()
{
$this->reportable(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
}

View file

@ -3,24 +3,12 @@
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class LoginController extends Controller
{
use AuthenticatesUsers;
use ThrottlesLogins;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
@ -32,6 +20,16 @@ class LoginController extends Controller
$this->middleware('guest:backend')->except('logout');
}
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard()
{
return Auth::guard('backend');
}
/**
* Show the application's login form.
*
@ -43,57 +41,11 @@ class LoginController extends Controller
}
/**
* Attempt to log the user into the application.
*
* @param \Illuminate\Http\Request $request
* @return boolean
*/
protected function attemptLogin(Request $request)
{
return Auth::guard('backend')->attempt(
$this->credentials($request),
$request->filled('remember')
);
}
/**
* The user has been authenticated.
*
* @param \Illuminate\Http\Request $request
* @param null $user
* @return mixed
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing, Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
*/
protected function authenticated(Request $request, $user)
{
/** @var \App\Models\User|null */
$user = Auth::guard('backend')->user();
if ($user) {
$user->last_connection = now();
$user->saveOrFail();
Session::flash('info', trans('Vous êtes à présent connecté'));
}
}
/**
* The user has logged out of the application.
*
* @param \Illuminate\Http\Request $request
* @return mixed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
protected function loggedOut(Request $request)
{
Session::flash('info', trans('Vous êtes à présent déconnecté'));
}
/**
* Get the post register / login redirect path.
*
* Where to redirect users after login.
* @return string
*/
public function redirectPath()
protected function redirectTo(): string
{
return response()->redirectToRoute('bo.dashboard')->getTargetUrl();
return response()->redirectToIntended()->getTargetUrl();
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers\Bo\Auth;
use App\Http\Controllers\Controller;
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()
{
return Auth::guard('backend');
}
/**
* Where to redirect users after login.
* @return string
*/
protected function redirectTo(): string
{
return \route('bo.login');
}
}

View file

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

View file

@ -1,18 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
class HomeController extends Controller
{
/**
* Dashboard view
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function dashboard()
{
return view('bo.dashboard');
}
}

View file

@ -20,12 +20,13 @@ class Controller extends BaseController
* Build Search query on specified fields
* splitting search into words or using whole sentence
*
* @param 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 Builder
* @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,
@ -56,10 +57,10 @@ class Controller extends BaseController
/**
* Build query search on all fields
*
* @param Builder $query
* @param string $search
* @param array $fields
* @return Builder
* @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
{
@ -86,9 +87,9 @@ class Controller extends BaseController
/**
* Sort columns with a query
*
* @param Builder $query
* @param array $ignore
* @return Builder
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $ignore
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function sortQuery(Builder $query, array $ignore = []): Builder
{
@ -96,11 +97,14 @@ class Controller extends BaseController
$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");
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Fo;
use App\Http\Controllers\Controller;
class HomeController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Contracts\View\View
*/
public function index()
{
return view('fo.pages.home');
}
}

View file

@ -34,12 +34,16 @@ class Kernel extends HttpKernel
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
]
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
@ -59,6 +63,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
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
];
}

View file

@ -10,13 +10,13 @@ class Authenticate extends Middleware
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @param mixed $request
* @return string|null
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing
*/
protected function redirectTo($request)
{
$logins = [
'frontend' => route('login'),
'backend' => route('bo.login')
];
if (!$request->expectsJson()) {
@ -31,7 +31,7 @@ class Authenticate extends Middleware
}
}
}
return collect($logins)->first() ?? '/';
return route('login');
}
abort(401);
}

View file

@ -2,7 +2,7 @@
namespace App\Http\Middleware;
use App\Lib\ToolboxHelper;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Http\Request;
class HtmlSanitizer
@ -18,11 +18,17 @@ class HtmlSanitizer
public function handle(Request $request, \Closure $next, ...$params)
{
$inputs = $request->all();
array_walk_recursive($inputs, function (&$input, $name) use ($params) {
/** @var callable|null */
$cleanInput = null;
$cleanInput = function (&$input, $name, $rootName = null) use ($params, &$cleanInput) {
// * Nested element array
if (is_array($input)) {
array_walk_recursive($input, $cleanInput, $name);
}
if (!is_string($input)) {
return;
}
if (in_array($name, $params)) {
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.
@ -39,7 +45,8 @@ class HtmlSanitizer
} else {
$input = strip_tags($input);
}
});
};
\array_walk($inputs, $cleanInput);
$request->merge($inputs);
return $next($request);
}

View file

@ -11,6 +11,5 @@ class PreventRequestsDuringMaintenance extends Middleware
*
* @var array<int, string>
*/
protected $except = [
];
protected $except = [];
}

View file

@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -10,21 +11,28 @@ class RedirectIfAuthenticated
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null ...$guards
* @phpcs:disable Generic.Files.LineLength.TooLong
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @param string|null ...$guards
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
* @throws \RuntimeException If guard is unhandled.
* @phpcs:enable
*/
public function handle(Request $request, \Closure $next, ...$guards)
public function handle(Request $request, Closure $next, ...$guards)
{
$logins = [
'backend' => route('bo.dashboard')
];
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return \redirect()->to($logins[$guard]);
switch ($guard) {
case 'frontend':
return \redirect()->route('home');
case 'backend':
return \redirect()->route('bo.dashboard');
default:
throw new \RuntimeException("Unhandled guard `{$guard}` .");
}
}
}

View file

@ -32,9 +32,10 @@ class SecureHeaders
{
View::share('nonce', $nonce = \uniqid());
// * For VueJS dev tool to execute
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-$nonce'";
$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)) : '';
\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');
@ -48,16 +49,24 @@ class SecureHeaders
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob:;",
"img-src * data: blob:;",
// "frame-src https://www.youtube.com;",
"font-src 'self' data:;",
'frame-src ' . implode(' ', []) . ';',
"font-src 'self' data: ;",
// ! Using CKEditor will must have unsafe-inline style
// phpcs:ignore Generic.Files.LineLength.TooLong
"style-src 'self' 'unsafe-inline';",
"style-src 'self' 'unsafe-inline' " . \implode(' ', [
// * File Manager
'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;"
"script-src 'self' {$jsDev} " . \implode(' ', [
'https: blob:',
]) . ';'
];
$response->headers->set(
'Content-Security-Policy',
implode(' ', $cspPolicies) . $reportUri

View file

@ -20,7 +20,7 @@ class TrustProxies extends Middleware
* @var integer
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |

View file

@ -11,6 +11,5 @@ class VerifyCsrfToken extends Middleware
*
* @var array<int, string>
*/
protected $except = [
];
protected $except = ['/csp-report'];
}

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($param))[0] ?? null;
}
switch ($type) {
case 'Lambda':
case 'Object':
continue 2;
case 'array':
$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,444 @@
<?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} === $model->getOriginal($attribute)) {
return;
}
$originalPath = \ltrim(\urldecode($model->getOriginal($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.
*/
public static function storeFile(
Model $model,
$file,
bool $slugify = false,
string $filename = null,
bool $private = false
): string {
$tableName = $model->getTable();
if (\is_object($file) and $file instanceof \Illuminate\Http\UploadedFile) {
if ($private) {
throw new \RuntimeException('Private storage for uploaded files is unhandled');
}
/** @var \Illuminate\Http\UploadedFile $file */
$filename = $filename ?: $file->getClientOriginalName();
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
$newPath = self::prepareMoveFile($tableName, $filename);
$newPath = str_replace(storage_path('app'), '', $newPath);
$file->storePubliclyAs(
pathinfo($newPath, \PATHINFO_DIRNAME),
pathinfo($newPath, \PATHINFO_BASENAME)
);
return 'storage' . str_replace('/public', '', $newPath);
}
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 + beautifier
$filename = self::sanitizeFileName($filename);
// * Security filter
$filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
// * 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 has 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,237 @@
<?php
namespace App\Lib\Helpers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ToolboxHelper
{
/**
* 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 => ':attribute : Il est interdit de coller une url du back office dans ce champ'
]);
}
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 => sprintf(
':attribute : Il est interdit de coller une url de %s dans ce champ',
config('app.name')
)
]);
}
return $match;
} catch (NotFoundHttpException $e) {
return $match;
}
},
$text
);
return $text;
}
/**
* Mutltibyte string replace
* @param mixed $search
* @param mixed $replace
* @param mixed $subject
* @param integer $count
* @return mixed
*/
public static function mbReplace($search, $replace, $subject, int &$count = 0)
{
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 mixed $value
* @param integer|null $id
* @param callable|null $query
* @return void
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
*/
public static function assertFieldIsUnique(
string $table,
string $field,
$value,
?int $id = null,
?callable $query = null
) {
if (!$query) {
Validator::make([$field => $value], [
self::mbReplace('.', '\.', $field) => Rule::unique($table, $field)->ignore($id),
], [
self::mbReplace('.', '\.', "{$field}.unique")
=> trans('La valeur :value pour le champ :attribute est déjà utilisé', [
'attribute' => $field,
'value' => $value
])
])->validate();
return;
}
Validator::make([$field => $value], [
$field => Rule::unique($table)->where($query)->ignore($id),
], [
$field . '.unique' => trans('La valeur pour le champ :attribute est déjà utilisé', [
'attribute' => $field
])
])->validate();
}
/**
* 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));
}
}
}

View file

@ -1,499 +0,0 @@
<?php
namespace App\Lib;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ToolboxHelper
{
/**
* 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 => sprintf(
'%s : Il est interdit de coller une url du back office dans ce champ',
trans($field)
)
]);
}
return $match;
} catch (NotFoundHttpException $e) {
return $match;
}
},
$text
);
return $text;
}
/**
* Get files path uploaded with ckeditor custom upload plugin (crud-policies)
*
* @param string $text
* @param array $detectExtensions
* @return array
*/
public static function getCkEditorLocalUploadedFiles(
string $text,
array $detectExtensions = ['png', 'svg', 'jpg', 'gif']
): array {
$out = [];
\preg_match_all(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/src="(?P<url>\/storage\/images\/crud-policies\/uploads\/\d{4}-\d{2}-\d{2}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[-a-zA-Z0-9@%._\+~#=]{2,256}\.(?:' . implode('|', $detectExtensions) . '))"/',
$text,
$out
);
return collect($out['url'] ?? [])->map(function (string $urlEncodedFilePath) {
return \urldecode($urlEncodedFilePath);
})->all();
}
/**
* Assert That only external url is present on txt
*
* @param string $field
* @param string $text
* @return string
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
*/
public static function assertNoInternalUrls(string $field, string $text): string
{
self::assertNoInternalBoUrls($field, $text);
$routes = Route::getRoutes();
// * Url replace and email replace.
$text = preg_replace_callback(
// phpcs:ignore Generic.Files.LineLength.TooLong
'/(?P<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 => sprintf(
'%s : Il est interdit de coller une url de %s dans ce champ',
trans($field),
config('app.name')
)
]);
}
return $match;
} catch (NotFoundHttpException $e) {
return $match;
}
},
$text
);
return $text;
}
/**
* Generates an array from a dot separated string
*
* @param string $path
* @param mixed $value
* @param string $separator
* @return array
*/
public static function dotStringToNestedArray(string $path, $value, string $separator = '.'): array
{
$arr = $value;
$keys = array_reverse(explode($separator, $path));
foreach ($keys as $key) {
$arr = [$key => $arr];
}
return $arr;
}
/**
* Destroy image
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeImage(Model $model, string $attribute)
{
$path = ltrim($model->{$attribute}, '/');
if (File::exists($path) and \strpos($path, 'storage') === 0) {
File::delete($path);
}
}
/**
* Destroy image
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeOldImage(Model $model, string $attribute)
{
$originalPath = ltrim($model->getOriginal($attribute), '/');
if (File::exists($originalPath) and \strpos($originalPath, 'storage') === 0) {
File::delete($originalPath);
}
}
/**
* Remove pictures in ckeditor
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeImagesOnCkEditor(Model $model, string $attribute)
{
// * Remove inline uploaded images
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
->map(function (string $originalPicturePath) {
// * If a picture is not used anymore
if (File::exists(\public_path($originalPicturePath))) {
File::delete(\public_path($originalPicturePath));
}
});
}
/**
* Remove unused pictures in ckeditor
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeUnusedImagesOnCkEditor(Model $model, string $attribute)
{
// * Remove unused inline uploaded images
if ($model->{$attribute} != $model->getOriginal($attribute)) {
$updatedPicturesPath = collect(
ToolboxHelper::getCkEditorLocalUploadedFiles($model->{$attribute})
);
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
->map(function (string $originalPicturePath) use ($updatedPicturesPath) {
// * If a picture is not used anymore
if (!$updatedPicturesPath->contains($originalPicturePath)) {
if (File::exists(\public_path($originalPicturePath))) {
File::delete(\public_path($originalPicturePath));
}
}
});
}
}
/**
* Store an image of different types
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $image
* @return string
* @throws \ErrorException If file copy fails.
*/
public static function storeImage(Model $model, $image): string
{
$table = $model->getTable();
if (\is_object($image) and $image instanceof \Illuminate\Http\UploadedFile) {
/** @var \Illuminate\Http\UploadedFile $image */
$newPath = self::prepareMoveFile($table, $image->getClientOriginalName());
$newPath = str_replace(storage_path('app'), '', $newPath);
$image->storePubliclyAs(
pathinfo($newPath, \PATHINFO_DIRNAME),
pathinfo($newPath, \PATHINFO_BASENAME)
);
return 'storage' . str_replace('/public', '', $newPath);
}
if (\is_object($image) and $image instanceof \SplFileInfo) {
/** @var \SplFileInfo $image */
$newPath = self::prepareMoveFile($table, $image->getBaseName());
if (!File::copy($image->getRealPath(), $newPath)) {
throw new \ErrorException(sprintf(
'Failed to copy file %s to %s',
$image->getRealPath(),
$newPath
));
}
return \ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/');
}
return strval($image);
}
/**
* Prepare storage path and return
* the image full path
*
* @param string $table Model table name.
* @param string $filename The basename for the file.
* @return string
*/
private static function prepareMoveFile(string $table, string $filename): string
{
// * Security filter + beautifier
$filename = self::sanitizeFileName($filename);
// * Security filter
$filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
// * Slugify filename
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
(\strlen($ext) ? ".{$ext}" : '');
$pathFolder = \storage_path(sprintf('app/public/images/%s', $table));
if (!File::exists($pathFolder)) {
File::makeDirectory($pathFolder, 0755, true);
}
$filename = sprintf('%s-%s', Str::uuid(), $filename);
$fullPath = sprintf('%s/%s', $pathFolder, $filename);
if (File::exists($fullPath)) {
File::delete($fullPath);
}
return $fullPath;
}
/**
* Asserts that the field is unique
*
* @param string $table
* @param string $field
* @param mixed $value
* @param integer|null $id
* @param callable|null $query
* @return void
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
*/
public static function assertFieldIsUnique(
string $table,
string $field,
$value,
?int $id = null,
?callable $query = null
) {
if (!$query) {
Validator::make([$field => $value], [
$field => Rule::unique($table, $field)->ignore($id),
], [
$field . '.unique' => trans('La valeur :value pour le champ :attribute est déjà utilisé', [
'attribute' => $field,
'value' => $value
])
])->validate();
return;
}
Validator::make([$field => $value], [
$field => Rule::unique($table)->where($query)->ignore($id),
], [
$field . '.unique' => trans('La valeur pour le champ :attribute est déjà utilisé', [
'attribute' => $field
])
])->validate();
}
/**
* Validate recaptcha V3
*
* @param Request $request
* @return void
*/
public static function validateRecaptchaV3(Request $request)
{
Validator::validate($request->all(), [
'g-recaptcha-response' => [
'required', 'string',
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
function ($attribute, $value, $fail) {
try {
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('recaptcha.private'),
'response' => \strval($value),
'remoteip' => app('clientIp')
]);
if ($e = $response->toException()) {
throw $e;
}
if (
!$response->json('success') or
!$response->json('challenge_ts') or
Carbon::parse($response->json('challenge_ts'))->lessThan(\now('utc')->subMinutes(5)) or
$response->json('hostname') !== \parse_url(config('app.url'), \PHP_URL_HOST) or
!$response->json('score')
) {
throw new \ErrorException();
}
if (\floatval($response->json('score')) < 0.8) {
$fail('Vous n\'avez pas obtenu un score assez élevé lors de la validation du recaptcha');
}
} catch (\Exception $e) {
$fail('Erreur lors de la validation du formulaire veuillez réssayer');
} //end try
}
]
], [], ['g-recaptcha-response' => 'reCaptcha']);
}
/**
* * Register CSP report URI
*
* Put this function inside RouteServiceProvider,
* and call it in RouteServiceProvider::boot
*
* ! Use this in conjonction with kwaadpepper/exception-handler
*
* @return void
*/
public static function cspReportRoute()
{
// * CSP REPORT URI, Please add /csp-report to VerifyCsrfToken $except middleware
\Illuminate\Support\Facades\Route::post('/csp-report', function () {
$isCspRequest = request()->header('Content-Type') === 'application/csp-report';
if (!$isCspRequest or !request()->json('csp-report') or !is_array(request()->json('csp-report'))) {
abort(401);
}
// * Flatten if needed
$cspReport = collect(request()->json('csp-report'))
->flatMap(function ($v, $k) {
return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
})->all();
// * Filter array keys
$cspReport = collect(array_intersect_key($cspReport, [
'blocked-uri' => '', 'document-uri' => '', 'original-policy' => '',
'referrer' => '', 'violated-directive' => ''
]));
report(new \Illuminate\Http\Client\HttpClientException(sprintf(
"CSP REPORT for %s : \n %s",
$cspReport->get('document-uri'),
$cspReport->implode("\n")
)));
})->middleware('throttle:5,2')
->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
->name('cspReportUri');
}
/**
* 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

@ -2,12 +2,10 @@
namespace App\Models;
use App\Lib\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class User extends Authenticatable
{
@ -20,7 +18,8 @@ class User extends Authenticatable
* @var array<int, string>
*/
protected $fillable = [
'name',
'firstname',
'lastname',
'email',
'password',
];
@ -42,7 +41,6 @@ class User extends Authenticatable
*/
protected $casts = [
'email_verified_at' => 'datetime',
'last_connection' => 'datetime',
];
/**
@ -53,34 +51,15 @@ class User extends Authenticatable
protected static function booted()
{
// * EVENT CLOSURES.
static::creating(function (self $user) {
static::assertEmailIsUnique($user->email);
static::creating(function (User $user) {
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($user->password);
}
$user->remember_token = Str::random(10);
});
static::updating(function (self $user) {
static::assertEmailIsUnique($user->email, $user->id);
static::updating(function (User $user) {
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($user->password);
}
$user->remember_token = Str::random(10);
});
}
// * METHODS
/**
* Asserts using validation that the email is unique
*
* @param mixed $email
* @param integer|null $id
* @return void
* @throws \Illuminate\Validation\ValidationException If user email as already used.
*/
private static function assertEmailIsUnique($email, ?int $id = null)
{
ToolboxHelper::assertFieldIsUnique((new self())->getTable(), 'email', $email, $id);
}
}

View file

@ -4,20 +4,12 @@ namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
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.
*
@ -25,34 +17,47 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
// * HTTPS FIX
URL::forceScheme('https');
// * HA PROXY FIX
App::singleton('clientIp', function () {
if (config('app.env') === 'local') {
return request()->getClientIp();
} else {
return request()->server('HTTP_X_FORWARDED_FOR') ??
request()->getClientIp();
}
});
// * LOCAL FORCE eg: for Carbon
// * Remove numeric to prevent float seperator to breaking numeric transactions
\collect([\LC_TIME, \LC_MESSAGES, \LC_COLLATE, \LC_CTYPE, \LC_MONETARY])
->map(function ($contant) {
setlocale(
$contant,
\sprintf(
'%s_%s.UTF-8',
config('app.locale'),
\strtoupper(config('app.locale'))
)
);
});
setlocale(LC_ALL, \sprintf(
'%s_%s.UTF-8',
config('app.locale'),
\strtoupper(config('app.locale'))
));
Carbon::setLocale(config('app.locale'));
// * Force bootstrap paginator from Alexis with tooltips.
Paginator::defaultView('bo.modules.pagination');
$this->sharetoView();
}
// * HA PROXY FIX
app()->singleton('clientIp', function () {
if (
config('app.env') === 'local'
) {
return request()->getClientIp();
/**
* Share Vars to view
*
* @return void
*/
private function sharetoView()
{
if (!app()->runningInConsole()) {
// * FORCE BOOTSTRAP PAGINATOR (or custom if in front)
// Early boot, request wont be filled as expected.
if (collect(explode('/', \request()->getPathInfo()))->get(1) === 'bo') {
Paginator::useBootstrap();
} else {
// FIXME: change this if needed.
Paginator::useBootstrap();
// Paginator::defaultview('fo.modules.pagination'); .
}
return request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
});
}
}
}

View file

@ -6,13 +6,6 @@ use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvid
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [];
/**
* Register any authentication / authorization services.
*

View file

@ -2,7 +2,6 @@
namespace App\Providers;
use App\Lib\ToolboxHelper;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
@ -18,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
*
* @var string
*/
public const HOME = '/home';
public const HOME = '/';
/**
* Define your route model bindings, pattern filters, etc.
@ -29,16 +28,16 @@ class RouteServiceProvider extends ServiceProvider
{
$this->configureRateLimiting();
ToolboxHelper::cspReportRoute();
$this->cspReportRoute();
$this->routes(function () {
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web-fo.php'));
->group(base_path('routes/web-bo.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web-bo.php'));
->group(base_path('routes/web-fo.php'));
});
}
@ -49,8 +48,50 @@ class RouteServiceProvider extends ServiceProvider
*/
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(150)->by(optional($request->user())->id ?: app('clientIp'));
});
RateLimiter::for('web', function (Request $request) {
return Limit::perMinute(150)->by(optional($request->user())->id ?: app('clientIp'));
});
RateLimiter::for('credentials', function (Request $request) {
return Limit::perMinute(30)->by(optional($request->user())->id ?: $request->ip());
});
}
/**
* * 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

@ -6,6 +6,7 @@ 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
{
@ -35,8 +36,8 @@ class TranslationServiceProvider extends ServiceProvider
*/
private function collectLocalesStrings(): Collection
{
return collect(File::allFiles(resource_path('lang/')))->flatMap(function ($file) {
if ($file->getRelativePath()) {
return collect(File::allFiles(resource_path('lang/')))->flatMap(function (SplFileInfo $file) {
if ($file->getRelativePath() and \strpos($file->getRelativePath(), 'vendor') !== 0) {
return [$file->getRelativePath() => ''];
}
})->keys();

View file

@ -1,7 +1,12 @@
{
"name": "laravel/laravel",
"version": "1.0.0",
"type": "project",
"time": "2023-03-02",
"description": "The Laravel Framework.",
"support": {
"email": "equipe.web@ldc.fr"
},
"keywords": [
"framework",
"laravel"
@ -9,28 +14,34 @@
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"diglactic/laravel-breadcrumbs": "^8.0",
"diglactic/laravel-breadcrumbs": "^6.1",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"kwaadpepper/exception-handler": "^0.0.9",
"kwaadpepper/refresh-sitemap": "^1.0",
"kwaadpepper/enum": "^2.1",
"kwaadpepper/image-resizer": "^0.1.2",
"laravel/framework": "^8.75",
"laravel/tinker": "^2.5",
"laravel/ui": "^3.4",
"ldc/web-admin-secure-laravel": "1.0.1",
"ldc/web-status-checker-laravel": "1.0.3",
"ldc/web-admin-secure-laravel": "^1.0",
"ldc/web-status-checker-laravel": "^1.0",
"mews/purifier": "^3.3",
"sentry/sentry-laravel": "^3.2",
"spatie/laravel-mix-preload": "^1.3"
},
"require-dev": {
"facade/ignition": "^2.5",
"fakerphp/faker": "^1.9.1",
"laravel-lang/lang": "10.0",
"laravel/sail": "^1.0.1",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^5.10",
"phpunit/phpunit": "^9.5.10",
"slevomat/coding-standard": "^8.3"
"slevomat/coding-standard": "^8.8"
},
"repositories": {
"lab.s-web.fr/23": {
"type": "composer",
"url": "https://lab.s-web.fr/api/v4/group/23/-/packages/composer/packages.json"
}
},
"autoload": {
"psr-4": {
@ -70,14 +81,11 @@
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"gitlab-domains": [
"lab.s-web.fr"
]
},
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": {
"gitlab.com/16669037": {
"type": "composer",
"url": "https://gitlab.com/api/v4/group/16669037/-/packages/composer/packages.json"
}
}
"prefer-stable": true
}

3249
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -67,7 +67,7 @@ return [
|
*/
'timezone' => 'UTC',
'timezone' => 'Europe/Paris',
/*
|--------------------------------------------------------------------------
@ -80,7 +80,7 @@ return [
|
*/
'locale' => 'en',
'locale' => 'fr',
/*
|--------------------------------------------------------------------------
@ -106,7 +106,7 @@ return [
|
*/
'faker_locale' => 'en_US',
'faker_locale' => 'fr_FR',
/*
|--------------------------------------------------------------------------
@ -173,10 +173,9 @@ return [
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// phpcs:ignore
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TranslationServiceProvider::class
],
@ -221,8 +220,6 @@ return [
'Queue' => Illuminate\Support\Facades\Queue::class,
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
// phpcs:ignore
// 'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,

View file

@ -14,7 +14,7 @@ return [
*/
'defaults' => [
'guard' => 'backend',
'guard' => 'frontend',
'passwords' => 'users',
],
@ -40,6 +40,12 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'frontend' => [
'driver' => 'session',
// ! Remplacer "users" par le model authenticable voulu
'provider' => 'users',
],
],
/*

View file

@ -62,8 +62,7 @@ return [
env('MEMCACHED_PASSWORD'),
],
'options' => [
// phpcs:ignore
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
// Memcached::OPT_CONNECT_TIMEOUT => 2000, .
],
'servers' => [
[

View file

@ -15,7 +15,7 @@ return [
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'paths' => ['api/*'],
'allowed_methods' => ['*'],

View file

@ -1,15 +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' => explode(",", env('MAIL_LIST_SERVICEWEB'))
];

View file

@ -50,7 +50,7 @@ return [
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single'],
'channels' => ['single', 'sentry'],
'ignore_exceptions' => false,
],
@ -60,6 +60,11 @@ return [
'level' => env('LOG_LEVEL', 'debug'),
],
'sentry' => [
'driver' => 'sentry',
'level' => env('LOG_LEVEL', 'error'),
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),

View file

@ -29,8 +29,15 @@ return [
// * For iframes
'URI.SafeIframeRegexp' => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/)%",
'HTML.Doctype' => 'HTML 4.01 Transitional',
'HTML.Allowed' => 'h1[style],h2[style],h3[style],h4[style],h5[style],h6[style],figure[class|style],oembed[url],div[style],b,strong,i,em,u,a[href|title|target|rel],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',
'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-color',
// TODO: Rules en FULL CKEDITOR.
// * Legacy filter en full avec safe legacy.
// 'HTML.Allowed' => 'h1[style],h2[style],h3[style],h4[style],h5[style],h6[style],figure[class|style],oembed[url],div[style],b,strong,i,em,u,a[href|title|target|rel],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',
// * Restricted basics.
'HTML.Allowed' => 'br,p[style],b,u,i,a[href|title|target|rel],strong,span[class],ul,ol,li,h2,h3,h4,hr',
// * 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-color',
// * Restricted basics.
'CSS.AllowedProperties' => 'text-align',
'AutoFormat.AutoParagraph' => false,
'AutoFormat.RemoveEmpty' => false,
'AutoFormat.RemoveEmpty.RemoveNbsp' => false,

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

@ -14,14 +14,20 @@ return [
|
*/
'google' => [
'analytics' => [
'id' => env('GOOGLE_ANALYTICS_ID')
],
'recaptcha' => [
'public' => env('GOOGLE_RECAPTCHA_PUBLIC'),
'private' => env('GOOGLE_RECAPTCHA_PRIVATE'),
'version' => env('GOOGLE_RECAPTCHA_VERSION', "3") === "3" ? true : false
]
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
],
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
];

View file

@ -15,6 +15,7 @@ return [
'paths' => [
resource_path('views'),
resource_path('views/vendor'),
],
/*

View file

@ -15,9 +15,11 @@ class UserFactory extends Factory
public function definition()
{
return [
'name' => $this->faker->name(),
'firstname' => $this->faker->firstname(),
'lastname' => $this->faker->lastname(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
// * password
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'remember_token' => Str::random(10),
];
@ -30,8 +32,7 @@ class UserFactory extends Factory
*/
public function unverified()
{
// phpcs:ignore
return $this->state(function (array $attributes) {
return $this->state(function () {
return [
'email_verified_at' => null,
];

View file

@ -15,12 +15,12 @@ class CreateUsersTable extends Migration
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('firstname');
$table->string('lastname');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->date('last_connection')->nullable();
$table->timestamps();
});
}

View file

@ -1,36 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePersonalAccessTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('personal_access_tokens');
}
}

View file

@ -13,6 +13,5 @@ class DatabaseSeeder extends Seeder
*/
public function run()
{
// \App\Models\User::factory(10)->create();
}
}

View file

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

7641
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,32 +8,56 @@
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production",
"eslint": "node ./node_modules/.bin/eslint --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 --max-warnings 0 '**/*.scss'",
"stylelint-ci": "node ./node_modules/.bin/stylelint --max-warnings 0 '**/*.scss' -f json"
"eslint": "node ./node_modules/.bin/eslint -c .eslintrc.json ./resources",
"eslint-ci": "node ./node_modules/.bin/eslint -c .eslintrc.json ./resources -f junit",
"stylelint": "npx stylelint '**/*.scss'",
"stylelint-ci": "npx stylelint '**/*.scss' -f json"
},
"devDependencies": {
"@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",
"@types/bootstrap": "^5.2.1",
"@types/webpack-env": "^1.17.0",
"@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/cropperjs": "^1.3.0",
"@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",
"@vue/eslint-config-typescript": "^11.0.0",
"axios": "^0.21",
"bootstrap": "^5.2.0",
"bootstrap": "^5.2.3",
"clean-webpack-plugin": "^4.0.0",
"cropperjs": "^1.5.12",
"eslint": "^8.23.0",
"eslint-plugin-vue": "^9.4.0",
"eslint-webpack-plugin": "^3.2.0",
"laravel-mix": "^6.0.6",
"laravel-mix-clean": "^0.1.0",
"laravel-mix-eslint": "^0.2.0",
"laravel-mix-polyfill": "^3.0.1",
"laravel-mix-stylelint": "^1.0.0",
"postcss": "^8.1.14",
"prettier": "^2.7.1",
"resolve-url-loader": "^5.0.0",
"sass": "^1.54.0",
"sass": "^1.54.8",
"sass-loader": "^12.6.0",
"sweetalert2": "^11.4.24",
"slugify": "^1.6.5",
"stylelint": "^14.11.0",
"stylelint-config-idiomatic-order": "^8.1.0",
"stylelint-config-sass-guidelines": "^9.0.1",
"stylelint-config-standard-scss": "^6.1.0",
"stylelint-webpack-plugin": "^3.3.0",
"sweetalert2": "^11.4.8",
"toastify-js": "^1.12.0",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"vue": "^3.2.37",
"typescript": "^4.8.2",
"typescript-eslint": "^0.0.1-alpha.0",
"vue": "^3.2.38",
"vue-eslint-parser": "^9.1.0",
"vue-loader": "^16.8.3"
}
}

View file

@ -33,7 +33,6 @@
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
<rule ref="Generic.Metrics.CyclomaticComplexity"/>
<rule ref="Generic.Metrics.NestingLevel"/>
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
<rule ref="Generic.NamingConventions.CamelCapsFunctionName"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>

31
phpunit.xml Normal file
View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</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"/>
</php>
</phpunit>

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View file

@ -2,8 +2,8 @@
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/assets/images/mstile-150x150.png"/>
<TileColor>#091c58</TileColor>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon-16x16-bo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

BIN
public/favicon-32x32-bo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/favicon-bo-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
public/favicon-bo-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

BIN
public/favicon-bo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -7,17 +7,17 @@ define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Check If The Application Is Under Maintenance
| Check If Application Is Under Maintenance
|--------------------------------------------------------------------------
|
| If the application is in maintenance / demo mode via the "down" command
| we will load this file so that any pre-rendered content can be shown
| If the application is maintenance / demo mode via the "down" command we
| will require this file so that any prerendered template can be shown
| instead of starting the framework, which could cause an exception.
|
*/
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
if (file_exists(__DIR__.'/../storage/framework/maintenance.php')) {
require __DIR__.'/../storage/framework/maintenance.php';
}
/*
@ -48,8 +48,8 @@ $app = require_once __DIR__.'/../bootstrap/app.php';
$kernel = $app->make(Kernel::class);
$response = $kernel->handle(
$response = tap($kernel->handle(
$request = Request::capture()
)->send();
))->send();
$kernel->terminate($request, $response);

BIN
public/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,187 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="621.000000pt" height="621.000000pt" viewBox="0 0 621.000000 621.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,621.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M375 6180 c-126 -3 -238 -9 -247 -14 -10 -4 -33 -29 -52 -55 l-35
-47 -3 -2149 c-2 -1183 -1 -2517 2 -2966 5 -799 5 -817 25 -839 11 -13 40 -36
64 -52 l45 -28 2940 0 2940 0 43 21 c29 14 52 35 68 63 l25 43 0 2919 c-1
1605 -5 2939 -9 2963 -11 55 -37 89 -87 115 -21 11 -40 24 -42 28 -4 9 -5301
7 -5677 -2z m5447 -366 c11 -7 9 -5386 -2 -5404 -8 -13 -318 -15 -2672 -18
-1465 -2 -2681 1 -2703 5 l-40 9 0 2704 0 2705 2435 5 c2310 5 2966 4 2982 -6z"/>
<path d="M4185 5150 c3 -5 13 -10 21 -10 8 0 13 -4 10 -8 -3 -5 3 -8 12 -8 9
1 18 -3 20 -9 4 -13 -54 -25 -65 -14 -13 13 -61 -13 -67 -36 -3 -10 0 -23 7
-28 7 -5 2 -6 -12 -2 -16 5 -21 4 -17 -4 4 -6 2 -11 -3 -11 -6 0 -11 -7 -11
-15 0 -21 16 -19 41 6 20 20 39 22 42 4 1 -12 -84 -78 -91 -70 -13 12 -34 -41
-62 -158 -16 -68 -30 -127 -30 -130 0 -2 9 -7 20 -10 23 -6 37 -34 23 -48 -15
-15 -43 1 -44 25 0 20 -1 20 -9 1 -11 -26 -12 -48 -2 -41 4 2 6 -24 4 -59 -2
-36 -1 -72 2 -81 5 -14 10 -13 31 10 13 14 28 33 33 42 12 22 35 8 29 -17 -3
-12 -1 -17 5 -13 6 4 9 1 6 -7 -3 -8 -17 -14 -31 -14 -14 1 -25 -2 -24 -5 4
-20 -17 -50 -36 -50 -21 0 -22 -3 -18 -81 2 -62 0 -80 -9 -74 -6 4 -10 18 -7
31 2 13 1 24 -3 24 -3 0 -6 9 -6 21 1 15 -9 24 -34 34 -19 7 -69 27 -110 45
-269 113 -518 162 -830 163 -338 1 -598 -52 -840 -173 -498 -249 -802 -698
-891 -1315 -31 -217 -31 -594 0 -780 12 -71 23 -143 24 -160 1 -16 6 -36 10
-43 5 -7 10 -22 13 -35 7 -40 13 -61 38 -137 14 -41 30 -83 36 -92 5 -10 10
-23 10 -30 0 -6 23 -57 51 -112 249 -492 658 -780 1224 -862 150 -22 457 -30
545 -15 36 6 115 20 175 30 121 21 233 47 249 60 6 4 31 13 56 20 82 24 225
89 347 159 73 42 82 50 87 82 8 48 6 431 -2 452 -6 15 -12 12 -42 -19 -85 -88
-335 -229 -512 -288 -205 -68 -285 -79 -558 -80 -199 0 -261 3 -330 18 -248
53 -451 162 -602 326 -80 87 -124 151 -179 262 -22 46 -45 92 -50 102 -5 10
-7 22 -4 27 3 4 2 10 -2 12 -16 7 -55 148 -80 288 -21 117 -27 187 -30 365 -3
125 0 259 6 310 16 136 51 319 62 326 5 3 7 13 4 21 -4 9 -2 19 4 22 6 4 8 13
5 20 -3 8 -1 17 5 21 6 3 11 12 11 20 0 8 5 26 12 42 6 15 12 30 13 33 8 31
83 173 122 231 59 86 216 242 297 294 133 85 274 136 457 166 116 18 160 21
329 16 293 -7 461 -50 737 -186 57 -28 107 -51 113 -51 5 0 10 -8 10 -17 0
-15 2 -16 10 -3 6 10 10 11 10 3 0 -7 18 -22 40 -33 22 -11 40 -25 40 -31 0
-6 5 -7 10 -4 6 4 10 -9 10 -33 0 -22 3 -54 6 -71 l7 -31 -47 0 c-34 0 -51 -5
-63 -20 -19 -22 -43 -26 -60 -9 -6 6 -37 16 -69 24 -33 7 -87 20 -121 28 -35
8 -63 11 -63 7 0 -29 -76 -13 -82 16 -2 12 -20 21 -58 30 -50 12 -56 11 -70
-5 -21 -26 -60 -33 -67 -14 -3 9 -1 24 5 34 9 14 8 18 -6 18 -9 1 -34 4 -54 7
-31 5 -38 3 -38 -10 0 -23 -35 -20 -59 5 -17 19 -29 20 -123 18 -114 -3 -138
-7 -116 -20 10 -7 10 -9 1 -9 -7 0 -14 -10 -14 -22 -1 -19 -3 -17 -9 7 -6 25
-8 26 -9 7 0 -13 -5 -21 -10 -18 -5 4 -12 0 -15 -6 -3 -7 -2 1 1 17 6 27 5 29
-11 16 -14 -12 -16 -12 -10 3 3 9 1 16 -5 16 -6 0 -11 -5 -11 -11 0 -5 -4 -8
-9 -5 -5 3 -12 -1 -15 -10 -8 -21 -8 -26 2 -21 4 3 7 -8 7 -24 0 -39 -11 -37
-19 4 -6 29 -8 30 -18 13 -8 -15 -8 -21 0 -24 7 -2 12 -22 12 -43 0 -24 4 -38
10 -34 6 3 10 -1 10 -9 0 -9 4 -16 9 -16 5 0 17 -7 25 -16 15 -14 16 -13 18
13 0 15 1 33 1 40 0 6 6 9 14 6 10 -4 11 -9 4 -16 -16 -16 -14 -62 3 -76 18
-15 56 -22 56 -11 0 5 -4 11 -9 14 -10 7 -3 44 12 64 17 22 38 -5 27 -34 -14
-37 -22 -92 -11 -75 6 9 11 10 15 2 10 -15 22 -14 40 5 9 8 16 11 16 5 0 -5
-5 -13 -10 -16 -8 -5 -7 -11 1 -21 6 -7 15 -11 20 -9 4 3 16 1 26 -4 38 -20
105 -41 114 -36 5 4 9 14 9 24 0 9 6 26 14 37 14 17 16 18 31 3 15 -15 15 -18
-9 -36 -18 -14 -23 -26 -19 -42 7 -26 48 -29 62 -3 10 16 10 16 11 0 0 -10 5
-18 11 -18 8 0 8 -4 0 -14 -10 -12 3 -22 71 -56 46 -23 90 -48 98 -55 8 -6 41
-26 73 -44 31 -18 57 -35 57 -37 0 -3 -6 -2 -12 1 -7 4 -4 -1 6 -9 14 -11 22
-13 27 -5 3 6 9 0 13 -15 6 -26 19 -31 64 -27 12 1 22 -3 22 -9 0 -5 7 -10 15
-10 8 0 15 5 15 10 0 16 40 11 47 -6 3 -9 -5 -26 -20 -42 -31 -32 -24 -62 9
-37 25 19 31 19 44 -4 11 -22 2 -84 -11 -75 -5 3 -9 0 -9 -6 0 -7 -6 -10 -14
-7 -9 4 -16 -4 -21 -21 -3 -15 -8 -34 -11 -43 -5 -19 63 -95 83 -91 8 2 12 -1
9 -7 -11 -17 12 -51 29 -45 8 4 15 1 15 -6 0 -6 5 -8 10 -5 6 4 8 10 5 15 -3
4 4 10 15 13 11 3 20 14 20 24 0 25 8 33 26 26 8 -3 14 -16 14 -28 0 -13 5
-27 10 -30 6 -3 10 1 10 10 0 30 29 -10 32 -44 3 -35 23 -48 37 -24 8 15 11
15 26 3 14 -12 18 -12 25 0 5 8 6 18 4 22 -9 15 7 48 23 48 10 0 14 -6 10 -20
-3 -11 -3 -20 -1 -20 7 0 49 25 54 32 3 4 12 3 20 -2 10 -7 11 -10 3 -10 -8 0
-13 -13 -13 -34 0 -29 3 -33 23 -29 13 3 24 11 24 18 0 8 -4 12 -8 10 -4 -3
-9 1 -12 9 -15 37 70 58 108 26 14 -11 19 -12 22 -2 3 8 30 12 88 12 101 0
115 10 115 80 0 46 -13 99 -28 110 -8 6 -13 14 -100 170 -35 63 -77 131 -92
151 -35 44 -28 57 14 26 17 -13 39 -26 49 -30 9 -3 17 -11 17 -17 0 -5 6 -10
13 -10 7 0 31 -13 53 -29 70 -52 127 -45 105 12 -7 18 -6 30 5 45 9 13 11 23
4 27 -5 3 -10 -1 -10 -9 0 -10 -6 -13 -17 -9 -30 10 -53 35 -53 56 0 20 1 19
26 -4 l27 -24 -7 29 -7 29 53 -7 c169 -20 267 -19 254 2 -2 4 10 8 26 9 40 3
129 46 106 51 -10 2 -18 8 -18 13 0 6 -7 6 -19 -1 -24 -13 -41 -13 -41 -1 0 6
8 9 18 9 34 -2 106 31 102 46 -1 5 4 13 12 17 9 6 6 9 -10 9 -15 0 -22 5 -19
13 2 6 16 11 31 9 20 -2 29 3 38 24 11 23 10 28 -7 40 -15 11 -16 14 -3 14 24
0 31 51 8 57 -21 6 -16 33 6 33 9 0 14 11 14 29 0 24 -5 29 -30 33 -16 3 -30
1 -30 -3 0 -3 -11 -9 -25 -11 -19 -4 -25 -1 -25 12 0 12 10 19 30 22 17 3 30
1 30 -4 0 -4 7 -8 16 -8 8 0 12 5 9 10 -3 6 -2 10 4 10 5 0 9 9 8 20 -1 11 -4
20 -8 20 -4 0 -5 9 -2 19 4 17 -2 21 -41 26 -25 4 -46 12 -46 17 0 6 -7 8 -15
4 -8 -3 -15 0 -15 5 0 6 8 9 18 6 32 -9 37 -9 68 -8 28 1 31 4 28 27 -3 30
-21 46 -44 39 -43 -12 -85 -9 -85 5 0 11 11 14 46 13 44 -2 45 -2 45 27 0 16
-3 27 -8 24 -4 -3 -8 0 -8 6 0 8 -10 9 -29 5 -34 -7 -101 17 -101 37 0 10 2
10 9 -1 5 -7 14 -10 20 -7 6 4 11 2 11 -4 0 -7 7 -10 15 -6 8 3 15 1 15 -4 0
-13 11 -12 30 0 13 9 13 11 0 20 -10 6 -11 10 -3 10 7 0 -12 25 -43 56 -30 31
-58 53 -62 50 -3 -4 0 -11 8 -16 11 -7 12 -10 2 -10 -7 0 0 -11 15 -25 28 -24
28 -24 5 -25 -13 0 -21 4 -17 10 6 10 -51 44 -63 36 -4 -3 -14 -1 -21 4 -18
10 -5 31 14 23 11 -4 13 -1 9 11 -5 13 -2 15 12 10 14 -5 16 -4 8 5 -6 6 -34
14 -62 18 -45 5 -52 3 -52 -11 0 -9 11 -28 24 -41 19 -19 21 -26 11 -35 -9 -8
-19 -1 -41 27 -18 24 -24 38 -16 41 23 7 12 22 -18 22 -23 0 -28 4 -23 16 4
11 8 12 13 4 8 -13 20 46 20 98 0 42 -36 118 -90 190 -21 29 -25 31 -32 15 -4
-11 -6 -4 -4 17 3 22 -2 43 -11 55 -15 19 -15 19 -9 1 5 -16 4 -17 -9 -6 -8 7
-15 8 -15 2 0 -12 -34 -22 -48 -13 -6 4 3 13 20 20 20 9 26 16 18 21 -9 6 -9
9 0 12 7 2 2 14 -14 31 -14 15 -23 27 -20 27 3 0 -2 7 -11 16 -9 9 -14 23 -11
30 3 8 1 14 -4 14 -7 0 -19 23 -25 50 -1 3 -6 16 -12 30 -6 14 -16 44 -22 68
-8 27 -16 40 -23 36 -7 -4 -8 -3 -4 5 4 6 1 18 -8 26 -11 11 -16 12 -19 2 -3
-6 -4 2 -2 19 2 17 -1 37 -7 45 -7 11 -8 9 -3 -6 5 -16 2 -16 -18 5 -13 14
-21 29 -19 34 3 5 0 6 -6 2 -6 -3 -25 2 -44 13 -35 20 -134 29 -123 11z m105
-45 c0 -5 -5 -3 -10 5 -5 8 -10 20 -10 25 0 6 5 3 10 -5 5 -8 10 -19 10 -25z
m-15 -65 c-4 -13 -12 -19 -21 -16 -7 3 -16 1 -19 -4 -7 -11 -55 -14 -55 -4 0
3 5 14 10 25 11 20 50 27 50 9 0 -5 7 -10 15 -10 8 0 15 8 15 17 0 9 3 14 6
10 3 -3 3 -15 -1 -27z m95 -10 c0 -19 -48 -60 -70 -60 -28 0 -26 33 3 53 25
18 67 22 67 7z m87 -94 c0 -8 -6 -12 -12 -10 -6 3 -12 11 -12 18 0 8 6 12 12
10 6 -3 12 -11 12 -18z m-48 6 c9 7 5 0 -8 -15 -26 -29 -51 -36 -51 -14 0 20
22 39 34 27 6 -6 15 -5 25 2z m-14 -52 c-3 -5 0 -10 8 -10 8 0 20 -7 27 -15
10 -12 10 -15 0 -15 -17 0 -61 36 -53 44 10 10 25 7 18 -4z m79 -56 c17 -7 10
-34 -9 -34 -17 0 -28 17 -20 30 7 12 8 12 29 4z m-155 -43 c12 -24 -6 -79 -29
-86 -10 -3 -18 -11 -16 -18 1 -7 1 -9 -1 -4 -3 4 -10 7 -16 7 -16 0 -47 31
-47 47 0 7 16 29 35 48 35 36 58 38 74 6z m75 0 c4 -5 0 -12 -6 -14 -7 -3 -3
-6 10 -6 12 -1 22 4 22 11 0 9 3 9 11 1 17 -17 1 -52 -28 -59 -29 -8 -44 9
-38 41 2 11 4 23 4 28 1 10 19 9 25 -2z m116 -58 c0 -23 -17 -49 -26 -41 -9
10 4 58 16 58 6 0 10 -8 10 -17z m-360 -13 c23 -23 24 -29 10 -64 -7 -19 -17
-26 -35 -26 -14 0 -25 5 -25 11 0 5 -5 7 -12 3 -7 -5 -9 -2 -5 8 4 11 -2 18
-18 22 -22 6 -41 33 -30 44 8 9 35 -20 29 -32 -3 -6 6 4 21 22 31 37 38 39 65
12z m293 -46 c8 -9 -14 -54 -24 -48 -5 3 -9 0 -9 -7 0 -10 -3 -10 -11 -2 -8 8
-6 18 6 37 17 26 27 31 38 20z m80 -40 c11 -11 -5 -34 -24 -34 -18 0 -26 20
-12 33 8 9 28 9 36 1z m94 -66 c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3 -3 4 -12
1 -19z m-427 -16 c0 -10 -14 -31 -30 -47 -26 -25 -32 -27 -40 -15 -13 20 -12
33 3 52 23 30 67 36 67 10z m106 -34 c4 13 14 22 26 22 17 0 19 -4 13 -27 -13
-57 -115 -188 -128 -166 -2 4 -8 32 -12 61 -6 43 -3 61 11 88 10 19 22 34 26
34 4 0 -1 -15 -12 -32 -32 -54 -22 -57 12 -5 31 48 32 48 44 25 l14 -23 6 23z
m96 -163 c3 3 3 -6 0 -20 -2 -14 -13 -30 -23 -36 -11 -5 -19 -15 -19 -22 0 -7
-20 -33 -45 -57 -53 -52 -65 -46 -65 31 0 37 5 52 26 73 29 29 49 34 40 11 -8
-23 8 -18 34 10 16 17 27 22 35 15 7 -5 14 -7 17 -5z m-357 -191 c-18 -43 -35
-36 -36 14 l-1 37 -8 -30 c-7 -26 -8 -24 -7 16 0 33 4 46 13 42 6 -2 16 1 20
8 5 9 12 3 21 -19 11 -27 11 -38 -2 -68z m701 10 c13 -3 24 -10 24 -16 0 -5
-5 -6 -10 -3 -6 4 -18 1 -26 -6 -12 -10 -18 -9 -32 4 -13 14 -14 17 -2 17 13
0 13 2 0 11 -12 9 -12 10 3 5 11 -3 30 -9 43 -12z m164 -51 c0 -19 -20 -26
-35 -13 -21 18 -18 25 10 25 14 0 25 -5 25 -12z m-657 -18 c-3 -9 -8 -14 -10
-11 -3 3 -2 9 2 15 9 16 15 13 8 -4z m25 -42 c-8 -10 -6 -13 5 -13 10 0 17
-11 19 -27 2 -16 6 -26 9 -23 4 3 16 -4 28 -14 21 -19 21 -19 42 0 22 19 22
19 10 -1 -7 -11 -27 -45 -45 -75 -18 -30 -38 -54 -45 -52 -16 3 -41 -23 -41
-43 0 -9 -4 -14 -9 -10 -5 3 -12 1 -16 -5 -3 -5 -12 -10 -20 -10 -22 0 -8 198
16 229 11 13 19 31 19 40 0 25 22 44 31 29 4 -7 3 -18 -3 -25z m-188 -38 c0
-19 -4 -36 -9 -40 -15 -8 -19 6 -14 47 6 46 23 40 23 -7z m69 -102 c-3 -96
-13 -144 -33 -155 -9 -5 -16 -15 -16 -22 0 -8 -7 -19 -15 -26 -13 -11 -15 0
-15 93 0 133 7 169 30 162 9 -3 23 1 30 10 7 8 14 15 17 15 2 0 3 -35 2 -77z
m-142 -35 c-2 -13 -4 -5 -4 17 -1 22 1 32 4 23 2 -10 2 -28 0 -40z m-777 -166
c27 -21 20 -42 -14 -42 -26 0 -51 18 -59 43 -9 24 43 23 73 -1z m1827 12 c-3
-3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m-2127 -27 c0 -24 -19 -42
-31 -30 -8 8 -8 16 1 32 11 22 30 20 30 -2z m1757 8 c0 -9 -9 -15 -21 -15 -21
1 -21 1 -2 15 24 18 23 18 23 0z m-1317 -44 c8 -16 6 -21 -11 -26 -24 -8 -55
16 -45 34 11 18 44 13 56 -8z m-151 -25 c5 -3 14 -19 19 -36 9 -25 8 -31 -8
-40 -30 -16 -47 -12 -69 16 -25 32 -26 38 -6 58 14 14 43 15 64 2z m929 -49
c33 -18 42 -44 18 -53 -19 -7 -105 56 -106 78 0 19 15 14 88 -25z m941 22 c-7
-4 -21 -3 -31 2 -25 14 -23 16 13 11 21 -3 27 -7 18 -13z m-1284 1 c3 -6 -1
-7 -9 -4 -18 7 -21 14 -7 14 6 0 13 -4 16 -10z m-354 -25 c19 -1 43 -6 53 -12
15 -9 15 -11 -4 -28 -30 -28 -75 -28 -102 0 -30 32 -33 58 -5 49 12 -4 38 -8
58 -9z m508 -1 c2 -2 -8 -4 -24 -4 -21 0 -25 -3 -17 -13 9 -10 -1 -12 -54 -6
-79 8 -95 19 -28 19 35 0 45 3 34 10 -10 7 1 8 35 4 28 -4 52 -8 54 -10z
m1056 -23 c0 -23 -23 -24 -35 -1 -10 19 -9 21 12 18 13 -2 23 -9 23 -17z
m-1945 -32 c-12 -41 -24 -53 -14 -14 7 32 13 45 19 45 2 0 0 -14 -5 -31z m566
13 c28 -19 24 -48 -14 -111 -1 -2 -13 2 -27 9 -20 11 -27 11 -40 0 -21 -18
-31 -5 -11 15 9 9 16 24 16 33 0 42 46 75 76 54z m322 -39 c12 -3 22 -11 22
-18 0 -17 -53 -72 -57 -59 -2 6 -17 9 -33 6 -24 -3 -32 1 -41 22 -9 19 -20 26
-43 28 -17 0 -37 2 -45 2 -22 2 -25 24 -4 30 20 5 165 -2 201 -11z m-818 -13
c0 -5 -2 -10 -4 -10 -3 0 -8 5 -11 10 -3 6 -1 10 4 10 6 0 11 -4 11 -10z
m1832 -14 c7 4 8 2 4 -5 -4 -6 -12 -9 -17 -6 -5 4 -9 -4 -9 -16 0 -12 -3 -20
-6 -16 -4 3 -3 19 1 34 4 15 10 22 12 15 3 -6 10 -9 15 -6z m-1615 -30 c-8
-19 -37 -21 -37 -3 0 7 5 18 12 25 15 15 33 -1 25 -22z m76 -77 c-5 -19 -33
-27 -33 -9 0 6 5 10 10 10 6 0 10 10 10 23 0 16 3 18 9 8 5 -7 6 -22 4 -32z
m102 -39 c-3 -5 -14 -10 -23 -10 -15 0 -15 2 -2 10 20 13 33 13 25 0z m312
-85 c0 -8 -4 -12 -9 -9 -5 3 -6 10 -3 15 9 13 12 11 12 -6z m503 -166 c0 -6
-4 -7 -10 -4 -5 3 -10 11 -10 16 0 6 5 7 10 4 6 -3 10 -11 10 -16z m-433 -61
c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3 -3 4 -12 1 -19z m3 -28 c0 -5 -2 -10 -4
-10 -3 0 -8 5 -11 10 -3 6 -1 10 4 10 6 0 11 -4 11 -10z m-42 -18 c2 -7 -2
-10 -12 -6 -9 3 -16 11 -16 16 0 13 23 5 28 -10z m139 -14 c-3 -8 -6 -5 -6 6
-1 11 2 17 5 13 3 -3 4 -12 1 -19z m-47 -91 c0 -5 -4 -5 -10 -2 -5 3 -10 14
-10 23 0 15 2 15 10 2 5 -8 10 -19 10 -23z m281 6 c13 -16 12 -17 -3 -4 -10 7
-18 15 -18 17 0 8 8 3 21 -13z m196 -15 c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3
-3 4 -12 1 -19z m50 -14 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z
m-122 -24 c3 -5 1 -10 -4 -10 -6 0 -11 5 -11 10 0 6 2 10 4 10 3 0 8 -4 11
-10z m-245 -36 c0 -8 -4 -12 -10 -9 -5 3 -10 13 -10 21 0 8 5 12 10 9 6 -3 10
-13 10 -21z"/>
<path d="M2911 3704 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M3630 3139 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
-5 -10 -11z"/>
<path d="M3580 3129 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
-5 -10 -11z"/>
<path d="M4023 2723 c-1 -33 1 -35 9 -17 6 14 7 28 1 38 -7 12 -9 7 -10 -21z"/>
<path d="M4280 2693 c0 -5 5 -15 10 -23 8 -12 10 -11 10 8 0 12 -4 22 -10 22
-5 0 -10 -3 -10 -7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -1,14 +1 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/assets/images/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png"
}
],
"theme_color": "#091c58",
"background_color": "#091c58",
"display": "standalone"
}
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

28
public/web.config Normal file
View file

@ -0,0 +1,28 @@
<!--
Rewrites requires Microsoft URL Rewrite Module for IIS
Download: https://www.microsoft.com/en-us/download/details.aspx?id=47337
Debug Help: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-failed-request-tracing-to-trace-rewrite-rules
-->
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="Imported Rule 1" stopProcessing="true">
<match url="^(.*)/$" ignoreCase="false" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
</conditions>
<action type="Redirect" redirectType="Permanent" url="/{R:1}" />
</rule>
<rule name="Imported Rule 2" stopProcessing="true">
<match url="^" ignoreCase="false" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
</conditions>
<action type="Rewrite" url="index.php" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

View file

View file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -1,39 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="180.000000pt" height="180.000000pt" viewBox="0 0 180.000000 180.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,180.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1585 1356 l30 -25 -40 -41 c-55 -58 -135 -91 -135 -56 0 8 9 33 20
57 10 24 15 48 11 52 -5 5 -300 7 -657 5 -594 -4 -656 -6 -731 -23 l-83 -19 0
-123 c0 -68 4 -123 9 -123 23 0 33 -64 29 -195 -2 -112 -6 -139 -19 -149 -14
-9 -17 -34 -18 -135 l-2 -124 173 8 c95 4 247 11 338 15 161 8 875 4 935 -5
17 -2 103 -7 193 -11 l162 -7 0 143 c0 79 -3 140 -7 137 -3 -4 -12 -2 -20 4
-17 15 -19 314 -2 325 6 3 9 15 6 25 -3 11 1 26 9 33 10 10 14 39 14 94 l0 79
-52 16 c-29 9 -69 19 -88 23 -19 3 -46 15 -59 25 -13 11 -29 19 -35 19 -6 0 3
-11 19 -24z m-1306 -105 c9 -6 11 -20 6 -50 -4 -22 -7 -149 -7 -281 -1 -254 2
-270 46 -270 23 0 55 29 75 67 6 12 11 68 11 125 0 178 36 276 122 338 71 52
141 35 209 -50 71 -88 79 -350 16 -464 -50 -90 -147 -129 -215 -87 -50 32 -59
62 -33 106 13 22 16 35 9 33 -7 -2 -27 -32 -44 -67 -17 -35 -39 -70 -49 -77
-14 -10 -57 -14 -163 -14 l-145 0 7 48 c11 77 9 564 -2 594 -12 30 -3 43 38
51 44 8 105 7 119 -2z m717 -52 c14 -12 16 -42 13 -252 l-4 -239 28 -24 c33
-28 37 -29 63 -10 18 14 19 29 19 273 l0 258 51 3 c39 2 54 -1 68 -17 16 -17
16 -28 7 -115 -14 -125 -14 -328 0 -360 6 -13 27 -32 45 -42 19 -10 34 -24 34
-30 0 -6 -21 -28 -47 -48 -55 -43 -76 -42 -137 6 -34 27 -40 29 -48 14 -13
-23 -62 -46 -99 -46 -22 0 -41 10 -65 34 l-34 34 0 218 c-1 144 -5 240 -14
282 -12 56 -12 63 3 72 21 14 96 6 117 -11z m599 -33 c36 -28 105 -153 105
-191 0 -39 -37 -72 -117 -105 -87 -36 -123 -63 -123 -94 0 -27 27 -68 63 -96
l28 -22 56 42 c65 50 87 55 91 22 8 -57 -62 -140 -136 -163 -65 -19 -105 -5
-176 61 -35 33 -69 60 -75 60 -8 0 -7 6 3 16 9 8 16 23 16 32 8 332 9 343 62
406 58 71 136 83 203 32z"/>
<path d="M573 1093 c-14 -22 -19 -58 -24 -192 -3 -91 -2 -176 2 -188 13 -35
53 -57 79 -43 34 18 40 52 40 214 0 159 -9 209 -40 226 -28 15 -38 12 -57 -17z"/>
<path d="M1480 1087 c-38 -62 -36 -138 5 -153 21 -8 61 20 80 56 14 26 14 33
0 68 -9 22 -27 44 -41 51 -22 10 -26 8 -44 -22z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

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