Skip to content
Open
4 changes: 4 additions & 0 deletions app/config/eccube/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ services:
lazy: true
public: true

Eccube\Service\EnvFileService:
arguments:
$projectDir: '%kernel.project_dir%'

Eccube\Service\Composer\ComposerProcessService:
lazy: true

Expand Down
65 changes: 53 additions & 12 deletions src/Eccube/Controller/Admin/Setting/System/SecurityController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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',
];
Comment on lines +32 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[高] 7 キーの OR 判定なので、1 キーが OS 環境変数として設定されているだけで、反映されるはずの残り 6 キーまで保存できなくなります。

EnvFileService::getIneffectiveReasons() は対象キーのどれか 1 つでも該当すれば REASON_OVERRIDDEN を返して break します(EnvFileService.php:69-74)。その結果、isEffective(self::ENV_KEYS)SecurityController.php:65)はセキュリティ設定画面全体をブロックします。

ECCUBE_ADMIN_ROUTE だけを OS 環境変数で設定する構成は現実的で、docker-compose.yml:50-59 も各キーを個別にコメントアウトして例示しています(ECCUBE_ADMIN_ROUTE は 54 行目)。この構成では:

  • ECCUBE_ADMIN_ROUTE の変更 → 確かに反映されない(ブロックは妥当)
  • TRUSTED_HOSTS / ECCUBE_FORCE_SSL / IP 制限 4 キー → .env に書けば反映される

にもかかわらず、後者も含めて登録ボタンが無効化され、POST も拒否されます。index.php:41-46 のコメント(「OS 環境変数を保護しつつ、.env 側の非 OS 変数は反映される」)が示すとおり、Docker 環境でも .env 側のキーは生きているので、これは今まで管理画面から変更できていた設定が変更できなくなる退行になります。PR 本文の「既存機能の仕様変更はありません」とも齟齬があると思います。

getIneffectiveReasons() が「どのキーが上書きされているか」まで返すようにして、

  • 上書きされているキーを名指しで警告する
  • 該当キーに対応するフォーム項目だけ無効化する(あるいは書き込み時に該当キーだけスキップする)
  • 画面全体のブロックは not_found / not_writable / local_php(=キーによらず全滅するケース)に限定する

という切り分けはいかがでしょうか。少なくとも「1 キーの上書きで画面全体を止める」のは過剰だと思います。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
過剰ブロックの退行、ご指摘のとおりでした。切り分けを入れました。

  • EnvFileService::getOverriddenKeys(array $keys) を新設し、上書きされているキーの部分集合を返すようにしました。
  • getIneffectiveReasons() はファイル単位で全滅する理由(not_found / not_writable / local_php)だけに限定。
  • セキュリティ設定は、上書きされているキーだけを書き込み対象から除外し、そのキーを名指しで警告します。反映できる残りのキーは保存でき、登録ボタンも有効のままです。
  • テンプレート設定は対象キーが ECCUBE_TEMPLATE_CODE の 1 つだけなので、それが上書きされていれば実質全滅として従来どおり保存拒否+無効化にしています。

「1 キーの上書きで画面全体を止める」挙動は解消しました。


/**
* 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,
) {
}

/**
Expand All @@ -44,13 +61,20 @@ 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 ファイル自体が読み込まれない状況(全キー反映不可)では保存を拒否する
$ineffectiveReasons = $this->envFileService->getIneffectiveReasons();
if ([] !== $ineffectiveReasons) {
$this->addError('admin.common.save_error', 'admin');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[低] POST 拒否時のメッセージが汎用の admin.common.save_error だけなので、理由が分かりません。

GET 時は admin.system.env.ineffective.<reason> で理由を出しているので、POST 拒否時も同じ理由メッセージを併せて出すと、ボタン無効化を回避して直接 POST が飛んできたケース(あるいは表示後に環境が変わったケース)でも原因が伝わります。

$reasons = $this->envFileService->getIneffectiveReasons(self::ENV_KEYS);
if ([] !== $reasons) {
    $this->addError('admin.common.save_error', 'admin');
    foreach ($reasons as $reason) {
        $this->addError('admin.system.env.ineffective.'.$reason, 'admin');
    }

    return $this->redirectToRoute('admin_setting_system_security');
}

TemplateController.php:72-76 も同様です。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
ご提案どおり、POST 拒否時に admin.common.save_error に加えて理由メッセージ(admin.system.env.ineffective.<reason> と、上書きキーがある場合は %keys% 入りの文言)を併記するようにしました。TemplateController 側も同様です。ボタン無効化を回避した直接 POST でも原因が伝わります。

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);
Expand All @@ -69,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);
}

// 上書きされ反映されなかったキーは名指しで警告する
if ([] !== $overriddenKeys) {
$this->addWarning(trans('admin.system.env.ineffective.overridden', ['%keys%' => implode(', ', $overriddenKeys)]), 'admin');
}

// 管理画面URLの更新. 変更されている場合はログアウトし再ログインさせる.
// 管理画面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'],
]);
Expand Down Expand Up @@ -115,13 +148,21 @@ 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();
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,
];
}
}
57 changes: 42 additions & 15 deletions src/Eccube/Controller/Admin/Store/TemplateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
{
}

Expand All @@ -59,10 +65,28 @@ 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 を上書きしている
$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');
}

$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(),
Expand All @@ -72,33 +96,36 @@ 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();
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,
// この画面が扱うキーは 1 つのみのため, 上書きされていれば実質全滅として無効化する。
'envWritable' => [] === $ineffectiveReasons && [] === $overriddenKeys,
];
}

/**
* テンプレート一覧からのダウンロード
*/
#[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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中] download() / delete() からの Request $request 引数削除は、本 PR の目的と無関係な public シグネチャ変更です。

