La gestion des droits et utilisateur dans Symfony


Dans ce nouveau chapitre, nous allons découvrir comment Symfony gère les sessions utilisateurs, comment restreindre les accès à certaines actions suivant certains droits et continuer notre module membre par la même occasion.






Insertion des membres dans la base de données


Tout d'abord, commençons par terminer la méthode de création d'utilisateur afin d'insérer les membres qui s'inscrivent dans la base de données.

Dans la classe actions du module membre, nous allons modifier la méthode processForm afin de pouvoir insérer le membre créé dans la BDD.

Code php:
  protected function processForm(sfWebRequest $request, sfForm $form)
  {
    $form->bind($request->getParameter($form->getName()));
	
    if ($form->isValid())
    {
      $membres = $form->save();
	  
	  // On set les valeurs par défaut
	  
	  $membres->setIpInscription($_SERVER['REMOTE_ADDR']);
	  $membres->setDateInscription(date('Y-m-d H:i:s'));
	  $membres->setLvl(1); // 1 = Membre / 9 = Admin
	  $membres->setEtat(0); // 0 = Non activé / 1 = Actif / 2 = Banni
	  $membres->save();

      $this->redirect('membre/login?login='.$membres->getLogin());
    }
  }


Une fois le membre inséré avec la méthode save() du formulaire, nous le redirigeons sur la page d'identification.
Toutefois, si vous avez bien suivi, a aucun moment nous cryptons le mot de passe en MD5, il sera sauvegardé en clair si nous suivons cette méthode.
Il faut donc que l'on ajoute une nouvelle méthode à notre classe MembresForm.class.php:

Code php:
    public function updatePassColumn($value)
	{
		if(!empty($value)) { $value = md5($value); }
	
		return $value;
	}


Cette méthode updatePassColumn sera automatiquement appelée après la validation du formulaire, et prendra en paramètre la valeur du mot de passe entré, pour le retourner chiffré.
Il en va de même pour toutes les méthodes "magiques" du type updateXXXXXColumn.
Ou XXXXXX représente le nom d'un paramètre de l'objet.


Envoi d'email avec Symfony: Vérification de l'email


Nous n'avons pas encore testé la validité du compte. Afin d'éviter d'avoir une base de données remplie d'email invalides, et de s'assurer de la possibilité de contact du membre en cas de problèmes (droits sur images, abus etc.), nous allons créer un système de validation de l'email.

Commençons par installer le nécessaire pour envoyer des emails avec Symfony.

Il est conseillé d'utiliser le plugin SwiftMailer qui est d'ailleurs géré par Sensio Labs à présent.

Installation de Swift Mailer



Il suffit de suivre le guide officiel: http://www.symfony-project.org/cookbook/1_2/fr/email



Une fois que vous l'avez installé, nous allons pouvoir créer notre email de confirmation.
Dans un premier temps, créons un module mail dans lequel nous stockerons tous nos templates d'emails etc.

symfony generate:module frontend mail


Nous allons créer deux templates dans le module mail:

_confirmationCreationText.php

Code php:
<?php echo __('Welcome'); ?> <?php echo $login; ?> !

<?php echo __('Your account has been well created on WatchMyDesk website.'); ?>

<?php echo __('However, you need to confirm your email address by clicking on the following link:'); ?> <?php echo url_for('membre/validate?login='.$login.'&code='.$code); ?>

<?php echo __('Kind regards,'); ?>

<?php echo __('The Watch My Desk team.'); ?>



et _confirmationCreationHtml.php

Code php:
<p><?php echo __('Welcome'); ?> <?php echo $login; ?> !</p>

<p><?php echo __('Your account has been well created on WatchMyDesk website.'); ?></p>

<p><?php echo __('However, you need to confirm your email address by clicking on the following link:'); ?> <a href="<?php echo url_for('membre/validate?login='.$login.'&code='.$code); ?>"><?php echo __('Validate my account'); ?></a></p>

<p><?php echo __('Kind regards,'); ?></p>

<p><?php echo __('The Watch My Desk team.'); ?></p>



Vous l'aurez remarqué, nous n'avons pas de champ dédié au code de validation, nous allons donc en générer un avec un algorithme basé sur les données du membre:

Code php:
$this->code = substr(md5($membres->getIpInscription().$membres->getLogin().$membres->getDateInscription()),0, 8);


