first commit
@ -0,0 +1,17 @@
|
||||
{
|
||||
"editor.formatOnType": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.tabSize": 4,
|
||||
"[php]": {
|
||||
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
|
||||
},
|
||||
"intelephense.telemetry.enabled": false,
|
||||
"intelephense.files.associations": ["*.php"],
|
||||
// Indiquer le chemin d'installation de SPIP pour l'autocomplétion et la PHPdoc
|
||||
// Format Windows
|
||||
// "intelephense.environment.includePaths": ["..\\.."],
|
||||
// Format Linux
|
||||
// "intelephense.environment.includePaths": ["../.."],
|
||||
"intelephense.format.braces": "k&r"
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
# SPIP GraphQL
|
||||
|
||||
## 1. But
|
||||
|
||||
Exposer un endpoint graphQL avec les données de SPIP grâce à la bibliothèque [graphql-php](https://github.com/webonyx/graphql-php)
|
||||
|
||||

|
||||
|
||||
## 2. Fonctionnement du plugin
|
||||
- Création d 'une table `meta_graphql` pour stocker les informations de configuration du plugin
|
||||
- Expose un point d'entrée unique sur `spip.php?action=graphql` grâce à la librairie [graphql-php](https://github.com/webonyx/graphql-php). Ce fichier prépare le contexte et lance le serveur graphQL en attente des requêtes. Nul besoin du plugin `http` !
|
||||
- Toute la logique de l'API se situe dans le dossier `/schema`
|
||||
|
||||
## 3. Fonctionnement de l'API
|
||||
### a. Apprendre graphQL
|
||||
Si vous ne connaissaez pas graphQL, voici [la documentation](https://graphql.org/learn/).
|
||||
|
||||
### b. Les types disponibles
|
||||
Les requêtes graphQL sont fortement typées. Les types sont créés dynamiquement en fonction des objets éditoriaux que vous aurez sélectionnés dans le Back-Office.
|
||||
|
||||
Grâce au [schéma d'introspection](https://graphql.org/learn/introspection/), les clients peuvent afficher les types de données disponibles. Voici les types actuellement disponibles :
|
||||
|
||||
- Les types de bases (scalaires) : `ID`, `String`, `Int`, `Boolean`
|
||||
- Le type scalaire personnalisé `Date` qui permet d'indiquer le format des dates attendu
|
||||
- Le type `MetaList` qui représente les metas exposées
|
||||
- Le type `ENUM Collection` pour récupérer les collections exposées
|
||||
- Le type `Interface Node` utilisé par tous les objets. Ce type expose les champs `id`, `slug`, `typeCollection`, `points` et `rang`
|
||||
- Le type `Interface Objet` utilisé par les objets éditoriaux (sauf `Auteurs` et `Syndic`). Ce type expose les champs `id`, `titre`, `texte`, `slug`, `typeCollection`, `points`, `rang` et `maj`
|
||||
- Les types `MonObjet` liés aux objets éditoriaux (implémentant `Objet` ou non). Le champs `points` (et non le shampooing :p ) sert lors de la requête `recherche`. Il y a aussi le champ `slug`, le champ `type` (il existe aussi le champ `__typeName` fourni par graphQL) et le champ `rang` qui ont été ajoutés.
|
||||
|
||||
### c. Les requêtes disponibles
|
||||
Les requêtes disponibles sont aussi affichées par les clients grâce au schéma d'introspection.
|
||||
Le client JS intégré au plugin (ou votre extension navigateur) utilise ce schéma pour afficher les requêtes disponibles.
|
||||
Pour le moment, les requêtes disponibles sont :
|
||||
- `getMetas`
|
||||
- `getCollections`
|
||||
- `recherche` qui prend un paramètre `texte`
|
||||
- `maCollection` et `getMonObjet` pour chaque collection exposée
|
||||
|
||||
Les requêtes `getMonObjet` reçoivent un `id` en paramètre et les requêtes `maCollection` peuvent recevoir les paramètres suivants :
|
||||
- `where` (un tableau de string comme `['id_rubrique=1','id_trad=2']`)
|
||||
- `pagination` pour indiquer le nombre d'items dans la pagination
|
||||
- `page` pour indiquer la page voulue dans la pagination
|
||||
|
||||
## 4. Tester les requêtes
|
||||
Le endpoint est joignable sur `spip.php?action=graphql`
|
||||
|
||||
Vous pouvez utiliser [graphiql](https://github.com/graphql/graphiql) qui est intégré dans le plugin ou une extension pour votre navigateur. Concernant Firefox, j'utilise [Altair GraphQL Client](https://addons.mozilla.org/fr/firefox/addon/altair-graphql-client/). A part pour le client intégré qui pointe directement sur votre endpoint, il faut renseigner l'url complète de votre API : http://domain.local/spip.php?action=graphql
|
||||
|
||||
Voici un exemple de requête graphQL que vous pouvez tester :
|
||||
```
|
||||
{
|
||||
getMetas {
|
||||
nom_site
|
||||
}
|
||||
articles(
|
||||
where: ["id_rubrique=3", "maj>2023-05-19 11:00:00"]
|
||||
page: 1
|
||||
pagination: 5
|
||||
) {
|
||||
...objetFields
|
||||
}
|
||||
getArticle(id:2) {
|
||||
...objetFields
|
||||
auteurs (where: ["bio!=''"]) {
|
||||
nom
|
||||
email
|
||||
bio
|
||||
}
|
||||
pensebetes {
|
||||
...objetFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment objetFields on Objet {
|
||||
id
|
||||
titre
|
||||
texte
|
||||
maj
|
||||
}
|
||||
```
|
||||
|
||||
Pour la recherche, voici un exemple :
|
||||
```
|
||||
query {
|
||||
recherche(texte: "Hello") {
|
||||
... on Article {
|
||||
id
|
||||
slug
|
||||
titre
|
||||
texte
|
||||
maj
|
||||
points
|
||||
}
|
||||
... on Auteur {
|
||||
id
|
||||
nom
|
||||
points
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Si vous activez le jeton, vous devez l'intégrer dans l'en-tête de vos requêtes.
|
||||
|
||||
Par exemple avec le client graphiql intégré dans le back-office :
|
||||

|
||||
|
||||
Pour utiliser des variables dans vos clients, voici [la manière recommandée](https://www.apollographql.com/docs/react/data/operation-best-practices/#use-graphql-variables-to-provide-arguments) pour pouvoir facilement injecter, par ex, une saisie utilisateur dans la requête :
|
||||

|
||||
|
||||
## 5. Contribuer
|
||||
N'hésitez pas à contribuer : le code est extrêmement bien commenté
|
||||
|
||||
### Pré-requis
|
||||
- Avoir un site SPIP fonctionnel avec des objets éditoriaux.
|
||||
- Avoir installé [composer](https://getcomposer.org/).
|
||||
- Activez le mode debug du plugin pour récupérer les erreurs PHP dans la réponse de l'API.
|
||||
- En cas d'ajout ou de suppression de fichier dans le dossier `schema`, lancez la commande `composer update` pour prendre en compte vos modifications.
|
||||
|
||||
### Lien vers la documentation de la librairie graphql-php
|
||||
|
||||
https://webonyx.github.io/graphql-php/
|
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
include_spip('inc/config');
|
||||
include_spip('graphql_fonctions');
|
||||
include_once _DIR_PLUGIN_GRAPHQL . 'vendor/autoload.php';
|
||||
|
||||
use SPIP\GraphQL\SchemaSPIP;
|
||||
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Error\DebugFlag;
|
||||
|
||||
// Point d'entrée de l'API GraphQL
|
||||
function action_graphql_dist() {
|
||||
try {
|
||||
// Vérification du jeton
|
||||
if (!graphql_verif_jeton(getallheaders()))
|
||||
throw new RuntimeException(_T('graphql:erreur_jeton'));
|
||||
|
||||
// Construction du schéma
|
||||
// https://webonyx.github.io/graphql-php/schema-definition/#lazy-loading-of-types
|
||||
$schemaSPIP = new SchemaSPIP();
|
||||
$schema = new Schema([
|
||||
'query' => $schemaSPIP->get('Query'),
|
||||
'typeLoader' => static fn (string $name): Type => $schemaSPIP->get($name),
|
||||
]);
|
||||
|
||||
// Récupération de la requête
|
||||
$rawInput = file_get_contents('php://input');
|
||||
if ($rawInput === false) {
|
||||
throw new RuntimeException('Failed to get php://input');
|
||||
}
|
||||
$input = json_decode($rawInput, true);
|
||||
$query = (is_array($input) && array_key_exists('query', $input)) ? $input['query'] : '';
|
||||
$variableValues = isset($input['variables']) ? $input['variables'] : null;
|
||||
if (!$query) throw new RuntimeException(_T('graphql:erreur_pas_requete'));
|
||||
|
||||
// Préparation de la requête
|
||||
// https://webonyx.github.io/graphql-php/executing-queries/#using-facade-method
|
||||
|
||||
// Permet d'utiliser le resolver par défaut
|
||||
// https://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver
|
||||
$rootValue = '';
|
||||
|
||||
// Contexte de la requête (pour passer des données communes à toutes les requêtes)
|
||||
$contexte = [
|
||||
'rootUrl' => lire_config("adresse_site"),
|
||||
'request' => $_REQUEST
|
||||
];
|
||||
|
||||
// Exécution de la requête
|
||||
$output = GraphQL::executeQuery(
|
||||
$schema,
|
||||
$query,
|
||||
$rootValue,
|
||||
$contexte,
|
||||
$variableValues,
|
||||
);
|
||||
|
||||
// On vérifie si le mode debug est activé
|
||||
if (in_array('true', lire_config('/meta_graphql/config/debug', []))) {
|
||||
$output = $output->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE);
|
||||
} else {
|
||||
$output = $output->toArray();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Gestion des erreurs
|
||||
$output = [
|
||||
'errors' => [
|
||||
[
|
||||
'message' => $e->getMessage()
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Envoi de la réponse
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(
|
||||
$output,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
}
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 39 KiB |
@ -0,0 +1,10 @@
|
||||
{
|
||||
"require": {
|
||||
"webonyx/graphql-php": "^15.4"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"schema/"
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "1b371bf28124abe412fa0937f28fb61f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "webonyx/graphql-php",
|
||||
"version": "v15.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/webonyx/graphql-php.git",
|
||||
"reference": "99290f7945a5b39ad823f7600fa196de62597e9d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/99290f7945a5b39ad823f7600fa196de62597e9d",
|
||||
"reference": "99290f7945a5b39ad823f7600fa196de62597e9d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.4 || ^8"
|
||||
},
|
||||
"require-dev": {
|
||||
"amphp/amp": "^2.6",
|
||||
"amphp/http-server": "^2.1",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.4",
|
||||
"ergebnis/composer-normalize": "^2.28",
|
||||
"mll-lab/php-cs-fixer-config": "^5.0",
|
||||
"nyholm/psr7": "^1.5",
|
||||
"phpbench/phpbench": "^1.2",
|
||||
"phpstan/extension-installer": "^1.1",
|
||||
"phpstan/phpstan": "1.10.15",
|
||||
"phpstan/phpstan-phpunit": "1.3.11",
|
||||
"phpstan/phpstan-strict-rules": "1.5.1",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psr/http-message": "^1 || ^2",
|
||||
"react/http": "^1.6",
|
||||
"react/promise": "^2.9",
|
||||
"rector/rector": "^0.16.0",
|
||||
"symfony/polyfill-php81": "^1.23",
|
||||
"symfony/var-exporter": "^5 || ^6",
|
||||
"thecodingmachine/safe": "^1.3 || ^2"
|
||||
},
|
||||
"suggest": {
|
||||
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
|
||||
"psr/http-message": "To use standard GraphQL server",
|
||||
"react/promise": "To leverage async resolving on React PHP platform"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GraphQL\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A PHP port of GraphQL reference implementation",
|
||||
"homepage": "https://github.com/webonyx/graphql-php",
|
||||
"keywords": [
|
||||
"api",
|
||||
"graphql"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/webonyx/graphql-php/issues",
|
||||
"source": "https://github.com/webonyx/graphql-php/tree/v15.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://opencollective.com/webonyx-graphql-php",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-05-11T10:26:08+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.3.0"
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<div class="formulaire_spip formulaire_configurer formulaire_#FORM">
|
||||
<h3 class="titrem"><:graphql:cfg_api:></h3>
|
||||
[
|
||||
<p class="reponse_formulaire reponse_formulaire_ok">(#ENV*{message_ok})</p>
|
||||
] [
|
||||
<p class="reponse_formulaire reponse_formulaire_erreur">(#ENV*{message_erreur})</p>
|
||||
]
|
||||
<form method="post" action="#ENV{action}">
|
||||
<div>
|
||||
#ACTION_FORMULAIRE
|
||||
<div class="editer-groupe">#GENERER_SAISIES{#ENV{_saisies}}</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,176 @@
|
||||
<?php
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
include_spip('base/objets');
|
||||
define('_GRAPHQL_OBJETS_NON_CONFIGURABLES', ['forums']);
|
||||
|
||||
function formulaires_configurer_collections_saisies_dist() {
|
||||
$collections_non = array_merge(_GRAPHQL_OBJETS_NON_CONFIGURABLES, ['depots', 'paquets', 'plugins']);
|
||||
$saisies = [];
|
||||
$tables = lister_tables_objets_sql();
|
||||
ksort($tables);
|
||||
foreach ($tables as $table => $infos) {
|
||||
$collection = $infos['table_objet'];
|
||||
$champ_id = $infos['key']['PRIMARY KEY'];
|
||||
if (sql_countsel($table) == '0') {
|
||||
array_push($collections_non, $collection);
|
||||
}
|
||||
|
||||
if (!in_array($collection, $collections_non)) {
|
||||
$champs = [];
|
||||
foreach ($infos["field"] as $nom_champ => $def) {
|
||||
if (
|
||||
$nom_champ != $champ_id
|
||||
&& (in_array($collection, GRAPHQL_COLLECTIONS_NON_OBJET)
|
||||
|| !in_array($nom_champ, GRAPHQL_CHAMPS_INTERFACE_OBJET))
|
||||
) {
|
||||
$champs[$nom_champ] = $nom_champ;
|
||||
}
|
||||
}
|
||||
|
||||
// On n'affiche que les tables auxiliaires finissant par "_liens"
|
||||
// Qui sont autorisées et qui ont du contenu lié
|
||||
$collections_autorisees = lire_config("/meta_graphql/objets_editoriaux", []);
|
||||
$collections_liees = [];
|
||||
foreach (lister_tables_auxiliaires() as $table_aux => $infos_aux) {
|
||||
if (
|
||||
preg_match("#^spip_(\w+)_liens$#", $table_aux, $matches) &&
|
||||
array_key_exists($matches[1], $collections_autorisees) &&
|
||||
sql_countsel($table_aux, "objet='" . objet_type($table) . "'") != 0
|
||||
) {
|
||||
$collections_liees[$matches[1]] = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
$saisies_fieldset = [
|
||||
[
|
||||
'saisie' => 'checkbox',
|
||||
'options' => [
|
||||
'nom' => $collection . '_exposer',
|
||||
'conteneur_class' => 'pleine_largeur',
|
||||
'data' => [
|
||||
'actif' => 'Actif',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'saisie' => 'input',
|
||||
'options' => [
|
||||
'nom' => $collection . '_pagination',
|
||||
'conteneur_class' => 'pleine_largeur',
|
||||
'explication' => _T('graphql:pagination'),
|
||||
'explication_apres' => _T('graphql:desc_arg_pagination'),
|
||||
'type' => 'number',
|
||||
'defaut' => '10',
|
||||
'afficher_si' => '@' . $collection . '_exposer@=="actif"',
|
||||
],
|
||||
],
|
||||
[
|
||||
'saisie' => 'selection_multiple',
|
||||
'options' => [
|
||||
'nom' => $collection . '_champs',
|
||||
'conteneur_class' => 'pleine_largeur',
|
||||
'explication' => _T('graphql:champs_objet'),
|
||||
'afficher_si' => '@' . $collection . '_exposer@=="actif"',
|
||||
'data' => $champs,
|
||||
'multiple' => 'oui',
|
||||
"cacher_option_intro" => "oui",
|
||||
'size' => 5,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (!empty($collections_liees)) {
|
||||
$saisies_fieldset[] = [
|
||||
'saisie' => 'selection_multiple',
|
||||
'options' => [
|
||||
'nom' => $collection . '_liaisons',
|
||||
'conteneur_class' => 'pleine_largeur',
|
||||
'explication' => _T('graphql:cfg_collections_liees'),
|
||||
'afficher_si' => '@' . $collection . '_exposer@=="actif"',
|
||||
'data' => $collections_liees,
|
||||
'multiple' => 'oui',
|
||||
'option_intro' => _T('graphql:aucune'),
|
||||
'size' => 5,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$saisies_fieldset = array_merge($saisies_fieldset, [
|
||||
[
|
||||
'saisie' => 'select_all',
|
||||
'options' => [
|
||||
'nom' => $collection . '_select_all',
|
||||
'conteneur_class' => 'pleine_largeur',
|
||||
'afficher_si' => '@' . $collection . '_exposer@=="actif"',
|
||||
],
|
||||
],
|
||||
[
|
||||
'saisie' => 'submit',
|
||||
'options' => [
|
||||
'nom' => $collection . '_submit',
|
||||
'conteneur_class' => 'pleine_largeur',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$saisies[] = [
|
||||
'saisie' => 'deplier_collection',
|
||||
'options' => [
|
||||
'nom' => $collection . '_deplier',
|
||||
'conteneur_class' => 'pleine_largeur',
|
||||
'collection' => $collection
|
||||
],
|
||||
];
|
||||
|
||||
$saisies[] = [
|
||||
'saisie' => 'fieldset',
|
||||
'options' => [
|
||||
'nom' => $collection,
|
||||
'conteneur_class' => 'config_' . $collection
|
||||
],
|
||||
'saisies' => $saisies_fieldset,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $saisies;
|
||||
}
|
||||
|
||||
function formulaires_configurer_collections_charger_dist() {
|
||||
$valeurs = [];
|
||||
$objets_editoriaux = lire_config('/meta_graphql/objets_editoriaux', array());
|
||||
|
||||
foreach ($objets_editoriaux as $collection => $statut) {
|
||||
$valeurs[$collection . "_exposer"] = "actif";
|
||||
$valeurs[$collection . "_pagination"] = $statut["pagination"];
|
||||
$valeurs[$collection . "_champs"] = $statut["champs"];
|
||||
$valeurs[$collection . "_liaisons"] = $statut["liaisons"];
|
||||
}
|
||||
return $valeurs;
|
||||
}
|
||||
|
||||
function formulaires_configurer_collections_traiter_dist() {
|
||||
$ret = [];
|
||||
$objets_editoriaux = [];
|
||||
foreach (lister_tables_objets_sql() as $table => $infos) {
|
||||
$collection = $infos['table_objet'];
|
||||
if (_request($collection . '_exposer')) {
|
||||
$objets_editoriaux[$collection] = [
|
||||
'pagination' => _request($collection . '_pagination'),
|
||||
'champs' => _request($collection . '_champs') ? _request($collection . '_champs') : [],
|
||||
'liaisons' => _request($collection . '_liaisons'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (ecrire_config('/meta_graphql/objets_editoriaux', $objets_editoriaux)) {
|
||||
$ret['message_ok'] = _T('config_info_enregistree');
|
||||
} else {
|
||||
$ret['message_erreur'] = _T('erreur_technique_enregistrement_impossible');
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<div class="formulaire_spip formulaire_configurer formulaire_#FORM">
|
||||
<h3 class="titrem"><:graphql:cfg_general:></h3>
|
||||
|
||||
[<p class="reponse_formulaire reponse_formulaire_ok">(#ENV*{message_ok})</p>]
|
||||
[<p class="reponse_formulaire reponse_formulaire_erreur">(#ENV*{message_erreur})</p>]
|
||||
|
||||
<form method="post" action="#ENV{action}">
|
||||
<div>
|
||||
#ACTION_FORMULAIRE{#ENV{action}}
|
||||
|
||||
<div class="editer-groupe">
|
||||
[(#SAISIE{checkbox, debug, label=<:graphql:debug_titre:>, explication=<:graphql:debug_desc:>,data=[(#ARRAY{true,<:item_oui:>})]})]
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_meta_table" value="meta_graphql" />
|
||||
<input type="hidden" name="_meta_casier" value="config" />
|
||||
<p class="boutons"><span class="image_loading"> </span><input type="submit" class="submit" value="<:bouton_enregistrer:>" /></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,46 @@
|
||||
<div class="formulaire_spip formulaire_configurer formulaire_#FORM">
|
||||
<h3 class="titrem"><:graphql:cfg_jeton:></h3>
|
||||
[<p class="reponse_formulaire reponse_formulaire_ok">(#ENV*{message_ok})</p>]
|
||||
[<p class="reponse_formulaire reponse_formulaire_erreur">(#ENV*{message_erreur})</p>]
|
||||
<form method="post" action="#ENV{action}">
|
||||
<div>
|
||||
#ACTION_FORMULAIRE{#ENV{action}}
|
||||
<div class="editer-groupe">#GENERER_SAISIES{#ENV{_saisies}}</div>
|
||||
<div
|
||||
class="editer afficher_si_visible api_key_buttons"
|
||||
data-afficher_si='afficher_si({"champ":"api_key_active","total":"","operateur":"==","valeur":"oui"})'
|
||||
>
|
||||
<a
|
||||
class="btn tooltip"
|
||||
onclick="apiKeyAction(event, 'generate')"
|
||||
onmouseout="outapiKeyAction(event, 'generate')"
|
||||
>
|
||||
<span class="tooltiptext"><:graphql:generate:></span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path
|
||||
d="M142.9 142.9c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8H463.5c0 0 0 0 0 0H472c13.3 0 24-10.7 24-24V72c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1C73.2 122 55.6 150.7 44.8 181.4c-5.9 16.7 2.9 34.9 19.5 40.8s34.9-2.9 40.8-19.5c7.7-21.8 20.2-42.3 37.8-59.8zM16 312v7.6 .7V440c0 9.7 5.8 18.5 14.8 22.2s19.3 1.7 26.2-5.2l41.6-41.6c87.6 86.5 228.7 86.2 315.8-1c24.4-24.4 42.1-53.1 52.9-83.7c5.9-16.7-2.9-34.9-19.5-40.8s-34.9 2.9-40.8 19.5c-7.7 21.8-20.2 42.3-37.8 59.8c-62.2 62.2-162.7 62.5-225.3 1L185 329c6.9-6.9 8.9-17.2 5.2-26.2s-12.5-14.8-22.2-14.8H48.4h-.7H40c-13.3 0-24 10.7-24 24z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
id="copyAPIkeyToClipboard"
|
||||
class="btn tooltip"
|
||||
onclick="apiKeyAction(event, 'copy')"
|
||||
onmouseout="outapiKeyAction(event, 'copy')"
|
||||
>
|
||||
<span class="tooltiptext"><:graphql:copier:></span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path
|
||||
d="M448 384H256c-35.3 0-64-28.7-64-64V64c0-35.3 28.7-64 64-64H396.1c12.7 0 24.9 5.1 33.9 14.1l67.9 67.9c9 9 14.1 21.2 14.1 33.9V320c0 35.3-28.7 64-64 64zM64 128h96v48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16H256c8.8 0 16-7.2 16-16V416h48v32c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V192c0-35.3 28.7-64 64-64z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<input type="hidden" name="_meta_table" value="meta_graphql" />
|
||||
<p class="boutons">
|
||||
<span class="image_loading"> </span>
|
||||
<input type="submit" class="submit" value="<:bouton_enregistrer:>" />
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
function formulaires_configurer_jeton_saisies_dist() {
|
||||
$saisies = [];
|
||||
$saisies[] = array(
|
||||
'saisie' => 'checkbox',
|
||||
'options' => array(
|
||||
'nom' => 'api_key_active',
|
||||
'label' => _T('graphql:activer_api_key'),
|
||||
'data' => array(
|
||||
'oui' => _T('graphql:activer'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$saisies[] = array(
|
||||
'saisie' => 'input',
|
||||
'options' => array(
|
||||
'nom' => "jeton",
|
||||
'label' => _T('graphql:titre_configurer_jeton'),
|
||||
'readonly' => 'oui',
|
||||
'afficher_si' => '@api_key_active@ == "oui"',
|
||||
'afficher_si_avec_post' => 'oui',
|
||||
),
|
||||
);
|
||||
|
||||
return $saisies;
|
||||
}
|
||||
|
||||
function formulaires_configurer_jeton_charger_dist() {
|
||||
$valeurs = array();
|
||||
$jeton = lire_config('/meta_graphql/jeton', "");
|
||||
|
||||
if (empty($jeton)) {
|
||||
$valeurs["api_key_active"] = "non";
|
||||
$valeurs["jeton"] = graphql_generate_key();
|
||||
} else {
|
||||
$valeurs["api_key_active"] = "oui";
|
||||
$valeurs["jeton"] = $jeton;
|
||||
}
|
||||
|
||||
return $valeurs;
|
||||
}
|
||||
|
||||
function formulaires_configurer_jeton_traiter_dist() {
|
||||
$ret = array();
|
||||
$choix = _request("api_key_active") ? _request("api_key_active") : "";
|
||||
$jeton = ($choix) ? _request("jeton") : "";
|
||||
|
||||
if (ecrire_config('/meta_graphql/jeton', $jeton)) {
|
||||
$ret['message_ok'] = _T('config_info_enregistree');
|
||||
} else {
|
||||
$ret['message_erreur'] = _T('erreur_technique_enregistrement_impossible');
|
||||
}
|
||||
// $ret['editable'] = true;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
function graphql_generate_key() {
|
||||
$d = time() * 1000;
|
||||
|
||||
if (function_exists('hrtime')) {
|
||||
$hrtime = hrtime();
|
||||
$d += round($hrtime[1] / 1000000);
|
||||
} elseif (function_exists('microtime')) {
|
||||
$microtime = microtime(true);
|
||||
$d += round($microtime * 1000);
|
||||
}
|
||||
|
||||
$uuid = sprintf('%08s-%04s-4%03x-%04x-%012s', substr($d, -8), substr($d, -12, 4), mt_rand(0, 0xfff), mt_rand(0, 0x3fff) | 0x8000, bin2hex(random_bytes(6)));
|
||||
|
||||
return $uuid;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<div class="formulaire_spip formulaire_configurer formulaire_#FORM">
|
||||
<h3 class="titrem"><:graphql:cfg_meta:></h3>
|
||||
[<p class="reponse_formulaire reponse_formulaire_ok">(#ENV*{message_ok})</p>]
|
||||
[<p class="reponse_formulaire reponse_formulaire_erreur">(#ENV*{message_erreur})</p>]
|
||||
<form method="post" action="#ENV{action}">
|
||||
<div>
|
||||
#ACTION_FORMULAIRE
|
||||
<div class="editer-groupe">
|
||||
#GENERER_SAISIES{#ENV{_saisies}}
|
||||
</div>
|
||||
<input type="hidden" name="_meta_table" value="meta_graphql" />
|
||||
<p class="boutons">
|
||||
<span class="image_loading"> </span>
|
||||
<input type="submit" class="submit" value="<:bouton_enregistrer:>" />
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
include_spip('base/objets');
|
||||
|
||||
function formulaires_configurer_meta_saisies_dist() {
|
||||
$saisies = [];
|
||||
$saisies[] = [
|
||||
'saisie' => 'checkbox',
|
||||
'options' => [
|
||||
'nom' => 'meta',
|
||||
'label' => _T('graphql:options_spip'),
|
||||
'data' => [
|
||||
'email_webmaster' => _T('email_webmaster'),
|
||||
'nom_site' => _T('nom_site'),
|
||||
'slogan_site' => _T('slogan_site'),
|
||||
'adresse_site' => _T('adresse_site'),
|
||||
'descriptif_site' => _T('descriptif_site'),
|
||||
],
|
||||
],
|
||||
];
|
||||
return $saisies;
|
||||
}
|
||||
|
||||
function formulaires_configurer_meta_charger_dist() {
|
||||
$valeurs = [];
|
||||
$meta = lire_config('/meta_graphql/meta', "");
|
||||
|
||||
$valeurs["meta"] = $meta;
|
||||
|
||||
return $valeurs;
|
||||
}
|
||||
|
||||
function formulaires_configurer_meta_traiter_dist() {
|
||||
$ret = [];
|
||||
$metas = is_null(_request('meta')) ? [] : _request('meta');
|
||||
|
||||
if (ecrire_config('/meta_graphql/meta', $metas)) {
|
||||
$ret['message_ok'] = _T('config_info_enregistree');
|
||||
} else {
|
||||
$ret['message_erreur'] = _T('erreur_technique_enregistrement_impossible');
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
include_once _DIR_PLUGIN_GRAPHQL . 'vendor/autoload.php';
|
||||
|
||||
/**
|
||||
* Fonction d'installation et de mise à jour du plugin
|
||||
*
|
||||
* Vous pouvez :
|
||||
*
|
||||
* - créer la structure SQL,
|
||||
* - insérer du pre-contenu,
|
||||
* - installer des valeurs de configuration,
|
||||
* - mettre à jour la structure SQL
|
||||
*
|
||||
* @param string $nom_meta_base_version
|
||||
* Nom de la meta informant de la version du schéma de données du plugin installé dans SPIP
|
||||
* @param string $version_cible
|
||||
* Version du schéma de données dans ce plugin (déclaré dans paquet.xml)
|
||||
* @return void
|
||||
**/
|
||||
function graphql_upgrade($nom_meta_base_version, $version_cible) {
|
||||
$maj = array();
|
||||
// Création de la table qui stockera la config du plugin
|
||||
$maj['create'][] = array('installer_table_meta', 'meta_graphql');
|
||||
|
||||
// Enregistrement de valeurs par défaut dans la table de config du plugin
|
||||
$maj['create'][] = array('ecrire_config', '/meta_graphql/jeton', '');
|
||||
|
||||
include_spip('base/upgrade');
|
||||
maj_plugin($nom_meta_base_version, $version_cible, $maj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction de désinstallation du plugin.
|
||||
*
|
||||
* Vous devez :
|
||||
*
|
||||
* - nettoyer toutes les données ajoutées par le plugin et son utilisation
|
||||
* - supprimer les tables et les champs créés par le plugin.
|
||||
*
|
||||
* @param string $nom_meta_base_version
|
||||
* Nom de la meta informant de la version du schéma de données du plugin installé dans SPIP
|
||||
* @return void
|
||||
**/
|
||||
function graphql_vider_tables($nom_meta_base_version) {
|
||||
// Suppression de la table qui stocke la config du plugin
|
||||
sql_drop_table('spip_meta_graphql');
|
||||
// Effacement de la version du schema du plugin dans la table spip_meta
|
||||
effacer_meta($nom_meta_base_version);
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Obligatoire
|
||||
function graphql_autoriser() {
|
||||
}
|
||||
|
||||
function autoriser_configurergraphql_menu_dist($faire, $type, $id, $qui, $opt) {
|
||||
return ($qui['webmestre'] == 'oui');
|
||||
}
|
||||
|
||||
function autoriser_graphql_dist($faire, $type, $id, $qui, $opt) {
|
||||
return ($qui['webmestre'] == 'oui');
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
function graphql_verif_jeton($headers) {
|
||||
$token_param = lire_config('/meta_graphql/jeton', '');
|
||||
if ($token_param == '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$token_envoye = '';
|
||||
if (isset($headers["X_AUTH_TOKEN"])) {
|
||||
$token_envoye = $headers["X_AUTH_TOKEN"];
|
||||
}
|
||||
|
||||
return ($token_envoye == $token_param);
|
||||
}
|
||||
|
||||
function graphql_getCollectionInfos(string $collection): array {
|
||||
// spip_articles
|
||||
$infos['table_collection'] = table_objet_sql($collection);
|
||||
// infos SQL de la table
|
||||
$infos['table_infos'] = lister_tables_objets_sql($infos['table_collection']);
|
||||
// Champs SQL de la table
|
||||
$infos['champs'] = $infos['table_infos']['field'];
|
||||
// id_article
|
||||
$infos['champ_id'] = id_table_objet($infos['table_collection']);
|
||||
// article
|
||||
$infos['type_objet'] = objet_type($collection);
|
||||
// Article
|
||||
$infos['nameObjet'] = ucfirst($infos['type_objet']);
|
||||
|
||||
return $infos;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
define('GRAPHQL_COLLECTIONS_NON_OBJET', ['auteurs', 'syndic', 'documents']);
|
||||
define('GRAPHQL_CHAMPS_COMMUNS', ['id', 'slug', 'typeCollection', 'points', 'rang']);
|
||||
define('GRAPHQL_CHAMPS_INTERFACE_OBJET', ['titre', 'texte', 'maj']);
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
function graphql_header_prive($flux) {
|
||||
$flux .= recuperer_fond('prive/js/graphql');
|
||||
$flux .= '<script
|
||||
src="https://unpkg.com/react@17/umd/react.development.js"
|
||||
integrity="sha512-Vf2xGDzpqUOEIKO+X2rgTLWPY+65++WPwCHkX2nFMu9IcstumPsf/uKKRd5prX3wOu8Q0GBylRpsDB26R6ExOg=="
|
||||
crossorigin="anonymous"></script>';
|
||||
$flux .= '<script
|
||||
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
|
||||
integrity="sha512-Wr9OKCTtq1anK0hq5bY3X/AvDI5EflDSAh0mE9gma+4hl+kXdTJPKZ3TwLMBcrgUeoY0s3dq9JjhCQc7vddtFg=="
|
||||
crossorigin="anonymous"></script>';
|
||||
$flux .= '<link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />';
|
||||
return $flux;
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$GLOBALS[$GLOBALS['idx_lang']] = array(
|
||||
// A
|
||||
'activer' => 'Activer',
|
||||
'activer_api_key' => 'Clé API',
|
||||
'aucune' => 'Aucune',
|
||||
|
||||
// C
|
||||
"cfg_api" => 'Sélectionner les collections à exposer sur l\'API',
|
||||
'cfg_collections_liees' => 'Sélectionner les collections liées à exposer',
|
||||
'cfg_general' => 'Réglagles généraux',
|
||||
'cfg_jeton' => 'Sécurisez votre API',
|
||||
'cfg_meta' => 'Sélectionner les metas à exposer sur l\'API',
|
||||
'champs_objet' => 'Sélectionner les champs que vous souhaitez exposer sur l\'API',
|
||||
'collection' => 'Collection',
|
||||
'copier' => 'Copier dans le presse-papier',
|
||||
|
||||
// D
|
||||
'debug_titre' => 'Mode Debug',
|
||||
'debug_desc' => 'Afficher les retours d\'erreur',
|
||||
'desc_arg_id' => 'ID de l\'objet',
|
||||
'desc_arg_page' => 'Page retournée pour la pagination',
|
||||
'desc_arg_pagination' => 'Nombre d\'objets maximal retournés',
|
||||
'desc_arg_texte' => 'Votre recherche',
|
||||
'desc_arg_where' => 'Clause WHERE de votre requête sous forme de tableau (AND)',
|
||||
'desc_query_collection' => 'Retourne une collection d\'objets',
|
||||
'desc_query_collections' => 'Retourne la liste des collections disponibles',
|
||||
'desc_query_getmeta' => 'Retourne les métas autorisées',
|
||||
'desc_query_objet' => 'Retourne un objet',
|
||||
'desc_query_recherche' => 'Résultats de recherche sur les objets',
|
||||
'desc_type_base' => 'Type de base permettant de partager des champs',
|
||||
'desc_type_collection' => 'Énumération des collections disponibles',
|
||||
'desc_type_date' => 'Format : AAAA-MM-JJ HH:MM:SS',
|
||||
'desc_type_interface' => 'Un objet éditorial de SPIP',
|
||||
'desc_type_metalist' => 'Métas autorisées',
|
||||
'desc_type_objet' => 'Un objet',
|
||||
'desc_type_query' => 'Liste des requêtes disponibles',
|
||||
'desc_type_searchresult' => 'Résultats de recherche',
|
||||
|
||||
// E
|
||||
'erreur_cache' => 'Le schéma d\'introspection est absent. Veuillez exposer des données',
|
||||
'erreur_clause_where' => 'Erreur dans la clause where',
|
||||
'erreur_pas_requete' => 'Aucune requête',
|
||||
'erreur_jeton' => 'Mauvais Jeton API',
|
||||
|
||||
// G
|
||||
'generate' => 'Re-générer',
|
||||
|
||||
// O
|
||||
'options_spip' => 'Options SPIP',
|
||||
|
||||
// P
|
||||
'pagination' => 'Pagination',
|
||||
|
||||
// S
|
||||
'select_all' => 'Tout sélectionner',
|
||||
|
||||
// T
|
||||
'titre_configurer_jeton' => 'Jeton API',
|
||||
'titre_graphiql' => 'GraphiQL IDE',
|
||||
'titre_graphql_configurer' => 'Réglages',
|
||||
'titre_graphql_expositions' => 'Données à exposer',
|
||||
'titre_graphql_jeton' => 'Jeton API',
|
||||
'titre_page_configurer_graphql' => 'GraphQL',
|
||||
'type_meta_desc' => 'Retourne les metas autorisées'
|
||||
);
|
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
if (!defined('_ECRIRE_INC_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$GLOBALS[$GLOBALS['idx_lang']] = array(
|
||||
|
||||
// C
|
||||
'graphql_nom' => 'SPIP GraphQL',
|
||||
'graphql_slogan' => 'Permet d\'exposer un endpoint GraphQL',
|
||||
);
|
@ -0,0 +1,27 @@
|
||||
<paquet
|
||||
prefix="graphql"
|
||||
categorie="outil"
|
||||
version="0.1.0"
|
||||
etat="dev"
|
||||
compatibilite="[4.0.0;4.2.*]"
|
||||
logo="prive/themes/spip/images/graphql-64.png"
|
||||
documentation=""
|
||||
schema="1.0.0"
|
||||
>
|
||||
<nom>Endpoint graphQL</nom>
|
||||
|
||||
<auteur mail="paidge_cs@hotmail.com">Pierre-jean CHANCELLIER</auteur>
|
||||
|
||||
<licence>GNU/GPL v3</licence>
|
||||
|
||||
<necessite nom="saisies" compatibilite="[4.7.1;]" />
|
||||
|
||||
<menu nom="graphql" titre="graphql:titre_page_configurer_graphql" icone="images/graphql-16.png" parent="menu_publication" action="graphiql" />
|
||||
|
||||
<onglet nom="graphql_ide" titre="graphql:titre_graphiql" icone="" parent="graphql" action="graphiql" />
|
||||
<onglet nom="graphql_configurer" titre="graphql:titre_graphql_configurer" icone="" parent="graphql" action="configurer_graphql" />
|
||||
<onglet nom="graphql_expositions" titre="graphql:titre_graphql_expositions" icone="" parent="graphql" action="graphql_expositions" />
|
||||
|
||||
<pipeline nom="header_prive" inclure="graphql_pipelines.php" />
|
||||
<pipeline nom="autoriser" inclure="graphql_autorisations.php" />
|
||||
</paquet>
|
@ -0,0 +1,70 @@
|
||||
<script type="text/javascript">
|
||||
// Blocs dépliables
|
||||
$(function() {
|
||||
$('.formulaire_configurer_collections .deplier_collection').on('click', function(e) {
|
||||
e.preventDefault()
|
||||
const collection = $(this).data('collection')
|
||||
$('.config_' + collection + ' > fieldset').toggleClass('ouvert')
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
function generateUUID() {
|
||||
var d = new Date().getTime()
|
||||
|
||||
if (window.performance && typeof window.performance.now === "function") {
|
||||
d += performance.now()
|
||||
}
|
||||
|
||||
var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
var r = (d + Math.random() * 16) % 16 | 0
|
||||
d = Math.floor(d / 16)
|
||||
return (c == "x" ? r : (r & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
function apiKeyAction(e, action) {
|
||||
var jetonField = document.getElementById("champ_jeton")
|
||||
var tooltip = $(e.target).children("span.tooltiptext")[0]
|
||||
|
||||
switch (action) {
|
||||
case "copy":
|
||||
jetonField.select()
|
||||
jetonField.setSelectionRange(0, 99999) // For mobile devices
|
||||
|
||||
if (window.isSecureContext && navigator.clipboard) {
|
||||
navigator.clipboard.writeText(content)
|
||||
} else {
|
||||
document.execCommand("copy")
|
||||
}
|
||||
$(tooltip).text("<?=_T('graphql:copied') ?>")
|
||||
break
|
||||
case "generate":
|
||||
var text = generateUUID()
|
||||
$(jetonField).val(text)
|
||||
$(tooltip).text("<?=_T('graphql:generated') ?>")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function outapiKeyAction(e, action) {
|
||||
var tooltip = $(e.target).children("span.tooltiptext")[0]
|
||||
|
||||
switch (action) {
|
||||
case "copy":
|
||||
$(tooltip).text("<?=_T('graphql:copier') ?>")
|
||||
break
|
||||
case "generate":
|
||||
$(tooltip).text("<?=_T('graphql:generate') ?>")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllFields(e) {
|
||||
e.preventDefault()
|
||||
$(e.target).parentsUntil("fieldset").find("select option").prop("selected", true)
|
||||
}
|
||||
</script>
|
@ -0,0 +1,4 @@
|
||||
[(#AUTORISER{graphql}|sinon_interdire_acces)]
|
||||
[(#VAL{graphql}|barre_onglets{graphql_configurer})]
|
||||
<div class="ajax">#FORMULAIRE_CONFIGURER_JETON</div>
|
||||
<div class="ajax">#FORMULAIRE_CONFIGURER_GRAPHQL</div>
|
@ -0,0 +1,17 @@
|
||||
[(#AUTORISER{graphql}|sinon_interdire_acces)]
|
||||
[(#VAL{graphql}|barre_onglets{graphql_ide})]
|
||||
<div id="graphiql">Loading...</div>
|
||||
<script
|
||||
src="https://unpkg.com/graphiql/graphiql.min.js"
|
||||
type="application/javascript"></script>
|
||||
<script>
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: GraphiQL.createFetcher({
|
||||
url: "/spip.php?action=graphql",
|
||||
}),
|
||||
defaultEditorToolsVisibility: true,
|
||||
}),
|
||||
document.getElementById("graphiql")
|
||||
)
|
||||
</script>
|
@ -0,0 +1,5 @@
|
||||
[(#AUTORISER{graphql}|sinon_interdire_acces)]
|
||||
[(#VAL{graphql}|barre_onglets{graphql_expositions})]
|
||||
<div>#FORMULAIRE_CONFIGURER_COLLECTIONS</div>
|
||||
<div class="ajax">#FORMULAIRE_CONFIGURER_META</div>
|
||||
|
@ -0,0 +1 @@
|
||||
#LARGEUR_ECRAN{pleine_largeur}
|
@ -0,0 +1,152 @@
|
||||
[(#REM)
|
||||
|
||||
Ce squelette definit les styles de l'espace prive
|
||||
|
||||
Note: l'entete "Vary:" sert a repousser l'entete par
|
||||
defaut "Vary: Cookie,Accept-Encoding", qui est (un peu)
|
||||
genant en cas de "rotation du cookie de session" apres
|
||||
un changement d'IP (effet de clignotement).
|
||||
|
||||
ATTENTION: il faut absolument le charset sinon Firefox croit que
|
||||
c'est du text/html !
|
||||
<style>
|
||||
]
|
||||
#CACHE{3600*100,cache-client}
|
||||
#HTTP_HEADER{Content-Type: text/css; charset=iso-8859-15}
|
||||
#HTTP_HEADER{Vary: Accept-Encoding}
|
||||
|
||||
|
||||
#graphiql {
|
||||
min-height: 450px;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
#graphiql .graphiql-doc-explorer-argument,
|
||||
#graphiql .graphiql-doc-explorer-item {
|
||||
background: var(--spip-color-theme-lightest);
|
||||
padding: 0.5rem 1rem;
|
||||
margin-top: 5px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.graphiql-markdown-description {
|
||||
font-style: italic;
|
||||
color: hsla(var(--spip-color-theme-light--hsl),1);
|
||||
}
|
||||
|
||||
/* Containers */
|
||||
#graphiql .graphiql-container {
|
||||
--color-primary: var(--spip-color-theme-dark--hsl);
|
||||
--editor-background: hsla(var(--spip-color-theme-light--hsl),var(--alpha-background-light));
|
||||
}
|
||||
|
||||
#graphiql .graphiql-container .graphiql-sessions {
|
||||
--color-neutral: var(--spip-color-theme-darker--hsl);
|
||||
}
|
||||
|
||||
/* Boutons */
|
||||
#graphiql .graphiql-tab.graphiql-tab-active {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
#graphiql button.graphiql-tab-button {
|
||||
padding: var(--px-4) var(--px-8);
|
||||
}
|
||||
|
||||
#graphiql button.graphiql-un-styled {
|
||||
background-color: var(--spip-color-gray-lighter);
|
||||
color: var(--spip-color-theme-dark);
|
||||
}
|
||||
|
||||
#graphiql button.graphiql-un-styled.active {
|
||||
background-color: var(--spip-color-theme-dark);
|
||||
color: var(--spip-color-white);
|
||||
}
|
||||
|
||||
/* svg icons */
|
||||
#graphiql button.graphiql-tab-add > svg,
|
||||
#graphiql .graphiql-container .graphiql-chevron-icon,
|
||||
#graphiql .graphiql-toolbar-icon {
|
||||
--color-neutral: var(--spip-color-theme-dark--hsl);
|
||||
}
|
||||
|
||||
/* Editeur de texte */
|
||||
#graphiql .cm-s-graphiql .cm-punctuation {
|
||||
--color-neutral: var(--spip-color-theme--80);
|
||||
}
|
||||
|
||||
#graphiql .cm-s-graphiql .CodeMirror-linenumber {
|
||||
--color-neutral: var(--spip-color-theme--70);
|
||||
}
|
||||
|
||||
.loading * {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.api_key_buttons > .btn > svg {
|
||||
pointer-events: none;
|
||||
fill: white;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext {
|
||||
visibility: hidden;
|
||||
width: 140px;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
bottom: 150%;
|
||||
left: 50%;
|
||||
margin-left: -75px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #555 transparent transparent transparent;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.formulaire_spip div.choix.choix_actif {
|
||||
border: 0;
|
||||
padding: 0 0 0 var(--spip-form-input-padding-x);
|
||||
}
|
||||
|
||||
/* Boîtes dépliables */
|
||||
.formulaire_configurer_collections .avec_sous_saisies > fieldset {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 1s ease;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.formulaire_configurer_collections .avec_sous_saisies > fieldset.ouvert {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.formulaire_configurer_collections .avec_sous_saisies > fieldset > .editer-groupe {
|
||||
overflow: hidden;
|
||||
}
|
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 581 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,3 @@
|
||||
[<button class="deplier_collection" data-collection="#ENV{collection}">
|
||||
<:graphql:collection:> (#ENV{collection}|majuscules)
|
||||
</button>]
|
@ -0,0 +1 @@
|
||||
<button class="btn" onclick="selectAllFields(event)"><:graphql:select_all:></button>
|
@ -0,0 +1,4 @@
|
||||
<p class="boutons">
|
||||
<span class="image_loading"> </span>
|
||||
<input type="submit" class="submit" value="<:bouton_enregistrer:>" />
|
||||
</p>
|
@ -0,0 +1,11 @@
|
||||
Exposer dynamiquement chaque collection, ainsi que ses champs, sélectionnés dans le navigateur.
|
||||
|
||||
|
||||
- Le fichier `SchemaSPIP.php` gère toute la logique de construction du schéma
|
||||
- Le fichier `ReponseSPIP.php` gère les fonctions utilisées dans les réponses.
|
||||
|
||||
## TODO
|
||||
- [x] gérer les collections liées
|
||||
- [x] Voir pour la requête concernant la recherche
|
||||
- [x] créer un type de base pour tous les objets éditoriaux avec les champs en commun
|
||||
- [x] Voir pour mettre en place le schéma en [lazy loading](https://webonyx.github.io/graphql-php/schema-definition/#lazy-loading-of-types) grâce à un `TypeRegistry`
|
@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPIP\GraphQL;
|
||||
|
||||
class ReponseSPIP {
|
||||
public static function findMeta(): array {
|
||||
$metas = lire_config("/meta_graphql/meta", []);
|
||||
$where = "nom IN ('" . implode("','", $metas) . "')";
|
||||
$liste_meta_publique = sql_allfetsel("nom, valeur", "spip_meta", $where);
|
||||
|
||||
$retourMeta = [];
|
||||
if (!empty($liste_meta_publique)) {
|
||||
foreach ($liste_meta_publique as $meta) {
|
||||
$retourMeta[$meta["nom"]] = $meta["valeur"];
|
||||
}
|
||||
}
|
||||
|
||||
return $retourMeta;
|
||||
}
|
||||
|
||||
public static function recherche(string $texte): array {
|
||||
$retour = [];
|
||||
$collections_autorisees = lire_config('/meta_graphql/objets_editoriaux');
|
||||
|
||||
foreach ($collections_autorisees as $collection => $config) {
|
||||
$result = self::searchCollection($collection, $texte);
|
||||
|
||||
$retour = array_merge(
|
||||
$retour,
|
||||
$result
|
||||
);
|
||||
}
|
||||
|
||||
return $retour;
|
||||
}
|
||||
|
||||
public static function afficheCollections(): array {
|
||||
$collections_autorisees = lire_config('/meta_graphql/objets_editoriaux');
|
||||
$reponse = [];
|
||||
|
||||
foreach ($collections_autorisees as $collection => $config) {
|
||||
$reponse[] = strtoupper($collection);
|
||||
}
|
||||
return $reponse;
|
||||
}
|
||||
|
||||
// Récupération d'une collection d'objets
|
||||
public static function findCollection(string $collection, array $where, int $pagination, int $page = 0): ?array {
|
||||
$retour_objets = [];
|
||||
$table = table_objet_sql($collection);
|
||||
|
||||
$offset = $page * $pagination;
|
||||
|
||||
$objets = sql_allfetsel(
|
||||
self::getSelect($table),
|
||||
$table . ' AS collection',
|
||||
self::getWhere($table, $where),
|
||||
'',
|
||||
'',
|
||||
"$offset,$pagination"
|
||||
);
|
||||
|
||||
foreach ($objets as $objet) {
|
||||
$retour_objets[] = self::findObjet((int) $objet['id'], $collection, $objet);
|
||||
}
|
||||
|
||||
return $retour_objets;
|
||||
}
|
||||
|
||||
// Récupération d'un objet
|
||||
public static function findObjet(int $id, string $type_objet, array $objet = []): ?array {
|
||||
include_spip('inc/filtres');
|
||||
|
||||
$collection = table_objet($type_objet);
|
||||
|
||||
if (preg_match('#^get(\w+)$#', $collection, $matches)) {
|
||||
$collection = strtolower($matches[1]);
|
||||
}
|
||||
|
||||
$type_objet = objet_type($collection);
|
||||
$champ_id = id_table_objet($collection);
|
||||
|
||||
if (empty($objet)) {
|
||||
$table = table_objet_sql($collection);
|
||||
$where = [$champ_id . "=" . $id];
|
||||
$objet = sql_fetsel(
|
||||
self::getSelect($table),
|
||||
$table . ' AS collection',
|
||||
self::getWhere($table, $where)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$objet) return [];
|
||||
|
||||
// Champ de base pour créer le champ slug
|
||||
$slug_origin = self::champSlug($collection);
|
||||
|
||||
// Récupération des champs
|
||||
foreach ($objet as $champ => $value) {
|
||||
switch ($champ) {
|
||||
case $champ_id:
|
||||
$objet['id'] = $value;
|
||||
break;
|
||||
case "titre":
|
||||
$objet[$champ] = supprimer_numero($value);
|
||||
$objet['rang'] = (preg_match("#^([0-9]+)[.][[:space:]]#", $value, $matches)) ?
|
||||
$matches[1] : '0';
|
||||
break;
|
||||
case "fichier":
|
||||
$objet[$champ] = url_absolue(_DIR_IMG . $value);
|
||||
break;
|
||||
case "saisies":
|
||||
// TODO : transformer le tableau de saisies en html
|
||||
$objet[$champ] = $value;
|
||||
break;
|
||||
case "texte":
|
||||
case "surtitre":
|
||||
case "soustitre":
|
||||
case "descriptif":
|
||||
case "chapo":
|
||||
case "bio":
|
||||
case "credits":
|
||||
// Transformation des liens et des balises
|
||||
$objet[$champ] = liens_absolus(trim(str_replace("\n", '<br>', propre($value))));
|
||||
break;
|
||||
// Gestion des laisions SQL 1 => N
|
||||
case "id_secteur":
|
||||
$objet['secteur'] = ($collection != 'rubriques' || $value != $id) ?
|
||||
self::findObjet((int) $value, 'rubrique') :
|
||||
null;
|
||||
break;
|
||||
case "id_rubrique":
|
||||
$objet['rubrique'] = self::findObjet((int) $value, 'rubrique');
|
||||
break;
|
||||
case "id_trad":
|
||||
case "id_parent":
|
||||
$champ = preg_replace('#(id_)#', '', $champ);
|
||||
$objet[$champ] = ($value !== 0) ? self::findObjet((int) $value, $type_objet) : null;
|
||||
break;
|
||||
case "id_vignette":
|
||||
$objet['vignette'] = ($value !== 0) ? self::findObjet((int) $value, 'document') : null;
|
||||
break;
|
||||
case "id_groupe":
|
||||
$objet['groupe'] = ($value !== 0) ? self::findObjet((int) $value, 'groupe_mots') : null;
|
||||
break;
|
||||
default:
|
||||
$objet[$champ] = $value;
|
||||
break;
|
||||
}
|
||||
|
||||
// Récupération du slug de l'objet
|
||||
if ($champ == $slug_origin) {
|
||||
$objet['slug'] = identifiant_slug(supprimer_numero($value));
|
||||
}
|
||||
}
|
||||
|
||||
$objet['typeCollection'] = strtoupper($collection);
|
||||
|
||||
// Récupération du logo de l'objet s'il existe
|
||||
$chercher_logo = charger_fonction('chercher_logo', 'inc');
|
||||
if ($logo = $chercher_logo($id, $type_objet, 'on')) {
|
||||
if (!is_null($logo[0])) {
|
||||
$objet['logo'] = lire_config("adresse_site") . "/" . $logo[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Récupération des collections liées (liaisons SQL N => N)
|
||||
$collections_autorisees = lire_config("/meta_graphql/objets_editoriaux");
|
||||
$config_collection = $collections_autorisees[table_objet($type_objet)];
|
||||
|
||||
$liaisons_config = (array_key_exists('liaisons', $config_collection) &&
|
||||
is_array($config_collection['liaisons'])) ? $config_collection['liaisons'] : [];
|
||||
|
||||
foreach ($liaisons_config as $collection_liee) {
|
||||
include_spip('action/editer_liens');
|
||||
$objet[$collection_liee] = [];
|
||||
$table_liee = table_objet_sql($collection_liee);
|
||||
$type_objet_lie = objet_type($table_liee);
|
||||
$cle_collection_liee = id_table_objet($table_liee);
|
||||
|
||||
if (
|
||||
array_key_exists($collection_liee, $collections_autorisees)
|
||||
&& $liaison_col = objet_trouver_liens([$type_objet_lie => '*'], [$type_objet => $id])
|
||||
) {
|
||||
|
||||
foreach ($liaison_col as $l) {
|
||||
$ids[] = $l[$cle_collection_liee];
|
||||
}
|
||||
|
||||
$where = [sql_in($cle_collection_liee, $ids)];
|
||||
|
||||
$objet[$collection_liee] = self::findCollection(
|
||||
$collection_liee,
|
||||
$where,
|
||||
(int) $collections_autorisees[$collection_liee]['pagination']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $objet;
|
||||
}
|
||||
|
||||
public static function searchCollection(string $collection, string $recherche, array $where = []): array {
|
||||
include_spip('inc/prepare_recherche');
|
||||
include_spip('inc/filtres');
|
||||
|
||||
$retour_recherche = [];
|
||||
|
||||
// Préparation et exécution de la requête
|
||||
$table = table_objet_sql($collection);
|
||||
$select = self::getSelect($table);
|
||||
$prepare_requete = inc_prepare_recherche_dist($recherche, $table);
|
||||
$select[] = $prepare_requete[0];
|
||||
$from = [$table . ' as collection', table_objet_sql("resultats") . " as resultats",];
|
||||
$where[] = $prepare_requete[1];
|
||||
$where[] = 'collection.' . id_table_objet($table) . '=resultats.id';
|
||||
$result = sql_allfetsel($select, $from, $where, '', ['points DESC']);
|
||||
|
||||
foreach ($result as $objet) {
|
||||
$retour_recherche[] = self::findObjet((int) $objet['id'], $collection, $objet);
|
||||
}
|
||||
|
||||
return $retour_recherche;
|
||||
}
|
||||
|
||||
public static function champSlug(string $collection) {
|
||||
switch ($collection) {
|
||||
case "auteurs":
|
||||
return "nom";
|
||||
break;
|
||||
case "syndic":
|
||||
return "nom_site";
|
||||
break;
|
||||
default:
|
||||
return "titre";
|
||||
}
|
||||
}
|
||||
|
||||
private static function getSelect(string $table): array {
|
||||
$select = ['collection.' . id_table_objet($table) . ' as id'];
|
||||
|
||||
if (!in_array(table_objet($table), GRAPHQL_COLLECTIONS_NON_OBJET)) {
|
||||
foreach (GRAPHQL_CHAMPS_INTERFACE_OBJET as $champ_interface) {
|
||||
$select[] = 'collection.' . $champ_interface;
|
||||
}
|
||||
}
|
||||
|
||||
$select = array_merge(
|
||||
$select,
|
||||
lire_config('/meta_graphql/objets_editoriaux/' . table_objet($table) . '/champs', [])
|
||||
);
|
||||
|
||||
return $select;
|
||||
}
|
||||
|
||||
private static function getWhere(string $table, array $where = []): array {
|
||||
$table_infos = lister_tables_objets_sql($table);
|
||||
|
||||
$retour_where = [];
|
||||
if (!empty($where)) {
|
||||
// Format des dates : 2023-05-19 11:00:00
|
||||
foreach ($where as $clause) {
|
||||
if (preg_match('#^(\w+)(=|<|>|<>)((?:\w+|\s|:|-)+)$#', $clause, $matches)) {
|
||||
$retour_where[] = 'collection.' . $matches[1] . $matches[2] . sql_quote($matches[3]);
|
||||
} else {
|
||||
$retour_where[] = $clause;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($table != "spip_auteurs" && array_key_exists("statut", $table_infos["field"])) {
|
||||
$retour_where[] = "collection.statut=" . sql_quote('publie');
|
||||
}
|
||||
|
||||
return $retour_where;
|
||||
}
|
||||
}
|
@ -0,0 +1,397 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPIP\GraphQL;
|
||||
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
|
||||
|
||||
// Permet de créer des types ré-utilisables dans l'API
|
||||
// https://webonyx.github.io/graphql-php/schema-definition/#lazy-loading-of-types
|
||||
class SchemaSPIP {
|
||||
public array $collections_autorisees = [];
|
||||
public array $metas_autorisees = [];
|
||||
|
||||
private array $types = [];
|
||||
|
||||
public function __construct() {
|
||||
$this->collections_autorisees = lire_config('/meta_graphql/objets_editoriaux', []);
|
||||
$this->metas_autorisees = lire_config('/meta_graphql/meta', []);
|
||||
}
|
||||
|
||||
// Query, Date, MetaList, Article, SearchResult...
|
||||
public function get(string $name): Type {
|
||||
return $this->types[$name] ??= $this->getType($name);
|
||||
}
|
||||
|
||||
// Pour créer un nouveau Type de données, c'est par ici
|
||||
private function getType(string $name) {
|
||||
$typeDefinition = ['name' => $name];
|
||||
|
||||
switch ($name) {
|
||||
case 'Node':
|
||||
// Interface Objet pour mutualiser des champs entre objets
|
||||
$typeDefinition['description'] = _T('graphql:desc_type_base');
|
||||
|
||||
$typeDefinition['fields'] = function () {
|
||||
return $this->champsCommuns(GRAPHQL_CHAMPS_COMMUNS);
|
||||
};
|
||||
|
||||
$typeDefinition['resolveType'] = function ($value, $context, ResolveInfo $info) {
|
||||
// TODO : écrire le resolveType
|
||||
// switch ($info->fieldDefinition->name ?? null) {
|
||||
// case 'human': return MyTypes::human();
|
||||
// case 'droid': return MyTypes::droid();
|
||||
// default: throw new Exception("Unknown Character type: {$value->type ?? null}");
|
||||
// }
|
||||
};
|
||||
return new InterfaceType($typeDefinition);
|
||||
break;
|
||||
break;
|
||||
case 'Collection':
|
||||
// Liste des collections exposées
|
||||
$typeDefinition['description'] = _T('graphql:desc_type_collection');
|
||||
|
||||
foreach ($this->collections_autorisees as $collection => $config) {
|
||||
$typeDefinition['values'][] = strtoupper($collection);
|
||||
}
|
||||
|
||||
return new EnumType($typeDefinition);
|
||||
break;
|
||||
case 'Date':
|
||||
// Type scalaire personnalisé
|
||||
// https://webonyx.github.io/graphql-php/type-definitions/scalars/#writing-custom-scalar-types
|
||||
// AAAA-MM-JJ HH:MM:SS
|
||||
$typeDefinition['description'] = _T('graphql:desc_type_date');
|
||||