Planète

Par kgaut
Kevin Gautreau

Drupal 8 - Ajouter une restriction par ip sur une route

Dans le fichier MODULE.routing.yml on va utiliser le requirement « _custom_access »

  1. module.ma_methode:
  2.   path: 'mon-module/mon-chemin'
  3.   defaults:
  4.   _controller: '\Drupal\module\Controller\monController::maMethode'
  5.   requirements:
  6.   _custom_access: '\Drupal\module\Controller\monController::maMethodeAccess'

Que l'on va implémenter dans notre contrôleur, ici monController.php :

  1. public function maMethodeAccess() {
  2. /** @var Request $request */
  3. $request = \Drupal::request();
  4. $ipAutorisees = \Drupal::config('iamyourstory.checkcard_api')->get('allowed_ips')
  5. $ipAutorisees = explode(',', $ipAutorisees);
  6. $ipAutorisees = array_map('trim', $ipAutorisees);
  7. return AccessResult::allowedIf(\in_array($request->server->get('REMOTE_ADDR'), $ipAutorisees));
  8. }

à noter :

  • Les ip autorisées sont stockées en configuration via un formulaire de config, les ip sont séparées par une virgule, d'où le explode.
  • J'utilise ensuite la fonction array_map('trim', $ipAutorisees) pour supprimer les éventuels espaces. Si la personne avait saisi « 127.0.0.1, 192.168.0.2 », mon tableau sans le trim sera ['127.0.0.1', ' 192.168.0.2'] (espace au départ de la seconde ip.)
Par kgaut
Kevin Gautreau

Drupal 8 - Entité - Champ de base Fichier (File)

Voici comment créer un champ « fichier » au sein d'un type d'entité custom dans drupal 8 :

  1. $fields['programme_pdf'] = BaseFieldDefinition::create('file')
  2. ->setLabel(t('Programme PDF'))
  3. ->setSetting('file_directory', 'formations/programme') // dossier d'upload
  4. ->setSetting('max_filesize', '10MB') // taille max du fichier
  5. ->setSetting('file_extensions', 'pdf') // extensions autorisées, à séparer par un espace
  6. ->setSetting('description_field', FALSE) // si on veut activer un champ « description »
  7. ->setDisplayOptions('form', [
  8. 'label' => 'hidden',
  9. 'type' => 'file_generic',
  10. 'weight' => 4,
  11. ])
  12. ->setDisplayConfigurable('form', TRUE)
  13. ->setDisplayConfigurable('view', TRUE);

 

Par kgaut
Kevin Gautreau

Drupal 8 - Entitée & Views - Créer une relations inverse

Prenons deux types d'entités custom : « Bière » et « Brasserie » avec une relation 1-n entre les deux dans le sens :

  • Une bière provient d'une et d'une seule brasserie
  • Une brasserie peut proposer N bières

 Ainsi :

drupal-views-relations.png

En drupalisme, on aurait une propriété « entity_reference » au niveau de notre bière qui fera référence à la brasserie.

Dans views, si on fait un listing des bières, pas de soucis pour accéder au contenu de la brasserie depuis la bière, par contre l'inverse n'est pas possible.

Depuis un listing de brasserie, il n'est pas possible d’accéder aux bières de la brasserie.

Pour cela il faut utiliser la classe en charge de views_data, définie dans l'annotation de notre type d'entité brasserie :

drupal-entitee-annotation.png

Et voila le contenu de ce fichier

  1.  
  2. namespace Drupal\mon_module\Entity\ViewsData;
  3.  
  4. use Drupal\views\EntityViewsData;
  5.  
  6. class BrasserieViewsData extends EntityViewsData {
  7.  
  8. /**
  9.   * {@inheritdoc}
  10.   */
  11. public function getViewsData() {
  12. $data = parent::getViewsData();
  13. $data['brasserie']['bieres'] = [
  14. 'title' => t('Bieres'),
  15. 'help' => t('Lie la brasserie aux bières produites'),
  16. 'relationship' => [
  17. 'group' => t('bieres'), // Affiché en information dans la partie « relationship » de views
  18. 'label' => t('Actions de formation'), // Affiché en information dans la partie « relationship » de views
  19. 'base' => 'biere', // Table de base de l'entitée cible
  20. 'field table' => 'biere', // Table contenant le champ de l'entitée cible sur lequel on fera la jointure
  21. 'base field' => 'brasserie',// Champ de l'entité cible Champ sur lequel on fera la jointure
  22. 'relationship field' => 'brasserie_id', // Champ de l'entité source sur lequel on fera la jointure
  23. 'id' => 'standard',
  24. ],
  25. ];
  26. return $data;
  27. }
  28. }

Et voila le travail !

views-relationship.png

Par kgaut
Kevin Gautreau

Drupal - PSA-2018-001 - Patch de sécurité déployé le 28/03/2018

Mise à jour du 29 mars 2018 :

Patchez votre site

Si vous n'avez pas encore patché / mis à jour votre site, faite le ! Téléchargement des nouvelles versions ou des patchs à cette adresse : https://www.drupal.org/sa-core-2018-002.

Si vous utilisez composer, la commande pour mettre à jour le core est :

  1. composer update drupal/core

Si vous utilisez drush

  1. drush rf
  2. drush up

La faille

