PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.13.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.13.0
2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 All 80 releases
← All changes | includes/api/form-builder-controller.php +93 -33 2.9.02.13.0 View file →
@@ -13,8 +13,42 @@
13 13 }
14 14
15 15 class FormBuilderController {
16 16
17 + /**
18 + * Verify the REST request nonce to protect the public form endpoints
19 + * against CSRF.
20 + *
21 + * The frontend sends the standard WordPress REST nonce via the
22 + * `X-WP-Nonce` header (see `ABlocksGlobal.nonce`, generated with
23 + * `wp_create_nonce( 'wp_rest' )`).
24 + *
25 + * @param WP_REST_Request $request
26 + *
27 + * @return true|WP_REST_Response True when valid, error response otherwise.
28 + */
29 + private function verify_nonce( WP_REST_Request $request ) {
30 + $nonce = $request->get_header( 'X-WP-Nonce' );
31 +
32 + if ( empty( $nonce ) ) {
33 + $nonce = $request->get_param( 'security' );
34 + }
35 +
36 + if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
37 + return new WP_REST_Response(
38 + [
39 + 'success' => false,
40 + 'data' => [
41 + 'message' => __( 'Security check failed. Please reload the page and try again.', 'ablocks' ),
42 + ],
43 + ],
44 + 403
45 + );
46 + }
47 +
48 + return true;
49 + }
50 +
17 51 public function register_routes() {
18 52
19 53 register_rest_route(
20 54 ABLOCKS_REST_NAMESPACE,
@@ -107,8 +141,13 @@
107 141 }
108 142
109 143 public function login( WP_REST_Request $request ) {
110 144
145 + $nonce_check = $this->verify_nonce( $request );
146 + if ( true !== $nonce_check ) {
147 + return $nonce_check;
148 + }
149 +
111 150 $params = $request->get_params();
112 151
113 152 $block_data = Helper::get_block_attributes(
114 153 $params['current_post_id'],
@@ -172,14 +211,19 @@
172 211 }
173 212
174 213 public function register( WP_REST_Request $request ) {
175 214
215 + $nonce_check = $this->verify_nonce( $request );
216 + if ( true !== $nonce_check ) {
217 + return $nonce_check;
218 + }
219 +
176 220 $params = $request->get_params();
177 221
178 222 $post_id = $params['current_post_id'];
179 223
180 224 if ( is_numeric( $post_id ) &&
181 - ! current_user_can( 'manage_options' ) &&
225 + ! current_user_can( 'edit_post', $post_id ) &&
182 226 get_post_status( $post_id ) !== 'publish'
183 227 ) {
184 228 return new WP_REST_Response(
185 229 [
@@ -344,8 +388,13 @@
344 388 }
345 389
346 390 public function forget_password( WP_REST_Request $request ) {
347 391
392 + $nonce_check = $this->verify_nonce( $request );
393 + if ( true !== $nonce_check ) {
394 + return $nonce_check;
395 + }
396 +
348 397 $params = $request->get_params();
349 398
350 399 $block_data = Helper::get_block_attributes(
351 400 $params['current_post_id'],
@@ -402,44 +451,16 @@
402 451 400
403 452 );
404 453 }
405 454
406 - if ( ! email_exists( $params['email'] ) ) {
407 - return new WP_REST_Response(
408 - [
409 - 'success' => false,
410 - 'data' => $this->prepare_res(
411 - [ 'message' => __( 'This email does not exist', 'ablocks' ) ],
412 - $block_data,
413 - $redirect_url
414 - ),
415 - ],
416 - 400
417 - );
418 - }
419 -
420 - $result = retrieve_password( $params['email'] );
421 -
422 - if ( is_wp_error( $result ) ) {
423 - return new WP_REST_Response(
424 - [
425 - 'success' => false,
426 - 'data' => $this->prepare_res(
427 - [ 'message' => esc_html( $result->get_error_message() ) ],
428 - $block_data,
429 - $redirect_url
430 - ),
431 - ],
432 - 400
433 - );
434 - }
435 -
436 - return new WP_REST_Response(
455 + // Generic response used whether or not the account exists, to avoid
456 + // leaking which emails are registered (user enumeration).
457 + $generic_response = new WP_REST_Response(
437 458 [
438 459 'success' => true,
439 460 'data' => $this->prepare_res(
440 461 [
441 - 'message' => __( 'Password reset email is sent', 'ablocks' ),
462 + 'message' => __( 'If an account exists for that email, a password reset link has been sent.', 'ablocks' ),
442 463 ],
443 464 $block_data,
444 465 $redirect_url
445 466 ),
@@ -445,13 +466,28 @@
445 466 ),
446 467 ],
447 468 200
448 469 );
470 +
471 + if ( ! email_exists( $params['email'] ) ) {
472 + return $generic_response;
473 + }
474 +
475 + // Ignore the result: a failure (e.g. an invalid user) must not reveal
476 + // account existence, so we still return the generic response.
477 + retrieve_password( $params['email'] );
478 +
479 + return $generic_response;
449 480 }
450 481
451 482
452 483 public function submit( WP_REST_Request $request ) {
453 484
485 + $nonce_check = $this->verify_nonce( $request );
486 + if ( true !== $nonce_check ) {
487 + return $nonce_check;
488 + }
489 +
454 490 $params = $request->get_params();
455 491
456 492 $block_data = Helper::get_block_attributes(
457 493 $params['current_post_id'],
@@ -495,8 +531,19 @@
495 531 } elseif ( $validate->has_message() ) {
496 532 $output['confirmationNotice'] = $validate->apply_vars( $block_data['parentAttributes']['confirmationNotice'] ?? __( 'Form successfully submitted!', 'ablocks' ) );
497 533 $output['message'] = $validate->get_message();
498 534
535 + /**
536 + * Fires after a form-builder submission has been validated and processed
537 + * successfully. Third-party automations (e.g. Zaplane) can hook this to
538 + * react to submissions.
539 + *
540 + * @param array $form_info { 'info' => [ type, postId, email, actions, config ], 'data' => [ field => [ 'value' => mixed ] ] }.
541 + * @param array $block_data Resolved form block attributes/inner blocks.
542 + * @param ValidateFormData $validate The validation object ( state_data holds submission_id ).
543 + */
544 + do_action( 'ablocks/form_builder/after_submission', $validate->form_info, $block_data, $validate );
545 +
499 546 wp_send_json_success( $output );
500 547 }
501 548
502 549 wp_send_json_error( [ 'message' => __( 'Action is not defined.', 'ablocks' ) ] );
@@ -645,8 +692,9 @@
645 692 'edit_users',
646 693 'delete_users',
647 694 'create_users',
648 695 'promote_users',
696 + 'edit_theme_options',
649 697 ];
650 698 foreach ( $privileged_caps as $cap ) {
651 699 if ( ! empty( $role_obj->capabilities[ $cap ] ) ) {
652 700 return false;
@@ -651,8 +699,20 @@
651 699 if ( ! empty( $role_obj->capabilities[ $cap ] ) ) {
652 700 return false;
653 701 }
654 702 }
703 +
704 + // The checks above read the role's stored capabilities, which never
705 + // include anything the permission map grants — those are added per
706 + // request and are invisible here. A role configured for the Site Editor
707 + // would sail through, so a self-registration form could hand a visitor
708 + // edit_theme_options. Ask the permission map directly.
709 + $bridged = \ABlocks\Permissions\Caps::native_bridge();
710 + $grants = \ABlocks\Permissions::get_role_grants( $role );
711 + if ( array_intersect( array_keys( $bridged ), $grants ) ) {
712 + return false;
713 + }
714 +
655 715 return true;
656 716 }
657 717
658 718 private function submit_schema() {