src/Controller/Plugin/PluginConfigController.php line 173

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Plugin;
  3. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  4. use Symfony\Component\HttpFoundation\JsonResponse;
  5. use Symfony\Component\HttpFoundation\Request;
  6. use Symfony\Component\Routing\Annotation\Route;
  7. /**
  8.  * Sert dynamiquement le config.json de chaque plugin Euro-Office.
  9.  *
  10.  * Avantage vs config.json statique : on injecte la section/workroom UUID
  11.  * dans l'URL de l'iframe du plugin (`variations[].url`) en query string,
  12.  * pour que le plugin lise son contexte sans postMessage cross-origin.
  13.  *
  14.  * REGISTRY : source unique de vérité pour la liste des plugins Labomega.
  15.  *  - slug : nom du dossier sous /public/plugins/
  16.  *  - featureKey : clé FeatureAccess (FeatureAccessService) → permet le
  17.  *    filtrage par utilisateur/rôle dans EditorConfigBuilder.
  18.  *  - guid : identifiant interne OnlyOffice (asc.{slug})
  19.  *  - size : taille [largeur, hauteur] de l'iframe en mode visuel
  20.  */
  21. class PluginConfigController extends AbstractController
  22. {
  23.     public const REGISTRY = [
  24.         // Plugin "Tags sémantiques" retiré (mai 2026) — les couleurs des
  25.         // marques ne s'affichaient pas dans OnlyOffice (limitation de
  26.         // l'éditeur sur les surlignages custom hors-DOCX) → UX confuse.
  27.         // Le code backend (SemanticTagPluginController, table
  28.         // semantic_annotation) reste en place : si la feature revient un
  29.         // jour avec un mode d'affichage natif, on remet juste le bloc ici.
  30.         'labomega-zotero' => [
  31.             'name' => 'Zotero Labomega',
  32.             'guid' => 'asc.{labomega-zotero}',
  33.             'description' => 'Recherche & insertion de références Zotero',
  34.             'featureKey' => 'plugin.zotero',
  35.             'size' => [400700],
  36.         ],
  37.         'labomega-evaluation' => [
  38.             'name' => 'Rapport d\'évaluation',
  39.             'guid' => 'asc.{labomega-evaluation}',
  40.             'description' => 'Génère un rapport d\'évaluation IA structuré',
  41.             'featureKey' => 'plugin.evaluation',
  42.             'size' => [450700],
  43.         ],
  44.         'labomega-draft' => [
  45.             'name' => 'Brouillon IA',
  46.             'guid' => 'asc.{labomega-draft}',
  47.             'description' => 'Aide à la rédaction IA en streaming',
  48.             'featureKey' => 'plugin.draft',
  49.             'size' => [400600],
  50.         ],
  51.         'labomega-plagiarism' => [
  52.             'name' => 'Plagiat',
  53.             'guid' => 'asc.{labomega-plagiarism}',
  54.             'description' => 'Détection de similarités entre documents',
  55.             'featureKey' => 'plugin.plagiarism',
  56.             'size' => [400600],
  57.         ],
  58.         'labomega-languagetool' => [
  59.             'name' => 'LanguageTool',
  60.             'guid' => 'asc.{labomega-languagetool}',
  61.             'description' => 'Vérification grammaticale & orthographique',
  62.             'featureKey' => 'plugin.languagetool',
  63.             'size' => [400600],
  64.         ],
  65.         'labomega-synthesis' => [
  66.             'name' => 'Synthèse multi-doc',
  67.             'guid' => 'asc.{labomega-synthesis}',
  68.             'description' => 'Synthèse IA croisée de plusieurs ressources',
  69.             'featureKey' => 'plugin.synthesis',
  70.             'size' => [450700],
  71.         ],
  72.         'labomega-translator' => [
  73.             'name' => 'Traducteur',
  74.             'guid' => 'asc.{labomega-translator}',
  75.             'description' => 'Traduction LibreTranslate intégrée',
  76.             'featureKey' => 'plugin.translator',
  77.             'size' => [400500],
  78.         ],
  79.         'labomega-semantic-scholar' => [
  80.             'name' => 'Semantic Scholar',
  81.             'guid' => 'asc.{labomega-semantic-scholar}',
  82.             'description' => 'Recherche bibliographique Semantic Scholar',
  83.             'featureKey' => 'plugin.semantic-scholar',
  84.             'size' => [450700],
  85.         ],
  86.         'labomega-arxiv' => [
  87.             'name' => 'arXiv',
  88.             'guid' => 'asc.{labomega-arxiv}',
  89.             'description' => 'Recherche de preprints arXiv (physique, math, CS, biologie)',
  90.             'featureKey' => 'plugin.arxiv',
  91.             'size' => [450700],
  92.         ],
  93.         'labomega-openalex' => [
  94.             'name' => 'OpenAlex',
  95.             'guid' => 'asc.{labomega-openalex}',
  96.             'description' => 'Catalogue ouvert OpenAlex (~250M papers, alternative SS)',
  97.             'featureKey' => 'plugin.openalex',
  98.             'size' => [450700],
  99.         ],
  100.         'labomega-crossref' => [
  101.             'name' => 'CrossRef',
  102.             'guid' => 'asc.{labomega-crossref}',
  103.             'description' => 'Recherche DOI/métadonnées CrossRef',
  104.             'featureKey' => 'plugin.crossref',
  105.             'size' => [450700],
  106.         ],
  107.         'labomega-pubmed' => [
  108.             'name' => 'PubMed',
  109.             'guid' => 'asc.{labomega-pubmed}',
  110.             'description' => 'Littérature biomédicale PubMed (NCBI, ~36M articles)',
  111.             'featureKey' => 'plugin.pubmed',
  112.             'size' => [450700],
  113.         ],
  114.         'labomega-hal' => [
  115.             'name' => 'HAL',
  116.             'guid' => 'asc.{labomega-hal}',
  117.             'description' => 'Archive ouverte HAL/CCSD — publications françaises (~1.5M, multidisciplinaire)',
  118.             'featureKey' => 'plugin.hal',
  119.             'size' => [450700],
  120.         ],
  121.         // Plugin "labomega-autoeval" retiré (mai 2026) — remplacé par
  122.         // "labomega-reports" qui implémente toute la logique côté Symfony
  123.         // (Qwen via LlmClient, plus de microservice Python autoeval externe).
  124.         'labomega-reports' => [
  125.             'name' => 'Rapports',
  126.             'guid' => 'asc.{labomega-reports}',
  127.             'description' => 'Génération assistée d\'un rapport à partir d\'une trame stockée en base.',
  128.             'featureKey' => 'plugin.reports',
  129.             'size' => [550800],
  130.         ],
  131.         'labomega-chat-rag' => [
  132.             'name' => 'Chat RAG workroom',
  133.             'guid' => 'asc.{labomega-chat-rag}',
  134.             'description' => 'Q&A en streaming sur les ressources et sections du workroom courant, avec citations sources',
  135.             'featureKey' => 'plugin.chat-rag',
  136.             'size' => [500750],
  137.         ],
  138.         'labomega-agenda' => [
  139.             'name' => 'Agenda',
  140.             'guid' => 'asc.{labomega-agenda}',
  141.             'description' => 'Événements partagés du workroom — réunions, jalons, deadlines, visios Jitsi (export iCal)',
  142.             'featureKey' => 'plugin.agenda',
  143.             'size' => [560720],
  144.         ],
  145.         'labomega-gantt' => [
  146.             'name' => 'Gantt',
  147.             'guid' => 'asc.{labomega-gantt}',
  148.             'description' => 'Diagramme de Gantt — tâches, jalons et dépendances du workroom',
  149.             'featureKey' => 'plugin.gantt',
  150.             'size' => [720720],
  151.         ],
  152.         // Séparateur visuel entre les plugins Labomega et les plugins natifs
  153.         // OnlyOffice (Mendeley, Speech, etc.). OnlyOffice n'a pas de séparateur
  154.         // natif dans son toolbar — c'est un faux plugin avec icône "ligne
  155.         // verticale orange". Toujours en DERNIÈRE position pour qu'il soit
  156.         // entre les Labomega et les natifs (qui sont rendus après).
  157.         // featureKey null → toujours visible (pas de gating FeatureAccess).
  158.         'labomega-separator' => [
  159.             'name' => '│',
  160.             'guid' => 'asc.{labomega-zz-separator}',
  161.             'description' => 'Séparateur visuel — plugins Labomega ↔ plugins natifs OnlyOffice',
  162.             'featureKey' => null,
  163.             'size' => [400320],
  164.         ],
  165.     ];
  166.     public function __construct(private readonly string $publicUrl) {}
  167.     #[Route('/plugins/{slug}/config.json'name'plugin_config'methods: ['GET'])]
  168.     public function config(string $slugRequest $request): JsonResponse
  169.     {
  170.         if (!isset(self::REGISTRY[$slug])) {
  171.             throw $this->createNotFoundException();
  172.         }
  173.         $meta self::REGISTRY[$slug];
  174.         // Forward section + workroom + access_token + apiBase à l'iframe plugin.
  175.         // - access_token : généré server-side par EditorConfigBuilder pour
  176.         //   contourner SameSite=Lax dans les iframes cross-site
  177.         // - apiBase : URL Symfony publique HTTPS (sinon les plugins fall back
  178.         //   sur `http://localhost:8082` → CORS / mixed content en preprod/prod)
  179.         $context = [
  180.             'section' => $request->query->get('section'),
  181.             'workroom' => $request->query->get('workroom'),
  182.             'access_token' => $request->query->get('access_token'),
  183.             'apiBase' => $request->query->get('apiBase'),
  184.         ];
  185.         $qs http_build_query(array_filter($context));
  186.         // baseUrl public (browser-accessible) pour que l'iframe charge depuis le bon host
  187.         $baseUrl rtrim($this->publicUrl'/').'/plugins/'.$slug.'/';
  188.         // IMPORTANT : OnlyOffice 9.x concatène `baseUrl + url` automatiquement.
  189.         // L'`url` doit donc être RELATIVE à `baseUrl`, pas absolue, sinon on
  190.         // obtient des doublons type `baseUrl/baseUrl/index.html` → 404.
  191.         // Idem pour `icons` (chemins relatifs à baseUrl).
  192.         $iframeUrl 'index.html'.($qs '?'.$qs '');
  193.         // Icons themed light/dark (format OnlyOffice 9.x `icons2`).
  194.         // - `icons` (legacy fallback) pointe vers la variante light.
  195.         // - `icons2` fournit les 5 résolutions (100%/125%/150%/175%/200%)
  196.         //   pour les 2 thèmes (light=icon foncé, dark=icon clair) → reste
  197.         //   lisible quel que soit le thème de l'éditeur.
  198.         $iconsLegacy = ['icons/light/icon.png''icons/light/icon@2x.png'];
  199.         $icons2 = [
  200.             [
  201.                 'style' => 'light',
  202.                 '100%' => ['normal' => 'icons/light/icon.png'],
  203.                 '125%' => ['normal' => 'icons/light/icon@1.25x.png'],
  204.                 '150%' => ['normal' => 'icons/light/icon@1.5x.png'],
  205.                 '175%' => ['normal' => 'icons/light/icon@1.75x.png'],
  206.                 '200%' => ['normal' => 'icons/light/icon@2x.png'],
  207.             ],
  208.             [
  209.                 'style' => 'dark',
  210.                 '100%' => ['normal' => 'icons/dark/icon.png'],
  211.                 '125%' => ['normal' => 'icons/dark/icon@1.25x.png'],
  212.                 '150%' => ['normal' => 'icons/dark/icon@1.5x.png'],
  213.                 '175%' => ['normal' => 'icons/dark/icon@1.75x.png'],
  214.                 '200%' => ['normal' => 'icons/dark/icon@2x.png'],
  215.             ],
  216.         ];
  217.         $response = new JsonResponse([
  218.             'name' => $meta['name'],
  219.             'guid' => $meta['guid'],
  220.             'baseUrl' => $baseUrl,
  221.             'variations' => [[
  222.                 'description' => $meta['description'],
  223.                 'url' => $iframeUrl,
  224.                 'icons' => $iconsLegacy,
  225.                 'icons2' => $icons2,
  226.                 'isViewer' => true,
  227.                 'EditorsSupport' => ['word'],
  228.                 'isVisual' => true,
  229.                 'isModal' => false,
  230.                 'isInsideMode' => false,
  231.                 'initDataType' => 'text',
  232.                 'initData' => '',
  233.                 'buttons' => [],
  234.                 'size' => $meta['size'],
  235.                 'events' => ['onClick'],
  236.             ]],
  237.         ]);
  238.         $response->setEncodingOptions(JSON_UNESCAPED_UNICODE);
  239.         // CORS pour permettre au DocServer de fetcher
  240.         $response->headers->set('Access-Control-Allow-Origin''*');
  241.         return $response;
  242.     }
  243. }