diff --git a/app/Enums/SpamBlockType.php b/app/Enums/SpamBlockType.php index f7946ce8e..247c83326 100644 --- a/app/Enums/SpamBlockType.php +++ b/app/Enums/SpamBlockType.php @@ -16,11 +16,13 @@ class SpamBlockType extends EnumsBase const email = 'email'; const domain = 'domain'; const ip_address = 'ip_address'; + const honeypot = 'honeypot'; // key/valueの連想配列 const enum = [ self::email => 'メールアドレス', self::domain => 'ドメイン', self::ip_address => 'IPアドレス', + self::honeypot => 'ハニーポット', ]; } diff --git a/app/Plugins/Manage/SpamManage/SpamManage.php b/app/Plugins/Manage/SpamManage/SpamManage.php index 0e7a27e85..1a50d42e7 100644 --- a/app/Plugins/Manage/SpamManage/SpamManage.php +++ b/app/Plugins/Manage/SpamManage/SpamManage.php @@ -102,10 +102,15 @@ public function store($request) abort(403, '権限がありません。'); } + // ハニーポットの場合は値不要、それ以外は必須 + $block_value_rules = $request->block_type === SpamBlockType::honeypot + ? ['nullable', 'max:255'] + : ['required', 'max:255']; + // 項目のエラーチェック $validator = Validator::make($request->all(), [ 'block_type' => ['required', 'in:' . implode(',', SpamBlockType::getMemberKeys())], - 'block_value' => ['required', 'max:255'], + 'block_value' => $block_value_rules, 'target_forms_id' => ['required_if:scope_type,form'], ], [ 'target_forms_id.required_if' => '適用範囲で特定フォームを選択した場合、フォームを選択してください。', @@ -129,12 +134,17 @@ public function store($request) $target_id = $request->target_forms_id; } + // ハニーポットの場合は値をnullにする + $block_value = $request->block_type === SpamBlockType::honeypot + ? null + : $request->block_value; + // スパムリストの追加 SpamList::create([ 'target_plugin_name' => 'forms', 'target_id' => $target_id, 'block_type' => $request->block_type, - 'block_value' => $request->block_value, + 'block_value' => $block_value, 'memo' => $request->memo, ]); @@ -177,9 +187,17 @@ public function update($request, $id) abort(403, '権限がありません。'); } + // スパムリストデータの呼び出し(バリデーション前に取得してblock_typeを参照) + $spam = SpamList::findOrFail($id); + + // ハニーポットの場合は値不要、それ以外は必須 + $block_value_rules = $spam->block_type === SpamBlockType::honeypot + ? ['nullable', 'max:255'] + : ['required', 'max:255']; + // 項目のエラーチェック $validator = Validator::make($request->all(), [ - 'block_value' => ['required', 'max:255'], + 'block_value' => $block_value_rules, 'target_forms_id' => ['required_if:scope_type,form'], ], [ 'target_forms_id.required_if' => '適用範囲で特定フォームを選択した場合、フォームを選択してください。', @@ -196,18 +214,20 @@ public function update($request, $id) ->withInput(); } - // スパムリストデータの呼び出し - $spam = SpamList::findOrFail($id); - // 適用範囲の処理 $target_id = null; if ($request->scope_type === 'form' && $request->filled('target_forms_id')) { $target_id = $request->target_forms_id; } + // ハニーポットの場合は値をnullにする + $block_value = $spam->block_type === SpamBlockType::honeypot + ? null + : $request->block_value; + // 更新 $spam->target_id = $target_id; - $spam->block_value = $request->block_value; + $spam->block_value = $block_value; $spam->memo = $request->memo; $spam->save(); diff --git a/app/Plugins/User/Forms/FormsPlugin.php b/app/Plugins/User/Forms/FormsPlugin.php index 53680e4b5..fb273ad39 100644 --- a/app/Plugins/User/Forms/FormsPlugin.php +++ b/app/Plugins/User/Forms/FormsPlugin.php @@ -458,6 +458,9 @@ public function index($request, $page_id, $frame_id, $errors = null) } } + // ハニーポット設定確認 + $has_honeypot = $this->hasHoneypot($form); + if ($form->form_mode == FormMode::form) { // フォーム return $this->view('forms', [ @@ -466,6 +469,7 @@ public function index($request, $page_id, $frame_id, $errors = null) 'forms_columns' => $forms_columns, 'forms_columns_id_select' => $forms_columns_id_select, 'errors' => $errors, + 'has_honeypot' => $has_honeypot, ]); } else { // アンケート @@ -475,6 +479,7 @@ public function index($request, $page_id, $frame_id, $errors = null) 'forms_columns' => $forms_columns, 'forms_columns_id_select' => $forms_columns_id_select, 'errors' => $errors, + 'has_honeypot' => $has_honeypot, ]); } } else { @@ -822,6 +827,16 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null) ]); } + // ハニーポットチェック + $honeypot_check = $this->checkHoneypot($request, $form); + if ($honeypot_check['blocked']) { + $this->recordHoneypotBlock($honeypot_check, $form->id); + + return $this->commonView('error_messages', [ + 'error_messages' => [__('messages.honeypot_blocked')], + ]); + } + // スパムフィルタリングチェック $spam_check = $this->checkSpamFilter($request, $form); if ($spam_check['blocked']) { @@ -949,6 +964,9 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null) } } + // ハニーポット設定確認 + $has_honeypot = $this->hasHoneypot($form); + // 表示テンプレートを呼び出す if ($form->form_mode == FormMode::form) { // フォーム @@ -958,6 +976,7 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null) 'form' => $form, 'forms_columns' => $forms_columns, 'uploads' => $uploads, + 'has_honeypot' => $has_honeypot, ]); } else { // アンケート @@ -967,6 +986,7 @@ public function publicConfirm($request, $page_id, $frame_id, $id = null) 'form' => $form, 'forms_columns' => $forms_columns, 'uploads' => $uploads, + 'has_honeypot' => $has_honeypot, ]); } } @@ -1022,6 +1042,18 @@ public function publicStore($request, $page_id, $frame_id, $id = null) return collect(['redirect_path' => url($this->page->permanent_link)]); } + // ハニーポットチェック(二重防御) + $honeypot_check = $this->checkHoneypot($request, $form); + if ($honeypot_check['blocked']) { + $this->recordHoneypotBlock($honeypot_check, $form->id); + + // エラーメッセージをセッションに保存 + session()->flash("spam_blocked_error_{$frame_id}", __('messages.honeypot_blocked')); + + // 初期表示にリダイレクトして、初期表示処理にまかせる(エラー表示) + return collect(['redirect_path' => url($this->page->permanent_link)]); + } + // スパムフィルタリングチェック(二重防御) $spam_check = $this->checkSpamFilter($request, $form); if ($spam_check['blocked']) { @@ -3210,6 +3242,101 @@ private function recordSpamBlock(array $spam_check, int $forms_id): void } } + /** + * ハニーポットチェック + * + * @param \Illuminate\Http\Request $request リクエスト + * @param Forms $form フォームデータ + * @return array ブロック情報の配列 + */ + private function checkHoneypot($request, $form): array + { + $client_ip = $request->ip(); + + // このフォーム用のハニーポット設定を取得 + $honeypot_spam_list = SpamList::where('target_plugin_name', 'forms') + ->where('block_type', SpamBlockType::honeypot) + ->where(function ($q) use ($form) { + $q->where('target_id', $form->id) + ->orWhereNull('target_id'); + }) + ->first(); + + // ハニーポットが設定されていなければ早期リターン + if (!$honeypot_spam_list) { + return [ + 'blocked' => false, + 'client_ip' => $client_ip, + 'matched_spam_list' => null, + ]; + } + + // ハニーポットフィールドに値があればボットと判定 + $honeypot_value = $request->input('website_url', ''); + if (!empty($honeypot_value)) { + return [ + 'blocked' => true, + 'client_ip' => $client_ip, + 'honeypot_value' => $honeypot_value, + 'matched_spam_list' => $honeypot_spam_list, + ]; + } + + return [ + 'blocked' => false, + 'client_ip' => $client_ip, + 'matched_spam_list' => $honeypot_spam_list, + ]; + } + + /** + * ハニーポットブロックのログ記録とDB履歴記録 + * + * @param array $honeypot_check checkHoneypot() の戻り値 + * @param int $forms_id フォームID + * @return void + */ + private function recordHoneypotBlock(array $honeypot_check, int $forms_id): void + { + Log::info('Honeypot blocked', [ + 'form_id' => $forms_id, + 'ip' => $honeypot_check['client_ip'], + 'honeypot_value' => $honeypot_check['honeypot_value'] ?? null, + 'spam_list_id' => $honeypot_check['matched_spam_list']->id ?? null, + ]); + + // 履歴記録は補助的な機能のため、DB記録が失敗しても本来のブロック処理(エラーメッセージ表示)を続行する + try { + SpamBlockHistory::create([ + 'spam_list_id' => $honeypot_check['matched_spam_list']->id ?? null, + 'forms_id' => $forms_id, + 'block_type' => SpamBlockType::honeypot, + 'block_value' => $honeypot_check['honeypot_value'] ?? null, + 'client_ip' => $honeypot_check['client_ip'], + 'submitted_email' => null, + ]); + } catch (\Exception $e) { + Log::error('Failed to record honeypot block history', ['error' => $e->getMessage()]); + } + } + + /** + * ハニーポットが設定されているか確認 + * + * @param Forms $form フォームデータ + * @return bool ハニーポットが設定されている場合true + */ + private function hasHoneypot($form): bool + { + return SpamList::where('target_plugin_name', 'forms') + ->where('block_type', SpamBlockType::honeypot) + ->where(function ($q) use ($form) { + $q->where('target_id', $form->id) + ->orWhereNull('target_id'); + }) + ->exists(); + } + /** * スパムフィルタリングチェック * @@ -3366,10 +3493,15 @@ public function saveSpamFilter($request, $page_id, $frame_id, $forms_id) */ public function addSpamList($request, $page_id, $frame_id, $forms_id) { + // ハニーポットの場合は値不要、それ以外は必須 + $block_value_rules = $request->block_type === SpamBlockType::honeypot + ? ['nullable', 'max:255'] + : ['required', 'max:255']; + // 項目のエラーチェック $validator = Validator::make($request->all(), [ 'block_type' => ['required', 'in:' . implode(',', SpamBlockType::getMemberKeys())], - 'block_value' => ['required', 'max:255'], + 'block_value' => $block_value_rules, ]); $validator->setAttributeNames([ 'block_type' => '種別', @@ -3383,12 +3515,17 @@ public function addSpamList($request, $page_id, $frame_id, $forms_id) ->withInput(); } + // ハニーポットの場合は値をnullにする + $block_value = $request->block_type === SpamBlockType::honeypot + ? null + : $request->block_value; + // スパムリストの追加 SpamList::create([ 'target_plugin_name' => 'forms', 'target_id' => $forms_id, 'block_type' => $request->block_type, - 'block_value' => $request->block_value, + 'block_value' => $block_value, 'memo' => $request->memo, ]); diff --git a/config/app.php b/config/app.php index 6fffab913..364559108 100644 --- a/config/app.php +++ b/config/app.php @@ -339,6 +339,7 @@ 'MenuFrameConfig' => \App\Enums\MenuFrameConfig::class, 'MailAuthMethod' => \App\Enums\MailAuthMethod::class, 'PhotoalbumPlayviewType' => \App\Enums\PhotoalbumPlayviewType::class, + 'SpamBlockType' => \App\Enums\SpamBlockType::class, // utils 'DateUtils' => \App\Utilities\Date\DateUtils::class, diff --git a/database/migrations/2026_01_29_100001_make_block_value_nullable_in_spam_lists.php b/database/migrations/2026_01_29_100001_make_block_value_nullable_in_spam_lists.php new file mode 100644 index 000000000..6907d033f --- /dev/null +++ b/database/migrations/2026_01_29_100001_make_block_value_nullable_in_spam_lists.php @@ -0,0 +1,34 @@ +string('block_value', 255)->nullable()->comment('ブロック対象の値(ハニーポットの場合はnull)')->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('spam_lists', function (Blueprint $table) { + $table->string('block_value', 255)->nullable(false)->comment('ブロック対象の値')->change(); + }); + } +} diff --git a/public/css/connect.css b/public/css/connect.css index 820aaf269..150ebede3 100644 --- a/public/css/connect.css +++ b/public/css/connect.css @@ -569,3 +569,15 @@ a.cc-cursor-text:hover { 0% {opacity: 0;} 100% {opacity: 1;} } + +/* ハニーポット(スパムボット対策用の隠しフィールド) +------------------------------------- */ +.connect-hp-field { + opacity: 0 !important; + position: absolute !important; + top: 0; + left: 0; + height: 0; + width: 0; + z-index: -1; +} diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php index 1e08c72bb..570f74d89 100644 --- a/resources/lang/en/messages.php +++ b/resources/lang/en/messages.php @@ -81,6 +81,7 @@ 'cannot_be_delete_refers_to_the_information' => 'It cannot be deleted because there is a part that refers to the information you are trying to delete.', 'there_is_an_error' => 'There is an error.', 'there_is_an_error_refer_to_the_message_of_each_item' => 'For details of the error, refer to the message of each item.', + 'honeypot_blocked' => 'Invalid submission detected.', 'both_required' => 'Please enter both.', 'search_results' => 'Search Results', 'cases' => 'cases', diff --git a/resources/lang/ja/messages.php b/resources/lang/ja/messages.php index 5957e3861..2b926102a 100644 --- a/resources/lang/ja/messages.php +++ b/resources/lang/ja/messages.php @@ -81,6 +81,7 @@ 'cannot_be_delete_refers_to_the_information' => '削除しようとしている情報を参照している箇所がある為、削除できません。', 'there_is_an_error' => 'エラーがあります。', 'there_is_an_error_refer_to_the_message_of_each_item' => 'エラーの詳細は各項目のメッセージを参照してください。', + 'honeypot_blocked' => '不正な投稿が検出されました。', 'both_required' => '両方の項目を入力してください。', 'search_results' => '検索結果', 'cases' => '件', diff --git a/resources/views/plugins/common/spam_block_type_badge.blade.php b/resources/views/plugins/common/spam_block_type_badge.blade.php new file mode 100644 index 000000000..d104e85f3 --- /dev/null +++ b/resources/views/plugins/common/spam_block_type_badge.blade.php @@ -0,0 +1,18 @@ +{{-- + * スパムブロック種別バッジ + * + * @param string $block_type SpamBlockType の値 + * + * @author 井上 雅人 + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category スパム管理 +--}} +@if ($block_type == SpamBlockType::email) + メールアドレス +@elseif ($block_type == SpamBlockType::domain) + ドメイン +@elseif ($block_type == SpamBlockType::ip_address) + IPアドレス +@elseif ($block_type == SpamBlockType::honeypot) + ハニーポット +@endif diff --git a/resources/views/plugins/manage/spam/block_history.blade.php b/resources/views/plugins/manage/spam/block_history.blade.php index d01df88ed..2629026eb 100644 --- a/resources/views/plugins/manage/spam/block_history.blade.php +++ b/resources/views/plugins/manage/spam/block_history.blade.php @@ -99,13 +99,7 @@ {{ $history->created_at ? $history->created_at->format('Y/m/d H:i') : '' }} - @if ($history->block_type == SpamBlockType::email) - メールアドレス - @elseif ($history->block_type == SpamBlockType::domain) - ドメイン - @else - IPアドレス - @endif + @include('plugins.common.spam_block_type_badge', ['block_type' => $history->block_type]) {{ $history->block_value }} diff --git a/resources/views/plugins/manage/spam/edit.blade.php b/resources/views/plugins/manage/spam/edit.blade.php index a2d72cbb9..645eabe15 100644 --- a/resources/views/plugins/manage/spam/edit.blade.php +++ b/resources/views/plugins/manage/spam/edit.blade.php @@ -37,10 +37,10 @@ -
- +
+
- + @include('plugins.common.errors_inline', ['name' => 'block_value'])
@@ -97,4 +97,14 @@
+ + @endsection diff --git a/resources/views/plugins/manage/spam/index.blade.php b/resources/views/plugins/manage/spam/index.blade.php index de444fb96..e9b50bb46 100644 --- a/resources/views/plugins/manage/spam/index.blade.php +++ b/resources/views/plugins/manage/spam/index.blade.php @@ -28,7 +28,7 @@ @include('plugins.common.errors_form_line')
- サイト全体で適用されるスパムリストを管理します。 + サイト全体で適用されるブロックリストを管理します。
{{-- 検索フォーム --}} @@ -66,9 +66,9 @@ - {{-- スパムリスト一覧 --}} + {{-- ブロックリスト一覧 --}}
- スパムリスト一覧 + ブロックリスト一覧
{{ csrf_field() }} @@ -97,13 +97,7 @@ @forelse($spam_lists as $spam) - @if ($spam->block_type == SpamBlockType::email) - メールアドレス - @elseif ($spam->block_type == SpamBlockType::domain) - ドメイン - @else - IPアドレス - @endif + @include('plugins.common.spam_block_type_badge', ['block_type' => $spam->block_type]) {{ $spam->block_value }} @@ -136,7 +130,7 @@ @empty - スパムリストは登録されていません。 + ブロックリストは登録されていません。 @endforelse @@ -148,8 +142,8 @@
- {{-- スパムリスト追加フォーム --}} -
スパムリストへ追加
+ {{-- ブロックリスト追加フォーム --}} +
ブロックリストへ追加
{{ csrf_field() }} @@ -168,15 +162,16 @@ ※ メールアドレス:完全一致でブロックします。
※ ドメイン:メールアドレスの@以降と一致する場合にブロックします。
※ メールアドレス・ドメインはフォームに「メールアドレス」型項目がある場合に有効です。
- ※ IPアドレス:送信元IPアドレスと一致する場合にブロックします。 + ※ IPアドレス:送信元IPアドレスと一致する場合にブロックします。
+ ※ ハニーポット:ボット対策用の隠しフィールドを設置します。値の入力は不要です。
-
- +
+
- + @include('plugins.common.errors_inline', ['name' => 'block_value'])
@@ -222,4 +217,23 @@
+ + @endsection diff --git a/resources/views/plugins/manage/spam/spam_tab.blade.php b/resources/views/plugins/manage/spam/spam_tab.blade.php index f85896e15..419c96800 100644 --- a/resources/views/plugins/manage/spam/spam_tab.blade.php +++ b/resources/views/plugins/manage/spam/spam_tab.blade.php @@ -15,9 +15,9 @@ diff --git a/resources/views/plugins/user/forms/default/forms.blade.php b/resources/views/plugins/user/forms/default/forms.blade.php index 73ad1fb02..12e844a6c 100644 --- a/resources/views/plugins/user/forms/default/forms.blade.php +++ b/resources/views/plugins/user/forms/default/forms.blade.php @@ -149,6 +149,9 @@ @endforeach + {{-- ハニーポットフィールド --}} + @include('plugins.user.forms.default.include_honeypot_field') + {{-- Captcha フィールド --}} @include('plugins.user.forms.default.forms_captcha_field') diff --git a/resources/views/plugins/user/forms/default/forms_confirm.blade.php b/resources/views/plugins/user/forms/default/forms_confirm.blade.php index e0adced1e..184fae4cf 100644 --- a/resources/views/plugins/user/forms/default/forms_confirm.blade.php +++ b/resources/views/plugins/user/forms/default/forms_confirm.blade.php @@ -36,6 +36,10 @@ function submit_forms_cancel() { {{ csrf_field() }} + {{-- ハニーポット値引き継ぎ(二重防御用) --}} + @if ($has_honeypot) + + @endif @foreach($forms_columns as $form_column)
diff --git a/resources/views/plugins/user/forms/default/forms_confirm_tandem.blade.php b/resources/views/plugins/user/forms/default/forms_confirm_tandem.blade.php index d6d663f6c..bdc55fcce 100644 --- a/resources/views/plugins/user/forms/default/forms_confirm_tandem.blade.php +++ b/resources/views/plugins/user/forms/default/forms_confirm_tandem.blade.php @@ -35,6 +35,10 @@ function submit_forms_cancel() { {{ csrf_field() }} + {{-- ハニーポット値引き継ぎ(二重防御用) --}} + @if ($has_honeypot) + + @endif @php $no = 1; @endphp @foreach($forms_columns as $form_column) diff --git a/resources/views/plugins/user/forms/default/forms_edit_spam_filter.blade.php b/resources/views/plugins/user/forms/default/forms_edit_spam_filter.blade.php index 322d56392..b182c260e 100644 --- a/resources/views/plugins/user/forms/default/forms_edit_spam_filter.blade.php +++ b/resources/views/plugins/user/forms/default/forms_edit_spam_filter.blade.php @@ -64,8 +64,8 @@
use_spam_filter_flag)) style="display: none;" @endif>
-{{-- 適用されるスパムリスト --}} -
適用されるスパムリスト
+{{-- 適用されるブロックリスト --}} +
適用されるブロックリスト
@@ -82,13 +82,7 @@ @forelse($spam_lists as $spam) @empty - + @endforelse
- @if ($spam->block_type == SpamBlockType::email) - メールアドレス - @elseif ($spam->block_type == SpamBlockType::domain) - ドメイン - @else - IPアドレス - @endif + @include('plugins.common.spam_block_type_badge', ['block_type' => $spam->block_type]) {{ $spam->block_value }} @@ -115,19 +109,19 @@
スパムリストは登録されていません。ブロックリストは登録されていません。
-※ 適用範囲が「全体」のスパムリストはスパム管理から編集できます。 +※ 適用範囲が「全体」のブロックリストはスパム管理から編集できます。
-{{-- スパムリスト追加フォーム --}} -
スパムリストへ追加(このフォーム用)
+{{-- ブロックリスト追加フォーム --}} +
ブロックリストへ追加(このフォーム用)
{{ csrf_field() }} @@ -147,15 +141,16 @@ ※ メールアドレス:完全一致でブロックします。
※ ドメイン:メールアドレスの@以降と一致する場合にブロックします。
※ メールアドレス・ドメインはフォームに「メールアドレス」型項目がある場合に有効です。
- ※ IPアドレス:送信元IPアドレスと一致する場合にブロックします。 + ※ IPアドレス:送信元IPアドレスと一致する場合にブロックします。
+ ※ ハニーポット:ボット対策用の隠しフィールドを設置します。値の入力は不要です。
-
- +
+
- + @include('plugins.common.errors_inline', ['name' => 'block_value'])
@@ -185,6 +180,21 @@ $('#spam_list_section').slideUp(); } }); + + // 種別選択時の値フィールド表示/非表示 + $('input[name="block_type"]').on('change', function() { + if ($(this).val() === '{{ SpamBlockType::honeypot }}') { + $('#block_value_group').slideUp(); + $('#block_value_input').val(''); + } else { + $('#block_value_group').slideDown(); + } + }); + + // 初期表示時のチェック + if ($('input[name="block_type"]:checked').val() === '{{ SpamBlockType::honeypot }}') { + $('#block_value_group').hide(); + } }); diff --git a/resources/views/plugins/user/forms/default/include_honeypot_field.blade.php b/resources/views/plugins/user/forms/default/include_honeypot_field.blade.php new file mode 100644 index 000000000..14a5de009 --- /dev/null +++ b/resources/views/plugins/user/forms/default/include_honeypot_field.blade.php @@ -0,0 +1,14 @@ +{{-- + * ハニーポットフィールド(スパムボット対策用の隠しフィールド) + * + * @author 井上 雅人 + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category フォームプラグイン + * @note CSSは public/css/connect.css の .connect-hp-field を参照 +--}} +@if ($has_honeypot) + +@endif diff --git a/resources/views/plugins/user/forms/default/index_tandem.blade.php b/resources/views/plugins/user/forms/default/index_tandem.blade.php index 4a8adce86..7ca165e68 100644 --- a/resources/views/plugins/user/forms/default/index_tandem.blade.php +++ b/resources/views/plugins/user/forms/default/index_tandem.blade.php @@ -119,6 +119,9 @@
@endforeach + {{-- ハニーポットフィールド --}} + @include('plugins.user.forms.default.include_honeypot_field') + {{-- Captcha フィールド --}} @php $is_tandem_template = true; @endphp @include('plugins.user.forms.default.forms_captcha_field') diff --git a/tests/Feature/Plugins/User/Forms/FormsHoneypotTest.php b/tests/Feature/Plugins/User/Forms/FormsHoneypotTest.php new file mode 100644 index 000000000..dfe9c3c70 --- /dev/null +++ b/tests/Feature/Plugins/User/Forms/FormsHoneypotTest.php @@ -0,0 +1,330 @@ + + * @copyright OpenSource-WorkShop Co.,Ltd. All Rights Reserved + * @category フォームプラグイン + */ +class FormsHoneypotTest extends TestCase +{ + use RefreshDatabase; + + protected function setUp(): void + { + parent::setUp(); + $this->seed(); + } + + /** + * テスト用のページ、フレーム、バケツ、フォームを作成 + */ + private function createFormSetup() + { + $page = Page::factory()->create(); + $bucket = Buckets::factory()->create(['plugin_name' => 'forms']); + $frame = Frame::factory()->create([ + 'page_id' => $page->id, + 'plugin_name' => 'forms', + 'bucket_id' => $bucket->id, + ]); + + $form = Forms::factory()->create(['bucket_id' => $bucket->id]); + + return [$page, $frame, $bucket, $form]; + } + + /** + * ハニーポットをスパムリストに追加 + */ + private function addHoneypotToSpamList($form, $is_global = false): SpamList + { + return SpamList::factory()->create([ + 'target_plugin_name' => 'forms', + 'target_id' => $is_global ? null : $form->id, + 'block_type' => SpamBlockType::honeypot, + 'block_value' => null, + ]); + } + + /** + * コンテンツ管理者ユーザーを作成 + */ + private function createAdminUser(): User + { + $user = User::factory()->create(); + UsersRoles::factory()->create([ + 'users_id' => $user->id, + 'target' => 'base', + 'role_name' => 'role_article_admin', + ]); + return $user; + } + + /** + * addSpamList(): ハニーポットをスパムリストに追加できる + */ + public function testAddSpamListCanAddHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/addSpamList/{$page->id}/{$frame->id}/{$form->id}", + [ + 'block_type' => SpamBlockType::honeypot, + // ハニーポットは値不要 + 'redirect_path' => "/plugin/forms/editSpamFilter/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + $response->assertSessionHas('flash_message'); + + $this->assertDatabaseHas('spam_lists', [ + 'target_plugin_name' => 'forms', + 'target_id' => $form->id, + 'block_type' => SpamBlockType::honeypot, + 'block_value' => null, + ]); + } + + /** + * deleteSpamList(): ハニーポットをスパムリストから削除できる + */ + public function testDeleteSpamListCanRemoveHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $admin = $this->createAdminUser(); + $honeypot = $this->addHoneypotToSpamList($form); + + // Act + $response = $this->actingAs($admin)->post( + "/redirect/plugin/forms/deleteSpamList/{$page->id}/{$frame->id}/{$honeypot->id}", + [ + 'redirect_path' => "/plugin/forms/editSpamFilter/{$page->id}/{$frame->id}", + ] + ); + + // Assert + $response->assertStatus(302); + // 論理削除されていることを確認 + $this->assertSoftDeleted('spam_lists', [ + 'id' => $honeypot->id, + ]); + } + + /** + * publicConfirm(): ハニーポットが空なら確認画面に進める(履歴は記録されない) + */ + public function testPublicConfirmAllowsEmptyHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $this->addHoneypotToSpamList($form); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + $history_count_before = SpamBlockHistory::count(); + + // Act: /plugin/ を使用して直接メソッドを呼び出す + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => '', // ハニーポットは空 + ] + ); + + // Assert: ハニーポットブロック履歴が作成されていない + $this->assertEquals($history_count_before, SpamBlockHistory::where('block_type', SpamBlockType::honeypot)->count()); + } + + /** + * publicConfirm(): ハニーポットに値があるとブロックされ、履歴が記録される + */ + public function testPublicConfirmBlocksFilledHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $honeypot = $this->addHoneypotToSpamList($form); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + // Act: /plugin/ を使用して直接メソッドを呼び出す + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'http://spam-site.com', // ハニーポットに値がある + ] + ); + + // Assert: ハニーポットブロック履歴が作成されている + $this->assertDatabaseHas('spam_block_histories', [ + 'forms_id' => $form->id, + 'block_type' => SpamBlockType::honeypot, + 'block_value' => 'http://spam-site.com', + 'spam_list_id' => $honeypot->id, + ]); + + // レスポンスにエラーメッセージが含まれている + $response->assertSee(__('messages.honeypot_blocked')); + } + + /** + * publicConfirm(): ハニーポット無効時は値があってもブロックされない + */ + public function testPublicConfirmDoesNotBlockWhenHoneypotDisabled(): void + { + // Arrange: ハニーポット無効(スパムリストに登録なし) + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + $history_count_before = SpamBlockHistory::count(); + + // Act + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'http://spam-site.com', // ハニーポットに値がある + ] + ); + + // Assert: ハニーポットブロック履歴が作成されていない + $this->assertEquals($history_count_before, SpamBlockHistory::where('block_type', SpamBlockType::honeypot)->count()); + } + + /** + * publicConfirm(): グローバルハニーポットでもブロックされる + */ + public function testPublicConfirmBlocksWithGlobalHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $honeypot = $this->addHoneypotToSpamList($form, true); // グローバル設定 + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + // Act + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'http://spam-site.com', + ] + ); + + // Assert: グローバルハニーポットでもブロックされる + $this->assertDatabaseHas('spam_block_histories', [ + 'forms_id' => $form->id, + 'block_type' => SpamBlockType::honeypot, + 'spam_list_id' => $honeypot->id, + ]); + } + + /** + * publicStore(): ハニーポットに値があるとブロックされる(二重防御) + */ + public function testPublicStoreBlocksFilledHoneypot(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $honeypot = $this->addHoneypotToSpamList($form); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + // Act: 直接登録にPOST (リダイレクト経由) + $response = $this->post( + "/redirect/plugin/forms/publicStore/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'http://spam-site.com', + 'redirect_path' => $page->permanent_link, + ] + ); + + // Assert: リダイレクトされる + $response->assertRedirect(); + + // spam_block_histories にレコードが作成されている + $this->assertDatabaseHas('spam_block_histories', [ + 'forms_id' => $form->id, + 'block_type' => SpamBlockType::honeypot, + 'spam_list_id' => $honeypot->id, + ]); + } + + /** + * 履歴に記録されるデータが正しい + */ + public function testHoneypotBlockHistoryRecordsCorrectData(): void + { + // Arrange + [$page, $frame, $bucket, $form] = $this->createFormSetup(); + $honeypot = $this->addHoneypotToSpamList($form); + + // テキストカラムを作成 + $text_column = FormsColumns::factory()->textType()->create(['forms_id' => $form->id]); + + // Act + $response = $this->post( + "/plugin/forms/publicConfirm/{$page->id}/{$frame->id}", + [ + 'forms_columns_value' => [ + $text_column->id => 'テスト入力', + ], + 'website_url' => 'bot-filled-value', + ], + ['REMOTE_ADDR' => '10.0.0.1'] + ); + + // Assert: 記録されたデータの各フィールドが正しい + $history = SpamBlockHistory::latest('id')->first(); + $this->assertNotNull($history); + $this->assertEquals($honeypot->id, $history->spam_list_id); + $this->assertEquals($form->id, $history->forms_id); + $this->assertEquals(SpamBlockType::honeypot, $history->block_type); + $this->assertEquals('bot-filled-value', $history->block_value); + $this->assertNull($history->submitted_email); + $this->assertNotNull($history->client_ip); + $this->assertNotNull($history->created_at); + } +}