From 82bbf22442ed2d131289a3217c2540410ccab7aa Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 23 Jul 2026 17:30:18 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat(admin):=20.env=20=E3=81=8C=E5=8F=8D?= =?UTF-8?q?=E6=98=A0=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E3=81=A7=E8=A8=AD=E5=AE=9A=E5=A4=89=E6=9B=B4=E3=82=92=E8=AD=A6?= =?UTF-8?q?=E5=91=8A=E3=83=BB=E6=8A=91=E6=AD=A2=E3=81=99=E3=82=8B=20(#6130?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit セキュリティ設定・テンプレート設定は .env を書き換えて機能するが、 docker-compose の環境変数や本番環境など .env が使われない場面では 書き換えても反映されず、ユーザーが変更できないことに気付けなかった。 - EnvFileService を新設。.env への書き込みが実行時に反映されない理由 (不在 / 書込不可 / .env.local.php スナップショット / 対象キーが OS 環境変数でオーバーライド)を検出する。 - SecurityController / TemplateController に適用し、反映されない状況では: - GET 時に該当理由の警告を表示し、登録ボタンを無効化 - POST 時はサーバ側で保存を拒否(多層防御) - 4 理由の汎用メッセージを i18n(ja/en) に追加。 - EnvFileService の単体テスト、両画面の反映不可時テストを追加。 Refs #6130 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/config/eccube/services.yaml | 4 + .../Setting/System/SecurityController.php | 33 ++++-- .../Admin/Store/TemplateController.php | 38 +++--- src/Eccube/Resource/locale/messages.en.yaml | 4 + src/Eccube/Resource/locale/messages.ja.yaml | 4 + .../admin/Setting/System/security.twig | 2 +- .../template/admin/Store/template.twig | 2 +- src/Eccube/Service/EnvFileService.php | 88 ++++++++++++++ .../Tests/Service/EnvFileServiceTest.php | 108 ++++++++++++++++++ .../Setting/System/SecurityControllerTest.php | 34 ++++++ .../Admin/Store/TemplateControllerTest.php | 14 ++- 11 files changed, 303 insertions(+), 28 deletions(-) create mode 100644 src/Eccube/Service/EnvFileService.php create mode 100644 tests/Eccube/Tests/Service/EnvFileServiceTest.php diff --git a/app/config/eccube/services.yaml b/app/config/eccube/services.yaml index ae666313f51..2d3d6c41338 100644 --- a/app/config/eccube/services.yaml +++ b/app/config/eccube/services.yaml @@ -58,6 +58,10 @@ services: lazy: true public: true + Eccube\Service\EnvFileService: + arguments: + $projectDir: '%kernel.project_dir%' + Eccube\Service\Composer\ComposerProcessService: lazy: true diff --git a/src/Eccube/Controller/Admin/Setting/System/SecurityController.php b/src/Eccube/Controller/Admin/Setting/System/SecurityController.php index 9e34fdbd1bd..a8d8ff38f7f 100644 --- a/src/Eccube/Controller/Admin/Setting/System/SecurityController.php +++ b/src/Eccube/Controller/Admin/Setting/System/SecurityController.php @@ -15,6 +15,7 @@ use Eccube\Controller\AbstractController; use Eccube\Form\Type\Admin\SecurityType; +use Eccube\Service\EnvFileService; use Eccube\Util\CacheUtil; use Eccube\Util\StringUtil; use Symfony\Bridge\Twig\Attribute\Template; @@ -25,11 +26,27 @@ class SecurityController extends AbstractController { + /** + * この画面が .env へ書き込む環境変数キー(OS 環境変数によるオーバーライド判定に使用). + */ + private const ENV_KEYS = [ + 'ECCUBE_FRONT_ALLOW_HOSTS', + 'ECCUBE_FRONT_DENY_HOSTS', + 'ECCUBE_ADMIN_ALLOW_HOSTS', + 'ECCUBE_ADMIN_DENY_HOSTS', + 'ECCUBE_FORCE_SSL', + 'TRUSTED_HOSTS', + 'ECCUBE_ADMIN_ROUTE', + ]; + /** * SecurityController constructor. */ - public function __construct(protected TokenStorageInterface $tokenStorage, private readonly CacheUtil $cacheUtil) - { + public function __construct( + protected TokenStorageInterface $tokenStorage, + private readonly CacheUtil $cacheUtil, + private readonly EnvFileService $envFileService, + ) { } /** @@ -44,8 +61,8 @@ public function index(Request $request): RedirectResponse|array $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - // .envファイルが存在しないときに設定は失敗する - if (file_exists($this->getParameter('kernel.project_dir').'/.env') === false) { + // .env への書き込みが反映されない状況では保存を拒否する + if (!$this->envFileService->isEffective(self::ENV_KEYS)) { $this->addError('admin.common.save_error', 'admin'); return $this->redirectToRoute('admin_setting_system_security'); @@ -115,13 +132,15 @@ public function index(Request $request): RedirectResponse|array $this->addWarning('admin.setting.system.security.admin_url_warning', 'admin'); } - // .envファイルが存在しない場合警告を出す。 - if (file_exists($this->getParameter('kernel.project_dir').'/.env') === false) { - $this->addWarning('admin.setting.system.security.not_found_env_file', 'admin'); + // .env への書き込みが反映されない状況では警告を出し、登録ボタンを無効化する。 + $ineffectiveReasons = $this->envFileService->getIneffectiveReasons(self::ENV_KEYS); + foreach ($ineffectiveReasons as $reason) { + $this->addWarning('admin.system.env.ineffective.'.$reason, 'admin'); } return [ 'form' => $form->createView(), + 'envWritable' => [] === $ineffectiveReasons, ]; } } diff --git a/src/Eccube/Controller/Admin/Store/TemplateController.php b/src/Eccube/Controller/Admin/Store/TemplateController.php index cca6c9be39e..2e96ffab323 100644 --- a/src/Eccube/Controller/Admin/Store/TemplateController.php +++ b/src/Eccube/Controller/Admin/Store/TemplateController.php @@ -18,6 +18,7 @@ use Eccube\Form\Type\Admin\TemplateType; use Eccube\Repository\Master\DeviceTypeRepository; use Eccube\Repository\TemplateRepository; +use Eccube\Service\EnvFileService; use Eccube\Util\CacheUtil; use Eccube\Util\StringUtil; use Symfony\Bridge\Twig\Attribute\Template; @@ -33,10 +34,15 @@ class TemplateController extends AbstractController { + /** + * この画面が .env へ書き込む環境変数キー(OS 環境変数によるオーバーライド判定に使用). + */ + private const ENV_KEYS = ['ECCUBE_TEMPLATE_CODE']; + /** * TemplateController constructor. */ - public function __construct(protected TemplateRepository $templateRepository, protected DeviceTypeRepository $deviceTypeRepository, private readonly CacheUtil $cacheUtil) + public function __construct(protected TemplateRepository $templateRepository, protected DeviceTypeRepository $deviceTypeRepository, private readonly CacheUtil $cacheUtil, private readonly EnvFileService $envFileService) { } @@ -59,10 +65,20 @@ public function index(Request $request): array|RedirectResponse $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + // .env への書き込みが反映されない状況では保存を拒否する + // - .env が存在しない / 書き込み不可 + // - .env.local.php(dump-env の最適化済みスナップショット)が .env より優先される + // - ECCUBE_TEMPLATE_CODE が OS のプロセス環境変数として設定され .env を上書きしている + if (!$this->envFileService->isEffective(self::ENV_KEYS)) { + $this->addError('admin.common.save_error', 'admin'); + + return $this->redirectToRoute('admin_store_template'); + } + $Template = $this->templateRepository->find($form['selected']->getData()); $envFile = $this->getParameter('kernel.project_dir').'/.env'; - $env = file_exists($envFile) ? file_get_contents($envFile) : ''; + $env = file_get_contents($envFile); $env = StringUtil::replaceOrAddEnv($env, [ 'ECCUBE_TEMPLATE_CODE' => $Template->getCode(), @@ -72,25 +88,21 @@ public function index(Request $request): array|RedirectResponse $this->addSuccess('admin.common.save_complete', 'admin'); - // 次のいずれかの場合、.env への書き込みが起動時のロードで反映されない: - // 1. ECCUBE_TEMPLATE_CODE がプロセス環境変数として設定されている(Docker などで明示的に設定した場合)。 - // bootEnv は OS 環境変数を上書きしないため .env の値が使われない。 - // 2. .env.local.php(dump-env の最適化済みスナップショット)が存在する。 - // bootEnv は .env より .env.local.php を優先するため、再度 `composer symfony:dump-env` を - // 実行するまで .env の変更が反映されない。 - $envLocalPhp = $this->getParameter('kernel.project_dir').'/.env.local.php'; - if (false !== getenv('ECCUBE_TEMPLATE_CODE') || file_exists($envLocalPhp)) { - $this->addWarning('admin.store.template.env_override_warning', 'admin'); - } - $this->cacheUtil->clearCache(); return $this->redirectToRoute('admin_store_template'); } + // .env への書き込みが反映されない状況では警告を出し、登録ボタンを無効化する。 + $ineffectiveReasons = $this->envFileService->getIneffectiveReasons(self::ENV_KEYS); + foreach ($ineffectiveReasons as $reason) { + $this->addWarning('admin.system.env.ineffective.'.$reason, 'admin'); + } + return [ 'form' => $form->createView(), 'Templates' => $Templates, + 'envWritable' => [] === $ineffectiveReasons, ]; } diff --git a/src/Eccube/Resource/locale/messages.en.yaml b/src/Eccube/Resource/locale/messages.en.yaml index a3f34448b1b..df5f814e363 100644 --- a/src/Eccube/Resource/locale/messages.en.yaml +++ b/src/Eccube/Resource/locale/messages.en.yaml @@ -1425,6 +1425,10 @@ admin.setting.system.security.ip_limit_invalid_ipv4: "%ip% is not an IPv4 addres admin.setting.system.security.ip_limit_invalid_https: "http is not allowed to do this setting." admin.setting.system.security.admin_url_warning: Please set the Admin Console URL that is hard to guess for security. admin.setting.system.security.not_found_env_file: .env file not found. If you do not use the .env file you can not change security config in Admin Console. +admin.system.env.ineffective.not_found: 'The .env file does not exist, so this setting cannot be changed from the admin screen. Please set the environment variables on the server directly.' +admin.system.env.ineffective.not_writable: 'The .env file is not writable, so this setting cannot be changed from the admin screen. Please check the write permission of the .env file.' +admin.system.env.ineffective.local_php: 'Because .env.local.php exists, changes to .env are not applied. Please change the environment variables on the server, or re-run composer symfony:dump-env.' +admin.system.env.ineffective.overridden: 'The environment variable for this setting is set as an OS process environment variable, so changes to .env are not applied. Please change the environment variable on the server directly.' admin.setting.system.security.trusted_hosts: Trusted hosts admin.setting.system.security.trusted_hosts_sample: ^example\.com$,^example\.org$ admin.setting.system.security.trusted_hosts_description: | diff --git a/src/Eccube/Resource/locale/messages.ja.yaml b/src/Eccube/Resource/locale/messages.ja.yaml index 9e16c989b3c..bfbb5a99904 100644 --- a/src/Eccube/Resource/locale/messages.ja.yaml +++ b/src/Eccube/Resource/locale/messages.ja.yaml @@ -1425,6 +1425,10 @@ admin.setting.system.security.ip_limit_invalid_ip_and_submask: "%ip%はIPv4/ビ admin.setting.system.security.ip_limit_invalid_https: "httpの場合には設定できません。" admin.setting.system.security.admin_url_warning: 管理画面URLは、セキュリティのため推測されにくいものを設定してください。 admin.setting.system.security.not_found_env_file: .envファイルが見つかりません。.envを利用していない場合はセキュリティ設定を管理画面から変更できません。 +admin.system.env.ineffective.not_found: '.env ファイルが存在しないため、この設定は管理画面から変更できません。サーバーの環境変数を直接設定してください。' +admin.system.env.ineffective.not_writable: '.env ファイルに書き込み権限がないため、この設定は管理画面から変更できません。.env ファイルの書き込み権限を確認してください。' +admin.system.env.ineffective.local_php: '.env.local.php が存在するため、.env への変更は反映されません。サーバーの環境変数を変更するか、composer symfony:dump-env を再実行してください。' +admin.system.env.ineffective.overridden: 'この設定の環境変数が OS のプロセス環境変数として設定されているため、.env への変更は反映されません。サーバーの環境変数を直接変更してください。' admin.setting.system.security.trusted_hosts: 信頼できるホスト名 admin.setting.system.security.trusted_hosts_sample: ^example\.com$,^example\.org$ admin.setting.system.security.trusted_hosts_description: | diff --git a/src/Eccube/Resource/template/admin/Setting/System/security.twig b/src/Eccube/Resource/template/admin/Setting/System/security.twig index 5afaf986d69..4b78931e948 100644 --- a/src/Eccube/Resource/template/admin/Setting/System/security.twig +++ b/src/Eccube/Resource/template/admin/Setting/System/security.twig @@ -219,7 +219,7 @@ file that was distributed with this source code.
- +
diff --git a/src/Eccube/Resource/template/admin/Store/template.twig b/src/Eccube/Resource/template/admin/Store/template.twig index 7d3adf5541f..642e61e4006 100644 --- a/src/Eccube/Resource/template/admin/Store/template.twig +++ b/src/Eccube/Resource/template/admin/Store/template.twig @@ -120,7 +120,7 @@ file that was distributed with this source code.
- +
diff --git a/src/Eccube/Service/EnvFileService.php b/src/Eccube/Service/EnvFileService.php new file mode 100644 index 00000000000..b5424c79cff --- /dev/null +++ b/src/Eccube/Service/EnvFileService.php @@ -0,0 +1,88 @@ +projectDir.'/.env'; + if (!file_exists($envFile)) { + $reasons[] = self::REASON_NOT_FOUND; + } elseif (!is_writable($envFile)) { + $reasons[] = self::REASON_NOT_WRITABLE; + } + + // .env.local.php があると bootEnv は .env より優先するため, .env の変更は反映されない + if (file_exists($this->projectDir.'/.env.local.php')) { + $reasons[] = self::REASON_LOCAL_PHP; + } + + // bootEnv は OS のプロセス環境変数を上書きしないため, getenv が値を返すキーは .env の変更が反映されない + foreach ($keys as $key) { + if (false !== getenv($key)) { + $reasons[] = self::REASON_OVERRIDDEN; + break; + } + } + + return $reasons; + } + + /** + * 対象の環境変数について, .env への書き込みが有効に反映されるか. + * + * @param string[] $keys 対象の環境変数キー + */ + public function isEffective(array $keys = []): bool + { + return [] === $this->getIneffectiveReasons($keys); + } +} diff --git a/tests/Eccube/Tests/Service/EnvFileServiceTest.php b/tests/Eccube/Tests/Service/EnvFileServiceTest.php new file mode 100644 index 00000000000..54c43b6d1b3 --- /dev/null +++ b/tests/Eccube/Tests/Service/EnvFileServiceTest.php @@ -0,0 +1,108 @@ +fs = new Filesystem(); + $this->projectDir = sys_get_temp_dir().'/eccube_env_test_'.uniqid(); + $this->fs->mkdir($this->projectDir); + } + + protected function tearDown(): void + { + $this->fs->remove($this->projectDir); + parent::tearDown(); + } + + public function testEffectiveWhenEnvExistsAndWritable(): void + { + $this->fs->dumpFile($this->projectDir.'/.env', "FOO=bar\n"); + + $service = new EnvFileService($this->projectDir); + + $this->assertSame([], $service->getIneffectiveReasons(['FOO'])); + $this->assertTrue($service->isEffective(['FOO'])); + } + + public function testNotFoundWhenEnvIsAbsent(): void + { + $service = new EnvFileService($this->projectDir); + + $this->assertContains(EnvFileService::REASON_NOT_FOUND, $service->getIneffectiveReasons()); + $this->assertFalse($service->isEffective()); + } + + public function testNotWritableWhenEnvIsReadOnly(): void + { + $envFile = $this->projectDir.'/.env'; + $this->fs->dumpFile($envFile, "FOO=bar\n"); + $this->fs->chmod($envFile, 0o444); + + $service = new EnvFileService($this->projectDir); + + // root で実行される環境では書き込み権限判定が効かないためスキップ + if (is_writable($envFile)) { + $this->markTestSkipped('.env is writable even with 0444 (running as root?)'); + } + + $this->assertContains(EnvFileService::REASON_NOT_WRITABLE, $service->getIneffectiveReasons()); + $this->assertFalse($service->isEffective()); + } + + public function testLocalPhpSnapshotMakesEnvIneffective(): void + { + $this->fs->dumpFile($this->projectDir.'/.env', "FOO=bar\n"); + $this->fs->dumpFile($this->projectDir.'/.env.local.php', "projectDir); + + $this->assertContains(EnvFileService::REASON_LOCAL_PHP, $service->getIneffectiveReasons()); + $this->assertFalse($service->isEffective()); + } + + public function testOverriddenWhenKeyIsInProcessEnv(): void + { + $this->fs->dumpFile($this->projectDir.'/.env', "FOO=bar\n"); + + $service = new EnvFileService($this->projectDir); + + putenv('ECCUBE_TEST_OVERRIDE_KEY=1'); + try { + $reasons = $service->getIneffectiveReasons(['ECCUBE_TEST_OVERRIDE_KEY']); + $this->assertContains(EnvFileService::REASON_OVERRIDDEN, $reasons); + $this->assertFalse($service->isEffective(['ECCUBE_TEST_OVERRIDE_KEY'])); + // 対象キーに含めなければ反映可能と判定される + $this->assertTrue($service->isEffective(['UNRELATED_KEY'])); + } finally { + putenv('ECCUBE_TEST_OVERRIDE_KEY'); + } + } +} diff --git a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php index 0a543075470..2a0549c0910 100644 --- a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php @@ -80,6 +80,40 @@ public function testSubmit() $this->assertMatchesRegularExpression('/ECCUBE_ADMIN_ROUTE='.$formData['admin_route_dir'].'/', file_get_contents($this->envFile)); } + /** + * 環境変数が OS のプロセス環境変数として設定されている場合、.env への書き込みは + * 反映されないため保存を拒否しエラーを表示することを確認する(#6130)。 + */ + #[Group(name: 'cache-clear')] + public function testSubmitRejectedWhenEnvOverridden() + { + $session = $this->createSession($this->client); + $formData = $this->createFormData(); + + // この画面が書き込む環境変数の1つをプロセス環境変数として設定 + putenv('ECCUBE_ADMIN_ROUTE=admin'); + try { + $this->client->request( + Request::METHOD_POST, + $this->generateUrl('admin_setting_system_security'), + [ + 'admin_security' => $formData, + ] + ); + + $this->assertTrue($this->client->getResponse()->isRedirection()); + + // 保存が拒否されエラーが表示される + $errors = $session->getFlashBag()->get('eccube.admin.error'); + $this->assertContains('admin.common.save_error', $errors); + + // .env は書き換えられていない + $this->assertDoesNotMatchRegularExpression('/ECCUBE_ADMIN_ROUTE='.$formData['admin_route_dir'].'/', file_get_contents($this->envFile)); + } finally { + putenv('ECCUBE_ADMIN_ROUTE'); + } + } + /** * Submit when empty */ diff --git a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php index f2dae84578f..a86296e56cd 100644 --- a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php @@ -119,10 +119,9 @@ public function testChangeTemplate() * テンプレートの変更(ECCUBE_TEMPLATE_CODEがプロセス環境変数として設定されている場合) * * Docker などでプロセス環境変数として ECCUBE_TEMPLATE_CODE が設定されている場合、 - * .env への書き込みは反映されないため警告が表示されることを確認する。 - * - * @group cache-clear + * .env への書き込みは反映されないため、保存を拒否しエラーを表示することを確認する。 */ + #[Group(name: 'cache-clear')] public function testChangeTemplateWithEnvOverride() { // テンプレートをアップロード @@ -146,9 +145,12 @@ public function testChangeTemplateWithEnvOverride() ]); $this->assertTrue($this->client->getResponse()->isRedirection()); - // 警告メッセージが表示されている - $warnings = $session->getFlashBag()->get('eccube.admin.warning'); - $this->assertContains('admin.store.template.env_override_warning', $warnings); + // 環境変数オーバーライド時は保存が拒否され、エラーが表示される + $errors = $session->getFlashBag()->get('eccube.admin.error'); + $this->assertContains('admin.common.save_error', $errors); + + // .env は書き換えられていない + $this->assertDoesNotMatchRegularExpression('/ECCUBE_TEMPLATE_CODE='.$Template->getCode().'/', file_get_contents($this->envFile)); } finally { // プロセス環境変数を元に戻す putenv('ECCUBE_TEMPLATE_CODE'); From 462396df51ce995e12de4437c967e3e9ef0ae866 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 23 Jul 2026 18:01:22 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20CodeRabbit=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E5=AF=BE=E5=BF=9C=20=E2=80=94=20.env.local.p?= =?UTF-8?q?hp=E6=A1=88=E5=86=85=E6=96=87=E3=83=BB=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=81=AEenv=E5=BE=A9=E5=85=83=E3=83=BB=E6=88=BB?= =?UTF-8?q?=E3=82=8A=E5=80=A4=E5=9E=8B=20(#6130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - messages(ja/en): local_php の案内を実行可能な手順に修正 (管理画面ではブロックされるため、サーバ側で .env 更新後に dump-env 再実行、 または .env.local.php 削除で再開、と明示) - テスト3件: putenv での復元を「無条件 unset」から「事前値を保存し復元」に変更し、 実行環境が設定済みの環境変数を消さないようにする - SecurityControllerTest::testSubmitRejectedWhenEnvOverridden に戻り値型 void を付与 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Eccube/Resource/locale/messages.en.yaml | 2 +- src/Eccube/Resource/locale/messages.ja.yaml | 2 +- tests/Eccube/Tests/Service/EnvFileServiceTest.php | 11 +++++++---- .../Admin/Setting/System/SecurityControllerTest.php | 9 ++++++--- .../Tests/Web/Admin/Store/TemplateControllerTest.php | 8 +++++--- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Eccube/Resource/locale/messages.en.yaml b/src/Eccube/Resource/locale/messages.en.yaml index df5f814e363..5894819b2a6 100644 --- a/src/Eccube/Resource/locale/messages.en.yaml +++ b/src/Eccube/Resource/locale/messages.en.yaml @@ -1427,7 +1427,7 @@ admin.setting.system.security.admin_url_warning: Please set the Admin Console UR admin.setting.system.security.not_found_env_file: .env file not found. If you do not use the .env file you can not change security config in Admin Console. admin.system.env.ineffective.not_found: 'The .env file does not exist, so this setting cannot be changed from the admin screen. Please set the environment variables on the server directly.' admin.system.env.ineffective.not_writable: 'The .env file is not writable, so this setting cannot be changed from the admin screen. Please check the write permission of the .env file.' -admin.system.env.ineffective.local_php: 'Because .env.local.php exists, changes to .env are not applied. Please change the environment variables on the server, or re-run composer symfony:dump-env.' +admin.system.env.ineffective.local_php: 'Because .env.local.php exists, changes to .env are not applied and cannot be made from the admin screen. Update .env on the server and re-run composer dump-env, or remove .env.local.php to re-enable changes from the admin screen.' admin.system.env.ineffective.overridden: 'The environment variable for this setting is set as an OS process environment variable, so changes to .env are not applied. Please change the environment variable on the server directly.' admin.setting.system.security.trusted_hosts: Trusted hosts admin.setting.system.security.trusted_hosts_sample: ^example\.com$,^example\.org$ diff --git a/src/Eccube/Resource/locale/messages.ja.yaml b/src/Eccube/Resource/locale/messages.ja.yaml index bfbb5a99904..28bde451671 100644 --- a/src/Eccube/Resource/locale/messages.ja.yaml +++ b/src/Eccube/Resource/locale/messages.ja.yaml @@ -1427,7 +1427,7 @@ admin.setting.system.security.admin_url_warning: 管理画面URLは、セキュ admin.setting.system.security.not_found_env_file: .envファイルが見つかりません。.envを利用していない場合はセキュリティ設定を管理画面から変更できません。 admin.system.env.ineffective.not_found: '.env ファイルが存在しないため、この設定は管理画面から変更できません。サーバーの環境変数を直接設定してください。' admin.system.env.ineffective.not_writable: '.env ファイルに書き込み権限がないため、この設定は管理画面から変更できません。.env ファイルの書き込み権限を確認してください。' -admin.system.env.ineffective.local_php: '.env.local.php が存在するため、.env への変更は反映されません。サーバーの環境変数を変更するか、composer symfony:dump-env を再実行してください。' +admin.system.env.ineffective.local_php: '.env.local.php が存在するため、.env への変更は反映されず、管理画面からは変更できません。サーバー側で .env を更新してから composer dump-env を再実行するか、.env.local.php を削除して管理画面からの変更を再開してください。' admin.system.env.ineffective.overridden: 'この設定の環境変数が OS のプロセス環境変数として設定されているため、.env への変更は反映されません。サーバーの環境変数を直接変更してください。' admin.setting.system.security.trusted_hosts: 信頼できるホスト名 admin.setting.system.security.trusted_hosts_sample: ^example\.com$,^example\.org$ diff --git a/tests/Eccube/Tests/Service/EnvFileServiceTest.php b/tests/Eccube/Tests/Service/EnvFileServiceTest.php index 54c43b6d1b3..8afd703189b 100644 --- a/tests/Eccube/Tests/Service/EnvFileServiceTest.php +++ b/tests/Eccube/Tests/Service/EnvFileServiceTest.php @@ -94,15 +94,18 @@ public function testOverriddenWhenKeyIsInProcessEnv(): void $service = new EnvFileService($this->projectDir); - putenv('ECCUBE_TEST_OVERRIDE_KEY=1'); + $key = 'ECCUBE_TEST_OVERRIDE_KEY'; + $original = getenv($key); + putenv($key.'=1'); try { - $reasons = $service->getIneffectiveReasons(['ECCUBE_TEST_OVERRIDE_KEY']); + $reasons = $service->getIneffectiveReasons([$key]); $this->assertContains(EnvFileService::REASON_OVERRIDDEN, $reasons); - $this->assertFalse($service->isEffective(['ECCUBE_TEST_OVERRIDE_KEY'])); + $this->assertFalse($service->isEffective([$key])); // 対象キーに含めなければ反映可能と判定される $this->assertTrue($service->isEffective(['UNRELATED_KEY'])); } finally { - putenv('ECCUBE_TEST_OVERRIDE_KEY'); + // 実行環境が事前に設定していた値を復元する + false === $original ? putenv($key) : putenv($key.'='.$original); } } } diff --git a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php index 2a0549c0910..fc8eb91bbcf 100644 --- a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php @@ -85,13 +85,15 @@ public function testSubmit() * 反映されないため保存を拒否しエラーを表示することを確認する(#6130)。 */ #[Group(name: 'cache-clear')] - public function testSubmitRejectedWhenEnvOverridden() + public function testSubmitRejectedWhenEnvOverridden(): void { $session = $this->createSession($this->client); $formData = $this->createFormData(); // この画面が書き込む環境変数の1つをプロセス環境変数として設定 - putenv('ECCUBE_ADMIN_ROUTE=admin'); + $key = 'ECCUBE_ADMIN_ROUTE'; + $original = getenv($key); + putenv($key.'=admin'); try { $this->client->request( Request::METHOD_POST, @@ -110,7 +112,8 @@ public function testSubmitRejectedWhenEnvOverridden() // .env は書き換えられていない $this->assertDoesNotMatchRegularExpression('/ECCUBE_ADMIN_ROUTE='.$formData['admin_route_dir'].'/', file_get_contents($this->envFile)); } finally { - putenv('ECCUBE_ADMIN_ROUTE'); + // 実行環境が事前に設定していた値を復元する + false === $original ? putenv($key) : putenv($key.'='.$original); } } diff --git a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php index a86296e56cd..a0894a3f02a 100644 --- a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php @@ -131,7 +131,9 @@ public function testChangeTemplateWithEnvOverride() $Template = $this->templateRepository->findOneBy(['code' => $this->code]); // プロセス環境変数として ECCUBE_TEMPLATE_CODE を設定 - putenv('ECCUBE_TEMPLATE_CODE=default'); + $key = 'ECCUBE_TEMPLATE_CODE'; + $original = getenv($key); + putenv($key.'=default'); try { $session = $this->createSession($this->client); @@ -152,8 +154,8 @@ public function testChangeTemplateWithEnvOverride() // .env は書き換えられていない $this->assertDoesNotMatchRegularExpression('/ECCUBE_TEMPLATE_CODE='.$Template->getCode().'/', file_get_contents($this->envFile)); } finally { - // プロセス環境変数を元に戻す - putenv('ECCUBE_TEMPLATE_CODE'); + // 実行環境が事前に設定していた値を復元する + false === $original ? putenv($key) : putenv($key.'='.$original); } } From d1f67abddd53ecae13530fbf08be845882523506 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 23 Jul 2026 18:04:26 +0900 Subject: [PATCH 3/6] =?UTF-8?q?test:=20.env=20=E5=8F=8D=E6=98=A0=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E6=99=82=E3=81=AE=E8=AD=A6=E5=91=8A=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=81=A8=E7=99=BB=E9=8C=B2=E3=83=9C=E3=82=BF=E3=83=B3=E7=84=A1?= =?UTF-8?q?=E5=8A=B9=E5=8C=96=E3=82=92=E6=A4=9C=E8=A8=BC=20(#6130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST 拒否に加え、GET 時の「警告表示」と「登録ボタン無効化」(多層防御の 警告・抑止側)を Security/Template 両画面で回帰検証する。 環境変数オーバーライド状態で画面を開き、submit ボタンに disabled が付き、 反映されない旨の警告メッセージが描画されることを確認する。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Setting/System/SecurityControllerTest.php | 27 +++++++++++++++++++ .../Admin/Store/TemplateControllerTest.php | 27 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php index fc8eb91bbcf..551a5de4add 100644 --- a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php @@ -80,6 +80,33 @@ public function testSubmit() $this->assertMatchesRegularExpression('/ECCUBE_ADMIN_ROUTE='.$formData['admin_route_dir'].'/', file_get_contents($this->envFile)); } + /** + * 環境変数が OS のプロセス環境変数として設定されている場合、画面表示時に + * 警告を表示し、登録ボタンを無効化することを確認する(#6130)。 + */ + #[Group(name: 'cache-clear')] + public function testDisplayWarningAndDisableButtonWhenEnvOverridden(): void + { + $key = 'ECCUBE_ADMIN_ROUTE'; + $original = getenv($key); + putenv($key.'=admin'); + try { + $crawler = $this->client->request(Request::METHOD_GET, $this->generateUrl('admin_setting_system_security')); + $this->assertTrue($this->client->getResponse()->isSuccessful()); + + // 登録ボタンが無効化されている + $this->assertGreaterThan(0, $crawler->filter('button[type="submit"][disabled]')->count()); + + // 反映されない旨の警告が表示されている + $this->assertStringContainsString( + trans('admin.system.env.ineffective.overridden'), + (string) $this->client->getResponse()->getContent() + ); + } finally { + false === $original ? putenv($key) : putenv($key.'='.$original); + } + } + /** * 環境変数が OS のプロセス環境変数として設定されている場合、.env への書き込みは * 反映されないため保存を拒否しエラーを表示することを確認する(#6130)。 diff --git a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php index a0894a3f02a..5d9a74cc7b0 100644 --- a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php @@ -89,6 +89,33 @@ public function testDisplayList() $this->assertTrue($this->client->getResponse()->isSuccessful()); } + /** + * ECCUBE_TEMPLATE_CODE がプロセス環境変数として設定されている場合、画面表示時に + * 警告を表示し、登録ボタンを無効化することを確認する(#6130)。 + */ + #[Group(name: 'cache-clear')] + public function testDisplayWarningAndDisableButtonWhenEnvOverridden() + { + $key = 'ECCUBE_TEMPLATE_CODE'; + $original = getenv($key); + putenv($key.'=default'); + try { + $crawler = $this->client->request(Request::METHOD_GET, $this->generateUrl('admin_store_template')); + $this->assertTrue($this->client->getResponse()->isSuccessful()); + + // 登録ボタンが無効化されている + $this->assertGreaterThan(0, $crawler->filter('button[type="submit"][disabled]')->count()); + + // 反映されない旨の警告が表示されている + $this->assertStringContainsString( + trans('admin.system.env.ineffective.overridden'), + (string) $this->client->getResponse()->getContent() + ); + } finally { + false === $original ? putenv($key) : putenv($key.'='.$original); + } + } + /** * テンプレートの変更 */ From 04bf71f6d9de34440aa0b946e0ee0b9f2ed01478 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Fri, 24 Jul 2026 11:07:18 +0900 Subject: [PATCH 4/6] =?UTF-8?q?style:=20TemplateController=20=E3=81=AE=20d?= =?UTF-8?q?ownload/delete=20=E3=81=8B=E3=82=89=E6=9C=AA=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E3=81=AE=20Request=20=E5=BC=95=E6=95=B0=E3=82=92=E9=99=A4?= =?UTF-8?q?=E5=8E=BB=20(#6130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rector (RemoveUnusedRequestParamRector) の指摘に対応。CI の rector ジョブを緑にする。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Eccube/Controller/Admin/Store/TemplateController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Eccube/Controller/Admin/Store/TemplateController.php b/src/Eccube/Controller/Admin/Store/TemplateController.php index 2e96ffab323..6ddd8dcfc2f 100644 --- a/src/Eccube/Controller/Admin/Store/TemplateController.php +++ b/src/Eccube/Controller/Admin/Store/TemplateController.php @@ -110,7 +110,7 @@ public function index(Request $request): array|RedirectResponse * テンプレート一覧からのダウンロード */ #[Route(path: '/%eccube_admin_route%/store/template/{id}/download', name: 'admin_store_template_download', requirements: ['id' => '\d+'], methods: ['GET'])] - public function download(Request $request, \Eccube\Entity\Template $Template): BinaryFileResponse + public function download(\Eccube\Entity\Template $Template): BinaryFileResponse { // 該当テンプレートのディレクトリ $templateCode = $Template->getCode(); @@ -167,7 +167,7 @@ public function download(Request $request, \Eccube\Entity\Template $Template): B } #[Route(path: '/%eccube_admin_route%/store/template/{id}/delete', name: 'admin_store_template_delete', requirements: ['id' => '\d+'], methods: ['DELETE'])] - public function delete(Request $request, \Eccube\Entity\Template $Template): RedirectResponse + public function delete(\Eccube\Entity\Template $Template): RedirectResponse { $this->isTokenValid(); From 9bb1d80a7f3920bb9096eeaed9a020a89e9fe292 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Tue, 28 Jul 2026 20:35:26 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix(admin):=20.env=20=E5=8F=8D=E6=98=A0?= =?UTF-8?q?=E5=8F=AF=E5=90=A6=E3=81=AE=E5=88=A4=E5=AE=9A=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E3=83=96=E3=83=BC=E3=83=88=E7=B5=8C=E8=B7=AF=E3=81=AB=E5=90=88?= =?UTF-8?q?=E3=82=8F=E3=81=9B=E9=81=8E=E5=89=B0=E3=83=96=E3=83=AD=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=82=92=E8=A7=A3=E6=B6=88=E3=81=99=E3=82=8B=20(#6130?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getenv 依存の判定を SYMFONY_DOTENV_VARS ベースへ変更し、index.php の boot_env(.., true) 経路で .env が勝つのに上書きと誤検知する退行を解消 - getOverriddenKeys をキー単位判定にし、1 キーの上書きで画面全体を止めない よう切り分け(Security は反映できる残キーを保存・ボタン有効を維持) - .env.local / .env.$APP_ENV(.local) のカスケード上書きを Dotenv::parse で 対象キー定義時のみ検出 - overridden 警告に原因キー名(%keys%)を埋め込み、POST 拒否時も理由を併記 - 参照されなくなった翻訳キーを削除し、両ブート経路・カスケードの検証を EnvFileServiceTest へ集約、コントローラテストの上書き再現を $_SERVER 方式へ Co-Authored-By: Claude Opus 4.8 --- .../Setting/System/SecurityController.php | 40 ++++-- .../Admin/Store/TemplateController.php | 23 +++- src/Eccube/Resource/locale/messages.en.yaml | 4 +- src/Eccube/Resource/locale/messages.ja.yaml | 4 +- src/Eccube/Service/EnvFileService.php | 109 +++++++++++++-- .../Tests/Service/EnvFileServiceTest.php | 130 ++++++++++++++++-- .../Setting/System/SecurityControllerTest.php | 107 +++++++++----- .../Admin/Store/TemplateControllerTest.php | 70 +++++++--- 8 files changed, 387 insertions(+), 100 deletions(-) diff --git a/src/Eccube/Controller/Admin/Setting/System/SecurityController.php b/src/Eccube/Controller/Admin/Setting/System/SecurityController.php index a8d8ff38f7f..4caece0aace 100644 --- a/src/Eccube/Controller/Admin/Setting/System/SecurityController.php +++ b/src/Eccube/Controller/Admin/Setting/System/SecurityController.php @@ -61,13 +61,20 @@ public function index(Request $request): RedirectResponse|array $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - // .env への書き込みが反映されない状況では保存を拒否する - if (!$this->envFileService->isEffective(self::ENV_KEYS)) { + // .env ファイル自体が読み込まれない状況(全キー反映不可)では保存を拒否する + $ineffectiveReasons = $this->envFileService->getIneffectiveReasons(); + if ([] !== $ineffectiveReasons) { $this->addError('admin.common.save_error', 'admin'); + foreach ($ineffectiveReasons as $reason) { + $this->addError('admin.system.env.ineffective.'.$reason, 'admin'); + } return $this->redirectToRoute('admin_setting_system_security'); } + // OS 環境変数やカスケードファイルで上書きされているキーは書き込んでも反映されないため除外する + $overriddenKeys = $this->envFileService->getOverriddenKeys(self::ENV_KEYS); + $data = $form->getData(); $envFile = $this->getParameter('kernel.project_dir').'/.env'; $env = file_get_contents($envFile); @@ -86,20 +93,29 @@ public function index(Request $request): RedirectResponse|array array_filter(\explode("\n", StringUtil::convertLineFeed($data['admin_deny_hosts'])), fn ($str) => StringUtil::isNotBlank($str)) ); - $env = StringUtil::replaceOrAddEnv($env, [ + // 上書きされているキーは書き込み対象から除外する + $replace = array_diff_key([ 'ECCUBE_FRONT_ALLOW_HOSTS' => "'{$frontAllowHosts}'", 'ECCUBE_FRONT_DENY_HOSTS' => "'{$frontDenyHosts}'", 'ECCUBE_ADMIN_ALLOW_HOSTS' => "'{$adminAllowHosts}'", 'ECCUBE_ADMIN_DENY_HOSTS' => "'{$adminDenyHosts}'", 'ECCUBE_FORCE_SSL' => $data['force_ssl'] ? '1' : '0', 'TRUSTED_HOSTS' => $data['trusted_hosts'], - ]); + ], array_flip($overriddenKeys)); - file_put_contents($envFile, $env); + if ([] !== $replace) { + $env = StringUtil::replaceOrAddEnv($env, $replace); + file_put_contents($envFile, $env); + } - // 管理画面URLの更新. 変更されている場合はログアウトし再ログインさせる. + // 上書きされ反映されなかったキーは名指しで警告する + if ([] !== $overriddenKeys) { + $this->addWarning(trans('admin.system.env.ineffective.overridden', ['%keys%' => implode(', ', $overriddenKeys)]), 'admin'); + } + + // 管理画面URLの更新. 上書きされておらず変更されている場合はログアウトし再ログインさせる. $adminRoute = $this->eccubeConfig['eccube_admin_route']; - if ($adminRoute !== $data['admin_route_dir']) { + if ($adminRoute !== $data['admin_route_dir'] && !in_array('ECCUBE_ADMIN_ROUTE', $overriddenKeys, true)) { $env = StringUtil::replaceOrAddEnv($env, [ 'ECCUBE_ADMIN_ROUTE' => $data['admin_route_dir'], ]); @@ -132,12 +148,18 @@ public function index(Request $request): RedirectResponse|array $this->addWarning('admin.setting.system.security.admin_url_warning', 'admin'); } - // .env への書き込みが反映されない状況では警告を出し、登録ボタンを無効化する。 - $ineffectiveReasons = $this->envFileService->getIneffectiveReasons(self::ENV_KEYS); + // .env ファイル自体が読み込まれない状況では警告を出し、登録ボタンを無効化する。 + $ineffectiveReasons = $this->envFileService->getIneffectiveReasons(); foreach ($ineffectiveReasons as $reason) { $this->addWarning('admin.system.env.ineffective.'.$reason, 'admin'); } + // 個々のキーが OS 環境変数等で上書きされている場合は該当キーを名指しで警告する。 + $overriddenKeys = $this->envFileService->getOverriddenKeys(self::ENV_KEYS); + if ([] !== $overriddenKeys) { + $this->addWarning(trans('admin.system.env.ineffective.overridden', ['%keys%' => implode(', ', $overriddenKeys)]), 'admin'); + } + return [ 'form' => $form->createView(), 'envWritable' => [] === $ineffectiveReasons, diff --git a/src/Eccube/Controller/Admin/Store/TemplateController.php b/src/Eccube/Controller/Admin/Store/TemplateController.php index 6ddd8dcfc2f..fc3a987eb08 100644 --- a/src/Eccube/Controller/Admin/Store/TemplateController.php +++ b/src/Eccube/Controller/Admin/Store/TemplateController.php @@ -68,9 +68,17 @@ public function index(Request $request): array|RedirectResponse // .env への書き込みが反映されない状況では保存を拒否する // - .env が存在しない / 書き込み不可 // - .env.local.php(dump-env の最適化済みスナップショット)が .env より優先される - // - ECCUBE_TEMPLATE_CODE が OS のプロセス環境変数として設定され .env を上書きしている - if (!$this->envFileService->isEffective(self::ENV_KEYS)) { + // - ECCUBE_TEMPLATE_CODE が OS 環境変数やカスケードファイルで .env を上書きしている + $ineffectiveReasons = $this->envFileService->getIneffectiveReasons(); + $overriddenKeys = $this->envFileService->getOverriddenKeys(self::ENV_KEYS); + if ([] !== $ineffectiveReasons || [] !== $overriddenKeys) { $this->addError('admin.common.save_error', 'admin'); + foreach ($ineffectiveReasons as $reason) { + $this->addError('admin.system.env.ineffective.'.$reason, 'admin'); + } + if ([] !== $overriddenKeys) { + $this->addError(trans('admin.system.env.ineffective.overridden', ['%keys%' => implode(', ', $overriddenKeys)]), 'admin'); + } return $this->redirectToRoute('admin_store_template'); } @@ -94,15 +102,22 @@ public function index(Request $request): array|RedirectResponse } // .env への書き込みが反映されない状況では警告を出し、登録ボタンを無効化する。 - $ineffectiveReasons = $this->envFileService->getIneffectiveReasons(self::ENV_KEYS); + $ineffectiveReasons = $this->envFileService->getIneffectiveReasons(); foreach ($ineffectiveReasons as $reason) { $this->addWarning('admin.system.env.ineffective.'.$reason, 'admin'); } + // 対象キー(ECCUBE_TEMPLATE_CODE)が上書きされている場合は名指しで警告する。 + $overriddenKeys = $this->envFileService->getOverriddenKeys(self::ENV_KEYS); + if ([] !== $overriddenKeys) { + $this->addWarning(trans('admin.system.env.ineffective.overridden', ['%keys%' => implode(', ', $overriddenKeys)]), 'admin'); + } + return [ 'form' => $form->createView(), 'Templates' => $Templates, - 'envWritable' => [] === $ineffectiveReasons, + // この画面が扱うキーは 1 つのみのため, 上書きされていれば実質全滅として無効化する。 + 'envWritable' => [] === $ineffectiveReasons && [] === $overriddenKeys, ]; } diff --git a/src/Eccube/Resource/locale/messages.en.yaml b/src/Eccube/Resource/locale/messages.en.yaml index ea4755889f8..acd8d8cf27a 100644 --- a/src/Eccube/Resource/locale/messages.en.yaml +++ b/src/Eccube/Resource/locale/messages.en.yaml @@ -1428,11 +1428,10 @@ admin.setting.system.security.force_ssl_description: Only https access is allowe admin.setting.system.security.ip_limit_invalid_ipv4: "%ip% is not an IPv4 address." admin.setting.system.security.ip_limit_invalid_https: "http is not allowed to do this setting." admin.setting.system.security.admin_url_warning: Please set the Admin Console URL that is hard to guess for security. -admin.setting.system.security.not_found_env_file: .env file not found. If you do not use the .env file you can not change security config in Admin Console. admin.system.env.ineffective.not_found: 'The .env file does not exist, so this setting cannot be changed from the admin screen. Please set the environment variables on the server directly.' admin.system.env.ineffective.not_writable: 'The .env file is not writable, so this setting cannot be changed from the admin screen. Please check the write permission of the .env file.' admin.system.env.ineffective.local_php: 'Because .env.local.php exists, changes to .env are not applied and cannot be made from the admin screen. Update .env on the server and re-run composer dump-env, or remove .env.local.php to re-enable changes from the admin screen.' -admin.system.env.ineffective.overridden: 'The environment variable for this setting is set as an OS process environment variable, so changes to .env are not applied. Please change the environment variable on the server directly.' +admin.system.env.ineffective.overridden: 'The environment variable(s) for this setting (%keys%) are overridden by OS process environment variables or a cascade file (.env.local, etc.), so changes to .env are not applied. Please change the environment variable on the server directly.' admin.setting.system.security.trusted_hosts: Trusted hosts admin.setting.system.security.trusted_hosts_sample: ^example\.com$,^example\.org$ admin.setting.system.security.trusted_hosts_description: | @@ -1519,7 +1518,6 @@ admin.store.template.delete_error__current_template: You are not allowed to dele admin.store.template.template_code_already_exists: This template code is already used. admin.store.template.upload_failed: Failed to upload admin.store.template.invalid_upload_file: Wrong extension of the file -admin.store.template.env_override_warning: "ECCUBE_TEMPLATE_CODE is set as an environment variable, so changes to .env will not take effect. To switch templates, update the ECCUBE_TEMPLATE_CODE environment variable." admin.store.plugin: Plugins admin.store.plugin.plugin_owners_install: Search Plugins admin.store.plugin.plugin_list: All Plugins diff --git a/src/Eccube/Resource/locale/messages.ja.yaml b/src/Eccube/Resource/locale/messages.ja.yaml index 6c275d9d4eb..10445399ff9 100644 --- a/src/Eccube/Resource/locale/messages.ja.yaml +++ b/src/Eccube/Resource/locale/messages.ja.yaml @@ -1428,11 +1428,10 @@ admin.setting.system.security.ip_limit_invalid_ipv4: "%ip%はIPv4アドレスで admin.setting.system.security.ip_limit_invalid_ip_and_submask: "%ip%はIPv4/ビットマスクの形式ではありません。" admin.setting.system.security.ip_limit_invalid_https: "httpの場合には設定できません。" admin.setting.system.security.admin_url_warning: 管理画面URLは、セキュリティのため推測されにくいものを設定してください。 -admin.setting.system.security.not_found_env_file: .envファイルが見つかりません。.envを利用していない場合はセキュリティ設定を管理画面から変更できません。 admin.system.env.ineffective.not_found: '.env ファイルが存在しないため、この設定は管理画面から変更できません。サーバーの環境変数を直接設定してください。' admin.system.env.ineffective.not_writable: '.env ファイルに書き込み権限がないため、この設定は管理画面から変更できません。.env ファイルの書き込み権限を確認してください。' admin.system.env.ineffective.local_php: '.env.local.php が存在するため、.env への変更は反映されず、管理画面からは変更できません。サーバー側で .env を更新してから composer dump-env を再実行するか、.env.local.php を削除して管理画面からの変更を再開してください。' -admin.system.env.ineffective.overridden: 'この設定の環境変数が OS のプロセス環境変数として設定されているため、.env への変更は反映されません。サーバーの環境変数を直接変更してください。' +admin.system.env.ineffective.overridden: 'この設定の環境変数(%keys%)が OS のプロセス環境変数またはカスケードファイル(.env.local 等)で上書きされているため、.env への変更は反映されません。サーバーの環境変数を直接変更してください。' admin.setting.system.security.trusted_hosts: 信頼できるホスト名 admin.setting.system.security.trusted_hosts_sample: ^example\.com$,^example\.org$ admin.setting.system.security.trusted_hosts_description: | @@ -1519,7 +1518,6 @@ admin.store.template.delete_error__current_template: 設定中のテンプレー admin.store.template.template_code_already_exists: テンプレートコードは既に利用されています admin.store.template.upload_failed: アップロードに失敗しました admin.store.template.invalid_upload_file: ファイルの拡張子に誤りがあります -admin.store.template.env_override_warning: "ECCUBE_TEMPLATE_CODE が環境変数として設定されているため、.env への変更は反映されません。テンプレートを切り替えるには環境変数の ECCUBE_TEMPLATE_CODE を変更してください。" admin.store.plugin: プラグイン admin.store.plugin.plugin_owners_install: プラグインを探す admin.store.plugin.plugin_list: プラグイン一覧 diff --git a/src/Eccube/Service/EnvFileService.php b/src/Eccube/Service/EnvFileService.php index b5424c79cff..f4642914c73 100644 --- a/src/Eccube/Service/EnvFileService.php +++ b/src/Eccube/Service/EnvFileService.php @@ -13,6 +13,9 @@ namespace Eccube\Service; +use Symfony\Component\Dotenv\Dotenv; +use Symfony\Component\Dotenv\Exception\FormatException; + /** * .env ファイルへの書き込みが実行時に反映されるかを判定するサービス. * @@ -20,6 +23,12 @@ * docker-compose の環境変数や本番環境では .env が使われず, 書き換えても反映されない. * ユーザーが変更できないことに気付けるよう, 反映されない理由を検出する. * + * 反映不可の理由は 2 種類に分かれる. + * - ファイル単位で全キーが反映されない理由(getIneffectiveReasons): + * .env が存在しない / 書き込み不可 / .env.local.php が優先される. + * - キー単位で反映されない理由(getOverriddenKeys): + * OS のプロセス環境変数, または .env.local 等のカスケードファイルが .env を上書きしている. + * * @see https://github.com/EC-CUBE/ec-cube/issues/6130 */ class EnvFileService @@ -41,15 +50,15 @@ public function __construct(private readonly string $projectDir) } /** - * .env への書き込みが実行時のロードに反映されない理由を返す. - * - * 空配列であれば .env への書き込みが有効に反映される. + * .env への書き込みが(キーによらず)実行時のロードに反映されない理由を返す. * - * @param string[] $keys 対象の環境変数キー(OS 環境変数によるオーバーライド判定に使用) + * ここで返るのはファイル単位で全キーが反映されない理由のみ. + * 空配列であれば, 少なくとも .env ファイル自体は読み込まれる状態にある. + * 個々のキーが上書きされているかは {@see getOverriddenKeys()} で判定する. * - * @return string[] REASON_* 定数の配列 + * @return string[] REASON_NOT_FOUND / REASON_NOT_WRITABLE / REASON_LOCAL_PHP の配列 */ - public function getIneffectiveReasons(array $keys = []): array + public function getIneffectiveReasons(): array { $reasons = []; @@ -65,15 +74,51 @@ public function getIneffectiveReasons(array $keys = []): array $reasons[] = self::REASON_LOCAL_PHP; } - // bootEnv は OS のプロセス環境変数を上書きしないため, getenv が値を返すキーは .env の変更が反映されない + return $reasons; + } + + /** + * 対象キーのうち, .env への書き込みが実行時に反映されないキー(上書きされているキー)を返す. + * + * Symfony Dotenv は putenv を使わないため getenv だけでは判定できない. + * .env 系ファイルから実際に populate されたキーは $_SERVER['SYMFONY_DOTENV_VARS'] に載るので, + * そこに含まれ, かつカスケードファイルで再定義されていないキーは「.env が実効」=反映されると判定する. + * それ以外で $_ENV / $_SERVER / getenv に値があるキーは OS 環境変数側が勝つため反映されない. + * + * @param string[] $keys 対象の環境変数キー + * + * @return string[] 上書きされているキーの部分集合 + */ + public function getOverriddenKeys(array $keys): array + { + if ([] === $keys) { + return []; + } + + // Dotenv が実際に .env 系ファイルから populate したキーの一覧. + // これらは .env(系)の値が実効値なので, .env への書き込みは反映される. + $dotenvVars = array_flip(array_filter(explode(',', + (string) ($_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '') + ))); + + // .env より後に読まれ .env の値を上書きするカスケードファイルで定義されたキー. + $cascadeKeys = $this->getCascadeDefinedKeys(); + + $overridden = []; foreach ($keys as $key) { - if (false !== getenv($key)) { - $reasons[] = self::REASON_OVERRIDDEN; - break; + // .env 系から populate されており, カスケードで再定義されていなければ反映される. + if (isset($dotenvVars[$key]) && !isset($cascadeKeys[$key])) { + continue; + } + // カスケードで再定義されている, もしくは Dotenv が触れていない + // プロセス環境変数($_ENV / $_SERVER / getenv)が存在する場合は反映されない. + if (isset($cascadeKeys[$key]) + || isset($_ENV[$key]) || isset($_SERVER[$key]) || false !== getenv($key)) { + $overridden[] = $key; } } - return $reasons; + return $overridden; } /** @@ -83,6 +128,46 @@ public function getIneffectiveReasons(array $keys = []): array */ public function isEffective(array $keys = []): bool { - return [] === $this->getIneffectiveReasons($keys); + return [] === $this->getIneffectiveReasons() && [] === $this->getOverriddenKeys($keys); + } + + /** + * .env の後に読まれるカスケードファイルで定義されているキーの一覧を返す. + * + * Dotenv::loadEnv は .env → .env.local → .env.$env → .env.$env.local の順に読み, + * 後から読んだファイルが .env の値を上書きする. ファイルの存在だけでは過剰なため, + * 実際にキーを定義しているファイルのみを対象にする. + * + * @return array キー名 => true + */ + private function getCascadeDefinedKeys(): array + { + $appEnv = $_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null; + $cascadeFiles = array_filter([ + '.env.local', + $appEnv ? '.env.'.$appEnv : null, + $appEnv ? '.env.'.$appEnv.'.local' : null, + ]); + + $defined = []; + foreach ($cascadeFiles as $file) { + $path = $this->projectDir.'/'.$file; + if (!is_file($path) || !is_readable($path)) { + continue; + } + + try { + $parsed = (new Dotenv())->parse((string) file_get_contents($path), $path); + } catch (FormatException) { + // 解析できないファイルは判定対象から除外する(管理画面を落とさない) + continue; + } + + foreach (array_keys($parsed) as $name) { + $defined[$name] = true; + } + } + + return $defined; } } diff --git a/tests/Eccube/Tests/Service/EnvFileServiceTest.php b/tests/Eccube/Tests/Service/EnvFileServiceTest.php index 8afd703189b..3c4286f0d1b 100644 --- a/tests/Eccube/Tests/Service/EnvFileServiceTest.php +++ b/tests/Eccube/Tests/Service/EnvFileServiceTest.php @@ -48,7 +48,7 @@ public function testEffectiveWhenEnvExistsAndWritable(): void $service = new EnvFileService($this->projectDir); - $this->assertSame([], $service->getIneffectiveReasons(['FOO'])); + $this->assertSame([], $service->getIneffectiveReasons()); $this->assertTrue($service->isEffective(['FOO'])); } @@ -88,24 +88,130 @@ public function testLocalPhpSnapshotMakesEnvIneffective(): void $this->assertFalse($service->isEffective()); } - public function testOverriddenWhenKeyIsInProcessEnv(): void + /** + * .env 系ファイルから Dotenv が populate したキー(SYMFONY_DOTENV_VARS に載る)は, + * $_SERVER / getenv に同名の値があっても「.env が実効」として上書き扱いしないこと. + * + * index.php:34 の boot_env(.., true) 経路(APP_ENV 未設定=非 Docker)では .env が + * OS 環境変数に勝つ. この経路を getenv だけで判定すると誤検知し, 反映される保存を + * ブロックしてしまう回帰を防ぐ(#6130 の核心). + */ + public function testNotOverriddenWhenKeyIsPopulatedFromDotenv(): void + { + $this->fs->dumpFile($this->projectDir.'/.env', "ECCUBE_TEST_KEY_A=fromdotenv\n"); + $service = new EnvFileService($this->projectDir); + $key = 'ECCUBE_TEST_KEY_A'; + + $this->withGlobals([ + 'server' => ['SYMFONY_DOTENV_VARS' => $key, $key => 'fromdotenv'], + ], function () use ($service, $key): void { + $this->assertSame([], $service->getOverriddenKeys([$key])); + $this->assertTrue($service->isEffective([$key])); + }); + } + + /** + * Dotenv が触れていない(SYMFONY_DOTENV_VARS 非掲載)のに $_SERVER 側に値がある + * キー(nginx fastcgi_param / Apache SetEnv 相当)は上書きと判定すること. + */ + public function testOverriddenWhenKeyOnlyInServer(): void { $this->fs->dumpFile($this->projectDir.'/.env', "FOO=bar\n"); + $service = new EnvFileService($this->projectDir); + $key = 'ECCUBE_TEST_KEY_B'; + + $this->withGlobals([ + 'server' => ['SYMFONY_DOTENV_VARS' => '', $key => 'osvalue'], + ], function () use ($service, $key): void { + $this->assertSame([$key], $service->getOverriddenKeys([$key])); + $this->assertFalse($service->isEffective([$key])); + }); + } + + /** + * .env.local / .env.$APP_ENV 等のカスケードファイルで対象キーが再定義されている場合, + * .env の値は上書きされるため上書きと判定すること. 対象キーを含まないファイルは無視すること. + */ + public function testOverriddenWhenKeyRedefinedInCascadeFile(): void + { + $this->fs->dumpFile($this->projectDir.'/.env', "ECCUBE_TEST_KEY_A=fromdotenv\nECCUBE_TEST_KEY_B=fromdotenv\n"); + // .env.local は KEY_A のみ再定義する + $this->fs->dumpFile($this->projectDir.'/.env.local', "ECCUBE_TEST_KEY_A=fromlocal\n"); + $service = new EnvFileService($this->projectDir); + + $this->withGlobals([ + 'server' => ['SYMFONY_DOTENV_VARS' => 'ECCUBE_TEST_KEY_A,ECCUBE_TEST_KEY_B'], + ], function () use ($service): void { + // KEY_A はカスケードで上書きされるため反映されない + $this->assertSame(['ECCUBE_TEST_KEY_A'], $service->getOverriddenKeys(['ECCUBE_TEST_KEY_A'])); + // KEY_B はカスケードに無く .env 由来のため反映される + $this->assertSame([], $service->getOverriddenKeys(['ECCUBE_TEST_KEY_B'])); + }); + } + + /** + * 複数キーのうち上書きされているキーだけを部分集合として返すこと(画面全体を止めないための土台). + */ + public function testGetOverriddenKeysReturnsOnlyOverriddenSubset(): void + { + $this->fs->dumpFile($this->projectDir.'/.env', "ECCUBE_TEST_KEY_A=fromdotenv\nECCUBE_TEST_KEY_B=fromdotenv\n"); + $service = new EnvFileService($this->projectDir); + + $this->withGlobals([ + // KEY_A/KEY_B は .env 由来。うち KEY_A だけ OS 環境変数が上書き(SYMFONY_DOTENV_VARS には未掲載) + 'server' => ['SYMFONY_DOTENV_VARS' => 'ECCUBE_TEST_KEY_B', 'ECCUBE_TEST_KEY_A' => 'osvalue'], + ], function () use ($service): void { + $this->assertSame( + ['ECCUBE_TEST_KEY_A'], + $service->getOverriddenKeys(['ECCUBE_TEST_KEY_A', 'ECCUBE_TEST_KEY_B']) + ); + }); + } + public function testGetOverriddenKeysReturnsEmptyForNoKeys(): void + { + $this->fs->dumpFile($this->projectDir.'/.env', "FOO=bar\n"); $service = new EnvFileService($this->projectDir); - $key = 'ECCUBE_TEST_OVERRIDE_KEY'; - $original = getenv($key); - putenv($key.'=1'); + $this->assertSame([], $service->getOverriddenKeys([])); + } + + /** + * $_SERVER / $_ENV の指定キーを一時的に差し替え, コールバック実行後に元へ復元する. + * + * @param array{server?: array, env?: array} $overrides + */ + private function withGlobals(array $overrides, callable $fn): void + { + $sentinel = '__ECCUBE_TEST_UNSET__'; + $backup = ['server' => [], 'env' => []]; + + foreach (($overrides['server'] ?? []) as $key => $value) { + $backup['server'][$key] = \array_key_exists($key, $_SERVER) ? $_SERVER[$key] : $sentinel; + $_SERVER[$key] = $value; + } + foreach (($overrides['env'] ?? []) as $key => $value) { + $backup['env'][$key] = \array_key_exists($key, $_ENV) ? $_ENV[$key] : $sentinel; + $_ENV[$key] = $value; + } + try { - $reasons = $service->getIneffectiveReasons([$key]); - $this->assertContains(EnvFileService::REASON_OVERRIDDEN, $reasons); - $this->assertFalse($service->isEffective([$key])); - // 対象キーに含めなければ反映可能と判定される - $this->assertTrue($service->isEffective(['UNRELATED_KEY'])); + $fn(); } finally { - // 実行環境が事前に設定していた値を復元する - false === $original ? putenv($key) : putenv($key.'='.$original); + foreach ($backup['server'] as $key => $value) { + if ($sentinel === $value) { + unset($_SERVER[$key]); + } else { + $_SERVER[$key] = $value; + } + } + foreach ($backup['env'] as $key => $value) { + if ($sentinel === $value) { + unset($_ENV[$key]); + } else { + $_ENV[$key] = $value; + } + } } } } diff --git a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php index 551a5de4add..55b36b5774d 100644 --- a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php @@ -55,7 +55,6 @@ public function testRouting() /** * Submit test */ - #[Group(name: 'cache-clear')] public function testSubmit() { $session = $this->createSession($this->client); @@ -81,67 +80,64 @@ public function testSubmit() } /** - * 環境変数が OS のプロセス環境変数として設定されている場合、画面表示時に - * 警告を表示し、登録ボタンを無効化することを確認する(#6130)。 + * 反映可能な環境(.env が書き込み可能)では登録ボタンが無効化されないことを確認する(対照)。 */ - #[Group(name: 'cache-clear')] - public function testDisplayWarningAndDisableButtonWhenEnvOverridden(): void + public function testDisplayButtonEnabledWhenEffective(): void { - $key = 'ECCUBE_ADMIN_ROUTE'; - $original = getenv($key); - putenv($key.'=admin'); - try { + $crawler = $this->client->request(Request::METHOD_GET, $this->generateUrl('admin_setting_system_security')); + $this->assertTrue($this->client->getResponse()->isSuccessful()); + $this->assertCount(0, $crawler->filter('button[type="submit"][disabled]')); + } + + /** + * 対象キーの 1 つ(ECCUBE_ADMIN_ROUTE)が OS 環境変数で上書きされていても、 + * 画面全体はブロックせず(登録ボタンは有効のまま)、該当キーを名指しで警告することを確認する(#6130 / #2)。 + */ + public function testDisplayWarnsNamedKeyButKeepsButtonEnabledWhenSingleKeyOverridden(): void + { + $this->forceKeyOverridden('ECCUBE_ADMIN_ROUTE', function (): void { $crawler = $this->client->request(Request::METHOD_GET, $this->generateUrl('admin_setting_system_security')); $this->assertTrue($this->client->getResponse()->isSuccessful()); - // 登録ボタンが無効化されている - $this->assertGreaterThan(0, $crawler->filter('button[type="submit"][disabled]')->count()); + // 残りのキーは .env に書けるため登録ボタンは無効化されない + $this->assertCount(0, $crawler->filter('button[type="submit"][disabled]')); - // 反映されない旨の警告が表示されている + // 上書きされているキーを名指しした警告が表示される $this->assertStringContainsString( - trans('admin.system.env.ineffective.overridden'), + trans('admin.system.env.ineffective.overridden', ['%keys%' => 'ECCUBE_ADMIN_ROUTE']), (string) $this->client->getResponse()->getContent() ); - } finally { - false === $original ? putenv($key) : putenv($key.'='.$original); - } + }); } /** - * 環境変数が OS のプロセス環境変数として設定されている場合、.env への書き込みは - * 反映されないため保存を拒否しエラーを表示することを確認する(#6130)。 + * 対象キーの 1 つ(ECCUBE_ADMIN_ROUTE)が上書きされている場合、そのキーは .env に書かず、 + * 反映される他キー(TRUSTED_HOSTS 等)は保存されることを確認する(#6130 / #2 の退行解消)。 */ - #[Group(name: 'cache-clear')] - public function testSubmitRejectedWhenEnvOverridden(): void + public function testSubmitSkipsOverriddenKeyButSavesOthers(): void { $session = $this->createSession($this->client); $formData = $this->createFormData(); - // この画面が書き込む環境変数の1つをプロセス環境変数として設定 - $key = 'ECCUBE_ADMIN_ROUTE'; - $original = getenv($key); - putenv($key.'=admin'); - try { + $this->forceKeyOverridden('ECCUBE_ADMIN_ROUTE', function () use ($session, $formData): void { $this->client->request( Request::METHOD_POST, $this->generateUrl('admin_setting_system_security'), - [ - 'admin_security' => $formData, - ] + ['admin_security' => $formData] ); $this->assertTrue($this->client->getResponse()->isRedirection()); - // 保存が拒否されエラーが表示される + // 保存自体は拒否されない(save_error は出ない) $errors = $session->getFlashBag()->get('eccube.admin.error'); - $this->assertContains('admin.common.save_error', $errors); - - // .env は書き換えられていない - $this->assertDoesNotMatchRegularExpression('/ECCUBE_ADMIN_ROUTE='.$formData['admin_route_dir'].'/', file_get_contents($this->envFile)); - } finally { - // 実行環境が事前に設定していた値を復元する - false === $original ? putenv($key) : putenv($key.'='.$original); - } + $this->assertNotContains('admin.common.save_error', $errors); + + $content = file_get_contents($this->envFile); + // 上書きされている ECCUBE_ADMIN_ROUTE は書き換わらない + $this->assertDoesNotMatchRegularExpression('/ECCUBE_ADMIN_ROUTE='.$formData['admin_route_dir'].'/', $content); + // 反映される TRUSTED_HOSTS は保存される + $this->assertStringContainsString('TRUSTED_HOSTS='.$formData['trusted_hosts'], (string) $content); + }); } /** @@ -171,6 +167,43 @@ public function testSubmitEmpty() $this->assertSame($this->env, $newEnv); } + /** + * 指定キーが「.env 由来でなく OS のプロセス環境変数で上書きされている」状態を + * 決定的に作り出してコールバックを実行し、終了後に $_SERVER を元へ復元する. + * + * テスト環境では bootEnv により .env のキーが SYMFONY_DOTENV_VARS に載るため + * putenv では上書き状態を再現できない(EnvFileService は SYMFONY_DOTENV_VARS を優先する)。 + * そこで当該キーを SYMFONY_DOTENV_VARS から除外し、$_SERVER にプロセス環境変数値を注入する. + */ + private function forceKeyOverridden(string $key, callable $fn): void + { + $sentinel = '__ECCUBE_TEST_UNSET__'; + $origVars = \array_key_exists('SYMFONY_DOTENV_VARS', $_SERVER) ? $_SERVER['SYMFONY_DOTENV_VARS'] : $sentinel; + $origVal = \array_key_exists($key, $_SERVER) ? $_SERVER[$key] : $sentinel; + + $vars = array_filter( + explode(',', (string) ($sentinel === $origVars ? '' : $origVars)), + fn ($k) => $k !== $key && '' !== $k + ); + $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', $vars); + $_SERVER[$key] = 'osvalue'; + + try { + $fn(); + } finally { + if ($sentinel === $origVars) { + unset($_SERVER['SYMFONY_DOTENV_VARS']); + } else { + $_SERVER['SYMFONY_DOTENV_VARS'] = $origVars; + } + if ($sentinel === $origVal) { + unset($_SERVER[$key]); + } else { + $_SERVER[$key] = $origVal; + } + } + } + /** * Submit form */ diff --git a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php index 5d9a74cc7b0..26f87c00e28 100644 --- a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php @@ -96,24 +96,19 @@ public function testDisplayList() #[Group(name: 'cache-clear')] public function testDisplayWarningAndDisableButtonWhenEnvOverridden() { - $key = 'ECCUBE_TEMPLATE_CODE'; - $original = getenv($key); - putenv($key.'=default'); - try { + $this->forceKeyOverridden('ECCUBE_TEMPLATE_CODE', function (): void { $crawler = $this->client->request(Request::METHOD_GET, $this->generateUrl('admin_store_template')); $this->assertTrue($this->client->getResponse()->isSuccessful()); - // 登録ボタンが無効化されている + // この画面が扱うキーは1つのみのため, 上書き時は登録ボタンが無効化されている $this->assertGreaterThan(0, $crawler->filter('button[type="submit"][disabled]')->count()); - // 反映されない旨の警告が表示されている + // 上書きされているキーを名指しした警告が表示されている $this->assertStringContainsString( - trans('admin.system.env.ineffective.overridden'), + trans('admin.system.env.ineffective.overridden', ['%keys%' => 'ECCUBE_TEMPLATE_CODE']), (string) $this->client->getResponse()->getContent() ); - } finally { - false === $original ? putenv($key) : putenv($key.'='.$original); - } + }); } /** @@ -156,13 +151,9 @@ public function testChangeTemplateWithEnvOverride() $this->verifyUpload(); $Template = $this->templateRepository->findOneBy(['code' => $this->code]); + $this->assertInstanceOf(Template::class, $Template); - // プロセス環境変数として ECCUBE_TEMPLATE_CODE を設定 - $key = 'ECCUBE_TEMPLATE_CODE'; - $original = getenv($key); - putenv($key.'=default'); - - try { + $this->forceKeyOverridden('ECCUBE_TEMPLATE_CODE', function () use ($Template): void { $session = $this->createSession($this->client); // テンプレートを選択 @@ -177,13 +168,15 @@ public function testChangeTemplateWithEnvOverride() // 環境変数オーバーライド時は保存が拒否され、エラーが表示される $errors = $session->getFlashBag()->get('eccube.admin.error'); $this->assertContains('admin.common.save_error', $errors); + // 原因となる上書きキー名も併記される(#8) + $this->assertContains( + trans('admin.system.env.ineffective.overridden', ['%keys%' => 'ECCUBE_TEMPLATE_CODE']), + $errors + ); // .env は書き換えられていない $this->assertDoesNotMatchRegularExpression('/ECCUBE_TEMPLATE_CODE='.$Template->getCode().'/', file_get_contents($this->envFile)); - } finally { - // 実行環境が事前に設定していた値を復元する - false === $original ? putenv($key) : putenv($key.'='.$original); - } + }); } /** @@ -299,6 +292,43 @@ public function testDelete() $this->assertFileDoesNotExist(static::getContainer()->getParameter('kernel.project_dir').'/app/template/'.$code); } + /** + * 指定キーが「.env 由来でなく OS のプロセス環境変数で上書きされている」状態を + * 決定的に作り出してコールバックを実行し、終了後に $_SERVER を元へ復元する. + * + * テスト環境では bootEnv により .env のキーが SYMFONY_DOTENV_VARS に載るため + * putenv では上書き状態を再現できない(EnvFileService は SYMFONY_DOTENV_VARS を優先する)。 + * そこで当該キーを SYMFONY_DOTENV_VARS から除外し、$_SERVER にプロセス環境変数値を注入する. + */ + private function forceKeyOverridden(string $key, callable $fn): void + { + $sentinel = '__ECCUBE_TEST_UNSET__'; + $origVars = \array_key_exists('SYMFONY_DOTENV_VARS', $_SERVER) ? $_SERVER['SYMFONY_DOTENV_VARS'] : $sentinel; + $origVal = \array_key_exists($key, $_SERVER) ? $_SERVER[$key] : $sentinel; + + $vars = array_filter( + explode(',', (string) ($sentinel === $origVars ? '' : $origVars)), + fn ($k) => $k !== $key && '' !== $k + ); + $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', $vars); + $_SERVER[$key] = 'osvalue'; + + try { + $fn(); + } finally { + if ($sentinel === $origVars) { + unset($_SERVER['SYMFONY_DOTENV_VARS']); + } else { + $_SERVER['SYMFONY_DOTENV_VARS'] = $origVars; + } + if ($sentinel === $origVal) { + unset($_SERVER[$key]); + } else { + $_SERVER[$key] = $origVal; + } + } + } + protected function scenarioUpload($uppercase = false) { $formData = $this->createFormData(); From c96c44897a6bab60527119e76de7ca4404cef997 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Wed, 29 Jul 2026 10:08:40 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix(test):=20=E7=92=B0=E5=A2=83=E5=A4=89?= =?UTF-8?q?=E6=95=B0=E4=B8=8A=E6=9B=B8=E3=81=8D=E3=81=AE=E5=86=8D=E7=8F=BE?= =?UTF-8?q?=E3=81=A7=E5=AE=9F=E5=8A=B9=E5=80=A4=E3=82=92=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E3=81=97=20.env=20=E9=9D=9E=E5=AE=9A=E7=BE=A9=E7=92=B0?= =?UTF-8?q?=E5=A2=83=E3=81=AE=20500=20=E3=82=92=E8=A7=A3=E6=B6=88=E3=81=99?= =?UTF-8?q?=E3=82=8B=20(#6130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit forceKeyOverridden が $_SERVER にダミー値 'osvalue' を注入していたため, .env に対象キーが無い環境(CI)で注入値がそのまま実効値になり, twig.paths が存在しない app/template/osvalue を指して FilesystemLoader::addPath が LoaderError を投げ, 全リクエストが 500 になっていた(PHPUnit 全 16 マトリクス fail). ローカルは .env に ECCUBE_TEMPLATE_CODE があり $_ENV が勝つため再現しなかった. - 2 ファイルに重複していたヘルパを EnvOverrideTrait へ集約し, 注入値を引数化 - TemplateControllerTest は eccube.theme, SecurityControllerTest は eccube_admin_route の実効値を注入し, 環境差なく上書き状態のみを再現する - 実効値でも SYMFONY_DOTENV_VARS 非掲載+$_SERVER 有りの前提は変わらないため 検出ロジックの検証意図は維持される Co-Authored-By: Claude Opus 5 (1M context) --- tests/Eccube/Tests/EnvOverrideTrait.php | 76 +++++++++++++++++++ .../Setting/System/SecurityControllerTest.php | 44 +++-------- .../Admin/Store/TemplateControllerTest.php | 44 +++-------- 3 files changed, 98 insertions(+), 66 deletions(-) create mode 100644 tests/Eccube/Tests/EnvOverrideTrait.php diff --git a/tests/Eccube/Tests/EnvOverrideTrait.php b/tests/Eccube/Tests/EnvOverrideTrait.php new file mode 100644 index 00000000000..67c248713ac --- /dev/null +++ b/tests/Eccube/Tests/EnvOverrideTrait.php @@ -0,0 +1,76 @@ + を指し, Twig の FilesystemLoader::addPath が + * LoaderError を投げて全リクエストが 500 になる(実効値なら環境差なく成立する). + * + * @param string $key 対象の環境変数キー + * @param string $value 注入する値. アプリケーションの実効値と同じ値を渡す + * @param callable $fn 上書き状態で実行する処理 + */ + protected function forceKeyOverridden(string $key, string $value, callable $fn): void + { + $sentinel = '__ECCUBE_TEST_UNSET__'; + $origVars = \array_key_exists('SYMFONY_DOTENV_VARS', $_SERVER) ? $_SERVER['SYMFONY_DOTENV_VARS'] : $sentinel; + $origVal = \array_key_exists($key, $_SERVER) ? $_SERVER[$key] : $sentinel; + + $vars = array_filter( + explode(',', (string) ($sentinel === $origVars ? '' : $origVars)), + fn ($k) => $k !== $key && '' !== $k + ); + $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', $vars); + $_SERVER[$key] = $value; + + try { + $fn(); + } finally { + if ($sentinel === $origVars) { + unset($_SERVER['SYMFONY_DOTENV_VARS']); + } else { + $_SERVER['SYMFONY_DOTENV_VARS'] = $origVars; + } + if ($sentinel === $origVal) { + unset($_SERVER[$key]); + } else { + $_SERVER[$key] = $origVal; + } + } + } +} diff --git a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php index 55b36b5774d..be39e71814d 100644 --- a/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Setting/System/SecurityControllerTest.php @@ -15,6 +15,7 @@ namespace Eccube\Tests\Web\Admin\Setting\System; +use Eccube\Tests\EnvOverrideTrait; use Eccube\Tests\Web\Admin\AbstractAdminWebTestCase; use PHPUnit\Framework\Attributes\Group; use Symfony\Component\HttpFoundation\Request; @@ -22,6 +23,8 @@ #[Group('cache-clear')] final class SecurityControllerTest extends AbstractAdminWebTestCase { + use EnvOverrideTrait; + protected $envFile; protected $env; @@ -95,7 +98,7 @@ public function testDisplayButtonEnabledWhenEffective(): void */ public function testDisplayWarnsNamedKeyButKeepsButtonEnabledWhenSingleKeyOverridden(): void { - $this->forceKeyOverridden('ECCUBE_ADMIN_ROUTE', function (): void { + $this->forceKeyOverridden('ECCUBE_ADMIN_ROUTE', $this->currentAdminRoute(), function (): void { $crawler = $this->client->request(Request::METHOD_GET, $this->generateUrl('admin_setting_system_security')); $this->assertTrue($this->client->getResponse()->isSuccessful()); @@ -119,7 +122,7 @@ public function testSubmitSkipsOverriddenKeyButSavesOthers(): void $session = $this->createSession($this->client); $formData = $this->createFormData(); - $this->forceKeyOverridden('ECCUBE_ADMIN_ROUTE', function () use ($session, $formData): void { + $this->forceKeyOverridden('ECCUBE_ADMIN_ROUTE', $this->currentAdminRoute(), function () use ($session, $formData): void { $this->client->request( Request::METHOD_POST, $this->generateUrl('admin_setting_system_security'), @@ -168,40 +171,15 @@ public function testSubmitEmpty() } /** - * 指定キーが「.env 由来でなく OS のプロセス環境変数で上書きされている」状態を - * 決定的に作り出してコールバックを実行し、終了後に $_SERVER を元へ復元する. + * 現在アプリケーションが使用している管理画面のルーティングプレフィックス. * - * テスト環境では bootEnv により .env のキーが SYMFONY_DOTENV_VARS に載るため - * putenv では上書き状態を再現できない(EnvFileService は SYMFONY_DOTENV_VARS を優先する)。 - * そこで当該キーを SYMFONY_DOTENV_VARS から除外し、$_SERVER にプロセス環境変数値を注入する. + * ECCUBE_ADMIN_ROUTE の上書きを再現する際、実効値と異なる値を注入すると + * 管理画面の URL 自体が変わってしまうため、実効値をそのまま注入する + * ({@see EnvOverrideTrait::forceKeyOverridden()}). */ - private function forceKeyOverridden(string $key, callable $fn): void + private function currentAdminRoute(): string { - $sentinel = '__ECCUBE_TEST_UNSET__'; - $origVars = \array_key_exists('SYMFONY_DOTENV_VARS', $_SERVER) ? $_SERVER['SYMFONY_DOTENV_VARS'] : $sentinel; - $origVal = \array_key_exists($key, $_SERVER) ? $_SERVER[$key] : $sentinel; - - $vars = array_filter( - explode(',', (string) ($sentinel === $origVars ? '' : $origVars)), - fn ($k) => $k !== $key && '' !== $k - ); - $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', $vars); - $_SERVER[$key] = 'osvalue'; - - try { - $fn(); - } finally { - if ($sentinel === $origVars) { - unset($_SERVER['SYMFONY_DOTENV_VARS']); - } else { - $_SERVER['SYMFONY_DOTENV_VARS'] = $origVars; - } - if ($sentinel === $origVal) { - unset($_SERVER[$key]); - } else { - $_SERVER[$key] = $origVal; - } - } + return (string) static::getContainer()->getParameter('eccube_admin_route'); } /** diff --git a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php index 26f87c00e28..dfb00fb45d1 100644 --- a/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php +++ b/tests/Eccube/Tests/Web/Admin/Store/TemplateControllerTest.php @@ -19,6 +19,7 @@ use Eccube\Entity\Template; use Eccube\Repository\Master\DeviceTypeRepository; use Eccube\Repository\TemplateRepository; +use Eccube\Tests\EnvOverrideTrait; use Eccube\Tests\Web\Admin\AbstractAdminWebTestCase; use Eccube\Util\StringUtil; use PHPUnit\Framework\Attributes\Group; @@ -29,6 +30,8 @@ final class TemplateControllerTest extends AbstractAdminWebTestCase { + use EnvOverrideTrait; + protected ?string $dir = null; protected ?UploadedFile $file = null; @@ -96,7 +99,7 @@ public function testDisplayList() #[Group(name: 'cache-clear')] public function testDisplayWarningAndDisableButtonWhenEnvOverridden() { - $this->forceKeyOverridden('ECCUBE_TEMPLATE_CODE', function (): void { + $this->forceKeyOverridden('ECCUBE_TEMPLATE_CODE', $this->currentThemeCode(), function (): void { $crawler = $this->client->request(Request::METHOD_GET, $this->generateUrl('admin_store_template')); $this->assertTrue($this->client->getResponse()->isSuccessful()); @@ -153,7 +156,7 @@ public function testChangeTemplateWithEnvOverride() $Template = $this->templateRepository->findOneBy(['code' => $this->code]); $this->assertInstanceOf(Template::class, $Template); - $this->forceKeyOverridden('ECCUBE_TEMPLATE_CODE', function () use ($Template): void { + $this->forceKeyOverridden('ECCUBE_TEMPLATE_CODE', $this->currentThemeCode(), function () use ($Template): void { $session = $this->createSession($this->client); // テンプレートを選択 @@ -293,40 +296,15 @@ public function testDelete() } /** - * 指定キーが「.env 由来でなく OS のプロセス環境変数で上書きされている」状態を - * 決定的に作り出してコールバックを実行し、終了後に $_SERVER を元へ復元する. + * 現在アプリケーションが使用しているテンプレートコード. * - * テスト環境では bootEnv により .env のキーが SYMFONY_DOTENV_VARS に載るため - * putenv では上書き状態を再現できない(EnvFileService は SYMFONY_DOTENV_VARS を優先する)。 - * そこで当該キーを SYMFONY_DOTENV_VARS から除外し、$_SERVER にプロセス環境変数値を注入する. + * ECCUBE_TEMPLATE_CODE の上書きを再現する際、実効値と異なる値を注入すると + * twig.paths が存在しない app/template/ を指し全リクエストが 500 になるため、 + * 実効値をそのまま注入する({@see EnvOverrideTrait::forceKeyOverridden()}). */ - private function forceKeyOverridden(string $key, callable $fn): void + private function currentThemeCode(): string { - $sentinel = '__ECCUBE_TEST_UNSET__'; - $origVars = \array_key_exists('SYMFONY_DOTENV_VARS', $_SERVER) ? $_SERVER['SYMFONY_DOTENV_VARS'] : $sentinel; - $origVal = \array_key_exists($key, $_SERVER) ? $_SERVER[$key] : $sentinel; - - $vars = array_filter( - explode(',', (string) ($sentinel === $origVars ? '' : $origVars)), - fn ($k) => $k !== $key && '' !== $k - ); - $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', $vars); - $_SERVER[$key] = 'osvalue'; - - try { - $fn(); - } finally { - if ($sentinel === $origVars) { - unset($_SERVER['SYMFONY_DOTENV_VARS']); - } else { - $_SERVER['SYMFONY_DOTENV_VARS'] = $origVars; - } - if ($sentinel === $origVal) { - unset($_SERVER[$key]); - } else { - $_SERVER[$key] = $origVal; - } - } + return (string) static::getContainer()->getParameter('eccube.theme'); } protected function scenarioUpload($uppercase = false)