resolve_token( $settings ) && '' !== $this->resolve_chat_id( $settings ); } public function process( array $values, array $form, int $entry_id, array $settings, $attempt = 1 ) { $token = $this->resolve_token( $settings ); $chat_id = $this->resolve_chat_id( $settings ); $url = 'https://api.telegram.org/bot' . $token . '/sendMessage'; self::$pending_form_id = $form['id']; self::$pending_context = [ 'values' => $values, 'form' => $form, 'entry_id' => $entry_id, 'settings' => $settings, 'attempt' => $attempt, 'url' => $url, ]; add_action( 'http_api_debug', [ __CLASS__, 'log_response' ], 10, 5 ); wp_remote_post( $url, [ 'timeout' => 8, 'blocking' => false, 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => wp_json_encode( [ 'chat_id' => $chat_id, 'text' => $this->build_message( $form, $values, $settings ), 'parse_mode' => 'HTML', ] ), ] ); remove_action( 'http_api_debug', [ __CLASS__, 'log_response' ], 10 ); self::$pending_form_id = null; self::$pending_context = null; } /** WP-Cron callback — same shape as Slack_Integration::handle_retry(). */ public static function handle_retry( $values, $form, $entry_id, $settings, $attempt ) { ( new self() )->process( $values, $form, $entry_id, $settings, $attempt ); } /** Fire-and-forget response logging + retry — identical mechanics to Slack_Integration::log_response(). */ public static function log_response( $response, $context, $class, $args, $url ) { if ( 'response' !== $context || null === self::$pending_form_id || ! self::$pending_context || $url !== self::$pending_context['url'] ) { return; } $failed = false; if ( is_wp_error( $response ) ) { $message = $response->get_error_message() ?: __( 'The request failed with no further detail.', 'chargeforms' ); Api_Log::log( 'telegram', self::$pending_form_id, self::redact_token( $url ), null, $message ); $failed = true; } else { $code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); // Telegram always responds with a JSON body containing "ok": // true/false, at HTTP 200 even for some errors — so the real // success signal is the decoded "ok" flag, not just the status // code (unlike Slack's plain-text "ok"/Discord's empty 204). $decoded = json_decode( (string) $body, true ); $failed = ! $code || $code >= 400 || ( is_array( $decoded ) && empty( $decoded['ok'] ) ); Api_Log::log( 'telegram', self::$pending_form_id, self::redact_token( $url ), $code ?: null, $body ?: __( 'No response received.', 'chargeforms' ) ); } if ( $failed && self::$pending_context['attempt'] < self::MAX_ATTEMPTS ) { $ctx = self::$pending_context; wp_schedule_single_event( time() + self::RETRY_DELAY_SECONDS, self::RETRY_HOOK, [ $ctx['values'], $ctx['form'], $ctx['entry_id'], $ctx['settings'], $ctx['attempt'] + 1 ] ); } } /** * Real credential leak found live: Telegram's own API shape puts the * bot token directly in the request URL (`api.telegram.org/bot/ * sendMessage`) — unlike every other integration here (Notion, HubSpot, * Google Sheets, Mailchimp), which sends its secret in an Authorization * header that never gets logged. That full URL, token included, was * being written verbatim into wp_chargeforms_api_log and rendered as-is * on Tools > API Logs — the token then persists in that log table (and * anything that reads it: CSV export, DB backups, a support-ticket * screenshot) well beyond its life in Settings, with no masking. */ private static function redact_token( $url ) { return preg_replace( '#/bot[^/]+/#', '/bot***REDACTED***/', $url ); } private function resolve_token( array $settings ) { $token = $settings['bot_token'] ?? Settings::get( 'telegram_default_bot_token' ); return is_string( $token ) ? trim( $token ) : ''; } private function resolve_chat_id( array $settings ) { $chat_id = $settings['chat_id'] ?? Settings::get( 'telegram_default_chat_id' ); return is_string( $chat_id ) || is_numeric( $chat_id ) ? trim( (string) $chat_id ) : ''; } /** * Same template-or-default-listing pattern as Slack_Integration::build_message() — * a custom template may reference `{field_id}` placeholders; without * one, a readable default listing every submitted field is generated. * Uses Telegram's HTML parse_mode (``) for field labels, matching * Slack's `*bold*`/Discord's `**bold**` equivalent emphasis. */ private function build_message( array $form, array $values, array $settings ) { $template = trim( (string) ( $settings['message_template'] ?? '' ) ); if ( '' !== $template ) { return preg_replace_callback( '/\{([a-zA-Z0-9_\-]+)\}/', function ( $m ) use ( $values ) { return isset( $values[ $m[1] ] ) ? (string) ( is_array( $values[ $m[1] ] ) ? wp_json_encode( $values[ $m[1] ] ) : $values[ $m[1] ] ) : $m[0]; }, $template ); } $lines = [ '' . sprintf( /* translators: %s: form title */ __( 'New submission — %s', 'chargeforms' ), esc_html( $form['title'] ?? '' ) ) . '' ]; foreach ( Field_Sanitizer::flatten_submittable_fields( $form['fields'] ) as $field ) { if ( in_array( $field['type'], Field_Sanitizer::NON_INPUT_TYPES, true ) || empty( $values[ $field['id'] ] ) ) { continue; } $value = $values[ $field['id'] ]; $lines[] = sprintf( '%s: %s', esc_html( $field['label'] ?? $field['id'] ), esc_html( is_array( $value ) ? wp_json_encode( $value ) : $value ) ); } return implode( "\n", $lines ); } }