PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / trunk
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder vtrunk
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 1.2.0 1.2.1 All 78 releases
ablocks / includes / api / form-builder-controller.php

form-builder-controller.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder trunk, at includes/api/form-builder-controller.php

733 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ABlocks\API;
4
5 use WP_REST_Request;
6 use WP_REST_Response;
7 use WP_Error;
8 use ABlocks\Helper;
9 use ABlocks\Blocks\FormBuilder\ValidateFormData;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 class FormBuilderController {
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
51 public function register_routes() {
52
53 register_rest_route(
54 ABLOCKS_REST_NAMESPACE,
55 '/form-builder/login',
56 [
57 'methods' => 'POST',
58 'callback' => [ $this, 'login' ],
59 'permission_callback' => '__return_true',
60 'args' => $this->login_schema(),
61 ]
62 );
63
64 register_rest_route(
65 ABLOCKS_REST_NAMESPACE,
66 '/form-builder/registration',
67 [
68 'methods' => 'POST',
69 'callback' => [ $this, 'register' ],
70 'permission_callback' => '__return_true',
71 'args' => $this->register_schema(),
72 ]
73 );
74
75 register_rest_route(
76 ABLOCKS_REST_NAMESPACE,
77 '/form-builder/forget_password',
78 [
79 'methods' => 'POST',
80 'callback' => [ $this, 'forget_password' ],
81 'permission_callback' => '__return_true',
82 'args' => $this->forget_schema(),
83 ]
84 );
85
86 register_rest_route(
87 ABLOCKS_REST_NAMESPACE,
88 '/form-builder/submit',
89 [
90 'methods' => 'POST',
91 'callback' => [ $this, 'submit' ],
92 'permission_callback' => '__return_true',
93 'args' => $this->submit_schema(),
94 ]
95 );
96
97 register_rest_route(
98 ABLOCKS_REST_NAMESPACE,
99 '/form-builder/subscription',
100 [
101 'methods' => 'POST',
102 'callback' => [ $this, 'submit' ],
103 'permission_callback' => '__return_true',
104 'args' => $this->submit_schema(),
105 ]
106 );
107 }
108
109 private function prepare_res( array $data, array $block_data, string $redirect_url ) : array {
110
111 $formType = $block_data['parentAttributes']['formType'] ?? '';
112
113 if ( $formType === 'login' ) {
114 $confirmationType = ( $block_data['parentAttributes']['loginRedirect'] ?? false )
115 ? 'redirect'
116 : 'success';
117
118 } elseif ( $formType === 'registration' ) {
119 $confirmationType = ( $block_data['parentAttributes']['registerRedirect'] ?? false )
120 ? 'redirect'
121 : 'success';
122
123 } else {
124 $confirmationType = $block_data['parentAttributes']['confirmationType'] ?? 'success';
125 }
126
127 return array_merge(
128 $data,
129 [
130 'afterFormSubmission' => $block_data['parentAttributes']['afterFormSubmission'] ?? 'reset',
131 'confirmationType' => $confirmationType,
132 'confirmationNotice' => $block_data['parentAttributes']['confirmationNotice']
133 ?? $data['message']
134 ?? __( 'Form successfully submitted!', 'ablocks' ),
135 'redirect_url' => esc_url( $redirect_url ),
136 'no_follow' => $block_data['parentAttributes']['link']['noFollow'] ?? '',
137 'link_target' => $block_data['parentAttributes']['link']['linkTarget'] ?? '',
138 'formType' => $formType,
139 ]
140 );
141 }
142
143 public function login( WP_REST_Request $request ) {
144
145 $nonce_check = $this->verify_nonce( $request );
146 if ( true !== $nonce_check ) {
147 return $nonce_check;
148 }
149
150 $params = $request->get_params();
151
152 $block_data = Helper::get_block_attributes(
153 $params['current_post_id'],
154 $params['block_id'],
155 'ablocks/form-builder'
156 );
157
158 $redirect_url = '';
159
160 if ( $block_data['parentAttributes']['loginRedirect'] ?? false ) {
161 $redirect_url =
162 \ABlocks\Blocks\FormBuilder\Helper::merge_query_params(
163 $block_data['parentAttributes']['link']['href'] ?? '',
164 $block_data['parentAttributes']['link']['keyValue'] ?? '',
165 true
166 );
167 }
168
169 $user = wp_signon(
170 [
171 'user_login' => $params['username'],
172 'user_password' => $params['password'],
173 'remember' => (bool) $params['rememberme'],
174 ],
175 is_ssl()
176 );
177
178 if ( is_wp_error( $user ) ) {
179 return new WP_REST_Response(
180 [
181 'success' => false,
182 'data' => $this->prepare_res(
183 [ 'message' => $user->get_error_message() ],
184 $block_data,
185 $redirect_url
186 ),
187 ],
188 400
189 );
190 }
191
192 wp_set_current_user( $user->ID );
193
194 if ( empty( $redirect_url ) ) {
195 $redirect_url = home_url( '/' );
196 }
197
198 return new WP_REST_Response(
199 [
200 'success' => true,
201 'data' => $this->prepare_res(
202 [
203 'message' => __( 'You have logged in successfully. Redirecting...', 'ablocks' ),
204 ],
205 $block_data,
206 $redirect_url
207 ),
208 ],
209 200
210 );
211 }
212
213 public function register( WP_REST_Request $request ) {
214
215 $nonce_check = $this->verify_nonce( $request );
216 if ( true !== $nonce_check ) {
217 return $nonce_check;
218 }
219
220 $params = $request->get_params();
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
237 $block_data = Helper::get_block_attributes(
238 $post_id,
239 $params['block_id'],
240 'ablocks/form-builder'
241 );
242
243 $redirect_url = '';
244
245 if ( $block_data['parentAttributes']['registerRedirect'] ?? false ) {
246 $redirect_url =
247 \ABlocks\Blocks\FormBuilder\Helper::merge_query_params(
248 $block_data['parentAttributes']['link']['href'] ?? '',
249 $block_data['parentAttributes']['link']['keyValue'] ?? '',
250 true
251 );
252 }
253
254 if ( ! get_option( 'users_can_register' ) ) {
255 return new WP_REST_Response(
256 [
257 'success' => false,
258 'data' => $this->prepare_res(
259 [ 'message' => __( 'User registration is turned off.', 'ablocks' ) ],
260 $block_data,
261 $redirect_url
262 ),
263 ],
264 403
265 );
266 }
267
268 if ( ! empty( $params['confirm_password'] ) && $params['password'] !== $params['confirm_password'] ) {
269 return new WP_REST_Response(
270 [
271 'success' => false,
272 'data' => $this->prepare_res(
273 [ 'message' => __( 'Passwords do not match.', 'ablocks' ) ],
274 $block_data,
275 $redirect_url
276 ),
277 ],
278 400
279 );
280 }
281
282 if ( username_exists( $params['username'] ) ) {
283 return new WP_REST_Response(
284 [
285 'success' => false,
286 'data' => $this->prepare_res(
287 [ 'message' => __( 'Username already exists.', 'ablocks' ) ],
288 $block_data,
289 $redirect_url
290 ),
291 ],
292 400
293 );
294 }
295
296 if ( email_exists( $params['email'] ) ) {
297 return new WP_REST_Response(
298 [
299 'success' => false,
300 'data' => $this->prepare_res(
301 [ 'message' => __( 'Email already exists.', 'ablocks' ) ],
302 $block_data,
303 $redirect_url
304 ),
305 ],
306 400
307 );
308 }
309
310 if ( strlen( $params['password'] ) < 6 ) {
311 return new WP_REST_Response(
312 [
313 'success' => false,
314 'data' => $this->prepare_res(
315 [ 'message' => __( 'Password must be at least 6 characters.', 'ablocks' ) ],
316 $block_data,
317 $redirect_url
318 ),
319 ],
320 400
321 );
322 }
323
324 $user_id = wp_create_user(
325 $params['username'],
326 $params['password'],
327 $params['email']
328 );
329
330 if ( is_wp_error( $user_id ) ) {
331 return new WP_REST_Response(
332 [
333 'success' => false,
334 'data' => $this->prepare_res(
335 [ 'message' => $user_id->get_error_message() ],
336 $block_data,
337 $redirect_url
338 ),
339 ],
340 400
341 );
342 }
343
344 // Assign role if defined
345 $role = $block_data['parentAttributes']['roleSlug'] ?? '';
346
347 if (
348 ! empty( $role ) &&
349 strtolower( $role ) !== 'default' &&
350 array_key_exists( $role, wp_roles()->roles ) &&
351 $this->is_safe_registration_role( $role )
352 ) {
353 ( new \WP_User( $user_id ) )->set_role( $role );
354 }
355
356 // Save custom fields
357 $reserved = [ 'username', 'email', 'password', 'current_post_id', 'block_id', 'confirm_password' ];
358 $custom = array_diff_key( $params, array_flip( $reserved ) );
359
360 foreach ( $custom as $key => $value ) {
361 update_user_meta(
362 $user_id,
363 'ablocks_' . sanitize_key( $key ),
364 sanitize_text_field( $value )
365 );
366 }
367
368 wp_set_current_user( $user_id );
369 wp_set_auth_cookie( $user_id );
370
371 if ( empty( $redirect_url ) ) {
372 $redirect_url = home_url( '/' );
373 }
374
375 return new WP_REST_Response(
376 [
377 'success' => true,
378 'data' => $this->prepare_res(
379 [
380 'message' => __( 'Registration completed successfully. Redirecting...', 'ablocks' ),
381 ],
382 $block_data,
383 $redirect_url
384 ),
385 ],
386 201
387 );
388 }
389
390 public function forget_password( WP_REST_Request $request ) {
391
392 $nonce_check = $this->verify_nonce( $request );
393 if ( true !== $nonce_check ) {
394 return $nonce_check;
395 }
396
397 $params = $request->get_params();
398
399 $block_data = Helper::get_block_attributes(
400 $params['current_post_id'],
401 $params['block_id'],
402 'ablocks/form-builder'
403 );
404
405 if ( empty( $block_data ) ) {
406 return new WP_REST_Response(
407 [
408 'success' => false,
409 'data' => [
410 'message' => __( 'Invalid form block.', 'ablocks' ),
411 ],
412 ],
413 400
414 );
415 }
416
417 $redirect_url = '';
418
419 if ( $block_data['parentAttributes']['registerRedirect'] ?? false ) {
420 $redirect_url = \ABlocks\Blocks\FormBuilder\Helper::merge_query_params(
421 $block_data['parentAttributes']['link']['href'] ?? '',
422 $block_data['parentAttributes']['link']['keyValue'] ?? '',
423 true
424 );
425 }
426
427 if ( empty( $params['email'] ?? '' ) ) {
428 return new WP_REST_Response(
429 [
430 'success' => false,
431 'data' => $this->prepare_res(
432 [ 'message' => __( 'Email field is required', 'ablocks' ) ],
433 $block_data,
434 $redirect_url
435 ),
436 ],
437 400
438 );
439 }
440
441 if ( ! is_email( $params['email'] ) ) {
442 return new WP_REST_Response(
443 [
444 'success' => false,
445 'data' => $this->prepare_res(
446 [ 'message' => __( 'Provide a valid email', 'ablocks' ) ],
447 $block_data,
448 $redirect_url
449 ),
450 ],
451 400
452 );
453 }
454
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(
458 [
459 'success' => true,
460 'data' => $this->prepare_res(
461 [
462 'message' => __( 'If an account exists for that email, a password reset link has been sent.', 'ablocks' ),
463 ],
464 $block_data,
465 $redirect_url
466 ),
467 ],
468 200
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;
480 }
481
482
483 public function submit( WP_REST_Request $request ) {
484
485 $nonce_check = $this->verify_nonce( $request );
486 if ( true !== $nonce_check ) {
487 return $nonce_check;
488 }
489
490 $params = $request->get_params();
491
492 $block_data = Helper::get_block_attributes(
493 $params['current_post_id'],
494 $params['block_id'],
495 'ablocks/form-builder'
496 );
497
498 if ( empty( $block_data ) ) {
499 return new WP_Error(
500 'invalid_block',
501 __( 'Invalid form block.', 'ablocks' ),
502 [ 'status' => 400 ]
503 );
504 }
505
506 $fields_to_skip = [ 'current_post_id', 'block_id' ];
507 $all_fields = array_diff_key( $params, array_flip( $fields_to_skip ) );
508
509 $actions = apply_filters(
510 'ablocks/form_builder/actions',
511 [
512 \ABlocks\Blocks\FormBuilder\Actions\SendEmails::class,
513 \ABlocks\Blocks\FormBuilder\Actions\SaveFormData::class,
514 \ABlocks\Blocks\FormBuilder\Actions\SendEmail::class,
515 \ABlocks\Blocks\FormBuilder\Actions\Subscribe::class,
516 ]
517 );
518
519 $validate = new ValidateFormData( $block_data, $all_fields );
520 $validate->actions( $actions );
521
522 $output = $validate->get_output();
523 $output['afterFormSubmission'] = $block_data['parentAttributes']['afterFormSubmission'] ?? 'reset';
524 $output['confirmationType'] = $block_data['parentAttributes']['confirmationType'] ?? 'success';
525 $output['formType'] = $block_data['parentAttributes']['formType'] ?? '';
526 $output['redirect_url'] = $block_data['parentAttributes']['link']['href'] ?? '';
527 $output['link_target'] = $block_data['parentAttributes']['link']['linkTarget'] ?? '';
528 if ( $validate->has_error() ) {
529 $output['message'] = $validate->get_error_message();
530 wp_send_json_error( $output );
531 } elseif ( $validate->has_message() ) {
532 $output['confirmationNotice'] = $validate->apply_vars( $block_data['parentAttributes']['confirmationNotice'] ?? __( 'Form successfully submitted!', 'ablocks' ) );
533 $output['message'] = $validate->get_message();
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
546 wp_send_json_success( $output );
547 }
548
549 wp_send_json_error( [ 'message' => __( 'Action is not defined.', 'ablocks' ) ] );
550
551 if ( $validate->has_error() ) {
552 return new WP_Error(
553 'form_error',
554 $validate->get_error_message(),
555 [ 'status' => 400 ]
556 );
557 }
558
559 return new WP_REST_Response( $output, 200 );
560 }
561
562 private function login_schema() {
563 return [
564
565 'username' => [
566 'required' => true,
567 'type' => 'string',
568 'minLength' => 3,
569 'maxLength' => 60,
570 'sanitize_callback' => 'sanitize_user',
571 'validate_callback' => function( $value ) {
572 return validate_username( $value );
573 },
574 ],
575
576 'password' => [
577 'required' => true,
578 'type' => 'string',
579 'minLength' => 6,
580 'maxLength' => 128,
581 ],
582
583 'rememberme' => [
584 'required' => false,
585 'type' => 'boolean',
586 'sanitize_callback' => 'rest_sanitize_boolean',
587 ],
588
589 'current_post_id' => [
590 'required' => true,
591 'type' => 'integer',
592 'sanitize_callback' => 'absint',
593 'validate_callback' => function( $value ) {
594 return $value > 0 && get_post( $value );
595 },
596 ],
597
598 'block_id' => [
599 'required' => true,
600 'type' => 'string',
601 'sanitize_callback' => 'sanitize_text_field',
602 'validate_callback' => function( $value ) {
603 return preg_match( '/^[a-zA-Z0-9_\-]+$/', $value );
604 },
605 ],
606 ];
607 }
608
609
610 private function register_schema() {
611 return [
612
613 'username' => [
614 'required' => true,
615 'type' => 'string',
616 'minLength' => 3,
617 'maxLength' => 60,
618 'sanitize_callback' => 'sanitize_user',
619 'validate_callback' => function( $value ) {
620 return validate_username( $value );
621 },
622 ],
623
624 'email' => [
625 'required' => true,
626 'type' => 'string',
627 'sanitize_callback' => 'sanitize_email',
628 'validate_callback' => function( $value ) {
629 return is_email( $value );
630 },
631 ],
632
633 'password' => [
634 'required' => true,
635 'type' => 'string',
636 'minLength' => 6,
637 'maxLength' => 128,
638 'validate_callback' => function( $value ) {
639 return strlen( $value ) >= 6;
640 },
641 ],
642
643 'current_post_id' => [
644 'required' => true,
645 'type' => 'integer',
646 'sanitize_callback' => 'absint',
647 'validate_callback' => function( $value ) {
648 return $value > 0 && get_post( $value );
649 },
650 ],
651
652 'block_id' => [
653 'required' => true,
654 'type' => 'string',
655 'sanitize_callback' => 'sanitize_text_field',
656 'validate_callback' => function( $value ) {
657 return preg_match( '/^[a-zA-Z0-9_\-]+$/', $value );
658 },
659 ],
660 ];
661 }
662
663
664 private function forget_schema() {
665 return [
666 'email' => [
667 'required' => true,
668 'type' => 'string',
669 'sanitize_callback' => 'sanitize_email',
670 ],
671 'current_post_id' => [
672 'required' => true,
673 'type' => 'integer',
674 'sanitize_callback' => 'absint',
675 ],
676 'block_id' => [
677 'required' => true,
678 'type' => 'string',
679 'sanitize_callback' => 'sanitize_text_field',
680 ],
681 ];
682 }
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 }
717
718 private function submit_schema() {
719 return [
720 'current_post_id' => [
721 'required' => true,
722 'type' => 'string',
723 'sanitize_callback' => 'sanitize_text_field',
724 ],
725 'block_id' => [
726 'required' => true,
727 'type' => 'string',
728 'sanitize_callback' => 'sanitize_text_field',
729 ],
730 ];
731 }
732 }
733