Il s'agit de variables get / post / cookies / pouvant contenir des « # » dans leurs noms et potentiellement déclencher des injections sql ? Je n'ai pas encore vu d'exploit mais les implications données sur la FAQ de la faille sont violente :

 

  • How difficult is it for the attacker to leverage the vulnerability? None (user visits page).
  • What privilege level is required for an exploit to be successful? None (all/anonymous users).
  • Does this vulnerability cause non-public data to be accessible? All non-public data is accessible.
  • Can this exploit allow system data (or data handled by the system) to be compromised? All data can be modified or deleted.
  • Does a known exploit exist? Theoretical or white-hat (no public exploit code or documentation on development exists)
  • What percentage of users are affected? Default or common module configurations are exploitable, but a config change can disable the exploit.

Source : https://groups.drupal.org/security/faq-2018-002

En tout cas le patch permet de filtrer ces tableaux et enlever toutes les éléments pouvant être dangereux.

Il est possible d'avoir une whitelist des paramètres à ne pas supprimer dans le cas où vous utilisez des noms de cookies, paramètres GET ou POST commençant par des « # ».

Pour cela, sous drupal 7, dans le fichier settings.php

  1. $conf['sanitize_input_whitelist'] = ['#clee_1', '#clee_2'];

Sous drupal 8, dans le fichier settings.php aussi :

  1. $settings['sanitize_input_whitelist'] = ['#clee_1', '#clee_2'];

Il y a aussi un système pour enregistrer à chaque fois que des paramètres sont supprimés.

Pour l'activer sous drupal 7 :

  1. $conf['sanitize_input_logging'] = TRUE;

Sous drupal 8 :

  1. $settings['sanitize_input_logging'] = TRUE;

Post original du 21/03/2018

Il vient d'être annoncé par l'équipe gérant la sécurité du CMS drupal qu'un gros correctif de sécurité pour drupal 7 et drupal 8 sera déployé mercredi 28/03/2018 entre 20h et 21h30 (heures Françaises). [Edit du 27/03/2018 : j'avais oublié le paramètre « heure d'été » dans mes conversions de timezones...]

On ne sait pas encore où se situe la faille. On sait seulement qu'elle semble (très) importante et qu'il est recommandé d'appliquer le patch immédiatement.

La dernière fois qu'un cas similaire s'est présenté, une fois le correctif disponible, les sites non patchés ont été très rapidement piratés. En effet : une fois que l'on a accès au patch, il est facile de trouver la faille corrigée et ainsi de l'exploiter sur des sites non protégés.

Si vous êtes développeur ou si vous gérez vos propres sites drupal, prévoyez une permanence mercredi soir pour vous occuper de ça.

Si vous avez un site drupal qui est géré par un tiers, assurez-vous qu'il soit au courant et qu'il s'en occupera au plus tôt.

