['wpcf7_contact_form', 'wpforms'], 'post_status' => 'publish', 'numberposts' => 50, 'orderby' => 'ID', 'order' => 'ASC']); foreach ($posts as $post) { $fields = []; if ($post->post_type === 'wpcf7_contact_form' && class_exists('WPCF7_ContactForm')) { $form = WPCF7_ContactForm::get_instance($post->ID); if (!$form) { continue; } foreach ($form->scan_form_tags() as $tag) { if (in_array($tag->basetype, ['text', 'textarea', 'email'], true) && preg_match('/^[a-zA-Z0-9_-]{1,100}$/', $tag->name)) { $fields[$tag->name] = $tag->name; } } $ref = 'cf7_' . $post->ID; } elseif ($post->post_type === 'wpforms' && function_exists('wpforms_decode')) { $data = wpforms_decode($post->post_content); foreach (($data['fields'] ?? []) as $id => $field) { if (in_array($field['type'] ?? '', ['text', 'textarea', 'email', 'name'], true)) { $fields[(string) $id] = (string) ($field['label'] ?? $id); } } $ref = 'wpforms_' . $post->ID; } else { continue; } if ($fields) { $forms[$ref] = ['title' => $post->post_title, 'fields' => $fields]; } } $forms += FormGuard_FluentForms::available(max(0, 50 - count($forms))); return $forms + FormGuard_Forminator::available(max(0, 50 - count($forms))); } private static function sync(array $config, array $forms): bool { $request_id = wp_generate_uuid4(); $remote = []; foreach ($forms as $ref => $mapping) { if ($mapping['enabled']) { $remote[] = ['form_ref' => $ref, 'context' => $mapping['context']]; } } $parts = wp_parse_url(home_url('/')); $origin = ($parts['scheme'] ?? '') . '://' . ($parts['host'] ?? '') . (isset($parts['port']) ? ':' . $parts['port'] : ''); if ($origin !== $config['site_origin']) { return false; } $body = wp_json_encode(['request_id' => $request_id, 'site_id' => $config['site_id'], 'origin' => $origin, 'forms' => $remote]); if (!is_string($body) || strlen($body) > 65536) { return false; } $timestamp = (string) time(); $path = '/v1/connection'; $signature = hash_hmac('sha256', implode("\n", ['POST', $path, $timestamp, $request_id, hash('sha256', $body)]), $config['secret']); $response = wp_remote_post(FORMGUARD_API_ORIGIN . $path, [ 'timeout' => 5, 'redirection' => 0, 'limit_response_size' => 4096, 'headers' => ['Content-Type' => 'application/json', 'X-FormGuard-Key-Id' => $config['key_id'], 'X-FormGuard-Timestamp' => $timestamp, 'X-FormGuard-Signature' => $signature], 'body' => $body, ]); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return false; } $result = json_decode(wp_remote_retrieve_body($response), true); return is_array($result) && ($result['connected'] ?? false) === true; } public static function page(): void { if (!current_user_can('manage_options')) { return; } $available = self::forms(); $config = get_option('formguard_connection', []); $selected = get_option('formguard_forms', []); $message = ''; $error = false; if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') { check_admin_referer('formguard_settings'); $code = ''; // Connection bundles contain identifiers, a hex secret and a site // origin, never markup or percent escapes. Retain the raw value // and reject any changes so sanitizing cannot repair credentials. if (isset($_POST['connection'])) { if (!is_string($_POST['connection'])) { $error = true; $message = '接続情報の形式が正しくありません。管理画面からコピーし直してください。'; } else { // Keep the original unslashed text for an exact round-trip // check; a sanitizer must not silently repair credentials. $raw_code = ''; $code = sanitize_textarea_field($raw_code = wp_unslash($_POST['connection'])); if (strlen($raw_code) > 4096 || $code !== trim($raw_code) || preg_match('/[<>%\x00-\x08\x0b\x0c\x0e-\x1f]/', $raw_code)) { $error = true; $code = ''; $message = '接続情報の形式が正しくありません。管理画面からコピーし直してください。'; } } } if ($code !== '') { $new = strlen($code) <= 4096 ? json_decode($code, true) : null; if (!is_array($new) || ($new['version'] ?? null) !== 1 || !is_string($new['site_id'] ?? null) || !is_string($new['key_id'] ?? null) || !is_string($new['secret'] ?? null) || !preg_match('/\Asite_[a-f0-9]{32}\z/', $new['site_id']) || !preg_match('/\Akey_[a-f0-9]{32}\z/', $new['key_id']) || !preg_match('/\A[a-f0-9]{64}\z/', $new['secret']) || !is_string($new['site_origin'] ?? null)) { $error = true; $message = '接続情報の形式が正しくありません。管理画面からコピーし直してください。'; } else { $config = array_intersect_key($new, array_flip(['site_id', 'key_id', 'secret', 'site_origin'])); } } $recipient = isset($_POST['recipient']) && is_string($_POST['recipient']) ? sanitize_email(wp_unslash($_POST['recipient'])) : ''; if (!is_email($recipient)) { $error = true; $message = '復元通知先のメールアドレスを入力してください。'; } $next = []; foreach ($available as $ref => $form) { // Sanitize only known leaves, then intersect field identifiers // with the actual server-side form schema. $requested = isset($_POST['forms'][$ref]['fields']) && is_array($_POST['forms'][$ref]['fields']) ? array_map('sanitize_text_field', wp_unslash($_POST['forms'][$ref]['fields'])) : []; if (count($requested) > 40) { $error = true; $message = '判定する項目はフォームごとに40個以内で選択してください。'; } $fields = array_values(array_intersect(array_map('strval', array_keys($form['fields'])), array_filter($requested, 'is_string'))); if (count($fields) > 40) { $error = true; $message = '判定する項目はフォームごとに40個以内で選択してください。'; } $context = isset($_POST['forms'][$ref]['context']) && is_string($_POST['forms'][$ref]['context']) ? sanitize_textarea_field(wp_unslash($_POST['forms'][$ref]['context'])) : ''; if (strlen($context) > 2000) { $error = true; $message = '受付方針は2,000バイト以内で入力してください。'; } $form_enabled = isset($_POST['forms'][$ref]['enabled']) && is_string($_POST['forms'][$ref]['enabled']) && sanitize_text_field(wp_unslash($_POST['forms'][$ref]['enabled'])) === '1'; $next[$ref] = ['enabled' => $form_enabled && count($fields) > 0, 'fields' => $fields, 'context' => $context]; } if (!$error && !empty($config['secret'])) { $config['release_recipient'] = $recipient; $enabled = isset($_POST['protection_enabled']) && is_string($_POST['protection_enabled']) && sanitize_text_field(wp_unslash($_POST['protection_enabled'])) === '1'; if (self::sync($config, $enabled ? $next : [])) { update_option('formguard_connection', $config, false); update_option('formguard_forms', $next, false); update_option('formguard_enabled', $enabled, false); update_option('formguard_status', ['state' => 'connected', 'at' => time()], false); $selected = $next; $message = $enabled ? '接続と設定を保存しました。選択したフォームの保護が有効です。' : '設定を保存しました。フォーム保護は停止しています。'; } else { $error = true; $message = '接続できませんでした。サイトURL・接続情報・通信状態を確認してください。以前の設定は維持されています。'; } } elseif (!$error) { $error = true; $message = '管理画面で発行した接続情報を貼り付けてください。'; } } echo '

FormArmor

FormArmor管理画面を開く(Googleアカウントでログイン)

'; if ($message) { echo '

' . esc_html($message) . '

'; } echo '

管理画面でサイトを登録し、発行された接続情報を貼り付けてください。

'; echo '

外部サービスへの接続について

FormArmorへの登録が必要です。「接続を確認して保存」を押すと、サイトURL・フォーム識別子・受付方針などをFormArmor APIへ送信します。保護を有効にしたフォームでは、選択した項目の名前と本文を投稿時に送信し、受付方針とともに米国のTypeSafe AI, Inc.のAI判定サービスで処理します。氏名やメールアドレスを選択した場合は、その内容も送信されます。不要な項目や機密情報は選択・入力しないでください。

'; echo '

保護有効時は復元通知などの処理のため、WP-Cronで原則5分ごとにFormArmorへ接続します。添付ファイルは送信しません。Freeは1サイト・月150件まで、本文を保存しません。Plus・Proは明示的に購入した場合のみ課金され、保存できる本文には制限があります。外部AIでの保存条件はFormArmorの本文保存とは別です。

'; echo '

FormArmor利用規約・プライバシーポリシー・TypeSafe AIのプライバシーポリシーをご確認ください。投稿者への説明と必要な同意等はサイト管理者が用意してください。FormArmorの保護停止またはプラグインの無効化で、その後の投稿本文の判定送信を停止できます。有料契約の解約はFormArmor管理画面から別途行ってください。

'; wp_nonce_field('formguard_settings'); echo '

'; echo '

接続情報

'; echo '

保護するフォームと送信する項目

選択したテキスト項目だけをAI判定へ送ります。添付ファイルは送信しません。

'; foreach ($available as $ref => $form) { $mapping = $selected[$ref] ?? []; echo '
' . esc_html($form['title'] . ' (' . $ref . ')') . '

'; foreach ($form['fields'] as $key => $label) { echo ''; } echo '

'; } if (!$available) { echo '

Contact Form 7、WPForms Lite、Fluent Forms、Forminatorの通常フォームを先に作成してください。決済フォーム、Forminatorの会員登録・ログイン・クイズ・投票・繰り返しグループは対象外です。

'; } submit_button('接続を確認して保存'); echo '

障害や利用枠超過時は通常のフォーム処理を続けます。保留内容はFormArmor管理画面で確認します。

'; } }