$state, 'scope' => self::scope( $s ), 'bound_domain' => 'bound' === $state ? self::normalize_domain( $bound_domain ) : '', ), false ); } /** * Update persisted activation from a licenses / activations payload. * * @param array $s Plugin settings. * @param array $d License payload. */ private static function sync_activation_from_license( array $s, array $d ) { $bound = isset( $d['bound_domain'] ) ? (string) $d['bound_domain'] : ''; if ( self::domain_matches_site( $bound ) ) { self::mark_activation( $s, 'bound', $bound ); return; } $state = isset( $d['ticket']['state'] ) ? (string) $d['ticket']['state'] : ''; if ( '' === self::normalize_domain( $bound ) || 'unbound' === $state ) { self::mark_activation( $s, 'unbound' ); } } /* * ---------- クラウド判定の一時停止(expired / inactive)---------- * 状態はサーバーが判断する。ここでは、サーバーが返した状態(licenses の ticket.state、 * evaluations の ai_status)だけを覚える。案内文の整理は多言語対応時に行う。 */ /** * Scope. * * @param array $s Parameter. */ private static function scope( array $s ) { return md5( self::api_url() . "\0" . (string) $s['cloud_key'] . "\0" . home_url() ); } /** * Is pause state. * * @param mixed $state Parameter. */ private static function is_pause_state( $state ) { return in_array( (string) $state, array( 'expired', 'inactive' ), true ); } /** * Paused state. * * @param array $s Parameter. */ private static function paused_state( array $s ) { $st = get_option( self::STATE_OPTION ); if ( is_array( $st ) && isset( $st['state'], $st['scope'] ) && self::is_pause_state( $st['state'] ) && self::scope( $s ) === $st['scope'] ) { return (string) $st['state']; } return self::migrate_paused_from_cache( $s ); } /** * Is expired. * * @param array $s Parameter. */ public static function is_expired( array $s ) { return 'expired' === self::paused_state( $s ); } /** * Migrate paused from cache. * * @param array $s Parameter. */ private static function migrate_paused_from_cache( array $s ) { $cached = get_transient( self::license_cache_key( $s ) ); if ( ! is_array( $cached ) || ! isset( $cached['ticket']['state'] ) || ! self::is_pause_state( $cached['ticket']['state'] ) ) { return ''; } $state = (string) $cached['ticket']['state']; self::mark_paused( $s, $state ); self::auto_disable( $s, $state ); return $state; } /** * Paused. * * @param array $s Parameter. */ public static function paused( array $s ) { return '' !== self::paused_state( $s ); } /** * Mark paused. * * @param array $s Parameter. * @param mixed $state Parameter. */ private static function mark_paused( array $s, $state ) { if ( ! self::is_pause_state( $state ) ) { return; } update_option( self::STATE_OPTION, array( 'state' => (string) $state, 'scope' => self::scope( $s ), 'checked_at' => time(), ), false ); } /** * Turn cloud_enabled off without recording auto-off (key/domain errors). */ private static function force_disable() { $current = AISFG_Settings::get(); if ( empty( $current['cloud_enabled'] ) ) { return; } self::$updating_settings = true; try { update_option( AISFG_Settings::OPTION, array_merge( $current, array( 'cloud_enabled' => 0 ) ) ); } finally { self::$updating_settings = false; } } /** * Error codes that mean the key or site is not allowed (not transient). * * @param mixed $code Parameter. */ private static function is_key_error_code( $code ) { return in_array( (string) $code, array( 'invalid_api_key', 'domain_mismatch' ), true ); } /** * Record a key/domain error, then turn the integration off. * * @param array $s Plugin settings. * @param string $code Error code from the server. */ private static function record_key_error( array $s, $code ) { if ( ! self::is_key_error_code( $code ) ) { return; } update_option( self::KEY_ERROR_OPTION, array( 'code' => (string) $code, 'scope' => self::scope( $s ), ), false ); self::force_disable(); } /** * Key/domain error code recorded for this site, or empty. * * @param array $s Plugin settings. * @return string */ public static function key_error_state( array $s ) { $st = get_option( self::KEY_ERROR_OPTION ); if ( ! is_array( $st ) || ! isset( $st['code'], $st['scope'] ) || self::scope( $s ) !== $st['scope'] ) { return ''; } return self::is_key_error_code( $st['code'] ) ? (string) $st['code'] : ''; } /** * Forget a recorded key/domain error. */ private static function clear_key_error() { delete_option( self::KEY_ERROR_OPTION ); } /** * If the cached ticket period has already ended, recheck licenses/current * (at most once per hour) before sending an evaluation. Auto-off only when * the fresh ticket is expired or inactive. * * @param array $s Plugin settings. * @return bool True when sending must stop for this request. */ private static function disable_if_period_ended( array $s ) { $current = AISFG_Settings::get(); if ( empty( $current['cloud_enabled'] ) ) { return false; } $cache_key = self::license_cache_key( $s ); $cached = get_transient( $cache_key ); if ( ! is_array( $cached ) || ! isset( $cached['ticket'] ) || ! is_array( $cached['ticket'] ) ) { return false; } $end = isset( $cached['ticket']['period_end'] ) ? (int) $cached['ticket']['period_end'] : 0; if ( $end <= 0 || $end >= time() ) { return false; } $next = isset( $cached['ticket']['next'] ) && is_array( $cached['ticket']['next'] ) ? $cached['ticket']['next'] : array(); $next_end = isset( $next['period_end'] ) ? (int) $next['period_end'] : 0; if ( $next_end > time() ) { delete_transient( $cache_key ); return false; } $recheck_key = 'aisfg_period_recheck_' . md5( self::scope( $s ) ); if ( false !== get_transient( $recheck_key ) ) { delete_transient( $cache_key ); return true; } set_transient( $recheck_key, 1, HOUR_IN_SECONDS ); list( $d, $err, $err_code ) = self::license( $s, true ); unset( $err, $err_code ); if ( ! is_array( $d ) || ! isset( $d['ticket'] ) || ! is_array( $d['ticket'] ) ) { delete_transient( $cache_key ); return true; } $state = isset( $d['ticket']['state'] ) ? (string) $d['ticket']['state'] : ''; if ( self::is_pause_state( $state ) ) { self::mark_paused( $s, $state ); self::auto_disable( $s, $state ); return true; } $fresh_end = isset( $d['ticket']['period_end'] ) ? (int) $d['ticket']['period_end'] : 0; $fresh_next = isset( $d['ticket']['next'] ) && is_array( $d['ticket']['next'] ) ? $d['ticket']['next'] : array(); $fresh_next_end = isset( $fresh_next['period_end'] ) ? (int) $fresh_next['period_end'] : 0; if ( $fresh_end > time() || $fresh_next_end > time() ) { return false; } delete_transient( $cache_key ); return true; } /** * Auto disable for expired/inactive only, and record aisfg_cloud_auto_off. * * @param array $s Plugin settings. * @param string $state Ticket or ai_status (expired|inactive). */ private static function auto_disable( array $s, $state ) { if ( ! self::is_pause_state( $state ) ) { return; } $current = AISFG_Settings::get(); if ( empty( $current['cloud_enabled'] ) ) { return; } self::force_disable(); update_option( self::AUTO_OFF_OPTION, array( 'state' => (string) $state, 'scope' => self::scope( $s ), 'at' => time(), ), false ); } /** * Auto off state. * * @param array $s Parameter. */ public static function auto_off_state( array $s ) { $auto_off = get_option( self::AUTO_OFF_OPTION ); if ( ! is_array( $auto_off ) || ! isset( $auto_off['state'], $auto_off['scope'] ) || self::scope( $s ) !== $auto_off['scope'] ) { return ''; } return (string) $auto_off['state']; } /** * Resume. * * @param mixed $only_auto_off Parameter. */ public static function resume( $only_auto_off = false ) { $current = AISFG_Settings::get(); if ( $only_auto_off && '' === self::auto_off_state( $current ) ) { return false; } if ( empty( $current['cloud_enabled'] ) ) { self::$updating_settings = true; try { update_option( AISFG_Settings::OPTION, array_merge( $current, array( 'cloud_enabled' => 1 ) ) ); } finally { self::$updating_settings = false; } } delete_option( self::AUTO_OFF_OPTION ); delete_option( self::STATE_OPTION ); self::clear_key_error(); return true; } /** * Remember. * * @param array $s Parameter. * @param array $d Parameter. */ private static function remember( array $s, array $d ) { self::sync_activation_from_license( $s, $d ); if ( isset( $d['ticket']['state'] ) && self::is_pause_state( $d['ticket']['state'] ) ) { self::mark_paused( $s, (string) $d['ticket']['state'] ); self::auto_disable( $s, (string) $d['ticket']['state'] ); } else { delete_option( self::STATE_OPTION ); } } /** * Cache ttl. */ private static function cache_ttl() { return self::LICENSE_CACHE_TTL; } /** * Mask text. * * @param mixed $text Parameter. * @param mixed $max Parameter. */ public static function mask_text( $text, $max = 2000 ) { $t = (string) $text; $t = preg_replace( '/[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/u', '[メール]', $t ); $t = preg_replace( '/\+\d{1,3}[-\s]?\d{1,4}[-\s]?\d{1,4}[-\s]?\d{3,4}/u', '[電話]', $t ); $t = preg_replace( '/(? max( 1, (int) $s['cloud_timeout'] ), 'redirection' => 0, 'user-agent' => 'AIS-FormGuard/' . AISFG_VERSION, 'headers' => array( 'X-API-Key' => $s['cloud_key'], 'Content-Type' => 'application/json', 'Accept' => 'application/json', ), ); $url = self::api_url() . $path; if ( ! empty( $query ) ) { $url = add_query_arg( $query, $url ); } if ( null !== $body ) { $args['body'] = wp_json_encode( $body ); } $res = ( 'GET' === $method ) ? wp_remote_get( $url, $args ) : wp_remote_post( $url, $args ); if ( is_wp_error( $res ) ) { return array( null, $res->get_error_message(), 'network_error', array() ); } $code = (int) wp_remote_retrieve_response_code( $res ); $data = json_decode( wp_remote_retrieve_body( $res ), true ); if ( $code < 200 || $code >= 300 || ! is_array( $data ) ) { $err_code = ''; $err_msg = 'HTTP ' . $code; $details = array(); if ( is_array( $data ) && isset( $data['error'] ) && is_array( $data['error'] ) ) { $err_code = isset( $data['error']['code'] ) ? (string) $data['error']['code'] : ''; if ( ! empty( $data['error']['message'] ) ) { $err_msg = (string) $data['error']['message']; } if ( isset( $data['error']['details'] ) && is_array( $data['error']['details'] ) ) { $details = $data['error']['details']; } } if ( '' === $err_code ) { $err_code = 'http_error'; } return array( null, $err_msg, $err_code, $details ); } return array( $data, '', '', array() ); } /** * Evaluate. * * @param array $s Parameter. * @param array $ctx Parameter. */ public static function evaluate( array $s, array $ctx ) { if ( self::unbound( $s ) ) { return null; } if ( '' !== self::key_error_state( $s ) ) { return null; } $current = AISFG_Settings::get(); if ( empty( $current['cloud_enabled'] ) ) { return null; } if ( self::disable_if_period_ended( $s ) ) { return null; } $body = array( 'request_id' => $ctx['request_id'], 'site_url' => home_url(), 'form_id' => (int) $ctx['form_id'], 'ip_masked' => (string) $ctx['ip_masked'], 'signals' => $ctx['signals'], 'content' => array( 'message' => self::mask_text( $ctx['text'] ) ), ); list( $d, $err, $err_code ) = self::request( $s, 'POST', '/v1/evaluations', $body ); unset( $err ); if ( 'forbidden' === $err_code ) { self::mark_activation( $s, 'unbound' ); return null; } if ( self::is_key_error_code( $err_code ) ) { self::record_key_error( $s, $err_code ); return null; } if ( ! is_array( $d ) || ! isset( $d['decision'] ) || ! in_array( $d['decision'], array( 'pass', 'review', 'block' ), true ) ) { return null; } if ( isset( $d['ai_status'] ) && self::is_pause_state( $d['ai_status'] ) ) { self::mark_paused( $s, (string) $d['ai_status'] ); self::auto_disable( $s, (string) $d['ai_status'] ); delete_transient( self::license_cache_key( $s ) ); } $reasons = array(); if ( isset( $d['reason_codes'] ) && is_array( $d['reason_codes'] ) ) { foreach ( $d['reason_codes'] as $c ) { if ( is_string( $c ) && preg_match( '/^[A-Z_]{1,40}$/', $c ) ) { $reasons[] = $c; } } } return array( 'decision' => $d['decision'], 'reasons' => $reasons, 'score' => isset( $d['score'] ) ? (int) $d['score'] : 0, 'ai_status' => isset( $d['ai_status'] ) ? (string) $d['ai_status'] : '', ); } /** * License cache key. * * @param array $s Parameter. */ public static function license_cache_key( array $s ) { $raw = self::api_url() . "\0" . (string) $s['cloud_key'] . "\0" . home_url(); return self::LICENSE_TRANSIENT . '_' . md5( $raw ); } /** * Cached license payload for this key and site. No HTTP. * * @param array $s Plugin settings. * @return array|null */ public static function cached_license( array $s ) { $cached = get_transient( self::license_cache_key( $s ) ); return is_array( $cached ) ? $cached : null; } /** * Store license payload with a local fetched_at timestamp. * * @param array $s Plugin settings. * @param array $d License payload. */ private static function store_license_cache( array $s, array $d ) { $d['fetched_at'] = time(); set_transient( self::license_cache_key( $s ), $d, self::cache_ttl() ); } /** * Forget local key state without calling the sales-mail service. * * @param array $s Plugin settings that still contain the current key. */ public static function forget_local_key( array $s ) { self::clear_license_cache( $s ); self::clear_period_recheck( $s ); delete_option( self::ACTIVATION_OPTION ); self::clear_key_error(); } /** * Delete period-recheck transients for this key (and any leftover prefix keys). * * @param array $s Plugin settings. */ public static function clear_period_recheck( array $s ) { delete_transient( 'aisfg_period_recheck_' . md5( self::scope( $s ) ) ); global $wpdb; if ( ! isset( $wpdb ) || ! isset( $wpdb->options ) ) { return; } foreach ( array( '_transient_aisfg_period_recheck_', '_transient_timeout_aisfg_period_recheck_' ) as $prefix ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- delete period-recheck transients by prefix when clearing a key. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( $prefix ) . '%' ) ); } } /** * License. * * @param array $s Parameter. * @param mixed $force Parameter. */ public static function license( array $s, $force = false ) { $key = self::license_cache_key( $s ); if ( ! $force ) { $cached = get_transient( $key ); if ( is_array( $cached ) && isset( $cached['site_id'] ) ) { self::remember( $s, $cached ); return array( $cached, '', '' ); } } list( $d, $err, $err_code ) = self::request( $s, 'GET', '/v1/licenses/current', null, array( 'site_url' => home_url() ) ); if ( ! is_array( $d ) || empty( $d['site_id'] ) || ! isset( $d['ticket'] ) || ! is_array( $d['ticket'] ) ) { if ( 'forbidden' === $err_code ) { self::mark_activation( $s, 'unbound' ); } elseif ( self::is_key_error_code( $err_code ) ) { self::record_key_error( $s, $err_code ); } return array( null, $err ? $err : 'invalid license response', $err_code ? $err_code : 'invalid_response' ); } self::clear_key_error(); self::remember( $s, $d ); self::store_license_cache( $s, $d ); return array( $d, '', '' ); } /** * When settings are saved unchanged, WordPress skips update_option_ hooks. * pre_update_option still runs, so consent-only save can activate. * * @param mixed $value New settings. * @param mixed $old_value Current settings. * @return mixed */ public static function bind_on_unchanged_save( $value, $old_value ) { if ( $value !== $old_value || ! is_array( $value ) ) { return $value; } $s = wp_parse_args( $value, AISFG_Settings::defaults() ); if ( empty( $s['cloud_enabled'] ) || '' === (string) $s['cloud_key'] ) { return $value; } if ( self::is_locally_bound( $s ) || ! self::posted_domain_consent() ) { return $value; } self::bind_or_refuse_on_save( $s ); return $value; } /** * Settings updated. * * @param mixed $old_value Parameter. * @param mixed $new_value Parameter. */ public static function settings_updated( $old_value, $new_value ) { if ( self::$updating_settings || ! is_array( $old_value ) || ! is_array( $new_value ) ) { return; } $new_value = wp_parse_args( $new_value, AISFG_Settings::defaults() ); if ( ! empty( $new_value['cloud_enabled'] ) ) { delete_option( 'aisfg_body_consent_notice' ); } if ( is_array( get_option( self::AUTO_OFF_OPTION ) ) && '' === self::auto_off_state( $new_value ) ) { delete_option( self::AUTO_OFF_OPTION ); } if ( is_array( get_option( self::KEY_ERROR_OPTION ) ) && '' === self::key_error_state( $new_value ) ) { self::clear_key_error(); } if ( empty( $new_value['cloud_enabled'] ) ) { return; } if ( empty( $new_value['cloud_key'] ) ) { return; } $old_on = ! empty( $old_value['cloud_enabled'] ); $old_key = isset( $old_value['cloud_key'] ) ? (string) $old_value['cloud_key'] : ''; if ( $old_on && $old_key === (string) $new_value['cloud_key'] && ( self::is_locally_bound( $new_value ) || ! self::posted_domain_consent() ) ) { return; } if ( ! self::api_ready() ) { self::force_disable(); self::flash( false, '接続先が未設定のため有効にできません。' ); return; } $was_off = empty( $old_value['cloud_enabled'] ); $locally_bound = self::is_locally_bound( $new_value ); if ( $locally_bound ) { if ( ! $was_off ) { return; } self::apply_license_on_enable( $new_value ); return; } self::bind_or_refuse_on_save( $new_value ); } /** * OFF→ON when this key is already bound locally: refresh licenses only. * * @param array $s Plugin settings. */ private static function apply_license_on_enable( array $s ) { list( $d, $err, $err_code ) = self::license( $s, true ); self::finish_license_enable( $s, $d, $err, $err_code, true ); } /** * ON save when local bind state is unknown or unbound: probe licenses, then * activate only if the key is not yet bound and the administrator consented. * * @param array $s Plugin settings. */ private static function bind_or_refuse_on_save( array $s ) { list( $d, $err, $err_code ) = self::license( $s, true ); if ( is_array( $d ) ) { if ( self::domain_matches_site( isset( $d['bound_domain'] ) ? $d['bound_domain'] : '' ) ) { self::finish_license_enable( $s, $d, '', '', false ); return; } if ( self::posted_domain_consent() ) { self::activate_then_refresh( $s ); return; } self::force_disable(); self::flash( false, self::CONSENT_REQUIRED_NOTICE ); return; } if ( 'forbidden' === $err_code ) { if ( self::posted_domain_consent() ) { self::activate_then_refresh( $s ); return; } self::force_disable(); self::flash( false, self::CONSENT_REQUIRED_NOTICE ); return; } self::finish_license_enable( $s, null, $err, $err_code, true ); } /** * Apply the licenses/current result after enable or probe. * * @param array $s Plugin settings. * @param array|null $d License payload. * @param string $err Error message. * @param string $err_code Error code. * @param bool $disable_on_client_error Whether to turn off on client errors. */ private static function finish_license_enable( array $s, $d, $err, $err_code, $disable_on_client_error ) { unset( $s ); $client_errors = array( 'invalid_api_key', 'domain_mismatch', 'not_found', 'invalid_request', 'invalid_response' ); if ( ! is_array( $d ) && ! in_array( $err_code, $client_errors, true ) ) { self::flash( false, '契約情報を確認できませんでしたが、営業メール判定は ON のまま保存しました(' . $err . ')。' ); return; } $state = is_array( $d ) && isset( $d['ticket']['state'] ) ? (string) $d['ticket']['state'] : ''; if ( self::is_pause_state( $state ) ) { self::flash( false, '有効な契約がないため営業メール判定を OFF に戻しました。プラン購入、または製品ページ(' . self::product_page_url() . ')をご確認ください。' ); return; } if ( in_array( $err_code, $client_errors, true ) ) { if ( $disable_on_client_error ) { self::force_disable(); } self::flash( false, self::enable_error_message( $err_code ) ); return; } delete_option( self::AUTO_OFF_OPTION ); } /** * POST activations, then refresh licenses/current. On failure keep cloud off. * * @param array $s Plugin settings. */ private static function activate_then_refresh( array $s ) { list( $ok, $message ) = self::test( $s ); if ( ! $ok ) { self::force_disable(); self::flash( false, $message ); return; } list( $d, $err, $err_code ) = self::license( $s, true ); if ( is_array( $d ) ) { $state = isset( $d['ticket']['state'] ) ? (string) $d['ticket']['state'] : ''; if ( self::is_pause_state( $state ) ) { self::auto_disable( $s, $state ); self::flash( false, '有効な契約がないため営業メール判定を OFF に戻しました。プラン購入、または製品ページ(' . self::product_page_url() . ')をご確認ください。' ); return; } delete_option( self::AUTO_OFF_OPTION ); self::flash( true, self::format_license_summary( $d ) ); return; } self::flash( false, '契約情報を確認できませんでしたが、営業メール判定は ON のまま保存しました(' . $err . ')。' ); unset( $err_code ); } /** * Whether the settings form posted domain-bind consent. * * @return bool */ private static function posted_domain_consent() { // Settings API verifies the options nonce before update_option. $confirm = isset( $_POST['aisfg_domain_confirm'] ) ? sanitize_text_field( wp_unslash( $_POST['aisfg_domain_confirm'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing return '1' === $confirm; } /** * Store a cloud-settings flash for the current user. * * @param bool $ok Success. * @param string $message Message. */ private static function flash( $ok, $message ) { set_transient( 'aisfg_cloudtest_' . get_current_user_id(), array( (bool) $ok, (string) $message ), 600 ); } /** * User-facing message for a licenses / activations client error on save. * * @param string $err_code Error code. * @return string */ private static function enable_error_message( $err_code ) { $messages = array( 'invalid_api_key' => 'API キーが正しくありません。営業メール判定を OFF に戻しました。', 'domain_mismatch' => self::DOMAIN_IN_USE_NOTICE, 'not_found' => '契約情報が見つかりませんでした。営業メール判定を OFF に戻しました。', 'invalid_request' => 'リクエストの内容が正しくありません。営業メール判定を OFF に戻しました。', 'invalid_response' => '契約情報の返事を読み取れませんでした。営業メール判定を OFF に戻しました。', ); return isset( $messages[ $err_code ] ) ? $messages[ $err_code ] : '営業メール判定を OFF に戻しました。'; } /** * Clear license cache. * * @param mixed $s Parameter. */ public static function clear_license_cache( $s = null ) { if ( is_array( $s ) ) { delete_transient( self::license_cache_key( $s ) ); } delete_transient( self::LICENSE_TRANSIENT ); } /** * Test. * * @param array $s Parameter. */ public static function test( array $s ) { if ( '' === $s['cloud_key'] ) { return array( false, 'API キーが未入力です。' ); } global $wp_version; list( $d, $err, $err_code ) = self::request( $s, 'POST', '/v1/sites/activations', array( 'site_url' => home_url(), 'plugin_version' => AISFG_VERSION, 'wp_version' => isset( $wp_version ) ? $wp_version : '', ) ); if ( 'domain_mismatch' === $err_code ) { return array( false, self::DOMAIN_IN_USE_NOTICE ); } if ( 'invalid_api_key' === $err_code ) { return array( false, 'API キーが正しくありません。営業メール判定を OFF に戻しました。' ); } if ( ! is_array( $d ) || empty( $d['site_id'] ) ) { return array( false, '接続できませんでした(' . $err . ')。API キーを確認してください。サーバー側の設定や、お使いのサーバーの外向き通信(HTTPS)の制限が原因の場合もあります。', ); } self::clear_license_cache( $s ); self::clear_key_error(); self::remember( $s, $d ); self::store_license_cache( $s, $d ); return array( true, self::format_license_summary( $d ) ); } /** * Format a unix timestamp in JST for display. * * @param int $timestamp Unix timestamp. * @return string Empty when timestamp is not positive. */ public static function format_jst( $timestamp ) { $ts = (int) $timestamp; if ( $ts <= 0 ) { return ''; } return wp_date( 'Y年n月j日 H:i', $ts, new DateTimeZone( 'Asia/Tokyo' ) ) . ' JST'; } /** * Human-readable ticket state. * * @param string $state Ticket state. * @return string */ public static function ticket_state_label( $state ) { $labels = array( 'active' => '有効', 'exhausted' => '回数切れ', 'expired' => '期限切れ', 'inactive' => '無効/未購入', 'unbound' => '未有効化', 'site_stopped' => '停止', 'customer_stopped' => '停止', ); $state = (string) $state; return isset( $labels[ $state ] ) ? $labels[ $state ] : $state; } /** * Format license summary. * * @param array $d Parameter. */ public static function format_license_summary( array $d ) { $bound = isset( $d['bound_domain'] ) ? (string) $d['bound_domain'] : ''; $ticket = isset( $d['ticket'] ) && is_array( $d['ticket'] ) ? $d['ticket'] : array(); $state = isset( $ticket['state'] ) ? (string) $ticket['state'] : 'inactive'; $remain = isset( $ticket['remaining'] ) ? (int) $ticket['remaining'] : 0; $limit = isset( $ticket['limit'] ) ? (int) $ticket['limit'] : 0; $end = isset( $ticket['period_end'] ) ? (int) $ticket['period_end'] : 0; $end_jst = $end > 0 ? self::format_jst( $end ) : '—'; $plan = ''; if ( ! empty( $ticket['plan_label'] ) ) { $plan = (string) $ticket['plan_label']; } elseif ( ! empty( $ticket['plan'] ) ) { $plan = (string) $ticket['plan']; } if ( in_array( $state, array( 'expired', 'inactive' ), true ) ) { $ai = 'クラウド判定:停止中(無料のローカル防御のみ)'; } else { $ai = ! empty( $d['llm_available'] ) ? 'AI 判定:利用できます' : 'AI 判定:利用できません(辞書ルールのみ)'; } $state_label = self::ticket_state_label( $state ); $parts = array( sprintf( 'サイト ID:%s', isset( $d['site_id'] ) ? $d['site_id'] : '' ), sprintf( '固定ドメイン:%s', $bound ? $bound : '(未固定)' ), ); if ( '' !== $plan ) { $parts[] = sprintf( '%s(%s)', $plan, $state_label ); } else { $parts[] = $state_label; } $parts[] = sprintf( '残り %d 回 / 上限 %d 回', $remain, $limit ); $parts[] = sprintf( '有効期限:%s', $end_jst ); $parts[] = $ai; $summary = implode( ' / ', $parts ); $next = isset( $ticket['next'] ) && is_array( $ticket['next'] ) ? $ticket['next'] : array(); if ( isset( $next['limit'] ) ) { $nplan = ''; if ( ! empty( $next['plan_label'] ) ) { $nplan = (string) $next['plan_label']; } elseif ( ! empty( $next['plan'] ) ) { $nplan = (string) $next['plan']; } $nstart = 0; if ( ! empty( $next['starts_at'] ) ) { $nstart = (int) $next['starts_at']; } elseif ( ! empty( $next['period_start'] ) ) { $nstart = (int) $next['period_start']; } $nstr = $nstart > 0 ? self::format_jst( $nstart ) : '—'; $ntotal = isset( $ticket['next_total'] ) ? (int) $ticket['next_total'] : 0; $summary = $summary . sprintf( ' / 次の期間:%s(%d回)%s から。繰り越しを含む合計の見込み %d回(今の期限までに使った分だけ減ります)', $nplan ? $nplan : '—', (int) $next['limit'], $nstr, $ntotal ); } return $summary; } }