PluginProbe
wpForo Forum / 3.0.2
wpForo Forum v3.0.2
3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 1.4.2 All 137 releases
wpforo / classes / AIAjaxTrait.php

AIAjaxTrait.php in wpForo Forum 3.0.2, at classes/AIAjaxTrait.php

295 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace wpforo\classes;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 /**
10 * Trait AIAjaxTrait
11 *
12 * Provides common AJAX handling functionality for AI classes.
13 * Reduces code duplication for nonce verification, permission checks,
14 * board context switching, and JSON responses.
15 *
16 * Usage:
17 * class MyAIClass {
18 * use AIAjaxTrait;
19 *
20 * public function ajax_my_action() {
21 * $this->verify_ajax_admin_request( 'my_nonce_action', 'nonce' );
22 * $this->switch_board_context();
23 *
24 * // Your logic here...
25 *
26 * $this->send_success( [ 'data' => $result ] );
27 * }
28 * }
29 */
30 trait AIAjaxTrait {
31
32 /**
33 * Verify AJAX request with nonce and admin permission check.
34 * Sends error response and dies if verification fails.
35 *
36 * @param string $nonce_action The nonce action name.
37 * @param string $nonce_param The nonce parameter name in $_REQUEST. Default 'nonce'.
38 *
39 * @return void
40 */
41 protected function verify_ajax_admin_request( $nonce_action, $nonce_param = 'nonce' ) {
42 check_ajax_referer( $nonce_action, $nonce_param );
43
44 if ( ! current_user_can( 'manage_options' ) ) {
45 $this->send_error( __( 'Permission denied', 'wpforo' ), 403 );
46 }
47 }
48
49 /**
50 * Verify AJAX request with nonce only (no permission check).
51 * Useful for frontend AJAX handlers that have custom permission logic.
52 * Sends error response and dies if verification fails.
53 *
54 * @param string $nonce_action The nonce action name.
55 * @param string $nonce_param The nonce parameter name in $_REQUEST. Default 'nonce'.
56 *
57 * @return void
58 */
59 protected function verify_ajax_nonce( $nonce_action, $nonce_param = 'nonce' ) {
60 if ( ! check_ajax_referer( $nonce_action, $nonce_param, false ) ) {
61 $this->send_error( __( 'Security check failed', 'wpforo' ), 403 );
62 }
63 }
64
65 /**
66 * Verify AJAX request with nonce and custom capability check.
67 * Sends error response and dies if verification fails.
68 *
69 * @param string $nonce_action The nonce action name.
70 * @param string $nonce_param The nonce parameter name in $_REQUEST. Default 'nonce'.
71 * @param string $capability The capability to check. Default 'manage_options'.
72 *
73 * @return void
74 */
75 protected function verify_ajax_request( $nonce_action, $nonce_param = 'nonce', $capability = 'manage_options' ) {
76 check_ajax_referer( $nonce_action, $nonce_param );
77
78 if ( ! current_user_can( $capability ) ) {
79 $this->send_error( __( 'Permission denied', 'wpforo' ), 403 );
80 }
81 }
82
83 /**
84 * Switch to the correct board context based on POST parameter.
85 * This is needed for multi-board setups where each board has separate data.
86 *
87 * @param string $param_name The POST parameter name containing board ID. Default 'boardid'.
88 *
89 * @return int The board ID that was switched to (0 if default board).
90 */
91 protected function switch_board_context( $param_name = 'boardid' ) {
92 $boardid = isset( $_POST[ $param_name ] ) ? intval( $_POST[ $param_name ] ) : 0;
93 if ( $boardid > 0 ) {
94 WPF()->change_board( $boardid );
95 }
96 return $boardid;
97 }
98
99 /**
100 * Send JSON error response and die.
101 *
102 * @param string $message Error message.
103 * @param int|null $status_code HTTP status code. Default null (WordPress default).
104 *
105 * @return void
106 */
107 protected function send_error( $message, $status_code = null ) {
108 wp_send_json_error( [ 'message' => $message ], $status_code );
109 }
110
111 /**
112 * Send JSON success response and die.
113 *
114 * @param array $data Response data array.
115 *
116 * @return void
117 */
118 protected function send_success( $data ) {
119 wp_send_json_success( $data );
120 }
121
122 /**
123 * Get sanitized POST parameter with default value.
124 *
125 * @param string $key Parameter name.
126 * @param mixed $default Default value if parameter is not set.
127 * @param string $sanitize Sanitization type: 'text', 'int', 'bool', 'email', 'url', 'array_int'.
128 * Default 'text'.
129 *
130 * @return mixed Sanitized value.
131 */
132 protected function get_post_param( $key, $default = '', $sanitize = 'text' ) {
133 if ( ! isset( $_POST[ $key ] ) ) {
134 return $default;
135 }
136
137 $value = $_POST[ $key ];
138
139 switch ( $sanitize ) {
140 case 'int':
141 return intval( $value );
142
143 case 'bool':
144 return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
145
146 case 'email':
147 return sanitize_email( $value );
148
149 case 'url':
150 return esc_url_raw( $value );
151
152 case 'array_int':
153 return is_array( $value ) ? array_map( 'intval', $value ) : [];
154
155 case 'array_text':
156 return is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : [];
157
158 case 'text':
159 default:
160 return sanitize_text_field( $value );
161 }
162 }
163
164 /**
165 * Get multiple POST parameters at once with sanitization.
166 *
167 * @param array $params Array of parameter definitions:
168 * [ 'key' => [ 'default' => '', 'sanitize' => 'text' ] ]
169 * or simple: [ 'key' => 'default_value' ] (uses 'text' sanitization)
170 *
171 * @return array Associative array of sanitized values.
172 */
173 protected function get_post_params( $params ) {
174 $result = [];
175
176 foreach ( $params as $key => $config ) {
177 if ( is_array( $config ) ) {
178 $default = $config['default'] ?? '';
179 $sanitize = $config['sanitize'] ?? 'text';
180 } else {
181 $default = $config;
182 $sanitize = 'text';
183 }
184
185 $result[ $key ] = $this->get_post_param( $key, $default, $sanitize );
186 }
187
188 return $result;
189 }
190
191 /**
192 * Validate required POST parameters exist.
193 * Sends error response and dies if any required parameter is missing.
194 *
195 * @param array $required Array of required parameter names.
196 *
197 * @return void
198 */
199 protected function require_post_params( $required ) {
200 $missing = [];
201 foreach ( $required as $param ) {
202 if ( ! isset( $_POST[ $param ] ) || $_POST[ $param ] === '' ) {
203 $missing[] = $param;
204 }
205 }
206
207 if ( ! empty( $missing ) ) {
208 $this->send_error(
209 sprintf( __( 'Missing required parameters: %s', 'wpforo' ), implode( ', ', $missing ) ),
210 400
211 );
212 }
213 }
214
215 /**
216 * Check if user has exceeded rate limit for an AI feature.
217 * Sends error response and dies if rate limited.
218 *
219 * Rate limits are stored in WordPress transients with daily reset.
220 * Moderators (usergroup[cans][ms]) are exempt from rate limits.
221 *
222 * @param string $feature Feature key: search, translation, summarization, suggestions, chatbot
223 *
224 * @return void
225 */
226 protected function check_rate_limit( $feature ) {
227 $user_id = get_current_user_id();
228
229 // Check if user is moderator (has 'ms' permission) - unlimited access
230 if ( $user_id && WPF()->usergroup->can( 'ms' ) ) {
231 return; // Moderators have unlimited access
232 }
233
234 // Get rate limit settings for this feature
235 $rate_limits = wpforo_setting( 'ai', 'rate_limits' );
236 if ( empty( $rate_limits ) || empty( $rate_limits[ $feature ] ) ) {
237 return; // No limits configured for this feature
238 }
239
240 $feature_limits = $rate_limits[ $feature ];
241
242 // Determine limit based on user type (guest vs logged-in)
243 if ( $user_id ) {
244 $limit = (int) ( $feature_limits['user'] ?? 0 );
245 $transient_key = "wpforo_ai_{$feature}_user_{$user_id}";
246 } else {
247 $limit = (int) ( $feature_limits['guest'] ?? 0 );
248 $ip = $this->get_client_ip();
249 $transient_key = "wpforo_ai_{$feature}_guest_" . md5( $ip );
250 }
251
252 // If limit is 0, feature is disabled for this user type
253 if ( $limit === 0 ) {
254 $this->send_error(
255 __( 'This feature is not available.', 'wpforo' ),
256 403
257 );
258 }
259
260 // Check current usage count
261 $current_count = (int) get_transient( $transient_key );
262
263 if ( $current_count >= $limit ) {
264
265 // Show different message for guests vs logged-in users
266 if ( ! $user_id ) {
267 $message = __( 'Daily limit reached. Please login to remove this limitation.', 'wpforo' );
268 } else {
269 $message = __( 'Daily limit reached. Please try again tomorrow.', 'wpforo' );
270 }
271 $this->send_error( $message, 429 );
272 }
273
274 // Increment counter (expires at midnight local time)
275 $seconds_until_midnight = strtotime( 'tomorrow' ) - time();
276 set_transient( $transient_key, $current_count + 1, $seconds_until_midnight );
277 }
278
279 /**
280 * Get the client IP address, considering proxy headers.
281 *
282 * @return string Client IP address
283 */
284 protected function get_client_ip() {
285 $ip_keys = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR' ];
286 foreach ( $ip_keys as $key ) {
287 if ( ! empty( $_SERVER[ $key ] ) ) {
288 $ip = explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) )[0];
289 return trim( $ip );
290 }
291 }
292 return '0.0.0.0';
293 }
294 }
295