This commit is contained in:
sebastien.hattry 2023-06-26 08:52:32 +00:00 • committed by Vincent BRIDIER
parent 2fb5a930b7
commit 6f266751f9
15 changed files with 609 additions and 336 deletions

View file

@ -36,6 +36,11 @@ MAIL_LIST_CONTACTFORM=
ANALYTICS_GOOGLE_ID=
# Google Recaptcha
GOOGLE_RECAPTCHA_PUBLIC=
GOOGLE_RECAPTCHA_PRIVATE=
GOOGLE_RECAPTCHA_VERSION=
LDC_WEB_API_KEY="MY_KEY"
LDC_STATUS_CHECKER_API_KEY="MY_KEY"
LDC_STATUS_CHECKER_UUID="UUID_PROJECT"
LDC_STATUS_CHECKER_UUID="UUID_PROJECT"

View file

@ -6,19 +6,10 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use App\Enums\Country;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
/**
* Perform any actions required when creating the object.
*/
public function __construct()
{
view()->share('availableCountries', Country::toArray());
}
}

View file

@ -39,6 +39,7 @@ class Kernel extends HttpKernel
'lang-check',
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
],
'api' => [

View file

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

View file

@ -2,6 +2,7 @@
namespace App\Http\Requests;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Foundation\Http\FormRequest;
class ContactFormRequest extends FormRequest
@ -23,6 +24,8 @@ class ContactFormRequest extends FormRequest
*/
public function rules()
{
ToolboxHelper::validateRecaptcha($this, \config('services.google.recaptcha.version'));
return [
'civilite' => 'required|in:monsieur,madame',
'name' => 'required|string|min:2',

View file

@ -0,0 +1,65 @@
<?php
namespace App\Lib\Helpers;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;
class ToolboxHelper
{
/**
* Validate recaptcha
*
* @param Request $request
* @param boolean $isV3
* @return void
*/
public static function validateRecaptcha(Request $request, bool $isV3 = true)
{
Validator::validate($request->all(), [
'g-recaptcha-response' => [
'required', 'string',
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
function ($attribute, $value, $fail) use ($isV3) {
try {
$response = Http::asForm()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.google.recaptcha.private'),
'response' => \strval($value),
'remoteip' => app('clientIp')
]);
if ($e = $response->toException()) {
throw $e;
}
if ($isV3) {
if (
!$response->json('success') or
!$response->json('challenge_ts') or
Carbon::parse($response->json('challenge_ts'))->lessThan(\now('utc')->subMinutes(5)) or
$response->json('hostname') !== \parse_url(config('app.url'), \PHP_URL_HOST) or
!$response->json('score')
) {
throw new \RuntimeException();
}
if (\floatval($response->json('score')) < 0.8) {
$fail("Vous n'avez pas obtenu un score assez élevé lors de la validation du recaptcha");
}
return;
}
if (
!$response->json('success') or
!$response->json('challenge_ts') or
Carbon::parse($response->json('challenge_ts'))->lessThan(\now('utc')->subMinutes(5)) or
$response->json('hostname') !== \parse_url(config('app.url'), \PHP_URL_HOST)
) {
throw new \RuntimeException();
}
} catch (\Exception $e) {
$fail('Erreur lors de la validation du formulaire veuillez réssayer');
} //end try
}
]
], [], ['g-recaptcha-response' => 'reCaptcha']);
}
}

View file

@ -4,6 +4,8 @@ namespace App\Providers;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use App\Enums\Country;
use Illuminate\Support\Facades\Cache;
class AppServiceProvider extends ServiceProvider
{
@ -24,5 +26,18 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
URL::forceScheme('https');
// * HA PROXY FIX
app()->singleton('clientIp', function () {
if (config('app.env') === 'local') {
return request()->getClientIp();
}
return request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
});
$listCountries = Cache::remember('listCountries', 60, function () {
return Country::toArray();
});
view()->share('availableCountries', $listCountries ?? Country::toArray());
}
}

View file

@ -70,14 +70,17 @@
"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": {
"lab.s-web.fr": {
"type": "composer",
"url": "https://gitlab.com/api/v4/group/16669037/-/packages/composer/packages.json"
"url": "https://lab.s-web.fr/api/v4/group/23/-/packages/composer/packages.json"
}
}
}

View file

@ -30,4 +30,12 @@ return [
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'google' => [
'recaptcha' => [
'public' => env('GOOGLE_RECAPTCHA_PUBLIC'),
'private' => env('GOOGLE_RECAPTCHA_PRIVATE'),
'version' => env('GOOGLE_RECAPTCHA_VERSION', "3") === "3" ? true : false
]
],
];

