| @@ -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() { |