t, $all_values ); $url = $widget ? home_url( '/' ) : get_permalink( $post->ID ); $date_time_format = _x( '%1$s \a\t %2$s', '{$date_format} \a\t {$time_format}', 'jetpack' ); $date_time_format = sprintf( $date_time_format, get_option( 'date_format' ), get_option( 'time_format' ) ); $time = date_i18n( $date_time_format, current_time( 'timestamp' ) ); // keep a copy of the feedback as a custom post type $feedback_status = $is_spam === true ? 'spam' : 'publish'; foreach ( (array) $akismet_values as $av_key => $av_value ) { $akismet_values[ $av_key ] = Grunion_Contact_Form_Plugin::strip_tags( $av_value ); } foreach ( (array) $all_values as $all_key => $all_value ) { $all_values[ $all_key ] = Grunion_Contact_Form_Plugin::strip_tags( $all_value ); } foreach ( (array) $extra_values as $ev_key => $ev_value ) { $extra_values[ $ev_key ] = Grunion_Contact_Form_Plugin::strip_tags( $ev_value ); } /* We need to make sure that the post author is always zero for contact * form submissions. This prevents export/import from trying to create * new users based on form submissions from people who were logged in * at the time. * * Unfortunately wp_insert_post() tries very hard to make sure the post * author gets the currently logged in user id. That is how we ended up * with this work around. */ add_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10, 2 ); $post_id = wp_insert_post( array( 'post_date' => addslashes( $feedback_time ), 'post_type' => 'feedback', 'post_status' => addslashes( $feedback_status ), 'post_parent' => (int) $post->ID, 'post_title' => addslashes( wp_kses( $feedback_title, array() ) ), 'post_content' => addslashes( wp_kses( $comment_content . "\n\n" . "AUTHOR: {$comment_author}\nAUTHOR EMAIL: {$comment_author_email}\nAUTHOR URL: {$comment_author_url}\nSUBJECT: {$subject}\nIP: {$comment_author_IP}\n" . @print_r( $all_values, true ), array() ) ), // so that search will pick up this data 'post_name' => $feedback_id, ) ); // once insert has finished we don't need this filter any more remove_filter( 'wp_insert_post_data', array( $plugin, 'insert_feedback_filter' ), 10 ); update_post_meta( $post_id, '_feedback_extra_fields', $this->addslashes_deep( $extra_values ) ); if ( 'publish' == $feedback_status ) { // Increase count of unread feedback. $unread = get_option( 'feedback_unread_count', 0 ) + 1; update_option( 'feedback_unread_count', $unread ); } if ( defined( 'AKISMET_VERSION' ) ) { update_post_meta( $post_id, '_feedback_akismet_values', $this->addslashes_deep( $akismet_values ) ); } $message = self::get_compiled_form( $post_id, $this ); array_push( $message, "
", '
', __( 'Time:', 'jetpack' ) . ' ' . $time . '
', __( 'IP Address:', 'jetpack' ) . ' ' . $comment_author_IP . '
', __( 'Contact Form URL:', 'jetpack' ) . ' ' . $url . '
' ); if ( is_user_logged_in() ) { array_push( $message, sprintf( '

' . __( 'Sent by a verified %s user.', 'jetpack' ) . '

', isset( $GLOBALS['current_site']->site_name ) && $GLOBALS['current_site']->site_name ? $GLOBALS['current_site']->site_name : '"' . get_option( 'blogname' ) . '"' ) ); } else { array_push( $message, '

' . __( 'Sent by an unverified visitor to your site.', 'jetpack' ) . '

