commit d97a78e6b6c9a6e9b22b677a4a1afa0d97d04838 Author: yacine Date: Sat Jun 20 20:32:55 2026 +0000 Initial commit: custom PrestaShop 9 EduBox image diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cfc7a41 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM prestashop/prestashop:9 + +# Apply EduBox patches so PrestaShop 9 works behind the dynamic-domain reverse proxy. +COPY edubox-tools.patch \ + edubox-link.patch \ + edubox-frontcontroller.patch \ + edubox-shop.patch \ + edubox-shopurl.patch \ + edubox-shopcontext.patch \ + edubox-asseturl.patch \ + edubox-install.patch \ + edubox-install-language.patch \ + edubox-language.patch \ + edubox-docker-run.patch \ + /tmp/ +RUN patch -p1 -d /var/www/html < /tmp/edubox-tools.patch && \ + patch -p1 -d /var/www/html < /tmp/edubox-link.patch && \ + patch -p1 -d /var/www/html < /tmp/edubox-frontcontroller.patch && \ + patch -p1 -d /var/www/html < /tmp/edubox-shop.patch && \ + patch -p1 -d /var/www/html < /tmp/edubox-shopurl.patch && \ + patch -p1 -d /var/www/html < /tmp/edubox-shopcontext.patch && \ + patch -p1 -d /var/www/html < /tmp/edubox-asseturl.patch && \ + patch -p1 -d /var/www/html < /tmp/edubox-install.patch && \ + patch -p1 -d /var/www/html < /tmp/edubox-install-language.patch && \ + patch -p1 -d /var/www/html < /tmp/edubox-language.patch && \ + patch -p1 -d / < /tmp/edubox-docker-run.patch && \ + rm /tmp/edubox-*.patch + +# Apache proxy configuration +COPY proxy.conf /etc/apache2/conf-enabled/edubox-proxy.conf + +# Pre-download French translation pack so the installer works offline. +# Agents may not have outbound internet access during installation. +# The official image copies /tmp/data-ps/prestashop/ into /var/www/html on first +# boot, so we place the pack there as well. +COPY translations-symfony-fr-FR.zip /tmp/data-ps/prestashop/translations/sf-fr-FR.zip +RUN chown -R www-data:www-data /tmp/data-ps/prestashop/translations + +# Early bootstrap normalisation for X-Forwarded-* headers +COPY defines_custom.inc.php /var/www/html/config/defines_custom.inc.php + +RUN chown -R www-data:www-data /var/www/html diff --git a/README.md b/README.md new file mode 100644 index 0000000..08ec883 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# EduBox PrestaShop 9 Image + +Image Docker patchée basée sur `prestashop/prestashop:9`, conçue pour fonctionner +avec le reverse proxy dynamique d'EduBox. + +## Pourquoi une image patchée ? + +PrestaShop 9 (Apache 2.4 + PHP 8.5) a deux problèmes majeurs derrière EduBox : + +1. Les headers `X-Forwarded-*` sont corrompus par Apache/PHP : `$_SERVER` les + reçoit sous forme d'arrays au lieu de strings. On contourne ce bug via + `getenv()` dans `config/defines_custom.inc.php`. +2. PrestaShop utilise partout le domaine stocké en base (`ps_shop_url`) et la + configuration `PS_SSL_ENABLED`. Derrière EduBox, le domaine public change à + chaque instance (`.alfrednobel.edudeploy.com`) et toutes les requêtes + publiques arrivent en HTTPS. Les patches forcent l'utilisation de l'hôte et + du protocole de la requête courante. + +## Build + +```bash +cd /opt/edubox/prestashop-image +docker build -t edubox-prestashop:9 . +``` + +## Patches appliqués + +| Patch | Fichier modifié | Objectif | +|-------|-----------------|----------| +| `edubox-tools.patch` | `classes/Tools.php` | `getShopDomain()` / `getShopDomainSsl()` utilisent `getHttpHost()` dynamiquement. | +| `edubox-link.patch` | `classes/Link.php` | `getBaseLink()` utilise `usingSecureMode()` et `getHttpHost()`. | +| `edubox-frontcontroller.patch` | `classes/controller/FrontController.php` | Désactive `sslRedirection()` pour éviter les boucles HTTP/HTTPS. | +| `edubox-shop.patch` | `classes/shop/Shop.php` | `Shop::initialize()` utilise le shop par défaut sans redirection forcée. | +| `edubox-shopcontext.patch` | `src/Core/Context/ShopContext.php` | `getBaseURL()` du BO est reconstruit à partir de la requête courante. | +| `edubox-asseturl.patch` | `src/Adapter/Assets/AssetUrlGeneratorTrait.php` | Les assets CCC utilisent le protocole de la requête, pas `PS_SSL_ENABLED`. | +| `edubox-install.patch` | `src/PrestaShopBundle/Install/Install.php` | `finalize()` respecte `PS_FOLDER_ADMIN` (évite le bug overlayfs `admin` → `admin-edubox`). | + +## Fichiers injectés + +- `proxy.conf` : Apache truste `X-Forwarded-Proto: https` pour positionner + `HTTPS=on` dans l'environnement PHP. +- `config/defines_custom.inc.php` : normalise `HTTP_X_FORWARDED_HOST`, + `HTTP_X_FORWARDED_PROTO` et `HTTP_HOST` corrompus ; définit + `PS_TRUSTED_PROXIES` pour Symfony. + +## Utilisation dans EduBox + +Le template PrestaShop 9 dans `server/prisma/seed.ts` utilise cette image : + +```yaml +app: + image: edubox-prestashop:9 +``` + +## Déploiement sur les agents + +L'image doit être accessible depuis chaque agent étudiant. Deux options : + +1. **Registry privé** (recommandé) : tagger et pousser l'image sur un registry + (Docker Hub, registry Gitea, GHCR, etc.) puis mettre à jour + `server/prisma/seed.ts` avec le nom complet (`monregistry/edubox-prestashop:9`). +2. **Build manuel sur chaque agent** : copier le dossier `prestashop-image` sur + l'agent et lancer `docker build` avant le premier déploiement. diff --git a/defines_custom.inc.php b/defines_custom.inc.php new file mode 100644 index 0000000..ad48c85 --- /dev/null +++ b/defines_custom.inc.php @@ -0,0 +1,34 @@ +isSecure() and +// $request->getHost() honour X-Forwarded-* headers. +putenv('PS_TRUSTED_PROXIES=127.0.0.1,REMOTE_ADDR'); +$_SERVER['PS_TRUSTED_PROXIES'] = '127.0.0.1,REMOTE_ADDR'; +$_ENV['PS_TRUSTED_PROXIES'] = '127.0.0.1,REMOTE_ADDR'; diff --git a/edubox-asseturl.patch b/edubox-asseturl.patch new file mode 100644 index 0000000..523e05e --- /dev/null +++ b/edubox-asseturl.patch @@ -0,0 +1,20 @@ +--- a/src/Adapter/Assets/AssetUrlGeneratorTrait.php ++++ b/src/Adapter/Assets/AssetUrlGeneratorTrait.php +@@ -49,12 +49,14 @@ trait AssetUrlGeneratorTrait + protected function getFQDN() + { + if (null === $this->fqdn) { +- if ($this->configuration->get('PS_SSL_ENABLED') && ToolsLegacy::usingSecureMode()) { +- $this->fqdn = $this->configuration->get('_PS_BASE_URL_SSL_'); +- } else { ++ // EduBox: rely on the current request security, not on PS_SSL_ENABLED. ++ // Behind the reverse proxy every public request is HTTPS. ++ if (ToolsLegacy::usingSecureMode()) { ++ $this->fqdn = $this->configuration->get('_PS_BASE_URL_SSL_') ?: $this->configuration->get('_PS_BASE_URL_'); ++ } else { + $this->fqdn = $this->configuration->get('_PS_BASE_URL_'); + } + } + + return $this->fqdn; + } diff --git a/edubox-docker-run.patch b/edubox-docker-run.patch new file mode 100644 index 0000000..185a172 --- /dev/null +++ b/edubox-docker-run.patch @@ -0,0 +1,16 @@ +--- a/tmp/docker_run.sh 2026-06-20 17:57:12.682339048 +0000 ++++ b/tmp/docker_run.sh 2026-06-20 17:57:12.852338398 +0000 +@@ -21,6 +21,13 @@ + + # From now, stop at error + set -e ++# EduBox: if a previous installation failed, install.lock remains but PrestaShop is not configured. ++# Remove the stale lock so the installer can run again on the next start. ++if [ -f ./install.lock ] && [ ! -f ./config/settings.inc.php ] && [ ! -f ./app/config/parameters.php ]; then ++ echo "\n* Stale install.lock detected, removing it to allow reinstallation ..." ++ rm -f ./install.lock ++fi ++ + + if [ ! -f ./config/settings.inc.php ] && [ ! -f ./app/config/parameters.php ] && [ ! -f ./install.lock ]; then + diff --git a/edubox-frontcontroller.patch b/edubox-frontcontroller.patch new file mode 100644 index 0000000..f2376da --- /dev/null +++ b/edubox-frontcontroller.patch @@ -0,0 +1,24 @@ +--- a/classes/controller/FrontController.php ++++ b/classes/controller/FrontController.php +@@ -849,18 +849,9 @@ + */ + protected function sslRedirection() + { +- // If we call a SSL controller without SSL or a non SSL controller with SSL, we redirect with the right protocol +- if (Configuration::get('PS_SSL_ENABLED') && $_SERVER['REQUEST_METHOD'] != 'POST' && $this->ssl != Tools::usingSecureMode()) { +- $this->context->cookie->disallowWriting(); +- header('HTTP/1.1 301 Moved Permanently'); +- header('Cache-Control: no-cache'); +- if ($this->ssl) { +- header('Location: ' . Tools::getShopDomainSsl(true) . $_SERVER['REQUEST_URI']); +- } else { +- header('Location: ' . Tools::getShopDomain(true) . $_SERVER['REQUEST_URI']); +- } +- exit; +- } ++ // EduBox: disabled. Behind the EduBox reverse proxy every request is ++ // served over HTTPS publicly, so PrestaShop must never redirect to HTTP. ++ return; + } + + /** diff --git a/edubox-install-language.patch b/edubox-install-language.patch new file mode 100644 index 0000000..e13d367 --- /dev/null +++ b/edubox-install-language.patch @@ -0,0 +1,30 @@ +--- a/src/PrestaShopBundle/Install/Install.php 2026-06-20 18:07:13.506985399 +0000 ++++ b/src/PrestaShopBundle/Install/Install.php 2026-06-20 18:07:22.294363061 +0000 +@@ -622,17 +622,20 @@ + 'locale' => (string) $xml->locale, + ]; + +- if (file_exists(_PS_TRANSLATIONS_DIR_ . (string) $iso . '.gzip') == false) { +- $language = EntityLanguage::downloadLanguagePack($iso, _PS_INSTALL_VERSION_); ++ // EduBox: skip legacy language pack download if Symfony pack is bundled ++ $errors = []; ++ $locale = $params_lang['locale']; ++ ++ if (!EntityLanguage::translationPackIsInCache($locale)) { ++ if (file_exists(_PS_TRANSLATIONS_DIR_ . (string) $iso . '.gzip') == false) { ++ $language = EntityLanguage::downloadLanguagePack($iso, _PS_INSTALL_VERSION_); + +- if ($language == false) { +- throw new PrestashopInstallerException($this->translator->trans('Cannot download language pack "%iso%"', ['%iso%' => $iso], 'Install')); ++ if ($language == false) { ++ throw new PrestashopInstallerException($this->translator->trans('Cannot download language pack "%iso%"', ['%iso%' => $iso], 'Install')); ++ } + } + } + +- $errors = []; +- $locale = $params_lang['locale']; +- + /* @todo check if a newer pack is available */ + if (!EntityLanguage::translationPackIsInCache($locale)) { + EntityLanguage::downloadXLFLanguagePack($locale, $errors); diff --git a/edubox-install.patch b/edubox-install.patch new file mode 100644 index 0000000..c9036ea --- /dev/null +++ b/edubox-install.patch @@ -0,0 +1,9 @@ +--- a/src/PrestaShopBundle/Install/Install.php ++++ b/src/PrestaShopBundle/Install/Install.php +@@ -1202,7 +1202,7 @@ class Install extends AbstractInstall + { +- $adminFolder = 'admin-dev'; ++ $adminFolder = getenv('PS_FOLDER_ADMIN') ?: 'admin-dev'; + + // If we need, we generate a random name for admin folder (for security purpose!) + if (file_exists(_PS_ROOT_DIR_ . '/admin/')) { diff --git a/edubox-language.patch b/edubox-language.patch new file mode 100644 index 0000000..5e52a88 --- /dev/null +++ b/edubox-language.patch @@ -0,0 +1,36 @@ +--- a/classes/Language.php ++++ b/classes/Language.php +@@ -1235,6 +1235,12 @@ + */ + public static function downloadXLFLanguagePack($locale, &$errors = [], $type = self::PACK_TYPE_SYMFONY) + { ++ // EduBox: if the translation pack is already present in the image, ++ // do not try to download it (agents may be offline). ++ if (static::translationPackIsInCache($locale, $type)) { ++ return true; ++ } ++ + $file = self::getPathToCachedTranslationPack($locale, $type); + $url = (self::PACK_TYPE_EMAILS === $type) ? self::EMAILS_LANGUAGE_PACK_URL : self::SF_LANGUAGE_PACK_URL; + $url = str_replace( +@@ -1697,7 +1703,9 @@ + */ + public static function translationPackIsInCache(string $locale, string $type = self::PACK_TYPE_SYMFONY): bool + { +- return file_exists(self::getPathToCachedTranslationPack($locale, $type)); ++ // EduBox: use runtime constant instead of class constant, because ++ // _PS_TRANSLATIONS_DIR_ may not be defined when this file is compiled. ++ return file_exists(_PS_TRANSLATIONS_DIR_ . $type . '-' . $locale . '.zip'); + } + + /** +@@ -1710,7 +1718,8 @@ + */ + private static function getPathToCachedTranslationPack(string $locale, string $type = self::PACK_TYPE_SYMFONY): string + { +- return self::TRANSLATION_PACK_CACHE_DIR . $type . '-' . $locale . '.zip'; ++ // EduBox: use runtime constant instead of class constant. ++ return _PS_TRANSLATIONS_DIR_ . $type . '-' . $locale . '.zip'; + } + + /** diff --git a/edubox-link.patch b/edubox-link.patch new file mode 100644 index 0000000..ad4a391 --- /dev/null +++ b/edubox-link.patch @@ -0,0 +1,46 @@ +--- a/classes/Link.php 2026-06-20 20:05:45.983104609 +0000 ++++ b/classes/Link.php 2026-06-20 20:05:46.195748630 +0000 +@@ -862,7 +862,7 @@ + public function getAdminBaseLink($idShop = null, $ssl = null, $relativeProtocol = false) + { + if (null === $ssl) { +- $ssl = Configuration::get('PS_SSL_ENABLED'); ++ $ssl = Tools::usingSecureMode(); + } + + if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE')) { +@@ -881,9 +881,10 @@ + } + + if ($relativeProtocol) { +- $base = '//' . ($ssl && $this->ssl_enable ? $shop->domain_ssl : $shop->domain); ++ $base = '//' . Tools::getHttpHost(false, false, true); + } else { +- $base = (($ssl && $this->ssl_enable) ? 'https://' . $shop->domain_ssl : 'http://' . $shop->domain); ++ $protocol = Tools::usingSecureMode() ? 'https://' : 'http://'; ++ $base = $protocol . Tools::getHttpHost(false, false, true); + } + + return $base . $shop->getBaseURI(); +@@ -1391,7 +1392,7 @@ + public function getBaseLink($idShop = null, $ssl = null, $relativeProtocol = false) + { + if (null === $ssl) { +- $ssl = Configuration::get('PS_SSL_ENABLED'); ++ $ssl = Tools::usingSecureMode(); + } + + if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE') && $idShop !== null) { +@@ -1401,9 +1402,10 @@ + } + + if ($relativeProtocol) { +- $base = '//' . ($ssl && $this->ssl_enable ? $shop->domain_ssl : $shop->domain); ++ $base = '//' . Tools::getHttpHost(false, false, true); + } else { +- $base = (($ssl && $this->ssl_enable) ? 'https://' . $shop->domain_ssl : 'http://' . $shop->domain); ++ $protocol = Tools::usingSecureMode() ? 'https://' : 'http://'; ++ $base = $protocol . Tools::getHttpHost(false, false, true); + } + + return $base . $shop->getBaseURI(); diff --git a/edubox-shop.patch b/edubox-shop.patch new file mode 100644 index 0000000..2fe03b8 --- /dev/null +++ b/edubox-shop.patch @@ -0,0 +1,46 @@ +--- a/classes/shop/Shop.php ++++ b/classes/shop/Shop.php +@@ -411,38 +411,14 @@ + } else { + $shop = new Shop($id_shop); + if (!Validate::isLoadedObject($shop) || !$shop->active) { +- // No shop found ... too bad, let's redirect to default shop +- $default_shop = new Shop((int) Configuration::get('PS_SHOP_DEFAULT')); ++ // EduBox: behind a reverse proxy with dynamic public domains, ++ // the requested host never matches ps_shop_url. Always use the ++ // default shop instead of redirecting to a fixed canonical URL. ++ $shop = new Shop((int) Configuration::get('PS_SHOP_DEFAULT')); + +- // Hmm there is something really bad in your Prestashop ! +- if (!Validate::isLoadedObject($default_shop)) { ++ if (!Validate::isLoadedObject($shop)) { + throw new PrestaShopException('Shop not found'); + } +- +- $params = $_GET; +- unset($params['id_shop']); +- $url = $default_shop->domain; +- if (!Configuration::get('PS_REWRITING_SETTINGS')) { +- $url .= $default_shop->getBaseURI() . 'index.php?' . http_build_query($params); +- } else { +- // Catch url with subdomain "www" +- if (strpos($url, 'www.') === 0 && 'www.' . $_SERVER['HTTP_HOST'] === $url || $_SERVER['HTTP_HOST'] === 'www.' . $url) { +- $url .= $_SERVER['REQUEST_URI']; +- } else { +- $url .= $default_shop->getBaseURI(); +- } +- +- if (count($params)) { +- $url .= '?' . http_build_query($params); +- } +- } +- +- $redirect_type = Configuration::get('PS_CANONICAL_REDIRECT'); +- $redirect_code = ($redirect_type == 1 ? '302' : '301'); +- $redirect_header = ($redirect_type == 1 ? 'Found' : 'Moved Permanently'); +- header('HTTP/1.0 ' . $redirect_code . ' ' . $redirect_header); +- header('Location: ' . Tools::getShopProtocol() . $url); +- exit; + } elseif (defined('_PS_ADMIN_DIR_') && empty($shop->physical_uri)) { + $shop_default = new Shop((int) Configuration::get('PS_SHOP_DEFAULT')); + $shop->physical_uri = $shop_default->physical_uri; diff --git a/edubox-shopcontext.patch b/edubox-shopcontext.patch new file mode 100644 index 0000000..61bfbfd --- /dev/null +++ b/edubox-shopcontext.patch @@ -0,0 +1,28 @@ +--- a/src/Core/Context/ShopContext.php ++++ b/src/Core/Context/ShopContext.php +@@ -9,6 +9,7 @@ declare(strict_types=1); + + namespace PrestaShop\PrestaShop\Core\Context; + ++use Tools; + use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint; + + /** +@@ -121,11 +122,12 @@ class ShopContext + + public function getBaseURL(): string + { +- if ($this->secured) { +- $url = 'https://' . $this->domainSSL; +- } else { +- $url = 'http://' . $this->domain; +- } ++ // EduBox: behind a reverse proxy with dynamic public domains the shop ++ // URL stored in the database is never the real public URL. Rebuild the ++ // base URL from the current request instead. ++ $secure = Tools::usingSecureMode(); ++ $domain = $secure ? Tools::getShopDomainSsl(false, false) : Tools::getShopDomain(false, false); ++ $url = ($secure ? 'https://' : 'http://') . $domain; + + return $url . $this->getBaseURI(); + } diff --git a/edubox-shopurl.patch b/edubox-shopurl.patch new file mode 100644 index 0000000..e176dbb --- /dev/null +++ b/edubox-shopurl.patch @@ -0,0 +1,23 @@ +--- a/classes/shop/ShopUrl.php 2026-06-20 19:37:00.962339755 +0000 ++++ b/classes/shop/ShopUrl.php 2026-06-20 19:37:01.182205146 +0000 +@@ -175,15 +175,14 @@ + + public static function getMainShopDomain($id_shop = null) + { +- ShopUrl::cacheMainDomainForShop($id_shop); +- +- return self::$main_domain[(int) $id_shop] ?? null; ++ // EduBox: dynamic public domain behind reverse proxy. ++ // Always use the request host instead of the domain stored in database. ++ return Tools::getHttpHost(false, false, true); + } + + public static function getMainShopDomainSSL($id_shop = null) + { +- ShopUrl::cacheMainDomainForShop($id_shop); +- +- return self::$main_domain_ssl[(int) $id_shop] ?? null; ++ // EduBox: dynamic public domain behind reverse proxy. ++ return Tools::getHttpHost(false, false, true); + } + } diff --git a/edubox-tools.patch b/edubox-tools.patch new file mode 100644 index 0000000..c677217 --- /dev/null +++ b/edubox-tools.patch @@ -0,0 +1,39 @@ +--- a/classes/Tools.php ++++ b/classes/Tools.php +@@ -269,9 +269,7 @@ + */ + public static function getShopDomain($http = false, $entities = false) + { +- if (!$domain = ShopUrl::getMainShopDomain()) { +- $domain = Tools::getHttpHost(); +- } ++ $domain = Tools::getHttpHost(false, false, true); + if ($entities) { + $domain = htmlspecialchars($domain, ENT_COMPAT, 'UTF-8'); + } +@@ -292,14 +290,12 @@ + */ + public static function getShopDomainSsl($http = false, $entities = false) + { +- if (!$domain = ShopUrl::getMainShopDomainSSL()) { +- $domain = Tools::getHttpHost(); +- } ++ $domain = Tools::getHttpHost(false, false, true); + if ($entities) { + $domain = htmlspecialchars($domain, ENT_COMPAT, 'UTF-8'); + } + if ($http) { +- $domain = static::getProtocol((bool) Configuration::get('PS_SSL_ENABLED')) . $domain; ++ $domain = static::getProtocol(Tools::usingSecureMode()) . $domain; + } + + return $domain; +@@ -2246,7 +2242,7 @@ + $rewrite_settings = (int) Configuration::get('PS_REWRITING_SETTINGS', null, null, (int) $uri['id_shop']); + } + +- $domain_rewrite_cond = 'RewriteCond %{HTTP_HOST} ^' . $domain . '$' . PHP_EOL; ++ $domain_rewrite_cond = ''; // EduBox: removed HTTP_HOST condition for dynamic domains + // Rewrite virtual multishop uri + if ($uri['virtual']) { + if (!$rewrite_settings) { diff --git a/proxy.conf b/proxy.conf new file mode 100644 index 0000000..1dd0b9c --- /dev/null +++ b/proxy.conf @@ -0,0 +1,10 @@ +# EduBox reverse proxy handling +# Apache sees HTTP requests from the EduBox resolver. The public request is HTTPS. +SetEnvIf X-Forwarded-Proto ^https$ HTTPS=on +SetEnvIf X-Forwarded-Proto ^https$ SERVER_PORT=443 + +# Enable .htaccess overrides for PrestaShop URL rewriting (images, products, etc.) + + AllowOverride All + Require all granted + diff --git a/translations-symfony-fr-FR.zip b/translations-symfony-fr-FR.zip new file mode 100644 index 0000000..3a13ad4 Binary files /dev/null and b/translations-symfony-fr-FR.zip differ