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 +130 -34 2.8.12.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,12 +211,32 @@
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
222 + $post_id = $params['current_post_id'];
223 +
224 + if ( is_numeric( $post_id ) &&
225 + ! current_user_can( 'edit_post', $post_id ) &&
226 + get_post_status( $post_id ) !== 'publish'
227 + ) {
228 + return new WP_REST_Response(
229 + [
230 + 'success' => false,
231 + 'data' => [ 'message' => __( 'Invalid post.', 'ablocks' ) ],
232 + ],
233 + 400
234 + );
235 + }
236 +
178 237 $block_data = Helper::get_block_attributes(
179 - $params['current_post_id'],
238 + $post_id,
180 239 $params['block_id'],
181 240 'ablocks/form-builder'
182 241 );
183 242
@@ -287,9 +346,10 @@
287 346
288 347 if (
289 348 ! empty( $role ) &&
290 349 strtolower( $role ) !== 'default' &&
291 - array_key_exists( $role, wp_roles()->roles )
350 + array_key_exists( $role, wp_roles()->roles ) &&
351 + $this->is_safe_registration_role( $role )
292 352 ) {
293 353 ( new \WP_User( $user_id ) )->set_role( $role );
294 354 }
295 355
@@ -328,8 +388,13 @@
328 388 }
329 389
330 390 public function forget_password( WP_REST_Request $request ) {
331 391
392 + $nonce_check = $this->verify_nonce( $request );
393 + if ( true !== $nonce_check ) {
394 + return $nonce_check;
395 + }
396 +
332 397 $params = $request->get_params();
333 398
334 399 $block_data = Helper::get_block_attributes(
335 400 $params['current_post_id'],
@@ -386,44 +451,16 @@
386 451 400
387 452 );
388 453 }
389 454
390 - if ( ! email_exists( $params['email'] ) ) {
391 - return new WP_REST_Response(
392 - [
393 - 'success' => false,
394 - 'data' => $this->prepare_res(
395 - [ 'message' => __( 'This email does not exist', 'ablocks' ) ],
396 - $block_data,
397 - $redirect_url
398 - ),
399 - ],
400 - 400
401 - );
402 - }
403 -
404 - $result = retrieve_password( $params['email'] );
405 -
406 - if ( is_wp_error( $result ) ) {
407 - return new WP_REST_Response(
408 - [
409 - 'success' => false,
410 - 'data' => $this->prepare_res(
411 - [ 'message' => esc_html( $result->get_error_message() ) ],
412 - $block_data,
413 - $redirect_url
414 - ),
415 - ],
416 - 400
417 - );
418 - }
419 -
420 - 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(
421 458 [
422 459 'success' => true,
423 460 'data' => $this->prepare_res(
424 461 [
425 - 'message' => __( 'Password reset email is sent', 'ablocks' ),
462 + 'message' => __( 'If an account exists for that email, a password reset link has been sent.', 'ablocks' ),
426 463 ],
427 464 $block_data,
428 465 $redirect_url
429 466 ),
@@ -429,13 +466,28 @@
429 466 ),
430 467 ],
431 468 200
432 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;
433 480 }
434 481
435 482
436 483 public function submit( WP_REST_Request $request ) {
437 484
485 + $nonce_check = $this->verify_nonce( $request );
486 + if ( true !== $nonce_check ) {
487 + return $nonce_check;
488 + }
489 +
438 490 $params = $request->get_params();
439 491
440 492 $block_data = Helper::get_block_attributes(
441 493 $params['current_post_id'],
@@ -479,8 +531,19 @@
479 531 } elseif ( $validate->has_message() ) {
480 532 $output['confirmationNotice'] = $validate->apply_vars( $block_data['parentAttributes']['confirmationNotice'] ?? __( 'Form successfully submitted!', 'ablocks' ) );
481 533 $output['message'] = $validate->get_message();
482 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 +
483 546 wp_send_json_success( $output );
484 547 }
485 548
486 549 wp_send_json_error( [ 'message' => __( 'Action is not defined.', 'ablocks' ) ] );
@@ -617,8 +680,41 @@
617 680 ],
618 681 ];
619 682 }
620 683
684 +
685 + private function is_safe_registration_role( string $role ) : bool {
686 + $role_obj = get_role( $role );
687 + if ( ! $role_obj ) {
688 + return false;
689 + }
690 + $privileged_caps = [
691 + 'manage_options',
692 + 'edit_users',
693 + 'delete_users',
694 + 'create_users',
695 + 'promote_users',
696 + 'edit_theme_options',
697 + ];
698 + foreach ( $privileged_caps as $cap ) {
699 + if ( ! empty( $role_obj->capabilities[ $cap ] ) ) {
700 + return false;
701 + }
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 +
715 + return true;
716 + }
621 717
622 718 private function submit_schema() {
623 719 return [
624 720 'current_post_id' => [