From daa737be3c35736f3386450133c55f6b32866273 Mon Sep 17 00:00:00 2001 From: Enej Bajgoric Date: Tue, 8 Jul 2025 15:01:44 -0700 Subject: [PATCH 01/48] Forms: Adds a new Form_Responses class --- .../changelog/update-form-response-storage | 4 + .../src/contact-form/class-contact-form.php | 12 +- .../src/contact-form/class-form-response.php | 806 ++++++++++++ .../src/contact-form/class-response-field.php | 157 +++ .../php/contact-form/Contact_Form_Test.php | 14 +- .../php/contact-form/Form_Response_Test.php | 1130 +++++++++++++++++ .../php/contact-form/Response_Field_Test.php | 110 ++ .../tests/php/contact-form/class-utility.php | 137 ++ 8 files changed, 2360 insertions(+), 10 deletions(-) create mode 100644 projects/packages/forms/changelog/update-form-response-storage create mode 100644 projects/packages/forms/src/contact-form/class-form-response.php create mode 100644 projects/packages/forms/src/contact-form/class-response-field.php create mode 100644 projects/packages/forms/tests/php/contact-form/Form_Response_Test.php create mode 100644 projects/packages/forms/tests/php/contact-form/Response_Field_Test.php create mode 100644 projects/packages/forms/tests/php/contact-form/class-utility.php diff --git a/projects/packages/forms/changelog/update-form-response-storage b/projects/packages/forms/changelog/update-form-response-storage new file mode 100644 index 0000000000000..bc56c7d94b590 --- /dev/null +++ b/projects/packages/forms/changelog/update-form-response-storage @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Forms: Adds a new Form_Responses class that keeps consistency between DB form response and the one before we submit it diff --git a/projects/packages/forms/src/contact-form/class-contact-form.php b/projects/packages/forms/src/contact-form/class-contact-form.php index 6a43e0e60fa08..ca3211cd003e8 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form.php +++ b/projects/packages/forms/src/contact-form/class-contact-form.php @@ -330,6 +330,14 @@ public static function get_default_subject( $attributes ) { return $default_subject; } + /** + * Get the forms processed count. + * + * @return int + */ + public static function get_forms_count() { + return count( self::$forms ); + } /** * Store shortcode content for recall later @@ -1415,7 +1423,8 @@ public function get_field_ids() { ); // Initialize marketing consent - $field_ids['email_marketing_consent'] = null; + $field_ids['email_marketing_consent'] = null; + $field_ids['email_marketing_consent_field'] = null; foreach ( $this->fields as $id => $field ) { $type = $field->get_attribute( 'type' ); @@ -1448,6 +1457,7 @@ public function get_field_ids() { case 'consent': // Set email marketing consent for the first Consent type field if ( null === $field_ids['email_marketing_consent'] ) { + $field_ids['email_marketing_consent_field'] = $id; if ( $field->value ) { $field_ids['email_marketing_consent'] = true; } else { diff --git a/projects/packages/forms/src/contact-form/class-form-response.php b/projects/packages/forms/src/contact-form/class-form-response.php new file mode 100644 index 0000000000000..7ececfeb7e4d8 --- /dev/null +++ b/projects/packages/forms/src/contact-form/class-form-response.php @@ -0,0 +1,806 @@ +feedback_post = $feedback_post; + $this->form = $form; + + if ( $this->feedback_post instanceof WP_Post ) { + + $parsed_content = static::parse_content( $this->feedback_post->post_content, $this->feedback_post->post_mime_type ); + + $this->status = $this->feedback_post->post_status; + $this->feedback_id = $this->feedback_post->post_name; + $this->entry_id = $this->feedback_post->post_parent ? (int) $this->feedback_post->post_parent : 0; + $current_post = $this->entry_id ? get_post( $this->entry_id ) : null; + + $this->fields = isset( $parsed_content['fields'] ) ? $parsed_content['fields'] : array(); + $this->ip_address = isset( $parsed_content['ip'] ) ? $parsed_content['ip'] : null; + $this->subject = isset( $parsed_content['subject'] ) ? $parsed_content['subject'] : ''; + $this->author = isset( $parsed_content['author'] ) ? $parsed_content['author'] : ''; + $this->author_email = isset( $parsed_content['author_email'] ) ? $parsed_content['author_email'] : ''; + $this->author_url = isset( $parsed_content['author_url'] ) ? $parsed_content['author_url'] : ''; + $this->comment_content = isset( $parsed_content['comment_content'] ) ? $parsed_content['comment_content'] : ''; + $this->has_consent = isset( $parsed_content['has_consent'] ) ? $parsed_content['has_consent'] : false; + $this->entry_title = isset( $parsed_content['entry_title'] ) ? $parsed_content['entry_title'] : ''; + $this->entry_page = isset( $parsed_content['entry_page'] ) ? (int) $parsed_content['entry_page'] : 1; + + } elseif ( is_array( $post_data ) && ! empty( $post_data ) ) { + + // If post_data is provided, use it to populate fields. + $this->status = $this->status; + $this->feedback_id = $this->get_computed_feedback_id(); + $this->fields = $this->get_computed_fields( $post_data ); + $this->ip_address = Contact_Form_Plugin::get_ip_address(); + $this->subject = $this->get_computed_subject( $post_data ); + $this->author = $this->get_computed_author( $post_data ); + $this->author_email = $this->get_computed_author_email( $post_data ); + $this->author_url = $this->get_computed_author_url( $post_data ); + $this->comment_content = $this->get_computed_comment_content( $post_data ); + $this->has_consent = $this->get_computed_consent( $post_data ); + $this->entry_id = ! empty( $current_post ) ? (int) $current_post->ID : 0; + $this->entry_title = ! empty( $current_post ) ? get_the_title( $current_post ) : ''; + $this->entry_page = $current_page_number; + } + + $this->entry_title = ! empty( $current_post ) ? get_the_title( $current_post ) : $this->entry_title; + $this->entry_permalink = ! empty( $current_post ) ? $this->get_computed_entry_permalink( $current_post, $this->entry_page ) : ''; + } + + /** + * Get a sanitized value from the post data. + * + * @param string $key The key to look for in the post data. + * @param array $post_data The post data array, typically $_POST. + * + * @return string|array The sanitized value, or an empty string if the key is not found. + */ + private function get_field_value( $key, $post_data ) { + if ( isset( $post_data[ $key ] ) ) { + if ( is_array( $post_data[ $key ] ) ) { + return array_map( 'sanitize_text_field', wp_unslash( $post_data[ $key ] ) ); + } else { + return sanitize_text_field( wp_unslash( $post_data[ $key ] ) ); + } + } + return ''; + } + + /** + * Create a response object from a feedback post ID. + * + * @param int $feedback_post_id The ID of the feedback post. + * @return static|null + */ + public static function get( $feedback_post_id ) { + + $feedback_post = get_post( $feedback_post_id ); + if ( ! $feedback_post || self::POST_TYPE !== $feedback_post->post_type ) { + return null; + } + + return new static( $feedback_post ); + } + + /** + * Create a response object from a form submission. + * + * @param array $post_data Typically $_POST. + * @param Contact_Form $form The form object. + * @param WP_Post|null $current_post The current post object, if available. + * @param int $current_page_number The current page number associated with the current post object entry. + * + * @return static + */ + public static function from_submission( $post_data, $form, $current_post = null, $current_page_number = 1 ) { + return new static( null, $form, $post_data, $current_post, $current_page_number ); + } + + /** + * Get all the fields of the response. + */ + public function get_fields() { + return $this->fields; + } + + /** + * Get all the values of the response. + * + * This is a convenience method to get all values in a simple array format. + * + * This is done for backwards compatibility. Use `get_fields()` instead. + * + * @return array + */ + private function get_all_values() { + $values = array(); + foreach ( $this->fields as $field ) { + if ( ! $field instanceof Response_Field ) { + continue; + } + $values[ $field->get_key() ] = $field->get_value(); + } + return $values; + } + + /** + * Get the feedback ID of the response. + * Which is the same as the post name for feedback entries. + * Please note that this is not the same as the feedback post ID. + * + * @return string + */ + public function get_feedback_id() { + return $this->feedback_id; + } + + /** + * Get the author name of the feedback entry. + * If the author is not provided we will use the email instead. + * + * @return string + */ + public function get_author() { + if ( ! empty( $this->author ) ) { + return $this->author; + } + if ( ! empty( $this->author_email ) ) { + return $this->author_email; + } + return $this->author; + } + + /** + * Get the author email of a feedback entry. + * + * @return string + */ + public function get_author_email() { + return $this->author_email; + } + + /** + * Get the author url of a feedback entry. + * + * @return string + */ + public function get_author_url() { + return $this->author_url; + } + + /** + * Get the comment content of a feedback entry. + * + * @return string + */ + public function get_comment_content() { + return $this->comment_content; + } + + /** + * Get the IP address of the submitted feedback request. + * + * @return string|null + */ + public function get_ip_address() { + return $this->ip_address; + } + + /** + * Get the email subject. + * + * @return string + */ + public function get_subject() { + return $this->subject; + } + + /** + * Gets the value of the consent field. + * + * @return bool + */ + public function has_consent() { + return $this->has_consent; + } + + /** + * Get the feedback status. For example 'publish', 'spam' or 'trash'. + * + * @return string + */ + public function get_status() { + return $this->status; + } + + /** + * Sets the status of the feedback. + * + * @param string $status The status to set for the feedback entry. + * @return void + */ + public function set_status( $status ) { + $this->status = $status; + } + + /** + * Get the entry ID of the post that the feedback was submitted from. + * + * This is the post ID of the post or page that the feedback was submitted from. + * + * @return int|null + */ + public function get_entry_id() { + return $this->entry_id; + } + + /** + * Get the entry title of the post that the feedback was submitted from. + * + * This is the title of the post or page that the feedback was submitted from. + * + * @return string + */ + public function get_entry_title() { + return $this->entry_title; + } + + /** + * Get the permalink of the post or page that the feedback was submitted from. + * This includes the page number if the feedback was submitted from a paginated form. + * + * @return string + */ + public function get_entry_permalink() { + return $this->entry_permalink; + } + /** + * Save the feedback entry to the database. + * + * @return int + */ + public function save() { + $post_id = wp_insert_post( + array( + 'post_type' => self::POST_TYPE, + 'post_status' => $this->status, + 'post_name' => $this->feedback_id, + 'post_content' => $this->serialize(), + 'post_mime_type' => 'v2', // a way to help us identify what version of the data this is. + 'post_parent' => $this->entry_id, + ) + ); + + $this->feedback_post = get_post( $post_id ); + return $this->feedback_post ? $this->feedback_post->ID : 0; + } + + /** + * Serialize the fields to JSON format. + * + * @return string + */ + public function serialize() { + + $fields_to_serialize = array( + 'subject' => $this->subject, + 'author' => $this->author, + 'author_email' => $this->author_email, + 'author_url' => $this->author_url, + 'comment_content' => $this->comment_content, + 'has_consent' => (bool) $this->has_consent, + 'entry_title' => $this->entry_title, + 'entry_page' => $this->entry_page, + 'ip' => $this->ip_address, + ); + + $fields_to_serialize['fields'] = array(); + foreach ( $this->fields as $field ) { + if ( ! $field instanceof Response_Field ) { + continue; + } + $fields_to_serialize['fields'][] = $field->serialize(); + } + + // Check if the IP should be included. + if ( apply_filters( 'jetpack_contact_form_forget_ip_address', false, $this->ip_address ) ) { + $fields_to_serialize['ip'] = null; + } + + return wp_json_encode( $fields_to_serialize ); + } + + /** + * Helper function to parse the post content. + * + * @param string $post_content The post content to parse. + * @param string|null $version The version of the content format. + * @return array Parsed fields. + */ + public static function parse_content( $post_content = '', $version = null ) { + if ( $version === 'v2' ) { + $decoded_content = json_decode( $post_content, true ); + if ( $decoded_content === null ) { + // If JSON decoding fails, try to decode the second try with stripslashes and trim. + // This is a workaround for some cases where the JSON data is not properly formatted. + $decoded_content = json_decode( stripslashes( trim( $post_content ) ), true ); + } + + if ( $decoded_content === null ) { + return array(); + } + $fields = array(); + foreach ( $decoded_content['fields'] as $field ) { + $fields[ $field['key'] ] = Response_Field::from_serialized( $field ); + } + $decoded_content['fields'] = $fields; + return $decoded_content; + } + + // parse_feedback_content + $all_values = array(); + $content = explode( '', $post_content ); + $lines = array(); + $comment_content = ''; + if ( count( $content ) > 1 ) { + $comment_content = $content[0]; + $content = str_ireplace( array( '
', ')

' ), '', $content[1] ); + + if ( str_contains( $content, 'JSON_DATA' ) ) { + $chunks = explode( "\nJSON_DATA", $content ); + + $all_values = json_decode( $chunks[1], true ); + + if ( $all_values === null ) { + // If JSON decoding fails, try to decode the second try with stripslashes and trim. + // This is a workaround for some cases where the JSON data is not properly formatted. + $all_values = json_decode( stripslashes( trim( $chunks[1] ) ), true ); + } + $lines = array_filter( explode( "\n", $chunks[0] ) ); + } else { + $fields_array = preg_replace( '/.*Array\s\( (.*)\)/msx', '$1', $content ); + + // This line of code is used to parse a string containing key-value pairs formatted as [Key] => Value and extract the keys and values into an array. + // The regular expression ensures that each key-value pair is correctly identified and captured. + // Given an input string + // [Key1] => Value1 + // [Key2] => Value2 + // it $matches[1]: The keys (e.g., Key1, Key2 ). + // and $matches[2]: The values (e.g., Value1, Value2 ). + preg_match_all( '/^\s*\[([^\]]+)\] =\>\; (.*)(?=^\s*(\[[^\]]+\] =\>\;)|\z)/msU', $fields_array, $matches ); + + if ( count( $matches ) > 1 ) { + $all_values = array_combine( array_map( 'trim', $matches[1] ), array_map( 'trim', $matches[2] ) ); + } + + $lines = array_filter( explode( "\n", $content ) ); + } + } + + $var_map = array( + 'AUTHOR' => 'author', + 'AUTHOR EMAIL' => 'author_email', + 'AUTHOR URL' => 'author_url', + 'SUBJECT' => 'subject', + 'IP' => 'ip', + ); + + $decoded_fields = array(); + + foreach ( $lines as $line ) { + $vars = explode( ': ', $line, 2 ); + if ( ! empty( $vars ) ) { + if ( isset( $var_map[ $vars[0] ] ) ) { + $type = $var_map[ $vars[0] ]; + $decoded_fields[ $type ] = self::strip_tags( trim( $vars[1] ) ); + } + } + } + // All fields should always be an array, even if empty. + if ( ! is_array( $all_values ) ) { + $all_values = array(); + } + + $non_user_fields = array( + 'email_marketing_consent', + 'entry_title', + 'entry_permalink', + 'entry_page', + 'feedback_id', + ); + + foreach ( $all_values as $key => $value ) { + $key = wp_strip_all_tags( $key ); + $label = ''; + if ( in_array( $key, $non_user_fields, true ) ) { + $decoded_fields[ $key ] = $value; + // Skip fields that are not user-submitted. + continue; + } + $decoded_fields['fields'][ $key ] = new Response_Field( $key, $label, $value ); + } + + $decoded_fields['comment_content'] = trim( self::strip_tags( $comment_content ) ); + + return $decoded_fields; + } + + /** + * Strips HTML tags from input. Output is NOT HTML safe. + * + * @param mixed $data_with_tags - data we're stripping HTML tags from. + * @return mixed + */ + public static function strip_tags( $data_with_tags ) { + $data_without_tags = array(); + if ( is_array( $data_with_tags ) ) { + foreach ( $data_with_tags as $index => $value ) { + if ( is_array( $value ) ) { + $data_without_tags[ $index ] = self::strip_tags( $value ); + continue; + } + + $index = sanitize_text_field( (string) $index ); + $value = wp_kses_post( (string) $value ); + $value = str_replace( '&', '&', $value ); // undo damage done by wp_kses_normalize_entities() + + $data_without_tags[ $index ] = $value; + } + } else { + $data_without_tags = wp_kses_post( (string) $data_with_tags ); + $data_without_tags = str_replace( '&', '&', $data_without_tags ); // undo damage done by wp_kses_normalize_entities() + } + + return $data_without_tags; + } + + /** + * Get the computed feedback id. + * + * @return string + */ + private function get_computed_feedback_id() { + $comment_author = $this->get_author(); + + // Build feedback reference + $feedback_time = \current_time( 'mysql' ); + return md5( "{$comment_author} - {$feedback_time}" ); + } + + /** + * Get all the fields of the response, computed from the post data. + * + * @param array $post_data The post data from the form submission. + * @return array An array of Response_Field objects. + */ + private function get_computed_fields( $post_data ) { + + $fields = array(); + + $field_ids = $this->form->get_field_ids(); + // For all fields, grab label and value + $i = 1; + foreach ( $field_ids['all'] as $field_id ) { + $field = $this->form->fields[ $field_id ]; + $type = $field->get_attribute( 'type' ); + if ( ! $field->is_field_renderable( $type ) ) { + continue; + } + + $value = $this->get_field_value( $field_id, $post_data ); + $label = wp_strip_all_tags( $field->get_attribute( 'label' ) ); + $key = $i . '_' . $label; + + $fields[ $key ] = new Response_Field( $key, $label, $value, $type ); + ++$i; // Increment prefix counter for the next field + } + + return $fields; + } + + /** + * Gets the computed subject. + * + * @param array $post_data The post data from the form submission. + * @return string + */ + private function get_computed_subject( $post_data ) { + + $contact_form_subject = $this->form->get_attribute( 'subject' ); + $field_ids = $this->form->get_field_ids(); + + if ( isset( $field_ids['subject'] ) ) { + $value = $this->get_field_value( $field_ids['subject'], $post_data ); + if ( ! empty( $value ) ) { + $contact_form_subject = $value; + } + } + + return apply_filters( 'contact_form_subject', $contact_form_subject, $this->get_all_values() ); + } + + /** + * Gets the computed author. + * + * @param array $post_data The post data from the form submission. + * @return string + */ + private function get_computed_author( $post_data ) { + $field_ids = $this->form->get_field_ids(); + if ( isset( $field_ids['name'] ) ) { + $value = $this->get_field_value( $field_ids['name'], $post_data ); + if ( is_string( $value ) ) { + return self::strip_tags( + stripslashes( + /** This filter is already documented in core/wp-includes/comment-functions.php */ + apply_filters( 'pre_comment_author_name', addslashes( $value ) ) + ) + ); + } + } + + return ''; + } + + /** + * Gets the computed author email. + * + * @param array $post_data The post data from the form submission. + * @return string + */ + private function get_computed_author_email( $post_data ) { + $field_ids = $this->form->get_field_ids(); + if ( isset( $field_ids['email'] ) ) { + $value = $this->get_field_value( $field_ids['email'], $post_data ); + if ( is_string( $value ) ) { + return self::strip_tags( + stripslashes( + /** This filter is already documented in core/wp-includes/comment-functions.php */ + apply_filters( 'pre_comment_author_email', addslashes( $value ) ) + ) + ); + } + } + return ''; + } + + /** + * Gets the computed author url. + * + * @param array $post_data The post data from the form submission. + * @return string + */ + private function get_computed_author_url( $post_data ) { + $field_ids = $this->form->get_field_ids(); + if ( isset( $field_ids['url'] ) ) { + $value = $this->get_field_value( $field_ids['url'], $post_data ); + if ( is_string( $value ) ) { + return self::strip_tags( + stripslashes( + /** This filter is already documented in core/wp-includes/comment-functions.php */ + apply_filters( 'pre_comment_author_url', addslashes( $value ) ) + ) + ); + } + } + return ''; + } + + /** + * Gets the computed comment content. + * + * @param array $post_data The post data from the form submission. + * @return string + */ + private function get_computed_comment_content( $post_data ) { + $field_ids = $this->form->get_field_ids(); + if ( isset( $field_ids['textarea'] ) ) { + $value = $this->get_field_value( $field_ids['textarea'], $post_data ); + if ( is_string( $value ) ) { + return trim( self::strip_tags( stripslashes( $value ) ) ); + } + } + return ''; + } + + /** + * Gets the computed consent. + * + * @param array $post_data The post data from the form submission. + * @return string + */ + private function get_computed_consent( $post_data ) { + $field_ids = $this->form->get_field_ids(); + + if ( isset( $field_ids['email_marketing_consent_field'] ) && $field_ids['email_marketing_consent_field'] !== null ) { + return (bool) $this->get_field_value( $field_ids['email_marketing_consent_field'], $post_data ); + } + + return false; + } + + /** + * Gets the permalink of post parent associated with the feedback. + * + * So that we can link the user to the URL that the feedback was created from. + * + * @param WP_Post|null $current_post The current post object, if available. + * @param int $current_page_number The current page number associated with the current post. + * + * @return string The permalink of the post or page that the feedback was submitted from. + */ + private function get_computed_entry_permalink( $current_post = null, $current_page_number = 1 ) { + + if ( $current_post instanceof WP_Post ) { + $permalink = get_the_permalink( $current_post ); + if ( $current_page_number > 1 ) { + $permalink = add_query_arg( 'page', $current_page_number, $permalink ); + } + return $permalink; + } + return ''; + } +} diff --git a/projects/packages/forms/src/contact-form/class-response-field.php b/projects/packages/forms/src/contact-form/class-response-field.php new file mode 100644 index 0000000000000..80c027f2eb10d --- /dev/null +++ b/projects/packages/forms/src/contact-form/class-response-field.php @@ -0,0 +1,157 @@ +key = $key; + $this->label = $label; + $this->value = $value; + $this->type = $type; + $this->meta = $meta; + } + + /** + * Get the value of the field. + * + * @return string + */ + public function get_key() { + return $this->key; + } + + /** + * Get the label of the field. + * + * @return string + */ + public function get_label() { + return $this->label; + } + + /** + * Get the value of the field. + * + * @return mixed + */ + public function get_value() { + return $this->value; + } + + /** + * Get the value of the field. + * + * @return mixed + */ + public function render_value() { + return $this->value; + } + + /** + * Get the type of the field. + * + * @return string + */ + public function get_type() { + return $this->type; + } + + /** + * Get the meta of the field. + * + * @return string + */ + public function get_meta() { + return $this->meta; + } + + /** + * Get the serialized representation of the field. + * + * @return array + */ + public function serialize() { + return array( + 'key' => $this->get_key(), + 'label' => $this->get_label(), + 'value' => $this->get_value(), + 'type' => $this->get_type(), + 'meta' => $this->get_meta(), + ); + } + /** + * Create a Response_Field object from serialized data. + * + * @param array $data The serialized data. + * + * @return Response_Field|null Returns a Response_Field object or null if the data is invalid. + */ + public static function from_serialized( $data ) { + if ( ! is_array( $data ) || ! isset( $data['key'] ) || ! isset( $data['value'] ) || ! isset( $data['label'] ) ) { + return null; + } + + return new self( + $data['key'], + $data['label'], + $data['value'], + isset( $data['type'] ) ? $data['type'] : 'basic', + isset( $data['meta'] ) ? $data['meta'] : array() + ); + } +} diff --git a/projects/packages/forms/tests/php/contact-form/Contact_Form_Test.php b/projects/packages/forms/tests/php/contact-form/Contact_Form_Test.php index fe63c94439a10..58c03b2566cb8 100644 --- a/projects/packages/forms/tests/php/contact-form/Contact_Form_Test.php +++ b/projects/packages/forms/tests/php/contact-form/Contact_Form_Test.php @@ -131,15 +131,11 @@ public function tear_down() { * @param string $form_id Optional form ID. If not provided, will use $this->post->ID. */ private function add_field_values( $values, $form_id = null ) { - $prefix = $form_id ? $form_id : 'g' . $this->post->ID; - $_POST = array(); - foreach ( $values as $key => $val ) { - if ( strpos( $key, 'contact-form' ) === 0 || strpos( $key, 'action' ) === 0 ) { - $_POST[ $key ] = $val; - } else { - $_POST[ $prefix . '-' . $key ] = $val; - } - } + Utility::add_post_request( + $values, + $form_id, + $this->post->ID + ); } /** diff --git a/projects/packages/forms/tests/php/contact-form/Form_Response_Test.php b/projects/packages/forms/tests/php/contact-form/Form_Response_Test.php new file mode 100644 index 0000000000000..7a18da98fd803 --- /dev/null +++ b/projects/packages/forms/tests/php/contact-form/Form_Response_Test.php @@ -0,0 +1,1130 @@ +assertNull( $response ); + } + + public function test_from_post_id_returns_instance_for_valid_feedback_post() { + $post_id = \wp_insert_post( + array( + 'post_type' => 'feedback', + 'post_status' => 'publish', + 'post_title' => 'Test Feedback', + 'post_content' => '{}', + 'page_template' => 'v2', + ) + ); + $response = Form_Response::get( $post_id ); + $this->assertInstanceOf( Form_Response::class, $response ); + } + + public function test_from_submission_sets_fields_and_post_data() { + $form = new Contact_Form( array() ); + $post_data = array( + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'message' => 'Hello!', + 'ignore' => 'should not be included', + ); + $response = Form_Response::from_submission( $post_data, $form ); + $this->assertInstanceOf( Form_Response::class, $response ); + } + + public function test_form_response_is_matches_empty_data() { + $form = new Contact_Form( array() ); + $post_data = array(); + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $response->serialize(), $post_response->serialize() ); + $this->assertEquals( $response->get_fields(), $post_response->get_fields() ); + } + + public function test_form_response_is_matches_submission_data() { + $name = 'John Doe'; + $email = 'john@example.com'; + $message = 'Test message'; + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => $name, + 'email' => $email, + 'message' => $message, + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $response->serialize(), $post_response->serialize() ); + $this->assertEquals( $response->get_fields(), $post_response->get_fields() ); + + foreach ( $response->get_fields() as $field ) { + $this->assertInstanceOf( Response_Field::class, $field ); + } + + foreach ( $post_response->get_fields() as $field ) { + $this->assertInstanceOf( Response_Field::class, $field ); + } + + foreach ( $post_response->get_fields() as $field_key => $field ) { + $this->assertEquals( $response->get_fields()[ $field_key ]->serialize(), $post_response->get_fields()[ $field_key ]->serialize(), 'Serialized response field should match' ); + } + + $this->assertEquals( $name, $response->get_fields()['1_Name']->get_value(), 'Response field value should match' ); + $this->assertEquals( $name, $post_response->get_fields()['1_Name']->get_value(), 'Saved response field value should match' ); + + $this->assertEquals( 'Name', $response->get_fields()['1_Name']->get_label(), 'Name response field label should match' ); + $this->assertEquals( 'Name', $post_response->get_fields()['1_Name']->get_label(), 'Saved response field label should match' ); + $this->assertEquals( 'name', $response->get_fields()['1_Name']->get_type(), 'Response field type should match' ); + $this->assertEquals( 'name', $post_response->get_fields()['1_Name']->get_type(), 'Saved response type value should match' ); + + $this->assertEquals( $email, $response->get_fields()['2_Email']->get_value(), 'Response field value should match' ); + $this->assertEquals( $email, $post_response->get_fields()['2_Email']->get_value(), 'Saved response field value should match' ); + + $this->assertEquals( $message, $response->get_fields()['3_Message']->get_value(), 'Response field value should match' ); + $this->assertEquals( $message, $post_response->get_fields()['3_Message']->get_value(), 'Saved Name response field value should match ' ); + } + + /** + * Test that a previously saved response can be handled correctly. + * + * This test checks if the Form_Response class can retrieve and handle + * a response that was saved in the legacy format. + */ + public function test_handle_previously_saved_response() { + + $post_id = Utility::create_legacy_feedback( + array( + '1_field' => 'value1', + '2_field' => 'value2', + ) + ); + + $response = Form_Response::get( $post_id ); + + $this->assertInstanceOf( Form_Response::class, $response ); + + $field = $response->get_fields()['1_field']; + + $this->assertInstanceOf( Response_Field::class, $field ); + $this->assertEquals( '1_field', $field->get_key() ); + $this->assertEquals( 'value1', $field->render_value() ); + $this->assertEquals( 'value1', $field->render_value() ); + $this->assertEquals( 'basic', $field->get_type() ); // Assuming 'basic' is the default type for a simple text field. + } + /** + * Tests that the feedback ID is computed correctly when saving a from response. + * + * It should be non empty and match the post slug. + */ + public function test_form_response_computed_feedback_id() { + $name = 'John Doe'; + $email = 'john@example.com'; + $message = 'Test message'; + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => $name, + 'email' => $email, + 'message' => $message, + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post = get_post( $post_id ); + + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $response->get_feedback_id(), $post_response->get_feedback_id(), 'Feedback ID should match' ); + $this->assertEquals( $post->post_name, $post_response->get_feedback_id(), 'Feedback ID should match post slug' ); + $this->assertNotEmpty( $post_response->get_feedback_id(), 'Feedback ID should not be empty' ); + } + + /** + * Test the IP address is included in the serialized response. + * It should be always available when the reponse is created during the form submission. + * + * It should only be empty if the response that has the filter applied to it. + */ + public function test_ip_address_included_in_serialized_response() { + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + // The IP address should be present. + $this->assertNotEmpty( $response->get_ip_address(), 'IP address should not be empty' ); + $this->assertNotEmpty( $post_response->get_ip_address(), 'IP address should not be empty' ); + $this->assertEquals( $response->get_ip_address(), $post_response->get_ip_address(), 'IP address should match' ); + + add_filter( 'jetpack_contact_form_forget_ip_address', '__return_true' ); + $new_post_id = $response->save(); + remove_filter( 'jetpack_contact_form_forget_ip_address', '__return_true' ); + + // The IP address should NOT be present. + $post_response = Form_Response::get( $new_post_id ); + $this->assertEmpty( $post_response->get_ip_address(), 'IP address should BE empty' ); + } + + /** + * Test the IP address is included in the serialized response. + * It should be always available when the reponse is created during the form submission. + * + * It should only be empty if the response that has the filter applied to it. + */ + public function test_ip_address_in_legacy() { + $ip = 'http://123.123.123.122'; + + $post_id = Utility::create_legacy_feedback( + array(), + null, + null, + null, + null, + $ip + ); + + $post_response = Form_Response::get( $post_id ); + $this->assertEquals( $ip, $post_response->get_ip_address(), 'IP should match the legacy feedback IP' ); + } + + /** + * Test the subject line is computed for legacy correctly. + */ + public function test_computed_subject_legacy() { + $subject = 'Test Subject'; + $post_id = Utility::create_legacy_feedback( + array(), + null, + null, + null, + null, + null, + $subject + ); + + $post_response = Form_Response::get( $post_id ); + $this->assertEquals( $subject, $post_response->get_subject(), 'Subject should match the legacy feedback post subject' ); + } + + /** + * Test the subject line is computed correctly. + */ + public function test_computed_form_subject() { + $subject = 'Test Subject'; + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + 'subject' => $subject, + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $subject, $response->get_subject(), 'Subject should match the form submission' ); + $this->assertEquals( $subject, $post_response->get_subject(), 'Subject should match the saved form submission' ); + } + + /** + * Test the subject line is computed via field + */ + public function test_computed_form_subject_field() { + $subject = 'Test Subject'; + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => 'John Doe', + 'subject' => $subject, + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Subject' type='subject' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $subject, $response->get_subject(), 'Subject should match the form submission' ); + $this->assertEquals( $subject, $post_response->get_subject(), 'Subject should match the saved form submission' ); + } + + /** + * Test the subject line is computed correctly when both a subject attribute and a field is present. + */ + public function test_computed_form_subject_field_overwrites() { + $subject = 'Test Subject'; + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => 'John Doe', + 'subject' => $subject, + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + 'subject' => $subject . ' (from form attributes)', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Subject' type='subject' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $subject, $response->get_subject(), 'Subject should match the form submission' ); + $this->assertEquals( $subject, $post_response->get_subject(), 'Subject should match the saved form submission' ); + } + + /** + * Test the subject line is computed correctly and the filter is applied correctly. + */ + public function test_computed_form_subject_field_overwrites_filter() { + $subject = 'Test Subject'; + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => 'John Doe', + 'subject' => $subject, + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + 'subject' => $subject . ' (from form attributes)', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Subject' type='subject' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + add_filter( 'contact_form_subject', array( $this, 'subject_from_filter' ), 10, 2 ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + remove_filter( 'contact_form_subject', array( $this, 'subject_from_filter' ), 10, 2 ); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( 'Overwritten Subject (from filter)', $response->get_subject(), 'Subject should match the form submission' ); + $this->assertEquals( 'Overwritten Subject (from filter)', $post_response->get_subject(), 'Subject should match the saved form submission' ); + } + /** + * Callback for the contact_form_subject filter. + * + * This function is used to overwrite the subject line when the filter is applied. + * + * @return string The overwritten subject line. + */ + public function subject_from_filter() { + // Overwrite the subject with a different value. + return 'Overwritten Subject (from filter)'; + } + + public function test_computed_name_for_legacy() { + $author = 'Mikey Mouse'; + $post_id = Utility::create_legacy_feedback( + array(), + null, + $author + ); + + $post_response = Form_Response::get( $post_id ); + $this->assertEquals( $author, $post_response->get_author(), 'Author should match the legacy feedback post author' ); + } + + public function test_computed_name() { + $author = 'Mikey Mouse'; + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => $author, + 'email' => 'email@email.com', + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $author, $response->get_author(), 'Author should match the form submission' ); + $this->assertEquals( $author, $post_response->get_author(), 'Author should match the saved form submission' ); + } + + public function test_computed_name_as_email() { + $author = ''; // author is empty + $email = 'email@email.com'; + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => $author, + 'email' => $email, + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $email, $response->get_author(), 'Author should match the form submission' ); + $this->assertEquals( $email, $post_response->get_author(), 'Author should match the saved form submission' ); + } + + public function test_computed_name_filter() { + $author = 'Mikey Mouse'; + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => $author, + 'email' => 'email@email.com', + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + add_filter( 'pre_comment_author_name', array( $this, 'set_filter_as_string' ) ); + $response = Form_Response::from_submission( $post_data, $form ); + remove_filter( 'pre_comment_author_name', array( $this, 'set_filter_as_string' ) ); + + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( 'STRING', $response->get_author(), 'Author should match the form submission' ); + $this->assertEquals( 'STRING', $post_response->get_author(), 'Author should match the saved form submission' ); + } + /** + * A helper function that sets the filter to return string 'STRING'. + * + * @return string + */ + public function set_filter_as_string() { + return 'STRING'; + } + + public function test_computed_email_for_legacy() { + $email = 'email@email.com'; + $post_id = Utility::create_legacy_feedback( + array(), + null, + null, + $email + ); + + $post_response = Form_Response::get( $post_id ); + $this->assertEquals( $email, $post_response->get_author_email(), 'Author email should match the legacy feedback post author email' ); + } + + public function test_computed_email() { + + $email = 'email@email.com'; + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => 'author ', + 'email' => $email, + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $email, $response->get_author_email(), 'Author email should match the form submission' ); + $this->assertEquals( $email, $post_response->get_author_email(), 'Author email should match the saved form submission' ); + } + + public function test_computed_email_filter() { + $email = 'email@email.com'; + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'name' => 'joe', + 'email' => $email, + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Name' type='name' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + add_filter( 'pre_comment_author_email', array( $this, 'set_filter_as_string' ) ); + $response = Form_Response::from_submission( $post_data, $form ); + remove_filter( 'pre_comment_author_email', array( $this, 'set_filter_as_string' ) ); + + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( 'STRING', $response->get_author_email(), 'Author email should match the form submission' ); + $this->assertEquals( 'STRING', $post_response->get_author_email(), 'Author email should match the saved form submission' ); + } + + public function test_computed_url_for_legacy() { + $url = 'https://wordpress.com'; + $post_id = Utility::create_legacy_feedback( + array(), + null, + null, + null, + $url + ); + + $post_response = Form_Response::get( $post_id ); + $this->assertEquals( $url, $post_response->get_author_url(), 'Author url should match the legacy feedback post author url' ); + } + + public function test_computed_url() { + $url = 'https://wordpress.com'; + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'url' => $url, + 'email' => 'email@email.com', + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Url' type='url' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $url, $response->get_author_url(), 'Author url should match the form submission' ); + $this->assertEquals( $url, $post_response->get_author_url(), 'Author url should match the saved form submission' ); + } + + public function test_computed_url_filter() { + $url = 'https://wordpress.com'; + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'url' => $url, + 'email' => 'email@email.com', + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Url' type='url' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + add_filter( 'pre_comment_author_url', array( $this, 'set_filter_as_string' ) ); + $response = Form_Response::from_submission( $post_data, $form ); + remove_filter( 'pre_comment_author_url', array( $this, 'set_filter_as_string' ) ); + + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( 'STRING', $response->get_author_url(), 'Author url should match the form submission' ); + $this->assertEquals( 'STRING', $post_response->get_author_url(), 'Author url should match the saved form submission' ); + } + + public function test_computed_comment_content_for_legacy() { + $content = 'Some comment content!'; + $post_id = Utility::create_legacy_feedback( + array(), + $content + ); + + $post_response = Form_Response::get( $post_id ); + $this->assertEquals( $content, $post_response->get_comment_content(), 'Comment content should match the legacy feedback post author url' ); + } + + public function test_computed_comment_content() { + $content = 'Some comment content!'; + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'url' => 'https://howdy.com', + 'email' => 'email@email.com', + 'message' => $content, + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Url' type='url' required='1'/][contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + // Create a contact form + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $content, $response->get_comment_content(), 'Comment content should match the form submission' ); + $this->assertEquals( $content, $post_response->get_comment_content(), 'Comment content should match the saved form submission' ); + } + + public function test_status_from_legacy() { + $status = 'spam'; + $post_id = Utility::create_legacy_feedback( + array(), + null, + null, + null, + null, + null, + null, + 'spam' + ); + + $post_response = Form_Response::get( $post_id ); + $this->assertEquals( $status, $post_response->get_status(), 'Status should match the legacy feedback status' ); + } + + public function test_computed_status() { + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( 'publish', $response->get_status(), 'Status should match the form submission' ); + $this->assertEquals( 'publish', $post_response->get_status(), 'Status should match the saved form submission' ); + } + + public function test_set_status() { + $status = 'trash'; + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + 'message' => 'Test message', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/][contact-field label='Message' type='textarea' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form ); + $response->set_status( $status ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + + $this->assertEquals( $status, $response->get_status(), 'Status should match the form submission' ); + $this->assertEquals( $status, $post_response->get_status(), 'Status should match the saved form submission' ); + } + + public function test_consent() { + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + 'consent' => 'Yes', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/][contact-field label='Consent' type='consent' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + $this->assertTrue( $response->has_consent(), 'Has consent should match the form submission' ); + $this->assertTrue( $post_response->has_consent(), 'Has consent should match the saved form submission' ); + } + + public function test_empty_consent() { + + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + 'consent' => '', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/][contact-field label='Consent' type='consent' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form ); + $post_id = $response->save(); + $post_response = Form_Response::get( $post_id ); + $this->assertFalse( $response->has_consent(), 'Has consent should match the form submission' ); + $this->assertFalse( $post_response->has_consent(), 'Has consent should match the saved form submission' ); + } + + /** + * Helper function for creating the post context. + * This is helpful for testing the post context. + **/ + private function create_post_context() { + $author_id = wp_insert_user( + array( + 'user_email' => 'john@example.com', + 'user_login' => 'test_user', + 'user_pass' => 'abc123', + ) + ); + + $post_id = wp_insert_post( + array( + 'post_title' => 'POST TITLE ' . microtime(), + 'post_content' => 'POST CONTENT', + 'post_status' => 'publish', + 'post_author' => $author_id, + ), + true + ); + + global $post; + $post = get_post( $post_id ); + return $post; + } + + /** + * Helper function for destroying the post context. + * This is helpful cleaning up the post context after the test. + **/ + private function destroy_post_context() { + global $post; + if ( $post ) { + wp_delete_user( $post->post_author, true ); + wp_delete_post( $post->ID, true ); + $post = null; + } + } + + public function test_compute_entry_ID_legacy() { + $current_post = $this->create_post_context(); + $post_id = Utility::create_legacy_feedback(); + $this->destroy_post_context(); + + $post_response = Form_Response::get( $post_id ); + $this->assertEquals( $current_post->ID, $post_response->get_entry_id(), 'Entry_ID should match the saved form submission' ); + } + + public function test_compute_entry_ID() { + $current_post = $this->create_post_context(); + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + 'consent' => '', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/][contact-field label='Consent' type='consent' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form, $current_post ); + $post_id = $response->save(); + $this->destroy_post_context(); + + $post_response = Form_Response::get( $post_id ); + $this->assertEquals( $current_post->ID, $response->get_entry_id(), 'Entry_ID should match the form submission' ); + $this->assertEquals( $current_post->ID, $post_response->get_entry_id(), 'Entry_ID should match the saved form submission' ); + } + + public function test_compute_entry_title() { + $current_post = $this->create_post_context(); + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form, $current_post ); + $post_id = $response->save(); + + $post_response = Form_Response::get( $post_id ); + $this->destroy_post_context(); + + $this->assertEquals( $current_post->post_title, $response->get_entry_title(), 'Post title should match the form submission' ); + $this->assertEquals( $current_post->post_title, $post_response->get_entry_title(), 'Post title should match the saved form submission' ); + } + + public function test_compute_entry_title_updated() { + $current_post = $this->create_post_context(); + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form, $current_post ); + $post_id = $response->save(); + + // Update the post title to simulate an update. + $update_title = 'Updated Title'; + wp_update_post( + array( + 'ID' => $current_post->ID, + 'post_title' => $update_title, + ) + ); + + $post_response = Form_Response::get( $post_id ); + $this->destroy_post_context(); + + $this->assertEquals( $update_title, $post_response->get_entry_title(), 'Post Title should match the new updated title saved form submission' ); + } + + public function test_compute_entry_title_deleted() { + $current_post = $this->create_post_context(); + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form, $current_post ); + $post_id = $response->save(); + $this->destroy_post_context(); + + $this->assertSame( '', get_the_title( $current_post->ID ), 'Post title should not be available after the post is deleted' ); + // At this point we should have a deleted post. + $post_response = Form_Response::get( $post_id ); + + $this->assertNotEmpty( $post_response->get_entry_title(), 'Post Title should NOT be empty after the post is deleted' ); + $this->assertEquals( $current_post->post_title, $post_response->get_entry_title(), 'Post Title should match the saved form submission Original post title' ); + } + + public function test_compute_entry_permalink() { + $current_post = $this->create_post_context(); + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form, $current_post ); + $post_id = $response->save(); + + $post_response = Form_Response::get( $post_id ); + $this->destroy_post_context(); + $current_permalink = get_the_permalink( $current_post ); + $this->assertEquals( $current_permalink, $response->get_entry_permalink(), 'Post permalink should match the form submission' ); + + $this->assertEquals( $current_permalink, $post_response->get_entry_permalink(), 'Post permalink should match the saved form submission' ); + } + + public function test_compute_entry_permalink_deleted_post() { + $current_post = $this->create_post_context(); + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form, $current_post ); + $post_id = $response->save(); + $this->destroy_post_context(); // Destroy the post context to simulate a deleted post. + $post_response = Form_Response::get( $post_id ); + $this->assertEmpty( $post_response->get_entry_permalink(), 'Post permalink should match the form submission' ); + } + + public function test_compute_entry_permalink_with_page_number() { + $current_post = $this->create_post_context(); + $form_id = Utility::get_form_id(); + // Create a form submission + $post_data = Utility::get_post_request( + array( + 'email' => 'email@email.com', + ), + 'g' . $form_id + ); + + $form = new Contact_Form( + array( + 'title' => 'Test Form', + 'description' => 'This is a test form.', + ), + "[contact-field label='Email' type='email' required='1'/]" + ); + + $response = Form_Response::from_submission( $post_data, $form, $current_post, 999 ); + $post_id = $response->save(); + + $post_response = Form_Response::get( $post_id ); + $this->destroy_post_context(); + + $this->assertStringContainsString( 'page=999', $response->get_entry_permalink(), 'Post permalink should match the form submission' ); + $this->assertStringContainsString( 'page=999', $post_response->get_entry_permalink(), 'Post permalink should match the saved form submission' ); + } +} diff --git a/projects/packages/forms/tests/php/contact-form/Response_Field_Test.php b/projects/packages/forms/tests/php/contact-form/Response_Field_Test.php new file mode 100644 index 0000000000000..4f54f903f2cc0 --- /dev/null +++ b/projects/packages/forms/tests/php/contact-form/Response_Field_Test.php @@ -0,0 +1,110 @@ +assertInstanceOf( Response_Field::class, $field ); + + $this->assertEquals( 'test_key', $field->get_key() ); + $this->assertEquals( 'test_label', $field->get_label() ); + $this->assertEquals( 'test_value', $field->get_value() ); + $this->assertEquals( 'basic', $field->get_type() ); + $this->assertEquals( array(), $field->get_meta() ); + } + + /** + * Test that the Response_Field class can be instantiated with additional parameters. + */ + public function test_response_field_with_additional_parameters() { + $field = new Response_Field( 'test_key', 'test_label', 'test_value', 'text', array( 'meta_key' => 'meta_value' ) ); + $this->assertEquals( 'test_key', $field->get_key() ); + $this->assertEquals( 'test_label', $field->get_label() ); + $this->assertEquals( 'test_value', $field->get_value() ); + $this->assertEquals( 'text', $field->get_type() ); + $this->assertEquals( array( 'meta_key' => 'meta_value' ), $field->get_meta() ); + } + + /** + * Test that the Response_Field class can handle empty values. + */ + public function test_response_field_with_empty_values() { + $field = new Response_Field( 'test_key', '', '' ); + $this->assertEquals( 'test_key', $field->get_key() ); + $this->assertSame( '', $field->get_label() ); + $this->assertSame( '', $field->get_value() ); + $this->assertEquals( 'basic', $field->get_type() ); + $this->assertEquals( array(), $field->get_meta() ); + } + + /** + * Test that the Response_Field can serealize and unserialize correctly. + */ + public function test_response_field_serialization() { + $field = new Response_Field( 'test_key', 'test_label', 'test_value', 'text', array( 'meta_key' => 'meta_value' ) ); + $serialized = $field->serialize(); + $unserialized = Response_Field::from_serialized( $serialized ); + + $this->assertInstanceOf( Response_Field::class, $unserialized ); + + $this->assertEquals( $serialized, $unserialized->serialize() ); + $this->assertEquals( 'test_key', $unserialized->get_key() ); + $this->assertEquals( 'test_label', $unserialized->get_label() ); + $this->assertEquals( 'test_value', $unserialized->get_value() ); + $this->assertEquals( 'text', $unserialized->get_type() ); + $this->assertEquals( array( 'meta_key' => 'meta_value' ), $unserialized->get_meta() ); + } + + /** + * Test that the Response_Field can serealize and unserialize correctly. + */ + public function test_response_from_serialized_is_null() { + + $unserialized = Response_Field::from_serialized( array( 'key' => 'test_key' ) ); + + $this->assertNull( $unserialized ); + + $unserialized = Response_Field::from_serialized( 'howdy' ); + + $this->assertNull( $unserialized ); + + $unserialized = Response_Field::from_serialized( array( 'value' => 'test_value' ) ); + + $this->assertNull( $unserialized ); + + $unserialized = Response_Field::from_serialized( array( 'label' => 'test_value' ) ); + + $this->assertNull( $unserialized ); + + $unserialized = Response_Field::from_serialized( + array( + 'label' => 'test_value', + 'value' => 'value', + ) + ); + + $this->assertNull( $unserialized ); + } +} diff --git a/projects/packages/forms/tests/php/contact-form/class-utility.php b/projects/packages/forms/tests/php/contact-form/class-utility.php new file mode 100644 index 0000000000000..3f8653696a058 --- /dev/null +++ b/projects/packages/forms/tests/php/contact-form/class-utility.php @@ -0,0 +1,137 @@ + 'value1', + 'field2' => 'value2', + 'email_marketing_consent' => 'yes', + ); + } + // Ensure all_values is an array and has the necessary keys. + $entry_values = array( + 'entry_title' => 'Cool Post Title', + 'entry_permalink' => 'https://example.com/post/123', + 'feedback_id' => $feedback_id, + ); + + if ( isset( $_POST['page'] ) ) { + $entry_values['entry_page'] = absint( wp_unslash( $_POST['page'] ) ); + } + + if ( ! isset( $all_values['email_marketing_consent'] ) ) { + $all_values['email_marketing_consent'] = false; + } + + $all_values = array_merge( + $all_values, + $entry_values + ); + + $content = addslashes( wp_kses( "$comment_content\n\nAUTHOR: {$comment_author}\nAUTHOR EMAIL: {$comment_author_email}\nAUTHOR URL: {$comment_author_url}\nSUBJECT: {$subject}\nIP: {$comment_ip_text}\nJSON_DATA\n" . wp_json_encode( $all_values ), array() ) ); + + // Create a mock post with JSON_DATA format + return wp_insert_post( + array( + 'post_date' => addslashes( $feedback_time ), + 'post_type' => 'feedback', + 'post_status' => addslashes( $status ), + 'post_parent' => $post ? $post->ID : 0, + 'post_title' => addslashes( wp_kses( $feedback_title, array() ) ), + 'post_content' => $content, // so that search will pick up this data + 'post_name' => $feedback_id, + ) + ); + } + + /** + * Adds the field values to the global $_POST value. + * + * @param array $values Array of form fields and values. + * @param string $form_id Optional form ID. If not provided, will use $post_id. + */ + public static function add_post_request( $values, $form_id = null, $post_id = 0 ) { + $post_data = self::get_post_request( $values, $form_id, $post_id ); + foreach ( $post_data as $key => $value ) { + $_POST[ $key ] = $value; + } + } + + public static function get_form_id( $attributes = array() ) { + global $post, $page; + + $count = Contact_Form::get_forms_count(); + + if ( ! empty( $attributes['widget'] ) && $attributes['widget'] ) { + $attributes['id'] = 'widget-' . $attributes['widget']; + } elseif ( ! empty( $attributes['block_template'] ) && $attributes['block_template'] ) { + + $attributes['id'] = 'block-template-' . $attributes['block_template']; + } elseif ( ! empty( $attributes['block_template_part'] ) && $attributes['block_template_part'] ) { + $attributes['id'] = 'block-template-part-' . $attributes['block_template_part']; + } elseif ( $post ) { + $attributes['id'] = $post->ID; + } + + if ( $count ) { + // Ensure 'id' exists in $attributes before trying to modify it + if ( ! isset( $attributes['id'] ) ) { + $attributes['id'] = ''; + } + + // When submitting the page number is not always set, so we need to handle that: TODO: This is a hack, we need to find a better way to handle form identification + $page_num = max( 1, intval( $page ) ); + + $attributes['id'] = $attributes['id'] . '-' . ( $count + 1 ) . '-' . $page_num; + } + return $attributes['id']; + } + + public static function get_post_request( $values, $form_id = null, $post_id = 0 ) { + $prefix = $form_id ? $form_id : 'g' . $post_id; + $post_data = array(); + foreach ( $values as $key => $val ) { + if ( strpos( $key, 'contact-form' ) === 0 || strpos( $key, 'action' ) === 0 ) { + $post_data[ $key ] = $val; + } else { + $post_data[ $prefix . '-' . $key ] = $val; + } + } + return $post_data; + } +} From d45083f4a212eb9657ce7d7f921ab36896a51a55 Mon Sep 17 00:00:00 2001 From: Enej Bajgoric Date: Tue, 8 Jul 2025 15:16:44 -0700 Subject: [PATCH 02/48] Add tests for Form_Response::strip_tags method --- .../php/contact-form/Form_Response_Test.php | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/projects/packages/forms/tests/php/contact-form/Form_Response_Test.php b/projects/packages/forms/tests/php/contact-form/Form_Response_Test.php index 7a18da98fd803..51aeffee66d64 100644 --- a/projects/packages/forms/tests/php/contact-form/Form_Response_Test.php +++ b/projects/packages/forms/tests/php/contact-form/Form_Response_Test.php @@ -1127,4 +1127,156 @@ public function test_compute_entry_permalink_with_page_number() { $this->assertStringContainsString( 'page=999', $response->get_entry_permalink(), 'Post permalink should match the form submission' ); $this->assertStringContainsString( 'page=999', $post_response->get_entry_permalink(), 'Post permalink should match the saved form submission' ); } + /** + * Test that strip_tags handles simple string without HTML tags. + */ + public function test_strip_tags_with_plain_string() { + $input = 'Hello, this is a plain text string.'; + $expected = 'Hello, this is a plain text string.'; + $result = Form_Response::strip_tags( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test that strip_tags removes script tags but keeps the content. + */ + public function test_strip_tags_removes_script_tags() { + $input = 'Hello world!'; + $expected = 'alert("XSS")Hello world!'; + $result = Form_Response::strip_tags( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test that strip_tags handles HTML entities correctly. + */ + public function test_strip_tags_handles_html_entities() { + $input = 'Hello & goodbye'; + $expected = 'Hello & goodbye'; + $result = Form_Response::strip_tags( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test that strip_tags handles arrays recursively. + */ + public function test_strip_tags_handles_arrays() { + $input = array( + 'field1' => 'Hello world', + 'field2' => array( + 'nested' => 'Test bold text', + 'deep' => array( + 'deeper' => 'More & testing', + ), + ), + ); + + $expected = array( + 'field1' => 'Hello alert("XSS")world', + 'field2' => array( + 'nested' => 'Test bold text', + 'deep' => array( + 'deeper' => 'More & testing', + ), + ), + ); + + $result = Form_Response::strip_tags( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test that strip_tags sanitizes array keys. + */ + public function test_strip_tags_sanitizes_array_keys() { + $input = array( + '' => 'value1', + 'normal_key' => 'value2', + ); + + $result = Form_Response::strip_tags( $input ); + + // Check that the results exist and are correct + // We need to check what key actually gets created after sanitization + $keys = array_keys( $result ); + $this->assertCount( 2, $keys ); + $this->assertEquals( 'value2', $result['normal_key'] ); + + // Find the sanitized key (should be the one that's not 'normal_key') + $sanitized_key = null; + foreach ( $keys as $key ) { + if ( $key !== 'normal_key' ) { + $sanitized_key = $key; + break; + } + } + $this->assertEquals( 'value1', $result[ $sanitized_key ] ); + } + + /** + * Test that strip_tags handles empty values. + */ + public function test_strip_tags_handles_empty_values() { + $this->assertSame( '', Form_Response::strip_tags( '' ) ); + $this->assertEquals( array(), Form_Response::strip_tags( array() ) ); + $this->assertSame( '0', Form_Response::strip_tags( 0 ) ); + $this->assertSame( '', Form_Response::strip_tags( null ) ); + } + + /** + * Test that strip_tags handles numeric values. + */ + public function test_strip_tags_handles_numeric_values() { + $this->assertSame( '123', Form_Response::strip_tags( 123 ) ); + $this->assertSame( '123.45', Form_Response::strip_tags( 123.45 ) ); + } + + /** + * Test that strip_tags preserves allowed HTML tags as per wp_kses_post. + */ + public function test_strip_tags_preserves_allowed_html() { + $input = '

This is a test with emphasis and links.

'; + $result = Form_Response::strip_tags( $input ); + + // wp_kses_post should preserve these tags - let's just verify we get a non-empty string with HTML + $this->assertNotEquals( '', $result ); + $this->assertStringContainsString( 'This is a', $result ); + $this->assertStringContainsString( 'test', $result ); + } + + /** + * Test that strip_tags removes dangerous HTML tags. + */ + public function test_strip_tags_removes_dangerous_html() { + $input = 'Hello world'; + $result = Form_Response::strip_tags( $input ); + + // These dangerous tags should be removed, but content might remain + $this->assertStringNotContainsString( 'Hello world!'; $expected = 'alert("XSS")Hello world!'; - $result = Form_Response::strip_tags( $input ); + $result = Feedback::strip_tags( $input ); $this->assertEquals( $expected, $result ); } @@ -1221,7 +1221,7 @@ public function test_strip_tags_removes_script_tags() { public function test_strip_tags_handles_html_entities() { $input = 'Hello & goodbye'; $expected = 'Hello & goodbye'; - $result = Form_Response::strip_tags( $input ); + $result = Feedback::strip_tags( $input ); $this->assertEquals( $expected, $result ); } @@ -1249,7 +1249,7 @@ public function test_strip_tags_handles_arrays() { ), ); - $result = Form_Response::strip_tags( $input ); + $result = Feedback::strip_tags( $input ); $this->assertEquals( $expected, $result ); } @@ -1262,7 +1262,7 @@ public function test_strip_tags_sanitizes_array_keys() { 'normal_key' => 'value2', ); - $result = Form_Response::strip_tags( $input ); + $result = Feedback::strip_tags( $input ); // Check that the results exist and are correct // We need to check what key actually gets created after sanitization @@ -1285,18 +1285,18 @@ public function test_strip_tags_sanitizes_array_keys() { * Test that strip_tags handles empty values. */ public function test_strip_tags_handles_empty_values() { - $this->assertSame( '', Form_Response::strip_tags( '' ) ); - $this->assertEquals( array(), Form_Response::strip_tags( array() ) ); - $this->assertSame( '0', Form_Response::strip_tags( 0 ) ); - $this->assertSame( '', Form_Response::strip_tags( null ) ); + $this->assertSame( '', Feedback::strip_tags( '' ) ); + $this->assertEquals( array(), Feedback::strip_tags( array() ) ); + $this->assertSame( '0', Feedback::strip_tags( 0 ) ); + $this->assertSame( '', Feedback::strip_tags( null ) ); } /** * Test that strip_tags handles numeric values. */ public function test_strip_tags_handles_numeric_values() { - $this->assertSame( '123', Form_Response::strip_tags( 123 ) ); - $this->assertSame( '123.45', Form_Response::strip_tags( 123.45 ) ); + $this->assertSame( '123', Feedback::strip_tags( 123 ) ); + $this->assertSame( '123.45', Feedback::strip_tags( 123.45 ) ); } /** @@ -1304,7 +1304,7 @@ public function test_strip_tags_handles_numeric_values() { */ public function test_strip_tags_preserves_allowed_html() { $input = '

This is a test with emphasis and links.

'; - $result = Form_Response::strip_tags( $input ); + $result = Feedback::strip_tags( $input ); // wp_kses_post should preserve these tags - let's just verify we get a non-empty string with HTML $this->assertNotEquals( '', $result ); @@ -1317,7 +1317,7 @@ public function test_strip_tags_preserves_allowed_html() { */ public function test_strip_tags_removes_dangerous_html() { $input = 'Hello world'; - $result = Form_Response::strip_tags( $input ); + $result = Feedback::strip_tags( $input ); // These dangerous tags should be removed, but content might remain $this->assertStringNotContainsString( 'Hello world!'; + $expected = 'alert("XSS")Hello world!'; + $result = Contact_Form_Plugin::strip_tags( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test that strip_tags handles HTML entities correctly. + */ + public function test_strip_tags_handles_html_entities() { + $input = 'Hello & goodbye'; + $expected = 'Hello & goodbye'; + $result = Contact_Form_Plugin::strip_tags( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test that strip_tags handles arrays recursively. + */ + public function test_strip_tags_handles_arrays() { + $input = array( + 'field1' => 'Hello world', + 'field2' => array( + 'nested' => 'Test bold text', + 'deep' => array( + 'deeper' => 'More & testing', + ), + ), + ); + + $expected = array( + 'field1' => 'Hello alert("XSS")world', + 'field2' => array( + 'nested' => 'Test bold text', + 'deep' => array( + 'deeper' => 'More & testing', + ), + ), + ); + + $result = Contact_Form_Plugin::strip_tags( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test that strip_tags sanitizes array keys. + */ + public function test_strip_tags_sanitizes_array_keys() { + $input = array( + '' => 'value1', + 'normal_key' => 'value2', + ); + + $result = Contact_Form_Plugin::strip_tags( $input ); + + // Check that the results exist and are correct + // We need to check what key actually gets created after sanitization + $keys = array_keys( $result ); + $this->assertCount( 2, $keys ); + $this->assertEquals( 'value2', $result['normal_key'] ); + + // Find the sanitized key (should be the one that's not 'normal_key') + $sanitized_key = null; + foreach ( $keys as $key ) { + if ( $key !== 'normal_key' ) { + $sanitized_key = $key; + break; + } + } + $this->assertEquals( 'value1', $result[ $sanitized_key ] ); + } + + /** + * Test that strip_tags handles empty values. + */ + public function test_strip_tags_handles_empty_values() { + $this->assertSame( '', Contact_Form_Plugin::strip_tags( '' ) ); + $this->assertEquals( array(), Contact_Form_Plugin::strip_tags( array() ) ); + $this->assertSame( '0', Contact_Form_Plugin::strip_tags( 0 ) ); + $this->assertSame( '', Contact_Form_Plugin::strip_tags( null ) ); + } + + /** + * Test that strip_tags handles numeric values. + */ + public function test_strip_tags_handles_numeric_values() { + $this->assertSame( '123', Contact_Form_Plugin::strip_tags( 123 ) ); + $this->assertSame( '123.45', Contact_Form_Plugin::strip_tags( 123.45 ) ); + } + + /** + * Test that strip_tags preserves allowed HTML tags as per wp_kses_post. + */ + public function test_strip_tags_preserves_allowed_html() { + $input = '

This is a test with emphasis and links.

'; + $result = Contact_Form_Plugin::strip_tags( $input ); + + // wp_kses_post should preserve these tags - let's just verify we get a non-empty string with HTML + $this->assertNotEquals( '', $result ); + $this->assertStringContainsString( 'This is a', $result ); + $this->assertStringContainsString( 'test', $result ); + } + + /** + * Test that strip_tags removes dangerous HTML tags. + */ + public function test_strip_tags_removes_dangerous_html() { + $input = 'Hello world'; + $result = Contact_Form_Plugin::strip_tags( $input ); + + // These dangerous tags should be removed, but content might remain + $this->assertStringNotContainsString( 'Hello world!'; - $expected = 'alert("XSS")Hello world!'; - $result = Feedback::strip_tags( $input ); - $this->assertEquals( $expected, $result ); - } - - /** - * Test that strip_tags handles HTML entities correctly. - */ - public function test_strip_tags_handles_html_entities() { - $input = 'Hello & goodbye'; - $expected = 'Hello & goodbye'; - $result = Feedback::strip_tags( $input ); - $this->assertEquals( $expected, $result ); - } - - /** - * Test that strip_tags handles arrays recursively. - */ - public function test_strip_tags_handles_arrays() { - $input = array( - 'field1' => 'Hello world', - 'field2' => array( - 'nested' => 'Test bold text', - 'deep' => array( - 'deeper' => 'More & testing', - ), - ), - ); - - $expected = array( - 'field1' => 'Hello alert("XSS")world', - 'field2' => array( - 'nested' => 'Test bold text', - 'deep' => array( - 'deeper' => 'More & testing', - ), - ), - ); - - $result = Feedback::strip_tags( $input ); - $this->assertEquals( $expected, $result ); - } - - /** - * Test that strip_tags sanitizes array keys. - */ - public function test_strip_tags_sanitizes_array_keys() { - $input = array( - '' => 'value1', - 'normal_key' => 'value2', - ); - - $result = Feedback::strip_tags( $input ); - - // Check that the results exist and are correct - // We need to check what key actually gets created after sanitization - $keys = array_keys( $result ); - $this->assertCount( 2, $keys ); - $this->assertEquals( 'value2', $result['normal_key'] ); - - // Find the sanitized key (should be the one that's not 'normal_key') - $sanitized_key = null; - foreach ( $keys as $key ) { - if ( $key !== 'normal_key' ) { - $sanitized_key = $key; - break; - } - } - $this->assertEquals( 'value1', $result[ $sanitized_key ] ); - } - - /** - * Test that strip_tags handles empty values. - */ - public function test_strip_tags_handles_empty_values() { - $this->assertSame( '', Feedback::strip_tags( '' ) ); - $this->assertEquals( array(), Feedback::strip_tags( array() ) ); - $this->assertSame( '0', Feedback::strip_tags( 0 ) ); - $this->assertSame( '', Feedback::strip_tags( null ) ); - } - - /** - * Test that strip_tags handles numeric values. - */ - public function test_strip_tags_handles_numeric_values() { - $this->assertSame( '123', Feedback::strip_tags( 123 ) ); - $this->assertSame( '123.45', Feedback::strip_tags( 123.45 ) ); - } - - /** - * Test that strip_tags preserves allowed HTML tags as per wp_kses_post. - */ - public function test_strip_tags_preserves_allowed_html() { - $input = '

This is a test with emphasis and links.

'; - $result = Feedback::strip_tags( $input ); - - // wp_kses_post should preserve these tags - let's just verify we get a non-empty string with HTML - $this->assertNotEquals( '', $result ); - $this->assertStringContainsString( 'This is a', $result ); - $this->assertStringContainsString( 'test', $result ); - } - - /** - * Test that strip_tags removes dangerous HTML tags. - */ - public function test_strip_tags_removes_dangerous_html() { - $input = 'Hello world'; - $result = Feedback::strip_tags( $input ); - - // These dangerous tags should be removed, but content might remain - $this->assertStringNotContainsString( '