設定'; 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 が有効になっていないため、スパム対策は動作していません。
' . esc_html( '営業メール判定を使うと、問い合わせ本文(メールアドレス・電話番号は伏せ字)を判定に使います。このため営業メール判定をいったん OFF にしました。内容をご確認のうえ、もう一度『営業メール判定を使う』を ON にしてください。' ) . '
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 ''; } 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 件)。この間の営業メールの判定は行っていませんが、問い合わせは通常どおり届いています。『営業メール判定(有料)』タブで『契約情報を更新』を押して確認してください。
' . esc_html( sprintf( '営業メール判定:%s/残り %d 回/有効期限 %s', AISFG_Cloud::ticket_state_label( $state ), $remain, $end_jst ) ) . '
'; } self::postbox_close(); } self::postbox_open( '件数' ); echo '自動でブロック中の接続元(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 '| 日時 | 扱い | 理由 | 件数 |
|---|---|---|---|
| %s | %s | %s | %s |
同じ理由・同じ接続元(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 '| 日付 | グラフ(緑:通常 / 赤:迷惑と思われる送信) | 通常 | 迷惑 |
|---|---|---|---|
| %s | %d | %d |
現在の保護レベル:%s
', esc_html( self::support_level_label( $s['level'] ) ) ); echo '迷惑な送信をどのくらい厳しく見分けるかを選びます。迷ったら「標準」のままで大丈夫です。数値の詳細も、この下で変えられます。
'; 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)の範囲を入力すると動きます。
| 接続元(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 ' |
多いため、上位 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 '| 範囲 | 件数 |
|---|---|
' . esc_html( AISFG_Report::range_label( $r->ip_masked ) ) . ' | ' . esc_html( number_format_i18n( (int) $r->c ) ) . ' |
プライバシーのため、記録には 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 ''; 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 'クラウド判定は現在準備中です。
'; 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( '更新後は『営業メール判定を使う』を ON にして保存してください。' ) . '
'; 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 ''; 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( '