rest_base = 'submission'; } /** * Registers the route. */ public function register_routes(): void { register_rest_route( $this->namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => array( 'form_id' => array( 'description' => __( 'Form being submitted.', 'devplugins-custom-form-builder' ), 'type' => 'integer', 'required' => true, 'minimum' => 1, 'sanitize_callback' => 'absint', ), 'post_id' => array( 'description' => __( 'Post the form was submitted from.', 'devplugins-custom-form-builder' ), 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint', ), 'fields' => array( 'description' => __( 'Submitted values, keyed by field key.', 'devplugins-custom-form-builder' ), 'type' => 'object', 'default' => array(), ), ), ), ) ); } /** * Decides whether a submission may be accepted. * * The route is public, because a form is public. What guards it is the form * nonce, plus the honeypot, rate limit and reCAPTCHA checks that run inside * the pipeline. * * @param WP_REST_Request $request Current request. * * @return true|WP_Error True when the submission may proceed. */ public function create_item_permissions_check( $request ): bool|WP_Error { $nonce = $request->get_param( FormRenderer::NONCE_FIELD ); $nonce = is_scalar( $nonce ) ? sanitize_text_field( (string) $nonce ) : ''; if ( Nonce::verify( $nonce, FormRenderer::NONCE_ACTION ) ) { return true; } return new WP_Error( 'formpilot_invalid_nonce', __( 'This form has expired. Please reload the page and try again.', 'devplugins-custom-form-builder' ), array( 'status' => 403 ) ); } /** * Stores a submission. * * @param WP_REST_Request $request Current request. * * @return WP_REST_Response|WP_Error The response. */ public function create_item( $request ): WP_REST_Response|WP_Error { $form_id = (int) $request->get_param( 'form_id' ); /* * Spam signals come from the request body. The guard used to read them * through Request, which sees only form-encoded POST and GET: a JSON body * never reaches them, so the honeypot never fired, every submission scored * as unstamped, and with a captcha configured every REST submission was * refused as spam. */ $signals = SpamGuard::signals( static fn( string $key ): string => is_scalar( $request->get_param( $key ) ) ? sanitize_text_field( (string) $request->get_param( $key ) ) : '' ); $result = $this->processor->process( $form_id, $this->values( $request ), $this->processor->context( (int) $request->get_param( 'post_id' ) ), $signals ); if ( $result instanceof WP_Error ) { return $result; } $response = rest_ensure_response( array( 'submission_id' => $result, 'message' => $this->messages->success( $this->processor->form( $form_id ) ), ) ); $response->set_status( 201 ); return $response; } /** * Reads the submitted values out of the request. * * Both shapes are accepted: `fields` as a JSON object, and the flat * `formpilot_fields[...]` form encoding a browser sends, so the same endpoint * serves an API client and a plain multipart post with a file. * * @param WP_REST_Request $request Current request. * * @return array Raw values keyed by field key. */ private function values( WP_REST_Request $request ): array { $values = $request->get_param( 'fields' ); $values = is_array( $values ) ? $values : array(); $posted = $request->get_param( FieldView::NAME_PREFIX ); if ( is_array( $posted ) ) { $values = array_merge( $values, $posted ); } return $values; } }