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






