'', 'text' => '', 'button' => get_option( 'postaramail_button_text', __( 'Subscribe', 'postaramail' ) ), 'name' => 'yes', 'automation' => '', 'color' => '', 'consent' => get_option( 'postaramail_consent_text', __( 'Yes, I’d like to receive emails. I can unsubscribe at any time.', 'postaramail' ) ), 'success' => get_option( 'postaramail_success_text', __( 'Thanks! You’re subscribed.', 'postaramail' ) ), ); } public static function shortcode( $atts ) { $a = shortcode_atts( self::defaults(), $atts, 'postaramail_form' ); return self::render( $a ); } public static function render( $a ) { if ( ! PostaraMail_API::configured() ) { return current_user_can( 'manage_options' ) ? '
' . esc_html__( 'PostaraMail form: connect your site (PostaraMail menu in the sidebar) to show the form.', 'postaramail' ) . '
' : ''; } wp_enqueue_style( 'postaramail-form' ); wp_enqueue_script( 'postaramail-form' ); if ( ! self::$used ) { self::$used = true; wp_localize_script( 'postaramail-form', 'PostaraMailForm', array( 'rest' => esc_url_raw( rest_url( 'postaramail/v1/subscribe' ) ), 'error' => __( 'Sorry, something went wrong. Please try again.', 'postaramail' ), ) ); } $uid = 'pm-' . wp_rand( 1000, 999999 ); $automation = '' !== (string) $a['automation'] ? absint( $a['automation'] ) : (int) get_option( 'postaramail_default_automation', 0 ); $color = preg_match( '/^#[0-9a-fA-F]{6}$/', (string) $a['color'] ) ? $a['color'] : ''; $result = isset( $_GET['pm_result'] ) ? sanitize_key( wp_unslash( $_GET['pm_result'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $style = $color ? ' style="--pm-accent:' . esc_attr( $color ) . ';"' : ''; $message = ''; if ( 'ok' === $result ) { $message = $a['success']; } elseif ( 'invalid_email' === $result ) { $message = __( 'Please enter a valid email address.', 'postaramail' ); } elseif ( 'consent' === $result ) { $message = __( 'Please tick the box to confirm you would like to receive emails.', 'postaramail' ); } elseif ( 'error' === $result ) { $message = __( 'Sorry, we couldn’t sign you up just now. Please try again in a little while.', 'postaramail' ); } ob_start(); ?> 'postaramail-block', 'render_callback' => array( __CLASS__, 'render_block' ), 'attributes' => array( 'title' => array( 'type' => 'string', 'default' => '', ), 'text' => array( 'type' => 'string', 'default' => '', ), 'button' => array( 'type' => 'string', 'default' => '', ), 'showName' => array( 'type' => 'boolean', 'default' => true, ), 'automation' => array( 'type' => 'string', 'default' => '', ), 'color' => array( 'type' => 'string', 'default' => '', ), ), ) ); } public static function render_block( $attrs ) { $a = self::defaults(); foreach ( array( 'title', 'text', 'button', 'automation', 'color' ) as $k ) { if ( ! empty( $attrs[ $k ] ) ) { $a[ $k ] = $attrs[ $k ]; } } $a['name'] = ( isset( $attrs['showName'] ) && false === $attrs['showName'] ) ? 'no' : 'yes'; return self::render( $a ); } // ---------- Submitting ---------- /** Signed so visitors can't enrol themselves in automations the site owner didn't choose. */ private static function sign( $value ) { return substr( hash_hmac( 'sha256', 'pm-sign:' . $value, wp_salt( 'auth' ) ), 0, 24 ); } public static function register_route() { register_rest_route( 'postaramail/v1', '/subscribe', array( 'methods' => 'POST', 'permission_callback' => '__return_true', 'callback' => array( __CLASS__, 'rest_subscribe' ), ) ); } /** Shared by the REST route and the no-JavaScript POST. */ public static function process( $p ) { // Honeypot: bots fill the hidden field. Pretend it worked. if ( ! empty( $p['pm_website'] ) ) { return array( 'ok' => true, 'bot' => true, ); } // Real people need a couple of seconds to fill in a form. The time comes from the server // (signed when the page was built), so a wrong clock on the visitor's device can't matter, // and pages cached for hours still work. $ts = isset( $p['pm_ts'] ) ? (int) $p['pm_ts'] : 0; if ( $ts > 0 && isset( $p['pm_tsig'] ) && hash_equals( self::sign( 'ts' . $ts ), (string) $p['pm_tsig'] ) && ( time() - $ts ) < 2 ) { return array( 'ok' => true, 'bot' => true, ); } if ( self::rate_limited() ) { return array( 'ok' => false, 'code' => 'rate_limited', 'message' => __( 'Too many attempts. Please wait a minute and try again.', 'postaramail' ), ); } if ( empty( $p['pm_consent'] ) ) { return array( 'ok' => false, 'code' => 'consent_required', ); } $email = isset( $p['pm_email'] ) ? sanitize_email( wp_unslash( $p['pm_email'] ) ) : ''; if ( ! is_email( $email ) ) { return array( 'ok' => false, 'code' => 'invalid_email', ); } $automation = isset( $p['pm_auto'] ) ? (int) $p['pm_auto'] : 0; $sig = isset( $p['pm_sig'] ) ? (string) $p['pm_sig'] : ''; if ( $automation > 0 && ! hash_equals( self::sign( 'a' . $automation ), $sig ) ) { $automation = 0; // Tampered: ignore it, still add the contact. } list( $first, $last ) = PostaraMail_Subscriber::split_name( isset( $p['pm_name'] ) ? wp_unslash( $p['pm_name'] ) : '' ); return PostaraMail_Subscriber::subscribe( array( 'email' => $email, 'first' => $first, 'last' => $last, 'automation' => $automation, 'source' => 'form', 'consent' => get_option( 'postaramail_consent_text', __( 'Yes, I’d like to receive emails. I can unsubscribe at any time.', 'postaramail' ) ) . ' [form]', ) ); } public static function rest_subscribe( $request ) { $r = self::process( $request->get_params() ); $success = get_option( 'postaramail_success_text', __( 'Thanks! You’re subscribed.', 'postaramail' ) ); if ( ! empty( $r['ok'] ) ) { return rest_ensure_response( array( 'ok' => true, 'message' => $success, ) ); } if ( isset( $r['code'] ) && 'consent_required' === $r['code'] ) { $r['message'] = __( 'Please tick the box to confirm you would like to receive emails.', 'postaramail' ); } $msg = isset( $r['message'] ) && $r['message'] ? $r['message'] : PostaraMail_Subscriber::visitor_message( $r, $success ); return new WP_REST_Response( array( 'ok' => false, 'message' => $msg, ), 'rate_limited' === ( isset( $r['code'] ) ? $r['code'] : '' ) ? 429 : 422 ); } public static function handle_post() { $r = self::process( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- public form on cached pages; protected by honeypot, timing and rate limit. $back = isset( $_POST['pm_back'] ) ? esc_url_raw( wp_unslash( $_POST['pm_back'] ) ) : home_url( '/' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $back = wp_validate_redirect( $back, home_url( '/' ) ); if ( ! empty( $r['ok'] ) ) { $code = 'ok'; } elseif ( isset( $r['code'] ) && 'invalid_email' === $r['code'] ) { $code = 'invalid_email'; } elseif ( isset( $r['code'] ) && 'consent_required' === $r['code'] ) { $code = 'consent'; } else { $code = 'error'; } wp_safe_redirect( add_query_arg( 'pm_result', $code, $back ) ); exit; } /** 8 attempts per 10 minutes per visitor address. */ private static function rate_limited() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown'; $key = 'pm_rl_' . md5( $ip ); $n = (int) get_transient( $key ); if ( $n >= 8 ) { return true; } set_transient( $key, $n + 1, 10 * MINUTE_IN_SECONDS ); return false; } }