Ce que l'on sait :

  1. Les versions 7 et 8 de drupal sont concernées. Potentiellement aussi la version 6 (pour plus d'informations spécifiques sur la version 6 : https://www.drupal.org/project/d6lts)
  2. Des patchs seront disponible en plus des nouvelles versions de drupal 7, 8.3, 8.4 et 8.5, la mise à disposition d'un patch permet une application du correctif plus rapide et moins risquée qu'une mise à jour complète d'un drupal.
  3. Le correctif ne nécessitera pas de mise à jour de la base de données.
  4. Il faudra aller vite, très vite : lors d'un précédent en 2015 (drupageddon) les premiers sites hackés l'avaient étés moins de 6h après la mise à disposition du patch.
  5. Le patch sera disponible à cette adresse : https://www.drupal.org/psa-2018-001 entre 20H et 21H30 ce mercredi.

 

Plus d'informations : https://www.drupal.org/psa-2018-001

Des petits sites, des gros sites, des micro sites avec Drupal 8

Drupal 8 est un outil dimensionné pour répondre aux besoins des projets web les plus ambitieux. Nous entendons beaucoup parler des notions de headless, de API first, de découplage, etc. qui résolument permettent des architectures solides pour projets ambitieux. Mais ce n'est pas pour autant que Drupal 8 ne propulse plus des sites plus classiques, et voire parfois beaucoup moins ambitieux : de simples, petits, sites Internet, mais pour lesquels nous souhaitons bénéficier de la modularité, de la souplesse et de la robustesse de Drupal.

Par GoZ
Fabien CLEMENT

Installer un Drupal multilingue depuis la configuration

Installer un Drupal multilingue depuis la configuration

Depuis que nous n'avons plus besoin de features pour gérer la configuration et depuis que nous pouvons gérer cette configuration via les fichiers yaml dans un répertoire de configuration, il est beaucoup plus facile de maintenir une configuration entre différents environnements. Depuis quelques temps, il est possible d'installer sans module supplémentaire un Drupal en se basant sur une configuration existante.

GoZ
lun 10/12/2018 - 11:12

Fournir un formulaire personnalisé aux entités de Drupal 8

A l'instar des modes d'affichage qui permettent d'afficher une entité de multiples manières, Drupal 8 permet de créer de multiples modes de saisie, ou formulaires, utilisables sur les entités, que ce soient les utilisateurs, les termes de taxonomy, les contenus ou n'importe quelle entité personnalisée. Découvrons ici comment utiliser ces modes de saisie, depuis leur création jusqu'à leur exploitation pour personnaliser la saisie par exemple des informations d'un utilisateur.

Par Artusamak
Julien Dubois

Créer un système d'annonces simple avec Drupal 8 (seconde partie)

Créer un système d'annonces simple avec Drupal 8 (seconde partie)
DuaelFr
mar 04/12/2018 - 09:30

La première partie de cet article décrivait l'analyse et l'implémentation du système d'annonce présent sur notre site. Dans cette partie nous aborderons l'affichage de l'annonce et la gestion du cache.

Corps

Dans la première partie de cet article, nous avons créé un formulaire qui nous permet de configurer une annonce, sa date de début, sa date de fin et son contenu. Désormais, il nous faut afficher ces informations en respectant la configuration, notamment les dates.

Pour afficher des informations dans une zone définie d'un site Drupal, en dehors de la zone de contenu, nous avons un mécanisme tout désigné : les blocs. Selon la demande initiale, nous devons donc créer un bloc qui sera présent sur toutes les pages du site, lorsque l'annonce est activée et que nous sommes entre la date de début et la date de fin de l'annonce.

Création du bloc

Nous l'avons déjà couvert dans les billets issus de notre formation, la création d'un bloc passe par la création d'un Plugin, soit la forme d'une classe PHP munie d'une Annotation. Dans notre cas, comme pour le formulaire de paramétrage, nous aurons besoin de pouvoir accéder aux données stockées dans la State API et nous aurons donc une dépendance à injecter. La différence principale est que la classe BlockBase n'implémente pas déjà l'interface nécessaire et qu'il faudra donc le faire nous même. Vous le constaterez, du fait que nous manipulions un Plugin et plus juste un formulaire, les méthodes create() et __construct() auront besoin de quelques paramètres complémentaires. Voyons voir ce que l'on doit mettre dans notre fichier src/Plugin/Block/Announcement.php.

namespace Drupal\hc_announce\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\State\StateInterface;

/**
* Provides a 'Announcement' block.
*
* @Block(
*  id = "announcement",
*  admin_label = @Translation("Announcement"),
* )
*/
class Announcement extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * @var array
   */
  protected $config;

  /**
   * Constructs a new Announcement object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param string $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\State\StateInterface $state
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    StateInterface $state
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->config = $state->get('hc_announcement');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('state')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    return ['#markup' => 'Content of the block'];
  }

}

Bien, maintenant que le bloc est créé et après une petite vidange du cache, vous devriez le voir apparaître dans la liste des blocs disponibles dans l'administration du site. Une fois positionné dans la région de notre choix, on a bien la chaîne "Content of the block" qui apparaît sur le site. Tâchons de faire un petit peu mieux en affichant au moins le contenu stocké dans le State pour que les personnes en charge de l'intégration puissent commencer à travailler sur l'aspect visuel. Nous enrichissons donc la méthode build() de notre bloc comme suit.

  /**
   * {@inheritdoc}
   */
  public function build() {
    $build = [];

    $build['#title'] = $this->config['title'];
    $build['#attributes'] = [
      'class' => ['announcement'],
    ];

    $build['announcement'] = [
      '#type' => 'container',
      '#attributes' => ['class' => ['announcement__content']],
      'content' => [
        '#type' => 'processed_text',
        '#text' => $this->config['announcement']['value'],
        '#format' => $this->config['announcement']['format'],
      ],
    ];

    return $build;
  }

Cela fonctionne bien. Cependant, nous ne respectons pas l'état d'activation de l'annonce ni ses dates de début et de fin. Pour l'état de l'annonce rien de plus simple. Avec un test dans la méthode build(), il est toujours possible de renvoyer un tableau vide si l'annonce est inactive. Pour manipuler les dates, nous avons par contre besoin d'une nouvelle dépendance sur le service datetime.time du cœur. Nous ajoutons donc ce service à la méthode create(), à la méthode __construct() et l'enregistrons dans un attribut $time au niveau de l'objet. Puis, nous ajoutons le code suivant à la méthode build() pour renvoyer un bloc vide si le bloc n'est pas supposé être actif.

    if (empty($this->config['enabled'])) {
      return $build;
    }

    $now = $this->time->getRequestTime();
    $start = (new \DateTime($this->config['start_date'] . ' 00:00:00'))->getTimestamp();
    $end = (new \DateTime($this->config['end_date'] . ' 23:59:59'))->getTimestamp();
    if ($now < $start || $now > $end) {
      return $build;
    }

Trop facile ? Vous n'avez pas tort...

Le problème du cache

Comme indiqué en introduction de cet article, ce bloc va être visible sur toutes les pages du site. Il est donc particulièrement important de bien gérer son cache car sinon nous pouvons être confrontés à deux problématiques majeures :

  1. le bloc est affiché quand il est supposé être inactif ou ne s'affiche pas quand il est supposé être actif,
  2. aucune des pages du site n'est mise en cache.

Nous avions déjà abordé le sujet du cache dans Drupal 8 dans un précédent article sans toutefois aborder le cas un peu spécifique des blocs. En effet, ces derniers utilisent les mêmes métadonnées que les render arrays abordés dans l'article mais définies par des méthodes spécifiques : getCacheContexts(), getCacheMaxAge() et getCacheTags(). Ces dernières vont permettre d'indiquer au service BlockManager, comment mettre en cache le bloc dans son intégralité. Lors du rendu d'une page, si le BlockManager se voit demander un bloc qu'il considère comme ayant un cache valide, il va le renvoyer sans même tenter d'instancier sa classe ou d'appeler sa méthode build() ! Par défaut, tous les blocs sont mis en cache de façon permanente et sans aucune variante. Comme pour le reste des métadonnées de cache, ces dernières se propagent vers le conteneur. Il n'est donc pas souhaitable de se faciliter la vie en désactivant totalement le cache de notre bloc sinon cela signifierait désactiver le cache de toutes les pages du site (en vrai c'est mieux fait que ça, mais on simplifie un peu pour avancer).