Un peu bourrin je vous l'accorde, mais au moins ça marche !

Envoyer un email avec Swift Mailer dans Symfony



Code php:
protected function processForm(sfWebRequest $request, sfForm $form)
  {
    $form->bind($request->getParameter($form->getName()));
	
    if ($form->isValid())
    {
      $membres = $form->save();
	  
	  // On set les valeurs par défaut
	  
	  $membres->setIpInscription($_SERVER['REMOTE_ADDR']);
	  $membres->setDateInscription(date('Y-m-d H:i:s'));
	  $membres->setLvl(1); // 1 = Membre / 9 = Admin
	  $membres->setEtat(0); // 0 = Non activé / 1 = Actif / 2 = Banni
	  $membres->save();
	  
	   // On envoi le mail de confirmation
        try
        {
            // Create the mailer and message objects
            $mailer = new Swift( new Swift_Connection_NativeMail());
            $message = new Swift_Message("Watch My Desk: Confirmation");

           // Render message parts
            $mailContext = array ("code" => substr( md5( $membres->getIpInscription() . $membres->getLogin() . $membres->getDateInscription() ),0, 8), "login" => $membres->getLogin());
            $message->attach( new Swift_Message_Part($this->getPartial('mail/confirmationCreationHtml', $mailContext), 'text/html'));
            $message->attach( new Swift_Message_Part($this->getPartial('mail/confirmationCreationText', $mailContext), 'text/plain'));

            // Send
            $mailer->send($message, $membres->getEmail(), sfConfig::get('app_site_emailnoreply'));
            $mailer->disconnect();
			  
        }
        catch(Exception $e)
        {
            // Il y'a des erreurs ... Echec de l'envoi: A traiter ...
            $mailer->disconnect();
	}
		
      	$this->redirect('membre/login?login='.$membres->getLogin());
    }
  }


Si vous avez lu le code, vous pouvez voir que l'on va utiliser un paramètre de configuration app_site_emailnoreply, que l'on va renseigner dans notre fichier de configuration app.yml pour indiquer notre email d'envoi des emails.

Code:
  site:
    emailnoreply:    [email protected]


N'oubliez pas de supprimer le cache pour que la valeur soit bien prise en compte dans la version de prod.

Après l'envoi de notre email, nous sommes redirigés sur la page de login.



L'objet User dans Symfony


Plus tôt dans le tutorial, nous avons brièvement vu comment récupérer l'objet utilisateur géré par Symfony.

L'objet user permet de gérer les sessions de chaque utilisateur.



Il permet aussi de faire interface avec des plugins créés spécialement pour gérer le module membre comme http://www.symfony-project.org/plugins/sfGuardPlugin ou http://www.symfony-project.org/plugins/sfDoctrineGuardPlugin.

Je n'ai pas utilisé ces plugins dans le tutorial afin de mieux vous faire comprendre tout le processus de développement sous Symfony, mais je vous conseille vivement d'en utiliser un afin de gagner en temps et fonctionnalités.

L'objet user est basé sur la classe apps/frontend/lib/myUser.class.php qui étend dans notre cas (Sans plugin) la classe sfBasicSecurityUser.

Pour récupérer l'objet user:
  • Depuis un template, utiliser: $sf_user
  • Depuis un fichier actions, components, utiliser: $this->getUser()


Une fois que l'on a récupérer l'objet, nous allons pouvoir:

  • Récupérer ou attribuer des paramètres en sessions avec les méthodes getAttribute('nom') et setAttribute('nom')

  • Gérer les droits/niveaux de l'utilisateur (membre, admin, rédacteur etc) avec les méthodes addCredential('niveau'), hasCredential('niveau'), removeCredential('nom')...

  • Gérer l'identification de l'utilisateur avec isAuthenticated() et setAuthenticated(true/false).



Mettons en pratique ces fonctions pour mieux comprendre comment elles fonctionnent avec la zone d'identification du site.


La zone d'identification de Watch My Desk


Nous allons maintenant créer le système d'identification des membres: Créez une nouvelle méthode dans le fichier actions du module membre:

Code php:
  public function executeLogin(sfWebRequest $request)
  {
    $this->form = new LoginForm();
  }


Comme pour la méthode d'inscription, nous allons utiliser un formulaire Symfony pour effectuer l'identification.

Créez la classe de formulaire LoginForm.class.php dans le répertoire lib/form/


Code php:
  class LoginForm extends sfForm
{
  public function configure()
  {
	$this->setWidgets(array(
      'login'   => new sfWidgetFormInput(),
      'pass'   => new sfWidgetFormInputPassword()
	));
	
	$this->widgetSchema->setNameFormat('login[%s]');
	
	$this->widgetSchema->setLabels(array(
      'login'   => "Login",
      'pass'   => "Password"
	));
	
	$this->setValidators(array(
      'login'   => new sfValidatorAnd(
	  		array(
				new sfValidatorString(
					array('required' => true, 'min_length' => 3, 'max_length' => 14),
					array(
						'min_length' => "The login is too short. 3 characters minimum.", 
						'max_length' => "The login is too long. 14 characters maximum", 
					)
				),
				new sfValidatorRegex(
					array('pattern' => '/^[a-zA-Z0-9-]+$/')
				)
			),
			array(),
			array(
				'required' => "The login is required",
				'invalid' => "The nickname could not contain special characters."	
			)
	  ),
      'password'   => new sfValidatorString(
			array('required' => true, 'min_length' => 6, 'max_length' => 20), 
			array(
				'min_length' => "The password is too short. 6 characters minimum.", 
				'max_length' => "The password is too long. 20 characters maximum", 
				'required' => "Password is required", 
				'invalid' => "The password must contains between 6 and 20 characters"
			)
	  ),
	));
	
  }
}


J'accélère sur le développement des formulaires, maintenant que vous savez comment ils fonctionnent.

Vous constaterez que le code avec l'inscription est quasi identique, voilà l'un des avantages des classes formulaire, elles sont très facilement réutilisables.

Créons aussi le template loginSuccess.php qui va avec l'action:

Code php:
<h2><?php echo __('Log in the website'); ?></h2>

<form action="<?php echo url_for('checkLogin'); ?>" method="post">
	<table>
	<?php echo $form; ?>
	<tr>
		<td colspan="2">
			<a href="<?php echo url_for('passLost'); ?>"><?php echo __('Password lost ?'); ?></a> - 
			<a href="<?php echo url_for('register'); ?>"><?php echo __('Create your account'); ?></a>
		</td>
	</tr>
	<tr>
		<td colspan="2"><input type='submit' value="<?php echo __('Log in'); ?>" /></td>
	</tr>
	</table>
</form>


Jusque là rien de bien sorcier, nous affichons simplement le formulaire avec les liens d'inscription et de récupération du mot de passe.
Lorsque nous soumettrons le formulaire, l'action doLogin sera appelée:

Code php:
	public function executeDoLogin(sfWebRequest $request)
	{
		$this->forward404Unless($request->isMethod('post'));
		
		$this->form = new LoginForm();
		
		$this->form->bind($request->getParameter('login'));
		
	    if ($this->form->isValid())
	    {
			// On set la session de l'utilisateur.
		}
		else {
			$this->setTemplate("login");
		}
		
	}


Je n'ai perdu personne en chemin ? N'hésitez pas à réagir sur le forum si vous souhaitez obtenir plus d'informations sur un point qui ne vous semble pas clair.

Et ajoutez les routes suivantes nécessaires pour afficher la page de login:

apps/frontend/config/routing.yml

Code:
checkLogin:
  url: /:sf_culture/check_login.html
  param: { module: membre, action: doLogin }
  requirements:
    sf_culture: (?:fr|en)
    
passLost:
  url: /:sf_culture/password_lost.html
  param: { module: membre, action: passPerdu }
  requirements:
    sf_culture: (?:fr|en)


Si on revient un peu en arrière sur notre formulaire d'identification.
Vous pourrez constater que l'on test la valeur des champs, qu'ils sont requis ...
Mais nous n'avons pas utilisé de méthode pour vérifier si l'identification était valide.

De la même manière que nous l'avions fait pour vérifier si le pseudo était disponible à l'inscription, nous allons ici effectuer un test de l'identification.

Voici ce que donne la classe LoginForm une fois les modifications ajoutées:

Code php:
<?php

class LoginForm extends sfForm
{
  public function configure()
  {
	$this->setWidgets(array(
      'login'   => new sfWidgetFormInput(),
      'pass'   => new sfWidgetFormInputPassword()
	));
	
	$this->widgetSchema->setNameFormat('login[%s]');
	
	$this->widgetSchema->setLabels(array(
      'login'   => "Login",
      'pass'   => "Password"
	));
	
	$this->setValidators(array(
      'login'   => new sfValidatorAnd(
	  		array(
				new sfValidatorString(
					array('required' => true, 'min_length' => 3, 'max_length' => 14),
					array(
						'min_length' => "The login is too short. 3 characters minimum.", 
						'max_length' => "The login is too long. 14 characters maximum", 
					)
				),
				new sfValidatorRegex(
					array('pattern' => '/^[a-zA-Z0-9-]+$/')
				)
			),
			array(),
			array(
				'required' => "The login is required",
				'invalid' => "The nickname could not contain special characters."	
			)
	  ),
      'pass'   => new sfValidatorString(
			array('required' => true, 'min_length' => 6, 'max_length' => 20), 
			array(
				'min_length' => "The password is too short. 6 characters minimum.", 
				'max_length' => "The password is too long. 20 characters maximum", 
				'required' => "Password is required", 
				'invalid' => "The password must contains between 6 and 20 characters"
			)
	  ),
	));
	
	$this->validatorSchema->setPostValidator(
	  new sfValidatorCallback(array('callback'=> array($this, 'checkLogin')))
    );
  }
  
