WP_REST_Server::CREATABLE, 'callback' => array( $this, 'check' ), 'permission_callback' => '__return_true', 'args' => array( 'code' => array( 'type' => 'string', 'required' => true, 'minLength' => 1, 'maxLength' => 100, ), self::HONEYPOT => array( 'type' => 'string', 'default' => '', 'maxLength' => 200, ), ), ) ); } /** * Hashed request context: IP, user agent and country. * * @return array{ip_hash: string, user_agent_hash: string, country: string} */ public static function request_context() { $ip = self::client_ip( (string) NovoVerify_Settings::get( 'ip_source' ) ); $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $context = array( 'ip_hash' => NovoVerify_Codes::hash_value( '' !== $ip ? $ip : 'unknown' ), 'user_agent_hash' => NovoVerify_Codes::hash_value( $user_agent ), 'country' => '', ); /** * Filters the request context stored with each check. * * @param array $context ip_hash, user_agent_hash, country. */ return apply_filters( 'novoverify_check_context', $context ); } /** * Visitor IP from the configured source, falling back to REMOTE_ADDR. * * Proxy headers are only trusted when the admin selected them, because anyone can * send such a header when the site is not really behind that proxy. * * @param string $source Key from NovoVerify_Settings::ip_sources(). * @return string IP address, or empty string. */ public static function client_ip( $source = 'remote_addr' ) { $sources = NovoVerify_Settings::ip_sources(); $ip = ''; if ( 'remote_addr' !== $source && isset( $sources[ $source ], $_SERVER[ $sources[ $source ] ] ) ) { $value = sanitize_text_field( wp_unslash( $_SERVER[ $sources[ $source ] ] ) ); // X-Forwarded-For may hold a list: "client, proxy1, proxy2". The first entry is the visitor. $first = trim( (string) strtok( $value, ',' ) ); if ( false !== filter_var( $first, FILTER_VALIDATE_IP ) ) { $ip = $first; } } if ( '' === $ip && isset( $_SERVER['REMOTE_ADDR'] ) ) { $remote = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); $ip = false !== filter_var( $remote, FILTER_VALIDATE_IP ) ? $remote : ''; } /** * Filters the visitor IP used for rate limiting and repeat detection. * * @param string $ip IP address. * @param string $source Configured IP source. */ return (string) apply_filters( 'novoverify_client_ip', $ip, $source ); } /** * Handles a check request. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function check( WP_REST_Request $request ) { if ( '' !== trim( (string) $request[ self::HONEYPOT ] ) ) { return new WP_Error( 'novoverify_rejected', __( 'Your request could not be processed. Please try again.', 'novoverify' ), array( 'status' => 400 ) ); } $context = self::request_context(); $retry_after = NovoVerify_Rate_Limit::check( $context['ip_hash'] ); if ( $retry_after > 0 ) { $error = new WP_Error( 'novoverify_rate_limited', MINUTE_IN_SECONDS >= $retry_after ? __( 'Too many checks in a short time. Please wait a minute and try again.', 'novoverify' ) : __( 'Too many checks in a short time. Please try again in an hour.', 'novoverify' ), array( 'status' => 429, 'retry_after' => $retry_after, ) ); return $error; } $check = NovoVerify_Codes::check( (string) $request['code'], $context ); $response = rest_ensure_response( self::format_response( $check, (string) $request['code'] ) ); $response->header( 'Cache-Control', 'no-store, max-age=0' ); return $response; } /** * Turns a check result into the public response. * * @param array $check Result of NovoVerify_Codes::check(). * @param string $input Code as entered. * @return array */ public static function format_response( array $check, $input ) { $row = $check['code']; $result = $check['result']; $found = is_object( $row ); $first_date = $found ? NovoVerify_Codes::format_datetime( $check['first_checked_at'], get_option( 'date_format' ) ) : ''; $code = $found ? ( '' !== $row->display_code ? $row->display_code : $row->code ) : mb_substr( sanitize_text_field( $input ), 0, 64 ); $product = $found && NovoVerify_Codes::RESULT_AUTHENTIC === $result ? (string) $row->product_name : ''; $texts = NovoVerify_Messages::get( $result, array( 'code' => $code, 'count' => (int) $check['previous_checks'], 'first_date' => $first_date, 'product' => $found ? (string) $row->product_name : '', ) ); $response = array( 'result' => $result, 'title' => $texts['title'], 'message' => $texts['message'], 'code' => $code, 'previous_checks' => (int) $check['previous_checks'], 'first_checked' => $first_date, 'details' => array(), ); if ( '' !== $product ) { $response['details'][] = array( 'label' => __( 'Product', 'novoverify' ), 'value' => $product, ); } /** * Filters the public check response. Values are printed as text, never as HTML. * * @param array $response Response data. * @param array $check Raw check result. * @param object|null $row Code row. */ return apply_filters( 'novoverify_check_response', $response, $check, $row ); } }