Compare commits

..

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

323 changed files with 5682 additions and 23270 deletions

View file

@ -31,9 +31,16 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
# SENTRY
SENTRY_LARAVEL_DSN=
# LDC WEB STATUS CHECKER
# 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
LDC_STATUS_CHECKER_API_KEY="MY_KEY"
LDC_STATUS_CHECKER_UUID="UUID_PROJECT"
# LDC Web Admin Secure
LDC_WEB_API_KEY="MY_KEY"

View file

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

9
.gitignore vendored
View file

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

118
.gitlab-ci.yml Normal file
View file

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

View file

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

View file

@ -1,43 +0,0 @@
{
"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
View file

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

10
CHANGELOG.md Normal file
View file

@ -0,0 +1,10 @@
# 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,7 +1,29 @@
## reste à faire
# Laravel Base Project
## Installation
```
cp .env.example .env
php artisan key:generate
```
FO :
ts
view
route
les fichiers à la racine
## 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
```

View file

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

View file

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

View file

@ -3,12 +3,24 @@
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.
@ -20,16 +32,6 @@ 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.
*
@ -41,11 +43,57 @@ class LoginController extends Controller
}
/**
* Where to redirect users after login.
* 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.
*
* @return string
*/
protected function redirectTo(): string
public function redirectPath()
{
return response()->redirectToIntended()->getTargetUrl();
return response()->redirectToRoute('bo.dashboard')->getTargetUrl();
}
}

View file

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

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

@ -0,0 +1,18 @@
<?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,13 +20,12 @@ class Controller extends BaseController
* Build Search query on specified fields
* splitting search into words or using whole sentence
*
* @param \Illuminate\Database\Eloquent\Builder $query The eloquent query builder.
* @param string $search The query string.
* @param callable $searchQuery The query string.
* @param string ...$fields Either a simple array or a string
* in case of a string the two fields
* would be concatenate to make the search.
* @return \Illuminate\Database\Eloquent\Builder
* @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
*/
protected function searchQuery(
Builder $query,
@ -57,10 +56,10 @@ class Controller extends BaseController
/**
* Build query search on all fields
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param array $fields
* @return \Illuminate\Database\Eloquent\Builder
* @param Builder $query
* @param string $search
* @param array $fields
* @return Builder
*/
private function searchOnFields(Builder $query, string $search, array $fields): Builder
{
@ -87,9 +86,9 @@ class Controller extends BaseController
/**
* Sort columns with a query
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $ignore
* @return \Illuminate\Database\Eloquent\Builder
* @param Builder $query
* @param array $ignore
* @return Builder
*/
protected function sortQuery(Builder $query, array $ignore = []): Builder
{
@ -97,14 +96,11 @@ 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

@ -1,18 +0,0 @@
<?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,16 +34,12 @@ 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,
],
]
];
/**
@ -63,6 +59,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 mixed $request
* @param \Illuminate\Http\Request $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 route('login');
return collect($logins)->first() ?? '/';
}
abort(401);
}

View file

@ -2,7 +2,7 @@
namespace App\Http\Middleware;
use App\Lib\Helpers\ToolboxHelper;
use App\Lib\ToolboxHelper;
use Illuminate\Http\Request;
class HtmlSanitizer
@ -18,17 +18,11 @@ class HtmlSanitizer
public function handle(Request $request, \Closure $next, ...$params)
{
$inputs = $request->all();
/** @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);
}
array_walk_recursive($inputs, function (&$input, $name) use ($params) {
if (!is_string($input)) {
return;
}
if (in_array($name, $params) or in_array($rootName ?? $name, $params)) {
if (in_array($name, $params)) {
// ? Is a json input ?
if ($isJson = \json_decode($input, true) and is_array($isJson)) {
// ! Decode each json field recursive.
@ -45,8 +39,7 @@ class HtmlSanitizer
} else {
$input = strip_tags($input);
}
};
\array_walk($inputs, $cleanInput);
});
$request->merge($inputs);
return $next($request);
}

View file

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

View file

@ -2,7 +2,6 @@
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -11,28 +10,21 @@ class RedirectIfAuthenticated
/**
* Handle an incoming request.
*
* @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
* @param \Illuminate\Http\Request $request
* @param \Closure $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()) {
switch ($guard) {
case 'frontend':
return \redirect()->route('home');
case 'backend':
return \redirect()->route('bo.dashboard');
default:
throw new \RuntimeException("Unhandled guard `{$guard}` .");
}
return \redirect()->to($logins[$guard]);
}
}

View file

@ -32,10 +32,9 @@ class SecureHeaders
{
View::share('nonce', $nonce = \uniqid());
// * For VueJS dev tool to execute
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-{$nonce}' 'strict-dynamic'";
$scheme = config('app.env') === 'local' ? 'http' : 'https';
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-$nonce'";
$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');
@ -49,24 +48,16 @@ class SecureHeaders
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
"connect-src 'self' blob:;",
"img-src * data: blob:;",
'frame-src ' . implode(' ', []) . ';',
"font-src 'self' data: ;",
// "frame-src https://www.youtube.com;",
"font-src 'self' data:;",
// ! Using CKEditor will must have unsafe-inline style
// phpcs:ignore Generic.Files.LineLength.TooLong
"style-src 'self' 'unsafe-inline' " . \implode(' ', [
// * File Manager
'https://cdn.jsdelivr.net',
'https://use.fontawesome.com',
'https://fonts.googleapis.com'
]) . ';',
"style-src 'self' 'unsafe-inline';",
// ! If you dont use ckeditor use this
// "style-src 'self' 'nonce-$nonce';",
// phpcs:ignore Generic.Files.LineLength.TooLong
"script-src 'self' {$jsDev} " . \implode(' ', [
'https: blob:',
]) . ';'
"script-src 'self' $jsDev;"
];
$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,5 +11,6 @@ class VerifyCsrfToken extends Middleware
*
* @var array<int, string>
*/
protected $except = ['/csp-report'];
protected $except = [
];
}

View file

@ -1,190 +0,0 @@
<?php
namespace App\Lib\Helpers;
class ExceptionsHelper
{
/**
* Handle sentry event
*
* @param \Sentry\Event $event
* @return null|\Sentry\Event
*/
public static function sentryEventHandler(\Sentry\Event $event): ?\Sentry\Event
{
// Remove unwanted reports.
$event->setModules([]);
$event->setOsContext(null);
/** @var \Sentry\ExceptionDataBag[] */
$exceptionBag = [];
foreach ($event->getExceptions() as $exceptionDataBag) {
$exceptionBag[] = self::getAnonymizedSentryException($exceptionDataBag);
}
$event->setExceptions($exceptionBag);
return $event;
}
/**
* Anonimize Sentry Exception bag
*
* @param \Sentry\ExceptionDataBag $exceptionDataBag
* @return \Sentry\ExceptionDataBag
*/
private static function getAnonymizedSentryException(
\Sentry\ExceptionDataBag $exceptionDataBag
): \Sentry\ExceptionDataBag {
$exceptionDataBag->setValue(self::getAnonymizedMessage($exceptionDataBag->getValue()));
if ($stackTrace = $exceptionDataBag->getStacktrace()) {
$exceptionDataBag->setStacktrace(self::getAnonymizedSentryStackTrace($stackTrace));
}
return $exceptionDataBag;
}
/**
* Anonymise sentry stacktrace
*
* @param \Sentry\Stacktrace $stackTrace
* @return \Sentry\Stacktrace
*/
private static function getAnonymizedSentryStackTrace(\Sentry\Stacktrace $stackTrace): \Sentry\Stacktrace
{
/** @var \Sentry\Frame[] */
$frames = \array_reverse($stackTrace->getFrames(), true);
$i = \count($frames) - 1;
// * Remove all but keep first (index.php).
while ($i-- > 0) {
$stackTrace->removeFrame(count($stackTrace->getFrames()) - 1);
}
foreach ($frames as $index => $frame) {
$frame->setPostContext([]);
$frame->setPreContext([]);
$frame->setContextLine('');
$frame->setVars(self::getAnonymizedSentryParams($frame->getVars()));
$stackTrace->addFrame($frame);
if ($index === 0) {
$stackTrace->removeFrame(count($stackTrace->getFrames()) - 1);
}
}
return $stackTrace;
}
/**
* Get anonymized sentry params
*
* @param mixed[] $params
* @return string[]
*/
private static function getAnonymizedSentryParams(array $params): array
{
foreach ($params as &$param) {
$type = \strtolower(\gettype($param));
if (\is_string($param)) {
$type = \explode(' ', \trim($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

@ -1,444 +0,0 @@
<?php
namespace App\Lib\Helpers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* This class is used to handle files in LDC Laravel stack
*
* * The storage strategy will be using date.
* The file name may contain sufix timestamp highresolution time (hrtime or microtime) if is duplicate.
* (function() { return time() . ' ' . Str::slug(microtime(true)) . ' ' . hrtime(true); })();
*
* posts/ - The model table name
* 10/ - Year
* 04/ - Month
* 19/ - Day
* 5f/ - First two letters of md5 file
* black-cat-under-red-car.png - Slugified file name
* black-cat-under-red-car-286071690833400.png - Slugified file name that is duplicated
*
* The final path will be storage/modelfiles/posts/10/04/19/5f/black-cat-under-red-car-286071690833400.png
*
* ! No limits of file per folder will be handled.
*
* @author Munsch Jérémy <jeremy.munsch@ldc.fr>
*/
class FileStorageHelper
{
/**
* Destroy the file associate with the attribute.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @return void
*/
public static function removeFile(Model $model, string $attribute): void
{
$path = \ltrim(\urldecode($model->{$attribute}), '/');
if (\strpos($path, 'storage') !== 0) {
return;
}
$path = ToolboxHelper::mbReplace('storage/', '', $path);
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
/**
* Destroy the file associate with the attribute old value (before updated to DB).
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
* @param boolean $onlyIfUpdated
* @return void
*/
public static function removeOldFile(Model $model, string $attribute, bool $onlyIfUpdated = true): void
{
// * Ignore If removal has to be done only if the field has changed and it actually hasnt.
if ($onlyIfUpdated and $model->{$attribute} === $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

@ -1,237 +0,0 @@
<?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));
}
}
}

499
app/Lib/ToolboxHelper.php Normal file
View file

@ -0,0 +1,499 @@
<?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,10 +2,12 @@
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
{
@ -18,8 +20,7 @@ class User extends Authenticatable
* @var array<int, string>
*/
protected $fillable = [
'firstname',
'lastname',
'name',
'email',
'password',
];
@ -41,6 +42,7 @@ class User extends Authenticatable
*/
protected $casts = [
'email_verified_at' => 'datetime',
'last_connection' => 'datetime',
];
/**
@ -51,15 +53,34 @@ class User extends Authenticatable
protected static function booted()
{
// * EVENT CLOSURES.
static::creating(function (User $user) {
static::creating(function (self $user) {
static::assertEmailIsUnique($user->email);
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($user->password);
}
$user->remember_token = Str::random(10);
});
static::updating(function (User $user) {
static::updating(function (self $user) {
static::assertEmailIsUnique($user->email, $user->id);
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,12 +4,20 @@ 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.
*
@ -17,47 +25,34 @@ 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
setlocale(LC_ALL, \sprintf(
'%s_%s.UTF-8',
config('app.locale'),
\strtoupper(config('app.locale'))
));
// * Remove numeric to prevent float seperator to breaking numeric transactions
\collect([\LC_TIME, \LC_MESSAGES, \LC_COLLATE, \LC_CTYPE, \LC_MONETARY])
->map(function ($contant) {
setlocale(
$contant,
\sprintf(
'%s_%s.UTF-8',
config('app.locale'),
\strtoupper(config('app.locale'))
)
);
});
Carbon::setLocale(config('app.locale'));
$this->sharetoView();
}
// * Force bootstrap paginator from Alexis with tooltips.
Paginator::defaultView('bo.modules.pagination');
/**
* 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'); .
// * HA PROXY FIX
app()->singleton('clientIp', function () {
if (
config('app.env') === 'local'
) {
return request()->getClientIp();
}
}
return request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
});
}
}

View file

@ -6,6 +6,13 @@ 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,6 +2,7 @@
namespace App\Providers;
use App\Lib\ToolboxHelper;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
@ -17,7 +18,7 @@ class RouteServiceProvider extends ServiceProvider
*
* @var string
*/
public const HOME = '/';
public const HOME = '/home';
/**
* Define your route model bindings, pattern filters, etc.
@ -28,16 +29,16 @@ class RouteServiceProvider extends ServiceProvider
{
$this->configureRateLimiting();
$this->cspReportRoute();
ToolboxHelper::cspReportRoute();
$this->routes(function () {
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web-bo.php'));
->group(base_path('routes/web-fo.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web-fo.php'));
->group(base_path('routes/web-bo.php'));
});
}
@ -48,50 +49,8 @@ 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,7 +6,6 @@ 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
{
@ -36,8 +35,8 @@ class TranslationServiceProvider extends ServiceProvider
*/
private function collectLocalesStrings(): Collection
{
return collect(File::allFiles(resource_path('lang/')))->flatMap(function (SplFileInfo $file) {
if ($file->getRelativePath() and \strpos($file->getRelativePath(), 'vendor') !== 0) {
return collect(File::allFiles(resource_path('lang/')))->flatMap(function ($file) {
if ($file->getRelativePath()) {
return [$file->getRelativePath() => ''];
}
})->keys();

View file

@ -1,12 +1,7 @@
{
"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"
@ -14,34 +9,28 @@
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"diglactic/laravel-breadcrumbs": "^6.1",
"diglactic/laravel-breadcrumbs": "^8.0",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"kwaadpepper/enum": "^2.1",
"kwaadpepper/image-resizer": "^0.1.2",
"kwaadpepper/exception-handler": "^0.0.9",
"kwaadpepper/refresh-sitemap": "^1.0",
"laravel/framework": "^8.75",
"laravel/tinker": "^2.5",
"laravel/ui": "^3.4",
"ldc/web-admin-secure-laravel": "^1.0",
"ldc/web-status-checker-laravel": "^1.0",
"ldc/web-admin-secure-laravel": "1.0.1",
"ldc/web-status-checker-laravel": "1.0.3",
"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.8"
},
"repositories": {
"lab.s-web.fr/23": {
"type": "composer",
"url": "https://lab.s-web.fr/api/v4/group/23/-/packages/composer/packages.json"
}
"slevomat/coding-standard": "^8.3"
},
"autoload": {
"psr-4": {
@ -81,11 +70,14 @@
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
},
"gitlab-domains": [
"lab.s-web.fr"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true
"prefer-stable": true,
"repositories": {
"gitlab.com/16669037": {
"type": "composer",
"url": "https://gitlab.com/api/v4/group/16669037/-/packages/composer/packages.json"
}
}
}

3175
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -67,7 +67,7 @@ return [
|
*/
'timezone' => 'Europe/Paris',
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
@ -80,7 +80,7 @@ return [
|
*/
'locale' => 'fr',
'locale' => 'en',
/*
|--------------------------------------------------------------------------
@ -106,7 +106,7 @@ return [
|
*/
'faker_locale' => 'fr_FR',
'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
@ -173,9 +173,10 @@ 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
],
@ -220,6 +221,8 @@ 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' => 'frontend',
'guard' => 'backend',
'passwords' => 'users',
],
@ -40,12 +40,6 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'frontend' => [
'driver' => 'session',
// ! Remplacer "users" par le model authenticable voulu
'provider' => 'users',
],
],
/*

View file

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

View file

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

View file

@ -0,0 +1,15 @@
<?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', 'sentry'],
'channels' => ['single'],
'ignore_exceptions' => false,
],
@ -60,11 +60,6 @@ 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,15 +29,8 @@ return [
// * For iframes
'URI.SafeIframeRegexp' => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/)%",
'HTML.Doctype' => 'HTML 4.01 Transitional',
// 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',
'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',
'AutoFormat.AutoParagraph' => false,
'AutoFormat.RemoveEmpty' => false,
'AutoFormat.RemoveEmpty.RemoveNbsp' => false,

View file

@ -1,61 +0,0 @@
<?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,20 +14,14 @@ return [
|
*/
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
'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
]
],
'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,7 +15,6 @@ return [
'paths' => [
resource_path('views'),
resource_path('views/vendor'),
],
/*

View file

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

View file

@ -0,0 +1,36 @@
<?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,5 +13,6 @@ class DatabaseSeeder extends Seeder
*/
public function run()
{
// \App\Models\User::factory(10)->create();
}
}

5
jsconfig.json Normal file
View file

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

7607
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,56 +8,32 @@
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production",
"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"
"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"
},
"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",
"@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",
"@types/bootstrap": "^5.2.1",
"@types/webpack-env": "^1.17.0",
"axios": "^0.21",
"bootstrap": "^5.2.3",
"bootstrap": "^5.2.0",
"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.8",
"sass": "^1.54.0",
"sass-loader": "^12.6.0",
"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",
"sweetalert2": "^11.4.24",
"ts-loader": "^9.3.1",
"typescript": "^4.8.2",
"typescript-eslint": "^0.0.1-alpha.0",
"vue": "^3.2.38",
"vue-eslint-parser": "^9.1.0",
"typescript": "^4.7.4",
"vue": "^3.2.37",
"vue-loader": "^16.8.3"
}
}

View file

@ -33,6 +33,7 @@
<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"/>

View file

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

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 0 B

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,187 +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="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>

Before

Width:  |  Height:  |  Size: 13 KiB

View file

@ -1 +1,14 @@
{"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"}
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/assets/images/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png"
}
],
"theme_color": "#091c58",
"background_color": "#091c58",
"display": "standalone"
}

View file

@ -1,28 +0,0 @@
<!--
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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

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

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

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

View file

@ -1 +0,0 @@
node_modules

View file

@ -1,49 +0,0 @@
Software License Agreement
==========================
Copyright (c) 2014-2021, CKSource - Frederico Knabben. All rights reserved.
Online builder code samples are licensed under the terms of the MIT License (see Appendix A):
http://en.wikipedia.org/wiki/MIT_License
CKEditor 5 collaboration features are only available under a commercial license. [Contact us](https://ckeditor.com/contact/) for more details.
Free 30-days trials of CKEditor 5 collaboration features are available:
* https://ckeditor.com/collaboration/ - Real-time collaboration (with all features).
* https://ckeditor.com/collaboration/comments/ - Inline comments feature (without real-time collaborative editing).
* https://ckeditor.com/collaboration/track-changes/ - Track changes feature (without real-time collaborative editing).
Trademarks
----------
CKEditor is a trademark of CKSource - Frederico Knabben. All other brand
and product names are trademarks, registered trademarks or service
marks of their respective holders.
---
Appendix A: The MIT License
---------------------------
The MIT License (MIT)
Copyright (c) 2014-2021, CKSource - Frederico Knabben
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,68 +0,0 @@
# CKEditor 5 editor generated with the online builder
This repository presents a CKEditor 5 editor build generated by the [Online builder tool](https://ckeditor.com/ckeditor-5/online-builder)
## Quick start
1. Open the `sample/index.html` page in the browser.
If you picked the real-time collaboration plugins:
2. Fill the dialog with correct token, websocket and upload URL endpoints. If you do not have these yet or do not know their meaning, [contact us](https://ckeditor.com/contact/).
3. Copy the URL and share it or paste in another tab to enjoy real-time collaborative editing.
If you picked the non-real-time collaboration plugins:
2. Fill the prompt with the license key. If you do not have the license key yet [contact us](https://ckeditor.com/contact/).
## Configuring build
Changes like changing toolbar items, changing order of icons or customizing plugin configurations should be relatively easy to make. Open the `sample/index.html` file and edit the script that initialized the CKEditor 5. Save the file and refresh the browser. That's all.
*Note:* If you have any problems with browser caching use the `Ctrl + R` or `Cmd + R` shortcut depending on your system.
However if you want to remove or add a plugin to the build you need to follow the next step of this guide.
Note that it is also possible to go back to the [Online builder tool](https://ckeditor.com/ckeditor-5/online-builder) and pick other set of plugins. But we encourage you to try the harder way and to learn the principles of Node.js and CKEditor 5 ecosystems that will allow you to do more cool things in the future!
### Installation
In order to rebuild the application you need to install all dependencies first. To do it, open the terminal in the project directory and type:
```
npm install
```
Make sure that you have the `node` and `npm` installed first. If not, then follow the instructions on the [Node.js documentation page](https://nodejs.org/en/).
### Adding or removing plugins
Now you can install additional plugin in the build. Just follow the [Adding a plugin to an editor tutorial](https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/installing-plugins.html#adding-a-plugin-to-an-editor)
### Rebuilding editor
If you have already done the [Installation](#installation) and [Adding or removing plugins](#adding-or-removing-plugins) steps, you're ready to rebuild the editor by running the following command:
```
npm run build
```
This will build the CKEditor 5 to the `build` directory. You can open your browser and you should be able to see the changes you've made in the code. If not, then try to refresh also the browser cache by typing `Ctrl + R` or `Cmd + R` depending on your system.
## What's next?
Follow the guides available on https://ckeditor.com/docs/ckeditor5/latest/framework/index.html and enjoy the document editing.
## FAQ
| Where is the place to report bugs and feature requests?
You can create an issue on https://github.com/ckeditor/ckeditor5/issues including the build id - `unyb7c61pdjq-xt0bz4ltrghq`. Make sure that the question / problem is unique, please look for a possibly asked questions in the search box. Duplicates will be closed.
| Where can I learn more about the CKEditor 5 framework?
Here: https://ckeditor.com/docs/ckeditor5/latest/framework/
| Is it possible to use online builder with common frameworks like React, Vue or Angular?
Not yet, but it these integrations will be available at some point in the future.

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