Gérer la temporalité du cache

Dans notre cas bien particulier, nous allons devoir prévoir trois cas distincts :

  1. si la date de début n'est pas encore arrivée nous devons mettre en cache jusqu'à cette date,
  2. si la date de début est passée mais la date de fin pas encore, nous devons mettre en cache jusqu'à la date de fin,
  3. si la date de début et de fin sont passées ou si l'annonce est désactivée, nous devons mettre en cache de façon permanente.

Pour atteindre ce but, nous allons simplement implémenter la méthode getCacheMaxAge() de la façon suivante :

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    $max_age = parent::getCacheMaxAge();
    if (!empty($this->config['enabled'])) {
      $now = $this->time->getRequestTime();
      $start = (new \DateTime($this->config['start_date'] . ' 00:00:00'))->getTimestamp();
      $end = (new \DateTime($this->config['end_date'] . ' 23:59:59'))->getTimestamp();

      if ($now < $start) {
        $max_age = Cache::mergeMaxAges($max_age, $start - $now);
      }
      elseif ($now < $end) {
        $max_age = Cache::mergeMaxAges($max_age, $end - $now);
      }
    }

    return $max_age;
  }

Notez l'usage de la méthode \Drupal\Core\Cache\Cache::mergeMaxAges() qui permet de proprement fusionner le max-age par défaut avec la valeur que l'on souhaite configurer. Dans notre cas, cela n'aurait rien changé de renvoyer directement la valeur calculée mais c'est une bonne habitude à prendre d'utiliser les méthodes de fusion des métadonnées de cache pour éviter les mauvaises surprises. La méthode mergeMaxAges() s'assure que l'on conserve toujours le max-age le plus contraignant (et c'est exactement pour cette raison que si on désactive le cache d'un bloc, toute la page est impactée).

Gérer les changements de configuration

Maintenance que la temporalité de notre cache est bien établie, il nous faut prendre en compte les changements qui pourraient venir de l'action d'une personne dans l'interface d'administration. En effet, si le titre ou une date change, par exemple, le cache du bloc devrait immédiatement être invalidé pour prendre en compte les nouvelles données. Dans le cas contraire, les gestionnaires du site n'auraient pas d'autre option que d'invalider la totalité du cache du site. Le mécanisme qui permet cette invalidation, ce sont les cache tags. La plupart des objets gérés par le cœur comme les entités de contenu ou de configuration disposent de cache tags pour les identifier mais ce n'est malheureusement pas le cas de la State API. Fort heureusement, c'est un problème très simple à contourner.

Tout d'abord, définissons un cache tag personnalisé et rattachons le à notre bloc grâce à l'implémentation de la méthode getCacheTags(). Son nom est arbitraire alors, comme souvent, nous allons le préfixer du nom du module pour éviter les collisions. Comme précédemment, nous allons utiliser une méthode pour fusionner ce nouveau cache tag avec d'éventuels autres définis par la classe parente.

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    return Cache::mergeTags(parent::getCacheTags(), ['hc_announcement_settings']);
  }

Ensuite, il nous faut juste modifier légèrement le formulaire d'administration de l'annonce pour lui demander d'invalider ce tag lors de l'enregistrement. Pour cela, nous avons besoin de faire appel au service cache_tags.invalidator que nous allons ajouter à notre injection de dépendances et à notre constructeur :

  /**
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
   */
  protected $invalidator;

  /**
   * Constructs a new AnnouncementSettingsForm object.
   *
   * @param \Drupal\Core\State\StateInterface $state
   * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator
   */
  public function __construct(StateInterface $state, CacheTagsInvalidatorInterface $invalidator) {
    $this->state = $state;
    $this->invalidator = $invalidator;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('state'),
      $container->get('cache_tags.invalidator')
    );
  }

Ceci étant fait, il ne nous reste plus qu'à ajouter une petite ligne dans la méthode submitForm() pour provoquer l'invalidation.

$this->invalidator->invalidateTags(['hc_announcement_settings']);

Un dernier "petit" problème

Désormais, notre bloc est mis en cache suivant une logique qui s'appuie sur le mécanisme d'âge maximum pour définir quand l'expiration a lieu de façon très précise. Nous nous attendons donc à ce que le bloc apparaisse ou disparaisse approximativement au moment indiqué et ça fonctionne particulièrement bien... pour les utilisateurs authentifiés...

Le module Internal Page Cache du cœur permet de mettre en cache les pages entières à destination des anonymes afin d'améliorer drastiquement les performances. Le problème, c'est que ce cache, très agressif, ne tient pas actuellement compte des métadonnées de cache et notamment pas du max-age. En attendant que le cœur change de fonctionnement, vous pouvez contourner ce problème en installant le module Cache Control Override, qui ne nécessite aucune configuration particulière.

