Un post validator pour gérer les dépendances entre les champs
Aujourd'hui, je partage un post validator qui permet de définir des dépendances entre des champs. Il permet par exemple de définir que le champ postal_code n'est requis que si le champ country vaut FR.
Voici donc cette classe :
<?php
/**
* sfValidatorSchemaDependency allows to define dependency between fields
*/
class sfValidatorSchemaDependency extends sfValidatorSchema
{
/**
* Constructor.
*
* Available options:
*
* * affected_field: The field that is required or not
* * expected_values: A key/value array that represents the fields and their value
* that must be matched to set the affected_field as required
*
* @param string $affected_field The field that is required or not
* @param array $expected_values A key/value array that represents the fields and their value
* that must be matched to set the affected_field as required
* @param array $options An array of options
* @param array $messages An array of error messages
*
* @see sfValidatorBase
*/
public function __construct($affected_field, $expected_values, $options = array(), $messages = array())
{
$this->addOption('affected_field', $affected_field);
$this->addOption('expected_values', $expected_values);
parent::__construct(null, $options, $messages);
}
/**
* @see sfValidatorBase
*/
protected function doClean($values)
{
if (null === $values)
{
$values = array();
}
if (!is_array($values))
{
throw new InvalidArgumentException('You must pass an array parameter to the clean() method');
}
$affected_field = isset($values[$this->getOption('affected_field')]) ? $values[$this->getOption('affected_field')] : null;
$bAllValuesMatched = true;
foreach ($this->getOption('expected_values') as $field => $value)
{
// if a field has not the expected value
if (!isset($values[$field]) || $values[$field] != $value)
{
$bAllValuesMatched = false;
break;
}
}
// if every field has the expected value, and affected_field not defined
if ($bAllValuesMatched && !$affected_field)
{
// "affected_field is required" error
throw new sfValidatorErrorSchema($this, array(
$this->getOption('affected_field') => new sfValidatorError($this, 'required')
));
}
return $values;
}
}
Et voici comment l'utiliser dans le cas de l'exemple de notre introduction :
/**
* @see sfForm::configure()
*/
public function configure()
{
// ...
$this->mergePostValidator(new sfValidatorSchemaDependency('postal_code', array('country' => 'FR')));
}
Ainsi, lors de la validation du formulaire, si le champ country vaut FR et si le champ postal_code est vide, une sfValidatorError de type required va être liée au champ postal_code.
Notez qu'il est possible de définir plusieurs dépendances, en passant plusieurs éléments dans le tableau en second paramètre du constructeur de sfValidatorSchemaDependency.
ValidatorEmailList : valider une liste d’e-mails
Voici un petit validateur pas très compliqué mais pratique, qui valide une liste d'adresses e-mails présentes dans un champ type "textarea".
-
class ValidatorEmailList extends sfValidatorBase
-
{
-
public function doClean($value)
-
{
-
$aValues = explode("\n", $value);
-
$aValues = array_map('trim', $aValues);
-
$aEmails = array();
-
-
$oEmailValidator = new sfValidatorEmail();
-
-
foreach ($aValues as $sEmail)
-
{
-
// ignore empty lines
-
if ($sEmail != '')
-
{
-
// verify email syntax using sfValidatorEmail
-
// sfValidatorError exception will be thrown if invalid
-
$oEmailValidator->clean($sEmail);
-
$aEmails[] = $sEmail;
-
}
-
}
-
-
return $aEmails;
-
}
-
}
Ça me fait penser qu'on pourrait améliorer ça en passant le validateur (ici "new sfValidatorEmail()") en option de ValidatorEmailList pour pouvoir valider des listes de ce qu'on veut en fait... Enfin je vous laisse le faire !
Projet sfAntiBruteForcePlugin
Récemment j'ai créé une petite application web pour jouer un peu avec les API de twitter, qui est sécurisée par un classique login / mot de passe : sécurité basique. Je me suis rapidement dit qu'il faudrait ajouter une sécurité pour parer aux attaques de type "brute force". Pour rappel, ce type d'attaque consiste à tenter un maximum de couples login / mot de passe sur un formulaire en espérant trouver des identifiants qui fonctionnent (plus d'info). J'ai rapidement cherché quelques informations à ce sujet et espéré trouvé un plugin symfony tout fait (on peut espérer non ?). Résultat : pas de plugin, mais quelques informations intéressantes qui m'ont donné envie de me lancer dans le développement du fameux plugin que je cherchais ! Je vais donc recenser ces informations ici, faire une sorte de cahier des charges collaboratif pour mon plugin, et j'espère bien que vous allez m'y aider !
La plupart des informations intéressantes que j'ai récoltées proviennent de ce tutoriel : un anti brute-force léger et rapide.
Le principe : empêcher un utilisateur et surtout un robot de tenter une infinité de couples login / mot de passe sur une page d'authentification donnée. Pour cela, on doit compter les tentatives erronées des utilisateurs, et les empêcher à partir d'un certain seuil de tentatives par unité de temps. Voici les questions qui en découlent.
Sur quoi se baser pour compter les tentatives ?
Sur l'IP du client ? Mauvaise idée, cette donnée n'est pas fiable, la plupart des hackers sauront la modifier et pourront donc faire une infinité de tentatives.
Sur les sessions ? Non plus, il suffit d'effacer le cookie et c'est reparti pour d'autres tentatives...
Sur le login utilisé pour l'authentification ? Ce n'est pas une solution parfaite, mais c'est la meilleure que j'ai trouvé à ce jour. L'inconvénient principal est que n'importe qui peut bloquer le compte de quelqu'un s'il connait son login...
Si vous avez d'autres idées, je suis preneur !
Comment stocker le nombre de tentatives effectuées ?
En base de données ? Ca semble être la solution la plus logique. Néanmoins, certains projets ne fonctionnent pas avec une base de données (et c'est mon cas ici !). Ce serait un peu intrusif de devoir créer une base pour ça, de charger un ORM etc.
Dans des fichiers ? C'est la solution qu'a choisi l'article ci-dessus. Niveau rapidité, charge etc., il faudrait faire un comparatif avec l'utilisation d'un ORM pour savoir qui est le meilleur. Cette solution est moins envahissante dans la mesure où elle fonctionnerait sur tous les projets symfony (à ma connaissance).
Si vous avez d'autres idées...
Où intervenir au niveau du code symfony ?
A la base, j'avais pensé qu'il faudrait faire un filtre pour contrôler les accès en amont dans l'application. Le problème est que, si je pars sur la solution d'utiliser le login pour compter les tentatives, j'ai besoin de ce login pour incrémenter son compteur de tentatives en cas d'échec. J'ai donc également besoin de savoir si la tentative d'autentification est un échec ou non. Pour cela, j'ai donc besoin d'internvenir au niveau du contrôleur, dans l'action qui gère l'authentification. J'imagine que le développeur devra ajouter un appel de ce genre lorsqu'une tentative d'authentification échouera :
-
sfAntiBruteForceManager::notifyFailedAuthentication($identifier);
Cette méthode aura pour but d'incrémenter le compteur d'échec pour cet utilisateur.
Il faudrait également, avant la tentative d'authentification, vérifier que l'utilisateur qui s'apprête à s'authentifier a le droit de le faire :
-
if (sfAntiBruteForceManager::canTryAuthentication($identifier))
-
{
-
// ...
-
}
Faut-il s'intégrer au plugin sfGuard ?
Dans un second temps peut-être, on va pas mettre la charrue avant les bœufs ! Mais bon, à creuser.
Voilà pour les premières pistes pour la réalisation de ce petit plugin qui me semblerait très utile pour tout développeur sensible à la sécurité de son application (aka tout bon développeur !). J'attends vos remarques / suggestions / idées avec impatience !
PS : suivez les évolutions sur la page officielle de sfAntiBruteForcePlugin.
sfProjectAnalyserPlugin : est-ce que ton code est beau ?
Si tu crois être un boss de Symfony, si tu penses que ton code respecte les standards les plus stricts, si tu es sûr que toutes tes méthodes sont documentées, si tu ne fais jamais de sfContext::getInstance(), alors tu peux te mesurer au terrible sfProjectAnalyserPlugin !
Ce plugin permet en effet de mesurer la qualité et la volumétrie d'un projet. Il peut compter le nombre d'applications, de modules, d'actions d'un projet, vérifier la longueur des fonctions et des templates, lève des alertes dès qu'il repère des choses anormales, et bien d'autres choses encore.
Ce n'est pas juste un jouet pour sortir quelques statistiques sur son travail. Tout son intérêt réside dans son utilisation en tant qu'outil d'intégration continue. Imaginez que vous commenciez un tout nouveau projet Symfony : si chaque matin, après votre nesquik, vous lancez l'analyse de votre projet et si vous suivez les précieux conseils donnés dans la page de résultat, vous aurez au final un code beau, propre et facile à maintenir ! Vous serez alors le roi du pétrole et vous pourrez faire un énorme don aux gentils développeurs de ce plugin, sans qui vous ne seriez rien !
Et si vous ne le trouvez pas assez strict (ou trop strict, honte sur vous :)), pas de problème ! Il est entièrement paramétrable via son fichier YAML.
L'analyse se lance en une simple commande Symfony :
symfony project:analyse --application="frontend" --env="dev" > analysis.html
Elle génère un rapport sous la forme d'un simple fichier HTML que vous pouvez bien sûr visualiser dans votre navigateur favori :
Pour davantage d'informations, rendez-vous sur la page du plugin sfProjectAnalyserPlugin.
PS : vous l'aurez peut-être deviné, je fais partie des développeurs de ce magnifique plugin ! Même si ma contribution n'a été que très modeste pour le moment... (je vais me rattraper Loïc !)
Symfony et Doctrine : connexions multiples
Bon, ok, pour un premier article, je ne me lance pas dans un concept super compliqué et inconnu de tous, certes. Considérons ça comme un rodage ! Je vais donc tâcher de faire un petit article sur ma récente première utilisation de connexions multiples à des bases de données, avec Symfony 1.4 et Doctrine 1.2.
A la base, mon projet se connectait à une seule base, la configuration classique, la routine. Un beau jour, j'ai eu besoin d'aller mettre à jour la valeur d'un champ dans une autre base de données. Deux possibilités se présentaient :
- utiliser les infâmes fonctions mysql_*() comme dans les années 80 (bon, c'est peut-être pas si vieux, mais presque)
- découvrir le monde merveilleux des connexions Doctrine multiples
Ne m'étant jamais penché sur ce point, j'avais peur que la seconde option soit trop lourde. Mais conseillé par mes collaborateurs d'SQL Technologies, la tâche s'est avérée finalement très simple.
Premièrement, il a fallu modifier le fichier config/databases.yml afin qu'il prenne en compte la seconde connexion :
-
all:
-
connection1:
-
class: sfDoctrineDatabase
-
param:
-
...
-
connection2:
-
class: sfDoctrineDatabase
-
param:
-
...
J'ai ensuite introspecté cette seconde base pour que Symfony génère son schéma (vous pouvez le faire à la main si vous êtes courageux !) :
-
symfony doctrine:build-schema
Et là, magie : dans le fichier config/doctrine/schema.yml, on voit désormais que chaque table est liée à une des deux connexions grâce au paramètre... "connection" !
-
MyTable1:
-
connection: connection1
-
columns:
-
...
-
MyTable2:
-
connection: connection2
-
columns:
-
...
Et lorsque l'on reconstruit le modèle, la magie opère ! Dans les fichiers Base*.class.php du modèle, une nouvelle ligne est apparue, permettant "d'attacher" chaque classe du modèle à sa connexion :
-
// Connection Component Binding
-
Doctrine_Manager::getInstance()->bindComponent('MyTable1', 'connection1');
Et c'est tout ! On peut alors utiliser nos objets presque sans se soucier d'où proviennent et où vont les données.
-
// stupid actions
-
$oObject1 = Doctrine::getTable('MyTable1')->find(7);
-
$oObject2 = Doctrine::getTable('MyTable2')->find(42);
-
$oObject2->setName($oObject1->getTitle())->save();
Vive Doctrine, et vive Symfony !

