Pour les besoins d'une de mes formations, j'ai dû mettre en place un drupal 8 découplé.
CORS (Cross-origin resource sharing)
Le « partage de ressources entre origines multiples » (Cross-Origin Resource Sharing, CORS) est désactivé par défaut dans drupal 8.
Pour régler cela : 2 types d'actions :
sur le serveur apache
-
sudo a2enmod headers
- Dans le fichier de conf d'apache (/etc/apache2/sites-available/mysite.conf :
<VirtualHost *:80> ServerAdmin admin@example.com ServerName local.d8-json.my DocumentRoot /home/yvan/dev/d8-json/web ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined </VirtualHost> <Directory /home/yvan/dev/d8-json/web> Options Indexes FollowSymLinks AllowOverride All Require all granted Header set Access-Control-Allow-Origin "*" </Directory>
Puis
sudo a2dissite d8-json.conf sudo systemctl reload apache2 sudo a2ensite d8-json.conf sudo systemctl reload apache2
Côté Drupal
- dans le fichier web/sites/default/services.yml :
cors.config:
enabled: true
# Specify allowed headers, like 'x-allowed-header'.
allowedHeaders: ['x-csrf-token', 'authorization', 'content-type', 'accept', 'origin', 'x-requested-with']
# Specify allowed request methods, specify ['*'] to allow all possible ones.
allowedMethods: ['POST', 'GET', 'OPTIONS', 'DELETE', 'PUT', 'PATCH']
# Configure requests allowed from specific origins.
allowedOrigins: ['*']
# Sets the Access-Control-Expose-Headers header.
exposedHeaders: false
# Sets the Access-Control-Max-Age header.
maxAge: false
# Sets the Access-Control-Allow-Credentials header.
supportsCredentials: false
Modules de drupal
-
activer les modules du coeur dans la catégorie "Web Services"
-
HAL
- HTTP Basic Authentification
- RESTful Web Services
- Serialization
-
- le module contrib
- REST UI (https://www.drupal.org/project/restui)
- Créer un module custom Ex : test_rest dans lequel j'ai créé
- le fichier test_rest.info.yml :
name: test_rest description: 'Test rest package: Coopernet type: module version: 1.0 core: 8.x dependencies: - drupal:node - drupal:path - drupal:text
- le fichier web/modules/custom/test_rest/src/Plugin/rest/resource/ListArticles.php
<?php namespace Drupal\test_rest\Plugin\rest\resource; use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; /** * Provides a Demo Resource * * @RestResource( * id = "list_articles", * label = @Translation("Articles Resource"), * uri_paths = { * "canonical" = "/test_rest/list_articles" * } * ) */ class ListCartes extends ResourceBase { /** * Responds to entity GET requests. * @return \Drupal\rest\ResourceResponse */ public function get() { $response = ['message' => 'Liste des articles bientôt !!!']; return new ResourceResponse($response); } }
- puis activer le point d'entrée (endpoint) de la ressource, en l'occurence "Articles Resource" dans l'interface de REST UI (/admin/config/services/rest) en donnant tous les droits nécessaires
- enfin, faire appel à ce point d'entrée vial js :
(function() { console.log("Hello"); // création de la requête const req = new XMLHttpRequest(); function enCours(event) { // On teste directement le status de notre instance de XMLHttpRequest if (this.status === 200) { // Tout baigne, voici le contenu de la réponse console.log("Contenu", this.responseText); } else { // On y est pas encore, voici le statut actuel console.log("Statut actuel", this.status, this.statusText); } } req.onload = enCours; // ouverture de la requête //req.open("GET", "/node/3?_format=hal_json", true); req.open("GET", "/memo/list_cartes?_format=json", true); // en complément req.setRequestHeader("Content-Type", "application/hal+json"); // envoi de la requête req.send(null); })();
- Par défaut le super utilisateur est autorisé à obtenir des données via tous les points d'entrée (endpoint) mais si vous souhaitez donner l'accès à d'autres utilisateurs, il faudra vous rendre sur l'interface classique des droits de drupal et gérer chaque accès (rubrique "RESTful Web Services")
S'identifier sur drupal avec une requête fetch js
fetch("http://local.d8.my/user/login?_format=json", { credentials: "same-origin", method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": token }, body: JSON.stringify({ name: "login", pass: "pwd;" }) }) .then(response => { console.log("statut de la réponse : ", response.status); return response.json() }) .then(data => { //console.log("success", data); if (data.current_user === undefined) { console.log("Erreur de login"); throw new Error("Erreur de login : " + data); } else { console.log("user", data.current_user); return data; } })
Attention, l'accès au login n'est possible que si l'utilisateur n'est pas encore identifié
Récupérer des informations sur les utilisateurs via les vues
Globalement, il est assez facile de récupérer tout type d'information via les "views"
Il suffit pour cela de créer une vue dont vous cocherez la case "REST EXPORT SETTINGS > Provide a REST export"
Exemple de vue à importer pour avoir accès aux infos concernant l'utilisateur courant
uuid: deb9920f-4f89-4db0-a2e2-446bf1ef4018 langcode: en status: true dependencies: module: - rest - serialization - user id: restuser label: restuser module: views description: '' tag: '' base_table: users_field_data base_field: uid display: default: display_plugin: default id: default display_title: Master position: 0 display_options: access: type: perm options: perm: 'access user profiles' cache: type: tag options: { } query: type: views_query options: disable_sql_rewrite: false distinct: false replica: false query_comment: '' query_tags: { } exposed_form: type: basic options: submit_button: Apply reset_button: false reset_button_label: Reset exposed_sorts_label: 'Sort by' expose_sort_order: true sort_asc_label: Asc sort_desc_label: Desc pager: type: mini options: items_per_page: 10 offset: 0 id: 0 total_pages: null expose: items_per_page: false items_per_page_label: 'Items per page' items_per_page_options: '5, 10, 25, 50' items_per_page_options_all: false items_per_page_options_all_label: '- All -' offset: false offset_label: Offset tags: previous: ‹‹ next: ›› style: type: serializer row: type: fields options: inline: { } separator: '' hide_empty: false default_field_elements: true fields: name: id: name table: users_field_data field: name entity_type: user entity_field: name label: '' alter: alter_text: false make_link: false absolute: false trim: false word_boundary: false ellipsis: false strip_tags: false html: false hide_empty: false empty_zero: false plugin_id: field relationship: none group_type: group admin_label: '' exclude: false element_type: '' element_class: '' element_label_type: '' element_label_class: '' element_label_colon: true element_wrapper_type: '' element_wrapper_class: '' element_default_classes: true empty: '' hide_alter_empty: true click_sort_column: value type: user_name settings: { } group_column: value group_columns: { } group_rows: true delta_limit: 0 delta_offset: 0 delta_reversed: false delta_first_last: false multi_type: separator separator: ', ' field_api_classes: false filters: status: value: '1' table: users_field_data field: status plugin_id: boolean entity_type: user entity_field: status id: status expose: operator: '' operator_limit_selection: false operator_list: { } group: 1 uid_current: id: uid_current table: users field: uid_current relationship: none group_type: group admin_label: '' operator: '=' value: '1' group: 1 exposed: false expose: operator_id: '' label: '' description: '' use_operator: false operator: '' operator_limit_selection: false operator_list: { } identifier: '' required: false remember: false multiple: false remember_roles: authenticated: authenticated is_grouped: false group_info: label: '' description: '' identifier: '' optional: true widget: select multiple: false remember: false default_group: All default_group_multiple: { } group_items: { } entity_type: user plugin_id: user_current sorts: { } header: { } footer: { } empty: { } relationships: { } arguments: { } display_extenders: { } cache_metadata: max-age: -1 contexts: - 'languages:language_content' - 'languages:language_interface' - request_format - url.query_args - user - user.permissions tags: { } rest_export_1: display_plugin: rest_export id: rest_export_1 display_title: 'REST export' position: 1 display_options: display_extenders: { } path: memo/views/getUser pager: type: some options: items_per_page: 10 offset: 0 style: type: serializer options: grouping: { } uses_fields: false formats: { } row: type: data_entity options: { } auth: - basic_auth cache_metadata: max-age: -1 contexts: - 'languages:language_content' - 'languages:language_interface' - request_format - user - user.permissions tags: { }
Autres points d'entrée user/login, user/login/status, user/logout, user/password
https://www.drupal.org/node/2720655
Problèmes rencontrés
Status 422
Failed to load resource: the server responded with a status of 422 (Unprocessable Entity)
rest/type/node/article does not correspond to an entity on this site
Solution : vider les caches : drush cr source : https://www.drupal.org/project/restui/issues/2599364
error 404 - No route found for "POST ...
Dans le cas d'une requête post, attention à ajouter une deuxième ligne concernant uri_paths :
uri_paths = {
* "canonical" = "/foolzz_rest_api/profile/{uid}",
* "https://www.drupal.org/link-relations/create" = "/foolzz_rest_api/profile/{uid}"
* }
Explications sur les tokens et les cookies
D'après ce que j'en comprends, quand on utilise js à l'intérieur d'un navigateur pour faire une requête rest (GET, POST, PATCH, DELETE...), le cookie de drupal est automatiquement envoyé. Donc, si un utilisateur malveillant arrive à s'introduire dans application Web écrite en JS et exécutée dans un navigateur, il va pouvoir effectuer une requête (POST/PATCH/DELETE) en utilisant le cookie du navigateur Web. Cependant, le navigateur ne saura pas inclure des en-têtes de requête supplémentaires. C'est pourquoi Drupal nécessite que l'en-tête de la requête spécifie un jeton CSRF. Tout comme, pour la même raison, nous avons un jeton CSRF dans les formulaires HTML.
- Log in to post comments