Conclusion

Au terme de cette paire d'articles, vous devriez y voir plus clair sur la façon de créer un formulaire et un bloc ainsi que sur la gestion du cache. Par cet exemple concret, j'espère que j'aurai réussi à vous faire visualiser des concepts qui étaient traités de façon bien plus académique dans nos précédents articles. S'il reste des zones d'ombre ou si vous voulez en savoir plus sur un point en particulier, n'hésitez pas à vous manifester dans les commentaires ci-dessous.

 

Crédit photo de couverture : Sue Cro

Catégories
Développement
Drupal
Drupal 8
Tags
state api
cache
blocs
Par Christophe MOLLET
Christophe Mollet

Comment installer Drupal 8

Cet article est un tutoriel présentant les étapes à suivre pour installer Drupal 8 sur linux. Il comprend une phase de préparation de l'environnement de travail puis les étapes de l'installation de Drupal 8.

Accessible pour tous et complet, il comprend aussi une section apportant des solutions aux erreurs couramment rencontrées lors de cette installation.

Par Artusamak
Julien Dubois

Créer un système d'annonces simple avec Drupal 8

Créer un système d'annonces simple avec Drupal 8
DuaelFr
jeu 22/11/2018 - 16:56

Développé initialement pour ce site, notre système d'annonce simple regroupe plusieurs concepts intéressants à mettre en valeur dans un article en deux parties. Ici, nous évoquerons la réflexion autour du stockage de l'information et la construction du formulaire.

Corps

Le besoin fonctionnel à l'origine de ce développement est relativement simple et se retrouve dans de nombreux projets : nous souhaitons pouvoir faire apparaître sur tout notre site un bandeau qui permettra de mettre en valeur, de façon bornée dans le temps, une information importante. Un peu d'analyse autour de cette demande nous permet de la détailler comme suit.

Une personne en charge du contenu du site doit pouvoir activer et configurer une annonce, qui apparaîtra toujours au même emplacement sur toutes les pages du site. La configuration doit permettre de choisir une date de démarrage et une date de fin d'apparition de l'annonce ainsi que son contenu.

Côté technique, la lecture de ce type de besoin soulève deux problématiques pour lesquelles il nous faudra faire preuve de vigilance :

  1. puisque cet élément est administrable, il faut qu'une interface facile d'accès et sécurisée soit mise en place ;
  2. puisque cette annonce va apparaître et disparaître sans nécessaire action humaine, il faut se méfier des problématiques liées au cache.

Saisie et stockage du contenu

En premier lieu, nous devons donc déterminer la manière dont sera stocké ce contenu car c'est de cela que découlera la façon dont il sera saisi. Plusieurs options s'offrent à nous :

  • une entité de contenu personnalisée,
  • un objet de configuration simple,
  • un bloc personnalisé,
  • un système de stockage à plat.

Choisir la bonne option

Puisque le besoin fonctionnel ne demande qu'à ce qu'une annonce soit configurée à la fois, l'utilisation d'une entité de contenu personnalisée semble un petit peu exagérée. En effet, générer une telle entité demande beaucoup de code (bien que l'on puisse utiliser un générateur pour aller plus vite) et nécessitera la création d'au moins une table dans la base de données, de nombreuses routes et contrôleurs pour l'administration. Cela aurait été tout à fait justifié s'il avait fallu pouvoir gérer plusieurs annonces à la fois mais dans notre cas cela représente trop de complexité.

Un objet de configuration simple pourrait également remplir son rôle pour le stockage mais, étant donné qu'il s'agit de permettre à la personne en charge du contenu de mettre en place cette annonce, nous ne conserverons pas cette option. En effet, nos procédures de déploiement provoquent par défaut un écrasement de la configuration de la production au profit de celle versionnée par l'équipe. Bien qu'il soit possible de préserver un ensemble d'objets de configuration lors d'un import, afin d'éviter à la personne en charge du contenu de perdre le fruit de son travail, cela nécessite d'y consacrer du temps et d'ajouter des modules au cœur. Dans le cadre de cette demande, nous avons d'autres options qui nous semblent plus judicieuses.

Un bloc personnalisé, basé sur un type de bloc spécialement conçu pour l'occasion, pourrait bien être une réponse pour sa simplicité de configuration et la mise à disposition d'une interface d'édition similaire à celle d'un nœud. Cette solution, bien que très alléchante, nous pose trois problèmes majeurs. Tout d'abord, nous n'avons pas le module Block Content actif sur notre site, ce qui signifie que nous ne l'activerions que pour un seul type de bloc qui ne contiendrait lui-même qu'un seul bloc. Deuxièmement, pour permettre à la personne en charge du contenu de modifier les champs de ce bloc, il aurait été nécessaire d'intégrer un module de la communauté supplémentaire, sinon elle aurait également pu modifier toute la configuration de tous les blocs. Enfin, la gestion très spécifique du cache dont nous avons fait mention précédemment aurait nécessité d'altérer en profondeur le fonctionnement des blocs personnalisés, ce qui nous semblait, une fois de plus, très disproportionné par rapport à la demande.

Finalement, c'est donc un système de stockage à plat, dans la State API, qui nous semble la meilleure option. En effet, le contenu de la State API est indépendant de la configuration et spécifique à l'environnement sur lequel il est défini. Son accès est très rapide et il ne nécessite aucune configuration particulière ni aucun module complémentaire. Son seul défaut par rapport aux entités de contenu personnalisées ou aux blocs personnalisés, c'est qu'il faut manuellement créer le formulaire qui permettra d'administrer l'annonce. Notez que cette option n'est viable que parce que les données que nous souhaitons stocker ne sont pas critiques pour le fonctionnement du site. Dans le cas contraire, conformément à ce qui est indiqué dans la documentation officielle, il nous aurait fallu trouver une autre solution.

Route et permissions

Entrons dans le vif du sujet ! Pour tout le code qui va suivre, il est important de noter qu'il prend place dans un module nommé hc_announce ; le namespace racine de tous les objets sera donc \Drupal\hc_announce et le nom du module sera utilisé, autant que possible, en préfixe de tous les noms machines qui pourraient entrer en collision avec d'autres modules.

En règle générale, je commence toujours par me poser la question des permissions. Dans notre cas, je souhaite pouvoir assigner le droit de modifier l'annonce indépendamment de toutes les autres permissions du site. Je vais donc créer un fichier hc_announce.permissions.yml contenant le code suivant :

administer announcement:
  title: 'Administer announcement'
  description: 'Allow to access the announcement settings form'

Ensuite, je passe à la route d'accès au formulaire, dans le fichier hc_announce.routing.yml j'ajoute l'entrée :

hc_announce.announcement_settings_form:
  path: '/admin/config/announcement'
  defaults:
    _form: '\Drupal\hc_announce\Form\AnnouncementSettingsForm'
    _title: 'Announcement settings'
  requirements:
    _permission: 'administer announcement'

Puis, comme je suis un peu perfectionniste et que je pense aux personnes en charge du contenu, j'ajoute cette route dans le menu d'administration grâce à une entrée dans le fichier hc_announce.links.menu.yml :

hc_announce.announcement_settings_form:
  title: Announcement
  parent: system.admin_config
  description: 'Announcement settings.'
  route_name: hc_announce.announcement_settings_form

Le lien "Announcement" apparaîtra donc dans le menu d'administration sous l'entrée "Configuration" matérialisée par la route parente system.admin_config.

Si vous voulez en savoir plus sur le fonctionnement des routes de Drupal 8, je vous renvoie à un ancien article issu de notre formation.

La base du formulaire

Ces actions préparatoires terminées, il est temps de passer aux choses sérieuses, le formulaire en lui même.

Il nous faut récupérer une date de début, une date de fin, un titre et un message au minimum. Pour des soucis pratiques, nous ajouterons un système pour activer ou désactiver l'annonce rapidement (c'est à dire sans avoir à jouer avec les dates). Pour clarifier l'utilisation de ce formulaire, nous n'afficherons les champs utiles que si la case à cocher pour activer l'annonce est cochée. C'est parti !

Nous créons donc le fichier src/Form/AnnouncementSettingsForm.php dans notre module avec le minimum pour que cela soit considéré comme un formulaire valide :

namespace Drupal\hc_announce\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
* Class AnnouncementSettingsForm.
*/
class AnnouncementSettingsForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'announcement_settings_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->messenger()->addStatus($this->t('The configuration options have been saved.'));
  }

}

Le résultat n'est pas encore exceptionnel mais il nous permet de valider que la route pointe bien sur notre classe.

Capture d'écran du formulaire ne présentant qu'un bouton de validation.

L'étape suivante consiste à étoffer la méthode buildForm() pour afficher tous les champs dont nous avons besoin.

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['enabled'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enable announcement'),
      '#default_value' => FALSE,
    ];

    $form['announcement'] = [
      '#type' => 'details',
      '#title' => $this->t('Announcement settings'),
      '#open' => TRUE,
      '#states' => [
        'visible' => [
          ':input[name="enabled"]' => ['checked' => TRUE],
        ],
      ],
    ];
    $form['announcement']['start_date'] = [
      '#type' => 'date',
      '#title' => $this->t('Start date'),
      '#default_value' => '',
      '#required' => TRUE,
    ];
    $form['announcement']['end_date'] = [
      '#type' => 'date',
      '#title' => $this->t('End date'),
      '#default_value' => '',
      '#required' => TRUE,
    ];
    $form['announcement']['title'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Announcement title'),
      '#default_value' => '',
      '#maxlength' => 64,
      '#size' => 64,
    ];
    $form['announcement']['announcement'] = [
      '#type' => 'text_format',
      '#title' => $this->t('Announcement'),
      '#default_value' => '',
      '#required' => TRUE,
      '#format' => 'full_html',
    ];
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

Ce qui nous donne un résultat déjà plus proche de nos besoins.

Capture d'écran du formulaire présentant tous les champs définis par le code : activation de l'annonce, date de début, de fin, titre et contenu.

Validation de la saisie

La magie de Drupal fait que les champs marqués comme required seront validés automatiquement, de même que les données relatives aux types de champs auront leur format vérifié avant soumission. Il ne nous reste qu'à vérifier que la date de fin est bien postérieure à la date de début et nous allons pour cela implémenter la méthode validateForm().

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();
    if ($values['end_date'] < $values['start_date']) {
      $form_state->setErrorByName('end_date', $this->t('The ending date must be greater or equal than the starting date.'));
    }
  }

