[label, modes[], ready(), send( $phone, $text ), call_start( $phone ), call_status( $check_id )]. * * @return array */ public static function providers() { $providers = array( 'smsru' => array( 'label' => 'SMS.ru', 'modes' => array( 'sms', 'call' ), 'ready' => array( 'PRPS_Smsru', 'ready' ), 'send' => array( 'PRPS_Smsru', 'send' ), 'call_start' => array( 'PRPS_Smsru', 'call_start' ), 'call_status' => array( 'PRPS_Smsru', 'call_status' ), ), ); /** * Провайдеры SMS и звонков. Pro добавляет SMSC и SMS Aero. * * @param array $providers Провайдеры. */ return (array) apply_filters( 'prps/providers', $providers ); } public static function provider() { $all = self::providers(); $key = (string) PRPS_Settings::get( 'provider' ); return $all[ $key ] ?? reset( $all ); } /** Провайдер настроен и выбранный способ им поддерживается. */ public static function ready() { $p = self::provider(); return $p && in_array( self::mode(), $p['modes'], true ) && call_user_func( $p['ready'] ); } public static function mode() { return 'call' === PRPS_Settings::get( 'phone_mode' ) ? 'call' : 'sms'; } /* --------------------------------------------------------------------- * Номер * ------------------------------------------------------------------- */ /** * Российский номер в виде 79XXXXXXXXX или пустая строка. * По 406-ФЗ вход по номеру допускается для номеров российских операторов. */ public static function normalize( $raw ) { $digits = preg_replace( '/\D+/', '', (string) $raw ); if ( 10 === strlen( $digits ) ) { $digits = '7' . $digits; } elseif ( 11 === strlen( $digits ) && '8' === $digits[0] ) { $digits = '7' . substr( $digits, 1 ); } $ok = (bool) preg_match( '/^7[3489]\d{9}$/', $digits ); /** * Проверка номера. Можно разрешить другие форматы. * * @param bool $ok Номер подходит. * @param string $digits Цифры номера. * @param string $raw Как ввёл посетитель. */ return apply_filters( 'prps/valid_phone', $ok, $digits, $raw ) ? $digits : ''; } /** +7 (912) 345-67-89 */ public static function pretty( $phone ) { return preg_match( '/^7(\d{3})(\d{3})(\d{2})(\d{2})$/', (string) $phone, $m ) ? '+7 (' . $m[1] . ') ' . $m[2] . '-' . $m[3] . '-' . $m[4] : (string) $phone; } /** +7 (912) ***-**-89 — для журналов и писем. */ public static function masked( $phone ) { return preg_match( '/^7(\d{3})\d{5}(\d{2})$/', (string) $phone, $m ) ? '+7 (' . $m[1] . ') ***-**-' . $m[2] : ''; } /* --------------------------------------------------------------------- * Лимиты * ------------------------------------------------------------------- */ public static function ip() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '0.0.0.0'; } private static function counter( $key, $limit ) { $name = 'prps_rl_' . md5( $key ); $n = (int) get_transient( $name ); if ( $n >= $limit ) { return false; } set_transient( $name, $n + 1, HOUR_IN_SECONDS ); return true; } private static function state_key( $phone ) { return 'prps_v_' . md5( $phone . wp_salt( 'nonce' ) ); } /* --------------------------------------------------------------------- * Отправка и проверка * ------------------------------------------------------------------- */ /** * Отправить код или начать проверку звонком. * * @return array|WP_Error Данные для формы. */ public static function start( $phone ) { if ( ! self::ready() ) { return new WP_Error( 'off', __( 'Вход по номеру телефона сейчас недоступен.', 'propusk' ) ); } $state = get_transient( self::state_key( $phone ) ); if ( is_array( $state ) && time() - (int) $state['sent'] < self::COOLDOWN ) { /* translators: %d: секунд */ return new WP_Error( 'wait', sprintf( __( 'Новый код можно запросить через %d с.', 'propusk' ), self::COOLDOWN - ( time() - (int) $state['sent'] ) ) ); } $s = PRPS_Settings::all(); if ( ! self::counter( 'p' . $phone, max( 1, (int) $s['limit_phone'] ) ) || ! self::counter( 'i' . self::ip(), max( 1, (int) $s['limit_ip'] ) ) ) { return new WP_Error( 'limit', __( 'Слишком много запросов. Попробуйте через час.', 'propusk' ) ); } $p = self::provider(); $mode = self::mode(); $new = array( 'mode' => $mode, 'sent' => time(), 'tries' => 0, ); if ( 'call' === $mode ) { $res = call_user_func( $p['call_start'], $phone ); if ( is_wp_error( $res ) ) { return $res; } $new['check'] = $res['check_id']; set_transient( self::state_key( $phone ), $new, self::TTL ); return array( 'mode' => 'call', /* translators: 1: номер посетителя, 2: бесплатный номер для звонка */ 'message' => sprintf( __( 'Позвоните с номера %1$s на %2$s — звонок бесплатный, трубку брать не будут. Ждём звонка…', 'propusk' ), self::pretty( $phone ), $res['call_phone_pretty'] ), 'call' => $res['call_phone'], ); } $len = min( 6, max( 4, (int) $s['code_length'] ) ); $code = str_pad( (string) random_int( 0, ( 10 ** $len ) - 1 ), $len, '0', STR_PAD_LEFT ); $new['hash'] = hash_hmac( 'sha256', $code, wp_salt( 'auth' ) ); $text = strtr( (string) $s['sms_text'], array( '{code}' => $code, '{site}' => wp_parse_url( home_url(), PHP_URL_HOST ) ) ); if ( false === strpos( $text, $code ) ) { $text .= ' ' . $code; } $res = call_user_func( $p['send'], $phone, $text ); if ( is_wp_error( $res ) ) { return $res; } set_transient( self::state_key( $phone ), $new, self::TTL ); return array( 'mode' => 'sms', /* translators: %s: номер */ 'message' => sprintf( __( 'Код отправлен в SMS на %s.', 'propusk' ), self::pretty( $phone ) ), 'length' => $len, ); } /** * Проверить код (или звонок). * * @return true|WP_Error|null null — звонка ещё не было. */ public static function verify( $phone, $code ) { $key = self::state_key( $phone ); $state = get_transient( $key ); if ( ! is_array( $state ) ) { return new WP_Error( 'expired', __( 'Код устарел. Запросите новый.', 'propusk' ) ); } if ( 'call' === $state['mode'] ) { $res = call_user_func( self::provider()['call_status'], $state['check'] ); if ( true === $res ) { delete_transient( $key ); } return $res; } if ( (int) $state['tries'] >= self::TRIES ) { delete_transient( $key ); return new WP_Error( 'tries', __( 'Слишком много попыток. Запросите новый код.', 'propusk' ) ); } if ( hash_equals( (string) $state['hash'], hash_hmac( 'sha256', preg_replace( '/\D+/', '', (string) $code ), wp_salt( 'auth' ) ) ) ) { delete_transient( $key ); return true; } ++$state['tries']; set_transient( $key, $state, max( 1, self::TTL - ( time() - (int) $state['sent'] ) ) ); return new WP_Error( 'code', __( 'Неверный код.', 'propusk' ) ); } /* --------------------------------------------------------------------- * AJAX * ------------------------------------------------------------------- */ private static function input() { check_ajax_referer( 'prps_front', 'nonce' ); $phone = self::normalize( isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '' ); if ( '' === $phone ) { wp_send_json_error( array( 'message' => __( 'Введите российский номер телефона: +7 и 10 цифр.', 'propusk' ) ) ); } return $phone; } public static function ajax_send() { $phone = self::input(); $res = self::start( $phone ); if ( is_wp_error( $res ) ) { wp_send_json_error( array( 'message' => $res->get_error_message() ) ); } wp_send_json_success( $res ); } public static function ajax_check() { $phone = self::input(); $code = isset( $_POST['code'] ) ? sanitize_text_field( wp_unslash( $_POST['code'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce проверен в input(). $res = self::verify( $phone, $code ); if ( null === $res ) { wp_send_json_success( array( 'wait' => true ) ); } if ( is_wp_error( $res ) ) { wp_send_json_error( array( 'message' => $res->get_error_message() ) ); } $redirect = isset( $_POST['redirect'] ) ? esc_url_raw( wp_unslash( $_POST['redirect'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce проверен в input(). $done = PRPS_Users::sign_in( array( 'provider' => 'phone', 'id' => $phone, 'phone' => $phone, ), $redirect ); if ( is_wp_error( $done ) ) { wp_send_json_error( array( 'message' => $done->get_error_message() ) ); } wp_send_json_success( array( 'redirect' => $done ) ); } }