# Analyse : Intégration de PrestaShop dans EduBox ## 1. Architecture actuelle d'EduBox (vue d'ensemble) ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Serveur cloud (ex: alfrednobel.edudeploy.com) │ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │ │ Caddy │──▶│ Next.js │──▶│ Resolver Go │──▶│ PostgreSQL │ │ │ │ TLS on-demand│ │ (dashboard) │ │ (proxy inst.)│ │ (état) │ │ │ └─────────────┘ └──────────────┘ └──────────────┘ └─────────────┘ │ │ │ │ WebSocket 3001 │ │ │ ▼ │ │ │ Agent EduBox (Go) sur PC étudiant via Tailscale/Headscale │ │ │ ┌─────────────┐ │ │ └─────────────▶│ WordPress │ (mu-plugin edubox-public-url.php) │ │ │ Docker/Podman │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### Flux d'une requête publique 1. Le navigateur demande `https://.alfrednobel.edudeploy.com/` 2. Caddy termine TLS et transmet au resolver Go (`:443 → resolver:2020`) 3. Le resolver lit PostgreSQL pour trouver le nœud (Tailscale IP) et le port de l'instance 4. Le resolver fait du reverse proxy HTTP vers `http://:` 5. Le resolver ajoute les headers `X-Forwarded-Proto: https`, `X-Forwarded-Host`, `X-Forwarded-Port: 443` 6. L'agent Go sur le PC étudiant a lancé le conteneur WordPress sur `localhost:` (bind `{PORT}:80`) 7. WordPress reçoit la requête en HTTP interne mais le **mu-plugin** `edubox-public-url.php` détecte `HTTP_HOST`/`X-Forwarded-Proto` et redéfinit `WP_HOME`/`WP_SITEURL` à la volée. ### Pourquoi WordPress fonctionne - WordPress permet de redéfinir `WP_HOME` et `WP_SITEURL` via `wp-config.php` ou un mu-plugin. - Le mu-plugin intercepte chaque requête et calcule l'URL publique depuis les headers proxy. - WordPress accepte d'être servi depuis plusieurs domaines simultanément (localhost + sous-domaine public). --- ## 2. Pourquoi PrestaShop est différent (et plus difficile) ### 2.1 Le domaine est stocké en base de données PrestaShop enregistre l'URL canonique à plusieurs endroits : - `ps_configuration` : - `PS_SHOP_DOMAIN` - `PS_SHOP_DOMAIN_SSL` - `PS_SSL_ENABLED` - `ps_shop_url` : - `domain` - `domain_ssl` Ces valeurs sont écrites **une seule fois lors de l'installation automatique** (`PS_INSTALL_AUTO=1`) via `index_cli.php --domain=`. ### 2.2 Redirections canoniques strictes Dans `classes/controller/FrontController.php` : ```php $match_url = Tools::getCurrentUrlProtocolPrefix() . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; if (!preg_match('/^' . Tools::pRegexp(rawurldecode($canonical_url), '/') . '([&?].*)?$/', $match_url)) { // ... redirect vers $canonical_url } ``` - Si l'URL demandée ne correspond pas exactement à l'URL canonique stockée, PrestaShop envoie une 301/302. - L'URL canonique est générée avec `Tools::getShopDomainSsl(true)` qui combine : - `ShopUrl::getMainShopDomainSSL()` (domaine figé en base) - `Configuration::get('PS_SSL_ENABLED')` (protocole figé en base) ### 2.3 Détection SSL `Tools::usingSecureMode()` truste bien `HTTP_X_FORWARDED_PROTO: https` (testé et confirmé dans `classes/Tools.php`) : ```php if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { return Tools::strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'; } ``` Donc le resolver transmet correctement le protocole. Le problème ne vient **pas** d'un défaut de PrestaShop sur la détection SSL en lui-même. ### 2.4 Le vrai problème : l'URL canonique est figée et ne correspond pas à l'URL demandée Scénario avec `PS_ENABLE_SSL=1` et `PS_DOMAIN=.alfrednobel.edudeploy.com` : - URL demandée : `https://.alfrednobel.edudeploy.com/` - Requête interne : `http://:/` avec `X-Forwarded-Proto: https` - PrestaShop détecte : secure mode = true - URL canonique : `https://.alfrednobel.edudeploy.com/` - `match_url` : `https://.alfrednobol.edudeploy.com/` - Devrait correspondre… mais dans les tests, une boucle a quand même eu lieu. Causes probables de la boucle : 1. **Port dans `HTTP_HOST`** : le resolver envoie `req.Host = host` (sans `:443`), mais Apache/PHP peut parfois enrichir `HTTP_HOST` avec le port interne (`.domain:80`) selon la configuration. 2. **Cache navigateur** : une 301 est mise en cache, masquant le fix. 3. **Apache `.htaccess` généré par PrestaShop** : contient des règles de rewrite qui peuvent interagir avec le proxy (ex: rediriger `/` vers `index.php` avec un protocole différent). 4. **Cookie/SESSION** : la session PHP ou un cookie sécurisé peut forcer une reconnexion/redirection. ### 2.5 Accès `localhost:` redirige vers le domaine public C'est le comportement normal de PrestaShop quand `PS_DOMAIN` est le domaine public. Pour EduBox, ce n'est pas bloquant si l'accès étudiant se fait via l'URL publique ou via le Tailscale IP du professeur. --- ## 3. Pistes de solutions ### Solution A — Utiliser le mécanisme natif `PS_HANDLE_DYNAMIC_DOMAIN=1` (recommandée à tester en premier) L'image Docker officielle `prestashop/prestashop` embarque un script `docker_updt_ps_domains.php` qui est copié à la racine et utilisé comme `DirectoryIndex` quand `PS_HANDLE_DYNAMIC_DOMAIN=1`. Ce script fait : ```php $domain = Tools::getHttpHost(); $url = ShopUrl::getShopUrls(Configuration::get('PS_SHOP_DEFAULT'))->where('main', '=', 1)->getFirst(); if ($url) { $url->domain = $domain; $url->domain_ssl = $domain; $url->save(); Configuration::updateValue('PS_SHOP_DOMAIN', $domain); Configuration::updateValue('PS_SHOP_DOMAIN_SSL', $domain); Tools::generateHtaccess(); Tools::generateRobotsFile(); Tools::clearSmartyCache(); Media::clearCache(); } Tools::redirect("index.php"); ``` **Avantages** - Mécanisme officiel, pas de module à maintenir. - Met à jour le domaine dynamiquement à chaque requête sur `/`. **Inconvénients** - Exécution PHP + requêtes SQL + régénération `.htaccess` à chaque requête sur `/` → latence perceptible. - Ne gère pas nativement le HTTPS vs HTTP (domain_ssl = domain, sans tenir compte de X-Forwarded-Proto). - Nécessite d'être combiné avec une confiance des headers proxy. **Implémentation proposée** Dans le `composeConfig` du template PrestaShop : ```yaml environment: PS_DOMAIN: {PUBLIC_DOMAIN} PS_ENABLE_SSL: "1" PS_HANDLE_DYNAMIC_DOMAIN: "1" PS_INSTALL_AUTO: "1" PS_INSTALL_DB: "0" ``` Et monter un fichier Apache `proxy.conf` dans `/etc/apache2/conf-enabled/` : ```apache SetEnvIf X-Forwarded-Proto https HTTPS=on SetEnvIf X-Forwarded-Proto https SERVER_PORT=443 SetEnvIf X-Forwarded-Host ^(.+)$ HTTP_HOST=$1 ``` Cela permet à `Tools::usingSecureMode()` de retourner `true` et à `Tools::getHttpHost()` de retourner le bon host public. **Risques** - Boucle possible si `PS_ENABLE_SSL=1` mais Apache ne reçoit pas `HTTPS=on` (d'où l'importance du `SetEnvIf`). - Performance : le `docker_updt_ps_domains.php` est exécuté à chaque hit sur `/`. --- ### Solution B — Créer un module/override PrestaShop "EduBox Public URL" (équivalent du mu-plugin WordPress) Créer un override `override/classes/Configuration.php` (ou un module hooké tôt) qui surcharge `Configuration::get()` pour les clés `PS_SHOP_DOMAIN`, `PS_SHOP_DOMAIN_SSL` et `PS_SSL_ENABLED`. Exemple d'override : ```php 0 && is_string(func_get_arg(0)) ? func_get_arg(0) : $id_lang; if ($key === 'PS_SHOP_DOMAIN' || $key === 'PS_SHOP_DOMAIN_SSL') { return Tools::getHttpHost(false, false, true); // host sans port } if ($key === 'PS_SSL_ENABLED') { return Tools::usingSecureMode() ? '1' : '0'; } return call_user_func_array(['ConfigurationCore', 'get'], func_get_args()); } } ``` **Avantages** - Le plus proche du mu-plugin WordPress : aucune réécriture de réponses, pas de latence sur `/`. - Une fois l'override chargé, toutes les URLs générées par PrestaShop utilisent le domaine de la requête courante. **Inconvénients / pièges** - L'override doit être pris en compte par l'autoloader. Sous PrestaShop 8, il faut vider `app/cache/prod/class_index.php`, `var/cache/*` et régénérer l'index avec `PrestaShop\Autoload\PrestashopAutoload::getInstance()->generateIndex()`. - L'image Docker officielle embarque un `class_index.php` pré-généré qu'il faut invalider. - Si l'installation se fait avec un override déjà présent, PrestaShop peut ne pas l'activer immédiatement. **Implémentation proposée** 1. Créer `agent/psplugins/Configuration.php` (embarqué dans l'agent). 2. Au `start`/`reset` d'une instance PrestaShop, l'agent : - copie l'override dans `/var/www/html/override/classes/Configuration.php` - vide les caches (`rm -rf app/cache/* var/cache/*`) - régénère l'autoloader (`php -r "require 'config/config.inc.php'; PrestaShop\Autoload\PrestashopAutoload::getInstance()->generateIndex();"`) 3. Ajouter le montage de l'override dans le `composeConfig` PrestaShop via un placeholder `{PS_OVERRIDES_DIR}`. 4. Garder la config Apache `SetEnvIf X-Forwarded-Proto https HTTPS=on` pour la détection SSL. **Risques** - Fragilité des versions : l'override dépend de la signature de `ConfigurationCore::get()` qui peut changer. - Nécessite de bien gérer les caches à chaque redémarrage du conteneur. --- ### Solution C — Installation "localhost" + réécriture complète par le proxy Installer PrestaShop avec `PS_DOMAIN=localhost:{PORT}` et `PS_ENABLE_SSL=0`. Le conteneur vit en HTTP interne. Le resolver réécrit : - Le header `Location` : `http://localhost:/...` → `https://.domain/...` - Le body HTML/CSS/JS : toutes les occurrences de `http://localhost:` et `//localhost:` C'est l'approche "WordPress-like". **Avantages** - Pas besoin de modifier PrestaShop. - Le conteneur est totalement agnostique du domaine public. **Inconvénients** - PrestaShop génère énormément d'URLs absolues (assets, liens admin, webhooks, modules, API). La réécriture body n'est jamais exhaustive. - Les requêtes AJAX/fetch peuvent pointer vers `localhost:` et échouer côté client. - Le back-office (`/admin`) génère des redirections complexes. - Très fragile sur le long terme. **Verdict** : **non recommandée** pour PrestaShop (contrairement à WordPress). --- ### Solution D — Image Docker PrestaShop personnalisée (patch durable) Créer un `Dockerfile` dérivé de `prestashop/prestashop:8.1` qui : 1. Applique un patch à `classes/Configuration.php` ou `classes/Tools.php` pour supporter nativement `X-Forwarded-Host`/`X-Forwarded-Proto`. 2. Ou embarque directement l'override EduBox + un script d'init qui vide les caches. 3. Configure Apache pour trust les headers proxy. **Avantages** - Totalement reproductible : pas d'opération manuelle sur le conteneur en cours de vie. - Peut être versionnée et testée indépendamment. **Inconvénients** - Nécessite de maintenir et publier une image Docker. - Ajoute une étape de build CI/CD. - Si PrestaShop sort une nouvelle version, il faut rebaser le patch. **Implémentation possible** ```dockerfile FROM prestashop/prestashop:8.1 COPY edubox-proxy.conf /etc/apache2/conf-enabled/ COPY edubox-override/ /var/www/html/override/ RUN chown -R www-data:www-data /var/www/html/override ``` --- ### Solution E — Proxy "intelligent" avec substitution à la volée Remplacer le resolver Go par (ou ajouter devant) un proxy qui fait de la substitution HTML/CSS/JS beaucoup plus fine, par exemple : - Nginx avec `sub_filter` - Apache `mod_substitute` - Un middleware Node.js type `http-proxy-middleware` avec `selfHandleResponse` **Avantages** - Peut corriger les liens absolus que PrestaShop génère. **Inconvénients** - Même problème que la solution C : impossible d'être exhaustif. - Ajoute de la latence et de la complexité. - Peut casser le JS (si on remplace des chaînes dans du code minifié). **Verdict** : complément possible, mais pas solution principale. --- ### Solution F — Désactiver les redirections canoniques et SSL forcé Forcer PrestaShop à ne plus faire de redirections canoniques (`PS_CANONICAL_REDIRECT=0`) et à désactiver SSL partout (`PS_SSL_ENABLED=0`, `PS_SSL_ENABLED_EVERYWHERE=0`). **Avantages** - Élimine les boucles de redirection. **Inconvénients** - Nécessite un accès au back-office pour modifier les paramètres. - Les liens générés restent en `http://` et pointent vers le domaine d'installation. - Mauvaise expérience utilisateur (avertissements navigateur, mixed-content si certains assets passent en http). **Verdict** : à éviter en production. --- ## 4. Recommandation Je recommande une **combinaison des solutions A et B**, testée méthodiquement : ### Phase 1 — Solution A (test rapide) 1. Réintroduire un template PrestaShop 8.1 dans `server/prisma/seed.ts` avec : - `PS_DOMAIN: {PUBLIC_DOMAIN}` - `PS_ENABLE_SSL: "1"` - `PS_HANDLE_DYNAMIC_DOMAIN: "1"` - montage d'une config Apache `proxy.conf` pour trust `X-Forwarded-Proto` 2. Rebuilder le serveur, relancer le seed. 3. Créer une nouvelle instance PrestaShop (pas de reset d'ancienne instance). 4. Tester avec `curl -v -L --max-redirs 5 https://.domain/` et en navigation privée. ### Phase 2 — Solution B (solution cible) Si la solution A est trop lente ou instable, passer à l'override `Configuration.php` : 1. Créer `agent/psplugins/Configuration.php`. 2. Modifier l'agent pour copier l'override et vider/régénérer les caches au démarrage d'une instance PrestaShop. 3. Ajouter le montage `{PS_OVERRIDES_DIR}` dans le compose template. 4. Conserver la config Apache `SetEnvIf X-Forwarded-Proto`. 5. Tester avec `localhost:` ET `https://.domain/`. ### Phase 3 (optionnel) — Solution D Si les solutions A/B sont trop fragiles d'une version de PrestaShop à l'autre, créer une image Docker dérivée patchée et la référencer dans le template. --- ## 5. Points d'attention pour l'implémentation ### Headers proxy Le resolver Go transmet déjà les bons headers. Vérifier qu'aucun middleware Next.js ne les modifie (le middleware actuel ne fait que `NextResponse.next()`). ### Binding de port Le commit `dd49993` a changé le binding de `127.0.0.1:{PORT}:80` à `{PORT}:80`. Cela signifie que le conteneur PrestaShop est accessible depuis n'importe quelle interface sur le PC étudiant. C'est nécessaire car le Tailscale proxy de l'agent écoute sur toutes les interfaces. **Il ne faut pas revenir à `127.0.0.1` sauf si on change la chaîne de proxy.** ### Cache navigateur Toujours tester en navigation privée et avec `curl -v -L --max-redirs 5` pour éviter les 301 mises en cache. ### Vider les caches PrestaShop À chaque changement de domaine/config, il faut vider : ```bash rm -rf /var/www/html/app/cache/* rm -rf /var/www/html/var/cache/* php -r "require '/var/www/html/config/config.inc.php'; PrestaShop\Autoload\PrestashopAutoload::getInstance()->generateIndex();" ``` ### Logs utiles Sur le PC étudiant : ```bash podman logs -f -app-1 podman exec -app-1 cat /var/log/apache2/error.log ``` Sur le serveur : ```bash docker logs -f edubox-resolver docker logs -f edubox-caddy ``` --- ## 6. Synthèse comparative | Solution | Complexité | Robustesse | Perf. | Maintien | Recommandation | |----------|-----------|------------|-------|----------|----------------| | A — `PS_HANDLE_DYNAMIC_DOMAIN` | Faible | Moyenne | Moyenne (latence `/`) | Faible | **À tester en premier** | | B — Override `Configuration` | Moyenne | Forte | Bonne | Moyen | **Solution cible** | | C — localhost + rewrite proxy | Moyenne | Faible | Bonne | Faible | Non recommandée | | D — Image Docker patchée | Forte | Très forte | Bonne | Fort | Option long terme | | E — Proxy substitution | Moyenne | Faible | Moyenne | Faible | Complément seulement | | F — Désactiver SSL/canonical | Faible | Faible | Bonne | Faible | À éviter |