[poo] exemple alternative à un captcha

Alternative au captcha

Description

Le tuto présente une alternative au captcha lors de l'utilisation d'un formulaire de contact (applicable également pour un livre d'or).

Avant-propos

Suite à quelques messages sur le forum concernant des problèmes liés aux captchas, je propose une petite alternative (déjà existante) à ceux-ci. L'alternative que nous allons voir est dans le cadre d'un formulaire de contact (applicable au livre d'or également). Il s'agit de la confirmation par email avant l'envoi définitif du message dans notre boîte mail ; un peu comme pour confirmer un mot de passe.
Alors, pour changer nous allons faire de l'objet avec du MVC, parce qu'il y en a un peu marre de voir/faire QUE du procédural avec des codes mélangés partout dans les applications comme ceux qui traînent sur le forum notamment. Attention ! Je n'explique pas l'objet, ce n'est pas le but ici. D'ailleurs je n'explique pas grand-chose, partant du principe que le tout a été très simplifié pour un résultat optimum. La compréhension n'est donc pas difficile.

Niveau requis : Novice et initié

Pré-requis

Il faut avoir une base d'objet et de MVC embarquée dans le cerveau.

Avantages

  • Pas de code illisible à taper (même pour ceux qui voient correctement)
  • L'envoi doit être confirmer.
  • On pourrait blacklisté les adresses emails indésirables au lieu des ips.

Inconvénients

L'internaute doit « sortir » de la page, et effectuer des opérations supplémentaires (ouvrir sa boîte mail, confirmer l'envoi)

Notre site

On supposera notre site web sous l'intitulé `MonSite.com'
On bossera dans 1,2,3...6 dossiers, qui sont :

Dossier tools/

Ce dossier contiendra la classe phpmailer. Cette classe permet d'envoyer des emails en toute simplicité. On se servira donc d'une solution existante et performante. Je ne m'attarderais pas sur la façon de l'utiliser ! De toute façon vous le verrez (de visu) par vous-même au cours de ce tuto.

Dossier config/

On va créer un fichier, qu'on nommera : config.inc.php
Ce fichier est la pièce maîtresse. C'est dedans qu'on initialisera notre site.

/* Initialisation du site */
define('NAME_SITE', 'Mon Site');
define('URL_SITE', 'http://www.monsite.com');
define('MAIL_POSTMASTER', 'postmaster@monsite.com');
define('MAIL_CONTACT', 'contact@monsite.com');

/* Connexion MYSQL */
define('DB_SERVEUR', ' '); // Nom du serveur
define('DB_BASE', ' '); // Nom de la base
define('DB_USER', ' '); // Nom de l'utilisateur de la base
define('DB_PASSWD', ' '); // Mot de passe pour accéder à la base
define('DB_DSN', 'mysql:host='.DB_SERVEUR.';dbname='.DB_BASE); // Driver PDO pour l'accès à la bdd

/* Définitions des dossiers */
define('CONTROL_DIR', dirname(__FILE__).'/../controllers/');
define('CLASS_DIR', dirname(__FILE__).'/../classes/');
define('THEME_DIR', dirname(__FILE__).'/../themes/');
define('MAILS_DIR', dirname(__FILE__).'/../mails/');
define('PHPMAILER_DIR', dirname(__FILE__).'/../tools/phpmailer/');

/* Autoload */
function __autoload($class) {
    if (!class_exists($class, false))
        require_once(CLASS_DIR.$class.'.php');
}

La fonction __autoload() s'occupera de charger nos classes automatiquement.
Le reste c'est du classique.

Dossier mails/

Dans ce dossier nous allons y placer tous les `gabarits' pour nos emails.
Pour les besoins de notre tuto il va en falloir 2 :

Fichier : valid_contact.txt

Le Lorem Ipsum est simplement du faux texte employé dans la composition
et la mise en page avant impression.
Le Lorem Ipsum est le faux texte standard de l'imprimerie depuis les années 1500,
quand un peintre anonyme assembla ensemble des morceaux de texte pour réaliser
un livre spécimen de polices de texte.

Copier ce lien dans votre navigateur pour confirmer :
{url_active_mail}

L'équipe {nom_site}

Ce fichier est celui qui envoie un lien de confirmation, pour vérifier que c'est bien un humain qui a pris contact avec nous.

Fichier : send_contact.txt

/*********************************************************/
Vous avez reçu un message depuis {nom_site}
/*********************************************************/

{nom} {prenom} a écrit :

{message}

/*********************************************************/
Fin message
/*********************************************************/

Ce fichier contiendra le message que l'internaute a voulu nous envoyer. En gros c'est l'email envoyé par l'internaute.

Dossier themes/

Fichier : contact.tpl

Il s'agit tout simplement du formulaire de contact.

<span style="color:#FF0000;">
    {errors}
    {statement_contact}
</span><br/>

<form action="contact.php" method="post">
    
    Nom : <input type="text" name="nom" value="{form_nom}"/>
    <br/><br/>

    Prénom : <input type="text" name="prenom" value="{form_prenom}"/>
    <br/><br/>

    Votre adresse mail : <input type="text" name="expediteur" value="{form_expediteur}"/>
    <br/><br/>

    Sujet du message : <select name="sujet">
        <option value="Contact" {form_sujet_option_contact}>Contact</option>
        <option value="Partenariat" {form_sujet_option_partenariat}>Partenariat</option>
        <option value="Autre" {form_sujet_option_autre}>Autre</option>
    </select><br/><br/>

    Message : <textarea name="message" rows="15" cols="30">
                    {form_message}
                </textarea>
                <br/><br/>
    
    <input type="submit" value="Envoyer"/>

</form>

Dossier classes/

Le nom de nos classes porte le nom de nos fichiers. Donc quand je ferais référence à la classe `Kapout', le nom du fichier est également Kapout ; ceci fait partie intégrante du chargement automatique des classes par __autoload().

Classe Database

Elle contient les méthodes qui permettent d'effectuer la connexion avec la base de données ainsi que l'exécution des requêtes.

abstract class Database
{
    private static $_instance = null;
    static public function instance()
    {
        if(is_null(self::$_instance)) {
            try {
                self::$_instance = new PDO(DB_DSN, DB_USER, DB_PASSWD);
                self::$_instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            }
            catch(Exception $e) {
                die($e->getMessage());
            }
        }
        return self::$_instance;
    }

static public function query($query, $args=array(), $mode=FALSE)
{
    $r = self::instance()->prepare($query);
    if(sizeof($args))
        $r->execute($args);
    else
        $r->execute();
    return (!$mode)? $r->fetch(PDO::FETCH_ASSOC) : $r->fetchAll(PDO::FETCH_ASSOC);
}

static public function exec($query, $args=array())
{
    $r = self::instance()->prepare($query);
    if(sizeof($args))
        return $r->execute($args);
    else
        return $r->execute();
}
  • Méthode instance() : Effectue la connexion avec la base de données et affiche un message d'erreur en cas d'impossibilité.
  • Méthode query() : Prend 3 arguments dont 2 facultatifs :
    • $query est la requête SQL avec marqueur '?',
    • $args est la/les valeur(s) de remplacements des marqueurs,
    • $mode désigne si on veut récupérer toutes les lignes ou une seule du jeu de résultats. Uniquement utilisé pour les SELECT
  • Méthode exec() : Prend 2 arguments dont 1 facultatif. Les mêmes que pour la méthode query(). Utilisés pour les autres types de requêtes.

Classe Mail

Gère l'envoie des emails à l'aide de la classe phpmailer, et les gabarits d'emails.

class Mail
{
    function sendMessage($to, $from, $fromName, $subject, $mailName, $mailVars, $toName=NULL)
    {
        require(PHPMAILER_DIR.'class.phpmailer.php');

        if(empty($to) OR empty($from) OR empty($fromName) OR empty($subject) OR empty($mailName)) 
            die('Erreur: les paramètres mail sont corrompus');

        $mail = new PHPMailer;
        $mail->SetLangage('fr');
        $mail->IsMail();
        $mail->From = $from;
        $mail->FromName = $formName;

        if($toName AND is_array($toName)) 
            die('Erreur: les paramètres mail sont corrompus');

        $mail->AddAddress($to, $toName);
        $mail->Subject = $subject;

        if(!file_exists(MAIL_DIR.$mailName.'.txt'))
            die('Erreur - Le template e-mail suivant est manquant : '.MAILS_DIR.$mailName.'.txt');
        else
            $bodyMail = strip_tags(html_entity_decode(file_get_contents(MAILS_DIR.$mailName.'.txt'), NULL, 'utf-8'));

        $mail->IsHTML(FALSE);

        foreach($mailVars as $key=> $value)
            $bodyMail = preg_replace('/\{'.$keys.'\}/', $value, $bodyMail);

        $mail->Body = $bodyMail;
        return ($mail->Send()) ? TRUE : FALSE;
    }
}

Prend 5 arguments dont 1 facultatif :

  • $to = destinataire du mail
  • $from = expéditeur
  • $fromName = nom de l'expéditeur
  • $subject = sujet du mail
  • $mailName = nom de l'email-template à charger
  • $mailVars = valeurs à remplacer dans le template {X}
  • $toName = nom du destinataire

Classe Template

Ce n'est pas du lourd comme moteur de template ! Mais il fera ce qu'on lui dit pour les besoins du tuto.

P.S : il ne gère pas les boucles...j'ai prévenu !!

class Template
{
    public function setBundle($file, $vars=array())
    {
        $contentTemplate = ' ';

        if(sizeof($file)) {
            foreach($file as $value)
                $contentTemplate .= Template::getContentFile(THEME_DIR.$value);
        }
        else
            $contentTemplate .= Template::getContentFile($file);

        if(sizeof($vars)) {
            foreach($vars as $keys=> $value)
                $contentTemplate = preg_replace('/\{'.$key.'\}/', $value, $contentTemplate);
        }
        return $contentTemplate;
    }

    static public function getContentFile($file)
    {
        if(!file_exists($file))
            die('Erreur - Le template suivant est manquant : '.$file);
        return file_get_contents($file);
    }
}
  • setBundle() prend 2 arguments dont 1 facultatif :
    • $file = noms des templates à charger « array(`head.tpl', `foot.tpl') ; »

- $vars = valeurs à remplacer dans le template {X}

  • getContentFile() prend un argument, le fichier à charger

Classe Contact

Enregistre dans la base de données le message que l'internaute tente de nous envoyer :

static public function saveMessage($nom, $prenom, $expediteur, $sujet, $message)
{
    Contact::cleannerMessage()
    $skey = Contact::returnKey($message.mktime());

    $mailVars = array('prenom' => $prenom, 'url_active_mail'=> URL_SITE.'/contact.php?idkey='.$skey, 'nom_site') => NAME_SITE);
    if(Mail::sendMessage($expediteur, MAIL_POSTMASTER, NAME_SITE, $sujet, 'valid_contact', $mailVars, $nom.' '.$prenom)) {
        $insert = Database::exec('INSERT INTO contact (skey, nom, prenom, expediteur, sujet, message, date_contact) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAND())', array($key, $nom, $prenom, $expediteur, $sujet, trim($message)));
        if(Database::instance()->lastInsertId())
            return true;
    }
    return false;
}

Confirme qu'il s'agit bien d'un humain à l'aide de l'id de confirmation contenu dans le lien de confirmation fournit en url :

static public function validMessage($skey)
{
   $row = Database::query('SELECT skey, nom, prenom, expediteur, sujet, message FROM contact WHERE skey=?', array($skey));
    if($row) {
        $mailVars = array(
            'prenom' => $row['prenom'],
            'nom' => $row['nom'],
            'message' => htmlspecialchars($row['message']."\n", ENT_NOQUOTES, 'UTF-8'),
            'nom_site' => NAME_SITE);
        if(Mail::sendMessage(MAIL_CONTACT, $row['expediteur'], NAME_SITE, $row['sujet'], 'send_contact', $mailVars, $row['nom'].' '.$row['prenom'])) {
            Contact::cleannerMessage($skey);
            return true;
        }
    }
    return false
}
static public function cleannerMessage($key=FALSE)
{
    if(!$key) {
        return Database::exec('DELETE FROM contact WHERE DATEDIFF(CURRENT_TIMESTAMP(), date_contact)>2');
    }
    else {
        return Database::exec('DELETE FROM contact WHERE skey=?', array($key));
    }
}

static public function returnkey($str)
{
    return sha1($str);
}
  • cleannerMessage() effectue le nettoyage de la base pour les messages vieux de plus de 2 jours ou supprime un message précédemment confirmer.
  • returnkey() renvoie un code id pour la confirmation

Dossier controllers/

Fichier contact.php

Ce fichier effectue les opérations de contrôle, pour la validation et l'envoi du formulaire ainsi que la confirmation du message par l'id.
Ce fichier utilise une classe de gestion pour formulaire nommée « Form », que j'ai développé auparavant et qui est disponible sur 'phpcs'.

try {
    $form = new Form('post');
    $getUrl = new Form('get');

    // On vérifie la validité du champs "nom"
    $form->issetValue('nom', 'Vous devez renseigner votre nom');
    $form->isExtendType('nom', '/[^a-zÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÒÓÔÕÖÙÚÛÜÝ]/i', 'Votre nom doit comporter uniquement des lettres');

    // On vérifie la validité du champs "prenom"
    $form->issetValue('prenom', 'Vous devez renseigner votre prénom');
    $form->isExtendType('prenom', '/[^a-zÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÒÓÔÕÖÙÚÛÜÝ]/i', 'Votre prénom doit comporter uniquement des lettres');

    // On vérifie la validité du champs "expediteur"
    $form->issetValue('expediteur', 'Vous devez renseigner votre adresse email');
    $form->isEmail('expediteur', 'saisissez une adresse email valide');

    // On vérifie la validité du champs "sujet"
    $form->issetValue('sujet', 'Veuillez indiquer pour quel service le message est adressé');
    $form->compareValue('sujet', $form->sujet, array('Contact','Partenariat','Autre'), ' ', 'Veuillez indiquer un sujet parmi ceux fournis');

    // On vérifie la validité du champs "message"
    $form->compareValue('message', strlen(trim($form->message)), 1, '<', 'Veuillez saisir votre message');
}
catch(Exception $e) {
    die($e->getMessage());
}
$varTemplate['statement_contact'] = ' ';
// Si le formulaire est OK
if($form->sendForm()) {
    if(Contact::saveMessage($form->nom, $form->prenom, $form->expediteur, $form->sujet, $form->message)) {
        $varTemplate['statement_contact'] = 'Un email vient de vous être envoyé pour confirmer l\'envoi de votre message';
        unset($form->nom, $form->prenom, $form->expediteur, $form->sujet, $form->message);
    }
    else
        $varTemplate['statement_contact'] = 'Veuillez tenter à nouveau';
}

// On récupère les erreurs du formulaire
$varTemplate['errors'] = $form->getError();

// On récupère les infos du formulaire
$varTemplate['form_nom'] = $form->nom;
$varTemplate['form_prenom'] = $form->prenom;
$varTemplate['form_expediteur'] = $form->expediteur;
$varTemplate['form_sujet_option_contact'] = ($form->sujet=='Contact') ? 'selected="selected"' : ' ';
$varTemplate['form_sujet_option_partenariat'] = ($form->sujet=='Partenariat') ? 'selected="selected"' : ' ';
$varTemplate['form_sujet_option_autre'] = ($form->sujet=='Autre') ? 'selected="selected"' : ' ';
$varTemplate['form_message'] = $form->message;

// Procédure pour la confirmation de l'envoi du message
if($getUrl->idkey) {
    if(Contact::validMessage($getUrl->idkey))
        $varTemplate['statement_contact'] = 'Votre message nous a bien été communiqué';
    else
        $varTemplate['statement_contact'] = 'Il est impossible de confirmer votre message';
}

Et pour finir...

Dossier racine/

Fichier contact.php

include(dirname(__FILE__).'/config/config.inc.php');

$varTemplate = array();

include(CONTROL_DIR.'contact.php');

$template = new Template();
$loadTemplate = array('header.tpl','contact.tpl','footer.tpl');
echo $template->setBundle($loadTemplate, $varTemplate);

C'est le fichier qui sera appelé par l'internaute : http://www.MonSite.com/contact.php.
C'est lui qui imbriquera les fichiers pour la page contact.php

Screenshots

Page contact.php initialisée

Aucuns champs remplis

Champs invalides

Envoi du mail de confirmation

Tentative de confirmation un id erroné

Id validé

Mot de la fin

Quand on regarde le tout on se dit c'est beaucoup de lignes pour une seule petite chose : OUI !
On aurait pu faire le tout avec moins de lignes de codes, moins de dossiers, moins de fichiers. Mais quand on fait de l'objet (du bon !) avec du MVC ce n'est pas possible de faire petit. Mais on y gagne en lisibilité !! L'application est propre et on sait qui fait quoi.
Je tiens à préciser que chaque ligne de code a été spécifiquement créée pour ce tuto, donc certaines choses ne sont pas forcément réutilisables pour d'autre cas de figure. Alors ce n'est même pas la peine de me dire « ben j'ai fait ci, j'ai fait ça et ça marche pas quand je veux... » Je ne répondrai pas !

Outils externes utilisés dans le tuto

Liens de téléchargement

Lien pour la télécharger la classe PhpMailer : http://phpmailer.worxware.com/
Lien pour la télécharger ma classe Form :
http://www.phpcs.com/codes/CLASSES-VALIDATION-FORMULAIRE_52367.aspx

Autres informations

  • Y a-t-il une page de test ? Non et il n'y en aura pas ! L'appli a été testée par mes soins tout est ok donc fonctionnel et puis je n'en ai pas envie !
  • Je le redis : « Aucune demande d'aide ne sera traitée ici, il y a le forum pour ça ! »
Ce document intitulé « [poo] exemple alternative à un captcha » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous