Doctrine 2 & i18n

Doctrine, Zend 3 Commentaires »

Doctrine 2 est dépourvu – en natif – de système de traduction.
Grâce au système d’extension, des contributeurs dont certains membres de l’équipe de développement Doctrine ont développés Gedmo, un pack d’extension pour Doctrine.

Cette extension inclue :

  • Tree
  • Translatable
  • Sluggable
  • Timestampable
  • Loggable
  • Sortable

Nous nous intéresserons à Translatable qui nous permettra de traduire nos données.

Travaillant avec le Framework Zend, les exemples seront orientés pour cette architecture, il est à noter que cela n’est pas un pré-requit et que cela fonctionnera avec ou sans Zend Framework.

Fonctionnement

Le module i18n de Gedmo fonctionne grâce à l’utilisation d’un table annexe contenant la traduction de certaines propriétés de vos Entités.
La table de traduction contient donc le nom du champs traduite, la langue, la valeur, le type d’objet ainsi qu’une reference vers cet objet.

Par défaut, l’extension aura besoin de la table : ext_translations
Les champs suivant seront ainsi necessaire :

  • id
  • content
  • field
  • locale
  • foreign_key
  • object_class

Voici un jeu de donnée qui vous permettra de mieux comprendre l’utilisation de ce type de structure :

  • content : Un día me voy a comprar el mundo
  • field : title
  • locale : es_es
  • foreign_key : 3
  • object_class : \Entity\Article
  • content : Un jour j’achèterai le monde
  • field : title
  • locale : fr_fr
  • foreign_key : 3
  • object_class : \Entity\Article
  • content : Le monde semble être à l’agonie, que pouvons nous faire pour le sauver.
  • field : description
  • locale : fr_fr
  • foreign_key : 3
  • object_class : \Entity\Article

A noter, que les données relatives à la langue par défaut de votre application sont stockés au sein de la table gerant directement votre entité.
Par défaut, nous utilisons la langue « en_US« , ce qui signifie que la table ext_translations ne contiendra pas de traduction pour « en_US ».
Dans mon exemple, ces données seront stockés dans la table « articles » de l’entité \Entity\Article

Ainsi, l’extension stockera l’ensemble des traductions de votre application au sein d’une même table.
Attention, le champs « forien_key » ne correspond pas à une clé étrangère pour votre base de donnée relationnelle.
Une des possibilités de l’extension consiste à pouvoir définir une table spécifique à la traduction d’une Entités, ainsi vous pourrez définir une vrai clé étrangère au sein de votre RDBMS.

Exploitation

Au niveau de l’exploitation des données,
Vous définirez la langue utilisée au sein de votre application lors de l’execution, cela correspondra normalement à la langue de l’utilisateur.
Il ne restera plus qu’a utiliser votre application comme vous en avez l’habitude.

Nous partons du principe que votre environnement Doctrine est correctement configuré et fonctionnel.

Voici la configuration minimum a définir :

$classLoader = new \Doctrine\Common\ClassLoader( 'Gedmo', APPLICATION_PATH . '/../library/');
$classLoader->register();
 
$config 	= new \Doctrine\ORM\Configuration();
 
$driverChain		= new \Doctrine\ORM\Mapping\Driver\DriverChain();
$entityDriver 		= new \Doctrine\ORM\Mapping\Driver\XmlDriver ();
 
$translatableDriver = $config->newDefaultAnnotationDriver( APPLICATION_PATH . '/../library/DoctrineExtensions/Gedmo/Translatable/Entity');
 
$driverChain->addDriver ($entityDriver,		'Entity\Article');
$driverChain->addDriver ($translatableDriver, 	'Gedmo\Translatable');
 
$config->setMetadataDriverImpl($driverChain);
 
$eventManager		= new \Doctrine\Common\EventManager();
$translatableListener 	= new \Gedmo\Translatable\TranslationListener();
 
$translatableListener->setTranslationFallback( true );
$eventManager->addEventSubscriber($translatableListener);
 
$entityManager = \Doctrine\ORM\EntityManager::create($conn, $config, $eventManager);

Ensuite, il faut configurer vos entités.
J’utilise le XML Mapping, vous trouverez facilement l’équivalence pour YAML / Annotation.

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
      xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
	<entity name="Entity\Article" table="articles">
		<id name="idArticle" type="integer" column="id_article">
			<generator strategy="AUTO" />
		</id>
		<field name="title" type="string">
			<gedmo:translatable/>
		</field>
		<field name="description" type="string">
			<gedmo:translatable/>
		</field>
	</entity>
</doctrine-mapping>

Nous avons simplement ajouté le noeud XML gedmo:translatable à notre champs afin de spécifier que celui-ci peut-être traduit.
A noter qu’il faut inclure l’espace de nom xmlns:gedmo.

Voici un exemple d’utilisation avec Doctrine

//En utilisant la langue par défaut qui est l'anglais,
$article 	= $entityManager->find('\Entity\Article', 3);
$article->getTitle (); 
//One day I'll buy the world
 
//En spécifiant la langue espagnole,
$translatableListener->setTranslatableLocale("es_es");
$article 	= $entityManager->find('\Entity\Article', 3);
$article->getTitle (); 
//Un día me voy a comprar el mundo

Entity de traduction

Nous avons vu précédemment que les traductions sont par défaut stockées dans la table ext_translations.
Voyons comment utiliser une table dédiée à la traduction d’une entité. Cela permettra en plus d’avoir une structure de base de donnée plus propre, et d’avoir des clés étrangère au sein de votre RDBMS.

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
      xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
	<entity name="Entity\Article" table="articles">
		<gedmo:translation entity="Entity\ArticleTranslation" />
		..
	</entity>
</doctrine-mapping>

Créons la class ArticleTranslation qui n’a rien de bien extraordinaire, sauf qu’elle doit étendre AbstractTranslation qui va définir les differentes propriétés de votre table de traduction.

namespace Entity
 
use Gedmo\Translatable\Entity\AbstractTranslation;
use Doctrine\ORM\Mapping as ORM;
 
class ArticleTranslation extends AbstractTranslation
{
 
}

A cela, ajoutons le fichier de mapping.

<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
      xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
	<entity name="Entity\ArticleTranslation" table="articles_translation" />
</doctrine-mapping>

Il ne vous reste plus qu’a créer la table article_translation avec les champs énuméré en début d’article.

Conclusion

Comparer à Doctrine 1, la mise en place de l’internationalisation pour votre application peut sembler plus difficile. Par contre, l’exploitation de cette implementation est tout simplement user-friendly.

Mots-clefs :, , , , , , , , , ,
 

AMF / Zend Authentication using Database

Flash, Flex, Zend 20 Commentaires »

Cet article s’inscrit à la suite du précèdent billet : Authentification & Autorisation avec Zend AMF.

Zend_Amf_Adobe_Auth est une classe livré avec Zend 1.10. Elle peut être utilisée comme service d’authentification pour un serveur Zend_Amf_Server. Le soucis, c’est que cette classe fonctionne conjointement avec un fichier XML réputé statique.

Voici donc le code d’une classe qui permet l’authentification d’un utilisateur AMF via la base de données.
Sachant que nous développons cela pour offrir des services AMF, cette classe utilisera la logique métier implémentée au sein des services.

class Auth_AmfDatabase extends Zend_Amf_Auth_Abstract
{
    protected $_acl;
    protected $_users = array();
 
    public function __construct()
    {
        $this->_acl = new Zend_Acl();
 
        $oServiceRole	= new Service_Role ();
        $oRoles		= $oServiceRole->fetchAll ( );
 
        foreach ( $oRoles as $oRole )
        	$this->_acl->addRole ( new Zend_Acl_Role( $oRole->title ) );
    }
 
    public function getAcl()
    {
        return $this->_acl;
    }
 
    public function authenticate()
    {
        if (empty($this->_username) ||
            empty($this->_password)) {
            require_once 'Zend/Auth/Adapter/Exception.php';
            throw new Zend_Auth_Adapter_Exception('Username/password should be set');
        }
 
        $oServiceUser	= new Service_User ();
        $oUser		= $oServiceUser->authentication ( $this->_username, $this->_password );
 
        if ( null == $oUser )
        {
        	return new Zend_Auth_Result(Zend_Auth_Result::FAILURE,
                null,
                array('Username and / or Password not found')
                );
        }
        else
        {
        	$id = new stdClass();
       	 	$id->role = $oUser->role->code;
       	 	$id->name = $oUser->username;
 
        	return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $id);
        }
    }
}

Le constructeur récupère l’ensemble des rôles existants en base de données pour peupler l’objet ACL.
La méthode authenticate va vérifier le couple Login / Password, et en cas de succès va retourner un objet Zend_Auth_Result contenant lui même un objet standard.
Cet objet standard doit avoir une propriété role et name, le rôle contiendra l’identifiant que vous avez définit dans vos autorisations ACL. Le name devra contenir le login de votre utilisateur.

Cette classe a été inspirée par Zend_Amf_Adobe_Auth. N’hésitez pas à l’adapter pour vos propres besoins.

Mots-clefs :, , , , ,
 

Authentification et Autorisation avec Zend AMF

Flash, Zend 22 Commentaires »

Nous allons voir comment mettre en place l’authentification et l’autorisation lorsque l’on souhaite travailler avec Zend et le protocole AMF.

Au niveau des pré-requit, vous devez avoir une architecture Zend et AMF opérationnelle.
Aussi, je vous conseils de lire la documentation sur Zend_Auth et Zend_Acl.
Vous devez avoir compris la notion de rôle et de ressource.

Objectifs

Si vous fournissez des services à travers le réseau, et qu’ils peuvent avoir un impact sur la cohérence des données, vous vous devez de sécuriser l’accès à ces services.

Sécurisé l’accès signifie restreinte l’accès, mais aussi chiffrer les communications.
Nous ne traiterons pas la partie chiffrement des échanges, pour cela je vous laissez chercher de la documentation sur HTTPS.

Point de départ

Ci-dessous, un exemple de contrôleur Zend offrant des services AMF.

class ServiceController extends Zend_Controller_Action
{
    public function init()
    {
        $this->_helper->viewRenderer->setNoRender();
        $this->_helper->layout->disableLayout();
    }
 
    public function amfAction()
    {
	$server = new Zend_Amf_Server();
 
	//Liste des services
	$server->setClass( "Service_User" );
	$server->setClass( "Service_Book" );
	$server->setClass( "ZendAmfServiceBrowser" );
 
	//Mapping des objets PHP avec les objets ActionScript		
	$server->setClassMap('UserDTO', 'Model_DTO_User');
	$server->setClassMap('BookDTO', 'Model_DTO_Book');
 
	$server->setProduction(false);
 
	ZendAmfServiceBrowser::$ZEND_AMF_SERVER = $server;
 
	echo ($server->handle());
    }
}

Nous avons donc deux services, et deux types d’objets pouvant être utilisés.

Vous remarquerez l’utilisation de ZamfBrowser, ce service couplé avec une application Adobe AIR permet de travailler facilement avec les services.

Voici à quoi ressemble les prototypes d’un de mes services.

class Service_Book
{
	public function createBook (Model_DTO_Book $oBook) {}
	public function readBook (Model_DTO_Book $oBook) {}
	public function updateBook (Model_DTO_Book $oBook) {}
	public function deleteBook (Model_DTO_Book $oBook) {}
	public function isRentBy (Model_DTO_User $oUser) {}
}

L’autre service est composé lui aussi des méthodes CRUD (Create, Read, Update, Delete), ainsi que d’autres fonctions métiers.

Sachant que mon application cliente Flash utilise ces services, il est très facile à un utilisateur de découvrir l’URI de la passerelle AMF.
En l’état, une personne ayant l’accès à ma passerelle AMF pourrai utiliser toutes les fonctions de mes services. Ce qui implique qu’il puisse modifier ma base de données.

Remarquer que si l’on supprime le service de ZamfBrowser, l’utilisateur n’aura pas accès à la liste exhaustive des services. Si vous êtes en production, je vous conseils donc de supprimer ce service pour des raisons de sécurité.

Mise en place de l’authentification

Nous allons commencer par mettre en place l’authentification,
Sachant que nous utilisons un client Flash, il est logique d’utiliser Zend_Amf_Adobe_Auth.

$authAdapter = new Zend_Amf_Adobe_Auth(APPLICATION_PATH . '/config/password.xml');
$server->setAuth($authAdapter );

Avec ces deux lignes, nous avons mis en place l’authentification au niveau du serveur.
Il vous reste à définir le fichier password.xml

<?xml version="1.0"?>
<roles>
   <role id="editor">
        <user name="stephane" password="P@ssword01"/>
        <user name="robert" password="P@ssword01"/>
   </role>
</roles>

Le fichier password.xml contient les identifiants et les mots de passe des utilisateurs.
C’est à ce niveau que vous définissez les affectations d’identifiants à un rôle.

Si vous avez besoin de plus de souplesse au niveau de la gestion des identifiants, je vous conseils la lecture de Authentification avec une table de base de données.

Il est à noter, que si vous ne spécifier aucun identifiant lors de votre connexion au serveur AMF vous serez considéré comme un invité : Zend_Amf_Constants::GUEST_ROLE

Mise en place de l’autorisation

Maintenant que nous avons correctement définit l’authentification, passons à l’autorisation.
C’est maintenant que nous allons donner les autorisations ou refus d’accès aux services.

$acl = new Zend_Acl();
$server->setAcl($acl);

Avec ces deux lignes, vous avez définit une Acces Control List.
Vous le constatez, aucune autorisation n’est définit; mais il est important de noter que maintenant, plus personne n’a accès à vos services.

Pour donner l’accès à nos deux services à n’importe qui, nous allons procéder comme suit :

$acl = new Zend_Acl();
$acl->addResource ( new Zend_Acl_Resource('Service_User') );
$acl->addResource ( new Zend_Acl_Resource('Service_Book') );
$acl->addResource ( new Zend_Acl_Resource('ZendAmfServiceBrowser') );
 
$acl->allow (null, 'Service_User');
$acl->allow (null, 'Service_Book');
$acl->allow (null, 'ZendAmfServiceBrowser);
$server->setAcl($acl);

Ci-dessus, nous avons une liste explicite des autorisations.
Si nous avions voulu faire dans l’implicite, nous aurions procédé ainsi :

$acl = new Zend_Acl();
$acl->allow ();
$server->setAcl($acl);

Pour aller plus loin dans l’explication, nous allons maintenant définir l’ensemble de nos autorisations.

Nous souhaitons que le rôle administrateur est accès à tout, et nous souhaitons que les utilisateurs non authentifiés n’est pas accès aux fonctions de mise à jour.

Rappelez-vous, nous avons vu plus haut que vous étiez connecter avec le rôle invité lorsque vous ne spécifiez aucun identifiant.

$acl = new Zend_Acl();
 
//Définition les rôles.
$acl->addRole ( new Zend_Acl_Role(Zend_Amf_Constants::GUEST_ROLE) );
 
//Définition des autorisations
$acl->allow ();
$acl->deny (Zend_Amf_Constants::GUEST_ROLE, null, "create");
$acl->deny (Zend_Amf_Constants::GUEST_ROLE, null, "update");
$acl->deny (Zend_Amf_Constants::GUEST_ROLE, null, "delete");
 
$server->setAcl($acl);

Premièrement,
Nous avons dû définir le rôle invité pour pouvoir travailler ses autorisations.

Deuxièmement,
Nous avons utilisé le principe des listes noires. Si nous avions utilisez un système de liste blanche, nous aurions du définir l’ensemble des rôles, et l’ensemble des autorisations.
C’est donc en partant du principe que tout le monde a accès à tout, que nous avons fermé les portes aux anonymes.

Explication des fonctions Deny & Allow :
Zend_Acl::deny et Zend_Acl::allow utilisent les mêmes arguments.
Le premier paramètre correspond à un rôle, cela ne change pas des ACL Zend classique.
Le deuxième paramètre correspond à une ressource. Avec Zend AMF, une ressource correspond à un Service.
Le troisième paramètre correspond ici à la fonction d’un service AMF.

Nous avons utiliser null au niveau du deuxième paramètre pour dire : tout les services.

initAcl

Ce dernier paragraphe pour vous parlez de mon opposition face aux spécifications de la documentation Zend_AMF_Server :

If the ACL object is set, and the class being called defines initAcl() method, this method will be called with the ACL object as an argument. The class then can create additional ACL rules and return TRUE, or return FALSE if no access control is required for this class.

En gros, il existe une autre manière de définir les autorisations,
Si vous avez définit un ACL au niveau de votre Serveur AMF. Ce dernier vérifiera l’existence d’une fonction initAcl sur les services lorsqu’ils seront appelés.

Cette fonction est ce que l’on appel un Hook, et vous permets de définir les autorisations et refus au sein de la définition même de vos services.
Autant vous dire que niveau maintenance et centralisation de l’information c’est très moyen..
De plus, si l’on pend l’exemple de ZendAmfServiceBrowser, vous allez devoir modifier la définition de ce service, et cela à chaque fois qu’il y aura une nouvelle version ?

Voici un exemple de fonctionnement

class Service_Book
{
	public function initAcl ($acl)
	{
		$acl->deny('editor', 'Services_Book', 'create');
		$acl->deny('editor', 'Services_Book', 'update');
		$acl->deny('editor', 'Services_Book', 'delete');
		return true;
	}
}

Pour aller plus loin : Vous trouverez un article dédié à l’authentification pour Zend AMF Server avec une base de données : AMF / Zend Authentication using Database

Mots-clefs :, , , , , , ,
 

Mise en production de Zend Framework sur un serveur mutualisé

Zend 18 Commentaires »

Zend Framework LogoLorsque l’on dispose d’un serveur dédié, la mise en production d’une application Zend Framework est assez simple; il suffit de modifier le DocumentRoot du VirtualHost.

Si l’on dispose d’un hébergement mutualisé, il faut aussi pouvoir proposer à nos visiteurs un accès au site via www.domain.tld et non pas www.domain.tld/public/

Première solution

La première solution, consiste à déplacer le fichier /public/index.php à la racine du domaine et de modifier la définition de la constante APPLICATION_PATH de ce même fichier.
Le seul soucis de cette solution, c’est que les appels aux ressources de type CSS ou JS devront être modifiés pour pointer vers www.domain.tld/public/
Personnellement, je trouve cela complètement nul…

Deuxième solution

La deuxième solution – bien plus élégante – passe par l’ajout d’un .htaccess à la racine du domaine.

RewriteEngine On
# Only apply to URLs on this domain
RewriteCond %{HTTP_HOST} ^(www.)?domain.tld$
 
# Only apply to URLs that aren't already under folder.
RewriteCond %{REQUEST_URI} !^/public/
 
# Don't apply to URLs that go to existing files or folders.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
 
# Rewrite all those to insert /folder.
RewriteRule ^(.*)$ /public/$1
 
# Also redirect the root folder.
RewriteCond %{HTTP_HOST} ^(www.)?domain.tld$
RewriteRule ^(/)?$ public/index.php [L]

Là, tout est magique et fonctionne sans aucune autre modification de l’application.
Pour protéger l’accès à vos autres répertoires, et principalement /application qui contient surement vos mots de passe BDD non chiffrés. Rajouter un fichier .htaccess contenant

deny from all
Mots-clefs :, , , , , , , ,
 

Transfer Object Assembler avec Doctrine 1

Doctrine, Zend 7 Commentaires »

doctrineLe pattern Transfert Object TO, Data Transfert Object DTO, ou encore Value Object VO permet d’échanger des données entre deux applications.

Wikipédia : Son but est de simplifier les transferts de données entre les sous-systèmes d’une application logiciel. Les objets de transfert de données sont souvent utilisés en conjonction des objets d’accès aux données.

Qui dit Transfert Object, veux aussi dire Transfert Object Assembler.

Conversion d’object

Le pattern Transfert Object Assembler permet de passer d’un DTO à un Objet d’accès au données, et inversement.
C’est le moyen le plus simple pour travailler avec le pattern Data Transert Object et un ORM.

Domain Object Model -> Data Transfert Object

Avec l’ORM Doctrine, le transfert d’un « Domain Object Model DOM » ou ici Doctrine_Record vers un DTO est très simple.
Il suffit de réaliser un parser qui analyse l’objet issu de Doctrine, et qui remplit les données du DTO.

Data Transfert Object -> Domain Object Model

Convertir un DTO vers un DOM peut s’avère plus complexe.
En effet, Doctrine utilise des collections d’objet qui lui sont propres, mais a aussi besoin de connaitre les clés primaires et leurs contenus pour pouvoir créer un objet. Car un objet issu de Doctrine_Record n’a pas les même propriétés quand il est existant en base de donnée, ou qu’il est en phase d’être sauvegardé.

Implémentation de Transfer Object Assembler

Une implémentation du pattern Transfert Object Assembler vient de voir le jour après moult et moult péripéties.

Cette implémentation permet donc le transférer de DTOs à partir de DOMs et inversement.
Associer au pattern Service Layer, cela peut s’avérer être très pratique.

Voici un exemple simple d’utilisation :

$oProgram = new ProgramDTO ();
$oProgram->title = "Dorothe";
 
$oVideoDTO = new VideoDTO ();
$oVideoDTO->idVideo = 30;
$oVideoDTO->program = $oProgram;
 
$oAssembler = $new Assembler ();
$oVideo = $oAssembler->createDomainObject ($oVideoDTO);
$oVideo->save ();

Nous avons ici créé une nouvelle entrée dans la table programs, et réalisé une liaison entre ce nouvel objet et la vidéo existante d’identifiant 30.
Le tout one-shot !

L’intérêt de cette implémentation, c’est d’être fonctionnel avec :

  • Les objets persistant
  • Les objets non-persistant
  • Les compositions d’objets sur N niveau

Utilisation avec le pattern Service Layer

Nous allons voir l’exemple le plus simple, soit l’implémentation des méthodes CRUD pour un objet générique.
L’utilisation du nommage des classes provient de l’utilisation de Zend Framework.

/**
 * @param Model_DTO_Video
 * @return Model_DTO_Video
 */
public function create (Model_DTO_Video $poVideoDTO)
{
	$oModelVideo		= null;
	$oAssembler		= new Model_Utils_Assembler ();
	$oModelVideo		= $oAssembler->createDomainObject($poVideoDTO, true);
	$oModelVideo->save ();
	$poVideoDTO->idShow	= $oModelVideo->idShow;
	return $poVideoDTO;
}
 
/**
 * @param Model_DTO_Video
 * @return Model_DTO_Video
 */
public function read (Model_DTO_Video $poVideoDTO)
{
	$query	= Doctrine_Query::create ()
		->select ("video.*")
		->from ("Model_Video video")
		->where ("video.id_show = ?", $piIdVideo);
	$oModelVideo	= $query->fetchOne ();
	$oAssembler	= new Model_Utils_Assembler ();
	return $oAssembler->createTransfertObject ($oModelVideo);
}
 
/**
 * @param Model_DTO_Video
 * @return Model_DTO_Video
 */
public function update (Model_DTO_Video $poVideoDTO)
{
	$oModelVideo	= null;
	$oAssembler	= new Model_Utils_Assembler ();
	$oModelVideo	= $oAssembler->createDomainObject($poVideoDTO, true);
	$oModelVideo->save ();
 
	return $poVideoDTO;
}
 
/**
 * @param Model_DTO_Video
 * @return Model_DTO_Video
 */
public function delete (Model_DTO_Video $poVideoDTO)
{
	$oModelVideo	= null;
	$oAssembler	= new Model_Utils_Assembler ();
	$oModelVideo	= $oAssembler->createDomainObject($poVideoDTO, false);
	$oModelVideo->delete ();
 
	return $poVideoDTO;
}

Téléchargement

Contenu de l’archive :

  • Implémentation du pattern Transfer Object Assembler
  • Interface du pattern Transfer Object Assembler
  • Classe de définition d’un DTO

Pour faire fonctionner l’assembler. Vous devez définir une classe qui hérite de Assembler, et ainsi surchargez la propriété $_relationships avec vos liaisons entre DTO et DOM.

Dernière mise à jour : 20 Septembre 2010
Transfert_Object_Assembler_Doctrine_1.2.zip – 6 Ko.

Mots-clefs :, , , , , , , ,
 
Designed by NattyWP Wordpress Themes.
Images by desEXign.