Initial commit: custom PrestaShop 9 EduBox image

This commit is contained in:
2026-06-20 20:32:55 +00:00
commit d97a78e6b6
16 changed files with 466 additions and 0 deletions
+42
View File
@@ -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
+63
View File
@@ -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 (`<id>.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.
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* EduBox reverse proxy normalisation for PrestaShop 9 running behind the
* EduBox dynamic-public-domain resolver.
*
* The official PrestaShop 9 + PHP 8.5 + Apache image has a bug where
* X-Forwarded-* headers are exposed to PHP as arrays whose value is the
* header name. getenv() returns the correct string, so we use it to
* reconstruct $_SERVER entries used by Tools::getHttpHost/ShopDomainSSL.
*/
if ($val = getenv('HTTP_X_FORWARDED_HOST')) {
$_SERVER['HTTP_X_FORWARDED_HOST'] = $val;
}
if ($val = getenv('HTTP_X_FORWARDED_PROTO')) {
$_SERVER['HTTP_X_FORWARDED_PROTO'] = $val;
}
// Apache/PHP 8.5 sometimes corrupts HTTP_HOST into an array; fall back safely.
if (!empty($_SERVER['HTTP_HOST']) && is_array($_SERVER['HTTP_HOST'])) {
$_SERVER['HTTP_HOST'] = !empty($_SERVER['SERVER_NAME']) && !is_array($_SERVER['SERVER_NAME'])
? $_SERVER['SERVER_NAME']
: (getenv('HTTP_X_FORWARDED_HOST') ?: 'localhost');
}
if (!empty($_SERVER['HTTPS']) && is_array($_SERVER['HTTPS'])) {
$_SERVER['HTTPS'] = 'off';
}
// Tell Symfony to trust the EduBox resolver so $request->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';
+20
View File
@@ -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;
}
+16
View File
@@ -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
+24
View File
@@ -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;
}
/**
+30
View File
@@ -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);
+9
View File
@@ -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/')) {
+36
View File
@@ -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';
}
/**
+46
View File
@@ -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();
+46
View File
@@ -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;
+28
View File
@@ -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();
}
+23
View File
@@ -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);
}
}
+39
View File
@@ -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) {
+10
View File
@@ -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.)
<Directory /var/www/html>
AllowOverride All
Require all granted
</Directory>
Binary file not shown.