Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/Plugins/Manage/SiteManage/SiteManage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
54 changes: 45 additions & 9 deletions app/Plugins/Manage/ThemeManage/ThemeManage.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Support\Facades\Validator;

use App\Plugins\Manage\ManagePluginBase;
use App\Utilities\File\FileUtils;

/**
* テーマ管理クラス
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -291,12 +292,13 @@ public function create($request, $id)
$request->flash();

$messages['dir_name.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-]*$/'],
'theme_name' => ['required'],
'theme_name' => $this->getThemeNameRules(),
], $messages);
$validator->setAttributeNames([
'dir_name' => 'ディレクトリ名',
Expand All @@ -318,7 +320,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 ファイルの作成
Expand Down Expand Up @@ -528,6 +530,37 @@ 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 ファイルの内容の生成
*
* テーマ名に ini の予約文字(半角カッコ等)が含まれていても読み込めるよう、値はダブルクォートで囲む。
*/
private function makeThemesIni($theme_name): string
{
return '[base]' . "\n" . 'theme_name = ' . FileUtils::escapeIniValue((string)$theme_name) . "\n";
}

/**
* ユーザ・テーマ名の取得
*/
Expand All @@ -538,7 +571,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 '';
}
Expand Down Expand Up @@ -588,10 +621,12 @@ public function saveName($request, $id)
// セッション初期化などのLaravel 処理
$request->flash();

$messages['theme_name.not_regex'] = self::THEME_NAME_NG_MESSAGE;

// 項目のエラーチェック
$validator = Validator::make($request->all(), [
'theme_name' => ['required'],
]);
'theme_name' => $this->getThemeNameRules(),
], $messages);
$validator->setAttributeNames([
'theme_name' => 'テーマ名',
]);
Expand All @@ -608,7 +643,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', [
Expand Down Expand Up @@ -897,12 +932,13 @@ public function generate($request, $id)
$request->flash();

$messages['dir_name.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-]*$/'],
'theme_name' => ['required'],
'theme_name' => $this->getThemeNameRules(),
], $messages);
$validator->setAttributeNames([
'dir_name' => 'ディレクトリ名',
Expand Down Expand Up @@ -931,7 +967,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 ファイルの作成
Expand Down
12 changes: 7 additions & 5 deletions app/Plugins/PluginBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use App\Models\Common\Numbers;
use App\Traits\ConnectMailTrait;
use App\Utilities\File\FileUtils;

/**
* プラグイン基底クラス
Expand Down Expand Up @@ -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') {
Expand All @@ -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));
Expand All @@ -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[] = $this->getThemeName($dir, $theme_inis) + array('themes' => $sub_themes);
}
} else {
// テーマ設定ファイルからテーマ名を探す。設定がなければディレクトリ名をテーマ名とする。
Expand Down
52 changes: 52 additions & 0 deletions app/Utilities/File/FileUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) . '"';
}

/**
* 指定ディレクトリの総使用量を計算(フォーマット済み文字列で返却)
*
Expand Down
135 changes: 135 additions & 0 deletions tests/Unit/Plugins/PluginBaseGetThemesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

namespace Tests\Unit\Plugins;

use App\Plugins\PluginBase;
use Illuminate\Support\Facades\File;
use ReflectionMethod;
use Tests\TestCase;

/**
* PluginBase::getThemes() を対象にした単体テスト。
*
* ページ管理・サイト管理・テーマチェンジャーが使うテーマ一覧取得で、
* themes.ini にダブルクォートなしの半角カッコがあっても画面が落ちないことを検証する。
*/
class PluginBaseGetThemesTest extends TestCase
{
/**
* テストで作成したテーマディレクトリ
*/
private $test_theme_dirs = [];

/**
* テストで作成したテーマディレクトリの削除
*/
protected function tearDown(): void
{
foreach ($this->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']);
}
}
Loading
Loading