159 lines
7.8 KiB
PHP
159 lines
7.8 KiB
PHP
<?php
|
||
// ============================================================================
|
||
// fonctions_sms.php - Envoi de SMS par l'API OVHcloud (synthèse §51, étape F).
|
||
// Commun au portail (code de vérification du numéro) et aux scripts natifs (alertes) : inclus par config/fonctions.php.
|
||
//
|
||
// Identifiants : dans config/confidentiel.php (jamais versionné), constantes OVH_SMS_*. Tant qu'elles sont absentes ou
|
||
// vides, Sms_configure() renvoie false et aucun envoi n'est tenté (l'échec est enregistré comme tel).
|
||
//
|
||
// Requête : POST https://eu.api.ovh.com/1.0/sms/{service}/jobs, signée :
|
||
// X-Ovh-Signature = "$1$" . sha1(AS + "+" + CK + "+" + METHODE + "+" + URL + "+" + CORPS + "+" + HORODATAGE)
|
||
// L'horodatage doit être proche de l'heure des serveurs OVH (VPS synchronisé par NTP ; sinon : erreur "Invalid
|
||
// signature" ou "Query out of time"). La clé n'a besoin que des droits : GET /sms/{service} (crédits) et
|
||
// POST /sms/{service}/jobs (envoi).
|
||
// ============================================================================
|
||
|
||
const OVH_API_RACINE = "https://eu.api.ovh.com/1.0";
|
||
const SMS_LONGUEUR_MAX = 160; // un seul SMS (alphabet GSM 7 bits) : ni découpage, ni surcoût
|
||
const SMS_VALIDITE_MINUTES = 2880; // téléphone éteint / hors réseau : remise tentée pendant 48 h
|
||
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// Les identifiants OVH sont-ils renseignés ? (Expéditeur facultatif : sans nom d'expéditeur validé, OVH envoie depuis
|
||
// un numéro court.)
|
||
// ----------------------------------------------------------------------------
|
||
function Sms_configure()
|
||
{
|
||
foreach (array('OVH_SMS_APPLICATION_KEY', 'OVH_SMS_APPLICATION_SECRET', 'OVH_SMS_CONSUMER_KEY', 'OVH_SMS_SERVICE') as $c)
|
||
if (!defined($c) || trim((string)constant($c)) === '') return false;
|
||
return function_exists('curl_init');
|
||
}
|
||
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// Ramène un texte à l'alphabet GSM 7 bits (accents sans équivalent remplacés par la lettre simple, typographie
|
||
// remplacée par l'équivalent ASCII, le reste par "?") et le limite à SMS_LONGUEUR_MAX (les caractères de l'extension
|
||
// GSM - € [ ] { } ^ ~ | \ - comptent double). Sans cela, un seul "ê" ferait passer tout le SMS en UCS-2 (70 caractères
|
||
// par SMS au lieu de 160).
|
||
// ----------------------------------------------------------------------------
|
||
function Texte_sms_gsm($texte)
|
||
{
|
||
static $base = "@£\$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà";
|
||
static $extension = "€[]{}^~|\\";
|
||
static $remplacements = array(
|
||
'â'=>'a', 'á'=>'a', 'ã'=>'a', 'ê'=>'e', 'ë'=>'e', 'î'=>'i', 'ï'=>'i', 'í'=>'i', 'ô'=>'o', 'ó'=>'o', 'õ'=>'o',
|
||
'û'=>'u', 'ú'=>'u', 'ç'=>'c', 'ÿ'=>'y', 'œ'=>'oe', 'À'=>'A', 'Â'=>'A', 'Á'=>'A', 'È'=>'E', 'Ê'=>'E', 'Ë'=>'E',
|
||
'Î'=>'I', 'Ï'=>'I', 'Ô'=>'O', 'Ù'=>'U', 'Û'=>'U', 'Œ'=>'OE', '’'=>"'", '‘'=>"'", '“'=>'"', '”'=>'"', '«'=>'"',
|
||
'»'=>'"', '–'=>'-', '—'=>'-', '…'=>'...', "\u{00A0}"=>' ', "\u{202F}"=>' ', "\t"=>' ', '°'=>' deg', '•'=>'-');
|
||
|
||
$texte = strtr((string)$texte, $remplacements);
|
||
preg_match_all('/./us', $texte, $caracteres);
|
||
$resultat = '';
|
||
$longueur = 0;
|
||
foreach ($caracteres[0] as $c)
|
||
{
|
||
if (strpos($extension, $c) !== false) $poids = 2;
|
||
else
|
||
{
|
||
$poids = 1;
|
||
if (strpos($base, $c) === false) $c = '?';
|
||
}
|
||
if ($longueur + $poids > SMS_LONGUEUR_MAX) break;
|
||
$resultat .= $c;
|
||
$longueur += $poids;
|
||
}
|
||
return $resultat;
|
||
}
|
||
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// Appel signé de l'API OVH.
|
||
// Retour : ['code' => code HTTP (0 si pas de réponse), 'donnees' => réponse JSON décodée (ou null), 'erreur' => texte]
|
||
// ----------------------------------------------------------------------------
|
||
function Appel_api_ovh($methode, $chemin, $corps = null)
|
||
{
|
||
$url = OVH_API_RACINE.$chemin;
|
||
$json = ($corps === null) ? '' : json_encode($corps, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
$horodatage = (string)time();
|
||
$signature = '$1$'.sha1(OVH_SMS_APPLICATION_SECRET.'+'.OVH_SMS_CONSUMER_KEY.'+'.$methode.'+'.$url.'+'.$json.'+'.$horodatage);
|
||
|
||
$ch = curl_init($url);
|
||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $methode);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||
'Content-Type: application/json; charset=utf-8',
|
||
'X-Ovh-Application: '.OVH_SMS_APPLICATION_KEY,
|
||
'X-Ovh-Consumer: '.OVH_SMS_CONSUMER_KEY,
|
||
'X-Ovh-Timestamp: '.$horodatage,
|
||
'X-Ovh-Signature: '.$signature));
|
||
if ($corps !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
|
||
$reponse = curl_exec($ch);
|
||
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||
$erreur_curl = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
if ($reponse === false) return array('code' => 0, 'donnees' => null, 'erreur' => "OVH injoignable : ".$erreur_curl);
|
||
$donnees = json_decode($reponse, true);
|
||
if ($code !== 200)
|
||
{
|
||
$message = is_array($donnees) && isset($donnees['message']) ? $donnees['message'] : substr($reponse, 0, 200);
|
||
return array('code' => $code, 'donnees' => $donnees, 'erreur' => "OVH HTTP ".$code." : ".$message);
|
||
}
|
||
return array('code' => $code, 'donnees' => $donnees, 'erreur' => '');
|
||
}
|
||
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// Envoi d'un SMS à un numéro E.164 (+33612345678).
|
||
// $erreur : motif de l'échec (sinon null)
|
||
// $reference : identifiant de l'envoi chez OVH (pour le suivi dans l'espace client), sinon null
|
||
// Retour : true si OVH a accepté le numéro (la remise effective au téléphone n'est pas connue à ce stade)
|
||
// ----------------------------------------------------------------------------
|
||
function Envoyer_sms_ovh($numero, $texte, &$erreur = null, &$reference = null)
|
||
{
|
||
$erreur = null;
|
||
$reference = null;
|
||
if (!Sms_configure()) { $erreur = "envoi de SMS non configuré (identifiants OVH absents)"; return false; }
|
||
if (preg_match('/^\+[1-9][0-9]{6,14}$/', (string)$numero) !== 1) { $erreur = "numéro invalide"; return false; }
|
||
|
||
$corps = array(
|
||
'message' => Texte_sms_gsm($texte),
|
||
'receivers' => array($numero),
|
||
'charset' => 'UTF-8',
|
||
'coding' => '7bit',
|
||
'noStopClause' => true, // SMS non commerciaux (alertes, codes) : pas de mention "STOP"
|
||
'priority' => 'high',
|
||
'validityPeriod' => SMS_VALIDITE_MINUTES);
|
||
$expediteur = defined('OVH_SMS_EXPEDITEUR') ? trim((string)OVH_SMS_EXPEDITEUR) : '';
|
||
if ($expediteur !== '') $corps['sender'] = $expediteur; // nom validé par OVH
|
||
else $corps['senderForResponse'] = true; // sinon : numéro court d'OVH
|
||
|
||
$r = Appel_api_ovh('POST', '/sms/'.rawurlencode(OVH_SMS_SERVICE).'/jobs', $corps);
|
||
if ($r['erreur'] !== '') { $erreur = $r['erreur']; return false; }
|
||
|
||
$valides = $r['donnees']['validReceivers'] ?? array();
|
||
if (!is_array($valides) || count($valides) == 0)
|
||
{
|
||
$erreur = "numéro refusé par OVH (".implode(', ', (array)($r['donnees']['invalidReceivers'] ?? array())).")";
|
||
return false;
|
||
}
|
||
$ids = $r['donnees']['ids'] ?? array();
|
||
if (is_array($ids) && count($ids) > 0) $reference = (string)$ids[0];
|
||
return true;
|
||
}
|
||
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// Crédits SMS restants sur le compte OVH, ou null (non configuré, erreur).
|
||
// ----------------------------------------------------------------------------
|
||
function Credits_sms_ovh(&$erreur = null)
|
||
{
|
||
$erreur = null;
|
||
if (!Sms_configure()) { $erreur = "envoi de SMS non configuré (identifiants OVH absents)"; return null; }
|
||
$r = Appel_api_ovh('GET', '/sms/'.rawurlencode(OVH_SMS_SERVICE));
|
||
if ($r['erreur'] !== '') { $erreur = $r['erreur']; return null; }
|
||
return isset($r['donnees']['creditsLeft']) ? (float)$r['donnees']['creditsLeft'] : null;
|
||
}
|