  public function checkLogin($validator, $values) {
        
		if (!empty($values['login']) && !empty($values['pass'])) {
			
			$membre = Doctrine::getTable('Membres')->findOneByLogin($values['login']);
			if ($membre) {
				
			    if ($membre->getPass() == md5($values['pass'])) {
			    	// Login correct !
					
					if ($membre->getEtat() == 1) {
																	
						return $values;	
					}
					else {
						if ($membre->getEtat() == 0) {
							throw new sfValidatorError($validator, 'Your account is still disabled. Please click on the confirmation email to activate your account.');
						}
						else {
							throw new sfValidatorError($validator, 'You are bannished from the website.');
						}
					}
					
	                	
	            } else {
	            	// Connexion incorrecte
	                throw new sfValidatorError($validator, 'Log in incorrect');
	            }
			}
			else {
				 throw new sfValidatorError($validator, 'This login does'nt exists');
			}
        }
    }
}


Le validateur callback va faire appel à la méthode checkLogin qui va vérifier si le login est correct ou non.
Nos utilisateurs auront différents états:
  • 0 : Utilisateur non activé
  • 1 : Utilisateur activé
  • 2 : Utilisateur banni


Vous pouvez à présent tester votre formulaire d'identification en changeant l'état de votre compte directement en base de données le temps que l'on fasse la méthode d'activation du compte.

La session de notre utilisateur


Une fois que l'utilisateur est identifié, il faut que nous stockions en session ses paramètres principaux: Id, Login, level, et lui attribuons ses droits.
Code php:
	public function executeDoLogin(sfWebRequest $request)
	{
		$this->forward404Unless($request->isMethod('post'));
		
		$this->form = new LoginForm();
		
		$this->form->bind($request->getParameter('login'));
		
	    if ($this->form->isValid())
	    {
            $membre = Doctrine::getTable('Membres')->findOneByLogin($request->getParameter('login[login]'));
			
            // On set la session de l'utilisateur.
            $this->getUser()->setAuthenticated(true);
            
            // On stocke les infos utiles dans la session utilisateur
            $this->getUser()->setAttribute("id", $membre->getId());
            $this->getUser()->setAttribute("login", $membre->getLogin());
            $this->getUser()->setAttribute("level", $membre->getLvl());
            
            // On set les accès de l'utilisateur
            switch($membre->getLvl()) {
                case 1:
                     $this->getUser()->addCredential("membre");
                break;
                case 9:
                    $this->getUser()->addCredential("admin");
                break;
                default:
                    $this->getUser()->addCredential("membre");
            }

            $this->redirect('localized_homepage');

		}
		else {
			$this->setTemplate("login");
		}
		
	}


Une fois identifié, l'utilisateur est redirigé sur la page d'accueil.
Modifions le layout afin de prendre en compte la session pour changer le contexte des menus:

Changez le menu ainsi:
Code html:
			<p class='boutons_top'>
				<a href="<?php echo url_for('@browse'); ?>" class='bouton'><?php echo __('Browse'); ?></a>
                <?php if (!$sf_user->isAuthenticated()): ?>
				<a href="<?php echo url_for('@share'); ?>" class='bouton'><?php echo __('Your desk'); ?></a>
				<a href="<?php echo url_for('@register'); ?>" class='bouton'><?php echo __('Join now'); ?></a>
				<a href="<?php echo url_for('@login'); ?>" class='bouton'><?php echo __('Log in'); ?></a>
                <?php else: ?>
                <a href="<?php echo url_for('@share'); ?>" class='bouton'><?php echo __('Your desk'); ?></a>
				<a href="<?php echo url_for('@disconnect'); ?>" class='bouton'><?php echo __('Disconnect'); ?></a>
                <?php endif; ?>
			</p>


Créez une route 'disconnect' dans votre fichier routing.yml pour pouvoir tester:

Code:
disconnect:
  url: /:sf_culture/disconnect.html
  param: { module: membre, action: deco }
  requirements:
    sf_culture: (?:fr|en)


Si vous avez bien réussi à vous identifier, vous devriez avoir le lien vers la déconnexion dans le menu du site.
Vous pouvez consulter les données stockées dans votre objet utilisateur directement à l'aide de la toolbar Symfony:



Très pratique pour faire du debug rapide et propre.

Passons maintenant au développement de la méthode de déconnexion:
Code php:
    public function executeDeco(sfWebRequest $request)
    {
        // Supprime l'état identifié de l'user
        $this->getUser()->setAuthenticated(false);
        
        // Supprime tous les accès de l'user
        $this->getUser()->clearCredentials();
        
        // Supprime les valeurs de la sessions que l'on a stocké
        $this->getUser()->getAttributeHolder()->remove('login');
        $this->getUser()->getAttributeHolder()->remove('id');
        $this->getUser()->getAttributeHolder()->remove('level');
        
        $this->redirect('localized_homepage');
    }


Cette dernière permettra de supprimer la session d'identification de l'utilisateur.

Et pour terminer le chapitre, faisons un peu de traduction:

symfony i18n:extract frontend fr --auto-save


Et c'est parti pour la modification de apps/frontend/i18n/fr/messages.xml:

<trans-unit id="40">
<source>Disconnect</source>
<target>Déconnexion</target>
</trans-unit>
<trans-unit id="41">
<source>Log in the website</source>
<target>Identification au site</target>
</trans-unit>
<trans-unit id="42">
<source>Password lost ?</source>
<target>Mot de passe perdu?</target>
</trans-unit>
<trans-unit id="43">
<source>Welcome</source>
<target>Bienvenu</target>
</trans-unit>
<trans-unit id="44">
<source>Your account has been well created on WatchMyDesk website.</source>
<target>Votre compte a bien été créé sur Watch My Desk.</target>
</trans-unit>
<trans-unit id="45">
<source>However, you need to confirm your email address by clicking on the following link:</source>
<target>Cependant, vous devez confirmer votre adresse email en cliquant sur le lien suivant:</target>
</trans-unit>
<trans-unit id="46">
<source>Kind regards,</source>
<target>Bien cordialement,</target>
</trans-unit>
<trans-unit id="47">
<source>The Watch My Desk team.</source>
<target>L'équipe de Watch My Desk.</target>
</trans-unit>
<trans-unit id="48">
<source>Validate my account</source>
<target>Valider mon compte</target>
</trans-unit>
<trans-unit id="49">
<source>Your account is still disabled. Please click on the confirmation email to activate your account.</source>
<target>Votre compte n'est toujours pas activé. Merci de cliquer sur le lien de confirmation reçu par email.</target>
</trans-unit>
<trans-unit id="50">
<source>You are bannished from the website.</source>
<target>Vous êtes banni du site.</target>
</trans-unit>
<trans-unit id="51">
<source>Log in incorrect</source>
<target>Identification incorrecte</target>
</trans-unit>
<trans-unit id="52">
<source>This login does'nt exists</source>
<target>Ce pseudo n'existe pas</target>
</trans-unit>

Supprimez le cache et testez les langues.

symfony cc







Rechercher sur la Ferme du web