設定'; array_unshift( $links, $item ); return $links; } /** * Register the admin menu. */ public static function menu() { add_menu_page( 'AIS FormGuard', 'AIS FormGuard', 'manage_options', AISFG_SLUG, array( __CLASS__, 'render' ), 'dashicons-shield', 80 ); } /** * Register settings. */ public static function register() { register_setting( 'aisfg_group', AISFG_Settings::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( 'AISFG_Settings', 'sanitize' ), 'default' => AISFG_Settings::defaults(), ) ); } /** * Show admin notices (plugins list and this plugin's screen only). */ public static function notice() { if ( defined( 'WPCF7_VERSION' ) || ! current_user_can( 'activate_plugins' ) ) { return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen ) { return; } $on_plugins = ( 'plugins' === $screen->id ); $on_aisfg = ( isset( $screen->id ) && false !== strpos( $screen->id, AISFG_SLUG ) ); if ( ! $on_plugins && ! $on_aisfg ) { return; } echo '

AIS FormGuard: Contact Form 7 が有効になっていないため、スパム対策は動作していません。

'; } /** * One-time notice after 1.0.0 body-consent migration. Independent of CF7. */ public static function consent_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( ! get_option( 'aisfg_body_consent_notice' ) ) { return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || ! isset( $screen->id ) ) { return; } $on_plugins = ( 'plugins' === $screen->id ); $on_aisfg = ( false !== strpos( $screen->id, AISFG_SLUG ) ); if ( ! $on_plugins && ! $on_aisfg ) { return; } echo '

' . esc_html( '営業メール判定を使うと、問い合わせ本文(メールアドレス・電話番号は伏せ字)を判定に使います。このため営業メール判定をいったん OFF にしました。内容をご確認のうえ、もう一度『営業メール判定を使う』を ON にしてください。' ) . '

'; delete_option( 'aisfg_body_consent_notice' ); } /** * Register suggested privacy policy text. */ public static function privacy_policy_content() { if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { return; } $content = '

AIS FormGuard は Contact Form 7 のスパム対策のために、次の情報をこのサイト上に保存します。

' . '' . '

任意のクラウド連携(営業メール判定)を有効にした場合のみ、営業メール判定サービスへ判定用データを送信します。連携がオンのときは、問い合わせ本文をメールアドレスと電話番号を伏せ字にして送ります。詳細はプラグインの readme(External services)を参照してください。判定に使った本文は営業メール判定サービスに永続保存しません。

'; wp_add_privacy_policy_content( 'AIS FormGuard', $content ); } /** * Enqueue admin CSS on this plugin's screen only. * * @param string $hook_suffix Current admin page hook. */ public static function enqueue_assets( $hook_suffix ) { if ( 'toplevel_page_' . AISFG_SLUG !== $hook_suffix ) { return; } $handle = 'aisfg-admin'; wp_register_style( $handle, false, array(), AISFG_VERSION ); wp_enqueue_style( $handle ); wp_add_inline_style( $handle, self::admin_css() ); wp_register_script( $handle, false, array(), AISFG_VERSION, true ); wp_enqueue_script( $handle ); wp_add_inline_script( $handle, self::admin_js() ); } /** * Admin screen behaviour: select-on-click for read-only textareas, * confirm dialogs for destructive buttons, and level presets. * Keep the domain-bind consent row as PHP rendered it. * Typing a key never shows or hides it. * * @return string */ private static function admin_js() { return '(function(){document.querySelectorAll("textarea[data-aisfg-select]").forEach(function(t){t.addEventListener("click",function(){t.select();});});document.querySelectorAll("[data-aisfg-confirm]").forEach(function(b){b.addEventListener("click",function(e){if(!window.confirm(b.getAttribute("data-aisfg-confirm"))){e.preventDefault();}});});var radios=document.querySelectorAll("input[name=\\"aisfg_settings[level]\\"]");if(!radios.length){return;}var keys=["min_seconds","rate_limit","rate_window_min","max_urls","dup_ttl_min"];function apply(r){keys.forEach(function(k){var el=document.getElementById("aisfg_"+k);var v=r.getAttribute("data-"+k);if(el&&v!==null){el.value=v;}});}radios.forEach(function(r){r.addEventListener("change",function(){apply(r);});});}());'; } /** * Admin CSS rules. * * @return string */ private static function admin_css() { return '.aisfg .aisfg-sub{font-size:13px;font-weight:400;color:#646970;margin-left:8px}' . '.aisfg .card{max-width:760px;padding:16px 20px;margin-top:16px}' . '.aisfg .card h2{margin-top:0}' . '.aisfg .steps{display:flex;gap:8px;margin:16px 0;flex-wrap:wrap}' . '.aisfg .steps span{padding:6px 12px;border-radius:16px;background:#f0f0f1;color:#50575e}' . '.aisfg .steps span.on{background:#2271b1;color:#fff;font-weight:600}' . '.aisfg .chk{margin:10px 0;padding:10px 12px;border-left:4px solid #ccc;background:#fff}' . '.aisfg .chk.ok{border-color:#00a32a}.aisfg .chk.warn{border-color:#dba617}.aisfg .chk.ng{border-color:#d63638}' . '.aisfg .chk b{display:block}' . '.aisfg .nums{display:flex;gap:12px;flex-wrap:wrap;margin:0}' . '.aisfg .num{flex:1;min-width:150px;background:#fff;border:1px solid #dcdcde;padding:12px 16px}' . '.aisfg .num b{display:block;font-size:26px;line-height:1.2}' . '.aisfg .lv{display:block;margin:10px 0;padding:10px 12px;border:1px solid #dcdcde;background:#fff}' . '.aisfg .lv small{display:block;color:#646970;margin-left:24px}' . '.aisfg .bar{display:inline-block;height:10px;vertical-align:middle;margin-right:2px}' . '.aisfg table.widefat{max-width:100%}' . '.aisfg .postbox{margin-top:16px;max-width:100%;min-width:0}' . '.aisfg .postbox-header h2{margin:0;padding:8px 12px;font-size:14px;line-height:1.4}' . '.aisfg .postbox .inside{margin:0;padding:12px 12px 16px}' . '.aisfg .postbox.aisfg-mode-monitor{box-shadow:inset 4px 0 0 #dba617}' . '.aisfg .postbox.aisfg-mode-enforce{box-shadow:inset 4px 0 0 #00a32a}' . '.aisfg .postbox .inside ul{margin:0.5em 0;padding-left:1.4em;list-style:disc}' . '.aisfg .postbox .inside li{margin:0.25em 0}' . '@media screen and (max-width:782px){.aisfg .postbox{margin-left:0;margin-right:0}}'; } /* ================= 操作の受け口(admin-post) ================= */ /** * Capability and nonce check. */ private static function guard() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( '権限がありません。' ); } check_admin_referer( 'aisfg_action' ); } /** * Redirect back to the settings page. * * @param array $args Parameter. */ private static function back( array $args = array() ) { if ( ! isset( $args['aisfg_step'] ) && ! isset( $args['tab'] ) ) { $tab = isset( $_POST['aisfg_tab'] ) ? sanitize_key( wp_unslash( $_POST['aisfg_tab'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification -- nonce verified in guard() before back(). $allowed = array_keys( self::allowed_tabs() ); if ( in_array( $tab, $allowed, true ) ) { $args['tab'] = $tab; } } wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php?page=' . AISFG_SLUG ) ) ); exit; } /** * Merge and save settings. * * @param array $changes Parameter. */ private static function save( array $changes ) { update_option( AISFG_Settings::OPTION, array_merge( AISFG_Settings::get(), $changes ) ); } /** * Handle save level. */ public static function handle_save_level() { self::guard(); $level = isset( $_POST['level'] ) ? sanitize_key( wp_unslash( $_POST['level'] ) ) : 'standard'; // phpcs:ignore WordPress.Security.NonceVerification -- nonce verified in guard(). AISFG_Settings::apply_level( $level ); self::save( array( 'report_enabled' => empty( $_POST['report_enabled'] ) ? 0 : 1, // phpcs:ignore WordPress.Security.NonceVerification -- nonce verified in guard(). 'report_email' => isset( $_POST['report_email'] ) ? sanitize_email( wp_unslash( $_POST['report_email'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification -- nonce verified in guard(). ) ); self::back( array( 'aisfg_step' => 3 ) ); } /** * Handle finish. */ public static function handle_finish() { self::guard(); self::save( array( 'setup_done' => 1, 'mode' => 'monitor', 'started_at' => time(), ) ); self::back( array( 'aisfg_msg' => 'finished' ) ); } /** * Handle set mode. */ public static function handle_set_mode() { self::guard(); $mode_raw = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification -- nonce verified in guard(). $mode = ( 'enforce' === $mode_raw ) ? 'enforce' : 'monitor'; self::save( array( 'mode' => $mode ) ); self::back( array( 'aisfg_msg' => 'enforce' === $mode ? 'enforced' : 'monitored' ) ); } /** * Handle selftest. */ public static function handle_selftest() { self::guard(); set_transient( 'aisfg_selftest_' . get_current_user_id(), AISFG_Env::selftest(), 600 ); self::back( array( 'aisfg_step' => 3 ) ); } /** * Handle test report. */ public static function handle_test_report() { self::guard(); self::back( array( 'aisfg_msg' => AISFG_Report::send_weekly( true ) ? 'report_sent' : 'report_failed' ) ); } /** * Handle cloud license refresh. */ public static function handle_cloud_license_refresh() { self::guard(); $s = AISFG_Settings::get(); if ( ! AISFG_Cloud::configured( $s ) ) { set_transient( 'aisfg_cloudtest_' . get_current_user_id(), array( false, '『営業メール判定を使う』を ON にして保存してから実行してください。' ), 600 ); self::back(); } $was_auto_off = '' !== AISFG_Cloud::auto_off_state( $s ); AISFG_Cloud::clear_license_cache( $s ); list( $d, $err, $err_code ) = AISFG_Cloud::license( $s, true ); if ( in_array( $err_code, array( 'invalid_api_key', 'domain_mismatch' ), true ) ) { set_transient( 'aisfg_cloudtest_' . get_current_user_id(), array( false, AISFG_Cloud::KEY_ERROR_NOTICE ), 600 ); } elseif ( 'forbidden' === $err_code || ( is_array( $d ) && isset( $d['ticket']['state'] ) && 'unbound' === $d['ticket']['state'] ) ) { set_transient( 'aisfg_cloudtest_' . get_current_user_id(), array( false, AISFG_Cloud::UNBOUND_NOTICE ), 600 ); } elseif ( ! is_array( $d ) ) { set_transient( 'aisfg_cloudtest_' . get_current_user_id(), array( false, '契約情報を取得できませんでした(' . $err . ')。' ), 600 ); } else { $state = isset( $d['ticket']['state'] ) ? (string) $d['ticket']['state'] : ''; $resumed = 'active' === $state && $was_auto_off && AISFG_Cloud::resume( true ); set_transient( 'aisfg_cloudtest_' . get_current_user_id(), array( true, ( $resumed ? '営業メール判定を ON にしました。' : '' ) . AISFG_Cloud::format_license_summary( $d ) ), 600 ); } self::back(); } /** * Handle dismiss upsell. */ public static function handle_dismiss_upsell() { self::guard(); update_user_meta( get_current_user_id(), 'aisfg_upsell_dismissed', 1 ); self::back(); } /** * Handle unban. */ public static function handle_unban() { self::guard(); $ip = isset( $_POST['ip'] ) ? AISFG_Ban::normalize( sanitize_text_field( wp_unslash( $_POST['ip'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification -- nonce verified in guard(). if ( '' !== $ip ) { AISFG_Ban::unban( $ip ); } self::back( array( 'aisfg_msg' => 'unbanned' ) ); } /** * Handle unban all. */ public static function handle_unban_all() { self::guard(); AISFG_Ban::unban_all(); self::back( array( 'aisfg_msg' => 'unbanned' ) ); } /** * Handle reset wizard. */ public static function handle_reset_wizard() { self::guard(); self::save( array( 'setup_done' => 0 ) ); self::back( array( 'aisfg_step' => 1 ) ); } /** * Remove the stored API key locally. Does not call the sales-mail service. */ public static function handle_delete_cloud_key() { self::guard(); $s = AISFG_Settings::get(); AISFG_Cloud::forget_local_key( $s ); self::save( array( 'cloud_key' => '', 'cloud_key_clear' => 1, 'cloud_enabled' => 0, ) ); self::back( array( 'aisfg_msg' => 'key_deleted', 'tab' => 'cloud', ) ); } /* ================= 表示の共通部品 ================= */ /** * Form open. * * @param mixed $action Parameter. */ private static function form_open( $action ) { echo '
'; echo ''; echo ''; wp_nonce_field( 'aisfg_action' ); } /** * Print flash messages. */ private static function messages() { $map = array( 'finished' => array( 'updated', 'セットアップが完了しました。まずは「見守りモード」で、しばらく様子を見ましょう。' ), 'enforced' => array( 'updated', '防御モードに切り替えました。迷惑と思われる送信は、メールが送られなくなります。' ), 'monitored' => array( 'updated', '見守りモードに戻しました。すべての送信がこれまでどおり届きます。' ), 'report_sent' => array( 'updated', 'テストメールを送信しました。届かない場合は迷惑メールフォルダもご確認ください。' ), 'report_failed' => array( 'error', 'メールを送信できませんでした。宛先のメールアドレスをご確認ください。' ), 'unbanned' => array( 'updated', '自動ブロックを解除しました。' ), 'key_deleted' => array( 'updated', 'API キーを削除しました。営業メール判定は OFF です。' ), ); $key = isset( $_GET['aisfg_msg'] ) ? sanitize_key( wp_unslash( $_GET['aisfg_msg'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification -- flash message key from our own redirect. if ( isset( $map[ $key ] ) ) { printf( '

%s

', 'error' === $map[ $key ][0] ? 'error' : 'success', esc_html( $map[ $key ][1] ) ); } } /* ================= 画面 ================= */ /** * Render the admin page. */ public static function render() { if ( ! current_user_can( 'manage_options' ) ) { return; } $s = AISFG_Settings::get(); $step = isset( $_GET['aisfg_step'] ) ? absint( sanitize_key( wp_unslash( $_GET['aisfg_step'] ) ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification -- wizard step from our own redirect; capability already checked. echo '
'; echo '

AIS FormGuard問い合わせフォームのスパム対策

'; self::messages(); settings_errors(); if ( empty( $s['setup_done'] ) || $step > 0 ) { self::render_wizard( max( 1, min( 3, $step ) ), $s ); } else { self::render_dashboard( $s ); } echo '
'; } /** * Allowed dashboard tabs and their labels. * * @return array */ private static function allowed_tabs() { return array( 'overview' => '概要', 'settings' => '設定', 'cloud' => '営業メール判定(有料)', 'help' => '困ったとき', ); } /** * Current dashboard tab from the URL. * * @return string */ private static function current_tab() { $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'overview'; // phpcs:ignore WordPress.Security.NonceVerification -- tab selector from our own links; capability already checked. return isset( self::allowed_tabs()[ $tab ] ) ? $tab : 'overview'; } /** * Settings page URL for a tab. * * @param string $tab Tab key. * @return string */ private static function tab_url( $tab ) { return admin_url( 'admin.php?page=' . AISFG_SLUG . '&tab=' . $tab ); } /** * Print WordPress nav tabs. * * @param string $current Current tab key. */ private static function render_tabs( $current ) { echo ''; } /** * Open a static postbox (no drag / no collapse). * * @param string $title Box heading. * @param string $extra_class Optional extra class for the box. */ private static function postbox_open( $title, $extra_class = '' ) { $class = 'postbox'; if ( '' !== $extra_class ) { $class .= ' ' . $extra_class; } echo '
'; echo '

' . esc_html( $title ) . '

'; echo '
'; } /** * Close a postbox. */ private static function postbox_close() { echo '
'; } /** * Render stepper. * * @param mixed $step Parameter. */ private static function render_stepper( $step ) { $labels = array( 1 => '① 環境チェック', 2 => '② 保護レベル', 3 => '③ 動作確認', ); echo '
'; foreach ( $labels as $n => $l ) { printf( '%s', $n === $step ? 'on' : '', esc_html( $l ) ); } echo '
'; } /** * Render wizard. * * @param mixed $step Parameter. * @param mixed $s Parameter. */ private static function render_wizard( $step, $s ) { echo '

かんたんセットアップ

'; echo '

3つのステップで完了します(所要時間の目安:3〜5分)。専門的な設定は必要ありません。

'; self::render_stepper( $step ); if ( 1 === $step ) { $has_ng = false; foreach ( AISFG_Env::checks() as $c ) { if ( 'ng' === $c[0] ) { $has_ng = true; } printf( '
%2$s %3$s', esc_attr( $c[0] ), esc_html( array( 'ok' => '✔', 'warn' => '△', 'ng' => '✖', )[ $c[0] ] ), esc_html( $c[1] ) ); self::print_env_check_body( $c[2] ); echo '
'; } if ( $has_ng ) { echo '

✖ の項目を直してから、このページを再読み込みしてください。

'; } else { echo '

次へ:保護レベルを選ぶ

'; } } if ( 2 === $step ) { self::form_open( 'save_level' ); echo '

迷惑な送信をどのくらい厳しく見分けるかを選びます。迷ったら「標準」のままで大丈夫です。後から変えられます。

'; $cur = ( 'custom' === $s['level'] ) ? 'standard' : $s['level']; foreach ( AISFG_Settings::presets() as $key => $p ) { printf( '', esc_attr( $key ), checked( $cur, $key, false ), esc_html( $p['label'] ), esc_html( $p['desc'] ) ); } echo '

週に1回のレポートメール

'; echo '

先週の状況(受付件数・迷惑と思われる送信の件数)をメールでお知らせします。

'; printf( '

', checked( ! empty( $s['report_enabled'] ), true, false ) ); printf( '


空欄の場合は、サイトの管理者メールアドレス(%2$s)に送ります。

', esc_attr( $s['report_email'] ), esc_attr( get_option( 'admin_email' ) ) ); echo '

'; } if ( 3 === $step ) { echo '

問題無く動くかを確認します。

'; echo '

A. 自動テスト

'; echo '

確認情報(トークン)は、フォームに隠して付ける署名です。偽物の送信や、ページを開いてすぐの送信を見分けます。

'; self::form_open( 'selftest' ); echo ''; $res = get_transient( 'aisfg_selftest_' . get_current_user_id() ); if ( is_array( $res ) ) { foreach ( $res as $r ) { printf( '
%s %s%s
', $r[0] ? 'ok' : 'ng', $r[0] ? '✔' : '✖', esc_html( $r[1] ), esc_html( $r[2] ) ); } } echo '

B. 実際にフォームから送ってみる(おすすめ)

'; echo '

フォームのあるページを開き、10秒ほど待ってから、ご自身のメールアドレスでテスト送信してみてください。

'; $pages = AISFG_Env::form_pages(); if ( $pages ) { echo '
    '; foreach ( $pages as $p ) { printf( '
  • %s(新しいタブで開きます)
  • ', esc_url( $p['url'] ), esc_html( $p['title'] ) ); } echo '
'; } else { echo '

フォームを設置しているページが見つかりませんでした。お手持ちのお問い合わせページを開いてください。

'; } echo '

送信後にこのページを再読み込みすると、下に記録が表示されます。

'; $rows = AISFG_Report::recent_all( 3 ); if ( $rows ) { echo ''; foreach ( $rows as $r ) { printf( '', esc_html( get_date_from_gmt( $r->created_at, 'n/j H:i' ) ), esc_html( self::decision_label( $r->decision ) ) ); } echo '
日時結果
%s%s
'; } else { echo '

まだ記録はありません。

'; } echo '

C. 完了

'; echo '

完了すると、最初は「見守りモード」で始まります。迷惑と思われる送信を記録するだけで、メールは今までどおり届きます。安心してお使いいただけます。

'; self::form_open( 'finish' ); echo ''; } echo '
'; } /** * Decision label. * * @param mixed $d Parameter. */ private static function decision_label( $d ) { $map = array( 'pass' => '通常の問い合わせ(通過)', 'block' => '迷惑と判断し、止めました', 'monitor' => '迷惑と思われる送信(記録のみ・通しました)', 'review' => '営業の疑い(目印を付けて送信しました)', ); return isset( $map[ $d ] ) ? $map[ $d ] : $d; } /** * Render dashboard. * * @param mixed $s Parameter. */ private static function render_dashboard( $s ) { $tab = self::current_tab(); self::render_tabs( $tab ); if ( 'settings' === $tab ) { self::render_tab_settings( $s ); } elseif ( 'cloud' === $tab ) { self::render_cloud_section( $s ); } elseif ( 'help' === $tab ) { self::render_tab_help( $s ); } else { self::render_tab_overview( $s ); } } /** * Overview tab. * * @param mixed $s Parameter. */ private static function render_tab_overview( $s ) { $st = AISFG_Report::stats( 7 ); $enforce = ( 'enforce' === $s['mode'] ); $days_run = $s['started_at'] ? (int) floor( ( time() - (int) $s['started_at'] ) / DAY_IN_SECONDS ) : 0; self::postbox_open( $enforce ? '迷惑な送信を止めています' : '記録だけしています(まだ止めていません)', $enforce ? 'aisfg-mode-enforce' : 'aisfg-mode-monitor' ); echo '

' . esc_html( $enforce ? '今は防御モードです。' : '今は見守りモードです。' ) . '

'; if ( $enforce ) { echo '

迷惑と思われる送信は、メールが送られません。

'; self::form_open( 'set_mode' ); echo ''; } else { echo '

すべてのメールがこれまでどおり届きます。

'; if ( $days_run >= 7 ) { echo '

見守りを始めて ' . (int) $days_run . ' 日たちました。下の「最近の検知」を見て、正規の問い合わせが混ざっていなければ、防御モードに切り替えられます。

'; } else { echo '

まずは1週間ほど、このまま様子を見ることをおすすめします。

'; } self::form_open( 'set_mode' ); echo ''; } self::postbox_close(); if ( AISFG_Cloud::configured( $s ) ) { self::postbox_open( '営業メール判定の状態' ); if ( AISFG_Report::cloud_errors( 24 ) > 0 ) { printf( '

営業メール判定サービスに接続できていません(直近24時間に %d 件)。この間の営業メールの判定は行っていませんが、問い合わせは通常どおり届いています。『営業メール判定(有料)』タブで『契約情報を更新』を押して確認してください。

', (int) AISFG_Report::cloud_errors( 24 ), esc_url( self::tab_url( 'cloud' ) ) ); } list( $lic ) = AISFG_Cloud::license( $s, false ); if ( is_array( $lic ) && isset( $lic['ticket'] ) && is_array( $lic['ticket'] ) ) { $ticket = $lic['ticket']; $state = isset( $ticket['state'] ) ? (string) $ticket['state'] : ''; $remain = isset( $ticket['remaining'] ) ? (int) $ticket['remaining'] : 0; $end = isset( $ticket['period_end'] ) ? (int) $ticket['period_end'] : 0; $end_jst = $end > 0 ? AISFG_Cloud::format_jst( $end ) : '—'; echo '

' . esc_html( sprintf( '営業メール判定:%s/残り %d 回/有効期限 %s', AISFG_Cloud::ticket_state_label( $state ), $remain, $end_jst ) ) . '

'; } self::postbox_close(); } self::postbox_open( '件数' ); echo '
'; printf( '
受け付けた送信%d直近7日間
', (int) $st['total'] ); printf( '
通常の問い合わせ%d通過
', (int) $st['pass'] ); $sub = $enforce ? '止めた件数' : '記録のみ(まだ止めていません)'; if ( $st['review'] > 0 ) { $sub .= '/うち営業の疑い ' . (int) $st['review'] . ' 件(目印を付けて送信)'; } printf( '
迷惑と思われる送信%d%s
', (int) $st['detected'], esc_html( $sub ) ); echo '
'; $ban = AISFG_Ban::summary(); if ( $ban['active'] > 0 ) { printf( '

自動でブロック中の接続元(IP):%d 件/入口でブロックした送信:%s 件(詳しくは『設定』タブの『攻撃元の自動ブロック』)

', (int) $ban['active'], esc_html( number_format_i18n( (int) $ban['blocked'] ) ), esc_url( self::tab_url( 'settings' ) ) ); } self::postbox_close(); self::postbox_open( '最近の検知' ); echo '

「迷惑と思われる」と判断された送信です。正規のお客様からの問い合わせが混ざっていないか、ときどきご確認ください。

'; $rows = AISFG_Report::recent_detections( 10 ); if ( $rows ) { echo ''; foreach ( $rows as $r ) { printf( '', esc_html( get_date_from_gmt( $r->last_at ? $r->last_at : $r->created_at, 'n/j H:i' ) ), esc_html( array( 'block' => '止めました', 'review' => '目印を付けて送信', 'monitor' => '記録のみ', )[ $r->decision ] ), esc_html( AISFG_Report::reasons_text( $r->reasons ) ), esc_html( number_format_i18n( (int) $r->hits ) ) ); } echo '
日時扱い理由件数
%s%s%s%s
'; echo '

同じ理由・同じ接続元(IP)からの送信が 1 時間以内に続くときは、1 行にまとめて「件数」で表します。

'; } else { echo '

まだ検知はありません。

'; } self::postbox_close(); $daily = AISFG_Report::daily( 7 ); $max = 1; foreach ( $daily as $d ) { $max = max( $max, $d['pass'] + $d['detected'] ); } self::postbox_open( '日ごとの状況' ); echo ''; foreach ( $daily as $date => $d ) { printf( '', esc_html( substr( $date, 5 ) ), (int) round( $d['pass'] / $max * 260 ), (int) round( $d['detected'] / $max * 260 ), (int) $d['pass'], (int) $d['detected'] ); } echo '
日付グラフ(緑:通常 / 赤:迷惑と思われる送信)通常迷惑
%s%d%d
'; self::postbox_close(); } /** * Settings tab. * * @param mixed $s Parameter. */ private static function render_tab_settings( $s ) { self::render_ban_section( $s ); self::render_weekly_report_box( $s ); self::render_advanced_box( $s ); } /** * Weekly report box. * * @param mixed $s Parameter. */ private static function render_weekly_report_box( $s ) { $opt = AISFG_Settings::OPTION; $admin_email = (string) get_option( 'admin_email' ); self::postbox_open( '週次レポートメール' ); echo '
'; settings_fields( 'aisfg_group' ); printf( '

', esc_attr( $opt ), checked( ! empty( $s['report_enabled'] ), true, false ) ); printf( '



空欄のときは、サイトの管理者メールアドレス(%3$s)に送ります。

', esc_attr( $opt ), esc_attr( $s['report_email'] ), esc_html( $admin_email ) ); submit_button( '保存する', 'secondary' ); echo '
'; self::form_open( 'test_report' ); echo ''; self::postbox_close(); } /** * Advanced settings box. * * @param mixed $s Parameter. */ private static function render_advanced_box( $s ) { self::postbox_open( '詳しい設定' ); printf( '

現在の保護レベル:%s

', esc_html( self::support_level_label( $s['level'] ) ) ); echo '

迷惑な送信をどのくらい厳しく見分けるかを選びます。迷ったら「標準」のままで大丈夫です。数値の詳細も、この下で変えられます。

'; echo '
'; settings_fields( 'aisfg_group' ); foreach ( AISFG_Settings::presets() as $key => $p ) { $vals = $p['values']; printf( '', esc_attr( AISFG_Settings::OPTION ), esc_attr( $key ), checked( $s['level'], $key, false ), (int) $vals['min_seconds'], (int) $vals['rate_limit'], (int) $vals['rate_window_min'], (int) $vals['max_urls'], (int) $vals['dup_ttl_min'], esc_html( $p['label'] ), esc_html( $p['desc'] ) ); } echo ''; self::field( $s, 'min_seconds', '最短の送信時間(秒)', 'ページを開いてからこの秒数より早い送信は、機械的な送信とみなします。' ); self::field( $s, 'rate_limit', '許可する送信回数', '同じ接続元(IP)から、下の時間内に送れる回数です。会社や学校など、同じ接続元(IP)を複数人で使う場合は増やしてください。' ); self::field( $s, 'rate_window_min', '上の回数を数える時間(分)', '' ); self::field( $s, 'max_urls', '許可するリンク(URL)の数', '本文にこの数より多いリンクがあると、迷惑と思われる送信とみなします。' ); self::field( $s, 'dup_ttl_min', '同じ内容を重複とみなす時間(分)', '' ); self::field( $s, 'token_ttl_hours', '確認情報の有効時間(時間)', 'ページのキャッシュを使っている場合は、長めにしてください。' ); self::field( $s, 'retention_days', '記録を残す日数', 'これを過ぎた記録は自動で削除されます。' ); echo ''; submit_button( '保存する', 'secondary' ); echo '
'; self::postbox_close(); } /** * Help tab. * * @param mixed $s Parameter. */ private static function render_tab_help( $s ) { $st = AISFG_Report::stats( 7 ); self::postbox_open( '困ったときの手順' ); echo '

問い合わせが届かない場合:まず『概要』タブの『見守りモードに戻す』を押してください。すべての送信が届くようになります。

'; self::postbox_close(); self::postbox_open( 'お問い合わせに貼る内容' ); echo '

サポートに相談する場合:下の内容をコピーして、お問い合わせに貼り付けてください(個人情報や問い合わせ内容は含まれません)。

'; echo ''; self::postbox_close(); self::postbox_open( 'セットアップのやり直し' ); self::form_open( 'reset_wizard' ); echo '

'; self::postbox_close(); } /** * Render ban section. * * @param mixed $s Parameter. */ private static function render_ban_section( $s ) { $opt = AISFG_Settings::OPTION; $c = AISFG_Ban::client(); self::postbox_open( '攻撃元の自動ブロック' ); echo '

隠し欄への入力など、確かなボットの手がかりが短い間に続いた接続元(IP)を、一定時間、フォーム送信の入口でブロックします。メール、判定、記録の処理に入る前に終わるので、サーバーの負担が下がります。

'; echo '

防御モードのときだけ動きます。見守りモードでは、ブロックしません。

'; echo '

一覧に載るのは、防御モードで、隠し欄への入力(HONEYPOT)または確認情報なし(TOKEN_MISSING)が、窓内のしきい値回(既定は 10 分以内に 3 回)続いた接続元(IP)だけです。URL 超過だけでは載りません。プロキシや CDN(Cloudflare など)の裏で「信頼するプロキシ」が空欄のときは、全員を誤認しないよう安全に停止します。

'; if ( 'enforce' === $s['mode'] && ! empty( $s['ban_enabled'] ) && ! $c['safe'] && $c['proxied'] ) { echo '

自動ブロックが動いていません。このサイトは、プロキシや CDN(Cloudflare など)の裏にある可能性があります。このままブロックすると、すべての訪問者を同じ接続元(IP)と誤認して、全員をブロックしてしまうおそれがあるため、安全のために止めています。下の「信頼するプロキシ」に、プロキシの接続元(IP)の範囲を入力すると動きます。

'; } echo '
'; settings_fields( 'aisfg_group' ); echo ''; printf( '', esc_attr( $opt ), checked( ! empty( $s['ban_enabled'] ), true, false ) ); printf( '', esc_attr( $opt ), (int) $s['ban_window_min'], (int) $s['ban_threshold'], (int) $s['ban_hours'] ); printf( '', esc_attr( $opt ), esc_textarea( $s['ban_allow'] ) ); printf( '', esc_attr( $opt ), esc_textarea( $s['trusted_proxies'] ) ); echo ''; submit_button( '保存する', 'secondary' ); echo '
'; self::postbox_close(); $active = AISFG_Ban::active( 200 ); self::postbox_open( 'ブロック中の接続元(IP)(' . (int) count( $active ) . ' 件)' ); if ( $active ) { echo ''; $ips = array(); foreach ( array_slice( $active, 0, 50 ) as $b ) { $ips[] = $b->ip; echo ''; } echo '
接続元(IP)解除の予定入口でブロックした回数
' . esc_html( $b->ip ) . '' . esc_html( get_date_from_gmt( $b->expires_at, 'n/j H:i' ) ) . '' . esc_html( number_format_i18n( (int) $b->blocked ) ) . ''; self::form_open( 'unban' ); echo '
'; if ( count( $active ) > 50 ) { echo '

多いため、上位 50 件を表示しています。

'; } self::form_open( 'unban_all' ); echo '

'; echo '

コピー用(1 行に 1 つ):サーバーのファイアウォールや Cloudflare に登録すると、WordPress に届く前に止められます。

'; echo ''; } else { echo '

ブロック中の接続元(IP)はありません。

'; } self::postbox_close(); self::postbox_open( '止めた・検知した送信が多い接続元(IP)の範囲(直近 7 日間)' ); $top = AISFG_Report::top_ranges( 7, 10 ); if ( $top ) { echo ''; $ranges = array(); foreach ( $top as $r ) { $ranges[] = AISFG_Report::range_label( $r->ip_masked ); echo ''; } echo '
範囲件数
' . esc_html( AISFG_Report::range_label( $r->ip_masked ) ) . '' . esc_html( number_format_i18n( (int) $r->c ) ) . '
'; echo '

プライバシーのため、記録には IP の末尾を伏せた値を使っています(IPv4 は /24 の範囲)。特定の範囲に集中しているときは、サーバーや Cloudflare で、範囲ごと止める判断材料になります。ただし、同じ範囲を使う本物のお客様も止まるため、慎重に判断してください。

'; echo ''; } else { echo '

まだ記録がありません。

'; } self::postbox_close(); } /** * Print a button-style purchase page link when a URL is configured. * * @param array $s Plugin settings. */ private static function render_purchase_page_button( array $s ) { $url = AISFG_Cloud::purchase_page_link( $s ); if ( '' === $url ) { return; } $label = ( '' !== $s['cloud_key'] ) ? '購入・更新ページを開く' : '購入ページを開く'; echo '

' . esc_html( $label ) . '

'; echo '

' . esc_html( '購入ページはブラウザで開きます。このサイトのサーバーからは通信しません。ページには、API キーから計算した照会用の値とこのサイトのドメインを渡します(キーそのものは渡しません)。' ) . '

'; } /** * Paid-feature bullets shown in the upsell card and when cloud is active. */ private static function render_cloud_feature_list() { echo ''; } /** * Render cloud section. * * @param mixed $s Parameter. */ private static function render_cloud_section( $s ) { $opt = AISFG_Settings::OPTION; $has_credentials = '' !== $s['cloud_key']; $upsell_dismissed = (bool) get_user_meta( get_current_user_id(), 'aisfg_upsell_dismissed', true ); $api_ready = AISFG_Cloud::api_ready(); $lic = null; $configured = AISFG_Cloud::configured( $s ); if ( $configured ) { list( $lic ) = AISFG_Cloud::license( $s, false ); } $show_ticket_guides = $configured && is_array( $lic ); $auto_state = AISFG_Cloud::auto_off_state( $s ); $auto_off = '' !== $auto_state; if ( $auto_off ) { $s['cloud_enabled'] = 0; $configured = false; } $ticket = is_array( $lic ) && isset( $lic['ticket'] ) && is_array( $lic['ticket'] ) ? $lic['ticket'] : array(); $renewal_open = ! empty( $ticket['renewal_open'] ); $addon_open = ! empty( $ticket['addon_open'] ); $next = isset( $ticket['next'] ) && is_array( $ticket['next'] ) ? $ticket['next'] : array(); $remain = isset( $ticket['remaining'] ) ? (int) $ticket['remaining'] : 0; $limit = isset( $ticket['limit'] ) ? (int) $ticket['limit'] : 0; $low_quota = $addon_open && ( 0 === $remain || ( $limit > 0 && ( $remain / $limit ) <= 0.2 ) ); $ticket_state = isset( $ticket['state'] ) ? (string) $ticket['state'] : ''; $show_upsell = ( ! $has_credentials || empty( $s['cloud_enabled'] ) ) && ! $upsell_dismissed && ! $auto_off; $cloud_active = $has_credentials && ! empty( $s['cloud_enabled'] ) && ! $auto_off; if ( ! $api_ready ) { self::postbox_open( '準備中です' ); echo '

クラウド判定は現在準備中です。

'; self::render_cloudtest_result(); self::postbox_close(); return; } if ( $show_upsell ) { self::postbox_open( '無料の防御をすり抜ける営業メールにお悩みの方へ' ); self::render_cloud_feature_list(); if ( ! $has_credentials ) { echo '

購入後、API キーの受け取り用のメールが届きます。表示されたキーをこの画面に貼ってください。

'; } self::render_purchase_page_button( $s ); self::form_open( 'dismiss_upsell' ); echo ''; self::postbox_close(); } self::postbox_open( '契約と接続の状況' ); if ( ! $show_upsell ) { self::render_purchase_page_button( $s ); } if ( $cloud_active ) { self::render_cloud_feature_list(); } $flash = self::cloud_flash_message(); if ( '' !== AISFG_Cloud::key_error_state( $s ) ) { $skip_key_error = self::flash_has_notice( $flash, AISFG_Cloud::KEY_ERROR_NOTICE ) || self::flash_has_notice( $flash, 'API キーが正しくありません' ) || self::flash_has_notice( $flash, AISFG_Cloud::DOMAIN_IN_USE_NOTICE ); if ( ! $skip_key_error ) { echo '
' . esc_html( AISFG_Cloud::KEY_ERROR_NOTICE ) . '
'; } } elseif ( AISFG_Cloud::unbound( $s ) ) { if ( ! self::flash_has_notice( $flash, AISFG_Cloud::UNBOUND_NOTICE ) && ! self::flash_has_notice( $flash, AISFG_Cloud::CONSENT_REQUIRED_NOTICE ) ) { echo '
' . esc_html( AISFG_Cloud::UNBOUND_NOTICE ) . '
'; } } if ( $auto_off ) { echo '
有効期限切れ(または無効)のため営業メール判定を自動で OFF にしました。'; echo '現在は無料版と同じローカル防御で動作しています。'; echo '

' . esc_html( '更新後は『営業メール判定を使う』を ON にして保存してください。' ) . '

'; echo '
'; } if ( $show_ticket_guides ) { if ( $renewal_open ) { echo '

' . esc_html( '更新を受け付けています。期限までに更新すると、残りの回数(回数追加分を含む)は新しい期間に繰り越されます。' ) . '

'; } if ( 'expired' === $ticket_state ) { echo '

' . esc_html( '有効期限が過ぎています。購入すると購入日から新しい1年が始まります(残りの回数は繰り越されません)。' ) . '

'; } if ( $low_quota ) { echo '

' . esc_html( '回数が少なくなっています。購入ページから回数を追加できます。' ) . '

'; } if ( isset( $next['limit'] ) ) { $nstart = 0; if ( ! empty( $next['starts_at'] ) ) { $nstart = (int) $next['starts_at']; } elseif ( ! empty( $next['period_start'] ) ) { $nstart = (int) $next['period_start']; } $next_jst = $nstart > 0 ? AISFG_Cloud::format_jst( $nstart ) : '—'; echo '

' . esc_html( '更新済み:次の期間は ' . $next_jst . ' から始まります。' ) . '

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

営業メール判定は OFF です。営業メール判定サービスとは通信しません。

'; } elseif ( is_array( $lic ) ) { $bound_label = AISFG_Cloud::bound_domain_label( $s, $lic ); $in_use = ( '' !== $bound_label ) ? ( 'このキーは ' . $bound_label . ' で使用中です。' ) : ''; $summary = AISFG_Cloud::format_license_summary( $lic ); if ( '' !== $in_use && ! self::flash_has_notice( $flash, $in_use ) ) { echo '

' . esc_html( $in_use ) . '

'; } if ( ! self::flash_has_notice( $flash, $summary ) ) { echo '

' . esc_html( $summary ) . '

'; } echo '

表示は最大約1時間キャッシュされます。「契約情報を更新」で再取得します。

'; } else { $fallback = AISFG_Cloud::unbound( $s ) ? AISFG_Cloud::UNBOUND_NOTICE : '契約情報を取得できませんでした。'; if ( ! self::flash_has_notice( $flash, $fallback ) && ! self::flash_has_notice( $flash, AISFG_Cloud::CONSENT_REQUIRED_NOTICE ) ) { echo '

' . esc_html( $fallback ) . '

'; } } if ( $configured ) { self::form_open( 'cloud_license_refresh' ); echo ''; } self::render_cloudtest_result(); self::postbox_close(); self::postbox_open( '営業メール判定の設定' ); echo '

営業メールの可能性が高い送信を見つけて、止めずに、件名に目印を付けて送ります。本物の問い合わせが間違って営業と判定されても、メールは届きます。ボット型のスパムは、これまでどおり止めます。

'; echo '

営業メール判定がオンのときは、見守りモードでも判定をサーバーへ送り、AI を使って判定したときだけ回数を消費します。防御モードでは判定結果に応じて件名に目印を付けます。サーバー送信時は最大2,000文字です。

'; echo '
'; settings_fields( 'aisfg_group' ); echo ''; printf( '', esc_attr( $opt ), checked( ! empty( $s['cloud_enabled'] ), true, false ) ); $host = (string) wp_parse_url( home_url(), PHP_URL_HOST ); $needs_consent = AISFG_Cloud::needs_domain_consent( $s ); printf( ''; printf( '', esc_attr( $opt ), checked( $s['review_action'], 'tag', false ), checked( $s['review_action'], 'route', false ) ); printf( '', esc_attr( $opt ), esc_attr( $s['review_tag'] ) ); printf( '', esc_attr( $opt ), esc_attr( $s['review_email'] ) ); printf( '', esc_attr( $opt ), (int) $s['cloud_timeout'] ); printf( '', $needs_consent ? '' : ' hidden', esc_html( 'このキーを ' . $host . ' 専用にします。後から他のサイトに付け替えることはできません。' ) ); echo ''; submit_button( '保存する', 'secondary' ); echo '
'; if ( '' !== $s['cloud_key'] ) { $confirm = 'API キーをこの画面から消します。キーはもう一度表示できないので、控えがない場合は元に戻せません。消しても、このキーはこのサイト(ドメイン)専用のままです。続けますか?'; self::form_open( 'delete_cloud_key' ); echo '

'; } self::postbox_close(); } /** * Latest cloud-settings flash message, if any. * * @return string */ private static function cloud_flash_message() { $res = get_transient( 'aisfg_cloudtest_' . get_current_user_id() ); return ( is_array( $res ) && isset( $res[1] ) ) ? (string) $res[1] : ''; } /** * Whether the save flash already contains this notice. * * @param string $flash Flash text. * @param string $notice Notice to compare. * @return bool */ private static function flash_has_notice( $flash, $notice ) { return '' !== $flash && '' !== $notice && false !== strpos( $flash, $notice ); } /** * Print the latest cloud-test / license flash, if any. */ private static function render_cloudtest_result() { $res = get_transient( 'aisfg_cloudtest_' . get_current_user_id() ); if ( ! is_array( $res ) ) { return; } delete_transient( 'aisfg_cloudtest_' . get_current_user_id() ); printf( '
%s %s
', $res[0] ? 'ok' : 'ng', $res[0] ? '✔' : '✖', esc_html( $res[1] ) ); } /** * Print an environment-check body. Link the settings tab when shown there. * * @param string $text Check body. */ private static function print_env_check_body( $text ) { $needle = '『設定』タブ'; $pos = strpos( $text, $needle ); if ( 'settings' !== self::current_tab() || false === $pos ) { echo esc_html( $text ); return; } echo esc_html( substr( $text, 0, $pos ) ); echo '' . esc_html( $needle ) . ''; echo esc_html( substr( $text, $pos + strlen( $needle ) ) ); } /** * Field. * * @param mixed $s Parameter. * @param mixed $key Parameter. * @param mixed $label Parameter. * @param mixed $desc Parameter. */ private static function field( $s, $key, $label, $desc ) { printf( ' %5$s', esc_attr( $key ), esc_html( $label ), esc_attr( AISFG_Settings::OPTION ), (int) $s[ $key ], esc_html( $desc ) ); } /** * Support info. * * @param mixed $s Settings. * @param mixed $st Stats. * @return string */ private static function support_info( $s, $st ) { global $wp_version; $unknown = '確認できない'; $proxy = ( '' !== $s['trusted_proxies'] ) ? '設定あり' : 'なし'; $lines = array( 'プラグインの版: ' . AISFG_VERSION, 'WordPress の版: ' . $wp_version, 'PHP の版: ' . PHP_VERSION, 'Contact Form 7 の版: ' . ( defined( 'WPCF7_VERSION' ) ? WPCF7_VERSION : '未有効' ), 'サイトのドメイン: ' . (string) wp_parse_url( home_url(), PHP_URL_HOST ), 'モード: ' . self::support_mode_label( $s['mode'] ), '保護レベル: ' . self::support_level_label( $s['level'] ), '自動ブロック: ' . self::support_on_off( $s['ban_enabled'] ) . '(' . (int) $s['ban_threshold'] . '回 / ' . (int) $s['ban_window_min'] . '分 → ' . (int) $s['ban_hours'] . '時間 / 信頼するプロキシ: ' . $proxy . ')', '週次レポート: ' . self::support_on_off( $s['report_enabled'] ), '詳しい設定: 最短' . (int) $s['min_seconds'] . '秒 / ' . (int) $s['rate_limit'] . '回 / ' . (int) $s['rate_window_min'] . '分あたり / リンク' . (int) $s['max_urls'] . '個 / 重複' . (int) $s['dup_ttl_min'] . '分 / 確認情報' . (int) $s['token_ttl_hours'] . '時間', '直近7日: 受付' . (int) $st['total'] . '件 / 通過' . (int) $st['pass'] . '件 / 検知' . (int) $st['detected'] . '件', 'キャッシュ系プラグイン: ' . ( AISFG_Env::cache_plugins() ? implode( ', ', AISFG_Env::cache_plugins() ) : 'なし' ), '営業の疑いの扱い: ' . self::support_review_action_label( $s['review_action'] ), '営業メール判定の待ち時間: ' . (int) $s['cloud_timeout'] . '秒', ); if ( empty( $s['cloud_enabled'] ) ) { $lines[] = '営業メール判定の連携: 連携していません'; } else { $lic = AISFG_Cloud::cached_license( $s ); $ticket = ( is_array( $lic ) && isset( $lic['ticket'] ) && is_array( $lic['ticket'] ) ) ? $lic['ticket'] : null; $lines[] = '営業メール判定の連携: ON'; $lines[] = 'このサイト専用: ' . self::support_domain_lock_label( $s ); $lines[] = 'サイト ID: ' . ( is_array( $lic ) && ! empty( $lic['site_id'] ) ? (string) $lic['site_id'] : $unknown ); $lines[] = 'プラン: ' . self::support_plan_label( $ticket ); if ( is_array( $ticket ) && isset( $ticket['remaining'], $ticket['limit'] ) ) { $lines[] = '残りの回数 / 上限: ' . (int) $ticket['remaining'] . '回 / ' . (int) $ticket['limit'] . '回'; } else { $lines[] = '残りの回数 / 上限: ' . $unknown; } $end = is_array( $ticket ) && ! empty( $ticket['period_end'] ) ? AISFG_Cloud::format_jst( (int) $ticket['period_end'] ) : ''; $lines[] = '有効期限: ' . ( '' !== $end ? $end : $unknown ); $lines[] = '次の契約: ' . self::support_next_contract_label( $ticket ); $lines[] = '営業メール判定サービスへの接続エラー(直近24時間): ' . AISFG_Report::cloud_errors( 24 ) . '件'; $fetched = is_array( $lic ) && ! empty( $lic['fetched_at'] ) ? AISFG_Cloud::format_jst( (int) $lic['fetched_at'] ) : ''; $lines[] = '最後に契約情報を取った日時: ' . ( '' !== $fetched ? $fetched : $unknown ); $lines[] = '接続先: ' . self::support_endpoint_label(); } return implode( "\n", $lines ); } /** * ON / OFF label. * * @param mixed $value Setting value. * @return string */ private static function support_on_off( $value ) { return ! empty( $value ) ? 'ON' : 'OFF'; } /** * Mode label used on the settings screen. * * @param mixed $mode Mode key. * @return string */ private static function support_mode_label( $mode ) { return ( 'enforce' === $mode ) ? '防御モード' : '見守りモード'; } /** * Protection level label used on the settings screen. * * @param mixed $level Level key. * @return string */ private static function support_level_label( $level ) { if ( 'custom' === $level ) { return 'カスタム'; } $presets = AISFG_Settings::presets(); return isset( $presets[ $level ]['label'] ) ? $presets[ $level ]['label'] : '確認できない'; } /** * Review-action label used on the settings screen. No destination email. * * @param mixed $action Review action key. * @return string */ private static function support_review_action_label( $action ) { return ( 'route' === $action ) ? '件名に目印を付けて、下の別の宛先に送る' : '件名に目印を付けて、いつもの宛先に送る'; } /** * API endpoint label (default or custom host only). * * @return string */ private static function support_endpoint_label() { if ( ! AISFG_Cloud::is_custom_api_url() ) { return '標準'; } $host = (string) wp_parse_url( AISFG_Cloud::api_url(), PHP_URL_HOST ); return ( '' !== $host ) ? ( 'カスタム(' . $host . ')' ) : 'カスタム'; } /** * Domain-lock status for support text. No HTTP. * * @param array $s Settings. * @return string */ private static function support_domain_lock_label( array $s ) { if ( 'domain_mismatch' === AISFG_Cloud::key_error_state( $s ) ) { return '他のサイトで使用中'; } if ( AISFG_Cloud::is_locally_bound( $s ) ) { return '設定済み'; } if ( AISFG_Cloud::unbound( $s ) ) { return 'まだ設定していません'; } $lic = AISFG_Cloud::cached_license( $s ); if ( is_array( $lic ) && ! empty( $lic['bound_domain'] ) && ! AISFG_Cloud::domain_matches_site( $lic['bound_domain'] ) ) { return '他のサイトで使用中'; } return '確認できない'; } /** * Plan name used on the purchase page. * * @param array|null $ticket Ticket payload. * @return string */ private static function support_plan_label( $ticket ) { if ( ! is_array( $ticket ) ) { return '確認できない'; } if ( ! empty( $ticket['plan_label'] ) ) { return (string) $ticket['plan_label']; } $map = array( 'lite' => 'ライト', 'standard' => 'スタンダード', 'premium' => 'プレミアム', 'business' => 'プレミアム', ); $plan = isset( $ticket['plan'] ) ? (string) $ticket['plan'] : ''; return isset( $map[ $plan ] ) ? $map[ $plan ] : '確認できない'; } /** * Next-contract line from cached ticket only. * * @param array|null $ticket Ticket payload. * @return string */ private static function support_next_contract_label( $ticket ) { if ( ! is_array( $ticket ) ) { return '確認できない'; } $next = isset( $ticket['next'] ) && is_array( $ticket['next'] ) ? $ticket['next'] : array(); if ( ! isset( $next['limit'] ) ) { return 'なし'; } $start = 0; if ( ! empty( $next['starts_at'] ) ) { $start = (int) $next['starts_at']; } elseif ( ! empty( $next['period_start'] ) ) { $start = (int) $next['period_start']; } $end = ! empty( $next['period_end'] ) ? (int) $next['period_end'] : 0; $start_jst = $start > 0 ? AISFG_Cloud::format_jst( $start ) : '確認できない'; $end_jst = $end > 0 ? AISFG_Cloud::format_jst( $end ) : '確認できない'; return 'あり(' . $start_jst . ' から ' . $end_jst . ')'; } }