Compare commits
No commits in common. "develop" and "laravel-8" have entirely different histories.
15
.env.example
|
|
@ -31,16 +31,9 @@ MAIL_ENCRYPTION=null
|
|||
MAIL_FROM_ADDRESS=null
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# Exception Handler
|
||||
MAIL_LIST_SERVICEWEB=
|
||||
# Google Analytics
|
||||
GOOGLE_ANALYTICS_ID=
|
||||
# Google Recaptcha
|
||||
GOOGLE_RECAPTCHA_PUBLIC=
|
||||
GOOGLE_RECAPTCHA_PRIVATE=
|
||||
GOOGLE_RECAPTCHA_VERSION=
|
||||
# LDC Web Status Checker
|
||||
# SENTRY
|
||||
SENTRY_LARAVEL_DSN=
|
||||
|
||||
# LDC WEB STATUS CHECKER
|
||||
LDC_STATUS_CHECKER_API_KEY="MY_KEY"
|
||||
LDC_STATUS_CHECKER_UUID="UUID_PROJECT"
|
||||
# LDC Web Admin Secure
|
||||
LDC_WEB_API_KEY="MY_KEY"
|
||||
|
|
|
|||
41
.eslintrc.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-strongly-recommended",
|
||||
"@vue/eslint-config-typescript"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.vue"],
|
||||
"rules": {
|
||||
"no-undef": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.vue"],
|
||||
"rules": {
|
||||
"indent": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["vue", "@typescript-eslint"],
|
||||
"ignorePatterns": ["webpack.mix.js", "**/vendor/*.js", "**/vendor/**/*.js"],
|
||||
"rules": {
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "double"],
|
||||
"semi": ["error", "always"],
|
||||
"vue/script-indent": ["error", 2, { "baseIndent": 0, "switchCase": 1 }],
|
||||
"vue/one-component-per-file": "off"
|
||||
}
|
||||
}
|
||||
9
.gitignore
vendored
|
|
@ -1,19 +1,14 @@
|
|||
/node_modules
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/css
|
||||
/public/js
|
||||
/public/images
|
||||
/public/fonts
|
||||
/public/build
|
||||
/public/medias
|
||||
/public/uploads
|
||||
/public/assets
|
||||
/public/cache
|
||||
/public/mix-manifest.json
|
||||
/public/sitemap.xml
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.phpunit.result.cache
|
||||
docker-compose.override.yml
|
||||
Homestead.json
|
||||
|
|
|
|||
118
.gitlab-ci.yml
|
|
@ -1,118 +0,0 @@
|
|||
stages:
|
||||
- Security
|
||||
- CodingStandards
|
||||
- Builds
|
||||
|
||||
composer-security:
|
||||
stage: Security
|
||||
image: $CI_REGISTRY/service-web-lab/ldc-web-images-docker/ldcweb-qa:php7.3-1.0
|
||||
script:
|
||||
- local-php-security-checker --format=json --path=./composer.lock > composer-security-checker-report.json
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'composer-security-checker'
|
||||
paths:
|
||||
- composer-security-checker-report.json
|
||||
allow_failure: true
|
||||
needs: []
|
||||
|
||||
npm-security:
|
||||
stage: Security
|
||||
image: node:17.9.1
|
||||
before_script:
|
||||
- npm install --location=global audit-ci@^6
|
||||
script:
|
||||
- audit-ci -p npm -o json > npm-security-checker-report.json
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'npm-security-checker'
|
||||
paths:
|
||||
- npm-security-checker-report.json
|
||||
allow_failure: true
|
||||
needs: []
|
||||
|
||||
phpcs:
|
||||
stage: CodingStandards
|
||||
image: $CI_REGISTRY/service-web-lab/ldc-web-images-docker/ldcweb-qa:php7.3-1.0
|
||||
before_script:
|
||||
- composer config gitlab-token.gitlab.com $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
|
||||
- composer install
|
||||
cache:
|
||||
paths:
|
||||
- vendor/
|
||||
script:
|
||||
- vendor/bin/phpcs --report=junit --report-file=phpcs-report.xml
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'phpcs'
|
||||
paths:
|
||||
- phpcs-report.xml
|
||||
reports:
|
||||
junit: phpcs-report.xml
|
||||
allow_failure: false
|
||||
needs:
|
||||
- composer-security
|
||||
- npm-security
|
||||
|
||||
eslint:
|
||||
stage: CodingStandards
|
||||
image: node:17.9.1
|
||||
before_script:
|
||||
- npm install
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
script:
|
||||
- npm run eslint-ci --silent 2>/dev/null 1> eslint-report.xml
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'eslint'
|
||||
paths:
|
||||
- eslint-report.xml
|
||||
reports:
|
||||
junit: eslint-report.xml
|
||||
allow_failure: false
|
||||
needs:
|
||||
- composer-security
|
||||
- npm-security
|
||||
|
||||
stylelint:
|
||||
stage: CodingStandards
|
||||
image: node:17.9.1
|
||||
before_script:
|
||||
- npm install
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
script:
|
||||
- npm run stylelint-ci --silent 2>/dev/null 1> stylelint-report.json
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'stylelint'
|
||||
paths:
|
||||
- stylelint-report.json
|
||||
allow_failure: false
|
||||
needs:
|
||||
- composer-security
|
||||
- npm-security
|
||||
|
||||
front-build:
|
||||
stage: Builds
|
||||
image: node:17.9.1
|
||||
before_script:
|
||||
- npm install
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
script:
|
||||
- npm run prod > front-build-report.json
|
||||
artifacts:
|
||||
when: on_failure
|
||||
expose_as: 'front-build-report'
|
||||
paths:
|
||||
- front-build-report.json
|
||||
allow_failure: false
|
||||
needs:
|
||||
- phpcs
|
||||
- eslint
|
||||
- stylelint
|
||||
14
.styleci.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
php:
|
||||
preset: laravel
|
||||
version: 8
|
||||
disabled:
|
||||
- no_unused_imports
|
||||
finder:
|
||||
not-name:
|
||||
- index.php
|
||||
- server.php
|
||||
js:
|
||||
finder:
|
||||
not-name:
|
||||
- webpack.mix.js
|
||||
css: true
|
||||
43
.stylelintrc.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-config-prettier",
|
||||
"stylelint-config-standard-scss",
|
||||
"stylelint-config-sass-guidelines",
|
||||
"stylelint-config-idiomatic-order"
|
||||
],
|
||||
"plugins": ["stylelint-scss"],
|
||||
"files": ["**/*.css", "**/*.scss"],
|
||||
"ignoreFiles": [
|
||||
"**/vendor/*.css",
|
||||
"**/vendor/**/*.css",
|
||||
"**/vendor/*.scss",
|
||||
"**/vendor/**/*.scss",
|
||||
"**/vendor/*.sass",
|
||||
"**/vendor/**/*.sass",
|
||||
"**/vendor/*.less",
|
||||
"**/vendor/**/*.less"
|
||||
],
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"string-quotes": "double",
|
||||
"selector-max-id": null,
|
||||
"color-no-invalid-hex": true,
|
||||
"order/properties-alphabetical-order": null,
|
||||
"max-nesting-depth": 5,
|
||||
"selector-max-compound-selectors": 5,
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"function-parentheses-space-inside": "never-single-line",
|
||||
"declaration-colon-newline-after": null,
|
||||
"scss/double-slash-comment-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"ignore": [
|
||||
"between-comments",
|
||||
"stylelint-commands",
|
||||
"inside-block"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
22
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.activeBackground": "#27b2e7",
|
||||
"activityBar.background": "#27b2e7",
|
||||
"activityBar.foreground": "#15202b",
|
||||
"activityBar.inactiveForeground": "#15202b99",
|
||||
"activityBarBadge.background": "#fce2f5",
|
||||
"activityBarBadge.foreground": "#15202b",
|
||||
"commandCenter.border": "#e7e7e799",
|
||||
"sash.hoverBorder": "#27b2e7",
|
||||
"statusBar.background": "#1695c5",
|
||||
"statusBar.foreground": "#e7e7e7",
|
||||
"statusBarItem.hoverBackground": "#27b2e7",
|
||||
"statusBarItem.remoteBackground": "#1695c5",
|
||||
"statusBarItem.remoteForeground": "#e7e7e7",
|
||||
"titleBar.activeBackground": "#1695c5",
|
||||
"titleBar.activeForeground": "#e7e7e7",
|
||||
"titleBar.inactiveBackground": "#1695c599",
|
||||
"titleBar.inactiveForeground": "#e7e7e799"
|
||||
},
|
||||
"peacock.remoteColor": "#1695c5"
|
||||
}
|
||||
10
CHANGELOG.md
|
|
@ -1,10 +0,0 @@
|
|||
# 01/08/2022
|
||||
init
|
||||
with packages :
|
||||
- kwaadpepper/exception-handler
|
||||
- kwaadpepper/refresh-sitemap
|
||||
- ldc/web-admin-secure-laravel
|
||||
- ldc/web-status-checker-laravel
|
||||
|
||||
with Google Services keys
|
||||
with gitlab-ci config file
|
||||
34
README.md
|
|
@ -1,29 +1,7 @@
|
|||
# Laravel Base Project
|
||||
## Installation
|
||||
```
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
```
|
||||
## reste à faire
|
||||
|
||||
## Crons
|
||||
Put every min in PROD :
|
||||
```
|
||||
php artisan schedule:run
|
||||
```
|
||||
|
||||
### LDC Web Status Report
|
||||
#### Available Commands
|
||||
```
|
||||
ldc:status-report
|
||||
ldc:send-status-report
|
||||
```
|
||||
### LDC Web Admin Secure
|
||||
#### Available Commands
|
||||
```
|
||||
ldcweb:admin-secure-sync
|
||||
```
|
||||
### Refresh-Sitemap
|
||||
#### Available Commands
|
||||
```
|
||||
sitemap:refresh --dry-run
|
||||
```
|
||||
FO :
|
||||
ts
|
||||
view
|
||||
route
|
||||
les fichiers à la racine
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ use Illuminate\Console\Command;
|
|||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
*/
|
||||
class CreateAdmin extends Command
|
||||
{
|
||||
/**
|
||||
|
|
@ -29,7 +26,7 @@ class CreateAdmin extends Command
|
|||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return integer
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
|
@ -50,41 +47,52 @@ class CreateAdmin extends Command
|
|||
continue;
|
||||
}
|
||||
}
|
||||
$name = null;
|
||||
while (is_null($name)) {
|
||||
$firstname = null;
|
||||
while (is_null($firstname)) {
|
||||
try {
|
||||
$tmp = $this->ask('Type the wanted user name', 'Admin');
|
||||
$validator = Validator::make(['name' => $tmp], [
|
||||
'name' => 'required|min:5,max:255'
|
||||
$tmp = $this->ask('Type the wanted user firstname', 'Admin');
|
||||
$validator = Validator::make(['firstname' => $tmp], [
|
||||
'firstname' => 'required|min:5,max:255'
|
||||
]);
|
||||
$validator->validate();
|
||||
$name = $tmp;
|
||||
$firstname = $tmp;
|
||||
} catch (ValidationException $e) {
|
||||
$this->error(sprintf(
|
||||
'Please provide a valid name %s',
|
||||
'Please provide a valid firstname %s',
|
||||
\implode(',', collect($e->errors())->flatten()->all())
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$password = null;
|
||||
while (
|
||||
($password = $this->secret(
|
||||
(!\is_null($password) ? 'Failed to confirm, ' : '') . 'Type the wanted user password'
|
||||
)) !== $this->secret('Confirm the password')
|
||||
) {
|
||||
continue;
|
||||
};
|
||||
$lastname = null;
|
||||
while (is_null($lastname)) {
|
||||
try {
|
||||
$tmp = $this->ask('Type the wanted user lastname', 'Saïtama');
|
||||
$validator = Validator::make(['lastname' => $tmp], [
|
||||
'lastname' => 'required|min:5,max:255'
|
||||
]);
|
||||
$validator->validate();
|
||||
$lastname = $tmp;
|
||||
} catch (ValidationException $e) {
|
||||
$this->error(sprintf(
|
||||
'Please provide a valid lastname %s',
|
||||
\implode(',', collect($e->errors())->flatten()->all())
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$admin = new User([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'password' => $password
|
||||
]);
|
||||
$admin->saveOrFail();
|
||||
$password = $this->secret('Type the wanted user password');
|
||||
|
||||
$user = new User();
|
||||
$user->firstname = $firstname;
|
||||
$user->lastname = $lastname;
|
||||
$user->email = $email;
|
||||
$user->password = $password;
|
||||
$user->email_verified_at = now();
|
||||
$user->saveOrFail();
|
||||
|
||||
$this->info('User created.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Kwaadpepper\ExceptionHandler\Exceptions\ExceptionHandler;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
|
|
@ -34,6 +34,9 @@ class Handler extends ExceptionHandler
|
|||
public function register()
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
if (app()->bound('sentry')) {
|
||||
app('sentry')->captureException($e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,24 +3,12 @@
|
|||
namespace App\Http\Controllers\Bo\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Foundation\Auth\ThrottlesLogins;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
use AuthenticatesUsers;
|
||||
use ThrottlesLogins;
|
||||
|
||||
/**
|
||||
* Where to redirect users after login.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = RouteServiceProvider::HOME;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
|
|
@ -32,6 +20,16 @@ class LoginController extends Controller
|
|||
$this->middleware('guest:backend')->except('logout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the guard to be used during authentication.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\StatefulGuard
|
||||
*/
|
||||
protected function guard()
|
||||
{
|
||||
return Auth::guard('backend');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application's login form.
|
||||
*
|
||||
|
|
@ -43,57 +41,11 @@ class LoginController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Attempt to log the user into the application.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return boolean
|
||||
*/
|
||||
protected function attemptLogin(Request $request)
|
||||
{
|
||||
return Auth::guard('backend')->attempt(
|
||||
$this->credentials($request),
|
||||
$request->filled('remember')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has been authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param null $user
|
||||
* @return mixed
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing, Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
|
||||
*/
|
||||
protected function authenticated(Request $request, $user)
|
||||
{
|
||||
/** @var \App\Models\User|null */
|
||||
$user = Auth::guard('backend')->user();
|
||||
if ($user) {
|
||||
$user->last_connection = now();
|
||||
$user->saveOrFail();
|
||||
Session::flash('info', trans('Vous êtes à présent connecté'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has logged out of the application.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return mixed
|
||||
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
||||
*/
|
||||
protected function loggedOut(Request $request)
|
||||
{
|
||||
Session::flash('info', trans('Vous êtes à présent déconnecté'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the post register / login redirect path.
|
||||
*
|
||||
* Where to redirect users after login.
|
||||
* @return string
|
||||
*/
|
||||
public function redirectPath()
|
||||
protected function redirectTo(): string
|
||||
{
|
||||
return response()->redirectToRoute('bo.dashboard')->getTargetUrl();
|
||||
return response()->redirectToIntended()->getTargetUrl();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
app/Http/Controllers/Bo/Auth/ResetPasswordController.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Bo\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
use ResetsPasswords;
|
||||
|
||||
/**
|
||||
* Get the guard to be used during authentication.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\StatefulGuard
|
||||
*/
|
||||
protected function guard()
|
||||
{
|
||||
return Auth::guard('backend');
|
||||
}
|
||||
|
||||
/**
|
||||
* Where to redirect users after login.
|
||||
* @return string
|
||||
*/
|
||||
protected function redirectTo(): string
|
||||
{
|
||||
return \route('bo.login');
|
||||
}
|
||||
}
|
||||
18
app/Http/Controllers/Bo/DashboardController.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Bo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Dashboard Page
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('bo.pages.dashboard');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Bo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Dashboard view
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
|
||||
*/
|
||||
public function dashboard()
|
||||
{
|
||||
return view('bo.dashboard');
|
||||
}
|
||||
}
|
||||
|
|
@ -20,12 +20,13 @@ class Controller extends BaseController
|
|||
* Build Search query on specified fields
|
||||
* splitting search into words or using whole sentence
|
||||
*
|
||||
* @param Builder $query The eloquent query builder.
|
||||
* @param string $search The query string.
|
||||
* @param callable $searchQuery The query string.
|
||||
* @param string ...$fields Either a simple array or a string
|
||||
* in case of a string the two fields would be concatenate to make the search.
|
||||
* @return Builder
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query The eloquent query builder.
|
||||
* @param string $search The query string.
|
||||
* @param callable $searchQuery The query string.
|
||||
* @param string ...$fields Either a simple array or a string
|
||||
* in case of a string the two fields
|
||||
* would be concatenate to make the search.
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function searchQuery(
|
||||
Builder $query,
|
||||
|
|
@ -56,10 +57,10 @@ class Controller extends BaseController
|
|||
/**
|
||||
* Build query search on all fields
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param string $search
|
||||
* @param array $fields
|
||||
* @return Builder
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $search
|
||||
* @param array $fields
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
private function searchOnFields(Builder $query, string $search, array $fields): Builder
|
||||
{
|
||||
|
|
@ -86,9 +87,9 @@ class Controller extends BaseController
|
|||
/**
|
||||
* Sort columns with a query
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param array $ignore
|
||||
* @return Builder
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param array $ignore
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function sortQuery(Builder $query, array $ignore = []): Builder
|
||||
{
|
||||
|
|
@ -96,11 +97,14 @@ class Controller extends BaseController
|
|||
$table = $query->getModel()->getTable();
|
||||
if (request()->sort_col) {
|
||||
Session::put("$rName.sort_col", request()->sort_col);
|
||||
Session::put("$rName.sorted", true);
|
||||
}
|
||||
if (request()->sort_way) {
|
||||
Session::put("$rName.sort_way", request()->sort_way);
|
||||
Session::put("$rName.sorted", true);
|
||||
}
|
||||
if (request()->rst) {
|
||||
Session::remove("$rName.sorted");
|
||||
Session::remove("$rName.sort_col");
|
||||
Session::remove("$rName.sort_way");
|
||||
}
|
||||
|
|
|
|||
18
app/Http/Controllers/Fo/HomeController.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Fo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('fo.pages.home');
|
||||
}
|
||||
}
|
||||
|
|
@ -34,12 +34,16 @@ class Kernel extends HttpKernel
|
|||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\SecureHeaders::class
|
||||
]
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -59,6 +63,6 @@ class Kernel extends HttpKernel
|
|||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class
|
||||
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ class Authenticate extends Middleware
|
|||
/**
|
||||
* Get the path the user should be redirected to when they are not authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param mixed $request
|
||||
* @return string|null
|
||||
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing
|
||||
*/
|
||||
protected function redirectTo($request)
|
||||
{
|
||||
$logins = [
|
||||
'frontend' => route('login'),
|
||||
'backend' => route('bo.login')
|
||||
];
|
||||
if (!$request->expectsJson()) {
|
||||
|
|
@ -31,7 +31,7 @@ class Authenticate extends Middleware
|
|||
}
|
||||
}
|
||||
}
|
||||
return collect($logins)->first() ?? '/';
|
||||
return route('login');
|
||||
}
|
||||
abort(401);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Lib\ToolboxHelper;
|
||||
use App\Lib\Helpers\ToolboxHelper;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HtmlSanitizer
|
||||
|
|
@ -18,11 +18,17 @@ class HtmlSanitizer
|
|||
public function handle(Request $request, \Closure $next, ...$params)
|
||||
{
|
||||
$inputs = $request->all();
|
||||
array_walk_recursive($inputs, function (&$input, $name) use ($params) {
|
||||
/** @var callable|null */
|
||||
$cleanInput = null;
|
||||
$cleanInput = function (&$input, $name, $rootName = null) use ($params, &$cleanInput) {
|
||||
// * Nested element array
|
||||
if (is_array($input)) {
|
||||
array_walk_recursive($input, $cleanInput, $name);
|
||||
}
|
||||
if (!is_string($input)) {
|
||||
return;
|
||||
}
|
||||
if (in_array($name, $params)) {
|
||||
if (in_array($name, $params) or in_array($rootName ?? $name, $params)) {
|
||||
// ? Is a json input ?
|
||||
if ($isJson = \json_decode($input, true) and is_array($isJson)) {
|
||||
// ! Decode each json field recursive.
|
||||
|
|
@ -39,7 +45,8 @@ class HtmlSanitizer
|
|||
} else {
|
||||
$input = strip_tags($input);
|
||||
}
|
||||
});
|
||||
};
|
||||
\array_walk($inputs, $cleanInput);
|
||||
$request->merge($inputs);
|
||||
return $next($request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,5 @@ class PreventRequestsDuringMaintenance extends Middleware
|
|||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
];
|
||||
protected $except = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
|
|
@ -10,21 +11,28 @@ class RedirectIfAuthenticated
|
|||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @param string|null ...$guards
|
||||
* @phpcs:disable Generic.Files.LineLength.TooLong
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
|
||||
* @param string|null ...$guards
|
||||
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
|
||||
* @throws \RuntimeException If guard is unhandled.
|
||||
* @phpcs:enable
|
||||
*/
|
||||
public function handle(Request $request, \Closure $next, ...$guards)
|
||||
public function handle(Request $request, Closure $next, ...$guards)
|
||||
{
|
||||
$logins = [
|
||||
'backend' => route('bo.dashboard')
|
||||
];
|
||||
$guards = empty($guards) ? [null] : $guards;
|
||||
|
||||
foreach ($guards as $guard) {
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return \redirect()->to($logins[$guard]);
|
||||
switch ($guard) {
|
||||
case 'frontend':
|
||||
return \redirect()->route('home');
|
||||
case 'backend':
|
||||
return \redirect()->route('bo.dashboard');
|
||||
default:
|
||||
throw new \RuntimeException("Unhandled guard `{$guard}` .");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,9 +32,10 @@ class SecureHeaders
|
|||
{
|
||||
View::share('nonce', $nonce = \uniqid());
|
||||
// * For VueJS dev tool to execute
|
||||
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-$nonce'";
|
||||
$jsDev = config('app.debug') ? "'unsafe-eval' 'unsafe-inline'" : "'nonce-{$nonce}' 'strict-dynamic'";
|
||||
$scheme = config('app.env') === 'local' ? 'http' : 'https';
|
||||
$reportUri = Route::has('cspReportUri') ?
|
||||
\sprintf('report-uri %s', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : '';
|
||||
\sprintf(' report-uri %s ;', \parse_url(route('cspReportUri'), \PHP_URL_PATH)) : '';
|
||||
$this->removeUnwantedHeaders($this->unwantedHeaderList);
|
||||
$response = $next($request);
|
||||
$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
|
||||
|
|
@ -48,16 +49,24 @@ class SecureHeaders
|
|||
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
|
||||
"connect-src 'self' blob:;",
|
||||
"img-src * data: blob:;",
|
||||
// "frame-src https://www.youtube.com;",
|
||||
"font-src 'self' data:;",
|
||||
'frame-src ' . implode(' ', []) . ';',
|
||||
"font-src 'self' data: ;",
|
||||
// ! Using CKEditor will must have unsafe-inline style
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
"style-src 'self' 'unsafe-inline';",
|
||||
"style-src 'self' 'unsafe-inline' " . \implode(' ', [
|
||||
// * File Manager
|
||||
'https://cdn.jsdelivr.net',
|
||||
'https://use.fontawesome.com',
|
||||
'https://fonts.googleapis.com'
|
||||
]) . ';',
|
||||
// ! If you dont use ckeditor use this
|
||||
// "style-src 'self' 'nonce-$nonce';",
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
"script-src 'self' $jsDev;"
|
||||
"script-src 'self' {$jsDev} " . \implode(' ', [
|
||||
'https: blob:',
|
||||
]) . ';'
|
||||
];
|
||||
|
||||
$response->headers->set(
|
||||
'Content-Security-Policy',
|
||||
implode(' ', $cspPolicies) . $reportUri
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -11,6 +11,5 @@ class VerifyCsrfToken extends Middleware
|
|||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
];
|
||||
protected $except = ['/csp-report'];
|
||||
}
|
||||
|
|
|
|||
190
app/Lib/Helpers/ExceptionsHelper.php
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\Helpers;
|
||||
|
||||
class ExceptionsHelper
|
||||
{
|
||||
/**
|
||||
* Handle sentry event
|
||||
*
|
||||
* @param \Sentry\Event $event
|
||||
* @return null|\Sentry\Event
|
||||
*/
|
||||
public static function sentryEventHandler(\Sentry\Event $event): ?\Sentry\Event
|
||||
{
|
||||
// Remove unwanted reports.
|
||||
$event->setModules([]);
|
||||
$event->setOsContext(null);
|
||||
|
||||
/** @var \Sentry\ExceptionDataBag[] */
|
||||
$exceptionBag = [];
|
||||
foreach ($event->getExceptions() as $exceptionDataBag) {
|
||||
$exceptionBag[] = self::getAnonymizedSentryException($exceptionDataBag);
|
||||
}
|
||||
$event->setExceptions($exceptionBag);
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonimize Sentry Exception bag
|
||||
*
|
||||
* @param \Sentry\ExceptionDataBag $exceptionDataBag
|
||||
* @return \Sentry\ExceptionDataBag
|
||||
*/
|
||||
private static function getAnonymizedSentryException(
|
||||
\Sentry\ExceptionDataBag $exceptionDataBag
|
||||
): \Sentry\ExceptionDataBag {
|
||||
$exceptionDataBag->setValue(self::getAnonymizedMessage($exceptionDataBag->getValue()));
|
||||
if ($stackTrace = $exceptionDataBag->getStacktrace()) {
|
||||
$exceptionDataBag->setStacktrace(self::getAnonymizedSentryStackTrace($stackTrace));
|
||||
}
|
||||
return $exceptionDataBag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymise sentry stacktrace
|
||||
*
|
||||
* @param \Sentry\Stacktrace $stackTrace
|
||||
* @return \Sentry\Stacktrace
|
||||
*/
|
||||
private static function getAnonymizedSentryStackTrace(\Sentry\Stacktrace $stackTrace): \Sentry\Stacktrace
|
||||
{
|
||||
/** @var \Sentry\Frame[] */
|
||||
$frames = \array_reverse($stackTrace->getFrames(), true);
|
||||
$i = \count($frames) - 1;
|
||||
// * Remove all but keep first (index.php).
|
||||
while ($i-- > 0) {
|
||||
$stackTrace->removeFrame(count($stackTrace->getFrames()) - 1);
|
||||
}
|
||||
foreach ($frames as $index => $frame) {
|
||||
$frame->setPostContext([]);
|
||||
$frame->setPreContext([]);
|
||||
$frame->setContextLine('');
|
||||
$frame->setVars(self::getAnonymizedSentryParams($frame->getVars()));
|
||||
$stackTrace->addFrame($frame);
|
||||
if ($index === 0) {
|
||||
$stackTrace->removeFrame(count($stackTrace->getFrames()) - 1);
|
||||
}
|
||||
}
|
||||
return $stackTrace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get anonymized sentry params
|
||||
*
|
||||
* @param mixed[] $params
|
||||
* @return string[]
|
||||
*/
|
||||
private static function getAnonymizedSentryParams(array $params): array
|
||||
{
|
||||
foreach ($params as &$param) {
|
||||
$type = \strtolower(\gettype($param));
|
||||
if (\is_string($param)) {
|
||||
$type = \explode(' ', \trim($param))[0] ?? null;
|
||||
}
|
||||
switch ($type) {
|
||||
case 'Lambda':
|
||||
case 'Object':
|
||||
continue 2;
|
||||
case 'array':
|
||||
$param = sprintf('Array(%d)', \count($param));
|
||||
continue 2;
|
||||
default:
|
||||
if (\is_numeric($param)) {
|
||||
$param = \ucfirst($type);
|
||||
continue 2;
|
||||
}
|
||||
if (\is_string($param)) {
|
||||
$param = sprintf('String(%d)', \strlen($param));
|
||||
continue 2;
|
||||
}
|
||||
} //end switch
|
||||
} //end foreach
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Anonymize at least Sql queries
|
||||
*
|
||||
* @param string $message
|
||||
* @return string
|
||||
*/
|
||||
public static function getAnonymizedMessage(string $message): string
|
||||
{
|
||||
$message = \preg_replace('/^(.*)( \(SQL:.*\))$/', '$1', $message) ?? $message;
|
||||
if ($sqlPos = \strpos($message, 'to use near')) {
|
||||
$message = \mb_strcut($message, 0, $sqlPos);
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize stack trace
|
||||
*
|
||||
* @param \Throwable $e
|
||||
* @return string
|
||||
*/
|
||||
public static function getAnonymizedStackTrace(\Throwable $e): string
|
||||
{
|
||||
$projectPath = \realpath(\sprintf('%s/../', \app_path()));
|
||||
$traceStack = $e->getTrace();
|
||||
$traceStr = '';
|
||||
$maxStackStage = 0;
|
||||
foreach ($traceStack as $stackStage => $stack) {
|
||||
$maxStackStage = $stackStage;
|
||||
$traceStr .= \sprintf(
|
||||
'#%s %s(%d): ',
|
||||
$stackStage,
|
||||
str_replace($projectPath, '', $stack['file'] ?? ''),
|
||||
$stack['line'] ?? ''
|
||||
);
|
||||
if (isset($stack['class'])) {
|
||||
$traceStr .= "{$stack['class']}->{$stack['function']}(";
|
||||
if (isset($stack['args'])) {
|
||||
$traceStr .= self::handleArgs($stack);
|
||||
}
|
||||
$traceStr .= ")\n";
|
||||
} elseif (isset($stack['function'])) {
|
||||
$traceStr .= "{$stack['function']}(";
|
||||
if (isset($stack['args'])) {
|
||||
$traceStr .= self::handleArgs($stack);
|
||||
}
|
||||
$traceStr .= ")\n";
|
||||
}
|
||||
} //end foreach
|
||||
$maxStackStage++;
|
||||
$traceStr .= "#$maxStackStage {main}";
|
||||
return $traceStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Args
|
||||
*
|
||||
* @param array $stack
|
||||
* @return string
|
||||
*/
|
||||
public static function handleArgs(array $stack): string
|
||||
{
|
||||
$traceArgs = '';
|
||||
$argsCount = count($stack['args']);
|
||||
for ($i = 0; $i < $argsCount; $i++) {
|
||||
$type = \gettype($stack['args'][$i]);
|
||||
switch ($type) {
|
||||
case 'object':
|
||||
$traceArgs .= sprintf('Object(%s)', \get_class($stack['args'][$i]));
|
||||
break;
|
||||
case 'array':
|
||||
$traceArgs .= sprintf('Array(%d)', \count($stack['args'][$i]));
|
||||
break;
|
||||
case 'string':
|
||||
$traceArgs .= sprintf('String(%d)', \strlen($stack['args'][$i]));
|
||||
break;
|
||||
default:
|
||||
$traceArgs .= \gettype($stack['args'][$i]);
|
||||
}
|
||||
$traceArgs .= ($i !== ($argsCount - 1) ? ',' : '');
|
||||
}
|
||||
return $traceArgs;
|
||||
}
|
||||
}
|
||||
444
app/Lib/Helpers/FileStorageHelper.php
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\Helpers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* This class is used to handle files in LDC Laravel stack
|
||||
*
|
||||
* * The storage strategy will be using date.
|
||||
* The file name may contain sufix timestamp highresolution time (hrtime or microtime) if is duplicate.
|
||||
* (function() { return time() . ' ' . Str::slug(microtime(true)) . ' ' . hrtime(true); })();
|
||||
*
|
||||
* posts/ - The model table name
|
||||
* 10/ - Year
|
||||
* 04/ - Month
|
||||
* 19/ - Day
|
||||
* 5f/ - First two letters of md5 file
|
||||
* black-cat-under-red-car.png - Slugified file name
|
||||
* black-cat-under-red-car-286071690833400.png - Slugified file name that is duplicated
|
||||
*
|
||||
* The final path will be storage/modelfiles/posts/10/04/19/5f/black-cat-under-red-car-286071690833400.png
|
||||
*
|
||||
* ! No limits of file per folder will be handled.
|
||||
*
|
||||
* @author Munsch Jérémy <jeremy.munsch@ldc.fr>
|
||||
*/
|
||||
class FileStorageHelper
|
||||
{
|
||||
/**
|
||||
* Destroy the file associate with the attribute.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeFile(Model $model, string $attribute): void
|
||||
{
|
||||
$path = \ltrim(\urldecode($model->{$attribute}), '/');
|
||||
if (\strpos($path, 'storage') !== 0) {
|
||||
return;
|
||||
}
|
||||
$path = ToolboxHelper::mbReplace('storage/', '', $path);
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the file associate with the attribute old value (before updated to DB).
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @param boolean $onlyIfUpdated
|
||||
* @return void
|
||||
*/
|
||||
public static function removeOldFile(Model $model, string $attribute, bool $onlyIfUpdated = true): void
|
||||
{
|
||||
// * Ignore If removal has to be done only if the field has changed and it actually hasnt.
|
||||
if ($onlyIfUpdated and $model->{$attribute} === $model->getOriginal($attribute)) {
|
||||
return;
|
||||
}
|
||||
$originalPath = \ltrim(\urldecode($model->getOriginal($attribute)), '/');
|
||||
if (\strpos($originalPath, 'storage') !== 0) {
|
||||
return;
|
||||
}
|
||||
$originalPath = ToolboxHelper::mbReplace('storage/', '', $originalPath);
|
||||
if (Storage::disk('public')->exists($originalPath)) {
|
||||
Storage::disk('public')->delete($originalPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a file using optimized storage strategy
|
||||
*
|
||||
* You can use filename param to overload file name.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model The model that will hold the picture.
|
||||
* @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $file The file, will just return itself is string.
|
||||
* @param boolean $slugify Force filename slugification.
|
||||
* @param string|null $filename This string may be used to store the file.
|
||||
* @param boolean $private To store the file in a private space.
|
||||
* @return string
|
||||
* @throws \RuntimeException If file copy fails.
|
||||
*/
|
||||
public static function storeFile(
|
||||
Model $model,
|
||||
$file,
|
||||
bool $slugify = false,
|
||||
string $filename = null,
|
||||
bool $private = false
|
||||
): string {
|
||||
$tableName = $model->getTable();
|
||||
if (\is_object($file) and $file instanceof \Illuminate\Http\UploadedFile) {
|
||||
if ($private) {
|
||||
throw new \RuntimeException('Private storage for uploaded files is unhandled');
|
||||
}
|
||||
/** @var \Illuminate\Http\UploadedFile $file */
|
||||
$filename = $filename ?: $file->getClientOriginalName();
|
||||
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
|
||||
$newPath = self::prepareMoveFile($tableName, $filename);
|
||||
$newPath = str_replace(storage_path('app'), '', $newPath);
|
||||
$file->storePubliclyAs(
|
||||
pathinfo($newPath, \PATHINFO_DIRNAME),
|
||||
pathinfo($newPath, \PATHINFO_BASENAME)
|
||||
);
|
||||
return 'storage' . str_replace('/public', '', $newPath);
|
||||
}
|
||||
if (\is_object($file) and $file instanceof \SplFileInfo) {
|
||||
/** @var \SplFileInfo $file */
|
||||
$filename = $filename ?: $file->getBaseName();
|
||||
$filename = $slugify ? self::slugifyFileName($filename) : $filename;
|
||||
$newPath = self::prepareMoveFile($tableName, $filename, $private);
|
||||
if (!File::copy($file->getRealPath(), $newPath)) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'Failed to copy file %s to %s',
|
||||
$file->getRealPath(),
|
||||
$newPath
|
||||
));
|
||||
}
|
||||
return $private ?
|
||||
\ltrim(str_replace(\storage_path('app/private'), '/storage/app/private', $newPath), '/') :
|
||||
\ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/');
|
||||
}
|
||||
return strval($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store all files from string attribute html
|
||||
* If oldHtmlString is passed, a diff is done with new and old files
|
||||
* to keep only files that are present in the new html string.
|
||||
*
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model The model that will hold the picture.
|
||||
* @param string $htmlString The html string to parse and modify files.
|
||||
* @param string|null $oldHtmlString The old html string with alreadyParsed
|
||||
* files using storeModelFilesFromHtml.
|
||||
* @return string Html with files pathes replaced using storage strategy.
|
||||
*/
|
||||
public static function storeModelFilesFromHtml(
|
||||
Model $model,
|
||||
string $htmlString,
|
||||
string $oldHtmlString = null
|
||||
): string {
|
||||
\collect(self::onlyUploadedFilesWithFileManager(self::findFilesInHtml($htmlString)))
|
||||
->each(function (string $path) use ($model, &$htmlString) {
|
||||
/** @var \Illuminate\Filesystem\FilesystemAdapter */
|
||||
$disk = Storage::disk('uploads');
|
||||
$fileInfo = new \SplFileInfo($disk->path(
|
||||
ToolboxHelper::mbReplace('/uploads/', '', \urldecode($path))
|
||||
));
|
||||
$htmlString = ToolboxHelper::mbReplace(
|
||||
$path,
|
||||
'/' . self::storeFile($model, $fileInfo, true),
|
||||
$htmlString
|
||||
);
|
||||
});
|
||||
|
||||
if ($oldHtmlString) {
|
||||
self::removeOldModelFilesFromHtml($htmlString, $oldHtmlString);
|
||||
}
|
||||
|
||||
return $htmlString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all old files from string attribute html that are stored in public that
|
||||
* are nove present anymore on new html string
|
||||
*
|
||||
* @param string $newHtmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
|
||||
* @param string $oldHtmlString The old html string with alreadyParsed files using storeModelFilesFromHtml.
|
||||
* @return void
|
||||
*/
|
||||
public static function removeOldModelFilesFromHtml(string $newHtmlString, string $oldHtmlString): void
|
||||
{
|
||||
self::removeOldFiles(self::findFilesInHtml($oldHtmlString), self::findFilesInHtml($newHtmlString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all old files that are stored in public that
|
||||
* are not present anymore on new array files
|
||||
*
|
||||
* @param string[] $oldFiles The old array files.
|
||||
* @param string[] $newFiles The new array files.
|
||||
* @return void
|
||||
*/
|
||||
public static function removeOldFiles(array $oldFiles, array $newFiles): void
|
||||
{
|
||||
$oldFiles = \collect(self::onlyUploadedFilesWithFileStorageHelper($oldFiles));
|
||||
$filesToKeep = \collect(self::onlyUploadedFilesWithFileStorageHelper($newFiles));
|
||||
$filesToRemove = $oldFiles->diff($filesToKeep);
|
||||
$filesToRemove->each(function (string $publicStorageFilePath) {
|
||||
$path = ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($publicStorageFilePath), '/'));
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all files from string attribute html that are stored in public
|
||||
*
|
||||
* @param string $htmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
|
||||
* @return void
|
||||
*/
|
||||
public static function removeModelFilesOfHtml(string $htmlString): void
|
||||
{
|
||||
$filesToRemove = \collect(self::onlyUploadedFilesWithFileStorageHelper(self::findFilesInHtml($htmlString)));
|
||||
$filesToRemove->each(function (string $publicStorageFilePath) {
|
||||
$path = ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($publicStorageFilePath), '/'));
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all occurence of files in html
|
||||
* @param string $html
|
||||
* @return array
|
||||
*/
|
||||
public static function findFilesInHtml(string $html): array
|
||||
{
|
||||
preg_match_all('/href="(?P<href>.*)"|src="(?P<src>.*)"|url="(?P<url>.*)"/Um', $html, $m, PREG_SET_ORDER, 0);
|
||||
return collect($m)->pluck('href')->filter()
|
||||
->concat(collect($m)->pluck('url')->filter())
|
||||
->concat(collect($m)->pluck('src')->filter())
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter array to get only uploaded files that exists
|
||||
* on disk 'public' (using helper storage strategy)
|
||||
*
|
||||
* @param array $urls
|
||||
* @return array
|
||||
*/
|
||||
public static function onlyUploadedFilesWithFileStorageHelper(array $urls): array
|
||||
{
|
||||
return \collect($urls)->unique()
|
||||
// * Filter to get only /uploads and using app url or empty
|
||||
->filter(function (string $url) {
|
||||
$path = \parse_url($url, \PHP_URL_PATH);
|
||||
return \strpos(\ltrim($path, '/'), 'storage/modelfiles') === 0;
|
||||
})->filter(function (string $url) {
|
||||
$path = \parse_url($url, \PHP_URL_PATH);
|
||||
return Storage::disk('public')
|
||||
->exists(ToolboxHelper::mbReplace('storage/', '', \ltrim(\urldecode($path), '/')));
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter array to get only uploaded files that exists
|
||||
* on disk 'uploads'
|
||||
*
|
||||
* @param array $urls
|
||||
* @return array
|
||||
*/
|
||||
private static function onlyUploadedFilesWithFileManager(array $urls): array
|
||||
{
|
||||
return \collect($urls)->unique()
|
||||
// * Filter to get only /uploads and using app url or empty
|
||||
->filter(function (string $url) {
|
||||
$host = \parse_url($url, \PHP_URL_HOST);
|
||||
$path = \parse_url($url, \PHP_URL_PATH);
|
||||
return ($host === \parse_url(config('app.url'), PHP_URL_HOST) or \is_null($host)) and
|
||||
\strpos(\ltrim($path, '/'), 'uploads') === 0;
|
||||
})->filter(function (string $url) {
|
||||
$path = \parse_url($url, \PHP_URL_PATH);
|
||||
return Storage::disk('uploads')
|
||||
->exists(ToolboxHelper::mbReplace('uploads/', '', \ltrim(\urldecode($path), '/')));
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare storage path and return
|
||||
* the file full path
|
||||
*
|
||||
* @param string $tableName Model table name.
|
||||
* @param string $filename The basename for the file.
|
||||
* @param boolean $private May the file be stored in a private dir.
|
||||
* @return string
|
||||
*/
|
||||
private static function prepareMoveFile(string $tableName, string $filename, bool $private = false): string
|
||||
{
|
||||
// * Security filter + beautifier
|
||||
$filename = self::sanitizeFileName($filename);
|
||||
// * Security filter
|
||||
$filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
|
||||
// * Slugify filename
|
||||
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
|
||||
(\strlen($ext) ? ".{$ext}" : '');
|
||||
|
||||
$folderPath = self::getStoragePath(
|
||||
$private ? 'app/private/modelfiles' : 'app/public/modelfiles',
|
||||
$tableName,
|
||||
$filename,
|
||||
$private
|
||||
);
|
||||
$filename = self::getFileUniqueName($folderPath, $filename);
|
||||
return "{$folderPath}/{$filename}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sub folders to store files using pattern
|
||||
* storage/modelfiles/posts/10/04/19/5f
|
||||
* year/month/day/md5 first 2 letters
|
||||
*
|
||||
* @param string $storage_dir
|
||||
* @param string $table
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
private static function getStoragePath(string $storage_dir, string $table, string $filename): string
|
||||
{
|
||||
$filenameHash = \md5($filename);
|
||||
$pathFolder = \storage_path(sprintf(
|
||||
'%s/%s/%s/%s/%s/%s',
|
||||
$storage_dir,
|
||||
$table,
|
||||
gmdate('y'),
|
||||
gmdate('m'),
|
||||
gmdate('d'),
|
||||
substr($filenameHash, 0, 2)
|
||||
));
|
||||
if (!File::exists($pathFolder)) {
|
||||
File::makeDirectory($pathFolder, 0755, true);
|
||||
}
|
||||
return $pathFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique filename if the path is already occupied
|
||||
*
|
||||
* @param string $folderPath
|
||||
* @param string $filename
|
||||
* @return string
|
||||
* @throws \RuntimeException If a unique filename cannot be obtained.
|
||||
*/
|
||||
private static function getFileUniqueName(string $folderPath, string $filename): string
|
||||
{
|
||||
$attempts = 0;
|
||||
$maxAttempts = 20;
|
||||
while (File::exists("{$folderPath}/{$filename}")) {
|
||||
if ($attempts++ >= $maxAttempts) {
|
||||
throw new \RuntimeException("Failed to has a unique filename for {$folderPath}/{$filename}");
|
||||
}
|
||||
$filename = pathinfo($filename, \PATHINFO_FILENAME) .
|
||||
\uniqid('-') . '.' .
|
||||
pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
}
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slugify the filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
public static function slugifyFileName(string $filename): string
|
||||
{
|
||||
return Str::slug(pathinfo($filename, \PATHINFO_FILENAME)) . '.' . pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize File name for uploaded files
|
||||
*
|
||||
* @param string $filename The original uploaded client file name.
|
||||
* @param boolean $beautify Beautify the filename.
|
||||
* @return string The sanitized file name.
|
||||
*/
|
||||
public static function sanitizeFileName(string $filename, bool $beautify = true): string
|
||||
{
|
||||
// * Sanitize filename
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
$filename = preg_replace(
|
||||
'~
|
||||
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
||||
[\x00-\x1F]| # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
|
||||
[\x7F\xA0\xAD]| # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
|
||||
[#\[\]@!$&\'()+,;=]| # URI reserved https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
[{}^\~`] # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
|
||||
~x',
|
||||
'-',
|
||||
$filename
|
||||
);
|
||||
// phpcs:enable
|
||||
// * Avoids '.' '..' or '.hiddenFiles' .
|
||||
$filename = Str::of($filename)->ltrim('.-');
|
||||
// * Optional beautification
|
||||
if ($beautify) {
|
||||
$filename = static::beautifyFilename($filename);
|
||||
}
|
||||
// * Maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
|
||||
/** @var string */
|
||||
$extension = pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
/** @var string */
|
||||
$filename = pathinfo($filename, \PATHINFO_FILENAME);
|
||||
|
||||
$encoding = mb_detect_encoding($filename);
|
||||
$encoding = $encoding ? $encoding : null;
|
||||
return \mb_strcut(
|
||||
$filename,
|
||||
0,
|
||||
255 - (!empty($extension) ? strlen($extension) + 1 : 0),
|
||||
$encoding
|
||||
) . (!empty($extension) ? ".$extension" : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautify a filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
private static function beautifyFilename(string $filename): string
|
||||
{
|
||||
// Reduce consecutive characters.
|
||||
$filename = preg_replace([
|
||||
// * "file name.zip" becomes 'file-name.zip'
|
||||
'/ +/',
|
||||
// * "file___name.zip" becomes 'file-name.zip'
|
||||
'/_+/',
|
||||
// * "file---name.zip" becomes 'file-name.zip'
|
||||
'/-+/'
|
||||
], '-', $filename);
|
||||
$filename = preg_replace([
|
||||
// * "file--.--.-.--name.zip" becomes "file.name.zip"
|
||||
'/-*\.-*/',
|
||||
// * "file...name..zip" becomes "file.name.zip"
|
||||
'/\.{2,}/'
|
||||
], '.', $filename);
|
||||
// Lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 .
|
||||
$filename = mb_strtolower($filename, mb_detect_encoding($filename));
|
||||
// * ".file-name.-" becomes "file-name"
|
||||
$filename = trim($filename, '.-');
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
237
app/Lib/Helpers/ToolboxHelper.php
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\Helpers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ToolboxHelper
|
||||
{
|
||||
/**
|
||||
* Strip scheme (https) on oembed urls
|
||||
* Only embed allowed are for MP4 self hosted videos right now
|
||||
*
|
||||
* https is force by ckeditor MediaElements which natively supports
|
||||
* only cloud hosted medias (youtube, spotify etc..)
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
public static function stripCkEditorSchemesOnMediaInputs(string $input): string
|
||||
{
|
||||
$input = \preg_replace('/(?<=<oembed url=")(https:\/\/)(?=.*?">)/m', '', $input);
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That no Back Office url is present on txt
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $text
|
||||
* @return string
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalBoUrls(string $field, string $text): string
|
||||
{
|
||||
$routes = Route::getRoutes();
|
||||
// * Url replace and email replace.
|
||||
$text = preg_replace_callback(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
|
||||
function ($matches) use ($routes, $field) {
|
||||
$match = collect($matches)->first() ?? '';
|
||||
$url = $match;
|
||||
try {
|
||||
$request = Request::create($url);
|
||||
// If a route is match then it do not throws exception.
|
||||
if (
|
||||
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
|
||||
trim($routes->match($request)->getPrefix() ?? '', '/') === 'bo'
|
||||
) {
|
||||
// Removes the matched url.
|
||||
throw ValidationException::withMessages([
|
||||
$field => ':attribute : Il est interdit de coller une url du back office dans ce champ'
|
||||
]);
|
||||
}
|
||||
return $match;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
return $match;
|
||||
}
|
||||
},
|
||||
$text
|
||||
);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That only external url is present on txt
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $text
|
||||
* @return string
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalUrls(string $field, string $text): string
|
||||
{
|
||||
self::assertNoInternalBoUrls($field, $text);
|
||||
$routes = Route::getRoutes();
|
||||
// * Url replace and email replace.
|
||||
$text = preg_replace_callback(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
|
||||
function ($matches) use ($routes, $field) {
|
||||
$match = collect($matches)->first() ?? '';
|
||||
$url = $match;
|
||||
try {
|
||||
$request = Request::create($url);
|
||||
// If a route is match then it do not throws exception.
|
||||
if (
|
||||
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
|
||||
$routes->match($request)
|
||||
) {
|
||||
// Removes the matched url.
|
||||
throw ValidationException::withMessages([
|
||||
$field => sprintf(
|
||||
':attribute : Il est interdit de coller une url de %s dans ce champ',
|
||||
config('app.name')
|
||||
)
|
||||
]);
|
||||
}
|
||||
return $match;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
return $match;
|
||||
}
|
||||
},
|
||||
$text
|
||||
);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutltibyte string replace
|
||||
* @param mixed $search
|
||||
* @param mixed $replace
|
||||
* @param mixed $subject
|
||||
* @param integer $count
|
||||
* @return mixed
|
||||
*/
|
||||
public static function mbReplace($search, $replace, $subject, int &$count = 0)
|
||||
{
|
||||
if (!is_array($search) && is_array($replace)) {
|
||||
return false;
|
||||
}
|
||||
if (is_array($subject)) {
|
||||
// Call mb_replace for each single string in $subject .
|
||||
foreach ($subject as &$string) {
|
||||
$string = &self::mbReplace($search, $replace, $string, $count);
|
||||
}
|
||||
} elseif (is_array($search)) {
|
||||
if (!is_array($replace)) {
|
||||
foreach ($search as &$string) {
|
||||
$subject = self::mbReplace($string, $replace, $subject, $count);
|
||||
}
|
||||
} else {
|
||||
$n = max(count($search), count($replace));
|
||||
while ($n--) {
|
||||
$subject = self::mbReplace(current($search), current($replace), $subject, $count);
|
||||
next($search);
|
||||
next($replace);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$parts = mb_split(preg_quote($search), $subject);
|
||||
$count = count($parts) - 1;
|
||||
$subject = implode($replace, $parts);
|
||||
} //end if
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the field is unique
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $field
|
||||
* @param mixed $value
|
||||
* @param integer|null $id
|
||||
* @param callable|null $query
|
||||
* @return void
|
||||
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
|
||||
*/
|
||||
public static function assertFieldIsUnique(
|
||||
string $table,
|
||||
string $field,
|
||||
$value,
|
||||
?int $id = null,
|
||||
?callable $query = null
|
||||
) {
|
||||
if (!$query) {
|
||||
Validator::make([$field => $value], [
|
||||
self::mbReplace('.', '\.', $field) => Rule::unique($table, $field)->ignore($id),
|
||||
], [
|
||||
self::mbReplace('.', '\.', "{$field}.unique")
|
||||
=> trans('La valeur :value pour le champ :attribute est déjà utilisé', [
|
||||
'attribute' => $field,
|
||||
'value' => $value
|
||||
])
|
||||
])->validate();
|
||||
return;
|
||||
}
|
||||
Validator::make([$field => $value], [
|
||||
$field => Rule::unique($table)->where($query)->ignore($id),
|
||||
], [
|
||||
$field . '.unique' => trans('La valeur pour le champ :attribute est déjà utilisé', [
|
||||
'attribute' => $field
|
||||
])
|
||||
])->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert deeply an object to array
|
||||
*
|
||||
* @param array|object $input
|
||||
* @return array
|
||||
* @throws \RuntimeException If $input var is a scalar value.
|
||||
*/
|
||||
public static function objectToArray($input): array
|
||||
{
|
||||
// Only process if it's an object or array being passed to the function.
|
||||
if (is_object($input) or is_array($input)) {
|
||||
$ret = (array)$input;
|
||||
foreach ($ret as &$item) {
|
||||
// Recursively process EACH element only if not scalar.
|
||||
$item = (is_object($item) or is_array($item)) ?
|
||||
self::objectToArray($item) : $item;
|
||||
}
|
||||
return $ret;
|
||||
} else {
|
||||
throw new \RuntimeException('cannot handle $input of type ' . \gettype($input));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert deeply an array to object
|
||||
*
|
||||
* @param array|object $input
|
||||
* @return object
|
||||
* @throws \RuntimeException If $input var is a scalar value.
|
||||
*/
|
||||
public static function arrayToObject($input): object
|
||||
{
|
||||
// Only process if it's an object or array being passed to the function.
|
||||
if (is_object($input) or is_array($input)) {
|
||||
$ret = (object)$input;
|
||||
foreach ($ret as &$item) {
|
||||
// Recursively process EACH element only if not scalar.
|
||||
$item = (is_object($item) or is_array($item)) ?
|
||||
self::arrayToObject($item) : $item;
|
||||
}
|
||||
return $ret;
|
||||
} else {
|
||||
throw new \RuntimeException('cannot handle $input of type ' . \gettype($input));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,499 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ToolboxHelper
|
||||
{
|
||||
/**
|
||||
* Strip scheme (https) on oembed urls
|
||||
* Only embed allowed are for MP4 self hosted videos right now
|
||||
*
|
||||
* https is force by ckeditor MediaElements which natively supports
|
||||
* only cloud hosted medias (youtube, spotify etc..)
|
||||
*
|
||||
* @param string $input
|
||||
* @return string
|
||||
*/
|
||||
public static function stripCkEditorSchemesOnMediaInputs(string $input): string
|
||||
{
|
||||
$input = \preg_replace('/(?<=<oembed url=")(https:\/\/)(?=.*?">)/m', '', $input);
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That no Back Office url is present on txt
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $text
|
||||
* @return string
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalBoUrls(string $field, string $text): string
|
||||
{
|
||||
$routes = Route::getRoutes();
|
||||
// * Url replace and email replace.
|
||||
$text = preg_replace_callback(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
|
||||
function ($matches) use ($routes, $field) {
|
||||
$match = collect($matches)->first() ?? '';
|
||||
$url = $match;
|
||||
try {
|
||||
$request = Request::create($url);
|
||||
// If a route is match then it do not throws exception.
|
||||
if (
|
||||
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
|
||||
trim($routes->match($request)->getPrefix() ?? '', '/') === 'bo'
|
||||
) {
|
||||
// Removes the matched url.
|
||||
throw ValidationException::withMessages([
|
||||
$field => sprintf(
|
||||
'%s : Il est interdit de coller une url du back office dans ce champ',
|
||||
trans($field)
|
||||
)
|
||||
]);
|
||||
}
|
||||
return $match;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
return $match;
|
||||
}
|
||||
},
|
||||
$text
|
||||
);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files path uploaded with ckeditor custom upload plugin (crud-policies)
|
||||
*
|
||||
* @param string $text
|
||||
* @param array $detectExtensions
|
||||
* @return array
|
||||
*/
|
||||
public static function getCkEditorLocalUploadedFiles(
|
||||
string $text,
|
||||
array $detectExtensions = ['png', 'svg', 'jpg', 'gif']
|
||||
): array {
|
||||
$out = [];
|
||||
\preg_match_all(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/src="(?P<url>\/storage\/images\/crud-policies\/uploads\/\d{4}-\d{2}-\d{2}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[-a-zA-Z0-9@%._\+~#=]{2,256}\.(?:' . implode('|', $detectExtensions) . '))"/',
|
||||
$text,
|
||||
$out
|
||||
);
|
||||
return collect($out['url'] ?? [])->map(function (string $urlEncodedFilePath) {
|
||||
return \urldecode($urlEncodedFilePath);
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert That only external url is present on txt
|
||||
*
|
||||
* @param string $field
|
||||
* @param string $text
|
||||
* @return string
|
||||
* @throws \Illuminate\Validation\ValidationException If url is a bo url.
|
||||
*/
|
||||
public static function assertNoInternalUrls(string $field, string $text): string
|
||||
{
|
||||
self::assertNoInternalBoUrls($field, $text);
|
||||
$routes = Route::getRoutes();
|
||||
// * Url replace and email replace.
|
||||
$text = preg_replace_callback(
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
'/(?P<url>(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*))/',
|
||||
function ($matches) use ($routes, $field) {
|
||||
$match = collect($matches)->first() ?? '';
|
||||
$url = $match;
|
||||
try {
|
||||
$request = Request::create($url);
|
||||
// If a route is match then it do not throws exception.
|
||||
if (
|
||||
\parse_url(config('app.url'), \PHP_URL_HOST) === \parse_url($url, \PHP_URL_HOST) &&
|
||||
$routes->match($request)
|
||||
) {
|
||||
// Removes the matched url.
|
||||
throw ValidationException::withMessages([
|
||||
$field => sprintf(
|
||||
'%s : Il est interdit de coller une url de %s dans ce champ',
|
||||
trans($field),
|
||||
config('app.name')
|
||||
)
|
||||
]);
|
||||
}
|
||||
return $match;
|
||||
} catch (NotFoundHttpException $e) {
|
||||
return $match;
|
||||
}
|
||||
},
|
||||
$text
|
||||
);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array from a dot separated string
|
||||
*
|
||||
* @param string $path
|
||||
* @param mixed $value
|
||||
* @param string $separator
|
||||
* @return array
|
||||
*/
|
||||
public static function dotStringToNestedArray(string $path, $value, string $separator = '.'): array
|
||||
{
|
||||
$arr = $value;
|
||||
$keys = array_reverse(explode($separator, $path));
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$arr = [$key => $arr];
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy image
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeImage(Model $model, string $attribute)
|
||||
{
|
||||
$path = ltrim($model->{$attribute}, '/');
|
||||
if (File::exists($path) and \strpos($path, 'storage') === 0) {
|
||||
File::delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy image
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeOldImage(Model $model, string $attribute)
|
||||
{
|
||||
$originalPath = ltrim($model->getOriginal($attribute), '/');
|
||||
if (File::exists($originalPath) and \strpos($originalPath, 'storage') === 0) {
|
||||
File::delete($originalPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove pictures in ckeditor
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeImagesOnCkEditor(Model $model, string $attribute)
|
||||
{
|
||||
// * Remove inline uploaded images
|
||||
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
|
||||
->map(function (string $originalPicturePath) {
|
||||
// * If a picture is not used anymore
|
||||
if (File::exists(\public_path($originalPicturePath))) {
|
||||
File::delete(\public_path($originalPicturePath));
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Remove unused pictures in ckeditor
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param string $attribute
|
||||
* @return void
|
||||
*/
|
||||
public static function removeUnusedImagesOnCkEditor(Model $model, string $attribute)
|
||||
{
|
||||
// * Remove unused inline uploaded images
|
||||
if ($model->{$attribute} != $model->getOriginal($attribute)) {
|
||||
$updatedPicturesPath = collect(
|
||||
ToolboxHelper::getCkEditorLocalUploadedFiles($model->{$attribute})
|
||||
);
|
||||
collect(ToolboxHelper::getCkEditorLocalUploadedFiles($model->getOriginal($attribute)))
|
||||
->map(function (string $originalPicturePath) use ($updatedPicturesPath) {
|
||||
// * If a picture is not used anymore
|
||||
if (!$updatedPicturesPath->contains($originalPicturePath)) {
|
||||
if (File::exists(\public_path($originalPicturePath))) {
|
||||
File::delete(\public_path($originalPicturePath));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an image of different types
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param \Illuminate\Http\UploadedFile|\SplFileInfo|string $image
|
||||
* @return string
|
||||
* @throws \ErrorException If file copy fails.
|
||||
*/
|
||||
public static function storeImage(Model $model, $image): string
|
||||
{
|
||||
$table = $model->getTable();
|
||||
if (\is_object($image) and $image instanceof \Illuminate\Http\UploadedFile) {
|
||||
/** @var \Illuminate\Http\UploadedFile $image */
|
||||
$newPath = self::prepareMoveFile($table, $image->getClientOriginalName());
|
||||
$newPath = str_replace(storage_path('app'), '', $newPath);
|
||||
$image->storePubliclyAs(
|
||||
pathinfo($newPath, \PATHINFO_DIRNAME),
|
||||
pathinfo($newPath, \PATHINFO_BASENAME)
|
||||
);
|
||||
return 'storage' . str_replace('/public', '', $newPath);
|
||||
}
|
||||
if (\is_object($image) and $image instanceof \SplFileInfo) {
|
||||
/** @var \SplFileInfo $image */
|
||||
$newPath = self::prepareMoveFile($table, $image->getBaseName());
|
||||
if (!File::copy($image->getRealPath(), $newPath)) {
|
||||
throw new \ErrorException(sprintf(
|
||||
'Failed to copy file %s to %s',
|
||||
$image->getRealPath(),
|
||||
$newPath
|
||||
));
|
||||
}
|
||||
return \ltrim(str_replace(\storage_path('app/public'), '/storage', $newPath), '/');
|
||||
}
|
||||
return strval($image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare storage path and return
|
||||
* the image full path
|
||||
*
|
||||
* @param string $table Model table name.
|
||||
* @param string $filename The basename for the file.
|
||||
* @return string
|
||||
*/
|
||||
private static function prepareMoveFile(string $table, string $filename): string
|
||||
{
|
||||
// * Security filter + beautifier
|
||||
$filename = self::sanitizeFileName($filename);
|
||||
// * Security filter
|
||||
$filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
|
||||
// * Slugify filename
|
||||
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
$filename = Str::slug(\pathinfo($filename, \PATHINFO_FILENAME)) .
|
||||
(\strlen($ext) ? ".{$ext}" : '');
|
||||
|
||||
$pathFolder = \storage_path(sprintf('app/public/images/%s', $table));
|
||||
if (!File::exists($pathFolder)) {
|
||||
File::makeDirectory($pathFolder, 0755, true);
|
||||
}
|
||||
$filename = sprintf('%s-%s', Str::uuid(), $filename);
|
||||
$fullPath = sprintf('%s/%s', $pathFolder, $filename);
|
||||
if (File::exists($fullPath)) {
|
||||
File::delete($fullPath);
|
||||
}
|
||||
return $fullPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the field is unique
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $field
|
||||
* @param mixed $value
|
||||
* @param integer|null $id
|
||||
* @param callable|null $query
|
||||
* @return void
|
||||
* @throws \Illuminate\Validation\ValidationException If the field is not unique.
|
||||
*/
|
||||
public static function assertFieldIsUnique(
|
||||
string $table,
|
||||
string $field,
|
||||
$value,
|
||||
?int $id = null,
|
||||
?callable $query = null
|
||||
) {
|
||||
if (!$query) {
|
||||
Validator::make([$field => $value], [
|
||||
$field => Rule::unique($table, $field)->ignore($id),
|
||||
], [
|
||||
$field . '.unique' => trans('La valeur :value pour le champ :attribute est déjà utilisé', [
|
||||
'attribute' => $field,
|
||||
'value' => $value
|
||||
])
|
||||
])->validate();
|
||||
return;
|
||||
}
|
||||
Validator::make([$field => $value], [
|
||||
$field => Rule::unique($table)->where($query)->ignore($id),
|
||||
], [
|
||||
$field . '.unique' => trans('La valeur pour le champ :attribute est déjà utilisé', [
|
||||
'attribute' => $field
|
||||
])
|
||||
])->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate recaptcha V3
|
||||
*
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
public static function validateRecaptchaV3(Request $request)
|
||||
{
|
||||
Validator::validate($request->all(), [
|
||||
'g-recaptcha-response' => [
|
||||
'required', 'string',
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
|
||||
function ($attribute, $value, $fail) {
|
||||
try {
|
||||
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
|
||||
'secret' => config('recaptcha.private'),
|
||||
'response' => \strval($value),
|
||||
'remoteip' => app('clientIp')
|
||||
]);
|
||||
if ($e = $response->toException()) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if (
|
||||
!$response->json('success') or
|
||||
!$response->json('challenge_ts') or
|
||||
Carbon::parse($response->json('challenge_ts'))->lessThan(\now('utc')->subMinutes(5)) or
|
||||
$response->json('hostname') !== \parse_url(config('app.url'), \PHP_URL_HOST) or
|
||||
!$response->json('score')
|
||||
) {
|
||||
throw new \ErrorException();
|
||||
}
|
||||
if (\floatval($response->json('score')) < 0.8) {
|
||||
$fail('Vous n\'avez pas obtenu un score assez élevé lors de la validation du recaptcha');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$fail('Erreur lors de la validation du formulaire veuillez réssayer');
|
||||
} //end try
|
||||
}
|
||||
]
|
||||
], [], ['g-recaptcha-response' => 'reCaptcha']);
|
||||
}
|
||||
|
||||
/**
|
||||
* * Register CSP report URI
|
||||
*
|
||||
* Put this function inside RouteServiceProvider,
|
||||
* and call it in RouteServiceProvider::boot
|
||||
*
|
||||
* ! Use this in conjonction with kwaadpepper/exception-handler
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function cspReportRoute()
|
||||
{
|
||||
// * CSP REPORT URI, Please add /csp-report to VerifyCsrfToken $except middleware
|
||||
\Illuminate\Support\Facades\Route::post('/csp-report', function () {
|
||||
$isCspRequest = request()->header('Content-Type') === 'application/csp-report';
|
||||
if (!$isCspRequest or !request()->json('csp-report') or !is_array(request()->json('csp-report'))) {
|
||||
abort(401);
|
||||
}
|
||||
// * Flatten if needed
|
||||
$cspReport = collect(request()->json('csp-report'))
|
||||
->flatMap(function ($v, $k) {
|
||||
return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
|
||||
})->all();
|
||||
// * Filter array keys
|
||||
$cspReport = collect(array_intersect_key($cspReport, [
|
||||
'blocked-uri' => '', 'document-uri' => '', 'original-policy' => '',
|
||||
'referrer' => '', 'violated-directive' => ''
|
||||
]));
|
||||
report(new \Illuminate\Http\Client\HttpClientException(sprintf(
|
||||
"CSP REPORT for %s : \n %s",
|
||||
$cspReport->get('document-uri'),
|
||||
$cspReport->implode("\n")
|
||||
)));
|
||||
})->middleware('throttle:5,2')
|
||||
->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
|
||||
->name('cspReportUri');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize File name for uploaded files
|
||||
*
|
||||
* @param string $filename The original uploaded client file name.
|
||||
* @param boolean $beautify Beautify the filename.
|
||||
* @return string The sanitized file name.
|
||||
*/
|
||||
public static function sanitizeFileName(string $filename, bool $beautify = true): string
|
||||
{
|
||||
// * Sanitize filename
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
$filename = preg_replace(
|
||||
'~
|
||||
[<>:"/\\\|?*]| # file system reserved https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
||||
[\x00-\x1F]| # control characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
|
||||
[\x7F\xA0\xAD]| # non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN
|
||||
[#\[\]@!$&\'()+,;=]| # URI reserved https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
[{}^\~`] # URL unsafe characters https://www.ietf.org/rfc/rfc1738.txt
|
||||
~x',
|
||||
'-',
|
||||
$filename
|
||||
);
|
||||
// phpcs:enable
|
||||
// * Avoids '.' '..' or '.hiddenFiles' .
|
||||
$filename = Str::of($filename)->ltrim('.-');
|
||||
// * Optional beautification
|
||||
if ($beautify) {
|
||||
$filename = static::beautifyFilename($filename);
|
||||
}
|
||||
// * Maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
|
||||
/** @var string */
|
||||
$extension = pathinfo($filename, \PATHINFO_EXTENSION);
|
||||
/** @var string */
|
||||
$filename = pathinfo($filename, \PATHINFO_FILENAME);
|
||||
|
||||
$encoding = mb_detect_encoding($filename);
|
||||
$encoding = $encoding ? $encoding : null;
|
||||
return \mb_strcut(
|
||||
$filename,
|
||||
0,
|
||||
255 - (!empty($extension) ? strlen($extension) + 1 : 0),
|
||||
$encoding
|
||||
) . (!empty($extension) ? ".$extension" : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautify a filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return string
|
||||
*/
|
||||
private static function beautifyFilename(string $filename): string
|
||||
{
|
||||
// Reduce consecutive characters.
|
||||
$filename = preg_replace([
|
||||
// * "file name.zip" becomes 'file-name.zip'
|
||||
'/ +/',
|
||||
// * "file___name.zip" becomes 'file-name.zip'
|
||||
'/_+/',
|
||||
// * "file---name.zip" becomes 'file-name.zip'
|
||||
'/-+/'
|
||||
], '-', $filename);
|
||||
$filename = preg_replace([
|
||||
// * "file--.--.-.--name.zip" becomes "file.name.zip"
|
||||
'/-*\.-*/',
|
||||
// * "file...name..zip" becomes "file.name.zip"
|
||||
'/\.{2,}/'
|
||||
], '.', $filename);
|
||||
// Lowercase for windows/unix interoperability http://support.microsoft.com/kb/100625 .
|
||||
$filename = mb_strtolower($filename, mb_detect_encoding($filename));
|
||||
// * ".file-name.-" becomes "file-name"
|
||||
$filename = trim($filename, '.-');
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,10 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Lib\ToolboxHelper;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
|
|
@ -20,7 +18,8 @@ class User extends Authenticatable
|
|||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
|
@ -42,7 +41,6 @@ class User extends Authenticatable
|
|||
*/
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'last_connection' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -53,34 +51,15 @@ class User extends Authenticatable
|
|||
protected static function booted()
|
||||
{
|
||||
// * EVENT CLOSURES.
|
||||
static::creating(function (self $user) {
|
||||
static::assertEmailIsUnique($user->email);
|
||||
static::creating(function (User $user) {
|
||||
if (Hash::needsRehash($user->password)) {
|
||||
$user->password = Hash::make($user->password);
|
||||
}
|
||||
$user->remember_token = Str::random(10);
|
||||
});
|
||||
static::updating(function (self $user) {
|
||||
static::assertEmailIsUnique($user->email, $user->id);
|
||||
static::updating(function (User $user) {
|
||||
if (Hash::needsRehash($user->password)) {
|
||||
$user->password = Hash::make($user->password);
|
||||
}
|
||||
$user->remember_token = Str::random(10);
|
||||
});
|
||||
}
|
||||
|
||||
// * METHODS
|
||||
|
||||
/**
|
||||
* Asserts using validation that the email is unique
|
||||
*
|
||||
* @param mixed $email
|
||||
* @param integer|null $id
|
||||
* @return void
|
||||
* @throws \Illuminate\Validation\ValidationException If user email as already used.
|
||||
*/
|
||||
private static function assertEmailIsUnique($email, ?int $id = null)
|
||||
{
|
||||
ToolboxHelper::assertFieldIsUnique((new self())->getTable(), 'email', $email, $id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,12 @@ namespace App\Providers;
|
|||
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
|
|
@ -25,34 +17,47 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot()
|
||||
{
|
||||
// * HTTPS FIX
|
||||
URL::forceScheme('https');
|
||||
|
||||
// * HA PROXY FIX
|
||||
App::singleton('clientIp', function () {
|
||||
if (config('app.env') === 'local') {
|
||||
return request()->getClientIp();
|
||||
} else {
|
||||
return request()->server('HTTP_X_FORWARDED_FOR') ??
|
||||
request()->getClientIp();
|
||||
}
|
||||
});
|
||||
|
||||
// * LOCAL FORCE eg: for Carbon
|
||||
// * Remove numeric to prevent float seperator to breaking numeric transactions
|
||||
\collect([\LC_TIME, \LC_MESSAGES, \LC_COLLATE, \LC_CTYPE, \LC_MONETARY])
|
||||
->map(function ($contant) {
|
||||
setlocale(
|
||||
$contant,
|
||||
\sprintf(
|
||||
'%s_%s.UTF-8',
|
||||
config('app.locale'),
|
||||
\strtoupper(config('app.locale'))
|
||||
)
|
||||
);
|
||||
});
|
||||
setlocale(LC_ALL, \sprintf(
|
||||
'%s_%s.UTF-8',
|
||||
config('app.locale'),
|
||||
\strtoupper(config('app.locale'))
|
||||
));
|
||||
Carbon::setLocale(config('app.locale'));
|
||||
|
||||
// * Force bootstrap paginator from Alexis with tooltips.
|
||||
Paginator::defaultView('bo.modules.pagination');
|
||||
$this->sharetoView();
|
||||
}
|
||||
|
||||
// * HA PROXY FIX
|
||||
app()->singleton('clientIp', function () {
|
||||
if (
|
||||
config('app.env') === 'local'
|
||||
) {
|
||||
return request()->getClientIp();
|
||||
/**
|
||||
* Share Vars to view
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function sharetoView()
|
||||
{
|
||||
if (!app()->runningInConsole()) {
|
||||
// * FORCE BOOTSTRAP PAGINATOR (or custom if in front)
|
||||
// Early boot, request wont be filled as expected.
|
||||
if (collect(explode('/', \request()->getPathInfo()))->get(1) === 'bo') {
|
||||
Paginator::useBootstrap();
|
||||
} else {
|
||||
// FIXME: change this if needed.
|
||||
Paginator::useBootstrap();
|
||||
// Paginator::defaultview('fo.modules.pagination'); .
|
||||
}
|
||||
return request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,6 @@ use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvid
|
|||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The policy mappings for the application.
|
||||
*
|
||||
* @var array<class-string, class-string>
|
||||
*/
|
||||
protected $policies = [];
|
||||
|
||||
/**
|
||||
* Register any authentication / authorization services.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Lib\ToolboxHelper;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -18,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
public const HOME = '/home';
|
||||
public const HOME = '/';
|
||||
|
||||
/**
|
||||
* Define your route model bindings, pattern filters, etc.
|
||||
|
|
@ -29,16 +28,16 @@ class RouteServiceProvider extends ServiceProvider
|
|||
{
|
||||
$this->configureRateLimiting();
|
||||
|
||||
ToolboxHelper::cspReportRoute();
|
||||
$this->cspReportRoute();
|
||||
|
||||
$this->routes(function () {
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web-fo.php'));
|
||||
->group(base_path('routes/web-bo.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web-bo.php'));
|
||||
->group(base_path('routes/web-fo.php'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -49,8 +48,50 @@ class RouteServiceProvider extends ServiceProvider
|
|||
*/
|
||||
protected function configureRateLimiting()
|
||||
{
|
||||
RateLimiter::for('api', function (Request $request) {
|
||||
return Limit::perMinute(150)->by(optional($request->user())->id ?: app('clientIp'));
|
||||
});
|
||||
RateLimiter::for('web', function (Request $request) {
|
||||
return Limit::perMinute(150)->by(optional($request->user())->id ?: app('clientIp'));
|
||||
});
|
||||
RateLimiter::for('credentials', function (Request $request) {
|
||||
return Limit::perMinute(30)->by(optional($request->user())->id ?: $request->ip());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* * Register CSP report URI
|
||||
*
|
||||
* Put this function inside RouteServiceProvider,
|
||||
* and call it in RouteServiceProvider::boot
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function cspReportRoute()
|
||||
{
|
||||
// * CSP REPORT URI, Please add /csp-report to VerifyCsrfToken $except middleware
|
||||
\Illuminate\Support\Facades\Route::post('/csp-report', function () {
|
||||
$isCspRequest = request()->header('Content-Type') === 'application/csp-report';
|
||||
if (!$isCspRequest or !request()->json('csp-report') or !is_array(request()->json('csp-report'))) {
|
||||
abort(401);
|
||||
}
|
||||
// * Flatten if needed
|
||||
$cspReport = collect(request()->json('csp-report'))
|
||||
->flatMap(function ($v, $k) {
|
||||
return [$k => is_array($v) ? collect($v)->flatten()->implode(', ') : $v];
|
||||
})->all();
|
||||
// * Filter array keys
|
||||
$cspReport = collect(array_intersect_key($cspReport, [
|
||||
'blocked-uri' => '', 'document-uri' => '', 'original-policy' => '',
|
||||
'referrer' => '', 'violated-directive' => ''
|
||||
]));
|
||||
\Illuminate\Support\Facades\Log::warning(sprintf(
|
||||
"CSP REPORT for %s : \n %s",
|
||||
$cspReport->get('document-uri'),
|
||||
$cspReport->implode("\n")
|
||||
));
|
||||
})->middleware('throttle:5,2')
|
||||
->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
|
||||
->name('cspReportUri');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use Illuminate\Support\Collection;
|
|||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
class TranslationServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -35,8 +36,8 @@ class TranslationServiceProvider extends ServiceProvider
|
|||
*/
|
||||
private function collectLocalesStrings(): Collection
|
||||
{
|
||||
return collect(File::allFiles(resource_path('lang/')))->flatMap(function ($file) {
|
||||
if ($file->getRelativePath()) {
|
||||
return collect(File::allFiles(resource_path('lang/')))->flatMap(function (SplFileInfo $file) {
|
||||
if ($file->getRelativePath() and \strpos($file->getRelativePath(), 'vendor') !== 0) {
|
||||
return [$file->getRelativePath() => ''];
|
||||
}
|
||||
})->keys();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
{
|
||||
"name": "laravel/laravel",
|
||||
"version": "1.0.0",
|
||||
"type": "project",
|
||||
"time": "2023-03-02",
|
||||
"description": "The Laravel Framework.",
|
||||
"support": {
|
||||
"email": "equipe.web@ldc.fr"
|
||||
},
|
||||
"keywords": [
|
||||
"framework",
|
||||
"laravel"
|
||||
|
|
@ -9,28 +14,34 @@
|
|||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^7.3|^8.0",
|
||||
"diglactic/laravel-breadcrumbs": "^8.0",
|
||||
"diglactic/laravel-breadcrumbs": "^6.1",
|
||||
"fruitcake/laravel-cors": "^2.0",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"kwaadpepper/exception-handler": "^0.0.9",
|
||||
"kwaadpepper/refresh-sitemap": "^1.0",
|
||||
"kwaadpepper/enum": "^2.1",
|
||||
"kwaadpepper/image-resizer": "^0.1.2",
|
||||
"laravel/framework": "^8.75",
|
||||
"laravel/tinker": "^2.5",
|
||||
"laravel/ui": "^3.4",
|
||||
"ldc/web-admin-secure-laravel": "1.0.1",
|
||||
"ldc/web-status-checker-laravel": "1.0.3",
|
||||
"ldc/web-admin-secure-laravel": "^1.0",
|
||||
"ldc/web-status-checker-laravel": "^1.0",
|
||||
"mews/purifier": "^3.3",
|
||||
"sentry/sentry-laravel": "^3.2",
|
||||
"spatie/laravel-mix-preload": "^1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"facade/ignition": "^2.5",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"laravel-lang/lang": "10.0",
|
||||
"laravel/sail": "^1.0.1",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^5.10",
|
||||
"phpunit/phpunit": "^9.5.10",
|
||||
"slevomat/coding-standard": "^8.3"
|
||||
"slevomat/coding-standard": "^8.8"
|
||||
},
|
||||
"repositories": {
|
||||
"lab.s-web.fr/23": {
|
||||
"type": "composer",
|
||||
"url": "https://lab.s-web.fr/api/v4/group/23/-/packages/composer/packages.json"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
@ -70,14 +81,11 @@
|
|||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
}
|
||||
},
|
||||
"gitlab-domains": [
|
||||
"lab.s-web.fr"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"repositories": {
|
||||
"gitlab.com/16669037": {
|
||||
"type": "composer",
|
||||
"url": "https://gitlab.com/api/v4/group/16669037/-/packages/composer/packages.json"
|
||||
}
|
||||
}
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
|
|
|||
3249
composer.lock
generated
|
|
@ -67,7 +67,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => 'Europe/Paris',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -80,7 +80,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'locale' => 'en',
|
||||
'locale' => 'fr',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -106,7 +106,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'faker_locale' => 'en_US',
|
||||
'faker_locale' => 'fr_FR',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -173,10 +173,9 @@ return [
|
|||
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\AuthServiceProvider::class,
|
||||
// phpcs:ignore
|
||||
// App\Providers\BroadcastServiceProvider::class,
|
||||
App\Providers\EventServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\TranslationServiceProvider::class
|
||||
|
||||
],
|
||||
|
||||
|
|
@ -221,8 +220,6 @@ return [
|
|||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
|
||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||
// phpcs:ignore
|
||||
// 'Redis' => Illuminate\Support\Facades\Redis::class,
|
||||
'Request' => Illuminate\Support\Facades\Request::class,
|
||||
'Response' => Illuminate\Support\Facades\Response::class,
|
||||
'Route' => Illuminate\Support\Facades\Route::class,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ return [
|
|||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => 'backend',
|
||||
'guard' => 'frontend',
|
||||
'passwords' => 'users',
|
||||
],
|
||||
|
||||
|
|
@ -40,6 +40,12 @@ return [
|
|||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'frontend' => [
|
||||
'driver' => 'session',
|
||||
// ! Remplacer "users" par le model authenticable voulu
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ return [
|
|||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// phpcs:ignore
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000, .
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
'paths' => ['api/*'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
/**
|
||||
* Send mail when app debug is turn off
|
||||
* so you can be warned if an unhandled exception occurs
|
||||
*/
|
||||
'enableMaiLog' => !config('app.debug'),
|
||||
|
||||
/**
|
||||
* Put a developper secured mailbox in here
|
||||
* so you can receive email if things go mad
|
||||
*/
|
||||
'contactsList' => explode(",", env('MAIL_LIST_SERVICEWEB'))
|
||||
];
|
||||
|
|
@ -50,7 +50,7 @@ return [
|
|||
'channels' => [
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => ['single'],
|
||||
'channels' => ['single', 'sentry'],
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
|
|
@ -60,6 +60,11 @@ return [
|
|||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
],
|
||||
|
||||
'sentry' => [
|
||||
'driver' => 'sentry',
|
||||
'level' => env('LOG_LEVEL', 'error'),
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
|
|
|
|||
|
|
@ -29,8 +29,15 @@ return [
|
|||
// * For iframes
|
||||
'URI.SafeIframeRegexp' => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/)%",
|
||||
'HTML.Doctype' => 'HTML 4.01 Transitional',
|
||||
'HTML.Allowed' => 'h1[style],h2[style],h3[style],h4[style],h5[style],h6[style],figure[class|style],oembed[url],div[style],b,strong,i,em,u,a[href|title|target|rel],ul[style],ol[style],dl[style],li,p[style],br,span[style|class],img[width|height|alt|src|style|class|title],iframe[src|width|height|class|frameborder|style],table[style|align|border|cellspacing|cellpadding|width],thead,tbody,th[colspan|align|valign|style],tr[align|valign|style],td[style|colspan|rowspan|scope|align|valign],hr,dl,dt',
|
||||
'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align,list-style-type,float,width,height,vertical-align,border-color',
|
||||
// TODO: Rules en FULL CKEDITOR.
|
||||
// * Legacy filter en full avec safe legacy.
|
||||
// 'HTML.Allowed' => 'h1[style],h2[style],h3[style],h4[style],h5[style],h6[style],figure[class|style],oembed[url],div[style],b,strong,i,em,u,a[href|title|target|rel],ul[style],ol[style],dl[style],li,p[style],br,span[style|class],img[width|height|alt|src|style|class|title],iframe[src|width|height|class|frameborder|style],table[style|align|border|cellspacing|cellpadding|width],thead,tbody,th[colspan|align|valign|style],tr[align|valign|style],td[style|colspan|rowspan|scope|align|valign],hr,dl,dt',
|
||||
// * Restricted basics.
|
||||
'HTML.Allowed' => 'br,p[style],b,u,i,a[href|title|target|rel],strong,span[class],ul,ol,li,h2,h3,h4,hr',
|
||||
// * Legacy filter en full avec safe legacy.
|
||||
// 'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align,list-style-type,float,width,height,vertical-align,border-color',
|
||||
// * Restricted basics.
|
||||
'CSS.AllowedProperties' => 'text-align',
|
||||
'AutoFormat.AutoParagraph' => false,
|
||||
'AutoFormat.RemoveEmpty' => false,
|
||||
'AutoFormat.RemoveEmpty.RemoveNbsp' => false,
|
||||
|
|
|
|||
61
config/sentry.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'dsn' => env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')),
|
||||
|
||||
// The release version of your application.
|
||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')).
|
||||
'release' => env('SENTRY_RELEASE'),
|
||||
|
||||
// When left empty or `null` the Laravel environment will be used.
|
||||
'environment' => env('SENTRY_ENVIRONMENT'),
|
||||
|
||||
'breadcrumbs' => [
|
||||
// Capture Laravel logs in breadcrumbs.
|
||||
'logs' => true,
|
||||
|
||||
// Capture SQL queries in breadcrumbs.
|
||||
'sql_queries' => true,
|
||||
|
||||
// Capture bindings on SQL queries logged in breadcrumbs.
|
||||
'sql_bindings' => false,
|
||||
|
||||
// Capture queue job information in breadcrumbs.
|
||||
'queue_info' => true,
|
||||
|
||||
// Capture command information in breadcrumbs.
|
||||
'command_info' => true,
|
||||
],
|
||||
|
||||
'tracing' => [
|
||||
// Trace queue jobs as their own transactions.
|
||||
'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', false),
|
||||
|
||||
// Capture queue jobs as spans when executed on the sync driver.
|
||||
'queue_jobs' => true,
|
||||
|
||||
// Capture SQL queries as spans.
|
||||
'sql_queries' => true,
|
||||
|
||||
// Try to find out where the SQL query originated from and add it to the query spans.
|
||||
'sql_origin' => true,
|
||||
|
||||
// Capture views as spans.
|
||||
'views' => true,
|
||||
|
||||
// Indicates if the tracing integrations supplied by Sentry should be loaded.
|
||||
'default_integrations' => true,
|
||||
|
||||
// Indicates that requests without a matching route should be traced.
|
||||
'missing_routes' => false,
|
||||
],
|
||||
|
||||
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send-default-pii.
|
||||
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
|
||||
|
||||
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate.
|
||||
'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_TRACES_SAMPLE_RATE'),
|
||||
|
||||
'before_send' => [\App\Lib\Helpers\ExceptionsHelper::class, 'sentryEventHandler'],
|
||||
];
|
||||
|
|
@ -14,14 +14,20 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'google' => [
|
||||
'analytics' => [
|
||||
'id' => env('GOOGLE_ANALYTICS_ID')
|
||||
],
|
||||
'recaptcha' => [
|
||||
'public' => env('GOOGLE_RECAPTCHA_PUBLIC'),
|
||||
'private' => env('GOOGLE_RECAPTCHA_PRIVATE'),
|
||||
'version' => env('GOOGLE_RECAPTCHA_VERSION', "3") === "3" ? true : false
|
||||
]
|
||||
'mailgun' => [
|
||||
'domain' => env('MAILGUN_DOMAIN'),
|
||||
'secret' => env('MAILGUN_SECRET'),
|
||||
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ return [
|
|||
|
||||
'paths' => [
|
||||
resource_path('views'),
|
||||
resource_path('views/vendor'),
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@ class UserFactory extends Factory
|
|||
public function definition()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->name(),
|
||||
'firstname' => $this->faker->firstname(),
|
||||
'lastname' => $this->faker->lastname(),
|
||||
'email' => $this->faker->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
// * password
|
||||
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
|
|
@ -30,8 +32,7 @@ class UserFactory extends Factory
|
|||
*/
|
||||
public function unverified()
|
||||
{
|
||||
// phpcs:ignore
|
||||
return $this->state(function (array $attributes) {
|
||||
return $this->state(function () {
|
||||
return [
|
||||
'email_verified_at' => null,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ class CreateUsersTable extends Migration
|
|||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('firstname');
|
||||
$table->string('lastname');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->date('last_connection')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePersonalAccessTokensTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->string('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,5 @@ class DatabaseSeeder extends Seeder
|
|||
*/
|
||||
public function run()
|
||||
{
|
||||
// \App\Models\User::factory(10)->create();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"include": [
|
||||
"./resources/js/**/*"
|
||||
]
|
||||
}
|
||||
7641
package-lock.json
generated
46
package.json
|
|
@ -8,32 +8,56 @@
|
|||
"hot": "mix watch --hot",
|
||||
"prod": "npm run production",
|
||||
"production": "mix --production",
|
||||
"eslint": "node ./node_modules/.bin/eslint --max-warnings 0 -c .eslintrc.json ./resources",
|
||||
"eslint-ci": "node ./node_modules/.bin/eslint --max-warnings 0 -c .eslintrc.json ./resources -f junit",
|
||||
"stylelint": "node ./node_modules/.bin/stylelint --max-warnings 0 '**/*.scss'",
|
||||
"stylelint-ci": "node ./node_modules/.bin/stylelint --max-warnings 0 '**/*.scss' -f json"
|
||||
"eslint": "node ./node_modules/.bin/eslint -c .eslintrc.json ./resources",
|
||||
"eslint-ci": "node ./node_modules/.bin/eslint -c .eslintrc.json ./resources -f junit",
|
||||
"stylelint": "npx stylelint '**/*.scss'",
|
||||
"stylelint-ci": "npx stylelint '**/*.scss' -f json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@types/bootstrap": "^5.2.1",
|
||||
"@types/webpack-env": "^1.17.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.1",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@sipec/vue3-tags-input": "^3.0.4",
|
||||
"@types/bootstrap": "^5.2.6",
|
||||
"@types/cropperjs": "^1.3.0",
|
||||
"@types/toastify-js": "^1.11.1",
|
||||
"@types/webpack-env": "^1.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"axios": "^0.21",
|
||||
"bootstrap": "^5.2.0",
|
||||
"bootstrap": "^5.2.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"cropperjs": "^1.5.12",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"eslint-webpack-plugin": "^3.2.0",
|
||||
"laravel-mix": "^6.0.6",
|
||||
"laravel-mix-clean": "^0.1.0",
|
||||
"laravel-mix-eslint": "^0.2.0",
|
||||
"laravel-mix-polyfill": "^3.0.1",
|
||||
"laravel-mix-stylelint": "^1.0.0",
|
||||
"postcss": "^8.1.14",
|
||||
"prettier": "^2.7.1",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.54.0",
|
||||
"sass": "^1.54.8",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sweetalert2": "^11.4.24",
|
||||
"slugify": "^1.6.5",
|
||||
"stylelint": "^14.11.0",
|
||||
"stylelint-config-idiomatic-order": "^8.1.0",
|
||||
"stylelint-config-sass-guidelines": "^9.0.1",
|
||||
"stylelint-config-standard-scss": "^6.1.0",
|
||||
"stylelint-webpack-plugin": "^3.3.0",
|
||||
"sweetalert2": "^11.4.8",
|
||||
"toastify-js": "^1.12.0",
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vue": "^3.2.37",
|
||||
"typescript": "^4.8.2",
|
||||
"typescript-eslint": "^0.0.1-alpha.0",
|
||||
"vue": "^3.2.38",
|
||||
"vue-eslint-parser": "^9.1.0",
|
||||
"vue-loader": "^16.8.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
|
||||
<rule ref="Generic.Metrics.CyclomaticComplexity"/>
|
||||
<rule ref="Generic.Metrics.NestingLevel"/>
|
||||
<rule ref="Generic.NamingConventions.ConstructorName"/>
|
||||
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
|
||||
<rule ref="Generic.NamingConventions.CamelCapsFunctionName"/>
|
||||
<rule ref="Generic.PHP.DeprecatedFunctions"/>
|
||||
|
|
|
|||
31
phpunit.xml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<php>
|
||||
<server name="APP_ENV" value="testing"/>
|
||||
<server name="BCRYPT_ROUNDS" value="4"/>
|
||||
<server name="CACHE_DRIVER" value="array"/>
|
||||
<!-- <server name="DB_CONNECTION" value="sqlite"/> -->
|
||||
<!-- <server name="DB_DATABASE" value=":memory:"/> -->
|
||||
<server name="MAIL_MAILER" value="array"/>
|
||||
<server name="QUEUE_CONNECTION" value="sync"/>
|
||||
<server name="SESSION_DRIVER" value="array"/>
|
||||
<server name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
BIN
public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
public/apple-touch-icon-bo.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
|
@ -2,8 +2,8 @@
|
|||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/assets/images/mstile-150x150.png"/>
|
||||
<TileColor>#091c58</TileColor>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
|
|
|
|||
BIN
public/favicon-16x16-bo.png
Normal file
|
After Width: | Height: | Size: 669 B |
BIN
public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 886 B |
BIN
public/favicon-32x32-bo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/favicon-bo-16x16.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/favicon-bo-32x32.png
Normal file
|
After Width: | Height: | Size: 8 KiB |
BIN
public/favicon-bo.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 15 KiB |
|
|
@ -7,17 +7,17 @@ define('LARAVEL_START', microtime(true));
|
|||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Check If The Application Is Under Maintenance
|
||||
| Check If Application Is Under Maintenance
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If the application is in maintenance / demo mode via the "down" command
|
||||
| we will load this file so that any pre-rendered content can be shown
|
||||
| If the application is maintenance / demo mode via the "down" command we
|
||||
| will require this file so that any prerendered template can be shown
|
||||
| instead of starting the framework, which could cause an exception.
|
||||
|
|
||||
*/
|
||||
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
if (file_exists(__DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require __DIR__.'/../storage/framework/maintenance.php';
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -48,8 +48,8 @@ $app = require_once __DIR__.'/../bootstrap/app.php';
|
|||
|
||||
$kernel = $app->make(Kernel::class);
|
||||
|
||||
$response = $kernel->handle(
|
||||
$response = tap($kernel->handle(
|
||||
$request = Request::capture()
|
||||
)->send();
|
||||
))->send();
|
||||
|
||||
$kernel->terminate($request, $response);
|
||||
|
|
|
|||
BIN
public/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
187
public/safari-pinned-tab.svg
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="621.000000pt" height="621.000000pt" viewBox="0 0 621.000000 621.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,621.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M375 6180 c-126 -3 -238 -9 -247 -14 -10 -4 -33 -29 -52 -55 l-35
|
||||
-47 -3 -2149 c-2 -1183 -1 -2517 2 -2966 5 -799 5 -817 25 -839 11 -13 40 -36
|
||||
64 -52 l45 -28 2940 0 2940 0 43 21 c29 14 52 35 68 63 l25 43 0 2919 c-1
|
||||
1605 -5 2939 -9 2963 -11 55 -37 89 -87 115 -21 11 -40 24 -42 28 -4 9 -5301
|
||||
7 -5677 -2z m5447 -366 c11 -7 9 -5386 -2 -5404 -8 -13 -318 -15 -2672 -18
|
||||
-1465 -2 -2681 1 -2703 5 l-40 9 0 2704 0 2705 2435 5 c2310 5 2966 4 2982 -6z"/>
|
||||
<path d="M4185 5150 c3 -5 13 -10 21 -10 8 0 13 -4 10 -8 -3 -5 3 -8 12 -8 9
|
||||
1 18 -3 20 -9 4 -13 -54 -25 -65 -14 -13 13 -61 -13 -67 -36 -3 -10 0 -23 7
|
||||
-28 7 -5 2 -6 -12 -2 -16 5 -21 4 -17 -4 4 -6 2 -11 -3 -11 -6 0 -11 -7 -11
|
||||
-15 0 -21 16 -19 41 6 20 20 39 22 42 4 1 -12 -84 -78 -91 -70 -13 12 -34 -41
|
||||
-62 -158 -16 -68 -30 -127 -30 -130 0 -2 9 -7 20 -10 23 -6 37 -34 23 -48 -15
|
||||
-15 -43 1 -44 25 0 20 -1 20 -9 1 -11 -26 -12 -48 -2 -41 4 2 6 -24 4 -59 -2
|
||||
-36 -1 -72 2 -81 5 -14 10 -13 31 10 13 14 28 33 33 42 12 22 35 8 29 -17 -3
|
||||
-12 -1 -17 5 -13 6 4 9 1 6 -7 -3 -8 -17 -14 -31 -14 -14 1 -25 -2 -24 -5 4
|
||||
-20 -17 -50 -36 -50 -21 0 -22 -3 -18 -81 2 -62 0 -80 -9 -74 -6 4 -10 18 -7
|
||||
31 2 13 1 24 -3 24 -3 0 -6 9 -6 21 1 15 -9 24 -34 34 -19 7 -69 27 -110 45
|
||||
-269 113 -518 162 -830 163 -338 1 -598 -52 -840 -173 -498 -249 -802 -698
|
||||
-891 -1315 -31 -217 -31 -594 0 -780 12 -71 23 -143 24 -160 1 -16 6 -36 10
|
||||
-43 5 -7 10 -22 13 -35 7 -40 13 -61 38 -137 14 -41 30 -83 36 -92 5 -10 10
|
||||
-23 10 -30 0 -6 23 -57 51 -112 249 -492 658 -780 1224 -862 150 -22 457 -30
|
||||
545 -15 36 6 115 20 175 30 121 21 233 47 249 60 6 4 31 13 56 20 82 24 225
|
||||
89 347 159 73 42 82 50 87 82 8 48 6 431 -2 452 -6 15 -12 12 -42 -19 -85 -88
|
||||
-335 -229 -512 -288 -205 -68 -285 -79 -558 -80 -199 0 -261 3 -330 18 -248
|
||||
53 -451 162 -602 326 -80 87 -124 151 -179 262 -22 46 -45 92 -50 102 -5 10
|
||||
-7 22 -4 27 3 4 2 10 -2 12 -16 7 -55 148 -80 288 -21 117 -27 187 -30 365 -3
|
||||
125 0 259 6 310 16 136 51 319 62 326 5 3 7 13 4 21 -4 9 -2 19 4 22 6 4 8 13
|
||||
5 20 -3 8 -1 17 5 21 6 3 11 12 11 20 0 8 5 26 12 42 6 15 12 30 13 33 8 31
|
||||
83 173 122 231 59 86 216 242 297 294 133 85 274 136 457 166 116 18 160 21
|
||||
329 16 293 -7 461 -50 737 -186 57 -28 107 -51 113 -51 5 0 10 -8 10 -17 0
|
||||
-15 2 -16 10 -3 6 10 10 11 10 3 0 -7 18 -22 40 -33 22 -11 40 -25 40 -31 0
|
||||
-6 5 -7 10 -4 6 4 10 -9 10 -33 0 -22 3 -54 6 -71 l7 -31 -47 0 c-34 0 -51 -5
|
||||
-63 -20 -19 -22 -43 -26 -60 -9 -6 6 -37 16 -69 24 -33 7 -87 20 -121 28 -35
|
||||
8 -63 11 -63 7 0 -29 -76 -13 -82 16 -2 12 -20 21 -58 30 -50 12 -56 11 -70
|
||||
-5 -21 -26 -60 -33 -67 -14 -3 9 -1 24 5 34 9 14 8 18 -6 18 -9 1 -34 4 -54 7
|
||||
-31 5 -38 3 -38 -10 0 -23 -35 -20 -59 5 -17 19 -29 20 -123 18 -114 -3 -138
|
||||
-7 -116 -20 10 -7 10 -9 1 -9 -7 0 -14 -10 -14 -22 -1 -19 -3 -17 -9 7 -6 25
|
||||
-8 26 -9 7 0 -13 -5 -21 -10 -18 -5 4 -12 0 -15 -6 -3 -7 -2 1 1 17 6 27 5 29
|
||||
-11 16 -14 -12 -16 -12 -10 3 3 9 1 16 -5 16 -6 0 -11 -5 -11 -11 0 -5 -4 -8
|
||||
-9 -5 -5 3 -12 -1 -15 -10 -8 -21 -8 -26 2 -21 4 3 7 -8 7 -24 0 -39 -11 -37
|
||||
-19 4 -6 29 -8 30 -18 13 -8 -15 -8 -21 0 -24 7 -2 12 -22 12 -43 0 -24 4 -38
|
||||
10 -34 6 3 10 -1 10 -9 0 -9 4 -16 9 -16 5 0 17 -7 25 -16 15 -14 16 -13 18
|
||||
13 0 15 1 33 1 40 0 6 6 9 14 6 10 -4 11 -9 4 -16 -16 -16 -14 -62 3 -76 18
|
||||
-15 56 -22 56 -11 0 5 -4 11 -9 14 -10 7 -3 44 12 64 17 22 38 -5 27 -34 -14
|
||||
-37 -22 -92 -11 -75 6 9 11 10 15 2 10 -15 22 -14 40 5 9 8 16 11 16 5 0 -5
|
||||
-5 -13 -10 -16 -8 -5 -7 -11 1 -21 6 -7 15 -11 20 -9 4 3 16 1 26 -4 38 -20
|
||||
105 -41 114 -36 5 4 9 14 9 24 0 9 6 26 14 37 14 17 16 18 31 3 15 -15 15 -18
|
||||
-9 -36 -18 -14 -23 -26 -19 -42 7 -26 48 -29 62 -3 10 16 10 16 11 0 0 -10 5
|
||||
-18 11 -18 8 0 8 -4 0 -14 -10 -12 3 -22 71 -56 46 -23 90 -48 98 -55 8 -6 41
|
||||
-26 73 -44 31 -18 57 -35 57 -37 0 -3 -6 -2 -12 1 -7 4 -4 -1 6 -9 14 -11 22
|
||||
-13 27 -5 3 6 9 0 13 -15 6 -26 19 -31 64 -27 12 1 22 -3 22 -9 0 -5 7 -10 15
|
||||
-10 8 0 15 5 15 10 0 16 40 11 47 -6 3 -9 -5 -26 -20 -42 -31 -32 -24 -62 9
|
||||
-37 25 19 31 19 44 -4 11 -22 2 -84 -11 -75 -5 3 -9 0 -9 -6 0 -7 -6 -10 -14
|
||||
-7 -9 4 -16 -4 -21 -21 -3 -15 -8 -34 -11 -43 -5 -19 63 -95 83 -91 8 2 12 -1
|
||||
9 -7 -11 -17 12 -51 29 -45 8 4 15 1 15 -6 0 -6 5 -8 10 -5 6 4 8 10 5 15 -3
|
||||
4 4 10 15 13 11 3 20 14 20 24 0 25 8 33 26 26 8 -3 14 -16 14 -28 0 -13 5
|
||||
-27 10 -30 6 -3 10 1 10 10 0 30 29 -10 32 -44 3 -35 23 -48 37 -24 8 15 11
|
||||
15 26 3 14 -12 18 -12 25 0 5 8 6 18 4 22 -9 15 7 48 23 48 10 0 14 -6 10 -20
|
||||
-3 -11 -3 -20 -1 -20 7 0 49 25 54 32 3 4 12 3 20 -2 10 -7 11 -10 3 -10 -8 0
|
||||
-13 -13 -13 -34 0 -29 3 -33 23 -29 13 3 24 11 24 18 0 8 -4 12 -8 10 -4 -3
|
||||
-9 1 -12 9 -15 37 70 58 108 26 14 -11 19 -12 22 -2 3 8 30 12 88 12 101 0
|
||||
115 10 115 80 0 46 -13 99 -28 110 -8 6 -13 14 -100 170 -35 63 -77 131 -92
|
||||
151 -35 44 -28 57 14 26 17 -13 39 -26 49 -30 9 -3 17 -11 17 -17 0 -5 6 -10
|
||||
13 -10 7 0 31 -13 53 -29 70 -52 127 -45 105 12 -7 18 -6 30 5 45 9 13 11 23
|
||||
4 27 -5 3 -10 -1 -10 -9 0 -10 -6 -13 -17 -9 -30 10 -53 35 -53 56 0 20 1 19
|
||||
26 -4 l27 -24 -7 29 -7 29 53 -7 c169 -20 267 -19 254 2 -2 4 10 8 26 9 40 3
|
||||
129 46 106 51 -10 2 -18 8 -18 13 0 6 -7 6 -19 -1 -24 -13 -41 -13 -41 -1 0 6
|
||||
8 9 18 9 34 -2 106 31 102 46 -1 5 4 13 12 17 9 6 6 9 -10 9 -15 0 -22 5 -19
|
||||
13 2 6 16 11 31 9 20 -2 29 3 38 24 11 23 10 28 -7 40 -15 11 -16 14 -3 14 24
|
||||
0 31 51 8 57 -21 6 -16 33 6 33 9 0 14 11 14 29 0 24 -5 29 -30 33 -16 3 -30
|
||||
1 -30 -3 0 -3 -11 -9 -25 -11 -19 -4 -25 -1 -25 12 0 12 10 19 30 22 17 3 30
|
||||
1 30 -4 0 -4 7 -8 16 -8 8 0 12 5 9 10 -3 6 -2 10 4 10 5 0 9 9 8 20 -1 11 -4
|
||||
20 -8 20 -4 0 -5 9 -2 19 4 17 -2 21 -41 26 -25 4 -46 12 -46 17 0 6 -7 8 -15
|
||||
4 -8 -3 -15 0 -15 5 0 6 8 9 18 6 32 -9 37 -9 68 -8 28 1 31 4 28 27 -3 30
|
||||
-21 46 -44 39 -43 -12 -85 -9 -85 5 0 11 11 14 46 13 44 -2 45 -2 45 27 0 16
|
||||
-3 27 -8 24 -4 -3 -8 0 -8 6 0 8 -10 9 -29 5 -34 -7 -101 17 -101 37 0 10 2
|
||||
10 9 -1 5 -7 14 -10 20 -7 6 4 11 2 11 -4 0 -7 7 -10 15 -6 8 3 15 1 15 -4 0
|
||||
-13 11 -12 30 0 13 9 13 11 0 20 -10 6 -11 10 -3 10 7 0 -12 25 -43 56 -30 31
|
||||
-58 53 -62 50 -3 -4 0 -11 8 -16 11 -7 12 -10 2 -10 -7 0 0 -11 15 -25 28 -24
|
||||
28 -24 5 -25 -13 0 -21 4 -17 10 6 10 -51 44 -63 36 -4 -3 -14 -1 -21 4 -18
|
||||
10 -5 31 14 23 11 -4 13 -1 9 11 -5 13 -2 15 12 10 14 -5 16 -4 8 5 -6 6 -34
|
||||
14 -62 18 -45 5 -52 3 -52 -11 0 -9 11 -28 24 -41 19 -19 21 -26 11 -35 -9 -8
|
||||
-19 -1 -41 27 -18 24 -24 38 -16 41 23 7 12 22 -18 22 -23 0 -28 4 -23 16 4
|
||||
11 8 12 13 4 8 -13 20 46 20 98 0 42 -36 118 -90 190 -21 29 -25 31 -32 15 -4
|
||||
-11 -6 -4 -4 17 3 22 -2 43 -11 55 -15 19 -15 19 -9 1 5 -16 4 -17 -9 -6 -8 7
|
||||
-15 8 -15 2 0 -12 -34 -22 -48 -13 -6 4 3 13 20 20 20 9 26 16 18 21 -9 6 -9
|
||||
9 0 12 7 2 2 14 -14 31 -14 15 -23 27 -20 27 3 0 -2 7 -11 16 -9 9 -14 23 -11
|
||||
30 3 8 1 14 -4 14 -7 0 -19 23 -25 50 -1 3 -6 16 -12 30 -6 14 -16 44 -22 68
|
||||
-8 27 -16 40 -23 36 -7 -4 -8 -3 -4 5 4 6 1 18 -8 26 -11 11 -16 12 -19 2 -3
|
||||
-6 -4 2 -2 19 2 17 -1 37 -7 45 -7 11 -8 9 -3 -6 5 -16 2 -16 -18 5 -13 14
|
||||
-21 29 -19 34 3 5 0 6 -6 2 -6 -3 -25 2 -44 13 -35 20 -134 29 -123 11z m105
|
||||
-45 c0 -5 -5 -3 -10 5 -5 8 -10 20 -10 25 0 6 5 3 10 -5 5 -8 10 -19 10 -25z
|
||||
m-15 -65 c-4 -13 -12 -19 -21 -16 -7 3 -16 1 -19 -4 -7 -11 -55 -14 -55 -4 0
|
||||
3 5 14 10 25 11 20 50 27 50 9 0 -5 7 -10 15 -10 8 0 15 8 15 17 0 9 3 14 6
|
||||
10 3 -3 3 -15 -1 -27z m95 -10 c0 -19 -48 -60 -70 -60 -28 0 -26 33 3 53 25
|
||||
18 67 22 67 7z m87 -94 c0 -8 -6 -12 -12 -10 -6 3 -12 11 -12 18 0 8 6 12 12
|
||||
10 6 -3 12 -11 12 -18z m-48 6 c9 7 5 0 -8 -15 -26 -29 -51 -36 -51 -14 0 20
|
||||
22 39 34 27 6 -6 15 -5 25 2z m-14 -52 c-3 -5 0 -10 8 -10 8 0 20 -7 27 -15
|
||||
10 -12 10 -15 0 -15 -17 0 -61 36 -53 44 10 10 25 7 18 -4z m79 -56 c17 -7 10
|
||||
-34 -9 -34 -17 0 -28 17 -20 30 7 12 8 12 29 4z m-155 -43 c12 -24 -6 -79 -29
|
||||
-86 -10 -3 -18 -11 -16 -18 1 -7 1 -9 -1 -4 -3 4 -10 7 -16 7 -16 0 -47 31
|
||||
-47 47 0 7 16 29 35 48 35 36 58 38 74 6z m75 0 c4 -5 0 -12 -6 -14 -7 -3 -3
|
||||
-6 10 -6 12 -1 22 4 22 11 0 9 3 9 11 1 17 -17 1 -52 -28 -59 -29 -8 -44 9
|
||||
-38 41 2 11 4 23 4 28 1 10 19 9 25 -2z m116 -58 c0 -23 -17 -49 -26 -41 -9
|
||||
10 4 58 16 58 6 0 10 -8 10 -17z m-360 -13 c23 -23 24 -29 10 -64 -7 -19 -17
|
||||
-26 -35 -26 -14 0 -25 5 -25 11 0 5 -5 7 -12 3 -7 -5 -9 -2 -5 8 4 11 -2 18
|
||||
-18 22 -22 6 -41 33 -30 44 8 9 35 -20 29 -32 -3 -6 6 4 21 22 31 37 38 39 65
|
||||
12z m293 -46 c8 -9 -14 -54 -24 -48 -5 3 -9 0 -9 -7 0 -10 -3 -10 -11 -2 -8 8
|
||||
-6 18 6 37 17 26 27 31 38 20z m80 -40 c11 -11 -5 -34 -24 -34 -18 0 -26 20
|
||||
-12 33 8 9 28 9 36 1z m94 -66 c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3 -3 4 -12
|
||||
1 -19z m-427 -16 c0 -10 -14 -31 -30 -47 -26 -25 -32 -27 -40 -15 -13 20 -12
|
||||
33 3 52 23 30 67 36 67 10z m106 -34 c4 13 14 22 26 22 17 0 19 -4 13 -27 -13
|
||||
-57 -115 -188 -128 -166 -2 4 -8 32 -12 61 -6 43 -3 61 11 88 10 19 22 34 26
|
||||
34 4 0 -1 -15 -12 -32 -32 -54 -22 -57 12 -5 31 48 32 48 44 25 l14 -23 6 23z
|
||||
m96 -163 c3 3 3 -6 0 -20 -2 -14 -13 -30 -23 -36 -11 -5 -19 -15 -19 -22 0 -7
|
||||
-20 -33 -45 -57 -53 -52 -65 -46 -65 31 0 37 5 52 26 73 29 29 49 34 40 11 -8
|
||||
-23 8 -18 34 10 16 17 27 22 35 15 7 -5 14 -7 17 -5z m-357 -191 c-18 -43 -35
|
||||
-36 -36 14 l-1 37 -8 -30 c-7 -26 -8 -24 -7 16 0 33 4 46 13 42 6 -2 16 1 20
|
||||
8 5 9 12 3 21 -19 11 -27 11 -38 -2 -68z m701 10 c13 -3 24 -10 24 -16 0 -5
|
||||
-5 -6 -10 -3 -6 4 -18 1 -26 -6 -12 -10 -18 -9 -32 4 -13 14 -14 17 -2 17 13
|
||||
0 13 2 0 11 -12 9 -12 10 3 5 11 -3 30 -9 43 -12z m164 -51 c0 -19 -20 -26
|
||||
-35 -13 -21 18 -18 25 10 25 14 0 25 -5 25 -12z m-657 -18 c-3 -9 -8 -14 -10
|
||||
-11 -3 3 -2 9 2 15 9 16 15 13 8 -4z m25 -42 c-8 -10 -6 -13 5 -13 10 0 17
|
||||
-11 19 -27 2 -16 6 -26 9 -23 4 3 16 -4 28 -14 21 -19 21 -19 42 0 22 19 22
|
||||
19 10 -1 -7 -11 -27 -45 -45 -75 -18 -30 -38 -54 -45 -52 -16 3 -41 -23 -41
|
||||
-43 0 -9 -4 -14 -9 -10 -5 3 -12 1 -16 -5 -3 -5 -12 -10 -20 -10 -22 0 -8 198
|
||||
16 229 11 13 19 31 19 40 0 25 22 44 31 29 4 -7 3 -18 -3 -25z m-188 -38 c0
|
||||
-19 -4 -36 -9 -40 -15 -8 -19 6 -14 47 6 46 23 40 23 -7z m69 -102 c-3 -96
|
||||
-13 -144 -33 -155 -9 -5 -16 -15 -16 -22 0 -8 -7 -19 -15 -26 -13 -11 -15 0
|
||||
-15 93 0 133 7 169 30 162 9 -3 23 1 30 10 7 8 14 15 17 15 2 0 3 -35 2 -77z
|
||||
m-142 -35 c-2 -13 -4 -5 -4 17 -1 22 1 32 4 23 2 -10 2 -28 0 -40z m-777 -166
|
||||
c27 -21 20 -42 -14 -42 -26 0 -51 18 -59 43 -9 24 43 23 73 -1z m1827 12 c-3
|
||||
-3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m-2127 -27 c0 -24 -19 -42
|
||||
-31 -30 -8 8 -8 16 1 32 11 22 30 20 30 -2z m1757 8 c0 -9 -9 -15 -21 -15 -21
|
||||
1 -21 1 -2 15 24 18 23 18 23 0z m-1317 -44 c8 -16 6 -21 -11 -26 -24 -8 -55
|
||||
16 -45 34 11 18 44 13 56 -8z m-151 -25 c5 -3 14 -19 19 -36 9 -25 8 -31 -8
|
||||
-40 -30 -16 -47 -12 -69 16 -25 32 -26 38 -6 58 14 14 43 15 64 2z m929 -49
|
||||
c33 -18 42 -44 18 -53 -19 -7 -105 56 -106 78 0 19 15 14 88 -25z m941 22 c-7
|
||||
-4 -21 -3 -31 2 -25 14 -23 16 13 11 21 -3 27 -7 18 -13z m-1284 1 c3 -6 -1
|
||||
-7 -9 -4 -18 7 -21 14 -7 14 6 0 13 -4 16 -10z m-354 -25 c19 -1 43 -6 53 -12
|
||||
15 -9 15 -11 -4 -28 -30 -28 -75 -28 -102 0 -30 32 -33 58 -5 49 12 -4 38 -8
|
||||
58 -9z m508 -1 c2 -2 -8 -4 -24 -4 -21 0 -25 -3 -17 -13 9 -10 -1 -12 -54 -6
|
||||
-79 8 -95 19 -28 19 35 0 45 3 34 10 -10 7 1 8 35 4 28 -4 52 -8 54 -10z
|
||||
m1056 -23 c0 -23 -23 -24 -35 -1 -10 19 -9 21 12 18 13 -2 23 -9 23 -17z
|
||||
m-1945 -32 c-12 -41 -24 -53 -14 -14 7 32 13 45 19 45 2 0 0 -14 -5 -31z m566
|
||||
13 c28 -19 24 -48 -14 -111 -1 -2 -13 2 -27 9 -20 11 -27 11 -40 0 -21 -18
|
||||
-31 -5 -11 15 9 9 16 24 16 33 0 42 46 75 76 54z m322 -39 c12 -3 22 -11 22
|
||||
-18 0 -17 -53 -72 -57 -59 -2 6 -17 9 -33 6 -24 -3 -32 1 -41 22 -9 19 -20 26
|
||||
-43 28 -17 0 -37 2 -45 2 -22 2 -25 24 -4 30 20 5 165 -2 201 -11z m-818 -13
|
||||
c0 -5 -2 -10 -4 -10 -3 0 -8 5 -11 10 -3 6 -1 10 4 10 6 0 11 -4 11 -10z
|
||||
m1832 -14 c7 4 8 2 4 -5 -4 -6 -12 -9 -17 -6 -5 4 -9 -4 -9 -16 0 -12 -3 -20
|
||||
-6 -16 -4 3 -3 19 1 34 4 15 10 22 12 15 3 -6 10 -9 15 -6z m-1615 -30 c-8
|
||||
-19 -37 -21 -37 -3 0 7 5 18 12 25 15 15 33 -1 25 -22z m76 -77 c-5 -19 -33
|
||||
-27 -33 -9 0 6 5 10 10 10 6 0 10 10 10 23 0 16 3 18 9 8 5 -7 6 -22 4 -32z
|
||||
m102 -39 c-3 -5 -14 -10 -23 -10 -15 0 -15 2 -2 10 20 13 33 13 25 0z m312
|
||||
-85 c0 -8 -4 -12 -9 -9 -5 3 -6 10 -3 15 9 13 12 11 12 -6z m503 -166 c0 -6
|
||||
-4 -7 -10 -4 -5 3 -10 11 -10 16 0 6 5 7 10 4 6 -3 10 -11 10 -16z m-433 -61
|
||||
c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3 -3 4 -12 1 -19z m3 -28 c0 -5 -2 -10 -4
|
||||
-10 -3 0 -8 5 -11 10 -3 6 -1 10 4 10 6 0 11 -4 11 -10z m-42 -18 c2 -7 -2
|
||||
-10 -12 -6 -9 3 -16 11 -16 16 0 13 23 5 28 -10z m139 -14 c-3 -8 -6 -5 -6 6
|
||||
-1 11 2 17 5 13 3 -3 4 -12 1 -19z m-47 -91 c0 -5 -4 -5 -10 -2 -5 3 -10 14
|
||||
-10 23 0 15 2 15 10 2 5 -8 10 -19 10 -23z m281 6 c13 -16 12 -17 -3 -4 -10 7
|
||||
-18 15 -18 17 0 8 8 3 21 -13z m196 -15 c-3 -8 -6 -5 -6 6 -1 11 2 17 5 13 3
|
||||
-3 4 -12 1 -19z m50 -14 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z
|
||||
m-122 -24 c3 -5 1 -10 -4 -10 -6 0 -11 5 -11 10 0 6 2 10 4 10 3 0 8 -4 11
|
||||
-10z m-245 -36 c0 -8 -4 -12 -10 -9 -5 3 -10 13 -10 21 0 8 5 12 10 9 6 -3 10
|
||||
-13 10 -21z"/>
|
||||
<path d="M2911 3704 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
|
||||
<path d="M3630 3139 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3580 3129 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M4023 2723 c-1 -33 1 -35 9 -17 6 14 7 28 1 38 -7 12 -9 7 -10 -21z"/>
|
||||
<path d="M4280 2693 c0 -5 5 -15 10 -23 8 -12 10 -11 10 8 0 12 -4 22 -10 22
|
||||
-5 0 -10 -3 -10 -7z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -1,14 +1 @@
|
|||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/images/android-chrome-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#091c58",
|
||||
"background_color": "#091c58",
|
||||
"display": "standalone"
|
||||
}
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
28
public/web.config
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<!--
|
||||
Rewrites requires Microsoft URL Rewrite Module for IIS
|
||||
Download: https://www.microsoft.com/en-us/download/details.aspx?id=47337
|
||||
Debug Help: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-failed-request-tracing-to-trace-rewrite-rules
|
||||
-->
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<rewrite>
|
||||
<rules>
|
||||
<rule name="Imported Rule 1" stopProcessing="true">
|
||||
<match url="^(.*)/$" ignoreCase="false" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
|
||||
</conditions>
|
||||
<action type="Redirect" redirectType="Permanent" url="/{R:1}" />
|
||||
</rule>
|
||||
<rule name="Imported Rule 2" stopProcessing="true">
|
||||
<match url="^" ignoreCase="false" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="index.php" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
0
resources/assets/fonts/bo/.gitkeep
Normal file
0
resources/assets/fonts/fo/.gitkeep
Normal file
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
0
resources/assets/images/bo/.gitkeep
Normal file
|
Before Width: | Height: | Size: 458 B |
|
Before Width: | Height: | Size: 896 B |
|
Before Width: | Height: | Size: 15 KiB |
0
resources/assets/images/fo/.gitkeep
Normal file
|
Before Width: | Height: | Size: 4.7 KiB |
|
|
@ -1,39 +0,0 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="180.000000pt" height="180.000000pt" viewBox="0 0 180.000000 180.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,180.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1585 1356 l30 -25 -40 -41 c-55 -58 -135 -91 -135 -56 0 8 9 33 20
|
||||
57 10 24 15 48 11 52 -5 5 -300 7 -657 5 -594 -4 -656 -6 -731 -23 l-83 -19 0
|
||||
-123 c0 -68 4 -123 9 -123 23 0 33 -64 29 -195 -2 -112 -6 -139 -19 -149 -14
|
||||
-9 -17 -34 -18 -135 l-2 -124 173 8 c95 4 247 11 338 15 161 8 875 4 935 -5
|
||||
17 -2 103 -7 193 -11 l162 -7 0 143 c0 79 -3 140 -7 137 -3 -4 -12 -2 -20 4
|
||||
-17 15 -19 314 -2 325 6 3 9 15 6 25 -3 11 1 26 9 33 10 10 14 39 14 94 l0 79
|
||||
-52 16 c-29 9 -69 19 -88 23 -19 3 -46 15 -59 25 -13 11 -29 19 -35 19 -6 0 3
|
||||
-11 19 -24z m-1306 -105 c9 -6 11 -20 6 -50 -4 -22 -7 -149 -7 -281 -1 -254 2
|
||||
-270 46 -270 23 0 55 29 75 67 6 12 11 68 11 125 0 178 36 276 122 338 71 52
|
||||
141 35 209 -50 71 -88 79 -350 16 -464 -50 -90 -147 -129 -215 -87 -50 32 -59
|
||||
62 -33 106 13 22 16 35 9 33 -7 -2 -27 -32 -44 -67 -17 -35 -39 -70 -49 -77
|
||||
-14 -10 -57 -14 -163 -14 l-145 0 7 48 c11 77 9 564 -2 594 -12 30 -3 43 38
|
||||
51 44 8 105 7 119 -2z m717 -52 c14 -12 16 -42 13 -252 l-4 -239 28 -24 c33
|
||||
-28 37 -29 63 -10 18 14 19 29 19 273 l0 258 51 3 c39 2 54 -1 68 -17 16 -17
|
||||
16 -28 7 -115 -14 -125 -14 -328 0 -360 6 -13 27 -32 45 -42 19 -10 34 -24 34
|
||||
-30 0 -6 -21 -28 -47 -48 -55 -43 -76 -42 -137 6 -34 27 -40 29 -48 14 -13
|
||||
-23 -62 -46 -99 -46 -22 0 -41 10 -65 34 l-34 34 0 218 c-1 144 -5 240 -14
|
||||
282 -12 56 -12 63 3 72 21 14 96 6 117 -11z m599 -33 c36 -28 105 -153 105
|
||||
-191 0 -39 -37 -72 -117 -105 -87 -36 -123 -63 -123 -94 0 -27 27 -68 63 -96
|
||||
l28 -22 56 42 c65 50 87 55 91 22 8 -57 -62 -140 -136 -163 -65 -19 -105 -5
|
||||
-176 61 -35 33 -69 60 -75 60 -8 0 -7 6 3 16 9 8 16 23 16 32 8 332 9 343 62
|
||||
406 58 71 136 83 203 32z"/>
|
||||
<path d="M573 1093 c-14 -22 -19 -58 -24 -192 -3 -91 -2 -176 2 -188 13 -35
|
||||
53 -57 79 -43 34 18 40 52 40 214 0 159 -9 209 -40 226 -28 15 -38 12 -57 -17z"/>
|
||||
<path d="M1480 1087 c-38 -62 -36 -138 5 -153 21 -8 61 20 80 56 14 26 14 33
|
||||
0 68 -9 22 -27 44 -41 51 -22 10 -26 8 -44 -22z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
BIN
resources/favicon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
resources/favicon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
resources/favicon/apple-touch-icon-bo.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
resources/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
resources/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 886 B |
BIN
resources/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
resources/favicon/favicon-bo-16x16.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
resources/favicon/favicon-bo-32x32.png
Normal file
|
After Width: | Height: | Size: 8 KiB |
BIN
resources/favicon/favicon-bo.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
resources/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
resources/favicon/site.webmanifest
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||