download()(113 行目)と delete()(170 行目)から Request $request が削除されていますが、.env の反映可否検出とは無関係な変更です。app/Customize やプラグインでこのコントローラを継承してメソッドをオーバーライドしている場合、シグネチャ不一致で fatal になります。

PR 本文の互換性チェックリストは Service クラスについてしか言及していませんが、コントローラの public メソッドも同じく拡張点なので、

  • 別 PR に分ける(未使用引数の整理としてまとめて実施する)
  • あるいは本 PR に含めるなら、PR 本文に破壊的変更として明記する

のどちらかが良いと思いますが、いかがでしょうか。4.4 はメジャー更新なので削除自体が不可能とは思いませんが、レビュー単位としては分けたほうが追いやすいと感じました。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nanasess
ご指摘ありがとうございます。本 PR の目的と無関係な public シグネチャ変更、という点はそのとおりです。ひとつ判断を仰ぎたい点があります。

この引数削除は、実は CI の Rector ジョブが要求しています。SymfonySetList::SYMFONY_CODE_QUALITY に含まれる RemoveUnusedRequestParamRector が、download() / delete() の未使用 Request $request を検出し削除を求めます(本 PR で同ファイルを触ったことで Rector のファイル単位キャッシュが無効化され、既存の未使用引数が再解析されて表面化しました)。そのため引数を元に戻すと Rector ジョブが赤になります。

対応として 2 案あります。どちらが望ましいでしょうか。

  • (A) 引数を残す(BC 維持)+ rector.phpwithSkip()RemoveUnusedRequestParamRector × TemplateController を理由コメント付きで追加し、CI を通す。
  • (B) Rector に従い削除したままとし、PR 本文に「public シグネチャの互換性に関わる変更」として明記する。

個人的には、拡張点である public メソッドの互換を優先するなら (A) が筋だと思いますが、プロジェクトの Rector 運用方針に合わせたく、ご意見をいただけますか。合意後に反映します。

{
// 該当テンプレートのディレクトリ
$templateCode = $Template->getCode();
Expand Down Expand Up @@ -155,7 +182,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();

Expand Down
6 changes: 4 additions & 2 deletions src/Eccube/Resource/locale/messages.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1428,7 +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(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: |
Expand Down Expand Up @@ -1515,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
Expand Down
6 changes: 4 additions & 2 deletions src/Eccube/Resource/locale/messages.ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1428,7 +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: 'この設定の環境変数(%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: |
Expand Down Expand Up @@ -1515,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: プラグイン一覧
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ file that was distributed with this source code.
<div class="col-6">
<div id="ex-conversion-action" class="row align-items-center justify-content-end">
<div class="col-auto">
<button type="submit" class="btn btn-ec-conversion px-5">{{ 'admin.common.registration'|trans }}</button>
<button type="submit" class="btn btn-ec-conversion px-5"{% if envWritable is defined and not envWritable %} disabled{% endif %}>{{ 'admin.common.registration'|trans }}</button>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/Eccube/Resource/template/admin/Store/template.twig
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ file that was distributed with this source code.
<div class="col-6">
<div id="ex-conversion-action" class="row align-items-center justify-content-end">
<div class="col-auto">
<button class="btn btn-ec-conversion px-5" type="submit">{{ 'admin.common.registration'|trans }}</button>
<button class="btn btn-ec-conversion px-5" type="submit"{% if envWritable is defined and not envWritable %} disabled{% endif %}>{{ 'admin.common.registration'|trans }}</button>
</div>
</div>
</div>
Expand Down
Loading
Loading