Skip to content

Commit ea9699c

Browse files
authored
Merge pull request #53 from GravityKit/develop
Release 1.7.0
2 parents bc37b0b + ed8eba1 commit ea9699c

6 files changed

Lines changed: 707 additions & 34 deletions

gravityforms-zero-spam.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Plugin Name: Gravity Forms Zero Spam
44
* Plugin URI: https://www.gravitykit.com?utm_source=plugin&utm_campaign=zero-spam&utm_content=pluginuri
55
* Description: Enhance Gravity Forms to include effective anti-spam measures—without using a CAPTCHA.
6-
* Version: 1.6.0
6+
* Version: 1.7.0
77
* Author: GravityKit
88
* Author URI: https://www.gravitykit.com?utm_source=plugin&utm_campaign=zero-spam&utm_content=authoruri
99
* Requires PHP: 7.4
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<?php
2+
/**
3+
* REST API and admin-ajax endpoints for token minting.
4+
*
5+
* @since 1.7.0
6+
*/
7+
8+
if ( ! defined( 'WPINC' ) ) {
9+
die;
10+
}
11+
12+
class GF_Zero_Spam_Token_Endpoint {
13+
14+
/**
15+
* Maximum token requests per IP per minute.
16+
*
17+
* @since 1.7.0
18+
*
19+
* @var int
20+
*/
21+
const RATE_LIMIT = 30;
22+
23+
/**
24+
* REST API namespace.
25+
*
26+
* @since 1.7.0
27+
*
28+
* @var string
29+
*/
30+
const REST_NAMESPACE = 'gf-zero-spam/v1';
31+
32+
/**
33+
* Registers hooks for both REST and admin-ajax endpoints.
34+
*
35+
* @since 1.7.0
36+
*/
37+
public function __construct() {
38+
add_action( 'rest_api_init', [ $this, 'register_rest_route' ] );
39+
add_action( 'wp_ajax_gf_zero_spam_token', [ $this, 'handle_ajax' ] );
40+
add_action( 'wp_ajax_nopriv_gf_zero_spam_token', [ $this, 'handle_ajax' ] );
41+
}
42+
43+
/**
44+
* Registers the REST API route for token minting.
45+
*
46+
* @since 1.7.0
47+
*
48+
* @return void
49+
*/
50+
public function register_rest_route() {
51+
register_rest_route(
52+
self::REST_NAMESPACE,
53+
'/token',
54+
[
55+
'methods' => 'GET',
56+
'callback' => [ $this, 'handle_rest' ],
57+
'permission_callback' => '__return_true',
58+
'args' => [
59+
'form_id' => [
60+
'required' => true,
61+
'type' => 'integer',
62+
'sanitize_callback' => 'absint',
63+
],
64+
],
65+
]
66+
);
67+
}
68+
69+
/**
70+
* Handles the REST API token request.
71+
*
72+
* @since 1.7.0
73+
*
74+
* @param WP_REST_Request $request The REST request.
75+
*
76+
* @return WP_REST_Response|WP_Error
77+
*/
78+
public function handle_rest( $request ) {
79+
$form_id = (int) $request->get_param( 'form_id' );
80+
81+
return $this->handle_token_request( $form_id );
82+
}
83+
84+
/**
85+
* Handles the admin-ajax token request.
86+
*
87+
* @since 1.7.0
88+
*
89+
* @return void
90+
*/
91+
public function handle_ajax() {
92+
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public endpoint; no nonce needed.
93+
$form_id = isset( $_REQUEST['form_id'] ) ? absint( $_REQUEST['form_id'] ) : 0;
94+
$result = $this->handle_token_request( $form_id );
95+
96+
nocache_headers();
97+
98+
if ( is_wp_error( $result ) ) {
99+
$error_data = $result->get_error_data();
100+
$status = is_array( $error_data ) && isset( $error_data['status'] ) ? (int) $error_data['status'] : 500;
101+
102+
wp_send_json_error( $result->get_error_message(), $status );
103+
}
104+
105+
wp_send_json( $result->get_data() );
106+
}
107+
108+
/**
109+
* Shared handler that validates the request and mints a token.
110+
*
111+
* @since 1.7.0
112+
*
113+
* @param int $form_id The form ID to mint a token for.
114+
*
115+
* @return WP_REST_Response|WP_Error
116+
*/
117+
private function handle_token_request( int $form_id ) {
118+
if ( $form_id < 1 ) {
119+
return new WP_Error( 'missing_form_id', __( 'A valid form_id is required.', 'gravity-forms-zero-spam' ), [ 'status' => 400 ] );
120+
}
121+
122+
$form = GFAPI::get_form( $form_id );
123+
124+
if ( ! $form ) {
125+
return new WP_Error( 'invalid_form', __( 'Form not found.', 'gravity-forms-zero-spam' ), [ 'status' => 400 ] );
126+
}
127+
128+
// Check if Zero Spam is enabled for this form.
129+
$enabled = gf_apply_filters( 'gf_zero_spam_check_key_field', $form_id, true, $form, [] );
130+
131+
if ( false === $enabled ) {
132+
return new WP_Error( 'zero_spam_disabled', __( 'Zero Spam is not enabled for this form.', 'gravity-forms-zero-spam' ), [ 'status' => 400 ] );
133+
}
134+
135+
$rate_check = $this->check_rate_limit();
136+
137+
if ( is_wp_error( $rate_check ) ) {
138+
return $rate_check;
139+
}
140+
141+
$ttl = 600;
142+
$token = GF_Zero_Spam_Token::mint( $form_id, $ttl );
143+
$expires = time() + $ttl;
144+
145+
$response = new WP_REST_Response(
146+
[
147+
'token' => $token,
148+
'expires' => $expires,
149+
]
150+
);
151+
152+
$response->header( 'Cache-Control', 'no-store, no-cache, must-revalidate' );
153+
154+
return $response;
155+
}
156+
157+
/**
158+
* Checks per-IP rate limit using transients.
159+
*
160+
* @since 1.7.0
161+
*
162+
* @return true|WP_Error True if within limits, WP_Error if exceeded.
163+
*/
164+
private function check_rate_limit() {
165+
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- IP used only for hashing.
166+
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : 'unknown';
167+
168+
/**
169+
* Filters the client IP address used for rate limiting.
170+
*
171+
* Useful for sites behind Cloudflare, load balancers, or reverse proxies
172+
* where REMOTE_ADDR is the proxy IP, not the visitor's IP.
173+
*
174+
* @since 1.7.0
175+
*
176+
* @param string $ip The client IP address. Default: $_SERVER['REMOTE_ADDR'].
177+
*/
178+
$ip = apply_filters( 'gf_zero_spam_client_ip', $ip );
179+
180+
$ip_hash = md5( $ip );
181+
$key = 'gf_zs_rate_' . $ip_hash;
182+
183+
$count = (int) get_transient( $key );
184+
185+
/**
186+
* Filters the maximum number of token requests allowed per IP per minute.
187+
*
188+
* Increase for sites behind corporate NAT or shared IP environments.
189+
*
190+
* @since 1.7.0
191+
*
192+
* @param int $limit The maximum request count per minute. Default: 30.
193+
*/
194+
$limit = (int) apply_filters( 'gf_zero_spam_rate_limit', self::RATE_LIMIT );
195+
196+
if ( $count >= $limit ) {
197+
return new WP_Error( 'rate_limited', __( 'Too many requests. Please try again later.', 'gravity-forms-zero-spam' ), [ 'status' => 429 ] );
198+
}
199+
200+
set_transient( $key, $count + 1, 60 );
201+
202+
return true;
203+
}
204+
}

0 commit comments

Comments
 (0)