forms = $forms; $this->entries = $entries; } /** * Registers the routes. */ public function register(): void { $id = array( 'id' => array( 'type' => 'integer', 'required' => true, 'minimum' => 1, ), ); $ns = Api::NAMESPACE; $this->add( $ns, '/forms', 'GET', 'list_forms', Capabilities::VIEW_FORM_ENTRIES ); $this->add( $ns, '/forms', 'POST', 'create_form', Capabilities::MANAGE_SETTINGS, array( 'title' => array( 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), 'definition' => array( 'type' => 'object', 'required' => true, ), ) ); $this->add( $ns, '/forms/(?P\d+)', 'POST', 'update_form', Capabilities::MANAGE_SETTINGS, $id + array( 'title' => array( 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), 'definition' => array( 'type' => 'object', 'required' => true, ), ) ); $this->add( $ns, '/forms/(?P\d+)/delete', 'POST', 'delete_form', Capabilities::MANAGE_SETTINGS, $id ); $this->add( $ns, '/forms/captcha', 'GET', 'get_captcha', Capabilities::MANAGE_SETTINGS ); $this->add( $ns, '/forms/captcha', 'POST', 'save_captcha', Capabilities::MANAGE_SETTINGS, array( 'provider' => array( 'type' => 'string', 'default' => '', 'enum' => array( '', 'turnstile', 'hcaptcha' ), ), 'site_key' => array( 'type' => 'string', 'default' => '', ), 'secret' => array( 'type' => 'string', 'default' => '', ), ) ); $this->add( $ns, '/entries', 'GET', 'list_entries', Capabilities::VIEW_FORM_ENTRIES, array( 'form_id' => array( 'type' => 'integer', 'default' => 0, 'minimum' => 0, ), 'search' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), 'mail' => array( 'type' => 'string', 'default' => '', 'enum' => array( '', FormStore::MAIL_SENT, FormStore::MAIL_FAILED, FormStore::MAIL_NONE ), ), 'page' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1, ), 'per_page' => array( 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 100, ), ) ); $this->add( $ns, '/entries/(?P\d+)/read', 'POST', 'mark_entry', Capabilities::VIEW_FORM_ENTRIES, $id + array( 'read' => array( 'type' => 'boolean', 'default' => true, ), ) ); $this->add( $ns, '/entries/(?P\d+)/delete', 'POST', 'delete_entry', Capabilities::VIEW_FORM_ENTRIES, $id ); } /** * Captcha settings, without the secret key. */ public function get_captcha(): \WP_REST_Response { return $this->reply( array( 'captcha' => Captcha::public_config() ) ); } /** * Saves the captcha settings. * * @param \WP_REST_Request $request Request. */ public function save_captcha( \WP_REST_Request $request ): \WP_REST_Response { $saved = Captcha::save( array( 'provider' => (string) $request->get_param( 'provider' ), 'site_key' => (string) $request->get_param( 'site_key' ), 'secret' => (string) $request->get_param( 'secret' ), ) ); if ( '' !== $saved['provider'] && ! Captcha::active() ) { return $this->reply( array( 'captcha' => Captcha::public_config() ), false, __( 'Saved, but the check stays off until both keys are filled in. Copy them from your Cloudflare or hCaptcha account.', 'forensix-digital' ), 400 ); } $message = __( 'The captcha check is off.', 'forensix-digital' ); if ( Captcha::active() ) { /* translators: %s: provider name. */ $message = sprintf( __( '%s is on for every form that uses the spam check.', 'forensix-digital' ), Captcha::provider_name() ); } return $this->reply( array( 'captcha' => Captcha::public_config() ), true, $message ); } /** * Forms with their templates and counters. */ public function list_forms(): \WP_REST_Response { $items = array(); foreach ( $this->forms->all() as $form ) { $items[] = array( 'id' => $form['id'], 'title' => $form['title'], 'definition' => $form['definition']->to_array(), 'shortcode' => '[forensix_form id="' . $form['id'] . '"]', ); } $templates = array(); foreach ( FormTemplates::all() as $key => $template ) { $templates[] = array( 'key' => $key, 'title' => $template['title'], 'description' => $template['description'], 'definition' => ( new FormDefinition( $template['definition'] ) )->to_array(), ); } return $this->reply( array( 'items' => $items, 'templates' => $templates, 'types' => FormDefinition::types(), 'unread' => $this->entries->unread(), 'failed' => $this->entries->failed_mail(), 'can_edit' => current_user_can( Capabilities::MANAGE_SETTINGS ), ) ); } /** * Creates a form. * * @param \WP_REST_Request $request Request. */ public function create_form( \WP_REST_Request $request ): \WP_REST_Response { $definition = new FormDefinition( (array) $request->get_param( 'definition' ) ); $check = $this->check( 0, (string) $request->get_param( 'title' ), $definition ); if ( null !== $check ) { return $check; } $id = $this->forms->create( (string) $request->get_param( 'title' ), $definition ); return $this->reply( array( 'id' => $id ), true, __( 'The form was saved.', 'forensix-digital' ) ); } /** * Changes a form. * * @param \WP_REST_Request $request Request. */ public function update_form( \WP_REST_Request $request ): \WP_REST_Response { $id = (int) $request->get_param( 'id' ); if ( null === $this->forms->find( $id ) ) { return $this->reply( array(), false, __( 'This form does not exist.', 'forensix-digital' ), 404 ); } $definition = new FormDefinition( (array) $request->get_param( 'definition' ) ); $check = $this->check( $id, (string) $request->get_param( 'title' ), $definition ); if ( null !== $check ) { return $check; } $this->forms->update( $id, (string) $request->get_param( 'title' ), $definition ); return $this->reply( array( 'id' => $id ), true, __( 'The form was saved.', 'forensix-digital' ) ); } /** * Deletes a form and its entries. * * @param \WP_REST_Request $request Request. */ public function delete_form( \WP_REST_Request $request ): \WP_REST_Response { $id = (int) $request->get_param( 'id' ); if ( null === $this->forms->find( $id ) ) { return $this->reply( array(), false, __( 'This form does not exist.', 'forensix-digital' ), 404 ); } $this->forms->delete( $id ); return $this->reply( array(), true, __( 'The form and its messages were deleted.', 'forensix-digital' ) ); } /** * A page of entries. * * @param \WP_REST_Request $request Request. */ public function list_entries( \WP_REST_Request $request ): \WP_REST_Response { $page = (int) $request->get_param( 'page' ); $per_page = (int) $request->get_param( 'per_page' ); $result = $this->entries->query( (int) $request->get_param( 'form_id' ), (string) $request->get_param( 'search' ), (string) $request->get_param( 'mail' ), $page, $per_page ); return $this->reply( array( 'items' => $result['items'], 'total' => $result['total'], 'has_more' => $page * $per_page < $result['total'], ) ); } /** * Marks an entry read or unread. * * @param \WP_REST_Request $request Request. */ public function mark_entry( \WP_REST_Request $request ): \WP_REST_Response { $id = (int) $request->get_param( 'id' ); if ( null === $this->entries->find( $id ) ) { return $this->reply( array(), false, __( 'This message does not exist.', 'forensix-digital' ), 404 ); } $this->entries->set_read( $id, (bool) $request->get_param( 'read' ) ); return $this->reply(); } /** * Deletes an entry. * * @param \WP_REST_Request $request Request. */ public function delete_entry( \WP_REST_Request $request ): \WP_REST_Response { $id = (int) $request->get_param( 'id' ); if ( null === $this->entries->find( $id ) ) { return $this->reply( array(), false, __( 'This message does not exist.', 'forensix-digital' ), 404 ); } $this->entries->delete( $id ); return $this->reply( array(), true, __( 'The message was deleted.', 'forensix-digital' ) ); } /** * Refuses a form that cannot work. * * @param int $form_id Form id, 0 for a new form. * @param string $title Title. * @param FormDefinition $definition Definition. */ private function check( int $form_id, string $title, FormDefinition $definition ): ?\WP_REST_Response { $problems = $definition->problems; /** * Lets an add-on refuse a form it cannot support, with plain-language reasons. * * @param string[] $problems Reasons, empty by default. * @param int $form_id Form id, 0 for a new form. * @param FormDefinition $definition Definition. */ $extra = apply_filters( 'forensix_form_save_problems', array(), $form_id, $definition ); $problems = array_merge( $problems, array_map( 'strval', (array) $extra ) ); if ( '' === trim( $title ) ) { $problems[] = __( 'Please give the form a title.', 'forensix-digital' ); } if ( array() === $definition->fields ) { $problems[] = __( 'Add at least one field.', 'forensix-digital' ); } if ( array() === $problems ) { return null; } return $this->reply( array( 'problems' => $problems ), false, implode( ' ', $problems ), 400 ); } /** * Builds a response in the shape of the other Forensix routes. * * @param array $data Data. * @phpstan-param array $data * @param bool $ok Whether it worked. * @param string $message Plain-language message. * @param int $status HTTP status. */ private function reply( array $data = array(), bool $ok = true, string $message = '', int $status = 200 ): \WP_REST_Response { $body = $data + array( 'ok' => $ok, 'message' => $message, ); return new \WP_REST_Response( $body, $status ); } /** * Registers one route with a capability check. * * @param string $ns Namespace. * @param string $path Path. * @param string $method HTTP method. * @param string $handler Method name. * @param string $capability Required capability. * @param array $args Argument schema. * @phpstan-param array $args */ private function add( string $ns, string $path, string $method, string $handler, string $capability, array $args = array() ): void { register_rest_route( $ns, $path, array( 'methods' => $method, 'callback' => array( $this, $handler ), 'permission_callback' => static fn(): bool => current_user_can( $capability ), 'args' => $args, ) ); } }