PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / 6.0.12
Kirki – Freeform Page Builder, Website Builder & Customizer v6.0.12
6.0.14 6.0.13 6.0.12 6.0.11 6.0.10 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.19 4.0.20 4.0.21 4.0.22 4.0.23 4.0.24 4.1 4.2.0 5.0.0 5.1.0 5.1.1 5.2.0 5.2.1 5.2.2 5.2.3 6.0.0 trunk 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.1.0 3.1.1 3.1.2
kirki / ComponentLibrary / controller / CompLibFormHandler.php
kirki / ComponentLibrary / controller Last commit date
CompLibFormHandler.php 3 weeks ago ElementGenerator.php 3 weeks ago ShowUserMetadata.php 2 months ago
CompLibFormHandler.php
637 lines
1 <?php
2
3 namespace KirkiComponentLib\Controller;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit; // Exit if accessed directly.
7 }
8
9 use Kirki\Ajax\Page;
10 use Kirki\HelperFunctions;
11 use WP_REST_Server;
12 use WP_REST_Controller;
13 use WP_REST_Response;
14
15 class CompLibFormHandler extends WP_REST_Controller {
16
17 protected $namespace = KIRKI_COMPONENT_LIBRARY_APP_PREFIX . '/v1';
18
19 public function __construct() {
20 $this->init_rest_api_endpoint( 'kirki-login', WP_REST_Server::CREATABLE, array( $this, 'handle_login' ), array( $this, 'guest_permissions_check' ) );
21 $this->init_rest_api_endpoint( 'kirki-register', WP_REST_Server::CREATABLE, array( $this, 'handle_register' ), array( $this, 'guest_permissions_check' ) );
22 $this->init_rest_api_endpoint( 'kirki-forgot-password', WP_REST_Server::CREATABLE, array( $this, 'handle_forgot_password' ), array( $this, 'guest_permissions_check' ) );
23 $this->init_rest_api_endpoint( 'kirki-change-password', WP_REST_Server::CREATABLE, array( $this, 'handle_change_password' ), array( $this, 'guest_permissions_check' ) );
24 $this->init_rest_api_endpoint( 'kirki-retrieve-username', WP_REST_Server::CREATABLE, array( $this, 'handle_retrieve_username' ), array( $this, 'guest_permissions_check' ) );
25 $this->init_rest_api_endpoint( 'kirki-comment', WP_REST_Server::CREATABLE, array( $this, 'handle_post_comment' ), array( $this, 'comment_permissions_check' ) );
26 }
27
28 public function init_rest_api_endpoint( $endpoint, $methods, $callback, $permission_callback = null ) {
29 add_action(
30 'rest_api_init',
31 function () use ( $endpoint, $methods, $callback, $permission_callback ) {
32 register_rest_route(
33 $this->namespace,
34 '/' . $endpoint,
35 array(
36 array(
37 'methods' => $methods,
38 'callback' => $callback,
39 'permission_callback' => $permission_callback ? $permission_callback : array( $this, 'get_item_permissions_check' ),
40 'args' => $this->get_endpoint_args_for_item_schema( $methods ),
41 ),
42 'schema' => array( $this, 'get_item_schema' ),
43 )
44 );
45 }
46 );
47 }
48
49 public function get_item_permissions_check( $request ) {
50 return true;
51 }
52
53 public function guest_permissions_check( $request ) {
54 return true;
55 }
56
57 public function comment_permissions_check( $request ) {
58 if ( ! is_user_logged_in() && get_option( 'default_comment_status' ) !== 'open' ) {
59 return new \WP_Error(
60 'rest_forbidden',
61 __( 'You must be logged in to post comments.' ),
62 array( 'status' => 401 )
63 );
64 }
65 return true;
66 }
67
68 private function wp_unique_username( $username, $suffix = 1 ) {
69 $original_username = $username;
70 while ( username_exists( $username ) ) {
71 $username = sprintf( '%s_%d', $original_username, $suffix++ );
72 }
73 return $username;
74 }
75
76 private function validate_meta_field( $field_name ) {
77 $allowed_meta_fields = apply_filters( 'kirki_allowed_registration_meta_fields', array(
78 'first_name',
79 'last_name',
80 'phone',
81 'company',
82 'address',
83 'city',
84 'state',
85 'country',
86 'zip',
87 ) );
88
89 if ( ! in_array( $field_name, $allowed_meta_fields, true ) ) {
90 return false;
91 }
92
93 if ( preg_match( '/[^a-z0-9_-]/i', $field_name ) ) {
94 return false;
95 }
96
97 return true;
98 }
99
100 /**
101 * Verify that the submitted emailSubject + emailBody were signed by the server
102 * at page-render time and have not been tampered with.
103 *
104 * IMPORTANT: $body_raw must be the raw JSON string as received from the request —
105 * never a re-encoded array. Re-encoding can produce different output than the
106 * original wp_json_encode() call, breaking the HMAC comparison.
107 *
108 * @param string $subject The email subject string.
109 * @param string $body_raw The raw emailBody JSON string from the request.
110 * @param string $signature The HMAC signature to verify against.
111 * @return bool
112 */
113 private function verify_email_template_signature( $subject, $body_raw, $signature ) {
114 if ( empty( $signature ) ) {
115 return false;
116 }
117
118 // Use the raw string directly — same as what was signed in ElementGenerator.
119 // Do NOT json_decode then re-encode here.
120 $payload = $subject . '|' . $body_raw;
121 $secret = AUTH_KEY . AUTH_SALT;
122 $expected = hash_hmac( 'sha256', $payload, $secret );
123
124 return hash_equals( $expected, $signature );
125 }
126
127 /**
128 * Build the email body from a verified emailBody definition.
129 * Chip values are resolved from a fixed server-controlled map.
130 *
131 * @param array $email_body_array
132 * @param array $chip_data
133 * @return string
134 */
135 private function build_email_body( array $email_body_array, array $chip_data ) {
136 $email_body = '';
137 foreach ( $email_body_array as $body_data ) {
138 if ( ! isset( $body_data['type'], $body_data['value'] ) ) {
139 continue;
140 }
141 if ( $body_data['type'] === 'text' ) {
142 $email_body .= $body_data['value'];
143 } elseif ( $body_data['type'] === 'chip' && isset( $chip_data[ $body_data['value'] ] ) ) {
144 $email_body .= $chip_data[ $body_data['value'] ];
145 }
146 }
147 return $email_body;
148 }
149
150 public function handle_post_comment( $request ) {
151 $form_data = $request->get_body_params();
152 $transient_name = $this->validate_nonce( 'kirki-comment' ); // note: typo fix from $transiet_name
153
154 $comment = isset( $form_data['comment'] ) ? sanitize_text_field( $form_data['comment'] ) : '';
155 $post_id = isset( $form_data['post_id'] ) ? absint( $form_data['post_id'] ) : 0;
156 $comment_parent = isset( $form_data['comment_parent'] ) ? absint( $form_data['comment_parent'] ) : 0;
157 $user_id = get_current_user_id();
158 $user = $user_id ? get_user_by( 'ID', $user_id ) : null;
159
160 // Resolve author identity.
161 if ( $user ) {
162 $name = $user->get( 'display_name' );
163 $email = $user->get( 'user_email' );
164 } else {
165 // Anonymous commenter: require name + valid email supplied in the form.
166 $name = isset( $form_data['name'] ) ? sanitize_text_field( $form_data['name'] ) : '';
167 $email = isset( $form_data['email'] ) ? sanitize_email( $form_data['email'] ) : '';
168
169 if ( empty( $name ) || empty( $email ) || ! is_email( $email ) ) {
170 return new WP_REST_Response(
171 array( 'message' => 'Name and a valid email address are required.' ),
172 400
173 );
174 }
175 }
176
177 $existing_comment_id = isset( $form_data['comment_id'] ) ? absint( $form_data['comment_id'] ) : 0;
178 $is_edit = $existing_comment_id !== 0;
179 $collection_type = isset( $form_data['collection_type'] ) ? sanitize_text_field( $form_data['collection_type'] ) : '';
180
181 // -----------------------------------------------------------------------
182 // EDIT PATH
183 // -----------------------------------------------------------------------
184 if ( $is_edit ) {
185 // FIX 1: Editing always requires an authenticated session.
186 if ( ! is_user_logged_in() ) {
187 return new WP_REST_Response(
188 array( 'message' => 'You must be logged in to edit a comment.' ),
189 401
190 );
191 }
192
193 $existing_comment = get_comment( $existing_comment_id );
194
195 if ( ! $existing_comment ) {
196 return new WP_REST_Response(
197 array( 'message' => 'Comment not found.' ),
198 404
199 );
200 }
201
202 // FIX 1 (cont.): strict ownership — user_id 0 must never match.
203 $is_owner = ( $user_id !== 0 && (int) $existing_comment->user_id === $user_id );
204 $is_moderator = current_user_can( 'moderate_comments' );
205
206 if ( ! $is_owner && ! $is_moderator ) {
207 return new WP_REST_Response(
208 array( 'message' => 'You are not authorized to edit this comment.' ),
209 403
210 );
211 }
212
213 $date = gmdate( 'Y-m-d H:i:s' );
214
215 global $wpdb;
216 $wpdb->update(
217 $wpdb->comments,
218 array(
219 'comment_content' => $comment,
220 'comment_date' => $date,
221 'comment_date_gmt' => get_gmt_from_date( $date ),
222 ),
223 array( 'comment_ID' => $existing_comment_id )
224 );
225
226 apply_filters(
227 'kirki_comment_added-' . $collection_type,
228 array(
229 'comment_ID' => $existing_comment_id,
230 'user_id' => $user_id,
231 'form_data' => $form_data,
232 )
233 );
234
235 delete_transient( $transient_name );
236 return new WP_REST_Response( array( 'message' => 'Comment updated.' ), 200 );
237 }
238
239 // -----------------------------------------------------------------------
240 // INSERT PATH
241 // -----------------------------------------------------------------------
242
243 // FIX 2: Build the comment array without hardcoding comment_approved=1,
244 // then route through wp_new_comment() so WordPress moderation, spam
245 // filters (Akismet, etc.), and flood checks all apply normally.
246 $comment_data = array(
247 'comment_post_ID' => $post_id,
248 'user_id' => $user_id,
249 'comment_author' => $name,
250 'comment_author_email' => $email,
251 'comment_author_IP' => isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '',
252 'comment_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
253 'comment_content' => $comment,
254 'comment_parent' => $comment_parent,
255 );
256
257 $comment_data = apply_filters( 'kirki_comment-' . $collection_type, $comment_data );
258
259 // wp_new_comment() runs duplicate/flood/spam checks, fires hooks, and
260 // respects the site's moderation settings.
261 $comment_id = wp_new_comment( $comment_data, true ); // true = return WP_Error on failure
262
263 if ( is_wp_error( $comment_id ) ) {
264 return new WP_REST_Response(
265 array( 'message' => $comment_id->get_error_message() ),
266 400
267 );
268 }
269
270 if ( ! $comment_id ) {
271 return new WP_REST_Response(
272 array( 'message' => 'Failed to add comment.' ),
273 400
274 );
275 }
276
277 apply_filters(
278 'kirki_comment_added-' . $collection_type,
279 array(
280 'comment_ID' => $comment_id,
281 'user_id' => $user_id,
282 'form_data' => $form_data,
283 )
284 );
285
286 delete_transient( $transient_name );
287 return new WP_REST_Response( array( 'message' => 'Comment added.' ), 200 );
288 }
289
290
291
292 public function handle_login( $request ) {
293 $form_data = $request->get_body_params();
294 $transiet_name = $this->validate_nonce( 'kirki-login' );
295
296 $username = isset( $form_data['username'] ) ? sanitize_text_field( $form_data['username'] ) : '';
297 $password = isset( $form_data['password'] ) ? sanitize_text_field( $form_data['password'] ) : '';
298 $email = isset( $form_data['email'] ) ? sanitize_email( $form_data['email'] ) : '';
299
300 if ( strlen( $username ) === 0 && isset( $form_data['email'] ) && strlen( $email ) > 0 ) {
301 $user = get_user_by( 'email', $email );
302 if ( $user ) {
303 $username = $user->get( 'user_login' );
304 } else {
305 $response = array(
306 'message' => 'Invalid username or password',
307 );
308 return new WP_REST_Response( $response, 401 );
309 }
310 }
311
312 if (
313 isset( $username ) && strlen( $username ) > 0 &&
314 isset( $password ) && strlen( $password ) > 0
315 ) {
316 $user = wp_signon(
317 array(
318 'user_login' => $username,
319 'user_password' => $password,
320 'remember' => true,
321 )
322 );
323
324 if ( is_wp_error( $user ) ) {
325 $response = array(
326 'message' => 'Invalid username or password',
327 );
328 return new WP_REST_Response( $response, 401 );
329 }
330 $response = array(
331 'message' => 'User logged in',
332 'user' => array(
333 'username' => $user->get( 'user_login' ),
334 'id' => $user->get( 'ID' ),
335 'display_name' => $user->get( 'display_name' ),
336 'email' => $user->get( 'user_email' ),
337 'user_type' => $user->get( 'user_type' ),
338 ),
339 );
340 delete_transient( $transiet_name );
341 return new WP_REST_Response( $response, 200 );
342 }
343 $response = array(
344 'message' => 'Invalid form data',
345 );
346 return new WP_REST_Response( $response, 400 );
347 }
348
349 public function handle_register( $request ) {
350 $can_register = get_option( 'users_can_register' );
351 if ( $can_register !== '1' ) {
352 $response = array(
353 'message' => 'User not allowed to register',
354 );
355 return new WP_REST_Response( $response, 500 );
356 };
357
358 $form_data = $request->get_body_params();
359 $transiet_name = $this->validate_nonce( 'kirki-register' );
360
361 $username = isset( $form_data['username'] ) ? sanitize_text_field( $form_data['username'] ) : '';
362 $email = isset( $form_data['email'] ) ? sanitize_email( $form_data['email'] ) : '';
363 $password = isset( $form_data['password'] ) ? sanitize_text_field( $form_data['password'] ) : '';
364
365 if ( strlen( $email ) > 0 && strlen( $username ) === 0 ) {
366 preg_match( '/^(.*?)@/', $email, $matches );
367 $username = $this->wp_unique_username( $matches[1] );
368 }
369
370 $user_data = array(
371 'user_login' => $username,
372 'user_email' => $email,
373 'user_pass' => $password,
374 'meta_input' => array(),
375 );
376
377 foreach ( $form_data as $name => $value ) {
378 if ( $name !== 'username' && $name !== 'email' && $name !== 'password' && $name !== 'confirm_password' ) {
379 if ( $this->validate_meta_field( $name ) ) {
380 $user_data['meta_input'][ KIRKI_COMPONENT_LIBRARY_APP_PREFIX . '_' . $name ] = sanitize_text_field( $value );
381 }
382 }
383 }
384
385 if (
386 isset( $username ) && strlen( $username ) > 0
387 && isset( $email ) && strlen( $email ) > 0 &&
388 isset( $password ) && strlen( $password ) > 0
389 ) {
390 $id = wp_insert_user( $user_data );
391
392 if ( is_wp_error( $id ) ) {
393 $response = array(
394 'message' => $id->errors[ array_key_first( $id->errors ) ],
395 );
396 return new WP_REST_Response( $response, 500 );
397 }
398
399 wp_new_user_notification( $id, null, 'both' );
400 $response = array(
401 'message' => 'User created',
402 'user_id' => $id,
403 );
404 delete_transient( $transiet_name );
405 return new WP_REST_Response( $response, 200 );
406 }
407 $response = array(
408 'message' => 'Invalid form data',
409 );
410 return new WP_REST_Response( $response, 400 );
411 }
412
413 public function handle_forgot_password( $request ) {
414 $form_data = $request->get_body_params();
415 $transiet_name = $this->validate_nonce( 'kirki-forgot-password' );
416
417 $email = isset( $form_data['email'] ) ? sanitize_email( $form_data['email'] ) : '';
418 $username = isset( $form_data['username'] ) ? sanitize_text_field( $form_data['username'] ) : '';
419
420 if ( strlen( $username ) === 0 && isset( $form_data['email'] ) && strlen( $email ) > 0 ) {
421 $user = get_user_by( 'email', $email );
422
423 if ( ! $user ) {
424 return new WP_REST_Response( array( 'message' => 'If an account exists with this email, you will receive a password reset link.' ), 200 );
425 }
426
427 $username = $user->get( 'user_login' );
428 }
429
430 if ( empty( $username ) ) {
431 return new WP_REST_Response( array( 'message' => 'Invalid request' ), 400 );
432 }
433
434 if ( isset( $username ) && strlen( $username ) > 0 ) {
435 $user = get_user_by( 'login', $username );
436
437 if ( ! $user ) {
438 $response = array(
439 'message' => 'If an account exists with this information, you will receive a password reset link.',
440 );
441 return new WP_REST_Response( $response, 200 );
442 }
443
444 $user_email = $user->get( 'user_email' );
445 if($email !== $user_email) {
446 $response = array(
447 'message' => 'If an account exists with this information, you will receive a password reset link.',
448 );
449 return new WP_REST_Response( $response, 200 );
450 }
451 $email = $user_email;
452
453 $key = get_password_reset_key( $user );
454 if ( is_wp_error( $key ) ) {
455 $response = array(
456 'message' => $key->get_error_message(),
457 );
458 return new WP_REST_Response( $response, 500 );
459 }
460
461 // Prepare email content.
462 $url = HelperFunctions::get_utility_page_url( Page::TYPE_FORGOT_PASSWORD );
463
464 $username = $user->user_login;
465 $chip_data = array(
466 'username' => $username,
467 'email' => $email,
468 'displayname' => $user->display_name,
469 'sitename' => get_bloginfo( 'name' ),
470 'reset_link' => "$url?action=rp&key=$key&login=" . rawurlencode( $username ),
471 );
472
473 $email_subject = isset( $form_data['emailSubject'] ) ? $form_data['emailSubject'] : '';
474 $email_body_raw = isset( $form_data['emailBody'] ) ? $form_data['emailBody'] : '[]';
475 $email_signature = isset( $form_data['emailSignature'] ) ? $form_data['emailSignature'] : '';
476
477 if ( ! $this->verify_email_template_signature( $email_subject, $email_body_raw, $email_signature ) ) {
478 wp_send_json_error( array( 'message' => 'Invalid request' ), 400 );
479 exit;
480 }
481
482 $email_body_array = json_decode( $email_body_raw, true );
483 if ( ! is_array( $email_body_array ) ) {
484 $email_body_array = array();
485 }
486
487 $email_body = $this->build_email_body( $email_body_array, $chip_data );
488
489 $email_body = nl2br( $email_body );
490
491 $headers = array( 'Content-Type: text/html; charset=UTF-8' );
492
493 // Send custom email.
494 apply_filters( 'kirki_element_smtp', '' );
495 $sent = wp_mail( $email, sanitize_text_field( $email_subject ), $email_body, $headers );
496
497 if ( $sent ) {
498 $response = array(
499 'message' => 'Email sent',
500 );
501 delete_transient( $transiet_name );
502 return new WP_REST_Response( $response, 200 );
503 } else {
504 $response = array(
505 'message' => 'Failed to send email',
506 );
507 return new WP_REST_Response( $response, 500 );
508 }
509 }
510
511 $response = array(
512 'message' => 'Invalid request',
513 );
514 return new WP_REST_Response( $response, 400 );
515 }
516
517 public function handle_change_password( $request ) {
518 $form_data = $request->get_body_params();
519 $transiet_name = $this->validate_nonce( 'kirki-change-password' );
520
521 $username = isset( $form_data['username'] ) ? sanitize_text_field( $form_data['username'] ) : '';
522 $reset_key = isset( $form_data['reset_key'] ) ? sanitize_text_field( $form_data['reset_key'] ) : '';
523 $new_password = isset( $form_data['new_password'] ) ? sanitize_text_field( $form_data['new_password'] ) : '';
524 $confirm_password = isset( $form_data['confirm_password'] ) ? sanitize_text_field( $form_data['confirm_password'] ) : '';
525
526 if ( empty( $reset_key ) || empty( $username ) || empty( $new_password ) || empty( $confirm_password ) ) {
527 wp_send_json_error( array( 'message' => 'Invalid request.' ), 400 );
528 exit;
529 }
530
531 if ( $new_password !== $confirm_password ) {
532 wp_send_json_error( array( 'message' => 'Passwords do not match.' ), 400 );
533 exit;
534 }
535
536 $user = check_password_reset_key( $reset_key, $username );
537
538 if ( is_wp_error( $user ) ) {
539 wp_send_json_error( array( 'message' => $user->get_error_message() ), 400 );
540 exit;
541 }
542
543 wp_set_password( $new_password, $user->ID );
544 delete_transient( $transiet_name );
545 wp_send_json_success( array( 'message' => 'Password reset successfully.' ) );
546 exit;
547 }
548
549 public function handle_retrieve_username( $request ) {
550 $form_data = $request->get_body_params();
551 $transiet_name = $this->validate_nonce( 'kirki-retrieve-username' );
552
553 $email = isset( $form_data['email'] ) ? sanitize_email( $form_data['email'] ) : '';
554
555 if ( empty( $email ) || ! is_email( $email ) ) {
556 wp_send_json_error( array( 'message' => 'Invalid email address.' ), 400 );
557 exit;
558 }
559
560 $user = get_user_by( 'email', $email );
561
562 if ( ! $user ) {
563 wp_send_json_success( array( 'message' => 'If an account exists with this email, you will receive your username.' ) );
564 exit;
565 }
566
567 $username = $user->user_login;
568 $chip_data = array(
569 'username' => $username,
570 'email' => $email,
571 'displayname' => $user->display_name,
572 'sitename' => get_bloginfo( 'name' ),
573 );
574
575 $email_subject = isset( $form_data['emailSubject'] ) ? $form_data['emailSubject'] : '';
576 $email_body_raw = isset( $form_data['emailBody'] ) ? $form_data['emailBody'] : '[]';
577 $email_signature = isset( $form_data['emailSignature'] ) ? $form_data['emailSignature'] : '';
578
579 if ( ! $this->verify_email_template_signature( $email_subject, $email_body_raw, $email_signature ) ) {
580 wp_send_json_error( array( 'message' => 'Invalid request' ), 400 );
581 exit;
582 }
583
584 $email_body_array = json_decode( $email_body_raw, true );
585 if ( ! is_array( $email_body_array ) ) {
586 $email_body_array = array();
587 }
588
589 $email_body = $this->build_email_body( $email_body_array, $chip_data );
590
591 $email_body = nl2br( $email_body );
592
593 $headers = array( 'Content-Type: text/html; charset=UTF-8' );
594
595 apply_filters( 'kirki_element_smtp', '' );
596 $email_sent = wp_mail( $email, sanitize_text_field( $email_subject ), $email_body, $headers );
597
598 if ( ! $email_sent ) {
599 wp_send_json_error( array( 'message' => 'Failed to send email. Please try again later.' ), 500 );
600 exit;
601 }
602
603 delete_transient( $transiet_name );
604 wp_send_json_success( array( 'message' => 'Username sent to your email address.' ) );
605 exit;
606 }
607
608 /**
609 * Validate the nonce from the request header and return true on success.
610 * Exits with an error response on failure.
611 *
612 * @param string $element_name
613 * @return true
614 */
615 public function validate_nonce( $element_name ) {
616 $nonce = isset( $_SERVER['HTTP_X_WP_ELEMENT_NONCE'] )
617 ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_ELEMENT_NONCE'] ) )
618 : null;
619
620 if ( ! $nonce ) {
621 wp_send_json_error( 'Missing nonce', 400 );
622 exit;
623 }
624
625 $action = KIRKI_COMPONENT_LIBRARY_APP_PREFIX . '_' . $element_name;
626
627 if ( ! wp_verify_nonce( $nonce, $action ) ) {
628 wp_send_json_error( 'Not authorized', 400 );
629 exit;
630 }
631
632 return true;
633 }
634 }
635
636 new CompLibFormHandler();
637