' ); } $message = join( $message, '' ); /** * Filters the message sent via email after a successful form submission. * * @module contact-form * * @since 1.3.1 * * @param string $message Feedback email message. */ $message = apply_filters( 'contact_form_message', $message ); // This is called after `contact_form_message`, in order to preserve back-compat $message = self::wrap_message_in_html_tags( $message ); update_post_meta( $post_id, '_feedback_email', $this->addslashes_deep( compact( 'to', 'message' ) ) ); /** * Fires right before the contact form message is sent via email to * the recipient specified in the contact form. * * @module contact-form * * @since 1.3.1 * * @param integer $post_id Post contact form lives on * @param array $all_values Contact form fields * @param array $extra_values Contact form fields not included in $all_values */ do_action( 'grunion_pre_message_sent', $post_id, $all_values, $extra_values ); // schedule deletes of old spam feedbacks if ( ! wp_next_scheduled( 'grunion_scheduled_delete' ) ) { wp_schedule_event( time() + 250, 'daily', 'grunion_scheduled_delete' ); } if ( $is_spam !== true && /** * Filter to choose whether an email should be sent after each successful contact form submission. * * @module contact-form * * @since 2.6.0 * * @param bool true Should an email be sent after a form submission. Default to true. * @param int $post_id Post ID. */ true === apply_filters( 'grunion_should_send_email', true, $post_id ) ) { self::wp_mail( $to, "{$spam}{$subject}", $message, $headers ); } elseif ( true === $is_spam && /** * Choose whether an email should be sent for each spam contact form submission. * * @module contact-form * * @since 1.3.1 * * @param bool false Should an email be sent after a spam form submission. Default to false. */ apply_filters( 'grunion_still_email_spam', false ) == true ) { // don't send spam by default. Filterable. self::wp_mail( $to, "{$spam}{$subject}", $message, $headers ); } if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { return self::success_message( $post_id, $this ); } $redirect = wp_get_referer(); if ( ! $redirect ) { // wp_get_referer() returns false if the referer is the same as the current page $redirect = $_SERVER['REQUEST_URI']; } $redirect = add_query_arg( urlencode_deep( array( 'contact-form-id' => $id, 'contact-form-sent' => $post_id, 'contact-form-hash' => $this->hash, '_wpnonce' => wp_create_nonce( "contact-form-sent-{$post_id}" ), // wp_nonce_url HTMLencodes :( ) ), $redirect ); /** * Filter the URL where the reader is redirected after submitting a form. * * @module contact-form * * @since 1.9.0 * * @param string $redirect Post submission URL. * @param int $id Contact Form ID. * @param int $post_id Post ID. */ $redirect = apply_filters( 'grunion_contact_form_redirect_url', $redirect, $id, $post_id ); wp_safe_redirect( $redirect ); exit; } /** * Wrapper for wp_mail() that enables HTML messages with text alternatives * * @param string|array $to Array or comma-separated list of email addresses to send message. * @param string $subject Email subject. * @param string $message Message contents. * @param string|array $headers Optional. Additional headers. * @param string|array $attachments Optional. Files to attach. * * @return bool Whether the email contents were sent successfully. */ public static function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) { add_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' ); add_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' ); $result = wp_mail( $to, $subject, $message, $headers, $attachments ); remove_filter( 'wp_mail_content_type', __CLASS__ . '::get_mail_content_type' ); remove_action( 'phpmailer_init', __CLASS__ . '::add_plain_text_alternative' ); return $result; } /** * Add a display name part to an email address * * SpamAssassin doesn't like addresses in HTML messages that are missing display names (e.g., `foo@bar.org` * instead of `"Foo Bar" `. * * @param string $address * * @return string */ function add_name_to_address( $address ) { // If it's just the address, without a display name if ( is_email( $address ) ) { $address_parts = explode( '@', $address ); $address = sprintf( '"%s" <%s>', $address_parts[0], $address ); } return $address; } /** * Get the content type that should be assigned to outbound emails * * @return string */ static function get_mail_content_type() { return 'text/html'; } /** * Wrap a message body with the appropriate in HTML tags * * This helps to ensure correct parsing by clients, and also helps avoid triggering spam filtering rules * * @param string $body * * @return string */ static function wrap_message_in_html_tags( $body ) { // Don't do anything if the message was already wrapped in HTML tags // That could have be done by a plugin via filters if ( false !== strpos( $body, ' %s " ), $body ); return $html_message; } /** * Add a plain-text alternative part to an outbound email * * This makes the message more accessible to mail clients that aren't HTML-aware, and decreases the likelihood * that the message will be flagged as spam. * * @param PHPMailer $phpmailer */ static function add_plain_text_alternative( $phpmailer ) { // Add an extra break so that the extra space above the

is preserved after the

is stripped out $alt_body = str_replace( '

', '


', $phpmailer->Body ); // Convert
to \n breaks, to preserve the space between lines that we want to keep $alt_body = str_replace( array( '
', '
' ), "\n", $alt_body ); // Convert


to an plain-text equivalent, to preserve the integrity of the message $alt_body = str_replace( array( "
", "
" ), "----\n", $alt_body ); // Trim the plain text message to remove the \n breaks that were after , , and $phpmailer->AltBody = trim( strip_tags( $alt_body ) ); } function addslashes_deep( $value ) { if ( is_array( $value ) ) { return array_map( array( $this, 'addslashes_deep' ), $value ); } elseif ( is_object( $value ) ) { $vars = get_object_vars( $value ); foreach ( $vars as $key => $data ) { $value->{$key} = $this->addslashes_deep( $data ); } return $value; } return addslashes( $value ); } } /** * Class for the contact-field shortcode. * Parses shortcode to output the contact form field as HTML. * Validates input. */ class Grunion_Contact_Form_Field extends Crunion_Contact_Form_Shortcode { public $shortcode_name = 'contact-field'; /** * @var Grunion_Contact_Form parent form */ public $form; /** * @var string default or POSTed value */ public $value; /** * @var bool Is the input invalid? */ public $error = false; /** * @param array $attributes An associative array of shortcode attributes. @see shortcode_atts() * @param null|string $content Null for selfclosing shortcodes. The inner content otherwise. * @param Grunion_Contact_Form $form The parent form */ function __construct( $attributes, $content = null, $form = null ) { $attributes = shortcode_atts( array( 'label' => null, 'type' => 'text', 'required' => false, 'options' => array(), 'id' => null, 'default' => null, 'values' => null, 'placeholder' => null, 'class' => null, ), $attributes, 'contact-field' ); // special default for subject field if ( 'subject' == $attributes['type'] && is_null( $attributes['default'] ) && ! is_null( $form ) ) { $attributes['default'] = $form->get_attribute( 'subject' ); } // allow required=1 or required=true if ( '1' == $attributes['required'] || 'true' == strtolower( $attributes['required'] ) ) { $attributes['required'] = true; } else { $attributes['required'] = false; } // parse out comma-separated options list (for selects, radios, and checkbox-multiples) if ( ! empty( $attributes['options'] ) && is_string( $attributes['options'] ) ) { $attributes['options'] = array_map( 'trim', explode( ',', $attributes['options'] ) ); if ( ! empty( $attributes['values'] ) && is_string( $attributes['values'] ) ) { $attributes['values'] = array_map( 'trim', explode( ',', $attributes['values'] ) ); } } if ( $form ) { // make a unique field ID based on the label, with an incrementing number if needed to avoid clashes $form_id = $form->get_attribute( 'id' ); $id = isset( $attributes['id'] ) ? $attributes['id'] : false; $unescaped_label = $this->unesc_attr( $attributes['label'] ); $unescaped_label = str_replace( '%', '-', $unescaped_label ); // jQuery doesn't like % in IDs? $unescaped_label = preg_replace( '/[^a-zA-Z0-9.-_:]/', '', $unescaped_label ); if ( empty( $id ) ) { $id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label ); $i = 0; $max_tries = 99; while ( isset( $form->fields[ $id ] ) ) { $i++; $id = sanitize_title_with_dashes( 'g' . $form_id . '-' . $unescaped_label . '-' . $i ); if ( $i > $max_tries ) { break; } } } $attributes['id'] = $id; } parent::__construct( $attributes, $content ); // Store parent form $this->form = $form; } /** * This field's input is invalid. Flag as invalid and add an error to the parent form * * @param string $message The error message to display on the form. */ function add_error( $message ) { $this->is_error = true; if ( ! is_wp_error( $this->form->errors ) ) { $this->form->errors = new WP_Error; } $this->form->errors->add( $this->get_attribute( 'id' ), $message ); } /** * Is the field input invalid? * * @see $error * * @return bool */ function is_error() { return $this->error; } /** * Validates the form input */ function validate() { // If it's not required, there's nothing to validate if ( ! $this->get_attribute( 'required' ) ) { return; } $field_id = $this->get_attribute( 'id' ); $field_type = $this->get_attribute( 'type' ); $field_label = $this->get_attribute( 'label' ); if ( isset( $_POST[ $field_id ] ) ) { if ( is_array( $_POST[ $field_id ] ) ) { $field_value = array_map( 'stripslashes', $_POST[ $field_id ] ); } else { $field_value = stripslashes( $_POST[ $field_id ] ); } } else { $field_value = ''; } switch ( $field_type ) { case 'email' : // Make sure the email address is valid if ( ! is_email( $field_value ) ) { /* translators: %s is the name of a form field */ $this->add_error( sprintf( __( '%s requires a valid email address', 'jetpack' ), $field_label ) ); } break; case 'checkbox-multiple' : // Check that there is at least one option selected if ( empty( $field_value ) ) { /* translators: %s is the name of a form field */ $this->add_error( sprintf( __( '%s requires at least one selection', 'jetpack' ), $field_label ) ); } break; default : // Just check for presence of any text if ( ! strlen( trim( $field_value ) ) ) { /* translators: %s is the name of a form field */ $this->add_error( sprintf( __( '%s is required', 'jetpack' ), $field_label ) ); } } } /** * Check the default value for options field * * @param string value * @param int index * @param string default value * * @return string */ public function get_option_value( $value, $index, $options ) { if ( empty( $value[ $index ] ) ) { return $options; } return $value[ $index ]; } /** * Outputs the HTML for this form field * * @return string HTML */ function render() { global $current_user, $user_identity; $r = ''; $field_id = $this->get_attribute( 'id' ); $field_type = $this->get_attribute( 'type' ); $field_label = $this->get_attribute( 'label' ); $field_required = $this->get_attribute( 'required' ); $placeholder = $this->get_attribute( 'placeholder' ); $class = 'date' === $field_type ? 'jp-contact-form-date' : $this->get_attribute( 'class' ); $field_placeholder = ( ! empty( $placeholder ) ) ? "placeholder='" . esc_attr( $placeholder ) . "'" : ''; $field_class = "class='" . trim( esc_attr( $field_type ) . ' ' . esc_attr( $class ) ) . "' "; if ( isset( $_POST[ $field_id ] ) ) { if ( is_array( $_POST[ $field_id ] ) ) { $this->value = array_map( 'stripslashes', $_POST[ $field_id ] ); } else { $this->value = stripslashes( (string) $_POST[ $field_id ] ); } } elseif ( isset( $_GET[ $field_id ] ) ) { $this->value = stripslashes( (string) $_GET[ $field_id ] ); } elseif ( is_user_logged_in() && ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || /** * Allow third-party tools to prefill the contact form with the user's details when they're logged in. * * @module contact-form * * @since 3.2.0 * * @param bool false Should the Contact Form be prefilled with your details when you're logged in. Default to false. */ true === apply_filters( 'jetpack_auto_fill_logged_in_user', false ) ) ) { // Special defaults for logged-in users switch ( $this->get_attribute( 'type' ) ) { case 'email' : $this->value = $current_user->data->user_email; break; case 'name' : $this->value = $user_identity; break; case 'url' : $this->value = $current_user->data->user_url; break; default : $this->value = $this->get_attribute( 'default' ); } } else { $this->value = $this->get_attribute( 'default' ); } $field_value = Grunion_Contact_Form_Plugin::strip_tags( $this->value ); $field_label = Grunion_Contact_Form_Plugin::strip_tags( $field_label ); /** * Filter the Contact Form required field text * * @module contact-form * * @since 3.8.0 * * @param string $var Required field text. Default is "(required)". */ $required_field_text = esc_html( apply_filters( 'jetpack_required_field_text', __( '(required)', 'jetpack' ) ) ); switch ( $field_type ) { case 'email' : $r .= "\n
\n"; $r .= "\t\t\n"; $r .= "\t\t\n"; $r .= "\t
\n"; break; case 'telephone' : $r .= "\n
\n"; $r .= "\t\t\n"; $r .= "\t\t\n"; break; case 'url' : $r .= "\n
\n"; $r .= "\t\t\n"; $r .= "\t\t\n"; $r .= "\t
\n"; break; case 'textarea' : $r .= "\n
\n"; $r .= "\t\t\n"; $r .= "\t\t\n"; $r .= "\t
\n"; break; case 'radio' : $r .= "\t
\n"; foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) { $option = Grunion_Contact_Form_Plugin::strip_tags( $option ); $r .= "\t\t\n"; $r .= "\t\t
\n"; } $r .= "\t\t
\n"; break; case 'checkbox' : $r .= "\t
\n"; $r .= "\t\t\n"; $r .= "\t\t
\n"; $r .= "\t
\n"; break; case 'checkbox-multiple' : $r .= "\t
\n"; foreach ( $this->get_attribute( 'options' ) as $optionIndex => $option ) { $option = Grunion_Contact_Form_Plugin::strip_tags( $option ); $r .= "\t\t\n"; $r .= "\t\t
\n"; } $r .= "\t\t
\n"; break; case 'select' : $r .= "\n
\n"; $r .= "\t\t\n"; $r .= "\t\n"; $r .= "\t
\n"; break; case 'date' : $r .= "\n
\n"; $r .= "\t\t\n"; $r .= "\t\t\n"; $r .= "\t
\n"; wp_enqueue_script( 'grunion-frontend', plugins_url( 'js/grunion-frontend.js', __FILE__ ), array( 'jquery', 'jquery-ui-datepicker' ) ); wp_enqueue_style( 'jp-jquery-ui-datepicker', plugins_url( 'css/jquery-ui-datepicker.css', __FILE__ ), array( 'dashicons' ), '1.0' ); // Using Core's built-in datepicker localization routine wp_localize_jquery_ui_datepicker(); break; default : // text field // note that any unknown types will produce a text input, so we can use arbitrary type names to handle // input fields like name, email, url that require special validation or handling at POST $r .= "\n
\n"; $r .= "\t\t\n"; $r .= "\t\t\n"; $r .= "\t
\n"; } /** * Filter the HTML of the Contact Form. * * @module contact-form * * @since 2.6.0 * * @param string $r Contact Form HTML output. * @param string $field_label Field label. * @param int|null $id Post ID. */ return apply_filters( 'grunion_contact_form_field_html', $r, $field_label, ( in_the_loop() ? get_the_ID() : null ) ); } } add_action( 'init', array( 'Grunion_Contact_Form_Plugin', 'init' ) ); add_action( 'grunion_scheduled_delete', 'grunion_delete_old_spam' ); /** * Deletes old spam feedbacks to keep the posts table size under control */ function grunion_delete_old_spam() { global $wpdb; $grunion_delete_limit = 100; $now_gmt = current_time( 'mysql', 1 ); $sql = $wpdb->prepare( " SELECT `ID` FROM $wpdb->posts WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > `post_date_gmt` AND `post_type` = 'feedback' AND `post_status` = 'spam' LIMIT %d ", $now_gmt, $grunion_delete_limit ); $post_ids = $wpdb->get_col( $sql ); foreach ( (array) $post_ids as $post_id ) { // force a full delete, skip the trash wp_delete_post( $post_id, true ); } // Arbitrary check points for running OPTIMIZE // nothing special about 5000 or 11 // just trying to periodically recover deleted rows $random_num = mt_rand( 1, 5000 ); if ( /** * Filter how often the module run OPTIMIZE TABLE on the core WP tables. * * @module contact-form * * @since 1.3.1 * * @param int $random_num Random number. */ apply_filters( 'grunion_optimize_table', ( $random_num == 11 ) ) ) { $wpdb->query( "OPTIMIZE TABLE $wpdb->posts" ); } // if we hit the max then schedule another run if ( count( $post_ids ) >= $grunion_delete_limit ) { wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' ); } }