<?php /*************************************************************************
* contact.php
* Copyright (c) François Pirsch 2007
*
http://aspirine.org/contact/
* Distribué sous licence BSD.
*
* Envoi par mail des données d'un formulaire de contact.
* Le formulaire lui-même doit être dans une page html séparée,
* il doit être envoyé vers ce script php avec la méthode POST.
*
* Il doit contenir un champ nommé "email"
* et un champ nommé "sujet" ou "subject".
* Tout champ nommé "email2" (utilisé pour confirmation) sera ignoré.
*
* 19 septembre 2008
* - Les destinataires peuvent être choisis avec des cases à cocher ou des
* boutons radio, et pas seulement dans une liste déroulante.
* - Quelques corrections de bugs - merci à Ben-J
*
* 2 septembre 2008
* - Utilisation uniquement de balises <?php pour un maximum de compatibilité
* - Les caractères spéciaux sont mieux pris en compte (<, >, antislashes...)
*
* 16 août 2008:
* - Les erreurs mysql sont détaillées.
* - possibilité de donner le nom de la base de données.
*
* 14 août 2008 :
* - correction de bug occasionnel
* - erreur explicite en cas de mauvaise config des destinataires au choix.
* - Pas d'erreur si le champ "email" est vide et pas obligatoire.
* - Le champ "from" par défaut est maintenant automatique, avec le
* nom de domaine correct.
*
* 29 décembre 2007 :
* - notification automatique en cas de nouvelle version.
* - intégration du système anti-spam recaptcha.net
* - possibilité pour le visiteur de choisir le destinataire.
* - possibilité de chiffrer le mail.
* - possibilité d'utiliser une BDD sur un autre serveur que localhost.
* - bug du formulaire vide corrigé.
*************************************************************************/
/*
Pour le chiffrement des mails, voir mcrypt, qui a priori devrait couramment être installée. Mais faudra vérifier.
http://fr.php.net/manual/fr/ref.mcrypt.php
*/
include 'contact.config.php'; // Provoque une erreur si le fichier est absent, et c'est bien.
@include 'recaptchalib.php'; // C'est en option, donc il ne doit pas y avoir d'erreur si le fichier est absent.
$VERSION_DATE = '2008-09-19';
$VERSION_INFO_PATH = 'http://aspirine.org/contact/version.xml';
/* Expéditeur par défaut.
* Si le visiteur ne laisse pas son adresse quand elle est facultative, le mail doit quand-même avoir un expéditeur
* et le nom de domaine doit correspondre à celui du site web, sinon le mail risque d'être refusé.
*/
$from = "contact.php@".$_SERVER["HTTP_HOST"]; // donne "contact.php@domaine_de_mon_site.com" par exemple.
/* ________________________________
* ___/ Quelques fonctions utilitaires \____________________________________________
*/
// Vérifie l'existence du domaine indiqué.
function HostExists($domaine) {
// if($domaine == 'ath.forthnet.gr') return true;
if (preg_match("/^([0-9]{1,3}\\.){3}[0-9]{1,3}$/", GetHostByName($domaine))) return true;
// Si la vérification a échoué, on réessaie éventuellement en ajoutant "www."
// C'est un peu foireux dans le cas des domaines avec uniquement un MX et pas de serveur web,
// Mais ça fonctionne dans la plupart des cas.
// Voir les fonctions checkdnsrr() ou getmxrr(), qui ne sont pour l'instant pas dispo
// sous windows.
if(substr($domaine, 0, 4) == "www.") return false;
return (preg_match("/^([0-9]{1,3}\\.){3}[0-9]{1,3}$/", GetHostByName("www.$domaine"))) ? true : false;
}
// Vérifie la validité de l'adresse.
function AdresseValide($adresse) {
if(strlen($adresse) > 100) return false;
$atom = "[!#-'*+\\-\\/-9=?A-Z^-~]+";
$regex_adresse = "/^$atom(\\.$atom)*@$atom(\\.$atom)*\\.[a-zA-Z]{2,4}$/";
if(!preg_match($regex_adresse, $adresse)) return false;
// On sait qu'on a un @ et qu'il est bien placé.
return HostExists(substr($adresse, strpos($adresse, '@')+1));
}
// Quoted Printable. Conforme au RFC 2045 -
http://rfc.net/rfc2045.html ?
function QPencode($str, $iso_tag)
{
global $is_quoted;
$is_quoted = false;
if(!defined('CRLF'))
define('CRLF', "\r\n");
$lines = preg_split("/\r?\n/", $str);
$out = '';
foreach ($lines as $line)
{
$newpara = '';
for ($j = 0; $j <= strlen($line) - 1; $j++)
{
$char = substr ( $line, $j, 1 );
$ascii = ord ( $char );
if ( $ascii < 32 || $ascii == 61 || $ascii > 126 )
{
$char = '=' . strtoupper ( dechex( $ascii ) );
$is_quoted = true;
}
if ( ( strlen ( $newpara ) + strlen ( $char ) ) >= 76 )
{
$out .= $newpara . '=' . CRLF; $newpara = '';
$is_quoted = true;
}
$newpara .= $char;
}
$out .= $newpara;
}
$out = trim ( $out );
// Ici on perd la conformité RFC 2045
if($is_quoted && $iso_tag) $out = "=?ISO-8859-1?Q?".ereg_replace("\\?", "=3F", $out)."?=";
return $out;
}
// Filtre une chaîne entrée par l'utilisateur.
// On interdit tous les caractères non ISO-8859 (heu ,en gros)
function filtre_securite($s) {
return preg_replace('/[^\\x20-\\x7f\\xa0-\\xff]/', '', $s);
}
function apostrophes($s) {
return preg_replace('/\\\\(["\'])/', '$1', $s);
}
/* _________________________________________
* ___/ Enregistrement dans une base de données \____________________________
*/
function enregistre_bdd() {
global $db_server;
global $db_login;
global $db_password;
global $db_database_name;
global $db_nom_de_la_table;
global $db_champs_a_enregistrer;
global $db_enregistrement;
global $message;
global $separateur;
if(!($dbLink = @mySql_connect($db_server, $db_login, $db_password)))
return "Impossible de se connecter au serveur MySQL.<br/>\nLe serveur MySQL dit : ".mySql_error($dbLink)."\n";
$ce_qui_va = "Connection au serveur MySQL OK.<br />\n";
if(!isset($db_database_name) || !$db_database_name)
$db_database_name = $db_login;
if(!mySql_select_db($db_database_name, $dbLink))
return "Impossible de sélectionner la base de données $db_database_name.<br/>\nLe serveur MySQL dit : ".mySql_error($dbLink)."\n";
$ce_qui_va = "Sélection de la base de données OK.<br />\n";
// On crée la table et ses colonnes selon les besoins.
if(!mySql_query("CREATE TABLE IF NOT EXISTS `$db_nom_de_la_table` (`n` INT UNSIGNED AUTO_INCREMENT, KEY `n` (`n`));", $dbLink))
return $ce_qui_va."Erreur à la création de la table.<br/>\nLe serveur MySQL dit : ".mySql_error($dbLink)."\n";
$ce_qui_va .= "Table OK.<br />\n";
$db_result = mySql_query("SHOW COLUMNS FROM `$db_nom_de_la_table`;", $dbLink);
$db_champs = array();
while ($row = mysql_fetch_array($db_result, MYSQL_NUM))
$db_champs[$row[0]] = 1;
$champs_a_ajouter = array();
if(is_array($db_champs_a_enregistrer))
foreach($db_champs_a_enregistrer as $champ) {
if(!$db_champs[$champ])
array_push($champs_a_ajouter, "ADD `$champ` TEXT");
}
if(count($champs_a_ajouter) &&
!mySql_query("ALTER TABLE `$db_nom_de_la_table` ".implode(', ', $champs_a_ajouter).";"))
return $ce_qui_va."Erreur en ajoutant les champs à la table.<br/>\nLe serveur MySQL dit : ".mySql_error($dbLink)."\n";
// Préparation des données à enregistrer.
$noms = '(';
$valeurs = 'VALUES (';
if(is_array($db_enregistrement))
foreach($db_enregistrement as $nom => $valeur) {
$noms .= "`$nom`, ";
$valeurs .= "'".mysql_real_escape_string($valeur, $dbLink)."', ";
}
$noms = substr($noms, 0, -2) . ')';
$valeurs = substr($valeurs, 0, -2) . ')';
// Insertion dans la base de données.
if(!mySql_query("INSERT INTO `$db_nom_de_la_table` $noms $valeurs;", $dbLink))
return $ce_qui_va."Erreur à l'enregistrement dans la table.\n$noms\n$valeurs<br/>\nLe serveur MySQL dit : ".mySql_error($dbLink)."";
$message = "Courrier numéro$separateur".mySql_insert_id($dbLink)."\n" . $message;
return "";
}
/* _____________________
* ___/ Programme principal \____________________________________________
*/
/*
* Initialisations.
*/
$erreur = ''; // Si cette variable contient un message, le mail n'est pas envoyé et le message est affiché.
$message = ''; // le message à envoyer, avec toutes les infos supplémentaires, en clair et en texte brut.
$message_html = ''; // le message envoyé, sans infos supplémentaires, en clair et présenté en html (version pour le visiteur)
$mail_text = ''; // le message envoyé, avec toutes les infos supplémentaires, éventuellement chiffré.
$separateur = ' = ';
if($formater_pour_tableur)
$separateur = "\t";
$horizontal_rule = str_repeat('-', 64);
$sujet = QPencode(apostrophes($sujet), true);
$db_enregistrement = array();
// Disponible seulement à partir de php 5.2
//$champs_a_enregistrer = array_fill_keys($db_champs_a_enregistrer, 1);
$hash_champs_a_enregistrer = array();
if(is_array($db_champs_a_enregistrer))
foreach($db_champs_a_enregistrer as $key)
$hash_champs_a_enregistrer[$key] = 1;
/* _______________________________________________________
* ___/ Vérification anti-spam avec recaptcha.net (en option) \__________
*/
if($recaptcha_privatekey) {
if(function_exists('recaptcha_check_answer')) {
$recaptcha_resp = recaptcha_check_answer ($recaptcha_privatekey,
$_SERVER["REMOTE_ADDR"],
$_POST["recaptcha_challenge_field"],
$_POST["recaptcha_response_field"]);
if (!$recaptcha_resp->is_valid) {
// Le simple fait d'alimenter la variable $erreur bloque l'envoi du mail
// et provoque l'affichage de la page d'erreur.
$erreur .= "La réponse au test reCAPTCHA n'est pas correcte. " .
"(message reCAPTCHA : " . $recaptcha_resp->error . ")\n";
}
}
else
$erreur .= "Il faut copier dans le même répertoire le fichier recaptchalib.php ".
"(<a href=\"http://code.google.com/p/recaptcha/downloads/list?q=label:phplib-Latest\">télécharger</a>).\n";
}
/*
* Vérification de la présence des champs obligatoires.
* On tient compte de la présence d'étoiles au début
* des noms de champs, pour la vérification en JS.
*/
if(is_array($champs_obligatoires))
foreach($champs_obligatoires as $champ) {
$valeur = $_POST[$champ];
if(!$valeur || preg_match("/^[\\n\\r]*(.)\\1*[\\n\\r]*$/", $valeur))
$erreur .= "Le champ $champ est obligatoire.\n";
}
if($erreur) $erreur .= "\n";
/*
* Récupération et préparation des données du formulaire.
*/
foreach($_POST as $key=>$value) {
$lkey = strtolower($key);
if($hash_champs_a_enregistrer[$key])
$db_enregistrement[$key] = apostrophes($value);
$ligne_a_envoyer = '';
if($lkey === 'email') {
// Adresse de l'expéditeur.
if(AdresseValide(trim($value))) {
$from = trim($value);
$ligne_a_envoyer = $key.$separateur.$from . "\n";
} else if (!in_array('email', $champs_obligatoires) && !$value) {
// Si l'adresse est vide et facultative, ça passe.
} else
$erreur .= "Votre adresse email est invalide.\n";
}
elseif($lkey === 'to') {
// Ajout d'éventuels destinataires sélectionnés par l'utilisateur.
// Les checkboxes envoient uen liste de valeurs, les radiobuttons et les select
// envoient une valeur unique ; on crée une liste dans tous les cas.
$list = is_array($value) ? $value : array(0 => $value);
foreach($list as $nom) {
$dest = $destinataires_au_choix[$nom];
if($dest) {
if($to) $to .= ', ';
$to .= $dest;
} else
$erreur .= "Il n'y a aucun destinataire correspondant à ".htmlentities($nom).".\n";
}
}
elseif(($lkey === 'sujet') || ($lkey === 'subject')) {
// Le sujet est limitée à 100 caractères pour éviter les buffer oveflows.
$sujet = QPencode(apostrophes(filtre_securite(substr($value, 0, 100))), true);
$ligne_a_envoyer = $key.$separateur.apostrophes(preg_replace("/\\r?\\n/", "\n\t", $value)) . "\n";
}
elseif($lkey === 'email2') {
if($value !== $_POST['email']) // Si l'email est vide, $from contient une adresse par défaut.
$erreur .= "Il y a une faute de frappe entre les deux adresses email.\n";
}
elseif(($lkey === 'recaptcha_challenge_field') || ($lkey === 'recaptcha_response_field')) {
// On ignore ces deux champs qui sont réservés à l'utilistation du service recaptcha.net
if(!isset($recaptcha_privatekey) || !$recaptcha_privatekey)
$erreur .= "Il faut entrer la clé privée de recaptcha dans le fichier contact.config.php " .
"pour que la vérification puisse se faire.\n";
}
else {
// N'importe quel autre élément du formulaire :
if(is_array($value)) $value = implode("\n", $value);
$ligne_a_envoyer = $key.$separateur.apostrophes(preg_replace("/\\r?\\n/", "\n\t", $value)) . "\n";
}
if($value || $envoyer_aussi_les_champs_vides)
$message .= $ligne_a_envoyer;
}
/* On teste s'il y a un message à envoyer. Si oui, et que c'est demandé,
* on le complète avec les données HTTP.
*/
$message_html = "";
if($message) {
$message = str_replace("\\\\", "\\", $message);
$message_html = htmlentities($message);
if(count($variables_http)) {
// Les variables http ne sont pas ajoutées au message HTML.
$message .= "$horizontal_rule\n";
if(is_array($variables_http))
foreach($variables_http as $nom) {
$message .= "$nom$separateur$_SERVER[$nom]\n";
if($hash_champs_a_enregistrer[$nom])
$db_enregistrement[$nom] = $_SERVER[$nom];
}
}
}
else
$erreur .= "Pas de données à envoyer\n";
/* _________________________________
* ___/ Chiffrement éventuel du message \_____________________________________
*/
$mail_text = $message;
$cle_aes = '';
if($cle_chiffrement) {
if (function_exists('mcrypt_module_open') && defined("MCRYPT_RIJNDAEL_128")) {
if(($cle_chiffrement[0] == '.') || ($cle_chiffrement[0] == '/'))
$cle_aes = @file_get_contents($cle_chiffrement);
else
$cle_aes = $cle_chiffrement;
if(($cle_aes !== false) && (strlen($cle_aes) == 16)) {
// Chiffrement en Rijndael 128 bits, en mode CBC
$td = mcrypt_module_open('rijndael-128', '', 'cbc', '');
// On génère un Vecteur d'Initialisation.
$size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
srand();
$iv = mcrypt_create_iv($size, MCRYPT_RAND); // seule la méthode rand est compatible unix ET windows.
// Chiffrement du message
mcrypt_generic_init($td, $cle_aes, $iv);
$mail_text = bin2hex(mcrypt_generic($td, $message));
// Libère le gestionnaire de chiffrement
mcrypt_generic_deinit($td);
// Chiffrement de l'IV en Rijndael 128 bits, en mode ECB. Il fait 128 bits, soit 16 octets, donc 32 caractères hexa.
// Message = IV chiffré en ECB + texte chiffré en CBC avec cet IV.
$td = mcrypt_module_open('rijndael-128', '', 'ecb', '');
mcrypt_generic_init($td, $cle_aes, '0123456789012345'); // iv inutilisé
$mail_text = bin2hex(mcrypt_generic($td, $iv)) . $mail_text;
mcrypt_generic_deinit($td);
// Découpe le message en
$mail_text = "---------- texte chiffré ----------\n" .
preg_replace("/.{64}/", "$0\n", $mail_text) .
"\n------- fin du texte chiffré ------\n\n" .
"Pour décoder ce message, il suffit d'utiliser (hors-ligne) la page http://aspirine.org/aesdecode.html\n";
}
else
$mail_text = "Impossible de chiffrer le mail, la clé de chiffrement " .
(($cle_aes === false) ? "est introuvable" : "fait ".strlen($cle_aes)." caractères au lieu de 16") . "\n$mail_text";
}
else
$mail_text = "Impossible de chiffrer le mail, la librairie mcrypt (2.4 minimum) n'est pas disponible.\n" . $mail_text;
}
/* _____________________________
* ___/ Vérification de mise à jour \_________________________________________________
*/
if($verifier_mises_a_jour) {
$version_info = @file_get_contents($VERSION_INFO_PATH);
if($version_info && preg_match('/<date>([^>]+)<\\/date>/', $version_info, $matches)) {
$new_date = $matches[1];
if($new_date > $VERSION_DATE)
$mail_text .= "\nVotre version de contact.php date du $VERSION_DATE.\n" .
"Une nouvelle version ($new_date) est disponible sur http://aspirine.org/contact/\n";
}
}
/* _____________________
* ___/ Envoi des résultats \_________________________________________________
*/
if($to)
{
// Option : enregistrement dans la base de données
if (!$erreur && $db_login && $db_password && $db_nom_de_la_table && count($db_champs_a_enregistrer))
$erreur .= enregistre_bdd();
// On ajoute un en-tête du type "Envoyé le lundi 10 février 2007 à 15h03 par
joe@saloon.fr"
setlocale (LC_TIME, 'fr_FR');
$entete = "Envoyé le ".strftime("%A %d %B %Y à %Hh%M")." par $from\n$horizontal_rule\n";
$message = $entete . $message;
$mail_text = $entete . $mail_text;
$message_html = $entete . $message_html;
$message_html = str_replace("\n", "<br />\n", $message_html);
$message_html = preg_replace("/\t+/", "<span style=\"white-space:pre\">$0</span>", $message_html);
// Si on a une adresse de destinataire, on envoie un mail.
$headers = "From: $from\r\nReturn-Path: $from\r\n";
if(!$erreur &&
!mail($to, $sujet, $mail_text, $headers))
$erreur = 'Problème technique lors de l\'envoi du mail. Pourtant il n\'y avait pas de souci dans le formulaire.';
// On utilise include() plutôt que readfile() sinon on ne peut pas
// mettre un fichier php.
if($erreur) {
$erreur = str_replace("\n", "<br />\n", $erreur);
if((substr($page_erreur, -5) == '.html') || (substr($page_erreur, -4) == '.htm'))
print ereg_replace("##+\\s*ERREUR\\s*##+", $erreur, file_get_contents($page_erreur));
else
include($page_erreur);
}
else include($page_ok);
}
else
{
// Pas d'adresse où envoyer le mail, on passe en mode DEBUG
// et on renvoie au navigateur pour affichage direct.
print "<h1>contact.php</h1>\n";
print "<h2>Mode DEBUG</h2>\n";
print "Aucune adresse de destinataire n'est précisée dans le fichier de configuration contact.config.php.\n";
print "<pre>Redirection en cas de succès : <a href=\"$page_ok\">$page_ok</a>\n";
print "Redirection en cas d'erreur : <a href=\"$page_erreur\">$page_erreur</a>\n\n";
if($erreur)
print "<span style=\"color:red; font-weight:bold\">ERREUR : $erreur</span>\n\n";
print "</pre>\nVoici le mail qui pourrait être envoyé (s'il n'y a pas d'erreur) :\n<pre>";
print "De : $from\nSujet : $sujet\n\n$message</pre>";
}
?>
Partager