*/ private static $errors = array(); /** * Values of the form posted in this request. * * @var array */ private static $old = array(); /** * Hooks. * * @return void */ public static function init() { add_shortcode( 'paygrab_register', array( __CLASS__, 'shortcode' ) ); add_action( 'template_redirect', array( __CLASS__, 'handle' ), 5 ); } /** * Whether a sign-up must be confirmed by a link in an email before the member is created. * * On when PAYGRAB's setting is on or the TCD theme (PANDORA etc.) has its email verification on. * With YOMERU Pro's verification on, YOMERU's own sign-up screen is used and this is not asked. * * @return bool */ public static function verification_on() { $on = (bool) Paygrab_Settings::get( 'email_verification', true ) || paygrab_tcd_theme_verification_on(); /** * Filters whether the sign-up form of PAYGRAB asks for email verification. * * @param bool $on Whether verification is on. */ return (bool) apply_filters( 'paygrab_email_verification', $on ); } /** * Signed timestamp put in the form, to reject bots that post instantly. * * @return string */ private static function stamp() { $t = (string) time(); return $t . '.' . substr( hash_hmac( 'sha256', $t, wp_salt( 'nonce' ) ), 0, 16 ); } /** * Whether a posted timestamp is genuine and the form was not sent too fast. * * @param string $stamp Posted value. * @return bool */ private static function stamp_ok( $stamp ) { $parts = explode( '.', (string) $stamp ); if ( 2 !== count( $parts ) || ! hash_equals( substr( hash_hmac( 'sha256', $parts[0], wp_salt( 'nonce' ) ), 0, 16 ), $parts[1] ) ) { return false; } $age = time() - (int) $parts[0]; return $age >= self::MIN_SECS && $age <= DAY_IN_SECONDS; } /** * Handle the posted form, the verification link and the resend button. * * @return void */ public static function handle() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- the token itself is the secret. if ( isset( $_GET['paygrab_verify'] ) ) { self::verify( sanitize_text_field( wp_unslash( $_GET['paygrab_verify'] ) ) ); } // phpcs:enable if ( 'POST' !== paygrab_request_method() ) { // Members who are already logged in do not need the sign-up form. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- screen selection only. if ( paygrab_is_page( 'register' ) && is_user_logged_in() && ! isset( $_GET['paygrab_done'] ) && ! isset( $_GET['paygrab_used'] ) ) { wp_safe_redirect( paygrab_request_return() ); exit; } return; } if ( isset( $_POST['paygrab_register'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- submit() checks the nonce. self::submit(); } elseif ( isset( $_POST['paygrab_resend'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- resend() checks the nonce. self::resend(); } } /** * Process the sign-up form. * * @return void */ private static function submit() { $name = isset( $_POST['paygrab_name'] ) ? sanitize_text_field( wp_unslash( $_POST['paygrab_name'] ) ) : ''; $email = isset( $_POST['paygrab_email'] ) ? sanitize_email( wp_unslash( $_POST['paygrab_email'] ) ) : ''; $pass = isset( $_POST['paygrab_password'] ) ? (string) wp_unslash( $_POST['paygrab_password'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- passwords are hashed, never shown. $agree = ! empty( $_POST['paygrab_agree'] ); $redirect = isset( $_POST['redirect_to'] ) ? wp_validate_redirect( esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ), '' ) : ''; $source = paygrab_request_source(); $product = paygrab_request_product(); $replace = isset( $_POST['paygrab_replace'] ) ? sanitize_text_field( wp_unslash( $_POST['paygrab_replace'] ) ) : ''; $email2 = isset( $_POST['paygrab_email2'] ) ? sanitize_email( wp_unslash( $_POST['paygrab_email2'] ) ) : ''; $honeypot = isset( $_POST['paygrab_website'] ) ? (string) wp_unslash( $_POST['paygrab_website'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $stamp = isset( $_POST['paygrab_ts'] ) ? sanitize_text_field( wp_unslash( $_POST['paygrab_ts'] ) ) : ''; self::$old = array( 'name' => $name, 'email' => $email, ); if ( ! isset( $_POST['_paygrab_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_paygrab_nonce'] ) ), 'paygrab_register' ) ) { self::$errors['form'] = __( 'The page expired. Please try again.', 'paygrab' ); return; } // Bots: filled the hidden field or posted too fast. Pretend it worked, send nothing. if ( '' !== $honeypot || ! self::stamp_ok( $stamp ) ) { self::finish( '' ); } // Only sign-ups that send an email are counted (mistakes in the form are not). if ( paygrab_rate_limited( 'register', 5, HOUR_IN_SECONDS, false ) ) { self::$errors['form'] = __( 'Too many sign-ups in a short time. Please try again later.', 'paygrab' ); return; } if ( '' === $name ) { self::$errors['name'] = __( 'Enter your name.', 'paygrab' ); } if ( ! is_email( $email ) ) { self::$errors['email'] = __( 'Enter a valid email address.', 'paygrab' ); } elseif ( email_exists( $email ) ) { self::$errors['email'] = __( 'This email address is already registered.', 'paygrab' ); self::$errors['exists'] = '1'; } // Without verification a typo would make a member nobody can reach, so the address is typed twice. if ( ! self::verification_on() && empty( self::$errors['email'] ) && strtolower( $email ) !== strtolower( $email2 ) ) { self::$errors['email2'] = __( 'The email addresses do not match.', 'paygrab' ); } if ( strlen( $pass ) < 8 ) { self::$errors['password'] = __( 'Use at least 8 characters.', 'paygrab' ); } if ( ! $agree ) { self::$errors['agree'] = __( 'You need to agree to the terms of use.', 'paygrab' ); } if ( self::$errors ) { return; } $data = array( 'name' => $name, 'email' => $email, 'hash' => wp_hash_password( $pass ), 'redirect' => $redirect, 'source' => $source, 'product' => $product ? $product->ID : 0, 'created' => time(), ); if ( ! self::verification_on() ) { // Verification is off: the member is created now and logged in (no email is sent). $user_id = self::create_member( $data ); if ( is_wp_error( $user_id ) ) { self::$errors['form'] = __( 'We could not finish your registration. Please try again.', 'paygrab' ); return; } paygrab_rate_limited( 'register', 5, HOUR_IN_SECONDS ); wp_safe_redirect( self::done_url( $data ) ); exit; } $token = wp_generate_password( 40, false, false ); // Only the newest sign-up for an address stays valid. $previous = get_transient( 'paygrab_reg_mail_' . md5( strtolower( $email ) ) ); if ( $previous ) { self::forget( $previous ); } // "I typed the wrong address": the sign-up being corrected stops working. $old = '' !== $replace ? self::pending_by_ref( $replace ) : null; if ( $old ) { self::forget( $old['id'] ); } $id = hash( 'sha256', $token ); set_transient( 'paygrab_reg_' . $id, $data, self::LINK_TTL ); set_transient( 'paygrab_reg_mail_' . md5( strtolower( $email ) ), $id, self::LINK_TTL ); set_transient( 'paygrab_reg_resend_' . $id, $token, self::LINK_TTL ); Paygrab_Mailer::verify_email( $email, $name, self::verify_url( $token ), time() + self::LINK_TTL ); paygrab_rate_limited( 'register', 5, HOUR_IN_SECONDS ); set_transient( 'paygrab_reg_wait_' . $id, time(), self::RESEND_WAIT ); self::finish( $id ); } /** * Drop a pending sign-up (its link stops working). * * @param string $id Pending sign-up ID. * @return void */ private static function forget( $id ) { $data = get_transient( 'paygrab_reg_' . $id ); if ( is_array( $data ) ) { delete_transient( 'paygrab_reg_mail_' . md5( strtolower( $data['email'] ) ) ); } foreach ( array( 'paygrab_reg_', 'paygrab_reg_resend_', 'paygrab_reg_wait_', 'paygrab_reg_count_' ) as $prefix ) { delete_transient( $prefix . $id ); } } /** * Seconds until the verification email can be sent again (0 = now). * * @param string $id Pending sign-up ID. * @return int */ public static function resend_wait( $id ) { $at = (int) get_transient( 'paygrab_reg_wait_' . $id ); return $at ? max( 0, self::RESEND_WAIT - ( time() - $at ) ) : 0; } /** * Show "check your email" (Post/Redirect/Get, without personal data in the URL). * * @param string $id Pending sign-up ID ('' for silently dropped bots). * @return never */ private static function finish( $id ) { $ref = '' !== $id ? $id : hash( 'sha256', wp_generate_password( 20, false, false ) ); wp_safe_redirect( add_query_arg( 'paygrab_sent', substr( $ref, 0, 32 ), self::page_url() ) ); exit; } /** * Find a pending sign-up from the short reference in the URL. * * @param string $ref Reference. * @return array{id:string,data:array}|null */ private static function pending_by_ref( $ref ) { global $wpdb; $ref = preg_replace( '/[^a-f0-9]/', '', (string) $ref ); if ( 32 !== strlen( $ref ) ) { return null; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- finds a pending sign-up (a transient) by a name pattern; core has no function for that. $name = $wpdb->get_var( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT 1", $wpdb->esc_like( '_transient_paygrab_reg_' . $ref ) . '%' ) ); if ( ! $name ) { return null; } $id = substr( $name, strlen( '_transient_paygrab_reg_' ) ); $data = get_transient( 'paygrab_reg_' . $id ); return is_array( $data ) ? array( 'id' => $id, 'data' => $data ) : null; } /** * Send the verification email again. * * @return void */ private static function resend() { $ref = isset( $_POST['ref'] ) ? sanitize_text_field( wp_unslash( $_POST['ref'] ) ) : ''; if ( ! isset( $_POST['_paygrab_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_paygrab_nonce'] ) ), 'paygrab_resend' ) ) { return; } $pending = self::pending_by_ref( $ref ); $state = 'resent'; if ( $pending ) { $id = $pending['id']; $count = (int) get_transient( 'paygrab_reg_count_' . $id ); $token = get_transient( 'paygrab_reg_resend_' . $id ); if ( $count >= self::RESEND_MAX ) { $state = 'toomany'; } elseif ( self::resend_wait( $id ) > 0 ) { $state = 'wait'; } elseif ( $token ) { // The same link as before, so the earlier email keeps working too. Paygrab_Mailer::verify_email( $pending['data']['email'], $pending['data']['name'], self::verify_url( $token ), (int) $pending['data']['created'] + self::LINK_TTL ); set_transient( 'paygrab_reg_count_' . $id, $count + 1, HOUR_IN_SECONDS ); set_transient( 'paygrab_reg_wait_' . $id, time(), self::RESEND_WAIT ); } } wp_safe_redirect( add_query_arg( array( 'paygrab_sent' => $ref, 'resent' => $state ), self::page_url() ) ); exit; } /** * URL of the registration page. * * @return string */ private static function page_url() { $url = paygrab_page_url( 'register' ); return $url ? $url : home_url( '/' ); } /** * Verification link. * * @param string $token Token. * @return string */ private static function verify_url( $token ) { return add_query_arg( 'paygrab_verify', $token, self::page_url() ); } /** * Open the verification link: create the user and log them in. * * @param string $token Token from the email. * @return never */ private static function verify( $token ) { $id = hash( 'sha256', (string) $token ); $data = get_transient( 'paygrab_reg_' . $id ); if ( ! is_array( $data ) ) { // Opened again after the registration was finished. $used = (int) get_transient( 'paygrab_reg_used_' . $id ); wp_safe_redirect( add_query_arg( $used ? 'paygrab_used' : 'paygrab_expired', 1, self::page_url() ) ); exit; } if ( email_exists( $data['email'] ) ) { self::forget( $id ); wp_safe_redirect( add_query_arg( 'paygrab_used', 1, self::page_url() ) ); exit; } self::forget( $id ); $user_id = self::create_member( $data ); if ( is_wp_error( $user_id ) ) { wp_safe_redirect( add_query_arg( 'paygrab_expired', 1, self::page_url() ) ); exit; } set_transient( 'paygrab_reg_used_' . $id, $user_id, WEEK_IN_SECONDS ); wp_safe_redirect( self::done_url( $data ) ); exit; } /** * Create the member from a finished sign-up and log them in. * * @param array $data Sign-up (name, email, hash, source). * @return int|WP_Error User ID. */ private static function create_member( array $data ) { $user_id = wp_insert_user( array( 'user_login' => self::username( $data['email'] ), 'user_email' => $data['email'], 'user_pass' => wp_generate_password( 32 ), 'display_name' => $data['name'], 'nickname' => $data['name'], 'role' => self::role(), ) ); if ( is_wp_error( $user_id ) ) { return $user_id; } // Keep the password the person chose (stored only as a hash). global $wpdb; $wpdb->update( $wpdb->users, array( 'user_pass' => $data['hash'] ), array( 'ID' => $user_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- sets the hash as it is; wp_insert_user() would hash it a second time. clean_user_cache( $user_id ); update_user_meta( $user_id, 'paygrab_registered', time() ); if ( ! empty( $data['source'] ) ) { // My page offers a way back to this article while nothing has been bought. update_user_meta( $user_id, 'paygrab_source', (int) $data['source'] ); } wp_set_current_user( $user_id ); wp_set_auth_cookie( $user_id, true ); /** * Fires after a member finished the sign-up (email verification, or right away when verification is off). * * @param int $user_id New user. */ do_action( 'paygrab_member_registered', $user_id ); return $user_id; } /** * Address of the "registration complete" screen. * * @param array $data Sign-up (source, redirect, product). * @return string */ private static function done_url( array $data ) { $args = array( 'paygrab_done' => 1 ); if ( ! empty( $data['source'] ) ) { $args[ PAYGRAB_SOURCE_ARG ] = (int) $data['source']; } elseif ( ! empty( $data['redirect'] ) ) { $args['redirect_to'] = rawurlencode( $data['redirect'] ); } if ( ! empty( $data['product'] ) ) { $args['paygrab_product'] = (int) $data['product']; } return add_query_arg( $args, self::page_url() ); } /** * Role given to new members. * * @return string */ private static function role() { $role = (string) Paygrab_Settings::get( 'register_role', 'subscriber' ); return paygrab_role_is_safe( $role ) ? $role : 'subscriber'; } /** * A free user name made from the email address. * * @param string $email Email. * @return string */ private static function username( $email ) { $base = sanitize_user( strtok( $email, '@' ), true ); $base = '' !== $base ? substr( $base, 0, 40 ) : 'member'; $name = $base; $i = 1; while ( username_exists( $name ) ) { $name = $base . ++$i; } return $name; } /** * [paygrab_register] shortcode. * * @return string */ public static function shortcode() { Paygrab_Frontend::enqueue(); $source = paygrab_request_source(); $product = paygrab_request_product(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- screen selection only. if ( isset( $_GET['paygrab_done'] ) ) { $redirect = isset( $_GET['redirect_to'] ) ? wp_validate_redirect( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ), '' ) : ''; return paygrab_get_template( 'register-done', array( 'user' => wp_get_current_user(), 'product' => $product, 'source' => $source, 'back' => $source ? paygrab_source_url( $source ) : $redirect, 'mypage' => paygrab_mypage_url(), ) ); } if ( isset( $_GET['paygrab_used'] ) ) { return paygrab_get_template( 'register-used', array( 'mypage' => is_user_logged_in() ? paygrab_mypage_url() : paygrab_login_url( paygrab_mypage_url() ) ) ); } if ( isset( $_GET['paygrab_expired'] ) ) { return paygrab_get_template( 'register-expired', array( 'register' => self::page_url(), 'login' => paygrab_login_url() ) ); } if ( isset( $_GET['paygrab_sent'] ) ) { $ref = sanitize_text_field( wp_unslash( $_GET['paygrab_sent'] ) ); $pending = self::pending_by_ref( $ref ); $state = isset( $_GET['resent'] ) ? sanitize_key( wp_unslash( $_GET['resent'] ) ) : ''; return paygrab_get_template( 'register-sent', array( 'email' => $pending ? $pending['data']['email'] : '', 'ref' => $ref, 'state' => in_array( $state, array( 'resent', 'wait', 'toomany' ), true ) ? $state : '', 'wait' => $pending ? self::resend_wait( $pending['id'] ) : 0, 'product' => $pending && ! empty( $pending['data']['product'] ) ? get_post( (int) $pending['data']['product'] ) : null, 'edit' => add_query_arg( 'paygrab_edit', $ref, self::page_url() ), 'contact' => paygrab_page_url( 'tokushoho' ), ) ); } $redirect = isset( $_GET['redirect_to'] ) ? wp_validate_redirect( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ), '' ) : ''; $edit = isset( $_GET['paygrab_edit'] ) ? sanitize_text_field( wp_unslash( $_GET['paygrab_edit'] ) ) : ''; // phpcs:enable $old = self::$old; if ( '' !== $edit && ! $old ) { // "I typed the wrong address": the name and the address come back (the password does not). $pending = self::pending_by_ref( $edit ); if ( $pending ) { $old = array( 'name' => $pending['data']['name'], 'email' => $pending['data']['email'], ); $source = ! empty( $pending['data']['source'] ) ? (int) $pending['data']['source'] : $source; $product = ! empty( $pending['data']['product'] ) ? get_post( (int) $pending['data']['product'] ) : $product; } } $args = array( 'source' => $source, 'product' => $product ? $product->ID : 0, ); return paygrab_get_template( 'register-form', array( 'errors' => self::$errors, 'old' => $old, 'redirect' => $redirect, 'source' => $source, 'product' => $product, 'replace' => $edit, 'verify' => self::verification_on(), 'stamp' => self::stamp(), 'login_url' => paygrab_login_url( $redirect, $args ), 'lost_url' => paygrab_page_url( 'login' ) ? add_query_arg( 'paygrab_lost', 'form', paygrab_page_url( 'login' ) ) : wp_lostpassword_url(), 'terms_url' => paygrab_page_url( 'terms' ), 'privacy_url' => get_privacy_policy_url(), 'terms_text' => trim( (string) Paygrab_Settings::get( 'terms_text' ) ), 'must_scroll' => (bool) Paygrab_Settings::get( 'terms_scroll' ), ) ); } }