'',
'success' => '',
'group' => '',
),
(array) $atts,
'at_form'
);
$settings = Settings::get_settings();
if ( empty( $settings['form_enabled'] ) ) {
return '';
}
Assets::register_style();
if ( Settings::get( 'enable_css', 1 ) ) {
wp_enqueue_style( Assets::STYLE );
}
// PRG success state.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['at_submitted'] ) && '1' === $_GET['at_submitted'] ) {
$msg = '' !== $atts['success']
? $atts['success']
: ( ! empty( $settings['form_success_message'] )
? $settings['form_success_message']
: __( 'Thank you! Your testimonial has been submitted and is awaiting review.', 'advanced-testimonial' ) );
return '
' . esc_html( $msg ) . '
';
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$error = isset( $_GET['at_form_error'] ) ? sanitize_key( $_GET['at_form_error'] ) : '';
$loader = new TemplateLoader();
return $loader->render(
$loader->locate( 'form' ),
array(
'at_form_atts' => $atts,
'at_form_settings' => $settings,
'at_form_error' => $error,
)
);
}
/**
* Process the form POST on template_redirect.
*
* @return void
*/
public function handle() {
if ( empty( $_POST[ self::ACTION_FIELD ] ) ) {
return;
}
$settings = Settings::get_settings();
// Enforced here as well as in render(): a page carrying the form can
// sit in a page cache, and its nonce stays valid for up to 24 hours,
// so switching the form off would otherwise keep accepting
// submissions until every cached nonce had expired.
if ( empty( $settings['form_enabled'] ) ) {
return;
}
// Nonce.
if ( ! wp_verify_nonce(
isset( $_POST[ self::NONCE_FIELD ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::NONCE_FIELD ] ) ) : '',
self::NONCE_ACTION
) ) {
// Nearly always an expired nonce on a cached page rather than an
// attack, so send the visitor back with a message they can act on.
$this->redirect_error( 'expired' );
}
// Honeypot — bots fill hidden fields that real users never see.
if ( ! empty( $_POST['at_url_confirm'] ) ) {
$this->redirect_success(); // Pretend success so bots don't know they failed.
}
// Rate limit: 1 successful submission per key per 5 minutes. The key
// defaults to the visitor IP, but behind a proxy or Cloudflare every
// visitor shares one REMOTE_ADDR — the filter lets the owner supply
// the real client IP (or any other per-visitor key) instead.
$rate_ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
$rate_key = self::RATE_PREFIX . md5(
/**
* Filter the rate-limit key for the submission form.
*
* @param string $rate_ip Visitor IP from REMOTE_ADDR.
*/
(string) apply_filters( 'advanced_testimonial_rate_limit_key', $rate_ip )
);
if ( get_transient( $rate_key ) ) {
$this->redirect_error( 'rate_limit' );
}
// Required fields.
$name = sanitize_text_field( wp_unslash( isset( $_POST['at_name'] ) ? $_POST['at_name'] : '' ) );
$content = sanitize_textarea_field( wp_unslash( isset( $_POST['at_content'] ) ? $_POST['at_content'] : '' ) );
if ( '' === $name || '' === $content ) {
$this->redirect_error( 'required' );
}
// Cap the free-text fields so a scripted POST cannot store megabytes.
if ( mb_strlen( $name ) > 200 || mb_strlen( $content ) > 5000 ) {
$this->redirect_error( 'too_long' );
}
// Rating.
$rating = min( 5, max( 0, (int) sanitize_text_field( wp_unslash( isset( $_POST['at_rating'] ) ? $_POST['at_rating'] : '0' ) ) ) );
if ( ! empty( $settings['form_require_rating'] ) && $rating < 1 ) {
$this->redirect_error( 'rating_required' );
}
// Optional fields.
$headline = sanitize_text_field( wp_unslash( isset( $_POST['at_headline'] ) ? $_POST['at_headline'] : '' ) );
$company = sanitize_text_field( wp_unslash( isset( $_POST['at_company'] ) ? $_POST['at_company'] : '' ) );
$designation = sanitize_text_field( wp_unslash( isset( $_POST['at_designation'] ) ? $_POST['at_designation'] : '' ) );
$location = sanitize_text_field( wp_unslash( isset( $_POST['at_location'] ) ? $_POST['at_location'] : '' ) );
$email = sanitize_email( wp_unslash( isset( $_POST['at_email'] ) ? $_POST['at_email'] : '' ) );
$website = esc_url_raw( wp_unslash( isset( $_POST['at_website'] ) ? $_POST['at_website'] : '' ) );
$group = sanitize_text_field( wp_unslash( isset( $_POST['at_group'] ) ? $_POST['at_group'] : '' ) );
// Insert post.
$status = ! empty( $settings['form_auto_publish'] ) ? 'publish' : 'pending';
$post_id = wp_insert_post(
array(
'post_type' => CPT::POST_TYPE,
'post_title' => $name,
'post_content' => $content,
'post_status' => $status,
)
);
if ( is_wp_error( $post_id ) || ! $post_id ) {
$this->redirect_error( 'server' );
}
// Save meta.
$meta_map = array(
'headline' => $headline,
'company' => $company,
'designation' => $designation,
'location' => $location,
'website' => $website,
);
foreach ( $meta_map as $key => $val ) {
if ( '' !== $val ) {
update_post_meta( $post_id, Helpers::meta_key( $key ), $val );
}
}
if ( $rating > 0 ) {
update_post_meta( $post_id, Helpers::meta_key( 'rating' ), (string) $rating );
}
if ( '' !== $email ) {
update_post_meta( $post_id, Helpers::meta_key( 'email' ), $email );
}
// Assign taxonomy group.
if ( '' !== $group ) {
$term = get_term_by( 'slug', $group, Taxonomy::TAXONOMY );
if ( $term ) {
wp_set_object_terms( $post_id, array( $term->term_id ), Taxonomy::TAXONOMY );
}
}
// Set rate limit transient.
set_transient( $rate_key, 1, 5 * MINUTE_IN_SECONDS );
// Notify admin.
$this->notify_admin( $post_id, $name, $content, $email, $settings );
$this->redirect_success();
}
/**
* Send an admin notification email.
*
* @param int $post_id Newly inserted post ID.
* @param string $name Submitter name.
* @param string $content Review text.
* @param string $email Submitter email (may be empty).
* @param array $settings Plugin settings.
* @return void
*/
private function notify_admin( $post_id, $name, $content, $email, $settings ) {
// Fall back to the site admin if the configured address is not valid,
// so a typo in Settings can never silently drop notifications.
$to = ! empty( $settings['form_notify_email'] ) && is_email( $settings['form_notify_email'] )
? $settings['form_notify_email']
: get_option( 'admin_email' );
$site = get_bloginfo( 'name' );
$subject = sprintf(
/* translators: %s: site name */
__( '[%s] New testimonial submitted', 'advanced-testimonial' ),
$site
);
$edit_url = admin_url( 'post.php?post=' . $post_id . '&action=edit' );
$body = sprintf(
/* translators: 1: author name, 2: author email, 3: review text, 4: edit URL */
__( "Author: %1\$s\nEmail: %2\$s\n\nReview:\n%3\$s\n\nReview it here:\n%4\$s", 'advanced-testimonial' ),
$name,
'' !== $email ? $email : '—',
$content,
$edit_url
);
wp_mail( $to, $subject, $body );
}
/**
* Redirect to the current URL with ?at_submitted=1.
*
* @return void
*/
private function redirect_success() {
wp_safe_redirect(
add_query_arg( 'at_submitted', '1', remove_query_arg( array( 'at_submitted', 'at_form_error' ) ) )
);
exit;
}
/**
* Redirect to the current URL with ?at_form_error={code}.
*
* @param string $code Error code.
* @return void
*/
private function redirect_error( $code ) {
wp_safe_redirect(
add_query_arg( 'at_form_error', $code, remove_query_arg( array( 'at_submitted', 'at_form_error' ) ) )
);
exit;
}
}