Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
# Pas de listing
Options -Indexes
# Réécriture activée
RewriteEngine On
# Autoriser UNIQUEMENT /sms/notify_rdv.php en HTTP
RewriteCond %{REQUEST_URI} !/sms/notify_rdv\.php$ [NC]
RewriteRule ^ - [F]
# notify_rdv.php : POST uniquement
<Files "notify_rdv.php">
<LimitExcept POST>
Require all denied
</LimitExcept>
</Files>
# Interdire l'accès direct aux fichiers sensibles
<FilesMatch "^(config|config\.sample|wa_client|templates)\.php$">
Require all denied
</FilesMatch>
# Bloquer les fichiers cachés/artefacts
<FilesMatch "^(\.env|\.git|composer\.json|composer\.lock|.*\.md|.*\.txt)$">
Require all denied
</FilesMatch>
+17
View File
@@ -0,0 +1,17 @@
<?php
// Interdit l'accès direct au fichier dans un navigateur
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'])) {
http_response_code(403);
exit('Forbidden');
}
// Secrets par variables d'environnement (idéal en production)
return [
// Récupération depuis l'ENV si dispo, sinon fallback
'phone_number_id' => getenv('WHATSAPP_PHONE_NUMBER_ID') ?: '720141641193323',
'access_token' => getenv('WHATSAPP_TOKEN') ?: 'EAAYZApQ01JR8BPVMSMYVAYHzg6WHfFJW5Q9fyphrxvDl32hIq4X169LujTe0ffmlscl5t0vEFuYJrjNUCWFBHYOUGRJU5UT4QQYJb1TwqFH1Pn9ZBu9UeAi0cp6vUKmKNV40B3JlN1bhE2u0czZBX8dXko3Oh97dCIMijcY4Ib88aZCMUBxjBzsSQuk7Rl52QwZDZD',
// Langue par défaut de tes templates
'default_lang' => 'ru',
// Timeout réseau cURL
'timeout' => 20,
];
+12
View File
@@ -0,0 +1,12 @@
<?php
if (!defined('APP_BOOT')) { http_response_code(403); exit('Forbidden'); }
// /sms/config.sample.php
return [
// Récupération depuis l'ENV si dispo, sinon fallback
'phone_number_id' => getenv('WHATSAPP_PHONE_NUMBER_ID') ?: '720141641193323',
'access_token' => getenv('WHATSAPP_TOKEN') ?: 'EAAYZApQ01JR8BPVMSMYVAYHzg6WHfFJW5Q9fyphrxvDl32hIq4X169LujTe0ffmlscl5t0vEFuYJrjNUCWFBHYOUGRJU5UT4QQYJb1TwqFH1Pn9ZBu9UeAi0cp6vUKmKNV40B3JlN1bhE2u0czZBX8dXko3Oh97dCIMijcY4Ib88aZCMUBxjBzsSQuk7Rl52QwZDZD',
// Langue par défaut de tes templates
'default_lang' => 'ru',
// Timeout réseau cURL
'timeout' => 20,
];
+88
View File
@@ -0,0 +1,88 @@
<?php
// /sms/notify_rdv.php
declare(strict_types=1);
namespace Sms;
require_once __DIR__ . '/wa_client.php';
function notify_rdv(string $to, string $prenomNom, string $dateHeureDDMMYYYY_HHMM): array
{
// Charge config & templates
$cfg = require __DIR__ . '/config.php';
$templates = require __DIR__ . '/templates.php';
$tplName = 'notification_rdv';
$tpl = $templates[$tplName] ?? null;
if (!$tpl) {
throw new \RuntimeException("Template {$tplName} non déclaré dans /sms/templates.php");
}
// Valide le format date/heure attendu "01/01/2025 12:00"
if (!preg_match('/^\d{2}\/\d{2}\/\d{4}\s\d{2}:\d{2}$/', $dateHeureDDMMYYYY_HHMM)) {
throw new \InvalidArgumentException('Format date/heure invalide. Attendu: "01/01/2025 12:00"');
}
// Vérifie le compte de variables attendu dans le BODY
$expected = (int)($tpl['components']['body_params'] ?? 0);
$actual = 2;
if ($expected !== $actual) {
throw new \RuntimeException("Le template {$tplName} attend {$expected} paramètres BODY, {$actual} fournis.");
}
$lang = $tpl['language'] ?: ($cfg['default_lang'] ?? 'ru');
$client = new WaClient($cfg);
// {{1}} = prénom nom ; {{2}} = date-heure
return $client->sendTemplate($tplName, $lang, $to, [$prenomNom, $dateHeureDDMMYYYY_HHMM]);
}
function notify_rdv_en_attente(string $to, string $prenomNom, string $dateHeureDDMMYYYY_HHMM, int $rdvId): array
{
// Config & templates
$cfg = is_file(__DIR__.'/config.php') ? require __DIR__.'/config.php' : [
'phone_number_id' => getenv('WHATSAPP_PHONE_NUMBER_ID') ?: '',
'access_token' => getenv('WHATSAPP_TOKEN') ?: '',
'default_lang' => 'ru',
'timeout' => 20,
];
if (($cfg['phone_number_id'] ?? '') === '' || ($cfg['access_token'] ?? '') === '') {
throw new \RuntimeException("Config WhatsApp manquante: crée /sms/config.php ou définis les variables d'environnement.");
}
$templates = require __DIR__ . '/templates.php';
$tplName = 'en_attente_confirmation';
$tpl = $templates[$tplName] ?? null;
if (!$tpl) {
throw new \RuntimeException("Template {$tplName} non déclaré dans /sms/templates.php");
}
// Valide date/heure "01/01/2025 12:00"
if (!preg_match('/^\d{2}\/\d{2}\/\d{4}\s\d{2}:\d{2}$/', $dateHeureDDMMYYYY_HHMM)) {
throw new \InvalidArgumentException('Format date/heure invalide. Attendu: "01/01/2025 12:00"');
}
// Vérifie compte de variables attendu
$expectedBody = (int)($tpl['components']['body_params'] ?? 0);
if ($expectedBody !== 2) {
throw new \RuntimeException("Le template {$tplName} attend {$expectedBody} paramètres BODY, 2 fournis.");
}
// Bouton URL index "0" avec 1 paramètre ({{1}})
$btnCfg = $tpl['components']['button_url_params']['0'] ?? 0;
if ((int)$btnCfg !== 1) {
throw new \RuntimeException("Le bouton URL index 0 du template {$tplName} doit attendre 1 paramètre.");
}
$lang = $tpl['language'] ?: ($cfg['default_lang'] ?? 'ru');
$client = new WaClient($cfg);
// BODY: {{1}} prénomNom, {{2}} dateHeure ; BUTTON index "0": {{1}} = rdvId
return $client->sendTemplate(
$tplName,
$lang,
$to,
[$prenomNom, $dateHeureDDMMYYYY_HHMM],
['0' => [(string)$rdvId]]
);
}
+23
View File
@@ -0,0 +1,23 @@
<?php
// /sms/templates.php
return [
// Notification Utility pour rendez-vous
'notification_rdv' => [
'language' => 'en', // ou 'fr' si tu crées une version FR
'components' => [
// BODY a exactement 2 variables: {{1}} = Nom client, {{2}} = DateHeure
'body_params' => 2,
// Si un jour tu ajoutes un HEADER ou un BUTTON avec variables, ajoute 'header_params' / 'button_url_params' etc.
],
],
// Nouveau template avec bouton URL
'en_attente_confirmation' => [
'language' => 'ru', // adapte si tu as une locale différente
'components' => [
'body_params' => 2, // {{1}} nom client, {{2}} date/heure
'button_url_params' => ['0' => 1] // bouton index "0" a 1 placeholder {{1}} (l’ID du RDV)
],
],
];
+115
View File
@@ -0,0 +1,115 @@
<?php
// /sms/wa_client.php
declare(strict_types=1);
namespace Sms;
final class WaClient
{
private string $phoneId;
private string $token;
private int $timeout;
public function __construct(array $cfg)
{
$this->phoneId = $cfg['phone_number_id'] ?? '';
$this->token = $cfg['access_token'] ?? '';
$this->timeout = (int)($cfg['timeout'] ?? 20);
if ($this->phoneId === '' || $this->token === '') {
throw new \RuntimeException('Config WhatsApp incomplète: phone_number_id / access_token manquants.');
}
}
/** Normalise en format international chiffres-only (WhatsApp: pas de "+") */
public static function normalizePhone(string $raw): string
{
$n = preg_replace('/\D+/', '', $raw ?? '');
// Ex: numéro FR commençant par 0 -> remplace 0 initial par 33
if (preg_match('/^0\d{9}$/', $n)) {
$n = '33' . substr($n, 1);
}
return $n;
}
/**
* Envoi d’un template Utility.
* $templateName: ex. 'notification_rdv'
* $lang: 'ru' (ou celui déclaré/validé chez Meta)
* $to: numéro destinataire (06..., +33..., 336..., etc.)
* $bodyParams: tableau des valeurs pour {{1}}, {{2}}, ...
*/
public function sendTemplate(string $templateName, string $lang, string $to, array $bodyParams, ?array $buttonUrlParams = null): array
{
$toNorm = self::normalizePhone($to);
if ($toNorm === '' || strlen($toNorm) < 8) {
throw new \InvalidArgumentException('Numéro destinataire invalide.');
}
// BODY params
$components = [[
'type' => 'body',
'parameters' => array_map(fn($t) => ['type'=>'text','text'=>(string)$t], $bodyParams)
]];
// BUTTON URL params (facultatif)
// $buttonUrlParams attendu sous la forme: ['0' => ['123']] // index "0" => [ valeurs pour {{1}}, {{2}} si existait ]
if ($buttonUrlParams) {
foreach ($buttonUrlParams as $index => $values) {
$params = array_map(fn($t) => ['type'=>'text','text'=>(string)$t], (array)$values);
$components[] = [
'type' => 'button',
'sub_type' => 'url',
'index' => (string)$index,
'parameters' => $params,
];
}
}
$payload = [
'messaging_product' => 'whatsapp',
'to' => $toNorm,
'type' => 'template',
'template' => [
'name' => $templateName,
'language' => ['code' => $lang],
'components' => $components,
]
];
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$ch = curl_init("https://graph.facebook.com/v23.0/{$this->phoneId}/messages");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$this->token}",
"Content-Type: application/json",
"Accept: application/json",
"Expect:",
"Connection: close",
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $json,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
$data = json_decode($resp ?? '', true);
return [
'http_code' => $code,
'error' => $err ?: null,
'response' => $data ?? $resp,
'payload' => $payload,
];
}
}