Le résultat avec le module du cœur Inline Form Error actif (vous devriez activer ce module sur tous vos projets par défaut) est plutôt sympa.

Champ date de fin en erreur indiquant que sa valeur doit être supérieur ou égale à la date de début.

Enregistrement et injection de dépendances

Pour finir, il va nous falloir enregistrer l'information dans la State API et nous assurer que le formulaire tiendra compte des données existantes lors de son prochain affichage. Contrairement aux formulaires de configuration qui proposent un ensemble de méthodes pour accéder aux objets de configuration, nous ne disposons d'aucune aide pour accéder à la State API. Pour y parvenir, nous pourrions faire appel directement à l'objet \Drupal qui contient toutes les méthodes d'assistance nécessaires mais, puisque nous aimons le travail bien fait et le respect des bonnes pratiques, mais aussi dans l'intérêt de l'exercice, nous allons faire appel à l'injection de dépendances.

Dans le cas d'un formulaire qui étend la classe FormBase, nous n'avons pas besoin de spécifier que nous souhaitons implémenter l'interface ContainerInjectionInterface car c'est déjà fait dans la hiérarchie de classe. Il nous suffit donc d'implémenter la méthode statique create() et de spécifier le constructeur de notre objet.

namespace Drupal\hc_announce\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\State\StateInterface;

/**
* Class AnnouncementSettingsForm.
*/
class AnnouncementSettingsForm extends FormBase {

  /**
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * Constructs a new AnnouncementSettingsForm object.
   *
   * @param \Drupal\Core\State\StateInterface $state
   */
  public function __construct(StateInterface $state) {
    $this->state = $state;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('state')
    );
  }

// [...]

Le service de construction de formulaire, sachant que notre classe implémente ContainerInjectionInterface, instanciera notre objet par l'intermédiaire de sa méthode create(), en lui fournissant le conteneur de services en paramètres. C'est cette méthode qui créera l'instance du formulaire à proprement parler en lui fournissant les services dont elle a besoin pour fonctionner, dans notre cas, la State API.

Maintenant qu'on a notre service à disposition, rien n'est plus simple que de stocker les données saisies. Il nous faut juste un peu enrichir la méthode submitForm().

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();

    $this->state->set('hc_announcement', [
      'enabled' => $values['enabled'],
      'start_date' => $values['start_date'],
      'end_date' => $values['end_date'],
      'title' => $values['title'],
      'announcement' => $values['announcement'],
    ]);

    $this->messenger()->addStatus($this->t('The configuration options have been saved.'));
  }

En faisant le choix de ne pas filtrer les données saisies, notamment pour le titre qui est un champ texte simple, nous gardons en tête qu'il faudra être vigilants lors de leur affichage. Heureusement, Twig est là pour nous aider à éviter les failles XSS ! Nous y reviendrons dans la seconde partie de cette série.

La dernière chose à faire est d'enrichir la méthode buildForm() pour afficher les données enregistrées dans la State API lors de l'affichage du formulaire. Je vous épargne la totalité du code pour vous montrer uniquement l'exemple sur le champ "enabled" :

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->state->get('hc_announcement');

    $form['enabled'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enable announcement'),
      '#default_value' => !empty($config['enabled']) ? $config['enabled'] : FALSE,
    ];

// [...]

Lorsque vous repasserez sur tous les champs pour utiliser les valeurs stockées, n'oubliez pas le #format du champ "announcement".

Et voilà le travail !

Formulaire de configuration de l'annonce complet affichant tous les champs ainsi que leurs données actuelles.

 

Voilà déjà une bonne chose de faîte ! Dans la prochaine partie de cet article, nous aborderons l'affichage de ces données en front et la gestion du cache.

À très bientôt !

 

Crédit photo de couverture : Sue Cro

Catégories
Développement
Drupal
Drupal 8
Tags
state api
formulaires
configuration
entités
Par Artusamak
Julien Dubois

Installer Drupal depuis la configuration

Installer Drupal depuis la configuration
DuaelFr
jeu 15/11/2018 - 09:30

Depuis Drupal 8.6.0 il est possible d'installer un site depuis un ensemble de fichiers de configuration, voyons comment cela fonctionne.

Corps

Depuis la sortie de Drupal 8 et de son système de gestion de la configuration, la communauté n'a cessé de demander à ce qu'il soit possible de procéder à une installation complète à partir d'un jeu de fichiers de configuration existant. Il aura fallu plus de deux ans et des efforts considérables de dizaines de contributeurs et contributrices pour rendre cela possible.

Utilité

Cette nouvelle fonctionnalité, jusqu'à présent accessible avec un peu de bricolage grâce au profil d'installation Configuration Installer, ouvre de nombreuses possibilités. Tout d'abord, il y a les agences qui souhaiteraient avoir un socle pour démarrer rapidement leurs projets (c'est notre cas), mais aussi les développeurs et développeuses qui veulent pouvoir expérimenter des choses tout en étant capables de reconstruire rapidement leur environnement si besoin ou de monter des environnements de test à la volée. Côté fonctionnel cela rend également plus simple le clonage de site sans avoir à nettoyer les contenus et cela permet également, dans certains cas comme pour un système d'usine à sites, de disposer d'un socle minimal de configuration disponible lors de la création d'un nouveau site.

