*/ private $transport_debug = array(); /** * Register hooks. */ public function __construct() { $this->textdomain = 'qs-cf7-api'; if ( is_admin() ) { $this->admin_notices = new QS_Admin_notices(); } $this->register_hooks(); } /** * Warn when Contact Form 7 is not active. * * @return void */ public function verify_dependencies() { if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( is_plugin_active( 'contact-form-7/wp-contact-form-7.php' ) || ! $this->admin_notices ) { return; } $this->admin_notices->wp_add_notice( array( 'id' => 'cf7-not-active', 'type' => 'warning', 'notice' => __( 'Contact Form 7 to API requires Contact Form 7 to be installed and active.', 'qs-cf7-api' ), 'dismissable_forever' => false, ) ); } /** * Register Contact Form 7 and admin hooks. * * Property registration runs on every request. Contact Form 7 drops * unknown properties unless they are added while the form object is built, * including on the front end during submit. * * @return void */ public function register_hooks() { add_filter( 'wpcf7_contact_form_properties', array( $this, 'add_sf_properties' ), 10, 2 ); add_filter( 'wpcf7_pre_construct_contact_form_properties', array( $this, 'add_sf_properties' ), 10, 2 ); add_action( 'wpcf7_before_send_mail', array( $this, 'qs_cf7_send_data_to_api' ), 10, 3 ); add_action( 'wpcf7_mail_sent', array( $this, 'apply_success_response' ) ); if ( ! is_admin() ) { return; } add_action( 'admin_init', array( $this, 'verify_dependencies' ) ); add_filter( 'wpcf7_editor_panels', array( $this, 'add_integrations_tab' ), 1, 1 ); add_action( 'wpcf7_save_contact_form', array( $this, 'qs_save_contact_form_details' ), 10, 1 ); } /** * Register the extra form properties Contact Form 7 should keep. * * @param array $properties Form properties. * @param object $contact_form Contact form. * @return array */ public function add_sf_properties( $properties, $contact_form ) { unset( $contact_form ); if ( ! is_array( $properties ) ) { $properties = array(); } if ( ! isset( $properties['wpcf7_api_data'] ) || ! is_array( $properties['wpcf7_api_data'] ) ) { $properties['wpcf7_api_data'] = array(); } if ( ! isset( $properties['wpcf7_api_data_map'] ) || ! is_array( $properties['wpcf7_api_data_map'] ) ) { $properties['wpcf7_api_data_map'] = array(); } $properties['template'] = qs_cf7_api_flatten_template( isset( $properties['template'] ) ? $properties['template'] : '' ); $properties['json_template'] = qs_cf7_api_flatten_template( isset( $properties['json_template'] ) ? $properties['json_template'] : '' ); return $properties; } /** * Add the API Integration tab. * * @param array $panels Editor panels. * @return array */ public function add_integrations_tab( $panels ) { if ( ! is_array( $panels ) ) { $panels = array(); } $panels['qs-cf7-api-integration'] = array( 'title' => __( 'API Integration', 'qs-cf7-api' ), 'callback' => array( $this, 'wpcf7_integrations' ), ); return $panels; } /** * Form-tags that have a name. * * @param object $post Contact form. * @return array */ public function get_mail_tags( $post ) { $mailtags = array(); if ( ! is_object( $post ) || ! method_exists( $post, 'scan_form_tags' ) ) { return $mailtags; } $tags = apply_filters( 'qs_cf7_collect_mail_tags', $post->scan_form_tags() ); if ( ! is_array( $tags ) ) { return $mailtags; } foreach ( $tags as $tag ) { $parts = $this->tag_parts( $tag ); if ( '' === $parts['name'] || '' === $parts['basetype'] ) { continue; } $mailtags[] = $tag; } return $mailtags; } /** * Render the API Integration tab. * * Contact Form 7 calls this with the contact form object. * * @param object $post Contact form. * @return void */ public function wpcf7_integrations( $post ) { $settings = $this->normalized_settings( $post->prop( 'wpcf7_api_data' ) ); $map = $post->prop( 'wpcf7_api_data_map' ); $map = is_array( $map ) ? $map : array(); $xml = qs_cf7_api_flatten_template( $post->prop( 'template' ) ); $json = qs_cf7_api_flatten_template( $post->prop( 'json_template' ) ); $mail_tags = $this->get_mail_tags( $post ); $debug = get_post_meta( $post->id(), '_qs_cf7_api_debug', true ); $debug = is_array( $debug ) ? $debug : array(); $debug_url = isset( $debug['url'] ) ? (string) $debug['url'] : ''; $debug_body = isset( $debug['params'] ) ? (string) $debug['params'] : ''; $debug_code = isset( $debug['code'] ) ? (string) $debug['code'] : ''; $debug_resp = isset( $debug['body'] ) ? (string) $debug['body'] : ''; $debug_err = isset( $debug['error'] ) ? (string) $debug['error'] : ''; $header_names = isset( $debug['header_names'] ) && is_array( $debug['header_names'] ) ? $debug['header_names'] : array(); $xml_placeholder = "*** THIS IS AN EXAMPLE ** USE YOUR XML ACCORDING TO YOUR API DOCUMENTATION **\n\n\n\n\n\n\n\n\n\n\n"; $json_placeholder = "*** THIS IS AN EXAMPLE ** USE YOUR JSON ACCORDING TO YOUR API DOCUMENTATION **\n{ \"name\":\"[your-name]\", \"age\":30, \"accepted\":[acceptance] }"; ?>



tag_parts( $mail_tag ); if ( 'checkbox' === $parts['basetype'] && $parts['values'] ) : foreach ( $parts['values'] as $checkbox_row ) : $saved = ''; if ( isset( $map[ $parts['name'] ] ) && is_array( $map[ $parts['name'] ] ) && isset( $map[ $parts['name'] ][ $checkbox_row ] ) ) { $saved = (string) $map[ $parts['name'] ][ $checkbox_row ]; } $input_id = 'sf-' . $parts['name'] . '-' . $checkbox_row; ?>

tag_parts( $mail_tag ); ?> []

tag_parts( $mail_tag ); if ( 'checkbox' === $parts['basetype'] && $parts['values'] ) : foreach ( $parts['values'] as $checkbox_row ) : ?> [] []

get_properties(); if ( ! is_array( $properties ) ) { $properties = array(); } $settings = $this->sanitize_settings( $this->read_post_array( 'wpcf7-sf' ) ); $map = $this->sanitize_map( $this->read_post_array( 'qs_wpcf7_api_map' ) ); $xml = $this->sanitize_template( $this->read_post_value( 'template' ) ); $json = $this->sanitize_template( $this->read_post_value( 'json_template' ) ); if ( 'json' === $settings['input_type'] || 'xml' === $settings['input_type'] ) { $template = ( 'json' === $settings['input_type'] ) ? $json : $xml; $map = qs_cf7_api_merge_template_map( $map, $template ); } $properties['wpcf7_api_data'] = $settings; $properties['wpcf7_api_data_map'] = $map; $properties['template'] = $xml; $properties['json_template'] = $json; $contact_form->set_properties( $properties ); } /** * Send the submission to the configured API before Contact Form 7 mails it. * * On Contact Form 7 5.2 and newer, setting $abort and a status other than * "init" skips mail() and keeps the status and response we set. A 2xx * response lets mail proceed. * * @param object $contact_form Contact form. * @param bool|null $abort Set true to stop mail. * @param object $submission Submission, when CF7 passes it. * @return void */ public function qs_cf7_send_data_to_api( $contact_form, &$abort = null, $submission = null ) { if ( ! is_object( $contact_form ) || ! method_exists( $contact_form, 'prop' ) ) { return; } $settings = $this->normalized_settings( $contact_form->prop( 'wpcf7_api_data' ) ); if ( 'on' !== $settings['send_to_api'] ) { return; } if ( ! is_object( $submission ) || ! method_exists( $submission, 'get_posted_data' ) ) { $submission = WPCF7_Submission::get_instance(); } if ( ! $submission ) { return; } $this->contact_form = $contact_form; $record = $this->get_record( $submission, $contact_form, $settings ); $record['url'] = $settings['base_url']; $template = $this->raw_template( $contact_form, $settings['input_type'] ); $record = apply_filters( 'cf7api_create_record', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy hook. $record, $submission->get_posted_data(), $contact_form->prop( 'wpcf7_api_data_map' ), $settings['input_type'], $template ); if ( ! is_array( $record ) ) { $record = array( 'url' => $settings['base_url'], 'fields' => array(), ); } do_action( 'qs_cf7_api_before_sent_to_api', $record ); $url = isset( $record['url'] ) ? (string) $record['url'] : ''; $validated = wp_http_validate_url( $url ); $validated = apply_filters( 'qs_cf7_api_validated_url', $validated, $url, $record ); if ( ! is_string( $validated ) || '' === $validated ) { $this->store_debug( $contact_form, array( 'url' => $url, 'params' => $this->preview_value( isset( $record['fields'] ) ? $record['fields'] : '' ), 'code' => '', 'body' => '', 'error' => __( 'The API URL is not valid.', 'qs-cf7-api' ), 'header_names' => array(), ) ); $this->fail_submission( $submission, __( 'The API URL is not valid.', 'qs-cf7-api' ), $abort ); return; } $record['url'] = $validated; $result = $this->send_lead( $record, $settings ); $interpreted = $this->interpret_http_result( $result ); $this->store_debug( $contact_form, $this->last_debug( $interpreted ) ); if ( ! is_wp_error( $result ) ) { do_action( 'qs_cf7_api_after_sent_to_api', $record, $result ); } if ( $interpreted['ok'] ) { if ( 'on' === $settings['override_message'] ) { $message = $this->message_from_body( $interpreted['body'] ); if ( '' !== $message ) { $this->success_response = $message; } } return; } $visitor_message = $this->failure_message( $contact_form, $settings, $interpreted ); $this->fail_submission( $submission, $visitor_message, $abort ); } /** * Replace the success message after Contact Form 7 writes mail_sent_ok. * * @return void */ public function apply_success_response() { if ( '' === $this->success_response ) { return; } $submission = WPCF7_Submission::get_instance(); if ( $submission && method_exists( $submission, 'set_response' ) ) { $submission->set_response( $this->success_response ); } $this->success_response = ''; } /** * Mark the submission failed and skip the Contact Form 7 email. * * @param object $submission Submission. * @param string $message Visitor-facing message. * @param bool|null $abort Abort flag passed by reference. * @return void */ private function fail_submission( $submission, $message, &$abort ) { $abort = true; if ( method_exists( $submission, 'set_status' ) ) { $submission->set_status( 'mail_failed' ); } if ( method_exists( $submission, 'set_response' ) ) { $submission->set_response( $message ); } } /** * Build the record passed to the request and to cf7api_create_record. * * @param object $submission Submission. * @param object $contact_form Contact form. * @param array $settings Normalized settings. * @return array{fields:mixed} */ private function get_record( $submission, $contact_form, $settings ) { $map = $contact_form->prop( 'wpcf7_api_data_map' ); if ( ! is_array( $map ) ) { $map = array(); } $type = $settings['input_type']; if ( 'params' === $type ) { return array( 'fields' => $this->param_fields( $map, $submission, $contact_form ), ); } $template = $this->raw_template( $contact_form, $type ); $tokens = qs_cf7_api_extract_template_tokens( $template ); foreach ( $map as $form_key => $unused ) { unset( $unused ); if ( is_string( $form_key ) && '' !== $form_key ) { $tokens[] = $form_key; } } $tokens = array_values( array_unique( $tokens ) ); $values = array(); foreach ( $tokens as $token ) { $value = $this->resolve_value( $token, $submission, $contact_form ); $filter_key = isset( $map[ $token ] ) ? $map[ $token ] : ''; $values[ $token ] = apply_filters( 'set_record_value', $value, $filter_key ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy hook. } return array( 'fields' => qs_cf7_api_fill_template( $template, $values, $type ), ); } /** * Mapped parameter fields. * * @param array $map Field map. * @param object $submission Submission. * @param object $contact_form Contact form. * @return array */ private function param_fields( $map, $submission, $contact_form ) { $fields = array(); foreach ( $map as $form_key => $api_key ) { if ( ! is_string( $form_key ) || '' === $form_key ) { continue; } if ( is_array( $api_key ) ) { foreach ( $this->posted_selection( $submission, $form_key ) as $option ) { if ( ! is_scalar( $option ) ) { continue; } $option = (string) $option; if ( '' === $option || ! isset( $api_key[ $option ] ) || '' === (string) $api_key[ $option ] ) { continue; } $fields[ (string) $api_key[ $option ] ] = apply_filters( 'set_record_value', $option, $api_key ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy hook. } continue; } if ( ! is_string( $api_key ) || '' === $api_key ) { continue; } $value = $this->resolve_value( $form_key, $submission, $contact_form ); if ( is_bool( $value ) ) { $value = $value ? 'true' : 'false'; } $fields[ $api_key ] = apply_filters( 'set_record_value', $value, $api_key ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy hook. } return $fields; } /** * Resolve one mail-tag or special tag to a PHP value. * * @param string $token Tag name without brackets. * @param object $submission Submission. * @param object $contact_form Contact form. * @return mixed */ private function resolve_value( $token, $submission, $contact_form ) { $uploaded = $submission->uploaded_files(); if ( is_array( $uploaded ) && isset( $uploaded[ $token ] ) ) { return $this->encode_files( $uploaded[ $token ] ); } if ( $this->is_acceptance_field( $contact_form, $token ) ) { $posted = $submission->get_posted_data( $token ); if ( is_array( $posted ) ) { $posted = reset( $posted ); } return ( null === $posted || '' === $posted ) ? false : true; } $all = $submission->get_posted_data(); if ( is_array( $all ) && array_key_exists( $token, $all ) ) { return $this->normalize_posted_value( $all[ $token ] ); } $option = $this->checkbox_option_value( $token, $submission, $contact_form ); if ( null !== $option ) { return $option; } return $this->special_mail_tag( $token ); } /** * Selected values for a field, kept as a list. * * @param object $submission Submission. * @param string $form_key Field name. * @return array */ private function posted_selection( $submission, $form_key ) { $posted = $submission->get_posted_data( $form_key ); if ( null === $posted || '' === $posted ) { return array(); } return array_values( (array) $posted ); } /** * Collapse a one-item array to a scalar and keep real multi-values. * * @param mixed $value Posted value. * @return mixed */ private function normalize_posted_value( $value ) { if ( ! is_array( $value ) ) { return $value; } $items = array(); foreach ( $value as $item ) { if ( null === $item || '' === $item ) { continue; } $items[] = $item; } if ( 1 === count( $items ) ) { return $items[0]; } return $items; } /** * Value of a "[checkbox-Option]" token, or null when the token is not one. * * @param string $token Template token. * @param object $submission Submission. * @param object $contact_form Contact form. * @return string|null Selected option text, empty string when not selected, null when not a checkbox token. */ private function checkbox_option_value( $token, $submission, $contact_form ) { if ( ! is_object( $contact_form ) || ! method_exists( $contact_form, 'scan_form_tags' ) ) { return null; } $tags = $contact_form->scan_form_tags(); if ( ! is_array( $tags ) ) { return null; } foreach ( $tags as $tag ) { $parts = $this->tag_parts( $tag ); if ( 'checkbox' !== $parts['basetype'] || '' === $parts['name'] ) { continue; } $prefix = $parts['name'] . '-'; if ( 0 !== strpos( $token, $prefix ) ) { continue; } $option = substr( $token, strlen( $prefix ) ); $selected = array_map( 'strval', $this->posted_selection( $submission, $parts['name'] ) ); return in_array( $option, $selected, true ) ? $option : ''; } return null; } /** * Resolve a Contact Form 7 special mail-tag such as [_remote_ip]. * * @param string $token Tag name without brackets. * @return string */ private function special_mail_tag( $token ) { if ( ! function_exists( 'wpcf7_mail_replace_tags' ) ) { return ''; } $tag = '[' . $token . ']'; $replaced = wpcf7_mail_replace_tags( $tag, array( 'html' => false, ) ); if ( ! is_string( $replaced ) || $replaced === $tag ) { return ''; } return $replaced; } /** * Base64-encode files Contact Form 7 already accepted. * * @param mixed $paths Paths from uploaded_files(). * @return string[] */ private function encode_files( $paths ) { $encoded = array(); foreach ( (array) $paths as $path ) { if ( ! is_string( $path ) || '' === $path ) { continue; } $real = realpath( $path ); if ( false === $real || ! is_file( $real ) || ! is_readable( $real ) ) { continue; } $size = filesize( $real ); if ( false === $size || $size > 8 * 1024 * 1024 ) { continue; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local temp upload already validated by Contact Form 7. WP_Filesystem is not available on the front-end submit. $contents = file_get_contents( $real ); if ( false === $contents ) { continue; } $encoded[] = base64_encode( $contents ); } return $encoded; } /** * Whether a field is an acceptance field, including required acceptance*. * * @param object $contact_form Contact form. * @param string $field_name Field name. * @return bool */ private function is_acceptance_field( $contact_form, $field_name ) { if ( ! is_object( $contact_form ) || ! method_exists( $contact_form, 'scan_form_tags' ) ) { return false; } $fields = $contact_form->scan_form_tags( array( 'basetype' => 'acceptance', 'name' => $field_name, ) ); return is_array( $fields ) && 1 === count( $fields ); } /** * Perform the HTTP request. * * @param array $record Record with url and fields. * @param array $settings Normalized settings. * @return array|WP_Error */ private function send_lead( $record, $settings ) { global $wp_version; $lead = isset( $record['fields'] ) ? $record['fields'] : ''; $url = (string) $record['url']; $method = $settings['method']; $type = $settings['input_type']; $headers = qs_cf7_api_parse_headers( $settings['headers'] ); $this->transport_debug = array( 'url' => $url, 'params' => $this->preview_value( $lead ), 'header_names' => array(), 'result' => null, ); $auth = qs_cf7_api_auth_header( $settings['basic_auth'], $settings['bearer_auth'] ); if ( '' !== $auth ) { $headers = qs_cf7_api_set_header( $headers, 'Authorization', $auth ); } $body = null; if ( 'GET' === $method && 'params' === $type && is_array( $lead ) ) { $url = qs_cf7_api_append_query( $url, $lead ); $body = null; } elseif ( 'json' === $type ) { $json = $this->parse_json( $lead ); if ( is_wp_error( $json ) ) { return $json; } $body = $json; $headers = qs_cf7_api_set_header( $headers, 'Content-Type', 'application/json; charset=utf-8' ); } elseif ( 'xml' === $type ) { $xml = $this->get_xml( $lead ); if ( is_wp_error( $xml ) ) { return $xml; } $body = $xml->asXML(); $headers = qs_cf7_api_set_header( $headers, 'Content-Type', 'text/xml; charset=utf-8' ); } else { $body = $lead; } $timeout = (int) apply_filters( 'qs_cf7_api_timeout', (int) $settings['timeout'], $record ); if ( $timeout < 1 ) { $timeout = 15; } $args = array( 'timeout' => $timeout, 'redirection' => 5, 'httpversion' => '1.1', 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url(), 'blocking' => true, 'headers' => $headers, 'body' => $body, ); $args = apply_filters( 'qs_cf7_api_get_args', $args ); if ( 'GET' === $method ) { $url = apply_filters( 'qs_cf7_api_get_url', $url, $record ); $result = wp_remote_get( $url, $args ); } else { $url = apply_filters( 'qs_cf7_api_post_url', $url ); $result = wp_remote_post( $url, $args ); } $this->remember_transport( $url, $body, $args, $result ); do_action( 'after_qs_cf7_api_send_lead', $result, $record ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy hook. return $result; } /** * Stash the last transport attempt for the debug meta written by the caller. * * @param string $url Final URL. * @param mixed $body Request body. * @param array $args wp_remote args. * @param array|WP_Error $result Remote result. * @return void */ private function remember_transport( $url, $body, $args, $result ) { $headers = array(); if ( isset( $args['headers'] ) && is_array( $args['headers'] ) ) { $headers = array_keys( $args['headers'] ); } $this->transport_debug = array( 'url' => (string) $url, 'params' => $this->preview_value( $body ), 'header_names' => $headers, 'result' => $result, ); } /** * Validate a JSON template and return the original string. * * @param mixed $lead Template or payload. * @return string|WP_Error */ private function parse_json( $lead ) { if ( is_array( $lead ) ) { $encoded = qs_cf7_api_json_encode( $lead ); if ( 'null' === $encoded && array() !== $lead ) { return new WP_Error( 'json-error', __( 'The JSON payload could not be encoded.', 'qs-cf7-api' ) ); } return $encoded; } $lead = qs_cf7_api_flatten_template( $lead ); json_decode( $lead ); if ( JSON_ERROR_NONE !== json_last_error() ) { return new WP_Error( 'json-error', json_last_error_msg() ); } return $lead; } /** * Parse an XML template. * * @param mixed $lead Template. * @return SimpleXMLElement|WP_Error */ private function get_xml( $lead ) { $lead = qs_cf7_api_flatten_template( $lead ); if ( ! function_exists( 'simplexml_load_string' ) ) { return new WP_Error( 'xml', __( 'SimpleXML is required to send XML.', 'qs-cf7-api' ) ); } if ( PHP_VERSION_ID < 80000 && function_exists( 'libxml_disable_entity_loader' ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated -- External entities stay off on PHP 7.4. @libxml_disable_entity_loader( true ); } libxml_use_internal_errors( true ); $xml = simplexml_load_string( $lead, 'SimpleXMLElement', LIBXML_NONET ); if ( false === $xml ) { return new WP_Error( 'xml', __( 'XML structure is incorrect.', 'qs-cf7-api' ) ); } return $xml; } /** * Normalize a stored settings array for the editor and the sender. * * @param mixed $stored Stored wpcf7_api_data. * @return array */ private function normalized_settings( $stored ) { $defaults = array( 'base_url' => '', 'basic_auth' => '', 'bearer_auth' => '', 'send_to_api' => '', 'override_message' => '', 'input_type' => 'params', 'method' => 'GET', 'timeout' => 15, 'headers' => '', ); if ( ! is_array( $stored ) ) { $stored = array(); } $settings = array_merge( $defaults, $stored ); $settings['input_type'] = $this->allow( $settings['input_type'], array( 'params', 'xml', 'json' ), 'params' ); $settings['method'] = $this->allow( $settings['method'], array( 'GET', 'POST' ), 'GET' ); $settings['timeout'] = qs_cf7_api_sanitize_timeout( $settings['timeout'] ); $settings['base_url'] = is_string( $settings['base_url'] ) ? $settings['base_url'] : ''; $settings['basic_auth'] = is_string( $settings['basic_auth'] ) ? $settings['basic_auth'] : ''; $settings['bearer_auth'] = is_string( $settings['bearer_auth'] ) ? $settings['bearer_auth'] : ''; $settings['headers'] = is_string( $settings['headers'] ) ? $settings['headers'] : ''; $settings['send_to_api'] = ( 'on' === $settings['send_to_api'] ) ? 'on' : ''; $settings['override_message'] = ( 'on' === $settings['override_message'] ) ? 'on' : ''; if ( 'xml' === $settings['input_type'] ) { $settings['method'] = 'POST'; } return $settings; } /** * Sanitize the posted settings block. * * @param array $raw Posted wpcf7-sf values, already unslashed. * @return array */ private function sanitize_settings( $raw ) { $input_type = isset( $raw['input_type'] ) ? (string) $raw['input_type'] : 'params'; $method = isset( $raw['method'] ) ? (string) $raw['method'] : 'GET'; return array( 'send_to_api' => ( isset( $raw['send_to_api'] ) && 'on' === $raw['send_to_api'] ) ? 'on' : '', 'override_message' => ( isset( $raw['override_message'] ) && 'on' === $raw['override_message'] ) ? 'on' : '', 'base_url' => isset( $raw['base_url'] ) ? esc_url_raw( trim( (string) $raw['base_url'] ) ) : '', 'basic_auth' => isset( $raw['basic_auth'] ) ? sanitize_text_field( (string) $raw['basic_auth'] ) : '', 'bearer_auth' => isset( $raw['bearer_auth'] ) ? sanitize_text_field( (string) $raw['bearer_auth'] ) : '', 'input_type' => $this->allow( $input_type, array( 'params', 'xml', 'json' ), 'params' ), 'method' => $this->allow( $method, array( 'GET', 'POST' ), 'GET' ), 'timeout' => qs_cf7_api_sanitize_timeout( isset( $raw['timeout'] ) ? $raw['timeout'] : 15 ), 'headers' => $this->sanitize_header_block( isset( $raw['headers'] ) ? $raw['headers'] : '' ), ); } /** * Sanitize a field map one scalar at a time. * * @param array $map Posted map. * @return array */ private function sanitize_map( $map ) { $clean = array(); foreach ( $map as $key => $value ) { $key = sanitize_text_field( (string) $key ); if ( '' === $key ) { continue; } if ( is_array( $value ) ) { $child = array(); foreach ( $value as $option => $api_key ) { $option = sanitize_text_field( (string) $option ); $api = sanitize_text_field( (string) $api_key ); if ( '' === $option ) { continue; } $child[ $option ] = $api; } $clean[ $key ] = $child; continue; } $clean[ $key ] = sanitize_text_field( (string) $value ); } return $clean; } /** * Keep JSON and XML punctuation. Strip NULs and invalid UTF-8 only. * * @param mixed $value Posted template. * @return string */ private function sanitize_template( $value ) { $value = qs_cf7_api_flatten_template( $value ); $value = wp_check_invalid_utf8( $value ); $value = wp_kses_no_null( $value ); return $value; } /** * Rebuild a header block from the lines that parse cleanly. * * @param mixed $text Posted header text. * @return string */ private function sanitize_header_block( $text ) { $headers = qs_cf7_api_parse_headers( wp_check_invalid_utf8( (string) $text ) ); $lines = array(); foreach ( $headers as $name => $value ) { $lines[] = sanitize_text_field( $name ) . ': ' . sanitize_text_field( $value ); } return implode( "\n", $lines ); } /** * Read one posted array. Contact Form 7 has already verified the save nonce. * * @param string $key Post key. * @return array */ private function read_post_array( $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- wpcf7_save_contact_form runs after Contact Form 7 verifies the editor nonce and capability. if ( ! isset( $_POST[ $key ] ) ) { return array(); } // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Unslash here; callers sanitize each key. $value = wp_unslash( $_POST[ $key ] ); return is_array( $value ) ? $value : array(); } /** * Read one posted scalar. Contact Form 7 has already verified the save nonce. * * @param string $key Post key. * @return string */ private function read_post_value( $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- wpcf7_save_contact_form runs after Contact Form 7 verifies the editor nonce and capability. if ( ! isset( $_POST[ $key ] ) ) { return ''; } // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Unslash here; sanitize_template() cleans the value. $value = wp_unslash( $_POST[ $key ] ); if ( is_array( $value ) ) { return qs_cf7_api_flatten_template( $value ); } return (string) $value; } /** * Template string for the active input type. * * @param object $contact_form Contact form. * @param string $type Input type. * @return string */ private function raw_template( $contact_form, $type ) { $key = ( 'json' === $type ) ? 'json_template' : 'template'; return qs_cf7_api_flatten_template( $contact_form->prop( $key ) ); } /** * Summarize a transport result. * * @param array|WP_Error $result Remote result. * @return array{ok:bool,code:int,body:string,error:string} */ private function interpret_http_result( $result ) { if ( is_wp_error( $result ) ) { return array( 'ok' => false, 'code' => 0, 'body' => '', 'error' => $result->get_error_message(), ); } $code = (int) wp_remote_retrieve_response_code( $result ); $body = wp_remote_retrieve_body( $result ); if ( ! is_string( $body ) ) { $body = ''; } $failed = qs_cf7_api_response_failed( $code, false ); return array( 'ok' => ! $failed, 'code' => $code, 'body' => $body, 'error' => $failed ? 'HTTP ' . $code : '', ); } /** * Visitor message for a failed call. * * @param object $contact_form Contact form. * @param array $settings Settings. * @param array $interpreted Interpreted result. * @return string */ private function failure_message( $contact_form, $settings, $interpreted ) { if ( 'on' === $settings['override_message'] ) { if ( '' !== $interpreted['body'] ) { $from_body = $this->message_from_body( $interpreted['body'] ); if ( '' !== $from_body ) { return $from_body; } } if ( '' !== $interpreted['error'] ) { return $this->trim_visitor_text( $interpreted['error'] ); } } if ( is_object( $contact_form ) && method_exists( $contact_form, 'message' ) ) { return (string) $contact_form->message( 'mail_sent_ng' ); } return __( 'The API request failed.', 'qs-cf7-api' ); } /** * Pull a message string out of a response body. * * @param string $body Response body. * @return string */ private function message_from_body( $body ) { $decoded = json_decode( (string) $body ); if ( JSON_ERROR_NONE === json_last_error() && is_object( $decoded ) && isset( $decoded->message ) && is_string( $decoded->message ) ) { return $this->trim_visitor_text( $decoded->message ); } return $this->trim_visitor_text( $body ); } /** * Limit text shown to the visitor. * * @param string $text Raw text. * @return string */ private function trim_visitor_text( $text ) { $text = wp_strip_all_tags( (string) $text ); if ( strlen( $text ) > 500 ) { $text = substr( $text, 0, 500 ); } return trim( $text ); } /** * Debug row stored for the editor. * * @param array $interpreted Interpreted result. * @return array */ private function last_debug( $interpreted ) { $transport = $this->transport_debug; $code = (string) $interpreted['code']; if ( '0' === $code ) { $code = ''; } return array( 'url' => isset( $transport['url'] ) ? (string) $transport['url'] : '', 'params' => isset( $transport['params'] ) ? (string) $transport['params'] : '', 'code' => $code, 'body' => $this->truncate( $interpreted['body'] ), 'error' => $this->truncate( $interpreted['error'] ), 'header_names' => isset( $transport['header_names'] ) && is_array( $transport['header_names'] ) ? $transport['header_names'] : array(), ); } /** * Persist the last call. Header values are not stored. * * @param object $contact_form Contact form. * @param array $debug Debug row. * @return void */ private function store_debug( $contact_form, $debug ) { if ( ! is_object( $contact_form ) || ! method_exists( $contact_form, 'id' ) ) { return; } $debug['params'] = $this->truncate( isset( $debug['params'] ) ? $debug['params'] : '' ); $debug['body'] = $this->truncate( isset( $debug['body'] ) ? $debug['body'] : '' ); $debug['error'] = $this->truncate( isset( $debug['error'] ) ? $debug['error'] : '' ); update_post_meta( $contact_form->id(), '_qs_cf7_api_debug', $debug ); delete_post_meta( $contact_form->id(), 'api_errors' ); } /** * Short preview of a request body for the debug log. * * @param mixed $value Body. * @return string */ private function preview_value( $value ) { if ( is_array( $value ) ) { $encoded = qs_cf7_api_json_encode( $value ); return $this->truncate( $encoded ); } return $this->truncate( qs_cf7_api_flatten_template( $value ) ); } /** * Cap stored debug text. * * @param mixed $text Text. * @return string */ private function truncate( $text ) { $text = (string) $text; if ( strlen( $text ) > 8192 ) { return substr( $text, 0, 8192 ); } return $text; } /** * Allow-list helper. * * @param mixed $value Candidate. * @param array $allowed Allowed values. * @param string $default Fallback. * @return string */ private function allow( $value, $allowed, $fallback ) { $value = (string) $value; if ( in_array( $value, $allowed, true ) ) { return $value; } return $fallback; } /** * Name, basetype, and values for a form-tag object or array. * * @param mixed $tag Form-tag. * @return array{name:string,basetype:string,values:array} */ private function tag_parts( $tag ) { $name = ''; $basetype = ''; $values = array(); if ( is_object( $tag ) ) { $name = isset( $tag->name ) ? (string) $tag->name : ''; $basetype = isset( $tag->basetype ) ? (string) $tag->basetype : ''; $values = ( isset( $tag->values ) && is_array( $tag->values ) ) ? $tag->values : array(); } elseif ( is_array( $tag ) ) { $name = isset( $tag['name'] ) ? (string) $tag['name'] : ''; $basetype = isset( $tag['basetype'] ) ? (string) $tag['basetype'] : ''; if ( '' === $basetype && isset( $tag['type'] ) ) { $basetype = rtrim( (string) $tag['type'], '*' ); } $values = ( isset( $tag['values'] ) && is_array( $tag['values'] ) ) ? $tag['values'] : array(); } return array( 'name' => trim( $name ), 'basetype' => trim( $basetype ), 'values' => $values, ); } }