574
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,33 @@ docReady(function () {
}
};
// PAGE CONTACT
let eltCountry = document.getElementById("country");
if (eltCountry) {
eltCountry.addEventListener("click", function () {
showDromField(this);
});
}
let eltsConsoRecla = document.getElementsByClassName("consoRecla");
if (eltsConsoRecla) {
for (let ii = 0; ii < eltsConsoRecla.length; ii++) {
eltsConsoRecla[ii].addEventListener("click", function () {
showReclaFields();
});
}
}
let eltsOtherRecla = document.getElementsByClassName("otherRecla");
if (eltsOtherRecla) {
for (let ii = 0; ii < eltsOtherRecla.length; ii++) {
eltsOtherRecla[ii].addEventListener("click", function () {
hideReclaFields();
});
}
}
// FIN PAGE CONTACT
document
.getElementById("backtotop")
.addEventListener("click", function () {

View file

@ -48,14 +48,14 @@
<div class="header-title sectiontitle align-center p-rel">
<h2 class="punchline-module__title align-center animate__animated fadeInUp animate__slow">{!! __('Une question ? Une idée ? Un conseil ?<br>Des informations sur nos produits ?') !!}</h2>
</div>
<p class="align-center">{!! __('N’hésitez-pas à nous contacter en renseignant le formulaire ci-dessous.<br>Nous traiterons votre demande avec la plus grande attention dans les meilleurs délais.') !!}</p>
<p class="align-center">{!! __('N\'hésitez-pas à nous contacter en renseignant le formulaire ci-dessous.<br>Nous traiterons votre demande avec la plus grande attention dans les meilleurs délais.') !!}</p>
@if(session()->has('status'))
<div class="form-result form-result_confirm">
<div class="form-result__title title title_small">{{ __('Votre message a bien été envoyé !') }}</div>
<div class="form-result__text">{{ __('Nos services mettent tout en oeuvre pour y répondre au plus vite.') }}</div>
</div>
@endif
<form action="{{route($routeprefixer . 'contact.submit')}}" method="post" class="form contact__form" enctype="multipart/form-data">
<form id="contact-form" action="{{route($routeprefixer . 'contact.submit')}}" method="post" class="form contact__form" enctype="multipart/form-data">
@csrf
@if ($errors->any())
@foreach ($errors->all() as $error)
@ -110,10 +110,10 @@
</div>
<div class="form-row">
<div class="country">
<select id="country" class="changeLength" maxlength="255" name="country" required onclick="javascript:showDromField(this);">
<select id="country" class="changeLength" maxlength="255" name="country" required>
<option value="" @if(!old('country')) selected @endif>{{ __('Sélectionnez un pays') }} *</option>
@foreach($availableCountries as $country)
<option value="{{ $country->label }}" @if(old('country') == $country->label) selected @endif>{{ __($country->label) }}</option>
<option value="{{ $country->label }}" @if(old('country') == $country->label) selected @endif>{{ $country->label }}</option>
@endforeach
</select>
</div>
@ -141,16 +141,16 @@
<legend>{{ __('Préciser votre demande') }} :</legend>
<div class="ck-button">
<label>
<input type="radio" value="Réclamation" name="topic" onclick="javascript:showReclaFields();" @if (old('topic')=="Réclamation") checked @endif><span>{{ __('Réclamation produit') }}</span>
<input class="consoRecla" type="radio" value="Réclamation" name="topic" @if (old('topic')=="Réclamation") checked @endif><span>{{ __('Réclamation produit') }}</span>
</label>
<label>
<input type="radio" value="La marque" name="topic" onclick="javascript:hideReclaFields();" @if (old('topic')=="La marque") checked @endif><span>{{ __('La marque') }}</span>
<input class="otherRecla" type="radio" value="La marque" name="topic" @if (old('topic')=="La marque") checked @endif><span>{{ __('La marque') }}</span>
</label>
<label>
<input type="radio" value="Contact Commercial" onclick="javascript:hideReclaFields();" name="topic" @if (old('topic')=="Contact Commercial") checked @endif><span>{{ __('Contact commercial') }}</span>
<input class="otherRecla" type="radio" value="Contact Commercial" name="topic" @if (old('topic')=="Contact Commercial") checked @endif><span>{{ __('Contact commercial') }}</span>
</label>
<label>
<input type="radio" value="Autre" name="topic" onclick="javascript:hideReclaFields();" @if (old('topic')=="Autre") checked @endif><span>{{ __('Autre demande') }}</span>
<input class="otherRecla" type="radio" value="Autre" name="topic" @if (old('topic')=="Autre") checked @endif><span>{{ __('Autre demande') }}</span>
</label>
</div>
<div class="form-row recla_field">
@ -198,6 +198,7 @@
<label for="rgpd">{{ __('rgpd.contact.acceptcgu') }}</label>
</div>
<div class="align-center">
<div class="g-recaptcha" data-sitekey="{{ config('services.google.recaptcha.public') }}"></div>
<input type="submit" value="Envoyer">
</div>
</form>
@ -207,53 +208,67 @@
@endsection
@push('scripts')
<script>
function showReclaFields() {
console.log('show');
var collection = document.getElementsByClassName('recla_field');
for(let elem of collection) {
elem.style.display = 'flex';
}
document.getElementById('lblFile').innerHTML = 'Photo de l\'emballage*';
document.getElementById('file').required = true;
}
function hideReclaFields() {
console.log('hide');
var collection = document.getElementsByClassName('recla_field');
for(let elem of collection) {
elem.style.display = 'none';
<script nonce="{{ $nonce }}">
function showReclaFields() {
var collection = document.getElementsByClassName('recla_field');
for(let elem of collection) {
elem.style.display = 'flex';
}
document.getElementById('lblFile').innerHTML = 'Photo de l\'emballage*';
document.getElementById('file').required = true;
}
document.getElementById('lblFile').innerHTML = 'Insérer une pièce jointe';
document.getElementById('file').required = false;
}
function showDromField(sel) {
if (document.getElementById('country').options[sel.selectedIndex].text=='France') {
function hideReclaFields() {
var collection = document.getElementsByClassName('recla_field');
for(let elem of collection) {
elem.style.display = 'none';
}
document.getElementById('lblFile').innerHTML = 'Insérer une pièce jointe';
document.getElementById('file').required = false;
}
function showDromField(sel) {
if (document.getElementById('country').options[sel.selectedIndex].text=='France') {
var dromList = document.getElementById('departement');
dromList.style.display = 'initial';
dromList.required = true;
} else {
hideDromField();
}
}
function hideDromField() {
var dromList = document.getElementById('departement');
dromList.style.display = 'initial';
dromList.required = true;
} else {
hideDromField();
dromList.style.display = 'none';
dromList.required = false;
}
}
function hideDromField() {
var dromList = document.getElementById('departement');
dromList.style.display = 'none';
dromList.required = false;
}
document.addEventListener('DOMContentLoaded', function () {
hideReclaFields();
hideDromField();
@if (old('topic')=="Réclamation")
showReclaFields();
@endif
@if (old('country')=="France")
showDromField();
@endif
});
document.addEventListener('DOMContentLoaded', function () {
hideReclaFields();
hideDromField();
@if (old('topic')=="Réclamation")
showReclaFields();
@endif
@if (old('country')=="France")
showDromField();
@endif
});
</script>
<script nonce="{{ $nonce }}">
window.__LDC_CN_Recaptcha = function() {
(function(d, t) {
let g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src = 'https://www.google.com/recaptcha/api.js';
s.parentNode.insertBefore(g, s)
}(document, 'script'));
};
function onSubmit(token) {
document.getElementById("contact-form").submit();
}
</script>
@endpush

View file

@ -17,9 +17,9 @@
</div>
<!-- Scripts -->
<script src="{{ mix('js/preload-front.js') }}" type="text/javascript"></script>
<script src="{{ mix('js/preload-front.js') }}" type="text/javascript" nonce="{{ $nonce }}"></script>
@stack('scripts')
</body>
</html>
</html>

View file

@ -124,11 +124,11 @@
</div>
</header>
@push('scripts')
<script>
<script nonce="{{ $nonce }}">
var loupe = document.querySelector("div#search");
loupe.addEventListener("click", function() {
document.querySelector("div.search-form").classList.toggle('is-visible');
});
</script>
@endpush
@endpush

View file

@ -86,7 +86,7 @@
</div>
@push('scripts')
<script>
<script nonce="{{ $nonce }}">
window.LDCWebCN = new LdcwebCn({
'analytics': {
'name': "{!! __('rgpd.analytics.title') !!}",
@ -112,7 +112,19 @@
gtag('js', new Date());
gtag('config', '{{ config('google.analyticsKey') }}');
}
}
},
'recaptcha': {
'name': "API - reCAPTACHA",
'description': 'Les cookies qui sont déposés via le recaptcha sur les formulaires ont pour finalité d\'empêcher que ceux-ci ne soient soumis par des robots.',
'link': 'https://policies.google.com/privacy',
'labelLink': 'En savoir plus',
'cookies': ['NID', '__Secure-3PAPISID', '__Secure-3PSID', '__Secure-3PSIDCC'],
'script': function() {
if (window.__LDC_CN_Recaptcha) {
window.__LDC_CN_Recaptcha();
}
}
},
});
</script>
@endpush