Préparation

Comme vous vous en doutez, la première étape est d'avoir un jeu de fichiers de configuration disponible. Si ce n'est pas votre cas, faîtes une installation classique avec le profil d'installation minimal, activez quelques modules, changez quelques options dans l'administration du site, puis exportez la configuration via le backoffice (chemin : admin/config/development/configuration/full/export) ou via Drush (drush config:export).

Placez vos fichiers de configuration dans un répertoire prévisible, hors de votre DocumentRoot si possible. Par exemple, si vous suivez l'organisation proposée par le drupal-project, cela se fera dans un répertoire config/sync à la racine du projet. Puis, indiquez l'emplacement de cette configuration dans votre fichier settings.php (ou settings.local.php si vous en avez un) de façon relative à la racine de Drupal, c'est à dire à l'emplacement du fichier index.php. Par exemple :

$config_directories[CONFIG_SYNC_DIRECTORY] = '../config/sync';

Note : il est aussi possible de stocker cette configuration dans le répertoire config/sync de votre profil d'installation sans avoir besoin de modifier le fichier de settings. Cela peut être très utile dans le cadre d'une distribution (interne ou contribuée) mais, dans la plupart des cas, un répertoire indépendant sera préférable car il permettra de maintenir la configuration à jour plus simplement au fil du projet.

Utilisation

Une fois votre référentiel de configuration et vos fichiers prêts, vous n'avez plus qu'à installer votre site.

C'est possible de réaliser cette opération depuis l'interface graphique fournie par Drupal, comme illustré ci-dessous.

Écran d'installation de Drupal faisant apparaître l'option "Use existing configuration" qui permet d'installer le site en se basant sur la configuration existante.

Cela est également possible via Drush 9.4 ou supérieur grâce à l'option --existing-config de la commande drush site:install.

Limitations et dépannage

Attention ! À l'heure actuelle, cette fonctionnalité ne fonctionne pas avec les profils d'installation qui implémentent un hook_install. C'est pour cette raison que je vous ai conseillé de tester en utilisant le profil minimal. Cela sera sans doute amené à changer dans le futur puisque la communauté travaille actuellement sur cette problématique.

De plus, si vous décidez de créer votre propre profil d'installation (ce que je recommande chaleureusement, j'en parlerai dans un futur billet), faîtes bien attention à l'export du fichier core.extension.yml. En effet, celui-ci contient la liste de tous les modules activés, y compris votre profil qui est considéré comme un module, ainsi qu'une référence directe au profil avec lequel le site a été installé. Si vous avez procédé à une installation via le profil minimal, il faudra remplacer ce dernier dans le fichier par le nom machine de votre propre profil sous les clefs "module" et "profile". Si vous ne faîtes pas cette modification, l'installation échouera.

Extrait d'un différentiel de code illustrant le changement de profil dans le fichier core.extension.yml.

Et voilà ! Vous savez tout. Si vous tombez sur des cas étranges, n'hésitez pas à nous laisser un petit commentaire !

 

Crédit photo de couverture : Sally Wilson

Catégories
Développement
Drupal
Drupal 8
Tags
configuration management
profil d'installation
Par Christophe MOLLET
Christophe Mollet

Retour sur le Meetup Drupal

Le meetup

Tout d’abord, je tiens à remercier l’ensemble des participants de s’être déplacé pour notre premier meetup drupal. Grâce à vous la communauté ne fait que s’agrandir.

Pour ceux qui auraient loupé ce Meetup voici un petit résumé.

Par admin

Sprint de traduction de la documentation à Montpellier !

Nous nous sommes retrouvés à huit, dès 9h30 ce samedi matin, dans les locaux de Smile à Montpellier.
Après avoir mené plusieurs discussions à bâton rompu sur des sujets très divers, qui tournaient souvent autour de la contribution, nous nous sommes attelés, en début d'après-midi, à celle qui nous est apparu comme la plus nécessaire : la traduction de la documentation Drupal 8. 
Nous avons donc fait un point sur les travaux en cours sur :

et sommes parvenus à traduire 7 issues (sur environ 150) ...

En parrallèle, 

  • Un atelier s'est tenu sur le thème "découverte du site building (structure, entités, contribution)", avec une présentation de Julien,
  • Nous avons échangé autour de thèmes très larges tel que, en vrac, les méthodes agiles, la démocratie liquide, les cycles de vie des modules, la vie en entreprise, le télétravail, les missions de l'association, le prochain Drupal Camp, Drush et Composer, les ressources serveur, le thème du prochain Meetup,

Nous n'avons pas consommé l'ensemble des victuailles, mais nous y sommes employés ...

Nous avons fixé la date du prochain meetup Drupal Montpellier au 14 Novembre.

Et nous sommes quittés regonflés et satisfaits ...

En page d'accueil : 
Version de Drupal : 

Surcharger une configuration de façon dynamique avec Drupal 8

Dans certains cas de figure, il peut être extrêmement intéressant de pouvoir surcharger une configuration de façon dynamique. Un des premiers cas d'usage immédiatement perceptible est dans le cas d'une usine à sites disposant d'un ensemble de fonctionnalités partagées et déployées, et donc de configurations identiques partagées.

Pages