From 545cb0fed159b204d5b333c194faa4a3bce37323 Mon Sep 17 00:00:00 2001 From: masaton0216 Date: Tue, 18 Aug 2026 16:50:56 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E3=83=86=E3=83=BC=E3=83=9E=E7=AE=A1?= =?UTF-8?q?=E7=90=86=20=E3=83=86=E3=83=BC=E3=83=9E=E5=90=8D=E3=81=AB?= =?UTF-8?q?=E5=8D=8A=E8=A7=92=E3=82=AB=E3=83=83=E3=82=B3=E3=81=8C=E3=81=82?= =?UTF-8?q?=E3=82=8B=E3=81=A8=E3=83=9A=E3=83=BC=E3=82=B8=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E3=81=AE=E5=88=9D=E6=9C=9F=E8=A1=A8=E7=A4=BA=E3=81=8C=E3=82=B7?= =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=A0=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AB?= =?UTF-8?q?=E3=81=AA=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Plugins/Manage/SiteManage/SiteManage.php | 2 +- .../Manage/ThemeManage/ThemeManage.php | 36 +++- app/Plugins/PluginBase.php | 12 +- app/Utilities/File/FileUtils.php | 52 ++++++ .../Unit/Plugins/PluginBaseGetThemesTest.php | 135 +++++++++++++++ .../File/FileUtilsParseIniFileTest.php | 163 ++++++++++++++++++ tests/Unit/Utilities/File/FileUtilsTest.php | 36 ++++ 7 files changed, 421 insertions(+), 15 deletions(-) create mode 100644 tests/Unit/Plugins/PluginBaseGetThemesTest.php create mode 100644 tests/Unit/Utilities/File/FileUtilsParseIniFileTest.php diff --git a/app/Plugins/Manage/SiteManage/SiteManage.php b/app/Plugins/Manage/SiteManage/SiteManage.php index 8302a0d39..ad74d01d8 100644 --- a/app/Plugins/Manage/SiteManage/SiteManage.php +++ b/app/Plugins/Manage/SiteManage/SiteManage.php @@ -1660,7 +1660,7 @@ public function downloadDocument($request) $dirs = array(); foreach ($tmp_dirs as $tmp_dir) { // テーマ設定ファイル取得 - $theme_inis = parse_ini_file(public_path() . '/themes/Users/' . basename($tmp_dir) . '/themes.ini'); + $theme_inis = FileUtils::parseIniFile(public_path() . '/themes/Users/' . basename($tmp_dir) . '/themes.ini'); $theme_name = ''; if (!empty($theme_inis) && array_key_exists('theme_name', $theme_inis)) { $theme_name = $theme_inis['theme_name']; diff --git a/app/Plugins/Manage/ThemeManage/ThemeManage.php b/app/Plugins/Manage/ThemeManage/ThemeManage.php index 27b76f27a..fb97d0287 100644 --- a/app/Plugins/Manage/ThemeManage/ThemeManage.php +++ b/app/Plugins/Manage/ThemeManage/ThemeManage.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Validator; use App\Plugins\Manage\ManagePluginBase; +use App\Utilities\File\FileUtils; /** * テーマ管理クラス @@ -257,7 +258,7 @@ public function index($request, $page_id = null, $errors = array()) $dirs = array(); foreach ($tmp_dirs as $tmp_dir) { // テーマ設定ファイル取得 - $theme_inis = parse_ini_file(public_path() . '/themes/Users/' . basename($tmp_dir) . '/themes.ini'); + $theme_inis = FileUtils::parseIniFile(public_path() . '/themes/Users/' . basename($tmp_dir) . '/themes.ini'); $theme_name = ''; if (!empty($theme_inis) && array_key_exists('theme_name', $theme_inis)) { $theme_name = $theme_inis['theme_name']; @@ -291,12 +292,14 @@ public function create($request, $id) $request->flash(); $messages['dir_name.regex'] = '入力された:attributeは使用できません。半角の英数字、アンダースコア(_)、ハイフン(-)のみを使い、先頭がハイフンにならないように入力してください。'; + $messages['theme_name.not_regex'] = '入力された:attributeは使用できません。ダブルクォート(")と円記号(\)は使用できません。'; // 項目のエラーチェック $validator = Validator::make($request->all(), [ /* regex:英数字_- OK */ 'dir_name' => ['required', 'regex:/^\w[\w-]*$/'], - 'theme_name' => ['required'], + /* not_regex:themes.ini に書き出せない文字("、\)は不可 */ + 'theme_name' => ['required', 'not_regex:/["\\\\]/'], ], $messages); $validator->setAttributeNames([ 'dir_name' => 'ディレクトリ名', @@ -318,7 +321,7 @@ public function create($request, $id) $result = File::makeDirectory(public_path() . '/themes/Users/' . basename($request->dir_name), 0775); // themes.ini ファイルの作成 - $themes_ini = '[base]' . "\n" . 'theme_name = ' . $request->theme_name; + $themes_ini = $this->makeThemesIni($request->theme_name); $result = File::put(public_path() . '/themes/Users/' . basename($request->dir_name) . '/themes.ini', $themes_ini); // themes.css ファイルの作成 @@ -528,6 +531,16 @@ public function saveJs($request, $id) ]); } + /** + * themes.ini ファイルの内容の生成 + * + * テーマ名に ini の予約文字(半角カッコ等)が含まれていても読み込めるよう、値はダブルクォートで囲む。 + */ + private function makeThemesIni($theme_name): string + { + return '[base]' . "\n" . 'theme_name = ' . FileUtils::escapeIniValue((string)$theme_name) . "\n"; + } + /** * ユーザ・テーマ名の取得 */ @@ -538,7 +551,7 @@ private function getUserThemeName($dir_name) return ''; } - $theme_inis = parse_ini_file($theme_ini_path); + $theme_inis = FileUtils::parseIniFile($theme_ini_path); if (empty($theme_inis) || !array_key_exists('theme_name', $theme_inis)) { return ''; } @@ -588,10 +601,13 @@ public function saveName($request, $id) // セッション初期化などのLaravel 処理 $request->flash(); + $messages['theme_name.not_regex'] = '入力された:attributeは使用できません。ダブルクォート(")と円記号(\)は使用できません。'; + // 項目のエラーチェック $validator = Validator::make($request->all(), [ - 'theme_name' => ['required'], - ]); + /* not_regex:themes.ini に書き出せない文字("、\)は不可 */ + 'theme_name' => ['required', 'not_regex:/["\\\\]/'], + ], $messages); $validator->setAttributeNames([ 'theme_name' => 'テーマ名', ]); @@ -608,7 +624,7 @@ public function saveName($request, $id) $theme_name = $request->theme_name; // themes.ini ファイルの保存 - $themes_ini = '[base]' . "\n" . 'theme_name = ' . $theme_name; + $themes_ini = $this->makeThemesIni($theme_name); $result = File::put(public_path() . '/themes/Users/' . $dir_name . '/themes.ini', $themes_ini); return view('plugins.manage.theme.theme_name_edit', [ @@ -897,12 +913,14 @@ public function generate($request, $id) $request->flash(); $messages['dir_name.regex'] = '入力された:attributeは使用できません。半角の英数字、アンダースコア(_)、ハイフン(-)のみを使い、先頭がハイフンにならないように入力してください。'; + $messages['theme_name.not_regex'] = '入力された:attributeは使用できません。ダブルクォート(")と円記号(\)は使用できません。'; // 項目のエラーチェック $validator = Validator::make($request->all(), [ // regex:英数字_- OK 'dir_name' => ['required', 'regex:/^\w[\w-]*$/'], - 'theme_name' => ['required'], + /* not_regex:themes.ini に書き出せない文字("、\)は不可 */ + 'theme_name' => ['required', 'not_regex:/["\\\\]/'], ], $messages); $validator->setAttributeNames([ 'dir_name' => 'ディレクトリ名', @@ -931,7 +949,7 @@ public function generate($request, $id) $result = File::makeDirectory(public_path() . '/themes/Users/' . basename($request->dir_name), 0775); // themes.ini ファイルの作成 - $themes_ini = '[base]' . "\n" . 'theme_name = ' . $request->theme_name; + $themes_ini = $this->makeThemesIni($request->theme_name); $result = File::put(public_path() . '/themes/Users/' . basename($request->dir_name) . '/themes.ini', $themes_ini); // themes.css ファイルの作成 diff --git a/app/Plugins/PluginBase.php b/app/Plugins/PluginBase.php index f80e4d33f..dd0cc2b92 100644 --- a/app/Plugins/PluginBase.php +++ b/app/Plugins/PluginBase.php @@ -10,6 +10,7 @@ use App\Models\Common\Numbers; use App\Traits\ConnectMailTrait; +use App\Utilities\File\FileUtils; /** * プラグイン基底クラス @@ -166,8 +167,8 @@ protected function getThemes() $themes = array(); // 画面に渡すテーマ配列 foreach ($dirs as $dir) { if (File::exists($dir."/themes.ini")) { - // テーマ設定ファイルのパース - $theme_inis = parse_ini_file($dir."/themes.ini"); + // テーマ設定ファイルのパース(壊れたファイルがあっても画面が落ちないよう、空配列が返る) + $theme_inis = FileUtils::parseIniFile($dir."/themes.ini"); // ディレクトリがテーマ・グループ用のものなら、その下のディレクトリを探す。 if (array_key_exists('theme_dir', $theme_inis) && $theme_inis['theme_dir'] == 'group') { @@ -178,8 +179,8 @@ protected function getThemes() asort($group_dirs); // ディレクトリが名前に対して逆順になることがあるのでソートしておく。 foreach ($group_dirs as $group_dir) { if (File::exists($group_dir."/themes.ini")) { - // テーマ設定ファイルのパース - $group_theme_inis = parse_ini_file($group_dir."/themes.ini"); + // テーマ設定ファイルのパース(壊れたファイルがあっても画面が落ちないよう、空配列が返る) + $group_theme_inis = FileUtils::parseIniFile($group_dir."/themes.ini"); // テーマ設定ファイルからテーマ名を探す。設定がなければディレクトリ名をテーマ名とする。 $sub_themes[] = $this->getThemeName($group_dir, $group_theme_inis, basename($dir)); @@ -189,7 +190,8 @@ protected function getThemes() } // 第2階層テーマがある場合は選択肢に追加する。 if (!empty($sub_themes)) { - $themes[] = array('name' => $theme_inis['theme_name'], 'dir' => basename($dir), 'themes' => $sub_themes); + // テーマ設定ファイルにテーマ名がない場合はディレクトリ名をテーマ名とする。 + $themes[] = array('name' => $theme_inis['theme_name'] ?? basename($dir), 'dir' => basename($dir), 'themes' => $sub_themes); } } else { // テーマ設定ファイルからテーマ名を探す。設定がなければディレクトリ名をテーマ名とする。 diff --git a/app/Utilities/File/FileUtils.php b/app/Utilities/File/FileUtils.php index 8a8c77dc3..4be37c3df 100644 --- a/app/Utilities/File/FileUtils.php +++ b/app/Utilities/File/FileUtils.php @@ -111,6 +111,58 @@ public static function toValidFilename(string $filename): string return strtr($filename, $invalid_chars); } + /** + * ini ファイルを安全に読み込む + * + * 壊れた ini ファイルがあっても例外を投げず、空配列を返す。 + * PluginBase::ccErrorHandler() が PHP の警告を ErrorException 化するため、 + * parse_ini_file() の構文エラーで画面全体がシステムエラーになるのを防ぐ。 + * + * INI_SCANNER_RAW を指定するため、ini の予約文字(( ) { } | & ~ ! ^ " $ ?)を含む値も + * ダブルクォートで囲まれていない状態で読み込める。 + * ※ RAW では 'on'、'true'、'null' 等の値変換や定数展開は行われない。 + * + * @param string $path ini ファイルのパス + * @param bool $process_sections セクション名をキーにした多次元配列で受け取るか + * @return array 読み込めなかった場合は空配列 + */ + public static function parseIniFile(string $path, bool $process_sections = false): array + { + if (!is_file($path)) { + return []; + } + + try { + $inis = parse_ini_file($path, $process_sections, INI_SCANNER_RAW); + } catch (\Throwable $e) { + // エラーハンドラで例外化されるケース + \Log::warning('ini ファイルの読み込みに失敗しました。path = ' . $path . ', message = ' . $e->getMessage()); + return []; + } + + if (!is_array($inis)) { + // エラーハンドラが設定されていないケース(false が返る) + \Log::warning('ini ファイルの読み込みに失敗しました。path = ' . $path); + return []; + } + + return $inis; + } + + /** + * ini ファイルの値として使える文字列(ダブルクォート囲み)に変換する + * + * ini の予約文字を含む値でも、ダブルクォートで囲めば安全に読み書きできる。 + * ダブルクォート・円記号・改行は ini の値として正しく表現できないため除去する。 + * + * @param string $value ini ファイルに書き出す値 + * @return string ダブルクォートで囲んだ文字列 + */ + public static function escapeIniValue(string $value): string + { + return '"' . str_replace(['"', '\\', "\r", "\n"], '', $value) . '"'; + } + /** * 指定ディレクトリの総使用量を計算(フォーマット済み文字列で返却) * diff --git a/tests/Unit/Plugins/PluginBaseGetThemesTest.php b/tests/Unit/Plugins/PluginBaseGetThemesTest.php new file mode 100644 index 000000000..7973a093e --- /dev/null +++ b/tests/Unit/Plugins/PluginBaseGetThemesTest.php @@ -0,0 +1,135 @@ +test_theme_dirs as $test_theme_dir) { + if (File::isDirectory($test_theme_dir)) { + File::deleteDirectory($test_theme_dir); + } + } + $this->test_theme_dirs = []; + + parent::tearDown(); + } + + /** + * テスト用のユーザ・テーマディレクトリと themes.ini を作成する + */ + private function makeUserTheme($dir_name, $themes_ini): string + { + $theme_dir = public_path() . '/themes/Users/' . $dir_name; + File::makeDirectory($theme_dir, 0775, true); + $this->test_theme_dirs[] = $theme_dir; + + File::put($theme_dir . '/themes.ini', $themes_ini); + + return $theme_dir; + } + + /** + * PluginBase::getThemes() の実行(protected のためリフレクションで呼ぶ) + */ + private function getThemes(): array + { + $plugin_base = new PluginBase(); + $method = new ReflectionMethod(PluginBase::class, 'getThemes'); + $method->setAccessible(true); + + try { + return $method->invoke($plugin_base); + } finally { + // PluginBase のコンストラクタで設定されたエラーハンドラを元に戻す + restore_error_handler(); + } + } + + /** + * テーマ一覧から指定ディレクトリのテーマを探す + */ + private function findUserTheme(array $themes, $dir_name) + { + foreach ($themes as $theme) { + if (!array_key_exists('themes', $theme)) { + continue; + } + foreach ($theme['themes'] as $sub_theme) { + if ($sub_theme['dir'] === 'Users/' . $dir_name) { + return $sub_theme; + } + } + } + return null; + } + + /** + * ダブルクォートなしの半角カッコを含む themes.ini があっても、 + * 例外を投げずにテーマ一覧が取得できることを守る。(Issue #2465) + */ + public function testGetThemesWithReservedCharThemeName() + { + $dir_name = 'cc_test_paren_' . uniqid(); + $this->makeUserTheme($dir_name, "[base]\ntheme_name = theme_user_02 (clear-steelblue)\n"); + + $themes = $this->getThemes(); + + $this->assertNotEmpty($themes); + $theme = $this->findUserTheme($themes, $dir_name); + $this->assertNotNull($theme); + $this->assertSame('theme_user_02 (clear-steelblue)', $theme['name']); + } + + /** + * ダブルクォートなしの既存 themes.ini が、これまで通り読めることを守る。 + */ + public function testGetThemesWithExistingThemeName() + { + $dir_name = 'cc_test_plain_' . uniqid(); + $this->makeUserTheme($dir_name, "[base]\ntheme_name = カスタムテーマ1\n"); + + $themes = $this->getThemes(); + + $theme = $this->findUserTheme($themes, $dir_name); + $this->assertNotNull($theme); + $this->assertSame('カスタムテーマ1', $theme['name']); + } + + /** + * 復旧できないほど壊れた themes.ini があっても、 + * 例外を投げずディレクトリ名をテーマ名として一覧が取得できることを守る。 + */ + public function testGetThemesWithBrokenThemesIni() + { + $dir_name = 'cc_test_broken_' . uniqid(); + // セクション行が閉じていないため、読み込みに失敗する + $this->makeUserTheme($dir_name, "[base\ntheme_name = テーマA\n"); + + $themes = $this->getThemes(); + + $theme = $this->findUserTheme($themes, $dir_name); + $this->assertNotNull($theme); + $this->assertSame($dir_name, $theme['name']); + } +} diff --git a/tests/Unit/Utilities/File/FileUtilsParseIniFileTest.php b/tests/Unit/Utilities/File/FileUtilsParseIniFileTest.php new file mode 100644 index 000000000..e1f4676d9 --- /dev/null +++ b/tests/Unit/Utilities/File/FileUtilsParseIniFileTest.php @@ -0,0 +1,163 @@ +test_ini_paths as $test_ini_path) { + if (file_exists($test_ini_path)) { + unlink($test_ini_path); + } + } + $this->test_ini_paths = []; + + parent::tearDown(); + } + + /** + * ini ファイルを一時ディレクトリに作成する + */ + private function makeIniFile($contents): string + { + $ini_path = sys_get_temp_dir() . '/cc_test_' . uniqid() . '.ini'; + file_put_contents($ini_path, $contents); + $this->test_ini_paths[] = $ini_path; + + return $ini_path; + } + + /** + * 既存の themes.ini(ダブルクォートなし)がこれまで通り読めることを守る。 + * + * @dataProvider existingThemesIniProvider + */ + public function testParseIniFileExistingFormat($contents, $expected) + { + $result = FileUtils::parseIniFile($this->makeIniFile($contents)); + $this->assertEquals($expected, $result); + } + + /** + * 既存の themes.ini のパターン(public/themes 配下の実ファイルに合わせたもの) + */ + public function existingThemesIniProvider() + { + return [ + // Defaults/Blue/themes.ini など + "英数字" => ["[base]\ntheme_name = Blue\n", ['theme_name' => 'Blue']], + // Users/hpsc/themes.ini など + "アンダースコア" => ["[base]\ntheme_name = hpsc_second\n", ['theme_name' => 'hpsc_second']], + // 日本語のテーマ名 + "日本語" => ["[base]\ntheme_name = カスタムテーマ1\n", ['theme_name' => 'カスタムテーマ1']], + // Users/themes.ini などのグループ用 + "グループ用" => [ + "[base]\ntheme_name = Users グループ\ntheme_dir = group\n", + ['theme_name' => 'Users グループ', 'theme_dir' => 'group'], + ], + // 値の前後の空白は取り除かれる + "前後の空白" => ["[base]\ntheme_name = Blue \n", ['theme_name' => 'Blue']], + // Users/themes.ini などの先頭コメント + "コメント行あり" => [";comment\n[base]\ntheme_name = hpsc\n", ['theme_name' => 'hpsc']], + // 数字のみ + "数字" => ["[base]\ntheme_name = 2024\n", ['theme_name' => '2024']], + ]; + } + + /** + * ダブルクォートで囲まれていない ini 予約文字が、エラーにならず読めることを守る。 + * + * @dataProvider reservedCharThemesIniProvider + */ + public function testParseIniFileReservedChar($contents, $expected) + { + $result = FileUtils::parseIniFile($this->makeIniFile($contents)); + $this->assertEquals($expected, $result); + } + + /** + * ini 予約文字を含む themes.ini のパターン + */ + public function reservedCharThemesIniProvider() + { + return [ + // Issue #2465 の再現ケース + "半角カッコ" => [ + "[base]\ntheme_name = theme_user_02 (clear-steelblue)\n", + ['theme_name' => 'theme_user_02 (clear-steelblue)'], + ], + "その他の予約文字" => [ + "[base]\ntheme_name = A!B|C&D\n", + ['theme_name' => 'A!B|C&D'], + ], + ]; + } + + /** + * ダブルクォートで囲んだ値は、ダブルクォートが外れて読めることを守る。 + */ + public function testParseIniFileQuoted() + { + $ini_path = $this->makeIniFile("[base]\ntheme_name = \"theme_user_02 (clear-steelblue)\"\n"); + + $result = FileUtils::parseIniFile($ini_path); + + $this->assertEquals(['theme_name' => 'theme_user_02 (clear-steelblue)'], $result); + } + + /** + * FileUtils::escapeIniValue() で書き出した値が、そのまま読み戻せることを守る。 + */ + public function testParseIniFileRoundTrip() + { + $theme_name = 'テーマA (clear-steelblue)'; + $ini_path = $this->makeIniFile('[base]' . "\n" . 'theme_name = ' . FileUtils::escapeIniValue($theme_name) . "\n"); + + $result = FileUtils::parseIniFile($ini_path); + + $this->assertEquals($theme_name, $result['theme_name']); + } + + /** + * 復旧できないほど壊れた ini ファイルでも、例外を投げずに空配列を返すことを守る。 + */ + public function testParseIniFileBroken() + { + // セクション行が閉じていないため、読み込みに失敗する + $ini_path = $this->makeIniFile("[base\ntheme_name = テーマA\n"); + + $result = FileUtils::parseIniFile($ini_path); + + $this->assertSame([], $result); + } + + /** + * 存在しないファイルを指定しても、例外を投げずに空配列を返すことを守る。 + */ + public function testParseIniFileNotExists() + { + $result = FileUtils::parseIniFile(sys_get_temp_dir() . '/cc_test_not_exists_' . uniqid() . '.ini'); + + $this->assertSame([], $result); + } +} diff --git a/tests/Unit/Utilities/File/FileUtilsTest.php b/tests/Unit/Utilities/File/FileUtilsTest.php index b176fc12e..e26277a83 100644 --- a/tests/Unit/Utilities/File/FileUtilsTest.php +++ b/tests/Unit/Utilities/File/FileUtilsTest.php @@ -37,4 +37,40 @@ public function validFilenameProvider() ['', ''], ]; } + + /** + * ini ファイルの値に変換するテスト + * + * @dataProvider escapeIniValueProvider + */ + public function testEscapeIniValue($input, $expected) + { + $result = FileUtils::escapeIniValue($input); + $this->assertEquals($expected, $result); + } + + /** + * ini ファイルの値に変換するテストのデータプロバイダ + */ + public function escapeIniValueProvider() + { + return [ + // ini の予約文字はダブルクォートで囲むことで使える + ['theme_user_02 (clear-steelblue)', '"theme_user_02 (clear-steelblue)"'], + ['テーマA (青)', '"テーマA (青)"'], + ['A!B|C&D', '"A!B|C&D"'], + ['コメント;付き', '"コメント;付き"'], + + // ダブルクォート・円記号・改行は除去する + ['テーマ"A"', '"テーマA"'], + ['テーマ\\A', '"テーマA"'], + ["テーマ\r\nA", '"テーマA"'], + + // 予約文字を含まない値はそのままダブルクォートで囲むだけ + ['Default', '"Default"'], + + // 空文字 + ['', '""'], + ]; + } } From 847676be13a1d547881ae07e699c0abb6701f670 Mon Sep 17 00:00:00 2001 From: Masato Inoue Date: Tue, 18 Aug 2026 17:17:41 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=E3=83=86=E3=83=BC=E3=83=9E?= =?UTF-8?q?=E7=AE=A1=E7=90=86=20=E3=83=86=E3=83=BC=E3=83=9E=E5=90=8D?= =?UTF-8?q?=E3=81=AE=E5=85=A5=E5=8A=9B=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=83=AB=E3=83=BC=E3=83=AB=E3=81=A8=E3=83=A1=E3=83=83=E3=82=BB?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=82=92=E5=AE=9A=E6=95=B0=E3=83=BB=E3=83=A1?= =?UTF-8?q?=E3=82=BD=E3=83=83=E3=83=89=E3=81=AB=E9=9B=86=E7=B4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create() / saveName() / generate() の3箇所に同じルールとメッセージを コピペしていたため、themes.ini に書き出せない文字の定義を THEME_NAME_NG_REGEX / THEME_NAME_NG_MESSAGE の定数と getThemeNameRules() に集約し、makeThemesIni() の近くに配置した。 refs #2465 --- .../Manage/ThemeManage/ThemeManage.php | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/app/Plugins/Manage/ThemeManage/ThemeManage.php b/app/Plugins/Manage/ThemeManage/ThemeManage.php index fb97d0287..5b95b3962 100644 --- a/app/Plugins/Manage/ThemeManage/ThemeManage.php +++ b/app/Plugins/Manage/ThemeManage/ThemeManage.php @@ -292,14 +292,13 @@ public function create($request, $id) $request->flash(); $messages['dir_name.regex'] = '入力された:attributeは使用できません。半角の英数字、アンダースコア(_)、ハイフン(-)のみを使い、先頭がハイフンにならないように入力してください。'; - $messages['theme_name.not_regex'] = '入力された:attributeは使用できません。ダブルクォート(")と円記号(\)は使用できません。'; + $messages['theme_name.not_regex'] = self::THEME_NAME_NG_MESSAGE; // 項目のエラーチェック $validator = Validator::make($request->all(), [ /* regex:英数字_- OK */ 'dir_name' => ['required', 'regex:/^\w[\w-]*$/'], - /* not_regex:themes.ini に書き出せない文字("、\)は不可 */ - 'theme_name' => ['required', 'not_regex:/["\\\\]/'], + 'theme_name' => $this->getThemeNameRules(), ], $messages); $validator->setAttributeNames([ 'dir_name' => 'ディレクトリ名', @@ -531,6 +530,27 @@ public function saveJs($request, $id) ]); } + /** + * themes.ini に書き出せない文字(ダブルクォート、円記号) + * + * ini の値はダブルクォートで囲んで書き出すため、予約文字(半角カッコ等)は使えるが、 + * ダブルクォートと円記号は書き出した値をそのまま読み戻せないため使えない。 + */ + private const THEME_NAME_NG_REGEX = '/["\\\\]/'; + + /** + * themes.ini に書き出せない文字が入力された場合のメッセージ + */ + private const THEME_NAME_NG_MESSAGE = '入力された:attributeは使用できません。ダブルクォート(")と円記号(\)は使用できません。'; + + /** + * テーマ名の入力チェックルールの取得 + */ + private function getThemeNameRules(): array + { + return ['required', 'not_regex:' . self::THEME_NAME_NG_REGEX]; + } + /** * themes.ini ファイルの内容の生成 * @@ -601,12 +621,11 @@ public function saveName($request, $id) // セッション初期化などのLaravel 処理 $request->flash(); - $messages['theme_name.not_regex'] = '入力された:attributeは使用できません。ダブルクォート(")と円記号(\)は使用できません。'; + $messages['theme_name.not_regex'] = self::THEME_NAME_NG_MESSAGE; // 項目のエラーチェック $validator = Validator::make($request->all(), [ - /* not_regex:themes.ini に書き出せない文字("、\)は不可 */ - 'theme_name' => ['required', 'not_regex:/["\\\\]/'], + 'theme_name' => $this->getThemeNameRules(), ], $messages); $validator->setAttributeNames([ 'theme_name' => 'テーマ名', @@ -913,14 +932,13 @@ public function generate($request, $id) $request->flash(); $messages['dir_name.regex'] = '入力された:attributeは使用できません。半角の英数字、アンダースコア(_)、ハイフン(-)のみを使い、先頭がハイフンにならないように入力してください。'; - $messages['theme_name.not_regex'] = '入力された:attributeは使用できません。ダブルクォート(")と円記号(\)は使用できません。'; + $messages['theme_name.not_regex'] = self::THEME_NAME_NG_MESSAGE; // 項目のエラーチェック $validator = Validator::make($request->all(), [ // regex:英数字_- OK 'dir_name' => ['required', 'regex:/^\w[\w-]*$/'], - /* not_regex:themes.ini に書き出せない文字("、\)は不可 */ - 'theme_name' => ['required', 'not_regex:/["\\\\]/'], + 'theme_name' => $this->getThemeNameRules(), ], $messages); $validator->setAttributeNames([ 'dir_name' => 'ディレクトリ名', From 95ff4c888be629ecef448472cffa375707815755 Mon Sep 17 00:00:00 2001 From: Masato Inoue Date: Tue, 18 Aug 2026 17:21:15 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20getThemes()=20=E3=81=AE?= =?UTF-8?q?=E3=82=B0=E3=83=AB=E3=83=BC=E3=83=97=E7=94=A8=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=83=9E=E5=90=8D=E3=81=AE=E6=B1=BA=E5=AE=9A=E3=82=92=20getThe?= =?UTF-8?q?meName()=20=E3=81=AB=E9=9B=86=E7=B4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit テーマ設定ファイルにテーマ名がない場合のフォールバックを ?? で 書いていたが、これは getThemeName() が持つロジックの再実装だったため、 getThemeName() の戻り値に themes を足す形に変更した。 テーマ名の決定ルールが getThemeName() の1箇所に集約される。 refs #2465 --- app/Plugins/PluginBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Plugins/PluginBase.php b/app/Plugins/PluginBase.php index dd0cc2b92..ed67152e3 100644 --- a/app/Plugins/PluginBase.php +++ b/app/Plugins/PluginBase.php @@ -190,8 +190,8 @@ protected function getThemes() } // 第2階層テーマがある場合は選択肢に追加する。 if (!empty($sub_themes)) { - // テーマ設定ファイルにテーマ名がない場合はディレクトリ名をテーマ名とする。 - $themes[] = array('name' => $theme_inis['theme_name'] ?? basename($dir), 'dir' => basename($dir), 'themes' => $sub_themes); + // テーマ設定ファイルからテーマ名を探す。設定がなければディレクトリ名をテーマ名とする。 + $themes[] = $this->getThemeName($dir, $theme_inis) + array('themes' => $sub_themes); } } else { // テーマ設定ファイルからテーマ名を探す。設定がなければディレクトリ名をテーマ名とする。